1
- import { field , logger } from "@coder/logger"
1
+ import { Logger , field , logger } from "@coder/logger"
2
2
import * as cp from "child_process"
3
3
import * as path from "path"
4
4
import * as rfs from "rotating-file-stream"
@@ -14,9 +14,9 @@ interface RelaunchMessage {
14
14
version : string
15
15
}
16
16
17
- export type Message = RelaunchMessage | HandshakeMessage
17
+ type Message = RelaunchMessage | HandshakeMessage
18
18
19
- export class ProcessError extends Error {
19
+ class ProcessError extends Error {
20
20
public constructor ( message : string , public readonly code : number | undefined ) {
21
21
super ( message )
22
22
this . name = this . constructor . name
@@ -25,16 +25,26 @@ export class ProcessError extends Error {
25
25
}
26
26
27
27
/**
28
- * Allows the wrapper and inner processes to communicate.
28
+ * Wrapper around a process that tries to gracefully exit when a process exits
29
+ * and provides a way to prevent `process.exit`.
29
30
*/
30
- export class IpcMain {
31
- private readonly _onMessage = new Emitter < Message > ( )
32
- public readonly onMessage = this . _onMessage . event
33
- private readonly _onDispose = new Emitter < NodeJS . Signals | undefined > ( )
31
+ abstract class Process {
32
+ /**
33
+ * Emit this to trigger a graceful exit.
34
+ */
35
+ protected readonly _onDispose = new Emitter < NodeJS . Signals | undefined > ( )
36
+
37
+ /**
38
+ * Emitted when the process is about to be disposed.
39
+ */
34
40
public readonly onDispose = this . _onDispose . event
35
- public readonly processExit : ( code ?: number ) => never = process . exit
36
41
37
- public constructor ( private readonly parentPid ?: number ) {
42
+ /**
43
+ * Uniquely named logger for the process.
44
+ */
45
+ public abstract logger : Logger
46
+
47
+ public constructor ( ) {
38
48
process . on ( "SIGINT" , ( ) => this . _onDispose . emit ( "SIGINT" ) )
39
49
process . on ( "SIGTERM" , ( ) => this . _onDispose . emit ( "SIGTERM" ) )
40
50
process . on ( "exit" , ( ) => this . _onDispose . emit ( undefined ) )
@@ -43,90 +53,89 @@ export class IpcMain {
43
53
// Remove listeners to avoid possibly triggering disposal again.
44
54
process . removeAllListeners ( )
45
55
46
- // Try waiting for other handlers run first then exit.
47
- logger . debug ( ` ${ parentPid ? "inner process" : "wrapper" } ${ process . pid } disposing` , field ( "code" , signal ) )
56
+ // Try waiting for other handlers to run first then exit.
57
+ this . logger . debug ( " disposing" , field ( "code" , signal ) )
48
58
wait . then ( ( ) => this . exit ( 0 ) )
49
59
setTimeout ( ( ) => this . exit ( 0 ) , 5000 )
50
60
} )
51
-
52
- // Kill the inner process if the parent dies. This is for the case where the
53
- // parent process is forcefully terminated and cannot clean up.
54
- if ( parentPid ) {
55
- setInterval ( ( ) => {
56
- try {
57
- // process.kill throws an exception if the process doesn't exist.
58
- process . kill ( parentPid , 0 )
59
- } catch ( _ ) {
60
- // Consider this an error since it should have been able to clean up
61
- // the child process unless it was forcefully killed.
62
- logger . error ( `parent process ${ parentPid } died` )
63
- this . _onDispose . emit ( undefined )
64
- }
65
- } , 5000 )
66
- }
67
61
}
68
62
69
63
/**
70
- * Ensure we control when the process exits.
64
+ * Ensure control over when the process exits.
71
65
*/
72
66
public preventExit ( ) : void {
73
- process . exit = function ( code ?: number ) {
74
- logger . warn ( `process.exit() was prevented: ${ code || "unknown code" } .` )
75
- } as ( code ?: number ) => never
67
+ ; ( process . exit as any ) = ( code ?: number ) => {
68
+ this . logger . warn ( `process.exit() was prevented: ${ code || "unknown code" } .` )
69
+ }
76
70
}
77
71
78
- public get isChild ( ) : boolean {
79
- return typeof this . parentPid !== "undefined"
80
- }
72
+ private readonly processExit : ( code ?: number ) => never = process . exit
81
73
74
+ /**
75
+ * Will always exit even if normal exit is being prevented.
76
+ */
82
77
public exit ( error ?: number | ProcessError ) : never {
83
78
if ( error && typeof error !== "number" ) {
84
79
this . processExit ( typeof error . code === "number" ? error . code : 1 )
85
80
} else {
86
81
this . processExit ( error )
87
82
}
88
83
}
84
+ }
89
85
90
- public handshake ( child ?: cp . ChildProcess ) : Promise < void > {
91
- return new Promise ( ( resolve , reject ) => {
92
- const target = child || process
86
+ /**
87
+ * Child process that will clean up after itself if the parent goes away and can
88
+ * perform a handshake with the parent and ask it to relaunch.
89
+ */
90
+ class ChildProcess extends Process {
91
+ public logger = logger . named ( `child:${ process . pid } ` )
92
+
93
+ public constructor ( private readonly parentPid : number ) {
94
+ super ( )
95
+
96
+ // Kill the inner process if the parent dies. This is for the case where the
97
+ // parent process is forcefully terminated and cannot clean up.
98
+ setInterval ( ( ) => {
99
+ try {
100
+ // process.kill throws an exception if the process doesn't exist.
101
+ process . kill ( this . parentPid , 0 )
102
+ } catch ( _ ) {
103
+ // Consider this an error since it should have been able to clean up
104
+ // the child process unless it was forcefully killed.
105
+ this . logger . error ( `parent process ${ parentPid } died` )
106
+ this . _onDispose . emit ( undefined )
107
+ }
108
+ } , 5000 )
109
+ }
110
+
111
+ /**
112
+ * Initiate the handshake and wait for a response from the parent.
113
+ */
114
+ public handshake ( ) : Promise < void > {
115
+ return new Promise ( ( resolve ) => {
93
116
const onMessage = ( message : Message ) : void => {
94
- logger . debug (
95
- `${ child ? "wrapper" : "inner process" } ${ process . pid } received message from ${
96
- child ? child . pid : this . parentPid
97
- } `,
98
- field ( "message" , message ) ,
99
- )
117
+ logger . debug ( `received message from ${ this . parentPid } ` , field ( "message" , message ) )
100
118
if ( message . type === "handshake" ) {
101
- target . removeListener ( "message" , onMessage )
102
- target . on ( "message" , ( msg ) => this . _onMessage . emit ( msg ) )
103
- // The wrapper responds once the inner process starts the handshake.
104
- if ( child ) {
105
- if ( ! target . send ) {
106
- throw new Error ( "child not spawned with IPC" )
107
- }
108
- target . send ( { type : "handshake" } )
109
- }
119
+ process . removeListener ( "message" , onMessage )
110
120
resolve ( )
111
121
}
112
122
}
113
- target . on ( "message" , onMessage )
114
- if ( child ) {
115
- child . once ( "error" , reject )
116
- child . once ( "exit" , ( code ) => {
117
- reject ( new ProcessError ( `Unexpected exit with code ${ code } ` , code !== null ? code : undefined ) )
118
- } )
119
- } else {
120
- // The inner process initiates the handshake.
121
- this . send ( { type : "handshake" } )
122
- }
123
+ // Initiate the handshake and wait for the reply.
124
+ process . on ( "message" , onMessage )
125
+ this . send ( { type : "handshake" } )
123
126
} )
124
127
}
125
128
129
+ /**
130
+ * Notify the parent process that it should relaunch the child.
131
+ */
126
132
public relaunch ( version : string ) : void {
127
133
this . send ( { type : "relaunch" , version } )
128
134
}
129
135
136
+ /**
137
+ * Send a message to the parent.
138
+ */
130
139
private send ( message : Message ) : void {
131
140
if ( ! process . send ) {
132
141
throw new Error ( "not spawned with IPC" )
@@ -135,59 +144,60 @@ export class IpcMain {
135
144
}
136
145
}
137
146
138
- /**
139
- * Channel for communication between the child and parent processes.
140
- */
141
- export const ipcMain = new IpcMain (
142
- typeof process . env . CODE_SERVER_PARENT_PID !== "undefined" ? parseInt ( process . env . CODE_SERVER_PARENT_PID ) : undefined ,
143
- )
144
-
145
147
export interface WrapperOptions {
146
148
maxMemory ?: number
147
149
nodeOptions ?: string
148
150
}
149
151
150
152
/**
151
- * Provides a way to wrap a process for the purpose of updating the running
152
- * instance.
153
+ * Parent process wrapper that spawns the child process and performs a handshake
154
+ * with it. Will relaunch the child if it receives a SIGUSR1 or is asked to by
155
+ * the child. If the child otherwise exits the parent will also exit.
153
156
*/
154
- export class WrapperProcess {
155
- private process ?: cp . ChildProcess
157
+ export class ParentProcess extends Process {
158
+ public logger = logger . named ( `parent:${ process . pid } ` )
159
+
160
+ private child ?: cp . ChildProcess
156
161
private started ?: Promise < void >
157
162
private readonly logStdoutStream : rfs . RotatingFileStream
158
163
private readonly logStderrStream : rfs . RotatingFileStream
159
164
165
+ protected readonly _onChildMessage = new Emitter < Message > ( )
166
+ protected readonly onChildMessage = this . _onChildMessage . event
167
+
160
168
public constructor ( private currentVersion : string , private readonly options ?: WrapperOptions ) {
169
+ super ( )
170
+
161
171
const opts = {
162
172
size : "10M" ,
163
173
maxFiles : 10 ,
164
174
}
165
175
this . logStdoutStream = rfs . createStream ( path . join ( paths . data , "coder-logs" , "code-server-stdout.log" ) , opts )
166
176
this . logStderrStream = rfs . createStream ( path . join ( paths . data , "coder-logs" , "code-server-stderr.log" ) , opts )
167
177
168
- ipcMain . onDispose ( ( ) => {
178
+ this . onDispose ( ( ) => {
169
179
this . disposeChild ( )
170
180
} )
171
181
172
- ipcMain . onMessage ( ( message ) => {
182
+ this . onChildMessage ( ( message ) => {
173
183
switch ( message . type ) {
174
184
case "relaunch" :
175
- logger . info ( `Relaunching: ${ this . currentVersion } -> ${ message . version } ` )
185
+ this . logger . info ( `Relaunching: ${ this . currentVersion } -> ${ message . version } ` )
176
186
this . currentVersion = message . version
177
187
this . relaunch ( )
178
188
break
179
189
default :
180
- logger . error ( `Unrecognized message ${ message } ` )
190
+ this . logger . error ( `Unrecognized message ${ message } ` )
181
191
break
182
192
}
183
193
} )
184
194
}
185
195
186
196
private disposeChild ( ) : void {
187
197
this . started = undefined
188
- if ( this . process ) {
189
- this . process . removeAllListeners ( )
190
- this . process . kill ( )
198
+ if ( this . child ) {
199
+ this . child . removeAllListeners ( )
200
+ this . child . kill ( )
191
201
}
192
202
}
193
203
@@ -196,16 +206,16 @@ export class WrapperProcess {
196
206
try {
197
207
await this . start ( )
198
208
} catch ( error ) {
199
- logger . error ( error . message )
200
- ipcMain . exit ( typeof error . code === "number" ? error . code : 1 )
209
+ this . logger . error ( error . message )
210
+ this . exit ( typeof error . code === "number" ? error . code : 1 )
201
211
}
202
212
}
203
213
204
214
public start ( ) : Promise < void > {
205
215
// If we have a process then we've already bound this.
206
- if ( ! this . process ) {
216
+ if ( ! this . child ) {
207
217
process . on ( "SIGUSR1" , async ( ) => {
208
- logger . info ( "Received SIGUSR1; hotswapping" )
218
+ this . logger . info ( "Received SIGUSR1; hotswapping" )
209
219
this . relaunch ( )
210
220
} )
211
221
}
@@ -217,7 +227,7 @@ export class WrapperProcess {
217
227
218
228
private async _start ( ) : Promise < void > {
219
229
const child = this . spawn ( )
220
- this . process = child
230
+ this . child = child
221
231
222
232
// Log both to stdout and to the log directory.
223
233
if ( child . stdout ) {
@@ -229,13 +239,13 @@ export class WrapperProcess {
229
239
child . stderr . pipe ( process . stderr )
230
240
}
231
241
232
- logger . debug ( `spawned inner process ${ child . pid } ` )
242
+ this . logger . debug ( `spawned inner process ${ child . pid } ` )
233
243
234
- await ipcMain . handshake ( child )
244
+ await this . handshake ( child )
235
245
236
246
child . once ( "exit" , ( code ) => {
237
- logger . debug ( `inner process ${ child . pid } exited unexpectedly` )
238
- ipcMain . exit ( code || 0 )
247
+ this . logger . debug ( `inner process ${ child . pid } exited unexpectedly` )
248
+ this . exit ( code || 0 )
239
249
} )
240
250
}
241
251
@@ -256,18 +266,52 @@ export class WrapperProcess {
256
266
stdio : [ "ipc" ] ,
257
267
} )
258
268
}
269
+
270
+ /**
271
+ * Wait for a handshake from the child then reply.
272
+ */
273
+ private handshake ( child : cp . ChildProcess ) : Promise < void > {
274
+ return new Promise ( ( resolve , reject ) => {
275
+ const onMessage = ( message : Message ) : void => {
276
+ logger . debug ( `received message from ${ child . pid } ` , field ( "message" , message ) )
277
+ if ( message . type === "handshake" ) {
278
+ child . removeListener ( "message" , onMessage )
279
+ child . on ( "message" , ( msg ) => this . _onChildMessage . emit ( msg ) )
280
+ child . send ( { type : "handshake" } )
281
+ resolve ( )
282
+ }
283
+ }
284
+ child . on ( "message" , onMessage )
285
+ child . once ( "error" , reject )
286
+ child . once ( "exit" , ( code ) => {
287
+ reject ( new ProcessError ( `Unexpected exit with code ${ code } ` , code !== null ? code : undefined ) )
288
+ } )
289
+ } )
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Process wrapper.
295
+ */
296
+ export const wrapper =
297
+ typeof process . env . CODE_SERVER_PARENT_PID !== "undefined"
298
+ ? new ChildProcess ( parseInt ( process . env . CODE_SERVER_PARENT_PID ) )
299
+ : new ParentProcess ( require ( "../../package.json" ) . version )
300
+
301
+ export function isChild ( proc : ChildProcess | ParentProcess ) : proc is ChildProcess {
302
+ return proc instanceof ChildProcess
259
303
}
260
304
261
305
// It's possible that the pipe has closed (for example if you run code-server
262
306
// --version | head -1). Assume that means we're done.
263
307
if ( ! process . stdout . isTTY ) {
264
- process . stdout . on ( "error" , ( ) => ipcMain . exit ( ) )
308
+ process . stdout . on ( "error" , ( ) => wrapper . exit ( ) )
265
309
}
266
310
267
311
// Don't let uncaught exceptions crash the process.
268
312
process . on ( "uncaughtException" , ( error ) => {
269
- logger . error ( `Uncaught exception: ${ error . message } ` )
313
+ wrapper . logger . error ( `Uncaught exception: ${ error . message } ` )
270
314
if ( typeof error . stack !== "undefined" ) {
271
- logger . error ( error . stack )
315
+ wrapper . logger . error ( error . stack )
272
316
}
273
317
} )
0 commit comments