4 Commits
Author SHA1 Message Date
foxyz 81b9d05de6 Release v1.3.2
ci / updater-windows (push) Skipped
ci / core (push) Successful in 29s
ci / server (push) Successful in 50s
release / linux (push) Successful in 4m26s
release / windows (push) Successful in 6m30s
2026-10-17 02:14:55 +03:00
foxyzandClaude Opus 5.5 f9b79aa47c Server test: flaky_link's silence limit 1.5 s (600 ms marked a reporting client gone on a busy CI runner)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 02:05:44 +03:00
foxyzandClaude Opus 5.5 e16cf50f2c Windows: every start ended itself after 3 s (1.3.0, 1.3.1)
ci / core (push) Successful in 28s
ci / server (push) Successful in 51s
ci / updater-windows (push) Successful in 6m27s
Win32Window::Create calls Destroy() before it makes the window, which
ran FlutterWindow::OnDestroy and armed the 3 s exit timer at launch: the
window showed, then the process ended with code 0, no crash, no close
lines. The timer is only armed with an engine to tear down. Found and
fixed by Fun Maker's agent on Windows (a debugger caught the exit on the
timer's thread).

The smoke runs missed it (they closed at ~4 s anyway): the app now stays
up 10 s, and the scripts require 'closing' and 'quitting' from its own
close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 02:04:55 +03:00
foxyz 8aa1186d29 Revert "Sounds: survive an audio device that crashes the app on open"
This reverts commit c91d57633f.
2026-10-17 02:04:33 +03:00
15 changed files with 45 additions and 113 deletions

No files matched your search

