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
22 changes: 22 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2849,6 +2849,9 @@ behavior is similar to `cp dir1/ dir2/`.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version: v16.10.0
pr-url: https://github.com/nodejs/node/pull/40013
description: The `fs` option does not need `open` method if an `fd` was provided.
Expand Down Expand Up @@ -2905,6 +2908,8 @@ changes:
* `highWaterMark` {integer} **Default:** `64 * 1024`
* `fs` {Object|null} **Default:** `null`
* `signal` {AbortSignal|null} **Default:** `null`
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.ReadStream}

`options` can include `start` and `end` values to read a range of bytes from
Expand All @@ -2925,6 +2930,12 @@ If `fd` points to a character device that only supports blocking reads
available. This can prevent the process from exiting and the stream from
closing naturally.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down Expand Up @@ -2975,6 +2986,9 @@ If `options` is a string, then it specifies the encoding.
<!-- YAML
added: v0.1.31
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63851
description: Add the `windowsHandle` option.
- version:
- v21.0.0
- v20.10.0
Expand Down Expand Up @@ -3035,6 +3049,8 @@ changes:
* `highWaterMark` {number} **Default:** `16384`
* `flush` {boolean} If `true`, the underlying file descriptor is flushed
prior to closing it. **Default:** `false`.
* `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place
of `fd`. Windows only. **Default:** `null`
* Returns: {fs.WriteStream}

`options` may also include a `start` option to allow writing data at some
Expand All @@ -3049,6 +3065,12 @@ then the file descriptor won't be closed, even if there's an error.
It is the application's responsibility to close it and make sure there's no
file descriptor leak.

On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To
use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle
obtained from another process, pass it as `windowsHandle`. The handle is wrapped
in a file descriptor that the stream owns and closes. The `windowsHandle` option
throws on non-Windows platforms and cannot be combined with the `fs` option.

By default, the stream will emit a `'close'` event after it has been
destroyed. Set the `emitClose` option to `false` to change this behavior.

Expand Down
38 changes: 36 additions & 2 deletions lib/internal/fs/streams.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ const {
} = primordials;

const {
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MISSING_OPTION,
ERR_OUT_OF_RANGE,
ERR_STREAM_DESTROYED,
ERR_SYSTEM_ERROR,
} = require('internal/errors').codes;
const {
isWindows,
kEmptyObject,
} = require('internal/util');
const {
Expand All @@ -40,6 +44,8 @@ const {
} = require('internal/fs/utils');
const { Readable, Writable, finished } = require('stream');
const { toPathIfFileURL } = require('internal/url');
const binding = internalBinding('fs');
const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs;
const kIoDone = Symbol('kIoDone');
const kIsPerformingIO = Symbol('kIsPerformingIO');

Expand Down Expand Up @@ -160,6 +166,26 @@ function importFd(stream, options) {
['number', 'FileHandle'], options.fd);
}

function importWindowsHandle(stream, options, flags) {
if (options.windowsHandle == null) {
throw new ERR_MISSING_OPTION('options.windowsHandle');
}
if (!isWindows) {
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle');
}
if (options.fs) {
// The HANDLE is wrapped using the real filesystem, so a custom fs
// implementation cannot be combined with it.
throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs');
}
if (typeof options.windowsHandle !== 'bigint') {
throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint',
options.windowsHandle);
}
stream[kFs] = fs;
return binding.handleToFd(options.windowsHandle, flags);
}

function ReadStream(path, options) {
if (!(this instanceof ReadStream))
return new ReadStream(path, options);
Expand All @@ -173,7 +199,11 @@ function ReadStream(path, options) {
options.autoDestroy = false;
}

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down Expand Up @@ -325,7 +355,11 @@ function WriteStream(path, options) {
// Only buffers are supported.
options.decodeStrings = true;

if (options.fd == null) {
if (options.fd != null && options.windowsHandle != null) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd');
} else if (options.windowsHandle != null) {
this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY));
} else if (options.fd == null) {
this.fd = null;
this[kFs] = options.fs || fs;
validateFunction(this[kFs].open, 'options.fs.open');
Expand Down
34 changes: 34 additions & 0 deletions src/node_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4132,6 +4132,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) {
return info;
}

#ifdef _WIN32
static void HandleToFd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 1);
CHECK(args[0]->IsBigInt());

int flags = 0;
if (args[1]->IsNumber()) {
flags = args[1].As<Int32>()->Value();
}

bool lossless;
int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
return THROW_ERR_OUT_OF_RANGE(
env, "windowsHandle does not fit into 64 bits");
}
intptr_t value = static_cast<intptr_t>(handle);

int fd = _open_osfhandle(value, flags);
if (fd == -1) {
return env->ThrowErrnoException(errno, "_open_osfhandle");
}
args.GetReturnValue().Set(fd);
}
#endif // _WIN32

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Expand Down Expand Up @@ -4198,6 +4225,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,

SetMethod(isolate, target, "mkdtemp", Mkdtemp);

#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
#endif

SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths);
SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile);
SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir);
Expand Down Expand Up @@ -4325,6 +4356,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);

registry->Register(Mkdtemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
registry->Register(NewFSReqCallback);

registry->Register(FileHandle::New);
Expand Down
64 changes: 64 additions & 0 deletions test/addons/fs-windows-handle/binding.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include <node.h>
#include <v8.h>

#ifdef _WIN32
#include <windows.h>
#endif

namespace {

using v8::BigInt;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values
// as JS bigints. These are NOT CRT file descriptors, so passing them as the
// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on
// Windows. Returns undefined on other platforms.
void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
#ifdef _WIN32
Local<Context> context = isolate->GetCurrentContext();

HANDLE read_handle = nullptr;
HANDLE write_handle = nullptr;
if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked()));
return;
}

Local<Object> result = Object::New(isolate);
result
->Set(context,
String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(),
BigInt::New(isolate,
static_cast<int64_t>(
reinterpret_cast<intptr_t>(read_handle))))
.Check();
result
->Set(context,
String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(),
BigInt::New(isolate,
static_cast<int64_t>(
reinterpret_cast<intptr_t>(write_handle))))
.Check();
args.GetReturnValue().Set(result);
#else
args.GetReturnValue().SetUndefined();
#endif
}

} // anonymous namespace

extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
Local<Value> module,
Local<Context> context) {
NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles);
}
9 changes: 9 additions & 0 deletions test/addons/fs-windows-handle/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
},
]
}
35 changes: 35 additions & 0 deletions test/addons/fs-windows-handle/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';
// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32
// HANDLE through the `windowsHandle` option, as happens when a parent process
// passes an inherited anonymous pipe handle. The addon produces such handles
// via CreatePipe(); Node must wrap them in CRT file descriptors instead of
// failing with EBADF.

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

if (!common.isWindows) {
common.skip('windowsHandle is Windows-only');
}

const assert = require('assert');
const fs = require('fs');

const binding = require(`./build/${common.buildType}/binding`);

const { readHandle, writeHandle } = binding.createPipeHandles();
assert.strictEqual(typeof readHandle, 'bigint');
assert.strictEqual(typeof writeHandle, 'bigint');

const payload = 'payload';

const chunks = [];
const rs = fs.createReadStream(null, { windowsHandle: readHandle });
rs.on('error', (err) => assert.fail(err));
rs.on('data', (chunk) => chunks.push(chunk));
rs.on('end', common.mustCall(() => {
assert.strictEqual(Buffer.concat(chunks).toString(), payload);
}));

const ws = fs.createWriteStream(null, { windowsHandle: writeHandle });
ws.on('error', (err) => assert.fail(err));
ws.end(payload);
47 changes: 47 additions & 0 deletions test/parallel/test-fs-stream-windows-handle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

// Tests option validation for the `windowsHandle` option of
// fs.createReadStream()/createWriteStream(). The functional round-trip on
// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is
// covered by test/addons/fs-windows-handle.

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

const handle = 1n;

for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), {
code: 'ERR_INCOMPATIBLE_OPTION_PAIR',
});
}

if (!common.isWindows) {
for (const create of [fs.createReadStream, fs.createWriteStream]) {
assert.throws(() => create(null, { windowsHandle: handle }), {
code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
});
}
return;
}

for (const create of [fs.createReadStream, fs.createWriteStream]) {
// Cannot be combined with a custom `fs` implementation.
assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), {
code: 'ERR_METHOD_NOT_IMPLEMENTED',
});

// Must be a bigint.
assert.throws(() => create(null, { windowsHandle: 'nope' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => create(null, { windowsHandle: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Must fit into 64 bits.
assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), {
code: 'ERR_OUT_OF_RANGE',
});
}