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
|
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`
|
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
|
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
|
server does not know stops the request there as `unknown-command`. On a framework with
|
||||||
`consolePlugins=False`, that is every plugin's command.
|
`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
|
// 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.
|
// 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
|
// **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
|
// 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.
|
// `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<CommandReply> Replies = new List<CommandReply>();
|
||||||
public List<string> Skipped = new List<string>();
|
public List<string> Skipped = new List<string>();
|
||||||
public int SkippedCount;
|
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>
|
/// <summary>One recipient a reward names: the Steam id, and the name the site knows.</summary>
|
||||||
@@ -18373,28 +18381,20 @@ namespace Oxide.Plugins
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var commands = new List<string>();
|
string teardownOf = StringField(cmd, "teardownOf");
|
||||||
string refusal = null;
|
|
||||||
|
|
||||||
foreach (string raw in Strings(cmd, "commands"))
|
if (!string.IsNullOrEmpty(teardownOf))
|
||||||
{
|
{
|
||||||
string line = (raw ?? string.Empty).Trim();
|
StartTeardownOf(key, teardownOf, reqId);
|
||||||
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
if (refusal == null && commands.Count == 0)
|
||||||
refusal = "there are no commands to run";
|
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;
|
List<CommandRecipient> recipients = null;
|
||||||
|
|
||||||
@@ -18419,12 +18419,20 @@ namespace Oxide.Plugins
|
|||||||
|
|
||||||
if (refusal == null && recipients.Count > CommandRecipientsMax)
|
if (refusal == null && recipients.Count > CommandRecipientsMax)
|
||||||
refusal = $"a reward runs for at most {CommandRecipientsMax} people at a time, not {recipients.Count}";
|
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.
|
// 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))
|
if (SteamIdPlaceholder.IsMatch(line) || NamePlaceholder.IsMatch(line))
|
||||||
{
|
{
|
||||||
@@ -18440,8 +18448,8 @@ namespace Oxide.Plugins
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The key is on disk before the first command runs (§16.4).
|
// 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() };
|
var record = new CommandRecord { Key = key, AtMs = NowMs(), Teardown = teardown };
|
||||||
_commandRecords[key] = record;
|
_commandRecords[key] = record;
|
||||||
SaveCommandRecords();
|
SaveCommandRecords();
|
||||||
|
|
||||||
@@ -18454,6 +18462,74 @@ namespace Oxide.Plugins
|
|||||||
StepCommandJob(job);
|
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)
|
private static bool HasControl(string text)
|
||||||
{
|
{
|
||||||
foreach (char c in text)
|
foreach (char c in text)
|
||||||
|
|||||||
Reference in New Issue
Block a user