forked from Foxyz/sync_replay_project
Updater: real-layout apply with rollback, in-app update + restart
- Strip the tarball's top-level folder; stage inside the install root (same filesystem) and swap with full rollback; keep file modes. - Semver prerelease ordering (1.2.3-rc1 < 1.2.3). - UpdateController: background check, download with progress, apply, restart; required-update wording for newer protocols; Check now. - update.sh/update.bat in the bundle; Windows apply path (untested). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2301aa4ec
commit
beae87b0cc
8 files changed
+1126
-369
No files matched your search
@@ -0,0 +1,160 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'package:sync_relay_core/core.dart';
|
||||
|
||||
import 'updater.dart';
|
||||
|
||||
// In-app update flow behind the Home update indicator. Same code path as
|
||||
// `--upgrade` (downloadAndApply); this adds state for the UI and the
|
||||
// restart. Check failures are logged only — the banner shows up just when
|
||||
// there is something to install.
|
||||
|
||||
final _log = createLogger('update');
|
||||
|
||||
enum UpdatePhase { idle, available, downloading, applying, restarting, failed }
|
||||
|
||||
class UpdateStatus {
|
||||
final UpdatePhase phase;
|
||||
final UpdateManifest? manifest;
|
||||
final double? progress;
|
||||
final String? error;
|
||||
// False for dev runs / unpacked builds: banner tells the user to update
|
||||
// by hand instead of offering the button.
|
||||
final bool canInstall;
|
||||
const UpdateStatus(this.phase,
|
||||
{this.manifest, this.progress, this.error, this.canInstall = false});
|
||||
}
|
||||
|
||||
typedef RestartFn = Future<void> Function(String executable, List<String> args);
|
||||
|
||||
// Release Hive's box locks before the new instance opens the same boxes.
|
||||
Future<void> _closeAndRestart(String executable, List<String> args) async {
|
||||
try {
|
||||
await Hive.close();
|
||||
} catch (e) {
|
||||
_log.warning('closing hive before restart: $e');
|
||||
}
|
||||
await restartApp(executable, args);
|
||||
}
|
||||
|
||||
class UpdateController {
|
||||
static final instance = UpdateController();
|
||||
|
||||
final String currentVersion;
|
||||
final String manifestUrl;
|
||||
final String? executablePath;
|
||||
final Directory? installRoot;
|
||||
final RestartFn restart;
|
||||
final bool skipDevBuilds;
|
||||
|
||||
UpdateController(
|
||||
{this.currentVersion = appVersion,
|
||||
this.manifestUrl = updateManifestUrl,
|
||||
this.executablePath,
|
||||
this.installRoot,
|
||||
this.restart = _closeAndRestart,
|
||||
this.skipDevBuilds = true});
|
||||
|
||||
final status = ValueNotifier(const UpdateStatus(UpdatePhase.idle));
|
||||
/// Outcome of the last finished check, for Settings/About wording
|
||||
/// ("Up to date", "Check failed: ..."); null until one finishes.
|
||||
final lastCheck = ValueNotifier<String?>(null);
|
||||
Future<void>? _check;
|
||||
late final String _exe = executablePath ?? currentExecutablePath();
|
||||
late final Directory? _root = installRoot ?? findInstallRoot(_exe);
|
||||
String get _exeName => _exe.replaceAll('\\', '/').split('/').last;
|
||||
|
||||
// Once per process; later calls return the same future.
|
||||
Future<void> checkOnce() => _check ??= _checkNow();
|
||||
|
||||
// User-triggered re-check ("Check now"); joins one already in flight.
|
||||
Future<void> checkNow() {
|
||||
final inFlight = _check;
|
||||
if (inFlight != null && _checking) return inFlight;
|
||||
return _check = _checkNow();
|
||||
}
|
||||
|
||||
bool _checking = false;
|
||||
|
||||
Future<void> _checkNow() async {
|
||||
_checking = true;
|
||||
try {
|
||||
await _runCheck();
|
||||
} finally {
|
||||
_checking = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runCheck() async {
|
||||
if (skipDevBuilds && currentVersion.endsWith('-dev')) {
|
||||
_log.info('dev build ($currentVersion), skipping update check');
|
||||
lastCheck.value = 'Dev build — update checks are off';
|
||||
return;
|
||||
}
|
||||
final root = _root;
|
||||
if (root != null) await cleanupUpdateLeftovers(root);
|
||||
try {
|
||||
final m = await fetchManifest(url: manifestUrl);
|
||||
if (!isNewerVersion(currentVersion, m.version)) {
|
||||
_log.info('up to date ($currentVersion, latest ${m.version})');
|
||||
lastCheck.value = 'Up to date';
|
||||
return;
|
||||
}
|
||||
_log.info('update available: ${m.version} (proto ${m.protocol})');
|
||||
status.value = UpdateStatus(UpdatePhase.available,
|
||||
manifest: m, canInstall: root != null);
|
||||
lastCheck.value = 'Update v${m.version} available';
|
||||
} catch (e) {
|
||||
_log.warning('update check failed: $e');
|
||||
lastCheck.value = 'Update check failed: $e';
|
||||
}
|
||||
}
|
||||
|
||||
void dismiss() {
|
||||
final s = status.value;
|
||||
if ((s.phase == UpdatePhase.available || s.phase == UpdatePhase.failed) &&
|
||||
!(s.manifest?.isRequired ?? false)) {
|
||||
status.value = const UpdateStatus(UpdatePhase.idle);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> install() async {
|
||||
final s = status.value;
|
||||
final m = s.manifest;
|
||||
final root = _root;
|
||||
if (m == null || root == null) return;
|
||||
if (s.phase != UpdatePhase.available && s.phase != UpdatePhase.failed) {
|
||||
return;
|
||||
}
|
||||
status.value = UpdateStatus(UpdatePhase.downloading,
|
||||
manifest: m, progress: 0, canInstall: true);
|
||||
var lastPct = -1;
|
||||
try {
|
||||
await downloadAndApply(m, root,
|
||||
executableName: installRoot == null ? _exeName : null,
|
||||
onProgress: (got, total) {
|
||||
if (total == null || total == 0) return;
|
||||
final pct = got * 100 ~/ total;
|
||||
if (pct == lastPct) return;
|
||||
lastPct = pct;
|
||||
status.value = UpdateStatus(UpdatePhase.downloading,
|
||||
manifest: m, progress: got / total, canInstall: true);
|
||||
if (pct == 100) {
|
||||
status.value =
|
||||
UpdateStatus(UpdatePhase.applying, manifest: m, canInstall: true);
|
||||
}
|
||||
}, say: (line) => _log.info(line));
|
||||
} catch (e) {
|
||||
_log.warning('update failed: $e');
|
||||
status.value = UpdateStatus(UpdatePhase.failed,
|
||||
manifest: m, error: '$e', canInstall: true);
|
||||
return;
|
||||
}
|
||||
_log.info('updated to ${m.version}, restarting');
|
||||
status.value =
|
||||
UpdateStatus(UpdatePhase.restarting, manifest: m, canInstall: true);
|
||||
await restart(_exe, launchArgs);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user