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#[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 let os = lua.create_string(&OS.to_lowercase())?;
46 let arch = lua.create_string(&ARCH.to_lowercase())?;
47 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 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 let fns = Functions::new(lua)?;
67 let process_exit = fns.exit;
68 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 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 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 let code = res
158 .status
159 .code()
160 .unwrap_or(i32::from(!res.stderr.is_empty()));
161
162 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}