Previously, cygwin read pipe used non-blocking mode althogh non- cygwin app uses blocking-mode by default. Despite this requirement, if a cygwin app is executed from a non-cygwwin app and the cygwin app exits, read pipe remains on non-blocking mode because of the commit fc691d0246b9. Due to this behaviour, the non-cygwin app cannot read the pipe correctly after that. Similarly, if a non- cygwin app is executed from a cygwin app and the non-cygwin app exits, the read pipe mode remains on blocking mode although cygwin read pipe should be non-blocking mode.
These bugs were provoked by pipe mode toggling between cygwin and non-cygwin apps. To make management of pipe mode simpler, This patch has re-designed the pipe implementation. In this new implementation, both read and wrie pipe basically use only blocking mode and the behaviour corresponding to the pipe mode is simulated in raw_read() and raw_wrie(). Only when the query handle is not available, the raw_write() cannot simulate non-blocking access. Therefore, the pipe mode is temporarily changed to non-blocking mode. This happens only if the pipe is created by non-cygwin app and cygwin inherits that. Addresses: https://github.com/git-for-windows/git/issues/5115 Fixes: fc691d0246b9 ("Cygwin: pipe: Make sure to set read pipe non-blocking for cygwin apps."); Reported-by: isaacag, Johannes Schindelin <johannes.schinde...@gmx.de> Reviewed-by: Signed-off-by: Takashi Yano <takashi.y...@nifty.ne.jp> --- winsup/cygwin/dtable.cc | 5 +- winsup/cygwin/fhandler/pipe.cc | 352 ++++++++++-------------- winsup/cygwin/local_includes/fhandler.h | 4 +- winsup/cygwin/select.cc | 19 +- 4 files changed, 157 insertions(+), 223 deletions(-) diff --git a/winsup/cygwin/dtable.cc b/winsup/cygwin/dtable.cc index 9508f3e0b..7303f7eac 100644 --- a/winsup/cygwin/dtable.cc +++ b/winsup/cygwin/dtable.cc @@ -410,9 +410,8 @@ dtable::init_std_file_from_handle (int fd, HANDLE handle) { fhandler_pipe *fhp = (fhandler_pipe *) fh; fhp->set_pipe_buf_size (); - /* Set read pipe always to nonblocking */ - fhp->set_pipe_non_blocking (fhp->get_device () == FH_PIPER ? - true : fhp->is_nonblocking ()); + /* Set pipe always blocking */ + fhp->set_pipe_non_blocking (false); } if (!fh->open_setup (openflags)) diff --git a/winsup/cygwin/fhandler/pipe.cc b/winsup/cygwin/fhandler/pipe.cc index c686df650..942de82b2 100644 --- a/winsup/cygwin/fhandler/pipe.cc +++ b/winsup/cygwin/fhandler/pipe.cc @@ -48,7 +48,7 @@ fhandler_pipe::fhandler_pipe () In addition to setting the blocking mode of the pipe handle, it also sets the pipe's read mode to byte_stream unconditionally. */ -void +bool fhandler_pipe::set_pipe_non_blocking (bool nonblocking) { NTSTATUS status; @@ -62,6 +62,7 @@ fhandler_pipe::set_pipe_non_blocking (bool nonblocking) FilePipeInformation); if (!NT_SUCCESS (status)) debug_printf ("NtSetInformationFile(FilePipeInformation): %y", status); + return NT_SUCCESS (status); } int @@ -91,10 +92,10 @@ fhandler_pipe::init (HANDLE f, DWORD a, mode_t mode, int64_t uniq_id) set_ino (uniq_id); set_unique_id (uniq_id | !!(mode & GENERIC_WRITE)); if (opened_properly) - /* Set read pipe always nonblocking to allow signal handling - even with FILE_SYNCHRONOUS_IO_NONALERT. */ - set_pipe_non_blocking (get_device () == FH_PIPER ? - true : is_nonblocking ()); + /* Set pipe always blocking and simulate non-blocking mode in + raw_read()/raw_write() to allow signal handling even with + FILE_SYNCHRONOUS_IO_NONALERT. */ + set_pipe_non_blocking (false); /* Store pipe name to path_conv pc for query_hdl check */ if (get_dev () == FH_PIPEW) @@ -333,17 +334,42 @@ fhandler_pipe::raw_read (void *ptr, size_t& len) ULONG len1 = (ULONG) (len - nbytes); DWORD select_sem_timeout = 0; - FILE_PIPE_LOCAL_INFORMATION fpli; - status = NtQueryInformationFile (get_handle (), &io, - &fpli, sizeof (fpli), - FilePipeLocalInformation); - if (NT_SUCCESS (status)) + DWORD n; + BOOL ret = PeekNamedPipe (get_handle (), NULL, 0, NULL, &n, NULL); + if (!ret) /* Broken pipe ? */ + break; + if (n == 0) { - if (fpli.ReadDataAvailable == 0 && nbytes != 0) + if (nbytes != 0) break; + if (is_nonblocking ()) + { + set_errno (EAGAIN); + nbytes = (size_t) -1; + break; + } + /* If the pipe is a non-cygwin pipe, select_sem trick + does not work. As a result, the following cygwait() + will return only after timeout occurs. This causes + performance degradation. However, setting timeout + to zero causes high CPU load. So, set timeout to + non-zero only when select_sem is valid or pipe is + not ready to read for more than t0_threshold. + This prevents both the performance degradation and + the high CPU load. */ + if (select_sem || GetTickCount64 () - t0 > t0_threshold) + select_sem_timeout = 1; + waitret = cygwait (select_sem, select_sem_timeout); + if (waitret == WAIT_CANCELED) + pthread::static_cancel_self (); + else if (waitret == WAIT_SIGNALED) + { + set_errno (EINTR); + nbytes = (size_t) -1; + break; + } + continue; } - else if (nbytes != 0) - break; status = NtReadFile (get_handle (), NULL, NULL, NULL, &io, ptr, len1, NULL, NULL); if (isclosed ()) /* A signal handler might have closed the fd. */ @@ -368,37 +394,6 @@ fhandler_pipe::raw_read (void *ptr, size_t& len) case STATUS_PIPE_BROKEN: /* This is really EOF. */ break; - case STATUS_PIPE_LISTENING: - case STATUS_PIPE_EMPTY: - if (nbytes != 0) - break; - if (is_nonblocking ()) - { - set_errno (EAGAIN); - nbytes = (size_t) -1; - break; - } - /* If the pipe is a non-cygwin pipe, select_sem trick - does not work. As a result, the following cygwait() - will return only after timeout occurs. This causes - performance degradation. However, setting timeout - to zero causes high CPU load. So, set timeout to - non-zero only when select_sem is valid or pipe is - not ready to read for more than t0_threshold. - This prevents both the performance degradation and - the high CPU load. */ - if (select_sem || GetTickCount64 () - t0 > t0_threshold) - select_sem_timeout = 1; - waitret = cygwait (select_sem, select_sem_timeout); - if (waitret == WAIT_CANCELED) - pthread::static_cancel_self (); - else if (waitret == WAIT_SIGNALED) - { - set_errno (EINTR); - nbytes = (size_t) -1; - break; - } - continue; default: __seterrno_from_nt_status (status); nbytes = (size_t) -1; @@ -446,12 +441,20 @@ fhandler_pipe_fifo::raw_write (const void *ptr, size_t len) return -1; } - if (len <= pipe_buf_size || pipe_buf_size == 0) + bool query_hdl_available = true; + ssize_t avail = pipe_buf_size; + if (is_nonblocking ()) + avail = pipe_data_available (-1, this, get_handle (), PDA_WRITE); + if (avail == 0) + { + set_errno (EAGAIN); + return -1; + } + + if (len <= (size_t) avail || pipe_buf_size == 0) chunk = len; - else if (is_nonblocking ()) - chunk = len = pipe_buf_size; else - chunk = pipe_buf_size; + chunk = avail; if (!(evt = CreateEvent (NULL, false, false, NULL))) { @@ -459,6 +462,28 @@ fhandler_pipe_fifo::raw_write (const void *ptr, size_t len) return -1; } + if (is_nonblocking ()) + { + fhandler_pipe *fh = (fhandler_pipe *) this; + query_hdl_available = + fh->get_query_handle () || fh->temporary_query_hdl (); + if (avail > 1 || query_hdl_available) + len = chunk; + else if (avail == 0) /* The pipe is really full. */ + { + set_errno (EAGAIN); + return -1; + } + else if (!fh->set_pipe_non_blocking (true)) + /* NtSetInformationFile() in set_pipe_non_blocking(true) + fails for unknown reasons if the pipe is not empty. + In this case, no pipe reader should be reading the pipe, + so pipe_data_available() has returned correct value. */ + len = chunk; /* The pipe is not empty. */ + else + chunk = len = min (len, pipe_buf_size); /* The pipe is empty. */ + } + /* Write in chunks, accumulating a total. If there's an error, just return the accumulated total unless the first write fails, in which case return -1. */ @@ -528,6 +553,12 @@ fhandler_pipe_fifo::raw_write (const void *ptr, size_t len) waitret = WAIT_SIGNALED; break; } + if (waitret == WAIT_TIMEOUT && is_nonblocking ()) + { + CancelIo (get_handle ()); + set_errno (EAGAIN); + goto out; + } cygwait (select_sem, 10, cw_cancel); } /* Loop in case of blocking write or SA_RESTART */ @@ -610,6 +641,9 @@ fhandler_pipe_fifo::raw_write (const void *ptr, size_t len) break; } out: + if (avail == 1 && !query_hdl_available) + ((fhandler_pipe *) this)->set_pipe_non_blocking (false); + CloseHandle (evt); if (status == STATUS_THREAD_SIGNALED && nbytes == 0) set_errno (EINTR); @@ -649,17 +683,6 @@ fhandler_pipe::fixup_after_fork (HANDLE parent) ReleaseMutex (hdl_cnt_mtx); } -void -fhandler_pipe::fixup_after_exec () -{ - /* Set read pipe itself always non-blocking for cygwin process. - Blocking/non-blocking is simulated in raw_read(). For write - pipe, follow is_nonblocking(). */ - bool mode = get_device () == FH_PIPEW ? is_nonblocking () : true; - set_pipe_non_blocking (mode); - fhandler_base::fixup_after_exec (); -} - int fhandler_pipe::dup (fhandler_base *child, int flags) { @@ -745,6 +768,7 @@ fhandler_pipe::close () } #define PIPE_INTRO "\\\\.\\pipe\\cygwin-" +#define PIPE_INTRO_NT "\\\\.\\pipe\\" /* Create a pipe, and return handles to the read and write ends, just like CreatePipe, but ensure that the write end permits @@ -769,8 +793,16 @@ fhandler_pipe::create (LPSECURITY_ATTRIBUTES sa_ptr, PHANDLE r, PHANDLE w, psize = DEFAULT_PIPEBUFSIZE; char pipename[MAX_PATH]; - size_t len = __small_sprintf (pipename, PIPE_INTRO "%S-", - &cygheap->installation_key); + size_t len = __small_sprintf(pipename, "%S-%u-pipe-nt-0x", + &cygheap->installation_key, + GetCurrentProcessId ()); + bool nt = !strncmp (name, pipename, len); + if (nt) + len = __small_sprintf (pipename, PIPE_INTRO_NT "%S-", + &cygheap->installation_key); + else + len = __small_sprintf (pipename, PIPE_INTRO "%S-", + &cygheap->installation_key); DWORD pipe_mode = PIPE_READMODE_BYTE | PIPE_REJECT_REMOTE_CLIENTS; if (!name) pipe_mode |= pipe_byte ? PIPE_TYPE_BYTE : PIPE_TYPE_MESSAGE; @@ -821,11 +853,27 @@ fhandler_pipe::create (LPSECURITY_ATTRIBUTES sa_ptr, PHANDLE r, PHANDLE w, definitely required for pty handling since fhandler_pty_master writes to the pipe in chunks, terminated by newline when CANON mode is specified. */ - *r = CreateNamedPipe (pipename, open_mode, pipe_mode, 1, psize, - psize, NMPWAIT_USE_DEFAULT_WAIT, sa_ptr); + /* PIPE_ACCESS_OUTBOUND is added to acquire FILE_WRITE_ATTRIBUTES right. + Unnecessary rights such as FILE_WRITE_DATA will be dropped later. */ + *r = CreateNamedPipe (pipename, open_mode | PIPE_ACCESS_OUTBOUND, + pipe_mode, 1, psize, psize, + NMPWAIT_USE_DEFAULT_WAIT, sa_ptr); if (*r != INVALID_HANDLE_VALUE) { + /* Drop unnecessary access rights. */ + HANDLE rtmp; + DWORD access = GENERIC_READ | FILE_WRITE_ATTRIBUTES; + if (nt) + /* Add this right as a marker of cygwin read pipe for query_hdl */ + access |= FILE_WRITE_EA; + if (DuplicateHandle (GetCurrentProcess (), r, + GetCurrentProcess (), &rtmp, + access, sa_ptr->bInheritHandle, 0)) + { + CloseHandle (*r); + *r = rtmp; + } debug_printf ("pipe read handle %p", *r); err = 0; break; @@ -916,6 +964,11 @@ is_running_as_service (void) simplicity, nt_create will omit the 'open_mode' and 'name' parameters, which aren't needed for our purposes. */ +/* Regardless of above comment, the current nt_create() is reverted to just + call fhandler_pipe::create() to allow adding FILE_FLAG_OVERLAPPED flag. + This is needed for query_hdl so that PeekNamedPipe() and NtQueryObject() + are not blocked even while reader is reading the pipe. */ + static int nt_create (LPSECURITY_ATTRIBUTES, HANDLE &, HANDLE &, DWORD, int64_t *); @@ -955,8 +1008,7 @@ fhandler_pipe::create (fhandler_pipe *fhs[2], unsigned psize, int mode) 0, sa->bInheritHandle, DUPLICATE_SAME_ACCESS)) goto err_close_select_sem0; - if (is_running_as_service () && - !DuplicateHandle (GetCurrentProcess (), r, + if (!DuplicateHandle (GetCurrentProcess (), r, GetCurrentProcess (), &fhs[1]->query_hdl, FILE_READ_DATA, sa->bInheritHandle, 0)) goto err_close_select_sem1; @@ -1005,131 +1057,29 @@ out: return res; } +/* Current nt_create() does not use NtCreateNamedPipe() at all. The + function name is just inherited from previous implementation of + fhandler_pipe. */ static int nt_create (LPSECURITY_ATTRIBUTES sa_ptr, HANDLE &r, HANDLE &w, DWORD psize, int64_t *unique_id) { - NTSTATUS status; - HANDLE npfsh; - ACCESS_MASK access; - OBJECT_ATTRIBUTES attr; - IO_STATUS_BLOCK io; - LARGE_INTEGER timeout; - - /* Default to error. */ - r = NULL; - w = NULL; - - status = fhandler_base::npfs_handle (npfsh); - if (!NT_SUCCESS (status)) - { - __seterrno_from_nt_status (status); - return GetLastError (); - } - - /* Ensure that there is enough pipe buffer space for atomic writes. */ - if (!psize) - psize = DEFAULT_PIPEBUFSIZE; - - UNICODE_STRING pipename; - WCHAR pipename_buf[MAX_PATH]; - size_t len = __small_swprintf (pipename_buf, L"%S-%u-", - &cygheap->installation_key, - GetCurrentProcessId ()); - - access = GENERIC_READ | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE; - access |= FILE_WRITE_EA; /* Add this right as a marker of cygwin read pipe */ - - ULONG pipe_type = pipe_byte ? FILE_PIPE_BYTE_STREAM_TYPE - : FILE_PIPE_MESSAGE_TYPE; - - /* Retry NtCreateNamedPipeFile as long as the pipe name is in use. - Retrying will probably never be necessary, but we want - to be as robust as possible. */ - DWORD err = 0; - while (!r) - { - static volatile ULONG pipe_unique_id; - LONG id = InterlockedIncrement ((LONG *) &pipe_unique_id); - __small_swprintf (pipename_buf + len, L"pipe-nt-%p", id); - if (unique_id) - *unique_id = ((int64_t) id << 32 | GetCurrentProcessId ()); - - debug_printf ("name %W, size %u, mode %s", pipename_buf, psize, - (pipe_type & FILE_PIPE_MESSAGE_TYPE) - ? "PIPE_TYPE_MESSAGE" : "PIPE_TYPE_BYTE"); - - RtlInitUnicodeString (&pipename, pipename_buf); - - InitializeObjectAttributes (&attr, &pipename, - sa_ptr->bInheritHandle ? OBJ_INHERIT : 0, - npfsh, sa_ptr->lpSecurityDescriptor); - - timeout.QuadPart = -500000; - /* Set FILE_SYNCHRONOUS_IO_NONALERT flag so that native - C# programs work with cygwin pipe. */ - status = NtCreateNamedPipeFile (&r, access, &attr, &io, - FILE_SHARE_READ | FILE_SHARE_WRITE, - FILE_CREATE, - FILE_SYNCHRONOUS_IO_NONALERT, pipe_type, - FILE_PIPE_BYTE_STREAM_MODE, - 0, 1, psize, psize, &timeout); - - if (NT_SUCCESS (status)) - { - debug_printf ("pipe read handle %p", r); - err = 0; - break; - } - - switch (status) - { - case STATUS_PIPE_BUSY: - case STATUS_INSTANCE_NOT_AVAILABLE: - case STATUS_PIPE_NOT_AVAILABLE: - /* The pipe is already open with compatible parameters. - Pick a new name and retry. */ - debug_printf ("pipe busy, retrying"); - r = NULL; - break; - case STATUS_ACCESS_DENIED: - /* The pipe is already open with incompatible parameters. - Pick a new name and retry. */ - debug_printf ("pipe access denied, retrying"); - r = NULL; - break; - default: - { - __seterrno_from_nt_status (status); - err = GetLastError (); - debug_printf ("failed, %E"); - r = NULL; - } - } - } - - if (err) - { - r = NULL; - return err; - } - - debug_printf ("NtOpenFile: name %S", &pipename); - - access = GENERIC_WRITE | FILE_READ_ATTRIBUTES | SYNCHRONIZE; - status = NtOpenFile (&w, access, &attr, &io, 0, 0); - if (!NT_SUCCESS (status)) - { - DWORD err = GetLastError (); - debug_printf ("NtOpenFile failed, r %p, %E", r); - if (r) - NtClose (r); - w = NULL; - return err; - } - - /* Success. */ - return 0; + char name[MAX_PATH]; + size_t len = __small_sprintf (name, "%u-", GetCurrentProcessId ()); + static volatile ULONG pipe_unique_id; + LONG id = InterlockedIncrement ((LONG *) &pipe_unique_id); + __small_sprintf (name + len, "pipe-nt-%p", id); + *unique_id = ((int64_t) id << 32 | GetCurrentProcessId ()); + + /* FILE_FLAG_OVERLAPPED is added here to allow PeekNamedPipe() and + NtQueryObject() be not blocked even while reader is reading the + pipe. + Accordig to the official document, to access the handle opened + with FILE_FLAG_OVERLAPPED, it is mandatory to pass the OVERLAPPED + structure. However, in fact, it seems that the access will fallback + to the blocking access if it is not specified. */ + return fhandler_pipe::create (sa_ptr, &r, &w, psize, name, + FILE_FLAG_OVERLAPPED, unique_id); } /* Called by dtable::init_std_file_from_handle for stdio handles @@ -1174,22 +1124,6 @@ fhandler_pipe::ioctl (unsigned int cmd, void *p) return 0; } -int -fhandler_pipe::fcntl (int cmd, intptr_t arg) -{ - if (cmd != F_SETFL) - return fhandler_base::fcntl (cmd, arg); - - const bool was_nonblocking = is_nonblocking (); - int res = fhandler_base::fcntl (cmd, arg); - const bool now_nonblocking = is_nonblocking (); - /* Do not set blocking mode for read pipe to allow signal handling - even with FILE_SYNCHRONOUS_IO_NONALERT. */ - if (now_nonblocking != was_nonblocking && get_device () != FH_PIPER) - set_pipe_non_blocking (now_nonblocking); - return res; -} - int fhandler_pipe::fstat (struct stat *buf) { @@ -1262,11 +1196,8 @@ fhandler_pipe::get_query_hdl_per_process (OBJECT_NAME_INFORMATION *ntfn) NTSTATUS status; ULONG len; - /* Non-cygwin app may call ReadFile() for empty pipe, which makes - NtQueryObject() for ObjectNameInformation block. Therefore, do - not try to get query_hdl for non-cygwin apps. */ _pinfo *p = pids[i]; - if (!p || ISSTATE (p, PID_NOTCYGWIN)) + if (!p) continue; HANDLE proc = OpenProcess (PROCESS_DUP_HANDLE @@ -1361,18 +1292,11 @@ fhandler_pipe::spawn_worker (int fileno_stdin, int fileno_stdout, if (cfd->get_dev () == FH_PIPEW && (fd == fileno_stdout || fd == fileno_stderr)) { + /* Setup for query_hdl stuff. Read the comment below. */ fhandler_pipe *pipe = (fhandler_pipe *)(fhandler_base *) cfd; - pipe->set_pipe_non_blocking (false); - - /* Setup for query_ndl stuff. Read the comment below. */ if (pipe->request_close_query_hdl ()) need_send_noncygchld_sig = true; } - else if (cfd->get_dev () == FH_PIPER && fd == fileno_stdin) - { - fhandler_pipe *pipe = (fhandler_pipe *)(fhandler_base *) cfd; - pipe->set_pipe_non_blocking (false); - } /* If multiple writers including non-cygwin app exist, the non-cygwin app cannot detect pipe closure on the read side when the pipe is diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/local_includes/fhandler.h index 1c339cfbc..a6281d0c0 100644 --- a/winsup/cygwin/local_includes/fhandler.h +++ b/winsup/cygwin/local_includes/fhandler.h @@ -1234,13 +1234,11 @@ public: int open (int flags, mode_t mode = 0); bool open_setup (int flags); void fixup_after_fork (HANDLE); - void fixup_after_exec (); int dup (fhandler_base *child, int); void set_close_on_exec (bool val); int close (); void raw_read (void *ptr, size_t& len); int ioctl (unsigned int cmd, void *); - int fcntl (int cmd, intptr_t); int fstat (struct stat *buf); int fstatvfs (struct statvfs *buf); int fadvise (off_t, off_t, int); @@ -1265,7 +1263,7 @@ public: fh->copy_from (this); return fh; } - void set_pipe_non_blocking (bool nonblocking); + bool set_pipe_non_blocking (bool nonblocking); HANDLE get_query_handle () const { return query_hdl; } void close_query_handle () { diff --git a/winsup/cygwin/select.cc b/winsup/cygwin/select.cc index bc02c3f9d..672e856ef 100644 --- a/winsup/cygwin/select.cc +++ b/winsup/cygwin/select.cc @@ -642,7 +642,7 @@ pipe_data_available (int fd, fhandler_base *fh, HANDLE h, int flags) Consequentially, the only reliable information is available on the read side, so fetch info from the read side via the pipe-specific query handle. Use fpli.WriteQuotaAvailable as storage for the actual - interesting value, which is the InboundQuote on the write side, + interesting value, which is the OutboundQuota on the write side, decremented by the number of bytes of data in that buffer. */ /* Note: Do not use NtQueryInformationFile() for query_hdl because NtQueryInformationFile() seems to interfere with reading pipes @@ -659,7 +659,20 @@ pipe_data_available (int fd, fhandler_base *fh, HANDLE h, int flags) if (!query_hdl) query_hdl = ((fhandler_pipe *) fh)->temporary_query_hdl (); if (!query_hdl) /* We cannot know actual write pipe space. */ - return (flags & PDA_SELECT) ? PIPE_BUF : 1; + { + /* NtSetInformationFile() in set_pipe_non_blocking(true) + fails for unknown reasons if the pipe is not empty. + In this case, no pipe reader should be reading the pipe, + so the pipe is really full if WriteQuotaAvailable + is zero.*/ + if (!((fhandler_pipe *) fh)->set_pipe_non_blocking (true)) + return 0; + /* Restore pipe mode to blocking mode */ + ((fhandler_pipe *) fh)->set_pipe_non_blocking (false); + /* The pipe is empty here because set_pipe_non_blocking(true) + has succeeded. */ + return fpli.OutboundQuota;; + } DWORD nbytes_in_pipe; BOOL res = PeekNamedPipe (query_hdl, NULL, 0, NULL, &nbytes_in_pipe, NULL); @@ -667,7 +680,7 @@ pipe_data_available (int fd, fhandler_base *fh, HANDLE h, int flags) CloseHandle (query_hdl); /* Close temporary query_hdl */ if (!res) /* We cannot know actual write pipe space. */ return (flags & PDA_SELECT) ? PIPE_BUF : 1; - fpli.WriteQuotaAvailable = fpli.InboundQuota - nbytes_in_pipe; + fpli.WriteQuotaAvailable = fpli.OutboundQuota - nbytes_in_pipe; } if (fpli.WriteQuotaAvailable > 0) { -- 2.45.1