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
}
+88 -2
View File
@@ -1,6 +1,7 @@
package os2 package os2
import "base:runtime" import "base:runtime"
import "core:strings"
import "core:time" import "core:time"
/* /*
@@ -343,10 +344,96 @@ Process_Desc :: struct {
handle inheritance properties, make sure to serialize all those calls. 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.
*/ */
@@ -405,7 +492,6 @@ 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,