//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) After=graphical-session.target PartOf=graphical-session.target [Service] Type=simple EnvironmentFile=%h/.config/luxtools-client/luxtools-client.env Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus 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=graphical-session.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 }