ninja-core 1.14.1

A powerful, cross-platform package manager and runtime for managing tools and plugins (shurikens)
Documentation
use super::shared::{canonicalize_cwd, resolve_spawn_command};
use crate::utils::{kill_process_by_name, kill_process_by_pid};
use log::{debug, error, info, warn};
use mlua::{Lua, Result, Table};
use std::{
    path::{Path, PathBuf},
    process::Stdio,
    time::Duration,
};

pub(crate) fn make_proc_module(lua: &Lua, base_cwd: Option<&Path>) -> Result<Table> {
    debug!(
        "make_proc_module: base_cwd = {:?}",
        base_cwd.map(|p| p.display().to_string())
    );

    let proc_module = lua.create_table()?;
    let proc_cwd: Option<PathBuf> = canonicalize_cwd(base_cwd);

    proc_module.set(
        "spawn",
        lua.create_async_function({
            let proc_cwd = proc_cwd.clone();
            move |lua, args: mlua::Value| {
                let proc_cwd = proc_cwd.clone();
                async move {
                    let (command, custom_cwd): (String, Option<PathBuf>) = match args {
                        mlua::Value::String(s) => (s.to_str()?.to_string(), None),
                        mlua::Value::Table(t) => {
                            let cmd: String = t.get("command").or_else(|_| t.get(1))?;
                            let cwd: Option<PathBuf> = t.get("cwd").ok();
                            (cmd, cwd)
                        }
                        _ => {
                            return Err(mlua::Error::external(
                                "spawn requires string or table with 'command' field",
                            ));
                        }
                    };

                    #[cfg(unix)]
                    {
                        debug!(
                            "proc.spawn unix: command='{}', custom_cwd={:?}",
                            command, custom_cwd
                        );
                        let cwd_to_use = custom_cwd.as_deref().or(proc_cwd.as_deref());
                        let resolved = resolve_spawn_command(&command, cwd_to_use, None)?;

                        let mut cmd = tokio::process::Command::new("sh");
                        cmd.args(["-c", &resolved]);

                        if let Some(cwd) = cwd_to_use {
                            cmd.current_dir(cwd);
                        }

                        cmd.stdin(Stdio::null())
                            .stdout(Stdio::null())
                            .stderr(Stdio::null());

                        unsafe {
                            cmd.pre_exec(|| {
                                libc::setsid();
                                Ok(())
                            });
                        }

                        let child = cmd.spawn().map_err(|e| {
                            error!("proc.spawn: failed to spawn '{}': {}", command, e);
                            mlua::Error::external(format!("spawn failed: {}", e))
                        })?;

                        let pid = child.id().unwrap_or(0);
                        debug!("proc.spawn: spawned detached process with pid={}", pid);

                        let result_table = lua.create_table()?;
                        result_table.set("pid", pid)?;
                        Ok(result_table)
                    }

                    #[cfg(windows)]
                    unsafe {
                        use std::iter::once;

                        use windows::{
                            Win32::System::Threading::{
                                CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, CreateProcessW,
                                DETACHED_PROCESS, PROCESS_INFORMATION, STARTUPINFOW,
                            },
                            core::{PCWSTR, PWSTR},
                        };

                        let cwd_to_use = custom_cwd.as_deref().or(proc_cwd.as_deref());

                        let command = resolve_spawn_command(&command, cwd_to_use, Some(true))?;
                        debug!(
                            "proc.spawn windows: command='{}', custom_cwd={:?}",
                            command, custom_cwd
                        );
                        let mut si = STARTUPINFOW::default();
                        si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;

                        let mut pi = PROCESS_INFORMATION::default();

                        let mut wide_command: Vec<u16> =
                            command.encode_utf16().chain(once(0)).collect();

                        let wide_cwd = if let Some(cwd) = cwd_to_use {
                            let cwd: Vec<u16> = cwd
                                .to_string_lossy()
                                .encode_utf16()
                                .chain(once(0))
                                .collect();
                            PCWSTR(cwd.as_ptr())
                        } else {
                            PCWSTR::null()
                        };

                        CreateProcessW(
                            PCWSTR::null(),
                            Some(PWSTR(wide_command.as_mut_ptr())),
                            None,
                            None,
                            false,
                            CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW | DETACHED_PROCESS,
                            None,
                            wide_cwd,
                            &si,
                            &mut pi,
                        )
                        .map_err(|e| {
                            mlua::Error::ExternalError(std::sync::Arc::new(std::io::Error::new(
                                std::io::ErrorKind::Other,
                                format!("Error code: {}, with message: {}", e.code(), e.message()),
                            )))
                        })?;

                        info!(
                            "proc.spawn: spawned detached process with pid={}",
                            pi.dwProcessId
                        );

                        let result_table = lua.create_table()?;
                        result_table.set("pid", pi.dwProcessId)?;
                        Ok(result_table)
                    }
                }
            }
        })?,
    )?;

    proc_module.set(
        "kill_pid",
        lua.create_function(|_, pid: u32| {
            debug!("proc.kill_pid: pid={}", pid);
            let result = kill_process_by_pid(pid);
            info!("proc.kill_pid: result={}", result);
            Ok(result)
        })?,
    )?;

    proc_module.set(
        "kill_name",
        lua.create_function(|_, name: String| {
            debug!("proc.kill_name: name='{}'", name);
            let result = kill_process_by_name(&name);
            debug!("proc.kill_name: result={}", result);
            Ok(result)
        })?,
    )?;

    proc_module.set(
        "exec",
        lua.create_async_function({
            let proc_cwd = proc_cwd.clone();
            move |lua, args: mlua::Value| {
                let proc_cwd = proc_cwd.clone();
                async move {
                    let (command, timeout_secs, custom_cwd): (
                        String,
                        Option<u64>,
                        Option<PathBuf>,
                    ) = match args {
                        mlua::Value::String(s) => (s.to_str()?.to_string(), None, None),
                        mlua::Value::Table(t) => {
                            let cmd: String = t.get("command").or_else(|_| t.get(1))?;
                            let timeout: Option<u64> = t.get("timeout").ok();
                            let cwd: Option<PathBuf> = t.get("cwd").ok();
                            (cmd, timeout, cwd)
                        }
                        _ => {
                            return Err(mlua::Error::external(
                                "exec requires string or table with 'command' field",
                            ));
                        }
                    };

                    debug!(
                        "proc.exec: command='{}', timeout={:?}, custom_cwd={:?}",
                        command, timeout_secs, custom_cwd
                    );

                    let cwd_to_use = custom_cwd.as_deref().or(proc_cwd.as_deref());
                    let use_backslash = cfg!(windows);
                    let resolved =
                        resolve_spawn_command(&command, cwd_to_use, Some(use_backslash))?;

                    let mut cmd =
                        tokio::process::Command::new(if cfg!(windows) { "cmd" } else { "sh" });

                    if cfg!(windows) {
                        cmd.args(["/C", &resolved]);
                    } else {
                        cmd.args(["-c", &resolved]);
                    }

                    if let Some(cwd) = cwd_to_use {
                        cmd.current_dir(cwd);
                    }

                    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());

                    let mut child = cmd.spawn().map_err(|e| {
                        error!("proc.exec: failed to spawn '{}': {}", resolved, e);
                        mlua::Error::external(e)
                    })?;

                    let status = if let Some(timeout_s) = timeout_secs {
                        let timeout = Duration::from_secs(timeout_s);
                        debug!("proc.exec: using timeout {:?}", timeout);
                        match tokio::time::timeout(timeout, child.wait()).await {
                            Ok(Ok(status)) => status,
                            Ok(Err(e)) => {
                                error!("proc.exec: wait failed for '{}': {}", resolved, e);
                                return Err(mlua::Error::external(e));
                            }
                            Err(_) => {
                                warn!("proc.exec: timeout for '{}', terminating", resolved);
                                let _ = child.kill().await;
                                return Err(mlua::Error::external("Process timeout"));
                            }
                        }
                    } else {
                        child.wait().await.map_err(|e| {
                            error!("proc.exec: wait failed for '{}': {}", resolved, e);
                            mlua::Error::external(e)
                        })?
                    };

                    let result = lua.create_table()?;
                    result.set("success", status.success())?;
                    result.set("exit_code", status.code().unwrap_or(-1))?;

                    debug!(
                        "proc.exec: completed '{}' with status: {:?}",
                        resolved, status
                    );

                    Ok(result)
                }
            }
        })?,
    )?;

    debug!("make_proc_module: done");
    Ok(proc_module)
}