From 4ab37c55cfdcb04a73e6b0778333e93d60816bc0 Mon Sep 17 00:00:00 2001 From: foxyz Date: Mon, 12 Oct 2026 19:05:32 +0300 Subject: [PATCH] Sessions leave redacted logs; viewers wait for a member relay's source before opening it Logs: one redaction pass (query strings to names, member tokens, passwords, cookies, auth headers) for every line; daily files with size and age caps in the app (hosted server included) and the dedicated server (--log-dir); swaps, relay channels, per-member /r /m /j outcomes, server fetches, mpv load errors, yt-dlp runs. Settings > About: Copy diagnostics, Open logs folder. Bilibili through someone else: the viewer opened /r/ before the source dialled in; the server holds that request and mpv gives up after 5 s ("could not be opened"). Viewers now open a member relay once it is live, the putter starts its source before its own player, and track sizes are read in parallel. Co-Authored-By: Claude Opus 5.5 --- README.md | 5 + apps/desktop/lib/main.dart | 2 + apps/desktop/lib/src/platform/app_log.dart | 98 +++++ .../lib/src/playback/playback_controller.dart | 36 +- .../lib/src/screens/home/about_page.dart | 41 +- .../lib/src/session/session_coordinator.dart | 49 ++- .../desktop/lib/src/state/server_isolate.dart | 4 +- apps/desktop/lib/src/state/session_state.dart | 3 +- apps/desktop/test/relay_e2e_test.dart | 41 ++ docs/ARCHITECTURE.md | 77 +++- docs/DEVELOPMENT.md | 21 +- packages/core/lib/src/link_probe.dart | 16 + packages/core/lib/src/logging.dart | 374 +++++++++++++++++- packages/core/lib/src/relay.dart | 40 +- packages/core/lib/src/relay_pieces.dart | 2 + packages/core/lib/src/yt_dlp.dart | 57 ++- packages/core/test/logging_test.dart | 89 +++++ packages/server/bin/server.dart | 12 +- packages/server/lib/src/jellyfin_relay.dart | 5 +- packages/server/lib/src/media_endpoint.dart | 28 +- packages/server/lib/src/queue_ops.dart | 8 + packages/server/lib/src/relay_routes.dart | 131 +++--- packages/server/lib/src/serve.dart | 3 +- packages/server/lib/src/server_hub.dart | 63 ++- packages/server/lib/src/sync_server.dart | 27 +- 25 files changed, 1124 insertions(+), 108 deletions(-) create mode 100644 apps/desktop/lib/src/platform/app_log.dart create mode 100644 packages/core/test/logging_test.dart diff --git a/README.md b/README.md index 54207a9..aafbaa8 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,7 @@ then everyone (you included) uses Join. | `--[no-]yt-dlp` | on | read web pages (YouTube, X, …) with yt-dlp when the server fetches a link | | `--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 | +| `--log-dir` | `logs` next to `--config`, else next to the binary (systemd: the unit's `StateDirectory`) | folder for the log files (`server-YYYY-MM-DD.log`, kept 7 days; passwords, tokens and cookies left out) | | `--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 | @@ -246,6 +247,10 @@ To run it as a service, unpack the tarball to `/opt/watch-together` and use [deploy/watch-together.service](deploy/watch-together.service) (copy it to `/etc/systemd/system/`, edit the flags, `systemctl enable --now watch-together`). +Something not playing? Settings → About → **Copy diagnostics** puts the +app's recent log (passwords, tokens and cookies left out) on the +clipboard: paste it to whoever helps you, from both sides if you can. + ## Join Join in the strip. Servers you got into are listed first (up to 6), with diff --git a/apps/desktop/lib/main.dart b/apps/desktop/lib/main.dart index 90f24e7..5be3e96 100644 --- a/apps/desktop/lib/main.dart +++ b/apps/desktop/lib/main.dart @@ -9,6 +9,7 @@ import 'package:window_manager/window_manager.dart'; import 'app.dart'; import 'src/playback/yt_dlp_app.dart'; +import 'src/platform/app_log.dart'; import 'src/platform/desktop_entry.dart'; import 'src/platform/window_memory.dart'; import 'src/screens/home/ui.dart'; @@ -41,6 +42,7 @@ Future main(List args) async { ), ); WidgetsFlutterBinding.ensureInitialized(); + await startAppLogging(); await Hive.initFlutter(); await AppSettings.instance.ensureLoaded(); await windowManager.ensureInitialized(); diff --git a/apps/desktop/lib/src/platform/app_log.dart b/apps/desktop/lib/src/platform/app_log.dart new file mode 100644 index 0000000..c318a17 --- /dev/null +++ b/apps/desktop/lib/src/platform/app_log.dart @@ -0,0 +1,98 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sync_relay_core/core.dart'; + +import '../playback/yt_dlp_app.dart'; +import '../screens/home/ui.dart' show appName, displayVersion; +import '../state/session_state.dart'; + +// The app's log on disk (/logs/app-YYYY-MM-DD.log; core's +// logging.dart writes, rotates and redacts it), debugPrint routed into it, +// and the "Copy diagnostics" text of Settings → About. + +final _appLog = createLogger('app'); + +/// The logs folder, once [startAppLogging] ran. +String? appLogDir; + +/// Opens the log files and routes `debugPrint` and uncaught errors into +/// the log. Needs Flutter's binding (path_provider). +Future startAppLogging() async { + try { + final base = await getApplicationSupportDirectory(); + final dir = '${base.path}${Platform.pathSeparator}logs'; + if (startLogFiles(dir) != null) appLogDir = dir; + } catch (e) { + _appLog.warning('no log files: $e'); + } + // Everything the app prints for diagnosis lands in the file too (the + // log's console output replaces the print). + debugPrint = (String? message, {int? wrapWidth}) { + if (message == null || message.isEmpty) return; + final bad = RegExp(r'fail|error|crash|exception', caseSensitive: false).hasMatch(message); + bad ? _appLog.warning(message) : _appLog.info(message); + }; + PlatformDispatcher.instance.onError = (error, stack) { + _appLog.severe('uncaught: $error', error, stack); + return false; + }; + _appLog.info('$appName ${displayVersion()} on ${Platform.operatingSystem} ' + '${Platform.operatingSystemVersion}, pid $pid, Dart ${Platform.version.split(' ').first}'); +} + +/// The text "Copy diagnostics" gives: versions, then the last [since] of +/// the app log (its hosted server's records included), already redacted. +Future diagnosticsText({Duration since = const Duration(hours: 6)}) async { + final b = StringBuffer() + ..writeln('$appName ${displayVersion()}') + ..writeln('OS: ${Platform.operatingSystem} ${Platform.operatingSystemVersion}') + ..writeln('Dart: ${Platform.version.split(' ').first}'); + try { + final mpv = await SessionState.instance.coordinator.playback.mpvVersion().timeout(const Duration(seconds: 2)); + b.writeln('mpv: ${mpv ?? 'no player yet'}'); + } catch (_) { + b.writeln('mpv: unknown'); + } + try { + final y = await AppYtDlp.instance.ytDlp.timeout(const Duration(seconds: 2)); + final cookies = y.cookies.read(count: false); + b.writeln('yt-dlp: ${y.version ?? 'not ready'}${y.lastError == null ? '' : ' (${y.lastError})'}, ' + 'cookies: ${cookies == null ? 'none' : 'set'}'); + } catch (_) { + b.writeln('yt-dlp: unknown'); + } + final files = logFiles; + b.writeln('Log: ${files?.currentPath ?? 'none (console only)'}'); + b.writeln('--- last ${since.inHours} h of the log ---'); + b.write(files?.recent(since) ?? ''); + return redactLog(b.toString()); +} + +/// Saves [text] next to the logs as `diagnostics-