From 1f7b1ea863c3131422352171be2dd14d309e9202 Mon Sep 17 00:00:00 2001 From: riccardom Date: Tue, 16 Jun 2026 17:30:07 +0200 Subject: [PATCH] IsRunning V1 --- client/internal/connect.go | 6 +++-- client/internal/connect_lifecycle.go | 36 +++++++++++++++++++++------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/client/internal/connect.go b/client/internal/connect.go index 3329c5cd2..48207d7ce 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -466,12 +466,14 @@ func parseRelayInfo(loginResp *mgmProto.LoginResponse) ([]string, *hmac.Token) { } // IsRunning reports whether a client run is currently in flight. It is the -// single source of truth for "is the client running", owned by the supervisor. +// single source of truth for "is the client running", answered by the +// supervisor via a serialized query (so it settles behind an in-flight stop). +// Intended for the external status surface (CLI/UI), not internal pre-checks. func (c *ConnectClient) IsRunning() bool { if c == nil || c.sup == nil { return false } - return c.sup.running.Load() + return c.sup.isRunning() } func (c *ConnectClient) Engine() *Engine { diff --git a/client/internal/connect_lifecycle.go b/client/internal/connect_lifecycle.go index 56854ee58..3c195a78d 100644 --- a/client/internal/connect_lifecycle.go +++ b/client/internal/connect_lifecycle.go @@ -3,7 +3,6 @@ package internal import ( "context" "errors" - "sync/atomic" "github.com/netbirdio/netbird/client/internal/profilemanager" ) @@ -18,13 +17,19 @@ type lifecycleOp int const ( opStart lifecycleOp = iota opStop + opStatus ) -// lifecycleCmd is a single start/stop request handed to the supervisor goroutine. +// lifecycleCmd is a single start/stop/status request handed to the supervisor +// goroutine. All three flow through the same cmdCh so they are strictly +// ordered (FIFO) with respect to each other. +// // done is the caller-supplied notification channel (nil for fire-and-forget): // - for opStart it receives the run's end result when the run terminates, or // errAlreadyRunning immediately if a run is already in flight. // - for opStop it receives nil once the in-flight run has fully unwound. +// +// reply is used only by opStatus: it receives whether a run is in flight. type lifecycleCmd struct { op lifecycleOp config *profilemanager.Config @@ -32,6 +37,7 @@ type lifecycleCmd struct { runningChan chan struct{} logPath string done chan error + reply chan bool } // runEndResult is sent by the run goroutine to the supervisor when a run ends, @@ -60,10 +66,6 @@ type supervisor struct { // runCancel cancels that run. curExecOp *lifecycleCmd runCancel context.CancelFunc - - // running mirrors "a run is in flight" for lock-free reads from outside the - // loop goroutine (the single source of truth for "is the client running"). - running atomic.Bool } func newSupervisor(ctx context.Context, run runFunc) *supervisor { @@ -89,6 +91,8 @@ func (s *supervisor) loop() { s.handleStart(cmd) case opStop: s.handleStop(cmd) + case opStatus: + cmd.reply <- (s.curExecOp != nil) } case res := <-s.runEnded: // Run ended on its own, without an explicit Stop. @@ -106,7 +110,6 @@ func (s *supervisor) handleStart(cmd lifecycleCmd) { runCtx, cancel := context.WithCancel(s.ctx) s.runCancel = cancel s.curExecOp = &cmd - s.running.Store(true) go func(ctx context.Context, cfg *profilemanager.Config, m MobileDependency, rc chan struct{}, lp string) { err := s.run(ctx, cfg, m, rc, lp) @@ -133,7 +136,6 @@ func (s *supervisor) handleStop(cmd lifecycleCmd) { // error back to whoever asked to be notified of the start. func (s *supervisor) finishRun(err error) { s.runCancel = nil - s.running.Store(false) if s.curExecOp != nil { notify(s.curExecOp.done, err) s.curExecOp = nil @@ -183,6 +185,24 @@ func (s *supervisor) start(config *profilemanager.Config, mobileDep MobileDepend } } +// isRunning asks the loop whether a run is in flight. The query is serialized +// with start/stop, so during a stop it waits for the teardown to settle and +// then reports the final state — never a transient "half-stopped". +func (s *supervisor) isRunning() bool { + reply := make(chan bool, 1) + select { + case s.cmdCh <- lifecycleCmd{op: opStatus, reply: reply}: + case <-s.ctx.Done(): + return false + } + select { + case r := <-reply: + return r + case <-s.ctx.Done(): + return false + } +} + // stop enqueues a stop and blocks until the in-flight run is fully torn down. func (s *supervisor) stop() error { done := make(chan error, 1)