Skip to content
Merged
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
5 changes: 5 additions & 0 deletions cmd/berth/agentprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions cmd/berthd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down
8 changes: 7 additions & 1 deletion docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
40 changes: 39 additions & 1 deletion internal/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func Render(s Spec) ([]byte, error) {
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
<key>ThrottleInterval</key><integer>5</integer>
<key>ProcessType</key><string>Background</string>
<key>ProcessType</key><string>Interactive</string>
`)
if s.KeepChildren {
b.WriteString("<key>AbandonProcessGroup</key><true/>\n")
Expand Down Expand Up @@ -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.
{"<key>ProcessType</key><string>Background</string>\n", "<key>ProcessType</key><string>Interactive</string>\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 {
Expand Down
42 changes: 42 additions & 0 deletions internal/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func TestUnitsRestartOnlyAfterFailure(t *testing.T) {
"<key>RunAtLoad</key><true/>",
"<string>/opt/berth/bin/berth</string>\n<string>agent</string>",
"<key>BERTH_HOME</key><string>/Users/alex/Library/Application Support/berth</string>",
"<key>ProcessType</key><string>Interactive</string>",
} {
if !strings.Contains(string(plist), want) {
t.Errorf("plist missing %q:\n%s", want, plist)
Expand Down Expand Up @@ -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), "<string>Interactive</string>", "<string>Background</string>", 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)
}
}
Loading