@metamask-previews/wallet-cli 0.0.0-preview-1bd5239bf → 0.0.0-preview-2855dbf4e

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +16 -2
  3. package/dist/commands/daemon/call.cjs +3 -7
  4. package/dist/commands/daemon/call.cjs.map +1 -1
  5. package/dist/commands/daemon/call.d.cts.map +1 -1
  6. package/dist/commands/daemon/call.d.mts.map +1 -1
  7. package/dist/commands/daemon/call.mjs +4 -8
  8. package/dist/commands/daemon/call.mjs.map +1 -1
  9. package/dist/commands/daemon/list.cjs +48 -0
  10. package/dist/commands/daemon/list.cjs.map +1 -0
  11. package/dist/commands/daemon/list.d.cts +7 -0
  12. package/dist/commands/daemon/list.d.cts.map +1 -0
  13. package/dist/commands/daemon/list.d.mts +7 -0
  14. package/dist/commands/daemon/list.d.mts.map +1 -0
  15. package/dist/commands/daemon/list.mjs +46 -0
  16. package/dist/commands/daemon/list.mjs.map +1 -0
  17. package/dist/daemon/daemon-entry.cjs +4 -0
  18. package/dist/daemon/daemon-entry.cjs.map +1 -1
  19. package/dist/daemon/daemon-entry.mjs +4 -0
  20. package/dist/daemon/daemon-entry.mjs.map +1 -1
  21. package/dist/daemon/daemon-spawn.cjs +46 -25
  22. package/dist/daemon/daemon-spawn.cjs.map +1 -1
  23. package/dist/daemon/daemon-spawn.d.cts.map +1 -1
  24. package/dist/daemon/daemon-spawn.d.mts.map +1 -1
  25. package/dist/daemon/daemon-spawn.mjs +47 -26
  26. package/dist/daemon/daemon-spawn.mjs.map +1 -1
  27. package/dist/daemon/socket-line.cjs +8 -4
  28. package/dist/daemon/socket-line.cjs.map +1 -1
  29. package/dist/daemon/socket-line.d.cts.map +1 -1
  30. package/dist/daemon/socket-line.d.mts.map +1 -1
  31. package/dist/daemon/socket-line.mjs +8 -4
  32. package/dist/daemon/socket-line.mjs.map +1 -1
  33. package/dist/daemon/stop-daemon.cjs +23 -9
  34. package/dist/daemon/stop-daemon.cjs.map +1 -1
  35. package/dist/daemon/stop-daemon.d.cts +17 -7
  36. package/dist/daemon/stop-daemon.d.cts.map +1 -1
  37. package/dist/daemon/stop-daemon.d.mts +17 -7
  38. package/dist/daemon/stop-daemon.d.mts.map +1 -1
  39. package/dist/daemon/stop-daemon.mjs +23 -9
  40. package/dist/daemon/stop-daemon.mjs.map +1 -1
  41. package/dist/daemon/utils.cjs +52 -1
  42. package/dist/daemon/utils.cjs.map +1 -1
  43. package/dist/daemon/utils.d.cts +28 -0
  44. package/dist/daemon/utils.d.cts.map +1 -1
  45. package/dist/daemon/utils.d.mts +28 -0
  46. package/dist/daemon/utils.d.mts.map +1 -1
  47. package/dist/daemon/utils.mjs +48 -0
  48. package/dist/daemon/utils.mjs.map +1 -1
  49. package/package.json +3 -2
@@ -5,6 +5,7 @@ const node_child_process_1 = require("node:child_process");
5
5
  const node_fs_1 = require("node:fs");
6
6
  const node_path_1 = require("node:path");
7
7
  const daemon_client_1 = require("./daemon-client.cjs");
8
+ const data_dir_1 = require("./data-dir.cjs");
8
9
  const paths_1 = require("./paths.cjs");
9
10
  const POLL_INTERVAL_MS = 100;
10
11
  const MAX_POLLS = 300; // 30 seconds
@@ -21,26 +22,41 @@ const MAX_POLLS = 300; // 30 seconds
21
22
  * @returns The state of the daemon and the socket path it's listening on.
22
23
  */
