#include "pipe_client.h" #include "daemon_log.h" #include #include #include PipeClient::~PipeClient() { Close(); } void PipeClient::Close() { if (m_hPipe != INVALID_HANDLE_VALUE) { CloseHandle(m_hPipe); m_hPipe = INVALID_HANDLE_VALUE; } // IN-05 (re-review): release the shared overlapped Event together with // the pipe handle. Safe to call twice — CloseHandle(NULL) is a no-op. if (m_hEvent) { CloseHandle(m_hEvent); m_hEvent = NULL; } } bool PipeClient::Connect(const char* pipeName) { // Retry 100 ms x 50 = ~5 s. Matches Open Question 4 startup race: the // driver's pipe server may not be live yet when the daemon launches. for (int attempt = 0; attempt < 50; ++attempt) { // Phase 16 VRCH-01 fix: daemon pipe is PIPE_ACCESS_INBOUND (server-side // read-only). The CreateFileA access mask must match, or the open fails // with ERROR_ACCESS_DENIED. Daemon only writes, never reads — hence // GENERIC_WRITE alone. HANDLE h = CreateFileA(pipeName, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); if (h != INVALID_HANDLE_VALUE) { DWORD mode = PIPE_READMODE_MESSAGE; if (!SetNamedPipeHandleState(h, &mode, NULL, NULL)) { DaemonLog("pipe SetNamedPipeHandleState failed err=%lu", GetLastError()); CloseHandle(h); return false; } // IN-05 (re-review): allocate the reusable manual-reset Event here, // once per connect, so SendCommand() doesn't churn 90 pairs/second. m_hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); if (!m_hEvent) { DaemonLog("pipe CreateEventW failed err=%lu", GetLastError()); CloseHandle(h); return false; } m_hPipe = h; DaemonLog("pipe connected on attempt %d", attempt + 1); return true; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } DaemonLog("pipe connect failed after 50 attempts (err=%lu)", GetLastError()); return false; } bool PipeClient::SendCommand(const char* line) { if (m_hPipe == INVALID_HANDLE_VALUE || !m_hEvent) return false; const size_t len = std::strlen(line); // IN-05 (re-review): reuse m_hEvent instead of per-call CreateEventW. // Manual-reset event — must ResetEvent before each new overlapped op so // stale signalled state from a prior tick can't fool WaitForSingleObject // into reporting "done" for a write that hasn't actually issued yet. ResetEvent(m_hEvent); OVERLAPPED ov = {}; ov.hEvent = m_hEvent; BOOL ok = WriteFile(m_hPipe, line, static_cast(len), NULL, &ov); DWORD err = GetLastError(); bool success = false; bool closePipeAfter = false; if (ok || err == ERROR_IO_PENDING) { DWORD wait = WaitForSingleObject(ov.hEvent, 100); // Pitfall 6: 100 ms cap if (wait == WAIT_OBJECT_0) { DWORD written = 0; success = GetOverlappedResult(m_hPipe, &ov, &written, FALSE) && written == len; } else { // WR-02 fix: on write timeout the server side may be wedged or the // pipe may be quietly broken. Leaving m_hPipe open causes the next // SendCommand to queue a fresh overlapped write on the same stale // handle — at 90 Hz that's a 90 Hz flood of timeouts with no path // to recovery. Close the pipe so IsConnected() flips false and // WriterThread stops emitting until (future design) a reconnect // path is added. Matches the policy of the WriteFile-failure // branch below. // IN-05 (re-review): CancelIo + a final GetOverlappedResult (with // bWait=TRUE) ensures the kernel is completely done with &ov and // m_hEvent before we return. Without this the shared m_hEvent // could be touched by a late-arriving I/O completion after we've // already issued the NEXT write (post-reconnect), tripping // spurious success on a later tick. We deliberately do NOT close // m_hEvent here — Close() owns that so it survives to report the // outcome of this timeout path deterministically. CancelIo(m_hPipe); DWORD ignored = 0; GetOverlappedResult(m_hPipe, &ov, &ignored, TRUE); DaemonLog("pipe write timeout (dropping frame) — closing pipe"); closePipeAfter = true; } } else { DaemonLog("pipe WriteFile failed err=%lu — closing pipe", err); Close(); } if (closePipeAfter) Close(); return success; }