mirror of
https://github.com/c3lang/c3c.git
synced 2026-02-27 12:01:16 +00:00
* Doc comment improvements * update `compression/qoi.c3` to use const enums * revert sweeping doc comment changes that impacted readability for now * Some tweaks. --------- Co-authored-by: Christoffer Lerno <christoffer@aegik.com>
542 lines
15 KiB
Plaintext
542 lines
15 KiB
Plaintext
module std::os::process @if(env::WIN32 || env::POSIX);
|
|
import std::io, libc, std::os;
|
|
|
|
// This code is based on https://github.com/sheredom/subprocess.h
|
|
|
|
faultdef
|
|
FAILED_TO_CREATE_PIPE,
|
|
FAILED_TO_OPEN_STDIN,
|
|
FAILED_TO_OPEN_STDOUT,
|
|
FAILED_TO_OPEN_STDERR,
|
|
FAILED_TO_START_PROCESS,
|
|
FAILED_TO_INITIALIZE_ACTIONS,
|
|
PROCESS_JOIN_FAILED,
|
|
PROCESS_TERMINATION_FAILED,
|
|
READ_FAILED;
|
|
|
|
struct SubProcess
|
|
{
|
|
CFile stdin_file;
|
|
CFile stdout_file;
|
|
CFile stderr_file;
|
|
|
|
Win32_HANDLE hProcess @if(env::WIN32);
|
|
Win32_HANDLE hStdInput @if(env::WIN32);
|
|
Win32_HANDLE hEventOutput @if(env::WIN32);
|
|
Win32_HANDLE hEventError @if(env::WIN32);
|
|
|
|
Pid_t child @if(!env::WIN32);
|
|
int return_status @if(!env::WIN32);
|
|
|
|
bool is_alive;
|
|
}
|
|
|
|
bitstruct SubProcessOptions : int
|
|
{
|
|
<* Combine stdout and stderr to the same file *>
|
|
bool combined_stdout_stderr;
|
|
<* Child process should inherit env variables of parent process *>
|
|
bool inherit_environment;
|
|
<* Enable async reading of stdout/stderr before completion *>
|
|
bool read_async;
|
|
<* Spawn child process without window if supported *>
|
|
bool no_window;
|
|
<*
|
|
Search for program names in the PATH variable. Always enabled on Windows.
|
|
Note: this will **not** search for paths in any provided custom environment
|
|
and instead uses the PATH of the spawning process.
|
|
*>
|
|
bool search_user_path;
|
|
<* Inherit the parent's stdin, stdout, and stderr handles instead of creating pipes *>
|
|
bool inherit_stdio;
|
|
}
|
|
|
|
fn void? create_named_pipe_helper(void** rd, void **wr) @local @if(env::WIN32)
|
|
{
|
|
Win32_SECURITY_ATTRIBUTES sa_attr = { Win32_SECURITY_ATTRIBUTES.sizeof, null, 1 };
|
|
|
|
tlocal long index = 0;
|
|
long unique = index++;
|
|
String s = string::bformat(&&(char[128]){}, `\\.\pipe\c3_subprocess.%08x.%08x.%d`, win32::getCurrentProcessId(), win32::getCurrentThreadId(), unique);
|
|
Win32_LPCSTR str = (Win32_LPCSTR)s.ptr;
|
|
*rd = win32::createNamedPipeA(
|
|
str,
|
|
win32::PIPE_ACCESS_INBOUND | win32::FILE_FLAG_OVERLAPPED,
|
|
win32::PIPE_TYPE_BYTE | win32::PIPE_WAIT,
|
|
1, 4096, 4096, 0, &sa_attr);
|
|
if (win32::INVALID_HANDLE_VALUE == *rd) return FAILED_TO_CREATE_PIPE?;
|
|
*wr = win32::createFileA(
|
|
str, win32::GENERIC_WRITE, 0, &sa_attr,
|
|
win32::OPEN_EXISTING, win32::FILE_ATTRIBUTE_NORMAL, null);
|
|
if (win32::INVALID_HANDLE_VALUE == *wr) return FAILED_TO_CREATE_PIPE?;
|
|
}
|
|
|
|
fn WString convert_command_line_win32(String[] command_line) @inline @if(env::WIN32) @local
|
|
{
|
|
DString str = dstring::temp_with_capacity(512);
|
|
foreach LINE: (i, s : command_line)
|
|
{
|
|
if (i != 0) str.append(' ');
|
|
|
|
do CHECK_WS:
|
|
{
|
|
foreach (c : s)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '\t':
|
|
case ' ':
|
|
case '\v':
|
|
break CHECK_WS;
|
|
}
|
|
}
|
|
str.append(s);
|
|
continue LINE;
|
|
};
|
|
str.append('"');
|
|
foreach (j, c : s)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '\\':
|
|
if (j != s.len - 1 && s[j + 1] == '"') str.append('\\');
|
|
case '"':
|
|
str.append('\\');
|
|
}
|
|
str.append(c);
|
|
}
|
|
str.append('"');
|
|
}
|
|
str.append('\0');
|
|
return str.str_view().to_temp_wstring()!!;
|
|
}
|
|
|
|
<*
|
|
@require !environment || !options.inherit_environment
|
|
*>
|
|
fn SubProcess? create(String[] command_line, SubProcessOptions options = {}, String[] environment = {}) @if(env::WIN32)
|
|
{
|
|
Win32_DWORD flags = win32::CREATE_UNICODE_ENVIRONMENT;
|
|
Win32_PROCESS_INFORMATION process_info;
|
|
Win32_SECURITY_ATTRIBUTES sa_attr = { Win32_SECURITY_ATTRIBUTES.sizeof, null, 1 };
|
|
Win32_STARTUPINFOW start_info = {
|
|
.cb = Win32_STARTUPINFOW.sizeof,
|
|
};
|
|
|
|
if (options.no_window) flags |= win32::CREATE_NO_WINDOW;
|
|
|
|
// Only set STARTF_USESTDHANDLES if we're not inheriting stdio
|
|
if (!options.inherit_stdio) start_info.dwFlags = win32::STARTF_USESTDHANDLES;
|
|
|
|
CFile stdin;
|
|
CFile stdout;
|
|
CFile stderr;
|
|
void* rd = null;
|
|
void* wr = null;
|
|
|
|
// Only create pipes if not inheriting stdio
|
|
if (!options.inherit_stdio)
|
|
{
|
|
if (!win32::createPipe(&rd, &wr, &sa_attr, 0)) return FAILED_TO_CREATE_PIPE?;
|
|
// TODO defer catch
|
|
if (!win32::setHandleInformation(wr, win32::HANDLE_FLAG_INHERIT, 0)) return FAILED_TO_CREATE_PIPE?;
|
|
}
|
|
|
|
@stack_mem(2048; Allocator mem)
|
|
{
|
|
WString used_environment = null;
|
|
if (!options.inherit_environment)
|
|
{
|
|
DString env = dstring::new_with_capacity(mem, 64);
|
|
if (!environment.len)
|
|
{
|
|
env.append("\0");
|
|
}
|
|
foreach (String s : environment)
|
|
{
|
|
env.append(s);
|
|
env.append("\0");
|
|
}
|
|
env.append("\0");
|
|
used_environment = env.str_view().to_wstring(mem)!;
|
|
}
|
|
|
|
// Handle stdin pipe if not inheriting
|
|
if (!options.inherit_stdio)
|
|
{
|
|
int fd = win32::_open_osfhandle((iptr)wr, 0);
|
|
if (fd != -1)
|
|
{
|
|
stdin = win32::_fdopen(fd, "wb");
|
|
if (!stdin) return FAILED_TO_OPEN_STDIN?;
|
|
}
|
|
start_info.hStdInput = rd;
|
|
|
|
// Handle stdout pipe
|
|
if (options.read_async)
|
|
{
|
|
create_named_pipe_helper(&rd, &wr)!;
|
|
}
|
|
else
|
|
{
|
|
if (!win32::createPipe(&rd, &wr, &sa_attr, 0)) return FAILED_TO_CREATE_PIPE?;
|
|
}
|
|
if (!win32::setHandleInformation(rd, win32::HANDLE_FLAG_INHERIT, 0)) return FAILED_TO_CREATE_PIPE?;
|
|
|
|
fd = win32::_open_osfhandle((iptr)rd, 0);
|
|
if (fd != -1)
|
|
{
|
|
stdout = win32::_fdopen(fd, "rb");
|
|
if (!stdout) return FAILED_TO_OPEN_STDOUT?;
|
|
}
|
|
|
|
start_info.hStdOutput = wr;
|
|
|
|
// Handle stderr pipe or combine with stdout
|
|
do
|
|
{
|
|
if (options.combined_stdout_stderr)
|
|
{
|
|
stderr = stdout;
|
|
start_info.hStdError = start_info.hStdOutput;
|
|
break;
|
|
}
|
|
if (options.read_async)
|
|
{
|
|
create_named_pipe_helper(&rd, &wr)!;
|
|
}
|
|
else
|
|
{
|
|
if (!win32::createPipe(&rd, &wr, &sa_attr, 0)) return FAILED_TO_CREATE_PIPE?;
|
|
}
|
|
if (!win32::setHandleInformation(rd, win32::HANDLE_FLAG_INHERIT, 0)) return FAILED_TO_CREATE_PIPE?;
|
|
|
|
fd = win32::_open_osfhandle((iptr)rd, 0);
|
|
if (fd != -1)
|
|
{
|
|
stderr = win32::_fdopen(fd, "rb");
|
|
if (!stderr) return FAILED_TO_OPEN_STDERR?;
|
|
}
|
|
start_info.hStdError = wr;
|
|
};
|
|
}
|
|
|
|
void *event_output = null;
|
|
void *event_error = null;
|
|
if (!options.inherit_stdio && options.read_async)
|
|
{
|
|
event_output = win32::createEventA(&sa_attr, 1, 1, null);
|
|
event_error = win32::createEventA(&sa_attr, 1, 1, null);
|
|
}
|
|
|
|
if (!win32::createProcessW(
|
|
null,
|
|
convert_command_line_win32(command_line),
|
|
null, // process security attributes
|
|
null, // primary thread security attributes
|
|
1, // handles are inherited
|
|
flags, // creation flags
|
|
used_environment, // environment
|
|
null, // use parent dir
|
|
&start_info, // startup info ptr
|
|
&process_info)) return FAILED_TO_START_PROCESS?;
|
|
};
|
|
|
|
// We don't need the handle of the primary thread in the called process.
|
|
win32::closeHandle(process_info.hThread);
|
|
|
|
// Close handles if not inheriting stdio
|
|
if (!options.inherit_stdio && start_info.hStdOutput)
|
|
{
|
|
win32::closeHandle(start_info.hStdOutput);
|
|
if (start_info.hStdOutput != start_info.hStdError) win32::closeHandle(start_info.hStdError);
|
|
}
|
|
|
|
return {
|
|
.hProcess = process_info.hProcess,
|
|
.hStdInput = options.inherit_stdio ? null : start_info.hStdInput,
|
|
.stdin_file = stdin,
|
|
.stdout_file = stdout,
|
|
.stderr_file = stderr,
|
|
.is_alive = true,
|
|
};
|
|
}
|
|
|
|
<*
|
|
@require command_line.len > 0
|
|
*>
|
|
fn ZString* copy_command_line(Allocator mem, String[] command_line) @local @inline @if(env::POSIX)
|
|
{
|
|
ZString* copy = allocator::alloc_array(mem, ZString, command_line.len + 1);
|
|
foreach (i, str : command_line)
|
|
{
|
|
copy[i] = str.zstr_copy(mem);
|
|
}
|
|
copy[command_line.len] = null;
|
|
return copy;
|
|
}
|
|
|
|
const ZString[1] EMPTY_ENVIRONMENT @if(env::POSIX) = { null };
|
|
fn ZString* copy_env(Allocator mem, String[] environment) @local @inline @if(env::POSIX)
|
|
{
|
|
if (!environment.len) return &EMPTY_ENVIRONMENT;
|
|
ZString* copy = allocator::alloc_array(mem, ZString, environment.len + 1);
|
|
copy[environment.len] = null;
|
|
foreach (i, str : environment)
|
|
{
|
|
copy[i] = str.zstr_copy(mem);
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
fn String? execute_stdout_to_buffer(char[] buffer, String[] command_line, SubProcessOptions options = {}, String[] environment = {})
|
|
{
|
|
SubProcess process = process::create(command_line, options, environment)!;
|
|
process.join()!;
|
|
usz len = process.read_stdout(buffer.ptr, buffer.len)!;
|
|
if (len == 0) return "";
|
|
return (String)buffer[:len - 1];
|
|
}
|
|
|
|
<*
|
|
@require !environment || !options.inherit_environment
|
|
*>
|
|
fn SubProcess? create(String[] command_line, SubProcessOptions options = {}, String[] environment = {}) @if(env::POSIX)
|
|
{
|
|
CInt[2] stdinfd;
|
|
CInt[2] stdoutfd;
|
|
CInt[2] stderrfd;
|
|
CFile stdin = null;
|
|
CFile stdout = null;
|
|
CFile stderr = null;
|
|
|
|
Posix_spawn_file_actions_t actions;
|
|
if (posix::spawn_file_actions_init(&actions)) return FAILED_TO_INITIALIZE_ACTIONS?;
|
|
defer posix::spawn_file_actions_destroy(&actions);
|
|
|
|
// Only set up pipes if not inheriting stdio
|
|
if (!options.inherit_stdio)
|
|
{
|
|
if (posix::pipe(&stdinfd)) return FAILED_TO_OPEN_STDIN?;
|
|
if (posix::pipe(&stdoutfd)) return FAILED_TO_OPEN_STDOUT?;
|
|
if (!options.combined_stdout_stderr && posix::pipe(&stderrfd)) return FAILED_TO_OPEN_STDERR?;
|
|
|
|
if (posix::spawn_file_actions_addclose(&actions, stdinfd[1])) return FAILED_TO_OPEN_STDIN?;
|
|
if (posix::spawn_file_actions_adddup2(&actions, stdinfd[0], libc::STDIN_FD)) return FAILED_TO_OPEN_STDIN?;
|
|
if (posix::spawn_file_actions_addclose(&actions, stdoutfd[0])) return FAILED_TO_OPEN_STDOUT?;
|
|
if (posix::spawn_file_actions_adddup2(&actions, stdoutfd[1], libc::STDOUT_FD)) return FAILED_TO_OPEN_STDOUT?;
|
|
|
|
if (options.combined_stdout_stderr)
|
|
{
|
|
if (posix::spawn_file_actions_adddup2(&actions, libc::STDOUT_FD, libc::STDERR_FD)) return FAILED_TO_OPEN_STDERR?;
|
|
}
|
|
else
|
|
{
|
|
if (posix::spawn_file_actions_addclose(&actions, stderrfd[0])) return FAILED_TO_OPEN_STDERR?;
|
|
if (posix::spawn_file_actions_adddup2(&actions, stderrfd[1], libc::STDERR_FD)) return FAILED_TO_OPEN_STDERR?;
|
|
}
|
|
}
|
|
|
|
Pid_t child;
|
|
@stack_mem(2048; Allocator mem)
|
|
{
|
|
ZString* command_line_copy = copy_command_line(mem, command_line);
|
|
ZString* used_environment = options.inherit_environment ? posix::environ : copy_env(mem, environment);
|
|
if (options.search_user_path)
|
|
{
|
|
if (posix::spawnp(&child, command_line_copy[0], &actions, null, command_line_copy, used_environment)) return FAILED_TO_START_PROCESS?;
|
|
}
|
|
else
|
|
{
|
|
if (posix::spawnp(&child, command_line_copy[0], &actions, null, command_line_copy, used_environment)) return FAILED_TO_START_PROCESS?;
|
|
}
|
|
};
|
|
|
|
// Only set up file handles if not inheriting stdio
|
|
if (!options.inherit_stdio)
|
|
{
|
|
libc::close(stdinfd[0]);
|
|
stdin = libc::fdopen(stdinfd[1], "wb");
|
|
libc::close(stdoutfd[1]);
|
|
stdout = libc::fdopen(stdoutfd[0], "rb");
|
|
|
|
do
|
|
{
|
|
if (options.combined_stdout_stderr)
|
|
{
|
|
stderr = stdout;
|
|
break;
|
|
}
|
|
libc::close(stderrfd[1]);
|
|
stderr = libc::fdopen(stderrfd[0], "rb");
|
|
};
|
|
}
|
|
|
|
return {
|
|
.stdin_file = stdin,
|
|
.stdout_file = stdout,
|
|
.stderr_file = stderr,
|
|
.child = child,
|
|
.is_alive = true,
|
|
};
|
|
}
|
|
|
|
fn CInt? SubProcess.join(&self) @if(env::POSIX)
|
|
{
|
|
if (self.stdin_file)
|
|
{
|
|
libc::fclose(self.stdin_file);
|
|
self.stdin_file = null;
|
|
}
|
|
CInt status;
|
|
if (self.child && self.child != posix::waitpid(self.child, &status, 0)) return PROCESS_JOIN_FAILED?;
|
|
|
|
self.child = 0;
|
|
self.is_alive = false;
|
|
|
|
return self.return_status = posix::wIFEXITED(status) ? posix::wEXITSTATUS(status) : libc::EXIT_FAILURE;
|
|
}
|
|
|
|
fn File SubProcess.stdout(&self)
|
|
{
|
|
if (!self.stdout_file) return (File){}; // Return an empty File struct
|
|
return file::from_handle(self.stdout_file);
|
|
}
|
|
|
|
fn File SubProcess.stderr(&self)
|
|
{
|
|
if (!self.stderr_file) return (File){}; // Return an empty File struct
|
|
return file::from_handle(self.stderr_file);
|
|
}
|
|
|
|
fn CInt? SubProcess.join(&self) @if(env::WIN32)
|
|
{
|
|
if (self.stdin_file)
|
|
{
|
|
libc::fclose(self.stdin_file);
|
|
self.stdin_file = null;
|
|
}
|
|
if (self.hStdInput)
|
|
{
|
|
win32::closeHandle(self.hStdInput);
|
|
self.hStdInput = null;
|
|
}
|
|
win32::waitForSingleObject(self.hProcess, win32::INFINITE);
|
|
Win32_DWORD return_code @noinit;
|
|
if (!win32::getExitCodeProcess(self.hProcess, &return_code)) return PROCESS_JOIN_FAILED?;
|
|
self.is_alive = false;
|
|
return return_code;
|
|
}
|
|
|
|
fn bool SubProcess.destroy(&self)
|
|
{
|
|
if (self.stdin_file) libc::fclose(self.stdin_file);
|
|
if (self.stdout_file)
|
|
{
|
|
libc::fclose(self.stdout_file);
|
|
if (self.stdout_file != self.stderr_file) libc::fclose(self.stderr_file);
|
|
}
|
|
self.stdin_file = self.stdout_file = self.stderr_file = null;
|
|
$if env::WIN32:
|
|
if (self.hProcess) win32::closeHandle(self.hProcess);
|
|
if (self.hStdInput) win32::closeHandle(self.hStdInput);
|
|
if (self.hEventOutput) win32::closeHandle(self.hEventOutput);
|
|
if (self.hEventError) win32::closeHandle(self.hEventError);
|
|
self.hProcess = self.hStdInput = self.hEventOutput = self.hEventError = null;
|
|
$endif;
|
|
return true;
|
|
}
|
|
|
|
fn void? SubProcess.terminate(&self)
|
|
{
|
|
$if env::WIN32:
|
|
if (!win32::terminateProcess(self.hProcess, 99)) return PROCESS_TERMINATION_FAILED?;
|
|
$else
|
|
if (posix::kill(self.child, 9)) return PROCESS_TERMINATION_FAILED?;
|
|
$endif
|
|
}
|
|
|
|
<*
|
|
@require size <= Win32_DWORD.max
|
|
*>
|
|
fn usz? read_from_file_win32(CFile file, Win32_HANDLE event_handle, char* buffer, usz size) @if(env::WIN32) @local
|
|
{
|
|
CInt fd = libc::fileno(file);
|
|
Win32_DWORD bytes_read = 0;
|
|
Win32_OVERLAPPED overlapped = { .hEvent = event_handle };
|
|
Win32_HANDLE handle = (Win32_HANDLE)win32::_get_osfhandle(fd);
|
|
if (!win32::readFile(handle, buffer, (Win32_DWORD)size, &bytes_read, &overlapped))
|
|
{
|
|
// Means we've got an async read!
|
|
if (win32::getLastError() == win32::ERROR_IO_PENDING)
|
|
{
|
|
if (!win32::getOverlappedResult(handle, &overlapped, &bytes_read, 1))
|
|
{
|
|
switch (win32::getLastError())
|
|
{
|
|
case win32::ERROR_IO_INCOMPLETE:
|
|
case win32::ERROR_HANDLE_EOF:
|
|
break;
|
|
default:
|
|
return READ_FAILED?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bytes_read;
|
|
}
|
|
fn usz? read_from_file_posix(CFile file, char* buffer, usz size) @if(env::POSIX) @local
|
|
{
|
|
isz bytes_read = libc::read(libc::fileno(file), buffer, size);
|
|
if (bytes_read < 0) return READ_FAILED?;
|
|
return bytes_read;
|
|
}
|
|
|
|
fn usz? SubProcess.read_stdout(&self, char* buffer, usz size)
|
|
{
|
|
if (!self.stdout_file) return 0; // No output available when inheriting stdio
|
|
|
|
$if env::WIN32:
|
|
return read_from_file_win32(self.stdout_file, self.hEventOutput, buffer, size);
|
|
$else
|
|
return read_from_file_posix(self.stdout_file, buffer, size);
|
|
$endif
|
|
}
|
|
|
|
|
|
fn usz? SubProcess.read_stderr(&self, char* buffer, usz size)
|
|
{
|
|
if (!self.stderr_file) return 0; // No output available when inheriting stdio
|
|
|
|
$if env::WIN32:
|
|
return read_from_file_win32(self.stderr_file, self.hEventError, buffer, size);
|
|
$else
|
|
return read_from_file_posix(self.stderr_file, buffer, size);
|
|
$endif
|
|
}
|
|
|
|
fn bool? SubProcess.is_running(&self)
|
|
{
|
|
if (!self.is_alive) return false;
|
|
$if env::WIN32:
|
|
bool is_alive = win32::waitForSingleObject(self.hProcess, 0) != win32::WAIT_OBJECT_0;
|
|
if (!is_alive) self.is_alive = false;
|
|
return is_alive;
|
|
$else
|
|
CInt status;
|
|
bool is_alive = posix::waitpid(self.child, &status, posix::WNOHANG) == 0;
|
|
if (is_alive) return true;
|
|
self.is_alive = false;
|
|
self.return_status = posix::wIFEXITED(status) ? posix::wEXITSTATUS(status) : libc::EXIT_FAILURE;
|
|
self.child = 0;
|
|
self.join()!;
|
|
return false;
|
|
$endif
|
|
}
|
|
|
|
fn usz? SubProcess.write_to_stdin(&self, char[] buffer)
|
|
{
|
|
if (!self.stdin_file) return FAILED_TO_OPEN_STDIN?;
|
|
return os::native_fwrite(self.stdin_file, buffer);
|
|
}
|