mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
src: add environment cleanup hooks
This adds pairs of methods to the `Environment` class and to public APIs which can add and remove cleanup handlers. Unlike `AtExit`, this API targets addon developers rather than embedders, giving them (and Node’s internals) the ability to register per-`Environment` cleanup work. We may want to replace `AtExit` with this API at some point. Many thanks for Stephen Belanger for reviewing the original version of this commit in the Ayo.js project. Refs: https://github.com/ayojs/ayo/pull/82 PR-URL: https://github.com/nodejs/node/pull/19377 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) \
|
||||
|
||||
29
src/env.cc
29
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_);
|
||||
|
||||
31
src/env.h
31
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);
|
||||
|
||||
20
src/node.cc
20
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);
|
||||
|
||||
13
src/node.h
13
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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
24
test/addons-napi/test_cleanup_hook/binding.cc
Normal file
24
test/addons-napi/test_cleanup_hook/binding.cc
Normal file
@@ -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)
|
||||
9
test/addons-napi/test_cleanup_hook/binding.gyp
Normal file
9
test/addons-napi/test_cleanup_hook/binding.gyp
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
'targets': [
|
||||
{
|
||||
'target_name': 'binding',
|
||||
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
|
||||
'sources': [ 'binding.cc' ]
|
||||
}
|
||||
]
|
||||
}
|
||||
12
test/addons-napi/test_cleanup_hook/test.js
Normal file
12
test/addons-napi/test_cleanup_hook/test.js
Normal file
@@ -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)');
|
||||
}
|
||||
Reference in New Issue
Block a user