Skip to content

Conversation

@jomardyan
Copy link
Owner

Summary

  • add helper script for starting the WEBAPI service with uvicorn using HOST/PORT defaults
  • update dashboard form to include predefined run options alongside manual arguments
  • document the helper script in the WEBAPI quick start instructions

Testing

  • python -m py_compile WEBAPI/api.py

Codex Task

Copilot AI review requested due to automatic review settings November 20, 2025 14:15
@jomardyan jomardyan merged commit 73ecee5 into main Nov 20, 2025
6 of 8 checks passed
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a 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()

P0 Badge Fix invalid try/finally/except ordering in _execute_script

_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".

Copy link

Copilot AI left a 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

Comment on lines +267 to +273
<button class="primary" onclick="viewLogs('${run.id}')">Logs</button>
${canCancel ? `<button onclick="cancelRun('${run.id}')">Cancel</button>` : ''}
</td>
</tr>`;
}).join('');
}

Copy link

Copilot AI Nov 20, 2025

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.

Suggested change
<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);
});
});

Copilot uses AI. Check for mistakes.
exit 1
fi

echo "Starting Script Runner WEBAPI at ${HOST}:${PORT} (dashboard available at http://${HOST}:${PORT})"
Copy link

Copilot AI Nov 20, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +331 to +352
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:
Copy link

Copilot AI Nov 20, 2025

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.

Suggested change
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",
)

Copilot uses AI. Check for mistakes.
cancel_event = threading.Event()
with RUNS_LOCK:
RUNS[run_id] = record
RUN_HANDLES[run_id] = {"cancel_event": cancel_event, "runner": None}
Copy link

Copilot AI Nov 20, 2025

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.

Suggested change
RUN_HANDLES[run_id] = {"cancel_event": cancel_event, "runner": None}
with RUN_HANDLES_LOCK:
RUN_HANDLES[run_id] = {"cancel_event": cancel_event, "runner": None}

Copilot uses AI. Check for mistakes.
Comment on lines +327 to +328
def cancel_run(run_id: str) -> Dict[str, str]:
handle = RUN_HANDLES.get(run_id)
Copy link

Copilot AI Nov 20, 2025

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.

Copilot uses AI. Check for mistakes.
}
const runs = await res.json();
if (!runs.length) {
runsTable.innerHTML = '<tr><td colspan="7">No runs yet.</td></tr>';
Copy link

Copilot AI Nov 20, 2025

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.

Suggested change
runsTable.innerHTML = '<tr><td colspan="7">No runs yet.</td></tr>';
runsTable.innerHTML = '<tr><td colspan="8">No runs yet.</td></tr>';

Copilot uses AI. Check for mistakes.
history_db=payload.history_db,
enable_history=payload.enable_history,
)
RUN_HANDLES[run_id]["runner"] = runner
Copy link

Copilot AI Nov 20, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +138
<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>
Copy link

Copilot AI Nov 20, 2025

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" ... />.

Copilot uses AI. Check for mistakes.
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()
Copy link

Copilot AI Nov 20, 2025

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.

Copilot uses AI. Check for mistakes.
<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>
Copy link

Copilot AI Nov 20, 2025

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".

Suggested change
<button id="refresh">Refresh Runs</button>
<button id="refresh-runs">Refresh Runs</button>

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants