Trac registration request and bug/fix submission
Shifu Jiang <[email protected]>
| Newsgroups | gmane.editors.lyx.devel |
|---|---|
| Message-ID | <CAGVjYf=uLu2dEwpuS1nUCtGF77-WF1zLCM_4XPY35XzenGpQ_g@mail.gmail.com> |
Hi, I would like to register on lyx trac and submit a bug ticket with the proposed fix. The problem: On Windows with LyX 2.5.1, LyXServer drops roughly half of the responses to client commands, even though every command is executed. Clients that poll the reply pipe intermittently receive nothing, must time out and retry, and after enough round-trips can wedge the server permanently (all pipe instances consumed) until LyX is restarted. Please see orig and patch attached. Best, Shifu -- lyx-devel mailing list [email protected] https://lists.lyx.org/mailman/listinfo/lyx-devel
lyxserver-windows-pipe-delivery-race.patch
(application/octet-stream, 14.6 KB)
diff --git a/src/Server.cpp b/src/Server.cpp
index 522060e..1d9748b 100644
--- a/src/Server.cpp
+++ b/src/Server.cpp
@@ -132,6 +132,7 @@ DWORD WINAPI pipeServerWrapper(void * arg)
lyxcomm->closeHandles();
CloseHandle(lyxcomm->server_thread_);
CloseHandle(lyxcomm->stopserver_);
+ CloseHandle(lyxcomm->reply_event_);
CloseHandle(lyxcomm->outbuf_mutex_);
lyxerr << "LyXComm: Closing connection" << endl;
}
@@ -141,6 +142,7 @@ DWORD WINAPI pipeServerWrapper(void * arg)
LyXComm::LyXComm(string const & pip, Server * cli, ClientCallbackfct ccb)
: stopserver_(0),
+ reply_event_(0),
ready_(false), pipename_(pip), client_(cli), clientcb_(ccb),
deferred_loading_(false)
{
@@ -197,6 +199,8 @@ bool LyXComm::pipeServer()
// Add the stopserver_ event
event_[MAX_PIPES] = stopserver_;
+ // Add the reply event (set by send() when a reply is appended to outbuf_)
+ event_[MAX_PIPES + 1] = reply_event_;
// We made it!
LYXERR(Debug::LYXSERVER, "LyXComm: Connection established");
@@ -207,45 +211,75 @@ bool LyXComm::pipeServer()
while (!checkStopServer()) {
// Indefinitely wait for the completion of an overlapped
- // read, write, or connect operation.
- DWORD wait = WaitForMultipleObjects(MAX_PIPES + 1, event_,
+ // read, write, or connect operation, or for a new reply.
+ DWORD wait = WaitForMultipleObjects(MAX_PIPES + 2, event_,
FALSE, INFINITE);
// Determine which pipe instance completed the operation.
i = wait - WAIT_OBJECT_0;
- LASSERT(i <= MAX_PIPES, /**/);
+ LASSERT(i <= MAX_PIPES + 1, /**/);
// Check whether we were waked up for stopping the pipe server.
if (i == MAX_PIPES)
break;
+ if (i == MAX_PIPES + 1) {
+ // A reply was appended to outbuf_ by send() while the
+ // loop was idle. Without this event the reply used to
+ // wait for the next connection, where the client's
+ // name filter discarded it as stale (a ~50% response
+ // loss for polling clients). Deliver it now to any
+ // outpipe that is already connected and waiting.
+ ResetEvent(reply_event_);
+ if (!deliverOutbuf())
+ return false;
+ continue;
+ }
+
bool const is_outpipe = i >= MAX_CLIENTS;
// Get the result if the operation was pending.
if (pipe_[i].pending_io) {
- success = GetOverlappedResult(pipe_[i].handle,
+ BOOL ok = GetOverlappedResult(pipe_[i].handle,
&pipe_[i].overlap, &status, FALSE);
+ // The manual-reset event stays signaled after the
+ // operation completes; clear it so the loop does not
+ // spin on this pipe.
+ ResetEvent(event_[i]);
switch (pipe_[i].state) {
case CONNECTING_STATE:
// Pending connect operation
- if (!success) {
+ if (!ok) {
error = GetLastError();
- lyxerr << "LyXComm: "
- << errormsg(error) << endl;
+ lyxerr << "LyXComm: " << errormsg(error) << endl;
if (!resetPipe(i, true))
return false;
continue;
}
pipe_[i].state = is_outpipe ? WRITING_STATE
: READING_STATE;
+ // The connect is done: no I/O is in flight.
+ pipe_[i].pending_io = false;
+ // Never hand a stale reply to a freshly connected
+ // reader.
+ pipe_[i].iobuf.erase();
+ if (is_outpipe) {
+ // A reader connected. If a reply is already
+ // pending (open-drain-close clients connect
+ // the .out pipe after sending the command),
+ // deliver it now.
+ if (!deliverOutbuf())
+ return false;
+ continue;
+ }
break;
case READING_STATE:
// Pending read operation
LASSERT(!is_outpipe, /**/);
- if (!success || status == 0) {
- if (!resetPipe(i, !success))
+ if (!ok || status == 0) {
+ if (!resetPipe(i, !ok))
return false;
continue;
}
@@ -254,30 +288,34 @@ bool LyXComm::pipeServer()
break;
case WRITING_STATE:
- // Pending write operation
+ // A pending write operation on an outpipe
+ // completed.
LASSERT(is_outpipe, /**/);
- // Let's see whether we have a reply
- if (!outbuf_.empty()) {
- // Yep. Deliver it to all pipe
- // instances if we get ownership
- // of the mutex, otherwise we'll
- // try again the next round.
- DWORD result = WaitForSingleObject(
- outbuf_mutex_, 200);
- if (result == WAIT_OBJECT_0) {
- DWORD j = MAX_CLIENTS;
- while (j < MAX_PIPES) {
- pipe_[j].iobuf = outbuf_;
- ++j;
- }
- outbuf_.erase();
- }
- ReleaseMutex(outbuf_mutex_);
- }
- if (pipe_[i].iobuf.empty())
+ if (!ok) {
+ // Genuine write failure (e.g. the reader
+ // closed): reset.
+ pipe_[i].iobuf.erase();
pipe_[i].pending_io = false;
- break;
+ if (!resetPipe(i, false))
+ return false;
+ continue;
+ }
+ // Deliver any replies that arrived while this
+ // write was in flight, then reset to wait for
+ // the next reader.
+ if (!deliverOutbuf())
+ return false;
+ pipe_[i].iobuf.erase();
+ pipe_[i].pending_io = false;
+ waitForReaderConsume();
+ if (!resetPipe(i, false))
+ return false;
+ continue;
}
+ } else {
+ // Spurious wake on a pipe with no pending I/O
+ // (leftover manual-reset signal): clear it.
+ ResetEvent(event_[i]);
}
// Operate according to the pipe state
@@ -332,53 +370,39 @@ bool LyXComm::pipeServer()
while (pipe_[i].nbytes && !checkStopServer(100))
;
pipe_[i].pending_io = false;
+ // Re-arm the read so a follow-up command on the same
+ // connection is picked up, and a closed client pipe
+ // recycles the instance (the pending read completes
+ // with ERROR_BROKEN_PIPE). Without this the instance
+ // is consumed forever after the first command.
pipe_[i].state = READING_STATE;
+ DWORD rstatus = 0;
+ bool const rok = ReadFile(pipe_[i].handle,
+ pipe_[i].readbuf, PIPE_BUFSIZE - 1,
+ &rstatus, &pipe_[i].overlap);
+ if (rok && rstatus != 0) {
+ // A follow-up command arrived synchronously.
+ pipe_[i].nbytes = rstatus;
+ pipe_[i].state = WRITING_STATE;
+ } else if (!rok
+ && GetLastError() == ERROR_IO_PENDING) {
+ // The read is in flight; its completion
+ // handles the result.
+ pipe_[i].pending_io = true;
+ } else {
+ // 0-byte read or real error: the client is
+ // gone. Recycle the instance.
+ pipe_[i].iobuf.erase();
+ if (!resetPipe(i, false))
+ return false;
+ }
continue;
}
- // This is an output pipe instance. Initiate the
- // overlapped write operation or monitor its progress.
-
- if (pipe_[i].pending_io) {
- success = WriteFile(pipe_[i].handle,
- pipe_[i].iobuf.c_str(),
- pipe_[i].iobuf.length(),
- &status,
- &pipe_[i].overlap);
- }
-
- if (success && !pipe_[i].iobuf.empty()
- && status == pipe_[i].iobuf.length()) {
- // The write operation completed successfully.
- pipe_[i].iobuf.erase();
- pipe_[i].pending_io = false;
- if (!resetPipe(i))
- return false;
- continue;
- }
-
- error = GetLastError();
-
- if (success && (error == ERROR_IO_PENDING || error == NO_ERROR)) {
- // The write operation is still pending.
- // We get here when a reader is started
- // well before a reply is ready, so delay
- // a bit in order to not burden the cpu.
- checkStopServer(100);
- pipe_[i].pending_io = true;
- continue;
- }
-
- success = error == ERROR_NO_DATA;
-
- // Client closed connection (ERROR_NO_DATA) or
- // an error occurred; in either case, reset the pipe.
- if (!success) {
- lyxerr << "LyXComm: Error sending message: "
- << pipe_[i].iobuf << "\nLyXComm: "
- << errormsg(error) << endl;
- }
- if (!resetPipe(i, !success))
+ // This is an output pipe instance. Start a write when
+ // a reply is pending and none is in flight; otherwise
+ // wait for the reply event to arm one.
+ if (!startOutWrite(i))
return false;
break;
case CONNECTING_STATE:
@@ -481,6 +505,9 @@ bool LyXComm::resetPipe(DWORD index, bool close_handle)
bool const is_outpipe = index >= MAX_CLIENTS;
+ // Any buffered data belongs to the previous connection; drop it.
+ pipe_[index].iobuf.erase();
+
if (close_handle) {
DWORD const open_mode = is_outpipe ? PIPE_ACCESS_OUTBOUND
: PIPE_ACCESS_INBOUND;
@@ -488,7 +515,6 @@ bool LyXComm::resetPipe(DWORD index, bool close_handle)
CloseHandle(pipe_[index].handle);
- pipe_[index].iobuf.erase();
pipe_[index].handle = CreateNamedPipeA(name.c_str(),
open_mode | FILE_FLAG_OVERLAPPED, PIPE_WAIT,
MAX_CLIENTS, PIPE_BUFSIZE, PIPE_BUFSIZE,
@@ -511,6 +537,85 @@ bool LyXComm::resetPipe(DWORD index, bool close_handle)
}
+bool LyXComm::deliverOutbuf()
+{
+ // Deliver the pending reply buffer to every outpipe instance that is
+ // connected and idle, and start the writes. Instances that are not
+ // connected yet are deliberately skipped: a stale reply must never
+ // be handed to a reader that connects later. If no reader is
+ // connected, outbuf_ is kept and the next connect delivers it.
+ if (outbuf_.empty())
+ return true;
+ DWORD result = WaitForSingleObject(outbuf_mutex_, 200);
+ if (result != WAIT_OBJECT_0)
+ // We didn't get ownership of the mutex; try again on the
+ // next event.
+ return true;
+ bool delivered = false;
+ DWORD j = MAX_CLIENTS;
+ while (j < MAX_PIPES) {
+ if (pipe_[j].state == WRITING_STATE && !pipe_[j].pending_io
+ && pipe_[j].iobuf.empty()) {
+ pipe_[j].iobuf = outbuf_;
+ if (!startOutWrite(j)) {
+ ReleaseMutex(outbuf_mutex_);
+ return false;
+ }
+ delivered = true;
+ }
+ ++j;
+ }
+ if (delivered)
+ outbuf_.erase();
+ ReleaseMutex(outbuf_mutex_);
+ return true;
+}
+
+
+void LyXComm::waitForReaderConsume()
+{
+ // DisconnectNamedPipe discards unread buffered data, which would
+ // drop a reply for a reader that has not issued its .out read yet
+ // (a read must be pending before the disconnect for the buffered
+ // data to survive). Give the reader a short bounded grace period to
+ // consume the reply before the instance is recycled; real clients
+ // issue their read within microseconds of connecting, so 100 ms is
+ // ample margin and matches the delay the original loop already used.
+ checkStopServer(100);
+}
+
+
+bool LyXComm::startOutWrite(DWORD j)
+{
+ // Start a write on outpipe j if it has a reply to send and no
+ // write is in flight. A 0-byte write must never be attempted: it
+ // fails with ERROR_ACCESS_DENIED and drops the connected reader.
+ if (pipe_[j].state != WRITING_STATE || pipe_[j].pending_io
+ || pipe_[j].iobuf.empty())
+ return true;
+ DWORD wstatus = 0;
+ bool const ok = WriteFile(pipe_[j].handle, pipe_[j].iobuf.c_str(),
+ pipe_[j].iobuf.length(), &wstatus, &pipe_[j].overlap);
+ if (ok && wstatus == pipe_[j].iobuf.length()) {
+ // Completed synchronously: the reply is in the pipe buffer.
+ pipe_[j].iobuf.erase();
+ pipe_[j].pending_io = false;
+ // DisconnectNamedPipe discards unread buffered data, so give
+ // the reader time to consume the reply before recycling.
+ waitForReaderConsume();
+ return resetPipe(j);
+ } else if (!ok && GetLastError() == ERROR_IO_PENDING) {
+ // Write in flight; the completion branch handles it.
+ pipe_[j].pending_io = true;
+ return true;
+ }
+ // Genuine failure (e.g. the reader closed): reset.
+ pipe_[j].iobuf.erase();
+ pipe_[j].pending_io = false;
+ return resetPipe(j, true);
+}
+
+
void LyXComm::openConnection()
{
LYXERR(Debug::LYXSERVER, "LyXComm: Opening connection");
@@ -562,6 +667,19 @@ void LyXComm::openConnection()
return;
}
+ // Manual-reset event, initial state = not signaled; set by send()
+ // whenever a reply is appended to outbuf_.
+ reply_event_ = CreateEvent(NULL, TRUE, FALSE, NULL);
+ if (!reply_event_) {
+ DWORD const error = GetLastError();
+ lyxerr << "LyXComm: Could not create reply event"
+ << "\nLyXComm: " << errormsg(error) << endl;
+ pipename_.erase();
+ CloseHandle(stopserver_);
+ CloseHandle(outbuf_mutex_);
+ return;
+ }
+
server_thread_ = CreateThread(NULL, 0, pipeServerWrapper,
static_cast<void *>(this), 0, NULL);
if (!server_thread_) {
@@ -570,6 +688,7 @@ void LyXComm::openConnection()
<< "\nLyXComm: " << errormsg(error) << endl;
pipename_.erase();
CloseHandle(stopserver_);
+ CloseHandle(reply_event_);
CloseHandle(outbuf_mutex_);
return;
}
@@ -597,6 +716,7 @@ void LyXComm::closeConnection()
CloseHandle(server_thread_);
ResetEvent(stopserver_);
CloseHandle(stopserver_);
+ CloseHandle(reply_event_);
CloseHandle(outbuf_mutex_);
}
@@ -615,6 +735,7 @@ void LyXComm::emergencyCleanup()
CloseHandle(server_thread_);
ResetEvent(stopserver_);
CloseHandle(stopserver_);
+ CloseHandle(reply_event_);
CloseHandle(outbuf_mutex_);
}
}
@@ -677,6 +798,9 @@ void LyXComm::send(string const & msg)
outbuf_.erase();
outbuf_ += msg;
ReleaseMutex(outbuf_mutex_);
+ // Wake the pipe loop so the reply is delivered to connected
+ // .out readers instead of waiting for the next event.
+ SetEvent(reply_event_);
} else {
// Something is fishy, better resetting the connection.
DWORD const error = GetLastError();
diff --git a/src/Server.h b/src/Server.h
index 215b7e9..0499b4d 100644
--- a/src/Server.h
+++ b/src/Server.h
@@ -159,6 +159,16 @@ private:
/// Reset an overlapped connection
bool resetPipe(DWORD, bool close_handle = false);
+ /// Deliver the pending reply buffer to connected outpipe instances
+ bool deliverOutbuf();
+
+ /// Start a write on outpipe j if it has data and no write in flight
+ bool startOutWrite(DWORD j);
+
+ /// Wait (bounded) for the .out reader to consume the reply before the
+ /// instance is disconnected (DisconnectNamedPipe discards unread data)
+ void waitForReaderConsume();
+
/// Close event and pipe handles
void closeHandles();
@@ -175,7 +185,7 @@ private:
PipeInst pipe_[MAX_PIPES];
/// Pipe server control events
- HANDLE event_[MAX_PIPES + 1];
+ HANDLE event_[MAX_PIPES + 2];
/// Reply buffer
std::string outbuf_;
@@ -186,6 +196,10 @@ private:
/// Windows event for stopping the pipe server
HANDLE stopserver_;
+ /// Windows event set by send() when a reply is appended to outbuf_, so
+ /// that the pipe loop wakes up and delivers it to connected .out readers
+ HANDLE reply_event_;
+
/// Pipe server thread handle
HANDLE server_thread_;
#endif
Server.cpp.orig
(application/octet-stream, 31.4 KB) - not displayed
Server.h.orig
(application/octet-stream, 5.9 KB) - not displayed