Bare run in a terminal prints the help screen; add -h/--help

A zero-argument invocation now shows the usage text when stdout is a
console (double-clicked exe, interactive shell). Without a terminal —
Docker entrypoint, services, pipes — a bare invocation still starts the
proxy, so the container image and service behavior are unchanged.
This commit is contained in:
mram
2026-09-21 08:06:35 +02:00
parent fbab0bba33
commit a0435c858e
2 changed files with 72 additions and 29 deletions
+5 -1
View File
@@ -91,9 +91,13 @@ override file values. Invalid values fail at startup.
```sh
go build ./cmd/gpu-turnstile
./gpu-turnstile
OLLAMA_URL=http://127.0.0.1:11435 COMFY_URL=http://127.0.0.1:8189 ./gpu-turnstile
```
Running the binary with no arguments in a terminal prints the help screen
(same as `-h`/`--help`); without a terminal (services, containers) a bare
invocation starts the proxy.
### Run natively on Windows (current primary deployment)
Download `gpu-turnstile.exe` from a release and install it as a Windows
+67 -28
View File
@@ -35,8 +35,74 @@ var version = "dev"
// process: a signed update has been staged and the GPU lock is idle.
const exitCodeUpdate = 3
// stdoutIsTerminal reports whether stdout is a console (char device), as
// opposed to a pipe or file — which is what Docker containers and services
// see.
func stdoutIsTerminal() bool {
fi, err := os.Stdout.Stat()
return err == nil && fi.Mode()&os.ModeCharDevice != 0
}
// parseFlags extracts -config <path> (or -config=<path>), the
// --install-service / --remove-service switches, --no-copy, -h/--help and
// the hidden --elevated-child marker from args.
func parseFlags(args []string) (configPath string, install, remove, noCopy, help, elevatedChild bool, rest []string) {
rest = args[:0]
for i := 0; i < len(args); i++ {
switch {
case args[i] == "-config" && i+1 < len(args):
configPath = args[i+1]
i++
case strings.HasPrefix(args[i], "-config="):
configPath = strings.TrimPrefix(args[i], "-config=")
case args[i] == "--install-service" || args[i] == "-install-service":
install = true
case args[i] == "--remove-service" || args[i] == "-remove-service":
remove = true
case args[i] == "--no-copy" || args[i] == "-no-copy":
noCopy = true
case args[i] == "-h" || args[i] == "--help" || args[i] == "-help":
help = true
case args[i] == "--elevated-child":
elevatedChild = true
default:
rest = append(rest, args[i])
}
}
return configPath, install, remove, noCopy, help, elevatedChild, rest
}
const usageText = `gpu-turnstile — GPU arbitration proxy for Ollama + ComfyUI
Usage:
gpu-turnstile -config <path> run the proxy
gpu-turnstile --install-service [--no-copy] [-config path] install + start as a service
gpu-turnstile --remove-service stop + uninstall the service
gpu-turnstile -h | --help this help
Options:
-config <path> config file (default: gpu-turnstile.env next to the exe)
--install-service copies the binary into the canonical location
(%ProgramFiles%\gpu-turnstile or /var/lib/gpu-turnstile)
unless --no-copy; on Windows a UAC prompt appears when
the shell is not elevated
--no-copy with --install-service: register the current location as-is
All runtime settings are environment variables or KEY=VALUE lines in the
config file (OLLAMA_URL, COMFY_URL, LOGLEVEL, ...); see README.md.
`
func main() {
configPath, install, remove, noCopy, elevatedChild, args := parseFlags(os.Args[1:])
configPath, install, remove, noCopy, help, elevatedChild, args := parseFlags(os.Args[1:])
bare := configPath == "" && !install && !remove && !elevatedChild && len(args) == 0
if help || (bare && stdoutIsTerminal()) {
// Bare invocation in a terminal (e.g. double-clicked on Windows)
// shows the help instead of starting a proxy window with no visible
// explanation. Without a terminal — Docker containers, services,
// pipes — a bare invocation starts the proxy as before.
fmt.Print(usageText)
return
}
if len(args) > 0 && args[0] == "service" {
// Legacy subcommand form: gpu-turnstile service install|remove.
if len(args) != 2 || (args[1] != "install" && args[1] != "remove") {
@@ -89,33 +155,6 @@ func main() {
}
}
// parseFlags extracts -config <path> (or -config=<path>), the
// --install-service / --remove-service switches, --no-copy and the hidden
// --elevated-child marker from args.
func parseFlags(args []string) (configPath string, install, remove, noCopy, elevatedChild bool, rest []string) {
rest = args[:0]
for i := 0; i < len(args); i++ {
switch {
case args[i] == "-config" && i+1 < len(args):
configPath = args[i+1]
i++
case strings.HasPrefix(args[i], "-config="):
configPath = strings.TrimPrefix(args[i], "-config=")
case args[i] == "--install-service" || args[i] == "-install-service":
install = true
case args[i] == "--remove-service" || args[i] == "-remove-service":
remove = true
case args[i] == "--no-copy" || args[i] == "-no-copy":
noCopy = true
case args[i] == "--elevated-child":
elevatedChild = true
default:
rest = append(rest, args[i])
}
}
return configPath, install, remove, noCopy, elevatedChild, rest
}
// defaultConfigPath returns gpu-turnstile.env next to the executable.
func defaultConfigPath() string {
exe, err := os.Executable()