Files
luxtools-client/internal/installer/installer_windows.go
2026-02-13 20:36:06 +01:00

210 lines
5.2 KiB
Go

//go:build windows
package installer
import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
type windowsConfig struct {
Listen string `json:"Listen"`
Allow []string `json:"Allow"`
}
func Install(opts InstallOptions) error {
if opts.Listen == "" {
opts.Listen = "127.0.0.1:8765"
}
installDir, err := windowsInstallDir()
if err != nil {
return err
}
exePath := filepath.Join(installDir, ServiceName+".exe")
configPath := filepath.Join(installDir, "config.json")
cfg := windowsConfig{Listen: opts.Listen, Allow: opts.Allow}
cfgBytes, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
if opts.DryRun {
return nil
}
if err := os.MkdirAll(installDir, 0o755); err != nil {
return err
}
if err := os.WriteFile(configPath, cfgBytes, 0o644); err != nil {
return err
}
self, err := os.Executable()
if err != nil {
return err
}
if err := copySelfAtomic(self, exePath, 0o755); err != nil {
return err
}
if err := registerWindowsScheduledTask(exePath, opts.Listen, opts.Allow); err != nil {
return err
}
return nil
}
func Uninstall(opts UninstallOptions) error {
installDir, err := windowsInstallDir()
if err != nil {
return err
}
exePath := filepath.Join(installDir, ServiceName+".exe")
configPath := filepath.Join(installDir, "config.json")
if opts.DryRun {
return nil
}
_ = unregisterWindowsScheduledTask()
if opts.KeepConfig {
_ = os.Remove(exePath)
// keep directory + config.json
return nil
}
_ = os.Remove(configPath)
_ = os.Remove(exePath)
_ = os.RemoveAll(installDir)
return nil
}
func windowsInstallDir() (string, error) {
if v := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); v != "" {
return filepath.Join(v, ServiceName), nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
// Fallback for older environments.
return filepath.Join(home, "AppData", "Local", ServiceName), nil
}
func registerWindowsScheduledTask(exePath, listen string, allow []string) error {
// Use PowerShell scheduled task cmdlets to ensure per-user + interactive logon.
allowJSON, err := json.Marshal(allow)
if err != nil {
return err
}
ps := `
param(
[string]$ExePath,
[string]$Listen,
[string]$AllowJson
)
$ErrorActionPreference = 'Stop'
$ServiceName = 'luxtools-client'
$TaskName = $ServiceName
function Quote-Arg([string]$s) {
if ($null -eq $s) { return '""' }
if ($s -match '[\s\"]') {
$escaped = $s -replace '"', '\\"'
return '"' + $escaped + '"'
}
return $s
}
# Stop existing
try { Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue | Out-Null } catch {}
try { schtasks.exe /End /TN $TaskName 2>$null | Out-Null } catch {}
# Build args
$Allow = @()
if ($AllowJson -and $AllowJson.Trim().Length -gt 0) {
$Allow = (ConvertFrom-Json -InputObject $AllowJson)
}
$argList = @('-listen', $Listen)
foreach ($p in $Allow) {
if ($p -and $p.ToString().Trim().Length -gt 0) {
$argList += @('-allow', $p)
}
}
$argString = ($argList | ForEach-Object { Quote-Arg $_ }) -join ' '
$principalUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$action = New-ScheduledTaskAction -Execute $ExePath -Argument $argString
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $principalUser
$principal = New-ScheduledTaskPrincipal -UserId $principalUser -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$task = New-ScheduledTask -Action $action -Trigger $trigger -Principal $principal -Settings $settings
Register-ScheduledTask -TaskName $TaskName -InputObject $task -Force | Out-Null
try { Start-ScheduledTask -TaskName $TaskName | Out-Null } catch {}
`
psFile, err := os.CreateTemp("", "luxtools-client-install-*.ps1")
if err != nil {
return err
}
psPath := psFile.Name()
if _, err := psFile.WriteString(ps); err != nil {
_ = psFile.Close()
_ = os.Remove(psPath)
return err
}
if err := psFile.Close(); err != nil {
_ = os.Remove(psPath)
return err
}
defer os.Remove(psPath)
cmd := exec.Command("powershell.exe",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-File", psPath,
"-ExePath", exePath,
"-Listen", listen,
"-AllowJson", string(allowJSON),
)
out, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(out))
if msg == "" {
return err
}
return errors.New(msg)
}
return nil
}
func unregisterWindowsScheduledTask() error {
ps := `
$ErrorActionPreference = 'SilentlyContinue'
$TaskName = 'luxtools-client'
try { Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue | Out-Null } catch {}
try { schtasks.exe /End /TN $TaskName 2>$null | Out-Null } catch {}
try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null } catch {}
try { schtasks.exe /Delete /TN $TaskName /F 2>$null | Out-Null } catch {}
`
cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", ps)
out, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(out))
if msg != "" {
return fmt.Errorf("%s", msg)
}
return err
}
return nil
}