23
24
  async function ensureDaemon(config) {
24
- const { socketPath } = (0, paths_1.getDaemonPaths)(config.dataDir);
25
+ const { socketPath, logPath } = (0, paths_1.getDaemonPaths)(config.dataDir);
25
26
  const initialPing = await (0, daemon_client_1.pingDaemon)(socketPath);
26
- if (initialPing.status === 'responsive') {
27
- return { state: 'already-running', socketPath };
28
- }
29
- if (initialPing.status === 'unreachable') {
30
- if (initialPing.reason === 'permission') {
31
- throw new Error(`Refusing to start: the socket at ${socketPath} is owned by another user. ` +
32
- `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +
27
+ switch (initialPing.status) {
28
+ case 'responsive':
29
+ return { state: 'already-running', socketPath };
30
+ case 'unreachable':
31
+ if (initialPing.reason === 'permission') {
32
+ throw new Error(`Refusing to start: the socket at ${socketPath} is owned by another user. ` +
33
+ `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +
34
+ `(${initialPing.error.message})`);
35
+ }
36
+ throw new Error(`Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +
37
+ `Run \`mm daemon stop\` (or \`mm daemon purge\`) before starting a new daemon. ` +
33
38
  `(${initialPing.error.message})`);
39
+ case 'absent':
40
+ break;
41
+ /* istanbul ignore next -- exhaustiveness guard; unreachable for the current PingResult union */
42
+ default: {
43
+ const exhaustiveCheck = initialPing;
44
+ throw new Error(`Unexpected daemon ping status: ${String(exhaustiveCheck)}`);
34
45
  }
35
- throw new Error(`Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +
36
- `Run \`mm daemon stop\` (or \`mm daemon purge\`) before starting a new daemon. ` +
37
- `(${initialPing.error.message})`);
38
46
  }
39
47
  process.stderr.write('Starting daemon...\n');
40
48
  const { entryPath, args } = resolveEntryPoint(config.packageRoot);
49
+ // Create the data directory before opening the log file inside it. The daemon
50
+ // entry also does this, but only after spawn — opening the log first would
51
+ // ENOENT on a fresh data directory.
52
+ await (0, data_dir_1.ensureOwnerOnlyDirectory)(config.dataDir);
53
+ // Redirect the detached daemon's stderr to its log file rather than
54
+ // discarding it, so a crash after startup stays diagnosable. stdout stays
55
+ // ignored — structured status goes through the file logger.
56
+ const logFileDescriptor = (0, node_fs_1.openSync)(logPath, 'a');
41
57
  const child = (0, node_child_process_1.spawn)(process.execPath, [...args, entryPath], {
42
58
  detached: true,
43
- stdio: 'ignore',
59
+ stdio: ['ignore', 'ignore', logFileDescriptor],
44
60
  env: {
45
61
  ...process.env,
46
62
  MM_DAEMON_DATA_DIR: config.dataDir,
@@ -50,29 +66,34 @@ async function ensureDaemon(config) {
50
66
  MM_WALLET_SRP: config.srp,
51
67
  },
52
68
  });
53
- const exitInfo = { value: null };
69
+ // The child dup'd the file descriptor into its stderr, so drop the parent's
70
+ // copy. Safe on the success path: `spawn` reports failures via the 'error'
71
+ // event, not a synchronous throw.
72
+ (0, node_fs_1.closeSync)(logFileDescriptor);
54
73
  // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may
55
- // never emit 'exit'. Capture it so the readiness loop can surface the real
56
- // cause immediately instead of hanging for the full timeout.
57
- const spawnError = { value: null };
74
+ // never emit 'exit', so 'error' is recorded first and not overwritten by a
75
+ // later 'exit' the loop surfaces the real cause instead of hanging.
76
+ const outcome = { current: { kind: 'pending' } };
58
77
  child.on('error', (error) => {
59
78
  process.stderr.write(`Failed to spawn daemon process: ${String(error)}\n`);
60
- spawnError.value = error;
79
+ outcome.current = { kind: 'error', error };
61
80
  });
62
81
  child.on('exit', (code, signal) => {
63
- exitInfo.value = { code, signal };
82
+ if (outcome.current.kind === 'pending') {
83
+ outcome.current = { kind: 'exited', code, signal };
84
+ }
64
85
  });
65
86
  child.unref();
66
87
  for (let i = 0; i < MAX_POLLS; i++) {
67
88
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
68
- if (spawnError.value !== null) {
69
- throw new Error(`Failed to spawn daemon process: ${spawnError.value.message}. ` +
70
- `Check the daemon log at ${(0, paths_1.getDaemonPaths)(config.dataDir).logPath}.`);
89
+ const settled = outcome.current;
90
+ if (settled.kind === 'error') {
91
+ throw new Error(`Failed to spawn daemon process: ${settled.error.message}. ` +
92
+ `Check the daemon log at ${logPath}.`);
71
93
  }
72
- if (exitInfo.value !== null) {
73
- const { code, signal } = exitInfo.value;
74
- throw new Error(`Daemon process exited during startup (code=${String(code)}, signal=${String(signal)}). ` +
75
- `Check the daemon log at ${(0, paths_1.getDaemonPaths)(config.dataDir).logPath}.`);
94
+ if (settled.kind === 'exited') {
95
+ throw new Error(`Daemon process exited during startup (code=${String(settled.code)}, signal=${String(settled.signal)}). ` +
96
+ `Check the daemon log at ${logPath}.`);
76
97
  }
77
98
  const ping = await (0, daemon_client_1.pingDaemon)(socketPath);
78
99
  if (ping.status === 'responsive') {
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-spawn.cjs","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { pingDaemon } from './daemon-client';\nimport { getDaemonPaths } from './paths';\nimport type { DaemonSpawnConfig } from './types';\n\nconst POLL_INTERVAL_MS = 100;\nconst MAX_POLLS = 300; // 30 seconds\n\n/**\n * Outcome of {@link ensureDaemon}.\n *\n * - `'already-running'`: a responsive daemon was found at the configured\n * socket path. The supplied flags (`infuraProjectId`, `password`, `srp`)\n * were NOT applied to that daemon; the caller should surface this so a\n * user who is trying to change them isn't silently ignored.\n * - `'started'`: a new daemon was spawned and is now responsive.\n */\nexport type EnsureDaemonResult = {\n state: 'already-running' | 'started';\n socketPath: string;\n};\n\n/**\n * Ensure the daemon is running. If a responsive daemon already exists, return\n * `'already-running'` (caller decides how to surface that). Otherwise spawn\n * one as a detached process and wait until the socket becomes responsive.\n *\n * Refuses to spawn when pinging the existing socket fails with anything other\n * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the\n * existing process and corrupt its PID file.\n *\n * @param config - Spawn configuration.\n * @returns The state of the daemon and the socket path it's listening on.\n */\nexport async function ensureDaemon(\n config: DaemonSpawnConfig,\n): Promise<EnsureDaemonResult> {\n const { socketPath } = getDaemonPaths(config.dataDir);\n\n const initialPing = await pingDaemon(socketPath);\n if (initialPing.status === 'responsive') {\n return { state: 'already-running', socketPath };\n }\n if (initialPing.status === 'unreachable') {\n if (initialPing.reason === 'permission') {\n throw new Error(\n `Refusing to start: the socket at ${socketPath} is owned by another user. ` +\n `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +\n `(${initialPing.error.message})`,\n );\n }\n throw new Error(\n `Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +\n `Run \\`mm daemon stop\\` (or \\`mm daemon purge\\`) before starting a new daemon. ` +\n `(${initialPing.error.message})`,\n );\n }\n\n process.stderr.write('Starting daemon...\\n');\n\n const { entryPath, args } = resolveEntryPoint(config.packageRoot);\n\n const child = spawn(process.execPath, [...args, entryPath], {\n detached: true,\n stdio: 'ignore',\n env: {\n ...process.env,\n MM_DAEMON_DATA_DIR: config.dataDir,\n MM_DAEMON_SOCKET_PATH: socketPath,\n INFURA_PROJECT_ID: config.infuraProjectId,\n MM_WALLET_PASSWORD: config.password,\n MM_WALLET_SRP: config.srp,\n },\n });\n\n type ExitInfo = { code: number | null; signal: NodeJS.Signals | null };\n const exitInfo: { value: ExitInfo | null } = { value: null };\n // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may\n // never emit 'exit'. Capture it so the readiness loop can surface the real\n // cause immediately instead of hanging for the full timeout.\n const spawnError: { value: Error | null } = { value: null };\n\n child.on('error', (error: Error) => {\n process.stderr.write(`Failed to spawn daemon process: ${String(error)}\\n`);\n spawnError.value = error;\n });\n child.on('exit', (code, signal) => {\n exitInfo.value = { code, signal };\n });\n child.unref();\n\n for (let i = 0; i < MAX_POLLS; i++) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n if (spawnError.value !== null) {\n throw new Error(\n `Failed to spawn daemon process: ${spawnError.value.message}. ` +\n `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`,\n );\n }\n if (exitInfo.value !== null) {\n const { code, signal } = exitInfo.value;\n throw new Error(\n `Daemon process exited during startup (code=${String(code)}, signal=${String(signal)}). ` +\n `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`,\n );\n }\n const ping = await pingDaemon(socketPath);\n if (ping.status === 'responsive') {\n process.stderr.write('Daemon ready.\\n');\n return { state: 'started', socketPath };\n }\n }\n\n throw new Error(\n `Daemon did not start within ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s`,\n );\n}\n\n/**\n * Resolve the daemon entry point path and any extra Node.js args needed.\n *\n * In production, uses the compiled dist output. In development, uses tsx\n * to run TypeScript source directly.\n *\n * @param packageRoot - The root directory of the wallet-cli package.\n * @returns The entry path and any extra node args.\n */\nfunction resolveEntryPoint(packageRoot: string): {\n entryPath: string;\n args: string[];\n} {\n const distEntry = join(packageRoot, 'dist', 'daemon', 'daemon-entry.mjs');\n if (existsSync(distEntry)) {\n return { entryPath: distEntry, args: [] };\n }\n\n const srcEntry = join(packageRoot, 'src', 'daemon', 'daemon-entry.ts');\n return {\n entryPath: srcEntry,\n args: ['--import', 'tsx'],\n };\n}\n"]}
1
+ {"version":3,"file":"daemon-spawn.cjs","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { closeSync, existsSync, openSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { pingDaemon } from './daemon-client';\nimport { ensureOwnerOnlyDirectory } from './data-dir';\nimport { getDaemonPaths } from './paths';\nimport type { DaemonSpawnConfig } from './types';\n\nconst POLL_INTERVAL_MS = 100;\nconst MAX_POLLS = 300; // 30 seconds\n\n/**\n * Outcome of {@link ensureDaemon}.\n *\n * - `'already-running'`: a responsive daemon was found at the configured\n * socket path. The supplied flags (`infuraProjectId`, `password`, `srp`)\n * were NOT applied to that daemon; the caller should surface this so a\n * user who is trying to change them isn't silently ignored.\n * - `'started'`: a new daemon was spawned and is now responsive.\n */\nexport type EnsureDaemonResult = {\n state: 'already-running' | 'started';\n socketPath: string;\n};\n\n/**\n * Ensure the daemon is running. If a responsive daemon already exists, return\n * `'already-running'` (caller decides how to surface that). Otherwise spawn\n * one as a detached process and wait until the socket becomes responsive.\n *\n * Refuses to spawn when pinging the existing socket fails with anything other\n * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the\n * existing process and corrupt its PID file.\n *\n * @param config - Spawn configuration.\n * @returns The state of the daemon and the socket path it's listening on.\n */\nexport async function ensureDaemon(\n config: DaemonSpawnConfig,\n): Promise<EnsureDaemonResult> {\n const { socketPath, logPath } = getDaemonPaths(config.dataDir);\n\n const initialPing = await pingDaemon(socketPath);\n switch (initialPing.status) {\n case 'responsive':\n return { state: 'already-running', socketPath };\n case 'unreachable':\n if (initialPing.reason === 'permission') {\n throw new Error(\n `Refusing to start: the socket at ${socketPath} is owned by another user. ` +\n `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +\n `(${initialPing.error.message})`,\n );\n }\n throw new Error(\n `Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +\n `Run \\`mm daemon stop\\` (or \\`mm daemon purge\\`) before starting a new daemon. ` +\n `(${initialPing.error.message})`,\n );\n case 'absent':\n break;\n /* istanbul ignore next -- exhaustiveness guard; unreachable for the current PingResult union */\n default: {\n const exhaustiveCheck: never = initialPing;\n throw new Error(\n `Unexpected daemon ping status: ${String(exhaustiveCheck)}`,\n );\n }\n }\n\n process.stderr.write('Starting daemon...\\n');\n\n const { entryPath, args } = resolveEntryPoint(config.packageRoot);\n\n // Create the data directory before opening the log file inside it. The daemon\n // entry also does this, but only after spawn — opening the log first would\n // ENOENT on a fresh data directory.\n await ensureOwnerOnlyDirectory(config.dataDir);\n\n // Redirect the detached daemon's stderr to its log file rather than\n // discarding it, so a crash after startup stays diagnosable. stdout stays\n // ignored — structured status goes through the file logger.\n const logFileDescriptor = openSync(logPath, 'a');\n const child = spawn(process.execPath, [...args, entryPath], {\n detached: true,\n stdio: ['ignore', 'ignore', logFileDescriptor],\n env: {\n ...process.env,\n MM_DAEMON_DATA_DIR: config.dataDir,\n MM_DAEMON_SOCKET_PATH: socketPath,\n INFURA_PROJECT_ID: config.infuraProjectId,\n MM_WALLET_PASSWORD: config.password,\n MM_WALLET_SRP: config.srp,\n },\n });\n // The child dup'd the file descriptor into its stderr, so drop the parent's\n // copy. Safe on the success path: `spawn` reports failures via the 'error'\n // event, not a synchronous throw.\n closeSync(logFileDescriptor);\n\n type StartupOutcome =\n | { kind: 'pending' }\n | { kind: 'error'; error: Error }\n | { kind: 'exited'; code: number | null; signal: NodeJS.Signals | null };\n\n // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may\n // never emit 'exit', so 'error' is recorded first and not overwritten by a\n // later 'exit' — the loop surfaces the real cause instead of hanging.\n const outcome: { current: StartupOutcome } = { current: { kind: 'pending' } };\n\n child.on('error', (error: Error) => {\n process.stderr.write(`Failed to spawn daemon process: ${String(error)}\\n`);\n outcome.current = { kind: 'error', error };\n });\n child.on('exit', (code, signal) => {\n if (outcome.current.kind === 'pending') {\n outcome.current = { kind: 'exited', code, signal };\n }\n });\n child.unref();\n\n for (let i = 0; i < MAX_POLLS; i++) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n const settled = outcome.current;\n if (settled.kind === 'error') {\n throw new Error(\n `Failed to spawn daemon process: ${settled.error.message}. ` +\n `Check the daemon log at ${logPath}.`,\n );\n }\n if (settled.kind === 'exited') {\n throw new Error(\n `Daemon process exited during startup (code=${String(settled.code)}, signal=${String(settled.signal)}). ` +\n `Check the daemon log at ${logPath}.`,\n );\n }\n const ping = await pingDaemon(socketPath);\n if (ping.status === 'responsive') {\n process.stderr.write('Daemon ready.\\n');\n return { state: 'started', socketPath };\n }\n }\n\n throw new Error(\n `Daemon did not start within ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s`,\n );\n}\n\n/**\n * Resolve the daemon entry point path and any extra Node.js args needed.\n *\n * In production, uses the compiled dist output. In development, uses tsx\n * to run TypeScript source directly.\n *\n * @param packageRoot - The root directory of the wallet-cli package.\n * @returns The entry path and any extra node args.\n */\nfunction resolveEntryPoint(packageRoot: string): {\n entryPath: string;\n args: string[];\n} {\n const distEntry = join(packageRoot, 'dist', 'daemon', 'daemon-entry.mjs');\n if (existsSync(distEntry)) {\n return { entryPath: distEntry, args: [] };\n }\n\n const srcEntry = join(packageRoot, 'src', 'daemon', 'daemon-entry.ts');\n return {\n entryPath: srcEntry,\n args: ['--import', 'tsx'],\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-spawn.d.cts","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAgB;AAKjD;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CAgF7B"}
1
+ {"version":3,"file":"daemon-spawn.d.cts","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAgB;AAKjD;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CA2G7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-spawn.d.mts","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAgB;AAKjD;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CAgF7B"}
1
+ {"version":3,"file":"daemon-spawn.d.mts","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAgB;AAKjD;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CA2G7B"}
@@ -1,7 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { closeSync, existsSync, openSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { pingDaemon } from "./daemon-client.mjs";
5
+ import { ensureOwnerOnlyDirectory } from "./data-dir.mjs";
5
6
  import { getDaemonPaths } from "./paths.mjs";
6
7
  const POLL_INTERVAL_MS = 100;
7
8
  const MAX_POLLS = 300; // 30 seconds
@@ -18,26 +19,41 @@ const MAX_POLLS = 300; // 30 seconds
18
19
  * @returns The state of the daemon and the socket path it's listening on.
19
20
  */
20
21
  export async function ensureDaemon(config) {
21
- const { socketPath } = getDaemonPaths(config.dataDir);
22
+ const { socketPath, logPath } = getDaemonPaths(config.dataDir);
22
23
  const initialPing = await pingDaemon(socketPath);
23
- if (initialPing.status === 'responsive') {
24
- return { state: 'already-running', socketPath };
25
- }
26
- if (initialPing.status === 'unreachable') {
27
- if (initialPing.reason === 'permission') {
28
- throw new Error(`Refusing to start: the socket at ${socketPath} is owned by another user. ` +
29
- `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +
24
+ switch (initialPing.status) {
25
+ case 'responsive':
26
+ return { state: 'already-running', socketPath };
27
+ case 'unreachable':
28
+ if (initialPing.reason === 'permission') {
29
+ throw new Error(`Refusing to start: the socket at ${socketPath} is owned by another user. ` +
30
+ `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +
31
+ `(${initialPing.error.message})`);
32
+ }
33
+ throw new Error(`Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +
34
+ `Run \`mm daemon stop\` (or \`mm daemon purge\`) before starting a new daemon. ` +
30
35
  `(${initialPing.error.message})`);
36
+ case 'absent':
37
+ break;
38
+ /* istanbul ignore next -- exhaustiveness guard; unreachable for the current PingResult union */
39
+ default: {
40
+ const exhaustiveCheck = initialPing;
41
+ throw new Error(`Unexpected daemon ping status: ${String(exhaustiveCheck)}`);
31
42
  }
32
- throw new Error(`Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +
33
- `Run \`mm daemon stop\` (or \`mm daemon purge\`) before starting a new daemon. ` +
34
- `(${initialPing.error.message})`);
35
43
  }
36
44
  process.stderr.write('Starting daemon...\n');
37
45
  const { entryPath, args } = resolveEntryPoint(config.packageRoot);
46
+ // Create the data directory before opening the log file inside it. The daemon
47
+ // entry also does this, but only after spawn — opening the log first would
48
+ // ENOENT on a fresh data directory.
49
+ await ensureOwnerOnlyDirectory(config.dataDir);
50
+ // Redirect the detached daemon's stderr to its log file rather than
51
+ // discarding it, so a crash after startup stays diagnosable. stdout stays
52
+ // ignored — structured status goes through the file logger.
53
+ const logFileDescriptor = openSync(logPath, 'a');
38
54
  const child = spawn(process.execPath, [...args, entryPath], {
39
55
  detached: true,
40
- stdio: 'ignore',
56
+ stdio: ['ignore', 'ignore', logFileDescriptor],
41
57
  env: {
42
58
  ...process.env,
43
59
  MM_DAEMON_DATA_DIR: config.dataDir,
@@ -47,29 +63,34 @@ export async function ensureDaemon(config) {
47
63
  MM_WALLET_SRP: config.srp,
48
64
  },
49
65
  });
50
- const exitInfo = { value: null };
66
+ // The child dup'd the file descriptor into its stderr, so drop the parent's
67
+ // copy. Safe on the success path: `spawn` reports failures via the 'error'
68
+ // event, not a synchronous throw.
69
+ closeSync(logFileDescriptor);
51
70
  // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may
52
- // never emit 'exit'. Capture it so the readiness loop can surface the real
53
- // cause immediately instead of hanging for the full timeout.
54
- const spawnError = { value: null };
71
+ // never emit 'exit', so 'error' is recorded first and not overwritten by a
72
+ // later 'exit' the loop surfaces the real cause instead of hanging.
73
+ const outcome = { current: { kind: 'pending' } };
55
74
  child.on('error', (error) => {
56
75
  process.stderr.write(`Failed to spawn daemon process: ${String(error)}\n`);
57
- spawnError.value = error;
76
+ outcome.current = { kind: 'error', error };
58
77
  });
59
78
  child.on('exit', (code, signal) => {
60
- exitInfo.value = { code, signal };
79
+ if (outcome.current.kind === 'pending') {
80
+ outcome.current = { kind: 'exited', code, signal };
81
+ }
61
82
  });
62
83
  child.unref();
63
84
  for (let i = 0; i < MAX_POLLS; i++) {
64
85
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
65
- if (spawnError.value !== null) {
66
- throw new Error(`Failed to spawn daemon process: ${spawnError.value.message}. ` +
67
- `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`);
86
+ const settled = outcome.current;
87
+ if (settled.kind === 'error') {
88
+ throw new Error(`Failed to spawn daemon process: ${settled.error.message}. ` +
89
+ `Check the daemon log at ${logPath}.`);
68
90
  }
69
- if (exitInfo.value !== null) {
70
- const { code, signal } = exitInfo.value;
71
- throw new Error(`Daemon process exited during startup (code=${String(code)}, signal=${String(signal)}). ` +
72
- `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`);
91
+ if (settled.kind === 'exited') {
92
+ throw new Error(`Daemon process exited during startup (code=${String(settled.code)}, signal=${String(settled.signal)}). ` +
93
+ `Check the daemon log at ${logPath}.`);
73
94
  }
74
95
  const ping = await pingDaemon(socketPath);
75
96
  if (ping.status === 'responsive') {
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-spawn.mjs","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { pingDaemon } from './daemon-client';\nimport { getDaemonPaths } from './paths';\nimport type { DaemonSpawnConfig } from './types';\n\nconst POLL_INTERVAL_MS = 100;\nconst MAX_POLLS = 300; // 30 seconds\n\n/**\n * Outcome of {@link ensureDaemon}.\n *\n * - `'already-running'`: a responsive daemon was found at the configured\n * socket path. The supplied flags (`infuraProjectId`, `password`, `srp`)\n * were NOT applied to that daemon; the caller should surface this so a\n * user who is trying to change them isn't silently ignored.\n * - `'started'`: a new daemon was spawned and is now responsive.\n */\nexport type EnsureDaemonResult = {\n state: 'already-running' | 'started';\n socketPath: string;\n};\n\n/**\n * Ensure the daemon is running. If a responsive daemon already exists, return\n * `'already-running'` (caller decides how to surface that). Otherwise spawn\n * one as a detached process and wait until the socket becomes responsive.\n *\n * Refuses to spawn when pinging the existing socket fails with anything other\n * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the\n * existing process and corrupt its PID file.\n *\n * @param config - Spawn configuration.\n * @returns The state of the daemon and the socket path it's listening on.\n */\nexport async function ensureDaemon(\n config: DaemonSpawnConfig,\n): Promise<EnsureDaemonResult> {\n const { socketPath } = getDaemonPaths(config.dataDir);\n\n const initialPing = await pingDaemon(socketPath);\n if (initialPing.status === 'responsive') {\n return { state: 'already-running', socketPath };\n }\n if (initialPing.status === 'unreachable') {\n if (initialPing.reason === 'permission') {\n throw new Error(\n `Refusing to start: the socket at ${socketPath} is owned by another user. ` +\n `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +\n `(${initialPing.error.message})`,\n );\n }\n throw new Error(\n `Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +\n `Run \\`mm daemon stop\\` (or \\`mm daemon purge\\`) before starting a new daemon. ` +\n `(${initialPing.error.message})`,\n );\n }\n\n process.stderr.write('Starting daemon...\\n');\n\n const { entryPath, args } = resolveEntryPoint(config.packageRoot);\n\n const child = spawn(process.execPath, [...args, entryPath], {\n detached: true,\n stdio: 'ignore',\n env: {\n ...process.env,\n MM_DAEMON_DATA_DIR: config.dataDir,\n MM_DAEMON_SOCKET_PATH: socketPath,\n INFURA_PROJECT_ID: config.infuraProjectId,\n MM_WALLET_PASSWORD: config.password,\n MM_WALLET_SRP: config.srp,\n },\n });\n\n type ExitInfo = { code: number | null; signal: NodeJS.Signals | null };\n const exitInfo: { value: ExitInfo | null } = { value: null };\n // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may\n // never emit 'exit'. Capture it so the readiness loop can surface the real\n // cause immediately instead of hanging for the full timeout.\n const spawnError: { value: Error | null } = { value: null };\n\n child.on('error', (error: Error) => {\n process.stderr.write(`Failed to spawn daemon process: ${String(error)}\\n`);\n spawnError.value = error;\n });\n child.on('exit', (code, signal) => {\n exitInfo.value = { code, signal };\n });\n child.unref();\n\n for (let i = 0; i < MAX_POLLS; i++) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n if (spawnError.value !== null) {\n throw new Error(\n `Failed to spawn daemon process: ${spawnError.value.message}. ` +\n `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`,\n );\n }\n if (exitInfo.value !== null) {\n const { code, signal } = exitInfo.value;\n throw new Error(\n `Daemon process exited during startup (code=${String(code)}, signal=${String(signal)}). ` +\n `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`,\n );\n }\n const ping = await pingDaemon(socketPath);\n if (ping.status === 'responsive') {\n process.stderr.write('Daemon ready.\\n');\n return { state: 'started', socketPath };\n }\n }\n\n throw new Error(\n `Daemon did not start within ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s`,\n );\n}\n\n/**\n * Resolve the daemon entry point path and any extra Node.js args needed.\n *\n * In production, uses the compiled dist output. In development, uses tsx\n * to run TypeScript source directly.\n *\n * @param packageRoot - The root directory of the wallet-cli package.\n * @returns The entry path and any extra node args.\n */\nfunction resolveEntryPoint(packageRoot: string): {\n entryPath: string;\n args: string[];\n} {\n const distEntry = join(packageRoot, 'dist', 'daemon', 'daemon-entry.mjs');\n if (existsSync(distEntry)) {\n return { entryPath: distEntry, args: [] };\n }\n\n const srcEntry = join(packageRoot, 'src', 'daemon', 'daemon-entry.ts');\n return {\n entryPath: srcEntry,\n args: ['--import', 'tsx'],\n };\n}\n"]}
1
+ {"version":3,"file":"daemon-spawn.mjs","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { closeSync, existsSync, openSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { pingDaemon } from './daemon-client';\nimport { ensureOwnerOnlyDirectory } from './data-dir';\nimport { getDaemonPaths } from './paths';\nimport type { DaemonSpawnConfig } from './types';\n\nconst POLL_INTERVAL_MS = 100;\nconst MAX_POLLS = 300; // 30 seconds\n\n/**\n * Outcome of {@link ensureDaemon}.\n *\n * - `'already-running'`: a responsive daemon was found at the configured\n * socket path. The supplied flags (`infuraProjectId`, `password`, `srp`)\n * were NOT applied to that daemon; the caller should surface this so a\n * user who is trying to change them isn't silently ignored.\n * - `'started'`: a new daemon was spawned and is now responsive.\n */\nexport type EnsureDaemonResult = {\n state: 'already-running' | 'started';\n socketPath: string;\n};\n\n/**\n * Ensure the daemon is running. If a responsive daemon already exists, return\n * `'already-running'` (caller decides how to surface that). Otherwise spawn\n * one as a detached process and wait until the socket becomes responsive.\n *\n * Refuses to spawn when pinging the existing socket fails with anything other\n * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the\n * existing process and corrupt its PID file.\n *\n * @param config - Spawn configuration.\n * @returns The state of the daemon and the socket path it's listening on.\n */\nexport async function ensureDaemon(\n config: DaemonSpawnConfig,\n): Promise<EnsureDaemonResult> {\n const { socketPath, logPath } = getDaemonPaths(config.dataDir);\n\n const initialPing = await pingDaemon(socketPath);\n switch (initialPing.status) {\n case 'responsive':\n return { state: 'already-running', socketPath };\n case 'unreachable':\n if (initialPing.reason === 'permission') {\n throw new Error(\n `Refusing to start: the socket at ${socketPath} is owned by another user. ` +\n `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +\n `(${initialPing.error.message})`,\n );\n }\n throw new Error(\n `Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +\n `Run \\`mm daemon stop\\` (or \\`mm daemon purge\\`) before starting a new daemon. ` +\n `(${initialPing.error.message})`,\n );\n case 'absent':\n break;\n /* istanbul ignore next -- exhaustiveness guard; unreachable for the current PingResult union */\n default: {\n const exhaustiveCheck: never = initialPing;\n throw new Error(\n `Unexpected daemon ping status: ${String(exhaustiveCheck)}`,\n );\n }\n }\n\n process.stderr.write('Starting daemon...\\n');\n\n const { entryPath, args } = resolveEntryPoint(config.packageRoot);\n\n // Create the data directory before opening the log file inside it. The daemon\n // entry also does this, but only after spawn — opening the log first would\n // ENOENT on a fresh data directory.\n await ensureOwnerOnlyDirectory(config.dataDir);\n\n // Redirect the detached daemon's stderr to its log file rather than\n // discarding it, so a crash after startup stays diagnosable. stdout stays\n // ignored — structured status goes through the file logger.\n const logFileDescriptor = openSync(logPath, 'a');\n const child = spawn(process.execPath, [...args, entryPath], {\n detached: true,\n stdio: ['ignore', 'ignore', logFileDescriptor],\n env: {\n ...process.env,\n MM_DAEMON_DATA_DIR: config.dataDir,\n MM_DAEMON_SOCKET_PATH: socketPath,\n INFURA_PROJECT_ID: config.infuraProjectId,\n MM_WALLET_PASSWORD: config.password,\n MM_WALLET_SRP: config.srp,\n },\n });\n // The child dup'd the file descriptor into its stderr, so drop the parent's\n // copy. Safe on the success path: `spawn` reports failures via the 'error'\n // event, not a synchronous throw.\n closeSync(logFileDescriptor);\n\n type StartupOutcome =\n | { kind: 'pending' }\n | { kind: 'error'; error: Error }\n | { kind: 'exited'; code: number | null; signal: NodeJS.Signals | null };\n\n // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may\n // never emit 'exit', so 'error' is recorded first and not overwritten by a\n // later 'exit' — the loop surfaces the real cause instead of hanging.\n const outcome: { current: StartupOutcome } = { current: { kind: 'pending' } };\n\n child.on('error', (error: Error) => {\n process.stderr.write(`Failed to spawn daemon process: ${String(error)}\\n`);\n outcome.current = { kind: 'error', error };\n });\n child.on('exit', (code, signal) => {\n if (outcome.current.kind === 'pending') {\n outcome.current = { kind: 'exited', code, signal };\n }\n });\n child.unref();\n\n for (let i = 0; i < MAX_POLLS; i++) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n const settled = outcome.current;\n if (settled.kind === 'error') {\n throw new Error(\n `Failed to spawn daemon process: ${settled.error.message}. ` +\n `Check the daemon log at ${logPath}.`,\n );\n }\n if (settled.kind === 'exited') {\n throw new Error(\n `Daemon process exited during startup (code=${String(settled.code)}, signal=${String(settled.signal)}). ` +\n `Check the daemon log at ${logPath}.`,\n );\n }\n const ping = await pingDaemon(socketPath);\n if (ping.status === 'responsive') {\n process.stderr.write('Daemon ready.\\n');\n return { state: 'started', socketPath };\n }\n }\n\n throw new Error(\n `Daemon did not start within ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s`,\n );\n}\n\n/**\n * Resolve the daemon entry point path and any extra Node.js args needed.\n *\n * In production, uses the compiled dist output. In development, uses tsx\n * to run TypeScript source directly.\n *\n * @param packageRoot - The root directory of the wallet-cli package.\n * @returns The entry path and any extra node args.\n */\nfunction resolveEntryPoint(packageRoot: string): {\n entryPath: string;\n args: string[];\n} {\n const distEntry = join(packageRoot, 'dist', 'daemon', 'daemon-entry.mjs');\n if (existsSync(distEntry)) {\n return { entryPath: distEntry, args: [] };\n }\n\n const srcEntry = join(packageRoot, 'src', 'daemon', 'daemon-entry.ts');\n return {\n entryPath: srcEntry,\n args: ['--import', 'tsx'],\n };\n}\n"]}
@@ -18,13 +18,17 @@ async function writeLine(socket, line) {
18
18
  };
19
19
  socket.once('error', onError);
20
20
  socket.write(`${line}\n`, (error) => {
21
- socket.removeListener('error', onError);
22
21
  if (error) {
22
+ // A failed write (e.g. EPIPE) is delivered BOTH here AND as a later
23
+ // 'error' event. Leave `onError` attached to handle that event —
24
+ // detaching here would let Node treat it as unhandled and crash the
25
+ // process. This reject settles the promise; `onError` detaches itself
26
+ // when/if the event arrives.
23
27
  reject(error);
28
+ return;
24
29
  }
25
- else {
26
- resolve();
27
- }
30
+ socket.removeListener('error', onError);
31
+ resolve();
28
32
  });
29
33
  });
30
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"socket-line.cjs","sourceRoot":"","sources":["../../src/daemon/socket-line.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Socket } from 'node:net';\n\n/**\n * Write a newline-delimited line to a socket.\n *\n * @param socket - The socket to write to.\n * @param line - The line to write (without trailing newline).\n */\nexport async function writeLine(socket: Socket, line: string): Promise<void> {\n return new Promise((resolve, reject) => {\n // A socket 'error' can fire while the write is in flight without ever\n // reaching the write callback; without this listener Node would treat it\n // as an unhandled 'error' event and crash the process.\n const onError = (error: Error): void => {\n socket.removeListener('error', onError);\n reject(error);\n };\n socket.once('error', onError);\n\n socket.write(`${line}\\n`, (error) => {\n socket.removeListener('error', onError);\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n\n/**\n * Read a single newline-delimited line from a socket.\n *\n * @param socket - The socket to read from.\n * @param timeoutMs - Optional timeout in milliseconds. Rejects with a timeout\n * error if no complete line is received within the limit.\n * @returns The line read (without trailing newline).\n */\nexport async function readLine(\n socket: Socket,\n timeoutMs?: number,\n): Promise<string> {\n return new Promise((resolve, reject) => {\n let buffer = '';\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n if (timeoutMs !== undefined) {\n timer = setTimeout(() => {\n cleanup();\n reject(new Error('Socket read timed out'));\n }, timeoutMs);\n }\n\n const onData = (data: Buffer): void => {\n buffer += data.toString();\n const idx = buffer.indexOf('\\n');\n if (idx !== -1) {\n cleanup();\n resolve(buffer.slice(0, idx));\n }\n };\n\n const onError = (error: Error): void => {\n cleanup();\n reject(error);\n };\n\n const onEnd = (): void => {\n cleanup();\n reject(new Error('Socket closed before response received'));\n };\n\n const onClose = (): void => {\n cleanup();\n reject(new Error('Socket closed before response received'));\n };\n\n /**\n * Remove listeners registered by this call and clear the timeout.\n */\n function cleanup(): void {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n socket.removeListener('data', onData);\n socket.removeListener('error', onError);\n socket.removeListener('end', onEnd);\n socket.removeListener('close', onClose);\n }\n\n socket.on('data', onData);\n socket.once('error', onError);\n socket.once('end', onEnd);\n socket.once('close', onClose);\n });\n}\n"]}
1
+ {"version":3,"file":"socket-line.cjs","sourceRoot":"","sources":["../../src/daemon/socket-line.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Socket } from 'node:net';\n\n/**\n * Write a newline-delimited line to a socket.\n *\n * @param socket - The socket to write to.\n * @param line - The line to write (without trailing newline).\n */\nexport async function writeLine(socket: Socket, line: string): Promise<void> {\n return new Promise((resolve, reject) => {\n // A socket 'error' can fire while the write is in flight without ever\n // reaching the write callback; without this listener Node would treat it\n // as an unhandled 'error' event and crash the process.\n const onError = (error: Error): void => {\n socket.removeListener('error', onError);\n reject(error);\n };\n socket.once('error', onError);\n\n socket.write(`${line}\\n`, (error) => {\n if (error) {\n // A failed write (e.g. EPIPE) is delivered BOTH here AND as a later\n // 'error' event. Leave `onError` attached to handle that event —\n // detaching here would let Node treat it as unhandled and crash the\n // process. This reject settles the promise; `onError` detaches itself\n // when/if the event arrives.\n reject(error);\n return;\n }\n socket.removeListener('error', onError);\n resolve();\n });\n });\n}\n\n/**\n * Read a single newline-delimited line from a socket.\n *\n * @param socket - The socket to read from.\n * @param timeoutMs - Optional timeout in milliseconds. Rejects with a timeout\n * error if no complete line is received within the limit.\n * @returns The line read (without trailing newline).\n */\nexport async function readLine(\n socket: Socket,\n timeoutMs?: number,\n): Promise<string> {\n return new Promise((resolve, reject) => {\n let buffer = '';\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n if (timeoutMs !== undefined) {\n timer = setTimeout(() => {\n cleanup();\n reject(new Error('Socket read timed out'));\n }, timeoutMs);\n }\n\n const onData = (data: Buffer): void => {\n buffer += data.toString();\n const idx = buffer.indexOf('\\n');\n if (idx !== -1) {\n cleanup();\n resolve(buffer.slice(0, idx));\n }\n };\n\n const onError = (error: Error): void => {\n cleanup();\n reject(error);\n };\n\n const onEnd = (): void => {\n cleanup();\n reject(new Error('Socket closed before response received'));\n };\n\n const onClose = (): void => {\n cleanup();\n reject(new Error('Socket closed before response received'));\n };\n\n /**\n * Remove listeners registered by this call and clear the timeout.\n */\n function cleanup(): void {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n socket.removeListener('data', onData);\n socket.removeListener('error', onError);\n socket.removeListener('end', onEnd);\n socket.removeListener('close', onClose);\n }\n\n socket.on('data', onData);\n socket.once('error', onError);\n socket.once('end', onEnd);\n socket.once('close', onClose);\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"socket-line.d.cts","sourceRoot":"","sources":["../../src/daemon/socket-line.ts"],"names":[],"mappings":";;;AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB;AAEvC;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAsDjB"}
1
+ {"version":3,"file":"socket-line.d.cts","sourceRoot":"","sources":["../../src/daemon/socket-line.ts"],"names":[],"mappings":";;;AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB;AAEvC;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB3E;AAED;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAsDjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"socket-line.d.mts","sourceRoot":"","sources":["../../src/daemon/socket-line.ts"],"names":[],"mappings":";;;AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB;AAEvC;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E;AAED;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAsDjB"}
1
+ {"version":3,"file":"socket-line.d.mts","sourceRoot":"","sources":["../../src/daemon/socket-line.ts"],"names":[],"mappings":";;;AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB;AAEvC;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB3E;AAED;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAsDjB"}
@@ -15,13 +15,17 @@ export async function writeLine(socket, line) {
15
15
  };
16
16
  socket.once('error', onError);
17
17
  socket.write(`${line}\n`, (error) => {
18
- socket.removeListener('error', onError);
19
18
  if (error) {
19
+ // A failed write (e.g. EPIPE) is delivered BOTH here AND as a later
20
+ // 'error' event. Leave `onError` attached to handle that event —
21
+ // detaching here would let Node treat it as unhandled and crash the
22
+ // process. This reject settles the promise; `onError` detaches itself
23
+ // when/if the event arrives.
20
24
  reject(error);
25
+ return;
21
26
  }
22
- else {
23
- resolve();
24
- }
27
+ socket.removeListener('error', onError);
28
+ resolve();
25
29
  });
26
30
  });
27
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"socket-line.mjs","sourceRoot":"","sources":["../../src/daemon/socket-line.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Socket } from 'node:net';\n\n/**\n * Write a newline-delimited line to a socket.\n *\n * @param socket - The socket to write to.\n * @param line - The line to write (without trailing newline).\n */\nexport async function writeLine(socket: Socket, line: string): Promise<void> {\n return new Promise((resolve, reject) => {\n // A socket 'error' can fire while the write is in flight without ever\n // reaching the write callback; without this listener Node would treat it\n // as an unhandled 'error' event and crash the process.\n const onError = (error: Error): void => {\n socket.removeListener('error', onError);\n reject(error);\n };\n socket.once('error', onError);\n\n socket.write(`${line}\\n`, (error) => {\n socket.removeListener('error', onError);\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n\n/**\n * Read a single newline-delimited line from a socket.\n *\n * @param socket - The socket to read from.\n * @param timeoutMs - Optional timeout in milliseconds. Rejects with a timeout\n * error if no complete line is received within the limit.\n * @returns The line read (without trailing newline).\n */\nexport async function readLine(\n socket: Socket,\n timeoutMs?: number,\n): Promise<string> {\n return new Promise((resolve, reject) => {\n let buffer = '';\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n if (timeoutMs !== undefined) {\n timer = setTimeout(() => {\n cleanup();\n reject(new Error('Socket read timed out'));\n }, timeoutMs);\n }\n\n const onData = (data: Buffer): void => {\n buffer += data.toString();\n const idx = buffer.indexOf('\\n');\n if (idx !== -1) {\n cleanup();\n resolve(buffer.slice(0, idx));\n }\n };\n\n const onError = (error: Error): void => {\n cleanup();\n reject(error);\n };\n\n const onEnd = (): void => {\n cleanup();\n reject(new Error('Socket closed before response received'));\n };\n\n const onClose = (): void => {\n cleanup();\n reject(new Error('Socket closed before response received'));\n };\n\n /**\n * Remove listeners registered by this call and clear the timeout.\n */\n function cleanup(): void {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n socket.removeListener('data', onData);\n socket.removeListener('error', onError);\n socket.removeListener('end', onEnd);\n socket.removeListener('close', onClose);\n }\n\n socket.on('data', onData);\n socket.once('error', onError);\n socket.once('end', onEnd);\n socket.once('close', onClose);\n });\n}\n"]}
1
+ {"version":3,"file":"socket-line.mjs","sourceRoot":"","sources":["../../src/daemon/socket-line.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Socket } from 'node:net';\n\n/**\n * Write a newline-delimited line to a socket.\n *\n * @param socket - The socket to write to.\n * @param line - The line to write (without trailing newline).\n */\nexport async function writeLine(socket: Socket, line: string): Promise<void> {\n return new Promise((resolve, reject) => {\n // A socket 'error' can fire while the write is in flight without ever\n // reaching the write callback; without this listener Node would treat it\n // as an unhandled 'error' event and crash the process.\n const onError = (error: Error): void => {\n socket.removeListener('error', onError);\n reject(error);\n };\n socket.once('error', onError);\n\n socket.write(`${line}\\n`, (error) => {\n if (error) {\n // A failed write (e.g. EPIPE) is delivered BOTH here AND as a later\n // 'error' event. Leave `onError` attached to handle that event —\n // detaching here would let Node treat it as unhandled and crash the\n // process. This reject settles the promise; `onError` detaches itself\n // when/if the event arrives.\n reject(error);\n return;\n }\n socket.removeListener('error', onError);\n resolve();\n });\n });\n}\n\n/**\n * Read a single newline-delimited line from a socket.\n *\n * @param socket - The socket to read from.\n * @param timeoutMs - Optional timeout in milliseconds. Rejects with a timeout\n * error if no complete line is received within the limit.\n * @returns The line read (without trailing newline).\n */\nexport async function readLine(\n socket: Socket,\n timeoutMs?: number,\n): Promise<string> {\n return new Promise((resolve, reject) => {\n let buffer = '';\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n if (timeoutMs !== undefined) {\n timer = setTimeout(() => {\n cleanup();\n reject(new Error('Socket read timed out'));\n }, timeoutMs);\n }\n\n const onData = (data: Buffer): void => {\n buffer += data.toString();\n const idx = buffer.indexOf('\\n');\n if (idx !== -1) {\n cleanup();\n resolve(buffer.slice(0, idx));\n }\n };\n\n const onError = (error: Error): void => {\n cleanup();\n reject(error);\n };\n\n const onEnd = (): void => {\n cleanup();\n reject(new Error('Socket closed before response received'));\n };\n\n const onClose = (): void => {\n cleanup();\n reject(new Error('Socket closed before response received'));\n };\n\n /**\n * Remove listeners registered by this call and clear the timeout.\n */\n function cleanup(): void {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n socket.removeListener('data', onData);\n socket.removeListener('error', onError);\n socket.removeListener('end', onEnd);\n socket.removeListener('close', onClose);\n }\n\n socket.on('data', onData);\n socket.once('error', onError);\n socket.once('end', onEnd);\n socket.once('close', onClose);\n });\n}\n"]}
@@ -5,15 +5,25 @@ const promises_1 = require("node:fs/promises");
5
5
  const daemon_client_1 = require("./daemon-client.cjs");
6
6
  const utils_1 = require("./utils.cjs");
7
7
  /**
8
- * Stop the daemon via a `shutdown` RPC call. Falls back to PID + SIGTERM if
9
- * the socket is unresponsive, and escalates to SIGKILL if SIGTERM is ignored.
8
+ * Stop the daemon, preferring a graceful shutdown.
10
9
  *
11
- * Signals are sent when EITHER the socket was observed (`responsive` or
12
- * `unreachable`) OR the recorded PID is still alive on its own. The
13
- * socket-absent + alive-PID branch trades a small risk of signalling a
10
+ * Resolution order when a live daemon is present:
11
+ * 1. If the socket is responsive, request a graceful `shutdown` over it.
12
+ * 2. If the recorded PID is still alive, escalate to SIGTERM.
13
+ * 3. ...then SIGKILL.
14
+ *
15
+ * Signals (steps 2-3) are only ever sent against a PID that is observed alive.
16
+ * The socket-absent + alive-PID branch trades a small risk of signalling a
14
17
  * recycled PID for the larger risk of leaving an orphan daemon holding the
15
- * SQLite database — which `daemon purge` would otherwise wipe out from
16
- * under it.
18
+ * SQLite database — which `daemon purge` would otherwise wipe out from under
19
+ * it.
20
+ *
21
+ * When the socket is NOT responsive AND the recorded PID is dead (or there is
22
+ * no PID file), there is no live daemon: a lingering socket or PID file is
23
+ * stale leftovers from a daemon that already exited — typically one that
24
+ * crashed without running its own cleanup. Those files are removed and the
25
+ * stop is reported as successful, rather than failing on a daemon that is
26
+ * already gone.
17
27
  *
18
28
  * @param socketPath - The daemon socket path.
19
29
  * @param pidPath - The daemon PID file path.
@@ -25,9 +35,13 @@ async function stopDaemon(socketPath, pidPath, log) {
25
35
  const ping = await (0, daemon_client_1.pingDaemon)(socketPath);
26
36
  const socketObserved = ping.status === 'responsive' || ping.status === 'unreachable';
27
37
  const processAlive = pid !== undefined && (0, utils_1.isProcessAlive)(pid);
28
- if (!socketObserved && !processAlive) {
29
- // No live daemon evidence. Just remove the stale PID file if any.
38
+ // Only `absent` and `refused` prove no live daemon; `permission`/`timeout`/
39
+ // `protocol` may be a wedged or foreign daemon, so those fall through.
40
+ const socketProvenGone = ping.status === 'absent' ||
41
+ (ping.status === 'unreachable' && ping.reason === 'refused');
42
+ if (socketProvenGone && !processAlive) {
30
43
  await cleanupFile(pidPath, 'PID file', log);
44
+ await cleanupFile(socketPath, 'socket file', log);
31
45
  return true;
32
46
  }
33
47
  log?.('Stopping daemon...');
@@ -1 +1 @@
1
- {"version":3,"file":"stop-daemon.cjs","sourceRoot":"","sources":["../../src/daemon/stop-daemon.ts"],"names":[],"mappings":"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","sourcesContent":["import { rm } from 'node:fs/promises';\n\nimport { pingDaemon, sendCommand } from './daemon-client';\nimport { isProcessAlive, readPidFile, sendSignal, waitFor } from './utils';\n\n/**\n * Stop the daemon via a `shutdown` RPC call. Falls back to PID + SIGTERM if\n * the socket is unresponsive, and escalates to SIGKILL if SIGTERM is ignored.\n *\n * Signals are sent when EITHER the socket was observed (`responsive` or\n * `unreachable`) OR the recorded PID is still alive on its own. The\n * socket-absent + alive-PID branch trades a small risk of signalling a\n * recycled PID for the larger risk of leaving an orphan daemon holding the\n * SQLite database — which `daemon purge` would otherwise wipe out from\n * under it.\n *\n * @param socketPath - The daemon socket path.\n * @param pidPath - The daemon PID file path.\n * @param log - Optional logging function for status messages.\n * @returns True if the daemon was stopped (or was not running).\n */\nexport async function stopDaemon(\n socketPath: string,\n pidPath: string,\n log?: (message: string) => void,\n): Promise<boolean> {\n const pid = await readPidFile(pidPath);\n const ping = await pingDaemon(socketPath);\n const socketObserved =\n ping.status === 'responsive' || ping.status === 'unreachable';\n const processAlive = pid !== undefined && isProcessAlive(pid);\n\n if (!socketObserved && !processAlive) {\n // No live daemon evidence. Just remove the stale PID file if any.\n await cleanupFile(pidPath, 'PID file', log);\n return true;\n }\n\n log?.('Stopping daemon...');\n\n let stopped = false;\n\n // Strategy 1: Graceful socket-based shutdown.\n if (ping.status === 'responsive') {\n try {\n await sendCommand({ socketPath, method: 'shutdown' });\n } catch (error) {\n log?.(`Graceful shutdown request failed: ${String(error)}`);\n }\n // A quiet socket does not prove the process exited; require the recorded\n // pid to be gone too, so a daemon that outlived its socket falls through to\n // SIGTERM/SIGKILL rather than being orphaned.\n stopped = await waitFor(\n async () =>\n (await pingDaemon(socketPath)).status !== 'responsive' &&\n (pid === undefined || !isProcessAlive(pid)),\n 5_000,\n );\n }\n\n // Strategy 2: SIGTERM. Signal when either the socket was observed or the\n // recorded PID is alive; the absent+alive case typically means someone\n // removed the socket from under a live daemon.\n if (!stopped && processAlive && pid !== undefined) {\n if (!socketObserved) {\n log?.(\n `Socket at ${socketPath} is absent but recorded pid ${pid} is alive; signalling anyway.`,\n );\n }\n try {\n if (sendSignal(pid, 'SIGTERM')) {\n stopped = await waitFor(() => !isProcessAlive(pid), 5_000);\n } else {\n stopped = true; // Process already gone (ESRCH).\n }\n } catch (error) {\n log?.(`SIGTERM failed: ${String(error)}`);\n }\n }\n\n // Strategy 3: SIGKILL.\n if (!stopped && processAlive && pid !== undefined) {\n try {\n if (sendSignal(pid, 'SIGKILL')) {\n stopped = await waitFor(() => !isProcessAlive(pid), 2_000);\n } else {\n stopped = true; // Process already gone (ESRCH).\n }\n } catch (error) {\n log?.(`SIGKILL failed: ${String(error)}`);\n }\n }\n\n if (stopped) {\n await cleanupFile(pidPath, 'PID file', log);\n await cleanupFile(socketPath, 'socket file', log);\n log?.('Daemon stopped.');\n }\n\n return stopped;\n}\n\n/**\n * Remove a file best-effort, logging any failure rather than letting it\n * propagate. ENOENT is silently ignored via `force: true`.\n *\n * @param path - The file path to remove.\n * @param label - Human-readable label for log messages.\n * @param log - Optional log sink.\n */\nasync function cleanupFile(\n path: string,\n label: string,\n log: ((message: string) => void) | undefined,\n): Promise<void> {\n await rm(path, { force: true }).catch((error: unknown) => {\n log?.(`Failed to remove ${label}: ${String(error)}`);\n });\n}\n"]}
1
+ {"version":3,"file":"stop-daemon.cjs","sourceRoot":"","sources":["../../src/daemon/stop-daemon.ts"],"names":[],"mappings":"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","sourcesContent":["import { rm } from 'node:fs/promises';\n\nimport { pingDaemon, sendCommand } from './daemon-client';\nimport { isProcessAlive, readPidFile, sendSignal, waitFor } from './utils';\n\n/**\n * Stop the daemon, preferring a graceful shutdown.\n *\n * Resolution order when a live daemon is present:\n * 1. If the socket is responsive, request a graceful `shutdown` over it.\n * 2. If the recorded PID is still alive, escalate to SIGTERM.\n * 3. ...then SIGKILL.\n *\n * Signals (steps 2-3) are only ever sent against a PID that is observed alive.\n * The socket-absent + alive-PID branch trades a small risk of signalling a\n * recycled PID for the larger risk of leaving an orphan daemon holding the\n * SQLite database — which `daemon purge` would otherwise wipe out from under\n * it.\n *\n * When the socket is NOT responsive AND the recorded PID is dead (or there is\n * no PID file), there is no live daemon: a lingering socket or PID file is\n * stale leftovers from a daemon that already exited — typically one that\n * crashed without running its own cleanup. Those files are removed and the\n * stop is reported as successful, rather than failing on a daemon that is\n * already gone.\n *\n * @param socketPath - The daemon socket path.\n * @param pidPath - The daemon PID file path.\n * @param log - Optional logging function for status messages.\n * @returns True if the daemon was stopped (or was not running).\n */\nexport async function stopDaemon(\n socketPath: string,\n pidPath: string,\n log?: (message: string) => void,\n): Promise<boolean> {\n const pid = await readPidFile(pidPath);\n const ping = await pingDaemon(socketPath);\n const socketObserved =\n ping.status === 'responsive' || ping.status === 'unreachable';\n const processAlive = pid !== undefined && isProcessAlive(pid);\n\n // Only `absent` and `refused` prove no live daemon; `permission`/`timeout`/\n // `protocol` may be a wedged or foreign daemon, so those fall through.\n const socketProvenGone =\n ping.status === 'absent' ||\n (ping.status === 'unreachable' && ping.reason === 'refused');\n\n if (socketProvenGone && !processAlive) {\n await cleanupFile(pidPath, 'PID file', log);\n await cleanupFile(socketPath, 'socket file', log);\n return true;\n }\n\n log?.('Stopping daemon...');\n\n let stopped = false;\n\n // Strategy 1: Graceful socket-based shutdown.\n if (ping.status === 'responsive') {\n try {\n await sendCommand({ socketPath, method: 'shutdown' });\n } catch (error) {\n log?.(`Graceful shutdown request failed: ${String(error)}`);\n }\n // A quiet socket does not prove the process exited; require the recorded\n // pid to be gone too, so a daemon that outlived its socket falls through to\n // SIGTERM/SIGKILL rather than being orphaned.\n stopped = await waitFor(\n async () =>\n (await pingDaemon(socketPath)).status !== 'responsive' &&\n (pid === undefined || !isProcessAlive(pid)),\n 5_000,\n );\n }\n\n // Strategy 2: SIGTERM. Signal when either the socket was observed or the\n // recorded PID is alive; the absent+alive case typically means someone\n // removed the socket from under a live daemon.\n if (!stopped && processAlive && pid !== undefined) {\n if (!socketObserved) {\n log?.(\n `Socket at ${socketPath} is absent but recorded pid ${pid} is alive; signalling anyway.`,\n );\n }\n try {\n if (sendSignal(pid, 'SIGTERM')) {\n stopped = await waitFor(() => !isProcessAlive(pid), 5_000);\n } else {\n stopped = true; // Process already gone (ESRCH).\n }\n } catch (error) {\n log?.(`SIGTERM failed: ${String(error)}`);\n }\n }\n\n // Strategy 3: SIGKILL.\n if (!stopped && processAlive && pid !== undefined) {\n try {\n if (sendSignal(pid, 'SIGKILL')) {\n stopped = await waitFor(() => !isProcessAlive(pid), 2_000);\n } else {\n stopped = true; // Process already gone (ESRCH).\n }\n } catch (error) {\n log?.(`SIGKILL failed: ${String(error)}`);\n }\n }\n\n if (stopped) {\n await cleanupFile(pidPath, 'PID file', log);\n await cleanupFile(socketPath, 'socket file', log);\n log?.('Daemon stopped.');\n }\n\n return stopped;\n}\n\n/**\n * Remove a file best-effort, logging any failure rather than letting it\n * propagate. ENOENT is silently ignored via `force: true`.\n *\n * @param path - The file path to remove.\n * @param label - Human-readable label for log messages.\n * @param log - Optional log sink.\n */\nasync function cleanupFile(\n path: string,\n label: string,\n log: ((message: string) => void) | undefined,\n): Promise<void> {\n await rm(path, { force: true }).catch((error: unknown) => {\n log?.(`Failed to remove ${label}: ${String(error)}`);\n });\n}\n"]}
@@ -1,13 +1,23 @@
1
1
  /**
2
- * Stop the daemon via a `shutdown` RPC call. Falls back to PID + SIGTERM if
3
- * the socket is unresponsive, and escalates to SIGKILL if SIGTERM is ignored.
2
+ * Stop the daemon, preferring a graceful shutdown.
4
3
  *
5
- * Signals are sent when EITHER the socket was observed (`responsive` or
6
- * `unreachable`) OR the recorded PID is still alive on its own. The
7
- * socket-absent + alive-PID branch trades a small risk of signalling a
4
+ * Resolution order when a live daemon is present:
5
+ * 1. If the socket is responsive, request a graceful `shutdown` over it.
6
+ * 2. If the recorded PID is still alive, escalate to SIGTERM.
7
+ * 3. ...then SIGKILL.
8
+ *
9
+ * Signals (steps 2-3) are only ever sent against a PID that is observed alive.
10
+ * The socket-absent + alive-PID branch trades a small risk of signalling a
8
11
  * recycled PID for the larger risk of leaving an orphan daemon holding the
9
- * SQLite database — which `daemon purge` would otherwise wipe out from
10
- * under it.
12
+ * SQLite database — which `daemon purge` would otherwise wipe out from under
13
+ * it.
14
+ *
15
+ * When the socket is NOT responsive AND the recorded PID is dead (or there is
16
+ * no PID file), there is no live daemon: a lingering socket or PID file is
17
+ * stale leftovers from a daemon that already exited — typically one that
18
+ * crashed without running its own cleanup. Those files are removed and the
19
+ * stop is reported as successful, rather than failing on a daemon that is
20
+ * already gone.
11
21
  *
12
22
  * @param socketPath - The daemon socket path.
13
23
  * @param pidPath - The daemon PID file path.
@@ -1 +1 @@
1
- {"version":3,"file":"stop-daemon.d.cts","sourceRoot":"","sources":["../../src/daemon/stop-daemon.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,OAAO,CAAC,OAAO,CAAC,CA2ElB"}
1
+ {"version":3,"file":"stop-daemon.d.cts","sourceRoot":"","sources":["../../src/daemon/stop-daemon.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,OAAO,CAAC,OAAO,CAAC,CAiFlB"}