amalgm 0.1.41 → 0.1.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/lib/cli.js +27 -0
- package/lib/process-cleanup.js +213 -0
- package/lib/service.js +33 -40
- package/lib/supervisor.js +20 -0
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +9 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +1 -1
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +8 -0
- package/runtime/scripts/fs-watcher.js +45 -10
- package/runtime/scripts/port-monitor.js +54 -16
package/lib/cli.js
CHANGED
|
@@ -28,6 +28,11 @@ const {
|
|
|
28
28
|
stopService,
|
|
29
29
|
uninstallService,
|
|
30
30
|
} = require('./service');
|
|
31
|
+
const {
|
|
32
|
+
cleanupStaleRuntimeProcesses,
|
|
33
|
+
runtimeServiceProcesses,
|
|
34
|
+
supervisorProcesses,
|
|
35
|
+
} = require('./process-cleanup');
|
|
31
36
|
const {
|
|
32
37
|
deleteComputerAuth,
|
|
33
38
|
loadComputerRecord,
|
|
@@ -846,6 +851,8 @@ async function start(options) {
|
|
|
846
851
|
await stop();
|
|
847
852
|
}
|
|
848
853
|
|
|
854
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
855
|
+
|
|
849
856
|
const blockedPorts = await unavailableServicePorts();
|
|
850
857
|
if (blockedPorts.length > 0) {
|
|
851
858
|
const detail = blockedPorts.map(([name, port]) => `${name} (${port})`).join(', ');
|
|
@@ -928,6 +935,7 @@ async function stop() {
|
|
|
928
935
|
} catch {
|
|
929
936
|
// noop
|
|
930
937
|
}
|
|
938
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
931
939
|
console.log('Amalgm is not running.');
|
|
932
940
|
return;
|
|
933
941
|
}
|
|
@@ -951,6 +959,7 @@ async function stop() {
|
|
|
951
959
|
} catch {
|
|
952
960
|
// noop
|
|
953
961
|
}
|
|
962
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
954
963
|
console.log('Amalgm stopped.');
|
|
955
964
|
}
|
|
956
965
|
|
|
@@ -1028,6 +1037,14 @@ async function status() {
|
|
|
1028
1037
|
}
|
|
1029
1038
|
console.log(`HMAC proxy: ${proxyTokenFromRecord(record) ? 'configured' : 'not configured'}`);
|
|
1030
1039
|
|
|
1040
|
+
const runtimeChildren = runtimeServiceProcesses();
|
|
1041
|
+
const runtimeSupervisors = supervisorProcesses();
|
|
1042
|
+
if (runtimeChildren.length > 5 || runtimeSupervisors.length > 1) {
|
|
1043
|
+
console.log(
|
|
1044
|
+
`Warning: multiple Amalgm runtime processes detected (${runtimeSupervisors.length} supervisor, ${runtimeChildren.length} services). Run \`amalgm stop && amalgm start\`.`,
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1031
1048
|
for (const [name, port] of servicePorts()) {
|
|
1032
1049
|
const ok = await checkHttp(port);
|
|
1033
1050
|
console.log(`${name.padEnd(13)} ${ok ? 'up' : 'down'} http://127.0.0.1:${port}`);
|
|
@@ -1101,6 +1118,16 @@ async function doctor() {
|
|
|
1101
1118
|
);
|
|
1102
1119
|
}
|
|
1103
1120
|
add('daemon', daemonRunning, daemonRunning ? `pid ${pid}` : 'stopped', daemonRunning ? 'ok' : 'warn');
|
|
1121
|
+
const runtimeChildren = runtimeServiceProcesses();
|
|
1122
|
+
const runtimeSupervisors = supervisorProcesses();
|
|
1123
|
+
const expectedServices = daemonRunning ? 5 : 0;
|
|
1124
|
+
const cleanProcessShape = runtimeSupervisors.length <= 1 && runtimeChildren.length <= expectedServices;
|
|
1125
|
+
add(
|
|
1126
|
+
'runtime processes',
|
|
1127
|
+
cleanProcessShape,
|
|
1128
|
+
`${runtimeSupervisors.length} supervisor, ${runtimeChildren.length} services`,
|
|
1129
|
+
cleanProcessShape ? 'ok' : 'warn',
|
|
1130
|
+
);
|
|
1104
1131
|
if (lastShutdown?.signal || lastShutdown?.reason) {
|
|
1105
1132
|
add(
|
|
1106
1133
|
'last shutdown',
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
AMALGM_DIR,
|
|
9
|
+
PACKAGE_ROOT,
|
|
10
|
+
RUNTIME_DIR,
|
|
11
|
+
} = require('./paths');
|
|
12
|
+
|
|
13
|
+
const RUNTIME_SERVICE_MARKERS = [
|
|
14
|
+
path.join(RUNTIME_DIR, 'scripts', 'port-monitor.js'),
|
|
15
|
+
path.join(RUNTIME_DIR, 'scripts', 'fs-watcher.js'),
|
|
16
|
+
path.join(RUNTIME_DIR, 'scripts', 'chat-server.js'),
|
|
17
|
+
path.join(RUNTIME_DIR, 'scripts', 'local-gateway.js'),
|
|
18
|
+
path.join(RUNTIME_DIR, 'scripts', 'amalgm-mcp', 'index.js'),
|
|
19
|
+
].map((value) => value.replace(/\\/g, '/'));
|
|
20
|
+
const RUNTIME_SERVICE_SUFFIXES = [
|
|
21
|
+
'runtime/scripts/port-monitor.js',
|
|
22
|
+
'runtime/scripts/fs-watcher.js',
|
|
23
|
+
'runtime/scripts/chat-server.js',
|
|
24
|
+
'runtime/scripts/local-gateway.js',
|
|
25
|
+
'runtime/scripts/amalgm-mcp/index.js',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const SUPERVISOR_MARKER = path.join(PACKAGE_ROOT, 'bin', 'amalgm.js').replace(/\\/g, '/');
|
|
29
|
+
|
|
30
|
+
function isPidRunning(pid) {
|
|
31
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function waitForExit(pid, timeoutMs = 5000) {
|
|
41
|
+
const deadline = Date.now() + timeoutMs;
|
|
42
|
+
while (Date.now() < deadline && isPidRunning(pid)) {
|
|
43
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
44
|
+
}
|
|
45
|
+
return !isPidRunning(pid);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readProcString(file) {
|
|
49
|
+
try {
|
|
50
|
+
return fs.readFileSync(file).toString('utf8');
|
|
51
|
+
} catch {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readProcCommand(pid) {
|
|
57
|
+
const cmdline = readProcString(`/proc/${pid}/cmdline`).replace(/\0/g, ' ').trim();
|
|
58
|
+
if (cmdline) return cmdline;
|
|
59
|
+
try {
|
|
60
|
+
const result = spawnSync('ps', ['-p', String(pid), '-o', 'command='], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
timeout: 1000,
|
|
63
|
+
});
|
|
64
|
+
return String(result.stdout || '').trim();
|
|
65
|
+
} catch {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readProcEnv(pid) {
|
|
71
|
+
const raw = readProcString(`/proc/${pid}/environ`);
|
|
72
|
+
if (!raw) return null;
|
|
73
|
+
const env = {};
|
|
74
|
+
for (const entry of raw.split('\0')) {
|
|
75
|
+
if (!entry) continue;
|
|
76
|
+
const index = entry.indexOf('=');
|
|
77
|
+
if (index <= 0) continue;
|
|
78
|
+
env[entry.slice(0, index)] = entry.slice(index + 1);
|
|
79
|
+
}
|
|
80
|
+
return env;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function listProcProcesses() {
|
|
84
|
+
try {
|
|
85
|
+
return fs.readdirSync('/proc')
|
|
86
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
87
|
+
.map((entry) => Number(entry))
|
|
88
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0)
|
|
89
|
+
.map((pid) => ({
|
|
90
|
+
pid,
|
|
91
|
+
command: readProcCommand(pid),
|
|
92
|
+
env: readProcEnv(pid),
|
|
93
|
+
}))
|
|
94
|
+
.filter((item) => item.command);
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function listPsProcesses() {
|
|
101
|
+
try {
|
|
102
|
+
const result = spawnSync('ps', ['-eo', 'pid=,command='], {
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
timeout: 2000,
|
|
105
|
+
});
|
|
106
|
+
if (result.status !== 0) return [];
|
|
107
|
+
return String(result.stdout || '')
|
|
108
|
+
.split('\n')
|
|
109
|
+
.map((line) => {
|
|
110
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
111
|
+
if (!match) return null;
|
|
112
|
+
return {
|
|
113
|
+
pid: Number(match[1]),
|
|
114
|
+
command: match[2],
|
|
115
|
+
env: null,
|
|
116
|
+
};
|
|
117
|
+
})
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
} catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function listProcesses() {
|
|
125
|
+
return listProcProcesses() || listPsProcesses();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizedCommand(command) {
|
|
129
|
+
return String(command || '').replace(/\\/g, '/');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function envMatchesAmalgmDir(env) {
|
|
133
|
+
if (!env || !env.AMALGM_DIR) return true;
|
|
134
|
+
return path.resolve(env.AMALGM_DIR) === path.resolve(AMALGM_DIR);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function commandLooksLikeRuntimeService(command) {
|
|
138
|
+
const normalized = normalizedCommand(command);
|
|
139
|
+
return RUNTIME_SERVICE_MARKERS.some((marker) => normalized.includes(marker))
|
|
140
|
+
|| RUNTIME_SERVICE_SUFFIXES.some((marker) => normalized.includes(marker));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function commandLooksLikeSupervisor(command) {
|
|
144
|
+
const normalized = normalizedCommand(command);
|
|
145
|
+
return normalized.includes(SUPERVISOR_MARKER) && /\brun\b/.test(normalized);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function runtimeServiceProcesses(options = {}) {
|
|
149
|
+
const exclude = new Set([process.pid, ...(options.excludePids || [])].filter(Number.isInteger));
|
|
150
|
+
return listProcesses()
|
|
151
|
+
.filter((item) => !exclude.has(item.pid))
|
|
152
|
+
.filter((item) => commandLooksLikeRuntimeService(item.command))
|
|
153
|
+
.filter((item) => envMatchesAmalgmDir(item.env));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function supervisorProcesses(options = {}) {
|
|
157
|
+
const exclude = new Set([process.pid, ...(options.excludePids || [])].filter(Number.isInteger));
|
|
158
|
+
return listProcesses()
|
|
159
|
+
.filter((item) => !exclude.has(item.pid))
|
|
160
|
+
.filter((item) => commandLooksLikeSupervisor(item.command))
|
|
161
|
+
.filter((item) => envMatchesAmalgmDir(item.env));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function terminateProcesses(processes, options = {}) {
|
|
165
|
+
const logger = options.logger || null;
|
|
166
|
+
const unique = [...new Map(processes.map((item) => [item.pid, item])).values()]
|
|
167
|
+
.filter((item) => isPidRunning(item.pid));
|
|
168
|
+
if (unique.length === 0) return [];
|
|
169
|
+
|
|
170
|
+
for (const item of unique) {
|
|
171
|
+
try {
|
|
172
|
+
process.kill(item.pid, 'SIGTERM');
|
|
173
|
+
} catch {
|
|
174
|
+
// It may have exited between discovery and shutdown.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const survivors = [];
|
|
179
|
+
for (const item of unique) {
|
|
180
|
+
if (!waitForExit(item.pid, options.termTimeoutMs || 3000)) survivors.push(item);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const item of survivors) {
|
|
184
|
+
try {
|
|
185
|
+
process.kill(item.pid, 'SIGKILL');
|
|
186
|
+
} catch {
|
|
187
|
+
// noop
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const item of survivors) waitForExit(item.pid, options.killTimeoutMs || 1500);
|
|
191
|
+
|
|
192
|
+
const stopped = unique.filter((item) => !isPidRunning(item.pid));
|
|
193
|
+
if (stopped.length > 0 && logger?.log) {
|
|
194
|
+
logger.log(`cleaned stale Amalgm runtime process(es): ${stopped.map((item) => item.pid).join(', ')}`);
|
|
195
|
+
}
|
|
196
|
+
return stopped;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function cleanupStaleRuntimeProcesses(options = {}) {
|
|
200
|
+
const stale = runtimeServiceProcesses(options);
|
|
201
|
+
const supervisors = options.includeSupervisors ? supervisorProcesses(options) : [];
|
|
202
|
+
return terminateProcesses([...stale, ...supervisors], options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
cleanupStaleRuntimeProcesses,
|
|
207
|
+
commandLooksLikeRuntimeService,
|
|
208
|
+
commandLooksLikeSupervisor,
|
|
209
|
+
isPidRunning,
|
|
210
|
+
runtimeServiceProcesses,
|
|
211
|
+
supervisorProcesses,
|
|
212
|
+
terminateProcesses,
|
|
213
|
+
};
|
package/lib/service.js
CHANGED
|
@@ -15,6 +15,9 @@ const {
|
|
|
15
15
|
SERVICE_STATE_FILE,
|
|
16
16
|
SERVICE_STOP_FILE,
|
|
17
17
|
} = require('./paths');
|
|
18
|
+
const {
|
|
19
|
+
cleanupStaleRuntimeProcesses,
|
|
20
|
+
} = require('./process-cleanup');
|
|
18
21
|
|
|
19
22
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
20
23
|
const SERVICE_LABEL = 'ai.amalgm.runtime';
|
|
@@ -250,6 +253,7 @@ function writePortableScript(localOnly) {
|
|
|
250
253
|
`LOCK_DIR=${shellQuote(path.join(SERVICE_DIR, 'lock'))}`,
|
|
251
254
|
`NODE_BIN=${shellQuote(process.execPath)}`,
|
|
252
255
|
`AMALGM_BIN=${shellQuote(AMALGM_BIN)}`,
|
|
256
|
+
`NODE_WATCHDOG=${shellQuote(SERVICE_NODE_SCRIPT_FILE)}`,
|
|
253
257
|
`DAEMON_LOG=${shellQuote(path.join(LOG_DIR, 'daemon.log'))}`,
|
|
254
258
|
`SERVICE_LOG=${shellQuote(SERVICE_LOG_FILE)}`,
|
|
255
259
|
`RESTART_DELAY=${shellQuote(String(process.env.AMALGM_SERVICE_RESTART_DELAY || '5'))}`,
|
|
@@ -261,42 +265,7 @@ function writePortableScript(localOnly) {
|
|
|
261
265
|
|
|
262
266
|
lines.push(
|
|
263
267
|
'mkdir -p "$AMALGM_DIR" "$LOG_DIR"',
|
|
264
|
-
'
|
|
265
|
-
' existing="$(cat "$PID_FILE" 2>/dev/null | sed -n \'s/.*"pid"[[:space:]]*:[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p\')"',
|
|
266
|
-
' [ -n "$existing" ] || existing="$(cat "$PID_FILE" 2>/dev/null || true)"',
|
|
267
|
-
' if [ -n "$existing" ] && kill -0 "$existing" 2>/dev/null; then',
|
|
268
|
-
' exit 0',
|
|
269
|
-
' fi',
|
|
270
|
-
' rm -rf "$LOCK_DIR"',
|
|
271
|
-
' mkdir "$LOCK_DIR" || exit 1',
|
|
272
|
-
'fi',
|
|
273
|
-
'child_pid=""',
|
|
274
|
-
'cleanup() {',
|
|
275
|
-
' touch "$STOP_FILE" 2>/dev/null || true',
|
|
276
|
-
' if [ -n "$child_pid" ] && kill -0 "$child_pid" 2>/dev/null; then',
|
|
277
|
-
' kill "$child_pid" 2>/dev/null || true',
|
|
278
|
-
' wait "$child_pid" 2>/dev/null || true',
|
|
279
|
-
' fi',
|
|
280
|
-
' rm -f "$PID_FILE"',
|
|
281
|
-
' rm -rf "$LOCK_DIR"',
|
|
282
|
-
' exit 0',
|
|
283
|
-
'}',
|
|
284
|
-
'trap cleanup TERM INT HUP',
|
|
285
|
-
'printf \'{"pid":%s,"started_at":"%s","backend":"portable"}\\n\' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$PID_FILE"',
|
|
286
|
-
'rm -f "$STOP_FILE"',
|
|
287
|
-
'while [ ! -f "$STOP_FILE" ]; do',
|
|
288
|
-
' printf \'[%s] [service] starting amalgm run\\n\' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$SERVICE_LOG"',
|
|
289
|
-
' "$NODE_BIN" "$AMALGM_BIN" run >> "$DAEMON_LOG" 2>&1 &',
|
|
290
|
-
' child_pid=$!',
|
|
291
|
-
' wait "$child_pid"',
|
|
292
|
-
' code=$?',
|
|
293
|
-
' child_pid=""',
|
|
294
|
-
' printf \'[%s] [service] amalgm run exited code=%s\\n\' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$code" >> "$SERVICE_LOG"',
|
|
295
|
-
' [ -f "$STOP_FILE" ] && break',
|
|
296
|
-
' sleep "$RESTART_DELAY"',
|
|
297
|
-
'done',
|
|
298
|
-
'rm -f "$PID_FILE"',
|
|
299
|
-
'rm -rf "$LOCK_DIR"',
|
|
268
|
+
'exec "$NODE_BIN" "$NODE_WATCHDOG"',
|
|
300
269
|
);
|
|
301
270
|
|
|
302
271
|
fs.writeFileSync(SERVICE_SCRIPT_FILE, `${lines.join('\n')}\n`, { mode: 0o700 });
|
|
@@ -330,6 +299,9 @@ function writePortableNodeScript(localOnly) {
|
|
|
330
299
|
const fs = require('fs');
|
|
331
300
|
const path = require('path');
|
|
332
301
|
const { spawn } = require('child_process');
|
|
302
|
+
const {
|
|
303
|
+
cleanupStaleRuntimeProcesses,
|
|
304
|
+
} = require(path.join(path.dirname(${JSON.stringify(AMALGM_BIN)}), '..', 'lib', 'process-cleanup'));
|
|
333
305
|
|
|
334
306
|
const config = ${JSON.stringify(config, null, 2)};
|
|
335
307
|
let child = null;
|
|
@@ -411,19 +383,39 @@ function cleanup(exitCode = 0) {
|
|
|
411
383
|
if (child && !child.killed) {
|
|
412
384
|
try { child.kill('SIGKILL'); } catch {}
|
|
413
385
|
}
|
|
386
|
+
cleanupStaleRuntimeProcesses({
|
|
387
|
+
excludePids: [process.pid],
|
|
388
|
+
includeSupervisors: true,
|
|
389
|
+
logger: { log: (message) => append(config.serviceLog, message) },
|
|
390
|
+
});
|
|
414
391
|
finish(exitCode);
|
|
415
392
|
}, 5000);
|
|
416
393
|
child.once('exit', () => {
|
|
417
394
|
clearTimeout(timer);
|
|
395
|
+
cleanupStaleRuntimeProcesses({
|
|
396
|
+
excludePids: [process.pid],
|
|
397
|
+
includeSupervisors: true,
|
|
398
|
+
logger: { log: (message) => append(config.serviceLog, message) },
|
|
399
|
+
});
|
|
418
400
|
finish(exitCode);
|
|
419
401
|
});
|
|
420
402
|
return;
|
|
421
403
|
}
|
|
404
|
+
cleanupStaleRuntimeProcesses({
|
|
405
|
+
excludePids: [process.pid],
|
|
406
|
+
includeSupervisors: true,
|
|
407
|
+
logger: { log: (message) => append(config.serviceLog, message) },
|
|
408
|
+
});
|
|
422
409
|
finish(exitCode);
|
|
423
410
|
}
|
|
424
411
|
|
|
425
412
|
function launch() {
|
|
426
413
|
if (stopping || fs.existsSync(config.stopFile)) return cleanup(0);
|
|
414
|
+
cleanupStaleRuntimeProcesses({
|
|
415
|
+
excludePids: [process.pid],
|
|
416
|
+
includeSupervisors: true,
|
|
417
|
+
logger: { log: (message) => append(config.serviceLog, message) },
|
|
418
|
+
});
|
|
427
419
|
append(config.serviceLog, 'starting amalgm run');
|
|
428
420
|
const fd = fs.openSync(config.daemonLog, 'a');
|
|
429
421
|
child = spawn(config.nodeBin, [config.amalgmBin, 'run'], {
|
|
@@ -655,10 +647,8 @@ function startPortable() {
|
|
|
655
647
|
removeFile(SERVICE_STOP_FILE);
|
|
656
648
|
ensureDir(LOG_DIR, 0o700);
|
|
657
649
|
const logFd = fs.openSync(SERVICE_LOG_FILE, 'a');
|
|
658
|
-
const command = process.
|
|
659
|
-
const args =
|
|
660
|
-
? [state?.files?.nodeScript || SERVICE_NODE_SCRIPT_FILE]
|
|
661
|
-
: [state?.files?.script || SERVICE_SCRIPT_FILE];
|
|
650
|
+
const command = process.execPath;
|
|
651
|
+
const args = [state?.files?.nodeScript || SERVICE_NODE_SCRIPT_FILE];
|
|
662
652
|
const child = spawn(command, args, {
|
|
663
653
|
detached: true,
|
|
664
654
|
env: {
|
|
@@ -682,6 +672,7 @@ function stopDaemonProcess() {
|
|
|
682
672
|
const pid = readPidFile(PID_FILE);
|
|
683
673
|
if (!isPidRunning(pid)) {
|
|
684
674
|
removeFile(PID_FILE);
|
|
675
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
685
676
|
return false;
|
|
686
677
|
}
|
|
687
678
|
try {
|
|
@@ -701,6 +692,7 @@ function stopDaemonProcess() {
|
|
|
701
692
|
}
|
|
702
693
|
}
|
|
703
694
|
removeFile(PID_FILE);
|
|
695
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
704
696
|
return true;
|
|
705
697
|
}
|
|
706
698
|
|
|
@@ -720,6 +712,7 @@ function stopPortable() {
|
|
|
720
712
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
721
713
|
}
|
|
722
714
|
removeFile(SERVICE_PID_FILE);
|
|
715
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
723
716
|
}
|
|
724
717
|
|
|
725
718
|
function startService(options = {}) {
|
package/lib/supervisor.js
CHANGED
|
@@ -26,6 +26,11 @@ const {
|
|
|
26
26
|
ensureAgentCommandShims,
|
|
27
27
|
ensureNativeBinaries,
|
|
28
28
|
} = require('../runtime/scripts/chat-core/tooling/native-binaries');
|
|
29
|
+
const {
|
|
30
|
+
cleanupStaleRuntimeProcesses,
|
|
31
|
+
isPidRunning,
|
|
32
|
+
supervisorProcesses,
|
|
33
|
+
} = require('./process-cleanup');
|
|
29
34
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
30
35
|
|
|
31
36
|
function ensureDir(dir, mode = 0o700) {
|
|
@@ -408,6 +413,21 @@ async function startSupervisor(options = {}) {
|
|
|
408
413
|
ensureDir(AMALGM_DIR, 0o700);
|
|
409
414
|
ensureDir(LOG_DIR, 0o700);
|
|
410
415
|
|
|
416
|
+
const activeSupervisors = supervisorProcesses({ excludePids: [process.pid] });
|
|
417
|
+
if (activeSupervisors.length > 0) {
|
|
418
|
+
const pids = activeSupervisors
|
|
419
|
+
.filter((item) => isPidRunning(item.pid))
|
|
420
|
+
.map((item) => item.pid);
|
|
421
|
+
if (pids.length > 0) {
|
|
422
|
+
throw new Error(`Amalgm runtime is already running (pid ${pids.join(', ')}). Run \`amalgm stop\` first.`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
cleanupStaleRuntimeProcesses({
|
|
427
|
+
excludePids: [process.pid],
|
|
428
|
+
logger: options.foreground ? console : null,
|
|
429
|
+
});
|
|
430
|
+
|
|
411
431
|
const nativeResult = ensureNativeBinaries({
|
|
412
432
|
logger: console,
|
|
413
433
|
quiet: !options.foreground,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
18
18
|
"prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
19
19
|
"pack:dry": "npm pack --dry-run",
|
|
20
|
-
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/service.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/fs-watcher.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js"
|
|
20
|
+
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/service.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/fs-watcher.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|
|
@@ -26,6 +26,13 @@ const stopping = new Set();
|
|
|
26
26
|
const restartTimers = new Map();
|
|
27
27
|
|
|
28
28
|
const ARTIFACT_ENV_ALLOWLIST = [
|
|
29
|
+
'AMALGM_BIND_HOST',
|
|
30
|
+
'AMALGM_DIR',
|
|
31
|
+
'AMALGM_GATEWAY_PORT',
|
|
32
|
+
'AMALGM_MCP_PORT',
|
|
33
|
+
'AMALGM_RUNTIME_TOKEN',
|
|
34
|
+
'AMALGM_WORKSPACES_DIR',
|
|
35
|
+
'CHAT_SERVER_PORT',
|
|
29
36
|
'PATH',
|
|
30
37
|
'HOME',
|
|
31
38
|
'USER',
|
|
@@ -81,6 +88,8 @@ function buildEnv(artifact) {
|
|
|
81
88
|
AMALGM_ARTIFACT_ID: artifact.id,
|
|
82
89
|
AMALGM_ARTIFACT_REF: artifact.artifactRef,
|
|
83
90
|
AMALGM_ARTIFACT_URL: artifact.publicUrl,
|
|
91
|
+
AMALGM_CHAT_SERVER_URL: `http://127.0.0.1:${process.env.CHAT_SERVER_PORT || 8084}`,
|
|
92
|
+
AMALGM_MCP_URL: `http://127.0.0.1:${process.env.AMALGM_MCP_PORT || 8083}`,
|
|
84
93
|
};
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -198,7 +198,7 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
|
|
|
198
198
|
if (hasSupabase()) {
|
|
199
199
|
supabasePatch('sessions', 'id', codeSessionId, {
|
|
200
200
|
last_message_at: new Date().toISOString(),
|
|
201
|
-
status: '
|
|
201
|
+
status: 'complete',
|
|
202
202
|
new_messages: true,
|
|
203
203
|
}).catch(() => {});
|
|
204
204
|
}
|
|
@@ -233,6 +233,14 @@ async function executeTask(task) {
|
|
|
233
233
|
console.log(
|
|
234
234
|
`[AmalgmMCP:Exec] Task ${task.id} ${status} in ${durationMs}ms (session: ${codeSessionId})`,
|
|
235
235
|
);
|
|
236
|
+
|
|
237
|
+
if (hasSupabase()) {
|
|
238
|
+
supabasePatch('sessions', 'id', codeSessionId, {
|
|
239
|
+
last_message_at: new Date().toISOString(),
|
|
240
|
+
status: 'complete',
|
|
241
|
+
new_messages: true,
|
|
242
|
+
}).catch(() => {});
|
|
243
|
+
}
|
|
236
244
|
} catch (err) {
|
|
237
245
|
if (err.name === 'AbortError') {
|
|
238
246
|
appendRunLog(task.id, {
|
|
@@ -86,12 +86,36 @@ const INTERNAL_SERVICE_PORTS = new Set([
|
|
|
86
86
|
const COMMON_DEV_PORTS = new Set([3000, 3001, 3002, 3003, 4000, 4200, 4321, 5000, 5001, 5173, 5174, 6006, 7000, 8000, 8001, 8888]);
|
|
87
87
|
const BLOCKED_PREVIEW_PROCESSES = new Set(['.opencode', 'controlce', 'figma_age', 'figma_agent', 'loom', 'opencode', 'rapportd']);
|
|
88
88
|
const DEV_PREVIEW_PROCESS_RE = /^(node|bun|deno|npm|pnpm|yarn|vite|next|astro|nuxt|svelte|remix|webpack|rspack|parcel|serve|http-server|tsx|python|python3|uvicorn|gunicorn|flask|django|ruby|rails|puma|php|java|go|air|cargo|trunk|dotnet|nginx)$/i;
|
|
89
|
+
const INTERNAL_COMMAND_RE = /(?:^|\s)(?:node(?:-[^\s]+)?\s+)?[^\s]*(?:\/|\\)runtime(?:\/|\\)scripts(?:\/|\\)(?:port-monitor|fs-watcher|chat-server|local-gateway|amalgm-mcp(?:\/|\\)index)\.js(?:\s|$)/i;
|
|
90
|
+
const BLOCKED_COMMAND_RE = /(?:agent-browser|chromium|chrome)(?:\s|$)/i;
|
|
89
91
|
|
|
90
|
-
function
|
|
92
|
+
function readProcessCommand(pid) {
|
|
93
|
+
if (!Number.isInteger(pid) || pid <= 0) return '';
|
|
94
|
+
try {
|
|
95
|
+
return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim();
|
|
96
|
+
} catch {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeProcessName(processName) {
|
|
102
|
+
const name = String(processName || '').trim().toLowerCase();
|
|
103
|
+
if (/^node(?:$|[-_])/.test(name)) return 'node';
|
|
104
|
+
return name;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function publicProcessName(info) {
|
|
108
|
+
if (!info || typeof info !== 'object') return info || null;
|
|
109
|
+
return info.processName || null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isPreviewablePort(port, processInfo) {
|
|
91
113
|
if (!Number.isInteger(port) || port < 3000 || port > 65535) return false;
|
|
92
114
|
if (INTERNAL_SERVICE_PORTS.has(port)) return false;
|
|
93
115
|
if (port >= 9100 && port <= 9199) return false;
|
|
94
|
-
const
|
|
116
|
+
const command = typeof processInfo === 'object' && processInfo ? String(processInfo.command || '') : '';
|
|
117
|
+
if (INTERNAL_COMMAND_RE.test(command) || BLOCKED_COMMAND_RE.test(command)) return false;
|
|
118
|
+
const name = normalizeProcessName(typeof processInfo === 'object' && processInfo ? processInfo.processName : processInfo);
|
|
95
119
|
if (BLOCKED_PREVIEW_PROCESSES.has(name) || name.startsWith('figma')) return false;
|
|
96
120
|
if (!name) return COMMON_DEV_PORTS.has(port);
|
|
97
121
|
if (DEV_PREVIEW_PROCESS_RE.test(name)) return true;
|
|
@@ -721,7 +745,7 @@ function broadcastPortEvent(event) {
|
|
|
721
745
|
}
|
|
722
746
|
|
|
723
747
|
function getListeningPorts() {
|
|
724
|
-
/** @type {Map<number, string|null>} */
|
|
748
|
+
/** @type {Map<number, { processName: string|null, pid: number|null, command: string }>} */
|
|
725
749
|
const currentPorts = new Map();
|
|
726
750
|
|
|
727
751
|
if (process.platform === 'darwin') {
|
|
@@ -731,9 +755,13 @@ function getListeningPorts() {
|
|
|
731
755
|
for (const line of lines) {
|
|
732
756
|
const trimmed = line.trim();
|
|
733
757
|
if (!trimmed) continue;
|
|
734
|
-
const match = trimmed.match(/^(\S+)\s
|
|
758
|
+
const match = trimmed.match(/^(\S+)\s+(\d+)\s+.+\sTCP\s+.+:(\d+)\s+\(LISTEN\)$/);
|
|
735
759
|
if (!match) continue;
|
|
736
|
-
currentPorts.set(parseInt(match[
|
|
760
|
+
currentPorts.set(parseInt(match[3], 10), {
|
|
761
|
+
processName: match[1] || null,
|
|
762
|
+
pid: parseInt(match[2], 10),
|
|
763
|
+
command: '',
|
|
764
|
+
});
|
|
737
765
|
}
|
|
738
766
|
|
|
739
767
|
return currentPorts;
|
|
@@ -749,8 +777,14 @@ function getListeningPorts() {
|
|
|
749
777
|
let processName = null;
|
|
750
778
|
const procMatch = line.match(/users:\(\("([^"]+)"/) || line.match(/\/([^\s/]+)\s*$/);
|
|
751
779
|
if (procMatch) processName = procMatch[1];
|
|
780
|
+
const pidMatch = line.match(/pid=(\d+)/);
|
|
781
|
+
const pid = pidMatch ? parseInt(pidMatch[1], 10) : null;
|
|
752
782
|
|
|
753
|
-
currentPorts.set(parseInt(portMatch[1], 10),
|
|
783
|
+
currentPorts.set(parseInt(portMatch[1], 10), {
|
|
784
|
+
processName,
|
|
785
|
+
pid,
|
|
786
|
+
command: pid ? readProcessCommand(pid) : '',
|
|
787
|
+
});
|
|
754
788
|
}
|
|
755
789
|
|
|
756
790
|
return currentPorts;
|
|
@@ -760,16 +794,17 @@ function checkPorts() {
|
|
|
760
794
|
try {
|
|
761
795
|
const currentPorts = getListeningPorts();
|
|
762
796
|
|
|
763
|
-
for (const [port,
|
|
764
|
-
if (!isPreviewablePort(port,
|
|
797
|
+
for (const [port, processInfo] of currentPorts) {
|
|
798
|
+
if (!isPreviewablePort(port, processInfo)) {
|
|
765
799
|
currentPorts.delete(port);
|
|
766
800
|
}
|
|
767
801
|
}
|
|
768
802
|
|
|
769
803
|
// Detect newly opened ports
|
|
770
|
-
for (const [port,
|
|
804
|
+
for (const [port, processInfo] of currentPorts) {
|
|
771
805
|
if (!knownPorts.has(port)) {
|
|
772
|
-
knownPorts.set(port,
|
|
806
|
+
knownPorts.set(port, processInfo);
|
|
807
|
+
const processName = publicProcessName(processInfo);
|
|
773
808
|
console.log(`[FsWatcher:Ports] Port opened: ${port} (${processName || 'unknown'})`);
|
|
774
809
|
broadcastPortEvent({ type: 'port', event: 'opened', port, processName, timestamp: Date.now() });
|
|
775
810
|
}
|
|
@@ -31,10 +31,23 @@ const INTERNAL_SERVICE_PORTS = new Set([
|
|
|
31
31
|
4096,
|
|
32
32
|
]);
|
|
33
33
|
const COMMON_DEV_PORTS = new Set([3000, 3001, 3002, 3003, 4000, 4200, 4321, 5000, 5001, 5173, 5174, 6006, 7000, 8000, 8001, 8888]);
|
|
34
|
+
const BLOCKED_PREVIEW_PROCESSES = new Set(['.opencode', 'controlce', 'figma_age', 'figma_agent', 'loom', 'opencode', 'rapportd']);
|
|
35
|
+
const DEV_PREVIEW_PROCESS_RE = /^(node|bun|deno|npm|pnpm|yarn|vite|next|astro|nuxt|svelte|remix|webpack|rspack|parcel|serve|http-server|tsx|python|python3|uvicorn|gunicorn|flask|django|ruby|rails|puma|php|java|go|air|cargo|trunk|dotnet|nginx)$/i;
|
|
36
|
+
const INTERNAL_COMMAND_RE = /(?:^|\s)(?:node(?:-[^\s]+)?\s+)?[^\s]*(?:\/|\\)runtime(?:\/|\\)scripts(?:\/|\\)(?:port-monitor|fs-watcher|chat-server|local-gateway|amalgm-mcp(?:\/|\\)index)\.js(?:\s|$)/i;
|
|
37
|
+
const BLOCKED_COMMAND_RE = /(?:agent-browser|chromium|chrome)(?:\s|$)/i;
|
|
34
38
|
|
|
35
39
|
const knownPorts = new Map();
|
|
36
40
|
const subscribers = new Set();
|
|
37
41
|
|
|
42
|
+
function readProcessCommand(pid) {
|
|
43
|
+
if (!Number.isInteger(pid) || pid <= 0) return '';
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim();
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
function readProcTcpPorts(file) {
|
|
39
52
|
try {
|
|
40
53
|
const text = fs.readFileSync(file, 'utf8');
|
|
@@ -66,8 +79,14 @@ function getListeningPorts() {
|
|
|
66
79
|
const output = execSync('lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null', { encoding: 'utf-8' });
|
|
67
80
|
const ports = new Map();
|
|
68
81
|
for (const line of output.split('\n').slice(1)) {
|
|
69
|
-
const match = line.trim().match(/^(\S+)\s
|
|
70
|
-
if (match)
|
|
82
|
+
const match = line.trim().match(/^(\S+)\s+(\d+)\s+.+\sTCP\s+.+:(\d+)\s+\(LISTEN\)$/);
|
|
83
|
+
if (match) {
|
|
84
|
+
ports.set(parseInt(match[3], 10), {
|
|
85
|
+
processName: match[1] || null,
|
|
86
|
+
pid: parseInt(match[2], 10),
|
|
87
|
+
command: '',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
71
90
|
}
|
|
72
91
|
return ports;
|
|
73
92
|
}
|
|
@@ -79,39 +98,58 @@ function getListeningPorts() {
|
|
|
79
98
|
const portMatch = line.match(/:(\d+)\s+/);
|
|
80
99
|
if (!portMatch) continue;
|
|
81
100
|
const procMatch = line.match(/users:\(\("([^"]+)"/) || line.match(/\/([^\s/]+)\s*$/);
|
|
82
|
-
|
|
101
|
+
const pidMatch = line.match(/pid=(\d+)/);
|
|
102
|
+
const pid = pidMatch ? parseInt(pidMatch[1], 10) : null;
|
|
103
|
+
ports.set(parseInt(portMatch[1], 10), {
|
|
104
|
+
processName: procMatch ? procMatch[1] : null,
|
|
105
|
+
pid,
|
|
106
|
+
command: pid ? readProcessCommand(pid) : '',
|
|
107
|
+
});
|
|
83
108
|
}
|
|
84
109
|
if (ports.size > 0) return ports;
|
|
85
110
|
} catch {
|
|
86
111
|
// Minimal Docker images often omit ss and netstat.
|
|
87
112
|
}
|
|
88
113
|
|
|
89
|
-
return new Map(getListeningPortsFromProc().map((port) => [port, null]));
|
|
114
|
+
return new Map(getListeningPortsFromProc().map((port) => [port, { processName: null, pid: null, command: '' }]));
|
|
90
115
|
}
|
|
91
116
|
|
|
92
|
-
function
|
|
117
|
+
function normalizeProcessName(processName) {
|
|
118
|
+
const name = String(processName || '').trim().toLowerCase();
|
|
119
|
+
if (/^node(?:$|[-_])/.test(name)) return 'node';
|
|
120
|
+
return name;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function publicProcessName(info) {
|
|
124
|
+
if (!info || typeof info !== 'object') return info || null;
|
|
125
|
+
return info.processName || null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isPreviewablePort(port, processInfo) {
|
|
93
129
|
if (!Number.isInteger(port) || port < 3000 || port > 65535) return false;
|
|
94
130
|
if (INTERNAL_SERVICE_PORTS.has(port)) return false;
|
|
95
131
|
if (port >= 9100 && port <= 9199) return false;
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
132
|
+
const command = typeof processInfo === 'object' && processInfo ? String(processInfo.command || '') : '';
|
|
133
|
+
if (INTERNAL_COMMAND_RE.test(command) || BLOCKED_COMMAND_RE.test(command)) return false;
|
|
134
|
+
const name = normalizeProcessName(typeof processInfo === 'object' && processInfo ? processInfo.processName : processInfo);
|
|
135
|
+
if (BLOCKED_PREVIEW_PROCESSES.has(name)) return false;
|
|
98
136
|
if (name.startsWith('figma')) return false;
|
|
99
137
|
if (!name) return COMMON_DEV_PORTS.has(port);
|
|
100
|
-
if (
|
|
138
|
+
if (DEV_PREVIEW_PROCESS_RE.test(name)) return true;
|
|
101
139
|
return COMMON_DEV_PORTS.has(port) && name === 'unknown';
|
|
102
140
|
}
|
|
103
141
|
|
|
104
142
|
function checkPorts() {
|
|
105
143
|
try {
|
|
106
144
|
const ports = getListeningPorts();
|
|
107
|
-
for (const [port,
|
|
108
|
-
if (!isPreviewablePort(port,
|
|
145
|
+
for (const [port, processInfo] of ports) {
|
|
146
|
+
if (!isPreviewablePort(port, processInfo)) ports.delete(port);
|
|
109
147
|
}
|
|
110
148
|
|
|
111
|
-
for (const [port,
|
|
149
|
+
for (const [port, processInfo] of ports) {
|
|
112
150
|
if (!knownPorts.has(port)) {
|
|
113
|
-
knownPorts.set(port,
|
|
114
|
-
broadcast({ type: 'port_opened', port, processName, timestamp: Date.now() });
|
|
151
|
+
knownPorts.set(port, processInfo);
|
|
152
|
+
broadcast({ type: 'port_opened', port, processName: publicProcessName(processInfo), timestamp: Date.now() });
|
|
115
153
|
}
|
|
116
154
|
}
|
|
117
155
|
|
|
@@ -143,7 +181,7 @@ const server = http.createServer((req, res) => {
|
|
|
143
181
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
144
182
|
res.end(JSON.stringify({
|
|
145
183
|
status: 'ok',
|
|
146
|
-
activePorts: [...knownPorts.entries()].map(([port,
|
|
184
|
+
activePorts: [...knownPorts.entries()].map(([port, processInfo]) => ({ port, processName: publicProcessName(processInfo) })),
|
|
147
185
|
}));
|
|
148
186
|
return;
|
|
149
187
|
}
|
|
@@ -154,7 +192,7 @@ const server = http.createServer((req, res) => {
|
|
|
154
192
|
'Cache-Control': 'no-cache, no-transform',
|
|
155
193
|
'Connection': 'keep-alive',
|
|
156
194
|
});
|
|
157
|
-
res.write(`event: init\ndata: ${JSON.stringify({ ports: [...knownPorts.entries()].map(([port,
|
|
195
|
+
res.write(`event: init\ndata: ${JSON.stringify({ ports: [...knownPorts.entries()].map(([port, processInfo]) => ({ port, processName: publicProcessName(processInfo) })) })}\n\n`);
|
|
158
196
|
subscribers.add(res);
|
|
159
197
|
req.on('close', () => subscribers.delete(res));
|
|
160
198
|
return;
|
|
@@ -162,7 +200,7 @@ const server = http.createServer((req, res) => {
|
|
|
162
200
|
|
|
163
201
|
if (url.pathname === '/api/ports') {
|
|
164
202
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
165
|
-
res.end(JSON.stringify({ ports: [...knownPorts.entries()].map(([port,
|
|
203
|
+
res.end(JSON.stringify({ ports: [...knownPorts.entries()].map(([port, processInfo]) => ({ port, processName: publicProcessName(processInfo) })) }));
|
|
166
204
|
return;
|
|
167
205
|
}
|
|
168
206
|
|