From 5f0d2db1a9407953323e70c093e3cd2270544049 Mon Sep 17 00:00:00 2001 From: Brett Williams Date: Sun, 30 Aug 2026 21:32:01 -0500 Subject: [PATCH] Serialize builds and add health endpoint to build agent - Add a non-blocking build lock so only one job runs at a time; concurrent upload/checkout requests get a 409 response - Add /healthz returning 200 when idle and 503 while a build is running - Ignore .DS_Store artifacts --- .gitignore | 3 ++- cross_py_builder/build_agent.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index af3e958..aec6bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ known_hosts __pycache__/ *.py[cod] -*.egg-info/ \ No newline at end of file +*.egg-info/ +.DS_Store \ No newline at end of file diff --git a/cross_py_builder/build_agent.py b/cross_py_builder/build_agent.py index 4d892e6..d156f39 100755 --- a/cross_py_builder/build_agent.py +++ b/cross_py_builder/build_agent.py @@ -5,6 +5,7 @@ import signal import socket import sys import tempfile +import threading import time from flask import Flask, request, jsonify, send_file @@ -30,6 +31,7 @@ LOCAL_DIR = os.path.dirname(__file__) BUILD_DIR = "pybuild-data" TMP_DIR = tempfile.gettempdir() system_status = {"status": "ready", "running_job": None} +build_lock = threading.Lock() def is_windows(): return platform.system().lower() == "windows" @@ -127,6 +129,12 @@ def status_page(): return (f"{APP_NAME} - Build Agent {build_agent_version} - \n" f"{system_os()} | {cpu_arch()} | {version} | {hostname} | {ZeroconfServer.get_local_ip()}") +@app.get("/healthz") +def healthz(): + ok = system_status['status'] == "ready" + return jsonify({"status": "ok" if ok else "busy", + "agent_version": build_agent_version}), 200 if ok else 503 + @app.get("/status") def status(): def get_directory_size(directory): @@ -173,6 +181,9 @@ def checkout_project(): if not request.is_json or not request.json.get('repo_url'): return jsonify({'error': 'Repository URL is required'}), 400 + if not build_lock.acquire(blocking=False): + return jsonify({'error': f"Another build is already running: {system_status['running_job']}"}), 409 + repo_url = request.json['repo_url'] print(f"\n========== Checking Out Git Project ==========") @@ -194,6 +205,8 @@ def checkout_project(): if isinstance(e, subprocess.CalledProcessError): return jsonify({'error': 'Failed to clone repository'}), 500 return jsonify({'error': f"Uncaught error processing checkout: {e}"}), 500 + finally: + build_lock.release() def safe_extract(zip_ref, dest_dir): """Extract a zipfile, rejecting members that would escape dest_dir.""" @@ -206,6 +219,9 @@ def safe_extract(zip_ref, dest_dir): @app.route('/upload', methods=['POST']) def upload_project(): + if not build_lock.acquire(blocking=False): + return jsonify({'error': f"Another build is already running: {system_status['running_job']}"}), 409 + try: start_time = datetime.datetime.now() if 'file' not in request.files: @@ -234,6 +250,8 @@ def upload_project(): system_status['status'] = "ready" shutil.rmtree(working_dir, ignore_errors=True) return jsonify({"error": f"Uncaught error processing job: {e}"}), 500 + finally: + build_lock.release() def install_and_build(project_path, job_id, start_time):