Install sets a default LOG_FILE in the env file on Windows (no console as a service)

This commit is contained in:
mram
2026-09-21 08:37:25 +02:00
parent 1394a76eea
commit dd3187f951
3 changed files with 61 additions and 22 deletions
+3 -2
View File
@@ -112,8 +112,9 @@ gpu-turnstile.exe --remove-service
```
Layout: `C:\Program Files\gpu-turnstile\` holds the exe and
`gpu-turnstile.env`, logs go to `C:\ProgramData\gpu-turnstile\` (set
`LOG_FILE` in the env file there is no console). The service always runs
`gpu-turnstile.env`, logs go to `C:\ProgramData\gpu-turnstile\` — the
installer sets `LOG_FILE` in the env file by default (there is no console;
your own `LOG_FILE` setting is kept). The service always runs
as the virtual account `NT SERVICE\gpu-turnstile` (low-privilege,
per-service, no password); the installer automatically grants it write
access to the install and data directories — nothing else to do.
+10 -4
View File
@@ -186,8 +186,13 @@ By default install creates the canonical layout and copies the binary into
it (Windows: `%ProgramFiles%\gpu-turnstile\`, plus
`%ProgramData%\gpu-turnstile\` for logs; Linux: `/var/lib/gpu-turnstile/`
with the config at `/etc/gpu-turnstile.env`). An existing config in the
target location is never overwritten. `--no-copy` registers the current
executable location as-is instead.
target location is never overwritten. On Windows the install also makes
sure the env file sets `LOG_FILE` to
`%ProgramData%\gpu-turnstile\gpu-turnstile.log` (appended only when no
`LOG_FILE=` line exists) since a service has no console; on Linux logs go
to the journal via stderr, so no default is set there. `--no-copy`
registers the current executable location as-is and leaves the config
untouched.
### Windows
@@ -195,8 +200,9 @@ executable location as-is instead.
`%ProgramData%\gpu-turnstile\`, copies the exe and (if none exists there
yet) the `gpu-turnstile.env` into the Program Files directory, and
registers that copy as a Windows service; recovery actions restart it
5 s after any failure. Logs go to the ProgramData directory via
`LOG_FILE` since there is no console.
5 s after any failure. The install ensures the env file sets `LOG_FILE`
to `%ProgramData%\gpu-turnstile\gpu-turnstile.log` since there is no
console — an existing `LOG_FILE` setting is kept.
- **Account**: the service always runs as the virtual account
`NT SERVICE\gpu-turnstile` — a per-service low-privilege identity the
SCM manages (no password, automatic logon-as-a-service right, no admin
+48 -16
View File
@@ -152,25 +152,33 @@ func Install(configPath string, copyBin bool) error {
}
}
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 same, _ := sameFileContent(exe, installedExe); !same {
if err := copyFile(exe, installedExe); err != nil {
return fmt.Errorf("copy binary to %s: %w", installedExe, err)
}
}
exe = installedExe
installDir, dataDir := installDirs()
if copyBin {
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
if !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 same, _ := sameFileContent(exe, installedExe); !same {
if err := copyFile(exe, installedExe); err != nil {
return fmt.Errorf("copy binary to %s: %w", installedExe, err)
}
}
exe = installedExe
if configPath != "" && !strings.EqualFold(configPath, targetCfg) {
if _, statErr := os.Stat(targetCfg); os.IsNotExist(statErr) {
copyFile(configPath, targetCfg) //nolint:errcheck // best effort
}
}
configPath = targetCfg
}
// A service has no console: without LOG_FILE the output vanishes, so
// the installed env file defaults to logging into ProgramData. An
// existing LOG_FILE setting is left alone.
if err := ensureLogFile(targetCfg, filepath.Join(dataDir, "gpu-turnstile.log")); err != nil {
return err
}
configPath = targetCfg
}
binPath := fmt.Sprintf(`"%s" -config "%s"`, exe, configPath)
@@ -304,6 +312,30 @@ func sameFileContent(a, b string) (bool, error) {
return bytes.Equal(ba, bb), nil
}
// ensureLogFile makes sure the env file at path sets LOG_FILE, creating
// the file or appending the line as needed. An existing LOG_FILE= line
// (even an empty one) is respected and left untouched.
func ensureLogFile(path, logPath string) error {
data, err := os.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("read %s: %w", path, err)
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "LOG_FILE=") {
return nil
}
}
s := string(data)
if s != "" && !strings.HasSuffix(s, "\n") {
s += "\n"
}
s += "LOG_FILE=" + logPath + "\n"
if err := os.WriteFile(path, []byte(s), 0o644); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
return nil
}
// copyFile copies src to dst (0755 on the new file).
func copyFile(src, dst string) error {
in, err := os.Open(src)