Skip to content

Commit eed4d8c

Browse files
Child Node processes poll and exit when parent has exited. Fixesaspnet#270
1 parent 1ce8a22 commit eed4d8c

File tree

6 files changed

+221
-14
lines changed

6 files changed

+221
-14
lines changed

‎src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
varhttp=__webpack_require__(3);
5959
varpath=__webpack_require__(4);
6060
varArgsUtil_1=__webpack_require__(5);
61+
varExitWhenParentExits_1=__webpack_require__(6);
6162
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
6263
// reference to Node's runtime 'require' function.
6364
vardynamicRequire=eval('require');
@@ -121,6 +122,7 @@
121122
// Signal to the NodeServices base class that we're ready to accept invocations
122123
console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
123124
});
125+
ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid));
124126
functionreadRequestBodyAsJson(request,callback){
125127
varrequestBodyAsString='';
126128
request
@@ -208,5 +210,72 @@
208210
exports.parseArgs=parseArgs;
209211

210212

213+
/***/},
214+
/* 6 */
215+
/***/function(module,exports){
216+
217+
/*
218+
In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit,
219+
because we have no further use for them. If the .NET process shuts down gracefully, it will run its
220+
finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately.
221+
222+
But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have
223+
any opportunity to shut down its child processes, and by default they will keep running. In this case, it's
224+
up to the child process to detect this has happened and terminate itself.
225+
226+
There are many possible approaches to detecting when a parent process has exited, most of which behave
227+
differently between Windows and Linux/OS X:
228+
229+
- On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when
230+
the parent does (http://stackoverflow.com/a/4657392). Not cross-platform.
231+
- The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)).
232+
But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X).
233+
- The child Node process can get a callback when its stdin/stdout are disconnected, as described at
234+
http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows
235+
causes the process to terminate prematurely.
236+
- I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes
237+
the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere.
238+
- You can poll to see if the parent process, or your stdin/stdout connection to it, is gone
239+
- You can directly pass a parent process PID to the child, and then have the child poll to see if it's
240+
still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists,
241+
as per https://nodejs.org/api/process.html#process_process_kill_pid_signal)
242+
- Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw.
243+
However I don't see this documented anywhere. It would be nice if you could just poll for whether or not
244+
process.stdout is still connected (without actually writing to it) but I haven't found any property whose
245+
value changes until you actually try to write to it.
246+
247+
Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling
248+
to check whether the parent PID is still running. So that's what we do here.
249+
*/
250+
"use strict";
251+
varpollIntervalMs=1000;
252+
functionexitWhenParentExits(parentPid){
253+
setInterval(function(){
254+
if(!processExists(parentPid)){
255+
// Can't log anything at this point, because out stdout was connected to the parent,
256+
// but the parent is gone.
257+
process.exit();
258+
}
259+
},pollIntervalMs);
260+
}
261+
exports.exitWhenParentExits=exitWhenParentExits;
262+
functionprocessExists(pid){
263+
try{
264+
// Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't
265+
// throw, that means it does exist.
266+
process.kill(pid,0);
267+
returntrue;
268+
}
269+
catch(ex){
270+
// If the reason for the error is that we don't have permission to ask about this process,
271+
// report that as a separate problem.
272+
if(ex.code==='EPERM'){
273+
thrownewError("Attempted to check whether process "+pid+" was running, but got a permissions error.");
274+
}
275+
returnfalse;
276+
}
277+
}
278+
279+
211280
/***/}
212281
/******/])));

‎src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js‎

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
/* 0 */
4545
/***/function(module,exports,__webpack_require__){
4646

47-
module.exports=__webpack_require__(6);
47+
module.exports=__webpack_require__(7);
4848

4949

5050
/***/},
@@ -124,17 +124,85 @@
124124

