@@ -23,6 +23,7 @@ public abstract class OutOfProcessNodeInstance : INodeInstance
2323 private bool _disposed ;
2424 private readonly StringAsTempFile _entryPointScript ;
2525 private readonly Process _nodeProcess ;
26+ private bool _nodeProcessNeedsRestart ;
2627
2728 public OutOfProcessNodeInstance ( string entryPointScript , string projectPath , string commandLineArguments = null )
2829 {
@@ -33,16 +34,19 @@ public OutOfProcessNodeInstance(string entryPointScript, string projectPath, str
3334
3435 public async Task < T > InvokeExportAsync < T > ( string moduleName , string exportNameOrNull , params object [ ] args )
3536 {
36- // Wait until the connection is established. This will throw if the connection fails to initialize.
37- await _connectionIsReadySource . Task ;
38-
39- if ( _nodeProcess . HasExited )
37+ if ( _nodeProcess . HasExited || _nodeProcessNeedsRestart )
4038 {
4139 // This special kind of exception triggers a transparent retry - NodeServicesImpl will launch
4240 // a new Node instance and pass the invocation to that one instead.
43- throw new NodeInvocationException ( "The Node process has exited" , null , nodeInstanceUnavailable : true ) ;
41+ var message = _nodeProcess . HasExited
42+ ? "The Node process has exited"
43+ : "The Node process needs to restart" ;
44+ throw new NodeInvocationException ( message , null , nodeInstanceUnavailable : true ) ;
4445 }
4546
47+ // Wait until the connection is established. This will throw if the connection fails to initialize.
48+ await _connectionIsReadySource . Task ;
49+
4650 return await InvokeExportAsync < T > ( new NodeInvocationInfo
4751 {
4852 ModuleName = moduleName ,
@@ -115,7 +119,17 @@ private static Process LaunchNodeProcess(string entryPointFilename, string proje
115119 startInfo . Environment [ "NODE_PATH" ] = nodePathValue ;
116120#endif
117121
118- return Process . Start ( startInfo ) ;
122+ var process = Process . Start ( startInfo ) ;
123+
124+ // On Mac at least, a killed child process is left open as a zombie until the parent
125+ // captures its exit code. We don't need the exit code for this process, and don't want
126+ // to use process.WaitForExit() explicitly (we'd have to block the thread until it really
127+ // has exited), but we don't want to leave zombies lying around either. It's sufficient
128+ // to use process.EnableRaisingEvents so that .NET will grab the exit code and let the
129+ // zombie be cleaned away without having to block our thread.
130+ process . EnableRaisingEvents = true ;
131+
132+ return process ;
119133 }
120134
121135 private void ConnectToInputOutputStreams ( )
@@ -134,7 +148,7 @@ private void ConnectToInputOutputStreams()
134148 // Temporarily, the file-watching logic is in Node, so look out for the
135149 // signal that we need to restart. This can be removed once the file-watching
136150 // logic is moved over to the .NET side.
137- Dispose ( ) ;
151+ _nodeProcessNeedsRestart = true ;
138152 }
139153 else if ( evt . Data != null )
140154 {
0 commit comments