Fix control-pipe client race: retry CreateFile on ERROR_PIPE_BUSY
WaitNamedPipe can report an instance that a concurrent client (the monitor polls status every second) grabs before our CreateFile runs; the single-attempt Ask then failed with 'no answer from the service'. Retry the wait+open until a 5s overall deadline.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"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.
|
// called" result, which means the connection is established.
|
||||||
var errnoPipeConnected = syscall.Errno(535) // ERROR_PIPE_CONNECTED
|
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
|
// pipeConn adapts a pipe handle to io.ReadWriteCloser. Close flushes first
|
||||||
// (FlushFileBuffers blocks until the client has read the reply) and then
|
// (FlushFileBuffers blocks until the client has read the reply) and then
|
||||||
// disconnects — closing the bare handle right after writing can discard
|
// 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.
|
// 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) {
|
func Ask(cmd string) (string, error) {
|
||||||
name, err := windows.UTF16PtrFromString(pipePath)
|
name, err := windows.UTF16PtrFromString(pipePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if err := waitNamedPipe(name, 2000); err != nil {
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
return "", ErrUnavailable
|
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user