Relaunch through UAC when (un)installing the service unprivileged

--install-service/--remove-service on Windows no longer fail with
'Access is denied' from a normal shell: the process re-runs itself via
ShellExecuteEx 'runas', waits for the elevated child and mirrors its
exit code. The child gets --elevated-child and pauses for a keypress so
its console output stays readable. Declining the prompt reports
'UAC prompt declined'.
This commit is contained in:
mram
2026-09-21 08:00:24 +02:00
parent 802a64280f
commit fbab0bba33
7 changed files with 157 additions and 16 deletions
+43 -9
View File
@@ -3,6 +3,7 @@
package main
import (
"bufio"
"context"
"errors"
"fmt"
@@ -35,7 +36,7 @@ var version = "dev"
const exitCodeUpdate = 3
func main() {
configPath, install, remove, noCopy, args := parseFlags(os.Args[1:])
configPath, install, remove, noCopy, elevatedChild, args := parseFlags(os.Args[1:])
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") {
@@ -51,9 +52,9 @@ func main() {
fmt.Fprintf(os.Stderr, "gpu-turnstile: --install-service and --remove-service are mutually exclusive\n")
os.Exit(2)
case install:
os.Exit(serviceCommand(configPath, true, noCopy))
os.Exit(serviceCommand(configPath, true, noCopy, elevatedChild))
case remove:
os.Exit(serviceCommand(configPath, false, noCopy))
os.Exit(serviceCommand(configPath, false, noCopy, elevatedChild))
}
if len(args) > 0 {
fmt.Fprintf(os.Stderr, "usage: gpu-turnstile [-config path] [--install-service [--no-copy] | --remove-service]\n")
@@ -89,8 +90,9 @@ func main() {
}
// parseFlags extracts -config <path> (or -config=<path>), the
// --install-service / --remove-service switches and --no-copy from args.
func parseFlags(args []string) (configPath string, install, remove, noCopy bool, rest []string) {
// --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 {
@@ -105,11 +107,13 @@ func parseFlags(args []string) (configPath string, install, remove, noCopy bool,
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, rest
return configPath, install, remove, noCopy, elevatedChild, rest
}
// defaultConfigPath returns gpu-turnstile.env next to the executable.
@@ -182,12 +186,36 @@ func newLogger(cfg config.Config) (*slog.Logger, io.Writer, io.Closer) {
}
// serviceCommand installs (copyBin = register the canonical-layout copy)
// or removes the service and reports the result.
func serviceCommand(configPath string, install, noCopy bool) int {
// or removes the service and reports the result. On Windows, when the
// shell is not elevated, the command relaunches itself through a UAC
// prompt and mirrors the elevated child's exit code. An elevated child
// waits for a keypress so its console window does not flash closed before
// the output can be read.
func serviceCommand(configPath string, install, noCopy, elevatedChild bool) int {
verb := "remove"
var err error
if install {
verb = "install"
}
if !service.Elevated() {
args := append(append([]string{}, os.Args[1:]...), "--elevated-child")
code, err := service.RelaunchElevated(args)
if errors.Is(err, service.ErrUserCancelled) {
fmt.Fprintln(os.Stderr, "gpu-turnstile: UAC prompt declined")
return 1
}
if err != nil {
fmt.Fprintf(os.Stderr, "gpu-turnstile: could not elevate: %v\n", err)
return 1
}
if code != 0 {
fmt.Fprintf(os.Stderr, "gpu-turnstile service %s failed in the elevated process (exit %d)\n", verb, code)
return code
}
fmt.Printf("service %s: %sd (elevated)\n", service.Name, verb)
return 0
}
var err error
if install {
path := resolveConfigPath(configPath)
if abs, absErr := filepath.Abs(path); absErr == nil {
path = abs
@@ -196,6 +224,12 @@ func serviceCommand(configPath string, install, noCopy bool) int {
} else {
err = service.Remove()
}
if elevatedChild {
defer func() {
fmt.Print("\nPress Enter to close this window...")
bufio.NewReader(os.Stdin).ReadString('\n')
}()
}
if err != nil {
fmt.Fprintf(os.Stderr, "gpu-turnstile service %s: %v\n", verb, err)
return 1