major bug fixes
This commit is contained in:
parent
9728906556
commit
d6144c4483
39 changed files with 483 additions and 53 deletions
|
|
@ -116,6 +116,11 @@ class RemoteHost {
|
|||
// ---- Handshake + per-client loop --------------------------------------
|
||||
|
||||
void _handleSocket(WebSocket socket) {
|
||||
// Keepalive: ping idle clients so a controller that vanished without a
|
||||
// close frame (crashed, walked out of Wi-Fi range) is detected and pruned
|
||||
// instead of lingering as a zombie in `_clients`.
|
||||
socket.pingInterval = const Duration(seconds: 15);
|
||||
|
||||
final nonce = RemoteAuth.newNonce();
|
||||
var authed = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,38 @@ class RemoteSession {
|
|||
StreamController<RemoteConnStatus>.broadcast();
|
||||
RemoteConnStatus _current = RemoteConnStatus.disconnected;
|
||||
|
||||
/// True once [close] (or a hard denial / cold-connect give-up) has run —
|
||||
/// stops all further reconnection.
|
||||
bool _closed = false;
|
||||
|
||||
/// A dial is in flight; guards against overlapping [_dial] calls (e.g. a
|
||||
/// backoff timer firing while [reconnectNow] also dials).
|
||||
bool _dialing = false;
|
||||
|
||||
/// True once we've completed a handshake at least once. Distinguishes a
|
||||
/// device that never answers (stale in the list — give up) from an
|
||||
/// established session that later dropped (lock / Wi-Fi blip — persist).
|
||||
bool _everConnected = false;
|
||||
|
||||
/// Consecutive failed dials since the last successful connection; indexes
|
||||
/// [_kBackoff].
|
||||
int _attempt = 0;
|
||||
Timer? _retryTimer;
|
||||
|
||||
static const Duration _kDialTimeout = Duration(seconds: 8);
|
||||
|
||||
/// Keepalive interval. Periodic pings hold the socket open through brief
|
||||
/// idle/doze windows and surface a dead peer promptly instead of leaving a
|
||||
/// silently half-open connection.
|
||||
static const Duration _kPingInterval = Duration(seconds: 5);
|
||||
|
||||
/// Reconnect backoff in seconds, held at the last value once reached.
|
||||
static const List<int> _kBackoff = [0, 1, 2, 4, 8, 15];
|
||||
|
||||
/// How many times to retry before the *first* successful handshake before
|
||||
/// giving up (a device that never answers is probably gone).
|
||||
static const int _kMaxColdAttempts = 3;
|
||||
|
||||
/// Host-reported friendly name, available after [RemoteConnStatus.connected].
|
||||
String? hostDevice;
|
||||
|
||||
|
|
@ -56,28 +88,89 @@ class RemoteSession {
|
|||
RemoteConnStatus get currentStatus => _current;
|
||||
|
||||
/// Dial the host and start the handshake. Status transitions are emitted on
|
||||
/// [status]; on success snapshots begin arriving on [snapshots].
|
||||
/// [status]; on success snapshots begin arriving on [snapshots]. The session
|
||||
/// then self-heals: a dropped socket is retried with backoff (see
|
||||
/// [_scheduleRetry]) rather than surfacing a terminal status, so control
|
||||
/// persists across a phone lock or Wi-Fi blip (bug-fixes #5).
|
||||
Future<void> connect() async {
|
||||
_closed = false;
|
||||
_attempt = 0;
|
||||
await _dial();
|
||||
}
|
||||
|
||||
Future<void> _dial() async {
|
||||
if (_closed || _dialing) return;
|
||||
_dialing = true;
|
||||
_set(RemoteConnStatus.connecting);
|
||||
try {
|
||||
final ws = await WebSocket.connect(device.wsUri.toString())
|
||||
.timeout(const Duration(seconds: 8));
|
||||
.timeout(_kDialTimeout);
|
||||
if (_closed) {
|
||||
unawaited(ws.close().catchError((_) {}));
|
||||
return;
|
||||
}
|
||||
ws.pingInterval = _kPingInterval;
|
||||
_socket = ws;
|
||||
_sub = ws.listen(
|
||||
_onData,
|
||||
onDone: () => _set(RemoteConnStatus.disconnected),
|
||||
onError: (_) => _set(RemoteConnStatus.error),
|
||||
onDone: _onDropped,
|
||||
onError: (_) => _onDropped(),
|
||||
cancelOnError: true,
|
||||
);
|
||||
} catch (_) {
|
||||
_set(RemoteConnStatus.error);
|
||||
_onDropped();
|
||||
} finally {
|
||||
_dialing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// The socket went away (drop, dial failure, or refused). Unless we've been
|
||||
/// [close]d, keep the session logically alive and retry.
|
||||
void _onDropped() {
|
||||
_sub = null;
|
||||
_socket = null;
|
||||
if (_closed) return;
|
||||
_scheduleRetry();
|
||||
}
|
||||
|
||||
void _scheduleRetry() {
|
||||
// Give up only if we never got a handshake in the first place — an
|
||||
// established session that dropped is retried indefinitely.
|
||||
if (!_everConnected && _attempt >= _kMaxColdAttempts) {
|
||||
_closed = true;
|
||||
_set(RemoteConnStatus.error);
|
||||
return;
|
||||
}
|
||||
_retryTimer?.cancel();
|
||||
final i = _attempt < _kBackoff.length ? _attempt : _kBackoff.length - 1;
|
||||
_attempt++;
|
||||
_set(RemoteConnStatus.connecting);
|
||||
_retryTimer = Timer(Duration(seconds: _kBackoff[i]), () {
|
||||
_retryTimer = null;
|
||||
unawaited(_dial());
|
||||
});
|
||||
}
|
||||
|
||||
/// Reset the backoff and re-dial immediately. Called when the app returns to
|
||||
/// the foreground: a controller is suspended while the phone is locked, so
|
||||
/// its socket to the host is usually dead on resume and we don't want to make
|
||||
/// the user wait out the backoff before control is restored (bug-fixes #5).
|
||||
void reconnectNow() {
|
||||
if (_closed) return;
|
||||
if (_current == RemoteConnStatus.connected && _socket != null) return;
|
||||
_attempt = 0;
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
unawaited(_dial());
|
||||
}
|
||||
|
||||
/// Send a control command to the host (no-op if not connected).
|
||||
void send(RemoteCommand cmd) => _sendMessage(CommandMessage(cmd));
|
||||
|
||||
Future<void> close() async {
|
||||
_closed = true;
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
await _sub?.cancel();
|
||||
_sub = null;
|
||||
try {
|
||||
|
|
@ -109,8 +202,15 @@ class RemoteSession {
|
|||
));
|
||||
} else if (msg is WelcomeMessage) {
|
||||
hostDevice = msg.device;
|
||||
_everConnected = true;
|
||||
_attempt = 0; // fresh backoff budget after a good connection
|
||||
_set(RemoteConnStatus.connected);
|
||||
} else if (msg is DenyMessage) {
|
||||
// A rejection (wrong account / protocol) won't fix itself — go terminal
|
||||
// and stop retrying.
|
||||
_closed = true;
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
_set(RemoteConnStatus.denied);
|
||||
_socket?.close().catchError((_) {});
|
||||
} else if (msg is SnapshotMessage) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue