66namespace Microsoft . AspNetCore . NodeServices . HostingModels
77{
88 /// <summary>
9- /// Class responsible for launching the Node child process, determining when it is ready to accept invocations,
10- /// and finally killing it when the parent process exits. Also it restarts the child process if it dies.
9+ /// Class responsible for launching a Node child process on the local machine, determining when it is ready to
10+ /// accept invocations, detecting if it dies on its own, and finally terminating it on disposal.
11+ ///
12+ /// This abstract base class uses the input/output streams of the child process to perform a simple handshake
13+ /// to determine when the child process is ready to accept invocations. This is agnostic to the mechanism that
14+ /// derived classes use to actually perform the invocations (e.g., they could use HTTP-RPC, or a binary TCP
15+ /// protocol, or any other RPC-type mechanism).
1116 /// </summary>
12- /// <seealso cref="Microsoft.AspNetCore.NodeServices.INodeInstance" />
17+ /// <seealso cref="Microsoft.AspNetCore.NodeServices.HostingModels. INodeInstance" />
1318 public abstract class OutOfProcessNodeInstance : INodeInstance
1419 {
15- private readonly object _childProcessLauncherLock ;
16- private string _commandLineArguments ;
17- private readonly StringAsTempFile _entryPointScript ;
18- private Process _nodeProcess ;
19- private TaskCompletionSource < bool > _nodeProcessIsReadySource ;
20- private readonly string _projectPath ;
20+ private const string ConnectionEstablishedMessage = "[Microsoft.AspNetCore.NodeServices:Listening]" ;
21+ private readonly TaskCompletionSource < object > _connectionIsReadySource = new TaskCompletionSource < object > ( ) ;
2122 private bool _disposed ;
23+ private readonly StringAsTempFile _entryPointScript ;
24+ private readonly Process _nodeProcess ;
2225
2326 public OutOfProcessNodeInstance ( string entryPointScript , string projectPath , string commandLineArguments = null )
2427 {
25- _childProcessLauncherLock = new object ( ) ;
2628 _entryPointScript = new StringAsTempFile ( entryPointScript ) ;
27- _projectPath = projectPath ;
28- _commandLineArguments = commandLineArguments ?? string . Empty ;
29+ _nodeProcess = LaunchNodeProcess ( _entryPointScript . FileName , projectPath , commandLineArguments ) ;
30+ ConnectToInputOutputStreams ( ) ;
2931 }
3032
31- public string CommandLineArguments
33+ public async Task < T > InvokeExportAsync < T > ( string moduleName , string exportNameOrNull , params object [ ] args )
3234 {
33- get { return _commandLineArguments ; }
34- set { _commandLineArguments = value ; }
35- }
35+ // Wait until the connection is established. This will throw if the connection fails to initialize.
36+ await _connectionIsReadySource . Task ;
3637
37- public Task < T > InvokeExportAsync < T > ( string moduleName , string exportNameOrNull , params object [ ] args )
38- {
39- return InvokeExportAsync < T > ( new NodeInvocationInfo
38+ if ( _nodeProcess . HasExited )
39+ {
40+ // This special kind of exception triggers a transparent retry - NodeServicesImpl will launch
41+ // a new Node instance and pass the invocation to that one instead.
42+ throw new NodeInvocationException ( "The Node process has exited" , null , nodeInstanceUnavailable : true ) ;
43+ }
44+
45+ return await InvokeExportAsync < T > ( new NodeInvocationInfo
4046 {
4147 ModuleName = moduleName ,
4248 ExportedFunctionName = exportNameOrNull ,
@@ -52,71 +58,74 @@ public void Dispose()
5258
5359 protected abstract Task < T > InvokeExportAsync < T > ( NodeInvocationInfo invocationInfo ) ;
5460
55- protected void ExitNodeProcess ( )
61+ protected virtual void OnOutputDataReceived ( string outputData )
62+ {
63+ Console . WriteLine ( "[Node] " + outputData ) ;
64+ }
65+
66+ protected virtual void OnErrorDataReceived ( string errorData )
67+ {
68+ Console . WriteLine ( "[Node] " + errorData ) ;
69+ }
70+
71+ protected virtual void Dispose ( bool disposing )
5672 {
57- if ( _nodeProcess != null && ! _nodeProcess . HasExited )
73+ if ( ! _disposed )
5874 {
75+ if ( disposing )
76+ {
77+ _entryPointScript . Dispose ( ) ;
78+ }
79+
80+ // Make sure the Node process is finished
5981 // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup?
60- _nodeProcess . Kill ( ) ;
82+ if ( ! _nodeProcess . HasExited )
83+ {
84+ _nodeProcess . Kill ( ) ;
85+ }
86+
87+ _disposed = true ;
6188 }
6289 }
6390
64- protected async Task EnsureReady ( )
91+ private static Process LaunchNodeProcess ( string entryPointFilename , string projectPath , string commandLineArguments )
6592 {
66- lock ( _childProcessLauncherLock )
93+ var startInfo = new ProcessStartInfo ( "node" )
6794 {
68- if ( _nodeProcess == null || _nodeProcess . HasExited )
69- {
70- this . OnBeforeLaunchProcess ( ) ;
95+ Arguments = "\" " + entryPointFilename + "\" " + ( commandLineArguments ?? string . Empty ) ,
96+ UseShellExecute = false ,
97+ RedirectStandardInput = true ,
98+ RedirectStandardOutput = true ,
99+ RedirectStandardError = true ,
100+ WorkingDirectory = projectPath
101+ } ;
71102
72- var startInfo = new ProcessStartInfo ( "node" )
73- {
74- Arguments = "\" " + _entryPointScript . FileName + "\" " + _commandLineArguments ,
75- UseShellExecute = false ,
76- RedirectStandardInput = true ,
77- RedirectStandardOutput = true ,
78- RedirectStandardError = true ,
79- WorkingDirectory = _projectPath
80- } ;
81-
82- // Append projectPath to NODE_PATH so it can locate node_modules
83- var existingNodePath = Environment . GetEnvironmentVariable ( "NODE_PATH" ) ?? string . Empty ;
84- if ( existingNodePath != string . Empty )
85- {
86- existingNodePath += ":" ;
87- }
103+ // Append projectPath to NODE_PATH so it can locate node_modules
104+ var existingNodePath = Environment . GetEnvironmentVariable ( "NODE_PATH" ) ?? string . Empty ;
105+ if ( existingNodePath != string . Empty )
106+ {
107+ existingNodePath += ":" ;
108+ }
88109
89- var nodePathValue = existingNodePath + Path . Combine ( _projectPath , "node_modules" ) ;
110+ var nodePathValue = existingNodePath + Path . Combine ( projectPath , "node_modules" ) ;
90111#if NET451
91- startInfo . EnvironmentVariables [ "NODE_PATH" ] = nodePathValue ;
112+ startInfo . EnvironmentVariables [ "NODE_PATH" ] = nodePathValue ;
92113#else
93- startInfo . Environment [ "NODE_PATH" ] = nodePathValue ;
114+ startInfo . Environment [ "NODE_PATH" ] = nodePathValue ;
94115#endif
95116
96- _nodeProcess = Process . Start ( startInfo ) ;
97- ConnectToInputOutputStreams ( ) ;
98- }
99- }
100-
101- var task = _nodeProcessIsReadySource . Task ;
102- var initializationSucceeded = await task ;
103-
104- if ( ! initializationSucceeded )
105- {
106- throw new InvalidOperationException ( "The Node.js process failed to initialize" , task . Exception ) ;
107- }
117+ return Process . Start ( startInfo ) ;
108118 }
109119
110120 private void ConnectToInputOutputStreams ( )
111121 {
112- var initializationIsCompleted = false ; // TODO: Make this thread-safe? (Interlocked.Exchange etc.)
113- _nodeProcessIsReadySource = new TaskCompletionSource < bool > ( ) ;
122+ var initializationIsCompleted = false ;
114123
115124 _nodeProcess . OutputDataReceived += ( sender , evt ) =>
116125 {
117- if ( evt . Data == "[Microsoft.AspNetCore.NodeServices:Listening]" && ! initializationIsCompleted )
126+ if ( evt . Data == ConnectionEstablishedMessage && ! initializationIsCompleted )
118127 {
119- _nodeProcessIsReadySource . SetResult ( true ) ;
128+ _connectionIsReadySource . SetResult ( null ) ;
120129 initializationIsCompleted = true ;
121130 }
122131 else if ( evt . Data != null )
@@ -129,48 +138,23 @@ private void ConnectToInputOutputStreams()
129138 {
130139 if ( evt . Data != null )
131140 {
132- OnErrorDataReceived ( evt . Data ) ;
133141 if ( ! initializationIsCompleted )
134142 {
135- _nodeProcessIsReadySource . SetResult ( false ) ;
143+ _connectionIsReadySource . SetException (
144+ new InvalidOperationException ( "The Node.js process failed to initialize: " + evt . Data ) ) ;
136145 initializationIsCompleted = true ;
137146 }
147+ else
148+ {
149+ OnErrorDataReceived ( evt . Data ) ;
150+ }
138151 }
139152 } ;
140153
141154 _nodeProcess . BeginOutputReadLine ( ) ;
142155 _nodeProcess . BeginErrorReadLine ( ) ;
143156 }
144157
145- protected virtual void OnBeforeLaunchProcess ( )
146- {
147- }
148-
149- protected virtual void OnOutputDataReceived ( string outputData )
150- {
151- Console . WriteLine ( "[Node] " + outputData ) ;
152- }
153-
154- protected virtual void OnErrorDataReceived ( string errorData )
155- {
156- Console . WriteLine ( "[Node] " + errorData ) ;
157- }
158-
159- protected virtual void Dispose ( bool disposing )
160- {
161- if ( ! _disposed )
162- {
163- if ( disposing )
164- {
165- _entryPointScript . Dispose ( ) ;
166- }
167-
168- ExitNodeProcess ( ) ;
169-
170- _disposed = true ;
171- }
172- }
173-
174158 ~ OutOfProcessNodeInstance ( )
175159 {
176160 Dispose ( false ) ;
0 commit comments