One updater for app and server: updater in core; watch-together-server --upgrade / --check-update; update.sh uses the server
The server's version is stamped like the app's (APP_VERSION) when packaged, so it can compare against the manifest; --upgrade swaps the whole install folder (app and server) and asks for a restart. update.sh/.bat no longer need GTK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
059b7dd306
commit
2d39018a73
27 files changed
+382
-171
No files matched your search
@@ -55,5 +55,8 @@ jobs:
|
|||||||
run: powershell -NoProfile -ExecutionPolicy Bypass -File tool/setup_flutter_windows.ps1
|
run: powershell -NoProfile -ExecutionPolicy Bypass -File tool/setup_flutter_windows.ps1
|
||||||
- name: Updater tests
|
- name: Updater tests
|
||||||
shell: powershell
|
shell: powershell
|
||||||
working-directory: apps/desktop
|
working-directory: packages/core
|
||||||
run: flutter test test/updater_test.dart
|
run: |
|
||||||
|
dart pub get
|
||||||
|
if ($LASTEXITCODE) { exit 1 }
|
||||||
|
dart test test/updater_test.dart
|
||||||
@@ -36,7 +36,10 @@ Update from the app: when a newer version exists, an update indicator
|
|||||||
appears in the strip at the top of the window and opens the update card. "Update and restart" downloads,
|
appears in the strip at the top of the window and opens the update card. "Update and restart" downloads,
|
||||||
verifies, applies in place and restarts; the install folder must be writable
|
verifies, applies in place and restarts; the install folder must be writable
|
||||||
by you. Settings has Check now and an auto-check toggle. From a terminal:
|
by you. Settings has Check now and an auto-check toggle. From a terminal:
|
||||||
`watch_together --upgrade`, or the shipped `update.sh` / `update.bat`. A
|
`./watch-together-server --upgrade` (no display needed, so it works on a
|
||||||
|
server box too), `watch_together --upgrade`, or the shipped `update.sh` /
|
||||||
|
`update.bat` (which run the first). Each updates the whole folder, app and
|
||||||
|
server; restart whichever is running to use the new version. A
|
||||||
release with a newer protocol is a required update: everyone on a server
|
release with a newer protocol is a required update: everyone on a server
|
||||||
must run the same protocol version, and a mismatch is refused with a message
|
must run the same protocol version, and a mismatch is refused with a message
|
||||||
telling you to update.
|
telling you to update.
|
||||||
@@ -170,6 +173,8 @@ then everyone (you included) uses Join.
|
|||||||
| `--yt-dlp-dir` | next to the binary | writable folder for the server's yt-dlp (systemd: the unit's `StateDirectory`) |
|
| `--yt-dlp-dir` | next to the binary | writable folder for the server's yt-dlp (systemd: the unit's `StateDirectory`) |
|
||||||
| `--yt-dlp-cookies` | none | cookies.txt or cookie-extension JSON export for that yt-dlp (sites that refuse guests); copied into the yt-dlp folder at start, removed from it when not given. **An account login**: keep it private, use a throwaway account for YouTube |
|
| `--yt-dlp-cookies` | none | cookies.txt or cookie-extension JSON export for that yt-dlp (sites that refuse guests); copied into the yt-dlp folder at start, removed from it when not given. **An account login**: keep it private, use a throwaway account for YouTube |
|
||||||
| `--config` | none | YAML or JSON file with the same settings (`port`, `secret`, `name`, `mediaPath`, `advertiseHost`, `ytDlp`, `ytDlpDir`, `ytDlpCookies`); replaces the flags |
|
| `--config` | none | YAML or JSON file with the same settings (`port`, `secret`, `name`, `mediaPath`, `advertiseHost`, `ytDlp`, `ytDlpDir`, `ytDlpCookies`); replaces the flags |
|
||||||
|
| `--upgrade` | | update the install folder to the latest release ([Install and update](#install-and-update)), then exit |
|
||||||
|
| `--check-update` | | print whether a newer release exists, then exit |
|
||||||
|
|
||||||
It prints `READY port=... name=...` once listening and stops cleanly on
|
It prints `READY port=... name=...` once listening and stops cleanly on
|
||||||
Ctrl+C or SIGTERM. A dedicated server has no host.
|
Ctrl+C or SIGTERM. A dedicated server has no host.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import 'src/platform/desktop_entry.dart';
|
|||||||
import 'src/screens/home/ui.dart';
|
import 'src/screens/home/ui.dart';
|
||||||
import 'src/state/session_state.dart';
|
import 'src/state/session_state.dart';
|
||||||
import 'src/state/settings.dart';
|
import 'src/state/settings.dart';
|
||||||
import 'src/update/updater.dart';
|
import 'src/update/update_controller.dart' show launchArgs;
|
||||||
|
|
||||||
Future<void> main(List<String> args) async {
|
Future<void> main(List<String> args) async {
|
||||||
launchArgs = List.unmodifiable(args);
|
launchArgs = List.unmodifiable(args);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:hive_ce/hive.dart';
|
|||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
|
|
||||||
import '../screens/home/picked_media.dart';
|
import '../screens/home/picked_media.dart';
|
||||||
import '../update/updater.dart' show appVersion;
|
|
||||||
|
|
||||||
// Linked Jellyfin servers, one account per ServerId, in their own Hive box.
|
// Linked Jellyfin servers, one account per ServerId, in their own Hive box.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:sync_relay_core/core.dart' show findInstallRoot;
|
||||||
import '../update/updater.dart' show findInstallRoot;
|
|
||||||
|
|
||||||
/// The app's id: GTK's application id, so the Wayland `app_id` the
|
/// The app's id: GTK's application id, so the Wayland `app_id` the
|
||||||
/// compositor matches to [desktopFileName] for the window's icon.
|
/// compositor matches to [desktopFileName] for the window's icon.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'dart:ui' show Color;
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
|
|
||||||
import '../update/updater.dart' show appVersion;
|
|
||||||
|
|
||||||
// AniList airing schedule (public GraphQL, no login): every episode airing
|
// AniList airing schedule (public GraphQL, no login): every episode airing
|
||||||
// in a time range, with covers, banners and popularity; and a title search
|
// in a time range, with covers, banners and popularity; and a title search
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'dart:io';
|
|||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
|
|
||||||
import '../jellyfin/accounts.dart';
|
import '../jellyfin/accounts.dart';
|
||||||
import '../update/updater.dart' show appVersion;
|
|
||||||
|
|
||||||
// AnimeFlow app API: what the media server tracks, has, downloads and was
|
// AnimeFlow app API: what the media server tracks, has, downloads and was
|
||||||
// asked for. Authenticated with the user's own Jellyfin access token
|
// asked for. Authenticated with the user's own Jellyfin access token
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:sync_relay_core/core.dart';
|
||||||
|
|
||||||
import '../../update/update_controller.dart';
|
import '../../update/update_controller.dart';
|
||||||
import '../../update/updater.dart';
|
|
||||||
import 'settings_page.dart';
|
import 'settings_page.dart';
|
||||||
import 'ui.dart';
|
import 'ui.dart';
|
||||||
import 'update_indicator.dart';
|
import 'update_indicator.dart';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@echo off
|
@echo off
|
||||||
rem Update this install in place (same as clicking Update in the app).
|
rem Update this install in place (same as clicking Update in the app).
|
||||||
"%~dp0watch_together.exe" --upgrade %*
|
"%~dp0watch-together-server.exe" --upgrade %*
|
||||||
pause
|
pause
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Update this install in place (same as clicking Update in the app).
|
# Update this install in place (same as clicking Update in the app).
|
||||||
cd "$(dirname "$0")" && exec ./watch_together --upgrade "$@"
|
# The server binary runs the updater: it needs no GTK/display (VPS).
|
||||||
|
cd "$(dirname "$0")" && exec ./watch-together-server --upgrade "$@"
|
||||||
@@ -4,11 +4,9 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
|
|
||||||
import 'updater.dart';
|
|
||||||
|
|
||||||
// In-app update flow behind the Home update indicator. Same code path as
|
// In-app update flow behind the Home update indicator. Same code path as
|
||||||
// `--upgrade` (downloadAndApply); this adds state for the UI and the
|
// `--upgrade` (core's updater, downloadAndApply); this adds state for the
|
||||||
// restart. Check failures are logged only — the banner shows up just when
|
// UI and the restart. Check failures are logged only — the banner shows up just when
|
||||||
// there is something to install.
|
// there is something to install.
|
||||||
|
|
||||||
final _log = createLogger('update');
|
final _log = createLogger('update');
|
||||||
@@ -27,6 +25,19 @@ class UpdateStatus {
|
|||||||
{this.manifest, this.progress, this.error, this.canInstall = false});
|
{this.manifest, this.progress, this.error, this.canInstall = false});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process args as main() received them — restart relaunches with these.
|
||||||
|
List<String> launchArgs = const [];
|
||||||
|
|
||||||
|
// Relaunch the (now replaced) binary detached with the same args, then
|
||||||
|
// 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);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
typedef RestartFn = Future<void> Function(String executable, List<String> args);
|
typedef RestartFn = Future<void> Function(String executable, List<String> args);
|
||||||
|
|
||||||
// Release Hive's box locks before the new instance opens the same boxes.
|
// Release Hive's box locks before the new instance opens the same boxes.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sync_relay_core/core.dart';
|
||||||
|
|
||||||
import '../update/update_controller.dart';
|
import '../update/update_controller.dart';
|
||||||
import '../update/updater.dart';
|
|
||||||
|
|
||||||
// "Update available" card (Home update indicator dialog). Kicks off the one
|
// "Update available" card (Home update indicator dialog). Kicks off the one
|
||||||
// background check on first build; renders nothing until there is an
|
// background check on first build; renders nothing until there is an
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
archive:
|
archive:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19
|
sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ dependencies:
|
|||||||
hive_ce_flutter: ^2.3.4
|
hive_ce_flutter: ^2.3.4
|
||||||
image: ^4.9.2
|
image: ^4.9.2
|
||||||
window_manager: ^0.5.2
|
window_manager: ^0.5.2
|
||||||
archive: ^4.0.7
|
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
# Drag & drop and clipboard files/images (vendored desktop_drop + pasteboard).
|
# Drag & drop and clipboard files/images (vendored desktop_drop + pasteboard).
|
||||||
fox_utils:
|
fox_utils:
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:sync_relay_core/core.dart';
|
||||||
|
import 'package:watch_together/src/update/update_controller.dart';
|
||||||
|
|
||||||
|
// Work dir base: WT_TEST_TMP or the system temp. Staging lives inside
|
||||||
|
// the install root, so the result doesn't depend on which filesystem
|
||||||
|
// this is (the old /tmp staging broke when /tmp was tmpfs).
|
||||||
|
final _tmpBase =
|
||||||
|
Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
|
||||||
|
|
||||||
|
Future<Directory> _work() async {
|
||||||
|
final base = Directory(_tmpBase);
|
||||||
|
await base.create(recursive: true);
|
||||||
|
return base.createTemp('wt-up-test');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux-layout tests check exec bits and symlinks, which NTFS lacks.
|
||||||
|
final _posixOnly = Platform.isWindows ? 'POSIX install layout' : null;
|
||||||
|
|
||||||
|
bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0;
|
||||||
|
|
||||||
|
// Builds the release archive exactly like tool/package_linux.sh:
|
||||||
|
// bundle copied into <stage>/watch-together-<ver>/, then
|
||||||
|
// `tar -czf out -C <stage> watch-together-<ver>`. [zip]: like
|
||||||
|
// tool/package_windows.ps1 (`tar -a -c -f out.zip`, bsdtar — Windows only).
|
||||||
|
Future<List<int>> _releaseTarball(Directory work,
|
||||||
|
{String version = '0.2.0',
|
||||||
|
bool windows = false,
|
||||||
|
bool bundle = true,
|
||||||
|
bool zip = false}) async {
|
||||||
|
final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}');
|
||||||
|
final folder = Directory('${stage.path}/watch-together-$version');
|
||||||
|
await Directory('${folder.path}/data/flutter_assets').create(recursive: true);
|
||||||
|
await File('${folder.path}/data/flutter_assets/fresh.cfg')
|
||||||
|
.writeAsString('fresh-config');
|
||||||
|
if (windows) {
|
||||||
|
await File('${folder.path}/watch_together.exe').writeAsString('binary-v2');
|
||||||
|
await File('${folder.path}/flutter_windows.dll').writeAsString('dll-v2');
|
||||||
|
} else {
|
||||||
|
await File('${folder.path}/watch_together').writeAsString('binary-v2');
|
||||||
|
if (bundle) {
|
||||||
|
await Directory('${folder.path}/lib').create();
|
||||||
|
await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2');
|
||||||
|
await Link('${folder.path}/lib/libapp.so').create('libapp.so.1');
|
||||||
|
}
|
||||||
|
await File('${folder.path}/update.sh')
|
||||||
|
.writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n');
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
final r = await Process.run('chmod',
|
||||||
|
['755', '${folder.path}/watch_together', '${folder.path}/update.sh']);
|
||||||
|
expect(r.exitCode, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final out = File('${work.path}/out-$version.${zip ? 'zip' : 'tar.gz'}');
|
||||||
|
final r = await Process.run('tar', [
|
||||||
|
...zip ? ['-a', '-c', '-f'] : ['-czf'],
|
||||||
|
out.path,
|
||||||
|
'-C',
|
||||||
|
stage.path,
|
||||||
|
'watch-together-$version',
|
||||||
|
]);
|
||||||
|
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||||
|
return out.readAsBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Directory> _fakeRoot(Directory work, {bool windows = false}) async {
|
||||||
|
final root = Directory('${work.path}/install');
|
||||||
|
await Directory('${root.path}/data').create(recursive: true);
|
||||||
|
await File('${root.path}/data/old.cfg').writeAsString('old-config');
|
||||||
|
if (windows) {
|
||||||
|
await File('${root.path}/watch_together.exe').writeAsString('binary-v1');
|
||||||
|
await File('${root.path}/flutter_windows.dll').writeAsString('dll-v1');
|
||||||
|
} else {
|
||||||
|
await Directory('${root.path}/lib').create();
|
||||||
|
await File('${root.path}/lib/libgone.so').writeAsString('lib-v1');
|
||||||
|
await File('${root.path}/watch_together').writeAsString('binary-v1');
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
await Process.run('chmod', ['755', '${root.path}/watch_together']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await File('${root.path}/user-notes.txt').writeAsString('mine');
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serves /manifest.json and the tarball. Fields can be overridden per test.
|
||||||
|
class _Server {
|
||||||
|
late HttpServer http;
|
||||||
|
List<int> tarball;
|
||||||
|
Map<String, Object?> manifestExtra = {};
|
||||||
|
bool brokenManifest = false;
|
||||||
|
int tarHits = 0;
|
||||||
|
_Server(this.tarball);
|
||||||
|
|
||||||
|
String get base => 'http://127.0.0.1:${http.port}';
|
||||||
|
String get manifestUrl => '$base/manifest.json';
|
||||||
|
|
||||||
|
Future<void> start() async {
|
||||||
|
http = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
|
http.listen((req) async {
|
||||||
|
if (req.uri.path == '/manifest.json' && !brokenManifest) {
|
||||||
|
req.response.write(jsonEncode({
|
||||||
|
'version': '0.2.0',
|
||||||
|
'protocol': RelayConstants.protoVersion,
|
||||||
|
'url': '$base/watch-together-0.2.0-linux-x64.tar.gz',
|
||||||
|
'sha256': sha256.convert(tarball).toString(),
|
||||||
|
'notes': 'test build',
|
||||||
|
...manifestExtra,
|
||||||
|
}));
|
||||||
|
} else if (req.uri.path == '/watch-together-0.2.0-linux-x64.tar.gz') {
|
||||||
|
tarHits++;
|
||||||
|
req.response.contentLength = tarball.length;
|
||||||
|
req.response.add(tarball);
|
||||||
|
} else {
|
||||||
|
req.response.statusCode = 404;
|
||||||
|
}
|
||||||
|
await req.response.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() => http.close(force: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The updater itself is tested in packages/core/test/updater_test.dart
|
||||||
|
// (fixtures above mirror it); this covers the app's UpdateController.
|
||||||
|
void main() {
|
||||||
|
group('in-app controller', () {
|
||||||
|
late Directory work;
|
||||||
|
late _Server server;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
work = await _work();
|
||||||
|
server = _Server(await _releaseTarball(work));
|
||||||
|
await server.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await server.stop();
|
||||||
|
if (await work.exists()) await work.delete(recursive: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('check, download with progress, apply, restart same binary',
|
||||||
|
() async {
|
||||||
|
final root = await _fakeRoot(work);
|
||||||
|
final restarts = <(String, List<String>)>[];
|
||||||
|
launchArgs = const ['--some-flag'];
|
||||||
|
final c = UpdateController(
|
||||||
|
currentVersion: '0.1.0',
|
||||||
|
manifestUrl: server.manifestUrl,
|
||||||
|
executablePath: '${root.path}/watch_together',
|
||||||
|
restart: (exe, args) async => restarts.add((exe, args)));
|
||||||
|
await c.checkOnce();
|
||||||
|
expect(c.status.value.phase, UpdatePhase.available);
|
||||||
|
expect(c.status.value.canInstall, isTrue);
|
||||||
|
expect(c.status.value.manifest!.isRequired, isFalse);
|
||||||
|
final progress = <double>[];
|
||||||
|
c.status.addListener(() {
|
||||||
|
final p = c.status.value.progress;
|
||||||
|
if (p != null) progress.add(p);
|
||||||
|
});
|
||||||
|
await c.install();
|
||||||
|
expect(progress.last, 1.0);
|
||||||
|
expect(c.status.value.phase, UpdatePhase.restarting);
|
||||||
|
expect(await File('${root.path}/watch_together').readAsString(),
|
||||||
|
'binary-v2');
|
||||||
|
expect(_isExec('${root.path}/watch_together'), isTrue);
|
||||||
|
expect(restarts.single.$1, '${root.path}/watch_together');
|
||||||
|
expect(restarts.single.$2, ['--some-flag']);
|
||||||
|
}, skip: _posixOnly);
|
||||||
|
|
||||||
|
test('newer protocol is a required update, cannot be dismissed',
|
||||||
|
() async {
|
||||||
|
server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1};
|
||||||
|
final c = UpdateController(
|
||||||
|
currentVersion: '0.1.0',
|
||||||
|
manifestUrl: server.manifestUrl,
|
||||||
|
executablePath: '${work.path}/stray-binary',
|
||||||
|
restart: (_, _) async {});
|
||||||
|
await c.checkOnce();
|
||||||
|
expect(c.status.value.manifest!.isRequired, isTrue);
|
||||||
|
expect(c.status.value.canInstall, isFalse);
|
||||||
|
c.dismiss();
|
||||||
|
expect(c.status.value.phase, UpdatePhase.available);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed check stays quiet; dev builds skip the check', () async {
|
||||||
|
server.brokenManifest = true;
|
||||||
|
final c = UpdateController(
|
||||||
|
currentVersion: '0.1.0',
|
||||||
|
manifestUrl: server.manifestUrl,
|
||||||
|
executablePath: '${work.path}/stray-binary',
|
||||||
|
restart: (_, _) async {});
|
||||||
|
await c.checkOnce();
|
||||||
|
expect(c.status.value.phase, UpdatePhase.idle);
|
||||||
|
server.brokenManifest = false;
|
||||||
|
final dev = UpdateController(
|
||||||
|
currentVersion: '0.0.0-dev',
|
||||||
|
manifestUrl: server.manifestUrl,
|
||||||
|
executablePath: '${work.path}/stray-binary',
|
||||||
|
restart: (_, _) async {});
|
||||||
|
await dev.checkOnce();
|
||||||
|
expect(dev.status.value.phase, UpdatePhase.idle);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed install shows error, install untouched, retry works',
|
||||||
|
() async {
|
||||||
|
server.manifestExtra = {'sha256': '0' * 64};
|
||||||
|
final root = await _fakeRoot(work);
|
||||||
|
var restarted = false;
|
||||||
|
final c = UpdateController(
|
||||||
|
currentVersion: '0.1.0',
|
||||||
|
manifestUrl: server.manifestUrl,
|
||||||
|
executablePath: '${root.path}/watch_together',
|
||||||
|
restart: (_, _) async => restarted = true);
|
||||||
|
await c.checkOnce();
|
||||||
|
await c.install();
|
||||||
|
expect(c.status.value.phase, UpdatePhase.failed);
|
||||||
|
expect(c.status.value.error, contains('sha256'));
|
||||||
|
expect(restarted, isFalse);
|
||||||
|
expect(await File('${root.path}/watch_together').readAsString(),
|
||||||
|
'binary-v1');
|
||||||
|
}, skip: _posixOnly);
|
||||||
|
});
|
||||||
|
}
|
||||||
+19
-10
@@ -7,9 +7,9 @@ Plain overview first: [HOW-IT-WORKS.md](HOW-IT-WORKS.md). Wire format:
|
|||||||
|
|
||||||
| Path | What | Rule |
|
| Path | What | Rule |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `packages/core` | protocol messages, sync engine (`SyncEngine`), constants, config, Range parsing, logging, Jellyfin client | pure Dart, no Flutter |
|
| `packages/core` | protocol messages, sync engine (`SyncEngine`), constants, config, Range parsing, logging, Jellyfin client, updater | pure Dart, no Flutter |
|
||||||
| `packages/server` | Relic HTTP + WebSocket server (`SyncServer`, `ServerHub`, `Session`, media endpoint, relay buffer, Jellyfin relay), the `serve()` entrypoint, `bin/server.dart` (dedicated server) | pure Dart, no Flutter |
|
| `packages/server` | Relic HTTP + WebSocket server (`SyncServer`, `ServerHub`, `Session`, media endpoint, relay buffer, Jellyfin relay), the `serve()` entrypoint, `bin/server.dart` (dedicated server) | pure Dart, no Flutter |
|
||||||
| `apps/desktop` | Flutter app: UI, `SyncClient`, `SessionCoordinator`, `PlaybackController` (media_kit/mpv), `ServerIsolate`, updater | ships with `watch-together-server` |
|
| `apps/desktop` | Flutter app: UI, `SyncClient`, `SessionCoordinator`, `PlaybackController` (media_kit/mpv), `ServerIsolate`, update UI (`UpdateController`) | ships with `watch-together-server` |
|
||||||
| `tool` | harnesses: `range_logger`, `shaper`, `fake_viewer`, `e2e_check` | dev only |
|
| `tool` | harnesses: `range_logger`, `shaper`, `fake_viewer`, `e2e_check` | dev only |
|
||||||
| `deploy` | systemd unit for the dedicated server | |
|
| `deploy` | systemd unit for the dedicated server | |
|
||||||
|
|
||||||
@@ -60,7 +60,8 @@ size (`ServerIsolate.setFile`): the host's in-session File swap then sends a
|
|||||||
- **Dedicated server**: `bin/server.dart` compiled with `dart compile exe`
|
- **Dedicated server**: `bin/server.dart` compiled with `dart compile exe`
|
||||||
and shipped next to the app as `watch-together-server`. It parses flags
|
and shipped next to the app as `watch-together-server`. It parses flags
|
||||||
(`--port`, `--secret`, `--name`, `--media`, `--advertise-host`,
|
(`--port`, `--secret`, `--name`, `--media`, `--advertise-host`,
|
||||||
`--jellyfin <file>`, `--[no-]yt-dlp`, `--yt-dlp-dir`, `--yt-dlp-cookies`, `--config`; default port 8080) into the same map and calls `serve`;
|
`--jellyfin <file>`, `--[no-]yt-dlp`, `--yt-dlp-dir`, `--yt-dlp-cookies`, `--config`; default port 8080) into the same map and calls `serve`
|
||||||
|
(`--upgrade` / `--check-update` run the [updater](#updater) instead);
|
||||||
SIGINT and SIGTERM stop it. A Flutter binary cannot run without a
|
SIGINT and SIGTERM stop it. A Flutter binary cannot run without a
|
||||||
display, which is why the server is its own build.
|
display, which is why the server is its own build.
|
||||||
|
|
||||||
@@ -1056,23 +1057,31 @@ changed. The runner also sets the window icon from the bundle (X11).
|
|||||||
|
|
||||||
## Updater
|
## Updater
|
||||||
|
|
||||||
`apps/desktop/lib/src/update/updater.dart` (no Flutter imports, unit tested).
|
One implementation, `packages/core/lib/src/updater.dart` (pure Dart, tested
|
||||||
Reads a manifest (`version`, `protocol`, `url`, optional `sha256`, `notes`)
|
in `packages/core/test/updater_test.dart`), with two front ends: the app and
|
||||||
|
the dedicated server. Both binaries ship in the same archive, so either one
|
||||||
|
updates the whole install folder. Reads a manifest (`version`, `protocol`, `url`, optional `sha256`, `notes`)
|
||||||
from `updateManifestUrl`, compares versions, downloads the tarball, checks
|
from `updateManifestUrl`, compares versions, downloads the tarball, checks
|
||||||
sha256 when present, then extracts into a staging dir inside the install
|
sha256 when present, then extracts into a staging dir inside the install
|
||||||
folder (same filesystem, so moves are renames; the folder must be
|
folder (same filesystem, so moves are renames; the folder must be
|
||||||
user-writable). Each top-level entry is moved aside and the new one moved in,
|
user-writable). Each top-level entry is moved aside and the new one moved in,
|
||||||
with rollback on failure (writing the running executable fails with ETXTBSY;
|
with rollback on failure (writing the running executable fails with ETXTBSY;
|
||||||
rename works). A packaged install is detected by `data/` and `lib/` next to
|
rename works). A packaged install is detected by `data/` and `lib/` next to
|
||||||
the binary; anything else exits with "update manually". A manifest whose
|
the binary (app or server, same folder); anything else exits with "update
|
||||||
|
manually". A manifest whose
|
||||||
protocol is newer than the local one is a required update. Windows apply
|
protocol is newer than the local one is a required update. Windows apply
|
||||||
renames in-use files to `*.old` (swept on next start); CI tests it against a
|
renames in-use files to `*.old` (swept on next start); CI tests it against a
|
||||||
running exe on NTFS. The manifest's `assets` maps `linux-x64` /
|
running exe on NTFS. The manifest's `assets` maps `linux-x64` /
|
||||||
`windows-x64` to archive URLs; top-level `url` (Linux) is for builds <= 0.2.1. The version is
|
`windows-x64` to archive URLs; top-level `url` (Linux) is for builds <= 0.2.1. The version is
|
||||||
stamped with `--dart-define=APP_VERSION` (dev runs report `0.0.0-dev` and skip
|
stamped into both binaries with `APP_VERSION` (`--dart-define` for the app,
|
||||||
checks). `watch_together --upgrade` (or the shipped `update.sh`) runs it from
|
`--define` for `dart compile exe`; dev runs report `0.0.0-dev` and the app
|
||||||
the CLI; `UpdateController` runs the same code for the strip's update indicator,
|
skips checks). CLI: `watch-together-server --upgrade` (no display needed;
|
||||||
which opens the update card, and restarts the app ("Update and restart").
|
the shipped `update.sh` / `update.bat` run it), `--check-update` prints only;
|
||||||
|
it ends with "restart the server to run <version>". `watch_together --upgrade`
|
||||||
|
does the same from the app binary. In the app, `UpdateController`
|
||||||
|
(`apps/desktop/lib/src/update/`) runs the same code for the strip's update
|
||||||
|
indicator, which opens the update card, and restarts the app ("Update and
|
||||||
|
restart", closing Hive first).
|
||||||
Settings has Check now and an auto-check toggle. The manifest is `updates/manifest.json` in
|
Settings has Check now and an auto-check toggle. The manifest is `updates/manifest.json` in
|
||||||
this repo, written by `tool/release.sh`.
|
this repo, written by `tool/release.sh`.
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -17,11 +17,11 @@ cd apps/desktop && flutter pub get && flutter run -d linux
|
|||||||
cd apps/desktop && flutter build linux --release --dart-define=APP_VERSION=<x.y.z>
|
cd apps/desktop && flutter build linux --release --dart-define=APP_VERSION=<x.y.z>
|
||||||
tool/package_linux.sh [--skip-build] [--noupload] # dist/*.tar.gz
|
tool/package_linux.sh [--skip-build] [--noupload] # dist/*.tar.gz
|
||||||
powershell -File tool/package_windows.ps1 [-SkipBuild] # dist/*.zip (on Windows)
|
powershell -File tool/package_windows.ps1 [-SkipBuild] # dist/*.zip (on Windows)
|
||||||
cd packages/server && dart compile exe bin/server.dart -o watch-together-server
|
cd packages/server && dart compile exe --define=APP_VERSION=<x.y.z> bin/server.dart -o watch-together-server
|
||||||
```
|
```
|
||||||
|
|
||||||
`APP_VERSION` defaults to `0.0.0-dev`; `package_linux.sh` stamps it from the
|
`APP_VERSION` defaults to `0.0.0-dev`; the packagers stamp it from the
|
||||||
desktop pubspec. Setting `DISCORD_WEBHOOK_URL` makes the packager post the
|
desktop pubspec into both the app and the server. Setting `DISCORD_WEBHOOK_URL` makes the packager post the
|
||||||
tarball to Discord (never commit it). The packager always compiles the
|
tarball to Discord (never commit it). The packager always compiles the
|
||||||
dedicated server (with the Flutter SDK's `dart`) into the tarball as
|
dedicated server (with the Flutter SDK's `dart`) into the tarball as
|
||||||
`watch-together-server`, also with `--skip-build`.
|
`watch-together-server`, also with `--skip-build`.
|
||||||
|
|||||||
+1
-2
@@ -36,8 +36,7 @@ off by deleting it in the same commit as the code. Wire changes also edit
|
|||||||
filter on the Schedule; today's "Following" (AnimeFlow's monitored
|
filter on the Schedule; today's "Following" (AnimeFlow's monitored
|
||||||
shows) becomes "On the server".
|
shows) becomes "On the server".
|
||||||
|
|
||||||
5. **Ops**: `watch-together-server --upgrade` (a VPS has no GTK for the
|
5. **Ops**: check shaders on Windows; the relay e2e "source drops
|
||||||
app's updater); check shaders on Windows; the relay e2e "source drops
|
|
||||||
uncleanly and resumes" fails about half the time under the full suite.
|
uncleanly and resumes" fails about half the time under the full suite.
|
||||||
|
|
||||||
6. **Windows: first real use.** CI builds and publishes
|
6. **Windows: first real use.** CI builds and publishes
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export 'src/yt_dlp.dart';
|
|||||||
export 'src/cookies.dart';
|
export 'src/cookies.dart';
|
||||||
export 'src/media_source.dart';
|
export 'src/media_source.dart';
|
||||||
export 'src/sync_engine.dart';
|
export 'src/sync_engine.dart';
|
||||||
|
export 'src/updater.dart';
|
||||||
export 'src/jellyfin/client.dart';
|
export 'src/jellyfin/client.dart';
|
||||||
export 'src/jellyfin/client_info.dart';
|
export 'src/jellyfin/client_info.dart';
|
||||||
export 'src/jellyfin/http.dart';
|
export 'src/jellyfin/http.dart';
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:archive/archive.dart';
|
import 'package:archive/archive.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:sync_relay_core/core.dart';
|
|
||||||
|
|
||||||
// Zero Flutter imports: this file must stay unit-testable with plain
|
import 'constants.dart';
|
||||||
// `flutter test` (no widgets, no media_kit). One code path for both the
|
import 'logging.dart';
|
||||||
// CLI (`watch_together --upgrade`) and the in-app banner: fetch manifest,
|
|
||||||
// download, verify sha256, apply over the live install.
|
// The one updater for both programs: the app (`watch_together --upgrade`
|
||||||
|
// and the in-app update card, via UpdateController) and the dedicated
|
||||||
|
// server (`watch-together-server --upgrade`). Both ship in the same
|
||||||
|
// release archive, so either one swaps the whole install folder: fetch
|
||||||
|
// manifest, download, verify sha256, apply over the live install.
|
||||||
//
|
//
|
||||||
// Apply stages inside the install root (same filesystem, so every move is
|
// Apply stages inside the install root (same filesystem, so every move is
|
||||||
// a rename — /tmp is often tmpfs and rename across filesystems fails).
|
// a rename — /tmp is often tmpfs and rename across filesystems fails).
|
||||||
@@ -28,9 +31,6 @@ const updateManifestUrl =
|
|||||||
const appVersion = String.fromEnvironment('APP_VERSION',
|
const appVersion = String.fromEnvironment('APP_VERSION',
|
||||||
defaultValue: '0.0.0-dev');
|
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.
|
// Key into the manifest's `assets` map, e.g. linux-x64, windows-x64.
|
||||||
final updatePlatform = '${Platform.operatingSystem}-x64';
|
final updatePlatform = '${Platform.operatingSystem}-x64';
|
||||||
|
|
||||||
@@ -518,29 +518,24 @@ Future<void> downloadAndApply(UpdateManifest manifest, Directory root,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relaunch the (now replaced) binary detached with the same args, then
|
|
||||||
// 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);
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
String requiredUpdateNote(UpdateManifest m) =>
|
String requiredUpdateNote(UpdateManifest m) =>
|
||||||
'Required update: servers on ${m.version} speak protocol ${m.protocol}, '
|
'Required update: servers on ${m.version} speak protocol ${m.protocol}, '
|
||||||
'this build speaks ${RelayConstants.protoVersion} and cannot join them.';
|
'this build speaks ${RelayConstants.protoVersion} and cannot join them.';
|
||||||
|
|
||||||
// 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). [program] names the front
|
||||||
|
// end in the first line; [restartNote] is the last line after an update.
|
||||||
|
// [checkOnly] reports what is available and changes nothing.
|
||||||
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})');
|
String program = 'watch-together',
|
||||||
|
String Function(String version)? restartNote,
|
||||||
|
bool checkOnly = false}) async {
|
||||||
|
stdout.writeln('$program $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);
|
||||||
@@ -559,6 +554,7 @@ Future<int> runUpgrade(
|
|||||||
if (manifest.isRequired) {
|
if (manifest.isRequired) {
|
||||||
stdout.writeln(requiredUpdateNote(manifest));
|
stdout.writeln(requiredUpdateNote(manifest));
|
||||||
}
|
}
|
||||||
|
if (checkOnly) return 0;
|
||||||
final exe = executablePath ?? currentExecutablePath();
|
final exe = executablePath ?? currentExecutablePath();
|
||||||
final root = installRoot ?? findInstallRoot(exe);
|
final root = installRoot ?? findInstallRoot(exe);
|
||||||
if (root == null) {
|
if (root == null) {
|
||||||
@@ -575,6 +571,7 @@ Future<int> runUpgrade(
|
|||||||
stdout.writeln('update failed: $e');
|
stdout.writeln('update failed: $e');
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
stdout.writeln('updated to ${manifest.version} — restart the app to use it.');
|
stdout.writeln(restartNote?.call(manifest.version) ??
|
||||||
|
'updated to ${manifest.version} — restart the app to use it.');
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.0"
|
version: "14.3.0"
|
||||||
|
archive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "6c5bcd986e06b94e3c40244af471750840a3d2341d1f9763a1100a14add517b4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -81,6 +89,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -201,6 +217,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.3"
|
version: "1.5.3"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.2"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ dependencies:
|
|||||||
logging: ^1.2.0
|
logging: ^1.2.0
|
||||||
yaml: ^3.1.0
|
yaml: ^3.1.0
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
|
archive: ^4.0.7
|
||||||
crypto: ^3.0.0
|
crypto: ^3.0.0
|
||||||
web_socket_channel: ^3.0.0
|
web_socket_channel: ^3.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:archive/archive.dart';
|
import 'package:archive/archive.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:sync_relay_core/core.dart';
|
import 'package:sync_relay_core/core.dart';
|
||||||
import 'package:watch_together/src/update/update_controller.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:watch_together/src/update/updater.dart';
|
|
||||||
|
|
||||||
// Work dir base: WT_TEST_TMP or the system temp. Staging lives inside
|
// Work dir base: WT_TEST_TMP or the system temp. Staging lives inside
|
||||||
// the install root, so the result doesn't depend on which filesystem
|
// the install root, so the result doesn't depend on which filesystem
|
||||||
@@ -460,102 +458,4 @@ void main() {
|
|||||||
expect(File('$exe.old').existsSync(), isFalse);
|
expect(File('$exe.old').existsSync(), isFalse);
|
||||||
}, skip: Platform.isWindows ? null : 'needs Windows');
|
}, skip: Platform.isWindows ? null : 'needs Windows');
|
||||||
});
|
});
|
||||||
|
|
||||||
group('in-app controller', () {
|
|
||||||
late Directory work;
|
|
||||||
late _Server server;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
work = await _work();
|
|
||||||
server = _Server(await _releaseTarball(work));
|
|
||||||
await server.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async {
|
|
||||||
await server.stop();
|
|
||||||
if (await work.exists()) await work.delete(recursive: true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('check, download with progress, apply, restart same binary',
|
|
||||||
() async {
|
|
||||||
final root = await _fakeRoot(work);
|
|
||||||
final restarts = <(String, List<String>)>[];
|
|
||||||
launchArgs = const ['--some-flag'];
|
|
||||||
final c = UpdateController(
|
|
||||||
currentVersion: '0.1.0',
|
|
||||||
manifestUrl: server.manifestUrl,
|
|
||||||
executablePath: '${root.path}/watch_together',
|
|
||||||
restart: (exe, args) async => restarts.add((exe, args)));
|
|
||||||
await c.checkOnce();
|
|
||||||
expect(c.status.value.phase, UpdatePhase.available);
|
|
||||||
expect(c.status.value.canInstall, isTrue);
|
|
||||||
expect(c.status.value.manifest!.isRequired, isFalse);
|
|
||||||
final progress = <double>[];
|
|
||||||
c.status.addListener(() {
|
|
||||||
final p = c.status.value.progress;
|
|
||||||
if (p != null) progress.add(p);
|
|
||||||
});
|
|
||||||
await c.install();
|
|
||||||
expect(progress.last, 1.0);
|
|
||||||
expect(c.status.value.phase, UpdatePhase.restarting);
|
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
|
||||||
'binary-v2');
|
|
||||||
expect(_isExec('${root.path}/watch_together'), isTrue);
|
|
||||||
expect(restarts.single.$1, '${root.path}/watch_together');
|
|
||||||
expect(restarts.single.$2, ['--some-flag']);
|
|
||||||
}, skip: _posixOnly);
|
|
||||||
|
|
||||||
test('newer protocol is a required update, cannot be dismissed',
|
|
||||||
() async {
|
|
||||||
server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1};
|
|
||||||
final c = UpdateController(
|
|
||||||
currentVersion: '0.1.0',
|
|
||||||
manifestUrl: server.manifestUrl,
|
|
||||||
executablePath: '${work.path}/stray-binary',
|
|
||||||
restart: (_, _) async {});
|
|
||||||
await c.checkOnce();
|
|
||||||
expect(c.status.value.manifest!.isRequired, isTrue);
|
|
||||||
expect(c.status.value.canInstall, isFalse);
|
|
||||||
c.dismiss();
|
|
||||||
expect(c.status.value.phase, UpdatePhase.available);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('failed check stays quiet; dev builds skip the check', () async {
|
|
||||||
server.brokenManifest = true;
|
|
||||||
final c = UpdateController(
|
|
||||||
currentVersion: '0.1.0',
|
|
||||||
manifestUrl: server.manifestUrl,
|
|
||||||
executablePath: '${work.path}/stray-binary',
|
|
||||||
restart: (_, _) async {});
|
|
||||||
await c.checkOnce();
|
|
||||||
expect(c.status.value.phase, UpdatePhase.idle);
|
|
||||||
server.brokenManifest = false;
|
|
||||||
final dev = UpdateController(
|
|
||||||
currentVersion: '0.0.0-dev',
|
|
||||||
manifestUrl: server.manifestUrl,
|
|
||||||
executablePath: '${work.path}/stray-binary',
|
|
||||||
restart: (_, _) async {});
|
|
||||||
await dev.checkOnce();
|
|
||||||
expect(dev.status.value.phase, UpdatePhase.idle);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('failed install shows error, install untouched, retry works',
|
|
||||||
() async {
|
|
||||||
server.manifestExtra = {'sha256': '0' * 64};
|
|
||||||
final root = await _fakeRoot(work);
|
|
||||||
var restarted = false;
|
|
||||||
final c = UpdateController(
|
|
||||||
currentVersion: '0.1.0',
|
|
||||||
manifestUrl: server.manifestUrl,
|
|
||||||
executablePath: '${root.path}/watch_together',
|
|
||||||
restart: (_, _) async => restarted = true);
|
|
||||||
await c.checkOnce();
|
|
||||||
await c.install();
|
|
||||||
expect(c.status.value.phase, UpdatePhase.failed);
|
|
||||||
expect(c.status.value.error, contains('sha256'));
|
|
||||||
expect(restarted, isFalse);
|
|
||||||
expect(await File('${root.path}/watch_together').readAsString(),
|
|
||||||
'binary-v1');
|
|
||||||
}, skip: _posixOnly);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,11 @@ Future<void> main(List<String> args) async {
|
|||||||
help: 'cookies.txt or browser-extension JSON export to log that yt-dlp in (sites that refuse guests). '
|
help: 'cookies.txt or browser-extension JSON export to log that yt-dlp in (sites that refuse guests). '
|
||||||
'An account login: keep it private. Copied into --yt-dlp-dir at start.')
|
'An account login: keep it private. Copied into --yt-dlp-dir at start.')
|
||||||
..addOption('config', help: 'YAML or JSON config file (keys as in AppConfig); overrides the other flags.')
|
..addOption('config', help: 'YAML or JSON config file (keys as in AppConfig); overrides the other flags.')
|
||||||
|
..addFlag('upgrade',
|
||||||
|
negatable: false,
|
||||||
|
help: 'Update this install (the whole folder, app included) to the latest release, then exit. '
|
||||||
|
'Restart the server afterwards to run it.')
|
||||||
|
..addFlag('check-update', negatable: false, help: 'Print whether a newer release exists, then exit.')
|
||||||
..addFlag('help', abbr: 'h', negatable: false, help: 'Show this help.');
|
..addFlag('help', abbr: 'h', negatable: false, help: 'Show this help.');
|
||||||
final ArgResults parsed;
|
final ArgResults parsed;
|
||||||
try {
|
try {
|
||||||
@@ -41,7 +46,14 @@ Future<void> main(List<String> args) async {
|
|||||||
}
|
}
|
||||||
if (parsed['help'] as bool) {
|
if (parsed['help'] as bool) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('watch-together-server (proto ${RelayConstants.protoVersion})\n\n${parser.usage}');
|
print('watch-together-server $appVersion (proto ${RelayConstants.protoVersion})\n\n${parser.usage}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parsed['upgrade'] as bool || parsed['check-update'] as bool) {
|
||||||
|
exitCode = await runUpgrade(
|
||||||
|
program: 'watch-together-server',
|
||||||
|
checkOnly: !(parsed['upgrade'] as bool),
|
||||||
|
restartNote: (v) => 'updated to $v — restart the server to run $v.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.0"
|
version: "14.3.0"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "6c5bcd986e06b94e3c40244af471750840a3d2341d1f9763a1100a14add517b4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
args:
|
args:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -81,6 +89,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -201,6 +217,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.3"
|
version: "1.5.3"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.2"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -319,7 +343,7 @@ packages:
|
|||||||
path: "../core"
|
path: "../core"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.1.0"
|
version: "1.0.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
# Usage: tool/package_linux.sh [--skip-build] [--noupload|--no-upload]
|
# Usage: tool/package_linux.sh [--skip-build] [--noupload|--no-upload]
|
||||||
# Output: dist/watch-together-<version>-linux-x64.tar.gz containing one
|
# Output: dist/watch-together-<version>-linux-x64.tar.gz containing one
|
||||||
# top-level folder with the executable, lib/ and data/ as built, plus
|
# top-level folder with the executable, lib/ and data/ as built, plus
|
||||||
# update.sh (runs the binary with --upgrade) and watch-together-server (the
|
# update.sh (runs watch-together-server --upgrade) and watch-together-server
|
||||||
# dedicated server, `dart compile exe` of packages/server/bin/server.dart,
|
# (the dedicated server, `dart compile exe` of packages/server/bin/server.dart,
|
||||||
# built every run, also with --skip-build). The in-app updater strips
|
# built every run, also with --skip-build; both binaries are stamped with
|
||||||
# that top-level folder — keep the layout in sync with updater_test.dart.
|
# APP_VERSION from the app's pubspec). The updater strips
|
||||||
|
# that top-level folder — keep the layout in sync with packages/core/test/updater_test.dart.
|
||||||
# Upload: set DISCORD_WEBHOOK_URL in the environment (never commit it).
|
# Upload: set DISCORD_WEBHOOK_URL in the environment (never commit it).
|
||||||
# The tarball must stay under Discord's upload limit (25 MB free tier).
|
# The tarball must stay under Discord's upload limit (25 MB free tier).
|
||||||
# The upload posts a Components V2 card (flag 1<<15): version header,
|
# The upload posts a Components V2 card (flag 1<<15): version header,
|
||||||
@@ -55,7 +56,7 @@ if FLUTTER="$(command -v flutter)"; then
|
|||||||
[[ -x "$CANDIDATE" ]] && DART="$CANDIDATE"
|
[[ -x "$CANDIDATE" ]] && DART="$CANDIDATE"
|
||||||
fi
|
fi
|
||||||
(cd "$ROOT/packages/server" && "$DART" pub get >/dev/null &&
|
(cd "$ROOT/packages/server" && "$DART" pub get >/dev/null &&
|
||||||
"$DART" compile exe bin/server.dart -o "$STAGE/$FOLDER/watch-together-server")
|
"$DART" compile exe --define=APP_VERSION="$VERSION" bin/server.dart -o "$STAGE/$FOLDER/watch-together-server")
|
||||||
tar -czf "$TARBALL" -C "$STAGE" "$FOLDER"
|
tar -czf "$TARBALL" -C "$STAGE" "$FOLDER"
|
||||||
rm -rf "$STAGE"
|
rm -rf "$STAGE"
|
||||||
ls -la "$TARBALL"
|
ls -la "$TARBALL"
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
# Usage: powershell -File tool/package_windows.ps1 [-SkipBuild]
|
# Usage: powershell -File tool/package_windows.ps1 [-SkipBuild]
|
||||||
# Output: dist/watch-together-<version>-windows-x64.zip containing one
|
# Output: dist/watch-together-<version>-windows-x64.zip containing one
|
||||||
# top-level folder with watch_together.exe, its DLLs and data/ as built,
|
# top-level folder with watch_together.exe, its DLLs and data/ as built,
|
||||||
# plus update.bat (runs the exe with --upgrade) and
|
# plus update.bat (runs watch-together-server.exe --upgrade) and
|
||||||
# watch-together-server.exe (dedicated server, `dart compile exe` of
|
# watch-together-server.exe (dedicated server, `dart compile exe` of
|
||||||
# packages/server/bin/server.dart). Same layout as tool/package_linux.sh;
|
# packages/server/bin/server.dart; both binaries stamped with APP_VERSION).
|
||||||
# the in-app updater strips the top-level folder.
|
# Same layout as tool/package_linux.sh; the updater strips the top-level
|
||||||
|
# folder.
|
||||||
# Needs: flutter on PATH, tar.exe (ships with Windows 10+, writes zip).
|
# Needs: flutter on PATH, tar.exe (ships with Windows 10+, writes zip).
|
||||||
param([switch]$SkipBuild)
|
param([switch]$SkipBuild)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -45,7 +46,7 @@ Copy-Item "$App/lib/src/update/bundle/update.bat" "$Stage/$Folder/update.bat"
|
|||||||
Push-Location (Join-Path $Root 'packages/server')
|
Push-Location (Join-Path $Root 'packages/server')
|
||||||
try {
|
try {
|
||||||
Invoke-Checked 'dart pub get' { dart pub get | Out-Null }
|
Invoke-Checked 'dart pub get' { dart pub get | Out-Null }
|
||||||
Invoke-Checked 'dart compile exe' { dart compile exe bin/server.dart -o "$Stage/$Folder/watch-together-server.exe" }
|
Invoke-Checked 'dart compile exe' { dart compile exe "--define=APP_VERSION=$Version" bin/server.dart -o "$Stage/$Folder/watch-together-server.exe" }
|
||||||
} finally { Pop-Location }
|
} finally { Pop-Location }
|
||||||
|
|
||||||
if (Test-Path $Zip) { Remove-Item -Force $Zip }
|
if (Test-Path $Zip) { Remove-Item -Force $Zip }
|
||||||
|
|||||||
Reference in new issue
Block a user