Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a6409831c | ||
|
|
c7b8747d85 | ||
|
|
09d4e81eab | ||
|
|
1264afe40e | ||
|
|
c95d401a36 | ||
|
|
f7ea30a494 | ||
|
|
620be9d57c |
@@ -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
|
||||||
|
|
||||||
@@ -361,6 +362,10 @@ func forceUpdateCommand(configPath string, elevatedChild bool) int {
|
|||||||
fmt.Fprintf(os.Stderr, "gpu-turnstile: cannot locate executable: %v\n", err)
|
fmt.Fprintf(os.Stderr, "gpu-turnstile: cannot locate executable: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
// One-shot CLI: the updater logs to stderr, never to the service's
|
||||||
|
// LOG_FILE — that file is ACL'd to the service account, and a CLI run
|
||||||
|
// has nothing worth persisting there.
|
||||||
|
cfg.LogFile = ""
|
||||||
log, _, logCloser := newLogger(cfg)
|
log, _, logCloser := newLogger(cfg)
|
||||||
defer logCloser.Close()
|
defer logCloser.Close()
|
||||||
if cfg.AppVersion == "dev" {
|
if cfg.AppVersion == "dev" {
|
||||||
@@ -1033,6 +1038,9 @@ type statusSnapshot struct {
|
|||||||
UptimeS int64 `json:"uptime_s"`
|
UptimeS int64 `json:"uptime_s"`
|
||||||
Downstreams []statusDownstream `json:"downstreams"`
|
Downstreams []statusDownstream `json:"downstreams"`
|
||||||
Lock statusLock `json:"lock"`
|
Lock statusLock `json:"lock"`
|
||||||
|
// MonitorNote is set client-side (never over the wire) when the
|
||||||
|
// monitor's own binary differs from the service's version.
|
||||||
|
MonitorNote string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// statusProvider assembles the one-line JSON snapshot for CmdStatus.
|
// statusProvider assembles the one-line JSON snapshot for CmdStatus.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,7 +22,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// monitorCommand renders a live status view of the running service,
|
// monitorCommand renders a live status view of the running service,
|
||||||
// refreshed every second from the control channel. Ctrl+C quits.
|
// refreshed every second from the control channel. When the service
|
||||||
|
// reports a different version and the executable on disk changed (the
|
||||||
|
// updater replaced it), the monitor restarts itself onto the new binary.
|
||||||
|
// Ctrl+C quits.
|
||||||
func monitorCommand() int {
|
func monitorCommand() int {
|
||||||
if !stdoutIsTerminal() {
|
if !stdoutIsTerminal() {
|
||||||
fmt.Fprintln(os.Stderr, "gpu-turnstile: --monitor needs an interactive terminal")
|
fmt.Fprintln(os.Stderr, "gpu-turnstile: --monitor needs an interactive terminal")
|
||||||
@@ -30,12 +34,26 @@ func monitorCommand() int {
|
|||||||
enableVirtualTerminal()
|
enableVirtualTerminal()
|
||||||
fmt.Print("\x1b[2J") // clear once; frames then redraw in place
|
fmt.Print("\x1b[2J") // clear once; frames then redraw in place
|
||||||
defer fmt.Print(cReset + "\n")
|
defer fmt.Print(cReset + "\n")
|
||||||
|
exe, _ := os.Executable()
|
||||||
|
var exeStamp time.Time
|
||||||
|
if st, err := os.Stat(exe); err == nil {
|
||||||
|
exeStamp = st.ModTime()
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
frame := renderWaiting()
|
frame := renderWaiting()
|
||||||
if reply, err := control.Ask(control.CmdStatus); err == nil {
|
if reply, err := control.Ask(control.CmdStatus); err == nil {
|
||||||
if msg, ok := strings.CutPrefix(reply, "OK "); ok {
|
if msg, ok := strings.CutPrefix(reply, "OK "); ok {
|
||||||
var snap statusSnapshot
|
var snap statusSnapshot
|
||||||
if json.Unmarshal([]byte(msg), &snap) == nil {
|
if json.Unmarshal([]byte(msg), &snap) == nil {
|
||||||
|
if snap.Version != "" && snap.Version != version {
|
||||||
|
if exeChanged(exe, exeStamp) {
|
||||||
|
fmt.Print("\x1b[2J\x1b[H")
|
||||||
|
fmt.Printf("gpu-turnstile: service updated to %s — restarting the monitor\n", snap.Version)
|
||||||
|
restartSelf(exe, "--monitor")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
snap.MonitorNote = fmt.Sprintf("note: the service runs %s, this monitor is %s", snap.Version, version)
|
||||||
|
}
|
||||||
frame = renderMonitor(snap, termWidth())
|
frame = renderMonitor(snap, termWidth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,6 +63,24 @@ func monitorCommand() int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exeChanged reports whether the executable on disk was replaced since the
|
||||||
|
// recorded stamp (the updater swaps it via rename, which changes ModTime).
|
||||||
|
func exeChanged(exe string, stamp time.Time) bool {
|
||||||
|
if exe == "" || stamp.IsZero() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
st, err := os.Stat(exe)
|
||||||
|
return err == nil && !st.ModTime().Equal(stamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// restartSelf starts a fresh copy of this executable with the given args on
|
||||||
|
// the same console; the caller exits right after.
|
||||||
|
func restartSelf(exe string, args ...string) {
|
||||||
|
cmd := exec.Command(exe, args...)
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
cmd.Start() //nolint:errcheck // best effort: on failure we just exit
|
||||||
|
}
|
||||||
|
|
||||||
func renderWaiting() string {
|
func renderWaiting() string {
|
||||||
return cDim + " gpu-turnstile — waiting for a running service…" + cReset + "\x1b[K\n"
|
return cDim + " gpu-turnstile — waiting for a running service…" + cReset + "\x1b[K\n"
|
||||||
}
|
}
|
||||||
@@ -81,6 +117,9 @@ func renderMonitor(snap statusSnapshot, width int) string {
|
|||||||
b.WriteString(fmt.Sprintf(" Queue: %s%d image job(s) waiting%s\x1b[K\n",
|
b.WriteString(fmt.Sprintf(" Queue: %s%d image job(s) waiting%s\x1b[K\n",
|
||||||
cYellow, snap.Lock.ImageQueue, cReset))
|
cYellow, snap.Lock.ImageQueue, cReset))
|
||||||
}
|
}
|
||||||
|
if snap.MonitorNote != "" {
|
||||||
|
b.WriteString(" " + cYellow + snap.MonitorNote + cReset + "\x1b[K\n")
|
||||||
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.7
|
||||||
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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -76,21 +77,43 @@ func Serve(ctx context.Context, h Handler, log *slog.Logger) error {
|
|||||||
log.Warn("control channel stopped", "err", err)
|
log.Warn("control channel stopped", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
// Blocks until a client connects — only then is the next
|
||||||
// Blocks until a client connects; on process exit the
|
// instance created, so instances are not burned without
|
||||||
// handle goes away with everything else.
|
// clients. ERROR_PIPE_CONNECTED means the client raced us
|
||||||
if err := windows.ConnectNamedPipe(pipe, nil); err != nil {
|
// and connected before the call: that is a success. Process
|
||||||
|
// exit reaps the blocked call on shutdown.
|
||||||
|
if err := windows.ConnectNamedPipe(pipe, nil); err != nil && err != errnoPipeConnected {
|
||||||
windows.CloseHandle(pipe)
|
windows.CloseHandle(pipe)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
f := os.NewFile(uintptr(pipe), pipePath)
|
go 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user