diff --git a/overlay/oxide/plugins/RunicGateway.cs b/overlay/oxide/plugins/RunicGateway.cs index d16d02e..b8595ae 100644 --- a/overlay/oxide/plugins/RunicGateway.cs +++ b/overlay/oxide/plugins/RunicGateway.cs @@ -65,6 +65,15 @@ namespace Oxide.Plugins /// private const int MaxBackoffMs = 5000; + /// + /// How long a link must stay up before its drop counts as an outage rather than a + /// refusal. The backoff resets only past it: a sidecar that closes every connection at + /// once — one refusing a plugin that names another server (D155), or one crash-looping — + /// would otherwise be redialled twice a second for ever, rebuilding the boards on the + /// main thread and printing two console lines each time. + /// + private const int StableLinkMs = 10000; + /// The longest line accepted from the sidecar. Over-long lines are discarded. private const int MaxInboundLineBytes = 1024 * 1024; @@ -244,6 +253,13 @@ namespace Oxide.Plugins _bootId = ResolveBootId(); _wipeId = ResolveWipeId(); + + // Before the link thread, not in `Loaded`: both frameworks call `Init` first, and a + // thread started here with `_config` still null dials the 7799 fallback. With one + // server per host that is its own sidecar by coincidence; with two it is the OTHER + // server's, whose sidecar then files this server's history under that one's id + // (found by the phase 18 walk, D154). + LoadConfigValues(); _running = true; // Expired link codes are dropped on a timer, not on lookup: an unconfirmed code is @@ -496,7 +512,6 @@ namespace Oxide.Plugins private void Loaded() { - LoadConfigValues(); Puts($"protocol {ProtocolVersion}, serverId '{_config.ServerId}', sidecar {_config.Host}:{_config.Port}"); // Here rather than at `OnServerInitialized`: the link thread starts in `Init`, so a @@ -564,6 +579,7 @@ namespace Oxide.Plugins { TcpClient client = null; Thread reader = null; + System.Diagnostics.Stopwatch upFor = null; int epoch = Interlocked.Increment(ref _epoch); @@ -582,7 +598,7 @@ namespace Oxide.Plugins _dead = false; _connected = true; _loggedConnectFailure = false; // so a LATER outage is reported again - backoffMs = 500; + upFor = System.Diagnostics.Stopwatch.StartNew(); Interlocked.Increment(ref _connects); Puts($"connected to {host}:{port}"); @@ -654,14 +670,25 @@ namespace Oxide.Plugins if (!_running) break; + // A link that held is an outage: start again from the shortest wait. One that was + // closed on arrival keeps doubling (see StableLinkMs). + if (upFor != null && upFor.ElapsedMilliseconds >= StableLinkMs) + backoffMs = 500; + // Nothing is listening yet, or the sidecar restarted. Both are normal. // - // `Wake`, not `Thread.Sleep`: Unload runs on the MAIN thread and joins this one, so - // an uninterruptible sleep here is a stall of up to MaxBackoffMs on every reload — - // Oxide reports it as `Calling 'Unload' took 1918ms`, and the server is frozen for - // that whole time. Unload sets `_running` false and signals `Wake`, so waiting on - // the handle turns that stall into an immediate return. - Wake.WaitOne(backoffMs); + // A handle, not `Thread.Sleep`: Unload runs on the MAIN thread and joins this one, + // so an uninterruptible sleep here is a stall of up to MaxBackoffMs on every reload + // — Oxide reports it as `Calling 'Unload' took 1918ms`, and the server is frozen for + // that whole time. Unload sets `_stopping`, so waiting on it turns that stall into + // an immediate return. + // + // `_stopping`, not `Wake`, for the reason `_stopping` gives: `Emit` signals `Wake` + // on every frame, so a backoff waited on it lasted until the next hook fired. That + // was invisible while a failed connect was silent; against a sidecar that refuses + // (D155) the phase 18 walk measured some 250 redials a second, each printing two + // console lines and rebuilding the boards. + _stopping.WaitOne(backoffMs); backoffMs = Math.Min(backoffMs * 2, MaxBackoffMs); }