125125
/***/},
126126
/* 6 */
127+
/***/function(module,exports){
128+
129+
/*
130+
In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit,
131+
because we have no further use for them. If the .NET process shuts down gracefully, it will run its
132+
finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately.
133+
134+
But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have
135+
any opportunity to shut down its child processes, and by default they will keep running. In this case, it's
136+
up to the child process to detect this has happened and terminate itself.
137+
138+
There are many possible approaches to detecting when a parent process has exited, most of which behave
139+
differently between Windows and Linux/OS X:
140+
141+
- On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when
142+
the parent does (http://stackoverflow.com/a/4657392). Not cross-platform.
143+
- The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)).
144+
But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X).
145+
- The child Node process can get a callback when its stdin/stdout are disconnected, as described at
146+
http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows
147+
causes the process to terminate prematurely.
148+
- I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes
149+
the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere.
150+
- You can poll to see if the parent process, or your stdin/stdout connection to it, is gone
151+
- You can directly pass a parent process PID to the child, and then have the child poll to see if it's
152+
still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists,
153+
as per https://nodejs.org/api/process.html#process_process_kill_pid_signal)
154+
- Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw.
155+
However I don't see this documented anywhere. It would be nice if you could just poll for whether or not
156+
process.stdout is still connected (without actually writing to it) but I haven't found any property whose
157+
value changes until you actually try to write to it.
158+
159+
Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling
160+
to check whether the parent PID is still running. So that's what we do here.
161+
*/
162+
"use strict";
163+
varpollIntervalMs=1000;
164+
functionexitWhenParentExits(parentPid){
165+
setInterval(function(){
166+
if(!processExists(parentPid)){
167+
// Can't log anything at this point, because out stdout was connected to the parent,
168+
// but the parent is gone.
169+
process.exit();
170+
}
171+
},pollIntervalMs);
172+
}
173+
exports.exitWhenParentExits=exitWhenParentExits;
174+
functionprocessExists(pid){
175+
try{
176+
// Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't
177+
// throw, that means it does exist.
178+
process.kill(pid,0);
179+
returntrue;
180+
}
181+
catch(ex){
182+
// If the reason for the error is that we don't have permission to ask about this process,
183+
// report that as a separate problem.
184+
if(ex.code==='EPERM'){
185+
thrownewError("Attempted to check whether process "+pid+" was running, but got a permissions error.");
186+
}
187+
returnfalse;
188+
}
189+
}
190+
191+
192+
/***/},
193+
/* 7 */
127194
/***/function(module,exports,__webpack_require__){
128195

129196
"use strict";
130197
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
131198
// but simplifies things for the consumer of this module.
132199
__webpack_require__(2);
133-
varnet=__webpack_require__(7);
200+
varnet=__webpack_require__(8);
134201
varpath=__webpack_require__(4);
135-
varreadline=__webpack_require__(8);
202+
varreadline=__webpack_require__(9);
136203
varArgsUtil_1=__webpack_require__(5);
137-
varvirtualConnectionServer=__webpack_require__(9);
204+
varExitWhenParentExits_1=__webpack_require__(6);
205+
varvirtualConnectionServer=__webpack_require__(10);
138206
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
139207
// reference to Node's runtime 'require' function.
140208
vardynamicRequire=eval('require');
@@ -189,27 +257,28 @@
189257
varparsedArgs=ArgsUtil_1.parseArgs(process.argv);
190258
varlistenAddress=(useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/')+parsedArgs.listenAddress;
191259
server.listen(listenAddress);
260+
ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid));
192261

193262

194263
/***/},
195-
/* 7 */
264+
/* 8 */
196265
/***/function(module,exports){
197266

198267
module.exports=require("net");
199268

200269
/***/},
201-
/* 8 */
270+
/* 9 */
202271
/***/function(module,exports){
203272

204273
module.exports=require("readline");
205274

206275
/***/},
207-
/* 9 */
276+
/* 10 */
208277
/***/function(module,exports,__webpack_require__){
209278

210279
"use strict";
211-
varevents_1=__webpack_require__(10);
212-
varVirtualConnection_1=__webpack_require__(11);
280+
varevents_1=__webpack_require__(11);
281+
varVirtualConnection_1=__webpack_require__(12);
213282
// Keep this in sync with the equivalent constant in the .NET code. Both sides split up their transmissions into frames with this max length,
214283
// and both will reject longer frames.
215284
varMaxFrameBodyLength=16*1024;
@@ -390,13 +459,13 @@
390459

391460

392461
/***/},
393-
/* 10 */
462+
/* 11 */
394463
/***/function(module,exports){
395464

396465
module.exports=require("events");
397466

398467
/***/},
399-
/* 11 */
468+
/* 12 */
400469
/***/function(module,exports,__webpack_require__){
401470

402471
"use strict";
@@ -405,7 +474,7 @@
405474
function__(){this.constructor=d;}
406475
d.prototype=b===null ? Object.create(b) : (__.prototype=b.prototype,new__());
407476
};
408-
varstream_1=__webpack_require__(12);
477+
varstream_1=__webpack_require__(13);
409478
/**
410479
* Represents a virtual connection. Multiple virtual connections may be multiplexed over a single physical socket connection.
411480
*/
@@ -446,7 +515,7 @@
446515

447516

