diff --git a/lib/fs.js b/lib/fs.js index 9554620a040a3a..aab963183a3557 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -37,7 +37,7 @@ const { Buffer } = require('buffer'); const errors = require('internal/errors'); const { Readable, Writable } = require('stream'); const EventEmitter = require('events'); -const { FSReqWrap } = binding; +const { FSReqWrap, FSReqPromise } = binding; const { FSEvent } = process.binding('fs_event_wrap'); const internalFS = require('internal/fs'); const { getPathFromURL } = require('internal/url'); @@ -259,13 +259,13 @@ Stats.prototype.isSocket = function() { const statValues = binding.getStatValues(); -function statsFromValues() { - return new Stats(statValues[0], statValues[1], statValues[2], statValues[3], - statValues[4], statValues[5], - statValues[6] < 0 ? undefined : statValues[6], statValues[7], - statValues[8], statValues[9] < 0 ? undefined : statValues[9], - statValues[10], statValues[11], statValues[12], - statValues[13]); +function statsFromValues(values = statValues) { + return new Stats(values[0], values[1], values[2], values[3], + values[4], values[5], + values[6] < 0 ? undefined : values[6], values[7], + values[8], values[9] < 0 ? undefined : values[9], + values[10], values[11], values[12], + values[13]); } // Don't allow mode to accidentally be overwritten. @@ -2479,3 +2479,457 @@ Object.defineProperty(fs, 'SyncWriteStream', { set: internalUtil.deprecate((val) => { SyncWriteStream = val; }, 'fs.SyncWriteStream is deprecated.', 'DEP0061') }); + + +// The Promises API + +async function* readEverything(fd) { + let eof = false; + while (!eof) { + const buffer = Buffer.allocUnsafe(kReadFileBufferLength); + const { length } = + await fs.promises.read(fd, buffer, 0, buffer.length); + if (length < buffer.length) + eof = true; + yield buffer.slice(0, length); + } +} + + +async function writeEverything(fd, buffer, offset, length, position) { + const { written } = + await fs.promises.write(fd, buffer, offset, length, position); + if (written !== length) { + offset += written; + length -= written; + if (position !== null) + position += written; + return writeEverything(fd, buffer, offset, length, position); + } +} + +const promises = { + async access(path, mode = fs.F_OK) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + + if (typeof path !== 'string' && !(path instanceof Buffer)) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'path', + ['string', 'Buffer', 'URL']); + } + + mode = mode | 0; + const req = new FSReqPromise(); + binding.access(pathModule.toNamespacedPath(path), mode, req); + return req.promise; + }, + + async appendFile(path, data, options) { + callback = maybeCallback(callback || options); + options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); + options = copyObject(options); + if (!options.flag || isFd(path)) + options.flag = 'a'; + return fs.promises.writeFile(path, data, options); + }, + + async chown(path, uid, gid) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.chown(pathModule.toNamespacedPath(path), uid, gid, req); + return req.promise; + }, + + async chmod(path, mode) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.chmod(pathModule.toNamespacedPath(path), + modeNum(mode), + req); + return req.promise; + }, + + async copyFile(src, dest, flags = 0) { + handleError((src = getPathFromURL(src))); + handleError((dest = getPathFromURL(dest))); + nullCheck(src); + nullCheck(dest); + src = pathModule.toNamespacedPath(src); + dest = pathModule.toNamespacedPath(dest); + flags = flags | 0; + const req = new FSReqPromise(); + binding.copyFile(src, dest, flags, req); + return req.promise; + }, + + async fchmod(fd, mode) { + mode = modeNum(mode); + if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); + const fdnum = fd.fd; + if (fdnum < 0 || fdnum > 0xFFFFFFFF) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'fd'); + if (!Number.isInteger(mode)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'mode', 'number'); + if (mode < 0 || mode > 0o777) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'mode'); + + const req = new FSReqPromise(); + binding.fchmod(fd, mode, req); + return req.promise; + }, + + async fdatasync(fd) { + if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); + const fdnum = fd.fd; + if (fdnum < 0 || fdnum > 0xFFFFFFFF) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'fd'); + const req = new FSReqPromise(); + binding.fdatasync(fdnum, req); + return req.promise; + }, + + async fchown(fd, uid, gid) { + if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); + const fdnum = fd.fd; + if (fdnum < 0 || fdnum > 0xFFFFFFFF) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'fd'); + if (!Number.isInteger(uid)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'uid', 'number'); + if (uid < 0) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'uid'); + if (!Number.isInteger(gid)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'gid', 'number'); + if (gid < 0) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'gid'); + + const req = new FSReqPromise(); + binding.fchown(fdnum, uid, gid, req); + return req.promise; + }, + + async fstat(fd) { + if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); + const fdnum = fd.fd; + if (fdnum < 0 || fdnum > 0xFFFFFFFF) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'fd'); + const req = new FSReqPromise(); + binding.fstat(fdnum, req); + return statsFromValues(await req.promise); + }, + + async fsync(fd) { + if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); + const fdnum = fd.fd; + if (fdnum < 0 || fdnum > 0xFFFFFFFF) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'fd'); + const req = new FSReqPromise(); + binding.fsync(fdnum, req); + return req.promise; + }, + + async ftruncate(fd, len = 0) { + if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); + const fdnum = fd.fd; + if (fdnum < 0 || fdnum > 0xFFFFFFFF) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'fd'); + if (!Number.isInteger(len)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'len', 'number'); + len = Math.max(0, len); + const req = new FSReqPromise(); + binding.ftruncate(fdnum, len, req); + return req.promise; + }, + + async futimes(fd, atime, mtime) { + if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); + const fdnum = fd.fd; + if (fdnum < 0 || fdnum > 0xFFFFFFFF) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'fd'); + atime = toUnixTimestamp(atime, 'atime'); + mtime = toUnixTimestamp(mtime, 'mtime'); + const req = new FSReqPromise(); + binding.futimes(fd, atime, mtime, req); + return req.promise; + }, + + async link(existingPath, newPath) { + handleError((existingPath = getPathFromURL(existingPath))); + handleError((newPath = getPathFromURL(newPath))); + nullCheck(existingPath); + nullCheck(newPath); + + const req = new FSReqPromise(); + binding.link(pathModule.toNamespacedPath(existingPath), + pathModule.toNamespacedPath(newPath), + req); + return req.promise; + }, + + async lstat(path) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.lstat(pathModule.toNamespacedPath(path), req); + return statsFromValues(await req.promise); + }, + + async mkdir(path, mode) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.mkdir(pathModule.toNamespacedPath(path), + modeNum(mode, 0o777), + req); + return req.promise; + }, + + async mkdtemp(prefix, options) { + options = getOptions(options, {}); + if (!prefix || typeof prefix !== 'string') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'prefix', + 'string', + prefix); + } + nullCheck(prefix); + const req = new FSReqPromise(); + binding.mkdtemp(`${prefix}XXXXXX`, options.encoding, req); + return req.promise; + }, + + async open(path, flags, mode) { + mode = modeNum(mode, 0o666); + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.openFD(pathModule.toNamespacedPath(path), + stringToFlags(flags), + mode, + req); + return req.promise; + }, + + async read(fd, buffer, offset, length, position) { + if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); + const fdnum = fd.fd; + if (fdnum < 0 || fdnum > 0xFFFFFFFF) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'fd'); + if (!isUint8Array(buffer)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'buffer', + ['Buffer', 'Uint8Array']); + + offset |= 0; + length |= 0; + + if (length === 0) + return { length: 0, buffer }; + + if (offset < 0 || offset >= buffer.length) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'offset'); + + if (length < 0 || offset + length > buffer.length) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'length'); + + if (!Number.isInteger(position)) + position = -1; + + const req = new FSReqPromise(); + binding.read(fdnum, buffer, offset, length, position, req); + const bytesRead = await req.promise; + return { length: bytesRead, buffer }; + }, + + async readFile(path, options) { + options = getOptions(options, { flag: 'r' }); + + if (typeof path === 'object' && Number.isInteger(path.fd)) + return readFd(path); + + return readFd(await fs.promises.open(path, options.flag, 0o666)); + + async function readFd(fd) { + const chunks = []; + let ret = Buffer.alloc(0); + for await(const chunk of readEverything(fd)) + chunks.push(chunk); + return Buffer.concat(chunks); + } + }, + + async readlink(path, options) { + options = getOptions(options, {}); + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.readlink(pathModule.toNamespacedPath(path), options.encoding, req); + return req.promise; + }, + + async realpath(path, options) { + options = getOptions(options, {}); + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.realpath(path, options.encoding, req); + return req.promise; + }, + + async rename(oldPath, newPath) { + handleError((oldPath = getPathFromURL(oldPath))); + handleError((newPath = getPathFromURL(newPath))); + nullCheck(oldPath); + nullCheck(newPath); + const req = new FSReqPromise(); + binding.rename(pathModule.toNamespacedPath(oldPath), + pathModule.toNamespacedPath(newPath), + req); + return req.promise; + }, + + async rmdir(path) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.rmdir(pathModule.toNamespacedPath(path), req); + return req.promise; + }, + + async stat(path) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.stat(pathModule.toNamespacedPath(path), req); + return statsFromValues(await req.promise); + }, + + async symlink(target, path, type_) { + var type = (typeof type_ === 'string' ? type_ : null); + handleError((target = getPathFromURL(target))); + handleError((path = getPathFromURL(path))); + nullCheck(target); + nullCheck(path); + const req = new FSReqPromise(); + binding.symlink(preprocessSymlinkDestination(target, type, path), + pathModule.toNamespacedPath(path), + type, + req); + return req.promise; + }, + + async truncate(path, len = 0) { + return fs.promises.ftruncate(await fs.promises.open(path, 'r+'), len); + }, + + async unlink(path) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.unlink(pathModule.toNamespacedPath(path), req); + return req.promise; + }, + + async utimes(path, atime, mtime) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + const req = new FSReqPromise(); + binding.utimes(pathModule.toNamespacedPath(path), + toUnixTimestamp(atime), + toUnixTimestamp(mtime), + req); + return req.promise; + }, + + async write(fd, buffer, offset, length, position) { + if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); + const fdnum = fd.fd; + if (fdnum < 0 || fdnum > 0xFFFFFFFF) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'fd'); + + const req = new FSReqPromise(); + + if (isUint8Array(buffer)) { + if (typeof offset !== 'number') { + offset = 0; + } + if (typeof length !== 'number') { + length = buffer.length - offset; + } + if (typeof position !== 'number') { + position = null; + } + binding.writeBuffer(fdnum, buffer, offset, length, position, req); + } else { + if (typeof buffer !== 'string') + buffer += ''; + if (typeof position !== 'function') { + if (typeof offset === 'function') { + position = offset; + offset = null; + } else { + position = length; + } + length = 'utf8'; + } + binding.writeString(fdnum, buffer, offset, length, req); + } + const bytesWritten = await req.promise; + return { length: bytesWritten, buffer }; + }, + + async writeFile(path, data, options) { + options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); + const flag = options.flag || 'w'; + + if (typeof path === 'object' && Number.isInteger(path.fd)) + return writeFd(path); + + return writeFd(await fs.promises.open(path, flag, options.mode)); + + async function writeFd(fd) { + const buffer = isUint8Array(data) ? + data : Buffer.from('' + data, options.encoding || 'utf8'); + const position = /a/.test(flag) ? null : 0; + + return writeEverything(fd, buffer, 0, buffer.length, position); + } + } +}; + + +if (constants.O_SYMLINK !== undefined) { + Object.assign(promises, { + async lchmod(path, mode) { + const fd = await fs.promises.open(path, + constants.O_WRONLY | + constants.O_SYMLINK); + return fs.promises.fchmod(fd, mode); + }, + + async lchown(path, uid, gid) { + const fd = await fs.promises.open(path, + constants.O_WRONLY | + constants.O_SYMLINK); + return fs.promises.fchown(fd, uid, gid); + } + }); +} + +Object.defineProperty(fs, 'promises', { + configurable: true, + enumerable: true, + get() { + require('internal/util').emitExperimentalWarning('fs.promises'); + return promises; + } +}); diff --git a/src/async_wrap.h b/src/async_wrap.h index ec9e162ca79bc4..2c3135c17e1796 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -36,6 +36,7 @@ namespace node { #define NODE_ASYNC_NON_CRYPTO_PROVIDER_TYPES(V) \ V(NONE) \ V(DNSCHANNEL) \ + V(FD) \ V(FSEVENTWRAP) \ V(FSREQWRAP) \ V(GETADDRINFOREQWRAP) \ diff --git a/src/env.h b/src/env.h index 814622994329f0..fac7df325dfd6b 100644 --- a/src/env.h +++ b/src/env.h @@ -222,6 +222,7 @@ class ModuleWrap; V(preference_string, "preference") \ V(priority_string, "priority") \ V(produce_cached_data_string, "produceCachedData") \ + V(promise_string, "promise") \ V(raw_string, "raw") \ V(read_host_object_string, "_readHostObject") \ V(readable_string, "readable") \ @@ -282,13 +283,14 @@ class ModuleWrap; V(async_hooks_after_function, v8::Function) \ V(async_hooks_promise_resolve_function, v8::Function) \ V(binding_cache_object, v8::Object) \ - V(internal_binding_cache_object, v8::Object) \ V(buffer_prototype_object, v8::Object) \ V(context, v8::Context) \ + V(fd_constructor_template, v8::ObjectTemplate) \ V(host_import_module_dynamically_callback, v8::Function) \ V(http2ping_constructor_template, v8::ObjectTemplate) \ V(http2stream_constructor_template, v8::ObjectTemplate) \ V(inspector_console_api_object, v8::Object) \ + V(internal_binding_cache_object, v8::Object) \ V(module_load_list_array, v8::Array) \ V(pbkdf2_constructor_template, v8::ObjectTemplate) \ V(pipe_constructor_template, v8::FunctionTemplate) \ @@ -309,7 +311,7 @@ class ModuleWrap; V(udp_constructor_function, v8::Function) \ V(vm_parsing_context_symbol, v8::Symbol) \ V(url_constructor_function, v8::Function) \ - V(write_wrap_constructor_function, v8::Function) \ + V(write_wrap_constructor_function, v8::Function) class Environment; diff --git a/src/node_file.cc b/src/node_file.cc index e7ad4900c43797..1a40efea22f4fc 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -92,6 +92,8 @@ using v8::Local; using v8::MaybeLocal; using v8::Number; using v8::Object; +using v8::ObjectTemplate; +using v8::PropertyCallbackInfo; using v8::String; using v8::Value; @@ -103,34 +105,86 @@ using v8::Value; if (*path == nullptr) \ return TYPE_ERROR( #path " must be a string or Buffer"); -FSReqWrap* FSReqWrap::New(Environment* env, - Local req, - const char* syscall, - const char* data, - enum encoding encoding, - Ownership ownership) { - const bool copy = (data != nullptr && ownership == COPY); - const size_t size = copy ? 1 + strlen(data) : 0; - FSReqWrap* that; - char* const storage = new char[sizeof(*that) + size]; - that = new(storage) FSReqWrap(env, req, syscall, data, encoding); - if (copy) - that->data_ = static_cast(memcpy(that->inline_data(), data, size)); - return that; +FD::FD(Environment* env, int fd) + : AsyncWrap(env, + env->fd_constructor_template()->NewInstance(env->context()) + .ToLocalChecked(), + AsyncWrap::PROVIDER_FD), + fd_(fd) { + MakeWeak(this); +} + +FD::~FD() { + Close(); + CHECK(persistent().IsEmpty()); + if (!object().IsEmpty()) + ClearWrap(object()); + persistent().Reset(); +} + +void FD::Close() { + uv_fs_t* req = new uv_fs_t; + auto after = [](uv_fs_t* req) { + // TODO(jasnell): For now, this is swallowing any errors that may be + // occurring while cleaning up. That is not necessarily a good thing, + // but as this is happening during GC, there's not a lot we can + // reasonably do here. + uv_fs_req_cleanup(req); + delete req; + }; + int ret = uv_fs_close(env()->event_loop(), req, fd_, after); + if (ret < 0) { + uv_fs_req_cleanup(req); + delete req; + } } +void FD::GetFD(Local property, + const PropertyCallbackInfo& info) { + FD* fd; + ASSIGN_OR_RETURN_UNWRAP(&fd, info.Holder()); + info.GetReturnValue().Set(fd->fd()); +} + +void FSReqWrap::Init(const char* syscall, + const char* data, + enum encoding encoding, + Ownership ownership) { + CHECK_EQ(info_, nullptr); // Only initialize once. + const bool copy = (data != nullptr && ownership == Ownership::COPY); + const size_t size = copy ? 1 + strlen(data) : 0; + char* const storage = new char[sizeof(*info_) + size]; + info_ = new(storage) FSReqInfo(syscall, data, encoding); + if (copy) { + info_->SetData( + static_cast(memcpy(info_->inline_data(), data, size))); + } +} void FSReqWrap::Dispose() { + if (info_ != nullptr) + info_->Dispose(); this->~FSReqWrap(); - delete[] reinterpret_cast(this); } +void FSReqInfo::Dispose() { + this->~FSReqInfo(); + delete[] reinterpret_cast(this); +} void FSReqWrap::Reject(Local reject) { Local argv[1] { reject }; MakeCallback(env()->oncomplete_string(), arraysize(argv), argv); } +void FSReqWrap::FillStatsArray(const uv_stat_t* stat) { + node::FillStatsArray(env()->fs_stats_field_array(), stat); +} + +void FSReqWrap::ResolveStat() { + Resolve(Undefined(env()->isolate())); +} + void FSReqWrap::Resolve(Local value) { Local argv[2] { Null(env()->isolate()), @@ -139,11 +193,60 @@ void FSReqWrap::Resolve(Local value) { MakeCallback(env()->oncomplete_string(), arraysize(argv), argv); } +void FSReqPromise::FillStatsArray(const uv_stat_t* stat) { + size_t length = sizeof(double) * 14; + void* buf = node::UncheckedMalloc(length); + if (buf == nullptr) + return; + + node::FillStatsArray(reinterpret_cast(buf), stat); + + Local ab = + ArrayBuffer::New(env()->isolate(), buf, length, + v8::ArrayBufferCreationMode::kInternalized); + object()->Set(env()->context(), + FIXED_ONE_BYTE_STRING(env()->isolate(), "statFields"), + Float64Array::New(ab, 0, 14)).FromJust(); +} + +void FSReqPromise::ResolveStat() { + Resolve( + object()->Get(env()->context(), + FIXED_ONE_BYTE_STRING(env()->isolate(), "statFields")) + .ToLocalChecked()); +} + +void FSReqPromise::Reject(Local reject) { + Local promise = + object()->Get(env()->context(), env()->promise_string()).ToLocalChecked(); + CHECK(promise->IsPromise()); + if (promise.As()->State() != Promise::kPending) + return; + Local resolver = promise.As(); + resolver->Reject(env()->context(), reject).FromJust(); +} + +void FSReqPromise::Resolve(Local value) { + Local promise = + object()->Get(env()->context(), env()->promise_string()).ToLocalChecked(); + CHECK(promise->IsPromise()); + if (promise.As()->State() != Promise::kPending) + return; + Local resolver = promise.As(); + resolver->Resolve(env()->context(), value).FromJust(); +} + void NewFSReqWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); - ClearWrap(args.This()); + Environment* env = Environment::GetCurrent(args.GetIsolate()); + new FSReqWrap(env, args.This()); } +void NewFSReqPromise(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args.GetIsolate()); + new FSReqPromise(env, args.This()); +} FSReqAfterScope::FSReqAfterScope(FSReqWrap* wrap, uv_fs_t* req) : wrap_(wrap), @@ -187,12 +290,10 @@ void AfterNoArgs(uv_fs_t* req) { void AfterStat(uv_fs_t* req) { FSReqWrap* req_wrap = static_cast(req->data); FSReqAfterScope after(req_wrap, req); - Environment* env = req_wrap->env(); if (after.Proceed()) { - FillStatsArray(env->fs_stats_field_array(), - static_cast(req->ptr)); - req_wrap->Resolve(Undefined(req_wrap->env()->isolate())); + req_wrap->FillStatsArray(&req->statbuf); + req_wrap->ResolveStat(); } } @@ -204,6 +305,16 @@ void AfterInteger(uv_fs_t* req) { req_wrap->Resolve(Integer::New(req_wrap->env()->isolate(), req->result)); } +void AfterFD(uv_fs_t* req) { + FSReqWrap* req_wrap = static_cast(req->data); + FSReqAfterScope after(req_wrap, req); + + if (after.Proceed()) { + FD* fd = new FD(req_wrap->env(), req->result); + req_wrap->Resolve(fd->object()); + } +} + void AfterStringPath(uv_fs_t* req) { FSReqWrap* req_wrap = static_cast(req->data); FSReqAfterScope after(req_wrap, req); @@ -214,7 +325,7 @@ void AfterStringPath(uv_fs_t* req) { if (after.Proceed()) { link = StringBytes::Encode(req_wrap->env()->isolate(), static_cast(req->path), - req_wrap->encoding_, + req_wrap->encoding(), &error); if (link.IsEmpty()) req_wrap->Reject(error); @@ -233,7 +344,7 @@ void AfterStringPtr(uv_fs_t* req) { if (after.Proceed()) { link = StringBytes::Encode(req_wrap->env()->isolate(), static_cast(req->ptr), - req_wrap->encoding_, + req_wrap->encoding(), &error); if (link.IsEmpty()) req_wrap->Reject(error); @@ -270,7 +381,7 @@ void AfterScanDir(uv_fs_t* req) { MaybeLocal filename = StringBytes::Encode(env->isolate(), ent.name, - req_wrap->encoding_, + req_wrap->encoding(), &error); if (filename.IsEmpty()) return req_wrap->Reject(error); @@ -311,7 +422,9 @@ template inline FSReqWrap* AsyncDestCall(Environment* env, Local req, const char* dest, enum encoding enc, const char* syscall, uv_fs_cb after, Func fn, Args... args) { - FSReqWrap* req_wrap = FSReqWrap::New(env, req, syscall, dest, enc); + FSReqWrap* req_wrap = Unwrap(req); + CHECK_NE(req_wrap, nullptr); + req_wrap->Init(syscall, dest, enc); int err = fn(env->event_loop(), req_wrap->req(), args..., after); req_wrap->Dispatched(); if (err < 0) { @@ -336,8 +449,9 @@ inline FSReqWrap* AsyncCall(Environment* env, Local req, #define ASYNC_DEST_CALL(after, func, request, dest, encoding, ...) \ Environment* env = Environment::GetCurrent(args); \ CHECK(request->IsObject()); \ - FSReqWrap* req_wrap = FSReqWrap::New(env, request.As(), \ - #func, dest, encoding); \ + FSReqWrap* req_wrap = Unwrap(request.As()); \ + CHECK_NE(req_wrap, nullptr); \ + req_wrap->Init(#func, dest, encoding); \ int err = uv_fs_ ## func(env->event_loop(), \ req_wrap->req(), \ __VA_ARGS__, \ @@ -903,6 +1017,36 @@ static void Open(const FunctionCallbackInfo& args) { } +static void OpenFD(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + int len = args.Length(); + if (len < 1) + return TYPE_ERROR("path required"); + if (len < 2) + return TYPE_ERROR("flags required"); + if (len < 3) + return TYPE_ERROR("mode required"); + if (!args[1]->IsInt32()) + return TYPE_ERROR("flags must be an int"); + if (!args[2]->IsInt32()) + return TYPE_ERROR("mode must be an int"); + + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) + + int flags = args[1]->Int32Value(); + int mode = static_cast(args[2]->Int32Value()); + + if (args[3]->IsObject()) { + ASYNC_CALL(AfterFD, open, args[3], UTF8, *path, flags, mode) + } else { + SYNC_CALL(open, *path, *path, flags, mode) + args.GetReturnValue().Set(SYNC_RESULT); + } +} + + static void CopyFile(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -1036,7 +1180,7 @@ static void WriteString(const FunctionCallbackInfo& args) { char* buf = nullptr; int64_t pos; size_t len; - FSReqWrap::Ownership ownership = FSReqWrap::COPY; + Ownership ownership = Ownership::COPY; // will assign buf and len if string was external if (!StringBytes::GetExternalParts(string, @@ -1048,7 +1192,7 @@ static void WriteString(const FunctionCallbackInfo& args) { // StorageSize may return too large a char, so correct the actual length // by the write size len = StringBytes::Write(env->isolate(), buf, len, args[1], enc); - ownership = FSReqWrap::MOVE; + ownership = Ownership::MOVE; } pos = GET_OFFSET(args[2]); req = args[4]; @@ -1062,13 +1206,14 @@ static void WriteString(const FunctionCallbackInfo& args) { inline ~Delete() { delete[] pointer_; } char* const pointer_; }; - Delete delete_on_return(ownership == FSReqWrap::MOVE ? buf : nullptr); + Delete delete_on_return(ownership == Ownership::MOVE ? buf : nullptr); SYNC_CALL(write, nullptr, fd, &uvbuf, 1, pos) return args.GetReturnValue().Set(SYNC_RESULT); } - FSReqWrap* req_wrap = - FSReqWrap::New(env, req.As(), "write", buf, UTF8, ownership); + FSReqWrap* req_wrap = Unwrap(req.As()); + CHECK_NE(req_wrap, nullptr); + req_wrap->Init("write", buf, UTF8, ownership); int err = uv_fs_write(env->event_loop(), req_wrap->req(), fd, @@ -1339,6 +1484,7 @@ void InitFs(Local target, env->SetMethod(target, "access", Access); env->SetMethod(target, "close", Close); env->SetMethod(target, "open", Open); + env->SetMethod(target, "openFD", OpenFD); env->SetMethod(target, "read", Read); env->SetMethod(target, "fdatasync", Fdatasync); env->SetMethod(target, "fsync", Fsync); @@ -1387,7 +1533,26 @@ void InitFs(Local target, Local wrapString = FIXED_ONE_BYTE_STRING(env->isolate(), "FSReqWrap"); fst->SetClassName(wrapString); - target->Set(wrapString, fst->GetFunction()); + target->Set(context, wrapString, fst->GetFunction()).FromJust(); + + // Create FunctionTemplate for FSReqWrap + Local pst = + FunctionTemplate::New(env->isolate(), NewFSReqPromise); + pst->InstanceTemplate()->SetInternalFieldCount(1); + AsyncWrap::AddWrapMethods(env, pst); + Local promiseString = + FIXED_ONE_BYTE_STRING(env->isolate(), "FSReqPromise"); + pst->SetClassName(promiseString); + target->Set(context, promiseString, pst->GetFunction()).FromJust(); + + // Create FunctionTemplate for FD + Local fd = FunctionTemplate::New(env->isolate()); + fd->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "FD")); + AsyncWrap::AddWrapMethods(env, fd); + Local fdt = fd->InstanceTemplate(); + fdt->SetAccessor(FIXED_ONE_BYTE_STRING(env->isolate(), "fd"), FD::GetFD); + fdt->SetInternalFieldCount(1); + env->set_fd_constructor_template(fdt); } } // namespace fs diff --git a/src/node_file.h b/src/node_file.h index db85451b67a9df..89120bbfc81895 100644 --- a/src/node_file.h +++ b/src/node_file.h @@ -9,31 +9,38 @@ namespace node { using v8::Context; +using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::Local; +using v8::MaybeLocal; using v8::Object; +using v8::Persistent; +using v8::Promise; +using v8::PropertyCallbackInfo; +using v8::String; using v8::Undefined; using v8::Value; namespace fs { -class FSReqWrap: public ReqWrap { +enum class Ownership { COPY, MOVE }; + +class FSReqInfo { public: - enum Ownership { COPY, MOVE }; + FSReqInfo(const char* syscall, + const char* data, + enum encoding encoding) + : encoding_(encoding), + syscall_(syscall), + data_(data) { } - inline static FSReqWrap* New(Environment* env, - Local req, - const char* syscall, - const char* data = nullptr, - enum encoding encoding = UTF8, - Ownership ownership = COPY); + virtual ~FSReqInfo() { + Release(); + } inline void Dispose(); - virtual void Reject(Local reject); - virtual void Resolve(Local value); - - void ReleaseEarly() { + void Release() { if (data_ != inline_data()) { delete[] data_; data_ = nullptr; @@ -42,20 +49,24 @@ class FSReqWrap: public ReqWrap { const char* syscall() const { return syscall_; } const char* data() const { return data_; } + void SetData(const char* data) { data_ = data; } const enum encoding encoding_; - size_t self_size() const override { return sizeof(*this); } + void* operator new(size_t size) = delete; + void* operator new(size_t size, char* storage) { return storage; } + char* inline_data() { return reinterpret_cast(this + 1); } - protected: - FSReqWrap(Environment* env, - Local req, - const char* syscall, - const char* data, - enum encoding encoding) - : ReqWrap(env, req, AsyncWrap::PROVIDER_FSREQWRAP), - encoding_(encoding), - syscall_(syscall), - data_(data) { + private: + const char* syscall_; + const char* data_; + + DISALLOW_COPY_AND_ASSIGN(FSReqInfo); +}; + +class FSReqWrap : public ReqWrap { + public: + FSReqWrap(Environment* env, Local req) + : ReqWrap(env, req, AsyncWrap::PROVIDER_FSREQWRAP) { Wrap(object(), this); } @@ -64,17 +75,62 @@ class FSReqWrap: public ReqWrap { ClearWrap(object()); } - void* operator new(size_t size) = delete; - void* operator new(size_t size, char* storage) { return storage; } - char* inline_data() { return reinterpret_cast(this + 1); } + inline void Init(const char* syscall, + const char* data = nullptr, + enum encoding encoding = UTF8, + Ownership ownership = Ownership::COPY); + + inline void Dispose(); + + virtual void FillStatsArray(const uv_stat_t* stat); + virtual void Reject(Local reject); + virtual void Resolve(Local value); + virtual void ResolveStat(); + + void ReleaseEarly() { + if (info_ != nullptr) + info_->Release(); + } + + const char* syscall() const { + return info_ != nullptr ? info_->syscall() : nullptr; + } + + const char* data() const { + return info_ != nullptr ? info_->data() : nullptr; + } + + enum encoding encoding() const { + return info_ != nullptr ? info_->encoding_ : UTF8; + } + + size_t self_size() const override { return sizeof(*this); } private: - const char* syscall_; - const char* data_; + FSReqInfo* info_ = nullptr; DISALLOW_COPY_AND_ASSIGN(FSReqWrap); }; +class FSReqPromise : public FSReqWrap { + public: + FSReqPromise(Environment* env, Local req) + : FSReqWrap(env, req) { + MaybeLocal promise = + Promise::Resolver::New(env->context()); + object()->Set(env->context(), + env->promise_string(), + promise.ToLocalChecked()); + } + + ~FSReqPromise() override {} + + void FillStatsArray(const uv_stat_t* stat) override; + void Reject(Local reject) override; + void Resolve(Local value) override; + void ResolveStat() override; +}; + class FSReqAfterScope { public: FSReqAfterScope(FSReqWrap* wrap, uv_fs_t* req); @@ -91,6 +147,26 @@ class FSReqAfterScope { Context::Scope context_scope_; }; + +// A wrapper for a file descriptor that will automatically close the fd when +// the object is garbage collected. +class FD : public AsyncWrap { + public: + FD(Environment* env, int fd); + virtual ~FD(); + + int fd() const { return fd_; } + size_t self_size() const override { return sizeof(*this); } + + static void GetFD(Local property, + const PropertyCallbackInfo& info); + private: + void Close(); + + int fd_; + v8::Persistent promise_; +}; + } // namespace fs } // namespace node