[client/ui] Add async Up mode to avoid blocking profile switches

The daemon's Up RPC previously always blocked in waitForUp (up to 50s)
until the engine connected. The UI does not need this — status updates
already flow through the SubscribeStatus stream.

Add bool async = 4 to UpRequest. When true the daemon starts
connectWithRetryRuns and returns immediately; the CLI path (async=false,
the default) is unchanged.

ProfileSwitcher.SwitchActive now sets Async:true so all three RPCs
(Status, Switch, Down, Up) return quickly. The background goroutine and
its associated race condition are removed entirely.
This commit is contained in:
Zoltan Papp
2026-05-13 15:51:36 +02:00
parent 803144e569
commit 53b2fb8dc1
5 changed files with 51 additions and 23 deletions

View File

@@ -42,6 +42,11 @@ type WaitSSOParams struct {
type UpParams struct {
ProfileName string `json:"profileName"`
Username string `json:"username"`
// Async instructs the daemon to start the connection and return
// immediately. Status updates flow via the SubscribeStatus stream.
// When false (the default) the RPC blocks until connected, which is
// the CLI behaviour.
Async bool `json:"async"`
}
// LogoutParams selects the profile the daemon should log out.
@@ -147,7 +152,7 @@ func (s *Connection) Up(ctx context.Context, p UpParams) error {
if err != nil {
return err
}
req := &proto.UpRequest{}
req := &proto.UpRequest{Async: p.Async}
if p.ProfileName != "" {
req.ProfileName = ptrStr(p.ProfileName)
}

View File

@@ -37,8 +37,9 @@ func NewProfileSwitcher(profiles *Profiles, connection *Connection, peers *Peers
}
// SwitchActive switches to the named profile applying the reconnect policy.
// It returns after the Switch RPC completes so the caller can refresh its UI
// immediately; Down and Up run in a background goroutine.
// All RPCs complete quickly: Up uses async mode so the daemon starts the
// connection attempt and returns immediately; status updates flow via the
// SubscribeStatus stream.
func (s *ProfileSwitcher) SwitchActive(ctx context.Context, p ProfileRef) error {
prevStatus := ""
if st, err := s.peers.Get(ctx); err == nil {
@@ -61,22 +62,21 @@ func (s *ProfileSwitcher) SwitchActive(ctx context.Context, p ProfileRef) error
return fmt.Errorf("switch profile %q: %w", p.ProfileName, err)
}
go func() {
bgCtx := context.Background()
if needsDown {
if err := s.connection.Down(bgCtx); err != nil {
log.Errorf("profileswitcher: Down: %v", err)
}
if needsDown {
if err := s.connection.Down(ctx); err != nil {
log.Errorf("profileswitcher: Down: %v", err)
}
if wasActive {
if err := s.connection.Up(bgCtx, UpParams{
ProfileName: p.ProfileName,
Username: p.Username,
}); err != nil {
log.Errorf("profileswitcher: Up %s: %v", p.ProfileName, err)
}
}
if wasActive {
if err := s.connection.Up(ctx, UpParams{
ProfileName: p.ProfileName,
Username: p.Username,
Async: true,
}); err != nil {
return fmt.Errorf("reconnect %q: %w", p.ProfileName, err)
}
}()
}
return nil
}