nomoreide 0.1.15 → 0.1.17
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 +20 -1
- package/dist/cli/commands.js +44 -9
- package/dist/cli/commands.js.map +1 -1
- package/dist/core/agent-context.d.ts +9 -0
- package/dist/core/agent-context.js +43 -0
- package/dist/core/agent-context.js.map +1 -0
- package/dist/core/config-store.js +29 -3
- package/dist/core/config-store.js.map +1 -1
- package/dist/core/docker-service-runner.d.ts +21 -0
- package/dist/core/docker-service-runner.js +80 -0
- package/dist/core/docker-service-runner.js.map +1 -0
- package/dist/core/git-manager.js +1 -1
- package/dist/core/git-manager.js.map +1 -1
- package/dist/core/http-inspector.d.ts +20 -0
- package/dist/core/http-inspector.js +106 -0
- package/dist/core/http-inspector.js.map +1 -0
- package/dist/core/log-store.d.ts +4 -0
- package/dist/core/log-store.js +34 -0
- package/dist/core/log-store.js.map +1 -1
- package/dist/core/port-utils.d.ts +6 -0
- package/dist/core/port-utils.js +44 -0
- package/dist/core/port-utils.js.map +1 -1
- package/dist/core/process-manager.d.ts +28 -2
- package/dist/core/process-manager.js +367 -14
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/service-health.d.ts +2 -1
- package/dist/core/service-health.js +9 -1
- package/dist/core/service-health.js.map +1 -1
- package/dist/core/ssh-service-runner.d.ts +7 -0
- package/dist/core/ssh-service-runner.js +31 -0
- package/dist/core/ssh-service-runner.js.map +1 -0
- package/dist/core/timeline-store.d.ts +16 -0
- package/dist/core/timeline-store.js +28 -0
- package/dist/core/timeline-store.js.map +1 -0
- package/dist/core/tool-call-store.d.ts +22 -0
- package/dist/core/tool-call-store.js +53 -0
- package/dist/core/tool-call-store.js.map +1 -0
- package/dist/core/types.d.ts +28 -3
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +18 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +5 -1
- package/dist/mcp/tools.js +133 -4
- package/dist/mcp/tools.js.map +1 -1
- package/dist/web/agent-info.d.ts +45 -0
- package/dist/web/agent-info.js +272 -0
- package/dist/web/agent-info.js.map +1 -0
- package/dist/web/client/assets/index-Bu23xSpP.css +1 -0
- package/dist/web/client/assets/index-CrlXHlUf.js +14 -0
- package/dist/web/client/index.html +2 -2
- package/dist/web/dashboard.d.ts +4 -1
- package/dist/web/dashboard.js +4 -0
- package/dist/web/dashboard.js.map +1 -1
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +137 -18
- package/dist/web/server.js.map +1 -1
- package/dist/web/ui-lifecycle.d.ts +2 -0
- package/dist/web/ui-lifecycle.js +1 -0
- package/dist/web/ui-lifecycle.js.map +1 -1
- package/dist/web/usage-info.d.ts +45 -0
- package/dist/web/usage-info.js +227 -0
- package/dist/web/usage-info.js.map +1 -0
- package/package.json +1 -1
- package/dist/web/client/assets/index-BHl3VycR.js +0 -12
- package/dist/web/client/assets/index-BbvMPKsZ.css +0 -1
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export interface PortHolder {
|
|
2
|
+
pid: number;
|
|
3
|
+
pgid?: number;
|
|
4
|
+
command: string;
|
|
5
|
+
}
|
|
1
6
|
export interface PortStatus {
|
|
2
7
|
port: number;
|
|
3
8
|
available: boolean;
|
|
@@ -15,3 +20,4 @@ export interface PortBindingStatus {
|
|
|
15
20
|
export declare function isPortAvailable(port: number, host?: string): Promise<boolean>;
|
|
16
21
|
export declare function getPortStatus(port: number): Promise<PortStatus>;
|
|
17
22
|
export declare function getPortBindingStatus(port: number): Promise<PortBindingStatus>;
|
|
23
|
+
export declare function getPortHolder(port: number): Promise<PortHolder | null>;
|
package/dist/core/port-utils.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
1
2
|
import net from "node:net";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
const execAsync = promisify(exec);
|
|
2
5
|
export async function isPortAvailable(port, host = "127.0.0.1") {
|
|
3
6
|
return (await checkHostPort(port, host)).available;
|
|
4
7
|
}
|
|
@@ -21,6 +24,47 @@ export async function getPortBindingStatus(port) {
|
|
|
21
24
|
hosts: statuses,
|
|
22
25
|
};
|
|
23
26
|
}
|
|
27
|
+
export async function getPortHolder(port) {
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execAsync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, {
|
|
30
|
+
timeout: 1500,
|
|
31
|
+
});
|
|
32
|
+
const pid = Number(stdout.trim().split(/\s+/)[0]);
|
|
33
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
pid,
|
|
37
|
+
pgid: await readPgid(pid),
|
|
38
|
+
command: await readCommand(pid),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function readPgid(pid) {
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await execAsync(`ps -o pgid= -p ${pid}`, {
|
|
48
|
+
timeout: 1000,
|
|
49
|
+
});
|
|
50
|
+
const pgid = Number(stdout.trim());
|
|
51
|
+
return Number.isFinite(pgid) && pgid > 0 ? pgid : undefined;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function readCommand(pid) {
|
|
58
|
+
try {
|
|
59
|
+
const { stdout } = await execAsync(`ps -o command= -p ${pid}`, {
|
|
60
|
+
timeout: 1000,
|
|
61
|
+
});
|
|
62
|
+
return stdout.trim().split("\n")[0] || `pid ${pid}`;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return `pid ${pid}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
24
68
|
async function checkHostPort(port, host) {
|
|
25
69
|
return new Promise((resolve, reject) => {
|
|
26
70
|
const server = net.createServer();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"port-utils.js","sourceRoot":"","sources":["../../src/core/port-utils.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"port-utils.js","sourceRoot":"","sources":["../../src/core/port-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,31 +1,57 @@
|
|
|
1
1
|
import type { ConfigStore } from "./config-store.js";
|
|
2
2
|
import type { LogStore } from "./log-store.js";
|
|
3
|
+
import { type PortHolder } from "./port-utils.js";
|
|
4
|
+
import type { TimelineStore } from "./timeline-store.js";
|
|
3
5
|
import type { ServiceStatus } from "./types.js";
|
|
4
6
|
interface ProcessManagerOptions {
|
|
5
7
|
configStore: ConfigStore;
|
|
6
8
|
logStore: LogStore;
|
|
7
9
|
stopTimeoutMs?: number;
|
|
10
|
+
timelineStore?: TimelineStore;
|
|
8
11
|
}
|
|
9
12
|
export interface NoMoreIdeStatus {
|
|
10
13
|
services: Record<string, ServiceStatus>;
|
|
11
14
|
}
|
|
15
|
+
export interface StartServiceOptions {
|
|
16
|
+
killHolder?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare class PortConflictError extends Error {
|
|
19
|
+
readonly code = "PORT_IN_USE";
|
|
20
|
+
readonly service: string;
|
|
21
|
+
readonly port: number;
|
|
22
|
+
readonly holder: PortHolder | null;
|
|
23
|
+
constructor(service: string, port: number, holder: PortHolder | null);
|
|
24
|
+
}
|
|
12
25
|
export declare class ProcessManager {
|
|
13
26
|
private readonly runtimes;
|
|
14
27
|
private readonly configStore;
|
|
15
28
|
private readonly logStore;
|
|
16
29
|
private readonly stopTimeoutMs;
|
|
30
|
+
private readonly timelineStore?;
|
|
17
31
|
constructor(options: ProcessManagerOptions);
|
|
18
|
-
startService(name: string): Promise<ServiceStatus>;
|
|
32
|
+
startService(name: string, options?: StartServiceOptions): Promise<ServiceStatus>;
|
|
19
33
|
stopService(name: string): Promise<ServiceStatus>;
|
|
20
|
-
restartService(name: string): Promise<ServiceStatus>;
|
|
34
|
+
restartService(name: string, options?: StartServiceOptions): Promise<ServiceStatus>;
|
|
35
|
+
setInspectorEnabled(name: string, enabled: boolean): Promise<ServiceStatus>;
|
|
36
|
+
private maybeStartInspector;
|
|
37
|
+
private stopInspector;
|
|
38
|
+
private inspectorStatus;
|
|
39
|
+
private recordInspectorEvent;
|
|
21
40
|
startBundle(name: string): Promise<ServiceStatus[]>;
|
|
22
41
|
stopBundle(name: string): Promise<ServiceStatus[]>;
|
|
23
42
|
restartBundle(name: string): Promise<ServiceStatus[]>;
|
|
24
43
|
stopAll(): Promise<void>;
|
|
44
|
+
killAllSync(signal?: NodeJS.Signals): void;
|
|
45
|
+
installShutdownHandlers(): () => void;
|
|
25
46
|
status(): NoMoreIdeStatus;
|
|
26
47
|
statusWithResources(): Promise<NoMoreIdeStatus>;
|
|
48
|
+
private startDockerComposeService;
|
|
49
|
+
private stopDockerComposeService;
|
|
50
|
+
readDockerServiceLogs(name: string, tail?: number): Promise<string>;
|
|
51
|
+
readDockerServiceStatus(name: string): Promise<import("./docker-service-runner.js").DockerContainerInfo>;
|
|
27
52
|
private getService;
|
|
28
53
|
private getBundle;
|
|
29
54
|
private captureStream;
|
|
55
|
+
private appendTimeline;
|
|
30
56
|
}
|
|
31
57
|
export {};
|
|
@@ -1,53 +1,103 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { readDockerServiceLogs, readDockerServiceStatus, startDockerService, stopDockerService, } from "./docker-service-runner.js";
|
|
3
|
+
import { startHttpInspector, } from "./http-inspector.js";
|
|
4
|
+
import { getPortHolder, isPortAvailable } from "./port-utils.js";
|
|
3
5
|
import { readProcessTree } from "./process-tree.js";
|
|
6
|
+
import { createSshCommand } from "./ssh-service-runner.js";
|
|
7
|
+
export class PortConflictError extends Error {
|
|
8
|
+
code = "PORT_IN_USE";
|
|
9
|
+
service;
|
|
10
|
+
port;
|
|
11
|
+
holder;
|
|
12
|
+
constructor(service, port, holder) {
|
|
13
|
+
const owner = holder ? ` (held by pid ${holder.pid} — ${holder.command})` : "";
|
|
14
|
+
super(`Port ${port} is already in use for ${service}${owner}.`);
|
|
15
|
+
this.name = "PortConflictError";
|
|
16
|
+
this.service = service;
|
|
17
|
+
this.port = port;
|
|
18
|
+
this.holder = holder;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
4
21
|
export class ProcessManager {
|
|
5
22
|
runtimes = new Map();
|
|
6
23
|
configStore;
|
|
7
24
|
logStore;
|
|
8
25
|
stopTimeoutMs;
|
|
26
|
+
timelineStore;
|
|
9
27
|
constructor(options) {
|
|
10
28
|
this.configStore = options.configStore;
|
|
11
29
|
this.logStore = options.logStore;
|
|
12
30
|
this.stopTimeoutMs = options.stopTimeoutMs ?? 3000;
|
|
31
|
+
this.timelineStore = options.timelineStore;
|
|
13
32
|
}
|
|
14
|
-
async startService(name) {
|
|
33
|
+
async startService(name, options = {}) {
|
|
15
34
|
const service = await this.getService(name);
|
|
16
35
|
const existing = this.runtimes.get(name);
|
|
17
36
|
if (existing?.status.state === "running") {
|
|
18
37
|
return { ...existing.status };
|
|
19
38
|
}
|
|
39
|
+
const kind = resolveKind(service);
|
|
40
|
+
if (kind === "docker-compose") {
|
|
41
|
+
return this.startDockerComposeService(name, service);
|
|
42
|
+
}
|
|
20
43
|
if (service.port && !(await isPortAvailable(service.port))) {
|
|
21
|
-
|
|
44
|
+
const holder = await getPortHolder(service.port);
|
|
45
|
+
if (options.killHolder && holder) {
|
|
46
|
+
await killHolder(holder);
|
|
47
|
+
if (!(await waitForPortFree(service.port, 3000))) {
|
|
48
|
+
throw new PortConflictError(name, service.port, holder);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
throw new PortConflictError(name, service.port, holder);
|
|
53
|
+
}
|
|
22
54
|
}
|
|
23
|
-
const child =
|
|
24
|
-
cwd: service.cwd,
|
|
25
|
-
env: { ...process.env, ...service.env },
|
|
26
|
-
shell: true,
|
|
27
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
-
});
|
|
55
|
+
const child = spawnService(name, service, kind);
|
|
29
56
|
const runtime = {
|
|
30
57
|
child,
|
|
31
58
|
stopping: false,
|
|
32
59
|
status: {
|
|
33
60
|
name,
|
|
34
61
|
state: "running",
|
|
62
|
+
kind,
|
|
63
|
+
host: kind === "ssh" ? service.host : undefined,
|
|
35
64
|
pid: child.pid,
|
|
36
65
|
startedAt: new Date().toISOString(),
|
|
37
66
|
},
|
|
38
67
|
};
|
|
39
68
|
this.runtimes.set(name, runtime);
|
|
69
|
+
void this.appendTimeline({
|
|
70
|
+
kind: "service.lifecycle",
|
|
71
|
+
service: name,
|
|
72
|
+
severity: "info",
|
|
73
|
+
title: `${name} started`,
|
|
74
|
+
data: {
|
|
75
|
+
pid: child.pid,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
40
78
|
this.captureStream(name, "stdout", child.stdout);
|
|
41
79
|
this.captureStream(name, "stderr", child.stderr);
|
|
42
80
|
child.once("exit", (exitCode, signal) => {
|
|
81
|
+
const nextState = runtime.stopping ? "stopped" : "exited";
|
|
43
82
|
runtime.status = {
|
|
44
83
|
...runtime.status,
|
|
45
|
-
state:
|
|
84
|
+
state: nextState,
|
|
46
85
|
exitedAt: new Date().toISOString(),
|
|
47
86
|
exitCode,
|
|
48
87
|
signal,
|
|
49
88
|
};
|
|
50
89
|
runtime.child = undefined;
|
|
90
|
+
void this.stopInspector(runtime);
|
|
91
|
+
void this.appendTimeline({
|
|
92
|
+
kind: "service.lifecycle",
|
|
93
|
+
service: name,
|
|
94
|
+
severity: nextState === "exited" && exitCode ? "error" : "info",
|
|
95
|
+
title: `${name} ${nextState}`,
|
|
96
|
+
data: {
|
|
97
|
+
exitCode,
|
|
98
|
+
signal,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
51
101
|
});
|
|
52
102
|
child.once("error", (error) => {
|
|
53
103
|
runtime.status = {
|
|
@@ -58,14 +108,30 @@ export class ProcessManager {
|
|
|
58
108
|
signal: null,
|
|
59
109
|
};
|
|
60
110
|
void this.logStore.append(name, "stderr", error.message);
|
|
111
|
+
void this.appendTimeline({
|
|
112
|
+
kind: "service.lifecycle",
|
|
113
|
+
service: name,
|
|
114
|
+
severity: "error",
|
|
115
|
+
title: `${name} failed`,
|
|
116
|
+
detail: error.message,
|
|
117
|
+
});
|
|
61
118
|
});
|
|
62
119
|
return { ...runtime.status };
|
|
63
120
|
}
|
|
64
121
|
async stopService(name) {
|
|
65
122
|
const runtime = this.runtimes.get(name);
|
|
123
|
+
if (runtime?.status.kind === "docker-compose" && runtime.status.state === "running") {
|
|
124
|
+
return this.stopDockerComposeService(name, runtime);
|
|
125
|
+
}
|
|
66
126
|
if (!runtime?.child || runtime.status.state !== "running") {
|
|
67
127
|
const stopped = { name, state: "stopped" };
|
|
68
128
|
this.runtimes.set(name, { status: stopped, stopping: false });
|
|
129
|
+
void this.appendTimeline({
|
|
130
|
+
kind: "service.lifecycle",
|
|
131
|
+
service: name,
|
|
132
|
+
severity: "info",
|
|
133
|
+
title: `${name} stopped`,
|
|
134
|
+
});
|
|
69
135
|
return stopped;
|
|
70
136
|
}
|
|
71
137
|
runtime.stopping = true;
|
|
@@ -77,11 +143,95 @@ export class ProcessManager {
|
|
|
77
143
|
};
|
|
78
144
|
runtime.status = stopped;
|
|
79
145
|
runtime.child = undefined;
|
|
146
|
+
void this.appendTimeline({
|
|
147
|
+
kind: "service.lifecycle",
|
|
148
|
+
service: name,
|
|
149
|
+
severity: "info",
|
|
150
|
+
title: `${name} stopped`,
|
|
151
|
+
});
|
|
80
152
|
return { ...stopped };
|
|
81
153
|
}
|
|
82
|
-
async restartService(name) {
|
|
154
|
+
async restartService(name, options = {}) {
|
|
83
155
|
await this.stopService(name);
|
|
84
|
-
return this.startService(name);
|
|
156
|
+
return this.startService(name, options);
|
|
157
|
+
}
|
|
158
|
+
async setInspectorEnabled(name, enabled) {
|
|
159
|
+
const runtime = this.runtimes.get(name);
|
|
160
|
+
if (!runtime) {
|
|
161
|
+
throw new Error(`Service "${name}" is not running.`);
|
|
162
|
+
}
|
|
163
|
+
runtime.inspectorEnabled = enabled;
|
|
164
|
+
if (enabled) {
|
|
165
|
+
await this.maybeStartInspector(name);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
await this.stopInspector(runtime);
|
|
169
|
+
}
|
|
170
|
+
runtime.status = {
|
|
171
|
+
...runtime.status,
|
|
172
|
+
inspector: this.inspectorStatus(runtime),
|
|
173
|
+
};
|
|
174
|
+
return { ...runtime.status };
|
|
175
|
+
}
|
|
176
|
+
async maybeStartInspector(name) {
|
|
177
|
+
const runtime = this.runtimes.get(name);
|
|
178
|
+
if (!runtime || !runtime.inspectorEnabled || runtime.inspectorHandle)
|
|
179
|
+
return;
|
|
180
|
+
const upstreamPort = portFromUrl(runtime.status.url);
|
|
181
|
+
if (!upstreamPort)
|
|
182
|
+
return;
|
|
183
|
+
const handle = await startHttpInspector({
|
|
184
|
+
upstreamPort,
|
|
185
|
+
onEvent: (event) => this.recordInspectorEvent(name, event),
|
|
186
|
+
});
|
|
187
|
+
runtime.inspectorHandle = handle;
|
|
188
|
+
runtime.status = {
|
|
189
|
+
...runtime.status,
|
|
190
|
+
inspector: { enabled: true, port: handle.port, upstreamPort },
|
|
191
|
+
};
|
|
192
|
+
void this.appendTimeline({
|
|
193
|
+
kind: "service.lifecycle",
|
|
194
|
+
service: name,
|
|
195
|
+
severity: "info",
|
|
196
|
+
title: `${name} HTTP inspector on :${handle.port}`,
|
|
197
|
+
detail: `Proxying to upstream :${upstreamPort}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async stopInspector(runtime) {
|
|
201
|
+
if (!runtime.inspectorHandle)
|
|
202
|
+
return;
|
|
203
|
+
const handle = runtime.inspectorHandle;
|
|
204
|
+
runtime.inspectorHandle = undefined;
|
|
205
|
+
await handle.stop();
|
|
206
|
+
}
|
|
207
|
+
inspectorStatus(runtime) {
|
|
208
|
+
if (!runtime.inspectorEnabled)
|
|
209
|
+
return undefined;
|
|
210
|
+
return {
|
|
211
|
+
enabled: true,
|
|
212
|
+
port: runtime.inspectorHandle?.port,
|
|
213
|
+
upstreamPort: portFromUrl(runtime.status.url),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
recordInspectorEvent(name, event) {
|
|
217
|
+
const severity = event.status >= 500 ? "error" : event.status >= 400 ? "warning" : "info";
|
|
218
|
+
void this.appendTimeline({
|
|
219
|
+
kind: "service.http",
|
|
220
|
+
service: name,
|
|
221
|
+
severity,
|
|
222
|
+
title: `${event.method} ${event.path} → ${event.status}`,
|
|
223
|
+
detail: `${event.durationMs} ms`,
|
|
224
|
+
timestamp: event.startedAt,
|
|
225
|
+
data: {
|
|
226
|
+
id: event.id,
|
|
227
|
+
method: event.method,
|
|
228
|
+
path: event.path,
|
|
229
|
+
status: event.status,
|
|
230
|
+
durationMs: event.durationMs,
|
|
231
|
+
reqBytes: event.reqBytes,
|
|
232
|
+
resBytes: event.resBytes,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
85
235
|
}
|
|
86
236
|
async startBundle(name) {
|
|
87
237
|
const bundle = await this.getBundle(name);
|
|
@@ -106,6 +256,38 @@ export class ProcessManager {
|
|
|
106
256
|
async stopAll() {
|
|
107
257
|
await Promise.all([...this.runtimes.keys()].map((name) => this.stopService(name)));
|
|
108
258
|
}
|
|
259
|
+
killAllSync(signal = "SIGTERM") {
|
|
260
|
+
for (const runtime of this.runtimes.values()) {
|
|
261
|
+
if (!runtime.child || runtime.child.exitCode !== null)
|
|
262
|
+
continue;
|
|
263
|
+
signalProcessTree(runtime.child, signal);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
installShutdownHandlers() {
|
|
267
|
+
let firing = false;
|
|
268
|
+
const handle = (signal) => () => {
|
|
269
|
+
if (firing)
|
|
270
|
+
return;
|
|
271
|
+
firing = true;
|
|
272
|
+
this.killAllSync("SIGTERM");
|
|
273
|
+
// Re-raise the signal with default behavior so the host exits.
|
|
274
|
+
setTimeout(() => process.kill(process.pid, signal), 50);
|
|
275
|
+
};
|
|
276
|
+
const onExit = () => this.killAllSync("SIGTERM");
|
|
277
|
+
const sigint = handle("SIGINT");
|
|
278
|
+
const sigterm = handle("SIGTERM");
|
|
279
|
+
const sighup = handle("SIGHUP");
|
|
280
|
+
process.once("SIGINT", sigint);
|
|
281
|
+
process.once("SIGTERM", sigterm);
|
|
282
|
+
process.once("SIGHUP", sighup);
|
|
283
|
+
process.once("exit", onExit);
|
|
284
|
+
return () => {
|
|
285
|
+
process.removeListener("SIGINT", sigint);
|
|
286
|
+
process.removeListener("SIGTERM", sigterm);
|
|
287
|
+
process.removeListener("SIGHUP", sighup);
|
|
288
|
+
process.removeListener("exit", onExit);
|
|
289
|
+
};
|
|
290
|
+
}
|
|
109
291
|
status() {
|
|
110
292
|
return {
|
|
111
293
|
services: Object.fromEntries([...this.runtimes.entries()].map(([name, runtime]) => [
|
|
@@ -121,10 +303,63 @@ export class ProcessManager {
|
|
|
121
303
|
if (status.pid && status.state === "running") {
|
|
122
304
|
status.processTree = await readProcessTree(status.pid);
|
|
123
305
|
}
|
|
306
|
+
status.inspector = this.inspectorStatus(runtime);
|
|
124
307
|
services[name] = status;
|
|
125
308
|
}
|
|
126
309
|
return { services };
|
|
127
310
|
}
|
|
311
|
+
async startDockerComposeService(name, service) {
|
|
312
|
+
const target = toDockerTarget(name, service);
|
|
313
|
+
const info = await startDockerService(target);
|
|
314
|
+
const status = {
|
|
315
|
+
name,
|
|
316
|
+
state: "running",
|
|
317
|
+
kind: "docker-compose",
|
|
318
|
+
startedAt: new Date().toISOString(),
|
|
319
|
+
containerId: info.containerId,
|
|
320
|
+
};
|
|
321
|
+
this.runtimes.set(name, { status, stopping: false });
|
|
322
|
+
void this.appendTimeline({
|
|
323
|
+
kind: "service.lifecycle",
|
|
324
|
+
service: name,
|
|
325
|
+
severity: "info",
|
|
326
|
+
title: `${name} started`,
|
|
327
|
+
data: { containerId: info.containerId },
|
|
328
|
+
});
|
|
329
|
+
return { ...status };
|
|
330
|
+
}
|
|
331
|
+
async stopDockerComposeService(name, runtime) {
|
|
332
|
+
const service = await this.getService(name);
|
|
333
|
+
runtime.stopping = true;
|
|
334
|
+
await stopDockerService(toDockerTarget(name, service));
|
|
335
|
+
const stopped = {
|
|
336
|
+
...runtime.status,
|
|
337
|
+
state: "stopped",
|
|
338
|
+
exitedAt: new Date().toISOString(),
|
|
339
|
+
};
|
|
340
|
+
runtime.status = stopped;
|
|
341
|
+
void this.appendTimeline({
|
|
342
|
+
kind: "service.lifecycle",
|
|
343
|
+
service: name,
|
|
344
|
+
severity: "info",
|
|
345
|
+
title: `${name} stopped`,
|
|
346
|
+
});
|
|
347
|
+
return { ...stopped };
|
|
348
|
+
}
|
|
349
|
+
async readDockerServiceLogs(name, tail = 120) {
|
|
350
|
+
const service = await this.getService(name);
|
|
351
|
+
if (resolveKind(service) !== "docker-compose") {
|
|
352
|
+
throw new Error(`Service "${name}" is not a docker-compose service.`);
|
|
353
|
+
}
|
|
354
|
+
return readDockerServiceLogs(toDockerTarget(name, service), tail);
|
|
355
|
+
}
|
|
356
|
+
async readDockerServiceStatus(name) {
|
|
357
|
+
const service = await this.getService(name);
|
|
358
|
+
if (resolveKind(service) !== "docker-compose") {
|
|
359
|
+
throw new Error(`Service "${name}" is not a docker-compose service.`);
|
|
360
|
+
}
|
|
361
|
+
return readDockerServiceStatus(toDockerTarget(name, service));
|
|
362
|
+
}
|
|
128
363
|
async getService(name) {
|
|
129
364
|
const config = await this.configStore.load();
|
|
130
365
|
const service = config.services.find((item) => item.name === name);
|
|
@@ -157,7 +392,15 @@ export class ProcessManager {
|
|
|
157
392
|
const runtime = this.runtimes.get(service);
|
|
158
393
|
if (runtime) {
|
|
159
394
|
runtime.status = { ...runtime.status, url };
|
|
395
|
+
void this.maybeStartInspector(service);
|
|
160
396
|
}
|
|
397
|
+
void this.appendTimeline({
|
|
398
|
+
kind: "service.port",
|
|
399
|
+
service,
|
|
400
|
+
severity: "info",
|
|
401
|
+
title: `${service} reported ${url}`,
|
|
402
|
+
detail: url,
|
|
403
|
+
});
|
|
161
404
|
}
|
|
162
405
|
void this.logStore.append(service, stream, line);
|
|
163
406
|
}
|
|
@@ -170,10 +413,62 @@ export class ProcessManager {
|
|
|
170
413
|
}
|
|
171
414
|
});
|
|
172
415
|
}
|
|
416
|
+
async appendTimeline(event) {
|
|
417
|
+
if (!this.timelineStore)
|
|
418
|
+
return;
|
|
419
|
+
await this.timelineStore.append(event);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function resolveKind(service) {
|
|
423
|
+
return service.kind ?? "local";
|
|
424
|
+
}
|
|
425
|
+
function toDockerTarget(name, service) {
|
|
426
|
+
if (!service.cwd || !service.composeService) {
|
|
427
|
+
throw new Error(`Service "${name}" is missing docker-compose cwd or composeService.`);
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
cwd: service.cwd,
|
|
431
|
+
composeFile: service.composeFile,
|
|
432
|
+
composeService: service.composeService,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function spawnService(name, service, kind) {
|
|
436
|
+
if (kind === "ssh") {
|
|
437
|
+
if (!service.host || !service.cwd || !service.command) {
|
|
438
|
+
throw new Error(`Service "${name}" is missing ssh host, cwd, or command.`);
|
|
439
|
+
}
|
|
440
|
+
const [bin, args] = createSshCommand({
|
|
441
|
+
host: service.host,
|
|
442
|
+
cwd: service.cwd,
|
|
443
|
+
command: service.command,
|
|
444
|
+
env: service.env,
|
|
445
|
+
});
|
|
446
|
+
return spawn(bin, args, {
|
|
447
|
+
env: { ...process.env },
|
|
448
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
449
|
+
detached: true,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (!service.command || !service.cwd) {
|
|
453
|
+
throw new Error(`Service "${name}" is missing command or cwd.`);
|
|
454
|
+
}
|
|
455
|
+
return spawn(service.command, {
|
|
456
|
+
cwd: service.cwd,
|
|
457
|
+
env: { ...process.env, ...service.env },
|
|
458
|
+
shell: true,
|
|
459
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
460
|
+
detached: true,
|
|
461
|
+
});
|
|
173
462
|
}
|
|
174
463
|
function localUrlFromLine(line) {
|
|
175
464
|
return line.match(/https?:\/\/(?:localhost|127\.0\.0\.1):\d+\/?/)?.[0];
|
|
176
465
|
}
|
|
466
|
+
function portFromUrl(url) {
|
|
467
|
+
if (!url)
|
|
468
|
+
return undefined;
|
|
469
|
+
const match = url.match(/:(\d+)/);
|
|
470
|
+
return match ? Number(match[1]) : undefined;
|
|
471
|
+
}
|
|
177
472
|
async function stopChild(child, timeoutMs) {
|
|
178
473
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
179
474
|
return;
|
|
@@ -188,11 +483,69 @@ async function stopChild(child, timeoutMs) {
|
|
|
188
483
|
}
|
|
189
484
|
};
|
|
190
485
|
const timer = setTimeout(() => {
|
|
191
|
-
child
|
|
486
|
+
signalProcessTree(child, "SIGKILL");
|
|
192
487
|
finish();
|
|
193
488
|
}, timeoutMs);
|
|
194
489
|
child.once("exit", finish);
|
|
195
|
-
child
|
|
490
|
+
signalProcessTree(child, "SIGTERM");
|
|
196
491
|
});
|
|
197
492
|
}
|
|
493
|
+
async function killHolder(holder) {
|
|
494
|
+
const target = holder.pgid ? -holder.pgid : holder.pid;
|
|
495
|
+
try {
|
|
496
|
+
process.kill(target, "SIGTERM");
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
const code = error.code;
|
|
500
|
+
if (code !== "ESRCH")
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
// Give the process a moment to exit cleanly before escalating.
|
|
504
|
+
for (let i = 0; i < 20; i += 1) {
|
|
505
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
506
|
+
try {
|
|
507
|
+
process.kill(holder.pid, 0);
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
process.kill(target, "SIGKILL");
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
// ignore
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function waitForPortFree(port, timeoutMs) {
|
|
521
|
+
const deadline = Date.now() + timeoutMs;
|
|
522
|
+
while (Date.now() < deadline) {
|
|
523
|
+
if (await isPortAvailable(port))
|
|
524
|
+
return true;
|
|
525
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
526
|
+
}
|
|
527
|
+
return isPortAvailable(port);
|
|
528
|
+
}
|
|
529
|
+
function signalProcessTree(child, signal) {
|
|
530
|
+
const pid = child.pid;
|
|
531
|
+
if (!pid)
|
|
532
|
+
return;
|
|
533
|
+
try {
|
|
534
|
+
// Negative PID = signal the whole process group whose leader is `pid`.
|
|
535
|
+
// The service was spawned with detached: true so child.pid === PGID.
|
|
536
|
+
process.kill(-pid, signal);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
const code = error.code;
|
|
540
|
+
if (code === "ESRCH")
|
|
541
|
+
return;
|
|
542
|
+
// Fall back to signaling the direct child if the group is gone.
|
|
543
|
+
try {
|
|
544
|
+
child.kill(signal);
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
// ignore
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
198
551
|
//# sourceMappingURL=process-manager.js.map
|