Skip to content

Instantly share code, notes, and snippets.

@msva
Forked from iMega/spawn.lua
Created March 9, 2024 14:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save msva/3d278d06c796f16e693245cd7e3bc989 to your computer and use it in GitHub Desktop.
Save msva/3d278d06c796f16e693245cd7e3bc989 to your computer and use it in GitHub Desktop.
Using LuaJIT FFI, spawn a Linux command in the background.
-- Spawn a command in the background, optionally redirecting stderr and stdout
--
-- requiring this file returns a function(cmd_line, stdout_redirect, stderr_redirect)
--
-- `cmd_line` is the command with possible arguments
-- optional `stdout_redirect` is io.stdout, io.stderr, or a filename. default/nil is io.stdout
-- optional `stderr_redirect` is io.stdout, io.stderr, or a filename. default/nil is io.stderr
--
-- Example:
-- luajit -e 'require("spawn")("cat /etc/network/interfaces", "foo1", io.stdout)'
--
local ffi = require 'ffi'
local C = ffi.C
ffi.cdef([[
typedef int32_t pid_t;
pid_t fork(void);
int open(const char *pathname, int flags, int mode);
int close(int fd);
int dup2(int oldfd, int newfd);
int execvp(const char *file, char *const argv[]);
]])
local bor = bit.bor
local ffi_cast = ffi.cast
local k_char_p_arr_t = ffi.typeof('const char * [?]')
local char_p_k_p_t = ffi.typeof('char * const *')
local octal = function(n) return tonumber(n, 8) end
local O_WRONLY = octal('0001')
local O_CREAT = octal('0100')
local S_IRUSR = octal('00400') -- user has read permission
local S_IWUSR = octal('00200') -- user has write permission
local FD_STDOUT = 1
local FD_STDERR = 2
-- split a string by spaces, except that single-quoted items are kept as a single token
local function tokenize_args( s )
local t = {}
local i, prev = 1, 1
local in_q = nil
local function capture_token()
local w = s:sub(prev, i-1)
if #w ~= 0 then t[#t+1] = w end
prev = i + 1
end
while i <= #s do
local c = s:sub(i, i)
if in_q then -- close quote?
if c == in_q then
capture_token()
in_q = nil
end
elseif c == ' ' then
capture_token()
elseif c == '\'' then
in_q = '\''
capture_token()
end
i = i + 1
end
-- final cleanup
capture_token()
return t
end
-- dest should be either 0 or 1 (FD_STDOUT or FD_STDERR)
local function redirect(io_or_filename, dest_fd)
if io_or_filename == nil then return end
-- first check for regular
if (io_or_filename == io.stdout or io_or_filename == FD_STDOUT) and dest_fd ~= FD_STDOUT then
C.dup2(FD_STDERR, FD_STDOUT)
elseif (io_or_filename == io.stderr or io_or_filename == FD_STDERR) and dest_fd ~= FD_STDERR then
C.dup2(FD_STDOUT, FD_STDERR)
-- otherwise handle file-based redirection
else
local fd = C.open(io_or_filename, bor(O_WRONLY, O_CREAT), bor(S_IRUSR, S_IWUSR))
if fd < 0 then error("couldn't open file '" .. fname .. "': " .. ffi.errno()) end
C.dup2(fd, dest_fd)
C.close(fd)
end
end
local function spawn(cmd_line, stdout_redirect, stderr_redirect)
local args = tokenize_args(cmd_line)
if not args or #args == 0 then error("couldn't tokenize cmd_line") end
local pid = C.fork()
if pid < 0 then
error("fork failed " .. ffi.errno())
elseif pid == 0 then -- child process
redirect(stdout_redirect, FD_STDOUT)
redirect(stderr_redirect, FD_STDERR)
local argv = k_char_p_arr_t(#args + 1) -- automatically NULL terminated
for i = 1, #args do
argv[i-1] = args[i] -- args is 1-based Lua table, argv is 0-based C array
end
local res = C.execvp(args[1], ffi_cast(char_p_k_p_t, argv))
if res == -1 then error("execvp failed with " .. ffi.errno()) end
-- HERE SHOULD BE UNREACHABLE!!
end
end
return spawn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment