diff --git a/internal/control/control_windows.go b/internal/control/control_windows.go index 0ea67ba..9b9a9ee 100644 --- a/internal/control/control_windows.go +++ b/internal/control/control_windows.go @@ -8,6 +8,7 @@ import ( "log/slog" "os" "syscall" + "time" "unsafe" "golang.org/x/sys/windows" @@ -99,6 +100,11 @@ func Serve(ctx context.Context, h Handler, log *slog.Logger) error { // called" result, which means the connection is established. var errnoPipeConnected = syscall.Errno(535) // ERROR_PIPE_CONNECTED +// errnoPipeBusy is CreateFile's "all pipe instances are busy" result. It +// loses the race against another client that grabbed the instance +// WaitNamedPipe just reported — the caller must wait and retry. +var errnoPipeBusy = syscall.Errno(231) // ERROR_PIPE_BUSY + // pipeConn adapts a pipe handle to io.ReadWriteCloser. Close flushes first // (FlushFileBuffers blocks until the client has read the reply) and then // disconnects — closing the bare handle right after writing can discard @@ -126,21 +132,36 @@ func (c *pipeConn) ForceClose() error { } // Ask sends one command to the running service and returns its reply. +// +// The server keeps exactly one listening instance per connection, so +// concurrent clients race for it: WaitNamedPipe can report an instance +// that another client grabs before our CreateFile runs (ERROR_PIPE_BUSY). +// Retry on that — with the monitor polling status every second, a single +// attempt loses that race regularly. func Ask(cmd string) (string, error) { name, err := windows.UTF16PtrFromString(pipePath) if err != nil { return "", err } - if err := waitNamedPipe(name, 2000); err != nil { - return "", ErrUnavailable + deadline := time.Now().Add(5 * time.Second) + for { + if err := waitNamedPipe(name, 2000); err != nil { + return "", ErrUnavailable + } + handle, err := windows.CreateFile(name, + windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil, + windows.OPEN_EXISTING, 0, 0) + if err == errnoPipeBusy { + if time.Now().After(deadline) { + return "", ErrUnavailable + } + continue + } + if err != nil { + return "", ErrUnavailable + } + f := os.NewFile(uintptr(handle), pipePath) + defer f.Close() + return readReply(f, cmd) } - handle, err := windows.CreateFile(name, - windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil, - windows.OPEN_EXISTING, 0, 0) - if err != nil { - return "", ErrUnavailable - } - f := os.NewFile(uintptr(handle), pipePath) - defer f.Close() - return readReply(f, cmd) }