From 616f4fbca864f3f8852b75477925421f4489954d Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 26 Sep 2026 20:21:06 -0500 Subject: [PATCH] =?UTF-8?q?fix(bridge):=20protocol=2013=20=E2=80=94=20a=20?= =?UTF-8?q?configuration=20reload=20that=20works=20on=20Carbon=20(W1-W5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The step-1 rig walk (PLAYER_WALK configuration 2, 2b, 4 on both rigs) found: - W3: Carbon's Oxide-compatible ReloadPlugin is not a reload. One call unloaded Kits and never compiled it again; the next did nothing. Carbon now reloads with `c.reload` on the server console (ConsoleSystem.Run), as an operator would at the panel. Oxide keeps ReloadPlugin, falling back to `oxide.reload`. - W4: Carbon's log is carbon/logs/Carbon.Core.log, with no *.txt in the directory at all, so the log evidence never fired on Carbon. Chosen by name. - W5: Carbon loads a plugin whose LoadConfig threw and raises Loaded in the same instant. OnPluginLoaded is now noted, and the job settles on the first log read a poll after it. Carbon's "Failed ILoadConfig" is on the word list. - W2: Oxide's "Could not initialize plugin 'Kits v4.4.9' (JsonReaderException: ...)" is on the word list, so the outcome quotes the exception. - W1: Oxide's idle compiler shutdown drops a queued reload with "Timed out waiting for plugin to be compiled: X". The reload is asked for once more; a second give-up settles at once by what is loaded. rg.config prints the log file it follows (log=). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- overlay/oxide/plugins/RunicGateway.cs | 176 +++++++++++++++++++++++--- 1 file changed, 156 insertions(+), 20 deletions(-) diff --git a/overlay/oxide/plugins/RunicGateway.cs b/overlay/oxide/plugins/RunicGateway.cs index 23082ac..144aba3 100644 --- a/overlay/oxide/plugins/RunicGateway.cs +++ b/overlay/oxide/plugins/RunicGateway.cs @@ -4124,23 +4124,47 @@ namespace Oxide.Plugins /// /// /// Lower case, matched case-insensitively. Oxide's are its compiler's and its loader's - /// own words ("Error while compiling", "Failed to initialize plugin", seen on the rig in - /// configuration step 4). Carbon's differ, and the rest of the list is there so that the - /// same file works on both (R19); the Carbon pass of the re-walk is what pins them. + /// own words ("Error while compiling", "Failed to initialize plugin", and "Could not + /// initialize plugin 'Kits v4.4.9' (JsonReaderException: …)" when a plugin's own + /// LoadConfig throws, seen on the rig in configuration step 4). The last one + /// carries the exception, so it is the line an operator needs; Oxide follows it with + /// "Previous version of plugin failed to load", which says nothing about why. Carbon's + /// differ, and the rest of the list is there so that the same file works on both (R19); + /// the Carbon pass of the re-walk is what pins them. The first pinned is Carbon + /// 2.0.259's "[Kits] Failed ILoadConfig (Could not convert string to double: abc. …)" — + /// after which Carbon loads the plugin anyway, broken, and raises Loaded. /// /// private static readonly string[] ConfigFailureWords = { "error while compiling", + "could not initialize", "failed to initialize", "failed initializing", "failed to compile", "failed compiling", "failed to load", "failed loading", + "failed iloadconfig", "unable to load", }; + /// + /// What the framework says when it gave up on a compile, beside the plugin's name. + /// + /// + /// Not a failure of the file: Oxide shuts its compiler down after a minute idle, and a + /// reload asked for in that moment is dropped with "Timed out waiting for plugin to be + /// compiled: ZoneManager" while the plugin carries on, loaded, on its old file (seen on + /// the rig, configuration step 2b). The compiler starts again on demand, so the reload + /// is asked for once more; only a second give-up settles the job. + /// + /// + private static readonly string[] ConfigStallWords = + { + "timed out waiting for plugin to be compiled", + }; + /// Which reload a job is waiting on: the edit's, or the restore's after a rollback. private enum ConfigPhase { @@ -4197,6 +4221,12 @@ namespace Oxide.Plugins /// Whether the target was seen leaving in this phase. It separates "never started" from "did not come back". public bool Unloaded; + /// Whether this phase's reload was already asked for again after the framework gave up compiling. + public bool Retried; + + /// When OnPluginLoaded named the target in this phase, or null. The next log read settles it. + public DateTime? LoadedAt; + /// The log file being read, and how far into it this job has read. public string LogPath; @@ -4652,16 +4682,32 @@ namespace Oxide.Plugins } /// - /// Main thread. OnPluginLoaded named the target: in the edit phase the write - /// stands; in the restore phase the plugin is back on its old file. + /// Main thread. OnPluginLoaded named the target. It is noted, not settled: the + /// next log read settles it (W5). + /// + /// + /// "Loaded" is not "loaded well" on Carbon. A config its LoadConfig cannot read + /// is logged as a failure and the plugin is loaded anyway, broken, in the same instant + /// (seen on the rig, configuration step 4). Settling on the hook reported that edit as + /// applied; reading the log first rolls it back, for about a second's wait. + /// /// private void ConfigReloadArrived() { ConfigWriteJob job = _configJob; - if (job == null) + if (job == null || job.LoadedAt.HasValue) return; + job.LoadedAt = DateTime.UtcNow; + } + + /// + /// Main thread. A load that a full log read after it found no failure: in the edit + /// phase the write stands; in the restore phase the plugin is back on its old file. + /// + private void ConfigLoadConfirmed(ConfigWriteJob job) + { if (job.Phase == ConfigPhase.Edit) SettleConfigJob(job, true, false, null, null, null); else @@ -4700,6 +4746,12 @@ namespace Oxide.Plugins string failure = ReadConfigFailure(job); + if (failure != null && IsCompileStallOf(failure, job.Reload)) + { + ConfigCompileStalled(job, failure); + return; + } + if (failure != null) { if (job.Phase == ConfigPhase.Edit) @@ -4716,9 +4768,53 @@ namespace Oxide.Plugins return; } + // A load is trusted once a read that began at least a poll after it found nothing: + // the failure it would have to beat is logged before the load, never after. + if (job.LoadedAt.HasValue && + (DateTime.UtcNow - job.LoadedAt.Value).TotalSeconds >= ConfigLogPollSeconds) + { + ConfigLoadConfirmed(job); + return; + } + if ((DateTime.UtcNow - job.PhaseStarted).TotalSeconds < ConfigReloadCeilingSeconds) return; + SettleByWhatIsLoaded(job, $"within {ConfigReloadCeilingSeconds:0}s"); + } + + /// + /// Main thread. The framework gave up compiling the target (W1). The first time, the + /// reload is asked for again with a fresh ceiling; the second time, the job is settled + /// at once by what is loaded — the same judgement the ceiling makes, without waiting + /// for it, because the framework has already said it is not going to finish. + /// + private void ConfigCompileStalled(ConfigWriteJob job, string line) + { + if (!job.Retried) + { + job.Retried = true; + job.PhaseStarted = DateTime.UtcNow; + job.Unloaded = false; + job.LoadedAt = null; + MarkLogPosition(job); + + Puts($"the framework gave up compiling '{job.Reload}'; asking for the reload once more"); + + if (ReloadPluginByName(job.Reload)) + return; + } + + SettleByWhatIsLoaded(job, $"before the framework gave up compiling it ({ShortLine(line)})"); + } + + /// + /// Main thread. Settles a reload that said nothing conclusive, by what is loaded — not + /// by the clock (F10). finishes the sentence: "within 30s", or + /// why it stopped sooner. + /// + private void SettleByWhatIsLoaded(ConfigWriteJob job, string when) + { bool loaded = IsPluginLoaded(job.Reload); if (job.Phase == ConfigPhase.Restore) @@ -4727,7 +4823,7 @@ namespace Oxide.Plugins loaded ? job.RollbackReason : $"{job.RollbackReason}; '{job.Reload}' did not come back on the restored file " + - $"within {ConfigReloadCeilingSeconds:0}s either", + $"{when} either", job.RollbackLog); return; } @@ -4740,7 +4836,7 @@ namespace Oxide.Plugins else if (loaded) { SettleConfigJob(job, false, false, null, - $"the framework did not reload '{job.Reload}' within {ConfigReloadCeilingSeconds:0}s; " + + $"the framework did not reload '{job.Reload}' {when}; " + "the file stands and applies at its next load", null); } @@ -4748,8 +4844,8 @@ namespace Oxide.Plugins { RollBackConfig(job, job.Unloaded - ? $"'{job.Reload}' unloaded and did not come back within {ConfigReloadCeilingSeconds:0}s" - : $"'{job.Reload}' did not load within {ConfigReloadCeilingSeconds:0}s", + ? $"'{job.Reload}' unloaded and did not come back {when}" + : $"'{job.Reload}' did not load {when}", LogTail()); } } @@ -4771,6 +4867,8 @@ namespace Oxide.Plugins job.Phase = ConfigPhase.Restore; job.PhaseStarted = DateTime.UtcNow; job.Unloaded = false; + job.Retried = false; + job.LoadedAt = null; job.RollbackReason = reason; job.RollbackLog = log; @@ -4909,7 +5007,7 @@ namespace Oxide.Plugins if (line.IndexOf(own, StringComparison.OrdinalIgnoreCase) >= 0) continue; - if (IsLoadFailureOf(line, job.Reload)) + if (IsLoadFailureOf(line, job.Reload) || IsCompileStallOf(line, job.Reload)) return line; } } @@ -4924,23 +5022,34 @@ namespace Oxide.Plugins /// Whether a log line says failed to compile or to initialise. private static bool IsLoadFailureOf(string line, string plugin) + { + return SaysAbout(line, plugin, ConfigFailureWords); + } + + /// Whether a log line says the framework gave up compiling (W1). + private static bool IsCompileStallOf(string line, string plugin) + { + return SaysAbout(line, plugin, ConfigStallWords); + } + + private static bool SaysAbout(string line, string plugin, string[] words) { if (string.IsNullOrEmpty(line) || string.IsNullOrEmpty(plugin)) return false; string lower = line.ToLowerInvariant(); - bool failed = false; + bool said = false; - foreach (string word in ConfigFailureWords) + foreach (string word in words) { if (lower.IndexOf(word, StringComparison.Ordinal) >= 0) { - failed = true; + said = true; break; } } - return failed && MentionsName(lower, plugin.ToLowerInvariant()); + return said && MentionsName(lower, plugin.ToLowerInvariant()); } /// @@ -5252,9 +5361,16 @@ namespace Oxide.Plugins } /// - /// The framework's newest log file, or null. The framework's own log is the newest - /// *.txt directly in LogDirectory; plugins that log to files write into - /// subdirectories of it, which this does not look in. + /// The framework's newest log file, or null. Oxide's own log is the newest *.txt + /// directly in LogDirectory; plugins that log to files write into subdirectories + /// of it, which this does not look in. + /// + /// + /// Carbon's is Carbon.Core.log in carbon/logs, beside four other + /// *.log files and no *.txt at all (read on the rig, Carbon 2.0.259). Its + /// Harmony log is the one written most often, so "newest" would pick the wrong file; + /// the name is the only reliable way to choose. + /// /// private static string NewestLogFile() { @@ -5265,6 +5381,12 @@ namespace Oxide.Plugins if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir)) return null; + if (IsCarbon()) + { + string core = Path.Combine(dir, "Carbon.Core.log"); + return File.Exists(core) ? core : null; + } + string newest = null; DateTime newestAt = DateTime.MinValue; @@ -5336,6 +5458,13 @@ namespace Oxide.Plugins /// private bool ReloadPluginByName(string name) { + // Carbon's Oxide-compatible ReloadPlugin is not a reload (W3, Carbon 2.0.259 on the + // rig): one call unloaded Kits and never compiled it again, the next did nothing at + // all. Its own console command, typed at the panel, reloads the same plugin in about a + // second — so on Carbon the reload IS that command, run on the server console. + if (IsCarbon()) + return RunReloadCommand("c.reload", name); + try { System.Reflection.MethodInfo reload = Interface.Oxide.GetType() @@ -5352,14 +5481,20 @@ namespace Oxide.Plugins Puts($"framework reload of '{name}' threw: {ex.Message}"); } + return RunReloadCommand("oxide.reload", name); + } + + /// Runs a reload command on the server console, as an operator at the panel would. + private bool RunReloadCommand(string command, string name) + { try { - covalence.Server.Command(IsCarbon() ? "c.reload" : "oxide.reload", name); + ConsoleSystem.Run(ConsoleSystem.Option.Server, command, name); return true; } catch (Exception ex) { - Puts($"console reload of '{name}' threw: {ex.Message}"); + Puts($"console {command} of '{name}' threw: {ex.Message}"); return false; } } @@ -10746,6 +10881,7 @@ namespace Oxide.Plugins arg.ReplyWith( $"protocol={ProtocolVersion} framework={(IsCarbon() ? "carbon" : "oxide")} " + $"root={(string.IsNullOrEmpty(root) ? "(none)" : root)}" + Environment.NewLine + + $"log={NewestLogFile() ?? "(none: a failed reload waits out the ceiling)"}" + Environment.NewLine + $"waiting={waiting}" + Environment.NewLine + $"last write: {_lastConfigSummary}"); }