Skip to content

Commit 85b37db

Browse files
committed
fs: add FileHandle object fd wrapper
The `node::fs::FileHandle` object wraps a file descriptor and will close it on garbage collection along with a process warning. The intent is to prevent (as much as possible) file descriptors from being leaked if the user does not close them explicitly. PR-URL: #18297 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 7154bc0 commit 85b37db

File tree

4 files changed

+277
-4
lines changed

4 files changed

+277
-4
lines changed

src/async_wrap.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ namespace node {
3636
#define NODE_ASYNC_NON_CRYPTO_PROVIDER_TYPES(V) \
3737
V(NONE) \
3838
V(DNSCHANNEL) \
39+
V(FILEHANDLE) \
40+
V(FILEHANDLECLOSEREQ) \
3941
V(FSEVENTWRAP) \
4042
V(FSREQWRAP) \
4143
V(FSREQPROMISE) \

src/env.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ class ModuleWrap;
283283
V(buffer_prototype_object, v8::Object) \
284284
V(context, v8::Context) \
285285
V(domain_callback, v8::Function) \
286+
V(fd_constructor_template, v8::ObjectTemplate) \
287+
V(fdclose_constructor_template, v8::ObjectTemplate) \
286288
V(host_import_module_dynamically_callback, v8::Function) \
287289
V(host_initialize_import_meta_object_callback, v8::Function) \
288290
V(http2ping_constructor_template, v8::ObjectTemplate) \

src/node_file.cc

Lines changed: 210 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ using v8::Local;
9797
using v8::MaybeLocal;
9898
using v8::Number;
9999
using v8::Object;
100+
using v8::ObjectTemplate;
100101
using v8::Promise;
101102
using v8::String;
102103
using v8::Undefined;
@@ -108,6 +109,150 @@ using v8::Value;
108109

109110
#define GET_OFFSET(a) ((a)->IsNumber() ? (a)->IntegerValue() : -1)
110111

112+
// The FileHandle object wraps a file descriptor and will close it on garbage
113+
// collection if necessary. If that happens, a process warning will be
114+
// emitted (or a fatal exception will occur if the fd cannot be closed.)
115+
FileHandle::FileHandle(Environment* env, int fd)
116+
: AsyncWrap(env,
117+
env->fd_constructor_template()
118+
->NewInstance(env->context()).ToLocalChecked(),
119+
AsyncWrap::PROVIDER_FILEHANDLE), fd_(fd) {
120+
MakeWeak<FileHandle>(this);
121+
v8::PropertyAttribute attr =
122+
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
123+
object()->DefineOwnProperty(env->context(),
124+
FIXED_ONE_BYTE_STRING(env->isolate(), "fd"),
125+
Integer::New(env->isolate(), fd),
126+
attr).FromJust();
127+
}
128+
129+
FileHandle::~FileHandle() {
130+
CHECK(!closing_); // We should not be deleting while explicitly closing!
131+
Close(); // Close synchronously and emit warning
132+
CHECK(closed_); // We have to be closed at the point
133+
CHECK(persistent().IsEmpty());
134+
}
135+
136+
137+
// Close the file descriptor if it hasn't already been closed. A process
138+
// warning will be emitted using a SetImmediate to avoid calling back to
139+
// JS during GC. If closing the fd fails at this point, a fatal exception
140+
// will crash the process immediately.
141+
inline void FileHandle::Close() {
142+
if (closed_) return;
143+
closed_ = true;
144+
uv_fs_t req;
145+
int ret = uv_fs_close(env()->event_loop(), &req, fd_, nullptr);
146+
uv_fs_req_cleanup(&req);
147+
148+
struct err_detail { int ret; int fd; };
149+
150+
err_detail* detail = new err_detail { ret, fd_ };
151+
152+
if (ret < 0) {
153+
// Do not unref this
154+
env()->SetImmediate([](Environment* env, void* data) {
155+
char msg[70];
156+
err_detail* detail = static_cast<err_detail*>(data);
157+
snprintf(msg, arraysize(msg),
158+
"Closing file descriptor %d on garbage collection failed",
159+
detail->fd);
160+
// This exception will end up being fatal for the process because
161+
// it is being thrown from within the SetImmediate handler and
162+
// there is no JS stack to bubble it to. In other words, tearing
163+
// down the process is the only reasonable thing we can do here.
164+
env->ThrowUVException(detail->ret, "close", msg);
165+
delete detail;
166+
}, detail);
167+
return;
168+
}
169+
170+
// If the close was successful, we still want to emit a process warning
171+
// to notify that the file descriptor was gc'd. We want to be noisy about
172+
// this because not explicitly closing the garbage collector is a bug.
173+
env()->SetUnrefImmediate([](Environment* env, void* data) {
174+
char msg[70];
175+
err_detail* detail = static_cast<err_detail*>(data);
176+
snprintf(msg, arraysize(msg),
177+
"Closing file descriptor %d on garbage collection",
178+
detail->fd);
179+
delete detail;
180+
ProcessEmitWarning(env, msg);
181+
}, detail);
182+
}
183+
184+
void FileHandle::CloseReq::Resolve() {
185+
InternalCallbackScope callback_scope(this);
186+
HandleScope scope(env()->isolate());
187+
Local<Promise> promise = promise_.Get(env()->isolate());
188+
Local<Promise::Resolver> resolver = promise.As<Promise::Resolver>();
189+
resolver->Resolve(env()->context(), Undefined(env()->isolate()));
190+
}
191+
192+
void FileHandle::CloseReq::Reject(Local<Value> reason) {
193+
InternalCallbackScope callback_scope(this);
194+
HandleScope scope(env()->isolate());
195+
Local<Promise> promise = promise_.Get(env()->isolate());
196+
Local<Promise::Resolver> resolver = promise.As<Promise::Resolver>();
197+
resolver->Reject(env()->context(), reason);
198+
}
199+
200+
FileHandle* FileHandle::CloseReq::fd() {
201+
HandleScope scope(env()->isolate());
202+
Local<Value> val = ref_.Get(env()->isolate());
203+
Local<Object> obj = val.As<Object>();
204+
return Unwrap<FileHandle>(obj);
205+
}
206+
207+
// Closes this FileHandle asynchronously and returns a Promise that will be
208+
// resolved when the callback is invoked, or rejects with a UVException if
209+
// there was a problem closing the fd. This is the preferred mechanism for
210+
// closing the FD object even tho the object will attempt to close
211+
// automatically on gc.
212+
inline Local<Promise> FileHandle::ClosePromise() {
213+
Isolate* isolate = env()->isolate();
214+
HandleScope scope(isolate);
215+
Local<Context> context = env()->context();
216+
auto maybe_resolver = Promise::Resolver::New(context);
217+
CHECK(!maybe_resolver.IsEmpty());
218+
Local<Promise::Resolver> resolver = maybe_resolver.ToLocalChecked();
219+
Local<Promise> promise = resolver.As<Promise>();
220+
if (!closed_ && !closing_) {
221+
closing_ = true;
222+
CloseReq* req = new CloseReq(env(), promise, object());
223+
auto AfterClose = [](uv_fs_t* req) {
224+
CloseReq* close = static_cast<CloseReq*>(req->data);
225+
CHECK_NE(close, nullptr);
226+
close->fd()->closing_ = false;
227+
Isolate* isolate = close->env()->isolate();
228+
if (req->result < 0) {
229+
close->Reject(UVException(isolate, req->result, "close"));
230+
} else {
231+
close->fd()->closed_ = true;
232+
close->Resolve();
233+
}
234+
delete close;
235+
};
236+
req->Dispatched();
237+
int ret = uv_fs_close(env()->event_loop(), req->req(), fd_, AfterClose);
238+
if (ret < 0) {
239+
req->Reject(UVException(isolate, ret, "close"));
240+
delete req;
241+
}
242+
} else {
243+
// Already closed. Just reject the promise immediately
244+
resolver->Reject(context, UVException(isolate, UV_EBADF, "close"));
245+
}
246+
return promise;
247+
}
248+
249+
void FileHandle::Close(const FunctionCallbackInfo<Value>& args) {
250+
FileHandle* fd;
251+
ASSIGN_OR_RETURN_UNWRAP(&fd, args.Holder());
252+
args.GetReturnValue().Set(fd->ClosePromise());
253+
}
254+
255+
111256
void FSReqWrap::Reject(Local<Value> reject) {
112257
MakeCallback(env()->oncomplete_string(), 1, &reject);
113258
}
@@ -142,8 +287,7 @@ FSReqPromise::FSReqPromise(Environment* env, Local<Object> req)
142287

143288
Local<ArrayBuffer> ab =
144289
ArrayBuffer::New(env->isolate(), statFields_,
145-
sizeof(double) * 14,
146-
v8::ArrayBufferCreationMode::kInternalized);
290+
sizeof(double) * 14);
147291
object()->Set(env->context(),
148292
env->statfields_string(),
149293
Float64Array::New(ab, 0, 14)).FromJust();
@@ -261,6 +405,16 @@ void AfterInteger(uv_fs_t* req) {
261405
req_wrap->Resolve(Integer::New(req_wrap->env()->isolate(), req->result));
262406
}
263407