-2
View File
@@ -49,8 +49,6 @@ Future<void> main(List<String> args) async {
await AppSettings.instance.ensureLoaded(); await AppSettings.instance.ensureLoaded();
// The composition root: every system, made once. // The composition root: every system, made once.
final graph = AppGraph.build(); final graph = AppGraph.build();
// Sounds stay off after an audio-device crash (see SoundEngine.crashMark).
graph.sfx.engine.crashMark = '${dirs.support}${Platform.pathSeparator}audio-init.pending';
final ports = graph.ports; final ports = graph.ports;
// Close button / Alt+F4: the orderly close ([shutDownApp]), so others // Close button / Alt+F4: the orderly close ([shutDownApp]), so others
// see "left", your server shuts down and no player outlives the engine. // see "left", your server shuts down and no player outlives the engine.
@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_soloud/flutter_soloud.dart'; import 'package:flutter_soloud/flutter_soloud.dart';
@@ -51,13 +50,6 @@ class SoundEngine {
Future<bool>? _ready; Future<bool>? _ready;
var _disposed = false; var _disposed = false;
/// A file that exists only while the audio device is opening (set by the
/// app). Found at the next start, the last open took the app down (a
/// native crash in the driver or miniaudio, seen on Windows): sounds stay
/// off rather than crash every start, until [retry] (Settings' preview).
String? crashMark;
var _skipped = false;
/// Per slot: the loaded key and its sound (null: failed to load). /// Per slot: the loaded key and its sound (null: failed to load).
final Map<String, (String, Future<Object?>)> _slots = {}; final Map<String, (String, Future<Object?>)> _slots = {};
/// Load names are unique: SoLoud hands back the old sound for a name it /// Load names are unique: SoLoud hands back the old sound for a name it
@@ -69,43 +61,15 @@ class SoundEngine {
Future<bool> _init() async { Future<bool> _init() async {
if (_disposed) return false; if (_disposed) return false;
final mark = crashMark == null ? null : File(crashMark!);
try {
if (mark != null && mark.existsSync()) {
_skipped = true;
_log.warning('no sounds: opening the audio device crashed the app last time '
'(${mark.path}); a sound preview in Settings tries again');
return false;
}
mark?.writeAsStringSync('opening the audio device');
} catch (_) {}
_log.info('opening the audio device');
try { try {
await _backend.init(); await _backend.init();
_log.info('audio device open');
return true; return true;
} catch (e) { } catch (e) {
_log.warning('no sounds: audio init failed: $e'); _log.warning('no sounds: audio init failed: $e');
return false; return false;
} finally {
try {
if (mark != null && mark.existsSync()) mark.deleteSync();
} catch (_) {}
} }
} }
/// Opens the device again after a crash mark kept sounds off (the user
/// asked for a sound). A real crash leaves the mark again.
void retry() {
if (!_skipped) return;
_skipped = false;
try {
final m = crashMark;
if (m != null) File(m).deleteSync();
} catch (_) {}
_ready = null;
}
/// Plays the sound of [slot] at [volume] (linear gain). [key] names what /// Plays the sound of [slot] at [volume] (linear gain). [key] names what
/// [wav] renders: the same key reuses the loaded sound; another key, or /// [wav] renders: the same key reuses the loaded sound; another key, or
/// [fresh], renders and loads it again. /// [fresh], renders and loads it again.
+12 -4
View File
@@ -12,9 +12,14 @@ import 'ports/player_host.dart';
// in a session player with a video output through the media pipeline, // in a session player with a video output through the media pipeline,
// play a second and close the normal way ([shutDownApp]). Progress goes to // play a second and close the normal way ([shutDownApp]). Progress goes to
// `STARSYNC_SMOKE_OUT` (one line per step: `start`, `opened <ms> render=<mode>`, // `STARSYNC_SMOKE_OUT` (one line per step: `start`, `opened <ms> render=<mode>`,
// `closing`, or `skip <why>` / `fail <why>`), so the script can tell a // `closing`, `quitting`, or `skip <why>` / `fail <why>`), so the script can
// runner that cannot show a window (skip) from a close that hangs or // tell a runner that cannot show a window (skip) from a close that hangs or
// leaves a process behind (fail). // leaves a process behind, or a process that ends on its own (fail). It
// stays up [smokeUptime] first: a start that dies by itself (the 1.3.0
// Windows exit timer) ends before `closing`.
/// How long the smoke run keeps the app up before closing it.
const smokeUptime = Duration(seconds: 10);
/// The smoke run's media, or null for a normal start. /// The smoke run's media, or null for a normal start.
String? get smokeMedia { String? get smokeMedia {
@@ -65,6 +70,9 @@ Future<void> runSmoke(AppGraph graph, String media) async {
} catch (e) { } catch (e) {
step('fail $e'); step('fail $e');
} }
final wait = smokeUptime - sw.elapsed;
if (wait > Duration.zero) await Future.delayed(wait);
step('closing'); step('closing');
await shutDownApp(); // Reached only through the app's own close (not a kill on the way).
await shutDownApp(beforeQuit: () async => step('quitting'));
} }
+2 -5
View File
@@ -37,11 +37,8 @@ class SynthSfx implements SfxService {
/// Plays [kind] even when it is switched off (Settings' preview), always /// Plays [kind] even when it is switched off (Settings' preview), always
/// freshly rendered: an edited spec plays after a hot reload. /// freshly rendered: an edited spec plays after a hot reload.
Future<void> preview(SfxKind kind, {int readyIndex = 1, SoundPreset? preset}) { Future<void> preview(SfxKind kind, {int readyIndex = 1, SoundPreset? preset}) =>
// Asked for: try the device again even after a crash kept it closed. _play(kind, readyIndex, preset ?? presetFor(kind), fresh: true);
engine.retry();
return _play(kind, readyIndex, preset ?? presetFor(kind), fresh: true);
}
/// One slot per sound and ready step: a new preset replaces it. /// One slot per sound and ready step: a new preset replaces it.
Future<void> _play(SfxKind kind, int readyIndex, SoundPreset preset, {bool fresh = false}) { Future<void> _play(SfxKind kind, int readyIndex, SoundPreset preset, {bool fresh = false}) {
+1 -1
View File
@@ -1,7 +1,7 @@
name: starsync name: starsync
description: Starsync desktop app (repo sync_relay_project). One binary. description: Starsync desktop app (repo sync_relay_project). One binary.
publish_to: none publish_to: none
version: 1.3.1 version: 1.3.2
environment: environment:
sdk: '>=3.13.0-0 <4.0.0' sdk: '>=3.13.0-0 <4.0.0'
-43
View File
@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@@ -148,46 +147,4 @@ void main() {
expect(b.deinits, 1); expect(b.deinits, 1);
}); });
}); });
group('crash mark', () {
late Directory dir;
setUp(() => dir = Directory.systemTemp.createTempSync('sound_mark'));
tearDown(() => dir.deleteSync(recursive: true));
test('exists only while the device opens', () async {
final mark = '${dir.path}/audio-init.pending';
var seen = false;
final b = _MarkBackend(() => seen = File(mark).existsSync());
final e = SoundEngine(backend: b)..crashMark = mark;
expect(await e.warmUp(), isTrue);
expect(seen, isTrue, reason: 'there while opening');
expect(File(mark).existsSync(), isFalse, reason: 'gone once open');
});
test('left by a crash: sounds stay off, a preview tries again', () async {
final mark = '${dir.path}/audio-init.pending';
File(mark).writeAsStringSync('opening the audio device');
final b = _FakeBackend();
final e = SoundEngine(backend: b)..crashMark = mark;
await e.play('chat', 'k', _bytes, volume: 1);
expect(b.inits, 0, reason: 'never opened after a crash');
expect(b.plays, isEmpty);
e.retry();
await e.play('chat', 'k', _bytes, volume: 1);
expect(b.inits, 1);
expect(b.plays, hasLength(1));
expect(File(mark).existsSync(), isFalse);
});
});
}
class _MarkBackend extends _FakeBackend {
final void Function() onInit;
_MarkBackend(this.onInit);
@override
Future<void> init() async {
onInit();
await super.init();
}
} }
@@ -80,7 +80,11 @@ bool FlutterWindow::OnCreate() {
void FlutterWindow::OnDestroy() { void FlutterWindow::OnDestroy() {
// The engine and the plugins go now; the app released its players first. // The engine and the plugins go now; the app released its players first.
ExitAfter(3000); // Win32Window::Create runs this too, before there is an engine: only a
// real teardown may end the process (else every start dies at 3 s).
if (flutter_controller_) {
ExitAfter(3000);
}
window_channel_ = nullptr; window_channel_ = nullptr;
if (flutter_controller_) { if (flutter_controller_) {
flutter_controller_ = nullptr; flutter_controller_ = nullptr;
+4 -7
View File
@@ -1946,12 +1946,7 @@ panel) and chat messages (Copy message, and a link in it).
switched off in `soundsOff`. The device opens 2 s after startup switched off in `soundsOff`. The device opens 2 s after startup
(`warmUp`), so neither startup nor the first sound waits on it; if it (`warmUp`), so neither startup nor the first sound waits on it; if it
can't open, the engine logs `no sounds: audio init failed` once and drops can't open, the engine logs `no sounds: audio init failed` once and drops
every play. Opening it is logged (`opening the audio device`, every play. App close disposes the engine (deinit, 500 ms bound)
`audio device open`) and leaves `<support>/audio-init.pending` until it
returns: a native crash there (a driver, miniaudio) leaves the file, and
the next start skips sounds (`no sounds: opening the audio device
crashed the app last time`) instead of crashing again; a sound preview
in Settings deletes it and tries once more. App close disposes the engine (deinit, 500 ms bound)
before leaving the server. Settings → Notifications & sounds has the volume, the before leaving the server. Settings → Notifications & sounds has the volume, the
overall sound set (`soundPreset`), and per sound a preview, its own set overall sound set (`soundPreset`), and per sound a preview, its own set
(`soundPresetFor`, else the overall one) and a switch; a preview always (`soundPresetFor`, else the overall one) and a switch; a preview always
@@ -2298,7 +2293,9 @@ app kills itself (`Process.killPid`, no exit handlers; it only gets there
if the close did not go through); the runners end the process 15 s after if the close did not go through); the runners end the process 15 s after
the first close request (Linux: a `delete-event` handler connected before the first close request (Linux: a `delete-event` handler connected before
window_manager's; Windows: the first `WM_CLOSE`), and Windows also 3 s window_manager's; Windows: the first `WM_CLOSE`), and Windows also 3 s
after the window is destroyed, while the engine and plugins go. after the window is destroyed, while the engine and plugins go (only
with an engine: `Win32Window::Create` runs `OnDestroy` before the window
exists, and a timer armed there ended every start at 3 s).
## Logging ## Logging
+6 -4
View File
@@ -210,10 +210,12 @@ updater tests, the release build and the close smoke run on that build
`STARSYNC_SMOKE=<clip>` (a 5 s clip from ffmpeg, else a WAV the script `STARSYNC_SMOKE=<clip>` (a 5 s clip from ffmpeg, else a WAV the script
writes) under its own profile (`STARSYNC_PROFILE=smoke`), opens the clip writes) under its own profile (`STARSYNC_PROFILE=smoke`), opens the clip
in a player with a video output through the media pipeline in a player with a video output through the media pipeline
(`platform/smoke.dart`), plays a second and closes the normal way; the (`platform/smoke.dart`), plays it, stays up 10 s from its start and
steps go to `STARSYNC_SMOKE_OUT` (`start`, `opened <ms> render=<mode>`, closes the normal way; the steps go to `STARSYNC_SMOKE_OUT` (`start`,
`closing`, or `skip` / `fail` with why). It fails when the app does not `opened <ms> render=<mode>`, `closing`, `quitting` just before the
exit within 45 s or leaves a `starsync` process, skips with a warning window closes, or `skip` / `fail` with why). It fails when the app ends
by itself before `closing` or never reaches `quitting`, does not exit
within 45 s or leaves a `starsync` process, skips with a warning
when the runner cannot bring the app or its video output up, and prints when the runner cannot bring the app or its video output up, and prints
the `smoke:`, `close:` and `mpv: render` lines of the app log. the `smoke:`, `close:` and `mpv: render` lines of the app log.
`tool/smoke_linux.sh` is the same on Linux (needs a display; after `tool/smoke_linux.sh` is the same on Linux (needs a display; after
+1 -1
View File
@@ -1,6 +1,6 @@
name: sync_relay_core name: sync_relay_core
description: Pure-Dart protocol, sync engine, media-source abstractions. No Flutter. description: Pure-Dart protocol, sync engine, media-source abstractions. No Flutter.
version: 1.3.1 version: 1.3.2
environment: environment:
sdk: ^3.0.0 sdk: ^3.0.0
dependencies: dependencies:
+1 -1
View File
@@ -1,7 +1,7 @@
name: sync_relay_server name: sync_relay_server
description: Pure-Dart sync + media server. Embeddable, no Flutter. description: Pure-Dart sync + media server. Embeddable, no Flutter.
publish_to: none publish_to: none
version: 1.3.1 version: 1.3.2
environment: environment:
sdk: ^3.0.0 sdk: ^3.0.0
dependencies: dependencies:
+4 -3
View File
@@ -72,9 +72,10 @@ void main() {
final clients = <_Client>[]; final clients = <_Client>[];
setUp(() async { setUp(() async {
// Short limits so the demo runs in a couple of seconds. // Short limits so the demo runs in a few seconds; silence well above
// a busy CI runner's stalls (600 ms marked a reporting client gone).
server = SyncServer(AppConfig(port: 0, secret: 'shh', name: 'test'), server = SyncServer(AppConfig(port: 0, secret: 'shh', name: 'test'),
liveness: const Liveness(silence: Duration(milliseconds: 600))); liveness: const Liveness(silence: Duration(milliseconds: 1500)));
await server.start(); await server.start();
proxy = await FlakyProxy.start(server.boundPort); proxy = await FlakyProxy.start(server.boundPort);
}); });
@@ -106,7 +107,7 @@ void main() {
.toJson()); .toJson());
c.send(const SwapIntent(media: HelloMedia(name: 'mine.mkv', size: 100, duration: 0, relay: MediaRelay())).toJson()); c.send(const SwapIntent(media: HelloMedia(name: 'mine.mkv', size: 100, duration: 0, relay: MediaRelay())).toJson());
await until(() => engine.queue.length == 1 && server.hub.relay != null && member()!.ready, 'ready, queued, relayed'); await until(() => engine.queue.length == 1 && server.hub.relay != null && member()!.ready, 'ready, queued, relayed');
await Future<void>.delayed(const Duration(milliseconds: 1200)); await Future<void>.delayed(const Duration(milliseconds: 2000));
expect(member()!.presence, isNot(Presence.gone), reason: 'reports through the proxy keep it present'); expect(member()!.presence, isNot(Presence.gone), reason: 'reports through the proxy keep it present');
proxy.last!.blackhole(); proxy.last!.blackhole();
+2
View File
@@ -34,6 +34,8 @@ grep -qx start "$OUT" 2>/dev/null || { [[ $exited == 1 ]] && { echo "skipped: th
grep -q '^skip ' "$OUT" && { echo "skipped: $(grep '^skip ' "$OUT")"; exit 0; } grep -q '^skip ' "$OUT" && { echo "skipped: $(grep '^skip ' "$OUT")"; exit 0; }
grep -q '^fail ' "$OUT" && { echo "smoke failed: $(grep '^fail ' "$OUT")" >&2; exit 1; } grep -q '^fail ' "$OUT" && { echo "smoke failed: $(grep '^fail ' "$OUT")" >&2; exit 1; }
grep -q '^opened ' "$OUT" || { echo "the media never opened" >&2; exit 1; } grep -q '^opened ' "$OUT" || { echo "the media never opened" >&2; exit 1; }
grep -qx closing "$OUT" || { echo "the app ended on its own before closing" >&2; exit 1; }
grep -qx quitting "$OUT" || { echo "the close did not reach its end (killed on the way?)" >&2; exit 1; }
[[ $exited == 1 ]] || { echo "the app did not close within $LIMIT s (P32)" >&2; exit 1; } [[ $exited == 1 ]] || { echo "the app did not close within $LIMIT s (P32)" >&2; exit 1; }
[[ -z "$left" ]] || { echo "a starsync process was left behind (P32)" >&2; exit 1; } [[ -z "$left" ]] || { echo "a starsync process was left behind (P32)" >&2; exit 1; }
echo "smoke ok: opened and closed in $secs s, no process left" echo "smoke ok: opened and closed in $secs s, no process left"
+2
View File
@@ -64,6 +64,8 @@ if (-not ($lines -contains 'start')) {
if ($lines | Where-Object { $_ -like 'skip *' }) { Write-Warning "skipped: $($lines -like 'skip *')"; exit 0 } if ($lines | Where-Object { $_ -like 'skip *' }) { Write-Warning "skipped: $($lines -like 'skip *')"; exit 0 }
if ($lines | Where-Object { $_ -like 'fail *' }) { Write-Error "smoke failed: $($lines -like 'fail *')"; exit 1 } if ($lines | Where-Object { $_ -like 'fail *' }) { Write-Error "smoke failed: $($lines -like 'fail *')"; exit 1 }
if (-not ($lines | Where-Object { $_ -like 'opened *' })) { Write-Error 'the media never opened'; exit 1 } if (-not ($lines | Where-Object { $_ -like 'opened *' })) { Write-Error 'the media never opened'; exit 1 }
if (-not ($lines -contains 'closing')) { Write-Error "the app ended on its own before closing (exit $($p.ExitCode))"; exit 1 }
if (-not ($lines -contains 'quitting')) { Write-Error 'the close did not reach its end (killed on the way?)'; exit 1 }
if (-not $exited) { Write-Error "the app did not close within $Limit s (P32)"; exit 1 } if (-not $exited) { Write-Error "the app did not close within $Limit s (P32)"; exit 1 }
if ($left) { Write-Error "a starsync process was left behind (P32)"; exit 1 } if ($left) { Write-Error "a starsync process was left behind (P32)"; exit 1 }
Write-Host "smoke ok: opened and closed in $secs s, no process left" Write-Host "smoke ok: opened and closed in $secs s, no process left"
+5 -5
View File
@@ -1,10 +1,10 @@
{ {
"version": "1.3.1", "version": "1.3.2",
"protocol": 16, "protocol": 16,
"url": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.1/starsync-1.3.1-linux-x64.tar.gz", "url": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.2/starsync-1.3.2-linux-x64.tar.gz",
"notes": "Starsync 1.3.1: fixes a crash on start on some Windows PCs (the audio device; the app now starts without sounds instead), joining another server no longer opens an empty player, a cleaner mini player and a softer Minimal progress line.", "notes": "Starsync 1.3.2: fixes Windows closing by itself about 3 seconds after every start (1.3.0 and 1.3.1).",
"assets": { "assets": {
"linux-x64": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.1/starsync-1.3.1-linux-x64.tar.gz", "linux-x64": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.2/starsync-1.3.2-linux-x64.tar.gz",
"windows-x64": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.1/starsync-1.3.1-windows-x64.zip" "windows-x64": "https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/v1.3.2/starsync-1.3.2-windows-x64.zip"
} }
} }