diff --git a/cmd/berth/agentprocess.go b/cmd/berth/agentprocess.go
index 491c58be6..de01022ce 100644
--- a/cmd/berth/agentprocess.go
+++ b/cmd/berth/agentprocess.go
@@ -97,6 +97,11 @@ func runAgent(l laptop) error {
ctx, stop := signalContext()
defer stop()
cfg := agent.Config{Dir: l.dir, Socket: l.socket()}
+ if spec, err := agentService(l); err == nil {
+ if _, err := service.Migrate(spec); err != nil {
+ fmt.Fprintf(os.Stderr, "berth agent: updating its service: %v\n", err)
+ }
+ }
// The app's Copy diagnostics shows what `berth doctor` would (GET /v1/doctor).
cfg.Doctor = func(ctx context.Context) []doctor.Check { return laptopChecks(ctx, l) }
// A second agent (a test, a throwaway home) needs its own ports.
diff --git a/cmd/berthd/main.go b/cmd/berthd/main.go
index 21194a018..949bab896 100644
--- a/cmd/berthd/main.go
+++ b/cmd/berthd/main.go
@@ -268,6 +268,9 @@ func serve(b boxHome, args []string) error {
// A plist from an older install gives launchd's bare PATH, without
// Homebrew's tmux; find it anyway.
service.AugmentPATH()
+ if _, err := service.Migrate(daemonService(b, "")); err != nil {
+ fmt.Fprintf(os.Stderr, "berthd: updating its launchd plist: %v\n", err)
+ }
}
utf8Locale()
if *listen == "" {
diff --git a/docs/changelog.mdx b/docs/changelog.mdx
index d2ef407ec..b9870409d 100644
--- a/docs/changelog.mdx
+++ b/docs/changelog.mdx
@@ -10,6 +10,13 @@ upgrades with `berth upgrade BOX`, or by running the
again.
## Unreleased
+- **Agents on a Mac run at full speed.** Shipyard's login agents (the
+ Mac app's agent, and `berthd` on a Mac box) used to ask macOS to run
+ them as background work, which slows their CPU and disk, and with them
+ every agent and build they start. They now run as interactive work.
+ Updating the app applies it to this Mac right away. A Mac box updated
+ with **Update all** gets it the next time it restarts or you log in
+ there; running the install command again applies it at once.
- **Back and Forward, as in a browser.** The arrows at the top of the
sidebar, ⌘[ and ⌘] (also in the Go menu and ⌘K), and a mouse's back and
forward buttons take you through the places you've been: a page such as
@@ -23,7 +30,6 @@ again.
a menu to add a project or a box. Those stay on the rail's own + and
the boxes row at the bottom of the sidebar.
-
## v0.3.29
- **Removing a worktree no longer asks first.** Remove worktree in the
diff --git a/internal/service/service.go b/internal/service/service.go
index a8a968d6d..fdab1605e 100644
--- a/internal/service/service.go
+++ b/internal/service/service.go
@@ -152,7 +152,7 @@ func Render(s Spec) ([]byte, error) {
RunAtLoad
KeepAliveSuccessfulExit
ThrottleInterval5
-ProcessTypeBackground
+ProcessTypeInteractive
`)
if s.KeepChildren {
b.WriteString("AbandonProcessGroup\n")
@@ -191,6 +191,44 @@ func Render(s Spec) ([]byte, error) {
func esc(s string) string { return html.EscapeString(s) }
+// Lines older releases wrote, and what Render writes in their place now.
+var migrations = []struct{ old, new string }{
+ // Background throttles the CPU and disk of the service and of everything
+ // it starts, agents and their builds included, as they inherit it.
+ {"ProcessTypeBackground\n", "ProcessTypeInteractive\n"},
+}
+
+// Migrate brings the unit an older release wrote for s up to date in place,
+// so it is Installed again, and says whether it changed it. A unit for
+// another program or BERTH_HOME (another install, a test's) is left alone.
+// Only the file changes: launchd reads it at the next load (a login, a
+// reinstall), and a running service is never restarted for it. A berthd
+// upgraded from the app swaps its binary without reinstalling, so this is
+// how its plist catches up.
+func Migrate(s Spec) (bool, error) {
+ u, ok, err := Read(s.Name)
+ if err != nil || !ok || u.Program != s.Program || u.Env["BERTH_HOME"] != s.Env["BERTH_HOME"] {
+ return false, err
+ }
+ have, err := os.ReadFile(u.Path)
+ if err != nil {
+ return false, err
+ }
+ path := u.Path
+ data := string(have)
+ for _, m := range migrations {
+ data = strings.ReplaceAll(data, m.old, m.new)
+ }
+ if data == string(have) {
+ return false, nil
+ }
+ tmp := path + ".tmp"
+ if err := os.WriteFile(tmp, []byte(data), 0o644); err != nil {
+ return false, err
+ }
+ return true, os.Rename(tmp, path)
+}
+
func systemdArgs(args []string) string {
quoted := make([]string, len(args))
for i, a := range args {
diff --git a/internal/service/service_test.go b/internal/service/service_test.go
index b5ecac16c..0956b6610 100644
--- a/internal/service/service_test.go
+++ b/internal/service/service_test.go
@@ -44,6 +44,7 @@ func TestUnitsRestartOnlyAfterFailure(t *testing.T) {
"RunAtLoad",
"/opt/berth/bin/berth\nagent",
"BERTH_HOME/Users/alex/Library/Application Support/berth",
+ "ProcessTypeInteractive",
} {
if !strings.Contains(string(plist), want) {
t.Errorf("plist missing %q:\n%s", want, plist)
@@ -327,3 +328,44 @@ func TestPreflightOnAMacNeedsALoginSession(t *testing.T) {
t.Errorf("without a GUI session: %v", err)
}
}
+
+func TestMigrateBringsAnOlderPlistUpToDateWithoutReloading(t *testing.T) {
+ home, calls := stub(t, "darwin")
+ plist, err := Render(spec)
+ if err != nil {
+ t.Fatal(err)
+ }
+ old := strings.Replace(string(plist), "Interactive", "Background", 1)
+ path := filepath.Join(home, "Library", "LaunchAgents", spec.Name+".plist")
+ os.MkdirAll(filepath.Dir(path), 0o755)
+ os.WriteFile(path, []byte(old), 0o644)
+
+ other := spec
+ other.Program = "/somewhere/else/berth"
+ if changed, err := Migrate(other); err != nil || changed {
+ t.Fatalf("another program's plist: changed=%v err=%v", changed, err)
+ }
+ if Installed(spec) {
+ t.Fatal("an older plist counts as installed before Migrate")
+ }
+ if changed, err := Migrate(spec); err != nil || !changed {
+ t.Fatalf("changed=%v err=%v", changed, err)
+ }
+ if !Installed(spec) {
+ data, _ := os.ReadFile(path)
+ t.Fatalf("after Migrate the plist is not what Render writes:\n%s", data)
+ }
+ if changed, err := Migrate(spec); err != nil || changed {
+ t.Fatalf("a second Migrate: changed=%v err=%v", changed, err)
+ }
+ if len(*calls) != 0 {
+ t.Fatalf("Migrate ran %v; it must not reload the service", *calls)
+ }
+}
+
+func TestMigrateWithoutAPlist(t *testing.T) {
+ stub(t, "darwin")
+ if changed, err := Migrate(spec); err != nil || changed {
+ t.Fatalf("changed=%v err=%v", changed, err)
+ }
+}