From dad4c10c8b06ce72d8d808ceea95c7d8efa4918d Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 12 Aug 2026 21:52:09 +0200 Subject: [PATCH] fix: stream ansible playbook logs in real time (#10669) Co-authored-by: Claude Opus 5 (1M context) --- backend/windmill-worker/nsjail/run.ansible.config.proto | 3 +++ backend/windmill-worker/src/ansible_executor.rs | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/backend/windmill-worker/nsjail/run.ansible.config.proto b/backend/windmill-worker/nsjail/run.ansible.config.proto index afaf066f17..ce5dab1dff 100644 --- a/backend/windmill-worker/nsjail/run.ansible.config.proto +++ b/backend/windmill-worker/nsjail/run.ansible.config.proto @@ -175,3 +175,6 @@ envar: "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" envar: "PYTHONPATH={ADDITIONAL_PYTHON_PATHS}" envar: "HOME=/tmp" envar: "ANSIBLE_CONFIG=/tmp/ansible.cfg" +# Ansible never flushes its own stdout; on the pipe Windmill gives it, python block-buffers +# and the job log arrives in bursts instead of as tasks run. Mirrors ansible_executor.rs. +envar: "PYTHONUNBUFFERED=1" diff --git a/backend/windmill-worker/src/ansible_executor.rs b/backend/windmill-worker/src/ansible_executor.rs index 5e151824f0..05bcf930fb 100644 --- a/backend/windmill-worker/src/ansible_executor.rs +++ b/backend/windmill-worker/src/ansible_executor.rs @@ -56,6 +56,12 @@ const WINDMILL_ANSIBLE_PASSWORD_FILENAME: &str = ".windmill.ansible_vault_passwo const DELEGATE_GIT_REPO_TARGET: &str = "delegate_git_repository"; +/// Ansible's `Display` writes to `sys.stdout` and never flushes, relying on a terminal being +/// line-buffered. Windmill hands it a pipe, where python block-buffers instead, so without this +/// the job log arrives in bursts rather than as tasks run. No ansible.cfg knob covers it. +/// The nsjail path sets the same var in `nsjail/run.ansible.config.proto`. +const PYTHONUNBUFFERED_ENV: &str = "PYTHONUNBUFFERED"; + /// Usable bytes in `sockaddr_un.sun_path` (108 minus the NUL). An ABI constant, not a /// filesystem limit — which is why only the socket breaks while every regular file in the /// same job dir is fine. @@ -662,6 +668,7 @@ async fn run_galaxy_install_from_requirements( galaxy_roles_cmd .current_dir(job_dir) .env_clear() + .env(PYTHONUNBUFFERED_ENV, "1") .envs(PROXY_ENVS.clone()) .env("PATH", PATH_ENV.as_str()) .env("TZ", TZ_ENV.as_str()) @@ -700,6 +707,7 @@ async fn run_galaxy_install_from_requirements( galaxy_collections_cmd .current_dir(job_dir) .env_clear() + .env(PYTHONUNBUFFERED_ENV, "1") .envs(PROXY_ENVS.clone()) .env("PATH", PATH_ENV.as_str()) .env("TZ", TZ_ENV.as_str()) @@ -1888,6 +1896,7 @@ fi ansible_cmd .current_dir(job_dir) .env_clear() + .env(PYTHONUNBUFFERED_ENV, "1") .envs(envs) .envs(reserved_variables) .env("PATH", PATH_ENV.as_str())