From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Shelley Vohr <shelley.vohr@gmail.com>
Date: Tue, 25 Aug 2020 19:34:12 -0700
Subject: n-api,src: provide asynchronous cleanup hooks

Backports https://github.com/nodejs/node/pull/34572 and
https://github.com/nodejs/node/pull/34819.

Sometimes addons need to perform cleanup actions, for example
closing libuv handles or waiting for requests to finish, that
cannot be performed synchronously.

Add C++ API and N-API functions that allow providing such
asynchronous cleanup hooks.

This patch can be removed when Electron upgrades to a version of Node.js
which contains the above referenced commit(s).

diff --git a/src/api/hooks.cc b/src/api/hooks.cc
index 037bdda6f41c825dd112b0cc9fca0ebde47c6163..3b16c0350d8a8494202144407664af41d338fe04 100644
--- a/src/api/hooks.cc
+++ b/src/api/hooks.cc
@@ -73,8 +73,35 @@ int EmitExit(Environment* env) {
       .ToChecked();
 }
 
+typedef void (*CleanupHook)(void* arg);
+typedef void (*AsyncCleanupHook)(void* arg, void(*)(void*), void*);
+
+struct AsyncCleanupHookInfo final {
+  Environment* env;
+  AsyncCleanupHook fun;
+  void* arg;
+  bool started = false;
+  // Use a self-reference to make sure the storage is kept alive while the
+  // cleanup hook is registered but not yet finished.
+  std::shared_ptr<AsyncCleanupHookInfo> self;
+};
+
+// Opaque type that is basically an alias for `shared_ptr<AsyncCleanupHookInfo>`
+// (but not publicly so for easier ABI/API changes). In particular,
+// std::shared_ptr does not generally maintain a consistent ABI even on a
+// specific platform.
+struct ACHHandle final {
+  std::shared_ptr<AsyncCleanupHookInfo> info;
+};
+// This is implemented as an operator on a struct because otherwise you can't
+// default-initialize AsyncCleanupHookHandle, because in C++ for a
+// std::unique_ptr to be default-initializable the deleter type also needs
+// to be default-initializable; in particular, function types don't satisfy
+// this.
+void DeleteACHHandle::operator ()(ACHHandle* handle) const { delete handle; }
+
 void AddEnvironmentCleanupHook(Isolate* isolate,
-                               void (*fun)(void* arg),
+                               CleanupHook fun,
                                void* arg) {
   Environment* env = Environment::GetCurrent(isolate);
   CHECK_NOT_NULL(env);
@@ -82,13 +109,50 @@ void AddEnvironmentCleanupHook(Isolate* isolate,
 }
 
 void RemoveEnvironmentCleanupHook(Isolate* isolate,
-                                  void (*fun)(void* arg),
+                                  CleanupHook fun,
                                   void* arg) {
   Environment* env = Environment::GetCurrent(isolate);
   CHECK_NOT_NULL(env);
   env->RemoveCleanupHook(fun, arg);
 }
 
+static void FinishAsyncCleanupHook(void* arg) {
+  AsyncCleanupHookInfo* info = static_cast<AsyncCleanupHookInfo*>(arg);
+  std::shared_ptr<AsyncCleanupHookInfo> keep_alive = info->self;
+
+  info->env->DecreaseWaitingRequestCounter();
+  info->self.reset();
+}
+
+static void RunAsyncCleanupHook(void* arg) {
+  AsyncCleanupHookInfo* info = static_cast<AsyncCleanupHookInfo*>(arg);
+  info->env->IncreaseWaitingRequestCounter();
+  info->started = true;
+  info->fun(info->arg, FinishAsyncCleanupHook, info);
+}
+
+AsyncCleanupHookHandle AddEnvironmentCleanupHook(
+    Isolate* isolate,
+    AsyncCleanupHook fun,
+    void* arg) {
+  Environment* env = Environment::GetCurrent(isolate);
+  CHECK_NOT_NULL(env);
+  auto info = std::make_shared<AsyncCleanupHookInfo>();
+  info->env = env;
+  info->fun = fun;
+  info->arg = arg;
+  info->self = info;
+  env->AddCleanupHook(RunAsyncCleanupHook, info.get());
+  return AsyncCleanupHookHandle(new ACHHandle { info });
+}
+
+void RemoveEnvironmentCleanupHook(
+    AsyncCleanupHookHandle handle) {
+  if (handle->info->started) return;
+  handle->info->self.reset();
+  handle->info->env->RemoveCleanupHook(RunAsyncCleanupHook, handle->info.get());
+}
+
 async_id AsyncHooksGetExecutionAsyncId(Isolate* isolate) {
   Environment* env = Environment::GetCurrent(isolate);
   if (env == nullptr) return -1;
diff --git a/src/node.h b/src/node.h
index 1e4e805fda7a8dc8d8a2f4f37180dc85ac67128e..0bf092cae8741459aa8ef883d18f5810b255abe3 100644
--- a/src/node.h
+++ b/src/node.h
@@ -739,6 +739,20 @@ NODE_EXTERN void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
                                               void (*fun)(void* arg),
                                               void* arg);
 
+/* These are async equivalents of the above. After the cleanup hook is invoked,
+ * `cb(cbarg)` *must* be called, and attempting to remove the cleanup hook will
+ * have no effect. */
+struct ACHHandle;
+struct NODE_EXTERN DeleteACHHandle { void operator()(ACHHandle*) const; };
+typedef std::unique_ptr<ACHHandle, DeleteACHHandle> AsyncCleanupHookHandle;
+
+NODE_EXTERN AsyncCleanupHookHandle AddEnvironmentCleanupHook(
+    v8::Isolate* isolate,
+    void (*fun)(void* arg, void (*cb)(void*), void* cbarg),
+    void* arg);
+
+NODE_EXTERN void RemoveEnvironmentCleanupHook(AsyncCleanupHookHandle holder);
+
 /* Returns the id of the current execution context. If the return value is
  * zero then no execution has been set. This will happen if the user handles
  * I/O from native code. */
diff --git a/src/node_api.cc b/src/node_api.cc
index fe24eca1b8e2d81fbafd0a1e2da38d957fbaa1c1..66168bd2c28ce6481516e63734616f487e3ec3e1 100644
--- a/src/node_api.cc
+++ b/src/node_api.cc
@@ -507,6 +507,70 @@ napi_status napi_remove_env_cleanup_hook(napi_env env,
   return napi_ok;
 }
 
+struct napi_async_cleanup_hook_handle__ {
+  napi_async_cleanup_hook_handle__(napi_env env,
+                                   napi_async_cleanup_hook user_hook,
+                                   void* user_data):
+      env_(env),
+      user_hook_(user_hook),
+      user_data_(user_data) {
+    handle_ = node::AddEnvironmentCleanupHook(env->isolate, Hook, this);
+    env->Ref();
+  }
+
+  ~napi_async_cleanup_hook_handle__() {
+    node::RemoveEnvironmentCleanupHook(std::move(handle_));
+    if (done_cb_ != nullptr)
+      done_cb_(done_data_);
+
+    // Release the `env` handle asynchronously since it would be surprising if
+    // a call to a N-API function would destroy `env` synchronously.
+    static_cast<node_napi_env>(env_)->node_env()
+        ->SetImmediate([env = env_](node::Environment*) { env->Unref(); });
+  }
+
+  static void Hook(void* data, void (*done_cb)(void*), void* done_data) {
+    auto handle = static_cast<napi_async_cleanup_hook_handle__*>(data);
+    handle->done_cb_ = done_cb;
+    handle->done_data_ = done_data;
+    handle->user_hook_(handle, handle->user_data_);
+  }
+
+  node::AsyncCleanupHookHandle handle_;
+  napi_env env_ = nullptr;
+  napi_async_cleanup_hook user_hook_ = nullptr;
+  void* user_data_ = nullptr;
+  void (*done_cb_)(void*) = nullptr;
+  void* done_data_ = nullptr;
+};
+
+napi_status napi_add_async_cleanup_hook(
+    napi_env env,
+    napi_async_cleanup_hook hook,
+    void* arg,
+    napi_async_cleanup_hook_handle* remove_handle) {
+  CHECK_ENV(env);
+  CHECK_ARG(env, hook);
+
+  napi_async_cleanup_hook_handle__* handle =
+    new napi_async_cleanup_hook_handle__(env, hook, arg);
+
+  if (remove_handle != nullptr)
+    *remove_handle = handle;
+
+  return napi_clear_last_error(env);
+}
+
+napi_status napi_remove_async_cleanup_hook(
+    napi_async_cleanup_hook_handle remove_handle) {
+
+  if (remove_handle == nullptr)
+    return napi_invalid_arg;
+
+  delete remove_handle;
+  return napi_ok;
+}
+
 napi_status napi_fatal_exception(napi_env env, napi_value err) {
   NAPI_PREAMBLE(env);
   CHECK_ARG(env, err);
diff --git a/src/node_api.h b/src/node_api.h
index 2f1b45572d8130f27492eb6188c1aa611f2e01a3..577a1dcd94987202819e7a36a2d9674f13d13614 100644
--- a/src/node_api.h
+++ b/src/node_api.h
@@ -250,6 +250,19 @@ napi_ref_threadsafe_function(napi_env env, napi_threadsafe_function func);
 
 #endif  // NAPI_VERSION >= 4
 
+#ifdef NAPI_EXPERIMENTAL
+
+NAPI_EXTERN napi_status napi_add_async_cleanup_hook(
+    napi_env env,
+    napi_async_cleanup_hook hook,
+    void* arg,
+    napi_async_cleanup_hook_handle* remove_handle);
+
+NAPI_EXTERN napi_status napi_remove_async_cleanup_hook(
+    napi_async_cleanup_hook_handle remove_handle);
+
+#endif  // NAPI_EXPERIMENTAL
+
 EXTERN_C_END
 
 #endif  // SRC_NODE_API_H_
diff --git a/src/node_api_types.h b/src/node_api_types.h
index 1c9a2b8aa21889c0d29fb02b234ae9698d122c2c..0e400e9676df5ba09d350fe7a2a70a1dc9e4d3d6 100644
--- a/src/node_api_types.h
+++ b/src/node_api_types.h
@@ -41,4 +41,10 @@ typedef struct {
   const char* release;
 } napi_node_version;
 
+#ifdef NAPI_EXPERIMENTAL
+typedef struct napi_async_cleanup_hook_handle__* napi_async_cleanup_hook_handle;
+typedef void (*napi_async_cleanup_hook)(napi_async_cleanup_hook_handle handle,
+                                        void* data);
+#endif  // NAPI_EXPERIMENTAL
+
 #endif  // SRC_NODE_API_TYPES_H_
