//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 }