Skip to content

Commit c60424b

Browse files
committed
fs: runtime deprecate closing fs.Dir on garbage collection
1 parent 8ec29f2 commit c60424b

File tree

5 files changed

+112
-5
lines changed

5 files changed

+112
-5
lines changed

doc/api/deprecations.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4104,6 +4104,45 @@ The `node:_http_agent`, `node:_http_client`, `node:_http_common`, `node:_http_in
41044104
`node:_http_outgoing` and `node:_http_server` modules are deprecated as they should be considered
41054105
an internal nodejs implementation rather than a public facing API, use `node:http` instead.
41064106

4107+
### DEP0200: Closing fs.Dir on garbage collection
4108+
4109+
<!-- YAML
4110+
changes:
4111+
- version: REPLACEME
4112+
pr-url: https://github.com/nodejs/node/pull/58850
4113+
description: Runtime deprecation.
4114+
-->
4115+
4116+
Type: Runtime
4117+
4118+
Allowing a [`fs.Dir`][] object to be closed on garbage collection is
4119+
deprecated. In the future, doing so might result in a thrown error that will
4120+
terminate the process.
4121+
4122+
Please ensure that all `fs.Dir` objects are explicitly closed using
4123+
`Dir.prototype.close()` or `using` keyword:
4124+
4125+
```mjs
4126+
import { opendir } from 'node:fs/promises';
4127+
4128+
{
4129+
await using dir = await opendir('/async/disposable/directory');
4130+
} // Closed by dir[Symbol.asyncDispose]()
4131+
4132+
{
4133+
using dir = await opendir('/sync/disposable/directory');
4134+
} // Closed by dir[Symbol.dispose]()
4135+
4136+
{
4137+
let dir;
4138+
try {
4139+
dir = await opendir('/legacy/closeable/directory');
4140+
} finally {
4141+
await dir?.close();
4142+
}
4143+
}
4144+
```
4145+
41074146
[DEP0142]: #dep0142-repl_builtinlibs
41084147
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
41094148
[RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3

src/env-inl.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,14 @@ inline void Environment::RemoveHeapSnapshotNearHeapLimitCallback(
918918
heap_limit);
919919
}
920920

921+
bool Environment::dir_gc_close_warning() const {
922+
return emit_dir_gc_warning_;
923+
}
924+
925+
void Environment::set_dir_gc_close_warning(bool on) {
926+
emit_dir_gc_warning_ = on;
927+
}
928+
921929
} // namespace node
922930

923931
// These two files depend on each other. Including base_object-inl.h after this

src/env.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,9 @@ class Environment final : public MemoryRetainer {
820820
inline node_module* extra_linked_bindings_tail();
821821
inline const Mutex& extra_linked_bindings_mutex() const;
822822

823+
inline bool dir_gc_close_warning() const;
824+
inline void set_dir_gc_close_warning(bool on);
825+
823826
inline void set_source_maps_enabled(bool on);
824827
inline bool source_maps_enabled() const;
825828

@@ -1102,6 +1105,7 @@ class Environment final : public MemoryRetainer {
11021105
std::shared_ptr<KVStore> env_vars_;
11031106
bool printed_error_ = false;
11041107
bool trace_sync_io_ = false;
1108+
bool emit_dir_gc_warning_ = true;
11051109
bool emit_env_nonstring_warning_ = true;
11061110
bool emit_err_name_warning_ = true;
11071111
bool source_maps_enabled_ = false;

src/node_dir.cc

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,27 @@ inline void DirHandle::GCClose() {
164164
}
165165

166166
// If the close was successful, we still want to emit a process warning
167-
// to notify that the file descriptor was gc'd. We want to be noisy about
167+
// to notify that the directory handle was gc'd. We want to be noisy about
168168
// this because not explicitly closing the DirHandle is a bug.
169169

170-
env()->SetImmediate([](Environment* env) {
171-
ProcessEmitWarning(env,
172-
"Closing directory handle on garbage collection");
173-
}, CallbackFlags::kUnrefed);
170+
env()->SetImmediate(
171+
[](Environment* env) {
172+
ProcessEmitWarning(env,
173+
"Closing directory handle on garbage collection");
174+
if (env->dir_gc_close_warning()) {
175+
env->set_dir_gc_close_warning(false);
176+
USE(ProcessEmitDeprecationWarning(
177+
env,
178+
"Closing a Dir object on garbage collection is deprecated. "
179+
"Please close Dir objects explicitly using "
180+
"Dir.prototype.close(), "
181+
"or by using explicit resource management ('using' keyword). "
182+
"In the future, an error will be thrown if a directory is closed "
183+
"during garbage collection.",
184+
"DEP0200"));
185+
}
186+
},
187+
CallbackFlags::kUnrefed);
174188
}
175189

176190
void AfterClose(uv_fs_t* req) {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Flags: --expose-gc --no-warnings
2+
3+
// Test that a runtime warning is emitted when a Dir object
4+
// is allowed to close on garbage collection. In the future, this
5+
// test should verify that closing on garbage collection throws a
6+
// process fatal exception.
7+
8+
import { expectWarning, mustCall } from '../common/index.mjs';
9+
import { rejects } from 'node:assert';
10+
import { opendir } from 'node:fs/promises';
11+
import { setImmediate } from 'node:timers';
12+
13+
const warning =
14+
'Closing a Dir object on garbage collection is deprecated. ' +
15+
'Please close Dir objects explicitly using Dir.prototype.close(), ' +
16+
"or by using explicit resource management ('using' keyword). " +
17+
'In the future, an error will be thrown if a directory is closed during garbage collection.';
18+
19+
// Test that closing Dir automatically using iterator doesn't emit warning
20+
{
21+
const dir = await opendir(import.meta.dirname);
22+
for await (const _ of dir) {}
23+
await rejects(dir.close(), { code: 'ERR_DIR_CLOSED' });
24+
}
25+
26+
{
27+
expectWarning({
28+
Warning: [['Closing directory handle on garbage collection']],
29+
DeprecationWarning: [[warning, 'DEP0200']],
30+
});
31+
32+
await opendir(import.meta.dirname).then(mustCall(() => {
33+
setImmediate(() => {
34+
// The dir is out of scope now
35+
globalThis.gc();
36+
37+
// Wait an extra event loop turn, as the warning is emitted from the
38+
// native layer in an unref()'ed setImmediate() callback.
39+
setImmediate(mustCall());
40+
});
41+
}));
42+
}

0 commit comments

Comments
 (0)