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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user