windmill-labs/windmill · error

OpenProcess({pid}): {e}

Error message

OpenProcess({pid}): {e}

What it means

assign_job_object opens the child process by PID with PROCESS_SET_QUOTA | PROCESS_TERMINATE rights so it can be bound to the job object. If OpenProcess fails (most commonly the process already exited, or access is denied), the job handle is closed and this io::Error is returned including the pid.

Source

Thrown at backend/windmill-worker/src/common.rs:903

        SetInformationJobObject(
            job,
            JobObjectExtendedLimitInformation,
            &info as *const _ as _,
            std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
        )
        .map_err(|e| {
            let _ = windows::Win32::Foundation::CloseHandle(job);
            std::io::Error::new(
                std::io::ErrorKind::Other,
                format!("SetInformationJobObject: {e}"),
            )
        })?;

        let process_handle = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
            .map_err(|e| {
                let _ = windows::Win32::Foundation::CloseHandle(job);
                std::io::Error::new(
                    std::io::ErrorKind::Other,
                    format!("OpenProcess({pid}): {e}"),
                )
            })?;

        let assign_result = AssignProcessToJobObject(job, process_handle);
        let _ = windows::Win32::Foundation::CloseHandle(process_handle);
        assign_result.map_err(|e| {
            let _ = windows::Win32::Foundation::CloseHandle(job);
            std::io::Error::new(
                std::io::ErrorKind::Other,
                format!("AssignProcessToJobObject: {e}"),
            )
        })?;

        Ok(Win32JobHandle(job))
    }
}

View on GitHub (pinned to e474e8803c)

Solutions

  1. Check the pid in the message: if the process exited, look for the child's own startup failure (missing binary/DLL, bad script) — the OpenProcess error is a symptom.
  2. Ensure the worker runs as the same or a more privileged user than the child; grant PROCESS_SET_QUOTA/PROCESS_TERMINATE access.
  3. Reduce time-to-assign or handle spawn failures earlier so dead pids are not passed to assign_job_object.
  4. Retry the job if it was a transient crash.
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check: confirm the child is still alive before assigning it to the job object
fn process_alive(pid: u32) -> bool {
    std::process::Command::new("tasklist")
        .args(["/FI", &format!("PID eq {pid}"), "/NH"])
        .output()
        .map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string()))
        .unwrap_or(false)
}

Try / catch

match assign_job_object(pid, true) {
    Err(e) if e.to_string().contains(&format!("OpenProcess({pid})")) => {
        // child already exited — treat as failed spawn, surface child startup logs
        Err(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("child {pid} exited before job assignment: {e}"),
        ))
    }
    other => other,
}

Prevention

When it happens

Trigger: assign_job_object called with a pid whose process has already terminated (race between spawn suspension and job assignment) or which the worker account cannot open with the required access rights.

Common situations: Child process crashing instantly on startup (bad interpreter, missing DLL) so the pid is dead by assignment time; protected processes; running the worker under a service account lacking rights to the child.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/1ee51b97b4390a54. Report an issue: GitHub.