mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
Bonsai: diagnose IfcTester webapp startup failures instead of a dead tab
Starting the IfcTester webapp spawned the Flask server subprocess without capturing its output, slept a blind 1.5 seconds, and opened the browser unconditionally. When the server died on startup (missing dependency, incomplete ifctester build, firewall blocking the bind) the user got an "Unable to connect" browser tab and zero diagnostics; the panel still claimed the webapp was running. Reported in the wild on Windows. Harden the launcher: - capture the subprocess stdout/stderr; - replace the blind sleep with a readiness poll (up to 10s): open the browser only once a TCP connect to the webapp port succeeds; if the process exits first, print its exit code and captured output to the console with a hint about ifctester/Flask/firewall; if it stays alive but never listens, print a timeout diagnostic; in both failure cases do not open the browser and reset webapp_is_running; - Stop operator: wait after kill on the timeout path and close the captured pipe in cleanup paths to avoid leaks. Verified live in headless Blender via the real operators: happy path (port connectable, browser-open recorder called once with the right URL, clean stop), instant-death path (recorder not called, console shows "exited with code 3. Output: SIMULATED STARTUP FAILURE" plus the hint, webapp_is_running False), and never-listens path (timeout diagnostic after ~10s, process still alive, Stop kills it cleanly). Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -351,9 +351,15 @@ class StartIfcTesterWebapp(bpy.types.Operator):
|
||||
env["BONSAI_LIB_PATH"] = str(bonsai_lib_path)
|
||||
env["BONSAI_VERSION"] = tool.Blender.get_bonsai_version()
|
||||
|
||||
# Start the Flask server as subprocess
|
||||
# Start the Flask server as subprocess. Capture stdout/stderr so that if the
|
||||
# server fails to start (missing dependency, bad build, port/firewall issue)
|
||||
# we have something to show the user instead of a silent failure.
|
||||
webapp_process = subprocess.Popen(
|
||||
[sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)], env=env
|
||||
[sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)],
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# Update properties
|
||||
@@ -361,14 +367,54 @@ class StartIfcTesterWebapp(bpy.types.Operator):
|
||||
props.websocket_server_port = websocket_port
|
||||
props.webapp_is_running = True
|
||||
|
||||
# Wait a moment for servers to start, then open browser
|
||||
def delayed_open_browser():
|
||||
import time
|
||||
# Poll until the webapp is actually listening (or has died / timed out) before
|
||||
# opening a browser tab. Opening the browser on a dead server just produces a
|
||||
# confusing "Unable to connect" tab with no diagnostics.
|
||||
def wait_for_webapp_and_open_browser():
|
||||
process = webapp_process
|
||||
deadline = time.monotonic() + 10.0
|
||||
connected = False
|
||||
|
||||
time.sleep(1.5)
|
||||
webbrowser.open(f"http://127.0.0.1:{webapp_port}?bonsai_server={websocket_port}")
|
||||
while time.monotonic() < deadline:
|
||||
if process.poll() is not None:
|
||||
# Process exited before it ever started listening.
|
||||
break
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", webapp_port), timeout=0.25):
|
||||
connected = True
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
time.sleep(0.25)
|
||||
|
||||
browser_thread = threading.Thread(target=delayed_open_browser, daemon=True)
|
||||
if connected:
|
||||
webbrowser.open(f"http://127.0.0.1:{webapp_port}?bonsai_server={websocket_port}")
|
||||
return
|
||||
|
||||
# Something went wrong: do NOT open a dead browser tab. Print diagnostics
|
||||
# to the console, since this runs in a daemon thread after execute() has
|
||||
# already returned and self.report() is no longer usable.
|
||||
exit_code = process.poll()
|
||||
if exit_code is not None:
|
||||
try:
|
||||
output, _ = process.communicate(timeout=2)
|
||||
except Exception:
|
||||
output = ""
|
||||
print(
|
||||
f"IfcTester webapp process exited with code {exit_code}. Output: {output}\n"
|
||||
"Ensure ifctester is up to date and Flask is available; check firewall/antivirus "
|
||||
"if the port is blocked."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"IfcTester webapp did not start listening on port {webapp_port} within 10s.\n"
|
||||
"Ensure ifctester is up to date and Flask is available; check firewall/antivirus "
|
||||
"if the port is blocked."
|
||||
)
|
||||
|
||||
props.webapp_is_running = False
|
||||
|
||||
browser_thread = threading.Thread(target=wait_for_webapp_and_open_browser, daemon=True)
|
||||
browser_thread.start()
|
||||
|
||||
self.report(
|
||||
@@ -380,6 +426,8 @@ class StartIfcTesterWebapp(bpy.types.Operator):
|
||||
# Clean up on error
|
||||
if webapp_process:
|
||||
webapp_process.terminate()
|
||||
if webapp_process.stdout:
|
||||
webapp_process.stdout.close()
|
||||
webapp_process = None
|
||||
if websocket_server_thread and websocket_app:
|
||||
# The websocket server will be cleaned up when the thread ends
|
||||
@@ -420,9 +468,12 @@ class StopIfcTesterWebapp(bpy.types.Operator):
|
||||
webapp_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
webapp_process.kill()
|
||||
webapp_process.wait(timeout=5)
|
||||
except Exception as e:
|
||||
errors.append(f"Error stopping webapp server: {str(e)}")
|
||||
finally:
|
||||
if webapp_process.stdout:
|
||||
webapp_process.stdout.close()
|
||||
webapp_process = None
|
||||
|
||||
# Stop websocket server
|
||||
|
||||
Reference in New Issue
Block a user