Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
983de49
Render Merlin startup script safely
maghuro Sep 27, 2026
5fc9304
Make Merlin hooks idempotent and uninstall safe
maghuro Sep 27, 2026
ab7277a
Validate Merlin service PID ownership
maghuro Sep 27, 2026
ef63ced
Make Merlin service install and uninstall reversible
maghuro Sep 27, 2026
e67e7b6
Rollback partial Merlin service installs cleanly
maghuro Sep 27, 2026
4dd7d05
Test Merlin service lifecycle safeguards
maghuro Sep 27, 2026
c1463c2
Fix Merlin PID ownership test escape
maghuro Sep 27, 2026
e00ac4a
Wait for Merlin service child exec
maghuro Sep 27, 2026
5f24b68
Test Merlin child exec startup wait
maghuro Sep 27, 2026
104fd0b
Preserve shared Merlin hook ownership on install rollback
maghuro Sep 27, 2026
74e0dc2
Report stopped Merlin service state correctly
maghuro Sep 27, 2026
eff5a93
Test Merlin stopped service status handling
maghuro Sep 27, 2026
19ae9cb
Preserve Merlin service argument boundaries
maghuro Sep 27, 2026
a5758e1
Test Merlin service shell quoting
maghuro Sep 27, 2026
604a367
Publish Merlin startup script atomically
maghuro Sep 27, 2026
f067b46
Test atomic Merlin startup publication
maghuro Sep 27, 2026
1d66661
Resume interrupted Merlin service installs safely
maghuro Sep 27, 2026
5824840
Validate dnsmasq PID before diskmon signal
maghuro Sep 27, 2026
412ea6f
Test diskmon dnsmasq PID ownership check
maghuro Sep 27, 2026
28720b8
Avoid destructive Merlin hook reappend
maghuro Sep 27, 2026
5eeefb8
Test non-destructive Merlin hook editing
maghuro Sep 27, 2026
fd8bae3
Keep Merlin directory fsync Unix-specific
maghuro Sep 27, 2026
8880826
Preserve pre-existing Merlin hook lines on rollback
maghuro Sep 27, 2026
b96a920
Test Merlin hook append provenance
maghuro Sep 27, 2026
43107b9
Fix Merlin hook editor test escape
maghuro Sep 27, 2026
0c6a4ce
Validate Merlin shared hooks and preserve concurrent lines
maghuro Sep 27, 2026
a4f5f75
Test Merlin shared hook executability
maghuro Sep 27, 2026
d27e84b
Propagate Merlin shared hook append failures
maghuro Sep 27, 2026
8390ac9
Test Merlin hook append failure propagation
maghuro Sep 27, 2026
228244b
Quote Merlin startup hook paths safely
maghuro Sep 27, 2026
18ce907
Share quoted Merlin startup hook lines
maghuro Sep 27, 2026
a76f486
Test quoting of Merlin shared hook paths
maghuro Sep 27, 2026
79e4692
Reject unsafe Merlin shared-hook symlinks
maghuro Sep 27, 2026
98a179d
Test safe Merlin shared-hook path validation
maghuro Sep 27, 2026
85c8239
Make Merlin hook creation race-safe and clean legacy lines
maghuro Sep 27, 2026
894b84a
Test legacy Merlin hook line cleanup
maghuro Sep 27, 2026
870c5ee
Set executable mode explicitly on new Merlin hooks
maghuro Sep 27, 2026
599f294
Test Merlin hook mode under restrictive umask
maghuro Sep 27, 2026
639a9fd
Scope restrictive umask regression to Linux
maghuro Sep 27, 2026
97b08f4
Scope Merlin POSIX mode tests off Windows
maghuro Sep 27, 2026
cd368d4
Reject symlinked Merlin startup scripts
maghuro Sep 27, 2026
79b0499
Make Merlin startup path validation testable
maghuro Sep 27, 2026
4cab0f1
Test Merlin startup symlink rejection
maghuro Sep 27, 2026
01d063d
Pin Merlin startup validation to one descriptor
maghuro Sep 27, 2026
9680e70
Keep Merlin startup tests portable off Linux
maghuro Sep 27, 2026
b611444
Use descriptor-pinned Merlin startup preparation
maghuro Sep 27, 2026
20a2f83
Test descriptor-pinned Merlin startup preparation
maghuro Sep 27, 2026
ae44e5e
Serialize Merlin service lifecycle mutations
maghuro Sep 27, 2026
4722c73
Serialize Merlin service lifecycle mutations
maghuro Sep 27, 2026
a043c2a
Migrate recognized legacy Merlin startup script safely
maghuro Sep 27, 2026
dc708c6
Migrate recognized legacy Merlin startup script portably
maghuro Sep 27, 2026
847c1a5
Migrate legacy Merlin startup scripts on service start
maghuro Sep 27, 2026
e4ecdf5
Test safe Merlin startup migration on upgrade
maghuro Sep 27, 2026
ac0626f
Open Merlin startup script writable for legacy migration
maghuro Sep 27, 2026
2365644
Open Merlin startup script writable for legacy migration
maghuro Sep 27, 2026
3da0f98
Harden Merlin lifecycle lock path
maghuro Sep 27, 2026
ab2ce89
Fix Merlin lock error style
maghuro Sep 27, 2026
ccf61fe
Preserve installed Merlin startup arguments during refresh
maghuro Sep 27, 2026
9fe809f
Migrate Merlin startup scripts atomically
maghuro Sep 27, 2026
289e609
Keep portable Merlin startup migration atomic
maghuro Sep 27, 2026
5f33499
Test Merlin startup argument recovery and conservative refresh
maghuro Sep 27, 2026
09b4279
Migrate Merlin startup from the new running binary
maghuro Sep 27, 2026
f846a1c
Run Merlin startup migration from upgraded binary
maghuro Sep 27, 2026
99968eb
Serialize Merlin stop and restart lifecycle
maghuro Sep 27, 2026
8029987
Make upgraded-binary Merlin migration testable
maghuro Sep 27, 2026
1d5b756
Test first upgraded-binary Merlin migration
maghuro Sep 27, 2026
51f2f93
Start safely when legacy Merlin migration cannot publish
maghuro Sep 27, 2026
55c3548
Publish Merlin startup migration with atomic target verification
maghuro Sep 27, 2026
6aba9a3
Revalidate portable Merlin startup migration content
maghuro Sep 27, 2026
cc594d2
Make Merlin migration fallback decision testable
maghuro Sep 27, 2026
dc8520d
Test safe fallback after Merlin migration failure
maghuro Sep 27, 2026
1541952
Test atomic Merlin migration race preservation
maghuro Sep 27, 2026
793e377
Keep published Merlin startup script for resumable install
maghuro Sep 27, 2026
643c574
Expose atomic Merlin restart transaction
maghuro Sep 27, 2026
4f43ebc
Serialize real CLI restart transactions on Merlin
maghuro Sep 27, 2026
d87063f
Preserve restart behavior after stop wait timeout
maghuro Sep 27, 2026
14cf317
Make Merlin migration rollback non-clobbering
maghuro Sep 27, 2026
9fff4c6
Limit atomic restart transaction to Merlin
maghuro Sep 27, 2026
ca31579
Keep non-Merlin restart wait flow unchanged
maghuro Sep 27, 2026
d6db326
Harden Merlin hook publication and PID stop handling
maghuro Sep 27, 2026
4f2e88c
Test Merlin stop uses validated PID
maghuro Sep 27, 2026
6db0253
Test shared Merlin hook publication is no-clobber
maghuro Sep 27, 2026
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
12 changes: 12 additions & 0 deletions cmd/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,18 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
os.Exit(0)
}

