// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// A temporary pipe used to read output from a spawned process
struct ReadFromProcess {
io : @event_loop.IoHandle
read_buf : @io.ReaderBuffer
}
///|
/// A temporary pipe used to write data to a spawned process
struct WriteToProcess(@event_loop.IoHandle)
///|
/// Create a temporary pipe for reading from stdout/stderr of a process.
/// The return value is a pair `(r, w)`,
/// where `r` is a temporary pipe that can be used to read process output,
/// and `w` should be passed to `@process.run`, `@process.spawn` etc.
///
/// `w` is temporary: it can only be passed to one `@process.run` call.
/// However, it is safe to pass `w` to both `stdout` and `stderr` of the same process.
///
/// If `shared` is `false` (the default), the created pipe is unique.
/// In this case, `w` can only be passed to one child process
/// (but it is ok to pass `w` to both stdout and stderr of the same process),
/// and `w` will be closed automatically after the child process started.
///
/// If `shared` is `true`, the created pipe is shared:
/// `w` can be passed to multiple children process to merge and collect their output.
/// In this case, `w` should be closed manually via `w.close()`
/// after being passed to the last child process.
/// Note that `w` can be closed immediately after the last child process *started*,
/// there is no need to wait for the termination of the child process.
/// If you forgot to close `w` or close it too late,
/// the read end `r` will fail to observe EOF from children process.
pub fn read_from_process(
shared~ : Bool = false,
) -> (ReadFromProcess, &ProcessOutput) raise {
let context = "@process.read_from_process()"
let (r, w) = @fd_util.pipe(
read_end_is_async=true,
write_end_is_async=false,
context~,
)
let r = @event_loop.IoHandle::from_fd(r, kind=Pipe)
let w = @event_loop.IoHandle::from_fd(w, kind=Pipe, is_async=false)
(
{ io: r, read_buf: @io.ReaderBuffer::new(), },
TempPipeWrite::{ pipe: w, shared, closed: false, },
)
}
///|
/// Create a temporary pipe for writing to stdin of a process.
/// The return value is a pair `(r, w)`,
/// where `w` is a temporary pipe that can be used to write to process output,
/// and `r` should be passed to `@process.run`.
pub fn write_to_process() -> (&ProcessInput, WriteToProcess) raise {
let context = "@process.write_to_process()"
let (r, w) = @fd_util.pipe(
read_end_is_async=false,
write_end_is_async=true,
context~,
)
let r = @event_loop.IoHandle::from_fd(r, kind=Pipe, is_async=false)
let w = @event_loop.IoHandle::from_fd(w, kind=Pipe)
(TempPipeRead::{ pipe: r, closed: false, }, w)
}
///|
/// Create a temporary pipe for connecting the output of
/// one child process to the input of another child process.
pub fn pipe() -> (&ProcessInput, &ProcessOutput) raise {
let context = "@process.pipe()"
let (r, w) = @fd_util.pipe(
read_end_is_async=false,
write_end_is_async=false,
context~,
)
let r = @event_loop.IoHandle::from_fd(r, kind=Pipe, is_async=false)
let w = @event_loop.IoHandle::from_fd(w, kind=Pipe, is_async=false)
(
TempPipeRead::{ pipe: r, closed: false, },
TempPipeWrite::{ pipe: w, shared: false, closed: false, },
)
}
///|
pub fn ReadFromProcess::close(self : ReadFromProcess) -> Unit {
self.io.close()
}
///|
pub impl @io.Reader for ReadFromProcess with fn _get_internal_buffer(self) {
self.read_buf
}
///|
pub impl @io.Reader for ReadFromProcess with fn _direct_read(
self,
buf,
offset~,
max_len~,
) {
self.io.read(
buf,
offset~,
len=max_len,
context="@process.ReadFromProcess::read()",
)
}
///|
pub extend ReadFromProcess with @io.Reader::{
read,
drop,
read_exactly,
read_some,
read_all,
read_until,
}
///|
pub fn WriteToProcess::close(self : WriteToProcess) -> Unit {
let WriteToProcess(io) = self
io.close()
}
///|
pub impl @io.Writer for WriteToProcess with fn write_once(
self,
buf,
offset~,
len~,
) {
let WriteToProcess(io) = self
io.write(buf, offset~, len~, context="@process.WriteToProcess::write()")
}
///|
pub extend WriteToProcess with @io.Writer::{write_once, write, write_reader}
///|
fn @fs.CreateMode::to_int(self : @fs.CreateMode) -> Int32 = "%identity"
///|
/// Redirect the output of a process to the file at `path`.
/// An output channel that should be passed to child process will be returned.
///
/// The meaning of `append`, `create_mode` and `permission` is the same as `@fs.open`,
/// see the document of `@fs.open` for more details.
///
/// If `shared` is `false` (the default), the returned channel `w` is unique.
/// In this case, `w` can only be passed to one child process
/// (but it is ok to pass `w` to both stdout and stderr of the same process),
/// and `w` will be closed automatically after the child process started.
///
/// If `shared` is `true`, the returned channel `w` is shared:
/// it can be passed to multiple children process to merge and collect their output.
/// In this case, `w` should be closed manually via `w.close()`
/// after being passed to the last child process.
/// Note that `w` can be closed immediately after the last child process *started*,
/// there is no need to wait for the termination of the child process.
pub async fn redirect_to_file(
path : String,
append~ : Bool = false,
create_mode~ : @fs.CreateMode = CreateOrTruncate,
permission~ : Int32 = 0o644,
shared~ : Bool = false,
) -> &ProcessOutput {
let (file, _) = @event_loop.open(
path,
1, // write only
create=create_mode.to_int(),
append~,
sync=0,
mode=permission,
context="@process.redirect_to_file()",
)
RedirectToFile::{ io: file, shared, closed: false, }
}
///|
/// Redirect the content of a file at `path` to the stdin of a process.
pub async fn redirect_from_file(path : String) -> &ProcessInput {
let (file, _) = @event_loop.open(
path,
0, // read only
create=0, // `OpenExisting`
append=false,
sync=0,
mode=0,
context="@process.redirect_from_file()",
)
RedirectToFile::{ io: file, shared: false, closed: false, }
}
///|
/// An entity that can be used to redirect stdin of a process
trait ProcessInput {
fn fd(Self) -> @fd_util.Fd raise
/// Close the input channel.
/// May be called automatically via `after_spawn`, or manually via `.close()`
fn do_close(Self) -> Unit = _
fn after_spawn(Self) -> Unit = _
}
///|
/// Close an input channel that will not be passed to a child process.
/// A channel is closed automatically once it is passed to a child process,
/// so this is only needed when a channel has been created
/// but spawning the process that would consume it fails or is abandoned.
/// Closing is idempotent, and a no-op for channels that only borrow
/// their file descriptor, such as `@stdio.stdin`.
#doc(hidden)
#deprecated
pub fn &ProcessInput::close(self : &ProcessInput) -> Unit {
self.do_close()
}
///|
impl ProcessInput with fn do_close(_) {
()
}
///|
impl ProcessInput with fn after_spawn(_) {
()
}
///|
#deprecated("use `@process.write_to_process()` or `@process.pipe()` instead")
pub impl ProcessInput for @pipe.PipeRead
///|
pub impl ProcessInput for @pipe.PipeRead with fn fd(self) {
self.fd()
}
///|
pub impl ProcessInput for @stdio.Input with fn fd(self) {
self.fd()
}
///|
/// An entity that can be used to redirect stdout/stderr of a process
trait ProcessOutput {
fn fd(Self) -> @fd_util.Fd raise
/// Close the output channel.
/// May be called automatically via `after_spawn`, or manually via `.close()`
fn do_close(Self) -> Unit = _
fn after_spawn(Self) -> Unit = _
}
///|
/// Close a shared channel for redirecting process output.
/// Once closed, the channel can no longer be passed to child process.
/// Note that unique channels are automatically closed after being passed to child process,
/// so there is no need to manually call `.close()` on a unique channel.
/// See `@process.read_from_process` for more details.
pub fn &ProcessOutput::close(self : &ProcessOutput) -> Unit {
self.do_close()
}
///|
impl ProcessOutput with fn do_close(_) {
()
}
///|
impl ProcessOutput with fn after_spawn(_) {
()
}
///|
#deprecated("use `@process.read_from_process(shared~)` or `@process.pipe()` instead")
pub impl ProcessOutput for @pipe.PipeWrite
///|
pub impl ProcessOutput for @pipe.PipeWrite with fn fd(self) {
self.fd()
}
///|
pub impl ProcessOutput for @stdio.Output with fn fd(self) {
self.fd()
}
///|
priv struct TempPipeRead {
pipe : @event_loop.IoHandle
mut closed : Bool
}
///|
priv struct TempPipeWrite {
pipe : @event_loop.IoHandle
shared : Bool
mut closed : Bool
}
///|
impl ProcessOutput for TempPipeWrite with fn fd(self) {
self.pipe.fd()
}
///|
impl ProcessOutput for TempPipeWrite with fn do_close(self) {
if !self.closed {
self.closed = true
self.pipe.close()
}
}
///|
impl ProcessOutput for TempPipeWrite with fn after_spawn(self) {
if !self.shared {
ProcessOutput::do_close(self)
}
}
///|
impl ProcessInput for TempPipeRead with fn fd(self) {
self.pipe.fd()
}
///|
impl ProcessInput for TempPipeRead with fn do_close(self) {
if !self.closed {
self.closed = true
self.pipe.close()
}
}
///|
impl ProcessInput for TempPipeRead with fn after_spawn(self) {
ProcessInput::do_close(self)
}
///|
priv struct RedirectToFile {
io : @event_loop.IoHandle
shared : Bool
mut closed : Bool
}
///|
impl ProcessOutput for RedirectToFile with fn fd(self) {
self.io.fd()
}
///|
impl ProcessOutput for RedirectToFile with fn do_close(self) {
if !self.closed {
self.closed = true
self.io.close()
}
}
///|
impl ProcessOutput for RedirectToFile with fn after_spawn(self) {
if !self.shared {
ProcessOutput::do_close(self)
}
}
///|
impl ProcessInput for RedirectToFile with fn fd(self) {
self.io.fd()
}
///|
impl ProcessInput for RedirectToFile with fn do_close(self) {
if !self.closed {
self.closed = true
self.io.close()
}
}
///|
impl ProcessInput for RedirectToFile with fn after_spawn(self) {
ProcessInput::do_close(self)
}