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}");
}