@blamejs/core 0.8.42 → 0.8.49
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/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/daemon.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.daemon
|
|
4
|
+
* @nav Production
|
|
5
|
+
* @title Daemon
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Long-running process orchestration — supervisor wiring around
|
|
9
|
+
* `b.appShutdown`, foreground signal handling, detached-fork spawn
|
|
10
|
+
* via `b.processSpawn`, PID-file health probes, and a
|
|
11
|
+
* SIGTERM-then-SIGKILL restart policy on stop.
|
|
12
|
+
*
|
|
13
|
+
* Two operator paths share one entry point:
|
|
14
|
+
*
|
|
15
|
+
* 1. Foreground service mode (no `command`): the current process
|
|
16
|
+
* acquires `pidFile`, redirects stdout/stderr to `logFile`, and
|
|
17
|
+
* installs signal handlers (defaults: SIGTERM, SIGINT, SIGHUP)
|
|
18
|
+
* that route through a `b.appShutdown` orchestrator the operator
|
|
19
|
+
* can extend with `addPhase`.
|
|
20
|
+
*
|
|
21
|
+
* 2. Detached fork mode (`command` + `args`): the parent spawns the
|
|
22
|
+
* child via `b.processSpawn` (filtered env), writes the child PID
|
|
23
|
+
* to `pidFile`, hands the log fd to the child's stdout/stderr,
|
|
24
|
+
* and returns immediately so the parent can exit.
|
|
25
|
+
*
|
|
26
|
+
* Stale-PID handling — when `pidFile` exists but the recorded PID is
|
|
27
|
+
* no longer alive, `start` and `stop` clean up the sidecar and emit
|
|
28
|
+
* `daemon.stale_pid_cleaned`. Cross-process linkage uses
|
|
29
|
+
* `b.appShutdown.pidLock`, which layers O_EXCL atomic-create +
|
|
30
|
+
* signal-0 liveness probe + reap-on-stale.
|
|
31
|
+
*
|
|
32
|
+
* Audit events: `daemon.started` (pidFile + logFile + commandKind +
|
|
33
|
+
* pid), `daemon.stopped` (pidFile + signal + waitMs + escalated),
|
|
34
|
+
* `daemon.stale_pid_cleaned` (pidFile + stalePid).
|
|
35
|
+
*
|
|
36
|
+
* @card
|
|
37
|
+
* Long-running process orchestration — supervisor wiring around `b.appShutdown`, foreground signal handling, detached-fork spawn via `b.processSpawn`, PID-file health probes, and a SIGTERM-then-SIGKILL restart policy on stop.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
var fs = require("fs");
|
|
41
|
+
var path = require("path");
|
|
42
|
+
var nb = require("./numeric-bounds");
|
|
43
|
+
var appShutdown = require("./app-shutdown");
|
|
44
|
+
var processSpawn = require("./process-spawn");
|
|
45
|
+
var lazyRequire = require("./lazy-require");
|
|
46
|
+
var safeAsync = require("./safe-async");
|
|
47
|
+
var atomicFile = require("./atomic-file");
|
|
48
|
+
var validateOpts = require("./validate-opts");
|
|
49
|
+
var C = require("./constants");
|
|
50
|
+
var { boot } = require("./log");
|
|
51
|
+
var { defineClass } = require("./framework-error");
|
|
52
|
+
|
|
53
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
54
|
+
|
|
55
|
+
var DaemonError = defineClass("DaemonError", { alwaysPermanent: true });
|
|
56
|
+
var log = boot("daemon");
|
|
57
|
+
|
|
58
|
+
// Tunables. Operator overrides via opts on stop(); for start() the
|
|
59
|
+
// defaults are baked in so the operator surface stays minimal.
|
|
60
|
+
var DEFAULT_STOP_TIMEOUT_MS = C.TIME.seconds(30);
|
|
61
|
+
var DEFAULT_STOP_SIGNAL = "SIGTERM";
|
|
62
|
+
var DEFAULT_POLL_MS = 100;
|
|
63
|
+
var DEFAULT_LOG_FILE_MODE = 0o600;
|
|
64
|
+
|
|
65
|
+
function _safeAuditEmit(action, outcome, metadata) {
|
|
66
|
+
try {
|
|
67
|
+
audit().safeEmit({
|
|
68
|
+
action: action,
|
|
69
|
+
outcome: outcome || "success",
|
|
70
|
+
metadata: metadata || {},
|
|
71
|
+
});
|
|
72
|
+
} catch (_e) { /* drop-silent — by design */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _isLivePid(pid) {
|
|
76
|
+
if (typeof pid !== "number" || !isFinite(pid) || pid <= 0) return false;
|
|
77
|
+
try { process.kill(pid, 0); return true; }
|
|
78
|
+
catch (e) { return e && e.code === "EPERM"; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _readPidFile(pidFile) {
|
|
82
|
+
try {
|
|
83
|
+
var raw = fs.readFileSync(pidFile, "utf8");
|
|
84
|
+
var pid = parseInt(String(raw).trim(), 10);
|
|
85
|
+
return isFinite(pid) && pid > 0 ? pid : null;
|
|
86
|
+
} catch (_e) { return null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _validateStartOpts(opts) {
|
|
90
|
+
validateOpts.requireObject(opts, "daemon.start", DaemonError, "daemon/bad-opts");
|
|
91
|
+
validateOpts.requireNonEmptyString(opts.pidFile,
|
|
92
|
+
"daemon.start: opts.pidFile (absolute path recommended)",
|
|
93
|
+
DaemonError, "daemon/bad-pid-file");
|
|
94
|
+
validateOpts.optionalNonEmptyString(opts.logFile,
|
|
95
|
+
"daemon.start: opts.logFile", DaemonError, "daemon/bad-log-file");
|
|
96
|
+
validateOpts.optionalNonEmptyStringArray(opts.signals,
|
|
97
|
+
"daemon.start: opts.signals", DaemonError, "daemon/bad-signals");
|
|
98
|
+
if (Array.isArray(opts.signals) && opts.signals.length === 0) {
|
|
99
|
+
throw new DaemonError("daemon/bad-signals",
|
|
100
|
+
"daemon.start: opts.signals must be a non-empty array of POSIX signal names");
|
|
101
|
+
}
|
|
102
|
+
validateOpts.optionalNonEmptyString(opts.command,
|
|
103
|
+
"daemon.start: opts.command (path to executable)",
|
|
104
|
+
DaemonError, "daemon/bad-command");
|
|
105
|
+
if (opts.args !== undefined && !Array.isArray(opts.args)) {
|
|
106
|
+
throw new DaemonError("daemon/bad-args",
|
|
107
|
+
"daemon.start: opts.args must be an array of strings when present");
|
|
108
|
+
}
|
|
109
|
+
if (opts.command === undefined && opts.args !== undefined) {
|
|
110
|
+
throw new DaemonError("daemon/bad-args",
|
|
111
|
+
"daemon.start: opts.args requires opts.command");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _validateStopOpts(opts) {
|
|
116
|
+
validateOpts.requireObject(opts, "daemon.stop", DaemonError, "daemon/bad-opts");
|
|
117
|
+
validateOpts.requireNonEmptyString(opts.pidFile,
|
|
118
|
+
"daemon.stop: opts.pidFile", DaemonError, "daemon/bad-pid-file");
|
|
119
|
+
validateOpts.optionalNonEmptyString(opts.signal,
|
|
120
|
+
"daemon.stop: opts.signal", DaemonError, "daemon/bad-signal");
|
|
121
|
+
nb.requirePositiveFiniteIntIfPresent(opts.timeoutMs,
|
|
122
|
+
"daemon.stop: opts.timeoutMs", DaemonError, "daemon/bad-timeout");
|
|
123
|
+
nb.requirePositiveFiniteIntIfPresent(opts.pollMs,
|
|
124
|
+
"daemon.stop: opts.pollMs", DaemonError, "daemon/bad-poll");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _maybeReapStale(pidFile) {
|
|
128
|
+
var existing = _readPidFile(pidFile);
|
|
129
|
+
if (existing === null) return false;
|
|
130
|
+
if (_isLivePid(existing) && existing !== process.pid) {
|
|
131
|
+
// Live owner — caller will receive a daemon/already-running below.
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (existing === process.pid) return false;
|
|
135
|
+
// Stale: PID is gone (or signal-0 returned ESRCH). Reap + audit.
|
|
136
|
+
try { fs.unlinkSync(pidFile); } catch (_e) { /* race: another reaper */ }
|
|
137
|
+
_safeAuditEmit("daemon.stale_pid_cleaned", "success", {
|
|
138
|
+
pidFile: pidFile,
|
|
139
|
+
stalePid: existing,
|
|
140
|
+
});
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Open the log file (append mode, 0o600) and return the fd.
|
|
145
|
+
// Used both by detached-spawn (passed via stdio) and by foreground
|
|
146
|
+
// redirect of the current process' stdout/stderr.
|
|
147
|
+
function _openLogFd(logFile) {
|
|
148
|
+
if (typeof logFile !== "string" || logFile.length === 0) return null;
|
|
149
|
+
atomicFile.ensureDir(path.dirname(logFile));
|
|
150
|
+
var fd = fs.openSync(logFile, "a", DEFAULT_LOG_FILE_MODE);
|
|
151
|
+
return fd;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Redirect the current process's stdout/stderr file descriptors at the
|
|
155
|
+
// given fd. Implemented via fs.writeSync streams: Node doesn't expose a
|
|
156
|
+
// portable dup2, so we replace process.stdout.write / process.stderr.write
|
|
157
|
+
// with a writer that pushes to the log fd. This is the standard
|
|
158
|
+
// pattern for foreground daemons that don't want to lose output when
|
|
159
|
+
// detached from a terminal.
|
|
160
|
+
function _redirectStdio(fd) {
|
|
161
|
+
if (typeof fd !== "number") return;
|
|
162
|
+
function _writer(chunk, encOrCb, maybeCb) {
|
|
163
|
+
var enc = typeof encOrCb === "string" ? encOrCb : "utf8";
|
|
164
|
+
var cb = typeof encOrCb === "function" ? encOrCb : maybeCb;
|
|
165
|
+
var buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), enc);
|
|
166
|
+
try { fs.writeSync(fd, buf); }
|
|
167
|
+
catch (_e) { /* log fd closed underneath us — drop */ }
|
|
168
|
+
if (typeof cb === "function") cb();
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
process.stdout.write = _writer;
|
|
172
|
+
process.stderr.write = _writer;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Track foreground orchestrators per pidFile so stop() / repeat
|
|
176
|
+
// start() in the same process don't double-install signals.
|
|
177
|
+
var _foregroundOrchestrators = Object.create(null);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @primitive b.daemon.start
|
|
181
|
+
* @signature b.daemon.start(opts)
|
|
182
|
+
* @since 0.6.0
|
|
183
|
+
* @status stable
|
|
184
|
+
* @related b.daemon.stop, b.appShutdown.create, b.processSpawn.spawn
|
|
185
|
+
*
|
|
186
|
+
* Acquire `pidFile`, optionally redirect stdout/stderr to `logFile`,
|
|
187
|
+
* and either install signal handlers in the current process
|
|
188
|
+
* (foreground mode) or spawn a detached child (when `command` is
|
|
189
|
+
* supplied). Reaps a stale pidfile before acquire and emits
|
|
190
|
+
* `daemon.stale_pid_cleaned` when one is found.
|
|
191
|
+
*
|
|
192
|
+
* Returns `{ pid, pidFile, logFile, mode }`. In foreground mode the
|
|
193
|
+
* return value also exposes `orchestrator` (the underlying
|
|
194
|
+
* `b.appShutdown` handle), `addPhase` (operator-supplied shutdown
|
|
195
|
+
* phases), and `shutdown` (manual trigger). In detached mode `mode`
|
|
196
|
+
* is `"detached"`; in foreground mode it is `"foreground"`.
|
|
197
|
+
*
|
|
198
|
+
* Throws `DaemonError("daemon/already-running")` when the pidfile is
|
|
199
|
+
* held by a live PID, `DaemonError("daemon/spawn-failed")` when the
|
|
200
|
+
* detached spawn errors, and `DaemonError("daemon/log-open-failed")`
|
|
201
|
+
* when the log file cannot be opened in foreground mode.
|
|
202
|
+
*
|
|
203
|
+
* @opts
|
|
204
|
+
* pidFile: string, // absolute path of the PID sidecar (required)
|
|
205
|
+
* logFile: string, // append-mode log; redirects stdout+stderr
|
|
206
|
+
* signals: string[], // foreground signals; default: SIGTERM/SIGINT/SIGHUP
|
|
207
|
+
* command: string, // executable for detached-fork mode
|
|
208
|
+
* args: string[], // argv for the detached child
|
|
209
|
+
* cwd: string, // cwd for the detached child
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* var handle = b.daemon.start({
|
|
213
|
+
* pidFile: "/tmp/blamejs-daemon-demo.pid",
|
|
214
|
+
* signals: ["SIGTERM", "SIGINT"],
|
|
215
|
+
* });
|
|
216
|
+
* handle.mode; // → "foreground"
|
|
217
|
+
* handle.pidFile; // → "/tmp/blamejs-daemon-demo.pid"
|
|
218
|
+
* typeof handle.shutdown; // → "function"
|
|
219
|
+
* await handle.shutdown();
|
|
220
|
+
*/
|
|
221
|
+
function start(opts) {
|
|
222
|
+
_validateStartOpts(opts);
|
|
223
|
+
var pidFile = opts.pidFile;
|
|
224
|
+
var logFile = opts.logFile || null;
|
|
225
|
+
var signals = Array.isArray(opts.signals) && opts.signals.length > 0
|
|
226
|
+
? opts.signals.slice()
|
|
227
|
+
: ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
228
|
+
|
|
229
|
+
// Reap a stale pidfile if present, then attempt acquire.
|
|
230
|
+
_maybeReapStale(pidFile);
|
|
231
|
+
|
|
232
|
+
// Detached-fork mode — caller wants us to spawn the child, write its
|
|
233
|
+
// PID into pidFile, and return without taking the lock ourselves.
|
|
234
|
+
if (typeof opts.command === "string" && opts.command.length > 0) {
|
|
235
|
+
var existingLive = _readPidFile(pidFile);
|
|
236
|
+
if (existingLive !== null && _isLivePid(existingLive)) {
|
|
237
|
+
throw new DaemonError("daemon/already-running",
|
|
238
|
+
"daemon.start: pidFile '" + pidFile + "' held by live PID " + existingLive);
|
|
239
|
+
}
|
|
240
|
+
var logFd = logFile ? _openLogFd(logFile) : "ignore";
|
|
241
|
+
var child;
|
|
242
|
+
try {
|
|
243
|
+
child = processSpawn.spawn(opts.command, opts.args || [], {
|
|
244
|
+
detached: true,
|
|
245
|
+
stdio: ["ignore", logFd, logFd],
|
|
246
|
+
cwd: typeof opts.cwd === "string" ? opts.cwd : undefined,
|
|
247
|
+
});
|
|
248
|
+
} catch (e) {
|
|
249
|
+
try { if (typeof logFd === "number") fs.closeSync(logFd); }
|
|
250
|
+
catch (_c) { /* best-effort */ }
|
|
251
|
+
throw new DaemonError("daemon/spawn-failed",
|
|
252
|
+
"daemon.start: spawn failed: " + ((e && e.message) || String(e)));
|
|
253
|
+
}
|
|
254
|
+
// Write the child's PID via atomic temp+rename so a concurrent
|
|
255
|
+
// observer never sees a half-written pidFile.
|
|
256
|
+
atomicFile.ensureDir(path.dirname(pidFile));
|
|
257
|
+
var pidStr = String(child.pid) + "\n";
|
|
258
|
+
atomicFile.writeSync(pidFile, pidStr, { fileMode: 0o600 });
|
|
259
|
+
// Detach so the child survives parent exit.
|
|
260
|
+
try { child.unref(); } catch (_u) { /* best-effort */ }
|
|
261
|
+
if (typeof logFd === "number") {
|
|
262
|
+
// Parent doesn't need its handle to the log; child inherited it.
|
|
263
|
+
try { fs.closeSync(logFd); } catch (_c) { /* best-effort */ }
|
|
264
|
+
}
|
|
265
|
+
_safeAuditEmit("daemon.started", "success", {
|
|
266
|
+
pidFile: pidFile,
|
|
267
|
+
logFile: logFile,
|
|
268
|
+
commandKind: "detached-fork",
|
|
269
|
+
pid: child.pid,
|
|
270
|
+
});
|
|
271
|
+
log("daemon started (detached) pid=" + child.pid + " pidFile=" + pidFile);
|
|
272
|
+
return { pid: child.pid, pidFile: pidFile, logFile: logFile, mode: "detached" };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Foreground mode — current process owns pidFile + signals.
|
|
276
|
+
var lock = appShutdown.pidLock(pidFile);
|
|
277
|
+
try { lock.acquire(); }
|
|
278
|
+
catch (e) {
|
|
279
|
+
if (e && /pidlock-held/.test(e.code || "")) {
|
|
280
|
+
throw new DaemonError("daemon/already-running",
|
|
281
|
+
"daemon.start: pidFile '" + pidFile + "' already held: " + e.message);
|
|
282
|
+
}
|
|
283
|
+
throw new DaemonError("daemon/pid-acquire-failed",
|
|
284
|
+
"daemon.start: failed to acquire pidFile '" + pidFile + "': " +
|
|
285
|
+
((e && e.message) || String(e)));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
var logFdForeground = null;
|
|
289
|
+
if (logFile) {
|
|
290
|
+
try {
|
|
291
|
+
logFdForeground = _openLogFd(logFile);
|
|
292
|
+
_redirectStdio(logFdForeground);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
try { lock.release(); } catch (_r) { /* best-effort */ }
|
|
295
|
+
throw new DaemonError("daemon/log-open-failed",
|
|
296
|
+
"daemon.start: failed to open logFile '" + logFile + "': " +
|
|
297
|
+
((e && e.message) || String(e)));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
var orchestrator = appShutdown.create({
|
|
302
|
+
signals: signals,
|
|
303
|
+
installSignalHandlers: true,
|
|
304
|
+
phases: [
|
|
305
|
+
{
|
|
306
|
+
name: "pidLock-release",
|
|
307
|
+
run: function () {
|
|
308
|
+
try { lock.release(); } catch (_e) { /* best-effort */ }
|
|
309
|
+
if (logFdForeground !== null) {
|
|
310
|
+
try { fs.closeSync(logFdForeground); } catch (_c) { /* best-effort */ }
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
timeoutMs: C.TIME.seconds(2),
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
});
|
|
317
|
+
_foregroundOrchestrators[pidFile] = orchestrator;
|
|
318
|
+
|
|
319
|
+
_safeAuditEmit("daemon.started", "success", {
|
|
320
|
+
pidFile: pidFile,
|
|
321
|
+
logFile: logFile,
|
|
322
|
+
commandKind: "foreground",
|
|
323
|
+
pid: process.pid,
|
|
324
|
+
signals: signals,
|
|
325
|
+
});
|
|
326
|
+
log("daemon started (foreground) pid=" + process.pid + " pidFile=" + pidFile);
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
pid: process.pid,
|
|
330
|
+
pidFile: pidFile,
|
|
331
|
+
logFile: logFile,
|
|
332
|
+
mode: "foreground",
|
|
333
|
+
orchestrator: orchestrator,
|
|
334
|
+
addPhase: orchestrator.addPhase,
|
|
335
|
+
shutdown: orchestrator.shutdown,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* @primitive b.daemon.stop
|
|
341
|
+
* @signature b.daemon.stop(opts)
|
|
342
|
+
* @since 0.6.0
|
|
343
|
+
* @status stable
|
|
344
|
+
* @related b.daemon.start, b.appShutdown.create
|
|
345
|
+
*
|
|
346
|
+
* Read `pidFile`, send `signal` (default `SIGTERM`), poll for exit up
|
|
347
|
+
* to `timeoutMs` (default 30 s), then escalate to `SIGKILL`. Cleans
|
|
348
|
+
* up the pidfile on successful exit and emits `daemon.stopped` with
|
|
349
|
+
* `escalated: true|false` recording whether SIGKILL was needed.
|
|
350
|
+
*
|
|
351
|
+
* Returns `{ stopped, pid, signal, escalated?, reason? }`. `reason`
|
|
352
|
+
* is `"no-pidfile"` when nothing was running and `"stale"` when the
|
|
353
|
+
* pidfile pointed at a dead PID (the file is removed and a
|
|
354
|
+
* `daemon.stale_pid_cleaned` audit row lands).
|
|
355
|
+
*
|
|
356
|
+
* @opts
|
|
357
|
+
* pidFile: string, // absolute path of the PID sidecar (required)
|
|
358
|
+
* signal: string, // initial signal; default "SIGTERM"
|
|
359
|
+
* timeoutMs: number, // wait before SIGKILL escalation; default 30 s
|
|
360
|
+
* pollMs: number, // liveness-probe interval; default 100 ms
|
|
361
|
+
* abortSignal: AbortSignal, // forwarded to b.safeAsync.sleep
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* var report = await b.daemon.stop({
|
|
365
|
+
* pidFile: "/tmp/blamejs-daemon-demo.pid",
|
|
366
|
+
* timeoutMs: b.constants.TIME.seconds(5),
|
|
367
|
+
* });
|
|
368
|
+
* report.stopped; // → false
|
|
369
|
+
* report.reason; // → "no-pidfile"
|
|
370
|
+
*/
|
|
371
|
+
async function stop(opts) {
|
|
372
|
+
_validateStopOpts(opts);
|
|
373
|
+
var pidFile = opts.pidFile;
|
|
374
|
+
var signal = opts.signal || DEFAULT_STOP_SIGNAL;
|
|
375
|
+
var timeoutMs = typeof opts.timeoutMs === "number" ? opts.timeoutMs : DEFAULT_STOP_TIMEOUT_MS;
|
|
376
|
+
var pollMs = typeof opts.pollMs === "number" ? opts.pollMs : DEFAULT_POLL_MS;
|
|
377
|
+
|
|
378
|
+
var pid = _readPidFile(pidFile);
|
|
379
|
+
if (pid === null) {
|
|
380
|
+
return { stopped: false, pid: null, reason: "no-pidfile" };
|
|
381
|
+
}
|
|
382
|
+
if (!_isLivePid(pid)) {
|
|
383
|
+
// Stale — clean up and report.
|
|
384
|
+
try { fs.unlinkSync(pidFile); } catch (_e) { /* best-effort */ }
|
|
385
|
+
_safeAuditEmit("daemon.stale_pid_cleaned", "success", { pidFile: pidFile, stalePid: pid });
|
|
386
|
+
return { stopped: false, pid: pid, reason: "stale" };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
var t0 = Date.now();
|
|
390
|
+
// First signal — typically SIGTERM. Wait up to timeoutMs for exit.
|
|
391
|
+
try { process.kill(pid, signal); }
|
|
392
|
+
catch (e) {
|
|
393
|
+
if (e && e.code === "ESRCH") {
|
|
394
|
+
// Died between read and kill — cleanup + report.
|
|
395
|
+
try { fs.unlinkSync(pidFile); } catch (_u) { /* best-effort */ }
|
|
396
|
+
_safeAuditEmit("daemon.stopped", "success", {
|
|
397
|
+
pidFile: pidFile, signal: signal, waitMs: Date.now() - t0, escalated: false,
|
|
398
|
+
});
|
|
399
|
+
return { stopped: true, pid: pid, signal: signal };
|
|
400
|
+
}
|
|
401
|
+
throw new DaemonError("daemon/kill-failed",
|
|
402
|
+
"daemon.stop: kill(" + pid + ", " + signal + ") failed: " + e.message);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
var deadline = t0 + timeoutMs;
|
|
406
|
+
while (Date.now() < deadline) {
|
|
407
|
+
if (!_isLivePid(pid)) {
|
|
408
|
+
try { fs.unlinkSync(pidFile); } catch (_u) { /* best-effort */ }
|
|
409
|
+
_safeAuditEmit("daemon.stopped", "success", {
|
|
410
|
+
pidFile: pidFile, signal: signal, waitMs: Date.now() - t0, escalated: false,
|
|
411
|
+
});
|
|
412
|
+
return { stopped: true, pid: pid, signal: signal };
|
|
413
|
+
}
|
|
414
|
+
await safeAsync.sleep(pollMs, { signal: opts.abortSignal });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Timed out — escalate to SIGKILL.
|
|
418
|
+
try { process.kill(pid, "SIGKILL"); }
|
|
419
|
+
catch (e) {
|
|
420
|
+
if (!(e && e.code === "ESRCH")) {
|
|
421
|
+
throw new DaemonError("daemon/kill-failed",
|
|
422
|
+
"daemon.stop: SIGKILL escalation failed for pid " + pid + ": " + e.message);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Wait briefly for the kernel to reap.
|
|
426
|
+
var killDeadline = Date.now() + C.TIME.seconds(2);
|
|
427
|
+
while (Date.now() < killDeadline) {
|
|
428
|
+
if (!_isLivePid(pid)) break;
|
|
429
|
+
await safeAsync.sleep(pollMs, { signal: opts.abortSignal });
|
|
430
|
+
}
|
|
431
|
+
try { fs.unlinkSync(pidFile); } catch (_u) { /* best-effort */ }
|
|
432
|
+
_safeAuditEmit("daemon.stopped", "success", {
|
|
433
|
+
pidFile: pidFile, signal: "SIGKILL", waitMs: Date.now() - t0, escalated: true,
|
|
434
|
+
});
|
|
435
|
+
return { stopped: true, pid: pid, signal: "SIGKILL", escalated: true };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Test-only — drop process-wide foreground orchestrator state so smoke
|
|
439
|
+
// tests can re-run start() in the same process without leaking signal
|
|
440
|
+
// handlers across cases.
|
|
441
|
+
function _resetForTest() {
|
|
442
|
+
var keys = Object.keys(_foregroundOrchestrators);
|
|
443
|
+
for (var i = 0; i < keys.length; i++) {
|
|
444
|
+
try { _foregroundOrchestrators[keys[i]]._resetForTest(); } catch (_e) { /* best-effort */ }
|
|
445
|
+
}
|
|
446
|
+
_foregroundOrchestrators = Object.create(null);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = {
|
|
450
|
+
start: start,
|
|
451
|
+
stop: stop,
|
|
452
|
+
DaemonError: DaemonError,
|
|
453
|
+
DEFAULT_STOP_SIGNAL: DEFAULT_STOP_SIGNAL,
|
|
454
|
+
DEFAULT_STOP_TIMEOUT_MS: DEFAULT_STOP_TIMEOUT_MS,
|
|
455
|
+
_resetForTest: _resetForTest,
|
|
456
|
+
};
|