美文网首页node程序员
node/electron插件: 由监听 Windows 打印机

node/electron插件: 由监听 Windows 打印机

作者: 三岁于辛 | 来源:发表于2019-03-24 12:16 被阅读0次

    写在前面

    这里说的插件,其实是基于 node-addon-api 编写的插件。有人会说,其实 github 上已经有人开源的打印机相关的组件。
    但是,它不是本人要的。
    本人需要的是:第一时间知道打印机的及打印任务的所有状态!

    最初实现

    开始写第一个版本时,因为进度需要,本人快速实现了一个 dll 版本,然后在 electron 中通过 ffi 组件调用本人的 dll 。它工作得很好,但是它调用链中增加了一层 ffi ,让本人很是介意~有点强迫症!!!

    重写版本

    第一个版本功能稳定后,本人深入挖了一下 ffi 的功能实现(本人不是写前端的,node也是初次接触),Get 到它本身也是 C/C++ 实现的组件,然后看了下 node 官方对组件开发的相关介绍,决定绕过 ffi 把本人的 dll 直接变成 node 的插件。

    开始填坑

    为什么说是开始填坑?
    因为本人的功能是 C/C++ & C# 混编的!这中间的坑只填过了,才知深浅。

    坑1:项目配置 —— 托管 /clr

    node 原生插件开发使用了 gyp 配置,为了方便大家使用,官方提供了开源配置项目 node-gyp,依葫芦画瓢,很快完成了 Hello World.,但是,咱怎么能忘记了混编呢?微软对于 C/C++ & C# 混编的配置选项叫 /clr 。找到 MSVSSettings.py 中 /clr 注释对应的配置选项为 CompileAsManaged ,当然也有人在 issue 里提了在 AdditionalOptions 里面增加 /clr ,本人不反对,本人也没有验证,而是选择使用开源代码提供的 CompileAsManaged 选项。有过混编经验的都知道,光改完 /clr 是远远不够,还要改程序集等等一堆选项。这里有一个小技巧,就是可以依赖 npm install 来处理,最终修改到的选项如下:

    "RuntimeLibrary": 2, #MultiThreadedDLL /MD
    "Optimization": 2,
    "RuntimeTypeInfo": "true",
    "CompileAsManaged": "true", # /clr
    "DebugInformationFormat": 3, #ProgramDatabase /Zi
    "ExceptionHandling": 0, #Async /EHa
    "BasicRuntimeChecks": 0, #Default
    

    坑2:项目配置 —— win_delay_load_hook

    踩过坑1后,开始写逻辑了,并且也顺利的实现了功能,开始调度时却被告之:

    正尝试在 OS 加载程序锁内执行托管代码。不要尝试在 DllMain 或映像初始化函数内运行托管代码,这样做会导致应用程序挂起。

    按第一版的实现,本人知道要在 dll 注册位置加上:

    #pragma unmanaged
    

    但是,这个位置具体在哪呢?第一反应应该就是 node 插件初始化的宏位置,但......
    于是又重新翻看了 node addon 的文档,找到了 win_delay_load_hook 这个配置,要设置成 true ,但其实它默认就是 true。既然是默认选项,为何还是不行呢?仔细看此配置的功能,它其实是在项目中默认增加了 win_delay_load_hook.cc 的文件,源文件位于 node-gyp/src 中,将其找出来看后才知道 dll 的入口在这,并且与 depend++ 查看 dll 的导出是一致的,在此文件中加上 #pragma unmanaged 后,程序能顺利运行了。

    这里有个小技巧:win_delay_load_hook.cc 默认在 node_modules 中,而且项目一般不会直接带上这个文件夹,也就是说如果每个开发人员重新 npm install 时此文件会被覆盖,我们其实可以在 gyp 配置中把 win_delay_load_hook 设置成 false ,同时把 win_delay_load_hook.cc 拷贝到项目的源文件中,编译文件中加上这个文件即可。
    最新修正:electron 的时候,win_delay_load_hook.cc 以上述操作会运行不了,所以需要修改 win_delay_load_hook 设置为 true ,然后在 copies 中增加 源文件目录中修改后的到 <(node_gyp_src)/src 中。

    坑3:异步多次回调

    node-addon-api 对异步工作有封装,详见 Napi::AsyncWorker 的使用,但是对于多次回调,这个类并没有支持得很好(也有可能是我使用不当),为了解决这个问题,本人翻了很多 github 上的项目,都没有很好的解决,后来在 github 上找到了 node-addon-examples 找到了 node-addon 的 C 实现 async_work_thread_safe_function 的 example 中有较好的实现,对比了它和 Napi::AsyncWorker 的逻辑过程,发现 Napi::AsyncWorker 应该是不能很好的完成本人需要的功能,所以决定自己实现,具体就是把 async_work_thread_safe_function 参照 Napi::AsyncWorker 改成了模板虚基类。感兴趣的可以联系。

    坑4:打印机监控线程与回调 JS 线程同步

    其实,多线程同步方式有很多,但是为了让 js 线程和工作线程不是一直处于工作状态中,而是有事件时才开始工作和回调,本人选择了 event & critical_section 一起来完成本工作,event 用于打印机事件到达后通知 js 线程取数据,而 critical_section 保证的是对于数据操作的唯一性。我相信大神们肯定有很多别的实现方式,比如说管道等。希望大家提供各种意见吧。

    关键实现

    // safe_async_worker.h
    template <typename T>
    class SafeAsyncWorker : public Napi::ObjectWrap<T>
    {
    public:
      SafeAsyncWorker(const Napi::CallbackInfo &info);
    
    protected:
      virtual void Execute() = 0;
      virtual Napi::Value Parse(napi_env env, void *data) = 0;
      virtual void Free(void *data) = 0;
    
      // Create a thread-safe function and an async queue work item. We pass the
      // thread-safe function to the async queue work item so the latter might have a
      // chance to call into JavaScript from the worker thread on which the
      // ExecuteWork callback runs.
      Napi::Value CreateAsyncWork(const Napi::CallbackInfo &cb);
    
      // This function runs on a worker thread. It has no access to the JavaScript
      // environment except through the thread-safe function.
      static void OnExecuteWork(napi_env env, void *data);
    
      // This function runs on the main thread after `ExecuteWork` exits.
      static void OnWorkComplete(napi_env env, napi_status status, void *data);
    
      // This function is responsible for converting data coming in from the worker
      // thread to napi_value items that can be passed into JavaScript, and for
      // calling the JavaScript function.
      static void OnCallJavaScript(napi_env env, napi_value js_cb, void *context, void *data);
    
      void SubmitWork(void *data);
    
      static Napi::FunctionReference constructor;
    
    private:
      napi_async_work work;
      napi_threadsafe_function tsfn;
    };
    
    // safe_async_worker.inl
    template <typename T>
    Napi::FunctionReference SafeAsyncWorker<T>::constructor;
    
    template <typename T>
    inline SafeAsyncWorker<T>::SafeAsyncWorker(const Napi::CallbackInfo &info)
        : Napi::ObjectWrap<T>(info)
    {
    }
    
    template <typename T>
    void printer::SafeAsyncWorker<T>::SubmitWork(void *data)
    {
      // Initiate the call into JavaScript. The call into JavaScript will not
      // have happened when this function returns, but it will be queued.
      assert(napi_call_threadsafe_function(tsfn, data, napi_tsfn_blocking) == napi_ok);
    }
    
    template <typename T>
    Napi::Value SafeAsyncWorker<T>::CreateAsyncWork(const Napi::CallbackInfo &cb)
    {
      Napi::Env env = cb.Env();
      napi_value work_name;
    
      // Create a string to describe this asynchronous operation.
      assert(napi_create_string_utf8(env,
                                     typeid(T).name(),
                                     NAPI_AUTO_LENGTH,
                                     &work_name) == napi_ok);
    
      // Convert the callback retrieved from JavaScript into a thread-safe function
      // which we can call from a worker thread.
      assert(napi_create_threadsafe_function(env,
                                             cb[0],
                                             NULL,
                                             work_name,
                                             0,
                                             1,
                                             NULL,
                                             NULL,
                                             this,
                                             OnCallJavaScript,
                                             &(tsfn)) == napi_ok);
    
      // Create an async work item, passing in the addon data, which will give the
      // worker thread access to the above-created thread-safe function.
      assert(napi_create_async_work(env,
                                    NULL,
                                    work_name,
                                    OnExecuteWork,
                                    OnWorkComplete,
                                    this,
                                    &(work)) == napi_ok);
    
      // Queue the work item for execution.
      assert(napi_queue_async_work(env, work) == napi_ok);
    
      // This causes `undefined` to be returned to JavaScript.
      return env.Undefined();
    }
    
    template <typename T>
    void SafeAsyncWorker<T>::OnExecuteWork(napi_env /*env*/, void *this_pointer)
    {
      T *self = static_cast<T *>(this_pointer);
    
      // We bracket the use of the thread-safe function by this thread by a call to
      // napi_acquire_threadsafe_function() here, and by a call to
      // napi_release_threadsafe_function() immediately prior to thread exit.
      assert(napi_acquire_threadsafe_function(self->tsfn) == napi_ok);
    #ifdef NAPI_CPP_EXCEPTIONS
      try
      {
        self->Execute();
      }
      catch (const std::exception &e)
      {
        // TODO
      }
    #else  // NAPI_CPP_EXCEPTIONS
      self->Execute();
    #endif // NAPI_CPP_EXCEPTIONS
    
      // Indicate that this thread will make no further use of the thread-safe function.
      assert(napi_release_threadsafe_function(self->tsfn,
                                              napi_tsfn_release) == napi_ok);
    }
    
    template <typename T>
    void SafeAsyncWorker<T>::OnWorkComplete(napi_env env, napi_status status, void *this_pointer)
    {
      T *self = (T *)this_pointer;
    
      // Clean up the thread-safe function and the work item associated with this
      // run.
      assert(napi_release_threadsafe_function(self->tsfn,
                                              napi_tsfn_release) == napi_ok);
      assert(napi_delete_async_work(env, self->work) == napi_ok);
    
      // Set both values to NULL so JavaScript can order a new run of the thread.
      self->work = NULL;
      self->tsfn = NULL;
    }
    
    template <typename T>
    void SafeAsyncWorker<T>::OnCallJavaScript(napi_env env, napi_value js_cb, void *this_pointer, void *data)
    {
      T *self = static_cast<T *>(this_pointer);
      if (env != NULL)
      {
        napi_value undefined;
    #ifdef NAPI_CPP_EXCEPTIONS
        try
        {
          napi_value js_value = self->Parse(env, data);
        }
        catch (const std::exception &e)
        {
          // TODO
        }
    #else  // NAPI_CPP_EXCEPTIONS
        napi_value js_value = self->Parse(env, data);
    #endif // NAPI_CPP_EXCEPTIONS
    
        // Retrieve the JavaScript `undefined` value so we can use it as the `this`
        // value of the JavaScript function call.
        assert(napi_get_undefined(env, &undefined) == napi_ok);
    
        // Call the JavaScript function and pass it the prime that the secondary
        // thread found.
        assert(napi_call_function(env,
                                  undefined,
                                  js_cb,
                                  1,
                                  &js_value,
                                  NULL) == napi_ok);
      }
      self->Free(data);
    }
    

    相关文章

      网友评论

        本文标题:node/electron插件: 由监听 Windows 打印机

        本文链接:https://www.haomeiwen.com/subject/mlrevqtx.html