408+
void AfterOpenFileHandle(uv_fs_t* req) {
409+
FSReqWrap* req_wrap = static_cast<FSReqWrap*>(req->data);
410+
FSReqAfterScope after(req_wrap, req);
411+
412+
if (after.Proceed()) {
413+
FileHandle* fd = new FileHandle(req_wrap->env(), req->result);
414+
req_wrap->Resolve(fd->object());
415+
}
416+
}
417+
264418
void AfterStringPath(uv_fs_t* req) {
265419
FSReqBase* req_wrap = static_cast<FSReqBase*>(req->data);
266420
FSReqAfterScope after(req_wrap, req);
@@ -969,6 +1123,7 @@ static void ReadDir(const FunctionCallbackInfo<Value>& args) {
9691123

9701124
static void Open(const FunctionCallbackInfo<Value>& args) {
9711125
Environment* env = Environment::GetCurrent(args);
1126+
Local<Context> context = env->context();
9721127

9731128
CHECK_GE(args.Length(), 3);
9741129
CHECK(args[1]->IsInt32());
@@ -977,8 +1132,8 @@ static void Open(const FunctionCallbackInfo<Value>& args) {
9771132
BufferValue path(env->isolate(), args[0]);
9781133
CHECK_NE(*path, nullptr);
9791134

980-
int flags = args[1]->Int32Value();
981-
int mode = static_cast<int>(args[2]->Int32Value());
1135+
int flags = args[1]->Int32Value(context).ToChecked();
1136+
int mode = args[2]->Int32Value(context).ToChecked();
9821137

9831138
if (args[3]->IsObject()) {
9841139
CHECK_EQ(args.Length(), 4);
@@ -990,6 +1145,35 @@ static void Open(const FunctionCallbackInfo<Value>& args) {
9901145
}
9911146
}
9921147

1148+
static void OpenFileHandle(const FunctionCallbackInfo<Value>& args) {
1149+
Environment* env = Environment::GetCurrent(args);
1150+
Local<Context> context = env->context();
1151+
1152+
CHECK_GE(args.Length(), 3);
1153+
CHECK(args[1]->IsInt32());
1154+
CHECK(args[2]->IsInt32());
1155+
1156+
BufferValue path(env->isolate(), args[0]);
1157+
CHECK_NE(*path, nullptr);
1158+
1159+
int flags = args[1]->Int32Value(context).ToChecked();
1160+
int mode = args[2]->Int32Value(context).ToChecked();
1161+
1162+
if (args[3]->IsObject()) {
1163+
CHECK_EQ(args.Length(), 4);
1164+
AsyncCall(env, args, "open", UTF8, AfterOpenFileHandle,
1165+
uv_fs_open, *path, flags, mode);
1166+
} else {
1167+
SYNC_CALL(open, *path, *path, flags, mode)
1168+
if (SYNC_RESULT < 0) {
1169+
args.GetReturnValue().Set(SYNC_RESULT);
1170+
} else {
1171+
HandleScope scope(env->isolate());
1172+
FileHandle* fd = new FileHandle(env, SYNC_RESULT);
1173+
args.GetReturnValue().Set(fd->object());
1174+
}
1175+
}
1176+
}
9931177

9941178
static void CopyFile(const FunctionCallbackInfo<Value>& args) {
9951179
Environment* env = Environment::GetCurrent(args);
@@ -1391,6 +1575,7 @@ void InitFs(Local<Object> target,
13911575
env->SetMethod(target, "access", Access);
13921576
env->SetMethod(target, "close", Close);
13931577
env->SetMethod(target, "open", Open);
1578+
env->SetMethod(target, "openFileHandle", OpenFileHandle);
13941579
env->SetMethod(target, "read", Read);
13951580
env->SetMethod(target, "fdatasync", Fdatasync);
13961581
env->SetMethod(target, "fsync", Fsync);
@@ -1452,6 +1637,27 @@ void InitFs(Local<Object> target,
14521637
FIXED_ONE_BYTE_STRING(env->isolate(), "FSReqPromise");
14531638
fpt->SetClassName(promiseString);
14541639
target->Set(context, promiseString, fpt->GetFunction()).FromJust();
1640+
1641+
// Create FunctionTemplate for FileHandle
1642+
Local<FunctionTemplate> fd = FunctionTemplate::New(env->isolate());
1643+
AsyncWrap::AddWrapMethods(env, fd);
1644+
env->SetProtoMethod(fd, "close", FileHandle::Close);
1645+
Local<ObjectTemplate> fdt = fd->InstanceTemplate();
1646+
fdt->SetInternalFieldCount(1);
1647+
Local<String> handleString =
1648+
FIXED_ONE_BYTE_STRING(env->isolate(), "FileHandle");
1649+
fd->SetClassName(handleString);
1650+
target->Set(context, handleString, fd->GetFunction()).FromJust();
1651+
env->set_fd_constructor_template(fdt);
1652+
1653+
// Create FunctionTemplate for FileHandle::CloseReq
1654+
Local<FunctionTemplate> fdclose = FunctionTemplate::New(env->isolate());
1655+
fdclose->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(),
1656+
"FileHandleCloseReq"));
1657+
AsyncWrap::AddWrapMethods(env, fdclose);
1658+
Local<ObjectTemplate> fdcloset = fdclose->InstanceTemplate();
1659+
fdcloset->SetInternalFieldCount(1);
1660+
env->set_fdclose_constructor_template(fdcloset);
14551661
}
14561662

14571663
} // namespace fs

src/node_file.h

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
namespace node {
1010

1111
using v8::Context;
12+
using v8::FunctionCallbackInfo;
1213
using v8::HandleScope;
1314
using v8::Local;
1415
using v8::Object;
16+
using v8::Persistent;
17+
using v8::Promise;
1518
using v8::Undefined;
1619
using v8::Value;
1720

@@ -112,6 +115,66 @@ class FSReqAfterScope {
112115
Context::Scope context_scope_;
113116
};
114117

118+
// A wrapper for a file descriptor that will automatically close the fd when
119+
// the object is garbage collected
120+
class FileHandle : public AsyncWrap {
121+
public:
122+
FileHandle(Environment* env, int fd);
123+
virtual ~FileHandle();
124+
125+
int fd() const { return fd_; }
126+
size_t self_size() const override { return sizeof(*this); }
127+
128+
// Will asynchronously close the FD and return a Promise that will
129+
// be resolved once closing is complete.
130+
static void Close(const FunctionCallbackInfo<Value>& args);
131+
132+
private:
133+
// Synchronous close that emits a warning
134+
inline void Close();
135+
136+
class CloseReq : public ReqWrap<uv_fs_t> {
137+
public:
138+
CloseReq(Environment* env,
139+
Local<Promise> promise,
140+
Local<Value> ref)
141+
: ReqWrap(env,
142+
env->fdclose_constructor_template()
143+
->NewInstance(env->context()).ToLocalChecked(),
144+
AsyncWrap::PROVIDER_FILEHANDLECLOSEREQ) {
145+
Wrap(object(), this);
146+
promise_.Reset(env->isolate(), promise);
147+
ref_.Reset(env->isolate(), ref);
148+
}
149+
~CloseReq() {
150+
uv_fs_req_cleanup(req());
151+
promise_.Empty();
152+
ref_.Empty();
153+
}
154+
155+
FileHandle* fd();
156+
157+
size_t self_size() const override { return sizeof(*this); }
158+
159+
void Resolve();
160+
161+
void Reject(Local<Value> reason);
162+
163+
private:
164+
Persistent<Promise> promise_;
165+
Persistent<Value> ref_;
166+
};
167+
168+
// Asynchronous close
169+
inline Local<Promise> ClosePromise();
170+
171+
int fd_;
172+
bool closing_ = false;
173+
bool closed_ = false;
174+
175+
DISALLOW_COPY_AND_ASSIGN(FileHandle);
176+
};
177+
115178
} // namespace fs
116179

117180
} // namespace node

0 commit comments

Comments
 (0)