Files
sync_replay_project/packages/server/lib/src/serve.dart
T
foxyzandClaude Opus 5.5 7487b4deb1 Protocol 15: one shared session log on the server, chat never logged, Mark a problem
Every app streams its redacted diagnostic lines (server time, batched
every 10 s, gzip, buffered while offline) to POST /l/; the server merges
them with its own into session-YYYY-MM-DD.log in time order, tagged by
who. Opt-out in Settings > About; Mark a problem (Ctrl+Shift+B, the
player menus, About) writes a marker with an optional note. Copy
diagnostics can include the server's session log. Chat text never
reaches any log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:41:06 +03:00

70 lines
2.8 KiB
Dart

import 'dart:async';
import 'dart:isolate';
import 'package:logging/logging.dart' as pkg_logging;
import 'package:sync_relay_core/core.dart';
import 'sync_server.dart';
/// The one server entrypoint: the dedicated binary and the app's hosting
/// isolate both run this. [config] holds primitives only (an
/// [AppConfig.toMap]) so it can cross an isolate boundary.
///
/// On [events] it reports `{'t':'ready','port':p,'stop':SendPort}` or
/// `{'t':'error','message':m}`, and forwards log records as
/// `{'t':'log','level','logger','message'[,'error'][,'stack']}`. Sending
/// `{'t':'stop'}` to the `stop` port stops the server; the returned future
/// completes once it is down. `{'t':'file','path':p,'reply':SendPort}` makes
/// a file on this machine the hosted file (the hosting app's in-session file
/// swap; a following `swap` with its name and size puts it up) and answers
/// `{'ok':true,'name':n,'size':s}` or `{'ok':false}`. Without [events] a
/// start failure throws. `'loopbackOnly': true` in [config] (next to the
/// [AppConfig] keys) binds 127.0.0.1 only: the app's solo session;
/// `'logDir'` is the folder of the session log ([SyncServer.logDir]).
Future<void> serve(Map<String, Object?> config, {SendPort? events}) async {
StreamSubscription<pkg_logging.LogRecord>? logs;
if (events != null) {
logs = pkg_logging.Logger.root.onRecord.listen((r) => events.send({
't': 'log',
'level': r.level.name,
'logger': r.loggerName,
'message': r.message,
if (r.error != null) 'error': '${r.error}',
if (r.stackTrace != null) 'stack': '${r.stackTrace}',
}));
}
final SyncServer server;
try {
final logDir = config['logDir'];
server = SyncServer(AppConfig.fromMap(config),
loopbackOnly: config['loopbackOnly'] == true, logDir: logDir is String && logDir.isNotEmpty ? logDir : null);
await server.start();
} catch (e) {
await logs?.cancel();
if (events == null) rethrow;
events.send({'t': 'error', 'message': '$e'});
return;
}
final control = ReceivePort('serve control');
events?.send({'t': 'ready', 'port': server.boundPort, 'stop': control.sendPort});
await for (final msg in control) {
if (msg is! Map) continue;
if (msg['t'] == 'stop') break;
if (msg['t'] == 'file' && msg['path'] is String && msg['reply'] is SendPort) {
final reply = msg['reply'] as SendPort;
try {
final ok = await server.setLocalFile(msg['path'] as String);
final media = server.session.media;
reply.send(ok && media != null
? {'ok': true, 'name': media.name, 'size': media.size}
: {'ok': false});
} catch (_) {
reply.send({'ok': false});
}
}
}
control.close();
await server.stop();
await logs?.cancel();
}