@yefengr/remote-pi 0.7.16 → 0.8.0
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 +44 -95
- package/dist/actions/handlers.js +1 -1
- package/dist/actions/handlers.js.map +1 -1
- package/dist/actions/registry.d.ts +1 -1
- package/dist/actions/registry.js +1 -1
- package/dist/{daemon/commands.d.ts → commands.d.ts} +3 -4
- package/dist/{daemon/commands.js → commands.js} +8 -151
- package/dist/commands.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -38
- package/dist/index.js.map +1 -1
- package/dist/pairing/storage.js +4 -4
- package/dist/pairing/storage.js.map +1 -1
- package/dist/protocol/v2/schemas.d.ts +4 -4
- package/dist/session/local_config.d.ts +0 -5
- package/dist/session/local_config.js +6 -29
- package/dist/session/local_config.js.map +1 -1
- package/dist/transport/relay_client.js.map +1 -1
- package/package.json +1 -8
- package/dist/bin/supervisord.d.ts +0 -2
- package/dist/bin/supervisord.js +0 -78
- package/dist/bin/supervisord.js.map +0 -1
- package/dist/daemon/cli-links.d.ts +0 -30
- package/dist/daemon/cli-links.js +0 -134
- package/dist/daemon/cli-links.js.map +0 -1
- package/dist/daemon/client.d.ts +0 -20
- package/dist/daemon/client.js +0 -131
- package/dist/daemon/client.js.map +0 -1
- package/dist/daemon/commands.js.map +0 -1
- package/dist/daemon/control_protocol.d.ts +0 -198
- package/dist/daemon/control_protocol.js +0 -40
- package/dist/daemon/control_protocol.js.map +0 -1
- package/dist/daemon/cron_log.d.ts +0 -24
- package/dist/daemon/cron_log.js +0 -57
- package/dist/daemon/cron_log.js.map +0 -1
- package/dist/daemon/cron_registry.d.ts +0 -43
- package/dist/daemon/cron_registry.js +0 -149
- package/dist/daemon/cron_registry.js.map +0 -1
- package/dist/daemon/install-runtime.d.ts +0 -10
- package/dist/daemon/install-runtime.js +0 -81
- package/dist/daemon/install-runtime.js.map +0 -1
- package/dist/daemon/install.d.ts +0 -110
- package/dist/daemon/install.js +0 -263
- package/dist/daemon/install.js.map +0 -1
- package/dist/daemon/registry.d.ts +0 -51
- package/dist/daemon/registry.js +0 -207
- package/dist/daemon/registry.js.map +0 -1
- package/dist/daemon/rpc_child.d.ts +0 -135
- package/dist/daemon/rpc_child.js +0 -472
- package/dist/daemon/rpc_child.js.map +0 -1
- package/dist/daemon/status.d.ts +0 -45
- package/dist/daemon/status.js +0 -87
- package/dist/daemon/status.js.map +0 -1
- package/dist/daemon/supervisor-ipc.d.ts +0 -2
- package/dist/daemon/supervisor-ipc.js +0 -20
- package/dist/daemon/supervisor-ipc.js.map +0 -1
- package/dist/daemon/supervisor.d.ts +0 -70
- package/dist/daemon/supervisor.js +0 -637
- package/dist/daemon/supervisor.js.map +0 -1
- package/dist/session/ipc.d.ts +0 -6
- package/dist/session/ipc.js +0 -15
- package/dist/session/ipc.js.map +0 -1
- package/docs/daemon.md +0 -175
- package/service-templates/launchd.plist.template +0 -35
- package/service-templates/systemd.service.template +0 -19
- package/service-templates/task-launcher.vbs.template +0 -10
- package/service-templates/task-scheduler.xml.template +0 -38
|
@@ -1,637 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { createServer } from "node:net";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { Cron } from "croner";
|
|
6
|
-
import { addDaemon, findDaemonByCwd, listDaemons, removeDaemon, setDaemonDesiredState, } from "./registry.js";
|
|
7
|
-
import { defaultAgentName } from "../session/local_config.js";
|
|
8
|
-
import { ipcAddress, usesNamedPipe } from "../session/ipc.js";
|
|
9
|
-
import { RpcChild, } from "./rpc_child.js";
|
|
10
|
-
import { encodeReply, parseRequest, } from "./control_protocol.js";
|
|
11
|
-
import { addJob as addCronJob, getJob as getCronJob, listJobs as listCronJobs, nextRunFor, recordRun, removeJob as removeCronJob, setJobEnabled, validateSchedule, } from "./cron_registry.js";
|
|
12
|
-
import { appendCronLog, readCronLog } from "./cron_log.js";
|
|
13
|
-
import { decideFireAction, infoFor } from "./status.js";
|
|
14
|
-
import { probeSupervisor } from "./supervisor-ipc.js";
|
|
15
|
-
const SUPERVISOR_SOCK_NAME = "supervisor.sock";
|
|
16
|
-
const RESTART_BACKOFFS_MS = [1_000, 5_000, 30_000, 5 * 60_000], RESTART_STABILITY_MS = 30_000, RECONCILE_INTERVAL_MS = 15_000, MISSING_CWD_CONFIRMATIONS = 2;
|
|
17
|
-
function supervisorSockPath() {
|
|
18
|
-
const root = process.env["REMOTE_PI_HOME"] || homedir();
|
|
19
|
-
return ipcAddress("supervisor", join(root, ".pi", "remote", SUPERVISOR_SOCK_NAME));
|
|
20
|
-
}
|
|
21
|
-
export class SupervisorAlreadyRunningError extends Error {
|
|
22
|
-
sockPath;
|
|
23
|
-
constructor(sockPath) {
|
|
24
|
-
super(`Another pi-supervisord is already running (UDS held at ${sockPath}).`);
|
|
25
|
-
this.sockPath = sockPath;
|
|
26
|
-
this.name = "SupervisorAlreadyRunningError";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export { decideFireAction } from "./status.js";
|
|
30
|
-
export class Supervisor {
|
|
31
|
-
opts;
|
|
32
|
-
server = null;
|
|
33
|
-
children = new Map();
|
|
34
|
-
cronJobs = new Map();
|
|
35
|
-
shuttingDown = false;
|
|
36
|
-
reconcileTimer = null;
|
|
37
|
-
reconcileRunning = false;
|
|
38
|
-
lifecycleQueues = new Map();
|
|
39
|
-
controlSockets = new Map();
|
|
40
|
-
activeRequests = new Set();
|
|
41
|
-
constructor(opts) {
|
|
42
|
-
this.opts = opts;
|
|
43
|
-
}
|
|
44
|
-
async start() {
|
|
45
|
-
this.mkdirParent();
|
|
46
|
-
await this.bindUds();
|
|
47
|
-
await this.reconcileRegistryCwds();
|
|
48
|
-
this.spawnDesiredEntries();
|
|
49
|
-
this.reconcileCron();
|
|
50
|
-
this.runCatchup();
|
|
51
|
-
const interval = this.opts.reconcileIntervalMs ?? RECONCILE_INTERVAL_MS;
|
|
52
|
-
this.reconcileTimer = setInterval(() => { void this.reconcileRegistryCwds(); }, interval);
|
|
53
|
-
this.reconcileTimer.unref();
|
|
54
|
-
}
|
|
55
|
-
async stop() {
|
|
56
|
-
this.shuttingDown = true;
|
|
57
|
-
if (this.reconcileTimer)
|
|
58
|
-
clearInterval(this.reconcileTimer);
|
|
59
|
-
this.reconcileTimer = null;
|
|
60
|
-
for (const cron of this.cronJobs.values())
|
|
61
|
-
cron.stop();
|
|
62
|
-
this.cronJobs.clear();
|
|
63
|
-
const server = this.server;
|
|
64
|
-
this.server = null;
|
|
65
|
-
const serverClosed = new Promise((resolve) => server ? server.close(() => resolve()) : resolve());
|
|
66
|
-
for (const [socket, state] of this.controlSockets) {
|
|
67
|
-
if (!state.requestStarted)
|
|
68
|
-
socket.destroy();
|
|
69
|
-
}
|
|
70
|
-
for (const slot of this.children.values()) {
|
|
71
|
-
this.cancelRetry(slot);
|
|
72
|
-
this.cancelStabilityReset(slot);
|
|
73
|
-
}
|
|
74
|
-
await Promise.all([...this.activeRequests]);
|
|
75
|
-
while (this.lifecycleQueues.size > 0)
|
|
76
|
-
await Promise.all([...this.lifecycleQueues.values()]);
|
|
77
|
-
await Promise.all([...this.children.values()].map(async (slot) => slot.child.stop()));
|
|
78
|
-
this.children.clear();
|
|
79
|
-
await serverClosed;
|
|
80
|
-
if (!usesNamedPipe()) {
|
|
81
|
-
try {
|
|
82
|
-
unlinkSync(supervisorSockPath());
|
|
83
|
-
}
|
|
84
|
-
catch { /* socket already absent */ }
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
mkdirParent() {
|
|
88
|
-
if (!usesNamedPipe())
|
|
89
|
-
mkdirSync(dirname(supervisorSockPath()), { recursive: true });
|
|
90
|
-
}
|
|
91
|
-
async bindUds() {
|
|
92
|
-
const path = supervisorSockPath();
|
|
93
|
-
const namedPipe = usesNamedPipe();
|
|
94
|
-
if (namedPipe || existsSync(path)) {
|
|
95
|
-
if (await probeSupervisor(path))
|
|
96
|
-
throw new SupervisorAlreadyRunningError(path);
|
|
97
|
-
if (!namedPipe) {
|
|
98
|
-
try {
|
|
99
|
-
unlinkSync(path);
|
|
100
|
-
}
|
|
101
|
-
catch { /* bind reports an actual race */ }
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const server = createServer((socket) => this.onConnection(socket));
|
|
105
|
-
await new Promise((resolve, reject) => {
|
|
106
|
-
server.once("error", reject);
|
|
107
|
-
server.listen(path, () => resolve());
|
|
108
|
-
});
|
|
109
|
-
this.server = server;
|
|
110
|
-
}
|
|
111
|
-
onConnection(socket) {
|
|
112
|
-
if (this.shuttingDown) {
|
|
113
|
-
socket.destroy();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const state = { requestStarted: false };
|
|
117
|
-
this.controlSockets.set(socket, state);
|
|
118
|
-
let buffer = "";
|
|
119
|
-
socket.setEncoding("utf8");
|
|
120
|
-
socket.on("data", (chunk) => {
|
|
121
|
-
if (state.requestStarted || this.shuttingDown)
|
|
122
|
-
return;
|
|
123
|
-
buffer += chunk;
|
|
124
|
-
const newline = buffer.indexOf("\n");
|
|
125
|
-
if (newline < 0)
|
|
126
|
-
return;
|
|
127
|
-
state.requestStarted = true;
|
|
128
|
-
let task;
|
|
129
|
-
task = this.handleRequest(buffer.slice(0, newline)).then((reply) => { socket.end(encodeReply(reply)); }, (error) => { socket.end(encodeReply({ ok: false, error: String(error) })); }).finally(() => this.activeRequests.delete(task));
|
|
130
|
-
this.activeRequests.add(task);
|
|
131
|
-
});
|
|
132
|
-
socket.on("close", () => this.controlSockets.delete(socket));
|
|
133
|
-
socket.on("error", () => { });
|
|
134
|
-
}
|
|
135
|
-
async handleRequest(line) {
|
|
136
|
-
let request;
|
|
137
|
-
try {
|
|
138
|
-
request = parseRequest(line);
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
return { ok: false, error: error.message };
|
|
142
|
-
}
|
|
143
|
-
await this.reconcileRegistryCwds();
|
|
144
|
-
switch (request.op) {
|
|
145
|
-
case "list": return { ok: true, data: { daemons: this.listInfo() } };
|
|
146
|
-
case "status": return { ok: true, data: { daemons: this.listInfo() } };
|
|
147
|
-
case "start_all": return this.startAll();
|
|
148
|
-
case "start": return this.startOne(request.id);
|
|
149
|
-
case "stop_all": return this.stopAll();
|
|
150
|
-
case "stop": return this.stopOne(request.id);
|
|
151
|
-
case "restart_all": return this.restartAll();
|
|
152
|
-
case "restart": return this.restartOne(request.id);
|
|
153
|
-
case "send": return this.send(request.id, request.text);
|
|
154
|
-
case "register": return this.register(request.cwd);
|
|
155
|
-
case "unregister": return this.unregister(request.id);
|
|
156
|
-
case "unregister_cwd": return this.unregisterCwd(request.cwd);
|
|
157
|
-
case "cron_add": return this.cronAdd(request);
|
|
158
|
-
case "cron_list": return { ok: true, data: { jobs: listCronJobs().map((job) => this.jobView(job)) } };
|
|
159
|
-
case "cron_remove": return this.cronRemove(request.job_id);
|
|
160
|
-
case "cron_enable": return this.cronEnable(request.job_id, request.enabled);
|
|
161
|
-
case "cron_run": return this.cronRun(request.job_id);
|
|
162
|
-
case "cron_log": return { ok: true, data: { entries: readCronLog({ jobId: request.job_id, tail: request.tail }) } };
|
|
163
|
-
default: return { ok: false, error: `unknown op: ${request.op}` };
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
listInfo() {
|
|
167
|
-
return listDaemons().map((entry) => this.infoFor(entry));
|
|
168
|
-
}
|
|
169
|
-
infoFor(entry) {
|
|
170
|
-
const slot = this.children.get(entry.id);
|
|
171
|
-
const snapshot = {
|
|
172
|
-
id: entry.id,
|
|
173
|
-
cwd: entry.cwd,
|
|
174
|
-
name: entry.name,
|
|
175
|
-
registration: existsSync(entry.cwd) ? "registered" : "missing",
|
|
176
|
-
desired: entry.desired_state,
|
|
177
|
-
process: slot?.child.processState ?? "absent",
|
|
178
|
-
runtime: slot?.child.runtimeState ?? "pending",
|
|
179
|
-
relay: slot?.child.relayState ?? "disconnected",
|
|
180
|
-
...(slot?.child.runtimeInstanceId ? { runtimeInstanceId: slot.child.runtimeInstanceId } : {}),
|
|
181
|
-
...(slot?.child.pid !== undefined ? { pid: slot.child.pid } : {}),
|
|
182
|
-
...(slot?.child.startedAt !== undefined ? { startedAt: slot.child.startedAt } : {}),
|
|
183
|
-
...(slot?.child.uptimeMs !== undefined ? { uptimeMs: slot.child.uptimeMs } : {}),
|
|
184
|
-
restartCount: slot?.child.restartCount ?? 0,
|
|
185
|
-
retrying: !!slot?.restartTimer,
|
|
186
|
-
...(slot?.nextRetryAt ? { nextRetryAt: slot.nextRetryAt } : {}),
|
|
187
|
-
blocked: !!slot?.blocked || slot?.child.state === "blocked",
|
|
188
|
-
...(slot?.error ? { error: slot.error } : {}),
|
|
189
|
-
};
|
|
190
|
-
return infoFor(snapshot);
|
|
191
|
-
}
|
|
192
|
-
async startAll() {
|
|
193
|
-
const started = [];
|
|
194
|
-
const alreadyRunning = [];
|
|
195
|
-
for (const entry of listDaemons()) {
|
|
196
|
-
const result = await this.startEntry(entry);
|
|
197
|
-
(result ? alreadyRunning : started).push(entry.id);
|
|
198
|
-
}
|
|
199
|
-
return { ok: true, data: { started, already_running: alreadyRunning } };
|
|
200
|
-
}
|
|
201
|
-
async startOne(id) {
|
|
202
|
-
const entry = listDaemons().find((item) => item.id === id);
|
|
203
|
-
if (!entry)
|
|
204
|
-
return { ok: false, error: `no daemon with id ${id}` };
|
|
205
|
-
const alreadyRunning = await this.startEntry(entry);
|
|
206
|
-
const updated = listDaemons().find((item) => item.id === id);
|
|
207
|
-
if (!updated)
|
|
208
|
-
return { ok: false, error: `no daemon with id ${id}` };
|
|
209
|
-
const info = this.infoFor(updated);
|
|
210
|
-
return { ok: true, data: { id, state: info.state, started: !alreadyRunning, desired_state: "running" } };
|
|
211
|
-
}
|
|
212
|
-
startEntry(entry) {
|
|
213
|
-
return this.enqueueLifecycle(entry.id, async () => {
|
|
214
|
-
const updated = setDaemonDesiredState(entry.id, "running");
|
|
215
|
-
if (!updated)
|
|
216
|
-
return true;
|
|
217
|
-
const slot = this.children.get(entry.id);
|
|
218
|
-
if (slot)
|
|
219
|
-
slot.entry = updated;
|
|
220
|
-
if (slot && (slot.child.processState === "running" || slot.child.processState === "spawning") && !slot.blocked)
|
|
221
|
-
return true;
|
|
222
|
-
if (slot) {
|
|
223
|
-
this.cancelRetry(slot);
|
|
224
|
-
slot.blocked = false;
|
|
225
|
-
slot.error = undefined;
|
|
226
|
-
slot.entry = updated;
|
|
227
|
-
if (slot.child.processState === "exited" || slot.child.pid === undefined) {
|
|
228
|
-
slot.child.spawn();
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
await slot.child.stop();
|
|
232
|
-
if (this.children.get(entry.id) === slot && !this.shuttingDown && slot.entry.desired_state === "running")
|
|
233
|
-
slot.child.spawn();
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
if (!this.shuttingDown)
|
|
237
|
-
this.createSlot(updated);
|
|
238
|
-
return false;
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
async stopAll() {
|
|
242
|
-
const stopped = [], alreadyStopped = [];
|
|
243
|
-
for (const entry of listDaemons())
|
|
244
|
-
(await this.stopEntry(entry) ? stopped : alreadyStopped).push(entry.id);
|
|
245
|
-
return { ok: true, data: { stopped, already_stopped: alreadyStopped } };
|
|
246
|
-
}
|
|
247
|
-
async stopOne(id) {
|
|
248
|
-
const entry = listDaemons().find((item) => item.id === id);
|
|
249
|
-
if (!entry)
|
|
250
|
-
return { ok: false, error: `no daemon with id ${id}` };
|
|
251
|
-
const stopped = await this.stopEntry(entry);
|
|
252
|
-
const updated = listDaemons().find((item) => item.id === id);
|
|
253
|
-
if (!updated)
|
|
254
|
-
return { ok: false, error: `no daemon with id ${id}` };
|
|
255
|
-
const info = this.infoFor(updated);
|
|
256
|
-
return { ok: true, data: { id, state: info.state, stopped, desired_state: "stopped" } };
|
|
257
|
-
}
|
|
258
|
-
stopEntry(entry) {
|
|
259
|
-
return this.enqueueLifecycle(entry.id, async () => {
|
|
260
|
-
const updated = setDaemonDesiredState(entry.id, "stopped");
|
|
261
|
-
if (!updated)
|
|
262
|
-
return false;
|
|
263
|
-
const slot = this.children.get(entry.id);
|
|
264
|
-
if (slot)
|
|
265
|
-
slot.entry = updated;
|
|
266
|
-
if (!slot)
|
|
267
|
-
return false;
|
|
268
|
-
this.cancelRetry(slot);
|
|
269
|
-
this.cancelStabilityReset(slot);
|
|
270
|
-
const wasLive = slot.child.processState === "running" || slot.child.processState === "spawning";
|
|
271
|
-
await slot.child.stop();
|
|
272
|
-
return wasLive;
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
async restartOne(id) {
|
|
276
|
-
const entry = listDaemons().find((item) => item.id === id);
|
|
277
|
-
if (!entry)
|
|
278
|
-
return { ok: false, error: `no daemon with id ${id}` };
|
|
279
|
-
await this.restartEntry(entry);
|
|
280
|
-
const updated = listDaemons().find((item) => item.id === id);
|
|
281
|
-
if (!updated)
|
|
282
|
-
return { ok: false, error: `no daemon with id ${id}` };
|
|
283
|
-
const info = this.infoFor(updated);
|
|
284
|
-
return { ok: true, data: { id, state: info.state, restarted: true, desired_state: "running" } };
|
|
285
|
-
}
|
|
286
|
-
async restartAll() {
|
|
287
|
-
const restarted = [];
|
|
288
|
-
for (const entry of listDaemons()) {
|
|
289
|
-
await this.restartEntry(entry);
|
|
290
|
-
restarted.push(entry.id);
|
|
291
|
-
}
|
|
292
|
-
return { ok: true, data: { restarted } };
|
|
293
|
-
}
|
|
294
|
-
restartEntry(entry) {
|
|
295
|
-
return this.enqueueLifecycle(entry.id, async () => {
|
|
296
|
-
const updated = setDaemonDesiredState(entry.id, "running");
|
|
297
|
-
if (!updated)
|
|
298
|
-
return;
|
|
299
|
-
const existing = this.children.get(entry.id);
|
|
300
|
-
if (existing) {
|
|
301
|
-
this.cancelRetry(existing);
|
|
302
|
-
this.cancelStabilityReset(existing);
|
|
303
|
-
existing.blocked = false;
|
|
304
|
-
existing.error = undefined;
|
|
305
|
-
await existing.child.stop();
|
|
306
|
-
if (this.children.get(entry.id) === existing)
|
|
307
|
-
this.children.delete(entry.id);
|
|
308
|
-
}
|
|
309
|
-
if (!this.shuttingDown)
|
|
310
|
-
this.createSlot(updated);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
async send(id, text) {
|
|
314
|
-
const entry = listDaemons().find((item) => item.id === id);
|
|
315
|
-
if (!entry)
|
|
316
|
-
return { ok: false, error: `no daemon with id ${id}` };
|
|
317
|
-
if (!existsSync(entry.cwd)) {
|
|
318
|
-
return { ok: true, data: { id, accepted: false, delivered: false, code: "cwd_missing", error: "daemon cwd no longer exists" } };
|
|
319
|
-
}
|
|
320
|
-
const info = this.infoFor(entry);
|
|
321
|
-
if (info.desired !== "running" || info.runtime !== "ready") {
|
|
322
|
-
return { ok: true, data: { id, accepted: false, delivered: false, code: "runtime_not_ready", error: `daemon is ${info.health}` } };
|
|
323
|
-
}
|
|
324
|
-
const slot = this.children.get(id);
|
|
325
|
-
if (!slot)
|
|
326
|
-
return { ok: true, data: { id, accepted: false, delivered: false, code: "runtime_missing" } };
|
|
327
|
-
const acceptance = await slot.child.sendPrompt(text);
|
|
328
|
-
return { ok: true, data: {
|
|
329
|
-
id,
|
|
330
|
-
accepted: acceptance.accepted,
|
|
331
|
-
delivered: acceptance.accepted,
|
|
332
|
-
...(acceptance.code ? { code: acceptance.code } : {}),
|
|
333
|
-
...(acceptance.error ? { error: acceptance.error } : {}),
|
|
334
|
-
} };
|
|
335
|
-
}
|
|
336
|
-
async register(cwd) {
|
|
337
|
-
try {
|
|
338
|
-
const entry = addDaemon(cwd);
|
|
339
|
-
await this.startEntry(entry);
|
|
340
|
-
return { ok: true, data: { id: entry.id, cwd: entry.cwd, name: entry.name, desired_state: entry.desired_state } };
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
return { ok: false, error: error.message };
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
unregister(id) {
|
|
347
|
-
return this.enqueueLifecycle(id, async () => {
|
|
348
|
-
const slot = this.children.get(id);
|
|
349
|
-
if (slot) {
|
|
350
|
-
this.cancelRetry(slot);
|
|
351
|
-
this.cancelStabilityReset(slot);
|
|
352
|
-
await slot.child.stop();
|
|
353
|
-
if (this.children.get(id) === slot)
|
|
354
|
-
this.children.delete(id);
|
|
355
|
-
}
|
|
356
|
-
try {
|
|
357
|
-
return { ok: true, data: removeDaemon(id) };
|
|
358
|
-
}
|
|
359
|
-
catch (error) {
|
|
360
|
-
return { ok: false, error: error.message };
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
async unregisterCwd(cwd) {
|
|
365
|
-
const entry = findDaemonByCwd(cwd);
|
|
366
|
-
if (!entry)
|
|
367
|
-
return { ok: true, data: { removed: false } };
|
|
368
|
-
return this.unregister(entry.id);
|
|
369
|
-
}
|
|
370
|
-
cronAdd(request) {
|
|
371
|
-
const validation = validateSchedule(request.schedule, request.tz);
|
|
372
|
-
if (!validation.ok)
|
|
373
|
-
return { ok: false, error: validation.error ?? "invalid schedule" };
|
|
374
|
-
const input = { daemon_id: request.daemon_id, schedule: request.schedule, prompt: request.prompt };
|
|
375
|
-
if (request.tz !== undefined)
|
|
376
|
-
input.tz = request.tz;
|
|
377
|
-
if (request.skip_if_busy !== undefined)
|
|
378
|
-
input.skip_if_busy = request.skip_if_busy;
|
|
379
|
-
if (request.catchup !== undefined)
|
|
380
|
-
input.catchup = request.catchup;
|
|
381
|
-
const job = addCronJob(input);
|
|
382
|
-
this.scheduleCron(job);
|
|
383
|
-
return { ok: true, data: { job: this.jobView(job) } };
|
|
384
|
-
}
|
|
385
|
-
cronRemove(jobId) {
|
|
386
|
-
const removed = removeCronJob(jobId);
|
|
387
|
-
this.stopCron(jobId);
|
|
388
|
-
return { ok: true, data: { removed } };
|
|
389
|
-
}
|
|
390
|
-
cronEnable(jobId, enabled) {
|
|
391
|
-
const updated = setJobEnabled(jobId, enabled);
|
|
392
|
-
if (updated) {
|
|
393
|
-
this.stopCron(jobId);
|
|
394
|
-
const job = getCronJob(jobId);
|
|
395
|
-
if (enabled && job)
|
|
396
|
-
this.scheduleCron(job);
|
|
397
|
-
}
|
|
398
|
-
return { ok: true, data: { job_id: jobId, enabled, updated } };
|
|
399
|
-
}
|
|
400
|
-
async cronRun(jobId) {
|
|
401
|
-
if (!getCronJob(jobId))
|
|
402
|
-
return { ok: false, error: `no cron job with id ${jobId}` };
|
|
403
|
-
return { ok: true, data: { job_id: jobId, result: await this.fireJob(jobId, { manual: true }) } };
|
|
404
|
-
}
|
|
405
|
-
jobView(job) {
|
|
406
|
-
const next = nextRunFor(job);
|
|
407
|
-
return { ...job, next_run: next?.toISOString() ?? null };
|
|
408
|
-
}
|
|
409
|
-
reconcileCron() {
|
|
410
|
-
for (const cron of this.cronJobs.values())
|
|
411
|
-
cron.stop();
|
|
412
|
-
this.cronJobs.clear();
|
|
413
|
-
for (const job of listCronJobs())
|
|
414
|
-
if (job.enabled)
|
|
415
|
-
this.scheduleCron(job);
|
|
416
|
-
}
|
|
417
|
-
scheduleCron(job) {
|
|
418
|
-
this.stopCron(job.id);
|
|
419
|
-
try {
|
|
420
|
-
const options = job.tz ? { timezone: job.tz, name: job.id } : { name: job.id };
|
|
421
|
-
this.cronJobs.set(job.id, new Cron(job.schedule, options, () => { void this.fireJob(job.id); }));
|
|
422
|
-
}
|
|
423
|
-
catch (error) {
|
|
424
|
-
process.stderr.write(`[remote-pi-supervisord] cron schedule failed for ${job.id}: ${String(error)}\n`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
stopCron(jobId) {
|
|
428
|
-
const cron = this.cronJobs.get(jobId);
|
|
429
|
-
if (cron) {
|
|
430
|
-
cron.stop();
|
|
431
|
-
this.cronJobs.delete(jobId);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
runCatchup() {
|
|
435
|
-
for (const job of listCronJobs()) {
|
|
436
|
-
if (!job.enabled || !job.catchup)
|
|
437
|
-
continue;
|
|
438
|
-
try {
|
|
439
|
-
const cron = new Cron(job.schedule, job.tz ? { timezone: job.tz } : {});
|
|
440
|
-
const previous = cron.previousRun();
|
|
441
|
-
cron.stop();
|
|
442
|
-
if (previous && previous.getTime() > (job.last_run ? Date.parse(job.last_run) : 0)) {
|
|
443
|
-
void this.fireJob(job.id, { manual: true });
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
catch { /* schedule was checked when added; corrupt persisted data simply skips */ }
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
async fireJob(jobId, options = {}) {
|
|
450
|
-
const job = getCronJob(jobId);
|
|
451
|
-
if (!job)
|
|
452
|
-
return "missing";
|
|
453
|
-
let result;
|
|
454
|
-
if (!job.enabled && !options.manual) {
|
|
455
|
-
result = "skipped_disabled";
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
const entry = listDaemons().find((item) => item.id === job.daemon_id);
|
|
459
|
-
const cwdExists = !!entry && existsSync(entry.cwd);
|
|
460
|
-
const slot = cwdExists && entry ? this.children.get(entry.id) : undefined;
|
|
461
|
-
const info = cwdExists && entry ? this.infoFor(entry) : undefined;
|
|
462
|
-
const busy = !!slot && info?.runtime === "ready" && job.skip_if_busy ? await slot.child.refreshBusy() : false;
|
|
463
|
-
const action = decideFireAction({
|
|
464
|
-
exists: cwdExists,
|
|
465
|
-
desired: entry?.desired_state ?? "stopped",
|
|
466
|
-
runtime: info?.runtime ?? "pending",
|
|
467
|
-
health: info?.health ?? "failed",
|
|
468
|
-
busy,
|
|
469
|
-
skipIfBusy: job.skip_if_busy,
|
|
470
|
-
retrying: info?.retrying ?? false,
|
|
471
|
-
});
|
|
472
|
-
if (action === "send") {
|
|
473
|
-
result = slot && (await slot.child.sendPrompt(job.prompt)).accepted ? "accepted" : "rejected";
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
result = this.cronResultFor(action);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
const at = new Date().toISOString();
|
|
480
|
-
recordRun(job.id, at, result);
|
|
481
|
-
appendCronLog({ job_id: job.id, daemon_id: job.daemon_id, schedule: job.schedule, result, prompt: job.prompt });
|
|
482
|
-
return result;
|
|
483
|
-
}
|
|
484
|
-
cronResultFor(action) {
|
|
485
|
-
switch (action) {
|
|
486
|
-
case "skip_busy": return "skipped_busy";
|
|
487
|
-
case "skip_desired_stopped": return "skipped_desired_stopped";
|
|
488
|
-
case "skip_starting": return "skipped_starting";
|
|
489
|
-
case "skip_retrying": return "skipped_retrying";
|
|
490
|
-
case "skip_failed": return "skipped_failed";
|
|
491
|
-
case "skip_blocked": return "skipped_blocked";
|
|
492
|
-
case "skip_missing": return "skipped_missing";
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
spawnDesiredEntries() {
|
|
496
|
-
for (const entry of listDaemons())
|
|
497
|
-
if (entry.desired_state === "running")
|
|
498
|
-
this.createSlot(entry);
|
|
499
|
-
}
|
|
500
|
-
enqueueLifecycle(id, operation) {
|
|
501
|
-
const previous = this.lifecycleQueues.get(id) ?? Promise.resolve();
|
|
502
|
-
let release;
|
|
503
|
-
const current = new Promise((resolve) => { release = resolve; });
|
|
504
|
-
this.lifecycleQueues.set(id, current);
|
|
505
|
-
return previous.then(operation).finally(() => {
|
|
506
|
-
release();
|
|
507
|
-
if (this.lifecycleQueues.get(id) === current)
|
|
508
|
-
this.lifecycleQueues.delete(id);
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
createSlot(entry) {
|
|
512
|
-
const config = { agent_name: entry.name || defaultAgentName(entry.cwd), auto_start_relay: true };
|
|
513
|
-
const childOptions = { endpointId: entry.id, cwd: entry.cwd, config };
|
|
514
|
-
if (this.opts.piBin)
|
|
515
|
-
childOptions.piBin = this.opts.piBin;
|
|
516
|
-
const child = this.opts.childFactory?.(childOptions) ?? new RpcChild(childOptions);
|
|
517
|
-
const slot = {
|
|
518
|
-
entry,
|
|
519
|
-
child,
|
|
520
|
-
restartTimer: null,
|
|
521
|
-
stabilityTimer: null,
|
|
522
|
-
restartAttempt: 0,
|
|
523
|
-
blocked: false,
|
|
524
|
-
missingCwdObservations: 0,
|
|
525
|
-
reconcileInProgress: false,
|
|
526
|
-
};
|
|
527
|
-
this.children.set(entry.id, slot);
|
|
528
|
-
child.on("exit", (event) => this.onChildExit(entry.id, slot, event));
|
|
529
|
-
child.on("runtime_ready", () => this.onRuntimeReady(entry.id, slot));
|
|
530
|
-
child.on("runtime_failed", (event) => this.onRuntimeFailed(entry.id, slot, event));
|
|
531
|
-
child.spawn();
|
|
532
|
-
}
|
|
533
|
-
onRuntimeReady(id, slot) {
|
|
534
|
-
if (this.children.get(id) !== slot || this.shuttingDown || slot.entry.desired_state !== "running")
|
|
535
|
-
return;
|
|
536
|
-
if (slot.stabilityTimer)
|
|
537
|
-
clearTimeout(slot.stabilityTimer);
|
|
538
|
-
slot.stabilityTimer = setTimeout(() => {
|
|
539
|
-
slot.stabilityTimer = null;
|
|
540
|
-
if (this.children.get(id) === slot && slot.child.runtimeState === "ready" && !slot.blocked)
|
|
541
|
-
slot.restartAttempt = 0;
|
|
542
|
-
}, RESTART_STABILITY_MS);
|
|
543
|
-
slot.stabilityTimer.unref();
|
|
544
|
-
}
|
|
545
|
-
onRuntimeFailed(id, slot, event) {
|
|
546
|
-
if (this.children.get(id) !== slot || this.shuttingDown || slot.entry.desired_state === "stopped")
|
|
547
|
-
return;
|
|
548
|
-
slot.error = { code: event.code, message: event.message, at: Date.now(), retryable: event.retryable, stage: event.stage ?? "runtime" };
|
|
549
|
-
if (!event.retryable) {
|
|
550
|
-
slot.blocked = true;
|
|
551
|
-
this.cancelRetry(slot);
|
|
552
|
-
this.cancelStabilityReset(slot);
|
|
553
|
-
void this.enqueueLifecycle(id, () => this.children.get(id) === slot ? slot.child.stop() : undefined);
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
this.scheduleRetry(slot, "runtime failure");
|
|
557
|
-
void this.enqueueLifecycle(id, () => this.children.get(id) === slot ? slot.child.stop() : undefined);
|
|
558
|
-
}
|
|
559
|
-
onChildExit(id, slot, event) {
|
|
560
|
-
if (this.children.get(id) !== slot || this.shuttingDown || slot.entry.desired_state === "stopped" || slot.blocked)
|
|
561
|
-
return;
|
|
562
|
-
this.cancelStabilityReset(slot);
|
|
563
|
-
if (!event.isCrash)
|
|
564
|
-
return;
|
|
565
|
-
slot.error ??= { code: "child_exited", message: `child exited (${String(event.code ?? event.signal)})`, at: Date.now(), retryable: true, stage: "process" };
|
|
566
|
-
this.scheduleRetry(slot, "child exit");
|
|
567
|
-
}
|
|
568
|
-
scheduleRetry(slot, _reason) {
|
|
569
|
-
if (slot.restartTimer || slot.blocked || slot.entry.desired_state !== "running")
|
|
570
|
-
return;
|
|
571
|
-
if (slot.restartAttempt >= RESTART_BACKOFFS_MS.length) {
|
|
572
|
-
slot.error = { code: "retry_exhausted", message: "transient restart budget exhausted", at: Date.now(), retryable: false, stage: "retry" };
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const delay = RESTART_BACKOFFS_MS[slot.restartAttempt];
|
|
576
|
-
slot.nextRetryAt = Date.now() + delay;
|
|
577
|
-
slot.restartTimer = setTimeout(() => {
|
|
578
|
-
slot.restartTimer = null;
|
|
579
|
-
slot.nextRetryAt = undefined;
|
|
580
|
-
void this.enqueueLifecycle(slot.entry.id, () => {
|
|
581
|
-
if (this.children.get(slot.entry.id) !== slot || this.shuttingDown || slot.blocked || slot.entry.desired_state !== "running")
|
|
582
|
-
return;
|
|
583
|
-
slot.restartAttempt += 1;
|
|
584
|
-
slot.child.noteRestart();
|
|
585
|
-
slot.child.spawn();
|
|
586
|
-
});
|
|
587
|
-
}, delay);
|
|
588
|
-
}
|
|
589
|
-
cancelRetry(slot) {
|
|
590
|
-
if (slot.restartTimer)
|
|
591
|
-
clearTimeout(slot.restartTimer);
|
|
592
|
-
slot.restartTimer = null;
|
|
593
|
-
slot.nextRetryAt = undefined;
|
|
594
|
-
}
|
|
595
|
-
cancelStabilityReset(slot) {
|
|
596
|
-
if (slot.stabilityTimer)
|
|
597
|
-
clearTimeout(slot.stabilityTimer);
|
|
598
|
-
slot.stabilityTimer = null;
|
|
599
|
-
}
|
|
600
|
-
async reconcileRegistryCwds() {
|
|
601
|
-
if (this.reconcileRunning)
|
|
602
|
-
return;
|
|
603
|
-
this.reconcileRunning = true;
|
|
604
|
-
try {
|
|
605
|
-
for (const entry of listDaemons()) {
|
|
606
|
-
const slot = this.children.get(entry.id);
|
|
607
|
-
if (existsSync(entry.cwd)) {
|
|
608
|
-
if (slot)
|
|
609
|
-
slot.missingCwdObservations = 0;
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
if (!slot) {
|
|
613
|
-
removeDaemon(entry.id);
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
slot.missingCwdObservations += 1;
|
|
617
|
-
if (slot.missingCwdObservations < MISSING_CWD_CONFIRMATIONS || slot.reconcileInProgress)
|
|
618
|
-
continue;
|
|
619
|
-
slot.reconcileInProgress = true;
|
|
620
|
-
this.cancelRetry(slot);
|
|
621
|
-
await this.enqueueLifecycle(entry.id, async () => {
|
|
622
|
-
if (this.children.get(entry.id) !== slot)
|
|
623
|
-
return;
|
|
624
|
-
await slot.child.stop();
|
|
625
|
-
if (this.children.get(entry.id) === slot)
|
|
626
|
-
this.children.delete(entry.id);
|
|
627
|
-
removeDaemon(entry.id);
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
finally {
|
|
632
|
-
this.reconcileRunning = false;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
export function getSupervisorSockPath() { return supervisorSockPath(); }
|
|
637
|
-
//# sourceMappingURL=supervisor.js.map
|