lune_std_process/
lib.rs

1#![allow(clippy::cargo_common_metadata)]
2
3use std::{
4    env::{
5        self,
6        consts::{ARCH, OS},
7    },
8    path::MAIN_SEPARATOR,
9    process::Stdio,
10};
11
12use mlua::prelude::*;
13
14use lune_utils::TableBuilder;
15use mlua_luau_scheduler::{Functions, LuaSpawnExt};
16use os_str_bytes::RawOsString;
17use tokio::io::AsyncWriteExt;
18
19mod options;
20mod tee_writer;
21mod wait_for_child;
22
23use self::options::ProcessSpawnOptions;
24use self::wait_for_child::{wait_for_child, WaitForChildResult};
25
26use lune_utils::path::get_current_dir;
27
28/**
29    Creates the `process` standard library module.
30
31    # Errors
32
33    Errors when out of memory.
34*/
35#[allow(clippy::missing_panics_doc)]
36pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
37    let mut cwd_str = get_current_dir()
38        .to_str()
39        .expect("cwd should be valid UTF-8")
40        .to_string();
41    if !cwd_str.ends_with(MAIN_SEPARATOR) {
42        cwd_str.push(MAIN_SEPARATOR);
43    }
44    // Create constants for OS & processor architecture
45    let os = lua.create_string(&OS.to_lowercase())?;
46    let arch = lua.create_string(&ARCH.to_lowercase())?;
47    // Create readonly args array
48    let args_vec = lua
49        .app_data_ref::<Vec<String>>()
50        .ok_or_else(|| LuaError::runtime("Missing args vec in Lua app data"))?
51        .clone();
52    let args_tab = TableBuilder::new(lua)?
53        .with_sequential_values(args_vec)?
54        .build_readonly()?;
55    // Create proxied table for env that gets & sets real env vars
56    let env_tab = TableBuilder::new(lua)?
57        .with_metatable(
58            TableBuilder::new(lua)?
59                .with_function(LuaMetaMethod::Index.name(), process_env_get)?
60                .with_function(LuaMetaMethod::NewIndex.name(), process_env_set)?
61                .with_function(LuaMetaMethod::Iter.name(), process_env_iter)?
62                .build_readonly()?,
63        )?
64        .build_readonly()?;
65    // Create our process exit function, the scheduler crate provides this
66    let fns = Functions::new(lua)?;
67    let process_exit = fns.exit;
68    // Create the full process table
69    TableBuilder::new(lua)?
70        .with_value("os", os)?
71        .with_value("arch", arch)?
72        .with_value("args", args_tab)?
73        .with_value("cwd", cwd_str)?
74        .with_value("env", env_tab)?
75        .with_value("exit", process_exit)?
76        .with_async_function("spawn", process_spawn)?
77        .build_readonly()
78}
79
80fn process_env_get<'lua>(
81    lua: &'lua Lua,
82    (_, key): (LuaValue<'lua>, String),
83) -> LuaResult<LuaValue<'lua>> {
84    match env::var_os(key) {
85        Some(value) => {
86            let raw_value = RawOsString::new(value);
87            Ok(LuaValue::String(
88                lua.create_string(raw_value.to_raw_bytes())?,
89            ))
90        }
91        None => Ok(LuaValue::Nil),
92    }
93}
94
95fn process_env_set<'lua>(
96    _: &'lua Lua,
97    (_, key, value): (LuaValue<'lua>, String, Option<String>),
98) -> LuaResult<()> {
99    // Make sure key is valid, otherwise set_var will panic
100    if key.is_empty() {
101        Err(LuaError::RuntimeError("Key must not be empty".to_string()))
102    } else if key.contains('=') {
103        Err(LuaError::RuntimeError(
104            "Key must not contain the equals character '='".to_string(),
105        ))
106    } else if key.contains('\0') {
107        Err(LuaError::RuntimeError(
108            "Key must not contain the NUL character".to_string(),
109        ))
110    } else if let Some(value) = value {
111        // Make sure value is valid, otherwise set_var will panic
112        if value.contains('\0') {
113            Err(LuaError::RuntimeError(
114                "Value must not contain the NUL character".to_string(),
115            ))
116        } else {
117            env::set_var(&key, &value);
118            Ok(())
119        }
120    } else {
121        env::remove_var(&key);
122        Ok(())
123    }
124}
125
126fn process_env_iter<'lua>(
127    lua: &'lua Lua,
128    (_, ()): (LuaValue<'lua>, ()),
129) -> LuaResult<LuaFunction<'lua>> {
130    let mut vars = env::vars_os().collect::<Vec<_>>().into_iter();
131    lua.create_function_mut(move |lua, (): ()| match vars.next() {
132        Some((key, value)) => {
133            let raw_key = RawOsString::new(key);
134            let raw_value = RawOsString::new(value);
135            Ok((
136                LuaValue::String(lua.create_string(raw_key.to_raw_bytes())?),
137                LuaValue::String(lua.create_string(raw_value.to_raw_bytes())?),
138            ))
139        }
140        None => Ok((LuaValue::Nil, LuaValue::Nil)),
141    })
142}
143
144async fn process_spawn(
145    lua: &Lua,
146    (program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
147) -> LuaResult<LuaTable> {
148    let res = lua.spawn(spawn_command(program, args, options)).await?;
149
150    /*
151        NOTE: If an exit code was not given by the child process,
152        we default to 1 if it yielded any error output, otherwise 0
153
154        An exit code may be missing if the process was terminated by
155        some external signal, which is the only time we use this default
156    */
157    let code = res
158        .status
159        .code()
160        .unwrap_or(i32::from(!res.stderr.is_empty()));
161
162    // Construct and return a readonly lua table with results
163    TableBuilder::new(lua)?
164        .with_value("ok", code == 0)?
165        .with_value("code", code)?
166        .with_value("stdout", lua.create_string(&res.stdout)?)?
167        .with_value("stderr", lua.create_string(&res.stderr)?)?
168        .build_readonly()
169}
170
171async fn spawn_command(
172    program: String,
173    args: Option<Vec<String>>,
174    mut options: ProcessSpawnOptions,
175) -> LuaResult<WaitForChildResult> {
176    let stdout = options.stdio.stdout;
177    let stderr = options.stdio.stderr;
178    let stdin = options.stdio.stdin.take();
179
180    let mut child = options
181        .into_command(program, args)
182        .stdin(if stdin.is_some() {
183            Stdio::piped()
184        } else {
185            Stdio::null()
186        })
187        .stdout(stdout.as_stdio())
188        .stderr(stderr.as_stdio())
189        .spawn()?;
190
191    if let Some(stdin) = stdin {
192        let mut child_stdin = child.stdin.take().unwrap();
193        child_stdin.write_all(&stdin).await.into_lua_err()?;
194    }
195
196    wait_for_child(child, stdout, stderr).await
197}