16d818d03d
Move autostart_*.go and tests from src/core to src/platform/autostart. Update src/gui/app.go to call autostart.SetAutostart / autostart.AutostartStatus. Add quoteDesktopExec to desktop_linux.go (was co-located in autostart_linux.go). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
186 lines
5.0 KiB
Go
186 lines
5.0 KiB
Go
//go:build linux
|
|
|
|
package autostart
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
|
)
|
|
|
|
const autostartDesktopFileName = "gosentry.desktop"
|
|
const legacyAutostartDesktopFileName = "pysentry.desktop"
|
|
|
|
func SetAutostart(enabled bool, executablePath string, iconPath string) error {
|
|
desktopPath, err := autostartDesktopPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// A desktop scheduler with a tray icon belongs to the graphical session, so
|
|
// Linux autostart is implemented through XDG Autostart instead of a systemd
|
|
// user service. systemd is tempting because it is explicit and scriptable,
|
|
// but it is the wrong owner for a windowed app that should inherit the
|
|
// desktop session environment and appear in the tray predictably.
|
|
if err := cleanupLegacySystemdAutostart(); err != nil {
|
|
return err
|
|
}
|
|
if err := cleanupLegacyDesktopAutostart(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if enabled {
|
|
if err := os.MkdirAll(filepath.Dir(desktopPath), 0o755); err != nil {
|
|
return err
|
|
}
|
|
desktopFile := fmt.Sprintf(`[Desktop Entry]
|
|
Type=Application
|
|
Name=GoSentry
|
|
Comment=GoSentry desktop scheduler
|
|
Exec=%s %s
|
|
%s
|
|
Terminal=false
|
|
X-GNOME-Autostart-enabled=true
|
|
`, quoteDesktopExec(executablePath), domain.StartInTrayArgument, desktopIconLine(iconPath))
|
|
return os.WriteFile(desktopPath, []byte(desktopFile), 0o644)
|
|
}
|
|
|
|
if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string) {
|
|
desktopPath, err := autostartDesktopPath()
|
|
if err != nil {
|
|
return false, "Cannot resolve XDG autostart directory"
|
|
}
|
|
if legacySystemdAutostartExists() {
|
|
return false, "Legacy systemd autostart entry still exists"
|
|
}
|
|
if legacyDesktopAutostartExists() {
|
|
return false, "Legacy desktop autostart entry still exists"
|
|
}
|
|
data, readErr := os.ReadFile(desktopPath)
|
|
|
|
if !expectedEnabled {
|
|
if os.IsNotExist(readErr) {
|
|
return true, "Autostart is off"
|
|
}
|
|
return false, "Autostart desktop entry exists while setting is off"
|
|
}
|
|
if readErr != nil {
|
|
return false, "Autostart desktop entry is missing"
|
|
}
|
|
expectedExec := "Exec=" + quoteDesktopExec(executablePath) + " " + domain.StartInTrayArgument
|
|
if !strings.Contains(string(data), expectedExec) {
|
|
return false, "Autostart desktop entry points to another executable"
|
|
}
|
|
return true, "Autostart is configured"
|
|
}
|
|
|
|
func autostartDesktopPath() (string, error) {
|
|
configHome := os.Getenv("XDG_CONFIG_HOME")
|
|
if configHome == "" {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
configHome = filepath.Join(home, ".config")
|
|
}
|
|
return filepath.Join(configHome, "autostart", autostartDesktopFileName), nil
|
|
}
|
|
|
|
func legacyAutostartDesktopPath() (string, error) {
|
|
configHome := os.Getenv("XDG_CONFIG_HOME")
|
|
if configHome == "" {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
configHome = filepath.Join(home, ".config")
|
|
}
|
|
return filepath.Join(configHome, "autostart", legacyAutostartDesktopFileName), nil
|
|
}
|
|
|
|
func quoteDesktopExec(path string) string {
|
|
return strconv.Quote(path)
|
|
}
|
|
|
|
func desktopIconLine(iconPath string) string {
|
|
if strings.TrimSpace(iconPath) == "" {
|
|
return ""
|
|
}
|
|
return "Icon=" + iconPath
|
|
}
|
|
|
|
func cleanupLegacySystemdAutostart() error {
|
|
unitPath, err := legacySystemdUnitPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := os.Stat(unitPath); os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
|
|
// Older PySentry builds used a systemd user unit for autostart. The current
|
|
// GoSentry implementation uses XDG Autostart because it is a GUI/tray
|
|
// application and should be launched by the desktop session. Disable and
|
|
// remove the old unit so the two mechanisms do not fight or start duplicates.
|
|
_ = exec.Command("systemctl", "--user", "disable", "pysentry.service").Run()
|
|
if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
_ = exec.Command("systemctl", "--user", "daemon-reload").Run()
|
|
return nil
|
|
}
|
|
|
|
func cleanupLegacyDesktopAutostart() error {
|
|
desktopPath, err := legacyAutostartDesktopPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// The old PySentry desktop file is removed proactively instead of tolerated
|
|
// alongside the new one. Leaving both files in place would risk duplicate
|
|
// launches or confusing status diagnostics after the rename.
|
|
if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func legacyDesktopAutostartExists() bool {
|
|
desktopPath, err := legacyAutostartDesktopPath()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = os.Stat(desktopPath)
|
|
return err == nil
|
|
}
|
|
|
|
func legacySystemdAutostartExists() bool {
|
|
unitPath, err := legacySystemdUnitPath()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = os.Stat(unitPath)
|
|
return err == nil
|
|
}
|
|
|
|
func legacySystemdUnitPath() (string, error) {
|
|
configHome := os.Getenv("XDG_CONFIG_HOME")
|
|
if configHome == "" {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
configHome = filepath.Join(home, ".config")
|
|
}
|
|
return filepath.Join(configHome, "systemd", "user", "pysentry.service"), nil
|
|
}
|