Skip to content

Instantly share code, notes, and snippets.

@johnd0e
Last active April 16, 2024 11:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johnd0e/aeda85f48f70f40e05e40b63fb25aa47 to your computer and use it in GitHub Desktop.
Save johnd0e/aeda85f48f70f40e05e40b63fb25aa47 to your computer and use it in GitHub Desktop.
[FAR luajit module]
--https://forum.farmanager.com/viewtopic.php?f=60&t=8696&p=167904#p167904
-- based on https://forum.farmanager.com/viewtopic.php?t=8696
local SYNTAX = [[
Executes a specified command, putting the specified text to it's input stream,
and providing convenient access to the command's output/err.
Syntax: obj = sh.piper(cmdline [,options])
cmdline: a string specifying the command to be executed
options: a table with optional keys:
- directory: a string specifying the working directory for the command
- input: a string representing the text to be provided as input to the command
- isOem: a boolean, if true then CP en-/decoding performed
- linewrap: a number specifying the maximum width for each line of output
obj: a table with the following keys:
- all: a string containing all the output generated by the executed command
- chunks: an iterator function that returns the output in raw chunks
- lines: an iterator function that returns the output line by line
- streamlines: an iterator function that returns the output line by line
in a stream-like manner. It adds more data to each line as it becomes available.
The iterator keeps returning the same line with more data until it is complete,
and then adds a second `true` value to indicate completion.
- ExitCode: an integer representing the exit code of the command
(obtained from GetExitCodeProcess)
]]
local function split (str, wrapAt)
local e, e2 = string.find(str, "[\r\n]")
local rest, ln
if e then
if string.sub(str, e, e+1)=="\r\n" then e2 = e+1 end
str, ln, rest = string.sub(str, 1, e-1), string.sub(str, e, e2), string.sub(str, e2+1)
end
if wrapAt and str:len()>wrapAt then
local pos = str:find("%S", wrapAt+1)
if pos==wrapAt+1 then
pos = str:sub(1, wrapAt):find("%S+$")
if not pos or pos==1 then pos = wrapAt+1 end
end
if pos then
local tail = str:sub(pos)
rest = rest and tail..ln..rest or tail
str = str:sub(1, pos-1)--:match"^(.-)%s*$"
end
end
return str, rest--, ln or ""
end
--[[
local function _lines (getchunk, wrapAt)
local rest
return function ()
repeat
if rest then
local line; line,rest = split(rest, wrapAt)
if rest then return line end
rest = line
end
local chunk = getchunk()
if chunk then
rest = rest and rest..chunk or chunk
end
until chunk==nil
return rest~="" and rest or nil
end
end
--]]
local function _streamlines (getchunk, wrapAt)
local rest
local curline = ""
return function (last)
if last then return curline end
if not rest then
rest = getchunk()
if rest==nil then return end
end
local line; line,rest = split(curline..rest, wrapAt)
curline = not rest and line or ""
return line, rest
end
end
local function _lines (getchunk, wrapAt)
local getter = _streamlines(getchunk, wrapAt)
return function ()
if not getter then return end
repeat
local line, rest = getter()
if rest then return line end
until not line
local lastline = getter"lastline"
getter = nil
return lastline
end
end
local ffi = require("ffi")
local C = ffi.C
local function safe_cdef(def)
pcall(ffi.cdef, def)
end
safe_cdef([[
typedef struct _STARTUPINFOW {
unsigned long cb;
wchar_t* lpReserved;
wchar_t* lpDesktop;
wchar_t* lpTitle;
unsigned long dwX;
unsigned long dwY;
unsigned long dwXSize;
unsigned long dwYSize;
unsigned long dwXCountChars;
unsigned long dwYCountChars;
unsigned long dwFillAttribute;
unsigned long dwFlags;
unsigned short wShowWindow;
unsigned short cbReserved2;
unsigned char* lpReserved2;
void* hStdInput;
void* hStdOutput;
void* hStdError;
} STARTUPINFOW;
]])
safe_cdef([[
typedef struct _PROCESS_INFORMATION {
void* hProcess;
void* hThread;
unsigned long dwProcessId;
unsigned long dwThreadId;
} PROCESS_INFORMATION;
]])
ffi.cdef[[
int32_t CreatePipe(void** hReadPipe,void** hWritePipe,SECURITY_ATTRIBUTES* lpPipeAttributes,unsigned long nSize);
int32_t CreateProcessW(const wchar_t* lpApplicationName,wchar_t* lpCommandLine,SECURITY_ATTRIBUTES* lpProcessAttributes,SECURITY_ATTRIBUTES* lpThreadAttributes,int32_t bInheritHandles,unsigned long dwCreationFlags,void* lpEnvironment,const wchar_t* lpCurrentDirectory,STARTUPINFOW* lpStartupInfo,PROCESS_INFORMATION* lpProcessInformation);
int32_t ReadFile(void* hFile,void* lpBuffer,unsigned long nNumberOfBytesToRead,unsigned long* lpNumberOfBytesRead,void* lpOverlapped);
int32_t CloseHandle(void* hObject);
void* GetStdHandle(unsigned long nStdHandle);
int32_t IsTextUnicode(const void* lpv,int iSize,int* lpiResult);
]]
ffi.cdef[[
//https://learn.microsoft.com/windows/win32/api/handleapi/nf-handleapi-sethandleinformation
BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags
);
//https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
void* lpOverlapped
);
//https://learn.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess
BOOL GetExitCodeProcess(
HANDLE hProcess,
LPDWORD lpExitCode
);
//https://learn.microsoft.com/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
//https://learn.microsoft.com/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
DWORD GetLastError();
]]
local HANDLE_FLAG_INHERIT = 0x00000001
local STILL_ACTIVE = 259
-- https://learn.microsoft.com/windows/win32/debug/system-error-codes--0-499-#ERROR_BROKEN_PIPE
local ERROR_BROKEN_PIPE = 109
local function bAssert (fn, source)
return function (...)
local res = fn(...)
if res==0 then
far.Message(debug.traceback(source):gsub("\t"," "), "Error", nil, "ewl")
return false
end
return res
end
end
local CreatePipe = bAssert(C.CreatePipe, "CreatePipe")
local CreateProcessW = bAssert(C.CreateProcessW, "CreateProcessW")
--local ReadFile = bAssert(C.ReadFile, "ReadFile")
local CloseHandle = bAssert(C.CloseHandle, "CloseHandle")
local SetHandleInformation = bAssert(C.SetHandleInformation, "SetHandleInformation")
local WriteFile = bAssert(C.WriteFile, "WriteFile")
local GetExitCodeProcess = bAssert(C.GetExitCodeProcess, "GetExitCodeProcess")
local advapi32 = ffi.load("advapi32")
local unicode = ffi.new("int[1]")
local IS_TEXT_UNICODE_ASCII16 = 0x0001
local IS_TEXT_UNICODE_STATISTICS = 0x0002
--https://learn.microsoft.com/windows/win32/api/winbase/nf-winbase-istextunicode
local function IsTextUnicode (buffer, len)
unicode[0] = bit64.bor(IS_TEXT_UNICODE_ASCII16, IS_TEXT_UNICODE_STATISTICS)
return advapi32.IsTextUnicode(buffer,len,unicode)~=0
end
local CREATE_NO_WINDOW = 0x8000000
local STD_INPUT_HANDLE = -10
local STARTF_USESTDHANDLES = 0x00000100
local function ToWChar (str)
str = win.Utf8ToUtf16(str)
local result = ffi.new("wchar_t[?]", #str/2+1)
ffi.copy(result,str)
return result
end
local function exists (directory)
local attr = win.GetFileAttr(directory)
return attr and attr:match"d"
end
local function piper (command, options)
options = options or {}
local directory, input = options.directory, options.input
assert(not directory or exists(directory), "Directory does not exist")
local pipe_security = ffi.new("SECURITY_ATTRIBUTES", ffi.sizeof("SECURITY_ATTRIBUTES"), nil, true)
local pipe_outRd, pipe_outWr = ffi.new("void*[1]"), ffi.new("void*[1]")
if not CreatePipe(pipe_outRd, pipe_outWr, pipe_security, 0) or
not SetHandleInformation(pipe_outRd[0], HANDLE_FLAG_INHERIT, 0) then
return
end
local pipe_inRd,pipe_inWr
if input then
pipe_inRd, pipe_inWr = ffi.new("void*[1]"), ffi.new("void*[1]")
if not CreatePipe(pipe_inRd, pipe_inWr, pipe_security, 0) or
not SetHandleInformation(pipe_inWr[0], HANDLE_FLAG_INHERIT, 0) then
return
end
end
local info = ffi.new("PROCESS_INFORMATION")
local startup = ffi.new("STARTUPINFOW")
startup.cb = ffi.sizeof(startup)
startup.dwFlags = STARTF_USESTDHANDLES
startup.hStdError = pipe_outWr[0]
startup.hStdOutput = pipe_outWr[0]
startup.hStdInput = input and pipe_inRd[0] or C.GetStdHandle(STD_INPUT_HANDLE)
if not CreateProcessW(ffi.NULL, ToWChar(command), ffi.NULL, ffi.NULL, true, CREATE_NO_WINDOW,
ffi.NULL, directory and ToWChar(directory), startup, info) then
return
end
CloseHandle(info.hThread)
CloseHandle(pipe_outWr[0])
if input then
CloseHandle(pipe_inRd[0])
local written = ffi.new("DWORD[1]")
input = options.isOem and win.Utf8ToOem(input) or input
if not WriteFile(pipe_inWr[0], input, string.len(input), written, ffi.NULL) then
return
end
CloseHandle(pipe_inWr[0])
--assert(written[0]==string.len(input))
end
local size=4096
local buffer=ffi.new("char[?]",size)
local readed=ffi.new("unsigned long[1]")
ffi.gc(pipe_outRd, function (arr) C.CloseHandle(arr[0]) end)
local out_chunks = function ()
local success = C.ReadFile(pipe_outRd[0], buffer, size, readed, ffi.NULL)
local lasterr = success==0 and C.GetLastError()
if lasterr or readed[0]==0 then
if lasterr and lasterr~=ERROR_BROKEN_PIPE then -- The pipe has been ended.
far.Message("ReadFile:", "Error", nil, "ewl")
end
CloseHandle(ffi.gc(pipe_outRd, nil)[0])
else
local str = ffi.string(buffer, readed[0])
if options.isOem then
str = (IsTextUnicode(buffer, readed[0]) and win.Utf16ToUtf8 or win.OemToUtf8)(str)
end
return str
end
end
local res = {
lines=_lines(out_chunks, options.linewrap),
streamlines=_streamlines(out_chunks, options.linewrap),
chunks=out_chunks,
hProcess=ffi.gc(info.hProcess, C.CloseHandle)
}
local exitcode = ffi.new("DWORD[1]")
local timeout = 500
return setmetatable(res, {
__index=function (t,k)
if k=="all" then
local out = {}
for chunk in out_chunks do
out[#out+1] = chunk
end
t.all = table.concat(out)
return t.all
elseif k=="ExitCode" then
C.WaitForSingleObject(info.hProcess, timeout)
GetExitCodeProcess(info.hProcess, exitcode)
if exitcode[0]~=STILL_ACTIVE then
t.ExitCode = exitcode[0]
CloseHandle(ffi.gc(info.hProcess, nil))
end
return exitcode[0]
end
end
})
end
if _cmdline=="" then
print(SYNTAX)
--[[ -- examples:
local command = "cmd /c type piper.lua"
print(sh.piper(command).all)
for line in sh.piper(command).lines do print(line) end
--]]
elseif _cmdline then
--print(piper(_cmdline:eval()).all)
for line in piper(_cmdline:eval()).lines do print(line) end
return
else
return piper
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment