joyent/libuv

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:

  1. 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_);
  1. Start read on the newly created pipe:
    uv_read_start(reinterpret_cast<uv_stream_t*>(&pipe_), onAlloc, onRead);

Child Process:

  1. 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);
  1. Start read on newly created pipe:
    uv_read_start(reinterpret_cast<uv_stream_t*>(&pipe_), onAlloc, onRead);
  1. 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?