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::*;
13use mlua_luau_scheduler::Functions;
14
15use os_str_bytes::RawOsString;
16
17use lune_utils::{path::get_current_dir, TableBuilder};
18
19mod create;
20mod exec;
21mod options;
22
23use self::options::ProcessSpawnOptions;
24
25const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
26
27/**
28    Returns a string containing type definitions for the `process` standard library.
29*/
30#[must_use]
31pub fn typedefs() -> String {
32    TYPEDEFS.to_string()
33}
34
35/**
36    Creates the `process` standard library module.
37
38    # Errors
39
40    Errors when out of memory.
41*/
42#[allow(clippy::missing_panics_doc)]
43pub fn module(lua: Lua) -> LuaResult<LuaTable> {
44    let mut cwd_str = get_current_dir()
45        .to_str()
46        .expect("cwd should be valid UTF-8")
47        .to_string();
48    if !cwd_str.ends_with(MAIN_SEPARATOR) {
49        cwd_str.push(MAIN_SEPARATOR);
50    }
51
52    // Create constants for OS & processor architecture
53    let os = lua.create_string(OS.to_lowercase())?;
54    let arch = lua.create_string(ARCH.to_lowercase())?;
55    let endianness = lua.create_string(if cfg!(target_endian = "big") {
56        "big"
57    } else {
58        "little"
59    })?;
60
61    // Create readonly args array
62    let args_vec = lua
63        .app_data_ref::<Vec<String>>()
64        .ok_or_else(|| LuaError::runtime("Missing args vec in Lua app data"))?
65        .clone();
66    let args_tab = TableBuilder::new(lua.clone())?
67        .with_sequential_values(args_vec)?
68        .build_readonly()?;
69
70    // Create proxied table for env that gets & sets real env vars
71    let env_tab = TableBuilder::new(lua.clone())?
72        .with_metatable(
73            TableBuilder::new(lua.clone())?
74                .with_function(LuaMetaMethod::Index.name(), process_env_get)?
75                .with_function(LuaMetaMethod::NewIndex.name(), process_env_set)?
76                .with_function(LuaMetaMethod::Iter.name(), process_env_iter)?
77                .build_readonly()?,
78        )?
79        .build_readonly()?;
80
81    // Create our process exit function, the scheduler crate provides this
82    let fns = Functions::new(lua.clone())?;
83    let process_exit = fns.exit;
84
85    // Create the full process table
86    TableBuilder::new(lua)?
87        .with_value("os", os)?
88        .with_value("arch", arch)?
89        .with_value("endianness", endianness)?
90        .with_value("args", args_tab)?
91        .with_value("cwd", cwd_str)?
92        .with_value("env", env_tab)?
93        .with_value("exit", process_exit)?
94        .with_async_function("exec", process_exec)?
95        .with_function("create", process_create)?
96        .build_readonly()
97}
98
99fn process_env_get(lua: &Lua, (_, key): (LuaValue, String)) -> LuaResult<LuaValue> {
100    match env::var_os(key) {
101        Some(value) => {
102            let raw_value = RawOsString::new(value);
103            Ok(LuaValue::String(
104                lua.create_string(raw_value.to_raw_bytes())?,
105            ))
106        }
107        None => Ok(LuaValue::Nil),
108    }
109}
110
111fn process_env_set(_: &Lua, (_, key, value): (LuaValue, String, Option<String>)) -> LuaResult<()> {
112    // Make sure key is valid, otherwise set_var will panic
113    if key.is_empty() {
114        Err(LuaError::RuntimeError("Key must not be empty".to_string()))
115    } else if key.contains('=') {
116        Err(LuaError::RuntimeError(
117            "Key must not contain the equals character '='".to_string(),
118        ))
119    } else if key.contains('\0') {
120        Err(LuaError::RuntimeError(
121            "Key must not contain the NUL character".to_string(),
122        ))
123    } else if let Some(value) = value {
124        // Make sure value is valid, otherwise set_var will panic
125        if value.contains('\0') {
126            Err(LuaError::RuntimeError(
127                "Value must not contain the NUL character".to_string(),
128            ))
129        } else {
130            env::set_var(&key, &value);
131            Ok(())
132        }
133    } else {
134        env::remove_var(&key);
135        Ok(())
136    }
137}
138
139fn process_env_iter(lua: &Lua, (_, ()): (LuaValue, ())) -> LuaResult<LuaFunction> {
140    let mut vars = env::vars_os().collect::<Vec<_>>().into_iter();
141    lua.create_function_mut(move |lua, (): ()| match vars.next() {
142        Some((key, value)) => {
143            let raw_key = RawOsString::new(key);
144            let raw_value = RawOsString::new(value);
145            Ok((
146                LuaValue::String(lua.create_string(raw_key.to_raw_bytes())?),
147                LuaValue::String(lua.create_string(raw_value.to_raw_bytes())?),
148            ))
149        }
150        None => Ok((LuaValue::Nil, LuaValue::Nil)),
151    })
152}
153
154async fn process_exec(
155    lua: Lua,
156    (program, args, mut options): (String, Option<Vec<String>>, ProcessSpawnOptions),
157) -> LuaResult<LuaTable> {
158    let stdin = options.stdio.stdin.take();
159    let stdout = options.stdio.stdout;
160    let stderr = options.stdio.stderr;
161
162    let child = options
163        .into_command(program, args)
164        .stdin(Stdio::piped())
165        .stdout(stdout.as_stdio())
166        .stderr(stderr.as_stdio())
167        .spawn()?;
168
169    exec::exec(lua, child, stdin, stdout, stderr).await
170}
171
172fn process_create(
173    lua: &Lua,
174    (program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
175) -> LuaResult<LuaValue> {
176    let child = options
177        .into_command(program, args)
178        .stdin(Stdio::piped())
179        .stdout(Stdio::piped())
180        .stderr(Stdio::piped())
181        .spawn()?;
182
183    create::Child::new(lua, child).into_lua(lua)
184}