//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 }