| name | updater-reliability |
| description | Implement safe and reliable updater system changes |
Skill: Updater Reliability
Use this skill when working on the two-phase update mechanism or updater components.
Goals
- Maintain reliability of the two-phase update flow
- Prevent update failures that could brick installations
- Keep updater simple and robust
- Enable both app and updater self-updating
Architecture overview
Two-phase update mechanism:
- Phase 1 (App): Downloads update, extracts to staging, copies updater to temp, launches temp updater
- Phase 2 (Temp Updater): Waits for app exit, replaces Burndown.exe and BurndownUpdater.exe, launches new app, self-terminates
Key insight: Temp updater runs from different location (temp dir), so it can replace both original files without Windows file locking issues.
Workflow
- Read updater architecture: Load
docs/updater_architecture.md
- Identify change scope: App-side vs updater-side vs shared
- Apply minimal changes: Preserve two-phase flow integrity
- Test thoroughly: Simulate update in test environment
- Validate: Check both successful and failed update scenarios
Key files
App-side (phase 1)
data/update_manager.py - Update orchestration
callbacks/app_update.py - Update UI callbacks
ui/update_notification.py - Update notification UI
data/version_tracker.py - Version comparison
Updater-side (phase 2)
updater/updater.py - Main updater logic
updater/file_ops.py - File replacement operations
build/updater.spec - Updater build config
Shared
data/installation_context.py - Detect installation type
build/generate_version_info.py - Version info for both executables
Enforcement points
Update state machine
States must follow this flow:
CHECKING → AVAILABLE → DOWNLOADING → READY → INSTALLING → INSTALLED
↓ ↓ ↓ ↓
ERROR CANCELLED ERROR ERROR
Never skip states or create invalid transitions.
File safety
def replace_file_safely(source, dest):
backup = dest + '.backup'
try:
shutil.copy2(dest, backup)
shutil.copy2(source, dest)
os.remove(backup)
except Exception as e:
if os.path.exists(backup):
shutil.copy2(backup, dest)
raise
shutil.copy2(source, dest)
Process synchronization
max_wait = 30
start = time.time()
while process_exists(app_pid):
if time.time() - start > max_wait:
raise TimeoutError("App did not exit")
time.sleep(0.5)
replace_files()
Logging
Updater logs go to dedicated file (critical for debugging failed updates):
log_path = temp_dir / "updater.log"
logging.basicConfig(filename=log_path, level=logging.DEBUG)
logger.info(f"Replacing {dest} with {source}")
logger.info(f"App PID: {app_pid}, waiting for exit...")
Critical paths
Download and staging
- Download ZIP to temp directory
- Verify ZIP integrity (size, hash if available)
- Extract to staging directory
- Verify extracted files exist
- Persist update path to database (for crash recovery)
Launch temp updater
- Copy updater executable to temp location with UUID
- Build command line args (--updater-exe, staging path, app PID, install dir)
- Launch temp updater as detached process
- App exits immediately (os._exit(0))
File replacement (temp updater)
- Wait for app process to exit (timeout 30s)
- Verify staging files exist
- Create backups of current files
- Replace Burndown.exe from staging
- Replace BurndownUpdater.exe from staging (self-update!)
- Launch new Burndown.exe
- Clean up staging and temp files
- Self-terminate (temp updater)
Guardrails
- Never break the two-phase flow pattern
- Never skip file backups before replacement
- Always wait for process exit before replacement
- Always log each step (errors go to updater.log)
- Test both successful and failed update scenarios
- Verify installation type detection (portable vs installed)
- Handle both legacy (BurndownChart.exe) and new (Burndown.exe) naming
Suggested validations
Manual testing
- Download update via UI
- Trigger installation
- Verify app restarts with new version
- Check updater.log for errors
- Verify both Burndown.exe and BurndownUpdater.exe were updated
Error scenarios to test
- Download interrupted (verify retry or cancellation)
- Insufficient disk space (verify error message)
- App doesn't exit in time (verify timeout handling)
- File replacement fails (verify rollback)
- New version fails to launch (verify error handling)
Code checks
- Run
get_errors on changed files
- Verify logging at each critical step
- Check exception handling covers all failure modes
- Confirm process wait logic has timeout
Risks to mitigate
- Bricked installation: If updater fails mid-replacement
- Mitigation: Atomic operations with backups
- Orphaned temp updater: If cleanup fails
- Mitigation: App cleans up old temp updaters on startup
- Update loop: If version detection broken
- Mitigation: Version comparison logic tested thoroughly
- File locking: If replace attempted while app running
- Mitigation: Process exit wait logic with timeout
Related artifacts
docs/updater_architecture.md - Complete architecture documentation
.github/instructions/build-pipeline.instructions.md - Build system rules
.github/skills/release-management/SKILL.md - Release workflow