@songsid/agend 2.1.2-beta.39 → 2.1.2-beta.40
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/dist/cli.js +51 -20
- package/dist/cli.js.map +1 -1
- package/dist/fleet-lock.d.ts +28 -0
- package/dist/fleet-lock.js +130 -0
- package/dist/fleet-lock.js.map +1 -0
- package/dist/fleet-manager.js +4 -0
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +1 -2
- package/dist/instance-lifecycle.js +2 -5
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/service-installer.d.ts +16 -0
- package/dist/service-installer.js +56 -1
- package/dist/service-installer.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
/** Whether a process command line identifies an AgEnD fleet process. */
|
|
6
|
+
export function isFleetStartCommandLine(commandLine) {
|
|
7
|
+
const normalized = commandLine.replace(/\0/g, " ").replace(/\s+/g, " ").trim();
|
|
8
|
+
return /\b(?:agend|(?:cli|daemon-entry)\.(?:js|ts))\b.*\bfleet\s+start\b/i.test(normalized);
|
|
9
|
+
}
|
|
10
|
+
function processAlive(pid) {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function processCommandLine(pid) {
|
|
20
|
+
try {
|
|
21
|
+
return readFileSync(`/proc/${pid}/cmdline`, "utf8").replace(/\0/g, " ").trim();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
try {
|
|
25
|
+
return execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
28
|
+
timeout: 1000,
|
|
29
|
+
}).trim();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function parseRecord(raw) {
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(raw);
|
|
39
|
+
if (!Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 1 || typeof value.nonce !== "string") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Atomically claim one fleet process per AGEND_HOME. O_EXCL is portable and,
|
|
50
|
+
* unlike fleet.pid, is acquired before FleetManager can mutate tmux or sockets.
|
|
51
|
+
*/
|
|
52
|
+
export function acquireFleetLock(dataDir, probe = {}) {
|
|
53
|
+
mkdirSync(dataDir, { recursive: true });
|
|
54
|
+
const lockPath = join(dataDir, "fleet.lock");
|
|
55
|
+
const pid = probe.pid ?? process.pid;
|
|
56
|
+
const record = {
|
|
57
|
+
pid,
|
|
58
|
+
nonce: probe.nonce ?? randomBytes(16).toString("hex"),
|
|
59
|
+
createdAt: new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
const serialized = JSON.stringify(record) + "\n";
|
|
62
|
+
const isAlive = probe.isProcessAlive ?? processAlive;
|
|
63
|
+
const readCommandLine = probe.readCommandLine ?? processCommandLine;
|
|
64
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
65
|
+
try {
|
|
66
|
+
writeFileSync(lockPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
67
|
+
return { path: lockPath, record, serialized };
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err.code !== "EEXIST")
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
let observed = "";
|
|
74
|
+
try {
|
|
75
|
+
observed = readFileSync(lockPath, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// The owner may have exited between EEXIST and read; retry O_EXCL.
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const owner = parseRecord(observed);
|
|
82
|
+
if (owner && isAlive(owner.pid)) {
|
|
83
|
+
const commandLine = readCommandLine(owner.pid);
|
|
84
|
+
if (isFleetStartCommandLine(commandLine)) {
|
|
85
|
+
throw new Error(`Fleet is already running (PID ${owner.pid}, lock: ${lockPath})`);
|
|
86
|
+
}
|
|
87
|
+
// An alive PID with an unreadable command line is not safe to steal from.
|
|
88
|
+
if (!commandLine) {
|
|
89
|
+
throw new Error(`Fleet lock is owned by live PID ${owner.pid}; refusing to replace it`);
|
|
90
|
+
}
|
|
91
|
+
// The PID was reused by an unrelated process: the lock is stale.
|
|
92
|
+
}
|
|
93
|
+
// Only unlink the exact stale record we inspected. A competing starter may
|
|
94
|
+
// already have replaced it; in that case leave its lock untouched and retry.
|
|
95
|
+
try {
|
|
96
|
+
if (readFileSync(lockPath, "utf8") === observed)
|
|
97
|
+
unlinkSync(lockPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Another contender changed/removed the file; retry the atomic claim.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Unable to acquire fleet lock at ${lockPath}`);
|
|
104
|
+
}
|
|
105
|
+
/** Delete only the exact lock record acquired by this process. */
|
|
106
|
+
export function releaseFleetLock(handle) {
|
|
107
|
+
if (!handle || !existsSync(handle.path))
|
|
108
|
+
return false;
|
|
109
|
+
try {
|
|
110
|
+
if (readFileSync(handle.path, "utf8") !== handle.serialized)
|
|
111
|
+
return false;
|
|
112
|
+
unlinkSync(handle.path);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let processFleetLock;
|
|
120
|
+
export function setProcessFleetLock(handle) {
|
|
121
|
+
processFleetLock = handle;
|
|
122
|
+
}
|
|
123
|
+
export function releaseProcessFleetLock() {
|
|
124
|
+
const handle = processFleetLock;
|
|
125
|
+
const released = releaseFleetLock(handle);
|
|
126
|
+
if (released)
|
|
127
|
+
processFleetLock = undefined;
|
|
128
|
+
return released;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=fleet-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet-lock.js","sourceRoot":"","sources":["../src/fleet-lock.ts"],"names":[],"mappings":"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"}
|
package/dist/fleet-manager.js
CHANGED
|
@@ -49,6 +49,7 @@ import { ClassicChannelManager, getClassicBackendChoices, isSelectableClassicBac
|
|
|
49
49
|
import { validateFleetConfig } from "./config-validator.js";
|
|
50
50
|
import { readLastInboundAt } from "./daemon.js";
|
|
51
51
|
import { clearPausedMarker } from "./pause-marker.js";
|
|
52
|
+
import { releaseProcessFleetLock } from "./fleet-lock.js";
|
|
52
53
|
import { getTmuxSession } from "./config.js";
|
|
53
54
|
export function resolveReplyThreadId(argsThreadId, instanceConfig) {
|
|
54
55
|
if (typeof argsThreadId === "string" && argsThreadId.length > 0) {
|
|
@@ -6199,6 +6200,9 @@ When users create specialized instances, suggest these configurations:
|
|
|
6199
6200
|
catch (e) {
|
|
6200
6201
|
this.logger.debug({ err: e }, "Failed to remove fleet PID file");
|
|
6201
6202
|
}
|
|
6203
|
+
// The lock contains a nonce, so an older/shutting-down process can never
|
|
6204
|
+
// remove a lock acquired by a newer fleet owner.
|
|
6205
|
+
releaseProcessFleetLock();
|
|
6202
6206
|
}
|
|
6203
6207
|
/**
|
|
6204
6208
|
* Prune stale external sessions by re-querying each daemon for live sessions.
|