6
6
namespace Microsoft . AspNetCore . NodeServices . HostingModels
7
7
{
8
8
/// <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).
11
16
/// </summary>
12
- /// <seealso cref="Microsoft.AspNetCore.NodeServices.INodeInstance" />
17
+ /// <seealso cref="Microsoft.AspNetCore.NodeServices.HostingModels. INodeInstance" />
13
18
public abstract class OutOfProcessNodeInstance : INodeInstance
14
19
{
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 > ( ) ;
21
22
private bool _disposed ;
23
+ private readonly StringAsTempFile _entryPointScript ;
24
+ private readonly Process _nodeProcess ;
22
25
23
26
public OutOfProcessNodeInstance ( string entryPointScript , string projectPath , string commandLineArguments = null )
24
27
{
25
- _childProcessLauncherLock = new object ( ) ;
26
28
_entryPointScript = new StringAsTempFile ( entryPointScript ) ;
27
- _projectPath = projectPath ;
28
- _commandLineArguments = commandLineArguments ?? string . Empty ;
29
+ _nodeProcess = LaunchNodeProcess ( _entryPointScript . FileName , projectPath , commandLineArguments ) ;
30
+ ConnectToInputOutputStreams ( ) ;
29
31
}
30
32
31
- public string CommandLineArguments
33
+ public async Task < T > InvokeExportAsync < T > ( string moduleName , string exportNameOrNull , params object [ ] args )
32
34
{
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 ;
36
37
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
40
46
{
41
47
ModuleName = moduleName ,
42
48
ExportedFunctionName = exportNameOrNull ,
@@ -52,71 +58,74 @@ public void Dispose()
52
58
53
59
protected abstract Task < T > InvokeExportAsync < T > ( NodeInvocationInfo invocationInfo ) ;
54
60
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 )
56
72
{
57
- if ( _nodeProcess != null && ! _nodeProcess . HasExited )
73
+ if ( ! _disposed )
58
74
{
75
+ if ( disposing )
76
+ {
77
+ _entryPointScript . Dispose ( ) ;
78
+ }
79
+
80
+ // Make sure the Node process is finished
59
81
// 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 ;
61
88
}
62
89
}
63
90
64
- protected async Task EnsureReady ( )
91
+ private static Process LaunchNodeProcess ( string entryPointFilename , string projectPath , string commandLineArguments )
65
92
{
66
- lock ( _childProcessLauncherLock )
93
+ var startInfo = new ProcessStartInfo ( "node" )
67
94
{
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
+ } ;
71
102
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
+ }
88
109
89
- var nodePathValue = existingNodePath + Path . Combine ( _projectPath , "node_modules" ) ;
110
+ var nodePathValue = existingNodePath + Path . Combine ( projectPath , "node_modules" ) ;
90
111
#if NET451
91
- startInfo . EnvironmentVariables [ "NODE_PATH" ] = nodePathValue ;
112
+ startInfo . EnvironmentVariables [ "NODE_PATH" ] = nodePathValue ;
92
113
#else
93
- startInfo . Environment [ "NODE_PATH" ] = nodePathValue ;
114
+ startInfo . Environment [ "NODE_PATH" ] = nodePathValue ;
94
115
#endif
95
116
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 ) ;
108
118
}
109
119
110
120
private void ConnectToInputOutputStreams ( )
111
121
{
112
- var initializationIsCompleted = false ; // TODO: Make this thread-safe? (Interlocked.Exchange etc.)
113
- _nodeProcessIsReadySource = new TaskCompletionSource < bool > ( ) ;
122
+ var initializationIsCompleted = false ;
114
123
115
124
_nodeProcess . OutputDataReceived += ( sender , evt ) =>
116
125
{
117
- if ( evt . Data == "[Microsoft.AspNetCore.NodeServices:Listening]" && ! initializationIsCompleted )
126
+ if ( evt . Data == ConnectionEstablishedMessage && ! initializationIsCompleted )
118
127
{
119
- _nodeProcessIsReadySource . SetResult ( true ) ;
128
+ _connectionIsReadySource . SetResult ( null ) ;
120
129
initializationIsCompleted = true ;
121
130
}
122
131
else if ( evt . Data != null )
@@ -129,48 +138,23 @@ private void ConnectToInputOutputStreams()
129
138
{
130
139
if ( evt . Data != null )
131
140
{
132
- OnErrorDataReceived ( evt . Data ) ;
133
141
if ( ! initializationIsCompleted )
134
142
{
135
- _nodeProcessIsReadySource . SetResult ( false ) ;
143
+ _connectionIsReadySource . SetException (
144
+ new InvalidOperationException ( "The Node.js process failed to initialize: " + evt . Data ) ) ;
136
145
initializationIsCompleted = true ;
137
146
}
147
+ else
148
+ {
149
+ OnErrorDataReceived ( evt . Data ) ;
150
+ }
138
151
}
139
152
} ;
140
153
141
154
_nodeProcess . BeginOutputReadLine ( ) ;
142
155
_nodeProcess . BeginErrorReadLine ( ) ;
143
156
}
144
157
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
-
174
158
~ OutOfProcessNodeInstance ( )
175
159
{
176
160
Dispose ( false ) ;
0 commit comments