448517
/***/},
449-
/* 12 */
518+
/* 13 */
450519
/***/function(module,exports){
451520

452521
module.exports=require("stream");

‎src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,10 @@ protected virtual ProcessStartInfo PrepareNodeProcessStartInfo(
114114
debuggingArgs=string.Empty;
115115
}
116116

117+
varthisProcessPid=Process.GetCurrentProcess().Id;
117118
varstartInfo=newProcessStartInfo("node")
118119
{
119-
Arguments=debuggingArgs+"\""+entryPointFilename+"\""+(commandLineArguments??string.Empty),
120+
Arguments=$"{debuggingArgs}\"{entryPointFilename}\"--parentPid {thisProcessPid}{commandLineArguments??string.Empty}",
120121
UseShellExecute=false,
121122
RedirectStandardInput=true,
122123
RedirectStandardOutput=true,

‎src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import './Util/OverrideStdOutputs'
44
import*ashttpfrom'http';
55
import*aspathfrom'path';
66
import{parseArgs}from'./Util/ArgsUtil';
7+
import{exitWhenParentExits}from'./Util/ExitWhenParentExits';
78

89
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
910
// reference to Node's runtime 'require' function.
@@ -73,6 +74,8 @@ server.listen(requestedPortOrZero, 'localhost', function (){
7374
console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
7475
});
7576

77+
exitWhenParentExits(parseInt(parsedArgs.parentPid));
78+
7679
functionreadRequestBodyAsJson(request,callback){
7780
letrequestBodyAsString='';
7881
request

‎src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as path from 'path'
66
import*asreadlinefrom'readline';
77
import{Duplex}from'stream';
88
import{parseArgs}from'./Util/ArgsUtil';
9+
import{exitWhenParentExits}from'./Util/ExitWhenParentExits';
910
import*asvirtualConnectionServerfrom'./VirtualConnections/VirtualConnectionServer';
1011

1112
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
@@ -69,6 +70,8 @@ const parsedArgs = parseArgs(process.argv);
6970
constlistenAddress=(useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/')+parsedArgs.listenAddress;
7071
server.listen(listenAddress);
7172

73+
exitWhenParentExits(parseInt(parsedArgs.parentPid));
74+
7275
interfaceRpcInvocation{
7376
moduleName: string;
7477
exportedFunctionName: string;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit,
3+
because we have no further use for them. If the .NET process shuts down gracefully, it will run its
4+
finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately.
5+
6+
But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have
7+
any opportunity to shut down its child processes, and by default they will keep running. In this case, it's
8+
up to the child process to detect this has happened and terminate itself.
9+
10+
There are many possible approaches to detecting when a parent process has exited, most of which behave
11+
differently between Windows and Linux/OS X:
12+
13+
- On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when
14+
the parent does (http://stackoverflow.com/a/4657392). Not cross-platform.
15+
- The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)).
16+
But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X).
17+
- The child Node process can get a callback when its stdin/stdout are disconnected, as described at
18+
http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows
19+
causes the process to terminate prematurely.
20+
- I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes
21+
the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere.
22+
- You can poll to see if the parent process, or your stdin/stdout connection to it, is gone
23+
- You can directly pass a parent process PID to the child, and then have the child poll to see if it's
24+
still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists,
25+
as per https://nodejs.org/api/process.html#process_process_kill_pid_signal)
26+
- Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw.
27+
However I don't see this documented anywhere. It would be nice if you could just poll for whether or not
28+
process.stdout is still connected (without actually writing to it) but I haven't found any property whose
29+
value changes until you actually try to write to it.
30+
31+
Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling
32+
to check whether the parent PID is still running. So that's what we do here.
33+
*/
34+
35+
constpollIntervalMs=1000;
36+
37+
exportfunctionexitWhenParentExits(parentPid: number){
38+
setInterval(()=>{
39+
if(!processExists(parentPid)){
40+
// Can't log anything at this point, because out stdout was connected to the parent,
41+
// but the parent is gone.
42+
process.exit();
43+
}
44+
},pollIntervalMs);
45+
}
46+
47+
functionprocessExists(pid: number){
48+
try{
49+
// Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't
50+
// throw, that means it does exist.
51+
process.kill(pid,0);
52+
returntrue;
53+
}catch(ex){
54+
// If the reason for the error is that we don't have permission to ask about this process,
55+
// report that as a separate problem.
56+
if(ex.code==='EPERM'){
57+
thrownewError(`Attempted to check whether process ${pid} was running, but got a permissions error.`);
58+
}
59+
60+
returnfalse;
61+
}
62+
}

0 commit comments

Comments
(0)