This commit is contained in:
1 parent
e29a5a3b0a
commit
96ef39ab1f
3 files changed
+334
-184
No files matched your search
@@ -25,12 +25,17 @@ final _log = createLogger('update');
|
||||
const updateManifestUrl =
|
||||
'https://git.senkochan.moe/Foxyz/sync_replay_project/raw/branch/main/updates/manifest.json';
|
||||
|
||||
const appVersion = String.fromEnvironment('APP_VERSION',
|
||||
defaultValue: '0.0.0-dev');
|
||||
const appVersion = String.fromEnvironment(
|
||||
'APP_VERSION',
|
||||
defaultValue: '0.0.0-dev',
|
||||
);
|
||||
|
||||
// Process args as main() received them — restart relaunches with these.
|
||||
List<String> launchArgs = const [];
|
||||
|
||||
// Key into the manifest's `assets` map, e.g. linux-x64, windows-x64.
|
||||
final updatePlatform = '${Platform.operatingSystem}-x64';
|
||||
|
||||
const _stagingDir = '.update-staging';
|
||||
const _backupDir = '.update-backup';
|
||||
const _downloadFile = '.update-download';
|
||||
@@ -41,31 +46,45 @@ class UpdateManifest {
|
||||
final String url;
|
||||
final String? sha256;
|
||||
final String notes;
|
||||
const UpdateManifest(
|
||||
{required this.version,
|
||||
required this.protocol,
|
||||
required this.url,
|
||||
this.sha256,
|
||||
required this.notes});
|
||||
const UpdateManifest({
|
||||
required this.version,
|
||||
required this.protocol,
|
||||
required this.url,
|
||||
this.sha256,
|
||||
required this.notes,
|
||||
});
|
||||
|
||||
factory UpdateManifest.fromJson(Map<String, Object?> json) {
|
||||
// `assets` maps platform → archive URL. Top-level `url` is the Linux
|
||||
// tarball, kept for builds from before `assets` existed (<= 0.2.1).
|
||||
factory UpdateManifest.fromJson(
|
||||
Map<String, Object?> json, {
|
||||
String? platform,
|
||||
}) {
|
||||
final version = json['version'];
|
||||
final protocol = json['protocol'];
|
||||
final url = json['url'];
|
||||
final assets = json['assets'];
|
||||
final url = assets is Map
|
||||
? assets[platform ?? updatePlatform]
|
||||
: json['url'];
|
||||
if (version is! String ||
|
||||
version.isEmpty ||
|
||||
protocol is! int ||
|
||||
url is! String ||
|
||||
url.isEmpty) {
|
||||
throw const FormatException('update manifest missing version/protocol/url');
|
||||
throw FormatException(
|
||||
assets is Map && version is String
|
||||
? 'release $version has no ${platform ?? updatePlatform} build'
|
||||
: 'update manifest missing version/protocol/url',
|
||||
);
|
||||
}
|
||||
final sha = json['sha256'];
|
||||
return UpdateManifest(
|
||||
version: version,
|
||||
protocol: protocol,
|
||||
url: url,
|
||||
sha256: sha is String && sha.isNotEmpty ? sha : null,
|
||||
notes: (json['notes'] as String?) ?? '');
|
||||
version: version,
|
||||
protocol: protocol,
|
||||
url: url,
|
||||
sha256: sha is String && sha.isNotEmpty ? sha : null,
|
||||
notes: (json['notes'] as String?) ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
// Newer wire protocol: this build can't join servers running it.
|
||||
@@ -117,10 +136,10 @@ int compareVersions(String a, String b) {
|
||||
final c = na != null && nb != null
|
||||
? na.compareTo(nb)
|
||||
: na != null
|
||||
? -1
|
||||
: nb != null
|
||||
? 1
|
||||
: ia[i].compareTo(ib[i]);
|
||||
? -1
|
||||
: nb != null
|
||||
? 1
|
||||
: ia[i].compareTo(ib[i]);
|
||||
if (c != 0) return c;
|
||||
}
|
||||
return ia.length.compareTo(ib.length);
|
||||
@@ -129,8 +148,10 @@ int compareVersions(String a, String b) {
|
||||
bool isNewerVersion(String current, String latest) =>
|
||||
compareVersions(latest, current) > 0;
|
||||
|
||||
Future<UpdateManifest> fetchManifest(
|
||||
{HttpClient? http, String url = updateManifestUrl}) async {
|
||||
Future<UpdateManifest> fetchManifest({
|
||||
HttpClient? http,
|
||||
String url = updateManifestUrl,
|
||||
}) async {
|
||||
final client = http ?? HttpClient();
|
||||
try {
|
||||
final req = await client
|
||||
@@ -153,8 +174,12 @@ Future<UpdateManifest> fetchManifest(
|
||||
|
||||
typedef DownloadProgress = void Function(int received, int? total);
|
||||
|
||||
Future<void> downloadFile(String url, File dest,
|
||||
{HttpClient? http, DownloadProgress? onProgress}) async {
|
||||
Future<void> downloadFile(
|
||||
String url,
|
||||
File dest, {
|
||||
HttpClient? http,
|
||||
DownloadProgress? onProgress,
|
||||
}) async {
|
||||
final client = http ?? HttpClient();
|
||||
try {
|
||||
final req = await client
|
||||
@@ -361,8 +386,13 @@ typedef ApplyHook = Future<void> Function(String step);
|
||||
|
||||
// Unpack [archive] into a staging dir inside [root], then swap it in.
|
||||
// On failure the install is left exactly as it was.
|
||||
Future<void> applyUpdate(File archive, Directory root,
|
||||
{String? executableName, bool? windows, ApplyHook? hook}) async {
|
||||
Future<void> applyUpdate(
|
||||
File archive,
|
||||
Directory root, {
|
||||
String? executableName,
|
||||
bool? windows,
|
||||
ApplyHook? hook,
|
||||
}) async {
|
||||
final win = windows ?? Platform.isWindows;
|
||||
final stage = Directory('${root.path}/$_stagingDir');
|
||||
if (await stage.exists()) await stage.delete(recursive: true);
|
||||
@@ -373,7 +403,8 @@ Future<void> applyUpdate(File archive, Directory root,
|
||||
(executableName != null &&
|
||||
!await File('${stage.path}/$executableName').exists())) {
|
||||
throw const FormatException(
|
||||
'archive is not a release bundle (missing data/, lib/ or binary)');
|
||||
'archive is not a release bundle (missing data/, lib/ or binary)',
|
||||
);
|
||||
}
|
||||
if (win) {
|
||||
await _swapWindows(stage, root, hook);
|
||||
@@ -392,13 +423,17 @@ class _Moved {
|
||||
_Moved(this.name, this.hadOld);
|
||||
}
|
||||
|
||||
Future<void> _swapPosix(Directory stage, Directory root, ApplyHook? hook) async {
|
||||
Future<void> _swapPosix(
|
||||
Directory stage,
|
||||
Directory root,
|
||||
ApplyHook? hook,
|
||||
) async {
|
||||
final backup = Directory('${root.path}/$_backupDir');
|
||||
if (await backup.exists()) await backup.delete(recursive: true);
|
||||
await backup.create();
|
||||
final names = [
|
||||
for (final e in await stage.list(followLinks: false).toList())
|
||||
e.path.substring(stage.path.length + 1)
|
||||
e.path.substring(stage.path.length + 1),
|
||||
]..sort();
|
||||
final done = <_Moved>[];
|
||||
try {
|
||||
@@ -423,8 +458,10 @@ Future<void> _swapPosix(Directory stage, Directory root, ApplyHook? hook) async
|
||||
if (m.hadOld) await _rename('${backup.path}/${m.name}', cur);
|
||||
} catch (re) {
|
||||
// Leave the backup dir in place so nothing is lost.
|
||||
_log.severe('rollback of ${m.name} failed: $re '
|
||||
'(old files kept in ${backup.path})');
|
||||
_log.severe(
|
||||
'rollback of ${m.name} failed: $re '
|
||||
'(old files kept in ${backup.path})',
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -437,10 +474,14 @@ Future<void> _swapPosix(Directory stage, Directory root, ApplyHook? hook) async
|
||||
// UNTESTED on Windows. File-level: a running exe/dll can be renamed but
|
||||
// not deleted or overwritten, so every existing file goes to `<f>.old`
|
||||
// first; leftovers are swept by cleanupUpdateLeftovers on next start.
|
||||
Future<void> _swapWindows(Directory stage, Directory root, ApplyHook? hook) async {
|
||||
Future<void> _swapWindows(
|
||||
Directory stage,
|
||||
Directory root,
|
||||
ApplyHook? hook,
|
||||
) async {
|
||||
final rels = <String>[
|
||||
await for (final e in stage.list(recursive: true, followLinks: false))
|
||||
if (e is File) e.path.substring(stage.path.length + 1)
|
||||
if (e is File) e.path.substring(stage.path.length + 1),
|
||||
]..sort();
|
||||
final done = <(String rel, String? old, bool movedIn)>[];
|
||||
try {
|
||||
@@ -486,11 +527,14 @@ Future<void> _swapWindows(Directory stage, Directory root, ApplyHook? hook) asyn
|
||||
|
||||
// Shared by the CLI and the in-app banner: download next to the install
|
||||
// (same filesystem), verify, apply, clean up.
|
||||
Future<void> downloadAndApply(UpdateManifest manifest, Directory root,
|
||||
{HttpClient? http,
|
||||
DownloadProgress? onProgress,
|
||||
String? executableName,
|
||||
void Function(String line)? say}) async {
|
||||
Future<void> downloadAndApply(
|
||||
UpdateManifest manifest,
|
||||
Directory root, {
|
||||
HttpClient? http,
|
||||
DownloadProgress? onProgress,
|
||||
String? executableName,
|
||||
void Function(String line)? say,
|
||||
}) async {
|
||||
final file = File('${root.path}/$_downloadFile');
|
||||
try {
|
||||
say?.call('downloading ${manifest.url} ...');
|
||||
@@ -498,8 +542,10 @@ Future<void> downloadAndApply(UpdateManifest manifest, Directory root,
|
||||
if (manifest.sha256 != null) {
|
||||
await verifySha256(file, manifest.sha256!);
|
||||
} else {
|
||||
say?.call('no checksum in manifest — skipping verification '
|
||||
'(HTTPS transit + gzip CRC still apply).');
|
||||
say?.call(
|
||||
'no checksum in manifest — skipping verification '
|
||||
'(HTTPS transit + gzip CRC still apply).',
|
||||
);
|
||||
}
|
||||
say?.call('verified, applying over ${root.path} ...');
|
||||
await applyUpdate(file, root, executableName: executableName);
|
||||
@@ -512,9 +558,12 @@ Future<void> downloadAndApply(UpdateManifest manifest, Directory root,
|
||||
// exit this process.
|
||||
Future<Never> restartApp(String executable, List<String> args) async {
|
||||
final keep = args.where((a) => a != '--upgrade').toList();
|
||||
await Process.start(executable, keep,
|
||||
mode: ProcessStartMode.detached,
|
||||
workingDirectory: Directory.current.path);
|
||||
await Process.start(
|
||||
executable,
|
||||
keep,
|
||||
mode: ProcessStartMode.detached,
|
||||
workingDirectory: Directory.current.path,
|
||||
);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@@ -524,13 +573,16 @@ String requiredUpdateNote(UpdateManifest m) =>
|
||||
|
||||
// Returns a process exit code: 0 ok / already current, 1 failure,
|
||||
// 2 not a packaged install (update manually).
|
||||
Future<int> runUpgrade(
|
||||
{String currentVersion = appVersion,
|
||||
String manifestUrl = updateManifestUrl,
|
||||
HttpClient? http,
|
||||
String? executablePath,
|
||||
Directory? installRoot}) async {
|
||||
stdout.writeln('watch-together $currentVersion (proto ${RelayConstants.protoVersion})');
|
||||
Future<int> runUpgrade({
|
||||
String currentVersion = appVersion,
|
||||
String manifestUrl = updateManifestUrl,
|
||||
HttpClient? http,
|
||||
String? executablePath,
|
||||
Directory? installRoot,
|
||||
}) async {
|
||||
stdout.writeln(
|
||||
'watch-together $currentVersion (proto ${RelayConstants.protoVersion})',
|
||||
);
|
||||
late final UpdateManifest manifest;
|
||||
try {
|
||||
manifest = await fetchManifest(http: http, url: manifestUrl);
|
||||
@@ -542,7 +594,9 @@ Future<int> runUpgrade(
|
||||
stdout.writeln('already up to date (${manifest.version}).');
|
||||
return 0;
|
||||
}
|
||||
stdout.writeln('update available: ${manifest.version} (proto ${manifest.protocol})');
|
||||
stdout.writeln(
|
||||
'update available: ${manifest.version} (proto ${manifest.protocol})',
|
||||
);
|
||||
if (manifest.notes.isNotEmpty) {
|
||||
stdout.writeln(manifest.notes);
|
||||
}
|
||||
@@ -552,15 +606,20 @@ Future<int> runUpgrade(
|
||||
final exe = executablePath ?? currentExecutablePath();
|
||||
final root = installRoot ?? findInstallRoot(exe);
|
||||
if (root == null) {
|
||||
stdout.writeln('not a packaged install — download the tarball and update manually.');
|
||||
stdout.writeln(
|
||||
'not a packaged install — download the tarball and update manually.',
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
try {
|
||||
await cleanupUpdateLeftovers(root);
|
||||
await downloadAndApply(manifest, root,
|
||||
http: http,
|
||||
executableName: installRoot == null ? _basename(exe) : null,
|
||||
say: stdout.writeln);
|
||||
await downloadAndApply(
|
||||
manifest,
|
||||
root,
|
||||
http: http,
|
||||
executableName: installRoot == null ? _basename(exe) : null,
|
||||
say: stdout.writeln,
|
||||
);
|
||||
} catch (e) {
|
||||
stdout.writeln('update failed: $e');
|
||||
return 1;
|
||||
|
||||
Reference in new issue
Block a user