diff --git a/client/systray/startup-creator.go b/client/systray/startup-creator.go new file mode 100644 index 000000000..cb354856d --- /dev/null +++ b/client/systray/startup-creator.go @@ -0,0 +1,76 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build cgo || !darwin + +// Package systray provides a minimal Tailscale systray application. +package systray + +import ( + "bufio" + "bytes" + _ "embed" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +//go:embed tailscale-systray.service +var embedSystemd string + +func InstallStartupScript(initSystem string) error { + switch initSystem { + case "systemd": + return installSystemd() + default: + return fmt.Errorf("unsupported init system '%s'", initSystem) + } +} + +func installSystemd() error { + // Find the path to tailscale, just in case it's not where the example file + // has it placed, and replace that before writing the file. + tailscaleBin, err := exec.LookPath("tailscale") + if err != nil { + return fmt.Errorf("failed to find tailscale binary %w", err) + } + + var output bytes.Buffer + scanner := bufio.NewScanner(strings.NewReader(embedSystemd)) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "ExecStart=") { + line = fmt.Sprintf("ExecStart=%s systray", tailscaleBin) + } + output.WriteString(line + "\n") + } + + configDir, err := os.UserConfigDir() + if err != nil { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("unable to locate user home: %w", err) + } + configDir = filepath.Join(homeDir, ".config") + } + + systemdDir := filepath.Join(configDir, "systemd", "user") + if err := os.MkdirAll(systemdDir, 0o755); err != nil { + return fmt.Errorf("failed creating systemd uuser dir: %w", err) + } + + serviceFile := filepath.Join(systemdDir, "tailscale-systray.service") + + if err := os.WriteFile(serviceFile, output.Bytes(), 0o755); err != nil { + return fmt.Errorf("failed writing systemd user service: %w", err) + } + + fmt.Printf("Successfully installed systemd service to: %s\n", serviceFile) + fmt.Println("To enable and start the service, run:") + fmt.Println(" systemctl --user daemon-reload") + fmt.Println(" systemctl --user enable --now tailscale-systray") + + return nil +} diff --git a/client/systray/tailscale-systray.service b/client/systray/tailscale-systray.service new file mode 100644 index 000000000..a4d987563 --- /dev/null +++ b/client/systray/tailscale-systray.service @@ -0,0 +1,10 @@ +[Unit] +Description=Tailscale System Tray +After=systemd.service + +[Service] +Type=simple +ExecStart=/usr/bin/tailscale systray + +[Install] +WantedBy=default.target diff --git a/cmd/tailscale/cli/systray.go b/cmd/tailscale/cli/systray.go index 05d688faa..c0296ae26 100644 --- a/cmd/tailscale/cli/systray.go +++ b/cmd/tailscale/cli/systray.go @@ -7,17 +7,41 @@ package cli import ( "context" + "flag" + "fmt" "github.com/peterbourgon/ff/v3/ffcli" "tailscale.com/client/systray" ) +var systrayArgs struct { + initSystem string + installStartup bool +} + var systrayCmd = &ffcli.Command{ Name: "systray", ShortUsage: "tailscale systray", ShortHelp: "Run a systray application to manage Tailscale", - Exec: func(_ context.Context, _ []string) error { - new(systray.Menu).Run(&localClient) - return nil - }, + LongHelp: `Run a systray application to manage Tailscale. +To have the application run on startup, use the --enable-startup flag.`, + Exec: runSystray, + FlagSet: (func() *flag.FlagSet { + fs := newFlagSet("systray") + fs.StringVar(&systrayArgs.initSystem, "enable-startup", "", + "Install startup script for init system. Currently supported systems are [systemd].") + return fs + })(), +} + +func runSystray(ctx context.Context, _ []string) error { + if systrayArgs.initSystem != "" { + if err := systray.InstallStartupScript(systrayArgs.initSystem); err != nil { + fmt.Printf("%s\n\n", err.Error()) + return flag.ErrHelp + } + return nil + } + new(systray.Menu).Run(&localClient) + return nil }