Add run_on_bind plugin for bind-triggered command execution - #899
Add run_on_bind plugin for bind-triggered command execution#899glowwserrano7 wants to merge 6 commits into
run_on_bind plugin for bind-triggered command execution#899Conversation
There was a problem hiding this comment.
Lintly has detected code quality issues in this pull request.
There was a problem hiding this comment.
Lintly has detected code quality issues in this pull request.
There was a problem hiding this comment.
Lintly has detected code quality issues in this pull request.
There was a problem hiding this comment.
Pull request overview
This PR extends the existing fetch_web actuation plugin to optionally execute post-bind commands (host or guest) after a web service bind is detected, and adds a new fetch_web_testing plugin to validate both fetched web output and cmd-on-bind side effects/output.
Changes:
- Add
cmd_on_bindsupport tofetch_web, including host/guest execution modes and optional shutdown after commands. - Persist guest command output to
guest_commands_output.txtfor later verification. - Introduce
fetch_web_testingplugin to verify fetch outputs, host marker files, and expected guest command output.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| pyplugins/actuation/fetch_web.py | Adds cmd-on-bind parsing/execution, host/guest modes, optional shutdown behavior, and guest-output capture. |
| pyplugins/testing/fetch_web_testing.py | New integration-test plugin to verify fetch outputs and validate cmd-on-bind host/guest outcomes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Only trigger cmd_on_bind for 0.0.0.0 to avoid running commands | ||
| # multiple times | ||
| if self.cmd_on_bind_structured and guest_ip == "0.0.0.0": | ||
| self.logger.info( | ||
| f"Bind detected on {guest_ip}:{guest_port}, spawning cmd_on_bind thread") | ||
| t = threading.Thread( | ||
| target=self._delayed_bind_workflow, args=(guest_ip, guest_port) | ||
| ) | ||
| t.daemon = True | ||
| t.start() | ||
|
|
| # Determine working directory for host mode | ||
| if self.outdir: | ||
| cwd = os.path.abspath( | ||
| os.path.join(self.outdir, "../..")) | ||
| self.logger.info( | ||
| f"Derived project root from outdir: {cwd}") | ||
| if not os.path.isdir(cwd): | ||
| self.logger.warning( | ||
| f"Project root does not exist: {cwd}" | ||
| ) | ||
| cwd = os.getcwd() | ||
| else: | ||
| cwd = os.getcwd() | ||
|
|
| if self.cmd_on_bind is not None: | ||
| # Check if it's list of dicts with mode/cmd | ||
| if isinstance( | ||
| self.cmd_on_bind, list) and len( | ||
| self.cmd_on_bind) > 0: | ||
| if isinstance(self.cmd_on_bind[0], dict): | ||
| self.cmd_on_bind_structured = self.cmd_on_bind | ||
| self.logger.info( | ||
| "Using structured cmd_on_bind with mode specification" | ||
| ) | ||
| else: | ||
| # List of strings (backward compatibility) | ||
| self.cmd_on_bind_structured = [ | ||
| {"mode": "guest", "cmd": [str(c)]} for c in self.cmd_on_bind | ||
| ] | ||
| self.logger.info("Converting legacy cmd_on_bind format") | ||
| elif isinstance(self.cmd_on_bind, str): | ||
| # Single string command (backward compatibility) | ||
| self.cmd_on_bind_structured = [ | ||
| {"mode": "guest", "cmd": [str(self.cmd_on_bind)]} | ||
| ] | ||
| self.logger.info("Converting single string cmd_on_bind") | ||
| else: | ||
| self.cmd_on_bind_structured = [] |
| self.cmd_on_bind_marker = self.get_arg("cmd_on_bind_marker") | ||
| self.cmd_on_bind_guest_output_contains = self.get_arg( | ||
| "cmd_on_bind_guest_output_contains") | ||
| self.cmd_wait_timeout = int(self.get_arg("cmd_wait_timeout") or 30) |
| project_root = os.path.abspath( | ||
| os.path.join(self.outdir, "../..")) | ||
| marker_path = os.path.join(project_root, marker) |
| # Guest-cmd must be enabled if any guest commands exist | ||
| has_guest_cmds = any( | ||
| entry.get("mode") == "guest" for entry in self.cmd_on_bind_structured) |
0b32794 to
0af50d8
Compare
There was a problem hiding this comment.
Lintly has detected code quality issues in this pull request.
run_on_bind plugin for bind-triggered command execution
Summary
Adds
run_on_bind, a standalone plugin that executes host or guest commands when a guestservice binds a port exposed to the host by the VPN plugin.
This started as an extension to
fetch_web. It is split into its own plugin so that:fetch_webremains responsible only for detecting web services and retrieving content.Changes
New:
run_on_bindon_bindevent and filters byproto,ports, and an optionalendpointsallowlist. No longer limited to ports 80/443.simultaneous binds can never produce concurrent
guest_cmd.pyinvocations.penguin's
proj_dirargument.guest_cmd.py. Requirescore.guest_cmd: true.delay(default 20s) before running, and a per-commandtimeout(default 120s) so anunresponsive guest cannot hang the run.
include commands that only apply to some targets, so requiring all of them to pass would
keep retrying endpoints on account of commands that were never going to work.
This keeps a run from wedging when the first endpoint to appear (often a loopback bind)
isn't the one that works. The first successful attempt is the last.
shutdown_after_cmdends the emulation after the first successful attempt.run_on_bind_output.txt— one block per command per attempt, recording endpoint,attempt, mode, command,
Status: SUCCESS|FAILED, return code, stdout and stderr. Everycommand is recorded regardless of outcome, including timeouts and missing executables.
New:
run_on_bind_testinghost_marker: a file a host-mode command is expected to create. Relative paths resolveagainst
proj_dir— the same working directory host commands run in — so a commandwriting a relative path and this check agree on where the file lands.
results/…resolvesagainst the output directory; absolute paths are used as-is.
output_contains: strings expected inrun_on_bind_output.txt.wait_timeout(default 30): polls for each expected file rather than sleeping a fixedinterval, so a passing run finishes as soon as the files appear.
run_on_bind_test.txt.fetch_web_testingscoped back to fetch_web onlyRemoved
cmd_on_bind_marker,cmd_on_bind_guest_output_contains, andcmd_wait_timeout;those checks now live in
run_on_bind_testing. Web-service verification (theweb_<ip>_<port>output files) is unchanged.Example
This verifies that host-mode commands create the expected marker file and that guest-mode
commands execute and produce the expected output.
commandsalso accepts a bare string or a list of strings, both of which default to guestmode:
Running alongside
fetch_webThe two plugins share no state — separate subscriptions, queues, worker threads and output
files — but either can end the emulation. When both are loaded, enable at most one shutdown
flag (
shutdown_after_cmd,shutdown_after_www,shutdown_on_failure); otherwise whicheverfinishes first calls
end_analysis()and truncates the other's in-flight work. To let bothcomplete, leave all three unset and bound the run with
core.timeout, which is otherwiseunbounded.