Skip to content

Commit 2f15628

Browse files
committed
fix: re-exec binary after auto-upgrade instead of requiring manual restart
When installed via `curl | bash`, brew installs the tap version which may be outdated. The old binary's auto-upgrade downloads the new version but continued running the old code in memory, causing hangs in older versions. Now uses syscall.Exec to seamlessly replace the process with the upgraded binary.
1 parent db6adab commit 2f15628

File tree

2 files changed

+37
-2
lines changed

2 files changed

+37
-2
lines changed

internal/updater/updater.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strconv"
1313
"strings"
1414
"sync"
15+
"syscall"
1516
"time"
1617

1718
"github.com/openbootdotdev/openboot/internal/ui"
@@ -193,8 +194,9 @@ func doBrewUpgrade(currentVersion, latestVersion string) {
193194
ui.Muted("Run 'brew upgrade openboot' to update manually")
194195
fmt.Println()
195196
} else {
196-
ui.Success(fmt.Sprintf("Updated to v%s. Restart openboot to use the new version.", latestClean))
197+
ui.Success(fmt.Sprintf("Updated to v%s. Restarting...", latestClean))
197198
fmt.Println()
199+
execSelf()
198200
}
199201
}
200202

@@ -208,8 +210,29 @@ func doDirectUpgrade(currentVersion, latestVersion string) {
208210
fmt.Println()
209211
return
210212
}
211-
ui.Success(fmt.Sprintf("Updated to v%s. Restart openboot to use the new version.", latestClean))
213+
ui.Success(fmt.Sprintf("Updated to v%s. Restarting...", latestClean))
212214
fmt.Println()
215+
execSelf()
216+
}
217+
218+
// execSelf is a package-level variable to allow test injection.
219+
var execSelf = func() {
220+
exe, err := os.Executable()
221+
if err != nil {
222+
ui.Warn(fmt.Sprintf("Could not restart: %v", err))
223+
ui.Muted("Please run the command again to use the new version.")
224+
return
225+
}
226+
exe, err = filepath.EvalSymlinks(exe)
227+
if err != nil {
228+
ui.Warn(fmt.Sprintf("Could not restart: %v", err))
229+
ui.Muted("Please run the command again to use the new version.")
230+
return
231+
}
232+
if err := syscall.Exec(exe, os.Args, os.Environ()); err != nil {
233+
ui.Warn(fmt.Sprintf("Could not restart: %v", err))
234+
ui.Muted("Please run the command again to use the new version.")
235+
}
213236
}
214237

215238
func DownloadAndReplace() error {

internal/updater/updater_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,15 @@ func TestDoBrewUpgrade_Success(t *testing.T) {
407407
execBrewUpgrade = func(formula string) error { calledWith = formula; return nil }
408408
defer func() { execBrewUpgrade = origExec }()
409409

410+
restarted := false
411+
origSelf := execSelf
412+
execSelf = func() { restarted = true }
413+
defer func() { execSelf = origSelf }()
414+
410415
doBrewUpgrade("1.0.0", "v2.0.0")
411416

412417
assert.Equal(t, brewFormula, calledWith)
418+
assert.True(t, restarted, "should re-exec after successful upgrade")
413419
}
414420

415421
func TestDoBrewUpgrade_Failure(t *testing.T) {
@@ -418,9 +424,15 @@ func TestDoBrewUpgrade_Failure(t *testing.T) {
418424
execBrewUpgrade = func(formula string) error { called = true; return fmt.Errorf("brew failed") }
419425
defer func() { execBrewUpgrade = origExec }()
420426

427+
restarted := false
428+
origSelf := execSelf
429+
execSelf = func() { restarted = true }
430+
defer func() { execSelf = origSelf }()
431+
421432
doBrewUpgrade("1.0.0", "v2.0.0")
422433

423434
assert.True(t, called, "brew should have been attempted")
435+
assert.False(t, restarted, "should not re-exec on failed upgrade")
424436
}
425437

426438
// --- End-to-end: AutoUpgrade with Homebrew (via resolveLatestVersion) ---

0 commit comments

Comments
 (0)