mirror of
https://github.com/blw1138/cross-py-builder.git
synced 2026-09-07 21:41:09 -05:00
Stream live worker progress via async build dispatch
The agent's /upload and /checkout_git were synchronous, blocking until a
build finished, so the controller could not relay live per-step progress.
Add ?async=1 support: the agent saves/clones the source, starts the build in
a background thread (holding build_lock), and returns 202 + {"id": ...}
immediately. The controller dispatches async, then polls /progress/<job_id>,
appending each step/log line to the job log and SSE stream, and treats the
build as done when progress clears (404) and the worker reports ready.
This commit is contained in:
@@ -220,6 +220,7 @@ def checkout_project():
|
||||
if not build_lock.acquire(blocking=False):
|
||||
return jsonify({'error': f"Another build is already running: {system_status['running_job']}"}), 409
|
||||
|
||||
async_mode = _is_async_request()
|
||||
repo_url = request.json['repo_url']
|
||||
print(f"\n========== Checking Out Git Project ==========")
|
||||
|
||||
@@ -233,6 +234,9 @@ def checkout_project():
|
||||
system_status['status'] = "cloning_repo"
|
||||
subprocess.check_call(['git', 'clone', repo_url, repo_dir])
|
||||
system_status['status'] = "ready"
|
||||
if async_mode:
|
||||
_run_build_background(job_id, repo_dir, start_time)
|
||||
return jsonify({"id": job_id, "async": True, "message": "Build started"}), 202
|
||||
return install_and_build(repo_dir, job_id, start_time)
|
||||
except Exception as e:
|
||||
print(f"Error processing checkout: {e}")
|
||||
@@ -244,7 +248,26 @@ def checkout_project():
|
||||
return jsonify({'error': 'Failed to clone repository'}), 500
|
||||
return jsonify({'error': f"Uncaught error processing checkout: {e}"}), 500
|
||||
finally:
|
||||
build_lock.release()
|
||||
if not async_mode:
|
||||
build_lock.release()
|
||||
|
||||
def _run_build_background(job_id, project_path, start_time):
|
||||
"""Run install_and_build in a background thread. Assumes build_lock is held;
|
||||
the thread releases it when the build finishes."""
|
||||
def runner():
|
||||
try:
|
||||
install_and_build(project_path, job_id, start_time)
|
||||
except Exception as e:
|
||||
print(f"Background build failed for {job_id}: {e}")
|
||||
_clear_progress(job_id)
|
||||
finally:
|
||||
build_lock.release()
|
||||
threading.Thread(target=runner, name=f"build-{job_id}", daemon=True).start()
|
||||
|
||||
|
||||
def _is_async_request():
|
||||
return request.args.get("async") in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def safe_extract(zip_ref, dest_dir):
|
||||
"""Extract a zipfile, rejecting members that would escape dest_dir."""
|
||||
@@ -260,6 +283,8 @@ def upload_project():
|
||||
if not build_lock.acquire(blocking=False):
|
||||
return jsonify({'error': f"Another build is already running: {system_status['running_job']}"}), 409
|
||||
|
||||
async_mode = _is_async_request()
|
||||
working_dir = None
|
||||
try:
|
||||
start_time = datetime.datetime.now()
|
||||
if 'file' not in request.files:
|
||||
@@ -283,15 +308,20 @@ def upload_project():
|
||||
print(f"Extracting uploaded project zip...")
|
||||
safe_extract(zip_ref, working_dir)
|
||||
|
||||
if async_mode:
|
||||
_run_build_background(job_id, working_dir, start_time)
|
||||
return jsonify({"id": job_id, "async": True, "message": "Build started"}), 202
|
||||
|
||||
return install_and_build(working_dir, job_id, start_time)
|
||||
except Exception as e:
|
||||
print(f"Uncaught error processing job: {e}")
|
||||
system_status['status'] = "ready"
|
||||
_clear_progress(job_id)
|
||||
shutil.rmtree(working_dir, ignore_errors=True)
|
||||
shutil.rmtree(working_dir, ignore_errors=True) if working_dir else None
|
||||
return jsonify({"error": f"Uncaught error processing job: {e}"}), 500
|
||||
finally:
|
||||
build_lock.release()
|
||||
if not async_mode:
|
||||
build_lock.release()
|
||||
|
||||
def install_and_build(project_path, job_id, start_time):
|
||||
_set_step(job_id, "starting", project_path=project_path)
|
||||
|
||||
Reference in New Issue
Block a user