-
Notifications
You must be signed in to change notification settings - Fork 77
Expand file tree
/
Copy pathNodeEmbeddingThreadRuntime.cs
More file actions
412 lines (361 loc) · 16.3 KB
/
NodeEmbeddingThreadRuntime.cs
File metadata and controls
412 lines (361 loc) · 16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace Microsoft.JavaScript.NodeApi.Runtime;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.JavaScript.NodeApi.Interop;
using static JSRuntime;
/// <summary>
/// A Node.js runtime environment with a dedicated main execution thread.
/// </summary>
/// <remarks>
/// Multiple Node.js environments may be created (concurrently) in the same process. Each
/// environment instance has its own dedicated execution thread. Except where otherwise documented,
/// all interaction with the environment and JavaScript values associated with the environment MUST
/// be executed on the environment's thread. Use the
/// <see cref="NodeEmbeddingThreadRuntime.SynchronizationContext" /> to switch to the thread.
/// </remarks>
public sealed class NodeEmbeddingThreadRuntime : IDisposable
{
private readonly JSValueScope _scope;
private readonly Thread _thread;
private readonly JSThreadSafeFunction? _completion;
public static explicit operator napi_env(NodeEmbeddingThreadRuntime environment) =>
(napi_env)environment._scope;
public static implicit operator JSValueScope(NodeEmbeddingThreadRuntime environment) =>
environment._scope;
internal NodeEmbeddingThreadRuntime(
NodeEmbeddingPlatform platform,
string? baseDir,
NodeEmbeddingRuntimeSettings? settings)
{
JSValueScope scope = null!;
JSSynchronizationContext syncContext = null!;
JSThreadSafeFunction? completion = null;
using ManualResetEvent loadedEvent = new(false);
_thread = new(() =>
{
using var runtime = NodeEmbeddingRuntime.Create(platform, settings);
// The new scope instance saves itself as the thread-local JSValueScope.Current.
using var nodeApiScope = new NodeEmbeddingNodeApiScope(runtime);
completion = new JSThreadSafeFunction(
maxQueueSize: 0,
initialThreadCount: 1,
asyncResourceName: (JSValue)nameof(NodeEmbeddingThreadRuntime));
scope = JSValueScope.Current;
syncContext = scope.RuntimeContext.SynchronizationContext;
if (!string.IsNullOrEmpty(baseDir))
{
JSValue.Global.SetProperty("__dirname", baseDir!);
InitializeModuleImportFunctions(scope.RuntimeContext, baseDir!);
}
loadedEvent.Set();
// Run the JS event loop until disposal unrefs the completion thread safe function.
try
{
runtime.RunEventLoop();
ExitCode = 0;
}
catch (Exception)
{
ExitCode = 1;
}
syncContext.Dispose();
});
_thread.Start();
loadedEvent.WaitOne();
_completion = completion;
_scope = scope;
SynchronizationContext = syncContext;
}
private static void InitializeModuleImportFunctions(
JSRuntimeContext runtimeContext,
string baseDir)
{
// The require function is available as a global in the embedding context.
JSFunction originalRequire = (JSFunction)JSValue.Global["require"];
JSReference originalRequireRef = new(originalRequire);
JSFunction envRequire = new("require", (modulePath) =>
{
Debug.WriteLine($"require('{(string)modulePath}')");
JSValue require = originalRequireRef.GetValue();
JSValue resolvedPath = ResolveModulePath(require, modulePath, baseDir);
return require.Call(thisArg: default, resolvedPath);
});
// Also set up a callback for require.resolve(), in case it is used by imported modules.
JSValue requireObject = (JSValue)envRequire;
requireObject["resolve"] = new JSFunction("resolve", (modulePath) =>
{
JSValue require = originalRequireRef.GetValue();
return ResolveModulePath(require, modulePath, baseDir);
});
JSValue.Global.SetProperty("require", envRequire);
runtimeContext.RequireFunction = envRequire;
// The import keyword is not a function and is only available through use of an
// external helper module.
#if NETFRAMEWORK || NETSTANDARD
string assemblyLocation = new Uri(typeof(NodeEmbeddingThreadRuntime).Assembly.CodeBase).LocalPath;
#else
#pragma warning disable IL3000 // Assembly.Location returns an empty string for assemblies embedded in a single-file app
string assemblyLocation = typeof(NodeEmbeddingThreadRuntime).Assembly.Location;
#pragma warning restore IL3000
#endif
if (!string.IsNullOrEmpty(assemblyLocation))
{
string importAdapterModulePath = Path.Combine(
Path.GetDirectoryName(assemblyLocation)!, "import.cjs");
if (File.Exists(importAdapterModulePath))
{
JSFunction originalImport = (JSFunction)originalRequire.CallAsStatic(
importAdapterModulePath);
JSReference originalImportRef = new(originalImport);
JSFunction envImport = new("import", (modulePath) =>
{
JSValue require = originalRequireRef.GetValue();
JSValue resolvedPath = ResolveModulePath(require, modulePath, baseDir);
JSValue moduleUri = "file://" + (string)resolvedPath;
JSValue import = originalImportRef.GetValue();
return import.Call(thisArg: default, moduleUri);
});
JSValue.Global.SetProperty("import", envImport);
runtimeContext.ImportFunction = envImport;
}
}
}
/// <summary>
/// Use the require.resolve() function with an explicit base directory to resolve both
/// CommonJS and ES modules.
/// </summary>
/// <param name="require">Require function.</param>
/// <param name="modulePath">Module name or path that was supplied to the require or import
/// function.</param>
/// <param name="baseDir">Base directory for the module resolution.</param>
/// <returns>Resolved module path.</returns>
/// <exception cref="JSException">Thrown if the module could not be resolved.</exception>
private static JSValue ResolveModulePath(
JSValue require,
JSValue modulePath,
string baseDir)
{
// Pass the base directory to require.resolve() via the options object.
JSObject options = new();
options["paths"] = new JSArray(new[] { (JSValue)baseDir! });
return require.CallMethod("resolve", modulePath, options);
}
/// <summary>
/// Gets a synchronization context that enables switching to the Node.js environment's thread
/// and returning to the thread after an `await`.
/// </summary>
/// <remarks>
/// Except where otherwise documented, all interaction with the environment and JavaScript
/// values associated with the environment MUST be executed on the environment's thread.
/// The "Post" and "Run" methods of this class use the synchronization context to switch to
/// the JS thread.
/// </remarks>
/// <seealso cref="Post(Action, bool) "/>
/// <seealso cref="Post(Func{Task}, bool)"/>
/// <seealso cref="Run(Action)"/>
/// <seealso cref="Run{T}(Func{T})"/>
/// <seealso cref="RunAsync(Func{Task})"/>
/// <seealso cref="RunAsync{T}(Func{Task{T}})"/>
public JSSynchronizationContext SynchronizationContext { get; }
/// <summary>
/// Gets the exit code returned by the environment after it is disposed, or null if the
/// environment is not disposed.
/// </summary>
public int? ExitCode { get; private set; }
/// <summary>
/// Gets a value indicating whether the Node.js environment is disposed.
/// </summary>
public bool IsDisposed { get; private set; }
/// <summary>
/// Disposes the Node.js environment, causing its main thread to exit.
/// </summary>
public void Dispose()
{
if (IsDisposed) return;
IsDisposed = true;
// Unreffing the completion should complete the Node.js event loop
// if it has nothing else to do.
// The Unref must be called in the JS thread.
_completion?.BlockingCall(() => _completion.Unref());
_thread.Join();
Debug.WriteLine($"Node.js environment exited with code: {ExitCode}");
}
public Uri StartInspector(int? port = null, string? host = null, bool? wait = null)
{
if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingThreadRuntime));
return SynchronizationContext.Run(() =>
{
JSValue inspector = JSValue.Global["require"].Call(JSValue.Undefined, "node:inspector");
inspector.CallMethod(
"open",
port != null ? (JSValue)port : JSValue.Undefined,
host != null ? (JSValue)host : JSValue.Undefined,
wait ?? false);
return new Uri((string)inspector.CallMethod("url"));
});
}
public void StopInspector()
{
if (IsDisposed) return;
SynchronizationContext.Run(() =>
{
JSValue inspector = JSValue.Global["require"].Call(JSValue.Undefined, "node:inspector");
inspector.CallMethod("close");
});
}
/// <summary>
/// Event args for an unhandled promise rejection in a Node.js environment.
/// </summary>
/// <seealso cref="UnhandledPromiseRejection" />
public class UnhandledPromiseRejectionEventArgs : EventArgs
{
public JSValue Error { get; set; }
}
private EventHandler<UnhandledPromiseRejectionEventArgs>? _unhandledPromiseRejection;
private JSReference? _unhandledPromiseRejectionListener;
/// <summary>
/// Event raised when there is an unhandled promise rejection in the Node.js environment.
/// </summary>
/// <remarks>
/// Event-handlers may be added or removed from any thread, however all events are raised
/// on the environment thread.
/// </remarks>
public event EventHandler<UnhandledPromiseRejectionEventArgs>? UnhandledPromiseRejection
{
add
{
if (IsDisposed) return;
if (_unhandledPromiseRejection == null)
{
SynchronizationContext.Run(AddUnhandledPromiseRejectionListener);
}
_unhandledPromiseRejection += value;
}
remove
{
if (IsDisposed) return;
_unhandledPromiseRejection -= value;
if (_unhandledPromiseRejection == null)
{
SynchronizationContext.Run(RemoveUnhandledPromiseRejectionListener);
}
}
}
private void AddUnhandledPromiseRejectionListener()
{
JSValue listener = JSValue.CreateFunction(
"unhandledRejection", OnUnhandledPromiseRejection);
_unhandledPromiseRejectionListener = new JSReference(listener);
JSValue.Global["process"].CallMethod("on", "unhandledRejection", listener);
}
private void RemoveUnhandledPromiseRejectionListener()
{
JSValue listener = _unhandledPromiseRejectionListener!.GetValue();
JSValue.Global["process"].CallMethod("off", "unhandledRejection", listener);
_unhandledPromiseRejectionListener.Dispose();
_unhandledPromiseRejectionListener = null;
}
private JSValue OnUnhandledPromiseRejection(JSCallbackArgs args)
{
_unhandledPromiseRejection?.Invoke(this, new UnhandledPromiseRejectionEventArgs
{
Error = args[0],
});
return JSValue.Undefined;
}
/// <summary>
/// Runs an action on the JS thread, without waiting for completion.
/// </summary>
/// <param name="action">The action to run.</param>
/// <param name="allowSync">True to allow the action to run immediately if the current
/// synchronization context is this one. By default the action will always be scheduled
/// for later execution.
/// </param>
public void Post(Action action, bool allowSync = false)
=> SynchronizationContext.Post(action, allowSync);
/// <summary>
/// Runs an asynchronous action on the JS thread, without waiting for completion.
/// </summary>
/// <param name="asyncAction">The action to run.</param>
/// <param name="allowSync">True to allow the action to run immediately if the current
/// synchronization context is this one. By default the action will always be scheduled
/// for later execution.
/// </param>
public void Post(Func<Task> asyncAction, bool allowSync = false)
=> SynchronizationContext.Post(asyncAction, allowSync);
/// <summary>
/// Runs an action on the JS thread, and waits for completion.
/// </summary>
/// <param name="action">The action to run.</param>
/// <exception cref="JSException">Any exception thrown by the action is wrapped in a
/// JS exception. The original exception is available via the
/// <see cref="Exception.InnerException" /> property.</exception>
public void Run(Action action) => SynchronizationContext.Run(action);
/// <summary>
/// Runs an action on the JS thread, and waits for the return value.
/// </summary>
/// <param name="action">The action to run.</param>
/// <exception cref="JSException">Any exception thrown by the action is wrapped in a
/// JS exception. The original exception is available via the
/// <see cref="Exception.InnerException" /> property.</exception>
public T Run<T>(Func<T> action) => SynchronizationContext.Run<T>(action);
/// <summary>
/// Runs an action on the JS thread, and asynchronously waits for completion.
/// </summary>
/// <param name="asyncAction">The action to run.</param>
public Task RunAsync(Func<Task> asyncAction) => SynchronizationContext.RunAsync(asyncAction);
/// <summary>
/// Runs an action on the JS thread, and asynchronously waits for the return value.
/// </summary>
/// <param name="asyncAction">The action to run.</param>
public Task<T> RunAsync<T>(Func<Task<T>> asyncAction)
=> SynchronizationContext.RunAsync<T>(asyncAction);
/// <summary>
/// Imports a module or module property from JavaScript.
/// </summary>
/// <param name="module">Name of the module being imported, or null to import a
/// global property. This is equivalent to the value provided to <c>import</c> or
/// <c>require()</c> in JavaScript. Required if <paramref name="property"/> is null.</param>
/// <param name="property">Name of a property on the module (or global), or null to import
/// the module object. Required if <paramref name="module"/> is null.</param>
/// <param name="esModule">True if importing an ES module. The default is false. Note when
/// importing an ES module the returned value will be a JS Promise object that resolves to the
/// imported value.</param>
/// <returns>The imported value.</returns>
/// <exception cref="ArgumentNullException">Both <paramref name="module" /> and
/// <paramref name="property" /> are null.</exception>
/// <exception cref="InvalidOperationException">The
/// <see cref="JSRuntimeContext.RequireFunction"/> property was not initialized.</exception>
public JSValue Import(string? module, string? property = null, bool esModule = false)
=> _scope.RuntimeContext.Import(module, property, esModule);
public Task<JSValue> ImportAsync(string? module, string? property = null, bool esModule = false)
=> _scope.RuntimeContext.ImportAsync(module, property, esModule);
/// <summary>
/// Runs garbage collection in the JS environment.
/// </summary>
/// <exception cref="InvalidOperationException">The Node.js platform was not initialized
/// with the --expose-gc option.</exception>
public void GC()
{
bool result = SynchronizationContext.Run(() =>
{
JSValue gcFunction = JSValue.Global["gc"];
if (gcFunction.TypeOf() != JSValueType.Function)
{
return false;
}
gcFunction.Call();
return true;
});
if (!result)
{
throw new InvalidOperationException("The global gc() function was not found. " +
"Make sure the Node.js platform was initialized with the `--enable-gc` option.");
}
}
}