Bump version and add changelog
This commit is contained in:
+83
-45
@@ -7,76 +7,68 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const autostartUnitName = "pysentry.service"
|
||||
const autostartDesktopFileName = "pysentry.desktop"
|
||||
|
||||
func SetAutostart(enabled bool, executablePath string) error {
|
||||
unitDir, err := userSystemdDir()
|
||||
desktopPath, err := autostartDesktopPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unitPath := filepath.Join(unitDir, autostartUnitName)
|
||||
|
||||
if enabled {
|
||||
if err := os.MkdirAll(unitDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
unit := fmt.Sprintf(`[Unit]
|
||||
Description=PySentry desktop scheduler
|
||||
|
||||
[Service]
|
||||
ExecStart=%s
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
`, executablePath)
|
||||
if err := os.WriteFile(unitPath, []byte(unit), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exec.Command("systemctl", "--user", "daemon-reload").Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return exec.Command("systemctl", "--user", "enable", "--now", autostartUnitName).Run()
|
||||
}
|
||||
|
||||
_ = exec.Command("systemctl", "--user", "disable", "--now", autostartUnitName).Run()
|
||||
if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
|
||||
if err := cleanupLegacySystemdAutostart(); err != nil {
|
||||
return err
|
||||
}
|
||||
return exec.Command("systemctl", "--user", "daemon-reload").Run()
|
||||
|
||||
if enabled {
|
||||
if err := os.MkdirAll(filepath.Dir(desktopPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
desktopFile := fmt.Sprintf(`[Desktop Entry]
|
||||
Type=Application
|
||||
Name=PySentry
|
||||
Comment=PySentry desktop scheduler
|
||||
Exec=%s
|
||||
Terminal=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
`, quoteDesktopExec(executablePath))
|
||||
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) {
|
||||
unitDir, err := userSystemdDir()
|
||||
desktopPath, err := autostartDesktopPath()
|
||||
if err != nil {
|
||||
return false, "Cannot resolve user systemd directory"
|
||||
return false, "Cannot resolve XDG autostart directory"
|
||||
}
|
||||
unitPath := filepath.Join(unitDir, autostartUnitName)
|
||||
data, readErr := os.ReadFile(unitPath)
|
||||
enabledErr := exec.Command("systemctl", "--user", "is-enabled", "--quiet", autostartUnitName).Run()
|
||||
if legacySystemdAutostartExists() {
|
||||
return false, "Legacy systemd autostart entry still exists"
|
||||
}
|
||||
data, readErr := os.ReadFile(desktopPath)
|
||||
|
||||
if !expectedEnabled {
|
||||
if os.IsNotExist(readErr) && enabledErr != nil {
|
||||
if os.IsNotExist(readErr) {
|
||||
return true, "Autostart is off"
|
||||
}
|
||||
return false, "Autostart unit exists while setting is off"
|
||||
return false, "Autostart desktop entry exists while setting is off"
|
||||
}
|
||||
if readErr != nil {
|
||||
return false, "Autostart unit is missing"
|
||||
return false, "Autostart desktop entry is missing"
|
||||
}
|
||||
if !strings.Contains(string(data), executablePath) {
|
||||
return false, "Autostart unit points to another executable"
|
||||
}
|
||||
if enabledErr != nil {
|
||||
return false, "Autostart unit is not enabled"
|
||||
if !strings.Contains(string(data), "Exec="+quoteDesktopExec(executablePath)) {
|
||||
return false, "Autostart desktop entry points to another executable"
|
||||
}
|
||||
return true, "Autostart is configured"
|
||||
}
|
||||
|
||||
func userSystemdDir() (string, error) {
|
||||
func autostartDesktopPath() (string, error) {
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
@@ -85,5 +77,51 @@ func userSystemdDir() (string, error) {
|
||||
}
|
||||
configHome = filepath.Join(home, ".config")
|
||||
}
|
||||
return filepath.Join(configHome, "systemd", "user"), nil
|
||||
return filepath.Join(configHome, "autostart", autostartDesktopFileName), nil
|
||||
}
|
||||
|
||||
func quoteDesktopExec(path string) string {
|
||||
return strconv.Quote(path)
|
||||
}
|
||||
|
||||
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
|
||||
// Linux implementation uses XDG Autostart because PySentry 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 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
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -17,7 +18,7 @@ func SetAutostart(enabled bool, executablePath string) error {
|
||||
configureHiddenWindow(deleteCommand)
|
||||
_ = deleteCommand.Run()
|
||||
|
||||
command := exec.Command("reg.exe", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName, "/t", "REG_SZ", "/d", fmt.Sprintf("%q", executablePath), "/f")
|
||||
command := exec.Command("reg.exe", "add", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autostartName, "/t", "REG_SZ", "/d", strconv.Quote(executablePath), "/f")
|
||||
configureHiddenWindow(command)
|
||||
return command.Run()
|
||||
}
|
||||
@@ -41,9 +42,32 @@ func AutostartStatus(expectedEnabled bool, executablePath string) (bool, string)
|
||||
return false, "Autostart entry is missing"
|
||||
}
|
||||
|
||||
text := strings.ReplaceAll(string(output), `"`, "")
|
||||
if !strings.Contains(text, executablePath) {
|
||||
actual, ok := parseRegistryRunValue(string(output))
|
||||
if !ok {
|
||||
return false, "Autostart entry cannot be read"
|
||||
}
|
||||
if !sameWindowsPath(actual, executablePath) {
|
||||
return false, "Autostart points to another executable"
|
||||
}
|
||||
return true, "Autostart is configured"
|
||||
}
|
||||
|
||||
func parseRegistryRunValue(output string) (string, bool) {
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
fields := strings.Fields(strings.TrimSpace(line))
|
||||
for index, field := range fields {
|
||||
if field == "REG_SZ" && index+1 < len(fields) {
|
||||
value := strings.Join(fields[index+1:], " ")
|
||||
value = strings.Trim(value, `"`)
|
||||
return value, value != ""
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func sameWindowsPath(left string, right string) bool {
|
||||
left = filepath.Clean(strings.Trim(left, `"`))
|
||||
right = filepath.Clean(strings.Trim(right, `"`))
|
||||
return strings.EqualFold(left, right)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build windows
|
||||
|
||||
package core
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseRegistryRunValue(t *testing.T) {
|
||||
output := `
|
||||
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
|
||||
PySentry REG_SZ "D:\Apps\PySentry\pysentry.exe"
|
||||
`
|
||||
value, ok := parseRegistryRunValue(output)
|
||||
if !ok {
|
||||
t.Fatal("expected registry value to parse")
|
||||
}
|
||||
if value != `D:\Apps\PySentry\pysentry.exe` {
|
||||
t.Fatalf("unexpected value: %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameWindowsPathIgnoresCaseAndQuotes(t *testing.T) {
|
||||
if !sameWindowsPath(`"D:\Apps\PySentry\pysentry.exe"`, `d:\apps\pysentry\pysentry.exe`) {
|
||||
t.Fatal("expected paths to match")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,4 +3,4 @@ package core
|
||||
// Version is the application version shown in the GUI and used by build
|
||||
// scripts in artifact names. It is a var rather than a const so release builds
|
||||
// can override it with Go ldflags when CI tags a build.
|
||||
var Version = "0.1.0"
|
||||
var Version = "0.2.0"
|
||||
|
||||
+5
-1
@@ -591,7 +591,7 @@ func settingsView(w fyne.Window, store *core.Store, jobs *[]job) fyne.CanvasObje
|
||||
startOnLogin.SetChecked(store.Config.StartOnLogin)
|
||||
autostartStatus := widget.NewLabel("")
|
||||
refreshAutostartStatus := func() {
|
||||
ok, message := core.AutostartStatus(startOnLogin.Checked, store.Paths.ExecutablePath)
|
||||
ok, message := core.AutostartStatus(store.Config.StartOnLogin, store.Paths.ExecutablePath)
|
||||
if ok {
|
||||
autostartStatus.SetText("OK: " + message)
|
||||
return
|
||||
@@ -599,6 +599,10 @@ func settingsView(w fyne.Window, store *core.Store, jobs *[]job) fyne.CanvasObje
|
||||
autostartStatus.SetText("Problem: " + message)
|
||||
}
|
||||
startOnLogin.OnChanged = func(bool) {
|
||||
if startOnLogin.Checked != store.Config.StartOnLogin {
|
||||
autostartStatus.SetText("Pending: save settings to apply")
|
||||
return
|
||||
}
|
||||
refreshAutostartStatus()
|
||||
}
|
||||
refreshAutostartStatus()
|
||||
|
||||
Reference in New Issue
Block a user