Merge pull request #4290 from flysand7/pipe-has-data

[os2/process]: Implement `process_exec`, and `pipe_has_data`
This commit is contained in:
gingerBill
2024-10-01 11:30:28 +01:00
committed by GitHub
7 changed files with 316 additions and 120 deletions
+3 -1
View File
@@ -55,13 +55,15 @@ _get_platform_error :: proc() -> Error {
case win32.ERROR_NEGATIVE_SEEK: case win32.ERROR_NEGATIVE_SEEK:
return .Invalid_Offset return .Invalid_Offset
case win32.ERROR_BROKEN_PIPE:
return .Broken_Pipe
case case
win32.ERROR_BAD_ARGUMENTS, win32.ERROR_BAD_ARGUMENTS,
win32.ERROR_INVALID_PARAMETER, win32.ERROR_INVALID_PARAMETER,
win32.ERROR_NOT_ENOUGH_MEMORY, win32.ERROR_NOT_ENOUGH_MEMORY,
win32.ERROR_NO_MORE_FILES, win32.ERROR_NO_MORE_FILES,
win32.ERROR_LOCK_VIOLATION, win32.ERROR_LOCK_VIOLATION,
win32.ERROR_BROKEN_PIPE,
win32.ERROR_CALL_NOT_IMPLEMENTED, win32.ERROR_CALL_NOT_IMPLEMENTED,
win32.ERROR_INSUFFICIENT_BUFFER, win32.ERROR_INSUFFICIENT_BUFFER,
win32.ERROR_INVALID_NAME, win32.ERROR_INVALID_NAME,
+37
View File
@@ -1,6 +1,43 @@
package os2 package os2
/*
Create an anonymous pipe.
This procedure creates an anonymous pipe, returning two ends of the pipe, `r`
and `w`. The file `r` is the readable end of the pipe. The file `w` is a
writeable end of the pipe.
Pipes are used as an inter-process communication mechanism, to communicate
between a parent and a child process. The child uses one end of the pipe to
write data, and the parent uses the other end to read from the pipe
(or vice-versa). When a parent passes one of the ends of the pipe to the child
process, that end of the pipe needs to be closed by the parent, before any data
is attempted to be read.
Although pipes look like files and is compatible with most file APIs in package
os2, the way it's meant to be read is different. Due to asynchronous nature of
the communication channel, the data may not be present at the time of a read
request. The other scenario is when a pipe has no data because the other end
of the pipe was closed by the child process.
*/
@(require_results) @(require_results)
pipe :: proc() -> (r, w: ^File, err: Error) { pipe :: proc() -> (r, w: ^File, err: Error) {
return _pipe() return _pipe()
} }
/*
Check if the pipe has any data.
This procedure checks whether a read-end of the pipe has data that can be
read, and returns `true`, if the pipe has readable data, and `false` if the
pipe is empty. This procedure does not block the execution of the current
thread.
**Note**: If the other end of the pipe was closed by the child process, the
`.Broken_Pipe`
can be returned by this procedure. Handle these errors accordingly.
*/
@(require_results)
pipe_has_data :: proc(r: ^File) -> (ok: bool, err: Error) {
return _pipe_has_data(r)
}
+26
View File
@@ -15,3 +15,29 @@ _pipe :: proc() -> (r, w: ^File, err: Error) {
return return
} }
@(require_results)
_pipe_has_data :: proc(r: ^File) -> (ok: bool, err: Error) {
if r == nil || r.impl == nil {
return false, nil
}
fd := linux.Fd((^File_Impl)(r.impl).fd)
poll_fds := []linux.Poll_Fd {
linux.Poll_Fd {
fd = fd,
events = {.IN, .HUP},
},
}
n, errno := linux.poll(poll_fds, 0)
if n != 1 || errno != nil {
return false, _get_platform_error(errno)
}
pipe_events := poll_fds[0].revents
if pipe_events >= {.IN} {
return true, nil
}
if pipe_events >= {.HUP} {
return false, .Broken_Pipe
}
return false, nil
}
+25
View File
@@ -44,3 +44,28 @@ _pipe :: proc() -> (r, w: ^File, err: Error) {
return return
} }
@(require_results)
_pipe_has_data :: proc(r: ^File) -> (ok: bool, err: Error) {
if r == nil || r.impl == nil {
return false, nil
}
fd := posix.FD((^File_Impl)(r.impl).fd)
poll_fds := []posix.pollfd {
posix.pollfd {
fd = fd,
events = {.IN, .HUP},
},
}
n := posix.poll(raw_data(poll_fds), u32(len(poll_fds)), 0)
if n != 1 {
return false, _get_platform_error()
}
pipe_events := poll_fds[0].revents
if pipe_events >= {.IN} {
return true, nil
}
if pipe_events >= {.HUP} {
return false, .Broken_Pipe
}
return false, nil
}
+12
View File
@@ -15,3 +15,15 @@ _pipe :: proc() -> (r, w: ^File, err: Error) {
return new_file(uintptr(p[0]), ""), new_file(uintptr(p[1]), ""), nil return new_file(uintptr(p[0]), ""), new_file(uintptr(p[1]), ""), nil
} }
@(require_results)
_pipe_has_data :: proc(r: ^File) -> (ok: bool, err: Error) {
if r == nil || r.impl == nil {
return false, nil
}
handle := win32.HANDLE((^File_Impl)(r.impl).fd)
bytes_available: u32
if !win32.PeekNamedPipe(handle, nil, 0, nil, &bytes_available, nil) {
return false, _get_platform_error()
}
return bytes_available > 0, nil
}
+205 -119
View File
@@ -1,16 +1,17 @@
package os2 package os2
import "base:runtime" import "base:runtime"
import "core:strings"
import "core:time" import "core:time"
/* /*
In procedures that explicitly state this as one of the allowed values, In procedures that explicitly state this as one of the allowed values,
specifies an infinite timeout. specifies an infinite timeout.
*/ */
TIMEOUT_INFINITE :: time.MIN_DURATION // Note(flysand): Any negative duration will be treated as infinity TIMEOUT_INFINITE :: time.MIN_DURATION // Note(flysand): Any negative duration will be treated as infinity
/* /*
Arguments to the current process. Arguments to the current process.
*/ */
args := get_args() args := get_args()
@@ -24,17 +25,17 @@ get_args :: proc() -> []string {
} }
/* /*
Exit the current process. Exit the current process.
*/ */
exit :: proc "contextless" (code: int) -> ! { exit :: proc "contextless" (code: int) -> ! {
_exit(code) _exit(code)
} }
/* /*
Obtain the UID of the current process. Obtain the UID of the current process.
**Note(windows)**: Windows doesn't follow the posix permissions model, so **Note(windows)**: Windows doesn't follow the posix permissions model, so
the function simply returns -1. the function simply returns -1.
*/ */
@(require_results) @(require_results)
get_uid :: proc() -> int { get_uid :: proc() -> int {
@@ -42,15 +43,15 @@ get_uid :: proc() -> int {
} }
/* /*
Obtain the effective UID of the current process. Obtain the effective UID of the current process.
The effective UID is typically the same as the UID of the process. In case The effective UID is typically the same as the UID of the process. In case
the process was run by a user with elevated permissions, the process may the process was run by a user with elevated permissions, the process may
lower the privilege to perform some tasks without privilege. In these cases lower the privilege to perform some tasks without privilege. In these cases
the real UID of the process and the effective UID are different. the real UID of the process and the effective UID are different.
**Note(windows)**: Windows doesn't follow the posix permissions model, so **Note(windows)**: Windows doesn't follow the posix permissions model, so
the function simply returns -1. the function simply returns -1.
*/ */
@(require_results) @(require_results)
get_euid :: proc() -> int { get_euid :: proc() -> int {
@@ -58,10 +59,10 @@ get_euid :: proc() -> int {
} }
/* /*
Obtain the GID of the current process. Obtain the GID of the current process.
**Note(windows)**: Windows doesn't follow the posix permissions model, so **Note(windows)**: Windows doesn't follow the posix permissions model, so
the function simply returns -1. the function simply returns -1.
*/ */
@(require_results) @(require_results)
get_gid :: proc() -> int { get_gid :: proc() -> int {
@@ -69,15 +70,15 @@ get_gid :: proc() -> int {
} }
/* /*
Obtain the effective GID of the current process. Obtain the effective GID of the current process.
The effective GID is typically the same as the GID of the process. In case
the process was run by a user with elevated permissions, the process may
lower the privilege to perform some tasks without privilege. In these cases
the real GID of the process and the effective GID are different.
**Note(windows)**: Windows doesn't follow the posix permissions model, so The effective GID is typically the same as the GID of the process. In case
the function simply returns -1. the process was run by a user with elevated permissions, the process may
lower the privilege to perform some tasks without privilege. In these cases
the real GID of the process and the effective GID are different.
**Note(windows)**: Windows doesn't follow the posix permissions model, so
the function simply returns -1.
*/ */
@(require_results) @(require_results)
get_egid :: proc() -> int { get_egid :: proc() -> int {
@@ -85,7 +86,7 @@ get_egid :: proc() -> int {
} }
/* /*
Obtain the ID of the current process. Obtain the ID of the current process.
*/ */
@(require_results) @(require_results)
get_pid :: proc() -> int { get_pid :: proc() -> int {
@@ -93,13 +94,13 @@ get_pid :: proc() -> int {
} }
/* /*
Obtain the ID of the parent process. Obtain the ID of the parent process.
**Note(windows)**: Windows does not mantain strong relationships between **Note(windows)**: Windows does not mantain strong relationships between
parent and child processes. This function returns the ID of the process parent and child processes. This function returns the ID of the process
that has created the current process. In case the parent has died, the ID that has created the current process. In case the parent has died, the ID
returned by this function can identify a non-existent or a different returned by this function can identify a non-existent or a different
process. process.
*/ */
@(require_results) @(require_results)
get_ppid :: proc() -> int { get_ppid :: proc() -> int {
@@ -107,7 +108,7 @@ get_ppid :: proc() -> int {
} }
/* /*
Obtain ID's of all processes running in the system. Obtain ID's of all processes running in the system.
*/ */
@(require_results) @(require_results)
process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) { process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) {
@@ -115,9 +116,9 @@ process_list :: proc(allocator: runtime.Allocator) -> ([]int, Error) {
} }
/* /*
Bit set specifying which fields of the `Process_Info` struct need to be Bit set specifying which fields of the `Process_Info` struct need to be
obtained by the `process_info()` procedure. Each bit corresponds to a obtained by the `process_info()` procedure. Each bit corresponds to a
field in the `Process_Info` struct. field in the `Process_Info` struct.
*/ */
Process_Info_Fields :: bit_set[Process_Info_Field] Process_Info_Fields :: bit_set[Process_Info_Field]
Process_Info_Field :: enum { Process_Info_Field :: enum {
@@ -134,8 +135,8 @@ Process_Info_Field :: enum {
ALL_INFO :: Process_Info_Fields{.Executable_Path, .PPid, .Priority, .Command_Line, .Command_Args, .Environment, .Username, .Working_Dir} ALL_INFO :: Process_Info_Fields{.Executable_Path, .PPid, .Priority, .Command_Line, .Command_Args, .Environment, .Username, .Working_Dir}
/* /*
Contains information about the process as obtained by the `process_info()` Contains information about the process as obtained by the `process_info()`
procedure. procedure.
*/ */
Process_Info :: struct { Process_Info :: struct {
// The information about a process the struct contains. `pid` is always // The information about a process the struct contains. `pid` is always
@@ -162,19 +163,19 @@ Process_Info :: struct {
} }
/* /*
Obtain information about a process. Obtain information about a process.
This procedure obtains an information, specified by `selection` parameter of This procedure obtains an information, specified by `selection` parameter of
a process given by `pid`. a process given by `pid`.
Use `free_process_info` to free the memory allocated by this procedure. The Use `free_process_info` to free the memory allocated by this procedure. The
`free_process_info` procedure needs to be called, even if this procedure `free_process_info` procedure needs to be called, even if this procedure
returned an error, as some of the fields may have been allocated. returned an error, as some of the fields may have been allocated.
**Note**: The resulting information may or may contain the fields specified **Note**: The resulting information may or may contain the fields specified
by the `selection` parameter. Always check whether the returned by the `selection` parameter. Always check whether the returned
`Process_Info` struct has the required fields before checking the error code `Process_Info` struct has the required fields before checking the error code
returned by this procedure. returned by this procedure.
*/ */
@(require_results) @(require_results)
process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) {
@@ -182,20 +183,20 @@ process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator:
} }
/* /*
Obtain information about a process. Obtain information about a process.
This procedure obtains information, specified by `selection` parameter This procedure obtains information, specified by `selection` parameter
about a process that has been opened by the application, specified in about a process that has been opened by the application, specified in
the `process` parameter. the `process` parameter.
Use `free_process_info` to free the memory allocated by this procedure. The Use `free_process_info` to free the memory allocated by this procedure. The
`free_process_info` procedure needs to be called, even if this procedure `free_process_info` procedure needs to be called, even if this procedure
returned an error, as some of the fields may have been allocated. returned an error, as some of the fields may have been allocated.
**Note**: The resulting information may or may contain the fields specified **Note**: The resulting information may or may contain the fields specified
by the `selection` parameter. Always check whether the returned by the `selection` parameter. Always check whether the returned
`Process_Info` struct has the required fields before checking the error code `Process_Info` struct has the required fields before checking the error code
returned by this procedure. returned by this procedure.
*/ */
@(require_results) @(require_results)
process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) {
@@ -203,19 +204,19 @@ process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields,
} }
/* /*
Obtain information about the current process. Obtain information about the current process.
This procedure obtains the information, specified by `selection` parameter This procedure obtains the information, specified by `selection` parameter
about the currently running process. about the currently running process.
Use `free_process_info` to free the memory allocated by this procedure. The Use `free_process_info` to free the memory allocated by this procedure. The
`free_process_info` procedure needs to be called, even if this procedure `free_process_info` procedure needs to be called, even if this procedure
returned an error, as some of the fields may have been allocated. returned an error, as some of the fields may have been allocated.
**Note**: The resulting information may or may contain the fields specified **Note**: The resulting information may or may contain the fields specified
by the `selection` parameter. Always check whether the returned by the `selection` parameter. Always check whether the returned
`Process_Info` struct has the required fields before checking the error code `Process_Info` struct has the required fields before checking the error code
returned by this procedure. returned by this procedure.
*/ */
@(require_results) @(require_results)
current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) { current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (Process_Info, Error) {
@@ -223,7 +224,7 @@ current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.
} }
/* /*
Obtain information about the specified process. Obtain information about the specified process.
*/ */
process_info :: proc { process_info :: proc {
process_info_by_pid, process_info_by_pid,
@@ -232,11 +233,11 @@ process_info :: proc {
} }
/* /*
Free the information about the process. Free the information about the process.
This procedure frees the memory occupied by process info using the provided This procedure frees the memory occupied by process info using the provided
allocator. The allocator needs to be the same allocator that was supplied allocator. The allocator needs to be the same allocator that was supplied
to the `process_info` function. to the `process_info` function.
*/ */
free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) { free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) {
delete(pi.executable_path, allocator) delete(pi.executable_path, allocator)
@@ -254,13 +255,13 @@ free_process_info :: proc(pi: Process_Info, allocator: runtime.Allocator) {
} }
/* /*
Represents a process handle. Represents a process handle.
When a process dies, the OS is free to re-use the pid of that process. The When a process dies, the OS is free to re-use the pid of that process. The
`Process` struct represents a handle to the process that will refer to a `Process` struct represents a handle to the process that will refer to a
specific process, even after it has died. specific process, even after it has died.
**Note(linux)**: The `handle` will be referring to pidfd. **Note(linux)**: The `handle` will be referring to pidfd.
*/ */
Process :: struct { Process :: struct {
pid: int, pid: int,
@@ -276,13 +277,13 @@ Process_Open_Flag :: enum {
} }
/* /*
Open a process handle using it's pid. Open a process handle using it's pid.
This procedure obtains a process handle of a process specified by `pid`. This procedure obtains a process handle of a process specified by `pid`.
This procedure can be subject to race conditions. See the description of This procedure can be subject to race conditions. See the description of
`Process`. `Process`.
Use `process_close()` function to close the process handle. Use `process_close()` function to close the process handle.
*/ */
@(require_results) @(require_results)
process_open :: proc(pid: int, flags := Process_Open_Flags {}) -> (Process, Error) { process_open :: proc(pid: int, flags := Process_Open_Flags {}) -> (Process, Error) {
@@ -322,31 +323,117 @@ Process_Desc :: struct {
} }
/* /*
Create a new process and obtain its handle. Create a new process and obtain its handle.
This procedure creates a new process, with a given command and environment This procedure creates a new process, with a given command and environment
strings as parameters. Use `environ()` to inherit the environment of the strings as parameters. Use `environ()` to inherit the environment of the
current process. current process.
The `desc` parameter specifies the description of how the process should The `desc` parameter specifies the description of how the process should
be created. It contains information such as the command line, the be created. It contains information such as the command line, the
environment of the process, the starting directory and many other options. environment of the process, the starting directory and many other options.
Most of the fields in the struct can be set to `nil` or an empty value. Most of the fields in the struct can be set to `nil` or an empty value.
Use `process_close` to close the handle to the process. Note, that this
is not the same as terminating the process. One can terminate the process
and not close the handle, in which case the handle would be leaked. In case
the function returns an error, an invalid handle is returned.
This procedure is not thread-safe. It may alter the inheritance properties Use `process_close` to close the handle to the process. Note, that this
of file handles in an unpredictable manner. In case multiple threads change is not the same as terminating the process. One can terminate the process
handle inheritance properties, make sure to serialize all those calls. and not close the handle, in which case the handle would be leaked. In case
the function returns an error, an invalid handle is returned.
This procedure is not thread-safe. It may alter the inheritance properties
of file handles in an unpredictable manner. In case multiple threads change
handle inheritance properties, make sure to serialize all those calls.
*/ */
@(require_results) @(require_results)
process_start :: proc(desc := Process_Desc {}) -> (Process, Error) { process_start :: proc(desc: Process_Desc) -> (Process, Error) {
return _process_start(desc) return _process_start(desc)
} }
/*
Execute the process and capture stdout and stderr streams.
This procedure creates a new process, with a given command and environment
strings as parameters, and waits until the process finishes execution. While
the process is running, this procedure accumulates the output of its stdout
and stderr streams and returns byte slices containing the captured data from
the streams.
This procedure expects that `stdout` and `stderr` fields of the `desc` parameter
are left at default, i.e. a `nil` value. You can not capture stdout/stderr and
redirect it to a file at the same time.
This procedure does not free `stdout` and `stderr` slices before an error is
returned. Make sure to call `delete` on these slices.
*/
@(require_results)
process_exec :: proc(
desc: Process_Desc,
allocator: runtime.Allocator,
loc := #caller_location,
) -> (
state: Process_State,
stdout: []u8,
stderr: []u8,
err: Error,
) {
assert(desc.stdout == nil, "Cannot redirect stdout when it's being captured", loc)
assert(desc.stderr == nil, "Cannot redirect stderr when it's being captured", loc)
stdout_r, stdout_w := pipe() or_return
defer close(stdout_r)
stderr_r, stderr_w := pipe() or_return
defer close(stdout_w)
process: Process
{
// NOTE(flysand): Make sure the write-ends are closed, regardless
// of the outcome. This makes read-ends readable on our side.
defer close(stdout_w)
defer close(stderr_w)
desc := desc
desc.stdout = stdout_w
desc.stderr = stderr_w
process = process_start(desc) or_return
}
stdout_builder := strings.builder_make(allocator) or_return
stderr_builder := strings.builder_make(allocator) or_return
read_data: for {
buf: [1024]u8
n: int
has_data: bool
hangup := false
has_data, err = pipe_has_data(stdout_r)
if has_data {
n, err = read(stdout_r, buf[:])
strings.write_bytes(&stdout_builder, buf[:n])
}
switch err {
case nil: // nothing
case .Broken_Pipe:
hangup = true
case:
return
}
has_data, err = pipe_has_data(stderr_r)
if has_data {
n, err = read(stderr_r, buf[:])
strings.write_bytes(&stderr_builder, buf[:n])
}
switch err {
case nil: // nothing
case .Broken_Pipe:
hangup = true
case:
return
}
if hangup {
break read_data
}
}
err = nil
stdout = transmute([]u8) strings.to_string(stdout_builder)
stderr = transmute([]u8) strings.to_string(stderr_builder)
state = process_wait(process) or_return
return
}
/* /*
The state of the process after it has finished execution. The state of the process after it has finished execution.
*/ */
@@ -371,17 +458,17 @@ Process_State :: struct {
} }
/* /*
Wait for a process event. Wait for a process event.
This procedure blocks the execution until the process has exited or the This procedure blocks the execution until the process has exited or the
timeout (if specified) has reached zero. If the timeout is `TIMEOUT_INFINITE`, timeout (if specified) has reached zero. If the timeout is `TIMEOUT_INFINITE`,
no timeout restriction is imposed and the procedure can block indefinately. no timeout restriction is imposed and the procedure can block indefinately.
If the timeout has expired, the `General_Error.Timeout` is returned as If the timeout has expired, the `General_Error.Timeout` is returned as
the error. the error.
If an error is returned for any other reason, other than timeout, the If an error is returned for any other reason, other than timeout, the
process state is considered undetermined. process state is considered undetermined.
*/ */
@(require_results) @(require_results)
process_wait :: proc(process: Process, timeout := TIMEOUT_INFINITE) -> (Process_State, Error) { process_wait :: proc(process: Process, timeout := TIMEOUT_INFINITE) -> (Process_State, Error) {
@@ -389,12 +476,12 @@ process_wait :: proc(process: Process, timeout := TIMEOUT_INFINITE) -> (Process_
} }
/* /*
Close the handle to a process. Close the handle to a process.
This procedure closes the handle associated with a process. It **does not** This procedure closes the handle associated with a process. It **does not**
terminate a process, in case it was running. In case a termination is terminate a process, in case it was running. In case a termination is
desired, kill the process first, wait for the process to finish, desired, kill the process first, wait for the process to finish,
then close the handle. then close the handle.
*/ */
@(require_results) @(require_results)
process_close :: proc(process: Process) -> (Error) { process_close :: proc(process: Process) -> (Error) {
@@ -402,10 +489,9 @@ process_close :: proc(process: Process) -> (Error) {
} }
/* /*
Terminate a process. Terminate a process.
This procedure terminates a process, specified by it's handle, `process`.
This procedure terminates a process, specified by it's handle, `process`.
*/ */
@(require_results) @(require_results)
process_kill :: proc(process: Process) -> (Error) { process_kill :: proc(process: Process) -> (Error) {
+8
View File
@@ -381,6 +381,14 @@ foreign kernel32 {
nDefaultTimeOut: DWORD, nDefaultTimeOut: DWORD,
lpSecurityAttributes: LPSECURITY_ATTRIBUTES, lpSecurityAttributes: LPSECURITY_ATTRIBUTES,
) -> HANDLE --- ) -> HANDLE ---
PeekNamedPipe :: proc(
hNamedPipe: HANDLE,
lpBuffer: rawptr,
nBufferSize: u32,
lpBytesRead: ^u32,
lpTotalBytesAvail: ^u32,
lpBytesLeftThisMessage: ^u32,
) -> BOOL ---
CancelIo :: proc(handle: HANDLE) -> BOOL --- CancelIo :: proc(handle: HANDLE) -> BOOL ---
GetOverlappedResult :: proc( GetOverlappedResult :: proc(
hFile: HANDLE, hFile: HANDLE,