GeezerVison is showing github.com — fs: add windowsHandle option to file streams · nodejs/node@656cfae
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/fs.md +22Lines changed: 22 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2933,6 +2933,9 @@ behavior is similar to `cp dir1/ dir2/`. | 2933 | 2933 | <!-- YAML | 2934 | 2934 | added: v0.1.31 | 2935 | 2935 | changes: | | 2936 | + - version: REPLACEME | | 2937 | + pr-url: https://github.com/nodejs/node/pull/63851 | | 2938 | + description: Add the `windowsHandle` option. | 2936 | 2939 | - version: v16.10.0 | 2937 | 2940 | pr-url: https://github.com/nodejs/node/pull/40013 | 2938 | 2941 | description: The `fs` option does not need `open` method if an `fd` was provided. | @@ -2989,6 +2992,8 @@ changes: | 2989 | 2992 | * `highWaterMark` {integer} **Default:** `64 * 1024` | 2990 | 2993 | * `fs` {Object|null} **Default:** `null` | 2991 | 2994 | * `signal` {AbortSignal|null} **Default:** `null` | | 2995 | + * `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place | | 2996 | + of `fd`. Windows only. **Default:** `null` | 2992 | 2997 | * Returns: {fs.ReadStream} | 2993 | 2998 |
| 2994 | 2999 | `options` can include `start` and `end` values to read a range of bytes from | @@ -3009,6 +3014,12 @@ If `fd` points to a character device that only supports blocking reads | 3009 | 3014 | available. This can prevent the process from exiting and the stream from | 3010 | 3015 | closing naturally. | 3011 | 3016 |
| | 3017 | +On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To | | 3018 | +use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle | | 3019 | +obtained from another process, pass it as `windowsHandle`. The handle is wrapped | | 3020 | +in a file descriptor that the stream owns and closes. The `windowsHandle` option | | 3021 | +throws on non-Windows platforms and cannot be combined with the `fs` option. | | 3022 | +
| 3012 | 3023 | By default, the stream will emit a `'close'` event after it has been | 3013 | 3024 | destroyed. Set the `emitClose` option to `false` to change this behavior. | 3014 | 3025 |
| @@ -3059,6 +3070,9 @@ If `options` is a string, then it specifies the encoding. | 3059 | 3070 | <!-- YAML | 3060 | 3071 | added: v0.1.31 | 3061 | 3072 | changes: | | 3073 | + - version: REPLACEME | | 3074 | + pr-url: https://github.com/nodejs/node/pull/63851 | | 3075 | + description: Add the `windowsHandle` option. | 3062 | 3076 | - version: v22.0.0 | 3063 | 3077 | pr-url: https://github.com/nodejs/node/pull/52037 | 3064 | 3078 | description: bump default highWaterMark. | @@ -3123,6 +3137,8 @@ changes: | 3123 | 3137 | [`stream.getDefaultHighWaterMark()`][]. | 3124 | 3138 | * `flush` {boolean} If `true`, the underlying file descriptor is flushed | 3125 | 3139 | prior to closing it. **Default:** `false`. | | 3140 | + * `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place | | 3141 | + of `fd`. Windows only. **Default:** `null` | 3126 | 3142 | * Returns: {fs.WriteStream} | 3127 | 3143 |
| 3128 | 3144 | `options` may also include a `start` option to allow writing data at some | @@ -3137,6 +3153,12 @@ then the file descriptor won't be closed, even if there's an error. | 3137 | 3153 | It is the application's responsibility to close it and make sure there's no | 3138 | 3154 | file descriptor leak. | 3139 | 3155 |
| | 3156 | +On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To | | 3157 | +use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle | | 3158 | +obtained from another process, pass it as `windowsHandle`. The handle is wrapped | | 3159 | +in a file descriptor that the stream owns and closes. The `windowsHandle` option | | 3160 | +throws on non-Windows platforms and cannot be combined with the `fs` option. | | 3161 | +
| 3140 | 3162 | By default, the stream will emit a `'close'` event after it has been | 3141 | 3163 | destroyed. Set the `emitClose` option to `false` to change this behavior. | 3142 | 3164 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/fs/streams.js +36-2Lines changed: 36 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -13,13 +13,17 @@ const { | 13 | 13 | } = primordials; | 14 | 14 |
| 15 | 15 | const { | | 16 | + ERR_FEATURE_UNAVAILABLE_ON_PLATFORM, | | 17 | + ERR_INCOMPATIBLE_OPTION_PAIR, | 16 | 18 | ERR_INVALID_ARG_TYPE, | 17 | 19 | ERR_METHOD_NOT_IMPLEMENTED, | | 20 | + ERR_MISSING_OPTION, | 18 | 21 | ERR_OUT_OF_RANGE, | 19 | 22 | ERR_STREAM_DESTROYED, | 20 | 23 | ERR_SYSTEM_ERROR, | 21 | 24 | } = require('internal/errors').codes; | 22 | 25 | const { | | 26 | + isWindows, | 23 | 27 | kEmptyObject, | 24 | 28 | } = require('internal/util'); | 25 | 29 | const { | @@ -40,6 +44,8 @@ const { | 40 | 44 | } = require('internal/fs/utils'); | 41 | 45 | const { Readable, Writable, finished } = require('stream'); | 42 | 46 | const { toPathIfFileURL } = require('internal/url'); | | 47 | +const binding = internalBinding('fs'); | | 48 | +const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs; | 43 | 49 | const kIoDone = Symbol('kIoDone'); | 44 | 50 | const kIsPerformingIO = Symbol('kIsPerformingIO'); | 45 | 51 |
| @@ -160,6 +166,26 @@ function importFd(stream, options) { | 160 | 166 | ['number', 'FileHandle'], options.fd); | 161 | 167 | } | 162 | 168 |
| | 169 | +function importWindowsHandle(stream, options, flags) { | | 170 | + if (options.windowsHandle == null) { | | 171 | + throw new ERR_MISSING_OPTION('options.windowsHandle'); | | 172 | + } | | 173 | + if (!isWindows) { | | 174 | + throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle'); | | 175 | + } | | 176 | + if (options.fs) { | | 177 | + // The HANDLE is wrapped using the real filesystem, so a custom fs | | 178 | + // implementation cannot be combined with it. | | 179 | + throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs'); | | 180 | + } | | 181 | + if (typeof options.windowsHandle !== 'bigint') { | | 182 | + throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint', | | 183 | + options.windowsHandle); | | 184 | + } | | 185 | + stream[kFs] = fs; | | 186 | + return binding.handleToFd(options.windowsHandle, flags); | | 187 | +} | | 188 | + | 163 | 189 | function ReadStream(path, options) { | 164 | 190 | if (!(this instanceof ReadStream)) | 165 | 191 | return new ReadStream(path, options); | @@ -173,7 +199,11 @@ function ReadStream(path, options) { | 173 | 199 | options.autoDestroy = false; | 174 | 200 | } | 175 | 201 |
| 176 | | - if (options.fd == null) { | | 202 | + if (options.fd != null && options.windowsHandle != null) { | | 203 | + throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd'); | | 204 | + } else if (options.windowsHandle != null) { | | 205 | + this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY)); | | 206 | + } else if (options.fd == null) { | 177 | 207 | this.fd = null; | 178 | 208 | this[kFs] = options.fs || fs; | 179 | 209 | validateFunction(this[kFs].open, 'options.fs.open'); | @@ -325,7 +355,11 @@ function WriteStream(path, options) { | 325 | 355 | // Only buffers are supported. | 326 | 356 | options.decodeStrings = true; | 327 | 357 |
| 328 | | - if (options.fd == null) { | | 358 | + if (options.fd != null && options.windowsHandle != null) { | | 359 | + throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd'); | | 360 | + } else if (options.windowsHandle != null) { | | 361 | + this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY)); | | 362 | + } else if (options.fd == null) { | 329 | 363 | this.fd = null; | 330 | 364 | this[kFs] = options.fs || fs; | 331 | 365 | validateFunction(this[kFs].open, 'options.fs.open'); | |
Collapse file Copy file name to clipboardExpand all lines: src/node_file.cc +34Lines changed: 34 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -4150,6 +4150,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) { | 4150 | 4150 | return info; | 4151 | 4151 | } | 4152 | 4152 |
| | 4153 | +#ifdef _WIN32 | | 4154 | +static void HandleToFd(const FunctionCallbackInfo<Value>& args) { | | 4155 | + Environment* env = Environment::GetCurrent(args); | | 4156 | + CHECK_GE(args.Length(), 1); | | 4157 | + CHECK(args[0]->IsBigInt()); | | 4158 | + | | 4159 | + int flags = 0; | | 4160 | + if (args[1]->IsNumber()) { | | 4161 | + flags = args[1].As<Int32>()->Value(); | | 4162 | + } | | 4163 | + | | 4164 | + bool lossless; | | 4165 | + int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless); | | 4166 | + if (!lossless) { | | 4167 | + return THROW_ERR_OUT_OF_RANGE(env, | | 4168 | + "windowsHandle does not fit into 64 bits"); | | 4169 | + } | | 4170 | + intptr_t value = static_cast<intptr_t>(handle); | | 4171 | + | | 4172 | + int fd = _open_osfhandle(value, flags); | | 4173 | + if (fd == -1) { | | 4174 | + return env->ThrowErrnoException(errno, "_open_osfhandle"); | | 4175 | + } | | 4176 | + args.GetReturnValue().Set(fd); | | 4177 | +} | | 4178 | +#endif // _WIN32 | | 4179 | + | 4153 | 4180 | void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data, | 4154 | 4181 | Local<ObjectTemplate> target) { | 4155 | 4182 | Isolate* isolate = isolate_data->isolate(); | @@ -4216,6 +4243,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, | 4216 | 4243 |
| 4217 | 4244 | SetMethod(isolate, target, "mkdtemp", Mkdtemp); | 4218 | 4245 |
| | 4246 | +#ifdef _WIN32 | | 4247 | + SetMethod(isolate, target, "handleToFd", HandleToFd); | | 4248 | +#endif | | 4249 | + | 4219 | 4250 | SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths); | 4220 | 4251 | SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile); | 4221 | 4252 | SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir); | @@ -4343,6 +4374,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { | 4343 | 4374 | registry->Register(LUTimes); | 4344 | 4375 |
| 4345 | 4376 | registry->Register(Mkdtemp); | | 4377 | +#ifdef _WIN32 | | 4378 | + registry->Register(HandleToFd); | | 4379 | +#endif | 4346 | 4380 | registry->Register(NewFSReqCallback); | 4347 | 4381 |
| 4348 | 4382 | registry->Register(FileHandle::New); | |
Collapse file Copy file name to clipboard +62Lines changed: 62 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +#include <node.h> | | 2 | +#include <v8.h> | | 3 | + | | 4 | +#ifdef _WIN32 | | 5 | +#include <windows.h> | | 6 | +#endif | | 7 | + | | 8 | +namespace { | | 9 | + | | 10 | +using v8::BigInt; | | 11 | +using v8::Context; | | 12 | +using v8::FunctionCallbackInfo; | | 13 | +using v8::Isolate; | | 14 | +using v8::Local; | | 15 | +using v8::Object; | | 16 | +using v8::String; | | 17 | +using v8::Value; | | 18 | + | | 19 | +// Creates an anonymous pipe and returns its raw Win32 read/write HANDLE values | | 20 | +// as JS bigints. These are NOT CRT file descriptors, so passing them as the | | 21 | +// `windowsHandle` stream option exercises the HANDLE -> fd conversion path on | | 22 | +// Windows. Returns undefined on other platforms. | | 23 | +void CreatePipeHandles(const FunctionCallbackInfo<Value>& args) { | | 24 | + Isolate* isolate = args.GetIsolate(); | | 25 | +#ifdef _WIN32 | | 26 | + Local<Context> context = isolate->GetCurrentContext(); | | 27 | + | | 28 | + HANDLE read_handle = nullptr; | | 29 | + HANDLE write_handle = nullptr; | | 30 | + if (!CreatePipe(&read_handle, &write_handle, nullptr, 0)) { | | 31 | + isolate->ThrowException(v8::Exception::Error( | | 32 | + String::NewFromUtf8(isolate, "CreatePipe failed").ToLocalChecked())); | | 33 | + return; | | 34 | + } | | 35 | + | | 36 | + Local<Object> result = Object::New(isolate); | | 37 | + result | | 38 | + ->Set(context, | | 39 | + String::NewFromUtf8(isolate, "readHandle").ToLocalChecked(), | | 40 | + BigInt::New( | | 41 | + isolate, | | 42 | + static_cast<int64_t>(reinterpret_cast<intptr_t>(read_handle)))) | | 43 | + .Check(); | | 44 | + result | | 45 | + ->Set(context, | | 46 | + String::NewFromUtf8(isolate, "writeHandle").ToLocalChecked(), | | 47 | + BigInt::New( | | 48 | + isolate, | | 49 | + static_cast<int64_t>(reinterpret_cast<intptr_t>(write_handle)))) | | 50 | + .Check(); | | 51 | + args.GetReturnValue().Set(result); | | 52 | +#else | | 53 | + args.GetReturnValue().SetUndefined(); | | 54 | +#endif | | 55 | +} | | 56 | + | | 57 | +} // anonymous namespace | | 58 | + | | 59 | +extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER( | | 60 | + Local<Object> exports, Local<Value> module, Local<Context> context) { | | 61 | + NODE_SET_METHOD(exports, "createPipeHandles", CreatePipeHandles); | | 62 | +} |
Collapse file Copy file name to clipboard +9Lines changed: 9 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +{ | | 2 | + 'targets': [ | | 3 | + { | | 4 | + 'target_name': 'binding', | | 5 | + 'sources': [ 'binding.cc' ], | | 6 | + 'includes': ['../common.gypi'], | | 7 | + }, | | 8 | + ] | | 9 | +} |
Collapse file Copy file name to clipboard +35Lines changed: 35 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | +// Verifies that fs.createReadStream()/createWriteStream() accept a raw Win32 | | 3 | +// HANDLE through the `windowsHandle` option, as happens when a parent process | | 4 | +// passes an inherited anonymous pipe handle. The addon produces such handles | | 5 | +// via CreatePipe(); Node must wrap them in CRT file descriptors instead of | | 6 | +// failing with EBADF. | | 7 | + | | 8 | +const common = require('../../common'); | | 9 | + | | 10 | +if (!common.isWindows) { | | 11 | + common.skip('windowsHandle is Windows-only'); | | 12 | +} | | 13 | + | | 14 | +const assert = require('assert'); | | 15 | +const fs = require('fs'); | | 16 | + | | 17 | +const binding = require(`./build/${common.buildType}/binding`); | | 18 | + | | 19 | +const { readHandle, writeHandle } = binding.createPipeHandles(); | | 20 | +assert.strictEqual(typeof readHandle, 'bigint'); | | 21 | +assert.strictEqual(typeof writeHandle, 'bigint'); | | 22 | + | | 23 | +const payload = 'payload'; | | 24 | + | | 25 | +const chunks = []; | | 26 | +const rs = fs.createReadStream(null, { windowsHandle: readHandle }); | | 27 | +rs.on('error', (err) => assert.fail(err)); | | 28 | +rs.on('data', (chunk) => chunks.push(chunk)); | | 29 | +rs.on('end', common.mustCall(() => { | | 30 | + assert.strictEqual(Buffer.concat(chunks).toString(), payload); | | 31 | +})); | | 32 | + | | 33 | +const ws = fs.createWriteStream(null, { windowsHandle: writeHandle }); | | 34 | +ws.on('error', (err) => assert.fail(err)); | | 35 | +ws.end(payload); |
Collapse file Copy file name to clipboard +47Lines changed: 47 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +// Tests option validation for the `windowsHandle` option of | | 4 | +// fs.createReadStream()/createWriteStream(). The functional round-trip on | | 5 | +// Windows (where a real Win32 HANDLE is wrapped in a file descriptor) is | | 6 | +// covered by test/addons/fs-windows-handle. | | 7 | + | | 8 | +const common = require('../common'); | | 9 | +const assert = require('assert'); | | 10 | +const fs = require('fs'); | | 11 | + | | 12 | +const handle = 1n; | | 13 | + | | 14 | +for (const create of [fs.createReadStream, fs.createWriteStream]) { | | 15 | + assert.throws(() => create(null, { windowsHandle: handle, fd: 2 }), { | | 16 | + code: 'ERR_INCOMPATIBLE_OPTION_PAIR', | | 17 | + }); | | 18 | +} | | 19 | + | | 20 | +if (!common.isWindows) { | | 21 | + for (const create of [fs.createReadStream, fs.createWriteStream]) { | | 22 | + assert.throws(() => create(null, { windowsHandle: handle }), { | | 23 | + code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM', | | 24 | + }); | | 25 | + } | | 26 | + return; | | 27 | +} | | 28 | + | | 29 | +for (const create of [fs.createReadStream, fs.createWriteStream]) { | | 30 | + // Cannot be combined with a custom `fs` implementation. | | 31 | + assert.throws(() => create(null, { windowsHandle: handle, fs: {} }), { | | 32 | + code: 'ERR_METHOD_NOT_IMPLEMENTED', | | 33 | + }); | | 34 | + | | 35 | + // Must be a bigint. | | 36 | + assert.throws(() => create(null, { windowsHandle: 'nope' }), { | | 37 | + code: 'ERR_INVALID_ARG_TYPE', | | 38 | + }); | | 39 | + assert.throws(() => create(null, { windowsHandle: 1 }), { | | 40 | + code: 'ERR_INVALID_ARG_TYPE', | | 41 | + }); | | 42 | + | | 43 | + // Must fit into 64 bits. | | 44 | + assert.throws(() => create(null, { windowsHandle: 2n ** 64n }), { | | 45 | + code: 'ERR_OUT_OF_RANGE', | | 46 | + }); | | 47 | +} |
|