From f4e032b6cdf6b5d00cb9e03000e55ee8ab89475c Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Thu, 22 May 2025 14:58:17 -0400 Subject: [PATCH] Fixed setup experience UI hanging when a step is removed from the payload (#29385) This is one facet of https://github.com/fleetdm/fleet/issues/28664 When you run gitops or otherwise just do something to remove a software installer from the setup experience list while it is running and then delete that software installer, setup experience fails to proceed past the "steps" screen because it is expecting all software in the initial payload to complete installation even if those installers were not in the current payload. This now tracks the status of items in the current payload and as a small enhancement deletes the items that disappear from the payload, which seemed like the best thing to do # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [x] Make sure fleetd is compatible with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)). - [x] Orbit runs on macOS, Linux and Windows. Check if the orbit feature/bugfix should only apply to one platform (`runtime.GOOS`). - [x] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [x] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). --- orbit/changes/29380-setup-experience-hanging | 1 + .../pkg/setup_experience/setup_experience.go | 50 ++++++++++--------- 2 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 orbit/changes/29380-setup-experience-hanging diff --git a/orbit/changes/29380-setup-experience-hanging b/orbit/changes/29380-setup-experience-hanging new file mode 100644 index 0000000000..7d2afe99e1 --- /dev/null +++ b/orbit/changes/29380-setup-experience-hanging @@ -0,0 +1 @@ +* Fixed an issue where the setup experience window would never finish and close if a software installer was deleted while it was running diff --git a/orbit/pkg/setup_experience/setup_experience.go b/orbit/pkg/setup_experience/setup_experience.go index e5258b572b..aca51bea3a 100644 --- a/orbit/pkg/setup_experience/setup_experience.go +++ b/orbit/pkg/setup_experience/setup_experience.go @@ -33,9 +33,7 @@ type SetupExperiencer struct { // its Run method is called within a WaitGroup, // and no other parts of Orbit need access to this field (or any other parts of the // SetupExperiencer), it's OK to not protect this with a lock. - sd *swiftdialog.SwiftDialog - // Name of each step -> is that step done - steps map[string]bool + sd *swiftdialog.SwiftDialog uiSteps map[string]swiftdialog.ListItem started bool } @@ -44,7 +42,6 @@ func NewSetupExperiencer(client Client, rootDirPath string) *SetupExperiencer { return &SetupExperiencer{ OrbitClient: client, closeChan: make(chan struct{}), - steps: make(map[string]bool), uiSteps: make(map[string]swiftdialog.ListItem), rootDirPath: rootDirPath, } @@ -109,15 +106,11 @@ func (s *SetupExperiencer) Run(oc *fleet.OrbitConfig) error { } } - s.steps["bootstrap"] = true - if isPending, name := anyProfilePending(payload.ConfigurationProfiles); isPending { log.Info().Msg(fmt.Sprintf("setup experience: profile pending: %s", name)) return nil } - s.steps["config_profiles"] = true - if payload.AccountConfiguration != nil { if payload.AccountConfiguration.Status != fleet.MDMAppleStatusAcknowledged && payload.AccountConfiguration.Status != fleet.MDMAppleStatusError && @@ -128,7 +121,9 @@ func (s *SetupExperiencer) Run(oc *fleet.OrbitConfig) error { } } - s.steps["account_config"] = true + // Note that we are setting this based on the current payload only just in case something + // was removed from the payload that was there earlier(e.g. a deleted software title). + allStepsDone := true // Now render the UI for the software and script. if len(payload.Software) > 0 || payload.Script != nil { @@ -145,6 +140,26 @@ func (s *SetupExperiencer) Run(oc *fleet.OrbitConfig) error { steps = append(steps, payload.Script) } + // Check for any items that were in the payload that are no longer there. This can happen + // if a software title was deleted, for instance + for uiStepName, uiStep := range s.uiSteps { + uiStepExistsInPayload := false + for _, step := range steps { + if uiStep.Title == step.Name { + uiStepExistsInPayload = true + break + } + } + if !uiStepExistsInPayload { + log.Info().Msgf("Setup Experience: list item %s removed from payload", uiStep.Title) + err = s.sd.DeleteListItemByTitle(uiStep.Title) + if err != nil { + log.Info().Err(err).Msg("deleting list item removed from payload from setup experience UI") + } + delete(s.uiSteps, uiStepName) + } + } + for _, step := range steps { currentStepState := resultToListItem(step) if priorStepState, ok := s.uiSteps[step.Name]; ok { @@ -156,7 +171,6 @@ func (s *SetupExperiencer) Run(oc *fleet.OrbitConfig) error { } } else { log.Info().Msgf("setup experience: no change in status for %s", step.Name) - continue } } else { err = s.sd.AddListItem(currentStepState) @@ -164,16 +178,16 @@ func (s *SetupExperiencer) Run(oc *fleet.OrbitConfig) error { log.Info().Err(err).Msg("adding list item in setup experience UI") } s.uiSteps[step.Name] = currentStepState - s.steps[step.Name] = false } if step.Status == fleet.SetupExperienceStatusFailure || step.Status == fleet.SetupExperienceStatusSuccess { stepsDone++ - s.steps[step.Name] = true // The swiftDialog progress bar is out of 100 for range int(float32(1) / float32(len(steps)) * 100) { prog++ } + } else { + allStepsDone = false } } @@ -193,7 +207,7 @@ func (s *SetupExperiencer) Run(oc *fleet.OrbitConfig) error { // If we get here, we can render the "done" UI. - if s.allStepsDone() { + if allStepsDone { if err := s.sd.SetMessage(doneMessage); err != nil { log.Info().Err(err).Msg("setting message in setup experience UI") } @@ -229,16 +243,6 @@ func (s *SetupExperiencer) Run(oc *fleet.OrbitConfig) error { return nil } -func (s *SetupExperiencer) allStepsDone() bool { - for _, done := range s.steps { - if !done { - return false - } - } - - return true -} - func anyProfilePending(profiles []*fleet.SetupExperienceConfigurationProfileResult) (bool, string) { for _, p := range profiles { if p.Status == fleet.MDMDeliveryPending {