Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion internal/autostart/autostart_android.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"os"
"path/filepath"
"strings"

"github.com/yetone/magpie/internal/edit"
)

// Termux:Boot runs executable scripts in ~/.termux/boot after Android boots.
Expand All @@ -25,7 +27,7 @@ func enable(exe string) error {
// Android's shell is available even before Termux's PATH is loaded.
q := "'" + strings.ReplaceAll(exe, "'", "'\"'\"'") + "'"
body := "#!/system/bin/sh\nexec " + q + " serve </dev/null >/dev/null 2>&1 &\n"
if err := os.WriteFile(p, []byte(body), 0o700); err != nil {
if err := edit.WriteAtomic(p, []byte(body)); err != nil {
return err
}
return os.Chmod(p, 0o700)
Expand Down
15 changes: 11 additions & 4 deletions internal/autostart/autostart_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"os"
"path/filepath"
"regexp"

"github.com/yetone/magpie/internal/edit"
)

const label = "com.yetone.magpie"
Expand All @@ -16,9 +18,14 @@ func record() string {
return filepath.Join(home, "Library", "LaunchAgents", label+".plist")
}

// launchd loads no job that names neither its own label nor a program to
// run: a record a write left short of either sits in LaunchAgents, is
// refused, and magpie never opens at login however long the file is there.
var ourLabel = regexp.MustCompile(`<key>Label</key>\s*<string>` + regexp.QuoteMeta(label) + `</string>`)

func enabled() bool {
_, err := os.Stat(record())
return err == nil
b, err := os.ReadFile(record())
return err == nil && ourLabel.Match(b) && program.Match(b)
}

// a launch agent the system loads at the next login: the app itself, run
Expand All @@ -28,7 +35,7 @@ func enable(exe string) error {
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
return err
}
return os.WriteFile(p, plist(exe), 0o644)
return edit.WriteAtomic(p, plist(exe))
}

// plist is the launch agent for exe. AbandonProcessGroup: launchd kills
Expand Down Expand Up @@ -72,7 +79,7 @@ func refresh() error {
if bytes.Equal(b, want) {
return nil
}
return os.WriteFile(p, want, 0o644)
return edit.WriteAtomic(p, want)
}

func disable() error {
Expand Down
36 changes: 36 additions & 0 deletions internal/autostart/autostart_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,39 @@ func TestRefreshOlderLaunchAgent(t *testing.T) {
t.Fatalf("a launch agent magpie didn't write was changed:\n%s", b)
}
}

// A record a write cut short is not one that opens magpie at login: launchd
// refuses a job with no program to run, and one with nothing in it at all,
// so Open at login reads off rather than saying on for a record that never
// starts anything.
func TestSetThenTruncatedRecordReadsAsOff(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
if err := Set(true); err != nil {
t.Fatal(err)
}
if !Enabled() {
t.Fatal("not on after Set(true)")
}
p := record()
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
// the head of the launch agent, cut off before the program it names,
// which is what a write that goes no further leaves behind
head := b[:strings.Index(string(b), "<key>ProgramArguments</key>")]
if err := os.WriteFile(p, head, 0o644); err != nil {
t.Fatal(err)
}
if Enabled() {
t.Errorf("a launch agent with no ProgramArguments reads as on:\n%s", head)
}
if err := os.WriteFile(p, nil, 0o644); err != nil {
t.Fatal(err)
}
if Enabled() {
t.Error("an empty launch agent reads as on")
}
}
16 changes: 13 additions & 3 deletions internal/autostart/autostart_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ package autostart
import (
"os"
"path/filepath"
"regexp"
"strings"

"github.com/yetone/magpie/internal/appdir"
"github.com/yetone/magpie/internal/edit"
)

func record() string {
Expand All @@ -19,9 +21,17 @@ func record() string {
return filepath.Join(dir, "autostart", "magpie.desktop")
}

// the two keys a desktop entry needs before the desktop starts it at login:
// no Type and it is not an application entry, no Exec and it names nothing
// to run. A .desktop a write left short of either reads as off, not as on.
var (
entryType = regexp.MustCompile(`(?m)^Type=Application$`)
entryExec = regexp.MustCompile(`(?m)^Exec=`)
)

func enabled() bool {
_, err := os.Stat(record())
return err == nil
b, err := os.ReadFile(record())
return err == nil && entryType.Match(b) && entryExec.Match(b)
}

// an XDG autostart entry, which the desktop starts at login
Expand All @@ -34,7 +44,7 @@ func enable(exe string) error {
q := strings.NewReplacer(`\`, `\\`, `"`, `\"`, "`", "\\`", `$`, `\$`).Replace(exe)
body := "[Desktop Entry]\nType=Application\nName=magpie\nComment=one place to pick every agent's model\n" +
"Exec=\"" + q + "\" " + Arg + "\nTerminal=false\nX-GNOME-Autostart-enabled=true\n"
return os.WriteFile(p, []byte(body), 0o644)
return edit.WriteAtomic(p, []byte(body))
}

func disable() error {
Expand Down
47 changes: 47 additions & 0 deletions internal/autostart/autostart_other_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//go:build !darwin && !windows && !android

package autostart

import (
"os"
"strings"
"testing"
)

// A record a write cut short is not one that opens magpie at login: the
// desktop starts no entry with no Exec, and none at all with nothing in it,
// so Open at login reads off rather than saying on for a record that never
// starts anything.
func TestSetThenTruncatedRecordReadsAsOff(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("XDG_CONFIG_HOME", home+"/.config")
t.Setenv("APPIMAGE", "/opt/my apps/magpie.AppImage")
if err := Set(true); err != nil {
t.Fatal(err)
}
if !Enabled() {
t.Fatal("not on after Set(true)")
}
p := record()
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
// the head of the entry, cut off before the Exec that names the
// program, which is what a write that goes no further leaves behind
head := b[:strings.Index(string(b), "Exec=")]
if err := os.WriteFile(p, head, 0o644); err != nil {
t.Fatal(err)
}
if Enabled() {
t.Errorf("a desktop entry with no Exec reads as on:\n%s", head)
}
if err := os.WriteFile(p, nil, 0o644); err != nil {
t.Fatal(err)
}
if Enabled() {
t.Error("an empty desktop entry reads as on")
}
}
Loading