[client] Drop DownAsync RPC in favor of bounded sync Down on quit

Remove the DownAsync RPC and call the plain Down RPC from the tray Quit
handler under the existing 5s client-side deadline.

Why DownAsync did not pay for itself:

- Its premise ("quitting never blocks on the engine shutdown") did not
  hold: engine.Stop() ran synchronously inside beginDown, so the GUI
  blocked on it either way. Only the retry-goroutine wait (typically
  near-zero, at most 5s when wedged) moved off the caller.

- The daemon's Down handler never consults the caller's context, so the
  client-side deadline in handleQuit already bounds how long Quit waits
  while the daemon completes the full teardown regardless of the caller
  timing out. Same disconnect guarantee, no extra API surface.

- A DownAsync that truly returned before the engine shutdown would have
  to return right after actCancel and stop the engine on a background
  goroutine, opening an Up-after-Down race (new engine starting while
  the old one is still tearing down) that would need an Up-side guard.
  That is a connect-lifecycle refactor (see the TODO in
  cleanupConnection about run-loop shutdown ownership), not a quit-UX
  fix.

Down keeps the error handling from the previous commit: engine.Stop()
failures are logged and cleanup continues so the daemon always returns
to Idle, and ErrServiceNotUp returns without an error log. The generated
proto files are restored to their previous content rather than
regenerated.
This commit is contained in:
Zoltan Papp
2026-07-20 19:04:27 +02:00
parent 79dd5c5fc1
commit 01ad77895e
7 changed files with 118 additions and 339 deletions

View File

@@ -1074,31 +1074,13 @@ func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfi
}
// Down engine work in the daemon.
func (s *Server) Down(_ context.Context, _ *proto.DownRequest) (*proto.DownResponse, error) {
giveUpChan, err := s.beginDown()
if err != nil {
return nil, err
}
s.finishDown(giveUpChan)
return &proto.DownResponse{}, nil
}
func (s *Server) DownAsync(_ context.Context, _ *proto.DownAsyncRequest) (*proto.DownAsyncResponse, error) {
giveUpChan, err := s.beginDown()
if err != nil {
return nil, err
}
go s.finishDown(giveUpChan)
return &proto.DownAsyncResponse{}, nil
}
func (s *Server) beginDown() (chan struct{}, error) {
func (s *Server) Down(ctx context.Context, _ *proto.DownRequest) (*proto.DownResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
giveUpChan := s.clientGiveUpChan
if err := s.cleanupConnection(); err != nil {
s.mutex.Unlock()
if errors.Is(err, ErrServiceNotUp) {
return nil, err
}
@@ -1106,10 +1088,8 @@ func (s *Server) beginDown() (chan struct{}, error) {
return nil, err
}
return giveUpChan, nil
}
s.mutex.Unlock()
func (s *Server) finishDown(giveUpChan chan struct{}) {
// Wait for the connectWithRetryRuns goroutine to finish with a short timeout.
// This prevents the goroutine from setting ErrResetConnection after Down() returns.
// The giveUpChan is closed by the goroutine's deferred cleanup (see
@@ -1138,6 +1118,8 @@ func (s *Server) finishDown(giveUpChan chan struct{}) {
// failed to log in.
s.statusRecorder.MarkManagementDisconnected(nil)
s.statusRecorder.MarkSignalDisconnected(nil)
return &proto.DownResponse{}, nil
}
func (s *Server) cleanupConnection() error {