forked from OutlineFoundation/outline-apps
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprocess.ts
More file actions
executable file
·154 lines (139 loc) · 5.52 KB
/
Copy pathprocess.ts
File metadata and controls
executable file
·154 lines (139 loc) · 5.52 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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// Copyright 2021 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {ChildProcess, spawn} from 'node:child_process';
import {basename} from 'node:path';
import process from 'node:process';
/**
* A child process is terminated abnormally, caused by a non-zero exit code.
*/
export class ProcessTerminatedExitCodeError extends Error {
constructor(public readonly exitCode: number) {
super(`Process terminated by non-zero exit code: ${exitCode}`);
}
}
/**
* A child process is terminated abnormally, caused by a signal string.
*/
export class ProcessTerminatedSignalError extends Error {
constructor(public readonly signal: string) {
super(`Process terminated by signal: ${signal}`);
}
}
// Simple "one shot" child process launcher.
//
// NOTE: Because there is no way in Node.js to tell whether a process launched successfully,
// #startInternal always succeeds; use #onExit to be notified when the process has exited
// (which may be immediately after calling #startInternal if, e.g. the binary cannot be
// found).
export class ChildProcessHelper {
private readonly processName: string;
private childProcess?: ChildProcess = null;
private waitProcessToExit?: Promise<void>;
/**
* Whether to enable verbose logging for the process. Must be called before launch().
*/
public isDebugModeEnabled = false;
private stdErrListener?: (data?: string | Buffer) => void;
constructor(private readonly path: string) {
this.processName = basename(this.path);
}
/**
* Start the process with the given args and wait for the process to exit. If `isDebugModeEnabled`
* is `true`, the process is started in verbose mode if supported.
*
* If the process does not exist normally (i.e., exit code !== 0 or received a signal), it will
* throw either `ProcessTerminatedExitCodeError` or `ProcessTerminatedSignalError`.
*
* @param args The args for the process
*/
async launch(args: string[]): Promise<void> {
if (this.childProcess) {
throw new Error(`subprocess ${this.processName} has already been launched`);
}
this.childProcess = spawn(this.path, args);
return (this.waitProcessToExit = new Promise((resolve, reject) => {
const onExit = (code?: number, signal?: string) => {
if (this.childProcess) {
this.childProcess.removeAllListeners();
this.childProcess = null;
} else {
// When listening to both the 'exit' and 'error' events, guard against accidentally
// invoking handler functions multiple times.
return;
}
logExit(this.processName, code, signal);
if (code === 0) {
resolve();
} else if (code) {
reject(new ProcessTerminatedExitCodeError(code));
} else {
reject(new ProcessTerminatedSignalError(signal));
}
};
const onStdErr = (data?: string | Buffer) => {
if (this.isDebugModeEnabled) {
console.error(`[STDERR - ${this.processName}]: ${data}`);
}
if (this.stdErrListener) {
this.stdErrListener(data);
}
};
this.childProcess.stderr.on('data', onStdErr.bind(this));
if (this.isDebugModeEnabled) {
// Redirect subprocess output while bypassing the Node console. This makes sure we don't
// send web traffic information to Sentry.
this.childProcess.stdout.pipe(process.stdout);
this.childProcess.stderr.pipe(process.stderr);
}
// We have to listen for both events: error means the process could not be launched and in that
// case exit will not be invoked.
this.childProcess.on('error', onExit.bind(this));
this.childProcess.on('exit', onExit.bind(this));
}));
}
/**
* Try to kill the process and wait for the process to exit.
*
* If the process does not exist normally (i.e., exit code !== 0 or received a signal), it will
* throw either `ProcessTerminatedExitCodeError` or `ProcessTerminatedSignalError`.
*/
stop(): Promise<void> {
if (!this.childProcess) {
// Never started.
return;
}
this.childProcess.kill();
return this.waitProcessToExit;
}
set onStdErr(listener: ((data?: string | Buffer) => void) | undefined) {
this.stdErrListener = listener;
if (!this.stdErrListener && !this.isDebugModeEnabled) {
this.childProcess?.stderr.removeAllListeners();
}
}
}
function logExit(processName: string, exitCode?: number, signal?: string) {
const prefix = `[EXIT - ${processName}]: `;
if (exitCode !== null) {
const log = exitCode === 0 ? console.log : console.error;
log(`${prefix}Exited with code ${exitCode}`);
} else if (signal !== null) {
const log = signal === 'SIGTERM' ? console.log : console.error;
log(`${prefix}Killed by signal ${signal}`);
} else {
// This should never happen. It likely signals an internal error in Node, as it is supposed to
// always pass either an exit code or an exit signal to the exit handler.
console.error(`${prefix}Process exited for an unknown reason.`);
}
}