@myassis/gateway 1.0.40 → 1.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/services/ServiceManager.js +494 -72
- package/dist/services/tools/exec.js +26 -7
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.updateService = exports.checkForUpdates = exports.restartService = exports.stopService = exports.startService = exports.uninstallService = exports.installService = exports.getServiceInfo = exports.SERVICE_DISPLAY_NAME = exports.SERVICE_NAME = void 0;
|
|
6
|
+
exports.execAsUser = exports.isHelperReady = exports.stopHelper = exports.updateService = exports.checkForUpdates = exports.restartService = exports.stopService = exports.startService = exports.uninstallService = exports.installService = exports.getServiceInfo = exports.SERVICE_DISPLAY_NAME = exports.SERVICE_NAME = void 0;
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const util_1 = require("util");
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -11,6 +11,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
11
11
|
const axios_1 = __importDefault(require("axios"));
|
|
12
12
|
const shared_1 = require("@myassis/shared");
|
|
13
13
|
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const net_1 = __importDefault(require("net"));
|
|
14
15
|
const logger = (0, shared_1.getLogger)('ServiceManager');
|
|
15
16
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
16
17
|
exports.SERVICE_NAME = 'myassis-gateway';
|
|
@@ -63,39 +64,148 @@ async function queryServiceWindows() {
|
|
|
63
64
|
return { installed: false, running: false };
|
|
64
65
|
}
|
|
65
66
|
}
|
|
67
|
+
// ─── 用户级进程管理 ─────────────────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* 获取 Gateway 可执行文件路径
|
|
70
|
+
*/
|
|
71
|
+
function getGatewayExePath() {
|
|
72
|
+
if (isPackagedExe()) {
|
|
73
|
+
return process.execPath;
|
|
74
|
+
}
|
|
75
|
+
const nodeExec = getNodeExec();
|
|
76
|
+
return nodeExec;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 检查 Gateway 进程是否在运行
|
|
80
|
+
*/
|
|
81
|
+
function isGatewayRunning() {
|
|
82
|
+
const pid = getStoredPid();
|
|
83
|
+
if (!pid)
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
// Windows: kill signal 0 探测进程是否存在
|
|
87
|
+
process.kill(pid, 0);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 从 PID 文件读取存储的 PID
|
|
96
|
+
*/
|
|
97
|
+
function getStoredPid() {
|
|
98
|
+
try {
|
|
99
|
+
if (!fs_1.default.existsSync(GATEWAY_PID_FILE))
|
|
100
|
+
return null;
|
|
101
|
+
const pid = parseInt(fs_1.default.readFileSync(GATEWAY_PID_FILE, 'utf8').trim(), 10);
|
|
102
|
+
return isNaN(pid) ? null : pid;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 停止 Gateway 用户进程(优雅终止)
|
|
110
|
+
*/
|
|
111
|
+
async function stopGatewayProcess() {
|
|
112
|
+
const pid = getStoredPid();
|
|
113
|
+
if (!pid)
|
|
114
|
+
return;
|
|
115
|
+
try {
|
|
116
|
+
process.kill(pid, 'SIGTERM');
|
|
117
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
118
|
+
}
|
|
119
|
+
catch { /* ignore */ }
|
|
120
|
+
// 强制终止(如果还没退出)
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 'SIGKILL');
|
|
123
|
+
}
|
|
124
|
+
catch { /* ignore */ }
|
|
125
|
+
try {
|
|
126
|
+
fs_1.default.unlinkSync(GATEWAY_PID_FILE);
|
|
127
|
+
}
|
|
128
|
+
catch { /* ignore */ }
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 写入 Gateway 启动脚本(用户级,不弹出窗口)
|
|
132
|
+
*/
|
|
133
|
+
function writeGatewayLauncherScript(exePath, scriptPath, workDir) {
|
|
134
|
+
// 写入 BOM + UTF-8 内容
|
|
135
|
+
const content = [
|
|
136
|
+
"$ErrorActionPreference = 'Stop'",
|
|
137
|
+
"$exe = '" + exePath.replace(/\\/g, '\\\\') + "'",
|
|
138
|
+
"$workDir = '" + workDir.replace(/\\/g, '\\\\') + "'",
|
|
139
|
+
"$pidFile = '" + GATEWAY_PID_FILE.replace(/\\/g, '\\\\') + "'",
|
|
140
|
+
"$port = 19090",
|
|
141
|
+
"$svcPort = 19091",
|
|
142
|
+
"",
|
|
143
|
+
"# 防止重复启动(检查端口)",
|
|
144
|
+
"$listener = [System.Net.Sockets.TcpListener]::Start($port)",
|
|
145
|
+
"$listener.Stop()",
|
|
146
|
+
"",
|
|
147
|
+
"# 启动 Gateway 进程(后台、无窗口)",
|
|
148
|
+
"$psi = New-Object System.Diagnostics.ProcessStartInfo",
|
|
149
|
+
"$psi.FileName = $exe",
|
|
150
|
+
"$psi.WorkingDirectory = $workDir",
|
|
151
|
+
"$psi.Arguments = '--gateway --port ' + $port + ' --service-port ' + $svcPort",
|
|
152
|
+
"$psi.UseShellExecute = $false",
|
|
153
|
+
"$psi.RedirectStandardOutput = $true",
|
|
154
|
+
"$psi.RedirectStandardError = $true",
|
|
155
|
+
"$psi.CreateNoWindow = $true",
|
|
156
|
+
"$psi.Environment['NODE_ENV'] = 'production'",
|
|
157
|
+
"$proc = [System.Diagnostics.Process]::Start($psi)",
|
|
158
|
+
"if ($proc) {",
|
|
159
|
+
" $proc.Id.ToString() | Out-File -FilePath $pidFile -Encoding UTF8",
|
|
160
|
+
" $proc.WaitForExit()",
|
|
161
|
+
"}",
|
|
162
|
+
].join('\r\n');
|
|
163
|
+
fs_1.default.writeFileSync(scriptPath, '\uFEFF' + content, 'utf8');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 检查 Run key 是否已注册
|
|
167
|
+
*/
|
|
168
|
+
function isRegisteredInRunKey() {
|
|
169
|
+
// 通过 PowerShell 读取 HKCU Run key
|
|
170
|
+
try {
|
|
171
|
+
const { execSync } = require('child_process');
|
|
172
|
+
const result = execSync(`powershell -NoProfile -Command "if (Test-Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run') { (Get-ItemProperty 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run').PSObject.Properties.Name -contains '${exports.SERVICE_NAME}' } else { $false }"`, { timeout: 5000, windowsHide: true });
|
|
173
|
+
return result.toString().trim() === 'True';
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 写入注册表 Run key(开机自启)
|
|
181
|
+
*/
|
|
182
|
+
async function registerRunKey(scriptPath) {
|
|
183
|
+
const regCmd = `powershell -NoProfile -Command "Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${exports.SERVICE_NAME}' -Value '\"${scriptPath}\"' -Type String"`;
|
|
184
|
+
await execAsync(regCmd, { timeout: 10000, windowsHide: true });
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 删除注册表 Run key
|
|
188
|
+
*/
|
|
189
|
+
async function unregisterRunKey() {
|
|
190
|
+
try {
|
|
191
|
+
const regCmd = `powershell -NoProfile -Command "Remove-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${exports.SERVICE_NAME}' -ErrorAction SilentlyContinue"`;
|
|
192
|
+
await execAsync(regCmd, { timeout: 10000, windowsHide: true });
|
|
193
|
+
}
|
|
194
|
+
catch { /* ignore */ }
|
|
195
|
+
}
|
|
196
|
+
// ─── Windows 用户级实现 ─────────────────────────────────────
|
|
66
197
|
async function installWindows() {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const workDir = isPkg ? path_1.default.dirname(exe) : path_1.default.dirname(script);
|
|
71
|
-
logger.info(`isPkg=${isPkg}, exe=${exe}, script=${script}, workDir=${workDir}`);
|
|
198
|
+
const exe = getGatewayExePath();
|
|
199
|
+
const workDir = path_1.default.dirname(exe);
|
|
200
|
+
logger.info(`installWindows: exe=${exe}, workDir=${workDir}`);
|
|
72
201
|
const { installed } = await queryServiceWindows();
|
|
73
202
|
if (installed)
|
|
74
203
|
await uninstallWindows();
|
|
75
|
-
const nssmPath = getNssmPath();
|
|
76
|
-
const useNssm = !!nssmPath && fs_1.default.existsSync(nssmPath);
|
|
77
204
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await execAsync(`"${nssmPath}" install ${exports.SERVICE_NAME} "${exe}"`, { timeout: 15000, cwd: workDir });
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
await execAsync(`"${nssmPath}" install ${exports.SERVICE_NAME} "${exe}" "${script}"`, { timeout: 15000, cwd: workDir });
|
|
84
|
-
}
|
|
85
|
-
await execAsync(`"${nssmPath}" set ${exports.SERVICE_NAME} AppDirectory "${workDir}"`, { timeout: 10000 });
|
|
86
|
-
await execAsync(`"${nssmPath}" set ${exports.SERVICE_NAME} DisplayName "${exports.SERVICE_DISPLAY_NAME}"`, { timeout: 10000 });
|
|
87
|
-
await execAsync(`"${nssmPath}" set ${exports.SERVICE_NAME} Start SERVICE_AUTO_START`, { timeout: 10000 });
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
if (isPkg) {
|
|
91
|
-
await execAsync(`sc.exe create ${exports.SERVICE_NAME} binPath= "\\"${exe}\\"" DisplayName= "${exports.SERVICE_DISPLAY_NAME}" start= auto`, { timeout: 15000 });
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
await execAsync(`sc.exe create ${exports.SERVICE_NAME} binPath= "\\"${exe}\\" \\"${script}\\"" DisplayName= "${exports.SERVICE_DISPLAY_NAME}" start= auto`, { timeout: 15000 });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
205
|
+
writeGatewayLauncherScript(exe, GATEWAY_LAUNCHER_FILE, workDir);
|
|
206
|
+
await registerRunKey(GATEWAY_LAUNCHER_FILE);
|
|
97
207
|
await startService();
|
|
98
|
-
return { success: true, message: '
|
|
208
|
+
return { success: true, message: 'Gateway 安装并启动成功(用户级进程)' };
|
|
99
209
|
}
|
|
100
210
|
catch (err) {
|
|
101
211
|
return { success: false, message: `安装失败: ${err.message}` };
|
|
@@ -104,48 +214,48 @@ async function installWindows() {
|
|
|
104
214
|
async function uninstallWindows() {
|
|
105
215
|
try {
|
|
106
216
|
await stopService();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
217
|
+
await unregisterRunKey();
|
|
218
|
+
try {
|
|
219
|
+
fs_1.default.unlinkSync(GATEWAY_PID_FILE);
|
|
110
220
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
return { success: true, message: '服务卸载成功' };
|
|
221
|
+
catch { /* ignore */ }
|
|
222
|
+
return { success: true, message: 'Gateway 已卸载(用户级进程)' };
|
|
115
223
|
}
|
|
116
224
|
catch (err) {
|
|
117
225
|
return { success: false, message: `卸载失败: ${err.message}` };
|
|
118
226
|
}
|
|
119
227
|
}
|
|
120
228
|
async function startServiceWindows() {
|
|
229
|
+
if (isGatewayRunning()) {
|
|
230
|
+
return { success: true, message: 'Gateway 已在运行' };
|
|
231
|
+
}
|
|
121
232
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
233
|
+
const exe = getGatewayExePath();
|
|
234
|
+
const workDir = path_1.default.dirname(exe);
|
|
235
|
+
if (!fs_1.default.existsSync(GATEWAY_LAUNCHER_FILE)) {
|
|
236
|
+
writeGatewayLauncherScript(exe, GATEWAY_LAUNCHER_FILE, workDir);
|
|
237
|
+
}
|
|
238
|
+
await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "${GATEWAY_LAUNCHER_FILE}"`, { timeout: 5000, windowsHide: true });
|
|
239
|
+
const maxWait = 5000;
|
|
240
|
+
const start = Date.now();
|
|
241
|
+
while (!isGatewayRunning() && Date.now() - start < maxWait) {
|
|
242
|
+
await new Promise(r => setTimeout(r, 500));
|
|
243
|
+
}
|
|
244
|
+
if (isGatewayRunning()) {
|
|
245
|
+
return { success: true, message: 'Gateway 启动成功' };
|
|
246
|
+
}
|
|
247
|
+
return { success: false, message: 'Gateway 进程未能启动,请检查日志' };
|
|
124
248
|
}
|
|
125
249
|
catch (err) {
|
|
126
250
|
return { success: false, message: `启动失败: ${err.message}` };
|
|
127
251
|
}
|
|
128
252
|
}
|
|
129
253
|
async function restartServiceWindows() {
|
|
130
|
-
const nssmPath = getNssmPath();
|
|
131
|
-
// 无 nssm 时需要用户手动更新
|
|
132
|
-
if (!fs_1.default.existsSync(nssmPath)) {
|
|
133
|
-
const info = await getServiceInfo();
|
|
134
|
-
if (info.installed) {
|
|
135
|
-
return {
|
|
136
|
-
success: false,
|
|
137
|
-
message: '检测到您尚未安装 nssm,无法自动重启服务。请手动执行以下步骤更新 Gateway:\n' +
|
|
138
|
-
'1. 在桌面上停止 Gateway 服务(系统托盘 → 右键 → 退出)\n' +
|
|
139
|
-
'2. 运行 `npm install -g @myassis/gateway@latest`\n' +
|
|
140
|
-
'3. 重新启动 Gateway 服务\n\n' +
|
|
141
|
-
'如需自动重启功能,请从 https://nssm.cc/download 下载 nssm 并放置到 Gateway 同目录下',
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
// 服务未安装时,可直接更新
|
|
145
|
-
}
|
|
146
254
|
try {
|
|
147
|
-
await
|
|
148
|
-
|
|
255
|
+
await stopService();
|
|
256
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
257
|
+
await startService();
|
|
258
|
+
return { success: true, message: 'Gateway 重启成功' };
|
|
149
259
|
}
|
|
150
260
|
catch (err) {
|
|
151
261
|
return { success: false, message: `重启失败: ${err.message}` };
|
|
@@ -153,8 +263,8 @@ async function restartServiceWindows() {
|
|
|
153
263
|
}
|
|
154
264
|
async function stopServiceWindows() {
|
|
155
265
|
try {
|
|
156
|
-
await
|
|
157
|
-
return { success: true, message: '
|
|
266
|
+
await stopGatewayProcess();
|
|
267
|
+
return { success: true, message: 'Gateway 已停止' };
|
|
158
268
|
}
|
|
159
269
|
catch (err) {
|
|
160
270
|
return { success: false, message: `停止失败: ${err.message}` };
|
|
@@ -189,21 +299,21 @@ async function installLinux() {
|
|
|
189
299
|
if (installed)
|
|
190
300
|
await stopService();
|
|
191
301
|
const execStart = isPkg ? exe : `${exe} ${script}`;
|
|
192
|
-
const unitContent = `[Unit]
|
|
193
|
-
Description=${exports.SERVICE_DISPLAY_NAME}
|
|
194
|
-
After=network.target
|
|
195
|
-
|
|
196
|
-
[Service]
|
|
197
|
-
Type=simple
|
|
198
|
-
User=${process.env.USER || 'root'}
|
|
199
|
-
WorkingDirectory=${workDir}
|
|
200
|
-
ExecStart=${execStart}
|
|
201
|
-
Restart=always
|
|
202
|
-
RestartSec=5
|
|
203
|
-
Environment=NODE_ENV=production
|
|
204
|
-
|
|
205
|
-
[Install]
|
|
206
|
-
WantedBy=multi-user.target
|
|
302
|
+
const unitContent = `[Unit]
|
|
303
|
+
Description=${exports.SERVICE_DISPLAY_NAME}
|
|
304
|
+
After=network.target
|
|
305
|
+
|
|
306
|
+
[Service]
|
|
307
|
+
Type=simple
|
|
308
|
+
User=${process.env.USER || 'root'}
|
|
309
|
+
WorkingDirectory=${workDir}
|
|
310
|
+
ExecStart=${execStart}
|
|
311
|
+
Restart=always
|
|
312
|
+
RestartSec=5
|
|
313
|
+
Environment=NODE_ENV=production
|
|
314
|
+
|
|
315
|
+
[Install]
|
|
316
|
+
WantedBy=multi-user.target
|
|
207
317
|
`;
|
|
208
318
|
await fs_1.default.promises.writeFile('/tmp/myassis-gateway.service', unitContent, 'utf8');
|
|
209
319
|
await execAsync('cp /tmp/myassis-gateway.service /etc/systemd/system/myassis-gateway.service', { timeout: 10000 });
|
|
@@ -549,3 +659,315 @@ async function updateService() {
|
|
|
549
659
|
}
|
|
550
660
|
}
|
|
551
661
|
exports.updateService = updateService;
|
|
662
|
+
// ═══════════════════════════════════════════════════════════
|
|
663
|
+
// 用户态 Helper 进程管理 — Windows 专用
|
|
664
|
+
// 目标:在当前登录用户的桌面会话中以该用户身份执行命令
|
|
665
|
+
// 架构:
|
|
666
|
+
// Service (SYSTEM) ──TCP/IPC──> Helper (当前登录用户)
|
|
667
|
+
// ═══════════════════════════════════════════════════════════
|
|
668
|
+
/** IPC 端口号(Service <-> Helper 通信) */
|
|
669
|
+
const HELPER_PORT = 19630;
|
|
670
|
+
/** Helper 超时时间(毫秒) */
|
|
671
|
+
const HELPER_TIMEOUT_MS = 60000;
|
|
672
|
+
/** Gateway PID 文件路径(用户级进程跟踪) */
|
|
673
|
+
const GATEWAY_PID_FILE = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-pid.txt');
|
|
674
|
+
/** Gateway 启动脚本路径 */
|
|
675
|
+
const GATEWAY_LAUNCHER_FILE = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-launcher.ps1');
|
|
676
|
+
/** 当前活跃的 Helper 进程 */
|
|
677
|
+
let helperProcess = null;
|
|
678
|
+
/** Helper 连接 socket */
|
|
679
|
+
let helperSocket = null;
|
|
680
|
+
/** Helper 是否就绪 */
|
|
681
|
+
let helperReady = false;
|
|
682
|
+
/** Helper 等待队列 */
|
|
683
|
+
const helperPending = new Map();
|
|
684
|
+
/**
|
|
685
|
+
* Helper Socket 收到的数据缓冲区(可能跨多条消息)
|
|
686
|
+
*/
|
|
687
|
+
let helperDataBuf = '';
|
|
688
|
+
/**
|
|
689
|
+
* 处理 Helper 返回的数据(JSON 数组,可能跨多次 TCP 推送)
|
|
690
|
+
*/
|
|
691
|
+
function handleHelperData(chunk) {
|
|
692
|
+
helperDataBuf += chunk;
|
|
693
|
+
// Helper 可能一次推送多条 JSON 行(每条以 \n 结尾)
|
|
694
|
+
const lines = helperDataBuf.split('\n');
|
|
695
|
+
// 保留最后不完整的行(等待下一块数据)
|
|
696
|
+
helperDataBuf = lines.pop() || '';
|
|
697
|
+
for (const line of lines) {
|
|
698
|
+
if (!line.trim())
|
|
699
|
+
continue;
|
|
700
|
+
try {
|
|
701
|
+
const resp = JSON.parse(line);
|
|
702
|
+
const pending = helperPending.get(resp.id);
|
|
703
|
+
if (!pending)
|
|
704
|
+
continue;
|
|
705
|
+
clearTimeout(pending.timer);
|
|
706
|
+
helperPending.delete(resp.id);
|
|
707
|
+
if (resp.success) {
|
|
708
|
+
pending.resolve({ stdout: resp.stdout || '', stderr: resp.stderr || '', exitCode: resp.exitCode ?? 0 });
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
pending.reject(new Error(resp.error || 'Helper command failed'));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch { /* ignore parse errors */ }
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* 停止 Helper 进程
|
|
719
|
+
*/
|
|
720
|
+
async function stopHelper() {
|
|
721
|
+
helperReady = false;
|
|
722
|
+
if (helperSocket) {
|
|
723
|
+
try {
|
|
724
|
+
helperSocket.destroy();
|
|
725
|
+
}
|
|
726
|
+
catch { /* ignore */ }
|
|
727
|
+
helperSocket = null;
|
|
728
|
+
}
|
|
729
|
+
if (helperProcess) {
|
|
730
|
+
try {
|
|
731
|
+
helperProcess.kill();
|
|
732
|
+
}
|
|
733
|
+
catch { /* ignore */ }
|
|
734
|
+
helperProcess = null;
|
|
735
|
+
}
|
|
736
|
+
for (const [id, p] of helperPending) {
|
|
737
|
+
clearTimeout(p.timer);
|
|
738
|
+
p.reject(new Error('Helper stopped'));
|
|
739
|
+
}
|
|
740
|
+
helperPending.clear();
|
|
741
|
+
}
|
|
742
|
+
exports.stopHelper = stopHelper;
|
|
743
|
+
/**
|
|
744
|
+
* 查询 Helper 是否就绪
|
|
745
|
+
*/
|
|
746
|
+
function isHelperReady() {
|
|
747
|
+
return helperReady && helperSocket !== null;
|
|
748
|
+
}
|
|
749
|
+
exports.isHelperReady = isHelperReady;
|
|
750
|
+
/**
|
|
751
|
+
* 通过 Helper 进程执行命令(以当前登录用户身份)
|
|
752
|
+
*/
|
|
753
|
+
async function execAsUser(command, cwd) {
|
|
754
|
+
// 确保 Helper 已启动
|
|
755
|
+
if (!helperReady || !helperSocket) {
|
|
756
|
+
const started = await spawnHelperAsUser();
|
|
757
|
+
if (!started) {
|
|
758
|
+
// Helper 启动失败,降级到直接 exec(exit=-2 表示降级执行)
|
|
759
|
+
logger.warn('Helper spawn failed, falling back to direct exec (exit=-2 = fallback used)');
|
|
760
|
+
try {
|
|
761
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
762
|
+
cwd: cwd || os_1.default.homedir(),
|
|
763
|
+
timeout: HELPER_TIMEOUT_MS,
|
|
764
|
+
windowsHide: true,
|
|
765
|
+
});
|
|
766
|
+
return { stdout, stderr, exitCode: 0 };
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
return { stdout: '', stderr: err?.message || String(err), exitCode: -2 };
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const id = newCmdId();
|
|
774
|
+
const timeout = HELPER_TIMEOUT_MS;
|
|
775
|
+
return new Promise((resolve, reject) => {
|
|
776
|
+
const timer = setTimeout(() => {
|
|
777
|
+
helperPending.delete(id);
|
|
778
|
+
reject(new Error('Command timeout'));
|
|
779
|
+
}, timeout);
|
|
780
|
+
helperPending.set(id, { resolve, reject, timer });
|
|
781
|
+
const req = JSON.stringify({ id, command, cwd: cwd || os_1.default.homedir() });
|
|
782
|
+
const data = Buffer.from(req + '\n', 'utf8');
|
|
783
|
+
if (!helperSocket) {
|
|
784
|
+
clearTimeout(timer);
|
|
785
|
+
helperPending.delete(id);
|
|
786
|
+
reject(new Error('Helper socket not available'));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
helperSocket.once('error', (err) => {
|
|
790
|
+
clearTimeout(timer);
|
|
791
|
+
helperPending.delete(id);
|
|
792
|
+
helperReady = false;
|
|
793
|
+
reject(err);
|
|
794
|
+
});
|
|
795
|
+
helperSocket.write(data, () => {
|
|
796
|
+
// 等待 Helper 响应(由 handleHelperData 处理)
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
exports.execAsUser = execAsUser;
|
|
801
|
+
/**
|
|
802
|
+
* 生成随机命令 ID
|
|
803
|
+
*/
|
|
804
|
+
function newCmdId() {
|
|
805
|
+
return Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* 生成 Helper 的 PowerShell 执行脚本
|
|
809
|
+
*/
|
|
810
|
+
function buildHelperScript() {
|
|
811
|
+
const l = [];
|
|
812
|
+
const push = (x) => l.push(x);
|
|
813
|
+
push('# User Helper Process - Myassis Gateway');
|
|
814
|
+
push('$port = ' + HELPER_PORT);
|
|
815
|
+
push("$ErrorActionPreference = 'Stop'");
|
|
816
|
+
push('function Write-Log { param([string]$m) Write-Host "[$(Get-Date -Format HH:mm:ss)] $m" }');
|
|
817
|
+
push('Write-Log "helper started (PID=$PID)"');
|
|
818
|
+
push('$listener = [System.Net.Sockets.TcpListener]::Start($port)');
|
|
819
|
+
push('$listener.Start()');
|
|
820
|
+
push('Write-Log "listening on port $port"');
|
|
821
|
+
push('while ($true) {');
|
|
822
|
+
push(' $client = $listener.AcceptTcpClient()');
|
|
823
|
+
push(' $stream = $client.GetStream()');
|
|
824
|
+
push(' $reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::UTF8)');
|
|
825
|
+
push(' $line = $reader.ReadLine()');
|
|
826
|
+
push(' $client.Close()');
|
|
827
|
+
push(' if (-not $line) { continue }');
|
|
828
|
+
push(' Write-Log "received: $line"');
|
|
829
|
+
push(' try {');
|
|
830
|
+
push(' $cmd = $line | ConvertFrom-Json');
|
|
831
|
+
push(' $id = $cmd.id');
|
|
832
|
+
push(' $command = $cmd.command');
|
|
833
|
+
push(' $cwd = if ($cmd.cwd) { $cmd.cwd } else { $env:USERPROFILE }');
|
|
834
|
+
push(' Write-Log "executing: $command"');
|
|
835
|
+
push(' $stdoutFile = "$env:TEMP\\myassis-stdout-$id.txt"');
|
|
836
|
+
push(' $stderrFile = "$env:TEMP\\myassis-stderr-$id.txt"');
|
|
837
|
+
push(' $proc = Start-Process -FilePath "cmd.exe" -ArgumentList "/c $command" -WorkingDirectory $cwd -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile');
|
|
838
|
+
push(' $stdout = if (Test-Path $stdoutFile) { Get-Content $stdoutFile -Raw -Encoding UTF8 } else { "" }');
|
|
839
|
+
push(' $stderr = if (Test-Path $stderrFile) { Get-Content $stderrFile -Raw -Encoding UTF8 } else { "" }');
|
|
840
|
+
push(' Remove-Item $stderrFile -Force -ErrorAction SilentlyContinue');
|
|
841
|
+
push(' Remove-Item $stdoutFile -Force -ErrorAction SilentlyContinue');
|
|
842
|
+
push(' $exitCode = $proc.ExitCode');
|
|
843
|
+
push(' Write-Log "done (exit=$exitCode)"');
|
|
844
|
+
push(' $resp = @{ id=$id; success=$true; stdout=$stdout; stderr=$stderr; exitCode=$exitCode } | ConvertTo-Json -Compress');
|
|
845
|
+
push(' $nl = [Environment]::NewLine');
|
|
846
|
+
push(' $bytes = [System.Text.Encoding]::UTF8.GetBytes($resp + $nl)');
|
|
847
|
+
push(' $client.Client.Send($bytes)');
|
|
848
|
+
push(' } catch {');
|
|
849
|
+
push(' Write-Log "error: $($_.Exception.Message)"');
|
|
850
|
+
push(' try {');
|
|
851
|
+
push(' $resp = @{ id=$id; success=$false; error=$($_.Exception.Message) } | ConvertTo-Json -Compress');
|
|
852
|
+
push(' $nl = [Environment]::NewLine');
|
|
853
|
+
push(' $bytes = [System.Text.Encoding]::UTF8.GetBytes($resp + $nl)');
|
|
854
|
+
push(' $client.Client.Send($bytes)');
|
|
855
|
+
push(' } catch { }');
|
|
856
|
+
push(' }');
|
|
857
|
+
push('}');
|
|
858
|
+
return l.join('\r\n');
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* 查找当前登录用户的会话 ID(Explorer.exe 所在会话)
|
|
862
|
+
*/
|
|
863
|
+
async function getUserSessionId() {
|
|
864
|
+
try {
|
|
865
|
+
const { stdout } = await execAsync('powershell -NoProfile -Command "Get-Process Explorer -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty SessionId"', { timeout: 5000 });
|
|
866
|
+
const n = parseInt(stdout.trim(), 10);
|
|
867
|
+
if (!isNaN(n) && n > 0)
|
|
868
|
+
return n;
|
|
869
|
+
}
|
|
870
|
+
catch { /* ignore */ }
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
async function spawnHelperAsUser() {
|
|
874
|
+
if (process.platform !== 'win32')
|
|
875
|
+
return false;
|
|
876
|
+
if (helperReady && helperSocket)
|
|
877
|
+
return true;
|
|
878
|
+
const sessionId = await getUserSessionId();
|
|
879
|
+
if (!sessionId) {
|
|
880
|
+
logger.warn('Cannot get user session ID');
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
logger.info('Trying to spawn helper in session ' + sessionId);
|
|
884
|
+
const scriptContent = buildHelperScript();
|
|
885
|
+
let scriptPath = '';
|
|
886
|
+
let launchPath = '';
|
|
887
|
+
try {
|
|
888
|
+
scriptPath = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-helper-' + Date.now() + '.ps1');
|
|
889
|
+
launchPath = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-launch-' + Date.now() + '.ps1');
|
|
890
|
+
// Write with BOM for UTF-8
|
|
891
|
+
fs_1.default.writeFileSync(scriptPath, '\uFEFF' + scriptContent, 'utf8');
|
|
892
|
+
const psLaunchScript = [
|
|
893
|
+
'$ErrorActionPreference = \'Stop\'',
|
|
894
|
+
'Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @\'',
|
|
895
|
+
' [DllImport("wtsapi32.dll", SetLastError=true)] public static extern bool WTSQueryUserToken(int sessionId, out IntPtr token);',
|
|
896
|
+
' [DllImport("advapi32.dll", SetLastError=true)] public static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);',
|
|
897
|
+
' [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; }',
|
|
898
|
+
' [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; }',
|
|
899
|
+
"'@",
|
|
900
|
+
'$sessionId = ' + sessionId,
|
|
901
|
+
'$tokenPtr = [IntPtr]::Zero',
|
|
902
|
+
'$ok = [Win32.NativeMethods]::WTSQueryUserToken($sessionId, [ref]$tokenPtr)',
|
|
903
|
+
'if (-not $ok) { Write-Error "WTSQueryUserToken failed"; exit 1 }',
|
|
904
|
+
'$si = New-Object Win32.NativeMethods+STARTUPINFO',
|
|
905
|
+
'$si.cb = [Runtime.InteropServices.Marshal]::SizeOf($si)',
|
|
906
|
+
'$si.lpDesktop = "WinSta0\\Default"',
|
|
907
|
+
'$pi = [Win32.NativeMethods+PROCESS_INFORMATION]::new()',
|
|
908
|
+
'$cmdLine = "powershell.exe -NoProfile -ExecutionPolicy Bypass -File \\\"" + "' + scriptPath.replace(/\\\\/g, '\\\\\\\\') + '" + "\\\""',
|
|
909
|
+
'$envBlock = [IntPtr]::Zero',
|
|
910
|
+
'$bInherit = $true',
|
|
911
|
+
'$CREATE_NO_WINDOW = 0x08000000',
|
|
912
|
+
'$CREATE_UNICODE_ENVIRONMENT = 0x00040000',
|
|
913
|
+
'$flags = $CREATE_NO_WINDOW -bor $CREATE_UNICODE_ENVIRONMENT',
|
|
914
|
+
'$result = [Win32.NativeMethods]::CreateProcessAsUser($tokenPtr, $null, $cmdLine, [IntPtr]::Zero, [IntPtr]::Zero, $bInherit, $flags, $envBlock, $null, [ref]$si, [ref]$pi)',
|
|
915
|
+
'[void][Win32.NativeMethods]',
|
|
916
|
+
'if (-not $result) { Write-Error "CreateProcessAsUser failed (code=$LASTEXITCODE)"; exit 1 }',
|
|
917
|
+
'[void][Runtime.InteropServices.Marshal]::Release($tokenPtr)',
|
|
918
|
+
'Write-Output "started pid=$($pi.dwProcessId)"',
|
|
919
|
+
].join('\r\n');
|
|
920
|
+
fs_1.default.writeFileSync(launchPath, '\uFEFF' + psLaunchScript, 'utf8');
|
|
921
|
+
const { stdout } = await execAsync('powershell -NoProfile -ExecutionPolicy Bypass -File "' + launchPath + '"', { timeout: 15000, windowsHide: true });
|
|
922
|
+
logger.info('Helper launch output: ' + stdout.trim());
|
|
923
|
+
// Wait for helper to become ready,连接成功后升级为持久 socket
|
|
924
|
+
const maxWait = 5000;
|
|
925
|
+
const start = Date.now();
|
|
926
|
+
while (Date.now() - start < maxWait) {
|
|
927
|
+
try {
|
|
928
|
+
await new Promise((resolve, reject) => {
|
|
929
|
+
const s = net_1.default.createConnection(HELPER_PORT, '127.0.0.1');
|
|
930
|
+
s.setTimeout(500);
|
|
931
|
+
s.on('connect', () => { s.destroy(); resolve(); });
|
|
932
|
+
s.on('timeout', () => { s.destroy(); reject(new Error('timeout')); });
|
|
933
|
+
s.on('error', () => { s.destroy(); reject(new Error('error')); });
|
|
934
|
+
});
|
|
935
|
+
// Helper 已就绪,建立持久 socket
|
|
936
|
+
helperSocket = net_1.default.createConnection(HELPER_PORT, '127.0.0.1');
|
|
937
|
+
helperSocket.setTimeout(0);
|
|
938
|
+
helperSocket.on('data', (chunk) => {
|
|
939
|
+
handleHelperData(chunk.toString('utf8'));
|
|
940
|
+
});
|
|
941
|
+
helperSocket.on('error', (err) => {
|
|
942
|
+
logger.error('Helper socket error: ' + err.message);
|
|
943
|
+
helperReady = false;
|
|
944
|
+
});
|
|
945
|
+
helperSocket.on('close', () => {
|
|
946
|
+
helperReady = false;
|
|
947
|
+
logger.info('Helper socket closed');
|
|
948
|
+
});
|
|
949
|
+
helperReady = true;
|
|
950
|
+
logger.info('Helper process ready');
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
await new Promise(r => setTimeout(r, 500));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return helperReady;
|
|
958
|
+
}
|
|
959
|
+
catch (err) {
|
|
960
|
+
logger.error('Helper launch failed: ' + (err.message || err));
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
finally {
|
|
964
|
+
try {
|
|
965
|
+
fs_1.default.unlinkSync(scriptPath);
|
|
966
|
+
}
|
|
967
|
+
catch { /* ignore */ }
|
|
968
|
+
try {
|
|
969
|
+
fs_1.default.unlinkSync(launchPath);
|
|
970
|
+
}
|
|
971
|
+
catch { /* ignore */ }
|
|
972
|
+
}
|
|
973
|
+
}
|
|
@@ -10,6 +10,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
10
10
|
const os_1 = __importDefault(require("os"));
|
|
11
11
|
const shared_1 = require("@myassis/shared");
|
|
12
12
|
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const ServiceManager_js_1 = require("../ServiceManager.js");
|
|
13
14
|
const logger = (0, shared_1.getLogger)('exec');
|
|
14
15
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
15
16
|
// 待批准的命令缓存:token -> { command, cwd, timeout, sessionId, expiresAt }
|
|
@@ -71,14 +72,15 @@ function generateApprovalToken() {
|
|
|
71
72
|
}
|
|
72
73
|
/** 检查命令是否危险 */
|
|
73
74
|
function isDangerousCommand(command) {
|
|
75
|
+
// 命令分隔符(起始位置、空格、shell 操作符)隔离的危险关键字
|
|
74
76
|
const dangerousPatterns = [
|
|
75
|
-
/rm\s+-rf\s+\//,
|
|
76
|
-
/format\s+[a-z]:/i,
|
|
77
|
-
/del\s+\/[sfq]\s+\*/i,
|
|
78
|
-
/shutdown/i,
|
|
79
|
-
/reboot/i,
|
|
80
|
-
/mkfs/i,
|
|
81
|
-
/dd\s+if=.*of=\/dev\//i,
|
|
77
|
+
/(^|[\s;|&])rm\s+-rf\s+\//,
|
|
78
|
+
/(^|[\s;|&])format\s+[a-z]:/i,
|
|
79
|
+
/(^|[\s;|&])del\s+\/[sfq]\s+\*/i,
|
|
80
|
+
/(^|[\s;|&])shutdown\b/i,
|
|
81
|
+
/(^|[\s;|&])reboot\b/i,
|
|
82
|
+
/(^|[\s;|&])mkfs\b/i,
|
|
83
|
+
/(^|[\s;|&])dd\s+if=.*of=\/dev\//i,
|
|
82
84
|
];
|
|
83
85
|
for (const reg of dangerousPatterns) {
|
|
84
86
|
if (reg.test(command))
|
|
@@ -174,6 +176,23 @@ exports.execTool = {
|
|
|
174
176
|
return;
|
|
175
177
|
}
|
|
176
178
|
}
|
|
179
|
+
// Windows 下通过 Helper 以登录用户身份执行
|
|
180
|
+
if (process.platform === 'win32') {
|
|
181
|
+
try {
|
|
182
|
+
const result = await (0, ServiceManager_js_1.execAsUser)(command, cwd);
|
|
183
|
+
resolve({
|
|
184
|
+
success: result.exitCode === 0,
|
|
185
|
+
output: result.stdout.substring(0, 100000),
|
|
186
|
+
errorMessage: result.stderr.substring(0, 100000),
|
|
187
|
+
exitCode: result.exitCode,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
resolve({ success: false, errorMessage: e?.message || String(e) });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
177
196
|
const options = {
|
|
178
197
|
cwd,
|
|
179
198
|
timeout,
|