210 lines
5.2 KiB
Go
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
|
|
}
|