@botcord/daemon 0.2.81 → 0.2.83
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/daemon-singleton.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { PID_PATH } from "./config.js";
|
|
4
5
|
const noopLogger = {
|
|
5
6
|
info() {
|
|
@@ -137,7 +138,17 @@ export function ensureNoOtherDaemonFromPidFile(opts = {}) {
|
|
|
137
138
|
return null;
|
|
138
139
|
}
|
|
139
140
|
export function writeCurrentPid(opts = {}) {
|
|
140
|
-
|
|
141
|
+
const pidPath = opts.pidPath ?? PID_PATH;
|
|
142
|
+
// Cloud-mode startup writes the PID file before `saveConfig` runs, so
|
|
143
|
+
// the daemon dir may not exist yet. mkdir its parent (0700) so the
|
|
144
|
+
// first write doesn't crash with ENOENT.
|
|
145
|
+
try {
|
|
146
|
+
mkdirSync(path.dirname(pidPath), { recursive: true, mode: 0o700 });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// best-effort — writeFileSync below will surface the real error
|
|
150
|
+
}
|
|
151
|
+
writeFileSync(pidPath, String(opts.currentPid ?? process.pid), { mode: 0o600 });
|
|
141
152
|
}
|
|
142
153
|
export function removePidFile(pidPath = PID_PATH) {
|
|
143
154
|
try {
|
|
@@ -147,12 +158,30 @@ export function removePidFile(pidPath = PID_PATH) {
|
|
|
147
158
|
// ignore
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
|
-
function isBotCordDaemonStartCommand(command) {
|
|
161
|
+
export function isBotCordDaemonStartCommand(command) {
|
|
151
162
|
if (!/\bstart\b/.test(command))
|
|
152
163
|
return false;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
// Only treat a row as "the daemon" when argv[0] is the real entry point:
|
|
165
|
+
// either the node interpreter (running `dist/index.js`) or the resolved
|
|
166
|
+
// `botcord-daemon` bin shim. Reject shell wrappers (`sh`, `bash`, `npm`,
|
|
167
|
+
// `npx`, `timeout`, ...) — their argv mentions `botcord-daemon` only as a
|
|
168
|
+
// literal arg, not as the executable being run. Killing a wrapper takes
|
|
169
|
+
// out the actual daemon it started, which is exactly the bug we want
|
|
170
|
+
// to avoid in cloud sandboxes.
|
|
171
|
+
const exe = (command.trim().split(/\s+/, 1)[0] ?? "").split("/").pop() ?? "";
|
|
172
|
+
const isNode = /^node(\d.*)?$/.test(exe);
|
|
173
|
+
const isDaemonBin = exe === "botcord-daemon";
|
|
174
|
+
if (!isNode && !isDaemonBin)
|
|
175
|
+
return false;
|
|
176
|
+
return (
|
|
177
|
+
// node-resolved bin shim, e.g. .../node_modules/.bin/botcord-daemon
|
|
178
|
+
/\/\.?bin\/botcord-daemon(?:\s|$)/.test(command) ||
|
|
179
|
+
// direct bin invocation, e.g. /usr/local/bin/botcord-daemon or argv[0]=botcord-daemon
|
|
180
|
+
/(?:^|\s)\S*botcord-daemon(?:\s|$)/.test(command) ||
|
|
181
|
+
// node running the published daemon entry script
|
|
182
|
+
/\bbotcord\S*\/daemon\/dist\/index\.js(?:\s|$)/.test(command) ||
|
|
183
|
+
// node running the in-repo daemon entry script (dev / monorepo)
|
|
184
|
+
/\bpackages\/daemon\/dist\/index\.js(?:\s|$)/.test(command));
|
|
156
185
|
}
|
|
157
186
|
function delay(ms) {
|
|
158
187
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
6
|
import {
|
|
7
7
|
ensureNoOtherDaemonFromPidFile,
|
|
8
|
+
isBotCordDaemonStartCommand,
|
|
8
9
|
parseDaemonProcesses,
|
|
9
10
|
readPid,
|
|
10
11
|
removePidFile,
|
|
@@ -44,6 +45,17 @@ describe("daemon singleton pid helpers", () => {
|
|
|
44
45
|
expect(readFileSync(pidPath, "utf8")).toBe("12345");
|
|
45
46
|
});
|
|
46
47
|
|
|
48
|
+
it("creates the parent directory if missing (cloud-mode first boot)", () => {
|
|
49
|
+
// Cloud-mode start writes the PID file before `saveConfig` runs, so
|
|
50
|
+
// ~/.botcord/daemon/ may not exist yet. Without the mkdir the daemon
|
|
51
|
+
// crashes immediately with ENOENT.
|
|
52
|
+
const pidPath = path.join(tmpDir, "fresh-cloud-home", "daemon", "daemon.pid");
|
|
53
|
+
|
|
54
|
+
writeCurrentPid({ pidPath, currentPid: 7890 });
|
|
55
|
+
|
|
56
|
+
expect(readPid(pidPath)).toBe(7890);
|
|
57
|
+
});
|
|
58
|
+
|
|
47
59
|
it("does not report the current process as another daemon", () => {
|
|
48
60
|
const pidPath = path.join(tmpDir, "daemon.pid");
|
|
49
61
|
writeCurrentPid({ pidPath, currentPid: process.pid });
|
|
@@ -90,6 +102,38 @@ describe("daemon singleton pid helpers", () => {
|
|
|
90
102
|
expect(out.map((p) => p.pid)).toEqual([111, 222]);
|
|
91
103
|
});
|
|
92
104
|
|
|
105
|
+
it("does not match shell wrappers whose argv mentions botcord-daemon as a literal", () => {
|
|
106
|
+
// These are the wrapper command lines we observed in cloud sandboxes;
|
|
107
|
+
// they must NOT be classified as the daemon, otherwise the singleton
|
|
108
|
+
// check kills the wrapper and takes the actual daemon down with it.
|
|
109
|
+
const wrappers = [
|
|
110
|
+
"npm exec --yes --package @botcord/daemon@latest -- botcord-daemon start --foreground",
|
|
111
|
+
"npx --yes --package @botcord/daemon@latest -- botcord-daemon start --foreground",
|
|
112
|
+
"sh -c botcord-daemon start --foreground",
|
|
113
|
+
"/bin/bash -l -c export npm_config_cache=/tmp/c; npm exec --yes --package @botcord/daemon@latest -- botcord-daemon start --foreground",
|
|
114
|
+
"timeout 30 npm exec --yes --package @botcord/daemon@latest -- botcord-daemon start --foreground",
|
|
115
|
+
];
|
|
116
|
+
for (const cmd of wrappers) {
|
|
117
|
+
expect(isBotCordDaemonStartCommand(cmd), `wrongly matched wrapper: ${cmd}`).toBe(false);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("matches the actual daemon entry processes", () => {
|
|
122
|
+
const matches = [
|
|
123
|
+
// node running the published daemon (npx / npm exec resolution under _npx)
|
|
124
|
+
"node /tmp/botcord-npm-cache/_npx/abc123/node_modules/@botcord/daemon/dist/index.js start --foreground",
|
|
125
|
+
// node running the resolved bin shim
|
|
126
|
+
"node /Users/me/.botcord/daemon/node_modules/.bin/botcord-daemon start --foreground",
|
|
127
|
+
// direct invocation of the bin
|
|
128
|
+
"/usr/local/bin/botcord-daemon start --foreground",
|
|
129
|
+
// monorepo dev: node running packages/daemon/dist/index.js
|
|
130
|
+
"node /home/dev/botcord/packages/daemon/dist/index.js start --foreground",
|
|
131
|
+
];
|
|
132
|
+
for (const cmd of matches) {
|
|
133
|
+
expect(isBotCordDaemonStartCommand(cmd), `failed to match daemon: ${cmd}`).toBe(true);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
93
137
|
it("terminates extra daemon processes discovered outside the pid file", async () => {
|
|
94
138
|
const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {
|
|
95
139
|
stdio: "ignore",
|
package/src/daemon-singleton.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { PID_PATH } from "./config.js";
|
|
4
5
|
|
|
5
6
|
export interface SingletonLogger {
|
|
@@ -182,7 +183,16 @@ export function writeCurrentPid(
|
|
|
182
183
|
currentPid?: number;
|
|
183
184
|
} = {},
|
|
184
185
|
): void {
|
|
185
|
-
|
|
186
|
+
const pidPath = opts.pidPath ?? PID_PATH;
|
|
187
|
+
// Cloud-mode startup writes the PID file before `saveConfig` runs, so
|
|
188
|
+
// the daemon dir may not exist yet. mkdir its parent (0700) so the
|
|
189
|
+
// first write doesn't crash with ENOENT.
|
|
190
|
+
try {
|
|
191
|
+
mkdirSync(path.dirname(pidPath), { recursive: true, mode: 0o700 });
|
|
192
|
+
} catch {
|
|
193
|
+
// best-effort — writeFileSync below will surface the real error
|
|
194
|
+
}
|
|
195
|
+
writeFileSync(pidPath, String(opts.currentPid ?? process.pid), { mode: 0o600 });
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
export function removePidFile(pidPath = PID_PATH): void {
|
|
@@ -193,12 +203,28 @@ export function removePidFile(pidPath = PID_PATH): void {
|
|
|
193
203
|
}
|
|
194
204
|
}
|
|
195
205
|
|
|
196
|
-
function isBotCordDaemonStartCommand(command: string): boolean {
|
|
206
|
+
export function isBotCordDaemonStartCommand(command: string): boolean {
|
|
197
207
|
if (!/\bstart\b/.test(command)) return false;
|
|
208
|
+
// Only treat a row as "the daemon" when argv[0] is the real entry point:
|
|
209
|
+
// either the node interpreter (running `dist/index.js`) or the resolved
|
|
210
|
+
// `botcord-daemon` bin shim. Reject shell wrappers (`sh`, `bash`, `npm`,
|
|
211
|
+
// `npx`, `timeout`, ...) — their argv mentions `botcord-daemon` only as a
|
|
212
|
+
// literal arg, not as the executable being run. Killing a wrapper takes
|
|
213
|
+
// out the actual daemon it started, which is exactly the bug we want
|
|
214
|
+
// to avoid in cloud sandboxes.
|
|
215
|
+
const exe = (command.trim().split(/\s+/, 1)[0] ?? "").split("/").pop() ?? "";
|
|
216
|
+
const isNode = /^node(\d.*)?$/.test(exe);
|
|
217
|
+
const isDaemonBin = exe === "botcord-daemon";
|
|
218
|
+
if (!isNode && !isDaemonBin) return false;
|
|
198
219
|
return (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
/
|
|
220
|
+
// node-resolved bin shim, e.g. .../node_modules/.bin/botcord-daemon
|
|
221
|
+
/\/\.?bin\/botcord-daemon(?:\s|$)/.test(command) ||
|
|
222
|
+
// direct bin invocation, e.g. /usr/local/bin/botcord-daemon or argv[0]=botcord-daemon
|
|
223
|
+
/(?:^|\s)\S*botcord-daemon(?:\s|$)/.test(command) ||
|
|
224
|
+
// node running the published daemon entry script
|
|
225
|
+
/\bbotcord\S*\/daemon\/dist\/index\.js(?:\s|$)/.test(command) ||
|
|
226
|
+
// node running the in-repo daemon entry script (dev / monorepo)
|
|
227
|
+
/\bpackages\/daemon\/dist\/index\.js(?:\s|$)/.test(command)
|
|
202
228
|
);
|
|
203
229
|
}
|
|
204
230
|
|