Self-install into canonical layout on both platforms, --no-copy to opt out
Windows: --install-service creates %ProgramFiles%\gpu-turnstile and %ProgramData%\gpu-turnstile, copies the exe and (if absent) the env file in, and registers the copy. Linux: binary goes to /var/lib/gpu-turnstile (not /usr/local/sbin: replacing a running binary needs directory write, which must not be granted on a shared system dir to a sandboxed service). --no-copy registers the current location as-is on both platforms.
This commit is contained in:
@@ -10,6 +10,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -30,6 +31,20 @@ const Name = "gpu-turnstile"
|
||||
// when the service is removed.
|
||||
const virtualAccount = `NT SERVICE\` + Name
|
||||
|
||||
// installDirs returns the canonical install (Program Files) and data
|
||||
// (ProgramData) directories.
|
||||
func installDirs() (install, data string) {
|
||||
pf := os.Getenv("ProgramFiles")
|
||||
if pf == "" {
|
||||
pf = `C:\Program Files`
|
||||
}
|
||||
pd := os.Getenv("ProgramData")
|
||||
if pd == "" {
|
||||
pd = `C:\ProgramData`
|
||||
}
|
||||
return filepath.Join(pf, Name), filepath.Join(pd, Name)
|
||||
}
|
||||
|
||||
// IsService reports whether the process is running as a Windows service.
|
||||
func IsService() bool {
|
||||
isSvc, err := svc.IsWindowsService()
|
||||
@@ -78,24 +93,51 @@ func (h *handler) Execute(_ []string, requests <-chan svc.ChangeRequest, status
|
||||
|
||||
// Install registers gpu-turnstile as an auto-start Windows service running
|
||||
// as the NT SERVICE\gpu-turnstile virtual account, whose binPath loads the
|
||||
// given config file. Recovery actions restart the service after 5s on
|
||||
// failure — this is also what brings up a staged update after the updater
|
||||
// exits with a non-zero code. After registering, the virtual account is
|
||||
// granted modify access to the install directory (self-updates rewrite the
|
||||
// exe) and to the LOG_FILE directory, and read access to the config file
|
||||
// if it lives elsewhere. The grants must come after CreateService: the
|
||||
// virtual account's SID only exists once the service is registered.
|
||||
func Install(configPath string) error {
|
||||
// given config file. With copyBin it first creates the canonical layout —
|
||||
// the binary is copied into %ProgramFiles%\gpu-turnstile and the config
|
||||
// next to it (an existing config there is kept), %ProgramData%\gpu-turnstile
|
||||
// is created for logs — and registers that copy; with copyBin=false the
|
||||
// current executable location is registered as-is. Recovery actions restart
|
||||
// the service after 5s on failure — this is also what brings up a staged
|
||||
// update after the updater exits with a non-zero code. After registering,
|
||||
// the virtual account is granted modify access to the install and data
|
||||
// directories (self-updates rewrite the exe), and read access to the
|
||||
// config file if it lives elsewhere. The grants must come after
|
||||
// CreateService: the virtual account's SID only exists once the service is
|
||||
// registered.
|
||||
func Install(configPath string, copyBin bool) error {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if abs, absErr := filepath.Abs(exe); absErr == nil {
|
||||
exe = abs
|
||||
}
|
||||
if configPath != "" {
|
||||
if abs, absErr := filepath.Abs(configPath); absErr == nil {
|
||||
configPath = abs
|
||||
}
|
||||
}
|
||||
|
||||
installDir, _ := installDirs()
|
||||
if copyBin && !strings.EqualFold(filepath.Dir(exe), installDir) {
|
||||
if err := os.MkdirAll(installDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create %s: %w", installDir, err)
|
||||
}
|
||||
installedExe := filepath.Join(installDir, "gpu-turnstile.exe")
|
||||
if err := copyFile(exe, installedExe); err != nil {
|
||||
return fmt.Errorf("copy binary to %s: %w", installedExe, err)
|
||||
}
|
||||
exe = installedExe
|
||||
targetCfg := filepath.Join(installDir, "gpu-turnstile.env")
|
||||
if configPath != "" && !strings.EqualFold(configPath, targetCfg) {
|
||||
if _, statErr := os.Stat(targetCfg); os.IsNotExist(statErr) {
|
||||
copyFile(configPath, targetCfg) //nolint:errcheck // best effort
|
||||
}
|
||||
configPath = targetCfg
|
||||
}
|
||||
}
|
||||
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to service manager (run as administrator): %w", err)
|
||||
@@ -129,8 +171,36 @@ func Install(configPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// grantAll gives the virtual account every ACL the service needs.
|
||||
// copyFile copies src to dst (0755 on the new file).
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
out.Close()
|
||||
return err
|
||||
}
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
// grantAll gives the virtual account every ACL the service needs: modify
|
||||
// on the install and ProgramData directories and the LOG_FILE directory
|
||||
// (if configured elsewhere), read on a config file outside the install
|
||||
// directory.
|
||||
func grantAll(exe, configPath string) error {
|
||||
_, dataDir := installDirs()
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create %s: %w", dataDir, err)
|
||||
}
|
||||
if err := grantAccess(dataDir, "(OI)(CI)(M)"); err != nil {
|
||||
return err
|
||||
}
|
||||
exeDir := filepath.Dir(exe)
|
||||
if err := grantAccess(exeDir, "(OI)(CI)(M)"); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user