{"record":{"id":"09efa483e65f911c","repo":"multica-ai/multica","slug":"stat-original-binary-w","errorCode":null,"errorMessage":"stat original binary: %w","messagePattern":"stat original binary: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/internal/cli/update.go","lineNumber":464,"sourceCode":"\tdir := filepath.Dir(exePath)\n\ttmpFile, err := os.CreateTemp(dir, \"multica-update-*\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"create temp file: %w\", err)\n\t}\n\ttmpPath := tmpFile.Name()\n\n\tif _, err := tmpFile.Write(binaryData); err != nil {\n\t\ttmpFile.Close()\n\t\tos.Remove(tmpPath)\n\t\treturn \"\", fmt.Errorf(\"write temp file: %w\", err)\n\t}\n\ttmpFile.Close()\n\n\t// Preserve original file permissions.\n\tinfo, err := os.Stat(exePath)\n\tif err != nil {\n\t\tos.Remove(tmpPath)\n\t\treturn \"\", fmt.Errorf(\"stat original binary: %w\", err)\n\t}\n\tif err := os.Chmod(tmpPath, info.Mode()); err != nil {\n\t\tos.Remove(tmpPath)\n\t\treturn \"\", fmt.Errorf(\"chmod temp file: %w\", err)\n\t}\n\n\t// Replace the original binary. On Windows this moves the running executable\n\t// aside first; on Unix a plain rename over the running inode is fine.\n\tif err := replaceBinary(tmpPath, exePath); err != nil {\n\t\tos.Remove(tmpPath)\n\t\treturn \"\", fmt.Errorf(\"replace binary: %w\", err)\n\t}\n\n\treturn fmt.Sprintf(\"Downloaded %s and replaced %s\", assetName, exePath), nil\n}\n\n// extractBinaryFromTarGz reads a .tar.gz stream and returns the contents of the\n// named file entry.","sourceCodeStart":446,"sourceCodeEnd":482,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/cli/update.go#L446-L482","documentation":"To preserve the original file mode, the updater stats the current binary (os.Stat(exePath)) before chmod-ing the temp file; failure is wrapped as 'stat original binary: %w' and the temp file is removed. It means the binary path resolved at the start of the update no longer exists at commit time.","triggerScenarios":"Another updater/process replaced or deleted the binary between Resolve and this point; the user uninstalled while the download was in flight; the symlink target vanished mid-update; the directory permissions changed.","commonSituations":"Two update pollers racing (systemd service plus a CLI run); a package-manager uninstall racing the daemon's self-update; dev environments rebuilding/removing binaries during a long download.","solutions":["Ensure only one updater runs at a time (lock file / single instance of the daemon).","If the binary was uninstalled or moved, reinstall it at the expected path and retry the update.","Check the wrapped fs.PathError for ENOENT vs EACCES to distinguish deleted vs unreadable.","Restart the daemon after manual binary changes so it re-resolves its path."],"exampleFix":"null","handlingStrategy":"validation","validationCode":"exe, _ := selfexec.Resolve()\nexe, _ = filepath.EvalSymlinks(exe)\nif _, err := os.Stat(exe); err != nil {\n    // binary moved/deleted since start: re-resolve before updating\n}","typeGuard":null,"tryCatchPattern":"out, err := cli.UpdateViaDownload(ver)\nif err != nil {\n    var pe *fs.PathError\n    if errors.As(err, &pe) && errors.Is(pe.Err, fs.ErrNotExist) && strings.HasPrefix(err.Error(), \"stat original binary\") {\n        // concurrent replace/uninstall raced us: restart the daemon and retry\n    }\n}","preventionTips":["Run exactly one updater instance (lock file or single daemon)","Restart the service after manual binary changes","Avoid uninstalling while update polls are in flight"],"tags":["filesystem","race","self-update","stat"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}