@opencode-cockpit/daemon 0.2.0 → 0.2.1
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.
|
@@ -4,6 +4,9 @@ import { waitFor } from "./wait.js";
|
|
|
4
4
|
import { PRESETS, presetByName, presetForCommand } from "./watch/presets.js";
|
|
5
5
|
import { compileRule, Watcher } from "./watch/watcher.js";
|
|
6
6
|
|
|
7
|
+
/** Preset name for a watcher with no patterns: it only reports the process dying. */
|
|
8
|
+
const EXIT_ONLY = "exit";
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* The `shell.*` methods, kept apart from the module's lifecycle and bookkeeping so each file has
|
|
9
12
|
* one job: this one maps protocol calls onto the module, `module.ts` owns the shells.
|
|
@@ -71,18 +74,28 @@ export function shellMethods(module) {
|
|
|
71
74
|
id,
|
|
72
75
|
signal,
|
|
73
76
|
graceMs
|
|
77
|
+
}, {
|
|
78
|
+
peer
|
|
74
79
|
}) => {
|
|
75
80
|
const shell = module.require(id);
|
|
76
|
-
await shell.stop(signal, graceMs
|
|
81
|
+
await shell.stop(signal, graceMs, {
|
|
82
|
+
reason: "request",
|
|
83
|
+
by: peer.name
|
|
84
|
+
});
|
|
77
85
|
await shell.exited;
|
|
78
86
|
return shell.info();
|
|
79
87
|
},
|
|
80
88
|
restart: async ({
|
|
81
89
|
id
|
|
90
|
+
}, {
|
|
91
|
+
peer
|
|
82
92
|
}) => {
|
|
83
93
|
const shell = module.require(id);
|
|
84
94
|
if (shell.running) {
|
|
85
|
-
await shell.stop("SIGTERM", 3000
|
|
95
|
+
await shell.stop("SIGTERM", 3000, {
|
|
96
|
+
reason: "request",
|
|
97
|
+
by: peer.name
|
|
98
|
+
});
|
|
86
99
|
await shell.exited;
|
|
87
100
|
}
|
|
88
101
|
module.spawn(shell);
|
|
@@ -90,10 +103,15 @@ export function shellMethods(module) {
|
|
|
90
103
|
},
|
|
91
104
|
remove: async ({
|
|
92
105
|
id
|
|
106
|
+
}, {
|
|
107
|
+
peer
|
|
93
108
|
}) => {
|
|
94
109
|
const shell = module.require(id);
|
|
95
110
|
if (shell.running) {
|
|
96
|
-
await shell.stop("SIGTERM", 3000
|
|
111
|
+
await shell.stop("SIGTERM", 3000, {
|
|
112
|
+
reason: "request",
|
|
113
|
+
by: peer.name
|
|
114
|
+
});
|
|
97
115
|
await shell.exited;
|
|
98
116
|
}
|
|
99
117
|
module.forget(shell);
|
|
@@ -140,14 +158,14 @@ export function shellMethods(module) {
|
|
|
140
158
|
}) => {
|
|
141
159
|
const shell = module.require(id);
|
|
142
160
|
const command = [shell.spec.command, ...shell.spec.args].join(" ");
|
|
143
|
-
const
|
|
161
|
+
const named = preset && preset !== "auto" && preset !== EXIT_ONLY;
|
|
162
|
+
const chosen = rule ? undefined : named ? presetByName(preset) ?? invalidParams(`no watch preset named "${preset}". Call shell.presets for the list, or pass your own rule (done/fail/ok patterns).`) : preset === EXIT_ONLY ? undefined : presetForCommand(command);
|
|
144
163
|
if (chosen instanceof Error) throw chosen;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
164
|
+
// No pattern fits a command like `sleep 300`, and that is still worth watching: an empty rule
|
|
165
|
+
// reports nothing until the process dies, which is exactly crash detection.
|
|
166
|
+
const watchRule = rule ?? chosen?.rule ?? {};
|
|
149
167
|
try {
|
|
150
|
-
shell.watcher = new Watcher(compileRule(watchRule), chosen?.name);
|
|
168
|
+
shell.watcher = new Watcher(compileRule(watchRule), chosen?.name ?? (rule ? undefined : EXIT_ONLY));
|
|
151
169
|
} catch (err) {
|
|
152
170
|
throw invalidParams(`invalid watch pattern: ${err instanceof Error ? err.message : String(err)}`);
|
|
153
171
|
}
|
|
@@ -43,7 +43,9 @@ export class ShellModule {
|
|
|
43
43
|
}
|
|
44
44
|
async stop() {
|
|
45
45
|
for (const id of [...this.idleTimers.keys()]) this.clearIdle(id);
|
|
46
|
-
await Promise.all([...this.shells.values()].map(s => s.stop("SIGTERM", 2000
|
|
46
|
+
await Promise.all([...this.shells.values()].map(s => s.stop("SIGTERM", 2000, {
|
|
47
|
+
reason: "shutdown"
|
|
48
|
+
}).catch(() => {})));
|
|
47
49
|
for (const detach of this.attachments.values()) detach();
|
|
48
50
|
for (const shell of this.shells.values()) shell.dispose();
|
|
49
51
|
this.attachments.clear();
|
|
@@ -64,6 +64,8 @@ export class Shell {
|
|
|
64
64
|
}
|
|
65
65
|
this.runStartLine = this.log.lastLine + 1;
|
|
66
66
|
this.stoppedBecause = undefined;
|
|
67
|
+
this.stopReason = undefined;
|
|
68
|
+
this.stoppedBy = undefined;
|
|
67
69
|
if (this.spec.logFile && !this.logWriter) {
|
|
68
70
|
mkdirSync(dirname(this.spec.logFile), {
|
|
69
71
|
recursive: true
|
|
@@ -111,6 +113,7 @@ export class Shell {
|
|
|
111
113
|
if (this.spec.timeoutMs) {
|
|
112
114
|
this.timeout = setTimeout(() => {
|
|
113
115
|
this.stoppedBecause = `reached its ${Math.round((this.spec.timeoutMs ?? 0) / 1000)}s time limit`;
|
|
116
|
+
this.stopReason = "timeout";
|
|
114
117
|
void this.stop("SIGTERM", 3000);
|
|
115
118
|
}, this.spec.timeoutMs);
|
|
116
119
|
}
|
|
@@ -119,6 +122,7 @@ export class Shell {
|
|
|
119
122
|
this.idleTimer = setInterval(() => {
|
|
120
123
|
if (!this.running || Date.now() - this.lastOutputAt < idleMs) return;
|
|
121
124
|
this.stoppedBecause = `produced no output for ${Math.round(idleMs / 1000)}s`;
|
|
125
|
+
this.stopReason = "idle";
|
|
122
126
|
void this.stop("SIGTERM", 3000);
|
|
123
127
|
}, Math.max(500, Math.floor(idleMs / 4)));
|
|
124
128
|
}
|
|
@@ -134,11 +138,18 @@ export class Shell {
|
|
|
134
138
|
if (this.running) this.pty?.resize(cols, rows);
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Signal the group, escalate to SIGKILL after `graceMs`, and reap stragglers. `cause` says who
|
|
143
|
+
* asked, so an exit can be reported as a stop rather than as an unexplained kill.
|
|
144
|
+
*/
|
|
145
|
+
async stop(signal = "SIGTERM", graceMs = 3000, cause = {
|
|
146
|
+
reason: "request"
|
|
147
|
+
}) {
|
|
139
148
|
const pty = this.pty;
|
|
140
149
|
if (!pty || !this.running) return;
|
|
141
150
|
this.stopRequested = true;
|
|
151
|
+
this.stopReason ??= cause.reason;
|
|
152
|
+
this.stoppedBy ??= cause.by;
|
|
142
153
|
pty.signal(signal);
|
|
143
154
|
const exited = await Promise.race([this.exitPromise.then(() => true), Bun.sleep(graceMs).then(() => false)]);
|
|
144
155
|
if (!exited) {
|
|
@@ -174,6 +185,8 @@ export class Shell {
|
|
|
174
185
|
if (this.exit?.signal) info.signal = this.exit.signal;
|
|
175
186
|
if (this.error) info.error = this.error;
|
|
176
187
|
if (this.summary) info.summary = this.summary;
|
|
188
|
+
if (this.stopReason) info.stopReason = this.stopReason;
|
|
189
|
+
if (this.stoppedBy) info.stoppedBy = this.stoppedBy;
|
|
177
190
|
if (this.spec.logFile) info.logFile = this.spec.logFile;
|
|
178
191
|
if (this.watcher) info.watch = this.watcher.state();
|
|
179
192
|
if (this.endedAt) info.endedAt = this.endedAt;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencode-cockpit/daemon",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "cockpitd: the process host behind opencode-cockpit (PTY shells, clean logs, wait conditions)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@opencode-cockpit/protocol": "0.2.
|
|
44
|
+
"@opencode-cockpit/protocol": "0.2.1",
|
|
45
45
|
"@xterm/headless": "6.0.0"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LogLine, Owner, ScreenResult, ShellInfo, ShellStatus } from "@opencode-cockpit/protocol/shell";
|
|
1
|
+
import type { LogLine, Owner, ScreenResult, ShellInfo, ShellStatus, StopReason } from "@opencode-cockpit/protocol/shell";
|
|
2
2
|
import { LineLog } from "./output/line-log.ts";
|
|
3
3
|
import { RawRing } from "./output/raw-ring.ts";
|
|
4
4
|
import { Screen } from "./output/screen.ts";
|
|
@@ -59,6 +59,8 @@ export declare class Shell {
|
|
|
59
59
|
private error;
|
|
60
60
|
/** Why the daemon stopped it, when it was not a user or agent request. */
|
|
61
61
|
private stoppedBecause;
|
|
62
|
+
private stopReason;
|
|
63
|
+
private stoppedBy;
|
|
62
64
|
private summary;
|
|
63
65
|
private stopRequested;
|
|
64
66
|
private timeout;
|
|
@@ -76,8 +78,14 @@ export declare class Shell {
|
|
|
76
78
|
start(): void;
|
|
77
79
|
write(data: string): number;
|
|
78
80
|
resize(cols: number, rows: number): void;
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Signal the group, escalate to SIGKILL after `graceMs`, and reap stragglers. `cause` says who
|
|
83
|
+
* asked, so an exit can be reported as a stop rather than as an unexplained kill.
|
|
84
|
+
*/
|
|
85
|
+
stop(signal?: NodeJS.Signals, graceMs?: number, cause?: {
|
|
86
|
+
reason: StopReason;
|
|
87
|
+
by?: string;
|
|
88
|
+
}): Promise<void>;
|
|
81
89
|
snapshot(): Promise<ScreenResult>;
|
|
82
90
|
info(): ShellInfo;
|
|
83
91
|
dispose(): void;
|