Implement a non-blocking child process interface (#211)

This commit is contained in:
Erica Marigold 2024-10-16 20:48:12 +01:00 committed by GitHub
parent 93fa14d832
commit 309c461e11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 414 additions and 64 deletions

View file

@ -129,7 +129,7 @@ end
]]
print("Sending 4 pings to google 🌏")
local result = process.spawn("ping", {
local result = process.exec("ping", {
"google.com",
"-c 4",
})

2
Cargo.lock generated
View file

@ -1629,6 +1629,8 @@ dependencies = [
name = "lune-std-process"
version = "0.1.3"
dependencies = [
"bstr",
"bytes",
"directories",
"lune-utils",
"mlua",

View file

@ -20,6 +20,9 @@ directories = "5.0"
pin-project = "1.0"
os_str_bytes = { version = "7.0", features = ["conversions"] }
bstr = "1.9"
bytes = "1.6.0"
tokio = { version = "1", default-features = false, features = [
"io-std",
"io-util",

View file

@ -1,27 +1,33 @@
#![allow(clippy::cargo_common_metadata)]
use std::{
cell::RefCell,
env::{
self,
consts::{ARCH, OS},
},
path::MAIN_SEPARATOR,
process::Stdio,
rc::Rc,
sync::Arc,
};
use mlua::prelude::*;
use lune_utils::TableBuilder;
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
use options::ProcessSpawnOptionsStdio;
use os_str_bytes::RawOsString;
use tokio::io::AsyncWriteExt;
use stream::{ChildProcessReader, ChildProcessWriter};
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
mod options;
mod stream;
mod tee_writer;
mod wait_for_child;
use self::options::ProcessSpawnOptions;
use self::wait_for_child::{wait_for_child, WaitForChildResult};
use self::wait_for_child::wait_for_child;
use lune_utils::path::get_current_dir;
@ -73,7 +79,8 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.with_value("cwd", cwd_str)?
.with_value("env", env_tab)?
.with_value("exit", process_exit)?
.with_async_function("spawn", process_spawn)?
.with_async_function("exec", process_exec)?
.with_function("create", process_create)?
.build_readonly()
}
@ -141,11 +148,16 @@ fn process_env_iter<'lua>(
})
}
async fn process_spawn(
async fn process_exec(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
let res = lua.spawn(spawn_command(program, args, options)).await?;
let res = lua
.spawn(async move {
let cmd = spawn_command_with_stdin(program, args, options.clone()).await?;
wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await
})
.await?;
/*
NOTE: If an exit code was not given by the child process,
@ -168,30 +180,104 @@ async fn process_spawn(
.build_readonly()
}
async fn spawn_command(
#[allow(clippy::await_holding_refcell_ref)]
fn process_create(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
// We do not want the user to provide stdio options for process.create,
// so we reset the options, regardless of what the user provides us
let mut spawn_options = options.clone();
spawn_options.stdio = ProcessSpawnOptionsStdio::default();
let (code_tx, code_rx) = tokio::sync::broadcast::channel(4);
let code_rx_rc = Rc::new(RefCell::new(code_rx));
let child = spawn_command(program, args, spawn_options)?;
let child_arc = Arc::new(RwLock::new(child));
let child_arc_clone = Arc::clone(&child_arc);
let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write());
let stdin = child_lock.stdin.take().unwrap();
let stdout = child_lock.stdout.take().unwrap();
let stderr = child_lock.stderr.take().unwrap();
let child_arc_inner = Arc::clone(&child_arc);
// Spawn a background task to wait for the child to exit and send the exit code
let status_handle = tokio::spawn(async move {
let res = child_arc_inner.write().await.wait().await;
if let Ok(output) = res {
let code = output.code().unwrap_or_default();
code_tx
.send(code)
.expect("ExitCode receiver was unexpectedly dropped");
}
});
TableBuilder::new(lua)?
.with_value("stdout", ChildProcessReader(stdout))?
.with_value("stderr", ChildProcessReader(stderr))?
.with_value("stdin", ChildProcessWriter(stdin))?
.with_async_function("kill", move |_, ()| {
// First, stop the status task so the RwLock is dropped
status_handle.abort();
let child_arc_clone = Arc::clone(&child_arc);
// Then get another RwLock to write to the child process and kill it
async move { Ok(child_arc_clone.write().await.kill().await?) }
})?
.with_async_function("status", move |lua, ()| {
let code_rx_rc_clone = Rc::clone(&code_rx_rc);
async move {
// Exit code of 9 corresponds to SIGKILL, which should be the only case where
// the receiver gets suddenly dropped
let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9);
TableBuilder::new(lua)?
.with_value("code", code)?
.with_value("ok", code == 0)?
.build_readonly()
}
})?
.build_readonly()
}
async fn spawn_command_with_stdin(
program: String,
args: Option<Vec<String>>,
mut options: ProcessSpawnOptions,
) -> LuaResult<WaitForChildResult> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
) -> LuaResult<Child> {
let stdin = options.stdio.stdin.take();
let mut child = options
.into_command(program, args)
.stdin(if stdin.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
let mut child = spawn_command(program, args, options)?;
if let Some(stdin) = stdin {
let mut child_stdin = child.stdin.take().unwrap();
child_stdin.write_all(&stdin).await.into_lua_err()?;
}
wait_for_child(child, stdout, stderr).await
Ok(child)
}
fn spawn_command(
program: String,
args: Option<Vec<String>>,
options: ProcessSpawnOptions,
) -> LuaResult<Child> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
let child = options
.into_command(program, args)
.stdin(Stdio::piped())
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
Ok(child)
}

View file

@ -0,0 +1,58 @@
use bstr::BString;
use bytes::BytesMut;
use mlua::prelude::*;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
const CHUNK_SIZE: usize = 8;
#[derive(Debug, Clone)]
pub struct ChildProcessReader<R: AsyncRead>(pub R);
#[derive(Debug, Clone)]
pub struct ChildProcessWriter<W: AsyncWrite>(pub W);
impl<R: AsyncRead + Unpin> ChildProcessReader<R> {
pub async fn read(&mut self, chunk_size: Option<usize>) -> LuaResult<Vec<u8>> {
let mut buf = BytesMut::with_capacity(chunk_size.unwrap_or(CHUNK_SIZE));
self.0.read_buf(&mut buf).await?;
Ok(buf.to_vec())
}
pub async fn read_to_end(&mut self) -> LuaResult<Vec<u8>> {
let mut buf = vec![];
self.0.read_to_end(&mut buf).await?;
Ok(buf)
}
}
impl<R: AsyncRead + Unpin + 'static> LuaUserData for ChildProcessReader<R> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("read", |lua, this, chunk_size: Option<usize>| async move {
let buf = this.read(chunk_size).await?;
if buf.is_empty() {
return Ok(LuaValue::Nil);
}
Ok(LuaValue::String(lua.create_string(buf)?))
});
methods.add_async_method_mut("readToEnd", |lua, this, ()| async {
Ok(lua.create_string(this.read_to_end().await?))
});
}
}
impl<W: AsyncWrite + Unpin> ChildProcessWriter<W> {
pub async fn write(&mut self, data: BString) -> LuaResult<()> {
self.0.write_all(data.as_ref()).await?;
Ok(())
}
}
impl<W: AsyncWrite + Unpin + 'static> LuaUserData for ChildProcessWriter<W> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("write", |_, this, data| async { this.write(data).await });
}
}

View file

@ -138,13 +138,16 @@ create_tests! {
process_cwd: "process/cwd",
process_env: "process/env",
process_exit: "process/exit",
process_spawn_async: "process/spawn/async",
process_spawn_basic: "process/spawn/basic",
process_spawn_cwd: "process/spawn/cwd",
process_spawn_no_panic: "process/spawn/no_panic",
process_spawn_shell: "process/spawn/shell",
process_spawn_stdin: "process/spawn/stdin",
process_spawn_stdio: "process/spawn/stdio",
process_exec_async: "process/exec/async",
process_exec_basic: "process/exec/basic",
process_exec_cwd: "process/exec/cwd",
process_exec_no_panic: "process/exec/no_panic",
process_exec_shell: "process/exec/shell",
process_exec_stdin: "process/exec/stdin",
process_exec_stdio: "process/exec/stdio",
process_spawn_non_blocking: "process/create/non_blocking",
process_spawn_status: "process/create/status",
process_spawn_stream: "process/create/stream",
}
#[cfg(feature = "std-regex")]

View file

@ -108,7 +108,7 @@ local BIN_ZLIB = if process.os == "macos" then "/opt/homebrew/bin/pigz" else "pi
local function checkInstalled(program: string, args: { string }?)
print("Checking if", program, "is installed")
local result = process.spawn(program, args)
local result = process.exec(program, args)
if not result.ok then
stdio.ewrite(string.format("Program '%s' is not installed\n", program))
process.exit(1)
@ -123,7 +123,7 @@ checkInstalled(BIN_ZLIB, { "--version" })
-- Run them to generate files
local function run(program: string, args: { string }): string
local result = process.spawn(program, args)
local result = process.exec(program, args)
if not result.ok then
stdio.ewrite(string.format("Command '%s' failed\n", program))
if #result.stdout > 0 then

View file

@ -31,7 +31,7 @@ if not runLocaleTests then
return
end
local dateCmd = process.spawn("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
local dateCmd = process.exec("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
env = {
LC_ALL = "fr_FR.UTF-8 ",
},

View file

@ -0,0 +1,21 @@
local process = require("@lune/process")
-- Killing a child process should work as expected
local message = "Hello, world!"
local child = process.create("cat")
child.stdin:write(message)
child.kill()
assert(child.status().code == 9, "Child process should have an exit code of 9 (SIGKILL)")
assert(
child.stdout:readToEnd() == message,
"Reading from stdout of child process should work even after kill"
)
local stdinWriteOk = pcall(function()
child.stdin:write(message)
end)
assert(not stdinWriteOk, "Writing to stdin of child process should not work after kill")

View file

@ -0,0 +1,13 @@
local process = require("@lune/process")
-- Spawning a child process should not block the thread
local childThread = coroutine.create(process.create)
local ok, err = coroutine.resume(childThread, "echo", { "hello, world" })
assert(ok, err)
assert(
coroutine.status(childThread) == "dead",
"Child process should not block the thread it is running on"
)

View file

@ -0,0 +1,15 @@
local process = require("@lune/process")
-- The exit code of an child process should be correct
local randomExitCode = math.random(0, 255)
local isOk = randomExitCode == 0
local child = process.create("exit", { tostring(randomExitCode) }, { shell = true })
local status = child.status()
assert(
status.code == randomExitCode,
`Child process exited with wrong exit code, expected {randomExitCode}`
)
assert(status.ok == isOk, `Child status should be {if status.ok then "ok" else "not ok"}`)

View file

@ -0,0 +1,18 @@
local process = require("@lune/process")
-- Should be able to write and read from child process streams
local msg = "hello, world"
local catChild = process.create("cat")
catChild.stdin:write(msg)
assert(
msg == catChild.stdout:read(#msg),
"Failed to write to stdin or read from stdout of child process"
)
local echoChild = if process.os == "windows"
then process.create("/c", { "echo", msg, "1>&2" }, { shell = "cmd" })
else process.create("echo", { msg, ">>/dev/stderr" }, { shell = true })
assert(msg == echoChild.stderr:read(#msg), "Failed to read from stderr of child process")

View file

@ -4,7 +4,7 @@ local task = require("@lune/task")
local IS_WINDOWS = process.os == "windows"
-- Spawning a process should not block any lua thread(s)
-- Executing a command should not block any lua thread(s)
local SLEEP_DURATION = 1 / 4
local SLEEP_SAMPLES = 2
@ -31,7 +31,7 @@ for i = 1, SLEEP_SAMPLES, 1 do
table.insert(args, 1, "-Milliseconds")
end
-- Windows does not have `sleep` as a process, so we use powershell instead.
process.spawn("sleep", args, if IS_WINDOWS then { shell = true } else nil)
process.exec("sleep", args, if IS_WINDOWS then { shell = true } else nil)
sleepCounter += 1
end)
end

View file

@ -2,7 +2,7 @@ local process = require("@lune/process")
local stdio = require("@lune/stdio")
local task = require("@lune/task")
-- Spawning a child process should work, with options
-- Executing a command should work, with options
local thread = task.delay(1, function()
stdio.ewrite("Spawning a process should take a reasonable amount of time\n")
@ -12,7 +12,7 @@ end)
local IS_WINDOWS = process.os == "windows"
local result = process.spawn(
local result = process.exec(
if IS_WINDOWS then "cmd" else "ls",
if IS_WINDOWS then { "/c", "dir" } else { "-a" }
)

View file

@ -6,7 +6,7 @@ local pwdCommand = if IS_WINDOWS then "cmd" else "pwd"
local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {}
-- Make sure the cwd option actually uses the directory we want
local rootPwd = process.spawn(pwdCommand, pwdArgs, {
local rootPwd = process.exec(pwdCommand, pwdArgs, {
cwd = "/",
}).stdout
rootPwd = string.gsub(rootPwd, "^%s+", "")
@ -27,24 +27,24 @@ end
-- Setting cwd should not change the cwd of this process
local pwdBefore = process.spawn(pwdCommand, pwdArgs).stdout
process.spawn("ls", {}, {
local pwdBefore = process.exec(pwdCommand, pwdArgs).stdout
process.exec("ls", {}, {
cwd = "/",
shell = true,
})
local pwdAfter = process.spawn(pwdCommand, pwdArgs).stdout
local pwdAfter = process.exec(pwdCommand, pwdArgs).stdout
assert(pwdBefore == pwdAfter, "Current working directory changed after running child process")
-- Setting the cwd on a child process should properly
-- replace any leading ~ with the users real home dir
local homeDir1 = process.spawn("echo $HOME", nil, {
local homeDir1 = process.exec("echo $HOME", nil, {
shell = true,
}).stdout
-- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return
-- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object
local homeDir2 = process.spawn(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, {
local homeDir2 = process.exec(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, {
shell = true,
cwd = "~",
}).stdout

View file

@ -0,0 +1,7 @@
local process = require("@lune/process")
-- Executing a non existent command as a child process
-- should not panic, but should error
local success = pcall(process.exec, "someProgramThatDoesNotExist")
assert(not success, "Spawned a non-existent program")

View file

@ -5,7 +5,7 @@ local IS_WINDOWS = process.os == "windows"
-- Default shell should be /bin/sh on unix and powershell on Windows,
-- note that powershell needs slightly different command flags for ls
local shellResult = process.spawn("ls", {
local shellResult = process.exec("ls", {
if IS_WINDOWS then "-Force" else "-a",
}, {
shell = true,

View file

@ -10,8 +10,8 @@ local echoMessage = "Hello from child process!"
-- When passing stdin to powershell on windows we must "accept" using the double newline
local result = if IS_WINDOWS
then process.spawn("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
else process.spawn("xargs", { "echo" }, { stdin = echoMessage })
then process.exec("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
else process.exec("xargs", { "echo" }, { stdin = echoMessage })
local resultStdout = if IS_WINDOWS
then string.sub(result.stdout, #result.stdout - #echoMessage - 1)

View file

@ -5,12 +5,12 @@ local IS_WINDOWS = process.os == "windows"
-- Inheriting stdio & environment variables should work
local echoMessage = "Hello from child process!"
local echoResult = process.spawn("echo", {
local echoResult = process.exec("echo", {
if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"',
}, {
env = { TEST_VAR = echoMessage },
shell = if IS_WINDOWS then "powershell" else "bash",
stdio = "inherit",
stdio = "inherit" :: process.SpawnOptionsStdioKind, -- FIXME: This should just work without a cast?
})
-- Windows uses \r\n (CRLF) and unix uses \n (LF)

View file

@ -1,7 +0,0 @@
local process = require("@lune/process")
-- Spawning a child process for a non-existent
-- program should not panic, but should error
local success = pcall(process.spawn, "someProgramThatDoesNotExist")
assert(not success, "Spawned a non-existent program")

View file

@ -109,7 +109,7 @@ assertContains(
local _, errorMessage = pcall(function()
local function innerInnerFn()
process.spawn("PROGRAM_THAT_DOES_NOT_EXIST")
process.exec("PROGRAM_THAT_DOES_NOT_EXIST")
end
local function innerFn()
innerInnerFn()

View file

@ -5,6 +5,9 @@ export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
export type SpawnOptionsStdio = {
stdout: SpawnOptionsStdioKind?,
stderr: SpawnOptionsStdioKind?,
}
export type ExecuteOptionsStdio = SpawnOptionsStdio & {
stdin: string?,
}
@ -12,27 +15,117 @@ export type SpawnOptionsStdio = {
@interface SpawnOptions
@within Process
A dictionary of options for `process.spawn`, with the following available values:
A dictionary of options for `process.create`, with the following available values:
* `cwd` - The current working directory for the process
* `env` - Extra environment variables to give to the process
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info
* `stdin` - Optional standard input to pass to spawned child process
]=]
export type SpawnOptions = {
cwd: string?,
env: { [string]: string }?,
shell: (boolean | string)?,
}
--[=[
@interface ExecuteOptions
@within Process
A dictionary of options for `process.exec`, with the following available values:
* `cwd` - The current working directory for the process
* `env` - Extra environment variables to give to the process
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `ExecuteOptionsStdio` for more info
* `stdin` - Optional standard input to pass to executed child process
]=]
export type ExecuteOptions = SpawnOptions & {
stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
}
--[=[
@interface SpawnResult
@class ChildProcessReader
@within Process
Result type for child processes in `process.spawn`.
A reader class to read data from a child process' streams in realtime.
]=]
local ChildProcessReader = {}
--[=[
@within ChildProcessReader
Reads a chunk of data (specified length or a default of 8 bytes at a time) from
the reader as a string. Returns nil if there is no more data to read.
This function may yield until there is new data to read from reader, if all data
till present has already been read, and the process has not exited.
@return The string containing the data read from the reader
]=]
function ChildProcessReader:read(chunkSize: number?): string?
return nil :: any
end
--[=[
@within ChildProcessReader
Reads all the data currently present in the reader as a string.
This function will yield until the process exits.
@return The string containing the data read from the reader
]=]
function ChildProcessReader:readToEnd(): string
return nil :: any
end
--[=[
@class ChildProcessWriter
@within Process
A writer class to write data to a child process' streams in realtime.
]=]
local ChildProcessWriter = {}
--[=[
@within ChildProcessWriter
Writes a buffer or string of data to the writer.
@param data The data to write to the writer
]=]
function ChildProcessWriter:write(data: buffer | string): ()
return nil :: any
end
--[=[
@interface ChildProcess
@within Process
Result type for child processes in `process.create`.
This is a dictionary containing the following values:
* `stdin` - A writer to write to the child process' stdin - see `ChildProcessWriter` for more info
* `stdout` - A reader to read from the child process' stdout - see `ChildProcessReader` for more info
* `stderr` - A reader to read from the child process' stderr - see `ChildProcessReader` for more info
* `kill` - A function that kills the child process
* `status` - A function that yields and returns the exit status of the child process
]=]
export type ChildProcess = {
stdin: typeof(ChildProcessWriter),
stdout: typeof(ChildProcessReader),
stderr: typeof(ChildProcessReader),
kill: () -> ();
status: () -> { ok: boolean, code: number }
}
--[=[
@interface ExecuteResult
@within Process
Result type for child processes in `process.exec`.
This is a dictionary containing the following values:
@ -41,7 +134,7 @@ export type SpawnOptions = {
* `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written
* `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written
]=]
export type SpawnResult = {
export type ExecuteResult = {
ok: boolean,
code: number,
stdout: string,
@ -73,8 +166,8 @@ export type SpawnResult = {
-- Getting the current os and processor architecture
print("Running " .. process.os .. " on " .. process.arch .. "!")
-- Spawning a child process
local result = process.spawn("program", {
-- Executing a command
local result = process.exec("program", {
"cli argument",
"other cli argument"
})
@ -83,6 +176,19 @@ export type SpawnResult = {
else
print(result.stderr)
end
-- Spawning a child process
local child = process.create("program", {
"cli argument",
"other cli argument"
})
-- Writing to the child process' stdin
child.stdin:write("Hello from Lune!")
-- Reading from the child process' stdout
local data = child.stdout:read()
print(buffer.tostring(data))
```
]=]
local process = {}
@ -163,19 +269,44 @@ end
--[=[
@within Process
Spawns a child process that will run the program `program`, and returns a dictionary that describes the final status and ouput of the child process.
Spawns a child process in the background that runs the program `program`, and immediately returns
readers and writers to communicate with it.
In order to execute a command and wait for its output, see `process.exec`.
The second argument, `params`, can be passed as a list of string parameters to give to the program.
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
Refer to the documentation for `SpawnOptions` for specific option keys and their values.
@param program The program to spawn as a child process
@param program The program to Execute as a child process
@param params Additional parameters to pass to the program
@param options A dictionary of options for the child process
@return A dictionary with the readers and writers to communicate with the child process
]=]
function process.create(program: string, params: { string }?, options: SpawnOptions?): ChildProcess
return nil :: any
end
--[=[
@within Process
Executes a child process that will execute the command `program`, waiting for it to exit.
Upon exit, it returns a dictionary that describes the final status and ouput of the child process.
In order to spawn a child process in the background, see `process.create`.
The second argument, `params`, can be passed as a list of string parameters to give to the program.
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
Refer to the documentation for `ExecuteOptions` for specific option keys and their values.
@param program The program to Execute as a child process
@param params Additional parameters to pass to the program
@param options A dictionary of options for the child process
@return A dictionary representing the result of the child process
]=]
function process.spawn(program: string, params: { string }?, options: SpawnOptions?): SpawnResult
function process.exec(program: string, params: { string }?, options: ExecuteOptions?): ExecuteResult
return nil :: any
end