Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple NodeJsEnvironments #427

Open
rmmason opened this issue Feb 17, 2025 · 6 comments
Open

Multiple NodeJsEnvironments #427

rmmason opened this issue Feb 17, 2025 · 6 comments
Assignees

Comments

@rmmason
Copy link

rmmason commented Feb 17, 2025

We had a project that was performing interop from .NET to JS using version 0.8.20 but were getting a couple of intermittent errors when trying to run outside of our development environment.

The two errors we are getting are:

  1. Already initialized. The initialize function can be used only once.
  2. The JS reference cannot be accessed from the current thread.

We tried to resolve this by updating to the latest nuget packages but it looks like there have been some breaking changes and the docs are no longer up-to-date.

We are using DI and used to store NodeJsPlatform as a singleton and register NodeJsEnvironment as scoped and pass this into another scoped service that executes some JavaScript to render a PDF report.

We have now replaced this with NodeEmbeddingPlatform as a singleton which is passed into the scoped report generation service which then creates a NodeEmbeddingThreadRuntime via platform.CreateThreadRuntime(); and executes await threadRuntime.Run(async () =>...

With the latest code when trying to create the NodeEmbeddingPlatform we get the following error:

"System.EntryPointNotFoundException : Unable to find an entry point named 'node_embedding_platform_create' in DLL."

Could someone please give us some hints as to how to get the latest version working and any pointers as to what our initial issues may be related to?

@rmmason
Copy link
Author

rmmason commented Feb 18, 2025

As an update. We managed to get version 0.9.6 running using the libnode libraries from here: https://github.com/alethic/Microsoft.JavaScript.LibNode/actions/runs/13220509513

But during testing the code inside the await threadRuntime.RunAsync(async () => ... ) ran to conclusion but the Task never returns.

I have gone back to the latest nuget package before NodeJsPlatform was removed (0.9.3) a simple test works but when we run within a Azure Functions problem the first run works but then we get a "JS reference cannot be accessed from the current thread" error.

I would really appreciate some help debugging the issues we are having.

@rmmason
Copy link
Author

rmmason commented Feb 18, 2025

I'm pretty sure we are doing something wrong in terms of the interop with a stream we are intending to pass to the JavaScript. Our intention is to create a MemoryStream and have the JavaScript fill the stream with data. The .NET hosting code looks like this:

What we want to do is pass a memory stream to the JS and have the JS fill the stream.

Our code at the .NET end currently looks like this:

public async Task RenderReportToStream(Stream stream, string reportJson)
{
    using NodejsEnvironment nodejsEnvironment = _nodejsPlatform.CreateEnvironment(_appBaseDir);

    await nodejsEnvironment.RunAsync(async () =>
    {
        if (Debugger.IsAttached)
        {
            var inspectorIUrl = nodejsEnvironment.StartInspector();
            Debug.WriteLine($"Inspector Attached at URL: {inspectorIUrl.AbsolutePath}");
        }

        var importJsValue = nodejsEnvironment.Import("@anon/pdf-report-renderer", "ReportRenderer", true);

        if (!importJsValue.IsPromise())
        {
            throw new Exception("Expected import JSValue to return a promise as we are importing as ES Module.");
        }

        var importJsValuePromise = importJsValue.As<JSPromise>() ?? throw new Exception("Expected import to be castable to promise but got <null>.");

        var importedReportRenderer = await importJsValuePromise.AsTask();

        var jsSampleReportJson = JSValue.CreateStringUtf16(reportJson);

        using MemoryStream ms = new MemoryStream();
        using Stream synchStream = NodeStream.Synchronized(ms);

        var marshaller = new JSMarshaller { AutoCamelCase = true };

        var jsStream = marshaller.ToJS(synchStream);

        JSValue jsReportRendererObj = importedReportRenderer.CallAsConstructor();

        var jsResult = jsReportRendererObj.CallMethod("renderReport", jsSampleReportJson, jsStream);

        if (!jsResult.IsPromise())
        {
            throw new Exception("Expected result from report renderer to be a promise.");
        }

        var jsPromise = jsResult.As<JSPromise>() ?? throw new Exception("Expected result from report renderer to be castable to promise but received <null>.");

        await jsPromise.AsTask();

        ms.Position = 0;
        ms.CopyTo(stream);

        if (Debugger.IsAttached)
        {
            nodejsEnvironment.StopInspector();
        }
    });
}

And the JavaScript being called looks like this:

import {
    ReportDefinitionBuilder,
    ReportDataModel
} from '@anon/pdf-report-viewer';
import { Duplex } from 'stream';

export class ReportRenderer {
    constructor() {
    }

    public async renderReport(jsonReportData: string, stream: Duplex): Promise<void> {
        let reportDataModel = JSON.parse(jsonReportData) as ReportDataModel;
        let reportDefinition = ReportDefinitionBuilder(reportDataModel);
        await reportDefinition.render();
        await reportDefinition.getArrayBuffer()
            .then(async (ab) => {
                stream.write(Buffer.from(ab), "binary");
            });
        return;
    }
}

I think we are probably going wrong when we create an instance of JSMarshaller and use this to wrap the MemoryStream. Could anyone please give us a hint at how this should be done?

@rmmason
Copy link
Author

rmmason commented Feb 26, 2025

As an update we downgraded to 0.9.3 and then returned the ArrayBuffer from the JavaScript and in .NET called GetArrayBufferInfo() to marshal this as a Span<byte>.

This worked well in development but as soon as we went into production this failed.

We traced this down to the fact that although the documentation seems to hint that multiple NodejsEnvironment instances are supported that this was not supported and when two environments were created that this caused errors.

The unsatisfactory and temporary solution to our problem was to use a Semaphore to ensure only a single thread could access the code that creates the NodeJsEnvironment and performs JS interop.

We would welcome some guidance on if multiple NodeJsEnvironments are supported and how we might be able to get this running. With this in mind I am renaming the issue to "Multiple NodeJsEnvironments".

@rmmason rmmason changed the title Upgrading from 0.8.20 to 0.9.6 Multiple NodeJsEnvironments Feb 26, 2025
@rmmason
Copy link
Author

rmmason commented Feb 26, 2025

Just to assist in recreating the issue. The following test will not run:

 [Fact]
 public async Task SupportsMultipleNodeJsEnvironments()
 {
     string appBaseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
     string libNodeRelatiivePath = Environment.Is64BitProcess ? "libnode\\64bit\\libnode.dll" : "libnode\\64bit\\libnode.dll";
     string libnodePath = Path.Combine(appBaseDir, libNodeRelatiivePath);
     var nodejsPlatform = new NodejsPlatform(libnodePath);

     Task r1 = Task.Run(async () =>
     {
         var environment1 = nodejsPlatform.CreateEnvironment(appBaseDir);
         await Task.Delay(1000);
     });

     Task r2 = Task.Run(async () =>
     {
         var environment2 = nodejsPlatform.CreateEnvironment(appBaseDir);
         await Task.Delay(1000);
     });

     await Task.WhenAll(r1, r2);
 }

The output window displays the following:

Log level is set to Informational (Default).
Connected to test environment '< Local Windows Environment >'
Test data store opened in 0.078 sec.
Building Test Projects
Starting test discovery for requested test run
========== Starting test discovery ==========
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.0.1+f8675c32e5 (64-bit .NET 9.0.1)
[xUnit.net 00:00:00.60]   Discovering: [***SNIPPED***].ReportRendering.Tests
[xUnit.net 00:00:00.67]   Discovered:  [***SNIPPED***].ReportRendering.Tests
========== Test discovery finished: 3 Tests found in 3.2 sec ==========
Executing test method: [***SNIPPED***].ReportRendering.Tests.NodeReportRendererTests.SupportsMultipleNodeJsEnvironments
========== Starting test run ==========
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.0.1+f8675c32e5 (64-bit .NET 9.0.1)
[xUnit.net 00:00:00.22]   Starting:    [***SNIPPED***].ReportRendering.Tests
C:\dev\[***SNIPPED***].Tests\bin\Debug\net9.0\testhost.exe[43968]: C:\dev\libnode\src\inspector_agent.cc:704: Assertion `(start_io_thread_async_initialized.exchange(true)) == (false)' failed.
 1: 00007FFAE72D4C1F v8_inspector::V8StackTrace::V8StackTrace+661342
 2: 00007FFAE73B0567 v8_inspector::V8StackTrace::V8StackTrace+1560742
 3: 00007FFAE73B0A45 v8_inspector::V8StackTrace::V8StackTrace+1561988
 4: 00007FFAE7572F15 v8_inspector::V8StackTrace::V8StackTrace+3406420
 5: 00007FFAE734BE09 v8_inspector::V8StackTrace::V8StackTrace+1149256
 6: 00007FFAE728FC6E v8_inspector::V8StackTrace::V8StackTrace+378797
 7: 00007FFAE7359E35 v8_inspector::V8StackTrace::V8StackTrace+1206644
 8: 00007FFAE728ABCF v8_inspector::V8StackTrace::V8StackTrace+358158
 9: 00007FFAE728AF64 v8_inspector::V8StackTrace::V8StackTrace+359075
10: 00007FFAE735A448 v8_inspector::V8StackTrace::V8StackTrace+1208199
11: 00007FFAA87CB383 
The active test run was aborted. Reason: Test host process crashed : C:\dev[***SNIPPED***]\bin\Debug\net9.0\testhost.exe[43968]: C:\dev\libnode\src\inspector_agent.cc:704: Assertion `(start_io_thread_async_initialized.exchange(true)) == (false)' failed.
 1: 00007FFAE72D4C1F v8_inspector::V8StackTrace::V8StackTrace+661342
 2: 00007FFAE73B0567 v8_inspector::V8StackTrace::V8StackTrace+1560742
 3: 00007FFAE73B0A45 v8_inspector::V8StackTrace::V8StackTrace+1561988
 4: 00007FFAE7572F15 v8_inspector::V8StackTrace::V8StackTrace+3406420
 5: 00007FFAE734BE09 v8_inspector::V8StackTrace::V8StackTrace+1149256
 6: 00007FFAE728FC6E v8_inspector::V8StackTrace::V8StackTrace+378797
 7: 00007FFAE7359E35 v8_inspector::V8StackTrace::V8StackTrace+1206644
 8: 00007FFAE728ABCF v8_inspector::V8StackTrace::V8StackTrace+358158
 9: 00007FFAE728AF64 v8_inspector::V8StackTrace::V8StackTrace+359075
10: 00007FFAE735A448 v8_inspector::V8StackTrace::V8StackTrace+1208199
11: 00007FFAA87CB383 

@vmoroz vmoroz self-assigned this Mar 14, 2025
@vmoroz vmoroz moved this to 📋 Backlog in node-api-dotnet tasks Mar 14, 2025
@jasongin
Copy link
Member

Related to #348. Maybe duplicate?

@jasongin
Copy link
Member

@vmoroz Is this resolved with the new embedding API?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: 📋 Backlog
Development

No branches or pull requests

3 participants