1+ # macOS Objective-C fork-safety guard.
2+ #
3+ # Gunicorn forks workers; on Darwin, Objective-C runtime fork-safety checks
4+ # can SIGSEGV workers unless OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES is set
5+ # in the OS environment *before* Python starts (setting it in Python is too
6+ # late — see langflow_launcher.py for the same pattern).
7+ #
8+ # The `langflow` console script routes through langflow_launcher.py which
9+ # handles this. This guard catches the bypass paths (`python -m langflow`,
10+ # `uv run python -m langflow`, etc.) so they're not silent footguns. Only
11+ # fires for direct CLI invocation; ordinary `import langflow.__main__` is
12+ # unaffected.
13+ if __name__ == "__main__" :
14+ import os as _os
15+ import platform as _platform
16+ import sys as _sys
17+
18+ if _platform .system () == "Darwin" and not _os .environ .get ("OBJC_DISABLE_INITIALIZE_FORK_SAFETY" ):
19+ _os .environ ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY" ] = "YES"
20+ _os .execv (_sys .executable , [_sys .executable , "-m" , "langflow.__main__" , * _sys .argv [1 :]]) # noqa: S606
21+
122import asyncio
223import inspect
324import os
829import time
930import warnings
1031from contextlib import suppress
32+ from functools import partial
1133from ipaddress import ip_address
1234from pathlib import Path
1335
@@ -132,6 +154,68 @@ def get_number_of_workers(workers=None):
132154 return workers
133155
134156
157+ # Platforms where `langflow run` bypasses Gunicorn and runs uvicorn directly
158+ # against a pre-built FastAPI app object. On Linux we use Gunicorn (multi-worker
159+ # via fork()); on Windows and macOS forking is unsafe (Windows lacks fork; macOS
160+ # fork-with-threads + libdispatch / asyncio kqueue state crashes workers).
161+ DIRECT_UVICORN_PLATFORMS : tuple [str , ...] = ("Windows" , "Darwin" )
162+
163+
164+ def use_direct_uvicorn (system : str | None = None ) -> bool :
165+ """Return True iff this platform launches with uvicorn directly (no Gunicorn)."""
166+ return (system or platform .system ()) in DIRECT_UVICORN_PLATFORMS
167+
168+
169+ def clamp_uvicorn_workers (requested : int , * , system : str | None = None ) -> int :
170+ """Clamp ``workers`` to 1 when running uvicorn against a pre-built app object.
171+
172+ uvicorn refuses to spawn multiple workers from an app *object* (it needs an
173+ import string), so on the direct-uvicorn platforms we cap workers at 1 and
174+ warn — preferable to uvicorn's own ``sys.exit(1)`` with a generic message.
175+ On Linux this is a no-op since Gunicorn handles multi-worker.
176+ """
177+ if requested > 1 and use_direct_uvicorn (system ):
178+ logger .warning (
179+ "Direct-uvicorn startup on %s does not support workers > 1 "
180+ "(uvicorn requires an import string for multi-worker mode). "
181+ "Falling back to a single worker; requested=%d." ,
182+ system or platform .system (),
183+ requested ,
184+ )
185+ return 1
186+ return requested
187+
188+
189+ def build_direct_uvicorn_kwargs (
190+ * ,
191+ host : str ,
192+ port : int ,
193+ log_level : str | None ,
194+ workers : int ,
195+ loop : str ,
196+ ssl_cert_file_path : str | None ,
197+ ssl_key_file_path : str | None ,
198+ system : str | None = None ,
199+ ) -> dict :
200+ """Build the kwargs dict for ``uvicorn.run(app, **kwargs)`` on Win/macOS.
201+
202+ Pins the option set (workers clamp, TLS certs, loop type) in one place so
203+ the launch site stays a single call and so tests can assert that things
204+ like TLS cert/key pass through. Mirrors the option set used on the
205+ Gunicorn (Linux) path so platform parity does not drift again.
206+ """
207+ return {
208+ "host" : host ,
209+ "port" : port ,
210+ "log_level" : log_level ,
211+ "reload" : False ,
212+ "workers" : clamp_uvicorn_workers (workers , system = system ),
213+ "loop" : loop ,
214+ "ssl_certfile" : ssl_cert_file_path ,
215+ "ssl_keyfile" : ssl_key_file_path ,
216+ }
217+
218+
135219def display_results (results ) -> None :
136220 """Display the results of the migration."""
137221 for table_results in results :
@@ -158,8 +242,6 @@ def set_var_for_macos_issue() -> None:
158242 import os
159243
160244 os .environ ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY" ] = "YES"
161- # https://stackoverflow.com/questions/75747888/uwsgi-segmentation-fault-with-flask-python-app-behind-nginx-after-running-for-2 # noqa: E501
162- os .environ ["no_proxy" ] = "*" # to avoid error with gunicorn
163245
164246
165247def wait_for_server_ready (host , port , protocol ) -> None :
@@ -338,8 +420,14 @@ def run(
338420 static_files_dir : Path | None = Path (frontend_path ) if frontend_path else None
339421
340422 # Step 2: Starting Core Services
423+ app = None
424+ app_factory = None
341425 with progress .step (2 ):
342- app = setup_app (static_files_dir = static_files_dir , backend_only = bool (backend_only ))
426+ # See DIRECT_UVICORN_PLATFORMS for the rationale (no fork on Win/macOS).
427+ if use_direct_uvicorn ():
428+ app = setup_app (static_files_dir = static_files_dir , backend_only = bool (backend_only ))
429+ else :
430+ app_factory = partial (setup_app , static_files_dir = static_files_dir , backend_only = bool (backend_only ))
343431
344432 # Step 3: Connecting Database (this happens inside setup_app via dependencies)
345433 with progress .step (3 ):
@@ -371,10 +459,27 @@ def run(
371459 pass # Starter projects are added during app startup
372460
373461 # Step 6: Launching Langflow
374- if platform .system () == "Windows" :
462+ if use_direct_uvicorn ():
463+ # LANGFLOW_GUNICORN_PRELOAD is a Gunicorn-only knob: it triggers fork-safe
464+ # master-process preload so workers inherit state via copy-on-write. On
465+ # the direct-uvicorn path there is no master/worker split and no fork,
466+ # so the env var is silently inert. Warn loudly so users diagnosing
467+ # "preload isn't doing anything on my Mac" don't have to read source.
468+ if os .environ .get ("LANGFLOW_GUNICORN_PRELOAD" , "false" ).lower () == "true" :
469+ logger .warning (
470+ "LANGFLOW_GUNICORN_PRELOAD=true is ignored on %s: this platform "
471+ "uses single-process uvicorn (no fork), so master preload / "
472+ "copy-on-write inheritance does not apply." ,
473+ platform .system (),
474+ )
475+
375476 with progress .step (6 ):
376477 import uvicorn
377478
479+ if app is None :
480+ msg = "Direct-uvicorn startup (Windows/macOS) requires a pre-built FastAPI application."
481+ raise RuntimeError (msg )
482+
378483 # Print summary and banner before starting the server, since uvicorn is a blocking call.
379484 # We _may_ be able to subprocess, but with window's spawn behavior, we'd have to move all
380485 # non-picklable code to the subprocess.
@@ -394,28 +499,35 @@ def run(
394499
395500 uvicorn .run (
396501 app ,
397- host = host ,
398- port = port ,
399- log_level = log_level ,
400- reload = False ,
401- workers = get_number_of_workers (workers ),
402- loop = loop_type ,
502+ ** build_direct_uvicorn_kwargs (
503+ host = host ,
504+ port = port ,
505+ log_level = log_level ,
506+ workers = get_number_of_workers (workers ),
507+ loop = loop_type ,
508+ ssl_cert_file_path = ssl_cert_file_path ,
509+ ssl_key_file_path = ssl_key_file_path ,
510+ ),
403511 )
404512 else :
405513 with progress .step (6 ):
406514 # Use Gunicorn with LangflowUvicornWorker for non-Windows systems
407515 from langflow .server import LangflowApplication
408516
517+ if app_factory is None :
518+ msg = "Gunicorn startup requires an application factory."
519+ raise RuntimeError (msg )
520+
409521 options = {
410522 "bind" : f"{ host } :{ port } " ,
411523 "workers" : get_number_of_workers (workers ),
412524 "timeout" : worker_timeout ,
413525 "certfile" : ssl_cert_file_path ,
414526 "keyfile" : ssl_key_file_path ,
415527 "log_level" : log_level .lower () if log_level is not None else "info" ,
416- "preload_app" : os .environ .get ("LANGFLOW_GUNICORN_PRELOAD" , "true " ).lower () == "true" ,
528+ "preload_app" : os .environ .get ("LANGFLOW_GUNICORN_PRELOAD" , "false " ).lower () == "true" ,
417529 }
418- server = LangflowApplication (app , options )
530+ server = LangflowApplication (app_factory , options )
419531
420532 # Start the webapp process
421533 process_manager .webapp_process = Process (target = server .run )
0 commit comments