@@ -125,6 +125,11 @@ type Service struct {
125125 platform * PlatformIO
126126 onEvent atomic.Pointer [func (int32 , string )]
127127 subCancel atomic.Pointer [func ()]
128+
129+ // startMu serializes StartWithContent calls. A concurrent caller waits
130+ // for the current startup to finish, then stops and restarts.
131+ startMu sync.Mutex
132+ startSeq atomic.Int64 // coalesce: only the latest caller proceeds
128133}
129134
130135func NewService () * Service { return & Service {platform : NewPlatformIO ()} }
@@ -175,28 +180,30 @@ func (s *Service) Init(optionsJSON string) error {
175180}
176181
177182func (s * Service ) StartWithContent (content , ruleSetProxy string ) error {
183+ // Translate config outside the lock — pure computation, no shared state.
178184 data := []byte (content )
179185 jsonContent , err := s .translateConfig (data , translator .DetectFormat (data ), ruleSetProxy )
180186 if err != nil {
181187 return err
182188 }
183189
184- // CAS loop to transition to Starting — only one concurrent caller wins.
185- for {
186- st := s .state .Load ()
187- if st != StateInitialized && st != StateRunning {
188- return fmt .Errorf ("start: invalid state %s" , st )
189- }
190- if s .casState (st , StateStarting ) {
191- break
192- }
190+ mySeq := s .startSeq .Add (1 )
191+
192+ s .startMu .Lock ()
193+ defer s .startMu .Unlock ()
194+
195+ // A newer request arrived while we waited — let it win.
196+ if s .startSeq .Load () != mySeq {
197+ return nil
193198 }
194199
195- old := s .running .Swap (nil )
196- s .unsubscribeHooks ()
200+ // Stop any running/starting instance first.
201+ if err := s .Stop (); err != nil {
202+ slog .Warn ("stop previous instance" , "error" , err )
203+ }
197204
198- if old != nil {
199- old . instance . Close ( )
205+ if ! s . casState ( StateInitialized , StateStarting ) {
206+ return fmt . Errorf ( "start: invalid state %s" , s . State () )
200207 }
201208
202209 if err := s .startWithJSON (jsonContent ); err != nil {
0 commit comments