197 lines
7.7 KiB
Python
197 lines
7.7 KiB
Python
import t, c
|
|
from stdint import *
|
|
import platmacro
|
|
import memhub
|
|
import string
|
|
|
|
# Windows backend dependencies
|
|
import w32.win32base
|
|
import w32.win32file
|
|
import w32.win32process
|
|
import w32.win32sync
|
|
|
|
# POSIX backend dependencies
|
|
import posix
|
|
|
|
|
|
# Subprocess output buffer size
|
|
OUTPUT_BUF_SIZE: t.CDefine = 65536
|
|
|
|
# Module global MBuddy pointer (initialized in main)
|
|
_mbuddy: memhub.MemBuddy | t.CPtr
|
|
|
|
|
|
# =============================================================================
|
|
# CompletedProcess result class
|
|
# =============================================================================
|
|
|
|
class CompletedProcess:
|
|
"""Result class for subprocess.run."""
|
|
args: str # Original command line
|
|
returncode: int # Process exit code (0 means success)
|
|
stdout: str # Standard output (when capture_output=True)
|
|
stderr: str # Standard error (when capture_output=True)
|
|
|
|
def __new__(self, args: str, returncode: int, stdout: str, stderr: str) -> t.CPtr:
|
|
# Heap allocation (avoiding use-after-return)
|
|
return t.CPtr(_mbuddy.alloc(CompletedProcess.__sizeof__()))
|
|
|
|
def __init__(self, args: str, returncode: int, stdout: str, stderr: str):
|
|
self.args = args
|
|
self.returncode = returncode
|
|
self.stdout = stdout
|
|
self.stderr = stderr
|
|
|
|
|
|
# =============================================================================
|
|
# Entry function
|
|
# =============================================================================
|
|
|
|
def run(args: str, capture_output: bool = True, text: bool = True) -> CompletedProcess | t.CPtr:
|
|
"""Run a subprocess synchronously and wait for it to finish.
|
|
The args are the full command line string.
|
|
If capture_output=True, stdout/stderr will be captured. If text=True, the output will be a string (currently it always returns a string).
|
|
Returns a CompletedProcess object."""
|
|
if platmacro.IS_WINDOWS:
|
|
return _run_win32(args, capture_output, text)
|
|
return _run_posix(args, capture_output, text)
|
|
|
|
|
|
# =============================================================================
|
|
# Windows backend implementation
|
|
# =============================================================================
|
|
|
|
def _run_win32(args: str, capture_output: bool, text: bool) -> CompletedProcess | t.CPtr:
|
|
"""Windows platform subprocess execution (CreateProcessA pipe)"""
|
|
# Copy command line (CreateProcessA requires a modifiable lpCommandLine)
|
|
args_len: t.CSizeT = len(args)
|
|
cmd_line: bytes = _mbuddy.alloc(args_len + 1)
|
|
if cmd_line == None:
|
|
return CompletedProcess(args, -1, None, None)
|
|
i: t.CSizeT = 0
|
|
for ch in args:
|
|
cmd_line[i] = ch
|
|
i += 1
|
|
cmd_line[args_len] = 0
|
|
|
|
# Security attributes (allow handle inheritance)
|
|
sa: w32.win32base.SECURITY_ATTRIBUTES = w32.win32base.SECURITY_ATTRIBUTES()
|
|
sa.nLength = w32.win32base.SECURITY_ATTRIBUTES.__sizeof__()
|
|
sa.lpSecurityDescriptor = t.CPtr(0)
|
|
sa.bInheritHandle = 1 # TRUE
|
|
|
|
# Create stdout pipe
|
|
stdout_read: w32.win32base.HANDLE = t.CPtr(0)
|
|
stdout_write: w32.win32base.HANDLE = t.CPtr(0)
|
|
if capture_output:
|
|
pipe_result: BOOL = w32.win32file.CreatePipe(
|
|
t.CPtr(c.Addr(stdout_read)),
|
|
t.CPtr(c.Addr(stdout_write)),
|
|
t.CPtr(c.Addr(sa)),
|
|
0
|
|
)
|
|
if pipe_result == 0:
|
|
return CompletedProcess(args, -1, None, None)
|
|
# Prevent read end from being inherited by child process
|
|
w32.win32file.SetHandleInformation(stdout_read, w32.win32file.HANDLE_FLAG_INHERIT, 0)
|
|
|
|
# Set startup information
|
|
si: w32.win32process.STARTUPINFOA = w32.win32process.STARTUPINFOA()
|
|
si.cb = w32.win32process.STARTUPINFOA.__sizeof__()
|
|
if capture_output:
|
|
si.dwFlags = w32.win32process.STARTF_USESTDHANDLES
|
|
si.hStdOutput = stdout_write
|
|
si.hStdError = stdout_write # stderr merged to stdout
|
|
|
|
pi: w32.win32process.PROCESS_INFORMATION = w32.win32process.PROCESS_INFORMATION()
|
|
|
|
# Create process
|
|
# use CREATE_NO_WINDOW when capture_output=True (output to pipe, no console)
|
|
# use 0 when capture_output=False (inherit parent process console, let stdout directly display)
|
|
create_flags: ULONG = w32.win32process.CREATE_NO_WINDOW if capture_output else 0
|
|
create_result: BOOL = w32.win32process.CreateProcessA(
|
|
t.CPtr(0), # lpApplicationName (NULL, parse from command line)
|
|
cmd_line, # lpCommandLine (modifiable)
|
|
t.CPtr(0), # lpProcessAttributes
|
|
t.CPtr(0), # lpThreadAttributes
|
|
1, # bInheritHandles = TRUE
|
|
create_flags, # dwCreationFlags
|
|
t.CPtr(0), # lpEnvironment (inherit from parent process)
|
|
t.CPtr(0), # lpCurrentDirectory (inherit from parent process)
|
|
t.CPtr(c.Addr(si)),
|
|
t.CPtr(c.Addr(pi))
|
|
)
|
|
|
|
if create_result == 0:
|
|
# Create process failed
|
|
if capture_output:
|
|
w32.win32base.CloseHandle(stdout_read)
|
|
w32.win32base.CloseHandle(stdout_write)
|
|
return CompletedProcess(args, -1, None, None)
|
|
|
|
# Close parent process's write end (child process already has a copy of it)
|
|
if capture_output:
|
|
w32.win32base.CloseHandle(stdout_write)
|
|
|
|
# Read stdout + wait for process to exit
|
|
# Note: must read and wait at the same time, or else deadlock will occur when child process outputs more than pipe buffer size
|
|
# (child process blocks on writing stdout, parent process blocks on WaitForSingleObject)
|
|
stdout_str: str = None
|
|
if capture_output:
|
|
buf_size: ULONG = OUTPUT_BUF_SIZE
|
|
buf: bytes = _mbuddy.alloc(buf_size)
|
|
if buf != None:
|
|
total: ULONG = 0
|
|
while 1:
|
|
bytes_read: ULONG = 0
|
|
read_result: BOOL = w32.win32file.ReadFile(
|
|
stdout_read,
|
|
buf + total,
|
|
buf_size - 1 - total,
|
|
t.CPtr(c.Addr(bytes_read)),
|
|
t.CPtr(0)
|
|
)
|
|
if read_result == 0 or bytes_read == 0:
|
|
break
|
|
total += bytes_read
|
|
# Buffer will be full, double it
|
|
if total >= buf_size - 1 - 4096:
|
|
new_size: ULONG = buf_size * 2
|
|
new_buf: bytes = _mbuddy.alloc(new_size)
|
|
if new_buf != None:
|
|
string.memcpy(new_buf, buf, total)
|
|
buf = new_buf
|
|
buf_size = new_size
|
|
else:
|
|
# Expand failed, stop reading
|
|
break
|
|
buf[total] = 0
|
|
stdout_str = str(buf)
|
|
|
|
# Wait for process to exit
|
|
w32.win32sync.WaitForSingleObject(pi.hProcess, w32.win32base.INFINITE)
|
|
|
|
# Get exit code
|
|
exit_code: ULONG = 0
|
|
w32.win32process.GetExitCodeProcess(pi.hProcess, t.CPtr(c.Addr(exit_code)))
|
|
|
|
# Close handles
|
|
w32.win32base.CloseHandle(pi.hProcess)
|
|
w32.win32base.CloseHandle(pi.hThread)
|
|
if capture_output:
|
|
w32.win32base.CloseHandle(stdout_read)
|
|
|
|
return CompletedProcess(args, exit_code, stdout_str, None)
|
|
|
|
|
|
# =============================================================================
|
|
# POSIX backend implementation
|
|
# =============================================================================
|
|
|
|
def _run_posix(args: str, capture_output: bool, text: bool) -> CompletedProcess | t.CPtr:
|
|
"""POSIX platform subprocess execution (fork, execvp, pipe, waitpid)
|
|
Note: Currently this is a stub function.
|
|
POSIX implementation requires pipe/fork/waitpid, which are not available on Windows MinGW.
|
|
It will be completed when testing on a POSIX platform."""
|
|
return CompletedProcess(args, -1, None, None)
|