amalgm 0.1.41 → 0.1.44
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/README.md +14 -2
- package/lib/cli.js +76 -42
- package/lib/process-cleanup.js +239 -0
- package/lib/runtime-manifest.js +131 -0
- package/lib/service.js +44 -41
- package/lib/supervisor.js +224 -51
- 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/local-gateway.js +4 -0
- package/runtime/scripts/port-monitor.js +54 -16
package/README.md
CHANGED
|
@@ -22,7 +22,19 @@ amalgm login --setup-code ABCD-EFGH-JKLM-NPQR
|
|
|
22
22
|
|
|
23
23
|
`amalgm doctor` checks the installed runtime, login record, service supervisor, required local ports, daemon state, and whether an HMAC proxy token is available.
|
|
24
24
|
|
|
25
|
-
`amalgm
|
|
25
|
+
`amalgm run` is the foreground runtime supervisor. It reads the declared service manifest, starts each local service, health-checks them, and restarts unhealthy children with backoff. In a container you control, make this the container command/entrypoint, ideally behind a tiny init such as Docker `--init`, `tini`, or the provider's init wrapper:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
amalgm run
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`amalgm start` is the host attachment helper. It installs/starts the best available launcher for the machine, then that launcher runs `amalgm run`:
|
|
32
|
+
|
|
33
|
+
- macOS host: LaunchAgent
|
|
34
|
+
- Linux host with real user systemd: systemd user service
|
|
35
|
+
- random/container shell where no service manager is available: portable best-effort watchdog
|
|
36
|
+
|
|
37
|
+
The runtime supervisor keeps the declared local essentials alive:
|
|
26
38
|
|
|
27
39
|
- port monitor on `8081`
|
|
28
40
|
- filesystem/auth watcher on `8082`
|
|
@@ -31,7 +43,7 @@ amalgm login --setup-code ABCD-EFGH-JKLM-NPQR
|
|
|
31
43
|
- events/previews/artifact tunnel to `wire.events.amalgm.ai`
|
|
32
44
|
- chat tunnel to `amalgm-chat-gateway`
|
|
33
45
|
|
|
34
|
-
The npm supervisor binds local services to `127.0.0.1` by default and connects them outward through the registered Amalgm tunnels. For development without cloud registration, use `amalgm start --local-only`. For direct foreground debugging, use `amalgm start --foreground`.
|
|
46
|
+
The npm supervisor binds local services to `127.0.0.1` by default and connects them outward through the registered Amalgm tunnels. For development without cloud registration, use `amalgm start --local-only`. For direct foreground debugging, use `amalgm run` or `amalgm start --foreground`.
|
|
35
47
|
|
|
36
48
|
Useful commands:
|
|
37
49
|
|
package/lib/cli.js
CHANGED
|
@@ -28,6 +28,17 @@ const {
|
|
|
28
28
|
stopService,
|
|
29
29
|
uninstallService,
|
|
30
30
|
} = require('./service');
|
|
31
|
+
const {
|
|
32
|
+
cleanupStaleRuntimeProcesses,
|
|
33
|
+
runtimeServiceProcesses,
|
|
34
|
+
supervisorProcesses,
|
|
35
|
+
} = require('./process-cleanup');
|
|
36
|
+
const {
|
|
37
|
+
runtimePortsForStatus,
|
|
38
|
+
runtimeServiceNames,
|
|
39
|
+
runtimeServiceScripts,
|
|
40
|
+
runtimeServices,
|
|
41
|
+
} = require('./runtime-manifest');
|
|
31
42
|
const {
|
|
32
43
|
deleteComputerAuth,
|
|
33
44
|
loadComputerRecord,
|
|
@@ -84,6 +95,7 @@ function usage() {
|
|
|
84
95
|
'Usage:',
|
|
85
96
|
' amalgm login [--app-url https://amalgm.ai] [--name "My Mac"] [--setup-code ABCD-EFGH-JKLM-NPQR] [--no-open] [--no-poll]',
|
|
86
97
|
' amalgm start [--foreground] [--local-only]',
|
|
98
|
+
' amalgm run [--local-only]',
|
|
87
99
|
' amalgm stop',
|
|
88
100
|
' amalgm status',
|
|
89
101
|
' amalgm doctor',
|
|
@@ -139,36 +151,13 @@ function runtimeEntry(relativePath) {
|
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
function runtimeMissingFiles() {
|
|
142
|
-
const required =
|
|
143
|
-
runtimeEntry('scripts/amalgm-mcp/index.js'),
|
|
144
|
-
runtimeEntry('scripts/chat-server.js'),
|
|
145
|
-
runtimeEntry('scripts/fs-watcher.js'),
|
|
146
|
-
runtimeEntry('scripts/local-gateway.js'),
|
|
147
|
-
runtimeEntry('scripts/port-monitor.js'),
|
|
148
|
-
];
|
|
154
|
+
const required = runtimeServiceScripts().map((script) => runtimeEntry(script));
|
|
149
155
|
return required.filter((file) => !fs.existsSync(file));
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
function servicePorts() {
|
|
153
159
|
const state = readJson(RUNTIME_STATE_FILE, null);
|
|
154
|
-
|
|
155
|
-
if (Number.isInteger(state?.gateway_port)) {
|
|
156
|
-
return [
|
|
157
|
-
['local-gateway', Number(state.gateway_port)],
|
|
158
|
-
['port-monitor', Number(statePorts.port_monitor || 0)],
|
|
159
|
-
['fs-watcher', Number(statePorts.fs_watcher || 0)],
|
|
160
|
-
['amalgm-mcp', Number(statePorts.amalgm_mcp || 0)],
|
|
161
|
-
['chat-server', Number(statePorts.chat_server || 0)],
|
|
162
|
-
].filter(([, port]) => Number.isInteger(port) && port > 0);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return [
|
|
166
|
-
['local-gateway', Number(process.env.AMALGM_GATEWAY_PORT || 28781)],
|
|
167
|
-
['port-monitor', Number(process.env.PORT_MONITOR_PORT || 8081)],
|
|
168
|
-
['fs-watcher', Number(process.env.FS_WATCHER_PORT || 8082)],
|
|
169
|
-
['amalgm-mcp', Number(process.env.AMALGM_MCP_PORT || 8083)],
|
|
170
|
-
['chat-server', Number(process.env.CHAT_SERVER_PORT || 8084)],
|
|
171
|
-
];
|
|
160
|
+
return runtimePortsForStatus(state, process.env);
|
|
172
161
|
}
|
|
173
162
|
|
|
174
163
|
function hasRequiredComputerFields(record) {
|
|
@@ -352,13 +341,7 @@ function isPortFree(port, host = '127.0.0.1') {
|
|
|
352
341
|
|
|
353
342
|
async function unavailableServicePorts() {
|
|
354
343
|
const results = [];
|
|
355
|
-
const explicitEnvByService = new Map([
|
|
356
|
-
['local-gateway', 'AMALGM_GATEWAY_PORT'],
|
|
357
|
-
['port-monitor', 'PORT_MONITOR_PORT'],
|
|
358
|
-
['fs-watcher', 'FS_WATCHER_PORT'],
|
|
359
|
-
['amalgm-mcp', 'AMALGM_MCP_PORT'],
|
|
360
|
-
['chat-server', 'CHAT_SERVER_PORT'],
|
|
361
|
-
]);
|
|
344
|
+
const explicitEnvByService = new Map(runtimeServices().map((service) => [service.name, service.envName]));
|
|
362
345
|
for (const [name, port] of servicePorts()) {
|
|
363
346
|
const envName = explicitEnvByService.get(name);
|
|
364
347
|
if (!envName || !process.env[envName]) continue;
|
|
@@ -846,6 +829,8 @@ async function start(options) {
|
|
|
846
829
|
await stop();
|
|
847
830
|
}
|
|
848
831
|
|
|
832
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
833
|
+
|
|
849
834
|
const blockedPorts = await unavailableServicePorts();
|
|
850
835
|
if (blockedPorts.length > 0) {
|
|
851
836
|
const detail = blockedPorts.map(([name, port]) => `${name} (${port})`).join(', ');
|
|
@@ -928,6 +913,7 @@ async function stop() {
|
|
|
928
913
|
} catch {
|
|
929
914
|
// noop
|
|
930
915
|
}
|
|
916
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
931
917
|
console.log('Amalgm is not running.');
|
|
932
918
|
return;
|
|
933
919
|
}
|
|
@@ -951,10 +937,11 @@ async function stop() {
|
|
|
951
937
|
} catch {
|
|
952
938
|
// noop
|
|
953
939
|
}
|
|
940
|
+
cleanupStaleRuntimeProcesses({ includeSupervisors: true });
|
|
954
941
|
console.log('Amalgm stopped.');
|
|
955
942
|
}
|
|
956
943
|
|
|
957
|
-
async function
|
|
944
|
+
async function checkHttpOnce(port, pathName = '/healthz', timeoutMs = 1500) {
|
|
958
945
|
return new Promise((resolve) => {
|
|
959
946
|
const req = http.request(
|
|
960
947
|
{
|
|
@@ -962,7 +949,7 @@ async function checkHttp(port, pathName = '/healthz') {
|
|
|
962
949
|
port,
|
|
963
950
|
path: pathName,
|
|
964
951
|
method: 'GET',
|
|
965
|
-
timeout:
|
|
952
|
+
timeout: timeoutMs,
|
|
966
953
|
},
|
|
967
954
|
(res) => {
|
|
968
955
|
res.resume();
|
|
@@ -978,6 +965,16 @@ async function checkHttp(port, pathName = '/healthz') {
|
|
|
978
965
|
});
|
|
979
966
|
}
|
|
980
967
|
|
|
968
|
+
async function checkHttp(port, pathName = '/healthz', options = {}) {
|
|
969
|
+
const attempts = Math.max(1, Number(options.attempts || 3));
|
|
970
|
+
const timeoutMs = Math.max(500, Number(options.timeoutMs || 1500));
|
|
971
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
972
|
+
if (await checkHttpOnce(port, pathName, timeoutMs)) return true;
|
|
973
|
+
if (attempt < attempts - 1) await sleep(150);
|
|
974
|
+
}
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
|
|
981
978
|
async function waitForServices(timeoutMs) {
|
|
982
979
|
const deadline = Date.now() + timeoutMs;
|
|
983
980
|
let latest = [];
|
|
@@ -1028,9 +1025,26 @@ async function status() {
|
|
|
1028
1025
|
}
|
|
1029
1026
|
console.log(`HMAC proxy: ${proxyTokenFromRecord(record) ? 'configured' : 'not configured'}`);
|
|
1030
1027
|
|
|
1028
|
+
const runtimeChildren = runtimeServiceProcesses();
|
|
1029
|
+
const runtimeSupervisors = supervisorProcesses();
|
|
1030
|
+
const expectedServiceCount = runtimeServiceNames().length;
|
|
1031
|
+
if (runtimeChildren.length > expectedServiceCount || runtimeSupervisors.length > 1) {
|
|
1032
|
+
console.log(
|
|
1033
|
+
`Warning: multiple Amalgm runtime processes detected (${runtimeSupervisors.length} supervisor, ${runtimeChildren.length} services). Run \`amalgm stop && amalgm start\`.`,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const serviceStateByName = new Map(
|
|
1038
|
+
(Array.isArray(runtimeState?.services) ? runtimeState.services : [])
|
|
1039
|
+
.map((service) => [service.name, service]),
|
|
1040
|
+
);
|
|
1031
1041
|
for (const [name, port] of servicePorts()) {
|
|
1032
1042
|
const ok = await checkHttp(port);
|
|
1033
|
-
|
|
1043
|
+
const serviceState = serviceStateByName.get(name);
|
|
1044
|
+
const detail = serviceState?.status && serviceState.status !== 'running'
|
|
1045
|
+
? ` (${serviceState.status}${serviceState.reason ? `: ${serviceState.reason}` : ''})`
|
|
1046
|
+
: '';
|
|
1047
|
+
console.log(`${name.padEnd(13)} ${ok ? 'up' : 'down'} http://127.0.0.1:${port}${detail}`);
|
|
1034
1048
|
}
|
|
1035
1049
|
}
|
|
1036
1050
|
|
|
@@ -1101,6 +1115,16 @@ async function doctor() {
|
|
|
1101
1115
|
);
|
|
1102
1116
|
}
|
|
1103
1117
|
add('daemon', daemonRunning, daemonRunning ? `pid ${pid}` : 'stopped', daemonRunning ? 'ok' : 'warn');
|
|
1118
|
+
const runtimeChildren = runtimeServiceProcesses();
|
|
1119
|
+
const runtimeSupervisors = supervisorProcesses();
|
|
1120
|
+
const expectedServices = daemonRunning ? runtimeServiceNames().length : 0;
|
|
1121
|
+
const cleanProcessShape = runtimeSupervisors.length <= 1 && runtimeChildren.length <= expectedServices;
|
|
1122
|
+
add(
|
|
1123
|
+
'runtime processes',
|
|
1124
|
+
cleanProcessShape,
|
|
1125
|
+
`${runtimeSupervisors.length} supervisor, ${runtimeChildren.length} services`,
|
|
1126
|
+
cleanProcessShape ? 'ok' : 'warn',
|
|
1127
|
+
);
|
|
1104
1128
|
if (lastShutdown?.signal || lastShutdown?.reason) {
|
|
1105
1129
|
add(
|
|
1106
1130
|
'last shutdown',
|
|
@@ -1109,15 +1133,25 @@ async function doctor() {
|
|
|
1109
1133
|
'warn',
|
|
1110
1134
|
);
|
|
1111
1135
|
}
|
|
1136
|
+
const runtimeServiceState = Array.isArray(readJson(RUNTIME_STATE_FILE, null)?.services)
|
|
1137
|
+
? readJson(RUNTIME_STATE_FILE, null).services
|
|
1138
|
+
: [];
|
|
1139
|
+
const unhealthyState = runtimeServiceState.filter((service) => (
|
|
1140
|
+
service.status && !['running', 'starting'].includes(service.status)
|
|
1141
|
+
));
|
|
1142
|
+
if (runtimeServiceState.length > 0) {
|
|
1143
|
+
add(
|
|
1144
|
+
'service states',
|
|
1145
|
+
unhealthyState.length === 0,
|
|
1146
|
+
unhealthyState.length === 0
|
|
1147
|
+
? `${runtimeServiceState.length} declared service(s) running/starting`
|
|
1148
|
+
: unhealthyState.map((service) => `${service.name}:${service.status}`).join(', '),
|
|
1149
|
+
unhealthyState.length === 0 ? 'ok' : 'warn',
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1112
1152
|
|
|
1113
1153
|
for (const [name, port] of servicePorts()) {
|
|
1114
|
-
const envName =
|
|
1115
|
-
'local-gateway': 'AMALGM_GATEWAY_PORT',
|
|
1116
|
-
'port-monitor': 'PORT_MONITOR_PORT',
|
|
1117
|
-
'fs-watcher': 'FS_WATCHER_PORT',
|
|
1118
|
-
'amalgm-mcp': 'AMALGM_MCP_PORT',
|
|
1119
|
-
'chat-server': 'CHAT_SERVER_PORT',
|
|
1120
|
-
}[name];
|
|
1154
|
+
const envName = runtimeServices().find((service) => service.name === name)?.envName;
|
|
1121
1155
|
const ok = daemonRunning ? await checkHttp(port) : (!process.env[envName] || await isPortFree(port));
|
|
1122
1156
|
add(
|
|
1123
1157
|
`${name} port`,
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
const {
|
|
13
|
+
runtimeServiceScripts,
|
|
14
|
+
} = require('./runtime-manifest');
|
|
15
|
+
|
|
16
|
+
const RUNTIME_SERVICE_MARKERS = runtimeServiceScripts()
|
|
17
|
+
.map((script) => path.join(RUNTIME_DIR, script).replace(/\\/g, '/'));
|
|
18
|
+
const RUNTIME_SERVICE_SUFFIXES = runtimeServiceScripts()
|
|
19
|
+
.map((script) => `runtime/${script}`.replace(/\\/g, '/'));
|
|
20
|
+
|
|
21
|
+
const SUPERVISOR_MARKER = path.join(PACKAGE_ROOT, 'bin', 'amalgm.js').replace(/\\/g, '/');
|
|
22
|
+
const SUPERVISOR_SUFFIX = '/bin/amalgm.js';
|
|
23
|
+
|
|
24
|
+
function isPidRunning(pid) {
|
|
25
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function waitForExit(pid, timeoutMs = 5000) {
|
|
35
|
+
const deadline = Date.now() + timeoutMs;
|
|
36
|
+
while (Date.now() < deadline && isPidRunning(pid)) {
|
|
37
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
38
|
+
}
|
|
39
|
+
return !isPidRunning(pid);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readProcString(file) {
|
|
43
|
+
try {
|
|
44
|
+
return fs.readFileSync(file).toString('utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readProcCommand(pid) {
|
|
51
|
+
const cmdline = readProcString(`/proc/${pid}/cmdline`).replace(/\0/g, ' ').trim();
|
|
52
|
+
if (cmdline) return cmdline;
|
|
53
|
+
try {
|
|
54
|
+
const result = spawnSync('ps', ['-p', String(pid), '-o', 'command='], {
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
timeout: 1000,
|
|
57
|
+
});
|
|
58
|
+
return String(result.stdout || '').trim();
|
|
59
|
+
} catch {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readProcPpid(pid) {
|
|
65
|
+
try {
|
|
66
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
67
|
+
const lastParen = stat.lastIndexOf(')');
|
|
68
|
+
if (lastParen === -1) return null;
|
|
69
|
+
const parts = stat.slice(lastParen + 2).trim().split(/\s+/);
|
|
70
|
+
const ppid = Number(parts[1]);
|
|
71
|
+
return Number.isInteger(ppid) ? ppid : null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readProcEnv(pid) {
|
|
78
|
+
const raw = readProcString(`/proc/${pid}/environ`);
|
|
79
|
+
if (!raw) return null;
|
|
80
|
+
const env = {};
|
|
81
|
+
for (const entry of raw.split('\0')) {
|
|
82
|
+
if (!entry) continue;
|
|
83
|
+
const index = entry.indexOf('=');
|
|
84
|
+
if (index <= 0) continue;
|
|
85
|
+
env[entry.slice(0, index)] = entry.slice(index + 1);
|
|
86
|
+
}
|
|
87
|
+
return env;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function listProcProcesses() {
|
|
91
|
+
try {
|
|
92
|
+
return fs.readdirSync('/proc')
|
|
93
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
94
|
+
.map((entry) => Number(entry))
|
|
95
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0)
|
|
96
|
+
.map((pid) => ({
|
|
97
|
+
pid,
|
|
98
|
+
ppid: readProcPpid(pid),
|
|
99
|
+
command: readProcCommand(pid),
|
|
100
|
+
env: readProcEnv(pid),
|
|
101
|
+
}))
|
|
102
|
+
.filter((item) => item.command);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function listPsProcesses() {
|
|
109
|
+
try {
|
|
110
|
+
const result = spawnSync('ps', ['-eo', 'pid=,ppid=,command='], {
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
timeout: 2000,
|
|
113
|
+
});
|
|
114
|
+
if (result.status !== 0) return [];
|
|
115
|
+
return String(result.stdout || '')
|
|
116
|
+
.split('\n')
|
|
117
|
+
.map((line) => {
|
|
118
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
119
|
+
if (!match) return null;
|
|
120
|
+
return {
|
|
121
|
+
pid: Number(match[1]),
|
|
122
|
+
ppid: Number(match[2]),
|
|
123
|
+
command: match[3],
|
|
124
|
+
env: null,
|
|
125
|
+
};
|
|
126
|
+
})
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function listProcesses() {
|
|
134
|
+
const processes = listProcProcesses() || listPsProcesses();
|
|
135
|
+
const commandByPid = new Map(processes.map((item) => [item.pid, item.command]));
|
|
136
|
+
return processes.map((item) => ({
|
|
137
|
+
...item,
|
|
138
|
+
parentCommand: commandByPid.get(item.ppid) || readProcCommand(item.ppid),
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizedCommand(command) {
|
|
143
|
+
return String(command || '').replace(/\\/g, '/');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function envMatchesAmalgmDir(env) {
|
|
147
|
+
if (!env || !env.AMALGM_DIR) return true;
|
|
148
|
+
return path.resolve(env.AMALGM_DIR) === path.resolve(AMALGM_DIR);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function commandLooksLikeRuntimeService(command, item = null, options = {}) {
|
|
152
|
+
const normalized = normalizedCommand(command);
|
|
153
|
+
if (RUNTIME_SERVICE_MARKERS.some((marker) => normalized.includes(marker))) return true;
|
|
154
|
+
if (!RUNTIME_SERVICE_SUFFIXES.some((marker) => normalized.includes(marker))) return false;
|
|
155
|
+
if (item?.env && envMatchesAmalgmDir(item.env)) return true;
|
|
156
|
+
if (!options.includeOrphanedForeign) return false;
|
|
157
|
+
const parent = normalizedCommand(item?.parentCommand || '');
|
|
158
|
+
return !commandLooksLikeAnySupervisor(parent);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function commandLooksLikeSupervisor(command) {
|
|
162
|
+
const normalized = normalizedCommand(command);
|
|
163
|
+
return normalized.includes(SUPERVISOR_MARKER) && /\brun\b/.test(normalized);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function commandLooksLikeAnySupervisor(command) {
|
|
167
|
+
const normalized = normalizedCommand(command);
|
|
168
|
+
return normalized.includes(SUPERVISOR_SUFFIX) && /\brun\b/.test(normalized);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function runtimeServiceProcesses(options = {}) {
|
|
172
|
+
const exclude = new Set([process.pid, ...(options.excludePids || [])].filter(Number.isInteger));
|
|
173
|
+
return listProcesses()
|
|
174
|
+
.filter((item) => !exclude.has(item.pid))
|
|
175
|
+
.filter((item) => commandLooksLikeRuntimeService(item.command, item, options))
|
|
176
|
+
.filter((item) => envMatchesAmalgmDir(item.env));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function supervisorProcesses(options = {}) {
|
|
180
|
+
const exclude = new Set([process.pid, ...(options.excludePids || [])].filter(Number.isInteger));
|
|
181
|
+
return listProcesses()
|
|
182
|
+
.filter((item) => !exclude.has(item.pid))
|
|
183
|
+
.filter((item) => commandLooksLikeSupervisor(item.command))
|
|
184
|
+
.filter((item) => envMatchesAmalgmDir(item.env));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function terminateProcesses(processes, options = {}) {
|
|
188
|
+
const logger = options.logger || null;
|
|
189
|
+
const unique = [...new Map(processes.map((item) => [item.pid, item])).values()]
|
|
190
|
+
.filter((item) => isPidRunning(item.pid));
|
|
191
|
+
if (unique.length === 0) return [];
|
|
192
|
+
|
|
193
|
+
for (const item of unique) {
|
|
194
|
+
try {
|
|
195
|
+
process.kill(item.pid, 'SIGTERM');
|
|
196
|
+
} catch {
|
|
197
|
+
// It may have exited between discovery and shutdown.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const survivors = [];
|
|
202
|
+
for (const item of unique) {
|
|
203
|
+
if (!waitForExit(item.pid, options.termTimeoutMs || 3000)) survivors.push(item);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const item of survivors) {
|
|
207
|
+
try {
|
|
208
|
+
process.kill(item.pid, 'SIGKILL');
|
|
209
|
+
} catch {
|
|
210
|
+
// noop
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const item of survivors) waitForExit(item.pid, options.killTimeoutMs || 1500);
|
|
214
|
+
|
|
215
|
+
const stopped = unique.filter((item) => !isPidRunning(item.pid));
|
|
216
|
+
if (stopped.length > 0 && logger?.log) {
|
|
217
|
+
logger.log(`cleaned stale Amalgm runtime process(es): ${stopped.map((item) => item.pid).join(', ')}`);
|
|
218
|
+
}
|
|
219
|
+
return stopped;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function cleanupStaleRuntimeProcesses(options = {}) {
|
|
223
|
+
const stale = runtimeServiceProcesses({
|
|
224
|
+
includeOrphanedForeign: true,
|
|
225
|
+
...options,
|
|
226
|
+
});
|
|
227
|
+
const supervisors = options.includeSupervisors ? supervisorProcesses(options) : [];
|
|
228
|
+
return terminateProcesses([...stale, ...supervisors], options);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
cleanupStaleRuntimeProcesses,
|
|
233
|
+
commandLooksLikeRuntimeService,
|
|
234
|
+
commandLooksLikeSupervisor,
|
|
235
|
+
isPidRunning,
|
|
236
|
+
runtimeServiceProcesses,
|
|
237
|
+
supervisorProcesses,
|
|
238
|
+
terminateProcesses,
|
|
239
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const RUNTIME_SERVICES = [
|
|
4
|
+
{
|
|
5
|
+
name: 'local-gateway',
|
|
6
|
+
script: 'scripts/local-gateway.js',
|
|
7
|
+
portKey: 'gateway',
|
|
8
|
+
envName: 'AMALGM_GATEWAY_PORT',
|
|
9
|
+
stateKey: 'gateway',
|
|
10
|
+
defaultPort: 28781,
|
|
11
|
+
launchOrder: 50,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'port-monitor',
|
|
15
|
+
script: 'scripts/port-monitor.js',
|
|
16
|
+
portKey: 'portMonitor',
|
|
17
|
+
envName: 'PORT_MONITOR_PORT',
|
|
18
|
+
stateKey: 'port_monitor',
|
|
19
|
+
defaultPort: 8081,
|
|
20
|
+
launchOrder: 10,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'fs-watcher',
|
|
24
|
+
script: 'scripts/fs-watcher.js',
|
|
25
|
+
portKey: 'fsWatcher',
|
|
26
|
+
envName: 'FS_WATCHER_PORT',
|
|
27
|
+
stateKey: 'fs_watcher',
|
|
28
|
+
defaultPort: 8082,
|
|
29
|
+
launchOrder: 20,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'amalgm-mcp',
|
|
33
|
+
script: 'scripts/amalgm-mcp/index.js',
|
|
34
|
+
portKey: 'amalgmMcp',
|
|
35
|
+
envName: 'AMALGM_MCP_PORT',
|
|
36
|
+
stateKey: 'amalgm_mcp',
|
|
37
|
+
defaultPort: 8083,
|
|
38
|
+
launchOrder: 40,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'chat-server',
|
|
42
|
+
script: 'scripts/chat-server.js',
|
|
43
|
+
portKey: 'chatServer',
|
|
44
|
+
envName: 'CHAT_SERVER_PORT',
|
|
45
|
+
stateKey: 'chat_server',
|
|
46
|
+
defaultPort: 8084,
|
|
47
|
+
launchOrder: 30,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function runtimeServices() {
|
|
52
|
+
return RUNTIME_SERVICES.map((service) => ({ ...service }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runtimeLaunchServices() {
|
|
56
|
+
return runtimeServices().sort((left, right) => left.launchOrder - right.launchOrder);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runtimeServiceNames() {
|
|
60
|
+
return RUNTIME_SERVICES.map((service) => service.name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function runtimeServiceScripts() {
|
|
64
|
+
return RUNTIME_SERVICES.map((service) => service.script);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runtimeServiceByName(name) {
|
|
68
|
+
return RUNTIME_SERVICES.find((service) => service.name === name) || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runtimePortsFromState(state) {
|
|
72
|
+
const statePorts = state?.ports || {};
|
|
73
|
+
const values = {};
|
|
74
|
+
for (const service of RUNTIME_SERVICES) {
|
|
75
|
+
if (service.stateKey === 'gateway' && Number.isInteger(state?.gateway_port)) {
|
|
76
|
+
values[service.name] = Number(state.gateway_port);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const port = Number(statePorts[service.stateKey] || 0);
|
|
80
|
+
if (Number.isInteger(port) && port > 0) values[service.name] = port;
|
|
81
|
+
}
|
|
82
|
+
return values;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runtimePortsFromEnv(env = process.env) {
|
|
86
|
+
const values = {};
|
|
87
|
+
for (const service of RUNTIME_SERVICES) {
|
|
88
|
+
const port = Number(env[service.envName] || service.defaultPort);
|
|
89
|
+
if (Number.isInteger(port) && port > 0) values[service.name] = port;
|
|
90
|
+
}
|
|
91
|
+
return values;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runtimePortsForStatus(state, env = process.env) {
|
|
95
|
+
const fromState = runtimePortsFromState(state);
|
|
96
|
+
const fromEnv = runtimePortsFromEnv(env);
|
|
97
|
+
return RUNTIME_SERVICES
|
|
98
|
+
.map((service) => [service.name, fromState[service.name] || fromEnv[service.name]])
|
|
99
|
+
.filter(([, port]) => Number.isInteger(port) && port > 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runtimePortsState(ports) {
|
|
103
|
+
const statePorts = {};
|
|
104
|
+
for (const service of RUNTIME_SERVICES) {
|
|
105
|
+
const port = Number(ports?.[service.portKey] || 0);
|
|
106
|
+
if (!Number.isInteger(port) || port <= 0) continue;
|
|
107
|
+
if (service.stateKey !== 'gateway') statePorts[service.stateKey] = port;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
gateway_port: Number(ports?.gateway || 0) || undefined,
|
|
111
|
+
gateway_url: ports?.gateway ? `http://127.0.0.1:${ports.gateway}` : undefined,
|
|
112
|
+
ports: statePorts,
|
|
113
|
+
services: RUNTIME_SERVICES.map((service) => ({
|
|
114
|
+
name: service.name,
|
|
115
|
+
port: Number(ports?.[service.portKey] || 0) || null,
|
|
116
|
+
health_path: '/healthz',
|
|
117
|
+
})),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
runtimeLaunchServices,
|
|
123
|
+
runtimePortsForStatus,
|
|
124
|
+
runtimePortsFromEnv,
|
|
125
|
+
runtimePortsFromState,
|
|
126
|
+
runtimePortsState,
|
|
127
|
+
runtimeServiceByName,
|
|
128
|
+
runtimeServiceNames,
|
|
129
|
+
runtimeServiceScripts,
|
|
130
|
+
runtimeServices,
|
|
131
|
+
};
|