-
-
Notifications
You must be signed in to change notification settings - Fork 280
Expand file tree
/
Copy pathdaemon-spawn.ts
More file actions
134 lines (120 loc) · 4.51 KB
/
daemon-spawn.ts
File metadata and controls
134 lines (120 loc) · 4.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { spawn } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { pingDaemon } from './daemon-client';
import { getDaemonPaths } from './paths';
import type { DaemonSpawnConfig } from './types';
const POLL_INTERVAL_MS = 100;
const MAX_POLLS = 300; // 30 seconds
/**
* Outcome of {@link ensureDaemon}.
*
* - `'already-running'`: a responsive daemon was found at the configured
* socket path. The supplied flags (`infuraProjectId`, `password`, `srp`)
* were NOT applied to that daemon; the caller should surface this so a
* user who is trying to change them isn't silently ignored.
* - `'started'`: a new daemon was spawned and is now responsive.
*/
export type EnsureDaemonResult = {
state: 'already-running' | 'started';
socketPath: string;
};
/**
* Ensure the daemon is running. If a responsive daemon already exists, return
* `'already-running'` (caller decides how to surface that). Otherwise spawn
* one as a detached process and wait until the socket becomes responsive.
*
* Refuses to spawn when pinging the existing socket fails with anything other
* than `ENOENT` (wedged or foreign daemon) — taking over could orphan the
* existing process and corrupt its PID file.
*
* @param config - Spawn configuration.
* @returns The state of the daemon and the socket path it's listening on.
*/
export async function ensureDaemon(
config: DaemonSpawnConfig,
): Promise<EnsureDaemonResult> {
const { socketPath } = getDaemonPaths(config.dataDir);
const initialPing = await pingDaemon(socketPath);
if (initialPing.status === 'responsive') {
return { state: 'already-running', socketPath };
}
if (initialPing.status === 'unreachable') {
if (initialPing.reason === 'permission') {
throw new Error(
`Refusing to start: the socket at ${socketPath} is owned by another user. ` +
`Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +
`(${initialPing.error.message})`,
);
}
throw new Error(
`Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +
`Run \`mm daemon stop\` (or \`mm daemon purge\`) before starting a new daemon. ` +
`(${initialPing.error.message})`,
);
}
process.stderr.write('Starting daemon...\n');
const { entryPath, args } = resolveEntryPoint(config.packageRoot);
const child = spawn(process.execPath, [...args, entryPath], {
detached: true,
stdio: 'ignore',
env: {
...process.env,
MM_DAEMON_DATA_DIR: config.dataDir,
MM_DAEMON_SOCKET_PATH: socketPath,
INFURA_PROJECT_ID: config.infuraProjectId,
MM_WALLET_PASSWORD: config.password.unwrap(),
MM_WALLET_SRP: config.srp.unwrap(),
},
});
type ExitInfo = { code: number | null; signal: NodeJS.Signals | null };
const exitInfo: { value: ExitInfo | null } = { value: null };
child.on('error', (error) => {
process.stderr.write(`Failed to spawn daemon process: ${String(error)}\n`);
});
child.on('exit', (code, signal) => {
exitInfo.value = { code, signal };
});
child.unref();
for (let i = 0; i < MAX_POLLS; i++) {
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
if (exitInfo.value !== null) {
const { code, signal } = exitInfo.value;
throw new Error(
`Daemon process exited during startup (code=${String(code)}, signal=${String(signal)}). ` +
`Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`,
);
}
const ping = await pingDaemon(socketPath);
if (ping.status === 'responsive') {
process.stderr.write('Daemon ready.\n');
return { state: 'started', socketPath };
}
}
throw new Error(
`Daemon did not start within ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s`,
);
}
/**
* Resolve the daemon entry point path and any extra Node.js args needed.
*
* In production, uses the compiled dist output. In development, uses tsx
* to run TypeScript source directly.
*
* @param packageRoot - The root directory of the wallet-cli package.
* @returns The entry path and any extra node args.
*/
function resolveEntryPoint(packageRoot: string): {
entryPath: string;
args: string[];
} {
const distEntry = join(packageRoot, 'dist', 'daemon', 'daemon-entry.mjs');
if (existsSync(distEntry)) {
return { entryPath: distEntry, args: [] };
}
const srcEntry = join(packageRoot, 'src', 'daemon', 'daemon-entry.ts');
return {
entryPath: srcEntry,
args: ['--import', 'tsx'],
};
}