@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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=phoenix.test.d.ts.map
@@ -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 waitForLockAcquisition: (lockFile: string) => Promise<boolean>;
2
- export declare const waitForLockFileBeingFilledWithInfo: (lockFile: string) => Promise<boolean>;
3
- export declare const waitForLockRelease: (lockFile: string) => Promise<boolean>;
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":"AAWA,eAAO,MAAM,sBAAsB,aAAoB,MAAM,qBAMzD,CAAC;AAEL,eAAO,MAAM,kCAAkC,aAAoB,MAAM,qBAMrE,CAAC;AAEL,eAAO,MAAM,kBAAkB,aAAoB,MAAM,qBAMrD,CAAC"}
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
- timestamp?: number;
4
+ started?: number;
12
5
  restarts?: number;
13
6
  running?: boolean;
14
7
  };
15
- export type Lock = {
16
- lockFile: string;
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?: string;
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":";AAkBA,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IAIjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;IAKZ,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAM/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,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,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvD,qBAAa,QAAQ;IAMP,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC,OAAO,CAAC,KAAK,CAAC,CAAa;IAC3B,OAAO,CAAC,MAAM,CAAC,CAAiC;IAChD,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,WAAW,CAAC,CAAU;gBAED,OAAO,EAAE,cAAc;IAG9C,KAAK;IAmDX;;OAEG;IAEG,IAAI;IAUV;;OAEG;IAEG,IAAI;IAUJ,OAAO;IAWP,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO;YAuBvC,YAAY;YAQZ,YAAY;IAS1B,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,IAAI;CAOb"}
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.4cbb52d",
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
- "ps-tree": "^1.2.0",
19
- "@dxos/async": "0.1.58-main.4cbb52d",
20
- "@dxos/context": "0.1.58-main.4cbb52d",
21
- "@dxos/invariant": "0.1.58-main.4cbb52d",
22
- "@dxos/keys": "0.1.58-main.4cbb52d",
23
- "@dxos/lock-file": "0.1.58-main.4cbb52d",
24
- "@dxos/log": "0.1.58-main.4cbb52d",
25
- "@dxos/node-std": "0.1.58-main.4cbb52d",
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
@@ -4,4 +4,6 @@
4
4
 
5
5
  export const LOCK_TIMEOUT = 1_000;
6
6
  export const LOCK_CHECK_INTERVAL = 50;
7
- export const DAEMON_START_TIMEOUT = 10_000;
7
+ export const WATCHDOG_START_TIMEOUT = 10_000;
8
+ export const WATCHDOG_STOP_TIMEOUT = 1_000;
9
+ export const WATCHDOG_CHECK_INTERVAL = 50;
package/src/index.ts CHANGED
@@ -6,5 +6,5 @@
6
6
  // Copyright (C) 2010 Charlie Robbins & the Contributors
7
7
  //
8
8
 
9
- export * from './daemon-manager';
9
+ export * from './phoenix';
10
10
  export * from './watchdog';
@@ -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 { DaemonManager } from './daemon-manager';
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.skip('start/stop detached watchdog', async () => {
34
- const uid = `test-${PublicKey.random().toHex()}`;
35
- const root = join(TEST_DIR, uid);
36
- afterTest(() => {
37
- fse.removeSync(root);
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 manager = new DaemonManager(root);
43
- const params = await manager.start({
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 manager = new DaemonManager(root);
60
- const info = await manager.list();
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
- const params = await manager.stop(uid);
62
+ await Phoenix.stop(pidFile);
67
63
 
68
64
  await waitForExpect(() => {
69
- const logs = readFileSync(params.logFile, { encoding: 'utf-8' });
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 { LOCK_CHECK_INTERVAL, LOCK_TIMEOUT } from './defs';
9
+ import { WATCHDOG_CHECK_INTERVAL, WATCHDOG_START_TIMEOUT, WATCHDOG_STOP_TIMEOUT } from './defs';
11
10
 
12
- export const waitForLockAcquisition = async (lockFile: string) =>
11
+ export const waitForPidCreation = async (pidFile: string) =>
13
12
  waitForCondition({
14
- condition: async () => await LockFile.isLocked(lockFile),
15
- timeout: LOCK_TIMEOUT,
16
- interval: LOCK_CHECK_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 waitForLockFileBeingFilledWithInfo = async (lockFile: string) =>
18
+ export const waitForPidDeletion = async (pidFile: string) =>
21
19
  waitForCondition({
22
- condition: () => readFileSync(lockFile, { encoding: 'utf-8' }).includes('pid'),
23
- timeout: LOCK_TIMEOUT,
24
- interval: LOCK_CHECK_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 waitForLockRelease = async (lockFile: string) =>
25
+ export const waitForPidFileBeingFilledWithInfo = async (pidFile: string) =>
29
26
  waitForCondition({
30
- condition: async () => !(await LockFile.isLocked(lockFile)),
31
- timeout: LOCK_TIMEOUT,
32
- interval: LOCK_CHECK_INTERVAL,
33
- error: new Error('Lock file is not being released.'),
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
  });
@@ -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 lockFile = join(TEST_DIR, `lock-${runId}.lock`);
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(lockFile, logFile, errFile));
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
- lockFile,
25
+ pidFile,
28
26
  logFile,
29
27
  errFile,
30
28
  });
31
29
 
32
- expect(await asyncTimeout(LockFile.isLocked(lockFile), 1000)).to.be.false;
30
+ expect(existsSync(pidFile)).to.be.false;
33
31
  await watchDog.start();
34
32
 
35
- expect(await asyncTimeout(LockFile.isLocked(lockFile), 1000)).to.be.true;
33
+ expect(existsSync(pidFile)).to.be.true;
36
34
 
37
- await watchDog.stop();
38
- expect(await asyncTimeout(LockFile.isLocked(lockFile), 1000)).to.be.false;
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 { 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
- };
13
+ import { waitForPidDeletion, waitForPidFileBeingFilledWithInfo } from './utils';
26
14
 
27
15
  export type ProcessInfo = WatchDogParams & {
28
16
  pid?: number;
29
- timestamp?: number;
17
+ started?: number;
30
18
  restarts?: number;
31
19
  running?: boolean;
32
20
  };
33
21
 
34
- export type Lock = {
35
- lockFile: string; // Path to lock file
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?: string; // Binary to run (default: 'node')
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 ${command} ${args?.join(' ')}`);
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
- // 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) => {
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
- invariant(this._child.pid, 'Child process has no pid.');
130
- this._log(`Started with pid ${this._child.pid}.`);
131
- }
132
-
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
- }
85
+ const childInfo: ProcessInfo = {
86
+ pid: this._child.pid,
87
+ started: Date.now(),
88
+ restarts: this._restarts,
89
+ ...this._params,
90
+ };
141
91
 
142
- await this._killWithSignal('SIGKILL');
92
+ writeFileSync(this._params.pidFile, JSON.stringify(childInfo, undefined, 2), { encoding: 'utf-8' });
143
93
 
144
- await this._releaseLock();
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
- await this._releaseLock();
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+',