@myassis/gateway 1.0.41 → 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 +197 -72
- package/package.json +1 -1
|
@@ -64,39 +64,148 @@ async function queryServiceWindows() {
|
|
|
64
64
|
return { installed: false, running: false };
|
|
65
65
|
}
|
|
66
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 用户级实现 ─────────────────────────────────────
|
|
67
197
|
async function installWindows() {
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const workDir = isPkg ? path_1.default.dirname(exe) : path_1.default.dirname(script);
|
|
72
|
-
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}`);
|
|
73
201
|
const { installed } = await queryServiceWindows();
|
|
74
202
|
if (installed)
|
|
75
203
|
await uninstallWindows();
|
|
76
|
-
const nssmPath = getNssmPath();
|
|
77
|
-
const useNssm = !!nssmPath && fs_1.default.existsSync(nssmPath);
|
|
78
204
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await execAsync(`"${nssmPath}" install ${exports.SERVICE_NAME} "${exe}"`, { timeout: 15000, cwd: workDir });
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
await execAsync(`"${nssmPath}" install ${exports.SERVICE_NAME} "${exe}" "${script}"`, { timeout: 15000, cwd: workDir });
|
|
85
|
-
}
|
|
86
|
-
await execAsync(`"${nssmPath}" set ${exports.SERVICE_NAME} AppDirectory "${workDir}"`, { timeout: 10000 });
|
|
87
|
-
await execAsync(`"${nssmPath}" set ${exports.SERVICE_NAME} DisplayName "${exports.SERVICE_DISPLAY_NAME}"`, { timeout: 10000 });
|
|
88
|
-
await execAsync(`"${nssmPath}" set ${exports.SERVICE_NAME} Start SERVICE_AUTO_START`, { timeout: 10000 });
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
if (isPkg) {
|
|
92
|
-
await execAsync(`sc.exe create ${exports.SERVICE_NAME} binPath= "\\"${exe}\\"" DisplayName= "${exports.SERVICE_DISPLAY_NAME}" start= auto`, { timeout: 15000 });
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
await execAsync(`sc.exe create ${exports.SERVICE_NAME} binPath= "\\"${exe}\\" \\"${script}\\"" DisplayName= "${exports.SERVICE_DISPLAY_NAME}" start= auto`, { timeout: 15000 });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
205
|
+
writeGatewayLauncherScript(exe, GATEWAY_LAUNCHER_FILE, workDir);
|
|
206
|
+
await registerRunKey(GATEWAY_LAUNCHER_FILE);
|
|
98
207
|
await startService();
|
|
99
|
-
return { success: true, message: '
|
|
208
|
+
return { success: true, message: 'Gateway 安装并启动成功(用户级进程)' };
|
|
100
209
|
}
|
|
101
210
|
catch (err) {
|
|
102
211
|
return { success: false, message: `安装失败: ${err.message}` };
|
|
@@ -105,48 +214,48 @@ async function installWindows() {
|
|
|
105
214
|
async function uninstallWindows() {
|
|
106
215
|
try {
|
|
107
216
|
await stopService();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
await execAsync(`sc.exe delete ${exports.SERVICE_NAME}`, { timeout: 10000 });
|
|
217
|
+
await unregisterRunKey();
|
|
218
|
+
try {
|
|
219
|
+
fs_1.default.unlinkSync(GATEWAY_PID_FILE);
|
|
114
220
|
}
|
|
115
|
-
|
|
221
|
+
catch { /* ignore */ }
|
|
222
|
+
return { success: true, message: 'Gateway 已卸载(用户级进程)' };
|
|
116
223
|
}
|
|
117
224
|
catch (err) {
|
|
118
225
|
return { success: false, message: `卸载失败: ${err.message}` };
|
|
119
226
|
}
|
|
120
227
|
}
|
|
121
228
|
async function startServiceWindows() {
|
|
229
|
+
if (isGatewayRunning()) {
|
|
230
|
+
return { success: true, message: 'Gateway 已在运行' };
|
|
231
|
+
}
|
|
122
232
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
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 进程未能启动,请检查日志' };
|
|
125
248
|
}
|
|
126
249
|
catch (err) {
|
|
127
250
|
return { success: false, message: `启动失败: ${err.message}` };
|
|
128
251
|
}
|
|
129
252
|
}
|
|
130
253
|
async function restartServiceWindows() {
|
|
131
|
-
const nssmPath = getNssmPath();
|
|
132
|
-
// 无 nssm 时需要用户手动更新
|
|
133
|
-
if (!fs_1.default.existsSync(nssmPath)) {
|
|
134
|
-
const info = await getServiceInfo();
|
|
135
|
-
if (info.installed) {
|
|
136
|
-
return {
|
|
137
|
-
success: false,
|
|
138
|
-
message: '检测到您尚未安装 nssm,无法自动重启服务。请手动执行以下步骤更新 Gateway:\n' +
|
|
139
|
-
'1. 在桌面上停止 Gateway 服务(系统托盘 → 右键 → 退出)\n' +
|
|
140
|
-
'2. 运行 `npm install -g @myassis/gateway@latest`\n' +
|
|
141
|
-
'3. 重新启动 Gateway 服务\n\n' +
|
|
142
|
-
'如需自动重启功能,请从 https://nssm.cc/download 下载 nssm 并放置到 Gateway 同目录下',
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
// 服务未安装时,可直接更新
|
|
146
|
-
}
|
|
147
254
|
try {
|
|
148
|
-
await
|
|
149
|
-
|
|
255
|
+
await stopService();
|
|
256
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
257
|
+
await startService();
|
|
258
|
+
return { success: true, message: 'Gateway 重启成功' };
|
|
150
259
|
}
|
|
151
260
|
catch (err) {
|
|
152
261
|
return { success: false, message: `重启失败: ${err.message}` };
|
|
@@ -154,8 +263,8 @@ async function restartServiceWindows() {
|
|
|
154
263
|
}
|
|
155
264
|
async function stopServiceWindows() {
|
|
156
265
|
try {
|
|
157
|
-
await
|
|
158
|
-
return { success: true, message: '
|
|
266
|
+
await stopGatewayProcess();
|
|
267
|
+
return { success: true, message: 'Gateway 已停止' };
|
|
159
268
|
}
|
|
160
269
|
catch (err) {
|
|
161
270
|
return { success: false, message: `停止失败: ${err.message}` };
|
|
@@ -190,21 +299,21 @@ async function installLinux() {
|
|
|
190
299
|
if (installed)
|
|
191
300
|
await stopService();
|
|
192
301
|
const execStart = isPkg ? exe : `${exe} ${script}`;
|
|
193
|
-
const unitContent = `[Unit]
|
|
194
|
-
Description=${exports.SERVICE_DISPLAY_NAME}
|
|
195
|
-
After=network.target
|
|
196
|
-
|
|
197
|
-
[Service]
|
|
198
|
-
Type=simple
|
|
199
|
-
User=${process.env.USER || 'root'}
|
|
200
|
-
WorkingDirectory=${workDir}
|
|
201
|
-
ExecStart=${execStart}
|
|
202
|
-
Restart=always
|
|
203
|
-
RestartSec=5
|
|
204
|
-
Environment=NODE_ENV=production
|
|
205
|
-
|
|
206
|
-
[Install]
|
|
207
|
-
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
|
|
208
317
|
`;
|
|
209
318
|
await fs_1.default.promises.writeFile('/tmp/myassis-gateway.service', unitContent, 'utf8');
|
|
210
319
|
await execAsync('cp /tmp/myassis-gateway.service /etc/systemd/system/myassis-gateway.service', { timeout: 10000 });
|
|
@@ -560,6 +669,10 @@ exports.updateService = updateService;
|
|
|
560
669
|
const HELPER_PORT = 19630;
|
|
561
670
|
/** Helper 超时时间(毫秒) */
|
|
562
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');
|
|
563
676
|
/** 当前活跃的 Helper 进程 */
|
|
564
677
|
let helperProcess = null;
|
|
565
678
|
/** Helper 连接 socket */
|
|
@@ -642,7 +755,19 @@ async function execAsUser(command, cwd) {
|
|
|
642
755
|
if (!helperReady || !helperSocket) {
|
|
643
756
|
const started = await spawnHelperAsUser();
|
|
644
757
|
if (!started) {
|
|
645
|
-
|
|
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
|
+
}
|
|
646
771
|
}
|
|
647
772
|
}
|
|
648
773
|
const id = newCmdId();
|