Compare commits

...
3 Commits
Author SHA1 Message Date
mram c95d401a36 Pin compose example to v0.2.5
ci / test (push) Successful in 14s
ci / docker (push) Successful in 1m6s
ci / release (push) Successful in 16s
2026-09-22 08:32:06 +02:00
mram f7ea30a494 Fix control pipe races: treat ERROR_PIPE_CONNECTED as success, flush+disconnect before close so replies are never discarded 2026-09-22 08:31:13 +02:00
mram 620be9d57c Add short flags: -i (install), -r (remove), -m (monitor) 2026-09-22 08:23:40 +02:00
3 changed files with 37 additions and 11 deletions
+7 -6
View File
@@ -68,9 +68,9 @@ func parseFlags(args []string) (configPath string, install, remove, noCopy, help
i++ i++
case strings.HasPrefix(args[i], "-config="): case strings.HasPrefix(args[i], "-config="):
configPath = strings.TrimPrefix(args[i], "-config=") configPath = strings.TrimPrefix(args[i], "-config=")
case args[i] == "--install-service" || args[i] == "-install-service": case args[i] == "--install-service" || args[i] == "-install-service" || args[i] == "-i":
install = true install = true
case args[i] == "--remove-service" || args[i] == "-remove-service": case args[i] == "--remove-service" || args[i] == "-remove-service" || args[i] == "-r":
remove = true remove = true
case args[i] == "--no-copy" || args[i] == "-no-copy": case args[i] == "--no-copy" || args[i] == "-no-copy":
noCopy = true noCopy = true
@@ -82,7 +82,7 @@ func parseFlags(args []string) (configPath string, install, remove, noCopy, help
forceUpdate = true forceUpdate = true
case args[i] == "--update-now" || args[i] == "-update-now": case args[i] == "--update-now" || args[i] == "-update-now":
updateNow = true updateNow = true
case args[i] == "--monitor" || args[i] == "-monitor": case args[i] == "--monitor" || args[i] == "-monitor" || args[i] == "-m":
monitor = true monitor = true
case args[i] == "--elevated-child": case args[i] == "--elevated-child":
elevatedChild = true elevatedChild = true
@@ -100,15 +100,16 @@ const usageText = `GPU arbitration proxy for Ollama + ComfyUI
Usage: Usage:
gpu-turnstile -config <path> run the proxy gpu-turnstile -config <path> run the proxy
gpu-turnstile --install-service [--no-copy] [-config path] install + start as a service gpu-turnstile -i | --install-service [--no-copy] [-config path]
gpu-turnstile --remove-service stop + uninstall the service install + start as a service
gpu-turnstile -r | --remove-service stop + uninstall the service
gpu-turnstile -v | --version print just the version gpu-turnstile -v | --version print just the version
gpu-turnstile --force-update check for a signed update now, gpu-turnstile --force-update check for a signed update now,
apply it and restart the service apply it and restart the service
(no admin needed when the service runs) (no admin needed when the service runs)
gpu-turnstile --update-now like --force-update, but only gpu-turnstile --update-now like --force-update, but only
through the running service through the running service
gpu-turnstile --monitor live status view (downstreams, gpu-turnstile -m | --monitor live status view (downstreams,
GPU lock, queue); Ctrl+C quits GPU lock, queue); Ctrl+C quits
gpu-turnstile -h | --help this help gpu-turnstile -h | --help this help
+1 -1
View File
@@ -6,7 +6,7 @@
# ComfyUI --listen 0.0.0.0 --port 8189). # ComfyUI --listen 0.0.0.0 --port 8189).
services: services:
gpu-turnstile: gpu-turnstile:
image: git.rambossek.at/public/gpu-turnstile:v0.2.4 image: git.rambossek.at/public/gpu-turnstile:v0.2.5
restart: unless-stopped restart: unless-stopped
environment: environment:
# Each consumer is enabled by setting its URL; leave one unset to # Each consumer is enabled by setting its URL; leave one unset to
+29 -4
View File
@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
"syscall"
"unsafe" "unsafe"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
@@ -78,19 +79,43 @@ func Serve(ctx context.Context, h Handler, log *slog.Logger) error {
} }
go func() { go func() {
// Blocks until a client connects; on process exit the // Blocks until a client connects; on process exit the
// handle goes away with everything else. // handle goes away with everything else. A client that
if err := windows.ConnectNamedPipe(pipe, nil); err != nil { // raced us and connected between CreateNamedPipe and
// ConnectNamedPipe reports ERROR_PIPE_CONNECTED — that is
// a success, not a failure.
if err := windows.ConnectNamedPipe(pipe, nil); err != nil && err != errnoPipeConnected {
windows.CloseHandle(pipe) windows.CloseHandle(pipe)
return return
} }
f := os.NewFile(uintptr(pipe), pipePath) serveConn(&pipeConn{f: os.NewFile(uintptr(pipe), pipePath), h: pipe}, h)
serveConn(f, h) // closes f, and with it the pipe handle
}() }()
} }
}() }()
return nil return nil
} }
// errnoPipeConnected is ConnectNamedPipe's "the client connected before we
// called" result, which means the connection is established.
var errnoPipeConnected = syscall.Errno(535) // ERROR_PIPE_CONNECTED
// 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
// unread reply bytes, which clients see as an empty, failed request.
type pipeConn struct {
f *os.File
h windows.Handle
}
func (c *pipeConn) Read(p []byte) (int, error) { return c.f.Read(p) }
func (c *pipeConn) Write(p []byte) (int, error) { return c.f.Write(p) }
func (c *pipeConn) Close() error {
windows.FlushFileBuffers(c.h) //nolint:errcheck // best effort
windows.DisconnectNamedPipe(c.h) //nolint:errcheck // best effort
return c.f.Close()
}
// Ask sends one command to the running service and returns its reply. // Ask sends one command to the running service and returns its reply.
func Ask(cmd string) (string, error) { func Ask(cmd string) (string, error) {
name, err := windows.UTF16PtrFromString(pipePath) name, err := windows.UTF16PtrFromString(pipePath)