Skip to content

Commit 39e8ae5

Browse files
committed
fs: runtime deprecate closing fs.Dir on garbage collection
1 parent dbd24b1 commit 39e8ae5

File tree

5 files changed

+105
-5
lines changed

5 files changed

+105
-5
lines changed

doc/api/deprecations.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4058,6 +4058,45 @@ Type: Documentation-only
40584058

40594059
The [`util.types.isNativeError`][] API is deprecated. Please use [`Error.isError`][] instead.
40604060

4061+
### DEP0198: Closing fs.Dir on garbage collection
4062+
4063+
<!-- YAML
4064+
changes:
4065+
- version: REPLACEME
4066+
pr-url: https://github.com/nodejs/node/pull/58850
4067+
description: Runtime deprecation.
4068+
-->
4069+
4070+
Type: Runtime
4071+
4072+
Allowing a [`fs.Dir`][] object to be closed on garbage collection is
4073+
deprecated. In the future, doing so might result in a thrown error that will
4074+
terminate the process.
4075+
4076+
Please ensure that all `fs.Dir` objects are explicitly closed using
4077+
`Dir.prototype.close()` or `using` keyword:
4078+
4079+
```mjs
4080+
import { opendir } from 'node:fs/promises';
4081+
4082+
{
4083+
await using dir = await opendir('/async/disposable/directory');
4084+
} // Closed by dir[Symbol.asyncDispose]()
4085+
4086+
{
4087+
using dir = await opendir('/sync/disposable/directory');
4088+
} // Closed by dir[Symbol.dispose]()
4089+
4090+
{
4091+
let dir;
4092+
try {
4093+
dir = await opendir('/legacy/closeable/directory');
4094+
} finally {
4095+
await dir?.close();
4096+
}
4097+
}
4098+
```
4099+
40614100
[DEP0142]: #dep0142-repl_builtinlibs
40624101
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
40634102
[RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3
@@ -4115,6 +4154,7 @@ The [`util.types.isNativeError`][] API is deprecated. Please use [`Error.isError
41154154
[`ecdh.setPublicKey()`]: crypto.md#ecdhsetpublickeypublickey-encoding
41164155
[`emitter.listenerCount(eventName)`]: events.md#emitterlistenercounteventname-listener
41174156
[`events.listenerCount(emitter, eventName)`]: events.md#eventslistenercountemitter-eventname
4157+
[`fs.Dir`]: fs.md#class-fsdir
41184158
[`fs.FileHandle`]: fs.md#class-filehandle
41194159
[`fs.access()`]: fs.md#fsaccesspath-mode-callback
41204160
[`fs.appendFile()`]: fs.md#fsappendfilepath-data-options-callback

src/env-inl.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,14 @@ inline void Environment::RemoveHeapSnapshotNearHeapLimitCallback(
912912
heap_limit);
913913
}
914914

915+
bool Environment::dir_gc_close_warning() const {
916+
return emit_dir_gc_warning_;
917+
}
918+
919+
void Environment::set_dir_gc_close_warning(bool on) {
920+
emit_dir_gc_warning_ = on;
921+
}
922+
915923
} // namespace node
916924

917925
// 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
@@ -822,6 +822,9 @@ class Environment final : public MemoryRetainer {
822822
inline node_module* extra_linked_bindings_tail();
823823
inline const Mutex& extra_linked_bindings_mutex() const;
824824

825+
inline bool dir_gc_close_warning() const;
826+
inline void set_dir_gc_close_warning(bool on);
827+
825828
inline void set_source_maps_enabled(bool on);
826829
inline bool source_maps_enabled() const;
827830

@@ -1101,6 +1104,7 @@ class Environment final : public MemoryRetainer {
11011104
std::shared_ptr<KVStore> env_vars_;
11021105
bool printed_error_ = false;
11031106
bool trace_sync_io_ = false;
1107+
bool emit_dir_gc_warning_ = true;
11041108
bool emit_env_nonstring_warning_ = true;
11051109
bool emit_err_name_warning_ = true;
11061110
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+
"DEP0198"));
185+
}
186+
},
187+
CallbackFlags::kUnrefed);
174188
}
175189

176190
void AfterClose(uv_fs_t* req) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 { opendir } from 'node:fs/promises';
10+
import { setImmediate } from 'node:timers';
11+
12+
const warning =
13+
'Closing a Dir object on garbage collection is deprecated. ' +
14+
'Please close Dir objects explicitly using Dir.prototype.close(), ' +
15+
"or by using explicit resource management ('using' keyword). " +
16+
'In the future, an error will be thrown if a directory is closed during garbage collection.';
17+
18+
{
19+
expectWarning({
20+
Warning: [['Closing directory handle on garbage collection']],
21+
DeprecationWarning: [[warning, 'DEP0198']],
22+
});
23+
24+
await opendir(import.meta.dirname).then(mustCall(() => {
25+
setImmediate(() => {
26+
// The dir is out of scope now
27+
globalThis.gc();
28+
29+
// Wait an extra event loop turn, as the warning is emitted from the
30+
// native layer in an unref()'ed setImmediate() callback.
31+
setImmediate(mustCall());
32+
});
33+
}));
34+
}

0 commit comments

Comments
 (0)