// On Merlin, self-upgrade replaces the executable before the old process
// invokes Service.Start. The newly launched `ctrld run` process is therefore
// the first guaranteed execution point for new service-lifecycle code.
// Migrate an exact ctrld-owned legacy startup script here. Failure is
// deliberately non-fatal: the legacy script remains atomically intact and
// the next daemon start retries the migration.
if router.Name() == "merlin" {
if err := router.RefreshMerlinStartupScriptFromRunningBinary(); err != nil {
mainLog.Load().Warn().Err(err).Msg("could not refresh Merlin startup script")
}
}

p.onStarted = append(p.onStarted, func() {
for _, lc := range p.cfg.Listener {
if shouldAllocateLoopbackIP(lc.IP) {
Expand Down
162 changes: 117 additions & 45 deletions cmd/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,18 @@ func initStopCmd() *cobra.Command {
return stopCmd
}

type restartWithBetweenService interface {
RestartWith(func() error) error
}

func restartServiceTransaction(s service.Service, between func() error) (bool, error) {
tx, ok := s.(restartWithBetweenService)
if !ok {
return false, nil
}
return true, tx.RestartWith(between)
}

func initRestartCmd() *cobra.Command {
restartCmd := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -726,49 +738,77 @@ func initRestartCmd() *cobra.Command {
}

doRestart := func() bool {
tasks := []task{
{s.Stop, true, "Stop"},
{func() error {
p.router.Cleanup()
// restore static DNS settings or DHCP
p.resetDNS(false, true)
return nil
}, false, "Cleanup"},
{func() error {
time.Sleep(time.Second * 1)
return nil
}, false, "Waiting for service to stop"},
}
if doTasks(tasks) {
between := func() error {
p.router.Cleanup()
// restore static DNS settings or DHCP
p.resetDNS(false, true)
time.Sleep(time.Second)

if router.WaitProcessExited() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

loop:
for {
select {
case <-ctx.Done():
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
break loop
return nil
default:
}
time.Sleep(time.Second)
if status, _ := s.Status(); status == service.StatusStopped {
break
}
time.Sleep(time.Second)
}
}
} else {
return false
return nil
}

tasks = []task{
{s.Start, true, "Start"},
if _, ok := s.(restartWithBetweenService); ok {
return doTasks([]task{
{func() error {
_, err := restartServiceTransaction(s, between)
return err
}, true, "Restart"},
})
}

return doTasks(tasks)
// Preserve the existing restart behavior byte-for-byte in spirit for
// non-Merlin service backends. Only Merlin opts into RestartWith.
tasks := []task{
{s.Stop, true, "Stop"},
{func() error {
p.router.Cleanup()
p.resetDNS(false, true)
return nil
}, false, "Cleanup"},
{func() error {
time.Sleep(time.Second)
return nil
}, false, "Waiting for service to stop"},
}
if !doTasks(tasks) {
return false
}
if router.WaitProcessExited() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

loop:
for {
select {
case <-ctx.Done():
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
break loop
default:
}
time.Sleep(time.Second)
if status, _ := s.Status(); status == service.StatusStopped {
break
}
}
}
return doTasks([]task{{s.Start, true, "Start"}})
}

if doRestart() {
Expand Down Expand Up @@ -1288,25 +1328,58 @@ func initUpgradeCmd() *cobra.Command {
if !svcInstalled {
return true
}
tasks := []task{
{s.Stop, true, "Stop"},
{func() error {
p.router.Cleanup()
// restore static DNS settings or DHCP
p.resetDNS(false, true)
return nil
}, false, "Cleanup"},
{func() error {
time.Sleep(time.Second * 1)
return nil
}, false, "Waiting for service to stop"},
}
if doTasks(tasks) {

between := func() error {
p.router.Cleanup()
// restore static DNS settings or DHCP
p.resetDNS(false, true)
time.Sleep(time.Second)

if router.WaitProcessExited() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

for {
select {
case <-ctx.Done():
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
return nil
default:
}
if status, _ := s.Status(); status == service.StatusStopped {
break
}
time.Sleep(time.Second)
}
}
return nil
}

if _, ok := s.(restartWithBetweenService); ok {
_, err := restartServiceTransaction(s, between)
if err != nil {
mainLog.Load().Error().Err(err).Msg("failed to restart ctrld service")
return false
}
} else {
// Keep the historical upgrade restart path for every non-Merlin
// backend; they do not participate in the new Merlin flock.
tasks := []task{
{s.Stop, true, "Stop"},
{func() error {
p.router.Cleanup()
p.resetDNS(false, true)
return nil
}, false, "Cleanup"},
{func() error {
time.Sleep(time.Second)
return nil
}, false, "Waiting for service to stop"},
}
if doTasks(tasks) && router.WaitProcessExited() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

loop:
for {
select {
Expand All @@ -1321,21 +1394,20 @@ func initUpgradeCmd() *cobra.Command {
}
}
}
if !doTasks([]task{{s.Start, true, "Start"}}) {
return false
}
}

tasks = []task{
{s.Start, true, "Start"},
}
if doTasks(tasks) {
if dir, err := socketDir(); err == nil {
if cc := newSocketControlClient(context.TODO(), s, dir); cc != nil {
_, _ = cc.post(ifacePath, nil)
return true
}
if dir, err := socketDir(); err == nil {
if cc := newSocketControlClient(context.TODO(), s, dir); cc != nil {
_, _ = cc.post(ifacePath, nil)
return true
}
}
return false
}

if svcInstalled {
mainLog.Load().Debug().Msg("Restarting ctrld service using new binary")
}
Expand Down
Loading
Loading