From: DoI <5291556+denandz@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:13:35 +0000 (+1200) Subject: curl: implement non-blocking STDIN read on Windows X-Git-Tag: rc-8_15_0-1~1 X-Git-Url: http://git.ipfire.org/gitweb/gitweb.cgi?a=commitdiff_plain;h=9a2663322c330ff11275abafd612e9c99407a94a;p=thirdparty%2Fcurl.git curl: implement non-blocking STDIN read on Windows 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 --- diff --git a/src/tool_cb_rea.c b/src/tool_cb_rea.c index da93a9d3d0..8fdd7db9d9 100644 --- a/src/tool_cb_rea.c +++ b/src/tool_cb_rea.c @@ -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)) { diff --git a/src/tool_doswin.c b/src/tool_doswin.c index 46993e43c7..82c8e9ae3c 100644 --- a/src/tool_doswin.c +++ b/src/tool_doswin.c @@ -38,6 +38,7 @@ #include "tool_bname.h" #include "tool_doswin.h" +#include "tool_msgs.h" #include #include /* 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 */ diff --git a/src/tool_doswin.h b/src/tool_doswin.h index 1e9656f30f..92948b9505 100644 --- a/src/tool_doswin.h +++ b/src/tool_doswin.h @@ -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 */ diff --git a/src/tool_operate.c b/src/tool_operate.c index 5f52838ce8..467fd36ea5 100644 --- a/src/tool_operate.c +++ b/src/tool_operate.c @@ -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));