fix: adds logging to langflow run command (#8466)
* Clean up initialiation of uv run langflow run * sigint fixes * Log level fixes and gunicorn logs removal * sqlalch contraints * Add back banner for windows * [autofix.ci] apply automated fixes * handle sigint sigterms * [autofix.ci] apply automated fixes * handle multiple sigints * more gracefully handle shutdown during init * [autofix.ci] apply automated fixes * swallow cancellation during log flush * fix path import and cleanup * [autofix.ci] apply automated fixes * start with step logs * 📝 (main.py): Comment out logger.info statement to prevent unnecessary logging of static files directory setup information. * 💡 (langflow/__main__.py): Comment out logger.info message for initializing Langflow to reduce noise in logs and improve readability. * uncomment logs, change to debug * 📝 (langflow): remove redundant logger info messages in signal handlers and shutdown process ♻️ (langflow): refactor shutdown process to use a progress indicator for better visibility and control ♻️ (langflow): refactor initialization process to remove redundant logger info messages 📝 (langflow): add methods to print shutdown summary and farewell message after shutdown is complete 📝 (langflow): add method to create a progress indicator for shutdown steps 📝 (langflow): add predefined shutdown steps for Langflow shutdown process 📝 (langflow): add method to create a progress indicator with predefined Langflow initialization steps 📝 (langflow): add method to create a progress indicator with predefined Langflow shutdown steps 📝 (langflow): add predefined shutdown steps in reverse order of initialization 📝 (langflow): add predefined initialization steps for Langflow initialization process 📝 (langflow): add method to print a summary of all completed shutdown steps * 📝 (api/v1/__init__.py): reorganize imports and include_router calls for better readability and maintainability 🔧 (api/v1/__init__.py): update __all__ list to explicitly include all routers for better module encapsulation 💡 (utils.py): add logging statement to indicate building custom components from specified paths * Fix ordering of operations for windows builds * use lowercase log levels for uvicorn * converts log to lower if set from env var * [autofix.ci] apply automated fixes * use a subprocess for windows so progress step can complete * [autofix.ci] apply automated fixes * test subprocess for windows * use subprocess for all architectures * Clean up global vars and simplify shutdown * [autofix.ci] apply automated fixes * 📝 (progress.py): add platform check to use different characters and icons based on the operating system for better compatibility and user experience * 📝 (langflow/__main__.py): use platform-specific characters for icons to prevent encoding issues and improve cross-platform compatibility * test for windows * remove print * move windows blocking call outside of progres step * [autofix.ci] apply automated fixes * log cleanup and timeout of teardown * remove seemingly unnecessary redirect * [autofix.ci] apply automated fixes * ruff * Add init file to cli dir * ruff: : * unused import * mypy fixes * mypy fixes * [autofix.ci] apply automated fixes * ruff * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * Try underscores so ci doesn't remove * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
parent
e5bec89cc8
commit
131322cb9b
15 changed files with 6191 additions and 4124 deletions
|
|
@ -215,8 +215,8 @@ nv-ingest = [
|
||||||
]
|
]
|
||||||
|
|
||||||
postgresql = [
|
postgresql = [
|
||||||
"sqlalchemy[postgresql_psycopg2binary]",
|
"sqlalchemy[postgresql_psycopg2binary]>=2.0.38,<3.0.0",
|
||||||
"sqlalchemy[postgresql_psycopg]",
|
"sqlalchemy[postgresql_psycopg]>=2.0.38,<3.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from rich.panel import Panel
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
|
||||||
|
from langflow.cli.progress import create_langflow_progress
|
||||||
from langflow.initial_setup.setup import get_or_create_default_folder
|
from langflow.initial_setup.setup import get_or_create_default_folder
|
||||||
from langflow.logging.logger import configure, logger
|
from langflow.logging.logger import configure, logger
|
||||||
from langflow.main import setup_app
|
from langflow.main import setup_app
|
||||||
|
|
@ -40,6 +41,54 @@ console = Console()
|
||||||
app = typer.Typer(no_args_is_help=True)
|
app = typer.Typer(no_args_is_help=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessManager:
|
||||||
|
"""Manages the lifecycle of the backend process."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.webapp_process = None
|
||||||
|
self.shutdown_in_progress = False
|
||||||
|
|
||||||
|
# params are required for signal handlers, even if they are not used
|
||||||
|
def handle_sigterm(self, _signum: int, _frame) -> None:
|
||||||
|
"""Handle SIGTERM signal gracefully."""
|
||||||
|
if self.shutdown_in_progress:
|
||||||
|
return # Already shutting down, ignore
|
||||||
|
self.shutdown_in_progress = True
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
|
# params are required for signal handlers, even if they are not used
|
||||||
|
def handle_sigint(self, _signum: int, _frame) -> None:
|
||||||
|
"""Handle SIGINT signal gracefully."""
|
||||||
|
if self.shutdown_in_progress:
|
||||||
|
return # Already shutting down, ignore
|
||||||
|
self.shutdown_in_progress = True
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Gracefully shutdown the webapp process."""
|
||||||
|
if self.webapp_process and self.webapp_process.is_alive():
|
||||||
|
# Just terminate the process - the actual shutdown progress is handled
|
||||||
|
# by the FastAPI lifespan context in main.py
|
||||||
|
self.webapp_process.terminate()
|
||||||
|
# The long wait allows the process to finish setup, preventing it from
|
||||||
|
# getting in a state where background tasks continue to do work after termination
|
||||||
|
# is sent.
|
||||||
|
self.webapp_process.join(timeout=30)
|
||||||
|
if self.webapp_process.is_alive():
|
||||||
|
logger.warning("Process didn't terminate gracefully, killing it.")
|
||||||
|
self.webapp_process.kill()
|
||||||
|
self.webapp_process.join()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
# Create a single instance of ProcessManager
|
||||||
|
process_manager = ProcessManager()
|
||||||
|
|
||||||
|
# Update signal handlers to use the instance methods
|
||||||
|
signal.signal(signal.SIGTERM, process_manager.handle_sigterm)
|
||||||
|
signal.signal(signal.SIGINT, process_manager.handle_sigint)
|
||||||
|
|
||||||
|
|
||||||
def get_number_of_workers(workers=None):
|
def get_number_of_workers(workers=None):
|
||||||
if workers == -1 or workers is None:
|
if workers == -1 or workers is None:
|
||||||
workers = (cpu_count() * 2) + 1
|
workers = (cpu_count() * 2) + 1
|
||||||
|
|
@ -78,11 +127,19 @@ def set_var_for_macos_issue() -> None:
|
||||||
logger.debug("Set OBJC_DISABLE_INITIALIZE_FORK_SAFETY to YES to avoid error")
|
logger.debug("Set OBJC_DISABLE_INITIALIZE_FORK_SAFETY to YES to avoid error")
|
||||||
|
|
||||||
|
|
||||||
def handle_sigterm(signum, frame): # noqa: ARG001
|
def wait_for_server_ready(host, port, protocol) -> None:
|
||||||
"""Handle SIGTERM signal gracefully."""
|
"""Wait for the server to become ready by polling the health endpoint."""
|
||||||
logger.info("Received SIGTERM signal. Performing graceful shutdown...")
|
status_code = 0
|
||||||
# Raise SystemExit to trigger graceful shutdown
|
while status_code != httpx.codes.OK:
|
||||||
sys.exit(0)
|
try:
|
||||||
|
status_code = httpx.get(
|
||||||
|
f"{protocol}://{host}:{port}/health", verify=host not in ("127.0.0.1", "localhost")
|
||||||
|
).status_code
|
||||||
|
except HTTPError:
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.opt(exception=True).debug("Error while waiting for the server to become ready.")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
|
@ -103,7 +160,11 @@ def run(
|
||||||
help="Path to the .env file containing environment variables.",
|
help="Path to the .env file containing environment variables.",
|
||||||
show_default=False,
|
show_default=False,
|
||||||
),
|
),
|
||||||
log_level: str | None = typer.Option(None, help="Logging level.", show_default=False),
|
log_level: str | None = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Logging level. One of: [debug, info, warning, error, critical]. Defaults to info.",
|
||||||
|
show_default=False,
|
||||||
|
),
|
||||||
log_file: Path | None = typer.Option(None, help="Path to the log file.", show_default=False),
|
log_file: Path | None = typer.Option(None, help="Path to the log file.", show_default=False),
|
||||||
cache: str | None = typer.Option( # noqa: ARG001
|
cache: str | None = typer.Option( # noqa: ARG001
|
||||||
None,
|
None,
|
||||||
|
|
@ -167,17 +228,33 @@ def run(
|
||||||
ssl_key_file_path: str | None = typer.Option(None, help="Defines the SSL key file path.", show_default=False),
|
ssl_key_file_path: str | None = typer.Option(None, help="Defines the SSL key file path.", show_default=False),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run Langflow."""
|
"""Run Langflow."""
|
||||||
# Register SIGTERM handler
|
|
||||||
signal.signal(signal.SIGTERM, handle_sigterm)
|
|
||||||
|
|
||||||
if env_file:
|
if env_file:
|
||||||
load_dotenv(env_file, override=True)
|
load_dotenv(env_file, override=True)
|
||||||
|
|
||||||
|
# Set default log level if not provided
|
||||||
|
log_level_str = "info" if log_level is None else log_level.lower()
|
||||||
|
|
||||||
|
# Must set as env var for child process to pick up
|
||||||
|
env_log_level = os.environ.get("LANGFLOW_LOG_LEVEL")
|
||||||
|
if env_log_level is None:
|
||||||
|
os.environ["LANGFLOW_LOG_LEVEL"] = log_level_str
|
||||||
|
else:
|
||||||
|
os.environ["LANGFLOW_LOG_LEVEL"] = env_log_level.lower()
|
||||||
|
|
||||||
configure(log_level=log_level, log_file=log_file)
|
configure(log_level=log_level, log_file=log_file)
|
||||||
|
|
||||||
|
# Create progress indicator (show verbose timing if log level is DEBUG)
|
||||||
|
verbose = log_level == "debug"
|
||||||
|
progress = create_langflow_progress(verbose=verbose)
|
||||||
|
|
||||||
|
# Step 0: Initializing Langflow
|
||||||
|
with progress.step(0):
|
||||||
logger.debug(f"Loading config from file: '{env_file}'" if env_file else "No env_file provided.")
|
logger.debug(f"Loading config from file: '{env_file}'" if env_file else "No env_file provided.")
|
||||||
set_var_for_macos_issue()
|
set_var_for_macos_issue()
|
||||||
settings_service = get_settings_service()
|
settings_service = get_settings_service()
|
||||||
|
|
||||||
|
# Step 1: Checking Environment
|
||||||
|
with progress.step(1):
|
||||||
for key, value in os.environ.items():
|
for key, value in os.environ.items():
|
||||||
new_key = key.replace("LANGFLOW_", "")
|
new_key = key.replace("LANGFLOW_", "")
|
||||||
if hasattr(settings_service.auth_settings, new_key):
|
if hasattr(settings_service.auth_settings, new_key):
|
||||||
|
|
@ -199,6 +276,7 @@ def run(
|
||||||
settings_service.auth_settings.set(arg, values[arg])
|
settings_service.auth_settings.set(arg, values[arg])
|
||||||
logger.debug(f"Loading config from cli parameter '{arg}': '{values[arg]}'")
|
logger.debug(f"Loading config from cli parameter '{arg}': '{values[arg]}'")
|
||||||
|
|
||||||
|
# Get final values from settings
|
||||||
host = settings_service.settings.host
|
host = settings_service.settings.host
|
||||||
port = settings_service.settings.port
|
port = settings_service.settings.port
|
||||||
workers = settings_service.settings.workers
|
workers = settings_service.settings.workers
|
||||||
|
|
@ -206,88 +284,92 @@ def run(
|
||||||
log_level = settings_service.settings.log_level
|
log_level = settings_service.settings.log_level
|
||||||
frontend_path = settings_service.settings.frontend_path
|
frontend_path = settings_service.settings.frontend_path
|
||||||
backend_only = settings_service.settings.backend_only
|
backend_only = settings_service.settings.backend_only
|
||||||
ssl_cert_file_path = settings_service.settings.ssl_cert_file if ssl_cert_file_path is None else ssl_cert_file_path
|
ssl_cert_file_path = (
|
||||||
|
settings_service.settings.ssl_cert_file if ssl_cert_file_path is None else ssl_cert_file_path
|
||||||
|
)
|
||||||
ssl_key_file_path = settings_service.settings.ssl_key_file if ssl_key_file_path is None else ssl_key_file_path
|
ssl_key_file_path = settings_service.settings.ssl_key_file if ssl_key_file_path is None else ssl_key_file_path
|
||||||
|
|
||||||
# create path object if frontend_path is provided
|
# create path object if frontend_path is provided
|
||||||
static_files_dir: Path | None = Path(frontend_path) if frontend_path else None
|
static_files_dir: Path | None = Path(frontend_path) if frontend_path else None
|
||||||
|
|
||||||
|
# Step 2: Starting Core Services
|
||||||
|
with progress.step(2):
|
||||||
app = setup_app(static_files_dir=static_files_dir, backend_only=backend_only)
|
app = setup_app(static_files_dir=static_files_dir, backend_only=backend_only)
|
||||||
|
|
||||||
|
# Step 3: Connecting Database (this happens inside setup_app via dependencies)
|
||||||
|
with progress.step(3):
|
||||||
# check if port is being used
|
# check if port is being used
|
||||||
if is_port_in_use(port, host):
|
if is_port_in_use(port, host):
|
||||||
port = get_free_port(port)
|
port = get_free_port(port)
|
||||||
|
|
||||||
|
protocol = "https" if ssl_cert_file_path and ssl_key_file_path else "http"
|
||||||
|
|
||||||
|
# Step 4: Loading Components (placeholder for components loading)
|
||||||
|
with progress.step(4):
|
||||||
|
pass # Components are loaded during app startup
|
||||||
|
|
||||||
|
# Step 5: Adding Starter Projects (placeholder for starter projects)
|
||||||
|
if get_settings_service().settings.create_starter_projects:
|
||||||
|
with progress.step(5):
|
||||||
|
pass # Starter projects are added during app startup
|
||||||
|
|
||||||
|
# Step 6: Launching Langflow
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
with progress.step(6):
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
# Print summary and banner before starting the server, since uvicorn is a blocking call.
|
||||||
|
# We _may_ be able to subprocess, but with window's spawn behavior, we'd have to move all
|
||||||
|
# non-picklable code to the subprocess.
|
||||||
|
progress.print_summary()
|
||||||
|
print_banner(host, port, protocol)
|
||||||
|
|
||||||
|
# Blocking call, so must be outside of the progress step
|
||||||
|
uvicorn.run(
|
||||||
|
app,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
log_level=log_level,
|
||||||
|
reload=False,
|
||||||
|
workers=get_number_of_workers(workers),
|
||||||
|
loop="asyncio",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
with progress.step(6):
|
||||||
|
# Use Gunicorn with LangflowUvicornWorker for non-Windows systems
|
||||||
|
from langflow.server import LangflowApplication
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
"bind": f"{host}:{port}",
|
"bind": f"{host}:{port}",
|
||||||
"workers": get_number_of_workers(workers),
|
"workers": get_number_of_workers(workers),
|
||||||
"timeout": worker_timeout,
|
"timeout": worker_timeout,
|
||||||
"certfile": ssl_cert_file_path,
|
"certfile": ssl_cert_file_path,
|
||||||
"keyfile": ssl_key_file_path,
|
"keyfile": ssl_key_file_path,
|
||||||
|
"log_level": log_level.lower(),
|
||||||
}
|
}
|
||||||
protocol = "https" if options["keyfile"] and options["certfile"] else "http"
|
server = LangflowApplication(app, options)
|
||||||
|
|
||||||
# Define an env variable to know if we are just testing the server
|
# Start the webapp process
|
||||||
if "pytest" in sys.modules:
|
process_manager.webapp_process = Process(target=server.run)
|
||||||
return
|
process_manager.webapp_process.start()
|
||||||
process: Process | None = None
|
|
||||||
try:
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
# Run using uvicorn on MacOS and Windows
|
|
||||||
# Windows doesn't support gunicorn
|
|
||||||
# MacOS requires an env variable to be set to use gunicorn
|
|
||||||
run_on_windows(host, port, log_level, options, app, protocol)
|
|
||||||
else:
|
|
||||||
# Run using gunicorn on Linux
|
|
||||||
process = run_on_mac_or_linux(host, port, log_level, options, app, protocol)
|
|
||||||
if open_browser and not backend_only:
|
|
||||||
browser_host = get_best_access_host(host, port)
|
|
||||||
click.launch(f"{protocol}://{browser_host}:{port}")
|
|
||||||
if process:
|
|
||||||
process.join()
|
|
||||||
except (KeyboardInterrupt, SystemExit) as e:
|
|
||||||
logger.info("Shutting down server...")
|
|
||||||
if process is not None:
|
|
||||||
process.terminate()
|
|
||||||
process.join(timeout=15) # Wait up to 15 seconds for process to terminate
|
|
||||||
if process.is_alive():
|
|
||||||
logger.warning("Process did not terminate gracefully, forcing...")
|
|
||||||
process.kill()
|
|
||||||
raise typer.Exit(0) from e
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(e)
|
|
||||||
if process is not None:
|
|
||||||
process.terminate()
|
|
||||||
raise typer.Exit(1) from e
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_server_ready(host, port, protocol) -> None:
|
|
||||||
"""Wait for the server to become ready by polling the health endpoint."""
|
|
||||||
status_code = 0
|
|
||||||
while status_code != httpx.codes.OK:
|
|
||||||
try:
|
|
||||||
status_code = httpx.get(
|
|
||||||
f"{protocol}://{host}:{port}/health", verify=host not in ("localhost", "127.0.0.1")
|
|
||||||
).status_code
|
|
||||||
except HTTPError:
|
|
||||||
time.sleep(1)
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
logger.opt(exception=True).debug("Error while waiting for the server to become ready.")
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
def run_on_mac_or_linux(host, port, log_level, options, app, protocol):
|
|
||||||
webapp_process = Process(target=run_langflow, args=(host, port, log_level, options, app))
|
|
||||||
webapp_process.start()
|
|
||||||
wait_for_server_ready(host, port, protocol)
|
wait_for_server_ready(host, port, protocol)
|
||||||
|
|
||||||
|
# Print summary and banner after server is ready
|
||||||
|
progress.print_summary()
|
||||||
print_banner(host, port, protocol)
|
print_banner(host, port, protocol)
|
||||||
return webapp_process
|
|
||||||
|
|
||||||
|
# Handle browser opening
|
||||||
|
if open_browser and not backend_only:
|
||||||
|
click.launch(f"{protocol}://{host}:{port}")
|
||||||
|
|
||||||
def run_on_windows(host, port, log_level, options, app, protocol) -> None:
|
try:
|
||||||
"""Run the Langflow server on Windows."""
|
process_manager.webapp_process.join()
|
||||||
print_banner(host, port, protocol)
|
except KeyboardInterrupt:
|
||||||
run_langflow(host, port, log_level, options, app)
|
# SIGINT should be handled by the signal handler, but leaving here for safety
|
||||||
|
logger.warning("KeyboardInterrupt caught in main thread")
|
||||||
|
finally:
|
||||||
|
process_manager.shutdown()
|
||||||
|
|
||||||
|
|
||||||
def is_port_in_use(port, host="localhost"):
|
def is_port_in_use(port, host="localhost"):
|
||||||
|
|
@ -466,9 +548,24 @@ def print_banner(host: str, port: int, protocol: str) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
title = f"[bold]Welcome to {styled_package_name}[/bold]\n"
|
title = f"[bold]Welcome to {styled_package_name}[/bold]\n"
|
||||||
|
|
||||||
|
# Use Windows-safe characters to prevent encoding issues
|
||||||
|
import platform
|
||||||
|
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
github_icon = "*"
|
||||||
|
discord_icon = "#"
|
||||||
|
arrow = "->"
|
||||||
|
status_icon = "[OK]"
|
||||||
|
else:
|
||||||
|
github_icon = ":star2:"
|
||||||
|
discord_icon = ":speech_balloon:"
|
||||||
|
arrow = "→"
|
||||||
|
status_icon = "🟢"
|
||||||
|
|
||||||
info_text = (
|
info_text = (
|
||||||
":star2: GitHub: Star for updates → https://github.com/langflow-ai/langflow\n"
|
f"{github_icon} GitHub: Star for updates {arrow} https://github.com/langflow-ai/langflow\n"
|
||||||
":speech_balloon: Discord: Join for support → https://discord.com/invite/EqksyE2EX9"
|
f"{discord_icon} Discord: Join for support {arrow} https://discord.com/invite/EqksyE2EX9"
|
||||||
)
|
)
|
||||||
telemetry_text = (
|
telemetry_text = (
|
||||||
(
|
(
|
||||||
|
|
@ -482,54 +579,13 @@ def print_banner(host: str, port: int, protocol: str) -> None:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
access_host = get_best_access_host(host, port)
|
access_host = get_best_access_host(host, port)
|
||||||
access_link = f"[bold]🟢 Open Langflow →[/bold] [link={protocol}://{access_host}:{port}]{protocol}://{access_host}:{port}[/link]"
|
access_link = f"[bold]{status_icon} Open Langflow {arrow}[/bold] [link={protocol}://{access_host}:{port}]{protocol}://{access_host}:{port}[/link]"
|
||||||
|
|
||||||
message = f"{title}\n{info_text}\n\n{telemetry_text}\n\n{access_link}"
|
message = f"{title}\n{info_text}\n\n{telemetry_text}\n\n{access_link}"
|
||||||
|
|
||||||
console.print(Panel.fit(message, border_style="#7528FC", padding=(1, 2)))
|
console.print(Panel.fit(message, border_style="#7528FC", padding=(1, 2)))
|
||||||
|
|
||||||
|
|
||||||
def run_langflow(host, port, log_level, options, app) -> None:
|
|
||||||
"""Run Langflow server on localhost."""
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
uvicorn.run(
|
|
||||||
app,
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
log_level=log_level.lower(),
|
|
||||||
loop="asyncio",
|
|
||||||
ssl_keyfile=options["keyfile"],
|
|
||||||
ssl_certfile=options["certfile"],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
from langflow.server import LangflowApplication
|
|
||||||
|
|
||||||
server = LangflowApplication(app, options)
|
|
||||||
|
|
||||||
def graceful_shutdown(signum, frame): # noqa: ARG001
|
|
||||||
"""Gracefully shutdown the server when receiving SIGTERM."""
|
|
||||||
# Suppress click exceptions during shutdown
|
|
||||||
import click
|
|
||||||
|
|
||||||
click.echo = lambda *args, **kwargs: None # noqa: ARG005
|
|
||||||
|
|
||||||
logger.info("Gracefully shutting down server...")
|
|
||||||
# For Gunicorn workers, we raise SystemExit to trigger graceful shutdown
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
# Register signal handlers
|
|
||||||
signal.signal(signal.SIGTERM, graceful_shutdown)
|
|
||||||
signal.signal(signal.SIGINT, graceful_shutdown)
|
|
||||||
|
|
||||||
try:
|
|
||||||
server.run()
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
|
||||||
# Suppress the exception output
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def superuser(
|
def superuser(
|
||||||
username: str = typer.Option(..., prompt=True, help="Username for the superuser."),
|
username: str = typer.Option(..., prompt=True, help="Username for the superuser."),
|
||||||
|
|
|
||||||
0
src/backend/base/langflow/cli/__init__.py
Normal file
0
src/backend/base/langflow/cli/__init__.py
Normal file
225
src/backend/base/langflow/cli/progress.py
Normal file
225
src/backend/base/langflow/cli/progress.py
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from collections.abc import Generator
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
MIN_DURATION_THRESHOLD = 0.1 # Minimum duration to show in seconds (100ms)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressIndicator:
|
||||||
|
"""A CLI progress indicator that shows user-friendly step-by-step progress.
|
||||||
|
|
||||||
|
Shows animated loading indicators (□ → ■) for each step of the initialization process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, verbose: bool = False):
|
||||||
|
self.verbose = verbose
|
||||||
|
self.steps: list[dict[str, Any]] = []
|
||||||
|
self.current_step = 0
|
||||||
|
self.running = False
|
||||||
|
self._stop_animation = False
|
||||||
|
self._animation_thread: threading.Thread | None = None
|
||||||
|
|
||||||
|
# Use Windows-safe characters on Windows to prevent encoding issues
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
self._animation_chars = ["-", "\\", "|", "/"] # ASCII spinner
|
||||||
|
self._success_icon = "+" # ASCII plus sign
|
||||||
|
self._failure_icon = "x" # ASCII x
|
||||||
|
self._farewell_emoji = ":)" # ASCII smiley
|
||||||
|
else:
|
||||||
|
self._animation_chars = ["□", "▢", "▣", "■"] # Unicode squares
|
||||||
|
self._success_icon = "✓" # Unicode checkmark
|
||||||
|
self._failure_icon = "✗" # Unicode cross
|
||||||
|
self._farewell_emoji = "👋" # Unicode wave
|
||||||
|
|
||||||
|
self._animation_index = 0
|
||||||
|
|
||||||
|
def add_step(self, title: str, description: str = "") -> None:
|
||||||
|
"""Add a step to the progress indicator."""
|
||||||
|
self.steps.append(
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"status": "pending", # pending, running, completed, failed
|
||||||
|
"start_time": None,
|
||||||
|
"end_time": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _animate_step(self, step_index: int) -> None:
|
||||||
|
"""Animate the current step with rotating square characters."""
|
||||||
|
if step_index >= len(self.steps):
|
||||||
|
return
|
||||||
|
|
||||||
|
step = self.steps[step_index]
|
||||||
|
|
||||||
|
while self.running and step["status"] == "running" and not self._stop_animation:
|
||||||
|
# Clear the current line and move cursor to beginning
|
||||||
|
sys.stdout.write("\r")
|
||||||
|
|
||||||
|
# Show the animated character
|
||||||
|
animation_char = self._animation_chars[self._animation_index]
|
||||||
|
|
||||||
|
# Print the step with animation
|
||||||
|
line = f"{animation_char} {step['title']}..."
|
||||||
|
sys.stdout.write(line)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# Update animation
|
||||||
|
self._animation_index = (self._animation_index + 1) % len(self._animation_chars)
|
||||||
|
|
||||||
|
time.sleep(0.15) # Animation speed
|
||||||
|
|
||||||
|
def start_step(self, step_index: int) -> None:
|
||||||
|
"""Start a specific step and begin animation."""
|
||||||
|
if step_index >= len(self.steps):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.current_step = step_index
|
||||||
|
step = self.steps[step_index]
|
||||||
|
step["status"] = "running"
|
||||||
|
step["start_time"] = time.time()
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self._stop_animation = False
|
||||||
|
|
||||||
|
# Start animation in a separate thread
|
||||||
|
self._animation_thread = threading.Thread(target=self._animate_step, args=(step_index,))
|
||||||
|
self._animation_thread.daemon = True
|
||||||
|
self._animation_thread.start()
|
||||||
|
|
||||||
|
def complete_step(self, step_index: int, *, success: bool = True) -> None:
|
||||||
|
"""Complete a step and stop its animation."""
|
||||||
|
if step_index >= len(self.steps):
|
||||||
|
return
|
||||||
|
|
||||||
|
step = self.steps[step_index]
|
||||||
|
step["status"] = "completed" if success else "failed"
|
||||||
|
step["end_time"] = time.time()
|
||||||
|
|
||||||
|
# Stop animation
|
||||||
|
self._stop_animation = True
|
||||||
|
if self._animation_thread and self._animation_thread.is_alive():
|
||||||
|
self._animation_thread.join(timeout=0.5)
|
||||||
|
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
# Clear the current line and print final result
|
||||||
|
sys.stdout.write("\r")
|
||||||
|
|
||||||
|
if success:
|
||||||
|
icon = click.style(self._success_icon, fg="green", bold=True)
|
||||||
|
title = click.style(step["title"], fg="green")
|
||||||
|
else:
|
||||||
|
icon = click.style(self._failure_icon, fg="red", bold=True)
|
||||||
|
title = click.style(step["title"], fg="red")
|
||||||
|
|
||||||
|
duration = ""
|
||||||
|
if step["start_time"] and step["end_time"]:
|
||||||
|
elapsed = step["end_time"] - step["start_time"]
|
||||||
|
if self.verbose and elapsed > MIN_DURATION_THRESHOLD: # Only show duration if verbose and > 100ms
|
||||||
|
duration = click.style(f" ({elapsed:.2f}s)", fg="bright_black")
|
||||||
|
|
||||||
|
line = f"{icon} {title}{duration}"
|
||||||
|
click.echo(line)
|
||||||
|
|
||||||
|
def fail_step(self, step_index: int, error_msg: str = "") -> None:
|
||||||
|
"""Mark a step as failed."""
|
||||||
|
self.complete_step(step_index, success=False)
|
||||||
|
if error_msg and self.verbose:
|
||||||
|
click.echo(click.style(f" Error: {error_msg}", fg="red"))
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def step(self, step_index: int) -> Generator[None, None, None]:
|
||||||
|
"""Context manager for running a step with automatic completion."""
|
||||||
|
try:
|
||||||
|
self.start_step(step_index)
|
||||||
|
yield
|
||||||
|
self.complete_step(step_index, success=True)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e) if self.verbose else ""
|
||||||
|
self.fail_step(step_index, error_msg)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def print_summary(self) -> None:
|
||||||
|
"""Print a summary of all completed steps."""
|
||||||
|
if not self.verbose:
|
||||||
|
return
|
||||||
|
|
||||||
|
completed_steps = [s for s in self.steps if s["status"] in ["completed", "failed"]]
|
||||||
|
if not completed_steps:
|
||||||
|
return
|
||||||
|
|
||||||
|
total_time = sum(
|
||||||
|
(s["end_time"] - s["start_time"]) for s in completed_steps if s["start_time"] and s["end_time"]
|
||||||
|
)
|
||||||
|
|
||||||
|
click.echo()
|
||||||
|
click.echo(click.style(f"Total initialization time: {total_time:.2f}s", fg="bright_black"))
|
||||||
|
|
||||||
|
def print_shutdown_summary(self) -> None:
|
||||||
|
"""Print a summary of all completed shutdown steps."""
|
||||||
|
if not self.verbose:
|
||||||
|
return
|
||||||
|
|
||||||
|
completed_steps = [s for s in self.steps if s["status"] in ["completed", "failed"]]
|
||||||
|
if not completed_steps:
|
||||||
|
return
|
||||||
|
|
||||||
|
total_time = sum(
|
||||||
|
(s["end_time"] - s["start_time"]) for s in completed_steps if s["start_time"] and s["end_time"]
|
||||||
|
)
|
||||||
|
|
||||||
|
click.echo()
|
||||||
|
click.echo(click.style(f"Total shutdown time: {total_time:.2f}s", fg="bright_black"))
|
||||||
|
|
||||||
|
def print_farewell_message(self) -> None:
|
||||||
|
"""Print a nice farewell message after shutdown is complete."""
|
||||||
|
click.echo()
|
||||||
|
farewell = click.style(f"{self._farewell_emoji} See you next time!", fg="bright_blue", bold=True)
|
||||||
|
click.echo(farewell)
|
||||||
|
|
||||||
|
|
||||||
|
def create_langflow_progress(*, verbose: bool = False) -> ProgressIndicator:
|
||||||
|
"""Create a progress indicator with predefined Langflow initialization steps."""
|
||||||
|
progress = ProgressIndicator(verbose=verbose)
|
||||||
|
|
||||||
|
# Define the initialization steps matching the order in main.py
|
||||||
|
steps = [
|
||||||
|
("Initializing Langflow", "Setting up basic configuration"),
|
||||||
|
("Checking Environment", "Loading environment variables and settings"),
|
||||||
|
("Starting Core Services", "Initializing database and core services"),
|
||||||
|
("Connecting Database", "Setting up database connection and migrations"),
|
||||||
|
("Loading Components", "Caching component types and custom components"),
|
||||||
|
("Adding Starter Projects", "Creating or updating starter project templates"),
|
||||||
|
("Launching Langflow", "Starting server and final setup"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for title, description in steps:
|
||||||
|
progress.add_step(title, description)
|
||||||
|
|
||||||
|
return progress
|
||||||
|
|
||||||
|
|
||||||
|
def create_langflow_shutdown_progress(*, verbose: bool = False) -> ProgressIndicator:
|
||||||
|
"""Create a progress indicator with predefined Langflow shutdown steps."""
|
||||||
|
progress = ProgressIndicator(verbose=verbose)
|
||||||
|
|
||||||
|
# Define the shutdown steps in reverse order of initialization
|
||||||
|
steps = [
|
||||||
|
("Stopping Server", "Gracefully stopping the web server"),
|
||||||
|
("Cancelling Background Tasks", "Stopping file synchronization and background jobs"),
|
||||||
|
("Cleaning Up Services", "Teardown database connections and services"),
|
||||||
|
("Clearing Temporary Files", "Removing temporary directories and cache"),
|
||||||
|
("Finalizing Shutdown", "Completing cleanup and logging"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for title, description in steps:
|
||||||
|
progress.add_step(title, description)
|
||||||
|
|
||||||
|
return progress
|
||||||
|
|
@ -536,6 +536,7 @@ def build_custom_components(components_paths: list[str]):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
logger.info(f"Building custom components from {components_paths}")
|
logger.info(f"Building custom components from {components_paths}")
|
||||||
|
|
||||||
custom_components_from_file: dict = {}
|
custom_components_from_file: dict = {}
|
||||||
processed_paths = set()
|
processed_paths = set()
|
||||||
for path in components_paths:
|
for path in components_paths:
|
||||||
|
|
@ -546,7 +547,7 @@ def build_custom_components(components_paths: list[str]):
|
||||||
custom_component_dict = build_custom_component_list_from_path(path_str)
|
custom_component_dict = build_custom_component_list_from_path(path_str)
|
||||||
if custom_component_dict:
|
if custom_component_dict:
|
||||||
category = next(iter(custom_component_dict))
|
category = next(iter(custom_component_dict))
|
||||||
logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
|
logger.debug(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
|
||||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||||
custom_components_from_file, custom_component_dict
|
custom_components_from_file, custom_component_dict
|
||||||
)
|
)
|
||||||
|
|
@ -560,7 +561,7 @@ async def abuild_custom_components(components_paths: list[str]):
|
||||||
if not components_paths:
|
if not components_paths:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
logger.info(f"Building custom components from {components_paths}")
|
logger.debug(f"Building custom components from {components_paths}")
|
||||||
custom_components_from_file: dict = {}
|
custom_components_from_file: dict = {}
|
||||||
processed_paths = set()
|
processed_paths = set()
|
||||||
for path in components_paths:
|
for path in components_paths:
|
||||||
|
|
@ -571,7 +572,7 @@ async def abuild_custom_components(components_paths: list[str]):
|
||||||
custom_component_dict = await abuild_custom_component_list_from_path(path_str)
|
custom_component_dict = await abuild_custom_component_list_from_path(path_str)
|
||||||
if custom_component_dict:
|
if custom_component_dict:
|
||||||
category = next(iter(custom_component_dict))
|
category = next(iter(custom_component_dict))
|
||||||
logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
|
logger.debug(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
|
||||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||||
custom_components_from_file, custom_component_dict
|
custom_components_from_file, custom_component_dict
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1015,7 +1015,7 @@ async def initialize_super_user_if_needed() -> None:
|
||||||
super_user = await create_super_user(db=async_session, username=username, password=password)
|
super_user = await create_super_user(db=async_session, username=username, password=password)
|
||||||
await get_variable_service().initialize_user_variables(super_user.id, async_session)
|
await get_variable_service().initialize_user_variables(super_user.id, async_session)
|
||||||
_ = await get_or_create_default_folder(async_session, super_user.id)
|
_ = await get_or_create_default_folder(async_session, super_user.id)
|
||||||
logger.info("Super user initialized")
|
logger.debug("Super user initialized")
|
||||||
|
|
||||||
|
|
||||||
async def get_or_create_default_folder(session: AsyncSession, user_id: UUID) -> FolderRead:
|
async def get_or_create_default_folder(session: AsyncSession, user_id: UUID) -> FolderRead:
|
||||||
|
|
@ -1058,6 +1058,7 @@ async def get_or_create_default_folder(session: AsyncSession, user_id: UUID) ->
|
||||||
async def sync_flows_from_fs():
|
async def sync_flows_from_fs():
|
||||||
flow_mtimes = {}
|
flow_mtimes = {}
|
||||||
fs_flows_polling_interval = get_settings_service().settings.fs_flows_polling_interval / 1000
|
fs_flows_polling_interval = get_settings_service().settings.fs_flows_polling_interval / 1000
|
||||||
|
try:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with session_scope() as session:
|
async with session_scope() as session:
|
||||||
|
|
@ -1084,6 +1085,18 @@ async def sync_flows_from_fs():
|
||||||
flow_mtimes[flow.id] = new_mtime
|
flow_mtimes[flow.id] = new_mtime
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
logger.exception(f"Error while handling flow file {path}")
|
logger.exception(f"Error while handling flow file {path}")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Flow sync cancelled")
|
||||||
|
break
|
||||||
|
except (sa.exc.OperationalError, ValueError) as e:
|
||||||
|
if "no active connection" in str(e) or "connection is closed" in str(e):
|
||||||
|
logger.debug("Database connection lost, assuming shutdown")
|
||||||
|
break # Exit gracefully, don't error
|
||||||
|
raise # Re-raise if it's a real connection problem
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
logger.exception("Error while syncing flows from database")
|
logger.exception("Error while syncing flows from database")
|
||||||
|
break
|
||||||
|
|
||||||
await asyncio.sleep(fs_flows_polling_interval)
|
await asyncio.sleep(fs_flows_polling_interval)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Flow sync task cancelled")
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from urllib.parse import urlencode
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import httpx
|
import httpx
|
||||||
|
import sqlalchemy
|
||||||
from fastapi import FastAPI, HTTPException, Request, Response, status
|
from fastapi import FastAPI, HTTPException, Request, Response, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
|
|
@ -124,6 +125,7 @@ def get_lifespan(*, fix_migration=False, version=None):
|
||||||
|
|
||||||
temp_dirs: list[TemporaryDirectory] = []
|
temp_dirs: list[TemporaryDirectory] = []
|
||||||
sync_flows_from_fs_task = None
|
sync_flows_from_fs_task = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start_time = asyncio.get_event_loop().time()
|
start_time = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
|
|
@ -180,26 +182,69 @@ def get_lifespan(*, fix_migration=False, version=None):
|
||||||
logger.debug(f"Total initialization time: {total_time:.2f}s")
|
logger.debug(f"Total initialization time: {total_time:.2f}s")
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Lifespan received cancellation signal")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if "langflow migration --fix" not in str(exc):
|
if "langflow migration --fix" not in str(exc):
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
# Clean shutdown
|
# Clean shutdown with progress indicator
|
||||||
logger.info("Cleaning up resources...")
|
# Create shutdown progress (show verbose timing if log level is DEBUG)
|
||||||
|
from langflow.cli.progress import create_langflow_shutdown_progress
|
||||||
|
|
||||||
|
log_level = os.getenv("LANGFLOW_LOG_LEVEL", "info").lower()
|
||||||
|
shutdown_progress = create_langflow_shutdown_progress(verbose=log_level == "debug")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 0: Stopping Server
|
||||||
|
with shutdown_progress.step(0):
|
||||||
|
logger.debug("Stopping server gracefully...")
|
||||||
|
# The actual server stopping is handled by the lifespan context
|
||||||
|
await asyncio.sleep(0.1) # Brief pause for visual effect
|
||||||
|
|
||||||
|
# Step 1: Cancelling Background Tasks
|
||||||
|
with shutdown_progress.step(1):
|
||||||
if sync_flows_from_fs_task:
|
if sync_flows_from_fs_task:
|
||||||
sync_flows_from_fs_task.cancel()
|
sync_flows_from_fs_task.cancel()
|
||||||
await asyncio.wait([sync_flows_from_fs_task])
|
await asyncio.wait([sync_flows_from_fs_task])
|
||||||
await teardown_services()
|
|
||||||
|
|
||||||
await asyncio.sleep(0.1) # let logger flush async logs
|
# Step 2: Cleaning Up Services
|
||||||
await logger.complete()
|
with shutdown_progress.step(2):
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(teardown_services(), timeout=10)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning("Teardown services timed out.")
|
||||||
|
|
||||||
|
# Step 3: Clearing Temporary Files
|
||||||
|
with shutdown_progress.step(3):
|
||||||
temp_dir_cleanups = [asyncio.to_thread(temp_dir.cleanup) for temp_dir in temp_dirs]
|
temp_dir_cleanups = [asyncio.to_thread(temp_dir.cleanup) for temp_dir in temp_dirs]
|
||||||
await asyncio.gather(*temp_dir_cleanups)
|
await asyncio.gather(*temp_dir_cleanups)
|
||||||
# Final message
|
|
||||||
|
# Step 4: Finalizing Shutdown
|
||||||
|
with shutdown_progress.step(4):
|
||||||
logger.debug("Langflow shutdown complete")
|
logger.debug("Langflow shutdown complete")
|
||||||
|
|
||||||
|
# Show completion summary and farewell
|
||||||
|
shutdown_progress.print_shutdown_summary()
|
||||||
|
shutdown_progress.print_farewell_message()
|
||||||
|
|
||||||
|
except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.DBAPIError) as e:
|
||||||
|
# Case where the database connection is closed during shutdown
|
||||||
|
logger.warning(f"Database teardown failed due to closed connection: {e}")
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# Swallow this - it's normal during shutdown
|
||||||
|
logger.debug("Teardown cancelled during shutdown.")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.exception(f"Unhandled error during cleanup: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.shield(asyncio.sleep(0.1)) # let logger flush async logs
|
||||||
|
await asyncio.shield(logger.complete())
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# Cancellation during logger flush is possible during shutdown, so we swallow it
|
||||||
|
pass
|
||||||
|
|
||||||
return lifespan
|
return lifespan
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -369,7 +414,6 @@ def get_static_files_dir():
|
||||||
def setup_app(static_files_dir: Path | None = None, *, backend_only: bool = False) -> FastAPI:
|
def setup_app(static_files_dir: Path | None = None, *, backend_only: bool = False) -> FastAPI:
|
||||||
"""Setup the FastAPI app."""
|
"""Setup the FastAPI app."""
|
||||||
# get the directory of the current file
|
# get the directory of the current file
|
||||||
logger.info(f"Setting up app with static files directory {static_files_dir}")
|
|
||||||
if not static_files_dir:
|
if not static_files_dir:
|
||||||
static_files_dir = get_static_files_dir()
|
static_files_dir = get_static_files_dir()
|
||||||
|
|
||||||
|
|
@ -377,6 +421,7 @@ def setup_app(static_files_dir: Path | None = None, *, backend_only: bool = Fals
|
||||||
msg = f"Static files directory {static_files_dir} does not exist."
|
msg = f"Static files directory {static_files_dir} does not exist."
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
if not backend_only and static_files_dir is not None:
|
if not backend_only and static_files_dir is not None:
|
||||||
setup_static_files(app, static_files_dir)
|
setup_static_files(app, static_files_dir)
|
||||||
return app
|
return app
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ class LangflowUvicornWorker(UvicornWorker):
|
||||||
|
|
||||||
def handle_exit(self, sig, frame):
|
def handle_exit(self, sig, frame):
|
||||||
if not self._has_exited:
|
if not self._has_exited:
|
||||||
print("👋 See you next time!") # noqa: T201
|
|
||||||
self._has_exited = True
|
self._has_exited = True
|
||||||
|
|
||||||
super().handle_exit(sig, frame)
|
super().handle_exit(sig, frame)
|
||||||
|
|
@ -46,6 +45,9 @@ class Logger(glogging.Logger):
|
||||||
|
|
||||||
def __init__(self, cfg) -> None:
|
def __init__(self, cfg) -> None:
|
||||||
super().__init__(cfg)
|
super().__init__(cfg)
|
||||||
|
logging.getLogger("gunicorn.error").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("gunicorn.access").setLevel(logging.WARNING)
|
||||||
|
|
||||||
logging.getLogger("gunicorn.error").handlers = [InterceptHandler()]
|
logging.getLogger("gunicorn.error").handlers = [InterceptHandler()]
|
||||||
logging.getLogger("gunicorn.access").handlers = [InterceptHandler()]
|
logging.getLogger("gunicorn.access").handlers = [InterceptHandler()]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,6 @@ class DatabaseService(Service):
|
||||||
command.ensure_version(alembic_cfg)
|
command.ensure_version(alembic_cfg)
|
||||||
# alembic_cfg.attributes["connection"].commit()
|
# alembic_cfg.attributes["connection"].commit()
|
||||||
command.upgrade(alembic_cfg, "head")
|
command.upgrade(alembic_cfg, "head")
|
||||||
logger.info("Alembic initialized")
|
|
||||||
|
|
||||||
def _run_migrations(self, should_initialize_alembic, fix) -> None:
|
def _run_migrations(self, should_initialize_alembic, fix) -> None:
|
||||||
# First we need to check if alembic has been initialized
|
# First we need to check if alembic has been initialized
|
||||||
|
|
@ -336,9 +335,7 @@ class DatabaseService(Service):
|
||||||
logger.exception(msg)
|
logger.exception(msg)
|
||||||
raise RuntimeError(msg) from exc
|
raise RuntimeError(msg) from exc
|
||||||
else:
|
else:
|
||||||
logger.info("Alembic initialized")
|
logger.debug("Alembic initialized")
|
||||||
|
|
||||||
logger.info(f"Running DB migrations in {self.script_location}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
buffer.write(f"{datetime.now(tz=timezone.utc).astimezone().isoformat()}: Checking migrations\n")
|
buffer.write(f"{datetime.now(tz=timezone.utc).astimezone().isoformat()}: Checking migrations\n")
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ class ServiceManager:
|
||||||
|
|
||||||
async def teardown(self) -> None:
|
async def teardown(self) -> None:
|
||||||
"""Teardown all the services."""
|
"""Teardown all the services."""
|
||||||
for service in self.services.values():
|
for service in list(self.services.values()):
|
||||||
if service is None:
|
if service is None:
|
||||||
continue
|
continue
|
||||||
logger.debug(f"Teardown service {service.name}")
|
logger.debug(f"Teardown service {service.name}")
|
||||||
|
|
|
||||||
|
|
@ -429,8 +429,14 @@ class Settings(BaseSettings):
|
||||||
logger.debug(f"Appending {langflow_component_path} to components_path")
|
logger.debug(f"Appending {langflow_component_path} to components_path")
|
||||||
|
|
||||||
if not value:
|
if not value:
|
||||||
value = []
|
value = [BASE_COMPONENTS_PATH]
|
||||||
logger.debug("Setting empty components path")
|
logger.debug("Setting default components path to components_path")
|
||||||
|
else:
|
||||||
|
if isinstance(value, Path):
|
||||||
|
value = [str(value)]
|
||||||
|
elif isinstance(value, list):
|
||||||
|
value = [str(p) if isinstance(p, Path) else p for p in value]
|
||||||
|
logger.debug("Adding default components path to components_path")
|
||||||
|
|
||||||
logger.debug(f"Components path: {value}")
|
logger.debug(f"Components path: {value}")
|
||||||
return value
|
return value
|
||||||
|
|
|
||||||
|
|
@ -131,17 +131,12 @@ async def teardown_superuser(settings_service, session: AsyncSession) -> None:
|
||||||
|
|
||||||
async def teardown_services() -> None:
|
async def teardown_services() -> None:
|
||||||
"""Teardown all the services."""
|
"""Teardown all the services."""
|
||||||
try:
|
|
||||||
async with get_db_service().with_session() as session:
|
async with get_db_service().with_session() as session:
|
||||||
await teardown_superuser(get_settings_service(), session)
|
await teardown_superuser(get_settings_service(), session)
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
logger.exception(exc)
|
|
||||||
try:
|
|
||||||
from langflow.services.manager import service_manager
|
from langflow.services.manager import service_manager
|
||||||
|
|
||||||
await service_manager.teardown()
|
await service_manager.teardown()
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
logger.exception(exc)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_settings_service() -> None:
|
def initialize_settings_service() -> None:
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,9 @@ class DatabaseVariableService(VariableService, Service):
|
||||||
|
|
||||||
async def initialize_user_variables(self, user_id: UUID | str, session: AsyncSession) -> None:
|
async def initialize_user_variables(self, user_id: UUID | str, session: AsyncSession) -> None:
|
||||||
if not self.settings_service.settings.store_environment_variables:
|
if not self.settings_service.settings.store_environment_variables:
|
||||||
logger.info("Skipping environment variable storage.")
|
logger.debug("Skipping environment variable storage.")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info("Storing environment variables in the database.")
|
|
||||||
for var_name in self.settings_service.settings.variables_to_get_from_environment:
|
for var_name in self.settings_service.settings.variables_to_get_from_environment:
|
||||||
if var_name in os.environ and os.environ[var_name].strip():
|
if var_name in os.environ and os.environ[var_name].strip():
|
||||||
value = os.environ[var_name].strip()
|
value = os.environ[var_name].strip()
|
||||||
|
|
@ -50,7 +49,7 @@ class DatabaseVariableService(VariableService, Service):
|
||||||
type_=CREDENTIAL_TYPE,
|
type_=CREDENTIAL_TYPE,
|
||||||
session=session,
|
session=session,
|
||||||
)
|
)
|
||||||
logger.info(f"Processed {var_name} variable from environment.")
|
logger.debug(f"Processed {var_name} variable from environment.")
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.exception(f"Error processing {var_name} variable: {e!s}")
|
logger.exception(f"Error processing {var_name} variable: {e!s}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -236,10 +236,10 @@ audio = [
|
||||||
]
|
]
|
||||||
|
|
||||||
postgresql = [
|
postgresql = [
|
||||||
"sqlalchemy[postgresql_psycopg2binary]",
|
"sqlalchemy[postgresql_psycopg2binary]>=2.0.38,<3.0.0",
|
||||||
"sqlalchemy[postgresql_psycopg]",
|
"sqlalchemy[postgresql_psycopg]>=2.0.38,<3.0.0",
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
local = [
|
local = [
|
||||||
"llama-cpp-python>=0.2.0",
|
"llama-cpp-python>=0.2.0",
|
||||||
"sentence-transformers>=2.0.0",
|
"sentence-transformers>=2.0.0",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue