-
Notifications
You must be signed in to change notification settings - Fork 0
Add WEBAPI serve script and dashboard run options #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add WEBAPI serve script and dashboard run options #6
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Python-Script-Runner/runner.py
Lines 6934 to 6937 in a0be769
| return result | |
| except subprocess.TimeoutExpired as e: | |
| monitor.stop() |
_execute_script now returns from a finally block and leaves the except subprocess.TimeoutExpired/Exception handlers after it (see lines 6934‑6937). Python requires except clauses to precede finally, so the module no longer parses—python -m py_compile runner.py raises SyntaxError: invalid syntax at line 6936. This prevents importing runner.py or starting the WEBAPI server; the try/except structure needs to be rearranged so the cancellation tracking doesn’t break compilation.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds a FastAPI-based web service wrapper around the ScriptRunner, providing HTTP endpoints for triggering script executions and a browser-based dashboard for monitoring runs. Key additions include cancellation support for active runs and a convenient shell script for starting the service.
- Adds cancellation mechanism to ScriptRunner via process tracking and a
cancel_active_run()method - Implements FastAPI web service with endpoints for running, listing, canceling scripts, and viewing logs
- Provides an interactive HTML dashboard with run options, filtering, and real-time updates
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
| runner.py | Adds process tracking fields and cancel_active_run() method to enable termination of running scripts via WEBAPI |
| WEBAPI/api.py | Implements FastAPI service with endpoints for script execution, status tracking, cancellation, and log retrieval |
| WEBAPI/static/index.html | Provides browser-based dashboard with form controls for launching scripts and monitoring their status |
| WEBAPI/serve.sh | Helper script to start the uvicorn server with configurable HOST/PORT environment variables |
| WEBAPI/requirements.txt | Specifies minimal dependencies (fastapi, uvicorn, pydantic) for the web service |
| WEBAPI/README.md | Documents WEBAPI features, quick start instructions, and deployment considerations |
| <button class="primary" onclick="viewLogs('${run.id}')">Logs</button> | ||
| ${canCancel ? `<button onclick="cancelRun('${run.id}')">Cancel</button>` : ''} | ||
| </td> | ||
| </tr>`; | ||
| }).join(''); | ||
| } | ||
|
|
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
XSS vulnerability: User-controlled data (run.id) is directly interpolated into HTML without escaping in the onclick handler. If a malicious run_id contains quotes or special characters, it could break out of the string and execute arbitrary JavaScript. Consider using data attributes or event delegation instead of inline onclick handlers.
| <button class="primary" onclick="viewLogs('${run.id}')">Logs</button> | |
| ${canCancel ? `<button onclick="cancelRun('${run.id}')">Cancel</button>` : ''} | |
| </td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| <button class="primary view-logs-btn" data-run-id="${run.id}">Logs</button> | |
| ${canCancel ? `<button class="cancel-run-btn" data-run-id="${run.id}">Cancel</button>` : ''} | |
| </td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| // Add event listeners for logs and cancel buttons after table is rendered | |
| runsTable.querySelectorAll('.view-logs-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const runId = btn.getAttribute('data-run-id'); | |
| viewLogs(runId); | |
| }); | |
| }); | |
| runsTable.querySelectorAll('.cancel-run-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const runId = btn.getAttribute('data-run-id'); | |
| cancelRun(runId); | |
| }); | |
| }); |
| exit 1 | ||
| fi | ||
|
|
||
| echo "Starting Script Runner WEBAPI at ${HOST}:${PORT} (dashboard available at http://${HOST}:${PORT})" |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The script displays the bind address as ${HOST}:${PORT} in the dashboard URL, but when HOST is set to 0.0.0.0, the URL http://0.0.0.0:9000 won't work as expected from a browser. Consider displaying localhost or 127.0.0.1 when HOST is 0.0.0.0 for better user experience, e.g., http://localhost:${PORT} or document that users should use their actual hostname.
| if not record: | ||
| record = RUN_STORE.get(run_id) | ||
| if not record: | ||
| raise HTTPException(status_code=404, detail="Run not found") | ||
| if record.status in {"completed", "failed", "cancelled"}: | ||
| raise HTTPException(status_code=409, detail="Run already finished") | ||
| if handle: | ||
| handle["cancel_event"].set() | ||
| runner = handle.get("runner") | ||
| if runner: | ||
| runner.cancel_active_run() | ||
| finished_at = datetime.utcnow() | ||
| updated = RunRecord( | ||
| id=record.id, | ||
| status="cancelled", | ||
| started_at=record.started_at, | ||
| finished_at=finished_at, | ||
| request=record.request, | ||
| result=record.result, | ||
| error="Run cancelled by user", | ||
| ) | ||
| with RUNS_LOCK: |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition in cancellation logic: The status check on line 335 and the actual cancellation operation (lines 337-341) are not atomic. Between checking the status and setting the cancel_event, the run could complete, leading to inconsistent state. The background task could finish and update the status to "completed" after the check but before the cancel_event is set, resulting in the record being marked as "cancelled" even though it completed successfully. Consider checking the status again after acquiring a lock or making the entire cancel operation atomic.
| if not record: | |
| record = RUN_STORE.get(run_id) | |
| if not record: | |
| raise HTTPException(status_code=404, detail="Run not found") | |
| if record.status in {"completed", "failed", "cancelled"}: | |
| raise HTTPException(status_code=409, detail="Run already finished") | |
| if handle: | |
| handle["cancel_event"].set() | |
| runner = handle.get("runner") | |
| if runner: | |
| runner.cancel_active_run() | |
| finished_at = datetime.utcnow() | |
| updated = RunRecord( | |
| id=record.id, | |
| status="cancelled", | |
| started_at=record.started_at, | |
| finished_at=finished_at, | |
| request=record.request, | |
| result=record.result, | |
| error="Run cancelled by user", | |
| ) | |
| with RUNS_LOCK: | |
| if not record: | |
| record = RUN_STORE.get(run_id) | |
| if not record: | |
| raise HTTPException(status_code=404, detail="Run not found") | |
| # Re-check status after acquiring lock to avoid race condition | |
| if record.status in {"completed", "failed", "cancelled"}: | |
| raise HTTPException(status_code=409, detail="Run already finished") | |
| if handle: | |
| handle["cancel_event"].set() | |
| runner = handle.get("runner") | |
| if runner: | |
| runner.cancel_active_run() | |
| finished_at = datetime.utcnow() | |
| updated = RunRecord( | |
| id=record.id, | |
| status="cancelled", | |
| started_at=record.started_at, | |
| finished_at=finished_at, | |
| request=record.request, | |
| result=record.result, | |
| error="Run cancelled by user", | |
| ) |
| cancel_event = threading.Event() | ||
| with RUNS_LOCK: | ||
| RUNS[run_id] = record | ||
| RUN_HANDLES[run_id] = {"cancel_event": cancel_event, "runner": None} |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential race condition here. The RUN_HANDLES dictionary is accessed without any locking mechanism, but it's modified in multiple threads (in _execute_run via finally block and in trigger_run). This could lead to race conditions when concurrent requests access or modify this shared state. Consider protecting RUN_HANDLES access with a lock similar to RUNS_LOCK.
| RUN_HANDLES[run_id] = {"cancel_event": cancel_event, "runner": None} | |
| with RUN_HANDLES_LOCK: | |
| RUN_HANDLES[run_id] = {"cancel_event": cancel_event, "runner": None} |
| def cancel_run(run_id: str) -> Dict[str, str]: | ||
| handle = RUN_HANDLES.get(run_id) |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing input validation for the run_id parameter. This endpoint accepts any string as a run_id without validating it's a valid UUID format, which could lead to unnecessary database queries or potential injection issues. Consider validating that run_id matches UUID format before querying the database.
| } | ||
| const runs = await res.json(); | ||
| if (!runs.length) { | ||
| runsTable.innerHTML = '<tr><td colspan="7">No runs yet.</td></tr>'; |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same colspan mismatch issue exists here. The table has 8 columns but the "No runs yet" fallback only spans 7 columns. This should be colspan="8" to match the table structure.
| runsTable.innerHTML = '<tr><td colspan="7">No runs yet.</td></tr>'; | |
| runsTable.innerHTML = '<tr><td colspan="8">No runs yet.</td></tr>'; |
| history_db=payload.history_db, | ||
| enable_history=payload.enable_history, | ||
| ) | ||
| RUN_HANDLES[run_id]["runner"] = runner |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition: RUN_HANDLES[run_id]["runner"] is accessed without synchronization. The RUN_HANDLES[run_id] dictionary is created in trigger_run but the "runner" key is set here in a background thread. If cancel_run is called between the creation of the handle and this line, it could access a handle with "runner": None, which is handled. However, there's no synchronization protecting the concurrent write to RUN_HANDLES[run_id], which could cause issues. Consider using a lock to protect RUN_HANDLES modifications.
| <label>Script Path | ||
| <input type="text" id="script-path" placeholder="./examples/sample.py" required /> | ||
| </label> | ||
| <label>Arguments (comma separated) | ||
| <input type="text" id="script-args" placeholder="--flag,value" /> | ||
| </label> | ||
| <label>Run Option | ||
| <select id="run-option"> | ||
| <option value="">Standard run</option> | ||
| <option value="--dry-run">Dry run (--dry-run)</option> | ||
| <option value="--disable-history">Disable history (--disable-history)</option> | ||
| <option value="--show-history">Show history (--show-history)</option> | ||
| </select> | ||
| </label> | ||
| <label>Timeout (seconds) | ||
| <input type="number" id="timeout" min="0" placeholder="Optional" /> | ||
| </label> | ||
| <label>Log Level | ||
| <select id="log-level"> | ||
| <option>INFO</option> | ||
| <option>DEBUG</option> | ||
| <option>WARNING</option> | ||
| <option>ERROR</option> | ||
| </select> | ||
| </label> | ||
| <label>Retry on Failure | ||
| <select id="retry"> | ||
| <option value="false">Disabled</option> | ||
| <option value="true">Enabled</option> | ||
| </select> | ||
| </label> |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Missing label associations: The form inputs (lines 109, 112, 123, etc.) don't have explicit for attributes linking the label to the input's id. While the current nested structure works, it's better for accessibility to use explicit associations with for attributes. For example: <label for="script-path">Script Path</label> <input type="text" id="script-path" ... />.
| def _execute_run(run_id: str, payload: RunRequest, cancel_event: threading.Event) -> None: | ||
| """Worker that executes the script and updates the run registry.""" | ||
|
|
||
| started_at = datetime.utcnow() |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using datetime.utcnow() is deprecated as of Python 3.12 in favor of datetime.now(timezone.utc). Consider updating to use the newer API to avoid future deprecation warnings.
| <label style="font-weight:600;">Page Size | ||
| <input type="number" id="page-size" value="50" min="1" max="200" /> | ||
| </label> | ||
| <button id="refresh">Refresh Runs</button> |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate ID attribute. There are two elements with id="refresh" - one button in the header (line 102) and another in the controls section (line 160). This violates HTML standards and can cause issues with JavaScript event handlers and accessibility. Consider renaming one of them, for example, the second one could be id="refresh-runs".
| <button id="refresh">Refresh Runs</button> | |
| <button id="refresh-runs">Refresh Runs</button> |
Summary
Testing
Codex Task