@dxos/phoenix 0.1.58-main.4cbb52d → 0.1.58-main.4e2a73a
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 +91 -241
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/defs.d.ts +3 -1
- 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/phoenix.d.ts +16 -0
- package/dist/types/src/phoenix.d.ts.map +1 -0
- package/dist/types/src/phoenix.test.d.ts +2 -0
- package/dist/types/src/phoenix.test.d.ts.map +1 -0
- 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 +5 -24
- package/dist/types/src/watchdog.d.ts.map +1 -1
- package/package.json +9 -12
- package/src/defs.ts +3 -1
- package/src/index.ts +1 -1
- package/src/{daemon-manager.test.ts → phoenix.test.ts} +17 -21
- package/src/phoenix.ts +94 -0
- package/src/utils.ts +15 -18
- package/src/watchdog.test.ts +8 -10
- package/src/watchdog.ts +29 -109
- package/dist/types/src/daemon-manager.d.ts +0 -17
- package/dist/types/src/daemon-manager.d.ts.map +0 -1
- package/dist/types/src/daemon-manager.test.d.ts +0 -2
- package/dist/types/src/daemon-manager.test.d.ts.map +0 -1
- package/src/daemon-manager.ts +0 -138
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phoenix.test.d.ts","sourceRoot":"","sources":["../../../src/phoenix.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const
|
|
1
|
+
export declare const waitForPidCreation: (pidFile: string) => Promise<boolean>;
|
|
2
|
+
export declare const waitForPidDeletion: (pidFile: string) => Promise<boolean>;
|
|
3
|
+
export declare const waitForPidFileBeingFilledWithInfo: (pidFile: string) => Promise<boolean>;
|
|
4
4
|
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/utils.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,kBAAkB,YAAmB,MAAM,qBAKpD,CAAC;AAEL,eAAO,MAAM,kBAAkB,YAAmB,MAAM,qBAKpD,CAAC;AAEL,eAAO,MAAM,iCAAiC,YAAmB,MAAM,qBAMnE,CAAC"}
|
|
@@ -1,55 +1,36 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
export type DaemonInfo = {
|
|
3
|
-
pid: number;
|
|
4
|
-
command: string;
|
|
5
|
-
args: string[];
|
|
6
|
-
cwd: string;
|
|
7
|
-
timestamp: number;
|
|
8
|
-
};
|
|
9
2
|
export type ProcessInfo = WatchDogParams & {
|
|
10
3
|
pid?: number;
|
|
11
|
-
|
|
4
|
+
started?: number;
|
|
12
5
|
restarts?: number;
|
|
13
6
|
running?: boolean;
|
|
14
7
|
};
|
|
15
|
-
export type
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export type Logs = {
|
|
8
|
+
export type WatchDogParams = {
|
|
9
|
+
profile?: string;
|
|
10
|
+
pidFile: string;
|
|
19
11
|
logFile: string;
|
|
20
12
|
errFile: string;
|
|
21
|
-
};
|
|
22
|
-
export type ChildParams = {
|
|
23
|
-
uid: string;
|
|
24
13
|
maxRestarts?: number | undefined;
|
|
25
14
|
killTree?: boolean | undefined;
|
|
26
|
-
command
|
|
15
|
+
command: string;
|
|
27
16
|
args?: string[] | undefined;
|
|
28
17
|
env?: NodeJS.ProcessEnv | undefined;
|
|
29
18
|
cwd?: string | undefined;
|
|
30
19
|
shell?: boolean | undefined;
|
|
31
20
|
};
|
|
32
|
-
export type WatchDogParams = ChildParams & Lock & Logs;
|
|
33
21
|
export declare class WatchDog {
|
|
34
22
|
private readonly _params;
|
|
35
23
|
private _lock?;
|
|
36
24
|
private _child?;
|
|
37
25
|
private _restarts;
|
|
38
|
-
private _processCtx?;
|
|
39
26
|
constructor(_params: WatchDogParams);
|
|
40
27
|
start(): Promise<void>;
|
|
41
|
-
/**
|
|
42
|
-
* Sends SIGINT to the child process and the tree it spawned (if `killTree` param is `true`).
|
|
43
|
-
*/
|
|
44
|
-
stop(): Promise<void>;
|
|
45
28
|
/**
|
|
46
29
|
* Sends SIGKILL to the child process and the tree it spawned (if `killTree` param is `true`).
|
|
47
30
|
*/
|
|
48
31
|
kill(): Promise<void>;
|
|
49
32
|
restart(): Promise<void>;
|
|
50
33
|
_killWithSignal(signal: number | NodeJS.Signals): Promise<void>;
|
|
51
|
-
private _acquireLock;
|
|
52
|
-
private _releaseLock;
|
|
53
34
|
private _log;
|
|
54
35
|
private _err;
|
|
55
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchdog.d.ts","sourceRoot":"","sources":["../../../src/watchdog.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"watchdog.d.ts","sourceRoot":"","sources":["../../../src/watchdog.ts"],"names":[],"mappings":";AAcA,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAKhB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAKhB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAM/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAM5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,GAAG,SAAS,CAAC;IACpC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B,CAAC;AAEF,qBAAa,QAAQ;IAKP,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,KAAK,CAAC,CAAa;IAC3B,OAAO,CAAC,MAAM,CAAC,CAAiC;IAChD,OAAO,CAAC,SAAS,CAAK;gBAEO,OAAO,EAAE,cAAc;IAG9C,KAAK;IAmCX;;OAEG;IAEG,IAAI;IAcJ,OAAO;IAWP,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO;IAMrD,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,IAAI;CAOb"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/phoenix",
|
|
3
|
-
"version": "0.1.58-main.
|
|
3
|
+
"version": "0.1.58-main.4e2a73a",
|
|
4
4
|
"description": "Basic node daemon.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -13,20 +13,17 @@
|
|
|
13
13
|
"src"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"fs-extra": "^8.1.0",
|
|
17
16
|
"pkg-up": "^3.1.0",
|
|
18
|
-
"
|
|
19
|
-
"@dxos/
|
|
20
|
-
"@dxos/
|
|
21
|
-
"@dxos/
|
|
22
|
-
"@dxos/
|
|
23
|
-
"@dxos/
|
|
24
|
-
"@dxos/
|
|
25
|
-
"@dxos/
|
|
26
|
-
"@dxos/util": "0.1.58-main.4cbb52d"
|
|
17
|
+
"@dxos/async": "0.1.58-main.4e2a73a",
|
|
18
|
+
"@dxos/context": "0.1.58-main.4e2a73a",
|
|
19
|
+
"@dxos/invariant": "0.1.58-main.4e2a73a",
|
|
20
|
+
"@dxos/keys": "0.1.58-main.4e2a73a",
|
|
21
|
+
"@dxos/lock-file": "0.1.58-main.4e2a73a",
|
|
22
|
+
"@dxos/log": "0.1.58-main.4e2a73a",
|
|
23
|
+
"@dxos/node-std": "0.1.58-main.4e2a73a",
|
|
24
|
+
"@dxos/util": "0.1.58-main.4e2a73a"
|
|
27
25
|
},
|
|
28
26
|
"devDependencies": {
|
|
29
|
-
"@types/fs-extra": "^9.0.4",
|
|
30
27
|
"@types/node": "^18.11.9",
|
|
31
28
|
"@types/ps-tree": "^1.1.2"
|
|
32
29
|
},
|
package/src/defs.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -3,18 +3,16 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { expect } from 'chai';
|
|
6
|
-
import fse from 'fs-extra';
|
|
7
6
|
import { spawn } from 'node:child_process';
|
|
8
7
|
import { existsSync, readFileSync } from 'node:fs';
|
|
9
8
|
import { join } from 'node:path';
|
|
10
9
|
import waitForExpect from 'wait-for-expect';
|
|
11
10
|
|
|
12
11
|
import { Trigger } from '@dxos/async';
|
|
13
|
-
import { PublicKey } from '@dxos/keys';
|
|
14
12
|
import { afterTest, describe, test } from '@dxos/test';
|
|
15
13
|
|
|
16
|
-
import {
|
|
17
|
-
import { TEST_DIR, neverEndingProcess } from './testing-utils';
|
|
14
|
+
import { Phoenix } from './phoenix';
|
|
15
|
+
import { TEST_DIR, clearFiles, neverEndingProcess } from './testing-utils';
|
|
18
16
|
|
|
19
17
|
describe('DaemonManager', () => {
|
|
20
18
|
test('kill process by pid', async () => {
|
|
@@ -30,21 +28,23 @@ describe('DaemonManager', () => {
|
|
|
30
28
|
});
|
|
31
29
|
|
|
32
30
|
// Fails on CI
|
|
33
|
-
test
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
test('start/stop detached watchdog', async () => {
|
|
32
|
+
const runId = Math.random().toString();
|
|
33
|
+
const pidFile = join(TEST_DIR, `pid-${runId}.pid`);
|
|
34
|
+
const logFile = join(TEST_DIR, `file-${runId}.log`);
|
|
35
|
+
const errFile = join(TEST_DIR, `err-${runId}.log`);
|
|
36
|
+
afterTest(() => clearFiles(pidFile, logFile, errFile));
|
|
39
37
|
|
|
40
38
|
// Start
|
|
41
39
|
{
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
uid,
|
|
40
|
+
const params = await Phoenix.start({
|
|
41
|
+
profile: runId,
|
|
45
42
|
command: 'node',
|
|
46
43
|
args: ['-e', `(${neverEndingProcess.toString()})()`],
|
|
47
44
|
maxRestarts: 0,
|
|
45
|
+
pidFile,
|
|
46
|
+
logFile,
|
|
47
|
+
errFile,
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
await waitForExpect(() => {
|
|
@@ -56,17 +56,13 @@ describe('DaemonManager', () => {
|
|
|
56
56
|
|
|
57
57
|
// Stop
|
|
58
58
|
{
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
expect(info.length).to.equal(1);
|
|
62
|
-
expect(info[0].running).to.be.true;
|
|
63
|
-
expect(info[0].uid).to.equal(uid);
|
|
64
|
-
expect(await manager.isRunning(uid)).to.be.true;
|
|
59
|
+
const info = Phoenix.info(pidFile);
|
|
60
|
+
expect(info.profile).to.equal(runId);
|
|
65
61
|
|
|
66
|
-
|
|
62
|
+
await Phoenix.stop(pidFile);
|
|
67
63
|
|
|
68
64
|
await waitForExpect(() => {
|
|
69
|
-
const logs = readFileSync(
|
|
65
|
+
const logs = readFileSync(logFile, { encoding: 'utf-8' });
|
|
70
66
|
expect(logs).to.contain('Stopped with exit code');
|
|
71
67
|
}, 1000);
|
|
72
68
|
}
|
package/src/phoenix.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -2,33 +2,30 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
6
|
|
|
7
7
|
import { waitForCondition } from '@dxos/async';
|
|
8
|
-
import { LockFile } from '@dxos/lock-file';
|
|
9
8
|
|
|
10
|
-
import {
|
|
9
|
+
import { WATCHDOG_CHECK_INTERVAL, WATCHDOG_START_TIMEOUT, WATCHDOG_STOP_TIMEOUT } from './defs';
|
|
11
10
|
|
|
12
|
-
export const
|
|
11
|
+
export const waitForPidCreation = async (pidFile: string) =>
|
|
13
12
|
waitForCondition({
|
|
14
|
-
condition:
|
|
15
|
-
timeout:
|
|
16
|
-
interval:
|
|
17
|
-
error: new Error('Lock file is not being acquired.'),
|
|
13
|
+
condition: () => existsSync(pidFile),
|
|
14
|
+
timeout: WATCHDOG_START_TIMEOUT,
|
|
15
|
+
interval: WATCHDOG_CHECK_INTERVAL,
|
|
18
16
|
});
|
|
19
17
|
|
|
20
|
-
export const
|
|
18
|
+
export const waitForPidDeletion = async (pidFile: string) =>
|
|
21
19
|
waitForCondition({
|
|
22
|
-
condition: () =>
|
|
23
|
-
timeout:
|
|
24
|
-
interval:
|
|
25
|
-
error: new Error('Lock file is not being propagated with info.'),
|
|
20
|
+
condition: () => !existsSync(pidFile),
|
|
21
|
+
timeout: WATCHDOG_STOP_TIMEOUT,
|
|
22
|
+
interval: WATCHDOG_CHECK_INTERVAL,
|
|
26
23
|
});
|
|
27
24
|
|
|
28
|
-
export const
|
|
25
|
+
export const waitForPidFileBeingFilledWithInfo = async (pidFile: string) =>
|
|
29
26
|
waitForCondition({
|
|
30
|
-
condition:
|
|
31
|
-
timeout:
|
|
32
|
-
interval:
|
|
33
|
-
error: new Error('Lock file is not being
|
|
27
|
+
condition: () => readFileSync(pidFile, { encoding: 'utf-8' }).includes('pid'),
|
|
28
|
+
timeout: WATCHDOG_START_TIMEOUT,
|
|
29
|
+
interval: WATCHDOG_CHECK_INTERVAL,
|
|
30
|
+
error: new Error('Lock file is not being propagated with info.'),
|
|
34
31
|
});
|
package/src/watchdog.test.ts
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { expect } from 'chai';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
|
|
8
|
-
import { asyncTimeout } from '@dxos/async';
|
|
9
|
-
import { LockFile } from '@dxos/lock-file';
|
|
10
9
|
import { afterTest, describe, test } from '@dxos/test';
|
|
11
10
|
|
|
12
11
|
import { TEST_DIR, clearFiles, neverEndingProcess } from './testing-utils';
|
|
@@ -15,26 +14,25 @@ import { WatchDog } from './watchdog';
|
|
|
15
14
|
describe('WatchDog', () => {
|
|
16
15
|
test('Start/stop process', async () => {
|
|
17
16
|
const runId = Math.random();
|
|
18
|
-
const
|
|
17
|
+
const pidFile = join(TEST_DIR, `pid-${runId}.pid`);
|
|
19
18
|
const logFile = join(TEST_DIR, `file-${runId}.log`);
|
|
20
19
|
const errFile = join(TEST_DIR, `err-${runId}.log`);
|
|
21
|
-
afterTest(() => clearFiles(
|
|
20
|
+
afterTest(() => clearFiles(pidFile, logFile, errFile));
|
|
22
21
|
|
|
23
22
|
const watchDog = new WatchDog({
|
|
24
|
-
uid: 'test',
|
|
25
23
|
command: 'node',
|
|
26
24
|
args: ['-e', `(${neverEndingProcess.toString()})()`],
|
|
27
|
-
|
|
25
|
+
pidFile,
|
|
28
26
|
logFile,
|
|
29
27
|
errFile,
|
|
30
28
|
});
|
|
31
29
|
|
|
32
|
-
expect(
|
|
30
|
+
expect(existsSync(pidFile)).to.be.false;
|
|
33
31
|
await watchDog.start();
|
|
34
32
|
|
|
35
|
-
expect(
|
|
33
|
+
expect(existsSync(pidFile)).to.be.true;
|
|
36
34
|
|
|
37
|
-
await watchDog.
|
|
38
|
-
expect(
|
|
35
|
+
await watchDog.kill();
|
|
36
|
+
expect(existsSync(pidFile)).to.be.false;
|
|
39
37
|
});
|
|
40
38
|
});
|
package/src/watchdog.ts
CHANGED
|
@@ -3,48 +3,31 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
|
|
6
|
-
import { writeFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { FileHandle } from 'node:fs/promises';
|
|
8
|
-
import { promisify } from 'node:util';
|
|
9
|
-
import psTree from 'ps-tree';
|
|
10
8
|
|
|
11
9
|
import { synchronized } from '@dxos/async';
|
|
12
|
-
import { Context } from '@dxos/context';
|
|
13
10
|
import { invariant } from '@dxos/invariant';
|
|
14
|
-
import { LockFile } from '@dxos/lock-file';
|
|
15
11
|
import { log } from '@dxos/log';
|
|
16
12
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
export type DaemonInfo = {
|
|
20
|
-
pid: number;
|
|
21
|
-
command: string;
|
|
22
|
-
args: string[];
|
|
23
|
-
cwd: string;
|
|
24
|
-
timestamp: number;
|
|
25
|
-
};
|
|
13
|
+
import { waitForPidDeletion, waitForPidFileBeingFilledWithInfo } from './utils';
|
|
26
14
|
|
|
27
15
|
export type ProcessInfo = WatchDogParams & {
|
|
28
16
|
pid?: number;
|
|
29
|
-
|
|
17
|
+
started?: number;
|
|
30
18
|
restarts?: number;
|
|
31
19
|
running?: boolean;
|
|
32
20
|
};
|
|
33
21
|
|
|
34
|
-
export type
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
export type WatchDogParams = {
|
|
23
|
+
profile?: string; // Human readable process identifier
|
|
24
|
+
pidFile: string; // Path to PID file
|
|
37
25
|
|
|
38
|
-
export type Logs = {
|
|
39
26
|
//
|
|
40
27
|
// Log files and associated logging options for this instance
|
|
41
28
|
//
|
|
42
29
|
logFile: string; // Path to log output all logs
|
|
43
30
|
errFile: string; // Path to log output from child stderr
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export type ChildParams = {
|
|
47
|
-
uid: string; // Unique identifier for this instance
|
|
48
31
|
|
|
49
32
|
//
|
|
50
33
|
// Basic configuration options
|
|
@@ -56,7 +39,7 @@ export type ChildParams = {
|
|
|
56
39
|
// Command to spawn as well as options and other vars
|
|
57
40
|
// (env, cwd, etc) to pass along
|
|
58
41
|
//
|
|
59
|
-
command
|
|
42
|
+
command: string; // Binary to run (default: 'node')
|
|
60
43
|
args?: string[] | undefined; // Additional arguments to pass to the script,
|
|
61
44
|
|
|
62
45
|
//
|
|
@@ -68,35 +51,19 @@ export type ChildParams = {
|
|
|
68
51
|
shell?: boolean | undefined;
|
|
69
52
|
};
|
|
70
53
|
|
|
71
|
-
export type WatchDogParams = ChildParams & Lock & Logs;
|
|
72
|
-
|
|
73
54
|
export class WatchDog {
|
|
74
55
|
private _lock?: FileHandle;
|
|
75
56
|
private _child?: ChildProcessWithoutNullStreams;
|
|
76
57
|
private _restarts = 0;
|
|
77
|
-
private _processCtx?: Context;
|
|
78
58
|
|
|
79
59
|
constructor(private readonly _params: WatchDogParams) {}
|
|
80
60
|
|
|
81
61
|
@synchronized
|
|
82
62
|
async start() {
|
|
83
|
-
await this._acquireLock();
|
|
84
|
-
this._log('Lock acquired.');
|
|
85
63
|
const { cwd, shell, env, command, args } = { cwd: process.cwd(), ...this._params };
|
|
86
|
-
invariant(command, 'Command is not defined.');
|
|
87
64
|
|
|
88
|
-
this._log(`Spawning process
|
|
65
|
+
this._log(`Spawning process \`\`\`${command} ${args?.join(' ')}\`\`\``);
|
|
89
66
|
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' });
|
|
100
67
|
|
|
101
68
|
this._child.stdout.on('data', (data: Uint8Array) => {
|
|
102
69
|
this._log(String(data));
|
|
@@ -104,44 +71,27 @@ export class WatchDog {
|
|
|
104
71
|
this._child.stderr.on('data', (data: Uint8Array) => {
|
|
105
72
|
this._err(data);
|
|
106
73
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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) => {
|
|
74
|
+
this._child.on('close', async (code: number, signal: number | NodeJS.Signals) => {
|
|
75
|
+
if (code && code !== 0 && signal !== 'SIGINT' && signal !== 'SIGKILL') {
|
|
76
|
+
this._err(`Died unexpectedly with exit code ${code} (signal: ${signal}).`);
|
|
77
|
+
await this.restart();
|
|
78
|
+
}
|
|
126
79
|
this._log(`Stopped with exit code ${code} (signal: ${signal}).`);
|
|
80
|
+
if (existsSync(this._params.pidFile)) {
|
|
81
|
+
unlinkSync(this._params.pidFile);
|
|
82
|
+
}
|
|
127
83
|
});
|
|
128
84
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
*/
|
|
136
|
-
@synchronized
|
|
137
|
-
async stop() {
|
|
138
|
-
if (!this._child) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
85
|
+
const childInfo: ProcessInfo = {
|
|
86
|
+
pid: this._child.pid,
|
|
87
|
+
started: Date.now(),
|
|
88
|
+
restarts: this._restarts,
|
|
89
|
+
...this._params,
|
|
90
|
+
};
|
|
141
91
|
|
|
142
|
-
|
|
92
|
+
writeFileSync(this._params.pidFile, JSON.stringify(childInfo, undefined, 2), { encoding: 'utf-8' });
|
|
143
93
|
|
|
144
|
-
await this.
|
|
94
|
+
await waitForPidFileBeingFilledWithInfo(this._params.pidFile);
|
|
145
95
|
}
|
|
146
96
|
|
|
147
97
|
/**
|
|
@@ -155,7 +105,11 @@ export class WatchDog {
|
|
|
155
105
|
|
|
156
106
|
await this._killWithSignal('SIGKILL');
|
|
157
107
|
|
|
158
|
-
|
|
108
|
+
if (existsSync(this._params.pidFile)) {
|
|
109
|
+
unlinkSync(this._params.pidFile);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await waitForPidDeletion(this._params.pidFile);
|
|
159
113
|
}
|
|
160
114
|
|
|
161
115
|
async restart() {
|
|
@@ -170,45 +124,11 @@ export class WatchDog {
|
|
|
170
124
|
}
|
|
171
125
|
|
|
172
126
|
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
|
-
|
|
190
127
|
invariant(this._child?.pid, 'Child process has no pid.');
|
|
191
128
|
this._child.kill(signal);
|
|
192
129
|
this._child = undefined;
|
|
193
130
|
}
|
|
194
131
|
|
|
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
|
-
|
|
212
132
|
private _log(message: string | Uint8Array) {
|
|
213
133
|
writeFileSync(this._params.logFile, message + '\n', {
|
|
214
134
|
flag: 'a+',
|