@dxos/phoenix 0.1.58-main.88ec9da → 0.1.58-main.935caa2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/node/index.cjs +241 -91
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/daemon-manager.d.ts +17 -0
- package/dist/types/src/daemon-manager.d.ts.map +1 -0
- package/dist/types/src/daemon-manager.test.d.ts +2 -0
- package/dist/types/src/daemon-manager.test.d.ts.map +1 -0
- package/dist/types/src/defs.d.ts +1 -3
- package/dist/types/src/defs.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/utils.d.ts +3 -3
- package/dist/types/src/utils.d.ts.map +1 -1
- package/dist/types/src/watchdog.d.ts +24 -5
- package/dist/types/src/watchdog.d.ts.map +1 -1
- package/package.json +12 -9
- package/src/{phoenix.test.ts → daemon-manager.test.ts} +21 -17
- package/src/daemon-manager.ts +138 -0
- package/src/defs.ts +1 -3
- package/src/index.ts +1 -1
- package/src/utils.ts +18 -15
- package/src/watchdog.test.ts +10 -8
- package/src/watchdog.ts +109 -29
- package/dist/types/src/phoenix.d.ts +0 -16
- package/dist/types/src/phoenix.d.ts.map +0 -1
- package/dist/types/src/phoenix.test.d.ts +0 -2
- package/dist/types/src/phoenix.test.d.ts.map +0 -1
- package/src/phoenix.ts +0 -94
package/src/watchdog.ts
CHANGED
|
@@ -3,31 +3,48 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
|
|
6
|
-
import {
|
|
6
|
+
import { writeFileSync } from 'node:fs';
|
|
7
7
|
import { FileHandle } from 'node:fs/promises';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import psTree from 'ps-tree';
|
|
8
10
|
|
|
9
11
|
import { synchronized } from '@dxos/async';
|
|
12
|
+
import { Context } from '@dxos/context';
|
|
10
13
|
import { invariant } from '@dxos/invariant';
|
|
14
|
+
import { LockFile } from '@dxos/lock-file';
|
|
11
15
|
import { log } from '@dxos/log';
|
|
12
16
|
|
|
13
|
-
import {
|
|
17
|
+
import { waitForLockAcquisition, waitForLockRelease } from './utils';
|
|
18
|
+
|
|
19
|
+
export type DaemonInfo = {
|
|
20
|
+
pid: number;
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
cwd: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
};
|
|
14
26
|
|
|
15
27
|
export type ProcessInfo = WatchDogParams & {
|
|
16
28
|
pid?: number;
|
|
17
|
-
|
|
29
|
+
timestamp?: number;
|
|
18
30
|
restarts?: number;
|
|
19
31
|
running?: boolean;
|
|
20
32
|
};
|
|
21
33
|
|
|
22
|
-
export type
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
export type Lock = {
|
|
35
|
+
lockFile: string; // Path to lock file
|
|
36
|
+
};
|
|
25
37
|
|
|
38
|
+
export type Logs = {
|
|
26
39
|
//
|
|
27
40
|
// Log files and associated logging options for this instance
|
|
28
41
|
//
|
|
29
42
|
logFile: string; // Path to log output all logs
|
|
30
43
|
errFile: string; // Path to log output from child stderr
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type ChildParams = {
|
|
47
|
+
uid: string; // Unique identifier for this instance
|
|
31
48
|
|
|
32
49
|
//
|
|
33
50
|
// Basic configuration options
|
|
@@ -39,7 +56,7 @@ export type WatchDogParams = {
|
|
|
39
56
|
// Command to spawn as well as options and other vars
|
|
40
57
|
// (env, cwd, etc) to pass along
|
|
41
58
|
//
|
|
42
|
-
command
|
|
59
|
+
command?: string; // Binary to run (default: 'node')
|
|
43
60
|
args?: string[] | undefined; // Additional arguments to pass to the script,
|
|
44
61
|
|
|
45
62
|
//
|
|
@@ -51,19 +68,35 @@ export type WatchDogParams = {
|
|
|
51
68
|
shell?: boolean | undefined;
|
|
52
69
|
};
|
|
53
70
|
|
|
71
|
+
export type WatchDogParams = ChildParams & Lock & Logs;
|
|
72
|
+
|
|
54
73
|
export class WatchDog {
|
|
55
74
|
private _lock?: FileHandle;
|
|
56
75
|
private _child?: ChildProcessWithoutNullStreams;
|
|
57
76
|
private _restarts = 0;
|
|
77
|
+
private _processCtx?: Context;
|
|
58
78
|
|
|
59
79
|
constructor(private readonly _params: WatchDogParams) {}
|
|
60
80
|
|
|
61
81
|
@synchronized
|
|
62
82
|
async start() {
|
|
83
|
+
await this._acquireLock();
|
|
84
|
+
this._log('Lock acquired.');
|
|
63
85
|
const { cwd, shell, env, command, args } = { cwd: process.cwd(), ...this._params };
|
|
86
|
+
invariant(command, 'Command is not defined.');
|
|
64
87
|
|
|
65
|
-
this._log(`Spawning process
|
|
88
|
+
this._log(`Spawning process ${command} ${args?.join(' ')}`);
|
|
66
89
|
this._child = spawn(command, args, { cwd, shell, env, stdio: 'pipe' });
|
|
90
|
+
this._processCtx = new Context();
|
|
91
|
+
|
|
92
|
+
const childInfo: ProcessInfo = {
|
|
93
|
+
pid: process.pid,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
restarts: this._restarts,
|
|
96
|
+
...this._params,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
writeFileSync(this._params.lockFile, JSON.stringify(childInfo, undefined, 2), { encoding: 'utf-8' });
|
|
67
100
|
|
|
68
101
|
this._child.stdout.on('data', (data: Uint8Array) => {
|
|
69
102
|
this._log(String(data));
|
|
@@ -71,27 +104,44 @@ export class WatchDog {
|
|
|
71
104
|
this._child.stderr.on('data', (data: Uint8Array) => {
|
|
72
105
|
this._err(data);
|
|
73
106
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
|
|
108
|
+
// Setup restart handler.
|
|
109
|
+
{
|
|
110
|
+
const restartHandler = async (code: number, signal: number | NodeJS.Signals) => {
|
|
111
|
+
if (code && code !== 0) {
|
|
112
|
+
this._err(`Died unexpectedly with exit code ${code} (signal: ${signal}).`);
|
|
113
|
+
await this.restart();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
this._child.on('close', restartHandler);
|
|
118
|
+
|
|
119
|
+
// We should unsubscribe from the event when the process is killed by us to not try to restart it.
|
|
120
|
+
this._processCtx.onDispose(() => {
|
|
121
|
+
this._child!.off('close', restartHandler);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this._child.on('close', (code: number, signal: number | NodeJS.Signals) => {
|
|
79
126
|
this._log(`Stopped with exit code ${code} (signal: ${signal}).`);
|
|
80
|
-
if (existsSync(this._params.pidFile)) {
|
|
81
|
-
unlinkSync(this._params.pidFile);
|
|
82
|
-
}
|
|
83
127
|
});
|
|
84
128
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
restarts: this._restarts,
|
|
89
|
-
...this._params,
|
|
90
|
-
};
|
|
129
|
+
invariant(this._child.pid, 'Child process has no pid.');
|
|
130
|
+
this._log(`Started with pid ${this._child.pid}.`);
|
|
131
|
+
}
|
|
91
132
|
|
|
92
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Sends SIGINT to the child process and the tree it spawned (if `killTree` param is `true`).
|
|
135
|
+
*/
|
|
136
|
+
@synchronized
|
|
137
|
+
async stop() {
|
|
138
|
+
if (!this._child) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await this._killWithSignal('SIGKILL');
|
|
93
143
|
|
|
94
|
-
await
|
|
144
|
+
await this._releaseLock();
|
|
95
145
|
}
|
|
96
146
|
|
|
97
147
|
/**
|
|
@@ -105,11 +155,7 @@ export class WatchDog {
|
|
|
105
155
|
|
|
106
156
|
await this._killWithSignal('SIGKILL');
|
|
107
157
|
|
|
108
|
-
|
|
109
|
-
unlinkSync(this._params.pidFile);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
await waitForPidDeletion(this._params.pidFile);
|
|
158
|
+
await this._releaseLock();
|
|
113
159
|
}
|
|
114
160
|
|
|
115
161
|
async restart() {
|
|
@@ -124,11 +170,45 @@ export class WatchDog {
|
|
|
124
170
|
}
|
|
125
171
|
|
|
126
172
|
async _killWithSignal(signal: number | NodeJS.Signals) {
|
|
173
|
+
invariant(this._processCtx, 'Process context is not defined.');
|
|
174
|
+
await this._processCtx.dispose();
|
|
175
|
+
|
|
176
|
+
// Kill child process tree.
|
|
177
|
+
if (this._params.killTree) {
|
|
178
|
+
if (process.platform !== 'win32') {
|
|
179
|
+
invariant(this._child?.pid, 'Child process has no pid.');
|
|
180
|
+
const children = await promisify(psTree)(this._child.pid);
|
|
181
|
+
children
|
|
182
|
+
.map((p) => p.PID)
|
|
183
|
+
.forEach((tpid) => {
|
|
184
|
+
invariant(tpid, 'Process id is not defined.');
|
|
185
|
+
process.kill(Number(tpid), signal);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
127
190
|
invariant(this._child?.pid, 'Child process has no pid.');
|
|
128
191
|
this._child.kill(signal);
|
|
129
192
|
this._child = undefined;
|
|
130
193
|
}
|
|
131
194
|
|
|
195
|
+
private async _acquireLock() {
|
|
196
|
+
if (await LockFile.isLocked(this._params.lockFile)) {
|
|
197
|
+
throw new Error('Lock file is already locked.');
|
|
198
|
+
}
|
|
199
|
+
this._lock = await LockFile.acquire(this._params.lockFile);
|
|
200
|
+
await waitForLockAcquisition(this._params.lockFile);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async _releaseLock() {
|
|
204
|
+
invariant(this._lock, 'Lock is not defined.');
|
|
205
|
+
|
|
206
|
+
await LockFile.release(this._lock);
|
|
207
|
+
await waitForLockRelease(this._params.lockFile);
|
|
208
|
+
|
|
209
|
+
this._lock = undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
132
212
|
private _log(message: string | Uint8Array) {
|
|
133
213
|
writeFileSync(this._params.logFile, message + '\n', {
|
|
134
214
|
flag: 'a+',
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { ProcessInfo, WatchDogParams } from './watchdog';
|
|
2
|
-
/**
|
|
3
|
-
* Utils to start/stop detached process with errors and logs handling.
|
|
4
|
-
*/
|
|
5
|
-
export declare class Phoenix {
|
|
6
|
-
/**
|
|
7
|
-
* Starts detached watchdog process which starts and monitors selected command.
|
|
8
|
-
*/
|
|
9
|
-
static start(params: WatchDogParams): Promise<ProcessInfo>;
|
|
10
|
-
/**
|
|
11
|
-
* Stops detached watchdog process by PID info written down in PID file.
|
|
12
|
-
*/
|
|
13
|
-
static stop(pidFile: string, force?: boolean): Promise<void>;
|
|
14
|
-
static info(pidFile: string): ProcessInfo;
|
|
15
|
-
}
|
|
16
|
-
//# sourceMappingURL=phoenix.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"phoenix.d.ts","sourceRoot":"","sources":["../../../src/phoenix.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEzD;;GAEG;AACH,qBAAa,OAAO;IAClB;;OAEG;WACU,KAAK,CAAC,MAAM,EAAE,cAAc;IAyCzC;;OAEG;WACU,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,UAAQ;IAwBhD,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW;CAG1C"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"phoenix.test.d.ts","sourceRoot":"","sources":["../../../src/phoenix.test.ts"],"names":[],"mappings":""}
|
package/src/phoenix.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { fork } from 'node:child_process';
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import { dirname, join } from 'node:path';
|
|
8
|
-
import pkgUp from 'pkg-up';
|
|
9
|
-
|
|
10
|
-
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import { log } from '@dxos/log';
|
|
12
|
-
|
|
13
|
-
import { waitForPidDeletion, waitForPidFileBeingFilledWithInfo } from './utils';
|
|
14
|
-
import { ProcessInfo, WatchDogParams } from './watchdog';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Utils to start/stop detached process with errors and logs handling.
|
|
18
|
-
*/
|
|
19
|
-
export class Phoenix {
|
|
20
|
-
/**
|
|
21
|
-
* Starts detached watchdog process which starts and monitors selected command.
|
|
22
|
-
*/
|
|
23
|
-
static async start(params: WatchDogParams) {
|
|
24
|
-
{
|
|
25
|
-
// Clear stale pid file.
|
|
26
|
-
if (existsSync(params.pidFile)) {
|
|
27
|
-
await Phoenix.stop(params.pidFile);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
await waitForPidDeletion(params.pidFile);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
{
|
|
34
|
-
// Create log folders.
|
|
35
|
-
[params.logFile, params.errFile, params.pidFile].forEach((filename) => {
|
|
36
|
-
if (!existsSync(filename)) {
|
|
37
|
-
mkdirSync(dirname(filename), { recursive: true });
|
|
38
|
-
writeFileSync(filename, '', { encoding: 'utf-8' });
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const watchdogPath = join(dirname(pkgUp.sync({ cwd: __dirname })!), 'bin', 'watchdog');
|
|
44
|
-
|
|
45
|
-
const watchDog = fork(watchdogPath, [JSON.stringify(params)], {
|
|
46
|
-
detached: true,
|
|
47
|
-
cwd: __dirname,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
watchDog.on('exit', (code, signal) => {
|
|
51
|
-
if (code && code !== 0) {
|
|
52
|
-
log.error('Monitor died unexpectedly', { code, signal });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
await waitForPidFileBeingFilledWithInfo(params.pidFile);
|
|
57
|
-
|
|
58
|
-
watchDog.disconnect();
|
|
59
|
-
watchDog.unref();
|
|
60
|
-
|
|
61
|
-
return Phoenix.info(params.pidFile);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Stops detached watchdog process by PID info written down in PID file.
|
|
66
|
-
*/
|
|
67
|
-
static async stop(pidFile: string, force = false) {
|
|
68
|
-
if (!existsSync(pidFile)) {
|
|
69
|
-
throw new Error('PID file does not exist');
|
|
70
|
-
}
|
|
71
|
-
const fileContent = readFileSync(pidFile, { encoding: 'utf-8' });
|
|
72
|
-
if (!fileContent.includes('pid')) {
|
|
73
|
-
throw new Error('Invalid PID file content');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const { pid } = JSON.parse(fileContent);
|
|
77
|
-
const signal: NodeJS.Signals = force ? 'SIGKILL' : 'SIGINT';
|
|
78
|
-
try {
|
|
79
|
-
process.kill(pid, signal);
|
|
80
|
-
} catch (err) {
|
|
81
|
-
invariant(err instanceof Error, 'Invalid error type');
|
|
82
|
-
if (err.message.includes('ESRCH') || err.name.includes('ESRCH')) {
|
|
83
|
-
// Process is already dead.
|
|
84
|
-
unlinkSync(pidFile);
|
|
85
|
-
} else {
|
|
86
|
-
throw err;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
static info(pidFile: string): ProcessInfo {
|
|
92
|
-
return JSON.parse(readFileSync(pidFile, { encoding: 'utf-8' }));
|
|
93
|
-
}
|
|
94
|
-
}
|