Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,46 @@ Error: Cannot load native addon because loading addons is disabled.
}
```

### `--enable-async-hooks`

<!-- YAML
added: REPLACEME
-->

Enable use of `async_hooks.createHook()`. This flag is required to enable
the `createHook` API, which is disabled by default.

Example:

```mjs
import { createHook } from 'node:async_hooks';

createHook({ init() {} }).enable();
```

```cjs
const { createHook } = require('node:async_hooks');

createHook({ init() {} }).enable();
```

```console
$ node index.js
node:async_hooks:160
throw new ERR_ASYNC_HOOKS_CREATE_HOOK_DISABLED();
^

Error [ERR_ASYNC_HOOKS_CREATE_HOOK_DISABLED]: async_hooks.createHook() is disabled. Use --enable-async-hooks to enable it.
at createHook (node:async_hooks:160:11)
at Object.<anonymous> (/home/user/index.js:3:1)
...
```

```console
$ node --enable-async-hooks index.js
# Works without error
```

### `--allow-child-process`

<!-- YAML
Expand Down Expand Up @@ -4268,6 +4308,7 @@ one is included in the list below.
<!-- node-options-node start -->

* `--allow-addons`
* `--enable-async-hooks`
* `--allow-child-process`
* `--allow-env`
* `--allow-ffi`
Expand Down
13 changes: 13 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,17 @@ by the `node:assert` module.
An attempt was made to register something that is not a function as an
`AsyncHooks` callback.

<a id="ERR_ASYNC_HOOKS_CREATE_HOOK_DISABLED"></a>

### `ERR_ASYNC_HOOKS_CREATE_HOOK_DISABLED`

<!-- YAML
added: REPLACEME
-->

An attempt was made to call [`async_hooks.createHook()`][] without the
[`--enable-async-hooks`][] flag.

<a id="ERR_ASYNC_LOADER_REQUEST_NEVER_SETTLED"></a>

### `ERR_ASYNC_LOADER_REQUEST_NEVER_SETTLED`
Expand Down Expand Up @@ -4773,6 +4784,7 @@ An error occurred trying to allocate memory. This should never happen.
[`"exports"`]: packages.md#exports
[`"imports"`]: packages.md#imports
[`'uncaughtException'`]: process.md#event-uncaughtexception
[`--enable-async-hooks`]: cli.md#--enable-async-hooks
[`--disable-proto=throw`]: cli.md#--disable-protomode
[`--force-fips`]: cli.md#--force-fips
[`--no-addons`]: cli.md#--no-addons
Expand All @@ -4798,6 +4810,7 @@ An error occurred trying to allocate memory. This should never happen.
[`Writable`]: stream.md#class-streamwritable
[`ZipBuffer`]: zlib.md#class-zlibzipbuffer
[`ZipFile`]: zlib.md#class-zlibzipfile
[`async_hooks.createHook()`]: async_hooks.md#async_hookscreatehookoptions
[`child_process`]: child_process.md
[`cipher.getAuthTag()`]: crypto.md#ciphergetauthtag
[`crypto.getDiffieHellman()`]: crypto.md#cryptogetdiffiehellmangroupname
Expand Down
32 changes: 32 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,36 @@ Error: Cannot load native addon because loading addons is disabled.
}
.Ed
.
.It Fl -enable-async-hooks
Enable use of \fBasync_hooks.createHook()\fR. This flag is required to enable
the \fBcreateHook\fR API, which is disabled by default.
Example:
.Bd -literal
import { createHook } from 'node:async_hooks';

createHook({ init() {} }).enable();
.Ed
.Bd -literal
const { createHook } = require('node:async_hooks');

createHook({ init() {} }).enable();
.Ed
.Bd -literal
$ node index.js
node:async_hooks:160
throw new ERR_ASYNC_HOOKS_CREATE_HOOK_DISABLED();
^

Error [ERR_ASYNC_HOOKS_CREATE_HOOK_DISABLED]: async_hooks.createHook() is disabled. Use --enable-async-hooks to enable it.
at createHook (node:async_hooks:160:11)
at Object.<anonymous> (/home/user/index.js:3:1)
...
.Ed
.Bd -literal
$ node --enable-async-hooks index.js
# Works without error
.Ed
.
.It Fl -allow-child-process
When using the Permission Model, the process will not be able to spawn any
child process by default.
Expand Down Expand Up @@ -2189,6 +2219,8 @@ one is included in the list below.
.It
\fB--allow-addons\fR
.It
\fB--enable-async-hooks\fR
.It
\fB--allow-child-process\fR
.It
\fB--allow-env\fR
Expand Down
5 changes: 5 additions & 0 deletions lib/async_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ const {

const {
ERR_ASYNC_CALLBACK,
ERR_ASYNC_HOOKS_CREATE_HOOK_DISABLED,
ERR_ASYNC_RESOURCE_DOMAIN_REMOVED,
ERR_ASYNC_TYPE,
ERR_INVALID_ASYNC_ID,
ERR_INVALID_ARG_VALUE,
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
const {
kEmptyObject,
} = require('internal/util');
Expand Down Expand Up @@ -168,6 +170,9 @@ class AsyncHook {


function createHook(fns) {
if (!getOptionValue('--enable-async-hooks')) {
throw new ERR_ASYNC_HOOKS_CREATE_HOOK_DISABLED();
}
return new AsyncHook(fns);
}

Expand Down
79 changes: 79 additions & 0 deletions lib/internal/async_hooks.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
'use strict';

const {
ArrayPrototypeIncludes,
ArrayPrototypeIndexOf,
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
ErrorCaptureStackTrace,
ObjectDefineProperty,
ObjectPrototypeHasOwnProperty,
Expand All @@ -10,6 +14,7 @@ const {
} = primordials;

const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const { ERR_ASYNC_CALLBACK } = require('internal/errors').codes;

const async_wrap = internalBinding('async_wrap');
const { setCallbackTrampoline } = async_wrap;
Expand Down Expand Up @@ -576,9 +581,83 @@ function triggerAsyncId() {

const kNoPromiseHook = Symbol('kNoPromiseHook');

// Minimal internal async hook used by Node.js internals (e.g. the inspector)
// that must create hooks without --enable-async-hooks. Unlike the public
// async_hooks.createHook() this does not support promise callbacks.
class AsyncHook {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joyeecheung this is what I did to decouple the inspector from the public API. I think this can be simplified much further.

constructor({ init, before, after, destroy }) {
if (init !== undefined && typeof init !== 'function')
throw new ERR_ASYNC_CALLBACK('hook.init');
if (before !== undefined && typeof before !== 'function')
throw new ERR_ASYNC_CALLBACK('hook.before');
if (after !== undefined && typeof after !== 'function')
throw new ERR_ASYNC_CALLBACK('hook.after');
if (destroy !== undefined && typeof destroy !== 'function')
throw new ERR_ASYNC_CALLBACK('hook.destroy');

// Internal hooks never track promises.
this[kNoPromiseHook] = true;
this[init_symbol] = init;
this[before_symbol] = before;
this[after_symbol] = after;
this[destroy_symbol] = destroy;
}

enable() {
const { 0: hooks_array, 1: hook_fields } = getHookArrays();

if (ArrayPrototypeIncludes(hooks_array, this))
return this;

const prev_kTotals = hook_fields[kTotals];

hook_fields[kTotals] = hook_fields[kInit] += +!!this[init_symbol];
hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol];
hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol];
hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol];
ArrayPrototypePush(hooks_array, this);

if (prev_kTotals === 0 && hook_fields[kTotals] > 0) {
enableHooks();
}

return this;
}

disable() {
const { 0: hooks_array, 1: hook_fields } = getHookArrays();

const index = ArrayPrototypeIndexOf(hooks_array, this);
if (index === -1)
return this;

const prev_kTotals = hook_fields[kTotals];

hook_fields[kTotals] = hook_fields[kInit] -= +!!this[init_symbol];
hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol];
hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol];
hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol];
ArrayPrototypeSplice(hooks_array, index, 1);

if (prev_kTotals > 0 && hook_fields[kTotals] === 0) {
disableHooks();
}

return this;
}
}

// Creates an internal AsyncHook without honoring --enable-async-hooks. Only for
// use by Node.js internals; userland goes through async_hooks.createHook().
function createHook(fns) {
return new AsyncHook(fns);
}

module.exports = {
executionAsyncId,
triggerAsyncId,
// Create an async hook without honoring --enable-async-hooks (internals only).
createHook,
// Private API
getHookArrays,
symbols: {
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,8 @@ E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError);
E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError);
E('ERR_ASSERTION', '%s', Error);
E('ERR_ASYNC_CALLBACK', '%s must be a function', TypeError);
E('ERR_ASYNC_HOOKS_CREATE_HOOK_DISABLED',
'async_hooks.createHook() is disabled. Use --enable-async-hooks to enable it.', Error);
E('ERR_ASYNC_LOADER_REQUEST_NEVER_SETTLED',
'Async loader request never settled', Error);
E('ERR_ASYNC_RESOURCE_DOMAIN_REMOVED',
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/inspector_async_hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ let config;

function lazyHookCreation() {
const inspector = internalBinding('inspector');
const { createHook } = require('async_hooks');
const { createHook } = require('internal/async_hooks');
config = internalBinding('config');

hook = createHook({
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
kAllowedInEnvvar,
false,
OptionNamespaces::kPermissionNamespace);
AddOption("--enable-async-hooks",
"enable use of async_hooks.createHook",
BOOL_FIELD(enable_async_hooks),
kAllowedInEnvvar);
AddOption("--allow-openssl-store",
"allow use of OpenSSL STORE loaders when any permissions are set",
BOOL_FIELD(allow_openssl_store),
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ class EnvironmentOptions : public Options {
DEFINE_BOOL_FIELD(allow_ffi) = false;
DEFINE_BOOL_FIELD(allow_openssl_store) = false;
DEFINE_BOOL_FIELD(allow_worker_threads) = false;
DEFINE_BOOL_FIELD(enable_async_hooks) = false;
DEFINE_BOOL_FIELD(experimental_vm_modules) = EXPERIMENTALS_DEFAULT_VALUE;
DEFINE_BOOL_FIELD(expose_internals) = false;
DEFINE_BOOL_FIELD(force_node_api_uncaught_exceptions_policy) = false;
Expand Down
1 change: 1 addition & 0 deletions test/addons/async-resource/test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --enable-async-hooks

const common = require('../../common');
const assert = require('assert');
Expand Down
1 change: 1 addition & 0 deletions test/addons/callback-scope/test-async-hooks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --enable-async-hooks

const common = require('../../common');
const assert = require('assert');
Expand Down
2 changes: 1 addition & 1 deletion test/async-hooks/init-hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
// Flags: --expose-gc
// Flags: --enable-async-hooks --expose-gc

require('../common');
const assert = require('assert');
Expand Down
1 change: 1 addition & 0 deletions test/async-hooks/test-async-await.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --enable-async-hooks
const common = require('../common');

// This test ensures async hooks are being properly called
Expand Down
1 change: 1 addition & 0 deletions test/async-hooks/test-async-exec-resource-http-32060.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --enable-async-hooks
const common = require('../common');
const assert = require('assert');
const {
Expand Down
1 change: 1 addition & 0 deletions test/async-hooks/test-async-exec-resource-http-agent.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --enable-async-hooks

const common = require('../common');

Expand Down
1 change: 1 addition & 0 deletions test/async-hooks/test-async-exec-resource-http.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --enable-async-hooks

const common = require('../common');
const assert = require('assert');
Expand Down
1 change: 1 addition & 0 deletions test/async-hooks/test-async-exec-resource-match.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --enable-async-hooks

const common = require('../common');
const assert = require('assert');
Expand Down
2 changes: 1 addition & 1 deletion test/async-hooks/test-async-local-storage-gcable.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
// Flags: --expose_gc
// Flags: --enable-async-hooks --expose_gc

// This test ensures that AsyncLocalStorage gets gced once it was disabled
// and no strong references remain in userland.
Expand Down
9 changes: 5 additions & 4 deletions test/async-hooks/test-callback-error.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --enable-async-hooks
const common = require('../common');
const assert = require('assert');
const { spawnSync } = require('child_process');
Expand Down Expand Up @@ -37,7 +38,7 @@ assert.ok(!arg);
// console.log should stay until this test's flakiness is solved
console.log('start case 1');
console.time('end case 1');
const child = spawnSync(process.execPath, [__filename, 'test_init_callback']);
const child = spawnSync(process.execPath, ['--enable-async-hooks', __filename, 'test_init_callback']);
assert.ifError(child.error);
const test_init_first_line = child.stderr.toString().split(/[\r\n]+/g)[0];
assert.strictEqual(test_init_first_line, 'Error: test_init_callback');
Expand All @@ -48,7 +49,7 @@ assert.ok(!arg);
{
console.log('start case 2');
console.time('end case 2');
const child = spawnSync(process.execPath, [__filename, 'test_callback']);
const child = spawnSync(process.execPath, ['--enable-async-hooks', __filename, 'test_callback']);
assert.ifError(child.error);
const test_callback_first_line = child.stderr.toString().split(/[\r\n]+/g)[0];
assert.strictEqual(test_callback_first_line, 'Error: test_callback');
Expand All @@ -61,10 +62,10 @@ assert.ok(!arg);
console.time('end case 3');
let program = process.execPath;
let args = [
'--abort-on-uncaught-exception', __filename, 'test_callback_abort' ];
'--enable-async-hooks', '--abort-on-uncaught-exception', __filename, 'test_callback_abort' ];
let options = {};
if (!common.isWindows) {
[program, options] = common.escapePOSIXShell`ulimit -c 0 && exec "${program}" ${args[0]} "${args[1]}" ${args[2]}`;
[program, options] = common.escapePOSIXShell`ulimit -c 0 && exec "${program}" ${args[0]} ${args[1]} "${args[2]}" ${args[3]}`;
args = [];
options.shell = true;
}
Expand Down
1 change: 1 addition & 0 deletions test/async-hooks/test-crypto-pbkdf2.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --enable-async-hooks

const common = require('../common');
if (!common.hasCrypto) {
Expand Down
Loading
Loading