forked from Foxyz/sync_replay_project
Restructure step R: the app is Starsync (dev.foxyz.starsync)
- Linux app id dev.foxyz.starsync, binary starsync; Windows starsync.exe, company foxyz; server starsync-server; Dart package starsync; archives starsync-<ver>-*. - 1.2.x compatibility: watch_together and watch-together-server launchers (Linux) / copies (Windows) in the archive; the updater accepts both names and relaunches the new binary; update.sh/.bat use whichever server exists. - First start copies (never moves) the old support folder and this app's Hive boxes out of ~/Documents into <support>/hive; a marker makes it run once; AppDirs is a port; settings schema version with ordered migrations. - Launcher entry dev.foxyz.starsync.desktop; the old one removed. - Repository name and URLs unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4ca02c3cf
commit
51c1c5ab78
96 files changed
+1169
-489
No files matched your search
@@ -142,7 +142,7 @@ typedef DiagTarget = ({Uri base, String token, double offsetMs});
|
||||
/// saying so per connection; problem marks ([markProblem]) go either way.
|
||||
class DiagUploader {
|
||||
final DiagTarget? Function() target;
|
||||
/// The app and its version, e.g. `Watch Together 1.4.0 on linux`.
|
||||
/// The app and its version, e.g. `Starsync 1.4.0 on linux`.
|
||||
final String version;
|
||||
final int bufferBytes;
|
||||
final int batchBytes;
|
||||
|
||||
@@ -7,9 +7,9 @@ import 'package:crypto/crypto.dart';
|
||||
import 'constants.dart';
|
||||
import 'logging.dart';
|
||||
|
||||
// The one updater for both programs: the app (`watch_together --upgrade`
|
||||
// The one updater for both programs: the app (`starsync --upgrade`
|
||||
// and the in-app update card, via UpdateController) and the dedicated
|
||||
// server (`watch-together-server --upgrade`). Both ship in the same
|
||||
// server (`starsync-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.
|
||||
//
|
||||
@@ -39,6 +39,41 @@ const appVersion = String.fromEnvironment('APP_VERSION',
|
||||
// Key into the manifest's `assets` map, e.g. linux-x64, windows-x64.
|
||||
final updatePlatform = '${Platform.operatingSystem}-x64';
|
||||
|
||||
/// The app's binary in a release bundle (`.exe` on Windows).
|
||||
const appBinaryName = 'starsync';
|
||||
/// The dedicated server's binary next to it (`.exe` on Windows).
|
||||
const serverBinaryName = 'starsync-server';
|
||||
/// The names up to 1.2.x. The archive still carries launchers by these
|
||||
/// names (tool/package_linux.sh, package_windows.ps1): a 1.2.x updater
|
||||
/// checks that its own name is in the archive and restarts it, and VPS
|
||||
/// units and `update.sh` copies run the old server name.
|
||||
const legacyAppBinaryName = 'watch_together';
|
||||
const legacyServerBinaryName = 'watch-together-server';
|
||||
|
||||
// For an old binary name, the new one (same extension); else null.
|
||||
String? _renamedBinary(String name) {
|
||||
final exe = name.toLowerCase().endsWith('.exe');
|
||||
final stem = exe ? name.substring(0, name.length - 4) : name;
|
||||
final renamed = switch (stem) {
|
||||
legacyAppBinaryName => appBinaryName,
|
||||
legacyServerBinaryName => serverBinaryName,
|
||||
_ => null,
|
||||
};
|
||||
return renamed == null ? null : '$renamed${exe ? name.substring(name.length - 4) : ''}';
|
||||
}
|
||||
|
||||
/// The binary to start after an update replaced [executablePath]: the
|
||||
/// same one, except that an old name (a launcher, or on Windows a copy
|
||||
/// of the new binary) gives way to the new binary next to it.
|
||||
String relaunchExecutable(String executablePath) {
|
||||
final norm = executablePath.replaceAll('\\', '/');
|
||||
final cut = norm.lastIndexOf('/');
|
||||
final renamed = _renamedBinary(norm.substring(cut + 1));
|
||||
if (renamed == null) return executablePath;
|
||||
final next = '${executablePath.substring(0, cut + 1)}$renamed';
|
||||
return File(next).existsSync() ? next : executablePath;
|
||||
}
|
||||
|
||||
const _stagingDir = '.update-staging';
|
||||
const _backupDir = '.update-backup';
|
||||
const _downloadFile = '.update-download';
|
||||
@@ -303,7 +338,7 @@ List<String> _safeSegments(String name) {
|
||||
}
|
||||
|
||||
// Release archives wrap the bundle in one top-level folder
|
||||
// (watch-together-<ver>/, see tool/package_linux.sh). Strip it; a flat
|
||||
// (starsync-<ver>/, see tool/package_linux.sh). Strip it; a flat
|
||||
// archive (data/, lib/ and the binary at the top) is used as is.
|
||||
int _wrapperDepth(List<List<String>> entries) {
|
||||
final tops = entries.where((s) => s.isNotEmpty).map((s) => s.first).toSet();
|
||||
@@ -414,7 +449,7 @@ Future<void> applyUpdate(File archive, Directory root,
|
||||
await _extract(await archive.readAsBytes(), stage, !win);
|
||||
if (!_isBundle(stage.path, win) ||
|
||||
(executableName != null &&
|
||||
!await File('${stage.path}/$executableName').exists())) {
|
||||
!await _hasBinary(stage.path, executableName))) {
|
||||
throw const FormatException(
|
||||
'archive is not a release bundle (missing data/, lib/ or binary)');
|
||||
}
|
||||
@@ -428,6 +463,14 @@ Future<void> applyUpdate(File archive, Directory root,
|
||||
}
|
||||
}
|
||||
|
||||
// The running binary's name, or for an old name its new one (an archive
|
||||
// without the old launchers still updates an install started by one).
|
||||
Future<bool> _hasBinary(String dir, String name) async {
|
||||
if (await File('$dir/$name').exists()) return true;
|
||||
final renamed = _renamedBinary(name);
|
||||
return renamed != null && await File('$dir/$renamed').exists();
|
||||
}
|
||||
|
||||
class _Moved {
|
||||
final String name;
|
||||
final bool hadOld;
|
||||
@@ -565,7 +608,7 @@ Future<int> runUpgrade(
|
||||
HttpClient? http,
|
||||
String? executablePath,
|
||||
Directory? installRoot,
|
||||
String program = 'watch-together',
|
||||
String program = 'starsync',
|
||||
String Function(String version)? restartNote,
|
||||
bool checkOnly = false}) async {
|
||||
stdout.writeln('$program $currentVersion (proto ${RelayConstants.protoVersion})');
|
||||
|
||||
@@ -24,34 +24,48 @@ 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
|
||||
// bundle copied into <stage>/starsync-<ver>/, then
|
||||
// `tar -czf out -C <stage> starsync-<ver>`. [zip]: like
|
||||
// tool/package_windows.ps1 (`tar -a -c -f out.zip`, bsdtar — Windows only).
|
||||
// [legacy]: with the launchers by the names up to 1.2.x (Linux: the shell
|
||||
// script of apps/desktop/lib/src/update/bundle/watch_together; Windows: a
|
||||
// copy of the exe). [app]: the app binary's content (Linux).
|
||||
Future<List<int>> _releaseTarball(Directory work,
|
||||
{String version = '0.2.0',
|
||||
bool windows = false,
|
||||
bool bundle = true,
|
||||
bool zip = false}) async {
|
||||
bool zip = false,
|
||||
bool legacy = true,
|
||||
String app = 'binary-v2'}) async {
|
||||
final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}');
|
||||
final folder = Directory('${stage.path}/watch-together-$version');
|
||||
final folder = Directory('${stage.path}/starsync-$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}/starsync.exe').writeAsString('binary-v2');
|
||||
if (legacy) {
|
||||
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');
|
||||
await File('${folder.path}/starsync').writeAsString(app);
|
||||
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');
|
||||
.writeAsString('#!/bin/sh\nexec ./starsync-server --upgrade\n');
|
||||
final exec = ['starsync', 'update.sh'];
|
||||
if (legacy) {
|
||||
await File('${folder.path}/watch_together').writeAsString(
|
||||
'#!/bin/sh\nexec "\$(dirname "\$(readlink -f "\$0")")/starsync" "\$@"\n');
|
||||
exec.add('watch_together');
|
||||
}
|
||||
if (!Platform.isWindows) {
|
||||
final r = await Process.run('chmod',
|
||||
['755', '${folder.path}/watch_together', '${folder.path}/update.sh']);
|
||||
final r = await Process.run(
|
||||
'chmod', ['755', for (final f in exec) '${folder.path}/$f']);
|
||||
expect(r.exitCode, 0);
|
||||
}
|
||||
}
|
||||
@@ -61,12 +75,13 @@ Future<List<int>> _releaseTarball(Directory work,
|
||||
out.path,
|
||||
'-C',
|
||||
stage.path,
|
||||
'watch-together-$version',
|
||||
'starsync-$version',
|
||||
]);
|
||||
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||
return out.readAsBytes();
|
||||
}
|
||||
|
||||
// An install as 1.2.x left it: the binary is still called watch_together.
|
||||
Future<Directory> _fakeRoot(Directory work, {bool windows = false}) async {
|
||||
final root = Directory('${work.path}/install');
|
||||
await Directory('${root.path}/data').create(recursive: true);
|
||||
@@ -111,12 +126,12 @@ class _Server {
|
||||
req.response.write(jsonEncode({
|
||||
'version': '0.2.0',
|
||||
'protocol': RelayConstants.protoVersion,
|
||||
'url': '$base/watch-together-0.2.0-linux-x64.tar.gz',
|
||||
'url': '$base/starsync-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') {
|
||||
} else if (req.uri.path == '/starsync-0.2.0-linux-x64.tar.gz') {
|
||||
tarHits++;
|
||||
req.response.contentLength = tarball.length;
|
||||
req.response.add(tarball);
|
||||
@@ -262,11 +277,12 @@ void main() {
|
||||
expect(rc, 0);
|
||||
expect(server.tarHits, 1);
|
||||
final p = root.path;
|
||||
expect(await File('$p/watch_together').readAsString(), 'binary-v2');
|
||||
expect(await File('$p/starsync').readAsString(), 'binary-v2');
|
||||
expect(await File('$p/data/flutter_assets/fresh.cfg').readAsString(),
|
||||
'fresh-config');
|
||||
expect(Directory('$p/watch-together-0.2.0').existsSync(), isFalse);
|
||||
expect(_isExec('$p/watch_together'), isTrue);
|
||||
expect(Directory('$p/starsync-0.2.0').existsSync(), isFalse);
|
||||
expect(_isExec('$p/starsync'), isTrue);
|
||||
expect(_isExec('$p/watch_together'), isTrue, reason: 'the old name, a launcher');
|
||||
expect(_isExec('$p/update.sh'), isTrue);
|
||||
expect(await Link('$p/lib/libapp.so').target(), 'libapp.so.1');
|
||||
expect(await File('$p/lib/libapp.so').readAsString(), 'lib-v2');
|
||||
@@ -310,7 +326,7 @@ void main() {
|
||||
manifestUrl: server.manifestUrl,
|
||||
installRoot: root);
|
||||
expect(rc, 0);
|
||||
expect(await File('${root.path}/watch_together').readAsString(),
|
||||
expect(await File('${root.path}/starsync').readAsString(),
|
||||
'binary-v2');
|
||||
}, skip: _posixOnly);
|
||||
|
||||
@@ -358,6 +374,7 @@ void main() {
|
||||
expect(await File('$p/lib/libgone.so').readAsString(), 'lib-v1');
|
||||
expect(await File('$p/data/old.cfg').readAsString(), 'old-config');
|
||||
expect(File('$p/update.sh').existsSync(), isFalse);
|
||||
expect(File('$p/starsync').existsSync(), isFalse);
|
||||
expect(_leftovers(root), isEmpty);
|
||||
}
|
||||
|
||||
@@ -367,6 +384,7 @@ void main() {
|
||||
'aside:data',
|
||||
'in:data',
|
||||
'aside:lib',
|
||||
'in:starsync',
|
||||
'in:update.sh',
|
||||
'aside:watch_together',
|
||||
'in:watch_together',
|
||||
@@ -423,12 +441,66 @@ void main() {
|
||||
expect(File('${root.path}/data/thing.dll.old').existsSync(), isFalse);
|
||||
});
|
||||
|
||||
// A 1.2.x updater checks its own name is in the archive, applies, then
|
||||
// restarts that name: the launcher must start the new binary.
|
||||
test('1.2.x install: the old name is in the archive and starts the new binary',
|
||||
() async {
|
||||
final root = await _fakeRoot(work);
|
||||
final f = File('${work.path}/script.tar.gz')
|
||||
..writeAsBytesSync(await _releaseTarball(work,
|
||||
app: '#!/bin/sh\necho "new binary: \$*"\n'));
|
||||
await applyUpdate(f, root,
|
||||
windows: false, executableName: legacyAppBinaryName);
|
||||
final r = await Process.run('${root.path}/watch_together', ['--x']);
|
||||
expect(r.exitCode, 0, reason: '${r.stderr}');
|
||||
expect('${r.stdout}'.trim(), 'new binary: --x');
|
||||
// This build's own restart goes to the new binary directly.
|
||||
expect(relaunchExecutable('${root.path}/watch_together'),
|
||||
'${root.path}/starsync');
|
||||
}, skip: _posixOnly);
|
||||
|
||||
test('started by an old name, an archive without it still applies',
|
||||
() async {
|
||||
final root = await _fakeRoot(work);
|
||||
final f = File('${work.path}/plain.tar.gz')
|
||||
..writeAsBytesSync(await _releaseTarball(work, legacy: false));
|
||||
await applyUpdate(f, root,
|
||||
windows: false, executableName: legacyAppBinaryName);
|
||||
expect(await File('${root.path}/starsync').readAsString(), 'binary-v2');
|
||||
// Unknown names still need their binary in the archive.
|
||||
final again = File('${work.path}/plain2.tar.gz')
|
||||
..writeAsBytesSync(await _releaseTarball(work, version: '0.3.0', legacy: false));
|
||||
await expectLater(
|
||||
applyUpdate(again, root, windows: false, executableName: 'other'),
|
||||
throwsFormatException);
|
||||
});
|
||||
|
||||
test('relaunchExecutable maps old names to the new binary next to them',
|
||||
() async {
|
||||
final dir = (await work.createTemp('r')).path;
|
||||
for (final n in ['starsync', 'starsync.exe', 'starsync-server']) {
|
||||
await File('$dir/$n').writeAsString('x');
|
||||
}
|
||||
expect(relaunchExecutable('$dir/watch_together'), '$dir/starsync');
|
||||
expect(relaunchExecutable('$dir/watch_together.exe'), '$dir/starsync.exe');
|
||||
expect(relaunchExecutable('$dir/watch-together-server'),
|
||||
'$dir/starsync-server');
|
||||
expect(relaunchExecutable('$dir/starsync'), '$dir/starsync');
|
||||
expect(relaunchExecutable('$dir/other'), '$dir/other');
|
||||
// No new binary next to it: the old name stays.
|
||||
expect(relaunchExecutable('${work.path}/watch_together'),
|
||||
'${work.path}/watch_together');
|
||||
expect(relaunchExecutable(r'C:\x\watch_together.exe'),
|
||||
r'C:\x\watch_together.exe');
|
||||
});
|
||||
|
||||
// Windows strategy exercised on Linux: proves the bookkeeping, not the
|
||||
// Windows file-locking semantics (no Windows runner yet).
|
||||
test('windows-style swap applies and parks old files', () async {
|
||||
final root = await _fakeRoot(work, windows: true);
|
||||
await applyUpdate(await archive(windows: true), root, windows: true);
|
||||
final p = root.path;
|
||||
expect(await File('$p/starsync.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/data/flutter_assets/fresh.cfg').readAsString(),
|
||||
@@ -452,6 +524,7 @@ void main() {
|
||||
expect(await File('$p/watch_together.exe').readAsString(), 'binary-v1');
|
||||
expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v1');
|
||||
expect(File('$p/data/flutter_assets/fresh.cfg').existsSync(), isFalse);
|
||||
expect(File('$p/starsync.exe').existsSync(), isFalse);
|
||||
expect(File('$p/flutter_windows.dll.old').existsSync(), isFalse);
|
||||
expect(_leftovers(root), isEmpty);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,8 @@ import 'package:sync_relay_core/core.dart';
|
||||
import 'package:sync_relay_server/server.dart';
|
||||
|
||||
/// Dedicated server: `dart compile exe` of this file is shipped as
|
||||
/// `watch-together-server`. Runs [serve] until SIGINT or SIGTERM.
|
||||
/// `starsync-server` (also started by the `watch-together-server`
|
||||
/// launcher). Runs [serve] until SIGINT or SIGTERM.
|
||||
Future<void> main(List<String> args) async {
|
||||
final parser = ArgParser()
|
||||
..addOption('port', abbr: 'p', defaultsTo: '8080', help: 'TCP port to listen on (all IPv4 interfaces).')
|
||||
@@ -50,12 +51,12 @@ Future<void> main(List<String> args) async {
|
||||
}
|
||||
if (parsed['help'] as bool) {
|
||||
// ignore: avoid_print
|
||||
print('watch-together-server $appVersion (proto ${RelayConstants.protoVersion})\n\n${parser.usage}');
|
||||
print('starsync-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',
|
||||
program: 'starsync-server',
|
||||
checkOnly: !(parsed['upgrade'] as bool),
|
||||
restartNote: (v) => 'updated to $v — restart the server to run $v.');
|
||||
return;
|
||||
@@ -67,7 +68,7 @@ Future<void> main(List<String> args) async {
|
||||
(configPath != null ? '${File(configPath).absolute.parent.path}${Platform.pathSeparator}logs' : serverDataDir('logs'));
|
||||
final files = startLogFiles(logDir, prefix: 'server');
|
||||
if (files != null) stdout.writeln('logs: ${files.dir}');
|
||||
pkg_logging.Logger('server').info('watch-together-server $appVersion (proto ${RelayConstants.protoVersion}) '
|
||||
pkg_logging.Logger('server').info('starsync-server $appVersion (proto ${RelayConstants.protoVersion}) '
|
||||
'on ${Platform.operatingSystem} ${Platform.operatingSystemVersion}, pid $pid');
|
||||
AppConfig config;
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user