]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
curl: implement non-blocking STDIN read on Windows
authorDoI <5291556+denandz@users.noreply.github.com>
Tue, 10 Jun 2025 11:13:35 +0000 (23:13 +1200)
committerDaniel Stenberg <daniel@haxx.se>
Sat, 21 Jun 2025 09:04:29 +0000 (11:04 +0200)
Implements a seperate read thread for STDIN on Windows when curl is run
with -T/--upload-file .

This uses a similar technique to the nmap/ncat project, spawning a
seperate thread which creates a loop-back bound socket, sending STDIN
into this socket, and reading from the other end of said TCP socket in a
non-blocking way in the rest of curl.

Fixes #17451
Closes #17572

src/tool_cb_rea.c
src/tool_doswin.c
src/tool_doswin.h
src/tool_operate.c

index da93a9d3d021a9c09f45f47f54ac4f54e947426d..8fdd7db9d941feb414132398c19e176fc7157221 100644 (file)
@@ -87,15 +87,39 @@ size_t tool_read_cb(char *buffer, size_t sz, size_t nmemb, void *userdata)
 #endif
   }
 
-  rc = read(per->infd, buffer, sz*nmemb);
-  if(rc < 0) {
-    if(errno == EAGAIN) {
-      CURL_SETERRNO(0);
-      config->readbusy = TRUE;
-      return CURL_READFUNC_PAUSE;
+  /* If we are on Windows, and using `-T .`, then per->infd points to a socket
+   connected to stdin via a reader thread, and needs to be read with recv()
+   Make sure we are in non-blocking mode and infd is not regular stdin
+   On Linux per->infd should be stdin (0) and the block below should not
+   execute */
+  if(!strcmp(per->uploadfile, ".") && per->infd > 0) {
+#if defined(_WIN32) && !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
+    rc = recv(per->infd, buffer, curlx_uztosi(sz * nmemb), 0);
+    if(rc < 0) {
+      if(SOCKERRNO == SOCKEWOULDBLOCK) {
+        CURL_SETERRNO(0);
+        config->readbusy = TRUE;
+        return CURL_READFUNC_PAUSE;
+      }
+
+      rc = 0;
+    }
+#else
+    warnf(per->config->global, "per->infd != 0: FD == %d. This behavior"
+          " is only supported on desktop Windows", per->infd);
+#endif
+  }
+  else {
+    rc = read(per->infd, buffer, sz*nmemb);
+    if(rc < 0) {
+      if(errno == EAGAIN) {
+        CURL_SETERRNO(0);
+        config->readbusy = TRUE;
+        return CURL_READFUNC_PAUSE;
+      }
+      /* since size_t is unsigned we cannot return negative values fine */
+      rc = 0;
     }
-    /* since size_t is unsigned we cannot return negative values fine */
-    rc = 0;
   }
   if((per->uploadfilesize != -1) &&
      (per->uploadedsofar + rc > per->uploadfilesize)) {
index 46993e43c71de634947cfc9266d1a1673d91d80d..82c8e9ae3c0b59426ccd5cae93676f6db6c0cb4b 100644 (file)
@@ -38,6 +38,7 @@
 
 #include "tool_bname.h"
 #include "tool_doswin.h"
+#include "tool_msgs.h"
 
 #include <curlx.h>
 #include <memdebug.h> /* keep this as LAST include */
@@ -741,6 +742,219 @@ CURLcode win32_init(void)
   return CURLE_OK;
 }
 
+#if !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
+/* The following STDIN non - blocking read techniques are heavily inspired
+   by nmap and ncat (https://nmap.org/ncat/) */
+struct win_thread_data {
+  /* This is a copy of the true stdin file handle before any redirection. It is
+      read by the thread. */
+  HANDLE stdin_handle;
+  /* This is the listen socket for the thread. It is closed after the first
+      connection. */
+  curl_socket_t socket_l;
+  /* This is the global config - used for printing errors and so forth */
+  struct GlobalConfig *global;
+};
+
+static DWORD WINAPI win_stdin_thread_func(void *thread_data)
+{
+  struct win_thread_data *tdata = (struct win_thread_data *)thread_data;
+  DWORD n;
+  int nwritten;
+  char buffer[BUFSIZ];
+  BOOL r;
+
+  SOCKADDR_IN clientAddr;
+  int clientAddrLen = sizeof(clientAddr);
+
+  curl_socket_t socket_w = accept(tdata->socket_l, (SOCKADDR*)&clientAddr,
+                           &clientAddrLen);
+
+  if(socket_w == CURL_SOCKET_BAD) {
+    errorf(tdata->global, "accept error: %08lx\n", GetLastError());
+    goto ThreadCleanup;
+  }
+
+  closesocket(tdata->socket_l); /* sclose here fails test 1498 */
+  tdata->socket_l = CURL_SOCKET_BAD;
+  if(shutdown(socket_w, SD_RECEIVE) == SOCKET_ERROR) {
+    errorf(tdata->global, "shutdown error: %08lx\n", GetLastError());
+    goto ThreadCleanup;
+  }
+  for(;;) {
+    r = ReadFile(tdata->stdin_handle, buffer, sizeof(buffer), &n, NULL);
+    if(r == 0)
+      break;
+    if(n == 0)
+      break;
+    nwritten = send(socket_w, buffer, n, 0);
+    if(nwritten == SOCKET_ERROR)
+      break;
+    if((DWORD)nwritten != n)
+      break;
+  }
+ThreadCleanup:
+  CloseHandle(tdata->stdin_handle);
+  tdata->stdin_handle = NULL;
+  if(tdata->socket_l != CURL_SOCKET_BAD) {
+    sclose(tdata->socket_l);
+    tdata->socket_l = CURL_SOCKET_BAD;
+  }
+  if(socket_w != CURL_SOCKET_BAD)
+    sclose(socket_w);
+
+  if(tdata) {
+    free(tdata);
+  }
+
+  return 0;
+}
+
+/* The background thread that reads and buffers the true stdin. */
+static HANDLE stdin_thread = NULL;
+static curl_socket_t socket_r = CURL_SOCKET_BAD;
+
+curl_socket_t win32_stdin_read_thread(struct GlobalConfig *global)
+{
+  int result;
+  bool r;
+  int rc = 0, socksize = 0;
+  struct win_thread_data *tdata = NULL;
+  SOCKADDR_IN selfaddr;
+
+  if(socket_r != CURL_SOCKET_BAD) {
+    assert(stdin_thread != NULL);
+    return socket_r;
+  }
+  assert(stdin_thread == NULL);
+
+  do {
+    /* Prepare handles for thread */
+    tdata = (struct win_thread_data*)calloc(1, sizeof(struct win_thread_data));
+    if(!tdata) {
+      errorf(global, "calloc() error");
+      break;
+    }
+    /* Create the listening socket for the thread. When it starts, it will
+    * accept our connection and begin writing STDIN data to the connection. */
+    tdata->socket_l = WSASocketW(AF_INET, SOCK_STREAM,
+                                 IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
+
+    if(tdata->socket_l == CURL_SOCKET_BAD) {
+      errorf(global, "WSASocketW error: %08lx", GetLastError());
+      break;
+    }
+
+    socksize = sizeof(selfaddr);
+    memset(&selfaddr, 0, socksize);
+    selfaddr.sin_family = AF_INET;
+    selfaddr.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK);
+    /* Bind to any available loopback port */
+    result = bind(tdata->socket_l, (SOCKADDR*)&selfaddr, socksize);
+    if(result == SOCKET_ERROR) {
+      errorf(global, "bind error: %08lx", GetLastError());
+      break;
+    }
+
+    /* Bind to any available loopback port */
+    result = getsockname(tdata->socket_l, (SOCKADDR*)&selfaddr, &socksize);
+    if(result == SOCKET_ERROR) {
+      errorf(global, "getsockname error: %08lx", GetLastError());
+      break;
+    }
+
+    result = listen(tdata->socket_l, 1);
+    if(result == SOCKET_ERROR) {
+      errorf(global, "listen error: %08lx\n", GetLastError());
+      break;
+    }
+
+    /* Make a copy of the stdin handle to be used by win_stdin_thread_func */
+    r = DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_INPUT_HANDLE),
+                        GetCurrentProcess(), &tdata->stdin_handle,
+                        0, FALSE, DUPLICATE_SAME_ACCESS);
+
+    if(!r) {
+      errorf(global, "DuplicateHandle error: %08lx", GetLastError());
+      break;
+    }
+
+    /* Start up the thread. We don't bother keeping a reference to it
+       because it runs until program termination. From here on out all reads
+       from the stdin handle or file descriptor 0 will be reading from the
+       socket that is fed by the thread. */
+    stdin_thread = CreateThread(NULL, 0, win_stdin_thread_func,
+                                tdata, 0, NULL);
+    if(!stdin_thread) {
+      errorf(global, "CreateThread error: %08lx", GetLastError());
+      break;
+    }
+
+    /* Connect to the thread and rearrange our own STDIN handles */
+    socket_r = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+    if(socket_r == CURL_SOCKET_BAD) {
+      errorf(global, "socket error: %08lx", GetLastError());
+      break;
+    }
+
+    /* Hard close the socket on closesocket() */
+    setsockopt(socket_r, SOL_SOCKET, SO_DONTLINGER, 0, 0);
+
+    if(connect(socket_r, (SOCKADDR*)&selfaddr, socksize) == SOCKET_ERROR) {
+      errorf(global, "connect error: %08lx", GetLastError());
+      break;
+    }
+
+    if(shutdown(socket_r, SD_SEND) == SOCKET_ERROR) {
+      errorf(global, "shutdown error: %08lx", GetLastError());
+      break;
+    }
+
+    /* Set the stdin handle to read from the socket. */
+    if(SetStdHandle(STD_INPUT_HANDLE, (HANDLE)socket_r) == 0) {
+      errorf(global, "SetStdHandle error: %08lx", GetLastError());
+      break;
+    }
+
+    rc = 1;
+  } while(0);
+
+  if(rc != 1) {
+    if(socket_r != CURL_SOCKET_BAD && tdata) {
+      if(GetStdHandle(STD_INPUT_HANDLE) == (HANDLE)socket_r &&
+        tdata->stdin_handle) {
+          /* restore STDIN */
+          SetStdHandle(STD_INPUT_HANDLE, tdata->stdin_handle);
+          tdata->stdin_handle = NULL;
+        }
+
+      sclose(socket_r);
+      socket_r = CURL_SOCKET_BAD;
+    }
+
+    if(stdin_thread) {
+      TerminateThread(stdin_thread, 1);
+      stdin_thread = NULL;
+    }
+
+    if(tdata) {
+      if(tdata->stdin_handle)
+        CloseHandle(tdata->stdin_handle);
+      if(tdata->socket_l != CURL_SOCKET_BAD)
+        sclose(tdata->socket_l);
+
+      free(tdata);
+    }
+
+    return CURL_SOCKET_BAD;
+  }
+
+  assert(socket_r != CURL_SOCKET_BAD);
+  return socket_r;
+}
+
+#endif /* !CURL_WINDOWS_UWP && !UNDER_CE */
+
 #endif /* _WIN32 */
 
 #endif /* _WIN32 || MSDOS */
