Creation of full duplex pipe via spawn causes deadlock on Windows.
Closed this issue · 2 comments
Recently tried to use the UV_CREATE_PIPE support in spawn to create a full duplex pipe. The methodology used is as follows:
Parent Process:
- Create a full duplex pipe on stderr shortly before calling spawn:
uv_pipe_init(loop, &pipe_, 0);
const int kFlags = UV_CREATE_PIPE | UV_READABLE_PIPE | UV_WRITABLE_PIPE;
slots[2].flags = static_cast<uv_stdio_flags>(kFlags);
slots[2].data.stream = reinterpret_cast<uv_stream_t*>(&pipe_);
- Start read on the newly created pipe:
uv_read_start(reinterpret_cast<uv_stream_t*>(&pipe_), onAlloc, onRead);
Child Process:
- Create a pipe object using uv_pipe_open:
uv_loop_t* loop = uv_default_loop();
uv_pipe_init(loop, &pipe_, 0);
int rval = uv_pipe_open(&pipe_, 2);
- Start read on newly created pipe:
uv_read_start(reinterpret_cast<uv_stream_t*>(&pipe_), onAlloc, onRead);
- Write a payload on the newly create pipe to send to parent:
const char* kPayload = "Hello Pipe";
uv_write_t* w = static_cast<uv_write_t*>(::calloc(1, sizeof(uv_write_t)));
uv_buf_t buf = uv_buf_init((char*)kPayload, strlen(kPayload));
uv_write(w, reinterpret_cast<uv_stream_t*>(&pipe_), &buf, 1, onWrite);
This works like a champ on Linux/OSX, however on windows the payload from the child never gets sent. Upon inspection, it appears that on Windows, the child's pipe is being created in non-overlapped mode unless you are in ipc mode trying to copy handles:
from process-stdio.c
child_pipe = CreateFileA(pipe_name,
client_access,
0,
&sa,
OPEN_EXISTING,
server_pipe->ipc ? FILE_FLAG_OVERLAPPED : 0,
NULL);
This is a problem in the child process as it is emulating overlapped mode by creating worker threads for both ::ReadFile and ::WriteFile (ie. uv_pipe_zero_readfile_thread_proc and uv_pipe_writefile_thread_proc). If both ReadFile and WriteFile are called on the same pipe handle, the result is deadlock.
I was able to work around this by patching the code to create the child's handle using FILE_FLAG_OVERLAPPED when in full duplex mode (see patch below). The patch is not completely satisfying; thoughts on a cleaner solution?
The Patch:
diff --git a/src/win/process-stdio.c b/src/win/process-stdio.c
index 98566da..c0000f7 100644
--- a/src/win/process-stdio.c
+++ b/src/win/process-stdio.c
@@ -101,6 +101,7 @@ static int uv__create_stdio_pipe_pair(uv_loop_t* loop,
SECURITY_ATTRIBUTES sa;
DWORD server_access = 0;
DWORD client_access = 0;
+ DWORD client_flags = 0;
HANDLE child_pipe = INVALID_HANDLE_VALUE;
int err;
@@ -117,6 +118,13 @@ static int uv__create_stdio_pipe_pair(uv_loop_t* loop,
client_access |= GENERIC_WRITE | FILE_READ_ATTRIBUTES;
}
+ /* If server is in IPC mode, or pipe is full duplex, create the client */
+ /* handle in OVERLAPPED mode. This will allow the the client to read/write */
+ /* the pipe without blocking in WriteFile() */
+ if (server_pipe->ipc || ((flags & UV_WRITABLE_PIPE) && (flags & UV_READABLE_PIPE))) {
+ client_flags |= FILE_FLAG_OVERLAPPED;
+ }
+
/* Create server pipe handle. */
err = uv_stdio_pipe_server(loop,
server_pipe,
@@ -136,7 +144,7 @@ static int uv__create_stdio_pipe_pair(uv_loop_t* loop,
0,
&sa,
OPEN_EXISTING,
- server_pipe->ipc ? FILE_FLAG_OVERLAPPED : 0,
+ client_flags,
NULL);
if (child_pipe == INVALID_HANDLE_VALUE) {
err = GetLastError();
Quick and dirty program demonstrating bug; tested on OSX and Windows.
#include <uv.h>
#include <stdio.h>
#include <stdlib.h>
#if defined(_WIN32)
#include <process.h>
#else
#include <sys/types.h>
#include <unistd.h>
#endif
//*****************************************************************************
// Helpers
int GetPID() {
#if defined(_WIN32)
return _getpid();
#else
return getpid();
#endif
}
void OnExit(uv_process_t* process, int64_t exit_status, int term_signal) {
fprintf(stdout, "[%i] parent on exit status: %lli sig:%i\n", GetPID(), exit_status, term_signal);
}
uv_process_t process_;
uv_pipe_t pipe_;
void onAlloc(uv_handle_t*,
size_t suggested_size,
uv_buf_t* buf)
{
void* b = ::malloc(suggested_size);
*buf = uv_buf_init((char*)b, suggested_size);
}
void onRead(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf)
{
if (nread > 0) {
fprintf(stdout, "[%i] OnRead: ", GetPID());
fwrite(buf->base, 1, nread, stdout);
fprintf(stdout, "\n");
}
}
void onWrite(uv_write_t* req, int status) {
fprintf(stdout, "[%i] onWrite. stopping loop \n", GetPID());
free(req);
uv_stop(uv_default_loop());
}
//*****************************************************************************
// Spawn itself as child process, inheriting stdout for output,
// creating a pipe on stderr to push data back from child process
void doSpawn(char* nm) {
const char* args[3] = {
nm,
"-",
NULL
};
uv_loop_t* loop = uv_default_loop();
uv_process_options_t uvopts;
memset(&uvopts, 0, sizeof(uvopts));
uvopts.file = nm;
uvopts.exit_cb = OnExit;
uvopts.args = (char**)args;
const size_t kNumSlots = 3;
uv_stdio_container_t slots[kNumSlots];
for (size_t i=0; i < kNumSlots; ++i) {
memset(&slots[i], 0, sizeof(uv_stdio_container_t));
slots[i].flags = UV_IGNORE;
}
slots[1].flags = UV_INHERIT_FD;
slots[1].data.fd = 1;
uv_pipe_init(loop, &pipe_, 0);
const int kFlags = UV_CREATE_PIPE | UV_READABLE_PIPE | UV_WRITABLE_PIPE;
slots[2].flags = static_cast<uv_stdio_flags>(kFlags);
slots[2].data.stream = reinterpret_cast<uv_stream_t*>(&pipe_);
uvopts.stdio_count = static_cast<int>(kNumSlots);
uvopts.stdio = slots;
int rval = uv_spawn(loop, &process_, &uvopts);
fprintf(stdout, "[%i] uv_spawn %i\n", GetPID(), rval);
uv_read_start(reinterpret_cast<uv_stream_t*>(&pipe_), onAlloc, onRead);
uv_run(loop, UV_RUN_DEFAULT);
}
//*****************************************************************************
// open the file descriptor in the child process, first writing directly
// later as a pipe.
void doOpen() {
// writing directly to file descriptor works cool and the gang
fprintf(stderr, " Hello FD from [%i]", GetPID());
// opening a pipe in child works as well
uv_loop_t* loop = uv_default_loop();
uv_pipe_init(loop, &pipe_, 0);
int rval = uv_pipe_open(&pipe_, 2);
fprintf(stdout, "[%i] uv_pipe_open %i\n", GetPID(), rval);
rval = uv_read_start(reinterpret_cast<uv_stream_t*>(&pipe_), onAlloc, onRead);
fprintf(stdout, "[%i] read started in child %i\n", GetPID(), rval);
// writing on the pipe works fine on linux, but freezes on windows:
uv_write_t* w = static_cast<uv_write_t*>(::calloc(1, sizeof(uv_write_t)));
const char* kPayload = "Hello Pipe";
uv_buf_t buf = uv_buf_init((char*)kPayload, strlen(kPayload));
fprintf(stdout, "[%i] about to write\n", GetPID());
uv_write(w, reinterpret_cast<uv_stream_t*>(&pipe_), &buf, 1, onWrite);
fprintf(stdout, "[%i] done write %i\n", GetPID(), rval);
uv_run(loop, UV_RUN_DEFAULT);
}
int main (int argc, char** argv) {
int rval = 0;
if (argc == 1) {
fprintf(stdout, "[%i] in parent\n", GetPID());
doSpawn(argv[0]);
} else {
fprintf(stdout, "[%i] in child\n", GetPID());
doOpen();
rval = 1;
}
return rval;
}
What's this?