@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/src/watchdog.ts CHANGED
@@ -3,31 +3,48 @@
3
3
  //
4
4
 
5
5
  import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
6
- import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
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 { waitForPidDeletion, waitForPidFileBeingFilledWithInfo } from './utils';
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
- started?: number;
29
+ timestamp?: number;
18
30
  restarts?: number;
19
31
  running?: boolean;
20
32
  };
21
33
 
22
- export type WatchDogParams = {
23
- profile?: string; // Human readable process identifier
24
- pidFile: string; // Path to PID file
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: string; // Binary to run (default: 'node')
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 \`\`\`${command} ${args?.join(' ')}\`\`\``);
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
- 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
- }
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
- const childInfo: ProcessInfo = {
86
- pid: this._child.pid,
87
- started: Date.now(),
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
- writeFileSync(this._params.pidFile, JSON.stringify(childInfo, undefined, 2), { encoding: 'utf-8' });
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 waitForPidFileBeingFilledWithInfo(this._params.pidFile);
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
- if (existsSync(this._params.pidFile)) {
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,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=phoenix.test.d.ts.map
@@ -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
- }