实现点击open with vscode传递端口进行端口映射
Some checks failed
CI/CD Pipeline for DevStar Extension / build (pull_request) Failing after 2m28s
Some checks failed
CI/CD Pipeline for DevStar Extension / build (pull_request) Failing after 2m28s
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"name": "devstar",
|
||||
"displayName": "%displayName%",
|
||||
"description": "%description%",
|
||||
"version": "0.3.9",
|
||||
"version": "0.4.0",
|
||||
"keywords": [],
|
||||
"publisher": "mengning",
|
||||
"engines": {
|
||||
@@ -136,4 +136,4 @@
|
||||
"extensionDependencies": [
|
||||
"ms-vscode.cpptools"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -43,31 +43,40 @@ export default class DevstarAPIHandler {
|
||||
}
|
||||
});
|
||||
|
||||
// 处理非200响应状态码
|
||||
// 检查响应状态码
|
||||
if (!response.ok) {
|
||||
const text = await response.text(); // 先读取文本防止json解析失败
|
||||
if (response.status == 401) {
|
||||
throw new Error('Token错误')
|
||||
const text = await response.text(); // 读取文本内容以便调试
|
||||
console.error(`HTTP Error: ${response.status} - ${text}`);
|
||||
if (response.status === 401) {
|
||||
throw new Error('Token错误');
|
||||
} else {
|
||||
throw new Error(`HTTP Error: ${response.status} - ${text}`);
|
||||
throw new Error(`HTTP Error: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 Content-Type 是否为 JSON
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
const text = await response.text(); // 读取文本内容以便调试
|
||||
console.error(`Unexpected Content-Type: ${contentType} - ${text}`);
|
||||
throw new Error(`Unexpected Content-Type: ${contentType}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const data = responseData.data
|
||||
if (data.username == undefined || data.username == "") {
|
||||
throw new Error('Token对应用户不存在')
|
||||
} else {
|
||||
// 验证用户名匹配
|
||||
if (data.username !== username) {
|
||||
throw new Error('Token与用户名不符');
|
||||
}
|
||||
const data = responseData.data;
|
||||
if (!data || !data.username) {
|
||||
throw new Error('Token对应用户不存在');
|
||||
}
|
||||
|
||||
// 验证用户名匹配
|
||||
if (data.username !== username) {
|
||||
throw new Error('Token与用户名不符');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export default class DSHome {
|
||||
await this.remoteContainer.firstOpenProject(data.host, data.hostname, data.port, data.username, data.path, this.context);
|
||||
break;
|
||||
case 'openRemoteFolder':
|
||||
this.remoteContainer.openRemoteFolder(data.host, data.port, data.username, data.path);
|
||||
this.remoteContainer.openRemoteFolder(data.host, data.port, data.username, data.path, this.context);
|
||||
break;
|
||||
case 'getDevstarDomain':
|
||||
panel.webview.postMessage({ command: 'getDevstarDomain', data: { devstarDomain: this.devstarDomain } });
|
||||
|
||||
46
src/main.ts
46
src/main.ts
@@ -4,7 +4,6 @@ import QuickAccessTreeProvider from './views/quick-access-tree';
|
||||
import DSHome from './home';
|
||||
import RemoteContainer, { openProjectWithoutLogging } from './remote-container';
|
||||
import User from './user';
|
||||
import DevstarAPIHandler from './devstar-api';
|
||||
import * as os from 'os';
|
||||
import * as utils from './utils';
|
||||
|
||||
@@ -67,7 +66,40 @@ export class DevStarExtension {
|
||||
const path = params.get('path');
|
||||
const accessToken = params.get('access_token');
|
||||
const devstarUsername = params.get('devstar_username');
|
||||
const devstarDomain = params.get('devstar_domain');
|
||||
const rawDevstarDomain = params.get('devstar_domain');
|
||||
let devstarDomain = rawDevstarDomain;
|
||||
if (rawDevstarDomain) {
|
||||
try {
|
||||
const url = new URL(rawDevstarDomain);
|
||||
devstarDomain = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
// 从 rawDevstarDomain 的查询参数中提取 forwardPorts
|
||||
const forwardPortsParam = url.searchParams.get('forwardPorts');
|
||||
if (forwardPortsParam) {
|
||||
const ports = forwardPortsParam.split(',').map(port => parseInt(port, 10)).filter(port => !isNaN(port));
|
||||
console.log('解析到的 forwardPorts 参数:', ports);
|
||||
context.globalState.update('forwardPorts', ports);
|
||||
} else {
|
||||
// 如果没有 forwardPorts 参数,清除 globalState 中的旧值
|
||||
console.log('未找到 forwardPorts 参数,清除旧的 forwardPorts 配置');
|
||||
context.globalState.update('forwardPorts', undefined);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Invalid devstar_domain URL:', error);
|
||||
}
|
||||
}
|
||||
console.log('sanitized_devstar_domain:', devstarDomain);
|
||||
|
||||
// 使用修正后的 devstar_domain
|
||||
if (devstarDomain) {
|
||||
this.user.setDevstarDomain(devstarDomain);
|
||||
this.remoteContainer.setUser(this.user);
|
||||
this.dsHome.setDevstarDomainAndHomePageURL(devstarDomain);
|
||||
this.dsHome.setUser(this.user);
|
||||
this.dsHome.setRemoteContainer(this.remoteContainer);
|
||||
|
||||
context.globalState.update('devstarDomain', devstarDomain);
|
||||
}
|
||||
|
||||
if (host && hostname && port && username && path) {
|
||||
const containerHost = host;
|
||||
@@ -167,7 +199,7 @@ export class DevStarExtension {
|
||||
);
|
||||
|
||||
this.registerGlobalCommands(context);
|
||||
|
||||
|
||||
//防止进入HOME页面
|
||||
// this.startDevStarHome();
|
||||
}
|
||||
@@ -179,10 +211,10 @@ export class DevStarExtension {
|
||||
this.dsHome.toggle()
|
||||
),
|
||||
vscode.commands.registerCommand('devstar.showPortMappings', () => {
|
||||
// 这里需要根据当前活动连接获取hostname和port
|
||||
// 简化实现:显示所有活动的端口映射
|
||||
this.remoteContainer.showPortMappingsInOutputChannel('current', 0);
|
||||
}),
|
||||
// 这里需要根据当前活动连接获取hostname和port
|
||||
// 简化实现:显示所有活动的端口映射
|
||||
this.remoteContainer.showPortMappingsInOutputChannel('current', 0);
|
||||
}),
|
||||
vscode.commands.registerCommand('devstar.clean', () => {
|
||||
// 先清除ssh key
|
||||
if (fs.existsSync(this.user.getUserPrivateKeyPath())) {
|
||||
|
||||
@@ -77,7 +77,7 @@ export default class RemoteContainer {
|
||||
await this.firstConnect(host, hostname, username, port, path)
|
||||
.then((res) => {
|
||||
if (res === 'success') {
|
||||
this.openRemoteFolder(host, port, username, path);
|
||||
this.openRemoteFolder(host, port, username, path, context);
|
||||
} else {
|
||||
vscode.window.showErrorMessage('首次连接容器失败,请检查网络和容器状态。');
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export default class RemoteContainer {
|
||||
/**
|
||||
* local environment,第一次连接其他项目
|
||||
*/
|
||||
async firstConnect(host: string, hostname: string, username: string, port: number, projectPath?: string): Promise<string> {
|
||||
async firstConnect(host: string, hostname: string, _username: string, port: number, projectPath?: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const ssh = new NodeSSH();
|
||||
|
||||
@@ -190,37 +190,6 @@ export default class RemoteContainer {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 devcontainer.json 中提取端口映射配置
|
||||
*/
|
||||
private async getPortsConfigFromDevContainer(ssh: any, containerPath: string): Promise<{
|
||||
portsAttributes: any;
|
||||
forwardPorts?: number[];
|
||||
otherPortsAttributes?: any;
|
||||
}> {
|
||||
try {
|
||||
const findResult = await ssh.execCommand(`find ${containerPath} -name "devcontainer.json" -type f`);
|
||||
|
||||
if (findResult.code === 0 && findResult.stdout.trim()) {
|
||||
const devcontainerPath = findResult.stdout.trim().split('\n')[0];
|
||||
|
||||
const readResult = await ssh.execCommand(`cat ${devcontainerPath}`);
|
||||
if (readResult.code === 0) {
|
||||
const devcontainerConfig = JSON.parse(readResult.stdout);
|
||||
|
||||
return {
|
||||
portsAttributes: devcontainerConfig.portsAttributes || {},
|
||||
forwardPorts: devcontainerConfig.forwardPorts,
|
||||
otherPortsAttributes: devcontainerConfig.otherPortsAttributes
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
return { portsAttributes: {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找可用的本地端口 - 优先使用相同端口
|
||||
*/
|
||||
@@ -279,74 +248,6 @@ export default class RemoteContainer {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立端口映射 - 根据 devcontainer.json 配置
|
||||
*/
|
||||
private async setupPortForwarding(hostname: string, port: number, containerPath: string): Promise<void> {
|
||||
const ssh = new NodeSSH();
|
||||
const portMappings: Array<{ containerPort: number, localPort: number, label: string, source: string }> = [];
|
||||
|
||||
try {
|
||||
await ssh.connect({
|
||||
host: hostname,
|
||||
username: 'root',
|
||||
port: port,
|
||||
privateKeyPath: this.user.getUserPrivateKeyPath(),
|
||||
readyTimeout: 30000,
|
||||
});
|
||||
|
||||
const portsConfig = await this.getPortsConfigFromDevContainer(ssh, containerPath);
|
||||
|
||||
if (portsConfig.forwardPorts && portsConfig.forwardPorts.length > 0) {
|
||||
for (const containerPort of portsConfig.forwardPorts) {
|
||||
const localPort = await this.findAvailableLocalPort(containerPort);
|
||||
await this.createSSHPortForward(hostname, port, containerPort, localPort);
|
||||
|
||||
portMappings.push({
|
||||
containerPort,
|
||||
localPort,
|
||||
label: `Port ${containerPort}`,
|
||||
source: 'forwardPorts'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [containerPortStr, attributes] of Object.entries(portsConfig.portsAttributes)) {
|
||||
const containerPort = parseInt(containerPortStr);
|
||||
if (!isNaN(containerPort)) {
|
||||
const alreadyMapped = portMappings.some(m => m.containerPort === containerPort);
|
||||
if (!alreadyMapped) {
|
||||
const localPort = await this.findAvailableLocalPort(containerPort);
|
||||
await this.createSSHPortForward(hostname, port, containerPort, localPort);
|
||||
|
||||
const label = (attributes as any).label || `Port ${containerPort}`;
|
||||
portMappings.push({
|
||||
containerPort,
|
||||
localPort,
|
||||
label,
|
||||
source: 'portsAttributes'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ssh.dispose();
|
||||
|
||||
const mappingKey = `${hostname}:${port}`;
|
||||
this.portMappings.set(mappingKey, portMappings);
|
||||
|
||||
if (portMappings.length > 0) {
|
||||
this.showPortMappingsSummary(portMappings);
|
||||
|
||||
this.registerPortMappingsCommands(mappingKey, portMappings);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
await ssh.dispose();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 SSH 端口转发
|
||||
*/
|
||||
@@ -369,10 +270,10 @@ export default class RemoteContainer {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
sshProcess.stdout.on('data', (data: Buffer) => {
|
||||
sshProcess.stdout.on('data', (_data: Buffer) => {
|
||||
});
|
||||
|
||||
sshProcess.stderr.on('data', (data: Buffer) => {
|
||||
sshProcess.stderr.on('data', (_data: Buffer) => {
|
||||
});
|
||||
|
||||
if (!this.sshProcesses) {
|
||||
@@ -535,12 +436,13 @@ export default class RemoteContainer {
|
||||
/**
|
||||
* local env
|
||||
*/
|
||||
async openRemoteFolder(host: string, port: number, username: string, path: string): Promise<void> {
|
||||
async openRemoteFolder(host: string, port: number, _username: string, path: string, context: vscode.ExtensionContext): Promise<void> {
|
||||
try {
|
||||
const sshConfig = await this.getSSHConfig(host);
|
||||
if (sshConfig) {
|
||||
try {
|
||||
await this.setupPortForwarding(sshConfig.hostname, port, path);
|
||||
// 调用 setupPortForwardingFromGlobalState 方法
|
||||
await this.setupPortForwardingFromGlobalState(sshConfig.hostname, port, context);
|
||||
|
||||
setTimeout(() => {
|
||||
this.showPortMappingsPanel(sshConfig.hostname, port);
|
||||
@@ -639,6 +541,47 @@ export default class RemoteContainer {
|
||||
this.portMappings.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 globalState 获取 forwardPorts 并建立端口映射
|
||||
*/
|
||||
public async setupPortForwardingFromGlobalState(hostname: string, port: number, context: vscode.ExtensionContext): Promise<void> {
|
||||
// 从 globalState 获取 forwardPorts 参数
|
||||
const forwardPorts: number[] | undefined = context.globalState.get('forwardPorts');
|
||||
|
||||
if (forwardPorts && forwardPorts.length > 0) {
|
||||
const portMappings: Array<{ containerPort: number, localPort: number, label: string, source: string }> = [];
|
||||
|
||||
for (const containerPort of forwardPorts) {
|
||||
const localPort = await this.findAvailableLocalPort(containerPort);
|
||||
try {
|
||||
await this.createSSHPortForward(hostname, port, containerPort, localPort);
|
||||
portMappings.push({
|
||||
containerPort,
|
||||
localPort,
|
||||
label: `Port ${containerPort}`,
|
||||
source: 'globalState forwardPorts'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`映射容器端口 ${containerPort} 到本地端口 ${localPort} 失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const mappingKey = `${hostname}:${port}`;
|
||||
this.portMappings.set(mappingKey, portMappings);
|
||||
|
||||
if (portMappings.length > 0) {
|
||||
this.showPortMappingsSummary(portMappings);
|
||||
this.registerPortMappingsCommands(mappingKey, portMappings);
|
||||
}
|
||||
|
||||
// 使用完毕后立即清除 globalState 中的 forwardPorts,避免影响下一个项目
|
||||
console.log('端口映射完成,清除 globalState 中的 forwardPorts');
|
||||
context.globalState.update('forwardPorts', undefined);
|
||||
} else {
|
||||
console.log('未找到 forwardPorts 参数,跳过端口映射设置。');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
57
src/user.ts
57
src/user.ts
@@ -117,8 +117,8 @@ export default class User {
|
||||
}
|
||||
|
||||
public async isLogged(): Promise<boolean> {
|
||||
const username: string|undefined = this.context.globalState.get(this.usernameKey)
|
||||
const userToken: string|undefined = this.context.globalState.get(this.userTokenKey)
|
||||
const username: string | undefined = this.context.globalState.get(this.usernameKey)
|
||||
const userToken: string | undefined = this.context.globalState.get(this.userTokenKey)
|
||||
if ((username != undefined && username != '') && (userToken != undefined && userToken != '')) {
|
||||
const devstarAPIHandler = new DevstarAPIHandler(this.devstarDomain)
|
||||
|
||||
@@ -172,7 +172,7 @@ export default class User {
|
||||
if (!this.isLogged()) {
|
||||
return '';
|
||||
} else {
|
||||
const username: string|undefined = this.context.globalState.get(this.usernameKey)
|
||||
const username: string | undefined = this.context.globalState.get(this.usernameKey)
|
||||
// islogged为true,username不为空
|
||||
return path.join(os.homedir(), '.ssh', `id_rsa_${username}_${this.devstarHostname}`)
|
||||
}
|
||||
@@ -182,7 +182,7 @@ export default class User {
|
||||
if (!this.isLogged()) {
|
||||
return '';
|
||||
} else {
|
||||
const username: string|undefined = this.context.globalState.get(this.usernameKey)
|
||||
const username: string | undefined = this.context.globalState.get(this.usernameKey)
|
||||
// islogged为true,username不为空
|
||||
return path.join(os.homedir(), '.ssh', `id_rsa_${username}_${this.devstarHostname}.pub`)
|
||||
}
|
||||
@@ -259,8 +259,55 @@ export default class User {
|
||||
this.updateLocalUserPrivateKeyPath(this.getUserPrivateKeyPath())
|
||||
console.log(`Update local user private key path.`)
|
||||
} catch (error) {
|
||||
const username: string|undefined = this.context.globalState.get(this.usernameKey)
|
||||
const username: string | undefined = this.context.globalState.get(this.usernameKey)
|
||||
console.error(`Failed to write public/private key into the user(${username}) ssh public/key file: `, error);
|
||||
}
|
||||
}
|
||||
|
||||
public async verifyToken(token: string, username: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(this.devstarDomain + `/api/devcontainer/user`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'token ' + token
|
||||
}
|
||||
});
|
||||
|
||||
// 检查响应状态码
|
||||
if (!response.ok) {
|
||||
const text = await response.text(); // 读取文本内容以便调试
|
||||
console.error(`HTTP Error: ${response.status} - ${text}`);
|
||||
if (response.status === 401) {
|
||||
throw new Error('Token错误');
|
||||
} else {
|
||||
throw new Error(`HTTP Error: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 Content-Type 是否为 JSON
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
const text = await response.text(); // 读取文本内容以便调试
|
||||
console.error(`Unexpected Content-Type: ${contentType} - ${text}`);
|
||||
throw new Error(`Unexpected Content-Type: ${contentType}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const data = responseData.data;
|
||||
if (!data || !data.username) {
|
||||
throw new Error('Token对应用户不存在');
|
||||
}
|
||||
|
||||
// 验证用户名匹配
|
||||
if (data.username !== username) {
|
||||
throw new Error('Token与用户名不符');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user