I wrote the code that should write some data to the file with the given file descriptor in the child process.
Main file:
'use strict';
const fs = require('fs');
const cp = require('child_process');
function generateClients(n) {
fs.open('clients.csv', 'w', (err, fd) => {
if (err) throw err;
const worker = cp.fork('./process');
worker.send({ fd, n });
worker.on('exit', (code) => {
console.log('Worker exited');
fs.close(fd);
});
});
}
generateClients(100);
Fork module file:
// filename: process.js
'use strict';
const fs = require('fs');
const os = require('os');
const genBirthday = () => '' + (Math.floor(Math.random() * 30) + 1) + '.' + (Math.floor(Math.random() * 11) + 1) + '.' + (Math.floor(Math.random() * 100) + 1900);
const genPassword = () => Math.random().toString(36).slice(-8);
process.on('message', (message) => {
console.log('Hello from worker');
for (let i = 0; i < message.n; i++) {
fs.write(message.fd, genBirthday() + ';' + genPassword() + os.EOL, (err) => {
if (err) process.exit(1);
});
}
});
After execution it gives that result to console:
Hello from worker
Assertion failed: ipc_frame.header.flags <= (UV_IPC_TCP_SERVER | UV_IPC_RAW_DATA | UV_IPC_TCP_CONNECTION), file src\win\pipe.c, line 1601
and file clients.csv is empty.
@primeare I can repro. Do you know if it works on POSIX?
You have to pass file descriptors explicitly via the stdio option when creating the child process:
const worker = cp.fork('./process', { stdio: [0, 1, 2, 'ipc', fd] });
You can also send sockets, server objects, etc. via worker.send(), but I'm not sure you can pass a raw file descriptor...
@mscdex on POSIX it does not explode.
refael@refaelux:/mnt/d/code/temp$ node master.js
Hello from worker
Worker exited
(node:14) DeprecationWarning: Calling an asynchronous function without callback is deprecated.
The file is empty, but it finishes.
worker.send({ fd, n });
Shouldn't this be worker.send({ fd: fd, n: n });?
Shouldn't this be worker.send({ fd: fd, n: n });?
@richardlau enjoy ES2015 NDM: Object initializer
I didn't know that, thanks.
@refack I think it would be practical and convenient to have an opportunity to send file descriptors via child.send() function. Could somebody make changes?
@refack I think it would be practical and convenient to have an opportunity to send file descriptors via child.send() function. Could somebody make changes?
Just to be clear, on POSIX it does not work, just doesn't explode.
If you think about it, what you are trying to do in not trivial. A file handle represents a process's view into a file. The process makes assumptions about that handle, like he know where the file pointer is, and the OS counts the numbers of readers and writers a file has. And so marshaling it to a different process is unsupported by WIN32 or POSIX (just think about how do you multiplex writes, or order reads and writes from different and all the ACID problems of parallel computing).
A solution might be to detect your intentions (not trivial), and open a pipe in the master, then use that pipe to pipe the writes from the slave. But you see...
BTW: @primeare this is a libuv (the library that solves these kind of things for nodejs in an OS agnostic way) issue you could ask there https://github.com/libuv/libuv/issues
@primeare if you look at @mscdex example, turns out nodejs lets you explicitly tell libuv you want to pass a file pointer, and it'll handle the rest.
master
'use strict';
const fs = require('fs');
const cp = require('child_process');
function generateClients(n) {
fs.open('clients.csv', 'w', (err, fd) => {
if (err) throw err;
const worker = cp.fork('./process', { stdio: [0, 1, 2, fd] }); //notice we're passing the fd as stdio[3]
worker.send({ fd, n }, () => {});
worker.on('exit', (code) => {
console.log('Worker exited');
fs.close(fd, () => {});
});
});
}
generateClients(100);
slave
// filename: process.js
'use strict';
const fs = require('fs');
const os = require('os');
const fd = 3 //for the passed stdio[3]
const genBirthday = () => '' + (Math.floor(Math.random() * 30) + 1) + '.' + (Math.floor(Math.random() * 11) + 1) + '.' + (Math.floor(Math.random() * 100) + 1900);
const genPassword = () => Math.random().toString(36).slice(-8);
process.on('message', (message) => {
console.log('Hello from worker');
for (let i = 0; i < message.n; i++) {
try {
fs.writeSync(fd, genBirthday() + ';' + genPassword() + os.EOL)
} catch (err) {
console.log('worker eered');
process.exit(1);
}
}
console.log('Byebye from worker');
process.exit()
});
@refack thanks for explanation 馃槃 I used the solution that @mscdex proposed and it works fine but looks strange. However, problem is solved at that point.
@refack interesting notice: using this workaround we increased file's write speed by at least two-five times. Demons magic 馃槅
increased file's write speed by at least two-five times. Demons magic 馃槅
What did you do before? reopen the file in each worker?
@refack no, file was opened in master and slaves was only writing to file throw file descriptor. No reopens.
@refack I don't know about Windows, but your example will not work on Unix since you need to create an IPC channel explicitly, so the options passed to fork() should be { stdio: [0, 1, 2, 'ipc', fd] } and the file descriptor in the child process will be 4. I believe it is a cross-platform way that should work on Windows too.
@primeare yeah, the solution proposed by @mscdex is correct and there's nothing strange in it. File descriptors can only be shared between the processes using special operating system's facilities (Unix domain sockets and the sendmsg system call on Unix) and there's no way for worker.send() to do that for raw file descriptors inside a message (like your example does) because it basically cannot distinguish between them and arbitrary integer numbers (though it maybe could accept raw file descriptors and not only handles as the second argument?). You could wrap a descriptor into a socket (new net.Socket({ fd })) and send it via worker.send() but, alas, that won't work for files, it must be something that epoll can work with.
@aqrln yes, { stdio: [0, 1, 2, 'ipc', fd] } workaround works cross-platform
@primeare it's not a workaround, that's just the correct way to do it. I admit that it's not that obvious, but well, it is documented.
I think it would be really nice to have a couple of examples in the documentation to avoid confusion.
Also there is a bug in Windows. Since stdio[3] is an icp channel, writing in the slave to fd==3 passes on the slave and crashes the master.
@refack can you please elaborate on that? Does it just exit with an unhandled JavaScript exception or crashes in native code? I can imagine it terminating because of not being able to parse what you have written as JSON, but that's not Windows-related.
Minimal repro on Windows
Master
'use strict';
const cp = require('child_process');
const worker = cp.fork('./process');
console.log('Master - Slave started', worker);
console.log('Master pre send');
worker.send({});
console.log('Master post send');
worker.on('exit', (code) => {
console.log('Master - Slave exited');
});
Slave
'use strict';
const fs = require('fs');
console.log('Slave starter');
process.on('message', (message) => {
console.log('Slave pre write');
fs.writeSync(3, "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU")
console.log('Slave post write');
});
d:\code\temp$ ..\node\Debug\node.exe master.js
Master - Slave started ChildProcess {
domain: null,
_events: { internalMessage: [Function] },
_eventsCount: 1,
_maxListeners: undefined,
_closesNeeded: 2,
_closesGot: 0,
connected: true,
signalCode: null,
exitCode: null,
killed: false,
spawnfile: 'd:\\code\\node\\Debug\\node.exe',
_handle: Process { owner: [Circular], onexit: [Function], pid: 11416 },
spawnargs: [ 'd:\\code\\node\\Debug\\node.exe', './process' ],
pid: 11416,
stdin: null,
stdout: null,
stderr: null,
stdio: [ null, null, null, null ],
channel:
Pipe {
bytesRead: 0,
_externalStream: [External],
fd: -1,
writeQueueSize: 0,
buffering: false,
onread: [Function],
sockets: { got: {}, send: {} } },
_channel: [Getter/Setter],
_handleQueue: null,
_pendingHandle: null,
send: [Function],
_send: [Function],
disconnect: [Function],
_disconnect: [Function] }
Master pre send
Master post send
Slave starter
Slave pre write
Assertion failed: ipc_frame.header.flags <= (UV_IPC_TCP_SERVER | UV_IPC_RAW_DATA | UV_IPC_TCP_CONNECTION), file src\win\pipe.c, line 1601
Slave post write
d:\code\temp$
@refack can you please elaborate on that?
We established that on windows fork opens an ipc channel on stdio[3].
If the slave writes to 3 the master explodes because it can't parse the headers.
Got somthing wierd on Ubuntu:
'use strict';
const cp = require('child_process');
const worker = cp.fork('./process', {stdio: [0,1,2,'ignore','ipc']});
console.log('Master - Slave started', worker);
console.log('Master pre send');
worker.send({});
console.log('Master post send');
worker.on('exit', (code) => {
console.log('Master - Slave exited');
});
// filename: slave.js
'use strict';
const fs = require('fs');
console.log('Slave starter');
process.on('message', (message) => {
console.log('Slave pre write');
fs.writeSync(4, "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU")
console.log('Slave post write');
process.exit()
});
windows
d:\code\temp$ ..\node\Debug\node.exe master.js
Master - Slave started ChildProcess {
domain: null,
_events: { internalMessage: [Function] },
_eventsCount: 1,
_maxListeners: undefined,
_closesNeeded: 2,
_closesGot: 0,
connected: true,
signalCode: null,
exitCode: null,
killed: false,
spawnfile: 'd:\\code\\node\\Debug\\node.exe',
_handle: Process { owner: [Circular], onexit: [Function], pid: 4912 },
spawnargs: [ 'd:\\code\\node\\Debug\\node.exe', './process' ],
pid: 4912,
stdin: null,
stdout: null,
stderr: null,
stdio: [ null, null, null, null, null ],
channel:
Pipe {
bytesRead: 0,
_externalStream: [External],
fd: -1,
writeQueueSize: 0,
buffering: false,
onread: [Function],
sockets: { got: {}, send: {} } },
_channel: [Getter/Setter],
_handleQueue: null,
_pendingHandle: null,
send: [Function],
_send: [Function],
disconnect: [Function],
_disconnect: [Function] }
Master pre send
Master post send
Slave starter
Slave pre write
Assertion failed: ipc_frame.header.flags <= (UV_IPC_TCP_SERVER | UV_IPC_RAW_DATA | UV_IPC_TCP_CONNECTION), file src\win\pipe.c, line 1601
Slave post write
Ubuntu
Enters a weird loop where the whole thing restarts forever.
@refack aha, I see. So, the master program from @primeare's example opens a file and its file descriptor is 3 (from what I see, _open_osfhandle returns the lowest available descriptor). Then it tells the worker process to write data to whatever occurs to be 3, and it writes to the IPC channel instead of a file.
Yep and 馃挜
@aqrln exactly that
@refack Conclusion? Can this be closed?
@refack thanks for explanation 馃槃 I used the solution that @mscdex proposed and it works fine but looks strange. However, problem is solved at that point.
@primeare We're closing this issue as it seems you found a solution. Feel free to reopen if you have new information.
@refack Conclusion? Can this be closed?
The snippet that I've given in https://github.com/nodejs/node/issues/12285#issuecomment-292744276 is just pathological, not a real issue.
Most helpful comment
I didn't know that, thanks.