@myassis/gateway 1.0.41 → 1.0.43
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 +174 -60
- package/package.json +1 -1
|
@@ -56,47 +56,145 @@ function getNssmPath() {
|
|
|
56
56
|
}
|
|
57
57
|
// ─── Windows 实现 ───────────────────────────────────────────
|
|
58
58
|
async function queryServiceWindows() {
|
|
59
|
+
const inRunKey = isRegisteredInRunKey();
|
|
60
|
+
const running = isGatewayRunning();
|
|
61
|
+
return { installed: inRunKey, running };
|
|
62
|
+
}
|
|
63
|
+
// ─── 用户级进程管理 ─────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* 获取 Gateway 可执行文件路径
|
|
66
|
+
*/
|
|
67
|
+
function getGatewayExePath() {
|
|
68
|
+
if (isPackagedExe()) {
|
|
69
|
+
return process.execPath;
|
|
70
|
+
}
|
|
71
|
+
const nodeExec = getNodeExec();
|
|
72
|
+
return nodeExec;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 检查 Gateway 进程是否在运行
|
|
76
|
+
*/
|
|
77
|
+
function isGatewayRunning() {
|
|
78
|
+
const pid = getStoredPid();
|
|
79
|
+
if (!pid)
|
|
80
|
+
return false;
|
|
59
81
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
// Windows: kill signal 0 探测进程是否存在
|
|
83
|
+
process.kill(pid, 0);
|
|
84
|
+
return true;
|
|
62
85
|
}
|
|
63
86
|
catch {
|
|
64
|
-
return
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 从 PID 文件读取存储的 PID
|
|
92
|
+
*/
|
|
93
|
+
function getStoredPid() {
|
|
94
|
+
try {
|
|
95
|
+
if (!fs_1.default.existsSync(GATEWAY_PID_FILE))
|
|
96
|
+
return null;
|
|
97
|
+
const pid = parseInt(fs_1.default.readFileSync(GATEWAY_PID_FILE, 'utf8').trim(), 10);
|
|
98
|
+
return isNaN(pid) ? null : pid;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 停止 Gateway 用户进程(优雅终止)
|
|
106
|
+
*/
|
|
107
|
+
async function stopGatewayProcess() {
|
|
108
|
+
const pid = getStoredPid();
|
|
109
|
+
if (!pid)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
process.kill(pid, 'SIGTERM');
|
|
113
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
114
|
+
}
|
|
115
|
+
catch { /* ignore */ }
|
|
116
|
+
// 强制终止(如果还没退出)
|
|
117
|
+
try {
|
|
118
|
+
process.kill(pid, 'SIGKILL');
|
|
119
|
+
}
|
|
120
|
+
catch { /* ignore */ }
|
|
121
|
+
try {
|
|
122
|
+
fs_1.default.unlinkSync(GATEWAY_PID_FILE);
|
|
123
|
+
}
|
|
124
|
+
catch { /* ignore */ }
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 写入 Gateway 启动脚本(用户级,不弹出窗口)
|
|
128
|
+
*/
|
|
129
|
+
function writeGatewayLauncherScript(exePath, scriptPath, workDir) {
|
|
130
|
+
// 写入 BOM + UTF-8 内容
|
|
131
|
+
const content = [
|
|
132
|
+
"$ErrorActionPreference = 'Stop'",
|
|
133
|
+
"$exe = '" + exePath.replace(/\\/g, '\\\\') + "'",
|
|
134
|
+
"$workDir = '" + workDir.replace(/\\/g, '\\\\') + "'",
|
|
135
|
+
"$pidFile = '" + GATEWAY_PID_FILE.replace(/\\/g, '\\\\') + "'",
|
|
136
|
+
"",
|
|
137
|
+
"# 启动 Gateway(使用 CLI start 命令,监听默认端口 3001)",
|
|
138
|
+
"$psi = New-Object System.Diagnostics.ProcessStartInfo",
|
|
139
|
+
"$psi.FileName = $exe",
|
|
140
|
+
"$psi.WorkingDirectory = $workDir",
|
|
141
|
+
"$psi.UseShellExecute = $false",
|
|
142
|
+
"$psi.RedirectStandardOutput = $true",
|
|
143
|
+
"$psi.RedirectStandardError = $true",
|
|
144
|
+
"$psi.CreateNoWindow = $true",
|
|
145
|
+
"$psi.Environment['NODE_ENV'] = 'production'",
|
|
146
|
+
"$proc = [System.Diagnostics.Process]::Start($psi)",
|
|
147
|
+
"if ($proc) {",
|
|
148
|
+
" $proc.Id.ToString() | Out-File -FilePath $pidFile -Encoding UTF8",
|
|
149
|
+
" $proc.WaitForExit()",
|
|
150
|
+
"}",
|
|
151
|
+
].join('\r\n');
|
|
152
|
+
fs_1.default.writeFileSync(scriptPath, '\uFEFF' + content, 'utf8');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 检查 Run key 是否已注册
|
|
156
|
+
*/
|
|
157
|
+
function isRegisteredInRunKey() {
|
|
158
|
+
// 通过 PowerShell 读取 HKCU Run key
|
|
159
|
+
try {
|
|
160
|
+
const { execSync } = require('child_process');
|
|
161
|
+
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 });
|
|
162
|
+
return result.toString().trim() === 'True';
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
65
166
|
}
|
|
66
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* 写入注册表 Run key(开机自启)
|
|
170
|
+
*/
|
|
171
|
+
async function registerRunKey(scriptPath) {
|
|
172
|
+
const regCmd = `powershell -NoProfile -Command "Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${exports.SERVICE_NAME}' -Value '\"${scriptPath}\"' -Type String"`;
|
|
173
|
+
await execAsync(regCmd, { timeout: 10000, windowsHide: true });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 删除注册表 Run key
|
|
177
|
+
*/
|
|
178
|
+
async function unregisterRunKey() {
|
|
179
|
+
try {
|
|
180
|
+
const regCmd = `powershell -NoProfile -Command "Remove-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${exports.SERVICE_NAME}' -ErrorAction SilentlyContinue"`;
|
|
181
|
+
await execAsync(regCmd, { timeout: 10000, windowsHide: true });
|
|
182
|
+
}
|
|
183
|
+
catch { /* ignore */ }
|
|
184
|
+
}
|
|
185
|
+
// ─── Windows 用户级实现 ─────────────────────────────────────
|
|
67
186
|
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}`);
|
|
187
|
+
const exe = getGatewayExePath();
|
|
188
|
+
const workDir = path_1.default.dirname(exe);
|
|
189
|
+
logger.info(`installWindows: exe=${exe}, workDir=${workDir}`);
|
|
73
190
|
const { installed } = await queryServiceWindows();
|
|
74
191
|
if (installed)
|
|
75
192
|
await uninstallWindows();
|
|
76
|
-
const nssmPath = getNssmPath();
|
|
77
|
-
const useNssm = !!nssmPath && fs_1.default.existsSync(nssmPath);
|
|
78
193
|
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
|
-
}
|
|
194
|
+
writeGatewayLauncherScript(exe, GATEWAY_LAUNCHER_FILE, workDir);
|
|
195
|
+
await registerRunKey(GATEWAY_LAUNCHER_FILE);
|
|
98
196
|
await startService();
|
|
99
|
-
return { success: true, message: '
|
|
197
|
+
return { success: true, message: 'Gateway 安装并启动成功(用户级进程)' };
|
|
100
198
|
}
|
|
101
199
|
catch (err) {
|
|
102
200
|
return { success: false, message: `安装失败: ${err.message}` };
|
|
@@ -105,48 +203,48 @@ async function installWindows() {
|
|
|
105
203
|
async function uninstallWindows() {
|
|
106
204
|
try {
|
|
107
205
|
await stopService();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
await execAsync(`sc.exe delete ${exports.SERVICE_NAME}`, { timeout: 10000 });
|
|
206
|
+
await unregisterRunKey();
|
|
207
|
+
try {
|
|
208
|
+
fs_1.default.unlinkSync(GATEWAY_PID_FILE);
|
|
114
209
|
}
|
|
115
|
-
|
|
210
|
+
catch { /* ignore */ }
|
|
211
|
+
return { success: true, message: 'Gateway 已卸载(用户级进程)' };
|
|
116
212
|
}
|
|
117
213
|
catch (err) {
|
|
118
214
|
return { success: false, message: `卸载失败: ${err.message}` };
|
|
119
215
|
}
|
|
120
216
|
}
|
|
121
217
|
async function startServiceWindows() {
|
|
218
|
+
if (isGatewayRunning()) {
|
|
219
|
+
return { success: true, message: 'Gateway 已在运行' };
|
|
220
|
+
}
|
|
122
221
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
222
|
+
const exe = getGatewayExePath();
|
|
223
|
+
const workDir = path_1.default.dirname(exe);
|
|
224
|
+
if (!fs_1.default.existsSync(GATEWAY_LAUNCHER_FILE)) {
|
|
225
|
+
writeGatewayLauncherScript(exe, GATEWAY_LAUNCHER_FILE, workDir);
|
|
226
|
+
}
|
|
227
|
+
await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "${GATEWAY_LAUNCHER_FILE}"`, { timeout: 5000, windowsHide: true });
|
|
228
|
+
const maxWait = 5000;
|
|
229
|
+
const start = Date.now();
|
|
230
|
+
while (!isGatewayRunning() && Date.now() - start < maxWait) {
|
|
231
|
+
await new Promise(r => setTimeout(r, 500));
|
|
232
|
+
}
|
|
233
|
+
if (isGatewayRunning()) {
|
|
234
|
+
return { success: true, message: 'Gateway 启动成功' };
|
|
235
|
+
}
|
|
236
|
+
return { success: false, message: 'Gateway 进程未能启动,请检查日志' };
|
|
125
237
|
}
|
|
126
238
|
catch (err) {
|
|
127
239
|
return { success: false, message: `启动失败: ${err.message}` };
|
|
128
240
|
}
|
|
129
241
|
}
|
|
130
242
|
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
243
|
try {
|
|
148
|
-
await
|
|
149
|
-
|
|
244
|
+
await stopService();
|
|
245
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
246
|
+
await startService();
|
|
247
|
+
return { success: true, message: 'Gateway 重启成功' };
|
|
150
248
|
}
|
|
151
249
|
catch (err) {
|
|
152
250
|
return { success: false, message: `重启失败: ${err.message}` };
|
|
@@ -154,8 +252,8 @@ async function restartServiceWindows() {
|
|
|
154
252
|
}
|
|
155
253
|
async function stopServiceWindows() {
|
|
156
254
|
try {
|
|
157
|
-
await
|
|
158
|
-
return { success: true, message: '
|
|
255
|
+
await stopGatewayProcess();
|
|
256
|
+
return { success: true, message: 'Gateway 已停止' };
|
|
159
257
|
}
|
|
160
258
|
catch (err) {
|
|
161
259
|
return { success: false, message: `停止失败: ${err.message}` };
|
|
@@ -560,6 +658,10 @@ exports.updateService = updateService;
|
|
|
560
658
|
const HELPER_PORT = 19630;
|
|
561
659
|
/** Helper 超时时间(毫秒) */
|
|
562
660
|
const HELPER_TIMEOUT_MS = 60000;
|
|
661
|
+
/** Gateway PID 文件路径(用户级进程跟踪) */
|
|
662
|
+
const GATEWAY_PID_FILE = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-pid.txt');
|
|
663
|
+
/** Gateway 启动脚本路径 */
|
|
664
|
+
const GATEWAY_LAUNCHER_FILE = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-launcher.ps1');
|
|
563
665
|
/** 当前活跃的 Helper 进程 */
|
|
564
666
|
let helperProcess = null;
|
|
565
667
|
/** Helper 连接 socket */
|
|
@@ -642,7 +744,19 @@ async function execAsUser(command, cwd) {
|
|
|
642
744
|
if (!helperReady || !helperSocket) {
|
|
643
745
|
const started = await spawnHelperAsUser();
|
|
644
746
|
if (!started) {
|
|
645
|
-
|
|
747
|
+
// Helper 启动失败,降级到直接 exec(exit=-2 表示降级执行)
|
|
748
|
+
logger.warn('Helper spawn failed, falling back to direct exec (exit=-2 = fallback used)');
|
|
749
|
+
try {
|
|
750
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
751
|
+
cwd: cwd || os_1.default.homedir(),
|
|
752
|
+
timeout: HELPER_TIMEOUT_MS,
|
|
753
|
+
windowsHide: true,
|
|
754
|
+
});
|
|
755
|
+
return { stdout, stderr, exitCode: 0 };
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
return { stdout: '', stderr: err?.message || String(err), exitCode: -2 };
|
|
759
|
+
}
|
|
646
760
|
}
|
|
647
761
|
}
|
|
648
762
|
const id = newCmdId();
|