|
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net"
|
|
|
|
|
"net/http"
|
|
|
|
@@ -45,8 +46,9 @@ func stdoutIsTerminal() bool {
|
|
|
|
|
|
|
|
|
|
// parseFlags extracts -config <path> (or -config=<path>), the
|
|
|
|
|
// --install-service / --remove-service switches, --no-copy, -h/--help,
|
|
|
|
|
// -v/--version and the hidden --elevated-child marker from args.
|
|
|
|
|
func parseFlags(args []string) (configPath string, install, remove, noCopy, help, showVersion, elevatedChild bool, rest []string) {
|
|
|
|
|
// -v/--version, --force-update and the hidden --elevated-child marker from
|
|
|
|
|
// args.
|
|
|
|
|
func parseFlags(args []string) (configPath string, install, remove, noCopy, help, showVersion, forceUpdate, elevatedChild bool, rest []string) {
|
|
|
|
|
rest = args[:0]
|
|
|
|
|
for i := 0; i < len(args); i++ {
|
|
|
|
|
switch {
|
|
|
|
@@ -65,13 +67,15 @@ func parseFlags(args []string) (configPath string, install, remove, noCopy, help
|
|
|
|
|
help = true
|
|
|
|
|
case args[i] == "-v" || args[i] == "--version" || args[i] == "-version":
|
|
|
|
|
showVersion = true
|
|
|
|
|
case args[i] == "--force-update" || args[i] == "-force-update":
|
|
|
|
|
forceUpdate = true
|
|
|
|
|
case args[i] == "--elevated-child":
|
|
|
|
|
elevatedChild = true
|
|
|
|
|
default:
|
|
|
|
|
rest = append(rest, args[i])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return configPath, install, remove, noCopy, help, showVersion, elevatedChild, rest
|
|
|
|
|
return configPath, install, remove, noCopy, help, showVersion, forceUpdate, elevatedChild, rest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// versionLine is printed at the top of every help and error screen.
|
|
|
|
@@ -84,6 +88,8 @@ Usage:
|
|
|
|
|
gpu-turnstile --install-service [--no-copy] [-config path] install + start as a service
|
|
|
|
|
gpu-turnstile --remove-service stop + uninstall the service
|
|
|
|
|
gpu-turnstile -v | --version print just the version
|
|
|
|
|
gpu-turnstile --force-update check for a signed update now,
|
|
|
|
|
apply it and restart the service
|
|
|
|
|
gpu-turnstile -h | --help this help
|
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
@@ -114,12 +120,12 @@ func fatalUsage(format string, args ...any) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
configPath, install, remove, noCopy, help, showVersion, elevatedChild, args := parseFlags(os.Args[1:])
|
|
|
|
|
configPath, install, remove, noCopy, help, showVersion, forceUpdate, elevatedChild, args := parseFlags(os.Args[1:])
|
|
|
|
|
if showVersion {
|
|
|
|
|
fmt.Println(version)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
bare := configPath == "" && !install && !remove && !elevatedChild && len(args) == 0
|
|
|
|
|
bare := configPath == "" && !install && !remove && !forceUpdate && !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
|
|
|
|
@@ -140,10 +146,14 @@ func main() {
|
|
|
|
|
switch {
|
|
|
|
|
case install && remove:
|
|
|
|
|
fatalUsage("error: --install-service and --remove-service are mutually exclusive")
|
|
|
|
|
case forceUpdate && (install || remove):
|
|
|
|
|
fatalUsage("error: --force-update cannot be combined with --install-service/--remove-service")
|
|
|
|
|
case install:
|
|
|
|
|
os.Exit(serviceCommand(configPath, true, noCopy, elevatedChild))
|
|
|
|
|
case remove:
|
|
|
|
|
os.Exit(serviceCommand(configPath, false, noCopy, elevatedChild))
|
|
|
|
|
case forceUpdate:
|
|
|
|
|
os.Exit(forceUpdateCommand(configPath, elevatedChild))
|
|
|
|
|
}
|
|
|
|
|
if len(args) > 0 {
|
|
|
|
|
fatalUsage("error: unknown arguments: %s", strings.Join(args, " "))
|
|
|
|
@@ -245,6 +255,102 @@ func newLogger(cfg config.Config) (*slog.Logger, io.Writer, io.Closer) {
|
|
|
|
|
return log, out, closer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// waitForEnter keeps an elevated child's console window open until the
|
|
|
|
|
// user has read the output.
|
|
|
|
|
func waitForEnter() {
|
|
|
|
|
fmt.Print("\nPress Enter to close this window...")
|
|
|
|
|
bufio.NewReader(os.Stdin).ReadString('\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// elevateAndMirror relaunches the current command elevated (UAC) and
|
|
|
|
|
// mirrors the child's exit code. verb is used in messages.
|
|
|
|
|
func elevateAndMirror(verb string) (int, bool) {
|
|
|
|
|
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, true
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "gpu-turnstile: could not elevate: %v\n", err)
|
|
|
|
|
return 1, true
|
|
|
|
|
}
|
|
|
|
|
if code != 0 {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "gpu-turnstile %s failed in the elevated process (exit %d)\n", verb, code)
|
|
|
|
|
return code, true
|
|
|
|
|
}
|
|
|
|
|
return 0, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isPermission reports whether err is a permission problem (Windows
|
|
|
|
|
// ERROR_ACCESS_DENIED, POSIX EACCES/EPERM, possibly wrapped).
|
|
|
|
|
func isPermission(err error) bool {
|
|
|
|
|
return errors.Is(err, fs.ErrPermission) || strings.Contains(strings.ToLower(err.Error()), "access is denied")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// forceUpdateCommand checks for a signed update immediately, stages it if
|
|
|
|
|
// newer, and restarts the service when it is running so the new binary
|
|
|
|
|
// takes effect. Staging into a system directory and restarting a service
|
|
|
|
|
// need admin rights; instead of prompting unconditionally, permission
|
|
|
|
|
// failures trigger the UAC relaunch so a dev copy in a user-writable
|
|
|
|
|
// directory updates without a prompt.
|
|
|
|
|
func forceUpdateCommand(configPath string, elevatedChild bool) int {
|
|
|
|
|
if elevatedChild {
|
|
|
|
|
defer waitForEnter()
|
|
|
|
|
}
|
|
|
|
|
cfg, err := loadMergedConfig(configPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%s\n\ngpu-turnstile: %v\n", versionLine(), err)
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
exePath, err := os.Executable()
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "gpu-turnstile: cannot locate executable: %v\n", err)
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
log, _, logCloser := newLogger(cfg)
|
|
|
|
|
defer logCloser.Close()
|
|
|
|
|
u := &update.Updater{Repo: cfg.UpdateRepo, Asset: cfg.UpdateAsset, Version: version, Log: log}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
|
|
|
defer cancel()
|
|
|
|
|
staged, err := u.Check(ctx, exePath)
|
|
|
|
|
if err != nil && isPermission(err) && !service.Elevated() {
|
|
|
|
|
code, _ := elevateAndMirror("--force-update")
|
|
|
|
|
if code == 0 {
|
|
|
|
|
fmt.Println("update applied (elevated)")
|
|
|
|
|
}
|
|
|
|
|
return code
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%s\n\ngpu-turnstile: update check failed: %v\n", versionLine(), err)
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
if !staged {
|
|
|
|
|
fmt.Printf("%s is up to date\n", versionLine())
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("%s: update staged\n", versionLine())
|
|
|
|
|
restarted, err := service.RestartIfRunning()
|
|
|
|
|
if err != nil && isPermission(err) && !service.Elevated() {
|
|
|
|
|
code, _ := elevateAndMirror("--force-update")
|
|
|
|
|
if code == 0 {
|
|
|
|
|
fmt.Println("update applied (elevated)")
|
|
|
|
|
}
|
|
|
|
|
return code
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "gpu-turnstile: update staged but service restart failed: %v\n", err)
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
if restarted {
|
|
|
|
|
fmt.Println("service restarted on the new version")
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Println("no running service; the new version applies on next start")
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// serviceCommand installs (copyBin = register the canonical-layout copy)
|
|
|
|
|
// or removes the service and reports the result. On Windows, when the
|
|
|
|
|
// shell is not elevated, the command relaunches itself through a UAC
|
|
|
|
@@ -252,28 +358,23 @@ func newLogger(cfg config.Config) (*slog.Logger, io.Writer, io.Closer) {
|
|
|
|
|
// 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, done := "remove", "removed"
|
|
|
|
|
verb, doneVerb := "remove", "removed"
|
|
|
|
|
if install {
|
|
|
|
|
verb, done = "install", "installed"
|
|
|
|
|
verb, doneVerb = "install", "installed"
|
|
|
|
|
}
|
|
|
|
|
if elevatedChild {
|
|
|
|
|
defer waitForEnter()
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
code, done := elevateAndMirror(verb)
|
|
|
|
|
if done && code != 0 {
|
|
|
|
|
return code
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("service %s: %s (elevated)\n", service.Name, done)
|
|
|
|
|
if done {
|
|
|
|
|
fmt.Printf("service %s: %s (elevated)\n", service.Name, doneVerb)
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var err error
|
|
|
|
|
if install {
|
|
|
|
|
path := resolveConfigPath(configPath)
|
|
|
|
@@ -284,17 +385,11 @@ func serviceCommand(configPath string, install, noCopy, elevatedChild 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
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("service %s: %s\n", service.Name, done)
|
|
|
|
|
fmt.Printf("service %s: %s\n", service.Name, doneVerb)
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|