From daf50261922754b4a5a2a62eff1394be4ab44758 Mon Sep 17 00:00:00 2001 From: riccardom Date: Thu, 18 Jun 2026 19:23:41 +0200 Subject: [PATCH] Adds restart for MDM --- client/internal/connect.go | 8 ++++++ client/internal/connect_lifecycle.go | 39 +++++++++++++++++++++++++--- client/server/mdm.go | 19 ++++++-------- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/client/internal/connect.go b/client/internal/connect.go index 04bb85c36..fb759ace4 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -111,6 +111,14 @@ func (c *ConnectClient) RunAsync(config *profilemanager.Config, md metadata.MD) c.sup.startAsync(config, md, c.mobileDependency(config), "", nil) } +// Restart atomically stops any in-flight run and starts a fresh one with the +// given config. The stop+start happens as a single supervisor operation, so no +// other lifecycle request can interleave between them — used for explicit +// restarts (e.g. an MDM policy change) that must not expose a "stopped" window. +func (c *ConnectClient) Restart(config *profilemanager.Config, md metadata.MD) { + c.sup.restartAsync(config, md, c.mobileDependency(config), "") +} + // WaitEstablishedOrDone blocks until the in-flight run becomes established (nil), // ends before that (the run error, or a sentinel on a clean stop), or ctx is // cancelled. Returns errNoRunInFlight if no run is in flight. Wraps the wait on diff --git a/client/internal/connect_lifecycle.go b/client/internal/connect_lifecycle.go index 859d20def..7515563d2 100644 --- a/client/internal/connect_lifecycle.go +++ b/client/internal/connect_lifecycle.go @@ -26,6 +26,7 @@ type lifecycleOp int const ( opStart lifecycleOp = iota opStop + opRestart opStatus opWaitEstablished ) @@ -119,6 +120,8 @@ func (s *supervisor) loop() { s.handleStart(cmd) case opStop: s.handleStop(cmd) + case opRestart: + s.handleRestart(cmd) case opStatus: cmd.reply <- (s.isRunningInternal()) case opWaitEstablished: @@ -159,14 +162,30 @@ func (s *supervisor) handleStop(cmd lifecycleCmd) { notify(cmd.done, nil) return } + s.stopCurrentRun() + notify(cmd.done, nil) +} - // Cancel the in-flight run and block the supervisor until it has fully - // unwound, so the next queued command (e.g. a fresh start) starts from a - // clean slate. The run goroutine reports completion via runEnded. +// handleRestart tears down any in-flight run and starts a fresh one in a single +// loop turn. No other command can interleave between the stop and the start +// (the loop is single-threaded), so the swap is atomic without relying on any +// daemon-side lock — that is what an explicit restart (e.g. MDM config change) +// needs to avoid a window where the client is observably stopped. +func (s *supervisor) handleRestart(cmd lifecycleCmd) { + if s.curStart != nil { + s.stopCurrentRun() + } + s.handleStart(cmd) +} + +// stopCurrentRun cancels the in-flight run and blocks the supervisor until it +// has fully unwound, so the next action starts from a clean slate. The run +// goroutine reports completion via runEnded. Caller must hold an in-flight run +// (curStart != nil). +func (s *supervisor) stopCurrentRun() { s.runCancel() res := <-s.runEnded s.finishRun(res.err) - notify(cmd.done, nil) } // finishRun resets lifecycle state after a run terminates and hands the run @@ -243,6 +262,18 @@ func (s *supervisor) startAsync(config *profilemanager.Config, md metadata.MD, m } } +// restartAsync enqueues an atomic stop+start without blocking. The supervisor +// tears down any in-flight run and starts a fresh one with the supplied config +// in a single loop turn (see handleRestart). Fire-and-forget: the new run owns +// its lifecycle channels, observed via waitEstablishedOrDone. +func (s *supervisor) restartAsync(config *profilemanager.Config, md metadata.MD, mobileDep MobileDependency, logPath string) { + cmd := lifecycleCmd{op: opRestart, config: config, md: md, mobileDep: mobileDep, logPath: logPath} + select { + case s.cmdCh <- cmd: + case <-s.ctx.Done(): + } +} + // start enqueues a start and blocks until the run terminates, preserving the // blocking contract of the legacy Run entry points. func (s *supervisor) start(config *profilemanager.Config, md metadata.MD, mobileDep MobileDependency, logPath string) error { diff --git a/client/server/mdm.go b/client/server/mdm.go index d9114ab89..ef18ba08e 100644 --- a/client/server/mdm.go +++ b/client/server/mdm.go @@ -61,13 +61,9 @@ func (s *Server) onMDMPolicyChange(_, _ *mdm.Policy) error { return nil } - // End the in-flight run through the supervisor. Stop blocks until the run - // has fully unwound (the supervisor is the single place a run is stopped), - // so by the time it returns we can safely start a fresh run with the new - // config — no separate quiescence wait needed. - if err := s.connectClient.Stop(); err != nil { - log.Warnf("MDM restart: failed to stop current run: %v", err) - } + // Cancel daemon-side login/status activities tied to the old run; the run + // itself is torn down atomically by the supervisor inside Restart (see + // restartEngineForMDMLocked), which stops and re-starts in one operation. if s.actCancel != nil { s.actCancel() } @@ -142,10 +138,11 @@ func (s *Server) restartEngineForMDMLocked() error { _, cancel := context.WithCancel(s.rootCtx) s.actCancel = cancel - log.Info("MDM restart: starting a fresh run with re-resolved config") - // MDM restart has no incoming RPC metadata; fire and forget (the run owns - // its established/done channels, the supervisor reconnects internally). - s.connectClient.RunAsync(config, nil) + log.Info("MDM restart: atomically restarting the run with re-resolved config") + // MDM restart has no incoming RPC metadata; fire and forget. Restart is a + // single supervisor op (atomic stop+start), so there is no observable + // "stopped" window between tearing down the old run and starting the new. + s.connectClient.Restart(config, nil) s.publishConfigChangedEvent("mdm") return nil }