index 1e9656f30f9f81bfe1dbe4b61e1e3ad9872dc9bb..92948b95056f2565de150b2ee93794516ab04c2e 100644 (file)
@@ -55,6 +55,10 @@ CURLcode FindWin32CACert(struct OperationConfig *config,
 struct curl_slist *GetLoadedModulePaths(void);
 CURLcode win32_init(void);
 
+#if !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
+curl_socket_t win32_stdin_read_thread(struct GlobalConfig *global);
+#endif /* !CURL_WINDOWS_UWP && !UNDER_CE */
+
 #endif /* _WIN32 */
 
 #endif /* _WIN32 || MSDOS */
index 5f52838ce820aacd0762c685b9651df82d37b431..467fd36ea54cbcc86720bab7cbf8b26dcba85e1a 100644 (file)
@@ -572,8 +572,22 @@ static CURLcode post_per_transfer(struct GlobalConfig *global,
   if(!curl || !config)
     return result;
 
-  if(per->infdopen)
-    close(per->infd);
+  if(per->uploadfile) {
+    if(!strcmp(per->uploadfile, ".") && per->infd > 0) {
+#if defined(_WIN32) && !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
+      sclose(per->infd);
+#else
+      warnf(per->config->global, "Closing per->infd != 0: FD == "
+            "%d. This behavior is only supported on desktop "
+            " Windows", per->infd);
+#endif
+    }
+  }
+  else {
+    if(per->infdopen) {
+      close(per->infd);
+    }
+  }
 
   if(per->skip)
     goto skip;
@@ -1066,6 +1080,26 @@ static void check_stdin_upload(struct GlobalConfig *global,
 
   CURLX_SET_BINMODE(stdin);
   if(!strcmp(per->uploadfile, ".")) {
+#if defined(_WIN32) && !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
+    /* non-blocking stdin behavior on Windows is challenging
+       Spawn a new thread that will read from stdin and write
+       out to a socket */
+    curl_socket_t f = win32_stdin_read_thread(global);
+
+    if(f == CURL_SOCKET_BAD)
+      warnf(global, "win32_stdin_read_thread returned INVALID_SOCKET "
+            "falling back to blocking mode");
+    else if(f > INT_MAX) {
+      warnf(global, "win32_stdin_read_thread returned identifier "
+            "larger than INT_MAX. This should not happen unless "
+            "the upper 32 bits of a Windows socket have started "
+            "being used for something... falling back to blocking "
+            "mode");
+      sclose(f);
+    }
+    else
+      per->infd = (int)f;
+#endif
     if(curlx_nonblock((curl_socket_t)per->infd, TRUE) < 0)
       warnf(global,
             "fcntl failed on fd=%d: %s", per->infd, strerror(errno));