@camerontaylor/paseo-server 0.13.0-fork.1 → 0.13.0-fork.2
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.
|
@@ -167,6 +167,14 @@ const ACP_PROBE_CLOSE_TIMEOUT_MS = 2000;
|
|
|
167
167
|
// request when the provider never answers, so these calls must be bounded
|
|
168
168
|
// or the provider process is never terminated.
|
|
169
169
|
const ACP_CLOSE_REQUEST_TIMEOUT_MS = 2000;
|
|
170
|
+
// An ACP adapter can front a daemon shared by other sessions: every `jcode acp`
|
|
171
|
+
// attaches to one setsid'd `jcode serve`, parented by whichever adapter started
|
|
172
|
+
// it. Terminating that adapter must not take the shared daemon down with it.
|
|
173
|
+
const ACP_PROVIDER_TERMINATION = {
|
|
174
|
+
gracefulTimeoutMs: 2000,
|
|
175
|
+
forceTimeoutMs: 2000,
|
|
176
|
+
spareDetachedOnForce: true,
|
|
177
|
+
};
|
|
170
178
|
const ACP_IMPORT_HISTORY_LOAD_TIMEOUT_MS = 30000;
|
|
171
179
|
const ACP_IMPORT_HISTORY_BUDGET_MS = 60000;
|
|
172
180
|
function summarizeMalformedACPStdoutError(error) {
|
|
@@ -1233,10 +1241,7 @@ export class ACPAgentSession {
|
|
|
1233
1241
|
async releaseHistoryRuntime() {
|
|
1234
1242
|
const child = this.child;
|
|
1235
1243
|
if (child) {
|
|
1236
|
-
const result = await this.terminateProcess(child,
|
|
1237
|
-
gracefulTimeoutMs: 2000,
|
|
1238
|
-
forceTimeoutMs: 2000,
|
|
1239
|
-
});
|
|
1244
|
+
const result = await this.terminateProcess(child, ACP_PROVIDER_TERMINATION);
|
|
1240
1245
|
if (result === "kill-timeout") {
|
|
1241
1246
|
// Keep the child registered so close() can retry the shutdown.
|
|
1242
1247
|
this.logger.warn("ACP history resume could not release the provider process");
|
|
@@ -2250,7 +2255,7 @@ export class ACPAgentSession {
|
|
|
2250
2255
|
await Promise.all(terminalTerminations);
|
|
2251
2256
|
this.terminalEntries.clear();
|
|
2252
2257
|
if (this.child) {
|
|
2253
|
-
await this.terminateProcess(this.child,
|
|
2258
|
+
await this.terminateProcess(this.child, ACP_PROVIDER_TERMINATION);
|
|
2254
2259
|
}
|
|
2255
2260
|
this.subscribers.clear();
|
|
2256
2261
|
this.connection = null;
|
|
@@ -3572,7 +3577,11 @@ function coerceSessionConfigMetadata(metadata) {
|
|
|
3572
3577
|
}
|
|
3573
3578
|
async function terminateChildProcess(child, timeoutMs, terminate) {
|
|
3574
3579
|
try {
|
|
3575
|
-
await terminate(child, {
|
|
3580
|
+
await terminate(child, {
|
|
3581
|
+
...ACP_PROVIDER_TERMINATION,
|
|
3582
|
+
gracefulTimeoutMs: timeoutMs,
|
|
3583
|
+
forceTimeoutMs: timeoutMs,
|
|
3584
|
+
});
|
|
3576
3585
|
}
|
|
3577
3586
|
finally {
|
|
3578
3587
|
child.stdin.destroy();
|
|
@@ -11,9 +11,26 @@ export interface TerminateWithTreeKillOptions {
|
|
|
11
11
|
gracefulTimeoutMs: number;
|
|
12
12
|
forceTimeoutMs?: number;
|
|
13
13
|
onForceSignal?: () => void;
|
|
14
|
+
spareDetachedOnForce?: boolean;
|
|
14
15
|
}
|
|
15
16
|
export type TerminateWithTreeKillResult = "already-exited" | "terminated" | "killed" | "kill-timeout";
|
|
16
17
|
export type ProcessTerminator = (child: TreeKillTarget, options: TerminateWithTreeKillOptions) => Promise<TerminateWithTreeKillResult>;
|
|
17
|
-
export
|
|
18
|
+
export interface ProcessTableEntry {
|
|
19
|
+
pid: number;
|
|
20
|
+
ppid: number;
|
|
21
|
+
sessionLeader: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type ProcessTableReader = () => Promise<ProcessTableEntry[] | null>;
|
|
24
|
+
export interface TreeKillDependencies {
|
|
25
|
+
readProcessTable: ProcessTableReader;
|
|
26
|
+
signalPid: (pid: number, signal: NodeJS.Signals) => void;
|
|
27
|
+
signalTree: (child: TreeKillTarget, signal: NodeJS.Signals) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createTreeKillTerminator(dependencies?: Partial<TreeKillDependencies>): ProcessTerminator;
|
|
30
|
+
export declare const terminateWithTreeKill: ProcessTerminator;
|
|
18
31
|
export declare function signalProcessTree(child: TreeKillTarget, signal: NodeJS.Signals): Promise<void>;
|
|
32
|
+
export declare function selectAttachedProcessTree(table: readonly ProcessTableEntry[], rootPid: number): number[];
|
|
33
|
+
export declare function readProcessTable(): Promise<ProcessTableEntry[] | null>;
|
|
34
|
+
export declare function parseProcStat(stat: string): ProcessTableEntry | null;
|
|
35
|
+
export declare function parsePsProcessTable(output: string): ProcessTableEntry[];
|
|
19
36
|
//# sourceMappingURL=tree-kill.d.ts.map
|
|
@@ -1,22 +1,38 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
1
3
|
import treeKill from "tree-kill";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
export function createTreeKillTerminator(dependencies = {}) {
|
|
5
|
+
const deps = {
|
|
6
|
+
readProcessTable: dependencies.readProcessTable ?? readProcessTable,
|
|
7
|
+
signalPid: dependencies.signalPid ?? ((pid, signal) => process.kill(pid, signal)),
|
|
8
|
+
signalTree: dependencies.signalTree ?? signalProcessTree,
|
|
9
|
+
};
|
|
10
|
+
return async (child, options) => {
|
|
11
|
+
if (isProcessExited(child)) {
|
|
12
|
+
return "already-exited";
|
|
13
|
+
}
|
|
14
|
+
const exitPromise = waitForProcessExit(child);
|
|
15
|
+
await signalAttachedProcessTree(child, options.gracefulSignal ?? "SIGTERM", deps);
|
|
16
|
+
if (await waitForExitOrTimeout(exitPromise, options.gracefulTimeoutMs)) {
|
|
17
|
+
return "terminated";
|
|
18
|
+
}
|
|
19
|
+
options.onForceSignal?.();
|
|
20
|
+
const forceSignal = options.forceSignal ?? "SIGKILL";
|
|
21
|
+
if (options.spareDetachedOnForce) {
|
|
22
|
+
await signalAttachedProcessTree(child, forceSignal, deps);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await deps.signalTree(child, forceSignal);
|
|
26
|
+
}
|
|
27
|
+
if (options.forceTimeoutMs === undefined) {
|
|
28
|
+
return "killed";
|
|
29
|
+
}
|
|
30
|
+
return (await waitForExitOrTimeout(exitPromise, options.forceTimeoutMs))
|
|
31
|
+
? "killed"
|
|
32
|
+
: "kill-timeout";
|
|
33
|
+
};
|
|
19
34
|
}
|
|
35
|
+
export const terminateWithTreeKill = createTreeKillTerminator();
|
|
20
36
|
export function signalProcessTree(child, signal) {
|
|
21
37
|
if (isProcessExited(child)) {
|
|
22
38
|
return Promise.resolve();
|
|
@@ -35,6 +51,136 @@ export function signalProcessTree(child, signal) {
|
|
|
35
51
|
});
|
|
36
52
|
});
|
|
37
53
|
}
|
|
54
|
+
// The root plus every descendant still in its session. A descendant that leads
|
|
55
|
+
// its own session called setsid() to detach (daemons, `spawn({ detached: true })`),
|
|
56
|
+
// so it and its subtree are left out. Process groups are not the boundary: shell
|
|
57
|
+
// job control gives each pipeline its own group inside the same session.
|
|
58
|
+
export function selectAttachedProcessTree(table, rootPid) {
|
|
59
|
+
const childrenByParent = new Map();
|
|
60
|
+
for (const entry of table) {
|
|
61
|
+
if (entry.pid === rootPid)
|
|
62
|
+
continue;
|
|
63
|
+
const siblings = childrenByParent.get(entry.ppid);
|
|
64
|
+
if (siblings) {
|
|
65
|
+
siblings.push(entry);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
childrenByParent.set(entry.ppid, [entry]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const attached = [rootPid];
|
|
72
|
+
const visited = new Set(attached);
|
|
73
|
+
for (let index = 0; index < attached.length; index += 1) {
|
|
74
|
+
for (const child of childrenByParent.get(attached[index]) ?? []) {
|
|
75
|
+
if (child.sessionLeader || visited.has(child.pid))
|
|
76
|
+
continue;
|
|
77
|
+
visited.add(child.pid);
|
|
78
|
+
attached.push(child.pid);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return attached;
|
|
82
|
+
}
|
|
83
|
+
async function signalAttachedProcessTree(child, signal, deps) {
|
|
84
|
+
if (isProcessExited(child)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const pid = child.pid;
|
|
88
|
+
if (typeof pid !== "number" || pid <= 0) {
|
|
89
|
+
signalDirectChild(child, signal);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const table = await deps.readProcessTable().catch(() => null);
|
|
93
|
+
if (!table) {
|
|
94
|
+
await deps.signalTree(child, signal);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const target of selectAttachedProcessTree(table, pid)) {
|
|
98
|
+
try {
|
|
99
|
+
deps.signalPid(target, signal);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
if (target === pid) {
|
|
103
|
+
signalDirectChild(child, signal);
|
|
104
|
+
}
|
|
105
|
+
// Descendants that exited since the snapshot are already gone.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function readProcessTable() {
|
|
110
|
+
if (process.platform === "win32") {
|
|
111
|
+
// tree-kill's taskkill /T owns Windows; there is no session to detach into.
|
|
112
|
+
return Promise.resolve(null);
|
|
113
|
+
}
|
|
114
|
+
if (process.platform === "linux") {
|
|
115
|
+
return readLinuxProcessTable();
|
|
116
|
+
}
|
|
117
|
+
return readPsProcessTable();
|
|
118
|
+
}
|
|
119
|
+
async function readLinuxProcessTable() {
|
|
120
|
+
let names;
|
|
121
|
+
try {
|
|
122
|
+
names = await readdir("/proc");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const entries = await Promise.all(names
|
|
128
|
+
.filter((name) => /^\d+$/.test(name))
|
|
129
|
+
.map(async (name) => {
|
|
130
|
+
try {
|
|
131
|
+
return parseProcStat(await readFile(`/proc/${name}/stat`, "utf8"));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}));
|
|
137
|
+
const table = entries.filter((entry) => entry !== null);
|
|
138
|
+
return table.length > 0 ? table : null;
|
|
139
|
+
}
|
|
140
|
+
// /proc/<pid>/stat is "pid (comm) state ppid pgrp session ...". comm may itself
|
|
141
|
+
// contain spaces and parentheses, so fields are read after the last ")".
|
|
142
|
+
export function parseProcStat(stat) {
|
|
143
|
+
const commEnd = stat.lastIndexOf(")");
|
|
144
|
+
if (commEnd === -1)
|
|
145
|
+
return null;
|
|
146
|
+
const pid = Number.parseInt(stat, 10);
|
|
147
|
+
const fields = stat
|
|
148
|
+
.slice(commEnd + 1)
|
|
149
|
+
.trim()
|
|
150
|
+
.split(/\s+/);
|
|
151
|
+
const ppid = Number.parseInt(fields[1] ?? "", 10);
|
|
152
|
+
const session = Number.parseInt(fields[3] ?? "", 10);
|
|
153
|
+
if (!Number.isInteger(pid) || !Number.isInteger(ppid) || !Number.isInteger(session)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return { pid, ppid, sessionLeader: session === pid };
|
|
157
|
+
}
|
|
158
|
+
// BSD ps (macOS) reports no usable session id, but both BSD and procps flag a
|
|
159
|
+
// session leader with "s" in STAT.
|
|
160
|
+
function readPsProcessTable() {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
execFile("ps", ["-A", "-o", "pid=,ppid=,stat="], { timeout: 2000, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {
|
|
163
|
+
if (error) {
|
|
164
|
+
resolve(null);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const table = parsePsProcessTable(stdout);
|
|
168
|
+
resolve(table.length > 0 ? table : null);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
export function parsePsProcessTable(output) {
|
|
173
|
+
const table = [];
|
|
174
|
+
for (const line of output.split("\n")) {
|
|
175
|
+
const [pidText, ppidText, stat = ""] = line.trim().split(/\s+/);
|
|
176
|
+
const pid = Number.parseInt(pidText ?? "", 10);
|
|
177
|
+
const ppid = Number.parseInt(ppidText ?? "", 10);
|
|
178
|
+
if (!Number.isInteger(pid) || !Number.isInteger(ppid))
|
|
179
|
+
continue;
|
|
180
|
+
table.push({ pid, ppid, sessionLeader: stat.includes("s") });
|
|
181
|
+
}
|
|
182
|
+
return table;
|
|
183
|
+
}
|
|
38
184
|
function signalDirectChild(child, signal) {
|
|
39
185
|
try {
|
|
40
186
|
child.kill(signal);
|
|
@@ -1,22 +1,38 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
1
3
|
import treeKill from "tree-kill";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
export function createTreeKillTerminator(dependencies = {}) {
|
|
5
|
+
const deps = {
|
|
6
|
+
readProcessTable: dependencies.readProcessTable ?? readProcessTable,
|
|
7
|
+
signalPid: dependencies.signalPid ?? ((pid, signal) => process.kill(pid, signal)),
|
|
8
|
+
signalTree: dependencies.signalTree ?? signalProcessTree,
|
|
9
|
+
};
|
|
10
|
+
return async (child, options) => {
|
|
11
|
+
if (isProcessExited(child)) {
|
|
12
|
+
return "already-exited";
|
|
13
|
+
}
|
|
14
|
+
const exitPromise = waitForProcessExit(child);
|
|
15
|
+
await signalAttachedProcessTree(child, options.gracefulSignal ?? "SIGTERM", deps);
|
|
16
|
+
if (await waitForExitOrTimeout(exitPromise, options.gracefulTimeoutMs)) {
|
|
17
|
+
return "terminated";
|
|
18
|
+
}
|
|
19
|
+
options.onForceSignal?.();
|
|
20
|
+
const forceSignal = options.forceSignal ?? "SIGKILL";
|
|
21
|
+
if (options.spareDetachedOnForce) {
|
|
22
|
+
await signalAttachedProcessTree(child, forceSignal, deps);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await deps.signalTree(child, forceSignal);
|
|
26
|
+
}
|
|
27
|
+
if (options.forceTimeoutMs === undefined) {
|
|
28
|
+
return "killed";
|
|
29
|
+
}
|
|
30
|
+
return (await waitForExitOrTimeout(exitPromise, options.forceTimeoutMs))
|
|
31
|
+
? "killed"
|
|
32
|
+
: "kill-timeout";
|
|
33
|
+
};
|
|
19
34
|
}
|
|
35
|
+
export const terminateWithTreeKill = createTreeKillTerminator();
|
|
20
36
|
export function signalProcessTree(child, signal) {
|
|
21
37
|
if (isProcessExited(child)) {
|
|
22
38
|
return Promise.resolve();
|
|
@@ -35,6 +51,136 @@ export function signalProcessTree(child, signal) {
|
|
|
35
51
|
});
|
|
36
52
|
});
|
|
37
53
|
}
|
|
54
|
+
// The root plus every descendant still in its session. A descendant that leads
|
|
55
|
+
// its own session called setsid() to detach (daemons, `spawn({ detached: true })`),
|
|
56
|
+
// so it and its subtree are left out. Process groups are not the boundary: shell
|
|
57
|
+
// job control gives each pipeline its own group inside the same session.
|
|
58
|
+
export function selectAttachedProcessTree(table, rootPid) {
|
|
59
|
+
const childrenByParent = new Map();
|
|
60
|
+
for (const entry of table) {
|
|
61
|
+
if (entry.pid === rootPid)
|
|
62
|
+
continue;
|
|
63
|
+
const siblings = childrenByParent.get(entry.ppid);
|
|
64
|
+
if (siblings) {
|
|
65
|
+
siblings.push(entry);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
childrenByParent.set(entry.ppid, [entry]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const attached = [rootPid];
|
|
72
|
+
const visited = new Set(attached);
|
|
73
|
+
for (let index = 0; index < attached.length; index += 1) {
|
|
74
|
+
for (const child of childrenByParent.get(attached[index]) ?? []) {
|
|
75
|
+
if (child.sessionLeader || visited.has(child.pid))
|
|
76
|
+
continue;
|
|
77
|
+
visited.add(child.pid);
|
|
78
|
+
attached.push(child.pid);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return attached;
|
|
82
|
+
}
|
|
83
|
+
async function signalAttachedProcessTree(child, signal, deps) {
|
|
84
|
+
if (isProcessExited(child)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const pid = child.pid;
|
|
88
|
+
if (typeof pid !== "number" || pid <= 0) {
|
|
89
|
+
signalDirectChild(child, signal);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const table = await deps.readProcessTable().catch(() => null);
|
|
93
|
+
if (!table) {
|
|
94
|
+
await deps.signalTree(child, signal);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const target of selectAttachedProcessTree(table, pid)) {
|
|
98
|
+
try {
|
|
99
|
+
deps.signalPid(target, signal);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
if (target === pid) {
|
|
103
|
+
signalDirectChild(child, signal);
|
|
104
|
+
}
|
|
105
|
+
// Descendants that exited since the snapshot are already gone.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function readProcessTable() {
|
|
110
|
+
if (process.platform === "win32") {
|
|
111
|
+
// tree-kill's taskkill /T owns Windows; there is no session to detach into.
|
|
112
|
+
return Promise.resolve(null);
|
|
113
|
+
}
|
|
114
|
+
if (process.platform === "linux") {
|
|
115
|
+
return readLinuxProcessTable();
|
|
116
|
+
}
|
|
117
|
+
return readPsProcessTable();
|
|
118
|
+
}
|
|
119
|
+
async function readLinuxProcessTable() {
|
|
120
|
+
let names;
|
|
121
|
+
try {
|
|
122
|
+
names = await readdir("/proc");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const entries = await Promise.all(names
|
|
128
|
+
.filter((name) => /^\d+$/.test(name))
|
|
129
|
+
.map(async (name) => {
|
|
130
|
+
try {
|
|
131
|
+
return parseProcStat(await readFile(`/proc/${name}/stat`, "utf8"));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}));
|
|
137
|
+
const table = entries.filter((entry) => entry !== null);
|
|
138
|
+
return table.length > 0 ? table : null;
|
|
139
|
+
}
|
|
140
|
+
// /proc/<pid>/stat is "pid (comm) state ppid pgrp session ...". comm may itself
|
|
141
|
+
// contain spaces and parentheses, so fields are read after the last ")".
|
|
142
|
+
export function parseProcStat(stat) {
|
|
143
|
+
const commEnd = stat.lastIndexOf(")");
|
|
144
|
+
if (commEnd === -1)
|
|
145
|
+
return null;
|
|
146
|
+
const pid = Number.parseInt(stat, 10);
|
|
147
|
+
const fields = stat
|
|
148
|
+
.slice(commEnd + 1)
|
|
149
|
+
.trim()
|
|
150
|
+
.split(/\s+/);
|
|
151
|
+
const ppid = Number.parseInt(fields[1] ?? "", 10);
|
|
152
|
+
const session = Number.parseInt(fields[3] ?? "", 10);
|
|
153
|
+
if (!Number.isInteger(pid) || !Number.isInteger(ppid) || !Number.isInteger(session)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return { pid, ppid, sessionLeader: session === pid };
|
|
157
|
+
}
|
|
158
|
+
// BSD ps (macOS) reports no usable session id, but both BSD and procps flag a
|
|
159
|
+
// session leader with "s" in STAT.
|
|
160
|
+
function readPsProcessTable() {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
execFile("ps", ["-A", "-o", "pid=,ppid=,stat="], { timeout: 2000, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {
|
|
163
|
+
if (error) {
|
|
164
|
+
resolve(null);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const table = parsePsProcessTable(stdout);
|
|
168
|
+
resolve(table.length > 0 ? table : null);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
export function parsePsProcessTable(output) {
|
|
173
|
+
const table = [];
|
|
174
|
+
for (const line of output.split("\n")) {
|
|
175
|
+
const [pidText, ppidText, stat = ""] = line.trim().split(/\s+/);
|
|
176
|
+
const pid = Number.parseInt(pidText ?? "", 10);
|
|
177
|
+
const ppid = Number.parseInt(ppidText ?? "", 10);
|
|
178
|
+
if (!Number.isInteger(pid) || !Number.isInteger(ppid))
|
|
179
|
+
continue;
|
|
180
|
+
table.push({ pid, ppid, sessionLeader: stat.includes("s") });
|
|
181
|
+
}
|
|
182
|
+
return table;
|
|
183
|
+
}
|
|
38
184
|
function signalDirectChild(child, signal) {
|
|
39
185
|
try {
|
|
40
186
|
child.kill(signal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camerontaylor/paseo-server",
|
|
3
|
-
"version": "0.13.0-fork.
|
|
3
|
+
"version": "0.13.0-fork.2",
|
|
4
4
|
"description": "Paseo backend server",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/server",
|
|
@@ -107,11 +107,11 @@
|
|
|
107
107
|
"@agentclientprotocol/sdk": "^0.17.1",
|
|
108
108
|
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
|
109
109
|
"@anthropic-ai/sdk": "^0.104.2",
|
|
110
|
-
"@camerontaylor/paseo-client": "0.13.0-fork.
|
|
111
|
-
"@camerontaylor/paseo-highlight": "0.13.0-fork.
|
|
112
|
-
"@camerontaylor/paseo-plugin": "0.13.0-fork.
|
|
113
|
-
"@camerontaylor/paseo-protocol": "0.13.0-fork.
|
|
114
|
-
"@camerontaylor/paseo-relay": "0.13.0-fork.
|
|
110
|
+
"@camerontaylor/paseo-client": "0.13.0-fork.2",
|
|
111
|
+
"@camerontaylor/paseo-highlight": "0.13.0-fork.2",
|
|
112
|
+
"@camerontaylor/paseo-plugin": "0.13.0-fork.2",
|
|
113
|
+
"@camerontaylor/paseo-protocol": "0.13.0-fork.2",
|
|
114
|
+
"@camerontaylor/paseo-relay": "0.13.0-fork.2",
|
|
115
115
|
"@isaacs/ttlcache": "^2.1.4",
|
|
116
116
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
117
117
|
"@opencode-ai/sdk": "1.14.46",
|