diff --git a/README.md b/README.md index 1b6734d..a486f32 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/gpu-turnstile/main.go b/cmd/gpu-turnstile/main.go index 59d69b6..5d89b97 100644 --- a/cmd/gpu-turnstile/main.go +++ b/cmd/gpu-turnstile/main.go @@ -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 (or -config=), 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 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 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 (or -config=), 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()