add install commands
This commit is contained in:
40
internal/installer/helpers.go
Normal file
40
internal/installer/helpers.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func copySelfAtomic(src, dst string, mode os.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp := dst + ".tmp"
|
||||
out, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, copyErr := io.Copy(out, in)
|
||||
closeErr := out.Close()
|
||||
if copyErr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return closeErr
|
||||
}
|
||||
if err := os.Rename(tmp, dst); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
25
internal/installer/installer.go
Normal file
25
internal/installer/installer.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package installer
|
||||
|
||||
import "runtime"
|
||||
|
||||
const ServiceName = "luxtools-client"
|
||||
|
||||
type InstallOptions struct {
|
||||
Listen string
|
||||
Allow []string
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
type UninstallOptions struct {
|
||||
KeepConfig bool
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
func Supported() bool {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "windows":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
148
internal/installer/installer_linux.go
Normal file
148
internal/installer/installer_linux.go
Normal file
@@ -0,0 +1,148 @@
|
||||
//go:build linux
|
||||
|
||||
package installer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Install(opts InstallOptions) error {
|
||||
if opts.Listen == "" {
|
||||
opts.Listen = "127.0.0.1:8765"
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installDir := filepath.Join(home, ".local", "share", ServiceName)
|
||||
binPath := filepath.Join(installDir, ServiceName)
|
||||
configDir := filepath.Join(home, ".config", ServiceName)
|
||||
envFile := filepath.Join(configDir, ServiceName+".env")
|
||||
unitDir := filepath.Join(home, ".config", "systemd", "user")
|
||||
unitFile := filepath.Join(unitDir, ServiceName+".service")
|
||||
|
||||
allowArgs := buildLinuxAllowArgs(opts.Allow)
|
||||
|
||||
if opts.DryRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(installDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(unitDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copySelfAtomic(exePath, binPath, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
env := fmt.Sprintf("# %s environment\nLISTEN=\"%s\"\nALLOW_ARGS=\"%s\"\n", ServiceName, escapeDoubleQuotes(opts.Listen), escapeDoubleQuotes(allowArgs))
|
||||
if err := os.WriteFile(envFile, []byte(env), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unit := `[Unit]
|
||||
Description=luxtools-client (local folder opener helper)
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=%h/.config/luxtools-client/luxtools-client.env
|
||||
ExecStart=/bin/sh -lc '%h/.local/share/luxtools-client/luxtools-client -listen "$LISTEN" $ALLOW_ARGS'
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
`
|
||||
if err := os.WriteFile(unitFile, []byte(unit), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := runCmd("systemctl", "--user", "daemon-reload"); err != nil {
|
||||
return fmt.Errorf("systemctl --user daemon-reload failed: %w", err)
|
||||
}
|
||||
_ = runCmd("systemctl", "--user", "enable", ServiceName)
|
||||
if err := runCmd("systemctl", "--user", "restart", ServiceName); err != nil {
|
||||
return fmt.Errorf("systemctl --user restart failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Uninstall(opts UninstallOptions) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installDir := filepath.Join(home, ".local", "share", ServiceName)
|
||||
configDir := filepath.Join(home, ".config", ServiceName)
|
||||
unitFile := filepath.Join(home, ".config", "systemd", "user", ServiceName+".service")
|
||||
|
||||
if opts.DryRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = runCmd("systemctl", "--user", "stop", ServiceName)
|
||||
_ = runCmd("systemctl", "--user", "disable", ServiceName)
|
||||
_ = runCmd("systemctl", "--user", "reset-failed", ServiceName)
|
||||
_ = os.Remove(unitFile)
|
||||
_ = runCmd("systemctl", "--user", "daemon-reload")
|
||||
_ = os.RemoveAll(installDir)
|
||||
if !opts.KeepConfig {
|
||||
_ = os.RemoveAll(configDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildLinuxAllowArgs(allowed []string) string {
|
||||
var b strings.Builder
|
||||
for _, p := range allowed {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
pEsc := strings.ReplaceAll(p, "\"", "\\\"")
|
||||
b.WriteString(" -allow \"")
|
||||
b.WriteString(pEsc)
|
||||
b.WriteString("\"")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func escapeDoubleQuotes(s string) string {
|
||||
return strings.ReplaceAll(s, "\"", "\\\"")
|
||||
}
|
||||
|
||||
func runCmd(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdout = io.Discard
|
||||
if err := cmd.Run(); err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
if msg != "" {
|
||||
return errors.New(msg)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
193
internal/installer/installer_windows.go
Normal file
193
internal/installer/installer_windows.go
Normal file
@@ -0,0 +1,193 @@
|
||||
//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 {}
|
||||
`
|
||||
|
||||
cmd := exec.Command("powershell.exe",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-Command", ps,
|
||||
exePath,
|
||||
listen,
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user