This commit is contained in:
1 parent
e29a5a3b0a
commit
96ef39ab1f
3 files changed
+262
-112
No files matched your search
@@ -26,11 +26,7 @@ jobs:
|
|||||||
Add-Content $env:GITHUB_PATH "$F\bin"
|
Add-Content $env:GITHUB_PATH "$F\bin"
|
||||||
& "$F\bin\flutter.bat" config --no-analytics --enable-windows-desktop | Out-Null
|
& "$F\bin\flutter.bat" config --no-analytics --enable-windows-desktop | Out-Null
|
||||||
& "$F\bin\flutter.bat" doctor -v
|
& "$F\bin\flutter.bat" doctor -v
|
||||||
- name: Build zip
|
- name: Updater tests
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: powershell -NoProfile -ExecutionPolicy Bypass -File tool/package_windows.ps1
|
working-directory: apps/desktop
|
||||||
- name: Inspect
|
run: flutter test test/updater_test.dart -r expanded
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
Get-ChildItem dist
|
|
||||||
tar.exe -tf (Get-ChildItem dist\*.zip)[0].FullName | Select-Object -First 40
|
|
||||||
@@ -25,12 +25,17 @@ final _log = createLogger('update');
|
|||||||
const updateManifestUrl =
|
const updateManifestUrl =
|
||||||
'https://git.senkochan.moe/Foxyz/sync_replay_project/raw/branch/main/updates/manifest.json';
|
'https://git.senkochan.moe/Foxyz/sync_replay_project/raw/branch/main/updates/manifest.json';
|
||||||
|
|
||||||
const appVersion = String.fromEnvironment('APP_VERSION',
|
const appVersion = String.fromEnvironment(
|
||||||
defaultValue: '0.0.0-dev');
|
'APP_VERSION',
|
||||||
|
defaultValue: '0.0.0-dev',
|
||||||
|
);
|
||||||
|
|
||||||
// Process args as main() received them — restart relaunches with these.
|
// Process args as main() received them — restart relaunches with these.
|
||||||
List<String> launchArgs = const [];
|
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 _stagingDir = '.update-staging';
|
||||||
const _backupDir = '.update-backup';
|
const _backupDir = '.update-backup';
|
||||||
const _downloadFile = '.update-download';
|
const _downloadFile = '.update-download';
|
||||||
@@ -41,23 +46,36 @@ class UpdateManifest {
|
|||||||
final String url;
|
final String url;
|
||||||
final String? sha256;
|
final String? sha256;
|
||||||
final String notes;
|
final String notes;
|
||||||
const UpdateManifest(
|
const UpdateManifest({
|
||||||
{required this.version,
|
required this.version,
|
||||||
required this.protocol,
|
required this.protocol,
|
||||||
required this.url,
|
required this.url,
|
||||||
this.sha256,
|
this.sha256,
|
||||||
required this.notes});
|
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 version = json['version'];
|
||||||
final protocol = json['protocol'];
|
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 ||
|
if (version is! String ||
|
||||||
version.isEmpty ||
|
version.isEmpty ||
|
||||||
protocol is! int ||
|
protocol is! int ||
|
||||||
url is! String ||
|
url is! String ||
|
||||||
url.isEmpty) {
|
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'];
|
final sha = json['sha256'];
|
||||||
return UpdateManifest(
|
return UpdateManifest(
|
||||||
@@ -65,7 +83,8 @@ class UpdateManifest {
|
|||||||
protocol: protocol,
|
protocol: protocol,
|
||||||
url: url,
|
url: url,
|
||||||
sha256: sha is String && sha.isNotEmpty ? sha : null,
|
sha256: sha is String && sha.isNotEmpty ? sha : null,
|
||||||
notes: (json['notes'] as String?) ?? '');
|
notes: (json['notes'] as String?) ?? '',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Newer wire protocol: this build can't join servers running it.
|
// Newer wire protocol: this build can't join servers running it.
|
||||||
@@ -129,8 +148,10 @@ int compareVersions(String a, String b) {
|
|||||||
bool isNewerVersion(String current, String latest) =>
|
bool isNewerVersion(String current, String latest) =>
|
||||||
compareVersions(latest, current) > 0;
|
compareVersions(latest, current) > 0;
|
||||||
|
|
||||||
Future<UpdateManifest> fetchManifest(
|
Future<UpdateManifest> fetchManifest({
|
||||||
{HttpClient? http, String url = updateManifestUrl}) async {
|
HttpClient? http,
|
||||||
|
String url = updateManifestUrl,
|
||||||
|
}) async {
|
||||||
final client = http ?? HttpClient();
|
final client = http ?? HttpClient();
|
||||||
try {
|
try {
|
||||||
final req = await client
|
final req = await client
|
||||||
@@ -153,8 +174,12 @@ Future<UpdateManifest> fetchManifest(
|
|||||||
|
|
||||||
typedef DownloadProgress = void Function(int received, int? total);
|
typedef DownloadProgress = void Function(int received, int? total);
|
||||||
|
|
||||||
Future<void> downloadFile(String url, File dest,
|
Future<void> downloadFile(
|
||||||
{HttpClient? http, DownloadProgress? onProgress}) async {
|
String url,
|
||||||
|
File dest, {
|
||||||
|
HttpClient? http,
|
||||||
|
DownloadProgress? onProgress,
|
||||||
|
}) async {
|
||||||
final client = http ?? HttpClient();
|
final client = http ?? HttpClient();
|
||||||
try {
|
try {
|
||||||
final req = await client
|
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.
|
// Unpack [archive] into a staging dir inside [root], then swap it in.
|
||||||
// On failure the install is left exactly as it was.
|
// On failure the install is left exactly as it was.
|
||||||
Future<void> applyUpdate(File archive, Directory root,
|
Future<void> applyUpdate(
|
||||||
{String? executableName, bool? windows, ApplyHook? hook}) async {
|
File archive,
|
||||||
|
Directory root, {
|
||||||
|
String? executableName,
|
||||||
|
bool? windows,
|
||||||
|
ApplyHook? hook,
|
||||||
|
}) async {
|
||||||
final win = windows ?? Platform.isWindows;
|
final win = windows ?? Platform.isWindows;
|
||||||
final stage = Directory('${root.path}/$_stagingDir');
|
final stage = Directory('${root.path}/$_stagingDir');
|
||||||
if (await stage.exists()) await stage.delete(recursive: true);
|
if (await stage.exists()) await stage.delete(recursive: true);
|
||||||
@@ -373,7 +403,8 @@ Future<void> applyUpdate(File archive, Directory root,
|
|||||||
(executableName != null &&
|
(executableName != null &&
|
||||||
!await File('${stage.path}/$executableName').exists())) {
|
!await File('${stage.path}/$executableName').exists())) {
|
||||||
throw const FormatException(
|
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) {
|
if (win) {
|
||||||
await _swapWindows(stage, root, hook);
|
await _swapWindows(stage, root, hook);
|
||||||
@@ -392,13 +423,17 @@ class _Moved {
|
|||||||
_Moved(this.name, this.hadOld);
|
_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');
|
final backup = Directory('${root.path}/$_backupDir');
|
||||||
if (await backup.exists()) await backup.delete(recursive: true);
|
if (await backup.exists()) await backup.delete(recursive: true);
|
||||||
await backup.create();
|
await backup.create();
|
||||||
final names = [
|
final names = [
|
||||||
for (final e in await stage.list(followLinks: false).toList())
|
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();
|
]..sort();
|
||||||
final done = <_Moved>[];
|
final done = <_Moved>[];
|
||||||
try {
|
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);
|
if (m.hadOld) await _rename('${backup.path}/${m.name}', cur);
|
||||||
} catch (re) {
|
} catch (re) {
|
||||||
// Leave the backup dir in place so nothing is lost.
|
// Leave the backup dir in place so nothing is lost.
|
||||||
_log.severe('rollback of ${m.name} failed: $re '
|
_log.severe(
|
||||||
'(old files kept in ${backup.path})');
|
'rollback of ${m.name} failed: $re '
|
||||||
|
'(old files kept in ${backup.path})',
|
||||||
|
);
|
||||||
rethrow;
|
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
|
// 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`
|
// not deleted or overwritten, so every existing file goes to `<f>.old`
|
||||||
// first; leftovers are swept by cleanupUpdateLeftovers on next start.
|
// 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>[
|
final rels = <String>[
|
||||||
await for (final e in stage.list(recursive: true, followLinks: false))
|
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();
|
]..sort();
|
||||||
final done = <(String rel, String? old, bool movedIn)>[];
|
final done = <(String rel, String? old, bool movedIn)>[];
|
||||||
try {
|
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
|
// Shared by the CLI and the in-app banner: download next to the install
|
||||||
// (same filesystem), verify, apply, clean up.
|
// (same filesystem), verify, apply, clean up.
|
||||||
Future<void> downloadAndApply(UpdateManifest manifest, Directory root,
|
Future<void> downloadAndApply(
|
||||||
{HttpClient? http,
|
UpdateManifest manifest,
|
||||||
|
Directory root, {
|
||||||
|
HttpClient? http,
|
||||||
DownloadProgress? onProgress,
|
DownloadProgress? onProgress,
|
||||||
String? executableName,
|
String? executableName,
|
||||||
void Function(String line)? say}) async {
|
void Function(String line)? say,
|
||||||
|
}) async {
|
||||||
final file = File('${root.path}/$_downloadFile');
|
final file = File('${root.path}/$_downloadFile');
|
||||||
try {
|
try {
|
||||||
say?.call('downloading ${manifest.url} ...');
|
say?.call('downloading ${manifest.url} ...');
|
||||||
@@ -498,8 +542,10 @@ Future<void> downloadAndApply(UpdateManifest manifest, Directory root,
|
|||||||
if (manifest.sha256 != null) {
|
if (manifest.sha256 != null) {
|
||||||
await verifySha256(file, manifest.sha256!);
|
await verifySha256(file, manifest.sha256!);
|
||||||
} else {
|
} else {
|
||||||
say?.call('no checksum in manifest — skipping verification '
|
say?.call(
|
||||||
'(HTTPS transit + gzip CRC still apply).');
|
'no checksum in manifest — skipping verification '
|
||||||
|
'(HTTPS transit + gzip CRC still apply).',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
say?.call('verified, applying over ${root.path} ...');
|
say?.call('verified, applying over ${root.path} ...');
|
||||||
await applyUpdate(file, root, executableName: executableName);
|
await applyUpdate(file, root, executableName: executableName);
|
||||||
@@ -512,9 +558,12 @@ Future<void> downloadAndApply(UpdateManifest manifest, Directory root,
|
|||||||
// exit this process.
|
// exit this process.
|
||||||
Future<Never> restartApp(String executable, List<String> args) async {
|
Future<Never> restartApp(String executable, List<String> args) async {
|
||||||
final keep = args.where((a) => a != '--upgrade').toList();
|
final keep = args.where((a) => a != '--upgrade').toList();
|
||||||
await Process.start(executable, keep,
|
await Process.start(
|
||||||
|
executable,
|
||||||
|
keep,
|
||||||
mode: ProcessStartMode.detached,
|
mode: ProcessStartMode.detached,
|
||||||
workingDirectory: Directory.current.path);
|
workingDirectory: Directory.current.path,
|
||||||
|
);
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,13 +573,16 @@ String requiredUpdateNote(UpdateManifest m) =>
|
|||||||
|
|
||||||
// Returns a process exit code: 0 ok / already current, 1 failure,
|
// Returns a process exit code: 0 ok / already current, 1 failure,
|
||||||
// 2 not a packaged install (update manually).
|
// 2 not a packaged install (update manually).
|
||||||
Future<int> runUpgrade(
|
Future<int> runUpgrade({
|
||||||
{String currentVersion = appVersion,
|
String currentVersion = appVersion,
|
||||||
String manifestUrl = updateManifestUrl,
|
String manifestUrl = updateManifestUrl,
|
||||||
HttpClient? http,
|
HttpClient? http,
|
||||||
String? executablePath,
|
String? executablePath,
|
||||||
Directory? installRoot}) async {
|
Directory? installRoot,
|
||||||
stdout.writeln('watch-together $currentVersion (proto ${RelayConstants.protoVersion})');
|
}) async {
|
||||||
|
stdout.writeln(
|
||||||
|
'watch-together $currentVersion (proto ${RelayConstants.protoVersion})',
|
||||||
|
);
|
||||||
late final UpdateManifest manifest;
|
late final UpdateManifest manifest;
|
||||||
try {
|
try {
|
||||||
manifest = await fetchManifest(http: http, url: manifestUrl);
|
manifest = await fetchManifest(http: http, url: manifestUrl);
|
||||||
@@ -542,7 +594,9 @@ Future<int> runUpgrade(
|
|||||||
stdout.writeln('already up to date (${manifest.version}).');
|
stdout.writeln('already up to date (${manifest.version}).');
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
stdout.writeln('update available: ${manifest.version} (proto ${manifest.protocol})');
|
stdout.writeln(
|
||||||
|
'update available: ${manifest.version} (proto ${manifest.protocol})',
|
||||||
|
);
|
||||||
if (manifest.notes.isNotEmpty) {
|
if (manifest.notes.isNotEmpty) {
|
||||||
stdout.writeln(manifest.notes);
|
stdout.writeln(manifest.notes);
|
||||||
}
|
}
|
||||||
@@ -552,15 +606,20 @@ Future<int> runUpgrade(
|
|||||||
final exe = executablePath ?? currentExecutablePath();
|
final exe = executablePath ?? currentExecutablePath();
|
||||||
final root = installRoot ?? findInstallRoot(exe);
|
final root = installRoot ?? findInstallRoot(exe);
|
||||||
if (root == null) {
|
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;
|
return 2;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await cleanupUpdateLeftovers(root);
|
await cleanupUpdateLeftovers(root);
|
||||||
await downloadAndApply(manifest, root,
|
await downloadAndApply(
|
||||||
|
manifest,
|
||||||
|
root,
|
||||||
http: http,
|
http: http,
|
||||||
executableName: installRoot == null ? _basename(exe) : null,
|
executableName: installRoot == null ? _basename(exe) : null,
|
||||||
say: stdout.writeln);
|
say: stdout.writeln,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
stdout.writeln('update failed: $e');
|
stdout.writeln('update failed: $e');
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -25,9 +25,15 @@ bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0;
|
|||||||
// Builds the release archive exactly like tool/package_linux.sh:
|
// Builds the release archive exactly like tool/package_linux.sh:
|
||||||
// bundle copied into <stage>/watch-together-<ver>/, then
|
// bundle copied into <stage>/watch-together-<ver>/, then
|
||||||
// `tar -czf out -C <stage> watch-together-<ver>`.
|
// `tar -czf out -C <stage> watch-together-<ver>`.
|
||||||
Future<List<int>> _releaseTarball(Directory work,
|
Future<List<int>> _releaseTarball(
|
||||||
{String version = '0.2.0', bool windows = false, bool bundle = true}) async {
|
Directory work, {
|
||||||
final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}');
|
String version = '0.2.0',
|
||||||
|
bool windows = false,
|
||||||
|
bool bundle = true,
|
||||||
|
}) async {
|
||||||
|
final stage = Directory(
|
||||||
|
'${work.path}/pkg-$version-${windows ? 'win' : 'lin'}',
|
||||||
|
);
|
||||||
final folder = Directory('${stage.path}/watch-together-$version');
|
final folder = Directory('${stage.path}/watch-together-$version');
|
||||||
await Directory('${folder.path}/data/flutter_assets').create(recursive: true);
|
await Directory('${folder.path}/data/flutter_assets').create(recursive: true);
|
||||||
await File('${folder.path}/data/flutter_assets/fresh.cfg')
|
await File('${folder.path}/data/flutter_assets/fresh.cfg')
|
||||||
@@ -44,8 +50,11 @@ Future<List<int>> _releaseTarball(Directory work,
|
|||||||
}
|
}
|
||||||
await File('${folder.path}/update.sh')
|
await File('${folder.path}/update.sh')
|
||||||
.writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n');
|
.writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n');
|
||||||
final r = await Process.run('chmod',
|
final r = await Process.run('chmod', [
|
||||||
['755', '${folder.path}/watch_together', '${folder.path}/update.sh']);
|
'755',
|
||||||
|
'${folder.path}/watch_together',
|
||||||
|
'${folder.path}/update.sh',
|
||||||
|
]);
|
||||||
expect(r.exitCode, 0);
|
expect(r.exitCode, 0);
|
||||||
}
|
}
|
||||||
final out = File('${work.path}/out-$version.tar.gz');
|
final out = File('${work.path}/out-$version.tar.gz');
|
||||||
@@ -99,14 +108,16 @@ class _Server {
|
|||||||
http = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
http = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
http.listen((req) async {
|
http.listen((req) async {
|
||||||
if (req.uri.path == '/manifest.json' && !brokenManifest) {
|
if (req.uri.path == '/manifest.json' && !brokenManifest) {
|
||||||
req.response.write(jsonEncode({
|
req.response.write(
|
||||||
|
jsonEncode({
|
||||||
'version': '0.2.0',
|
'version': '0.2.0',
|
||||||
'protocol': RelayConstants.protoVersion,
|
'protocol': RelayConstants.protoVersion,
|
||||||
'url': '$base/watch-together-0.2.0-linux-x64.tar.gz',
|
'url': '$base/watch-together-0.2.0-linux-x64.tar.gz',
|
||||||
'sha256': sha256.convert(tarball).toString(),
|
'sha256': sha256.convert(tarball).toString(),
|
||||||
'notes': 'test build',
|
'notes': 'test build',
|
||||||
...manifestExtra,
|
...manifestExtra,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
} else if (req.uri.path == '/watch-together-0.2.0-linux-x64.tar.gz') {
|
} else if (req.uri.path == '/watch-together-0.2.0-linux-x64.tar.gz') {
|
||||||
tarHits++;
|
tarHits++;
|
||||||
req.response.contentLength = tarball.length;
|
req.response.contentLength = tarball.length;
|
||||||
@@ -145,11 +156,9 @@ void main() {
|
|||||||
test('manifest rejects missing fields, sha256 is optional', () {
|
test('manifest rejects missing fields, sha256 is optional', () {
|
||||||
expect(() => UpdateManifest.fromJson({}), throwsFormatException);
|
expect(() => UpdateManifest.fromJson({}), throwsFormatException);
|
||||||
expect(
|
expect(
|
||||||
() => UpdateManifest.fromJson({
|
() => UpdateManifest.fromJson({'version': '0.2.0', 'protocol': 4}),
|
||||||
'version': '0.2.0',
|
throwsFormatException,
|
||||||
'protocol': 4,
|
);
|
||||||
}),
|
|
||||||
throwsFormatException);
|
|
||||||
final noSha = UpdateManifest.fromJson({
|
final noSha = UpdateManifest.fromJson({
|
||||||
'version': '0.2.0',
|
'version': '0.2.0',
|
||||||
'protocol': RelayConstants.protoVersion,
|
'protocol': RelayConstants.protoVersion,
|
||||||
@@ -163,19 +172,51 @@ void main() {
|
|||||||
'protocol': RelayConstants.protoVersion + 1,
|
'protocol': RelayConstants.protoVersion + 1,
|
||||||
'url': 'https://x/y.tar.gz',
|
'url': 'https://x/y.tar.gz',
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
isTrue);
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manifest picks the asset for this platform', () {
|
||||||
|
final json = {
|
||||||
|
'version': '0.2.2',
|
||||||
|
'protocol': RelayConstants.protoVersion,
|
||||||
|
'url': 'https://x/linux.tar.gz',
|
||||||
|
'assets': {
|
||||||
|
'linux-x64': 'https://x/linux.tar.gz',
|
||||||
|
'windows-x64': 'https://x/windows.zip',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
UpdateManifest.fromJson(json, platform: 'windows-x64').url,
|
||||||
|
'https://x/windows.zip',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
UpdateManifest.fromJson(json, platform: 'linux-x64').url,
|
||||||
|
'https://x/linux.tar.gz',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
() => UpdateManifest.fromJson(json, platform: 'macos-x64'),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('findInstallRoot honors bundle layout', () async {
|
test('findInstallRoot honors bundle layout', () async {
|
||||||
final work = await _work();
|
final work = await _work();
|
||||||
try {
|
try {
|
||||||
final root = await _fakeRoot(work);
|
final root = await _fakeRoot(work);
|
||||||
expect(findInstallRoot('${root.path}/watch_together', windows: false)?.path,
|
expect(
|
||||||
root.path);
|
findInstallRoot('${root.path}/watch_together', windows: false)?.path,
|
||||||
expect(findInstallRoot('${work.path}/stray-binary', windows: false), isNull);
|
root.path,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
findInstallRoot('${work.path}/stray-binary', windows: false),
|
||||||
|
isNull,
|
||||||
|
);
|
||||||
final win = await _fakeRoot(await work.createTemp('w'), windows: true);
|
final win = await _fakeRoot(await work.createTemp('w'), windows: true);
|
||||||
expect(findInstallRoot('${win.path}/watch_together.exe', windows: true)?.path,
|
expect(
|
||||||
win.path);
|
findInstallRoot('${win.path}/watch_together.exe', windows: true)?.path,
|
||||||
|
win.path,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await work.delete(recursive: true);
|
await work.delete(recursive: true);
|
||||||
}
|
}
|
||||||
@@ -196,19 +237,21 @@ void main() {
|
|||||||
if (await work.exists()) await work.delete(recursive: true);
|
if (await work.exists()) await work.delete(recursive: true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('real release layout lands at the install root, modes kept',
|
test('real release layout lands at the install root, modes kept', () async {
|
||||||
() async {
|
|
||||||
final root = await _fakeRoot(work);
|
final root = await _fakeRoot(work);
|
||||||
final rc = await runUpgrade(
|
final rc = await runUpgrade(
|
||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
installRoot: root);
|
installRoot: root,
|
||||||
|
);
|
||||||
expect(rc, 0);
|
expect(rc, 0);
|
||||||
expect(server.tarHits, 1);
|
expect(server.tarHits, 1);
|
||||||
final p = root.path;
|
final p = root.path;
|
||||||
expect(await File('$p/watch_together').readAsString(), 'binary-v2');
|
expect(await File('$p/watch_together').readAsString(), 'binary-v2');
|
||||||
expect(await File('$p/data/flutter_assets/fresh.cfg').readAsString(),
|
expect(
|
||||||
'fresh-config');
|
await File('$p/data/flutter_assets/fresh.cfg').readAsString(),
|
||||||
|
'fresh-config',
|
||||||
|
);
|
||||||
expect(Directory('$p/watch-together-0.2.0').existsSync(), isFalse);
|
expect(Directory('$p/watch-together-0.2.0').existsSync(), isFalse);
|
||||||
expect(_isExec('$p/watch_together'), isTrue);
|
expect(_isExec('$p/watch_together'), isTrue);
|
||||||
expect(_isExec('$p/update.sh'), isTrue);
|
expect(_isExec('$p/update.sh'), isTrue);
|
||||||
@@ -226,11 +269,14 @@ void main() {
|
|||||||
final rc = await runUpgrade(
|
final rc = await runUpgrade(
|
||||||
currentVersion: '0.2.0',
|
currentVersion: '0.2.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
installRoot: root);
|
installRoot: root,
|
||||||
|
);
|
||||||
expect(rc, 0);
|
expect(rc, 0);
|
||||||
expect(server.tarHits, 0);
|
expect(server.tarHits, 0);
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
expect(
|
||||||
'binary-v1');
|
await File('${root.path}/watch_together').readAsString(),
|
||||||
|
'binary-v1',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('bad hash aborts, install untouched', () async {
|
test('bad hash aborts, install untouched', () async {
|
||||||
@@ -239,10 +285,13 @@ void main() {
|
|||||||
final rc = await runUpgrade(
|
final rc = await runUpgrade(
|
||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
installRoot: root);
|
installRoot: root,
|
||||||
|
);
|
||||||
expect(rc, 1);
|
expect(rc, 1);
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
expect(
|
||||||
'binary-v1');
|
await File('${root.path}/watch_together').readAsString(),
|
||||||
|
'binary-v1',
|
||||||
|
);
|
||||||
expect(_leftovers(root), isEmpty);
|
expect(_leftovers(root), isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -252,34 +301,47 @@ void main() {
|
|||||||
final rc = await runUpgrade(
|
final rc = await runUpgrade(
|
||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
installRoot: root);
|
installRoot: root,
|
||||||
|
);
|
||||||
expect(rc, 0);
|
expect(rc, 0);
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
expect(
|
||||||
'binary-v2');
|
await File('${root.path}/watch_together').readAsString(),
|
||||||
|
'binary-v2',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unknown layout asks for manual update', () async {
|
test('unknown layout asks for manual update', () async {
|
||||||
final rc = await runUpgrade(
|
final rc = await runUpgrade(
|
||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
executablePath: '${work.path}/stray-binary');
|
executablePath: '${work.path}/stray-binary',
|
||||||
|
);
|
||||||
expect(rc, 2);
|
expect(rc, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('archive that is not a bundle is refused, install untouched',
|
test(
|
||||||
|
'archive that is not a bundle is refused, install untouched',
|
||||||
() async {
|
() async {
|
||||||
server.tarball = await _releaseTarball(work, version: '9.9.9', bundle: false);
|
server.tarball = await _releaseTarball(
|
||||||
|
work,
|
||||||
|
version: '9.9.9',
|
||||||
|
bundle: false,
|
||||||
|
);
|
||||||
final root = await _fakeRoot(work);
|
final root = await _fakeRoot(work);
|
||||||
final rc = await runUpgrade(
|
final rc = await runUpgrade(
|
||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
installRoot: root);
|
installRoot: root,
|
||||||
|
);
|
||||||
expect(rc, 1);
|
expect(rc, 1);
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
expect(
|
||||||
'binary-v1');
|
await File('${root.path}/watch_together').readAsString(),
|
||||||
|
'binary-v1',
|
||||||
|
);
|
||||||
expect(File('${root.path}/lib/libgone.so').existsSync(), isTrue);
|
expect(File('${root.path}/lib/libgone.so').existsSync(), isTrue);
|
||||||
expect(_leftovers(root), isEmpty);
|
expect(_leftovers(root), isEmpty);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('apply', () {
|
group('apply', () {
|
||||||
@@ -318,10 +380,16 @@ void main() {
|
|||||||
test('failure at $step rolls back', () async {
|
test('failure at $step rolls back', () async {
|
||||||
final root = await _fakeRoot(work);
|
final root = await _fakeRoot(work);
|
||||||
await expectLater(
|
await expectLater(
|
||||||
applyUpdate(await archive(), root, windows: false, hook: (s) async {
|
applyUpdate(
|
||||||
|
await archive(),
|
||||||
|
root,
|
||||||
|
windows: false,
|
||||||
|
hook: (s) async {
|
||||||
if (s == step) throw FileSystemException('injected', s);
|
if (s == step) throw FileSystemException('injected', s);
|
||||||
}),
|
},
|
||||||
throwsA(isA<FileSystemException>()));
|
),
|
||||||
|
throwsA(isA<FileSystemException>()),
|
||||||
|
);
|
||||||
await expectUntouched(root);
|
await expectUntouched(root);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -329,13 +397,20 @@ void main() {
|
|||||||
test('stages next to the install, never in the system temp', () async {
|
test('stages next to the install, never in the system temp', () async {
|
||||||
final root = await _fakeRoot(work);
|
final root = await _fakeRoot(work);
|
||||||
final seen = <String>[];
|
final seen = <String>[];
|
||||||
await applyUpdate(await archive(), root, windows: false, hook: (s) async {
|
await applyUpdate(
|
||||||
|
await archive(),
|
||||||
|
root,
|
||||||
|
windows: false,
|
||||||
|
hook: (s) async {
|
||||||
if (seen.isEmpty) {
|
if (seen.isEmpty) {
|
||||||
seen.add(Directory('${root.path}/.update-staging').existsSync()
|
seen.add(
|
||||||
|
Directory('${root.path}/.update-staging').existsSync()
|
||||||
? 'staged-in-root'
|
? 'staged-in-root'
|
||||||
: 'elsewhere');
|
: 'elsewhere',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
expect(seen, ['staged-in-root']);
|
expect(seen, ['staged-in-root']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,12 +421,16 @@ void main() {
|
|||||||
final f = File('${work.path}/evil.tar.gz')
|
final f = File('${work.path}/evil.tar.gz')
|
||||||
..writeAsBytesSync(GZipEncoder().encode(TarEncoder().encode(evil)));
|
..writeAsBytesSync(GZipEncoder().encode(TarEncoder().encode(evil)));
|
||||||
await expectLater(
|
await expectLater(
|
||||||
applyUpdate(f, root, windows: false), throwsFormatException);
|
applyUpdate(f, root, windows: false),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
final link = Archive()
|
final link = Archive()
|
||||||
..addFile(ArchiveFile.symlink('w/lib/x.so', '../../../etc/passwd'));
|
..addFile(ArchiveFile.symlink('w/lib/x.so', '../../../etc/passwd'));
|
||||||
f.writeAsBytesSync(GZipEncoder().encode(TarEncoder().encode(link)));
|
f.writeAsBytesSync(GZipEncoder().encode(TarEncoder().encode(link)));
|
||||||
await expectLater(
|
await expectLater(
|
||||||
applyUpdate(f, root, windows: false), throwsFormatException);
|
applyUpdate(f, root, windows: false),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
expect(File('${work.path}/escape').existsSync(), isFalse);
|
expect(File('${work.path}/escape').existsSync(), isFalse);
|
||||||
await expectUntouched(root);
|
await expectUntouched(root);
|
||||||
});
|
});
|
||||||
@@ -375,8 +454,10 @@ void main() {
|
|||||||
final p = root.path;
|
final p = root.path;
|
||||||
expect(await File('$p/watch_together.exe').readAsString(), 'binary-v2');
|
expect(await File('$p/watch_together.exe').readAsString(), 'binary-v2');
|
||||||
expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v2');
|
expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v2');
|
||||||
expect(await File('$p/data/flutter_assets/fresh.cfg').readAsString(),
|
expect(
|
||||||
'fresh-config');
|
await File('$p/data/flutter_assets/fresh.cfg').readAsString(),
|
||||||
|
'fresh-config',
|
||||||
|
);
|
||||||
// Not in use here, so the .old copies were deleted right away.
|
// Not in use here, so the .old copies were deleted right away.
|
||||||
expect(File('$p/watch_together.exe.old').existsSync(), isFalse);
|
expect(File('$p/watch_together.exe.old').existsSync(), isFalse);
|
||||||
expect(_leftovers(root), isEmpty);
|
expect(_leftovers(root), isEmpty);
|
||||||
@@ -385,13 +466,18 @@ void main() {
|
|||||||
test('windows-style swap rolls back', () async {
|
test('windows-style swap rolls back', () async {
|
||||||
final root = await _fakeRoot(work, windows: true);
|
final root = await _fakeRoot(work, windows: true);
|
||||||
await expectLater(
|
await expectLater(
|
||||||
applyUpdate(await archive(windows: true), root, windows: true,
|
applyUpdate(
|
||||||
|
await archive(windows: true),
|
||||||
|
root,
|
||||||
|
windows: true,
|
||||||
hook: (s) async {
|
hook: (s) async {
|
||||||
if (s == 'in:watch_together.exe') {
|
if (s == 'in:watch_together.exe') {
|
||||||
throw const FileSystemException('injected');
|
throw const FileSystemException('injected');
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
throwsA(isA<FileSystemException>()));
|
),
|
||||||
|
throwsA(isA<FileSystemException>()),
|
||||||
|
);
|
||||||
final p = root.path;
|
final p = root.path;
|
||||||
expect(await File('$p/watch_together.exe').readAsString(), 'binary-v1');
|
expect(await File('$p/watch_together.exe').readAsString(), 'binary-v1');
|
||||||
expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v1');
|
expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v1');
|
||||||
@@ -416,8 +502,7 @@ void main() {
|
|||||||
if (await work.exists()) await work.delete(recursive: true);
|
if (await work.exists()) await work.delete(recursive: true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('check, download with progress, apply, restart same binary',
|
test('check, download with progress, apply, restart same binary', () async {
|
||||||
() async {
|
|
||||||
final root = await _fakeRoot(work);
|
final root = await _fakeRoot(work);
|
||||||
final restarts = <(String, List<String>)>[];
|
final restarts = <(String, List<String>)>[];
|
||||||
launchArgs = const ['--some-flag'];
|
launchArgs = const ['--some-flag'];
|
||||||
@@ -425,7 +510,8 @@ void main() {
|
|||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
executablePath: '${root.path}/watch_together',
|
executablePath: '${root.path}/watch_together',
|
||||||
restart: (exe, args) async => restarts.add((exe, args)));
|
restart: (exe, args) async => restarts.add((exe, args)),
|
||||||
|
);
|
||||||
await c.checkOnce();
|
await c.checkOnce();
|
||||||
expect(c.status.value.phase, UpdatePhase.available);
|
expect(c.status.value.phase, UpdatePhase.available);
|
||||||
expect(c.status.value.canInstall, isTrue);
|
expect(c.status.value.canInstall, isTrue);
|
||||||
@@ -438,21 +524,23 @@ void main() {
|
|||||||
await c.install();
|
await c.install();
|
||||||
expect(progress.last, 1.0);
|
expect(progress.last, 1.0);
|
||||||
expect(c.status.value.phase, UpdatePhase.restarting);
|
expect(c.status.value.phase, UpdatePhase.restarting);
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
expect(
|
||||||
'binary-v2');
|
await File('${root.path}/watch_together').readAsString(),
|
||||||
|
'binary-v2',
|
||||||
|
);
|
||||||
expect(_isExec('${root.path}/watch_together'), isTrue);
|
expect(_isExec('${root.path}/watch_together'), isTrue);
|
||||||
expect(restarts.single.$1, '${root.path}/watch_together');
|
expect(restarts.single.$1, '${root.path}/watch_together');
|
||||||
expect(restarts.single.$2, ['--some-flag']);
|
expect(restarts.single.$2, ['--some-flag']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('newer protocol is a required update, cannot be dismissed',
|
test('newer protocol is a required update, cannot be dismissed', () async {
|
||||||
() async {
|
|
||||||
server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1};
|
server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1};
|
||||||
final c = UpdateController(
|
final c = UpdateController(
|
||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
executablePath: '${work.path}/stray-binary',
|
executablePath: '${work.path}/stray-binary',
|
||||||
restart: (_, _) async {});
|
restart: (_, _) async {},
|
||||||
|
);
|
||||||
await c.checkOnce();
|
await c.checkOnce();
|
||||||
expect(c.status.value.manifest!.isRequired, isTrue);
|
expect(c.status.value.manifest!.isRequired, isTrue);
|
||||||
expect(c.status.value.canInstall, isFalse);
|
expect(c.status.value.canInstall, isFalse);
|
||||||
@@ -466,7 +554,8 @@ void main() {
|
|||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
executablePath: '${work.path}/stray-binary',
|
executablePath: '${work.path}/stray-binary',
|
||||||
restart: (_, _) async {});
|
restart: (_, _) async {},
|
||||||
|
);
|
||||||
await c.checkOnce();
|
await c.checkOnce();
|
||||||
expect(c.status.value.phase, UpdatePhase.idle);
|
expect(c.status.value.phase, UpdatePhase.idle);
|
||||||
server.brokenManifest = false;
|
server.brokenManifest = false;
|
||||||
@@ -474,12 +563,14 @@ void main() {
|
|||||||
currentVersion: '0.0.0-dev',
|
currentVersion: '0.0.0-dev',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
executablePath: '${work.path}/stray-binary',
|
executablePath: '${work.path}/stray-binary',
|
||||||
restart: (_, _) async {});
|
restart: (_, _) async {},
|
||||||
|
);
|
||||||
await dev.checkOnce();
|
await dev.checkOnce();
|
||||||
expect(dev.status.value.phase, UpdatePhase.idle);
|
expect(dev.status.value.phase, UpdatePhase.idle);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('failed install shows error, install untouched, retry works',
|
test(
|
||||||
|
'failed install shows error, install untouched, retry works',
|
||||||
() async {
|
() async {
|
||||||
server.manifestExtra = {'sha256': '0' * 64};
|
server.manifestExtra = {'sha256': '0' * 64};
|
||||||
final root = await _fakeRoot(work);
|
final root = await _fakeRoot(work);
|
||||||
@@ -488,14 +579,18 @@ void main() {
|
|||||||
currentVersion: '0.1.0',
|
currentVersion: '0.1.0',
|
||||||
manifestUrl: server.manifestUrl,
|
manifestUrl: server.manifestUrl,
|
||||||
executablePath: '${root.path}/watch_together',
|
executablePath: '${root.path}/watch_together',
|
||||||
restart: (_, _) async => restarted = true);
|
restart: (_, _) async => restarted = true,
|
||||||
|
);
|
||||||
await c.checkOnce();
|
await c.checkOnce();
|
||||||
await c.install();
|
await c.install();
|
||||||
expect(c.status.value.phase, UpdatePhase.failed);
|
expect(c.status.value.phase, UpdatePhase.failed);
|
||||||
expect(c.status.value.error, contains('sha256'));
|
expect(c.status.value.error, contains('sha256'));
|
||||||
expect(restarted, isFalse);
|
expect(restarted, isFalse);
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
expect(
|
||||||
'binary-v1');
|
await File('${root.path}/watch_together').readAsString(),
|
||||||
});
|
'binary-v1',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user