{"record":{"id":"23793bc28ab3543a","repo":"MHSanaei/3x-ui","slug":"a-panel-update-is-already-in-progress","errorCode":null,"errorMessage":"a panel update is already in progress","messagePattern":"a panel update is already in progress","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/web/service/panel/panel.go","lineNumber":218,"sourceCode":"func (s *PanelService) GetUpdateStatus() *PanelUpdateStatus {\n\tdata, err := os.ReadFile(config.GetUpdateStatusFilePath())\n\tif err != nil {\n\t\treturn &PanelUpdateStatus{State: updateStatePending}\n\t}\n\tvar status PanelUpdateStatus\n\tif err := json.Unmarshal(data, &status); err != nil {\n\t\treturn &PanelUpdateStatus{State: updateStatePending}\n\t}\n\tif status.State != updateStateSuccess && status.State != updateStateFailed {\n\t\tstatus.State = updateStatePending\n\t}\n\treturn &status\n}\n\nfunc (s *PanelService) startUpdate(useDev bool) (int64, error) {\n\trunID := time.Now().UnixNano()\n\tif !acquireUpdateSlot(runID) {\n\t\treturn 0, fmt.Errorf(\"a panel update is already in progress\")\n\t}\n\tlaunched := false\n\tdefer func() {\n\t\tif !launched {\n\t\t\treleaseUpdateSlot()\n\t\t}\n\t}()\n\n\tif runtime.GOOS != \"linux\" {\n\t\treturn 0, fmt.Errorf(\"panel web update is supported only on Linux installations\")\n\t}\n\n\tbash, err := exec.LookPath(\"bash\")\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"bash is required to run the panel updater: %w\", err)\n\t}\n\n\tscriptPath, err := downloadPanelUpdater()","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/service/panel/panel.go#L200-L236","documentation":"startUpdate guards the whole self-update flow with a single in-process slot (acquireUpdateSlot/runID). If a previous update is genuinely still in flight (updateRunning=true), the new request gets 'a panel update is already in progress' and runID 0. The slot is released when the run finishes or on failure via the launched-flag defer — it is process-local, so a panel restart clears it.","triggerScenarios":"Clicking 'update' twice, or two admins/sessions triggering the update concurrently; polling the update API in a way that re-POSTs the start action.","commonSituations":"Impatient double-click in the UI; an automation script that fires start instead of polling status; browser retry after a slow response.","solutions":["Poll the update status endpoint instead of re-issuing start until the current run reports success/failed","If you are certain no update is running (e.g. a prior run crashed without releasing), restart the panel process to clear the slot","Make the UI/controller disable the button while state is pending"],"exampleFix":"// before\nif _, err := panelService.StartUpdate(false); err != nil { /* double fire */ }\n\n// after\nstatus, _ := panelService.GetUpdateStatus()\nif status.State == \"pending\" {\n    // poll GetUpdateStatus until success/failed instead of starting again\n} else if _, err := panelService.StartUpdate(false); err != nil { ... }","handlingStrategy":"validation","validationCode":"status, _ := panelService.GetUpdateStatus()\nif status.State == \"pending\" {\n    // poll status; do not call StartUpdate while pending\n}","typeGuard":null,"tryCatchPattern":"runID, err := panelService.StartUpdate(useDev)\nif err != nil && strings.Contains(err.Error(), \"already in progress\") {\n    // switch to polling GetUpdateStatus instead of erroring to the user\n}","preventionTips":["UI must disable the update button while status.State is pending","Automation should start once and poll, never re-POST on timeout"],"tags":["updates","concurrency","locking","panel-update"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}