From 004ebd1a518f14cf12ae3f0b6ca1ef613d10be3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Wed, 11 Jun 2025 10:58:48 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(handleFinish):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- .../main/deploy/core/DeploymentService.ts | 11 ++- electron/welcome/timeLine.vue | 84 ++++++++++++++++--- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/electron/main/deploy/core/DeploymentService.ts b/electron/main/deploy/core/DeploymentService.ts index 9f3b7046..25c4bf36 100644 --- a/electron/main/deploy/core/DeploymentService.ts +++ b/electron/main/deploy/core/DeploymentService.ts @@ -1227,8 +1227,15 @@ export class DeploymentService { // 创建临时文件写入内容,然后移动到 hosts 文件位置 const tempFile = '/tmp/hosts_new'; - // 写入新内容到临时文件,然后移动到 hosts 文件 - const command = `${sudoCommand}bash -c 'echo "${newContent.replace(/'/g, "'\"'\"'")}" > ${tempFile} && mv ${tempFile} ${hostsPath}'`; + // 先将内容写入临时文件,避免直接在命令行中处理复杂的字符串转义 + try { + fs.writeFileSync(tempFile, newContent); + } catch (error) { + throw new Error(`无法创建临时文件: ${error}`); + } + + // 移动临时文件到 hosts 文件位置 + const command = `${sudoCommand}bash -c 'mv ${tempFile} ${hostsPath}'`; await execAsyncWithAbort( command, diff --git a/electron/welcome/timeLine.vue b/electron/welcome/timeLine.vue index 77e02577..89d477c0 100644 --- a/electron/welcome/timeLine.vue +++ b/electron/welcome/timeLine.vue @@ -239,33 +239,91 @@ const handleFinish = async () => { try { // 1. 设置默认代理 URL if (window.eulercopilotWelcome?.config) { - await window.eulercopilotWelcome.config.setProxyUrl( - 'https://www.eulercopilot.local', - ); + try { + await window.eulercopilotWelcome.config.setProxyUrl( + 'https://www.eulercopilot.local', + ); + } catch (configError) { + console.error('❌ 设置代理 URL 失败:', configError); + } + } else { + console.warn('❌ 未找到 config API'); } // 2. 将域名添加到 /etc/hosts - await addHostsEntries(); + try { + await addHostsEntries(); + } catch (hostsError) { + console.error('❌ 添加 hosts 条目失败:', hostsError); + } - // 部署完成,可以通过 emit 事件通知父组件完成 - // emit('finish'); + // 3. 完成欢迎流程,关闭欢迎窗口并打开主窗口 + try { + if (window.eulercopilotWelcome?.welcome) { + await window.eulercopilotWelcome.welcome.complete(); + } else { + console.warn('❌ 未找到 welcome API'); + } + } catch (welcomeError) { + console.error('❌ 完成欢迎流程失败:', welcomeError); + } } catch (error) { - console.error('完成部署后续配置失败:', error); - // 这里可以显示错误提示,但不阻止部署完成 + console.error('❌ 完成部署后续配置失败:', error); + } finally { + console.log('🏁 handleFinish 执行完成'); } }; // 添加 hosts 条目 const addHostsEntries = async () => { try { + console.log('📝 检查 deployment API'); + console.log('window.eulercopilotWelcome:', window.eulercopilotWelcome); + if (window.eulercopilotWelcome && window.eulercopilotWelcome.deployment) { - await window.eulercopilotWelcome.deployment.addHostsEntries([ - 'www.eulercopilot.local', - 'authhub.eulercopilot.local', - ]); + console.log('✅ 找到 deployment API'); + console.log('deployment 对象:', window.eulercopilotWelcome.deployment); + console.log( + 'addHostsEntries 方法:', + window.eulercopilotWelcome.deployment.addHostsEntries, + ); + + if ( + typeof window.eulercopilotWelcome.deployment.addHostsEntries === + 'function' + ) { + console.log('✅ addHostsEntries 方法存在,开始调用'); + + const domains = [ + 'www.eulercopilot.local', + 'authhub.eulercopilot.local', + ]; + console.log('📝 要添加的域名:', domains); + + await window.eulercopilotWelcome.deployment.addHostsEntries(domains); + console.log('✅ addHostsEntries 调用成功'); + } else { + console.error('❌ addHostsEntries 方法不存在'); + throw new Error('addHostsEntries 方法不可用'); + } + } else { + console.warn('❌ 未找到 deployment API'); + console.log( + 'window.eulercopilotWelcome 的完整内容:', + JSON.stringify(window.eulercopilotWelcome, null, 2), + ); + throw new Error('部署服务 API 不可用'); } } catch (error) { - throw new Error(`添加 hosts 条目失败: ${error}`); + console.error('❌ addHostsEntries 失败:', error); + console.error('错误详情:', { + name: error instanceof Error ? error.name : 'Unknown', + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + throw new Error( + `添加 hosts 条目失败: ${error instanceof Error ? error.message : String(error)}`, + ); } }; -- Gitee From a060ff07fc844a35dab9ac349da454b8976bfe7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 10 Jun 2025 21:54:23 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(deploy):=20=E4=BC=98=E5=8C=96=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E6=B7=BB=E5=8A=A0=E9=80=BB=E8=BE=91=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E9=87=8D=E5=A4=8D=E5=B9=B6=E6=94=AF=E6=8C=81=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- .../main/deploy/core/DeploymentService.ts | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/electron/main/deploy/core/DeploymentService.ts b/electron/main/deploy/core/DeploymentService.ts index 25c4bf36..94cd3c37 100644 --- a/electron/main/deploy/core/DeploymentService.ts +++ b/electron/main/deploy/core/DeploymentService.ts @@ -1199,11 +1199,14 @@ export class DeploymentService { } // 过滤出需要添加的域名(避免重复添加) + // 使用正则表达式检测域名是否已存在,处理多个空格/tab的情况 const domainsToAdd = domains.filter((domain) => { - return ( - !hostsContent.includes(`127.0.0.1\t${domain}`) && - !hostsContent.includes(`127.0.0.1 ${domain}`) + // 匹配 127.0.0.1 + 一个或多个空白字符 + 域名 + 行尾或空白字符或注释 + const domainRegex = new RegExp( + `^127\\.0\\.0\\.1\\s+${domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$|#)`, + 'm', ); + return !domainRegex.test(hostsContent); }); if (domainsToAdd.length === 0) { @@ -1211,15 +1214,29 @@ export class DeploymentService { return; } + // 检查是否已经存在 openEuler Intelligence 注释标签 + const commentExists = hostsContent.includes( + '# openEuler Intelligence Local Deployment', + ); + // 构建要添加的内容 const entriesToAdd = domainsToAdd - .map((domain) => `127.0.0.1\t${domain}`) + .map((domain) => `127.0.0.1 ${domain}`) .join('\n'); - const newContent = - hostsContent.trim() + - '\n\n# EulerCopilot Local Deployment\n' + - entriesToAdd + - '\n'; + + let newContent: string; + if (commentExists) { + // 如果注释已存在,在注释后面插入新的域名条目 + const commentRegex = /(# openEuler Intelligence Local Deployment\n)/; + newContent = hostsContent.replace(commentRegex, `$1${entriesToAdd}\n`); + } else { + // 如果注释不存在,添加完整的注释块和域名条目 + newContent = + hostsContent.trim() + + '\n\n# openEuler Intelligence Local Deployment\n' + + entriesToAdd + + '\n'; + } // 使用管理员权限写入 hosts 文件 const sudoCommand = this.getSudoCommand(); -- Gitee From f01b3d134ef07ccd0535c9be174fc58ba2ed4814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 10 Jun 2025 22:02:22 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(deploy):=20=E9=87=8D=E6=9E=84sudo?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E7=AE=A1=E7=90=86=EF=BC=8C=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=89=A7=E8=A1=8C=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8C=81=E4=B9=85=E5=8C=96=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- .../main/deploy/core/DeploymentService.ts | 254 ++++++++++++------ 1 file changed, 172 insertions(+), 82 deletions(-) diff --git a/electron/main/deploy/core/DeploymentService.ts b/electron/main/deploy/core/DeploymentService.ts index 94cd3c37..a6ba7231 100644 --- a/electron/main/deploy/core/DeploymentService.ts +++ b/electron/main/deploy/core/DeploymentService.ts @@ -68,7 +68,6 @@ export class DeploymentService { }; private statusCallback?: (status: DeploymentStatus) => void; private abortController?: AbortController; - private currentProcess?: any; private sudoSessionActive: boolean = false; constructor() { @@ -215,7 +214,6 @@ export class DeploymentService { } finally { // 清理资源 this.abortController = undefined; - this.currentProcess = undefined; this.sudoSessionActive = false; // 重置sudo会话状态 } } @@ -427,24 +425,20 @@ export class DeploymentService { } try { - // 构建需要权限的命令,使用已建立的sudo会话 - const command = this.buildRootCommand( - toolsScriptPath, - false, - undefined, - { - KUBECONFIG: '/etc/rancher/k3s/k3s.yaml', - }, - ); + // 直接使用已建立的sudo会话执行脚本 + const envVars = { + KUBECONFIG: '/etc/rancher/k3s/k3s.yaml', + }; + + // 构建环境变量字符串 + const envString = Object.entries(envVars) + .map(([key, value]) => `${key}="${value}"`) + .join(' '); // 执行脚本 - await execAsyncWithAbort( - command, - { - cwd: scriptsPath, - timeout: 600000, // 10分钟超时,k3s安装可能需要较长时间 - }, - this.abortController?.signal, + await this.executeSudoCommand( + `${envString} bash "${toolsScriptPath}"`, + 600000, // 10分钟超时,k3s安装可能需要较长时间 ); } catch (error) { // 检查是否是超时错误 @@ -544,12 +538,7 @@ export class DeploymentService { if (stdout.trim() !== 'active') { // 尝试启动k3s服务 - const sudoCommand = this.getSudoCommand(); - await execAsyncWithAbort( - `${sudoCommand}systemctl start k3s`, - { timeout: 30000 }, - this.abortController?.signal, - ); + await this.executeSudoCommand('systemctl start k3s', 30000); // 再次检查状态 const { stdout: newStatus } = await execAsyncWithAbort( @@ -576,15 +565,13 @@ export class DeploymentService { const maxWaitTime = 60000; // 60秒 const checkInterval = 5000; // 5秒检查一次 const startTime = Date.now(); - const sudoCommand = this.getSudoCommand(); while (Date.now() - startTime < maxWaitTime) { try { // 检查k3s.yaml文件是否存在且可读 - const { stdout } = await execAsyncWithAbort( - `${sudoCommand}ls -la /etc/rancher/k3s/k3s.yaml`, - {}, - this.abortController?.signal, + const { stdout } = await this.executeSudoCommand( + 'ls -la /etc/rancher/k3s/k3s.yaml', + 10000, ); if (stdout.includes('k3s.yaml')) { @@ -610,13 +597,12 @@ export class DeploymentService { try { // 设置KUBECONFIG环境变量并测试连接 const kubeconfigPath = '/etc/rancher/k3s/k3s.yaml'; - const sudoCommand = this.getSudoCommand(); // 使用sudo权限执行kubectl命令,因为k3s.yaml文件只有root用户可以读取 - const { stdout } = await execAsyncWithAbort( - `${sudoCommand}bash -c 'KUBECONFIG=${kubeconfigPath} kubectl cluster-info'`, - { timeout: 15000 }, - this.abortController?.signal, + const { stdout } = await this.executeSudoCommand( + `KUBECONFIG=${kubeconfigPath} kubectl cluster-info`, + 15000, + { KUBECONFIG: kubeconfigPath }, ); if (!stdout.includes('is running at')) { @@ -624,10 +610,10 @@ export class DeploymentService { } // 验证节点状态 - const { stdout: nodeStatus } = await execAsyncWithAbort( - `${sudoCommand}bash -c 'KUBECONFIG=${kubeconfigPath} kubectl get nodes'`, - { timeout: 15000 }, - this.abortController?.signal, + const { stdout: nodeStatus } = await this.executeSudoCommand( + `KUBECONFIG=${kubeconfigPath} kubectl get nodes`, + 15000, + { KUBECONFIG: kubeconfigPath }, ); if (!nodeStatus.includes('Ready')) { @@ -699,36 +685,35 @@ export class DeploymentService { throw new Error(`脚本文件不存在: ${scriptPath}`); } - // 准备环境变量,过滤掉 undefined 值 - const baseEnv = { - ...process.env, + // 构建需要权限的命令 + const envVars = { ...script.envVars, // 确保 KUBECONFIG 环境变量正确设置 KUBECONFIG: '/etc/rancher/k3s/k3s.yaml', }; // 过滤掉 undefined 值,确保所有值都是字符串 - const execEnv = Object.fromEntries( - Object.entries(baseEnv).filter(([, value]) => value !== undefined), + const cleanEnvVars = Object.fromEntries( + Object.entries(envVars).filter(([, value]) => value !== undefined), ) as Record; - // 构建需要权限的命令 - const command = this.buildRootCommand( - scriptPath, - script.useInputRedirection, - script.useInputRedirection ? 'authhub.eulercopilot.local' : undefined, - execEnv, - ); - try { - // 给脚本添加执行权限并执行 - await execAsyncWithAbort( + // 使用已建立的sudo会话执行脚本,避免重复输入密码 + let command = `bash "${scriptPath}"`; + + if ( + script.useInputRedirection && + script.useInputRedirection === true + ) { + // 对于需要输入重定向的脚本,预设输入内容 + const inputData = 'authhub.eulercopilot.local'; + command = `echo "${inputData}" | ${command}`; + } + + await this.executeSudoCommand( command, - { - cwd: scriptsPath, - timeout: 600000, // 10分钟超时,某些服务安装可能需要较长时间 - }, - this.abortController?.signal, + 600000, // 10分钟超时,某些服务安装可能需要较长时间 + cleanEnvVars, ); } catch (error) { // 检查是否是超时错误 @@ -886,7 +871,8 @@ export class DeploymentService { currentStep: 'preparing-environment', }); - const sudoCommand = this.getSudoCommand(); + // 使用pkexec获取一次性权限,并创建一个长期有效的sudo会话 + await this.establishPersistentSudoSession(); // 检查是否需要安装基础工具 const missingTools = this.environmentCheckResult?.missingBasicTools || []; @@ -908,13 +894,9 @@ export class DeploymentService { } if (commands.length > 0) { - // 一次性执行所有需要权限的命令 + // 使用已建立的sudo会话执行命令 const combinedCommand = commands.join(' && '); - await execAsyncWithAbort( - `${sudoCommand}bash -c '${combinedCommand}'`, - { timeout: 300000 }, // 5分钟超时 - this.abortController?.signal, - ); + await this.executeSudoCommand(combinedCommand, 300000); // 5分钟超时 let message = '管理员权限获取成功'; if (missingTools.length > 0) { @@ -929,13 +911,6 @@ export class DeploymentService { currentStep: 'preparing-environment', }); } else { - // 没有需要执行的命令,只获取权限验证 - await execAsyncWithAbort( - `${sudoCommand}true`, - { timeout: 60000 }, // 60秒超时,给用户足够时间输入密码 - this.abortController?.signal, - ); - this.updateStatus({ message: '管理员权限获取成功', currentStep: 'preparing-environment', @@ -975,6 +950,129 @@ export class DeploymentService { } } + /** + * 建立持久化的sudo会话,只需要输入一次密码 + */ + private async establishPersistentSudoSession(): Promise { + if (process.platform !== 'linux') { + return; + } + + // 检查是否为root用户 + if (process.getuid && process.getuid() === 0) { + return; + } + + try { + // 首先检查用户是否有sudo权限,并获取密码 + const sudoCommand = this.getSudoCommand(); + + // 如果使用pkexec,获取一次权限验证后,创建一个sudo timestamp + if (sudoCommand.includes('pkexec')) { + // 使用pkexec验证权限并创建sudo timestamp + await execAsyncWithAbort( + `${sudoCommand}bash -c 'sudo -v'`, + { timeout: 60000 }, + this.abortController?.signal, + ); + } else { + // 如果不使用pkexec,直接验证sudo + await execAsyncWithAbort( + 'sudo -v', + { timeout: 60000 }, + this.abortController?.signal, + ); + } + + // 延长sudo timestamp,确保整个部署过程中sudo会话保持有效 + // 使用后台进程定期刷新sudo timestamp + this.startSudoKeepAlive(); + } catch (error) { + throw new Error( + `建立sudo会话失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + /** + * 启动sudo会话保活机制 + */ + private startSudoKeepAlive(): void { + if (process.platform !== 'linux') { + return; + } + + // 每4分钟刷新一次sudo timestamp(sudo默认超时是5分钟) + const keepAliveInterval = setInterval(async () => { + try { + if (this.sudoSessionActive && !this.abortController?.signal.aborted) { + await execAsyncWithAbort( + 'sudo -n true', // -n 参数表示非交互模式,如果需要密码会失败 + { timeout: 5000 }, + this.abortController?.signal, + ); + } else { + // 如果会话不活跃或被中断,停止保活 + clearInterval(keepAliveInterval); + } + } catch { + // sudo会话已过期或失败,停止保活 + clearInterval(keepAliveInterval); + this.sudoSessionActive = false; + } + }, 240000); // 4分钟 + + // 确保在部署结束时清理interval + if (this.abortController) { + this.abortController.signal.addEventListener('abort', () => { + clearInterval(keepAliveInterval); + }); + } + } + + /** + * 使用已建立的sudo会话执行命令 + */ + private async executeSudoCommand( + command: string, + timeout: number = 60000, + envVars?: Record, + ): Promise<{ stdout: string; stderr: string }> { + if (process.platform !== 'linux') { + // 非Linux系统直接执行 + return await execAsyncWithAbort( + command, + { timeout, env: { ...process.env, ...envVars } }, + this.abortController?.signal, + ); + } + + // 检查是否为root用户 + if (process.getuid && process.getuid() === 0) { + return await execAsyncWithAbort( + command, + { timeout, env: { ...process.env, ...envVars } }, + this.abortController?.signal, + ); + } + + // 构建环境变量字符串 + let envString = ''; + if (envVars && Object.keys(envVars).length > 0) { + const envPairs = Object.entries(envVars) + .map(([key, value]) => `${key}="${value}"`) + .join(' '); + envString = envPairs + ' '; + } + + // 使用已建立的sudo会话执行命令 + return await execAsyncWithAbort( + `sudo bash -c '${envString}${command}'`, + { timeout }, + this.abortController?.signal, + ); + } + /** * 获取合适的sudo命令前缀 */ @@ -1174,7 +1272,7 @@ export class DeploymentService { console.log('清理部署相关资源'); } this.abortController = undefined; - this.currentProcess = undefined; + this.sudoSessionActive = false; // 重置sudo会话状态 } } @@ -1239,8 +1337,6 @@ export class DeploymentService { } // 使用管理员权限写入 hosts 文件 - const sudoCommand = this.getSudoCommand(); - // 创建临时文件写入内容,然后移动到 hosts 文件位置 const tempFile = '/tmp/hosts_new'; @@ -1252,13 +1348,7 @@ export class DeploymentService { } // 移动临时文件到 hosts 文件位置 - const command = `${sudoCommand}bash -c 'mv ${tempFile} ${hostsPath}'`; - - await execAsyncWithAbort( - command, - { timeout: 30000 }, - undefined, // 这是部署完成后的操作,不需要 abortController - ); + await this.executeSudoCommand(`mv ${tempFile} ${hostsPath}`, 30000); console.log(`已添加以下域名到 hosts 文件: ${domainsToAdd.join(', ')}`); } catch (error) { -- Gitee From 481c6fd656c7240628b6acd5a405c2020d578a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 10 Jun 2025 22:10:57 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat(deploy):=20=E5=BC=95=E5=85=A5=E4=B8=B4?= =?UTF-8?q?=E6=97=B6sudo=E8=84=9A=E6=9C=AC=E4=BB=A5=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86=E5=B9=B6=E6=B8=85=E7=90=86?= =?UTF-8?q?=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- .../main/deploy/core/DeploymentService.ts | 148 +++++++++++------- 1 file changed, 88 insertions(+), 60 deletions(-) diff --git a/electron/main/deploy/core/DeploymentService.ts b/electron/main/deploy/core/DeploymentService.ts index a6ba7231..e96139cb 100644 --- a/electron/main/deploy/core/DeploymentService.ts +++ b/electron/main/deploy/core/DeploymentService.ts @@ -69,6 +69,7 @@ export class DeploymentService { private statusCallback?: (status: DeploymentStatus) => void; private abortController?: AbortController; private sudoSessionActive: boolean = false; + private tempSudoScriptPath?: string; constructor() { this.cachePath = getCachePath(); @@ -215,6 +216,7 @@ export class DeploymentService { // 清理资源 this.abortController = undefined; this.sudoSessionActive = false; // 重置sudo会话状态 + this.cleanupTemporarySudoScript(); // 清理临时sudo脚本 } } @@ -871,8 +873,8 @@ export class DeploymentService { currentStep: 'preparing-environment', }); - // 使用pkexec获取一次性权限,并创建一个长期有效的sudo会话 - await this.establishPersistentSudoSession(); + // 创建临时的sudo执行脚本 + await this.createTemporarySudoScript(); // 检查是否需要安装基础工具 const missingTools = this.environmentCheckResult?.missingBasicTools || []; @@ -894,7 +896,7 @@ export class DeploymentService { } if (commands.length > 0) { - // 使用已建立的sudo会话执行命令 + // 使用临时sudo脚本执行命令 const combinedCommand = commands.join(' && '); await this.executeSudoCommand(combinedCommand, 300000); // 5分钟超时 @@ -911,6 +913,9 @@ export class DeploymentService { currentStep: 'preparing-environment', }); } else { + // 即使没有要执行的命令,也要验证sudo脚本是否正常工作 + await this.executeSudoCommand('echo "权限验证成功"', 30000); + this.updateStatus({ message: '管理员权限获取成功', currentStep: 'preparing-environment', @@ -951,9 +956,9 @@ export class DeploymentService { } /** - * 建立持久化的sudo会话,只需要输入一次密码 + * 创建临时的sudo执行脚本,避免重复密码输入 */ - private async establishPersistentSudoSession(): Promise { + private async createTemporarySudoScript(): Promise { if (process.platform !== 'linux') { return; } @@ -964,74 +969,57 @@ export class DeploymentService { } try { - // 首先检查用户是否有sudo权限,并获取密码 + // 创建临时目录 + const tempDir = path.join(this.cachePath, 'temp-sudo'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + // 创建临时sudo脚本路径 + this.tempSudoScriptPath = path.join(tempDir, 'sudo-wrapper.sh'); + + // 创建一个简单的包装脚本,只是为了方便调用 + const scriptContent = `#!/bin/bash +# 临时sudo包装脚本,由openEuler Intelligence创建 +# 执行传入的命令 +exec "$@" +`; + + // 写入脚本文件 + fs.writeFileSync(this.tempSudoScriptPath, scriptContent, { mode: 0o755 }); + + // 使用pkexec验证一次权限,确保用户可以正常输入密码 const sudoCommand = this.getSudoCommand(); - // 如果使用pkexec,获取一次权限验证后,创建一个sudo timestamp if (sudoCommand.includes('pkexec')) { - // 使用pkexec验证权限并创建sudo timestamp + // 测试pkexec是否工作正常,执行一个简单的命令 await execAsyncWithAbort( - `${sudoCommand}bash -c 'sudo -v'`, - { timeout: 60000 }, + `${sudoCommand}echo "权限验证成功"`, + { timeout: 60000 }, // 给用户充足时间输入密码 this.abortController?.signal, ); } else { - // 如果不使用pkexec,直接验证sudo - await execAsyncWithAbort( - 'sudo -v', - { timeout: 60000 }, - this.abortController?.signal, - ); + throw new Error('当前系统不支持图形化权限验证工具'); } - - // 延长sudo timestamp,确保整个部署过程中sudo会话保持有效 - // 使用后台进程定期刷新sudo timestamp - this.startSudoKeepAlive(); } catch (error) { - throw new Error( - `建立sudo会话失败: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - /** - * 启动sudo会话保活机制 - */ - private startSudoKeepAlive(): void { - if (process.platform !== 'linux') { - return; - } - - // 每4分钟刷新一次sudo timestamp(sudo默认超时是5分钟) - const keepAliveInterval = setInterval(async () => { - try { - if (this.sudoSessionActive && !this.abortController?.signal.aborted) { - await execAsyncWithAbort( - 'sudo -n true', // -n 参数表示非交互模式,如果需要密码会失败 - { timeout: 5000 }, - this.abortController?.signal, - ); - } else { - // 如果会话不活跃或被中断,停止保活 - clearInterval(keepAliveInterval); + // 清理可能创建的文件 + if (this.tempSudoScriptPath && fs.existsSync(this.tempSudoScriptPath)) { + try { + fs.unlinkSync(this.tempSudoScriptPath); + } catch (cleanupError) { + // 忽略清理错误 + console.warn('清理临时文件失败:', cleanupError); } - } catch { - // sudo会话已过期或失败,停止保活 - clearInterval(keepAliveInterval); - this.sudoSessionActive = false; + this.tempSudoScriptPath = undefined; } - }, 240000); // 4分钟 - - // 确保在部署结束时清理interval - if (this.abortController) { - this.abortController.signal.addEventListener('abort', () => { - clearInterval(keepAliveInterval); - }); + throw new Error( + `初始化权限验证失败: ${error instanceof Error ? error.message : String(error)}`, + ); } } /** - * 使用已建立的sudo会话执行命令 + * 使用pkexec执行命令,在同一个部署过程中尽量减少密码输入 */ private async executeSudoCommand( command: string, @@ -1065,14 +1053,48 @@ export class DeploymentService { envString = envPairs + ' '; } - // 使用已建立的sudo会话执行命令 + // 使用pkexec执行命令 + // pkexec会缓存认证,在短时间内不会重复要求密码 + const sudoCommand = this.getSudoCommand(); + const fullCommand = `${sudoCommand}bash -c '${envString}${command}'`; + return await execAsyncWithAbort( - `sudo bash -c '${envString}${command}'`, + fullCommand, { timeout }, this.abortController?.signal, ); } + /** + * 清理临时sudo脚本 + */ + private cleanupTemporarySudoScript(): void { + if (this.tempSudoScriptPath && fs.existsSync(this.tempSudoScriptPath)) { + try { + // 删除临时脚本文件 + fs.unlinkSync(this.tempSudoScriptPath); + } catch (error) { + // 如果无法删除,记录警告但不抛出错误 + console.warn('清理临时sudo脚本时出错:', error); + } + this.tempSudoScriptPath = undefined; + } + + // 清理临时目录(如果为空) + const tempDir = path.join(this.cachePath, 'temp-sudo'); + if (fs.existsSync(tempDir)) { + try { + const files = fs.readdirSync(tempDir); + if (files.length === 0) { + fs.rmdirSync(tempDir); + } + } catch (error) { + // 忽略清理目录的错误 + console.warn('清理临时目录时出错:', error); + } + } + } + /** * 获取合适的sudo命令前缀 */ @@ -1273,6 +1295,7 @@ export class DeploymentService { } this.abortController = undefined; this.sudoSessionActive = false; // 重置sudo会话状态 + this.cleanupTemporarySudoScript(); // 清理临时sudo脚本 } } @@ -1362,9 +1385,14 @@ export class DeploymentService { * 清理部署文件 */ async cleanup(): Promise { + // 清理临时sudo脚本 + this.cleanupTemporarySudoScript(); + + // 清理部署文件 if (fs.existsSync(this.deploymentPath)) { fs.rmSync(this.deploymentPath, { recursive: true, force: true }); } + this.updateStatus({ status: 'idle', message: '清理完成', -- Gitee From e632af1a13dfc6944774ef6cc5811b0a3e44a369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 10 Jun 2025 22:28:36 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat(deploy):=20=E9=87=8D=E6=9E=84sudo?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E7=AE=A1=E7=90=86=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?sudo=E5=8A=A9=E6=89=8B=E8=BF=9B=E7=A8=8B=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=89=A7=E8=A1=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- .../main/deploy/core/DeploymentService.ts | 284 +++++++++++++----- 1 file changed, 213 insertions(+), 71 deletions(-) diff --git a/electron/main/deploy/core/DeploymentService.ts b/electron/main/deploy/core/DeploymentService.ts index e96139cb..42d4d8f7 100644 --- a/electron/main/deploy/core/DeploymentService.ts +++ b/electron/main/deploy/core/DeploymentService.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import * as fs from 'fs'; -import { exec } from 'child_process'; +import { exec, spawn } from 'child_process'; import { getCachePath } from '../../common/cache-conf'; import type { DeploymentParams, @@ -69,7 +69,7 @@ export class DeploymentService { private statusCallback?: (status: DeploymentStatus) => void; private abortController?: AbortController; private sudoSessionActive: boolean = false; - private tempSudoScriptPath?: string; + private sudoHelperProcess?: any; constructor() { this.cachePath = getCachePath(); @@ -216,7 +216,7 @@ export class DeploymentService { // 清理资源 this.abortController = undefined; this.sudoSessionActive = false; // 重置sudo会话状态 - this.cleanupTemporarySudoScript(); // 清理临时sudo脚本 + this.cleanupSudoHelper(); // 清理sudo助手进程 } } @@ -873,8 +873,8 @@ export class DeploymentService { currentStep: 'preparing-environment', }); - // 创建临时的sudo执行脚本 - await this.createTemporarySudoScript(); + // 启动sudo助手进程 + await this.startSudoHelper(); // 检查是否需要安装基础工具 const missingTools = this.environmentCheckResult?.missingBasicTools || []; @@ -896,7 +896,7 @@ export class DeploymentService { } if (commands.length > 0) { - // 使用临时sudo脚本执行命令 + // 使用sudo助手执行命令 const combinedCommand = commands.join(' && '); await this.executeSudoCommand(combinedCommand, 300000); // 5分钟超时 @@ -913,7 +913,7 @@ export class DeploymentService { currentStep: 'preparing-environment', }); } else { - // 即使没有要执行的命令,也要验证sudo脚本是否正常工作 + // 即使没有要执行的命令,也要验证sudo助手是否正常工作 await this.executeSudoCommand('echo "权限验证成功"', 30000); this.updateStatus({ @@ -956,9 +956,9 @@ export class DeploymentService { } /** - * 创建临时的sudo执行脚本,避免重复密码输入 + * 启动sudo助手进程,只需要一次密码输入 */ - private async createTemporarySudoScript(): Promise { + private async startSudoHelper(): Promise { if (process.platform !== 'linux') { return; } @@ -975,51 +975,102 @@ export class DeploymentService { fs.mkdirSync(tempDir, { recursive: true }); } - // 创建临时sudo脚本路径 - this.tempSudoScriptPath = path.join(tempDir, 'sudo-wrapper.sh'); - - // 创建一个简单的包装脚本,只是为了方便调用 - const scriptContent = `#!/bin/bash -# 临时sudo包装脚本,由openEuler Intelligence创建 -# 执行传入的命令 -exec "$@" + // 创建sudo助手脚本 + const helperScriptPath = path.join(tempDir, 'sudo-helper.sh'); + const helperScriptContent = `#!/bin/bash +# Sudo助手脚本,保持长期运行的sudo会话 + +# 读取命令并执行 +while IFS= read -r command; do + if [ "$command" = "EXIT" ]; then + break + fi + # 使用eval执行命令,支持复杂的命令结构 + eval "$command" + echo "COMMAND_DONE_$$" +done `; - // 写入脚本文件 - fs.writeFileSync(this.tempSudoScriptPath, scriptContent, { mode: 0o755 }); + // 写入助手脚本 + fs.writeFileSync(helperScriptPath, helperScriptContent, { mode: 0o755 }); - // 使用pkexec验证一次权限,确保用户可以正常输入密码 + // 使用pkexec启动助手进程,只需要输入一次密码 const sudoCommand = this.getSudoCommand(); - if (sudoCommand.includes('pkexec')) { - // 测试pkexec是否工作正常,执行一个简单的命令 - await execAsyncWithAbort( - `${sudoCommand}echo "权限验证成功"`, - { timeout: 60000 }, // 给用户充足时间输入密码 - this.abortController?.signal, - ); - } else { + if (!sudoCommand.includes('pkexec')) { throw new Error('当前系统不支持图形化权限验证工具'); } + + // 启动长期运行的sudo助手进程 + const command = `${sudoCommand}bash "${helperScriptPath}"`; + + this.sudoHelperProcess = spawn('bash', ['-c', command], { + stdio: ['pipe', 'pipe', 'pipe'], + }); + + // 等待进程启动并准备就绪 + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('sudo助手进程启动超时')); + }, 60000); // 60秒超时,给用户充足时间输入密码 + + let isResolved = false; + + this.sudoHelperProcess.stdout?.on('data', (data: Buffer) => { + const output = data.toString(); + // 检查是否是我们的命令完成标记,如果是说明进程已经启动 + if (output.includes('COMMAND_DONE_') && !isResolved) { + clearTimeout(timeout); + isResolved = true; + resolve(void 0); + } + }); + + this.sudoHelperProcess.on('error', (error: Error) => { + if (!isResolved) { + clearTimeout(timeout); + isResolved = true; + reject(error); + } + }); + + this.sudoHelperProcess.on('exit', (code: number) => { + if (!isResolved) { + clearTimeout(timeout); + isResolved = true; + reject(new Error(`sudo助手进程异常退出,代码: ${code}`)); + } + }); + + // 发送一个测试命令来确认进程正常工作 + this.sudoHelperProcess.stdin?.write('echo "Helper Ready"\n'); + }); + + // 清理临时脚本文件 + try { + fs.unlinkSync(helperScriptPath); + } catch { + // 忽略清理错误 + } } catch (error) { - // 清理可能创建的文件 - if (this.tempSudoScriptPath && fs.existsSync(this.tempSudoScriptPath)) { + // 清理可能启动的进程 + if (this.sudoHelperProcess) { try { - fs.unlinkSync(this.tempSudoScriptPath); - } catch (cleanupError) { + this.sudoHelperProcess.kill(); + } catch { // 忽略清理错误 - console.warn('清理临时文件失败:', cleanupError); } - this.tempSudoScriptPath = undefined; + this.sudoHelperProcess = undefined; } + throw new Error( - `初始化权限验证失败: ${error instanceof Error ? error.message : String(error)}`, + `启动sudo助手进程失败: ${error instanceof Error ? error.message : String(error)}`, ); } } /** - * 使用pkexec执行命令,在同一个部署过程中尽量减少密码输入 + * 使用sudo助手进程执行命令,无需重复密码输入 */ private async executeSudoCommand( command: string, @@ -1044,53 +1095,144 @@ exec "$@" ); } - // 构建环境变量字符串 - let envString = ''; - if (envVars && Object.keys(envVars).length > 0) { - const envPairs = Object.entries(envVars) - .map(([key, value]) => `${key}="${value}"`) - .join(' '); - envString = envPairs + ' '; + // 使用sudo助手进程执行命令 + if (!this.sudoHelperProcess) { + throw new Error('sudo助手进程未启动,请先初始化sudo会话'); } - // 使用pkexec执行命令 - // pkexec会缓存认证,在短时间内不会重复要求密码 - const sudoCommand = this.getSudoCommand(); - const fullCommand = `${sudoCommand}bash -c '${envString}${command}'`; + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error(`命令执行超时: ${command}`)); + }, timeout); - return await execAsyncWithAbort( - fullCommand, - { timeout }, - this.abortController?.signal, - ); + let stdout = ''; + let stderr = ''; + let isResolved = false; + + const dataHandler = (data: Buffer) => { + const output = data.toString(); + stdout += output; + + // 检查命令是否完成 + if ( + output.includes(`COMMAND_DONE_${this.sudoHelperProcess?.pid}`) && + !isResolved + ) { + clearTimeout(timeoutId); + isResolved = true; + // 移除完成标记 + stdout = stdout.replace( + new RegExp(`COMMAND_DONE_${this.sudoHelperProcess?.pid}\\s*`, 'g'), + '', + ); + resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); + } + }; + + const errorHandler = (data: Buffer) => { + stderr += data.toString(); + }; + + const processErrorHandler = (error: Error) => { + if (!isResolved) { + clearTimeout(timeoutId); + isResolved = true; + reject(error); + } + }; + + const processExitHandler = (code: number) => { + if (!isResolved) { + clearTimeout(timeoutId); + isResolved = true; + reject(new Error(`sudo助手进程异常退出,代码: ${code}`)); + } + }; + + // 绑定事件监听器 + this.sudoHelperProcess.stdout?.on('data', dataHandler); + this.sudoHelperProcess.stderr?.on('data', errorHandler); + this.sudoHelperProcess.on('error', processErrorHandler); + this.sudoHelperProcess.on('exit', processExitHandler); + + // 构建环境变量字符串 + let envString = ''; + if (envVars && Object.keys(envVars).length > 0) { + const envPairs = Object.entries(envVars) + .map(([key, value]) => `export ${key}="${value}"`) + .join('; '); + envString = envPairs + '; '; + } + + // 发送命令到助手进程 + const fullCommand = `${envString}${command}`; + this.sudoHelperProcess.stdin?.write(`${fullCommand}\n`); + + // 设置清理函数 + const cleanup = () => { + this.sudoHelperProcess.stdout?.off('data', dataHandler); + this.sudoHelperProcess.stderr?.off('data', errorHandler); + this.sudoHelperProcess.off('error', processErrorHandler); + this.sudoHelperProcess.off('exit', processExitHandler); + }; + + // 确保在resolve或reject时清理事件监听器 + const originalResolve = resolve; + const originalReject = reject; + + resolve = (value: any) => { + cleanup(); + originalResolve(value); + }; + + reject = (reason: any) => { + cleanup(); + originalReject(reason); + }; + }); } /** - * 清理临时sudo脚本 + * 清理sudo助手进程 */ - private cleanupTemporarySudoScript(): void { - if (this.tempSudoScriptPath && fs.existsSync(this.tempSudoScriptPath)) { + private cleanupSudoHelper(): void { + if (this.sudoHelperProcess) { try { - // 删除临时脚本文件 - fs.unlinkSync(this.tempSudoScriptPath); - } catch (error) { - // 如果无法删除,记录警告但不抛出错误 - console.warn('清理临时sudo脚本时出错:', error); + // 发送退出命令 + this.sudoHelperProcess.stdin?.write('EXIT\n'); + + // 等待一小段时间让进程正常退出 + setTimeout(() => { + if (this.sudoHelperProcess && !this.sudoHelperProcess.killed) { + this.sudoHelperProcess.kill('SIGTERM'); + } + }, 1000); + } catch { + // 强制终止进程 + try { + this.sudoHelperProcess.kill('SIGKILL'); + } catch { + // 忽略强制终止的错误 + } } - this.tempSudoScriptPath = undefined; + this.sudoHelperProcess = undefined; } - // 清理临时目录(如果为空) + // 清理临时目录 const tempDir = path.join(this.cachePath, 'temp-sudo'); if (fs.existsSync(tempDir)) { try { const files = fs.readdirSync(tempDir); - if (files.length === 0) { - fs.rmdirSync(tempDir); - } - } catch (error) { - // 忽略清理目录的错误 - console.warn('清理临时目录时出错:', error); + files.forEach((file) => { + try { + fs.unlinkSync(path.join(tempDir, file)); + } catch { + // 忽略文件删除错误 + } + }); + fs.rmdirSync(tempDir); + } catch { + // 忽略目录清理错误 } } } @@ -1295,7 +1437,7 @@ exec "$@" } this.abortController = undefined; this.sudoSessionActive = false; // 重置sudo会话状态 - this.cleanupTemporarySudoScript(); // 清理临时sudo脚本 + this.cleanupSudoHelper(); // 清理sudo助手进程 } } @@ -1385,8 +1527,8 @@ exec "$@" * 清理部署文件 */ async cleanup(): Promise { - // 清理临时sudo脚本 - this.cleanupTemporarySudoScript(); + // 清理sudo助手进程 + this.cleanupSudoHelper(); // 清理部署文件 if (fs.existsSync(this.deploymentPath)) { -- Gitee From 3ac1f8e2165e70bd01ec21716348af1dfae68d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 10 Jun 2025 23:10:45 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feat(deploy):=20=E5=A2=9E=E5=BC=BAsudo?= =?UTF-8?q?=E5=8A=A9=E6=89=8B=E8=BF=9B=E7=A8=8B=E7=9B=91=E6=8E=A7=E4=B8=8E?= =?UTF-8?q?=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=89=A7=E8=A1=8C=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- .../main/deploy/core/DeploymentService.ts | 603 ++++++++++++++---- 1 file changed, 485 insertions(+), 118 deletions(-) diff --git a/electron/main/deploy/core/DeploymentService.ts b/electron/main/deploy/core/DeploymentService.ts index 42d4d8f7..55f8d2ac 100644 --- a/electron/main/deploy/core/DeploymentService.ts +++ b/electron/main/deploy/core/DeploymentService.ts @@ -70,6 +70,7 @@ export class DeploymentService { private abortController?: AbortController; private sudoSessionActive: boolean = false; private sudoHelperProcess?: any; + private sudoHelperMonitorInterval?: NodeJS.Timeout; constructor() { this.cachePath = getCachePath(); @@ -297,7 +298,7 @@ export class DeploymentService { ); } - // 检查是否已经克隆过 + // 检查是否已经克 clone 过 const gitDir = path.join(this.deploymentPath, '.git'); if (fs.existsSync(gitDir)) { try { @@ -712,6 +713,15 @@ export class DeploymentService { command = `echo "${inputData}" | ${command}`; } + // 增加详细日志 + if (process.env.NODE_ENV === 'development') { + console.log(`执行脚本: ${script.displayName}`); + console.log(`脚本路径: ${scriptPath}`); + console.log(`执行命令: ${command}`); + console.log(`环境变量:`, cleanEnvVars); + console.log(`超时时间: ${600000}ms (10分钟)`); + } + await this.executeSudoCommand( command, 600000, // 10分钟超时,某些服务安装可能需要较长时间 @@ -923,6 +933,9 @@ export class DeploymentService { } this.sudoSessionActive = true; + + // 启动进程监控 + this.startSudoHelperMonitor(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -980,15 +993,70 @@ export class DeploymentService { const helperScriptContent = `#!/bin/bash # Sudo助手脚本,保持长期运行的sudo会话 -# 读取命令并执行 -while IFS= read -r command; do +# 不使用 set -e,因为我们需要手动处理错误以保持进程运行 +# set -o pipefail 也可能导致意外退出,所以也不使用 + +# 设置信号处理,确保优雅退出 +trap 'echo "HELPER_SIGNAL_RECEIVED_$$" >&2; exit 0' SIGTERM SIGINT + +# 输出调试信息 +echo "HELPER_STARTED_$$" >&2 + +# 主循环:读取命令并执行 +while true; do + # 检查是否有输入可读,避免阻塞 + if ! IFS= read -r -t 1 command 2>/dev/null; then + # 读取超时,继续循环(保持进程活跃) + continue + fi + + # 输出调试信息 + echo "RECEIVED_COMMAND: $command" >&2 + + # 检查退出命令 if [ "$command" = "EXIT" ]; then + echo "HELPER_EXITING_$$" >&2 break fi - # 使用eval执行命令,支持复杂的命令结构 - eval "$command" + + # 检查命令是否为空 + if [ -z "$command" ]; then + echo "EMPTY_COMMAND_$$" >&2 + echo "COMMAND_DONE_$$" + continue + fi + + # 检查健康检查命令 + if [[ "$command" == echo*HEALTH_CHECK* ]]; then + # 健康检查命令,直接执行 + eval "$command" 2>/dev/null || true + echo "COMMAND_DONE_$$" + continue + fi + + # 执行命令并捕获退出码,使用子shell避免影响主进程 + ( + # 在子shell中执行命令 + eval "$command" + ) + cmd_exit_code=$? + + # 根据退出码输出相应信息 + if [ $cmd_exit_code -eq 0 ]; then + echo "COMMAND_SUCCESS_$$" + else + echo "COMMAND_ERROR_\${cmd_exit_code}_$$" + fi + echo "COMMAND_DONE_$$" + + # 强制刷新输出缓冲区 + exec 1>&1 + exec 2>&2 done + +echo "HELPER_TERMINATED_$$" >&2 +exit 0 `; // 写入助手脚本 @@ -1006,31 +1074,66 @@ done this.sudoHelperProcess = spawn('bash', ['-c', command], { stdio: ['pipe', 'pipe', 'pipe'], + // 设置进程选项以提高稳定性 + env: { + ...process.env, + PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin', + }, }); // 等待进程启动并准备就绪 await new Promise((resolve, reject) => { const timeout = setTimeout(() => { - reject(new Error('sudo助手进程启动超时')); - }, 60000); // 60秒超时,给用户充足时间输入密码 + reject(new Error('sudo助手进程启动超时,请检查权限验证是否完成')); + }, 120000); // 增加到120秒超时,给用户更充足时间输入密码 let isResolved = false; + let helperStarted = false; this.sudoHelperProcess.stdout?.on('data', (data: Buffer) => { const output = data.toString(); - // 检查是否是我们的命令完成标记,如果是说明进程已经启动 - if (output.includes('COMMAND_DONE_') && !isResolved) { + + // 检查助手是否已启动 + if (output.includes('HELPER_STARTED_') && !helperStarted) { + helperStarted = true; + if (process.env.NODE_ENV === 'development') { + console.log('Sudo助手进程已启动'); + } + } + + // 检查是否是我们的命令完成标记,如果是说明进程已经启动并可以接收命令 + if ( + (output.includes('COMMAND_DONE_') || + output.includes('COMMAND_SUCCESS_')) && + !isResolved + ) { clearTimeout(timeout); isResolved = true; resolve(void 0); } }); + this.sudoHelperProcess.stderr?.on('data', (data: Buffer) => { + const errorOutput = data.toString(); + + // 检查助手启动信息 + if (errorOutput.includes('HELPER_STARTED_') && !helperStarted) { + helperStarted = true; + if (process.env.NODE_ENV === 'development') { + console.log('Sudo助手进程已启动 (从stderr)'); + } + } + + if (process.env.NODE_ENV === 'development') { + console.log('Sudo Helper Stderr:', errorOutput.trim()); + } + }); + this.sudoHelperProcess.on('error', (error: Error) => { if (!isResolved) { clearTimeout(timeout); isResolved = true; - reject(error); + reject(new Error(`sudo助手进程启动失败: ${error.message}`)); } }); @@ -1038,12 +1141,29 @@ done if (!isResolved) { clearTimeout(timeout); isResolved = true; - reject(new Error(`sudo助手进程异常退出,代码: ${code}`)); + + if (code === 126) { + reject( + new Error( + 'sudo助手进程启动失败: 权限被拒绝,请确保用户具有管理员权限', + ), + ); + } else if (code === 127) { + reject( + new Error('sudo助手进程启动失败: 找不到命令,请检查系统配置'), + ); + } else { + reject(new Error(`sudo助手进程启动时退出,代码: ${code}`)); + } } }); - // 发送一个测试命令来确认进程正常工作 - this.sudoHelperProcess.stdin?.write('echo "Helper Ready"\n'); + // 等待一小段时间确保进程完全启动,然后发送测试命令 + setTimeout(() => { + if (this.sudoHelperProcess && !this.sudoHelperProcess.killed) { + this.sudoHelperProcess.stdin?.write('echo "Helper Ready"\n'); + } + }, 2000); }); // 清理临时脚本文件 @@ -1070,132 +1190,118 @@ done } /** - * 使用sudo助手进程执行命令,无需重复密码输入 + * 尝试重新启动sudo助手进程 */ - private async executeSudoCommand( - command: string, - timeout: number = 60000, - envVars?: Record, - ): Promise<{ stdout: string; stderr: string }> { + private async restartSudoHelper(): Promise { if (process.platform !== 'linux') { - // 非Linux系统直接执行 - return await execAsyncWithAbort( - command, - { timeout, env: { ...process.env, ...envVars } }, - this.abortController?.signal, - ); - } - - // 检查是否为root用户 - if (process.getuid && process.getuid() === 0) { - return await execAsyncWithAbort( - command, - { timeout, env: { ...process.env, ...envVars } }, - this.abortController?.signal, - ); + return; } - // 使用sudo助手进程执行命令 - if (!this.sudoHelperProcess) { - throw new Error('sudo助手进程未启动,请先初始化sudo会话'); + if (process.env.NODE_ENV === 'development') { + console.log('尝试重新启动sudo助手进程...'); } - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - reject(new Error(`命令执行超时: ${command}`)); - }, timeout); + // 清理现有进程 + this.cleanupSudoHelper(); - let stdout = ''; - let stderr = ''; - let isResolved = false; + // 重置状态 + this.sudoSessionActive = false; - const dataHandler = (data: Buffer) => { - const output = data.toString(); - stdout += output; + // 重新启动助手进程 + await this.startSudoHelper(); - // 检查命令是否完成 - if ( - output.includes(`COMMAND_DONE_${this.sudoHelperProcess?.pid}`) && - !isResolved - ) { - clearTimeout(timeoutId); - isResolved = true; - // 移除完成标记 - stdout = stdout.replace( - new RegExp(`COMMAND_DONE_${this.sudoHelperProcess?.pid}\\s*`, 'g'), - '', - ); - resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); - } - }; + this.sudoSessionActive = true; - const errorHandler = (data: Buffer) => { - stderr += data.toString(); - }; + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程重新启动成功'); + } + } - const processErrorHandler = (error: Error) => { - if (!isResolved) { - clearTimeout(timeoutId); - isResolved = true; - reject(error); - } - }; + /** + * 启动sudo助手进程监控 + */ + private startSudoHelperMonitor(): void { + if (process.platform !== 'linux' || this.sudoHelperMonitorInterval) { + return; + } - const processExitHandler = (code: number) => { - if (!isResolved) { - clearTimeout(timeoutId); - isResolved = true; - reject(new Error(`sudo助手进程异常退出,代码: ${code}`)); + // 每30秒检查一次sudo助手进程状态 + this.sudoHelperMonitorInterval = setInterval(async () => { + if (this.sudoSessionActive && this.sudoHelperProcess) { + try { + // 检查进程是否仍然活跃 + if ( + this.sudoHelperProcess.killed || + this.sudoHelperProcess.exitCode !== null + ) { + if (process.env.NODE_ENV === 'development') { + console.log('检测到sudo助手进程已退出,准备重启...'); + } + + // 尝试重启进程 + try { + await this.restartSudoHelper(); + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程重启成功'); + } + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.error('sudo助手进程重启失败:', error); + } + // 重启失败,停止监控 + this.stopSudoHelperMonitor(); + this.sudoSessionActive = false; + } + } else { + // 进程仍在运行,进行健康检查 + try { + await this.checkSudoHelperHealth(); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程健康检查失败,准备重启:', error); + } + + try { + await this.restartSudoHelper(); + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程重启成功'); + } + } catch (restartError) { + if (process.env.NODE_ENV === 'development') { + console.error('sudo助手进程重启失败:', restartError); + } + // 重启失败,停止监控 + this.stopSudoHelperMonitor(); + this.sudoSessionActive = false; + } + } + } + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.error('sudo助手进程监控出错:', error); + } } - }; - - // 绑定事件监听器 - this.sudoHelperProcess.stdout?.on('data', dataHandler); - this.sudoHelperProcess.stderr?.on('data', errorHandler); - this.sudoHelperProcess.on('error', processErrorHandler); - this.sudoHelperProcess.on('exit', processExitHandler); - - // 构建环境变量字符串 - let envString = ''; - if (envVars && Object.keys(envVars).length > 0) { - const envPairs = Object.entries(envVars) - .map(([key, value]) => `export ${key}="${value}"`) - .join('; '); - envString = envPairs + '; '; } + }, 30000); // 30秒检查一次 + } - // 发送命令到助手进程 - const fullCommand = `${envString}${command}`; - this.sudoHelperProcess.stdin?.write(`${fullCommand}\n`); - - // 设置清理函数 - const cleanup = () => { - this.sudoHelperProcess.stdout?.off('data', dataHandler); - this.sudoHelperProcess.stderr?.off('data', errorHandler); - this.sudoHelperProcess.off('error', processErrorHandler); - this.sudoHelperProcess.off('exit', processExitHandler); - }; - - // 确保在resolve或reject时清理事件监听器 - const originalResolve = resolve; - const originalReject = reject; - - resolve = (value: any) => { - cleanup(); - originalResolve(value); - }; - - reject = (reason: any) => { - cleanup(); - originalReject(reason); - }; - }); + /** + * 停止sudo助手进程监控 + */ + private stopSudoHelperMonitor(): void { + if (this.sudoHelperMonitorInterval) { + clearInterval(this.sudoHelperMonitorInterval); + this.sudoHelperMonitorInterval = undefined; + } } /** * 清理sudo助手进程 */ private cleanupSudoHelper(): void { + // 停止进程监控 + this.stopSudoHelperMonitor(); + if (this.sudoHelperProcess) { try { // 发送退出命令 @@ -1541,4 +1647,265 @@ done currentStep: 'idle', }); } + + /** + * 检查sudo助手进程健康状态 + */ + private async checkSudoHelperHealth(): Promise { + if (!this.sudoHelperProcess || this.sudoHelperProcess.killed) { + throw new Error('sudo助手进程未运行'); + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('sudo助手进程健康检查超时')); + }, 5000); // 5秒超时 + + let isResolved = false; + const healthCheckId = Date.now(); + + const dataHandler = (data: Buffer) => { + const output = data.toString(); + if ( + output.includes(`HEALTH_CHECK_${healthCheckId}_DONE`) && + !isResolved + ) { + clearTimeout(timeout); + isResolved = true; + this.sudoHelperProcess.stdout?.off('data', dataHandler); + resolve(); + } + }; + + const errorHandler = (error: Error) => { + if (!isResolved) { + clearTimeout(timeout); + isResolved = true; + this.sudoHelperProcess.stdout?.off('data', dataHandler); + reject(error); + } + }; + + this.sudoHelperProcess.stdout?.on('data', dataHandler); + this.sudoHelperProcess.on('error', errorHandler); + + // 发送健康检查命令 + this.sudoHelperProcess.stdin?.write( + `echo "HEALTH_CHECK_${healthCheckId}_DONE"\n`, + ); + }); + } + + /** + * 使用sudo助手进程执行命令,无需重复密码输入 + */ + private async executeSudoCommand( + command: string, + timeout: number = 60000, + envVars?: Record, + ): Promise<{ stdout: string; stderr: string }> { + if (process.platform !== 'linux') { + // 非Linux系统直接执行 + return await execAsyncWithAbort( + command, + { timeout, env: { ...process.env, ...envVars } }, + this.abortController?.signal, + ); + } + + // 检查是否为root用户 + if (process.getuid && process.getuid() === 0) { + return await execAsyncWithAbort( + command, + { timeout, env: { ...process.env, ...envVars } }, + this.abortController?.signal, + ); + } + + // 检查sudo助手进程是否仍然活跃 + if (!this.sudoHelperProcess || this.sudoHelperProcess.killed) { + throw new Error('sudo助手进程未启动或已终止,请重新初始化sudo会话'); + } + + // 检查sudo助手进程健康状态,如果不健康则尝试重启 + try { + await this.checkSudoHelperHealth(); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程健康检查失败,尝试重启:', error); + } + + try { + await this.restartSudoHelper(); + } catch (restartError) { + throw new Error( + `sudo助手进程重启失败: ${restartError instanceof Error ? restartError.message : String(restartError)}`, + ); + } + } + + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error(`命令执行超时: ${command}`)); + }, timeout); + + let stdout = ''; + let stderr = ''; + let isResolved = false; + + const dataHandler = (data: Buffer) => { + const output = data.toString(); + stdout += output; + + // 检查命令是否成功完成 + if ( + output.includes(`COMMAND_SUCCESS_${this.sudoHelperProcess?.pid}`) && + !isResolved + ) { + clearTimeout(timeoutId); + isResolved = true; + // 移除状态标记 + stdout = stdout.replace( + new RegExp( + `COMMAND_(SUCCESS|DONE)_${this.sudoHelperProcess?.pid}\\s*`, + 'g', + ), + '', + ); + resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); + } + // 检查命令是否失败 + else if ( + output.includes(`COMMAND_ERROR_`) && + output.includes(`_${this.sudoHelperProcess?.pid}`) && + !isResolved + ) { + clearTimeout(timeoutId); + isResolved = true; + // 提取错误码 + const errorMatch = output.match( + new RegExp(`COMMAND_ERROR_(\\d+)_${this.sudoHelperProcess?.pid}`), + ); + const errorCode = errorMatch ? errorMatch[1] : 'unknown'; + + // 移除状态标记 + stdout = stdout.replace( + new RegExp( + `COMMAND_(ERROR_\\d+|DONE)_${this.sudoHelperProcess?.pid}\\s*`, + 'g', + ), + '', + ); + + reject( + new Error( + `命令执行失败 (退出码: ${errorCode}): ${stderr.trim() || stdout.trim()}`, + ), + ); + } + // 向后兼容:检查旧的完成标记 + else if ( + output.includes(`COMMAND_DONE_${this.sudoHelperProcess?.pid}`) && + !isResolved + ) { + clearTimeout(timeoutId); + isResolved = true; + // 移除完成标记 + stdout = stdout.replace( + new RegExp(`COMMAND_DONE_${this.sudoHelperProcess?.pid}\\s*`, 'g'), + '', + ); + resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); + } + }; + + const errorHandler = (data: Buffer) => { + const errorOutput = data.toString(); + stderr += errorOutput; + + // 检查是否有调试信息 + if (process.env.NODE_ENV === 'development') { + if ( + errorOutput.includes('HELPER_STARTED_') || + errorOutput.includes('RECEIVED_COMMAND:') || + errorOutput.includes('HELPER_EXITING_') || + errorOutput.includes('HELPER_TERMINATED_') + ) { + console.log('Sudo Helper Debug:', errorOutput.trim()); + } + } + }; + + const processErrorHandler = (error: Error) => { + if (!isResolved) { + clearTimeout(timeoutId); + isResolved = true; + reject(new Error(`sudo助手进程错误: ${error.message}`)); + } + }; + + const processExitHandler = (code: number) => { + if (!isResolved) { + clearTimeout(timeoutId); + isResolved = true; + + // 提供更详细的退出信息 + const stderrInfo = stderr.trim() ? `\nStderr: ${stderr.trim()}` : ''; + const stdoutInfo = stdout.trim() ? `\nStdout: ${stdout.trim()}` : ''; + + reject( + new Error( + `sudo助手进程异常退出,代码: ${code}${stderrInfo}${stdoutInfo}\n` + + `这可能是由于:\n` + + `1. 脚本执行时间过长导致进程超时\n` + + `2. 系统资源不足\n` + + `3. 权限问题或认证超时\n` + + `4. 脚本内部错误导致bash退出`, + ), + ); + } + }; + + // 绑定事件监听器 + this.sudoHelperProcess.stdout?.on('data', dataHandler); + this.sudoHelperProcess.stderr?.on('data', errorHandler); + this.sudoHelperProcess.on('error', processErrorHandler); + this.sudoHelperProcess.on('exit', processExitHandler); + + // 构建环境变量字符串 + let envString = ''; + if (envVars && Object.keys(envVars).length > 0) { + const envPairs = Object.entries(envVars) + .map(([key, value]) => `export ${key}="${value}"`) + .join('; '); + envString = envPairs + '; '; + } + + // 发送命令到助手进程 + const fullCommand = `${envString}${command}`; + this.sudoHelperProcess.stdin?.write(`${fullCommand}\n`); + + // 设置清理函数 + const cleanup = () => { + this.sudoHelperProcess.stdout?.off('data', dataHandler); + this.sudoHelperProcess.stderr?.off('data', errorHandler); + this.sudoHelperProcess.off('error', processErrorHandler); + this.sudoHelperProcess.off('exit', processExitHandler); + }; + + // 确保在resolve或reject时清理事件监听器 + const originalResolve = resolve; + const originalReject = reject; + + resolve = (value: any) => { + cleanup(); + originalResolve(value); + }; + + reject = (reason: any) => { + cleanup(); + originalReject(reason); + }; + }); + } } -- Gitee