Last active
April 16, 2024 11:26
-
-
Save johnd0e/aeda85f48f70f40e05e40b63fb25aa47 to your computer and use it in GitHub Desktop.
[FAR luajit module]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--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