summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/n-api.md52
-rw-r--r--src/env-inl.h23
-rw-r--r--src/env.cc29
-rw-r--r--src/env.h31
-rw-r--r--src/node.cc20
-rw-r--r--src/node.h13
-rw-r--r--src/node_api.cc22
-rw-r--r--src/node_api.h7
-rw-r--r--test/addons-napi/test_cleanup_hook/binding.cc24
-rw-r--r--test/addons-napi/test_cleanup_hook/binding.gyp9
-rw-r--r--test/addons-napi/test_cleanup_hook/test.js12
11 files changed, 241 insertions, 1 deletions
diff --git a/doc/api/n-api.md b/doc/api/n-api.md
index 739213c155..3748e252fc 100644
--- a/doc/api/n-api.md
+++ b/doc/api/n-api.md
@@ -886,6 +886,58 @@ If still valid, this API returns the `napi_value` representing the
JavaScript `Object` associated with the `napi_ref`. Otherwise, result
will be NULL.
+### Cleanup on exit of the current Node.js instance
+
+While a Node.js process typically releases all its resources when exiting,
+embedders of Node.js, or future Worker support, may require addons to register
+clean-up hooks that will be run once the current Node.js instance exits.
+
+N-API provides functions for registering and un-registering such callbacks.
+When those callbacks are run, all resources that are being held by the addon
+should be freed up.
+
+#### napi_add_env_cleanup_hook
+<!-- YAML
+added: REPLACEME
+-->
+```C
+NODE_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env,
+ void (*fun)(void* arg),
+ void* arg);
+```
+
+Registers `fun` as a function to be run with the `arg` parameter once the
+current Node.js environment exits.
+
+A function can safely be specified multiple times with different
+`arg` values. In that case, it will be called multiple times as well.
+Providing the same `fun` and `arg` values multiple times is not allowed
+and will lead the process to abort.
+
+The hooks will be called in reverse order, i.e. the most recently added one
+will be called first.
+
+Removing this hook can be done by using `napi_remove_env_cleanup_hook`.
+Typically, that happens when the resource for which this hook was added
+is being torn down anyway.
+
+#### napi_remove_env_cleanup_hook
+<!-- YAML
+added: REPLACEME
+-->
+```C
+NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env,
+ void (*fun)(void* arg),
+ void* arg);
+```
+
+Unregisters `fun` as a function to be run with the `arg` parameter once the
+current Node.js environment exits. Both the argument and the function value
+need to be exact matches.
+
+The function must have originally been registered
+with `napi_add_env_cleanup_hook`, otherwise the process will abort.
+
## Module registration
N-API modules are registered in a manner similar to other modules
except that instead of using the `NODE_MODULE` macro the following
diff --git a/src/env-inl.h b/src/env-inl.h
index 6202e50548..d3c0c211d9 100644
--- a/src/env-inl.h
+++ b/src/env-inl.h
@@ -629,6 +629,29 @@ inline void Environment::SetTemplateMethod(v8::Local<v8::FunctionTemplate> that,
t->SetClassName(name_string); // NODE_SET_METHOD() compatibility.
}
+void Environment::AddCleanupHook(void (*fn)(void*), void* arg) {
+ auto insertion_info = cleanup_hooks_.emplace(CleanupHookCallback {
+ fn, arg, cleanup_hook_counter_++
+ });
+ // Make sure there was no existing element with these values.
+ CHECK_EQ(insertion_info.second, true);
+}
+
+void Environment::RemoveCleanupHook(void (*fn)(void*), void* arg) {
+ CleanupHookCallback search { fn, arg, 0 };
+ cleanup_hooks_.erase(search);
+}
+
+size_t Environment::CleanupHookCallback::Hash::operator()(
+ const CleanupHookCallback& cb) const {
+ return std::hash<void*>()(cb.arg_);
+}
+
+bool Environment::CleanupHookCallback::Equal::operator()(
+ const CleanupHookCallback& a, const CleanupHookCallback& b) const {
+ return a.fn_ == b.fn_ && a.arg_ == b.arg_;
+}
+
#define VP(PropertyName, StringValue) V(v8::Private, PropertyName)
#define VS(PropertyName, StringValue) V(v8::String, PropertyName)
#define V(TypeName, PropertyName) \
diff --git a/src/env.cc b/src/env.cc
index 08d719a510..aadb81092e 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -305,6 +305,35 @@ void Environment::PrintSyncTrace() const {
fflush(stderr);
}
+void Environment::RunCleanup() {
+ while (!cleanup_hooks_.empty()) {
+ // Copy into a vector, since we can't sort an unordered_set in-place.
+ std::vector<CleanupHookCallback> callbacks(
+ cleanup_hooks_.begin(), cleanup_hooks_.end());
+ // We can't erase the copied elements from `cleanup_hooks_` yet, because we
+ // need to be able to check whether they were un-scheduled by another hook.
+
+ std::sort(callbacks.begin(), callbacks.end(),
+ [](const CleanupHookCallback& a, const CleanupHookCallback& b) {
+ // Sort in descending order so that the most recently inserted callbacks
+ // are run first.
+ return a.insertion_order_counter_ > b.insertion_order_counter_;
+ });
+
+ for (const CleanupHookCallback& cb : callbacks) {
+ if (cleanup_hooks_.count(cb) == 0) {
+ // This hook was removed from the `cleanup_hooks_` set during another
+ // hook that was run earlier. Nothing to do here.
+ continue;
+ }
+
+ cb.fn_(cb.arg_);
+ cleanup_hooks_.erase(cb);
+ CleanupHandles();
+ }
+ }
+}
+
void Environment::RunBeforeExitCallbacks() {
for (ExitCallback before_exit : before_exit_functions_) {
before_exit.cb_(before_exit.arg_);
diff --git a/src/env.h b/src/env.h
index c0d79883d0..3acb27c954 100644
--- a/src/env.h
+++ b/src/env.h
@@ -42,6 +42,7 @@
#include <stdint.h>
#include <vector>
#include <unordered_map>
+#include <unordered_set>
struct nghttp2_rcbuf;
@@ -775,6 +776,10 @@ class Environment {
v8::Local<v8::Value> GetNow();
+ inline void AddCleanupHook(void (*fn)(void*), void* arg);
+ inline void RemoveCleanupHook(void (*fn)(void*), void* arg);
+ void RunCleanup();
+
private:
inline void CreateImmediate(native_immediate_callback cb,
void* data,
@@ -863,6 +868,32 @@ class Environment {
void RunAndClearNativeImmediates();
static void CheckImmediate(uv_check_t* handle);
+ struct CleanupHookCallback {
+ void (*fn_)(void*);
+ void* arg_;
+
+ // We keep track of the insertion order for these objects, so that we can
+ // call the callbacks in reverse order when we are cleaning up.
+ uint64_t insertion_order_counter_;
+
+ // Only hashes `arg_`, since that is usually enough to identify the hook.
+ struct Hash {
+ inline size_t operator()(const CleanupHookCallback& cb) const;
+ };
+
+ // Compares by `fn_` and `arg_` being equal.
+ struct Equal {
+ inline bool operator()(const CleanupHookCallback& a,
+ const CleanupHookCallback& b) const;
+ };
+ };
+
+ // Use an unordered_set, so that we have efficient insertion and removal.
+ std::unordered_set<CleanupHookCallback,
+ CleanupHookCallback::Hash,
+ CleanupHookCallback::Equal> cleanup_hooks_;
+ uint64_t cleanup_hook_counter_ = 0;
+
static void EnvPromiseHook(v8::PromiseHookType type,
v8::Local<v8::Promise> promise,
v8::Local<v8::Value> parent);
diff --git a/src/node.cc b/src/node.cc
index 5ddbf99150..c95e084f0c 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -904,6 +904,22 @@ void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) {
env->AddPromiseHook(fn, arg);
}
+void AddEnvironmentCleanupHook(v8::Isolate* isolate,
+ void (*fun)(void* arg),
+ void* arg) {
+ Environment* env = Environment::GetCurrent(isolate);
+ env->AddCleanupHook(fun, arg);
+}
+
+
+void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
+ void (*fun)(void* arg),
+ void* arg) {
+ Environment* env = Environment::GetCurrent(isolate);
+ env->RemoveCleanupHook(fun, arg);
+}
+
+
CallbackScope::CallbackScope(Isolate* isolate,
Local<Object> object,
async_context asyncContext)
@@ -4435,7 +4451,7 @@ Environment* CreateEnvironment(IsolateData* isolate_data,
void FreeEnvironment(Environment* env) {
- env->CleanupHandles();
+ env->RunCleanup();
delete env;
}
@@ -4533,6 +4549,8 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data,
env.set_trace_sync_io(false);
const int exit_code = EmitExit(&env);
+
+ env.RunCleanup();
RunAtExit(&env);
v8_platform.DrainVMTasks(isolate);
diff --git a/src/node.h b/src/node.h
index 5a491c1abf..23e2e9995c 100644
--- a/src/node.h
+++ b/src/node.h
@@ -583,6 +583,19 @@ NODE_EXTERN void AddPromiseHook(v8::Isolate* isolate,
promise_hook_func fn,
void* arg);
+/* This is a lot like node::AtExit, except that the hooks added via this
+ * function are run before the AtExit ones and will always be registered
+ * for the current Environment instance.
+ * These functions are safe to use in an addon supporting multiple
+ * threads/isolates. */
+NODE_EXTERN void AddEnvironmentCleanupHook(v8::Isolate* isolate,
+ void (*fun)(void* arg),
+ void* arg);
+
+NODE_EXTERN void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
+ void (*fun)(void* arg),
+ void* arg);
+
/* 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 4878cf241e..d5437d70d9 100644
--- a/src/node_api.cc
+++ b/src/node_api.cc
@@ -902,6 +902,28 @@ void napi_module_register(napi_module* mod) {
node::node_module_register(nm);
}
+napi_status napi_add_env_cleanup_hook(napi_env env,
+ void (*fun)(void* arg),
+ void* arg) {
+ CHECK_ENV(env);
+ CHECK_ARG(env, fun);
+
+ node::AddEnvironmentCleanupHook(env->isolate, fun, arg);
+
+ return napi_ok;
+}
+
+napi_status napi_remove_env_cleanup_hook(napi_env env,
+ void (*fun)(void* arg),
+ void* arg) {
+ CHECK_ENV(env);
+ CHECK_ARG(env, fun);
+
+ node::RemoveEnvironmentCleanupHook(env->isolate, fun, arg);
+
+ return napi_ok;
+}
+
// Warning: Keep in-sync with napi_status enum
static
const char* error_messages[] = {nullptr,
diff --git a/src/node_api.h b/src/node_api.h
index b010d32db7..91c2775a03 100644
--- a/src/node_api.h
+++ b/src/node_api.h
@@ -118,6 +118,13 @@ EXTERN_C_START
NAPI_EXTERN void napi_module_register(napi_module* mod);
+NAPI_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env,
+ void (*fun)(void* arg),
+ void* arg);
+NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env,
+ void (*fun)(void* arg),
+ void* arg);
+
NAPI_EXTERN napi_status
napi_get_last_error_info(napi_env env,
const napi_extended_error_info** result);
diff --git a/test/addons-napi/test_cleanup_hook/binding.cc b/test/addons-napi/test_cleanup_hook/binding.cc
new file mode 100644
index 0000000000..66d53508c6
--- /dev/null
+++ b/test/addons-napi/test_cleanup_hook/binding.cc
@@ -0,0 +1,24 @@
+#include "node_api.h"
+#include "uv.h"
+#include "../common.h"
+
+namespace {
+
+void cleanup(void* arg) {
+ printf("cleanup(%d)\n", *static_cast<int*>(arg));
+}
+
+int secret = 42;
+int wrong_secret = 17;
+
+napi_value Init(napi_env env, napi_value exports) {
+ napi_add_env_cleanup_hook(env, cleanup, &wrong_secret);
+ napi_add_env_cleanup_hook(env, cleanup, &secret);
+ napi_remove_env_cleanup_hook(env, cleanup, &wrong_secret);
+
+ return nullptr;
+}
+
+} // anonymous namespace
+
+NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
diff --git a/test/addons-napi/test_cleanup_hook/binding.gyp b/test/addons-napi/test_cleanup_hook/binding.gyp
new file mode 100644
index 0000000000..7ede63d94a
--- /dev/null
+++ b/test/addons-napi/test_cleanup_hook/binding.gyp
@@ -0,0 +1,9 @@
+{
+ 'targets': [
+ {
+ 'target_name': 'binding',
+ 'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
+ 'sources': [ 'binding.cc' ]
+ }
+ ]
+}
diff --git a/test/addons-napi/test_cleanup_hook/test.js b/test/addons-napi/test_cleanup_hook/test.js
new file mode 100644
index 0000000000..354f444904
--- /dev/null
+++ b/test/addons-napi/test_cleanup_hook/test.js
@@ -0,0 +1,12 @@
+'use strict';
+const common = require('../../common');
+const assert = require('assert');
+const child_process = require('child_process');
+
+if (process.argv[2] === 'child') {
+ require(`./build/${common.buildType}/binding`);
+} else {
+ const { stdout } =
+ child_process.spawnSync(process.execPath, [__filename, 'child']);
+ assert.strictEqual(stdout.toString().trim(), 'cleanup(42)');
+}