使 Node.js 退出,无论本机模块异步调用挂起

Make Node.js exit regardless of a native module async call pending

我正在为 Node.js 编写基于 Napi 的模块。

该模块使用 WINAPI WaitForSingleObject(pid)。阻塞的 WINAPI 调用包含在 Napi::AsyncWorker.

问题

异步调用阻止 Node.js 退出。我希望 Node.js 在无事可做时退出,就像 child_process.unref() 一样。所以我想从 Node.js 事件循环中取消引用异步调用。

我没有时间制作它的 NPM 包,但这是我的解决方案。该解决方案适用于所有阻塞系统调用(将调用线程置于等待状态,如 Sleep() 所做的那样)。

想法是:

  1. 使用 std::thread 来阻止 WINAPI 调用。
  2. 使用napi_threadsafe_function从新线程安全地回调到Javascript。
  3. 使用 napi_unref_threadsafe_function 从 Node.js 事件循环中取消引用异步操作。

更多详情:

  1. 为了安全的同时调用,创建一个新的 void* context 和特定于线程的数据并传递它(由 N-API 支持)。
  2. 对于未引用的 napi_threadsafe_function,支持终结器。在此处停止等待线程以防止 Node.js 退出时出现崩溃消息。
  3. 我交替使用 C N-API and C++ node-addon-api。不幸的是,在撰写本文时,纯 C++ 解决方案是不可能的,因为 napi_unref_threadsafe_function 只能从 C API.
  4. 获得

addon.cc:

    #include <napi.h>
    #include <node_api.h>
    #include <windows.h>
    #include <tlhelp32.h>
    #include <sstream>
    #include <string>
    #include <iostream>
    #include <thread>

    using namespace std;
    using namespace Napi;

    struct my_context {
      std::thread nativeThread;
      HANDLE hStopEvent;
      napi_threadsafe_function tsfn;
    };

    std::string WaitForPid(int pid, my_context* context, bool* stopped) {
      HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
      if (process == NULL)
      {
        std::stringstream stream;
        stream << "OpenProcess failed: " << (int)GetLastError();
        return stream.str();
      }
      context->hStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
      if (process == NULL)
      {
        std::stringstream stream;
        stream << "CreateEvent failed: " << (int)GetLastError();
        return stream.str();
      }
      HANDLE hEvents[2] = {process, context->hStopEvent};
      DWORD waitResult = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE);
      if (waitResult == WAIT_FAILED)
      {
        std::stringstream stream;
        stream << "WaitForSingleObject failed: " << (int)GetLastError();
        return stream.str();
      } else if (waitResult == WAIT_OBJECT_0 + 1) {
        *stopped = true;
      }
      return std::string();
    }

    Value WaitForProcessToExit(CallbackInfo &info)
    {
      Env env = info.Env();

      if (info.Length() < 3) {
        TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException();
        return env.Null();
      }

      if (!info[0].IsNumber() || !info[1].IsBoolean() || !info[2].IsFunction()) {
        TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
        return env.Null();
      }

      int pid = info[0].As<Number>().Int32Value();
      bool unref = info[1].As<Boolean>();
      Function func = info[2].As<Function>();

      // Do async stuff

      my_context* context = new my_context();

      NAPI_THROW_IF_FAILED(env, napi_create_threadsafe_function(
      (napi_env) env,
      (napi_value) func,
      NULL,
      (napi_value) String::New(env, "WaitForProcessToExit"),
      0,
      1,
      NULL,
      [](napi_env env, void* finalize_data, void* finalize_hint /* Context attached to the TSFN */) {
        SetEvent(((my_context*)finalize_hint)->hStopEvent);
        ((my_context*)finalize_hint)->nativeThread.join();
        delete finalize_hint;
      }, 
      context, // Context attached to the TSFN
      [](napi_env env, napi_value js_callback, void* context, void* data) {
          std::string* error = (std::string*)data;
          // Transform native data into JS data, passing it to the provided `js_callback` (the TSFN's JavaScript function)
          napi_status status = napi_ok;

          napi_value global;
          status = napi_get_global(env, &global);
          if (status != napi_ok)
          {
            std::cout << "napi_get_global failed" << std::endl;
          }

          napi_value result;
          if (error->empty()) {
            status = napi_call_function(env, global, js_callback, 0, NULL, &result);
          } else {
            napi_value values[] = { (napi_value)Error::New(env, *error).Value() };
            status = napi_call_function(env, global, js_callback, 1, values, &result);
          }
          delete data;
          if (status != napi_ok)
          {
            std::cout << "napi_call_function failed" << std::endl;
          }
          status = napi_release_threadsafe_function(((my_context*)context)->tsfn, napi_tsfn_release);
          if (status != napi_ok)
          {
            std::cout << "napi_release_threadsafe_function failed" << std::endl;
          }
      },
      &(context->tsfn)), env.Undefined());

      context->nativeThread = std::thread([pid, context] {
        bool stopped = false;
        std::string error = WaitForPid(pid, context, &stopped);

        if (stopped) {
          return;
        }

        napi_status status = napi_call_threadsafe_function(context->tsfn, new std::string(error), napi_tsfn_blocking);
        if (status != napi_ok)
        {
          std::cout << "napi_call_threadsafe_function failed" << std::endl;
        }
      });

      if (unref) {
        NAPI_THROW_IF_FAILED(env, napi_unref_threadsafe_function((napi_env) env, context->tsfn), env.Undefined());
      }

      return env.Undefined();
    }

    Object Init(Env env, Object exports)
    {
      exports.Set(String::New(env, "WaitForProcessToExit"), Function::New(env, Winapi::WaitForProcessToExit));
      return exports;
    }

    NODE_API_MODULE(hello, Init)