manen

Fancy Lua REPL
Log | Files | Refs | README | LICENSE

commit 6657afd7f24752a16272eab467a974571a07d8ca
parent bd783f6193bb61f4b7bd58a99fa99af5306508fb
Author: Sylvia Ivory <git@sivory.net>
Date:   Wed,  2 Jul 2025 01:23:36 -0700

Allow cancelling system Lua execution

Diffstat:
MCargo.lock | 2++
MCargo.toml | 2++
Mlua/rpc.lua | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Dluau-shim | 8--------
Msrc/lua.rs | 118++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
5 files changed, 165 insertions(+), 32 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -728,11 +728,13 @@ dependencies = [ "emmylua_parser", "lazy_static", "mlua", + "nix", "nu-ansi-term", "reedline", "rexpect", "rowan", "send_wrapper", + "tempfile", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml @@ -23,9 +23,11 @@ directories = "6.0.0" emmylua_parser = "0.10.8" lazy_static = "1.5.0" mlua = { version = "0.10.5", features = ["anyhow", "send", "async", "macros"] } +nix = { version = "0.30.1", features = ["signal"] } nu-ansi-term = "0.50.1" reedline = "0.40.0" rexpect = "0.6.2" rowan = "0.16.1" send_wrapper = "0.6.0" +tempfile = "3.20.0" thiserror = "2.0.12" diff --git a/lua/rpc.lua b/lua/rpc.lua @@ -232,15 +232,72 @@ function rpc.globals() rpc.respond('globals', _G) end +local loadstring_luas = { + ['Lua 5.1'] = true, + ['Luau'] = true, +} + function rpc.exec(code) - local l = _VERSION == 'Lua 5.1' and loadstring or load - local res = l(code, 'repl') + local load_fn = loadstring_luas[_VERSION] and loadstring or load + local fn = load_fn(code, 'repl') + + if not fn then + fn = assert(load_fn('return (' .. code .. ')', 'repl')) + end + + local function cleanup() + if debug and debug.sethook then + debug.sethook() + end + + if io and io.open and rpc.cancel_file then + local f = io.open(rpc.cancel_file, 'w') + + if f then + f:close() + end + end + end + + if debug and debug.sethook and io and io.open and rpc.cancel_file then + local function cancel() + local f = assert(io.open(rpc.cancel_file, 'r')) + + local data = f:read('*a') - if not res then - res = assert(l('return (' .. code .. ')', 'repl')) + f:close() + + if data:find('stop') then + cleanup() + error('cancelled') + end + end + + -- we can't do every line like in MluaExecutor due to the FS call + debug.sethook(cancel, "", 500000) + end + + local success, res = pcall(fn) + + cleanup() + + if success then + rpc.respond('exec', res) + else + rpc.respond('error', res) + end +end + +function rpc.prepare(file) + -- LuaJIT can't stop due to JIT + if not io or jit then + rpc.respond('cancel', false) + return end - rpc.respond('exec', res()) + rpc.cancel_file = file + + rpc.respond('cancel', true) end for line in io.stdin:lines() do diff --git a/luau-shim b/luau-shim @@ -1,8 +0,0 @@ -#!/bin/bash - -# luau-shim -e <rpc code> -code="$2" - -luau << EOF -$code -EOF diff --git a/src/lua.rs b/src/lua.rs @@ -1,14 +1,20 @@ use std::{ + io::Write, process::Command, sync::{ Arc, RwLock, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicI32, Ordering}, }, }; use mlua::prelude::*; +use nix::{ + sys::signal::{Signal, kill}, + unistd::Pid, +}; use rexpect::session::{PtySession, spawn_command}; use send_wrapper::SendWrapper; +use tempfile::NamedTempFile; use thiserror::Error; pub trait LuaExecutor: Send + Sync { @@ -57,9 +63,13 @@ impl LuaExecutor for MluaExecutor { } pub struct SystemLuaExecutor { - process: RwLock<SendWrapper<PtySession>>, + session: RwLock<SendWrapper<PtySession>>, program: String, lua: Lua, + + cancellation_file: RwLock<Option<NamedTempFile>>, + pid: AtomicI32, + is_stopping: AtomicBool, } #[derive(Debug, Error)] @@ -68,6 +78,8 @@ pub enum SystemLuaError { Lua(#[from] LuaError), #[error("expect error: {0}")] Expect(#[from] rexpect::error::Error), + #[error("io error: {0}")] + Io(#[from] std::io::Error), #[error("restarted system Lua")] Restarted, #[error("runtime error")] @@ -77,19 +89,15 @@ pub enum SystemLuaError { enum RpcCommand { Globals, Exec(String), + Prepare(String), } impl RpcCommand { pub fn to_lua(&self) -> String { - let func = match self { - Self::Globals => "globals", - Self::Exec(_) => "exec", - }; - - if let Self::Exec(code) = self { - format!("{func}:{code}") - } else { - func.to_string() + match self { + Self::Globals => String::from("globals"), + Self::Exec(code) => format!("exec:{code}"), + Self::Prepare(file) => format!("prepare:{file}"), } } } @@ -98,33 +106,88 @@ const RPC_CODE: &str = include_str!("../lua/rpc.lua"); impl SystemLuaExecutor { pub fn new(program: &str) -> Result<Self, SystemLuaError> { + let (session, file) = Self::obtain_session(program)?; + let pid = session.process.child_pid.as_raw(); + Ok(Self { - process: RwLock::new(SendWrapper::new(Self::obtain_process(program)?)), + session: RwLock::new(SendWrapper::new(session)), program: program.to_string(), lua: unsafe { Lua::unsafe_new() }, + cancellation_file: RwLock::new(file), + pid: AtomicI32::new(pid), + is_stopping: AtomicBool::new(false), }) } - fn obtain_process(program: &str) -> Result<PtySession, SystemLuaError> { + fn obtain_session( + program: &str, + ) -> Result<(PtySession, Option<NamedTempFile>), SystemLuaError> { let mut cmd = Command::new(program); cmd.arg("-e"); cmd.arg(RPC_CODE); - Ok(spawn_command(cmd, None)?) + let mut session = spawn_command(cmd, None)?; + + // TODO; should this be in our cache/run dir? + let file = NamedTempFile::new()?; + + let prepare = RpcCommand::Prepare(file.path().to_string_lossy().to_string()); + + let cmd = prepare.to_lua(); + session.send_line(&cmd)?; + + let lua = Lua::new(); + + loop { + let code = session.read_line()?; + + if let Ok(prepare_result) = lua.load(&code).eval::<LuaTable>() { + if prepare_result.get::<bool>("data")? { + return Ok((session, Some(file))); + } else { + return Ok((session, None)); + } + } + } + } + + fn restart_process(&self, session: &mut SendWrapper<PtySession>) -> Result<(), SystemLuaError> { + let (pty, file) = Self::obtain_session(&self.program)?; + self.pid + .store(pty.process.child_pid.as_raw(), Ordering::Relaxed); + + *session = SendWrapper::new(pty); + + let mut cancellation_file = self + .cancellation_file + .write() + .expect("write cancellation_file"); + *cancellation_file = file; + + Ok(()) } fn request(&self, command: RpcCommand) -> Result<LuaTable, SystemLuaError> { - let mut process = self.process.write().expect("write process"); + self.is_stopping.store(false, Ordering::Relaxed); + + let mut session = self.session.write().expect("write process"); let cmd = command.to_lua(); - process.send_line(&cmd)?; + + if session.send_line(&cmd).is_err() { + // killed + self.restart_process(&mut session)?; + + return Err(SystemLuaError::Restarted); + } loop { - let code = match process.read_line() { + let code = match session.read_line() { Ok(code) => code, Err(rexpect::error::Error::EOF { .. }) => { - *process = SendWrapper::new(Self::obtain_process(&self.program)?); + self.restart_process(&mut session)?; + return Err(SystemLuaError::Restarted); } x => x?, @@ -157,6 +220,23 @@ impl LuaExecutor for SystemLuaExecutor { } fn cancel(&self) { - todo!() + let mut cancellation_file = self + .cancellation_file + .write() + .expect("write cancellation_file"); + + if !self.is_stopping.load(Ordering::Relaxed) { + self.is_stopping.store(true, Ordering::Relaxed); + + if let Some(file) = cancellation_file.as_mut() { + if file.write_all(b"stop").is_ok() && file.flush().is_ok() { + return; + } + } + } + + // Restart process + let pid = self.pid.load(Ordering::Relaxed); + let _ = kill(Pid::from_raw(pid), Signal::SIGKILL); } }