GeezerVison is showing github.com — fs: add windowsHandle option to file streams · nodejs/node@212fe77
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 |
|---|
@@ -2930,6 +2930,9 @@ behavior is similar to `cp dir1/ dir2/`. | 2930 | 2930 | <!-- YAML | 2931 | 2931 | added: v0.1.31 | 2932 | 2932 | changes: | | 2933 | + - version: REPLACEME | | 2934 | + pr-url: https://github.com/nodejs/node/pull/63851 | | 2935 | + description: Add the `windowsHandle` option. | 2933 | 2936 | - version: v16.10.0 | 2934 | 2937 | pr-url: https://github.com/nodejs/node/pull/40013 | 2935 | 2938 | description: The `fs` option does not need `open` method if an `fd` was provided. | @@ -2986,6 +2989,8 @@ changes: | 2986 | 2989 | * `highWaterMark` {integer} **Default:** `64 * 1024` | 2987 | 2990 | * `fs` {Object|null} **Default:** `null` | 2988 | 2991 | * `signal` {AbortSignal|null} **Default:** `null` | | 2992 | + * `windowsHandle` {bigint} A raw Win32 `HANDLE` value to read from, in place | | 2993 | + of `fd`. Windows only. **Default:** `null` | 2989 | 2994 | * Returns: {fs.ReadStream} | 2990 | 2995 |
| 2991 | 2996 | `options` can include `start` and `end` values to read a range of bytes from | @@ -3006,6 +3011,12 @@ If `fd` points to a character device that only supports blocking reads | 3006 | 3011 | available. This can prevent the process from exiting and the stream from | 3007 | 3012 | closing naturally. | 3008 | 3013 |
| | 3014 | +On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To | | 3015 | +use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle | | 3016 | +obtained from another process, pass it as `windowsHandle`. The handle is wrapped | | 3017 | +in a file descriptor that the stream owns and closes. The `windowsHandle` option | | 3018 | +throws on non-Windows platforms and cannot be combined with the `fs` option. | | 3019 | +
| 3009 | 3020 | By default, the stream will emit a `'close'` event after it has been | 3010 | 3021 | destroyed. Set the `emitClose` option to `false` to change this behavior. | 3011 | 3022 |
| @@ -3056,6 +3067,9 @@ If `options` is a string, then it specifies the encoding. | 3056 | 3067 | <!-- YAML | 3057 | 3068 | added: v0.1.31 | 3058 | 3069 | changes: | | 3070 | + - version: REPLACEME | | 3071 | + pr-url: https://github.com/nodejs/node/pull/63851 | | 3072 | + description: Add the `windowsHandle` option. | 3059 | 3073 | - version: v22.0.0 | 3060 | 3074 | pr-url: https://github.com/nodejs/node/pull/52037 | 3061 | 3075 | description: bump default highWaterMark. | @@ -3120,6 +3134,8 @@ changes: | 3120 | 3134 | [`stream.getDefaultHighWaterMark()`][]. | 3121 | 3135 | * `flush` {boolean} If `true`, the underlying file descriptor is flushed | 3122 | 3136 | prior to closing it. **Default:** `false`. | | 3137 | + * `windowsHandle` {bigint} A raw Win32 `HANDLE` value to write to, in place | | 3138 | + of `fd`. Windows only. **Default:** `null` | 3123 | 3139 | * Returns: {fs.WriteStream} | 3124 | 3140 |
| 3125 | 3141 | `options` may also include a `start` option to allow writing data at some | @@ -3134,6 +3150,12 @@ then the file descriptor won't be closed, even if there's an error. | 3134 | 3150 | It is the application's responsibility to close it and make sure there's no | 3135 | 3151 | file descriptor leak. | 3136 | 3152 |
| | 3153 | +On Windows, a value passed in `fd` is interpreted as a CRT file descriptor. To | | 3154 | +use a raw Win32 `HANDLE` instead, such as an inherited anonymous pipe handle | | 3155 | +obtained from another process, pass it as `windowsHandle`. The handle is wrapped | | 3156 | +in a file descriptor that the stream owns and closes. The `windowsHandle` option | | 3157 | +throws on non-Windows platforms and cannot be combined with the `fs` option. | | 3158 | +
| 3137 | 3159 | By default, the stream will emit a `'close'` event after it has been | 3138 | 3160 | destroyed. Set the `emitClose` option to `false` to change this behavior. | 3139 | 3161 |
| |
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,14 +13,18 @@ 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 { | 23 | 26 | deprecate, | | 27 | + isWindows, | 24 | 28 | kEmptyObject, | 25 | 29 | } = require('internal/util'); | 26 | 30 | const { | @@ -41,6 +45,8 @@ const { | 41 | 45 | } = require('internal/fs/utils'); | 42 | 46 | const { Readable, Writable, finished } = require('stream'); | 43 | 47 | const { toPathIfFileURL } = require('internal/url'); | | 48 | +const binding = internalBinding('fs'); | | 49 | +const { O_RDONLY, O_WRONLY } = internalBinding('constants').fs; | 44 | 50 | const kIoDone = Symbol('kIoDone'); | 45 | 51 | const kIsPerformingIO = Symbol('kIsPerformingIO'); | 46 | 52 |
| @@ -161,6 +167,26 @@ function importFd(stream, options) { | 161 | 167 | ['number', 'FileHandle'], options.fd); | 162 | 168 | } | 163 | 169 |
| | 170 | +function importWindowsHandle(stream, options, flags) { | | 171 | + if (options.windowsHandle == null) { | | 172 | + throw new ERR_MISSING_OPTION('options.windowsHandle'); | | 173 | + } | | 174 | + if (!isWindows) { | | 175 | + throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('windowsHandle'); | | 176 | + } | | 177 | + if (options.fs) { | | 178 | + // The HANDLE is wrapped using the real filesystem, so a custom fs | | 179 | + // implementation cannot be combined with it. | | 180 | + throw new ERR_METHOD_NOT_IMPLEMENTED('windowsHandle with fs'); | | 181 | + } | | 182 | + if (typeof options.windowsHandle !== 'bigint') { | | 183 | + throw new ERR_INVALID_ARG_TYPE('options.windowsHandle', 'bigint', | | 184 | + options.windowsHandle); | | 185 | + } | | 186 | + stream[kFs] = fs; | | 187 | + return binding.handleToFd(options.windowsHandle, flags); | | 188 | +} | | 189 | + | 164 | 190 | function ReadStream(path, options) { | 165 | 191 | if (!(this instanceof ReadStream)) | 166 | 192 | return new ReadStream(path, options); | @@ -174,7 +200,11 @@ function ReadStream(path, options) { | 174 | 200 | options.autoDestroy = false; | 175 | 201 | } | 176 | 202 |
| 177 | | - if (options.fd == null) { | | 203 | + if (options.fd != null && options.windowsHandle != null) { | | 204 | + throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd'); | | 205 | + } else if (options.windowsHandle != null) { | | 206 | + this.fd = getValidatedFd(importWindowsHandle(this, options, O_RDONLY)); | | 207 | + } else if (options.fd == null) { | 178 | 208 | this.fd = null; | 179 | 209 | this[kFs] = options.fs || fs; | 180 | 210 | validateFunction(this[kFs].open, 'options.fs.open'); | @@ -331,7 +361,11 @@ function WriteStream(path, options) { | 331 | 361 | // Only buffers are supported. | 332 | 362 | options.decodeStrings = true; | 333 | 363 |
| 334 | | - if (options.fd == null) { | | 364 | + if (options.fd != null && options.windowsHandle != null) { | | 365 | + throw new ERR_INCOMPATIBLE_OPTION_PAIR('windowsHandle', 'fd'); | | 366 | + } else if (options.windowsHandle != null) { | | 367 | + this.fd = getValidatedFd(importWindowsHandle(this, options, O_WRONLY)); | | 368 | + } else if (options.fd == null) { | 335 | 369 | this.fd = null; | 336 | 370 | this[kFs] = options.fs || fs; | 337 | 371 | 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 |
|---|
@@ -4144,6 +4144,33 @@ InternalFieldInfoBase* BindingData::Serialize(int index) { | 4144 | 4144 | return info; | 4145 | 4145 | } | 4146 | 4146 |
| | 4147 | +#ifdef _WIN32 | | 4148 | +static void HandleToFd(const FunctionCallbackInfo<Value>& args) { | | 4149 | + Environment* env = Environment::GetCurrent(args); | | 4150 | + CHECK_GE(args.Length(), 1); | | 4151 | + CHECK(args[0]->IsBigInt()); | | 4152 | + | | 4153 | + int flags = 0; | | 4154 | + if (args[1]->IsNumber()) { | | 4155 | + flags = args[1].As<Int32>()->Value(); | | 4156 | + } | | 4157 | + | | 4158 | + bool lossless; | | 4159 | + int64_t handle = args[0].As<BigInt>()->Int64Value(&lossless); | | 4160 | + if (!lossless) { | | 4161 | + return THROW_ERR_OUT_OF_RANGE(env, | | 4162 | + "windowsHandle does not fit into 64 bits"); | | 4163 | + } | | 4164 | + intptr_t value = static_cast<intptr_t>(handle); | | 4165 | + | | 4166 | + int fd = _open_osfhandle(value, flags); | | 4167 | + if (fd == -1) { | | 4168 | + return env->ThrowErrnoException(errno, "_open_osfhandle"); | | 4169 | + } | | 4170 | + args.GetReturnValue().Set(fd); | | 4171 | +} | | 4172 | +#endif // _WIN32 | | 4173 | + | 4147 | 4174 | void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data, | 4148 | 4175 | Local<ObjectTemplate> target) { | 4149 | 4176 | Isolate* isolate = isolate_data->isolate(); | @@ -4210,6 +4237,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, | 4210 | 4237 |
| 4211 | 4238 | SetMethod(isolate, target, "mkdtemp", Mkdtemp); | 4212 | 4239 |
| | 4240 | +#ifdef _WIN32 | | 4241 | + SetMethod(isolate, target, "handleToFd", HandleToFd); | | 4242 | +#endif | | 4243 | + | 4213 | 4244 | SetMethod(isolate, target, "cpSyncCheckPaths", CpSyncCheckPaths); | 4214 | 4245 | SetMethod(isolate, target, "cpSyncOverrideFile", CpSyncOverrideFile); | 4215 | 4246 | SetMethod(isolate, target, "cpSyncCopyDir", CpSyncCopyDir); | @@ -4337,6 +4368,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { | 4337 | 4368 | registry->Register(LUTimes); | 4338 | 4369 |
| 4339 | 4370 | registry->Register(Mkdtemp); | | 4371 | +#ifdef _WIN32 | | 4372 | + registry->Register(HandleToFd); | | 4373 | +#endif | 4340 | 4374 | registry->Register(NewFSReqCallback); | 4341 | 4375 |
| 4342 | 4376 | 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 | +} |
|