From 5e72a8302e47bb75828a97e3d97f8310c83caf87 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Wed, 6 Nov 2024 18:09:36 +0800 Subject: [PATCH] feat: devboxListView refactor --- .../ide/vscode/devbox/images/delete.svg | 1 + extensions/ide/vscode/devbox/package.json | 22 ++-- extensions/ide/vscode/devbox/src/api/ssh.ts | 51 ++++---- .../devbox/src/commands/remoteConnector.ts | 79 ++++-------- .../ide/vscode/devbox/src/constant/file.ts | 9 ++ extensions/ide/vscode/devbox/src/extension.ts | 8 +- .../DevboxListViewProvider.ts} | 43 +++--- .../ide/vscode/devbox/src/utils/handleUri.ts | 7 +- .../ide/vscode/devbox/src/utils/sshConfig.ts | 122 ++++++++++++++++++ 9 files changed, 225 insertions(+), 117 deletions(-) create mode 100644 extensions/ide/vscode/devbox/images/delete.svg create mode 100644 extensions/ide/vscode/devbox/src/constant/file.ts rename extensions/ide/vscode/devbox/src/{commands/treeview.ts => providers/DevboxListViewProvider.ts} (86%) diff --git a/extensions/ide/vscode/devbox/images/delete.svg b/extensions/ide/vscode/devbox/images/delete.svg new file mode 100644 index 00000000000..fbb59e83b77 --- /dev/null +++ b/extensions/ide/vscode/devbox/images/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/ide/vscode/devbox/package.json b/extensions/ide/vscode/devbox/package.json index 51116023733..f4c70e70eb4 100644 --- a/extensions/ide/vscode/devbox/package.json +++ b/extensions/ide/vscode/devbox/package.json @@ -58,22 +58,23 @@ "title": "Create Devbox", "icon": "images/create.svg" }, - { - "command": "devboxDashboard.deleteDevbox", - "title": "Delete Devbox" - }, { "command": "devboxDashboard.openDevbox", "title": "Open Devbox", "icon": "images/open.svg" }, + { + "command": "devboxDashboard.deleteDevbox", + "title": "Delete Devbox", + "icon": "images/delete.svg" + }, { "command": "devbox.openExternalLink", "title": "Devbox:Open in Browser" } ], "views": { - "devboxView": [ + "devboxListView": [ { "id": "devboxDashboard", "name": "My Projects" @@ -99,7 +100,7 @@ "viewsContainers": { "activitybar": [ { - "id": "devboxView", + "id": "devboxListView", "title": "Devbox", "icon": "images/explorer.svg" } @@ -138,13 +139,14 @@ ], "view/item/context": [ { - "command": "devboxDashboard.deleteDevbox", - "when": "view == devboxDashboard && viewItem == devbox" + "command": "devboxDashboard.openDevbox", + "when": "view == devboxDashboard && viewItem == devbox", + "group": "inline@1" }, { - "command": "devboxDashboard.openDevbox", + "command": "devboxDashboard.deleteDevbox", "when": "view == devboxDashboard && viewItem == devbox", - "group": "inline" + "group": "inline@2" } ] } diff --git a/extensions/ide/vscode/devbox/src/api/ssh.ts b/extensions/ide/vscode/devbox/src/api/ssh.ts index e634bcbd09f..55e605b497a 100644 --- a/extensions/ide/vscode/devbox/src/api/ssh.ts +++ b/extensions/ide/vscode/devbox/src/api/ssh.ts @@ -1,49 +1,52 @@ -const fs = require('fs') +import fs from 'fs' -export const parseSSHConfig = (configFilePath: string) => { +import { GlobalStateManager } from '../utils/globalStateManager' + +export const parseSSHConfig = (filePath: string) => { return new Promise((resolve, reject) => { - fs.readFile(configFilePath, 'utf-8', (err: any, data: any) => { + fs.readFile(filePath, 'utf-8', (err: any, data: any) => { if (err) { return reject(err) } const lines = data.split('\n') const devboxList = [] as any[] - let currentHost = {} as any - let lastComment = '' + let currentHostObj = {} as any lines.forEach((line: string) => { line = line.trim() - if (line.startsWith('#')) { - // 保存注释,特别是 WorkingDir 注释 - lastComment = line - if (line.startsWith('# WorkingDir:')) { - currentHost.remotePath = line.split(':')[1].trim() - } - } else if (line.startsWith('Host ')) { - // 如果当前有主机信息且是 usw.sailos.io,则保存 - if (currentHost.hostName === 'usw.sailos.io') { - devboxList.push(currentHost) + if (line.startsWith('Host ')) { + // TODO:这里改成注入,而不是硬编码 + if (!!currentHostObj.StrictHostKeyChecking) { + currentHostObj.remotePath = + GlobalStateManager.getWorkDir('remotePath') + devboxList.push(currentHostObj) } - // 开始新的主机信息 - currentHost = { host: line.split(' ')[1] } + currentHostObj = { host: line.split(' ')[1] } } else if (line.startsWith('HostName ')) { - currentHost.hostName = line.split(' ')[1] + currentHostObj.hostName = line.split(' ')[1] } else if (line.startsWith('User ')) { - currentHost.user = line.split(' ')[1] + currentHostObj.user = line.split(' ')[1] } else if (line.startsWith('Port ')) { - currentHost.port = line.split(' ')[1] + currentHostObj.port = line.split(' ')[1] } else if (line.startsWith('IdentityFile ')) { - currentHost.identityFile = line.split(' ')[1] + currentHostObj.identityFile = line.split(' ')[1] + } else if (line.startsWith('IdentitiesOnly ')) { + currentHostObj.IdentitiesOnly = line.split(' ')[1] + } else if (line.startsWith('StrictHostKeyChecking ')) { + currentHostObj.StrictHostKeyChecking = line.split(' ')[1] } }) - // 最后一个主机信息处理 - if (currentHost.hostName === 'usw.sailos.io') { - devboxList.push(currentHost) + // the last one + if (!!currentHostObj.StrictHostKeyChecking) { + currentHostObj.remotePath = GlobalStateManager.getWorkDir('remotePath') + devboxList.push(currentHostObj) } + console.log(devboxList) + resolve(devboxList) }) }) diff --git a/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts b/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts index 8548c4ce0f5..eb4e89450e3 100644 --- a/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts +++ b/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts @@ -1,4 +1,3 @@ -import path from 'path' import * as os from 'os' import * as fs from 'fs' import * as vscode from 'vscode' @@ -7,13 +6,15 @@ import { execSync } from 'child_process' import { Disposable } from '../common/dispose' import { modifiedRemoteSSHConfig } from '../utils/remoteSSHConfig' - -const defaultSSHConfigPath = path.resolve(os.homedir(), '.ssh/config') -const defaultDevboxSSHConfigPath = path.resolve( - os.homedir(), - '.ssh/sealos/devbox_config' -) -const defaultSSHKeyPath = path.resolve(os.homedir(), '.ssh/sealos') +import { + defaultSSHConfigPath, + defaultDevboxSSHConfigPath, + defaultSSHKeyPath, +} from '../constant/file' +import { + convertSSHConfigToVersion2, + ensureFileExists, +} from '../utils/sshConfig' export class RemoteSSHConnector extends Disposable { constructor(context: vscode.ExtensionContext) { @@ -26,6 +27,22 @@ export class RemoteSSHConnector extends Disposable { ) } } + private sshConfigPreProcess() { + // 1. ensure .ssh/config exists + ensureFileExists(defaultSSHConfigPath, '.ssh') + // 2. ensure .ssh/sealos/devbox_config exists + ensureFileExists(defaultDevboxSSHConfigPath, '.ssh/sealos') + // 3. ensure .ssh/config includes .ssh/sealos/devbox_config + const existingSSHConfig = fs.readFileSync(defaultSSHConfigPath, 'utf8') + if (!existingSSHConfig.includes('Include ~/.ssh/sealos/devbox_config')) { + let existingSSHConfig = fs.readFileSync(defaultSSHConfigPath, 'utf-8') + const newConfig = + 'Include ~/.ssh/sealos/devbox_config\n' + existingSSHConfig + fs.writeFileSync(defaultSSHConfigPath, newConfig) + } + // 4. ensure sshConfig from version1 to version2 + convertSSHConfigToVersion2(defaultDevboxSSHConfigPath) + } private async connectRemoteSSH(args: { sshDomain: string @@ -59,51 +76,9 @@ export class RemoteSSHConnector extends Disposable { }) const sshConfigString = SSHConfig.stringify(sshConfig) - try { - // 1. ensure .ssh/config exists - if (!fs.existsSync(defaultSSHConfigPath)) { - fs.mkdirSync(path.resolve(os.homedir(), '.ssh'), { - recursive: true, - }) - fs.writeFileSync(defaultSSHConfigPath, '', 'utf8') - // .ssh/config authority - if (os.platform() === 'win32') { - // Windows - execSync(`icacls "${defaultSSHConfigPath}" /inheritance:r`) - execSync( - `icacls "${defaultSSHConfigPath}" /grant:r ${process.env.USERNAME}:F` - ) - execSync(`icacls "${defaultSSHConfigPath}" /remove:g everyone`) - } else { - // Unix-like system (Mac, Linux) - execSync(`chmod 600 "${defaultSSHConfigPath}"`) - } - } - // 2. ensure .ssh/sealos/devbox_config exists and has the correct authority - if (!fs.existsSync(defaultDevboxSSHConfigPath)) { - fs.mkdirSync(path.resolve(os.homedir(), '.ssh/sealos'), { - recursive: true, - }) - fs.writeFileSync(defaultDevboxSSHConfigPath, '', 'utf8') - if (os.platform() === 'win32') { - execSync(`icacls "${defaultDevboxSSHConfigPath}" /inheritance:r`) - execSync( - `icacls "${defaultDevboxSSHConfigPath}" /grant:r ${process.env.USERNAME}:F` - ) - execSync(`icacls "${defaultDevboxSSHConfigPath}" /remove:g everyone`) - } else { - execSync(`chmod 600 "${defaultDevboxSSHConfigPath}"`) - } - } - // 3. ensure .ssh/config includes .ssh/sealos/devbox_config - const existingSSHConfig = fs.readFileSync(defaultSSHConfigPath, 'utf8') - if (!existingSSHConfig.includes('Include ~/.ssh/sealos/devbox_config')) { - let existingSSHConfig = fs.readFileSync(defaultSSHConfigPath, 'utf-8') - const newConfig = - 'Include ~/.ssh/sealos/devbox_config\n' + existingSSHConfig - fs.writeFileSync(defaultSSHConfigPath, newConfig) - } + this.sshConfigPreProcess() + try { // 读取现有的 devbox 配置文件 const existingDevboxConfigLines = fs .readFileSync(defaultDevboxSSHConfigPath, 'utf8') diff --git a/extensions/ide/vscode/devbox/src/constant/file.ts b/extensions/ide/vscode/devbox/src/constant/file.ts new file mode 100644 index 00000000000..71d5657f405 --- /dev/null +++ b/extensions/ide/vscode/devbox/src/constant/file.ts @@ -0,0 +1,9 @@ +import path from 'path' +import * as os from 'os' + +export const defaultSSHConfigPath = path.resolve(os.homedir(), '.ssh/config') +export const defaultDevboxSSHConfigPath = path.resolve( + os.homedir(), + '.ssh/sealos/devbox_config' +) +export const defaultSSHKeyPath = path.resolve(os.homedir(), '.ssh/sealos') diff --git a/extensions/ide/vscode/devbox/src/extension.ts b/extensions/ide/vscode/devbox/src/extension.ts index d2be9dee5c8..144ddac0bf3 100644 --- a/extensions/ide/vscode/devbox/src/extension.ts +++ b/extensions/ide/vscode/devbox/src/extension.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode' import { Webview } from './commands/webview' import { RemoteSSHConnector } from './commands/remoteConnector' -import { TreeView } from './commands/treeview' +import { DevboxListViewProvider } from './providers/DevboxListViewProvider' import { UriHandler } from './utils/handleUri' import { NetworkViewProvider } from './providers/NetworkViewProvider' import { DBViewProvider } from './providers/DbViewProvider' @@ -22,9 +22,9 @@ export async function activate(context: vscode.ExtensionContext) { const remoteConnector = new RemoteSSHConnector(context) context.subscriptions.push(remoteConnector) - // tree view - const treeView = new TreeView(context) - context.subscriptions.push(treeView) + // devboxList view + const devboxListViewProvider = new DevboxListViewProvider(context) + context.subscriptions.push(devboxListViewProvider) // token manager GlobalStateManager.init(context) diff --git a/extensions/ide/vscode/devbox/src/commands/treeview.ts b/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts similarity index 86% rename from extensions/ide/vscode/devbox/src/commands/treeview.ts rename to extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts index 73d318b9701..35cbb76027f 100644 --- a/extensions/ide/vscode/devbox/src/commands/treeview.ts +++ b/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts @@ -1,18 +1,18 @@ -import path from 'path' -import * as os from 'os' import * as vscode from 'vscode' import { parseSSHConfig } from '../api/ssh' import { Disposable } from '../common/dispose' import { DevboxListItem } from '../types/devbox' +import { defaultDevboxSSHConfigPath } from '../constant/file' -export class TreeView extends Disposable { +export class DevboxListViewProvider extends Disposable { constructor(context: vscode.ExtensionContext) { super() if (context.extension.extensionKind === vscode.ExtensionKind.UI) { + // view const projectTreeDataProvider = new MyTreeDataProvider('devboxDashboard') + // TODO: 完善 feedback部分 const feedbackTreeDataProvider = new MyTreeDataProvider('devboxFeedback') - // views const devboxDashboardView = vscode.window.createTreeView( 'devboxDashboard', { @@ -20,8 +20,6 @@ export class TreeView extends Disposable { } ) this._register(devboxDashboardView) - - // 添加视图可见性变化事件监听器 this._register( devboxDashboardView.onDidChangeVisibility(() => { if (devboxDashboardView.visible) { @@ -29,7 +27,6 @@ export class TreeView extends Disposable { } }) ) - // commands this._register( vscode.commands.registerCommand('devboxDashboard.refresh', () => { @@ -83,12 +80,7 @@ class MyTreeDataProvider implements vscode.TreeDataProvider { private refreshData(): void { if (this.treeName === 'devboxDashboard') { - const defaultSSHConfigPath = path.resolve( - os.homedir(), - '.ssh/sealos/devbox_config' - ) - - parseSSHConfig(defaultSSHConfigPath).then((data) => { + parseSSHConfig(defaultDevboxSSHConfigPath).then((data) => { this.treeData = data as DevboxListItem[] this._onDidChangeTreeData.fire(undefined) }) @@ -108,9 +100,11 @@ class MyTreeDataProvider implements vscode.TreeDataProvider { return element } + // TODO: 根据不同的代理跳转到不同的页面,而且可以进行设置里的配置 create(item: MyTreeItem) { - vscode.commands.executeCommand('devbox.openWebview') - vscode.window.showInformationMessage('create') + vscode.commands.executeCommand('devbox.openExternalLink', [ + 'https://usw.sailos.io/?openapp=system-devbox', + ]) } async open(item: MyTreeItem) { @@ -138,7 +132,7 @@ class MyTreeDataProvider implements vscode.TreeDataProvider { if (!element) { // 第一级:显示所有域名 const domains = [ - ...new Set(this.treeData.map((item) => item.host.split('-')[0])), + ...new Set(this.treeData.map((item) => item.host.split('_')[0])), ] return Promise.resolve( domains.map( @@ -157,10 +151,7 @@ class MyTreeDataProvider implements vscode.TreeDataProvider { ...new Set( this.treeData .filter((item) => item.host.startsWith(element.label as string)) - .map((item) => { - const parts = item.host.split('-') - return parts.slice(1, 3).join('-') - }) + .map((item) => item.host.split('_')[1]) ), ] return Promise.resolve( @@ -178,15 +169,15 @@ class MyTreeDataProvider implements vscode.TreeDataProvider { } else if (!element.devboxName) { // 第三级:显示指定命名空间下的所有 devbox const devboxes = this.treeData.filter((item) => { - const parts = item.host.split('-') + const parts = item.host.split('_') const domain = parts[0] - const namespace = parts.slice(1, 3).join('-') + const namespace = parts[1] return domain === element.domain && namespace === element.namespace }) return Promise.resolve( devboxes.map((devbox) => { - const parts = devbox.host.split('-') - const devboxName = parts.slice(3, -1).join('-') + const parts = devbox.host.split('_') + const devboxName = parts.slice(2).join('_') const treeItem = new MyTreeItem( devboxName, devbox.hostName, @@ -195,9 +186,9 @@ class MyTreeDataProvider implements vscode.TreeDataProvider { element.namespace, devboxName, devbox.host, - devbox.remotePath // 添加这个参数 + devbox.remotePath ) - treeItem.contextValue = 'devbox' // 确保设置了正确的 contextValue + treeItem.contextValue = 'devbox' return treeItem }) ) diff --git a/extensions/ide/vscode/devbox/src/utils/handleUri.ts b/extensions/ide/vscode/devbox/src/utils/handleUri.ts index 898c0800ca4..57a6b231ac6 100644 --- a/extensions/ide/vscode/devbox/src/utils/handleUri.ts +++ b/extensions/ide/vscode/devbox/src/utils/handleUri.ts @@ -20,6 +20,10 @@ export class UriHandler { GlobalStateManager.setToken(params.sshHostLabel, params.token) } + if (params.workingDir && params.sshHostLabel) { + GlobalStateManager.setWorkDir(params.sshHostLabel, params.workingDir) + } + if (params.sshPort === '0') { vscode.window.showInformationMessage( `SSH Port is not correct,maybe your devbox's nodeport is over the limit` @@ -44,13 +48,14 @@ export class UriHandler { } private validateParams(params: any): boolean { + console.log(params) return !!( params.sshDomain && params.sshPort && params.base64PrivateKey && params.sshHostLabel && params.workingDir && - params.authToken + params.token ) } } diff --git a/extensions/ide/vscode/devbox/src/utils/sshConfig.ts b/extensions/ide/vscode/devbox/src/utils/sshConfig.ts index e69de29bb2d..c25ccf1be83 100644 --- a/extensions/ide/vscode/devbox/src/utils/sshConfig.ts +++ b/extensions/ide/vscode/devbox/src/utils/sshConfig.ts @@ -0,0 +1,122 @@ +import * as os from 'os' +import * as fs from 'fs' +import path from 'path' +import { execSync } from 'child_process' +import { GlobalStateManager } from './globalStateManager' + +// 将老版本的 ssh 配置改成新版本的 ssh 配置 +// # WorkingDir: /home/sealos/project +// Host bja.sealos.run-ns-wappehp7-test-t6unaf4bbob +// HostName bja.sealos.run +// User sealos +// Port 40398 +// IdentityFile ~/.ssh/sealos/bja.sealos.run_ns-wappehp7_test +// IdentitiesOnly yes +// StrictHostKeyChecking no + +// 转换为下边的: +// Host的转换,去掉随机串,然后-改为_ +// 去掉WorkingDir 的注释,改为全局存储 +// Host usw.sailos.io_ns-rqtny6y6_devbox +// HostName usw.sailos.io +// User devbox +// Port 31328 +// IdentityFile ~/.ssh/sealos/usw.sailos.io_ns-rqtny6y6_devbox +// IdentitiesOnly yes +// StrictHostKeyChecking no +export function convertSSHConfigToVersion2(filePath: string) { + const output: Record> = {} + let result = '' + + fs.readFile(filePath, 'utf8', (err, data) => { + if (err) { + console.error('Error reading file:', err) + return + } + + const lines = data.split('\n') + let currentWorkDir: any = null + let formattedHostName = '' + + lines.forEach((line) => { + line = line.trim() + + // 处理 WorkingDir 行 + if (line.startsWith('# WorkingDir:')) { + currentWorkDir = line.split(': ')[1].trim() + return + } + + // 处理 Host 行 + const hostMatch = line.match(/^Host (.+)/) + if (hostMatch) { + let hostName = hostMatch[1] + if (hostName.includes('_ns-')) { + formattedHostName = hostName + } else { + hostName = hostName.replace(/-([^-\s]+)$/, '') + const namespace = hostName.match(/ns-([a-z0-9]+)(?=-)/) + if (namespace) { + formattedHostName = hostName.replace( + /^(.+)-ns-([a-z0-9]+)-(.+)$/, + '$1_ns-$2_$3' + ) + } else { + formattedHostName = hostName + } + } + + // 初始化 Host 对象 + output[formattedHostName] = { + workDir: currentWorkDir, + } + return + } + + // 处理其他配置项 + if (currentWorkDir && line) { + const keyValueMatch = line.match(/(\S+)\s+(.+)/) + if (keyValueMatch) { + const [_, key, value] = keyValueMatch + output[formattedHostName][key] = value + } + } + }) + console.log('output', output) + + for (const [host, config] of Object.entries(output)) { + if (config.workDir) { + GlobalStateManager.setWorkDir(host, config.workDir) + } + + result += `Host ${host}\n` + for (const [key, value] of Object.entries(config)) { + if (key !== 'workDir') { + result += ` ${key} ${value}\n` + } + } + result += '\n' + } + result = result.trim() + fs.writeFileSync(filePath, result, 'utf8') + }) +} + +export function ensureFileExists(filePath: string, parentDir: string) { + if (!fs.existsSync(filePath)) { + fs.mkdirSync(path.resolve(os.homedir(), parentDir), { + recursive: true, + }) + fs.writeFileSync(filePath, '', 'utf8') + // .ssh/config authority + if (os.platform() === 'win32') { + // Windows + execSync(`icacls "${filePath}" /inheritance:r`) + execSync(`icacls "${filePath}" /grant:r ${process.env.USERNAME}:F`) + execSync(`icacls "${filePath}" /remove:g everyone`) + } else { + // Unix-like system (Mac, Linux) + execSync(`chmod 600 "${filePath}"`) + } + } +}