feat(bridge): keep a step's teardown with its key — teardownOf (D385)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 9s
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 9s
Core records nothing for a step that failed or whose answer was lost, and
then reverts it with only the key: no server, no teardown. So a run request
now carries its `teardown`, stored in the key's record and never run with
it, and `{ key, teardownOf }` runs the teardown stored under `teardownOf`,
under its own key, at most once:
- only when something there may have run (some commands ran, or a reload
cut the run off); otherwise answered with nothing run and nothing kept;
- `busy` while the run it is for is still going;
- a reward carries no teardown (refused `malformed`), and a teardown takes
no placeholders.
Re-compiled and driven on both rigs (a failed run's teardown ran once, a
repeat ran nothing, nothing-ran and unknown keys ran nothing); rigs
restored to the protocol-13 bridge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -143,7 +143,9 @@ recipient of a reward who is online, with `{steamid}` and `{name}` filled in (th
|
||||
argument). That is how a plugin with no support of its own here is used from an event, for example
|
||||
RaidableBases' `rbe`. There is no allowlist: the site's switchboard is the switch, and `EventsEnabled`
|
||||
here is the server's. Each request runs at most once: its key is written to
|
||||
`data/RunicGateway/commands-run.json` before anything runs, and is kept for seven days. A command the
|
||||
`data/RunicGateway/commands-run.json` before anything runs, and is kept for seven days. A step's
|
||||
teardown commands are kept with its key too, so the site can have them run even for a step that failed
|
||||
part-way or whose answer was lost (`teardownOf`). A command the
|
||||
server does not know stops the request there as `unknown-command`. On a framework with
|
||||
`consolePlugins=False`, that is every plugin's command.
|
||||
|
||||
|
||||
@@ -18180,6 +18180,12 @@ namespace Oxide.Plugins
|
||||
// answer. A record left running by a restart or a reload is answered `interrupted`: some of
|
||||
// its commands may have run, and none is run again.
|
||||
//
|
||||
// **The teardown is kept with the key (D385).** A step's teardown commands come with its
|
||||
// run and are stored, not run. The site's ledger keeps its own copy and sends it back at
|
||||
// teardown; but core records nothing for a step that FAILED, or whose answer was lost, and
|
||||
// then asks with only the key. So `{ key, teardownOf }` runs the teardown stored under
|
||||
// `teardownOf`, when anything there may have run, under its own key, once.
|
||||
//
|
||||
// **Spread over frames.** A full reward is 100 recipients by 10 commands. A recipient's
|
||||
// commands run together in one frame, and a frame stops taking recipients past
|
||||
// `CommandsPerFrame` commands or `CommandFrameMs`, so the game never stalls on a step.
|
||||
@@ -18249,6 +18255,8 @@ namespace Oxide.Plugins
|
||||
public List<CommandReply> Replies = new List<CommandReply>();
|
||||
public List<string> Skipped = new List<string>();
|
||||
public int SkippedCount;
|
||||
/// <summary>The step's teardown commands, kept for <c>teardownOf</c>; never run with the record (D385).</summary>
|
||||
public List<string> Teardown = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>One recipient a reward names: the Steam id, and the name the site knows.</summary>
|
||||
@@ -18373,28 +18381,20 @@ namespace Oxide.Plugins
|
||||
return;
|
||||
}
|
||||
|
||||
var commands = new List<string>();
|
||||
string refusal = null;
|
||||
string teardownOf = StringField(cmd, "teardownOf");
|
||||
|
||||
foreach (string raw in Strings(cmd, "commands"))
|
||||
if (!string.IsNullOrEmpty(teardownOf))
|
||||
{
|
||||
string line = (raw ?? string.Empty).Trim();
|
||||
|
||||
if (line.Length == 0)
|
||||
continue;
|
||||
|
||||
if (line.Length > CommandLengthMax)
|
||||
refusal = $"a command is at most {CommandLengthMax} characters, and one is {line.Length}";
|
||||
else if (HasControl(line))
|
||||
refusal = "a command is one line, with no line breaks or control characters in it";
|
||||
|
||||
commands.Add(line);
|
||||
StartTeardownOf(key, teardownOf, reqId);
|
||||
return;
|
||||
}
|
||||
|
||||
string refusal = null;
|
||||
List<string> commands = CommandLines(cmd, "commands", ref refusal);
|
||||
List<string> teardown = CommandLines(cmd, "teardown", ref refusal);
|
||||
|
||||
if (refusal == null && commands.Count == 0)
|
||||
refusal = "there are no commands to run";
|
||||
else if (refusal == null && commands.Count > CommandsMax)
|
||||
refusal = $"at most {CommandsMax} commands are run at a time, not {commands.Count}";
|
||||
|
||||
List<CommandRecipient> recipients = null;
|
||||
|
||||
@@ -18419,12 +18419,20 @@ namespace Oxide.Plugins
|
||||
|
||||
if (refusal == null && recipients.Count > CommandRecipientsMax)
|
||||
refusal = $"a reward runs for at most {CommandRecipientsMax} people at a time, not {recipients.Count}";
|
||||
|
||||
if (refusal == null && teardown.Count > 0)
|
||||
refusal = "a reward has no teardown: nothing it runs is undone";
|
||||
}
|
||||
|
||||
if (refusal == null && recipients == null)
|
||||
if (refusal == null)
|
||||
{
|
||||
// Nobody to fill the placeholders with: refused rather than sent with the braces in.
|
||||
foreach (string line in commands)
|
||||
var unfilled = new List<string>(teardown);
|
||||
|
||||
if (recipients == null)
|
||||
unfilled.AddRange(commands);
|
||||
|
||||
foreach (string line in unfilled)
|
||||
{
|
||||
if (SteamIdPlaceholder.IsMatch(line) || NamePlaceholder.IsMatch(line))
|
||||
{
|
||||
@@ -18440,8 +18448,8 @@ namespace Oxide.Plugins
|
||||
return;
|
||||
}
|
||||
|
||||
// The key is on disk before the first command runs (§16.4).
|
||||
var record = new CommandRecord { Key = key, AtMs = NowMs() };
|
||||
// The key is on disk before the first command runs (§16.4), and the teardown with it.
|
||||
var record = new CommandRecord { Key = key, AtMs = NowMs(), Teardown = teardown };
|
||||
_commandRecords[key] = record;
|
||||
SaveCommandRecords();
|
||||
|
||||
@@ -18454,6 +18462,74 @@ namespace Oxide.Plugins
|
||||
StepCommandJob(job);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The command lines in one array field, trimmed, blanks dropped. A refusal is set on the
|
||||
/// first line too long or with a control character in it, or on more than
|
||||
/// <see cref="CommandsMax"/>; an earlier refusal is kept.
|
||||
/// </summary>
|
||||
private static List<string> CommandLines(Dictionary<string, object> cmd, string field, ref string refusal)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
foreach (string raw in Strings(cmd, field))
|
||||
{
|
||||
string line = (raw ?? string.Empty).Trim();
|
||||
|
||||
if (line.Length == 0)
|
||||
continue;
|
||||
|
||||
if (refusal == null && line.Length > CommandLengthMax)
|
||||
refusal = $"a command is at most {CommandLengthMax} characters, and one is {line.Length}";
|
||||
else if (refusal == null && HasControl(line))
|
||||
refusal = "a command is one line, with no line breaks or control characters in it";
|
||||
|
||||
lines.Add(line);
|
||||
}
|
||||
|
||||
if (refusal == null && lines.Count > CommandsMax)
|
||||
refusal = $"at most {CommandsMax} {field} commands are run at a time, not {lines.Count}";
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. D385: run the teardown stored with <paramref name="of"/>, under
|
||||
/// <paramref name="key"/>, when anything there may have run — some of its commands ran,
|
||||
/// or a reload cut it off. Nothing stored, or nothing ran: answered with nothing run and
|
||||
/// nothing recorded, which is a server this step never reached.
|
||||
/// </summary>
|
||||
private void StartTeardownOf(string key, string of, string reqId)
|
||||
{
|
||||
if (_commandJobs.ContainsKey(of))
|
||||
{
|
||||
EmitCommandError(reqId, "busy", "the commands this teardown is for are still running; ask again in a moment", null);
|
||||
return;
|
||||
}
|
||||
|
||||
CommandRecord first;
|
||||
bool due = _commandRecords.TryGetValue(of, out first)
|
||||
&& first.Teardown != null && first.Teardown.Count > 0
|
||||
&& (first.Ran > 0 || !first.Done);
|
||||
|
||||
if (!due)
|
||||
{
|
||||
EmitCommandAnswer(reqId, new CommandRecord { Key = key, Done = true }, false);
|
||||
return;
|
||||
}
|
||||
|
||||
var record = new CommandRecord { Key = key, AtMs = NowMs() };
|
||||
_commandRecords[key] = record;
|
||||
SaveCommandRecords();
|
||||
|
||||
var job = new CommandJob { Record = record, Commands = new List<string>(first.Teardown) };
|
||||
|
||||
if (reqId != null)
|
||||
job.Waiting.Add(reqId);
|
||||
|
||||
_commandJobs[key] = job;
|
||||
StepCommandJob(job);
|
||||
}
|
||||
|
||||
private static bool HasControl(string text)
|
||||
{
|
||||
foreach (char c in text)
|
||||
|
||||
Reference in New Issue
Block a user