class Process
# A struct representing the CPU current times of the process,
# in fractions of seconds.
#
# * *utime*: CPU time a process spent in userland.
# * *stime*: CPU time a process spent in the kernel.
# * *cutime*: CPU time a processes terminated children (and their terminated children) spent in the userland.
# * *cstime*: CPU time a processes terminated children (and their terminated children) spent in the kernel.
record Tms, utime : Float64, stime : Float64, cutime : Float64, cstime : Float64
end
require "crystal/system/process"
class Process
# Terminate the current process immediately. All open files, pipes and sockets
# are flushed and closed, all child processes are inherited by PID 1. This does
# not run any handlers registered with `at_exit`, use `::exit` for that.
#
# *status* is the exit status of the current process.
def self.exit(status : Int32 | Process::Status = 0) : NoReturn
Crystal::System::Process.exit(status)
end
# Returns the process identifier of the current process.
def self.pid : Int64
Crystal::System::Process.pid.to_i64
end
# Returns the process group identifier of the current process.
def self.pgid : Int64
Crystal::System::Process.pgid.to_i64
end
# Returns the process group identifier of the process identified by *pid*.
def self.pgid(pid : Int) : Int64
Crystal::System::Process.pgid(pid).to_i64
end
# Returns the process identifier of the parent process of the current process.
#
# On Windows, the parent is associated only at process creation time, and the
# system does not re-parent the current process if the parent terminates; thus
# `Process.exists?(Process.ppid)` is not guaranteed to be true.
def self.ppid : Int64
Crystal::System::Process.ppid.to_i64
end
# Sends *signal* to the process identified by *pid*.
def self.signal(signal : Signal, pid : Int) : Nil
Crystal::System::Process.signal(pid, signal.value)
end
# Installs *handler* as the new handler for interrupt requests. Removes any
# previously set interrupt handler.
#
# The handler is executed on a fresh fiber every time an interrupt occurs.
#
# * On Unix-like systems, this traps `SIGINT`.
# * On Windows, this captures Ctrl + C and
# Ctrl + Break signals sent to a console application.
@[Deprecated("Use `#on_terminate` instead")]
def self.on_interrupt(&handler : ->) : Nil
Crystal::System::Process.on_interrupt(&handler)
end
# Installs *handler* as the new handler for termination requests. Removes any
# previously set termination handler.
#
# The handler is executed on a fresh fiber every time an interrupt occurs.
#
# * On Unix-like systems, this traps `SIGINT`, `SIGHUP` and `SIGTERM`.
# * On Windows, this captures Ctrl + C,
# Ctrl + Break, terminal close, windows logoff
# and shutdown signals sent to a console application.
#
# ```
# wait_channel = Channel(Nil).new
#
# Process.on_terminate do |reason|
# case reason
# when .interrupted?
# puts "terminating gracefully"
# wait_channel.close
# when .terminal_disconnected?
# puts "reloading configuration"
# when .session_ended?
# puts "terminating forcefully"
# Process.exit
# end
# end
#
# wait_channel.receive?
# puts "bye"
# ```
def self.on_terminate(&handler : ::Process::ExitReason ->) : Nil
Crystal::System::Process.on_terminate(&handler)
end
# Ignores all interrupt requests. Removes any custom interrupt handler set
# with `#on_terminate`.
#
# * On Windows, interrupts generated by Ctrl + Break
# cannot be ignored in this way.
def self.ignore_interrupts! : Nil
Crystal::System::Process.ignore_interrupts!
end
# Restores default handling of interrupt requests.
def self.restore_interrupts! : Nil
Crystal::System::Process.restore_interrupts!
end
# Returns whether a debugger is attached to the current process.
#
# Currently supported on Windows and Linux. Always returns `false` on other
# systems.
@[Experimental]
def self.debugger_present? : Bool
Crystal::System::Process.debugger_present?
end
# Returns `true` if the process identified by *pid* is valid for
# a currently registered process, `false` otherwise. Note that this
# returns `true` for a process in the zombie or similar state.
def self.exists?(pid : Int) : Bool
Crystal::System::Process.exists?(pid)
end
# Returns a `Tms` for the current process. For the children times, only those
# of terminated children are returned on Unix; they are zero on Windows.
def self.times : Tms
Crystal::System::Process.times
end
# :nodoc:
#
# Runs the given block inside a new process and
# returns a `Process` representing the new child process.
#
# Available only on Unix-like operating systems.
@[Deprecated("Fork is no longer supported.")]
def self.fork(&) : Process
new Crystal::System::Process.fork { yield }
end
# :nodoc:
#
# Duplicates the current process.
# Returns a `Process` representing the new child process in the current process
# and `nil` inside the new child process.
#
# Available only on Unix-like operating systems.
@[Deprecated("Fork is no longer supported.")]
def self.fork : Process?
{% raise("Process fork is unsupported with multithread mode") if flag?(:preview_mt) %}
if pid = Crystal::System::Process.fork
new pid
end
end
# How to redirect the standard input, output and error IO of a process.
enum Redirect
# Pipe the IO so the parent process can read (or write) to the process IO
# through `#input`, `#output` or `#error`.
Pipe
# Discards the IO.
Close
# Use the IO of the parent process.
Inherit
end
# The standard `IO` configuration of a process.
alias Stdio = Redirect | IO
alias ExecStdio = Redirect | IO::FileDescriptor
alias Env = Nil | Hash(String, Nil) | Hash(String, String?) | Hash(String, String)
# Executes a child process and waits for it to complete, returning its status.
#
# See `Process.new` for the meaning of the parameters.
#
# Returns a `Process::Status` representing the child process' exit status.
#
# Raises `IO::Error` if the execution itself fails (for example because the
# executable does not exist or is not executable).
#
# Example:
#
# ```
# io = IO::Memory.new
# status = Process.run(%w[echo hello], output: io)
# io.to_s # => "hello\n"
# status # => Process::Status[0]
# ```
@[Experimental]
def self.run(args : Enumerable(String), *, env : Env = nil, clear_env : Bool = false,
input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil) : Process::Status
new(args, env: env, clear_env: clear_env, input: input, output: output, error: error, chdir: chdir).wait
end
# Executes a child process and waits for it to complete, returning its status.
#
# See `Process.new` for the meaning of the parameters.
#
# Returns a `Process::Status` representing the child process' exit status.
# The global `$?` variable is set to the returned status.
#
# Raises `IO::Error` if the execution itself fails (for example because the
# executable does not exist or is not executable).
#
# Example:
#
# ```
# status = Process.run("echo", ["hello"], output: Process::Redirect::Inherit)
# # outputs "hello\n"
# $? # => Process::Status[0]
# status # => Process::Status[0]
# ```
def self.run(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil) : Process::Status
status = new(command, args, env, clear_env, shell, input, output, error, chdir).wait
$? = status
status
end
# Executes a child process and waits for it to complete, returning its status.
#
# See `Process.new` for the meaning of the parameters.
#
# Returns a `Process::Status` representing the child process' exit status.
# The global `$?` variable is set to the returned status.
#
# Returns `nil` if the execution itself fails (for example because the
# executable does not exist or is not executable).
#
# Example:
#
# ```
# Process.run?(["true"]) # => Process::Status[0]
# Process.run?(["nonexistent"]) # => nil
# ```
@[Experimental]
def self.run?(args : Enumerable(String), *,
env : Env = nil, clear_env : Bool = false,
input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close,
chdir : Path | String? = nil) : Process::Status?
status = new(args, env: env, clear_env: clear_env, input: input, output: output, error: error, chdir: chdir) { return nil }.wait
status
end
# Executes a child process, yields the block, and then waits for it to finish.
#
# See `Process.new` for the meaning of the parameters.
#
# By default the process is configured to use pipes for input, output and error.
# These will be closed automatically at the end of the block.
#
# Returns a tuple with the process' exit status and the block's output value.
#
# Raises `IO::Error` if the execution itself fails (for example because the
# executable does not exist or is not executable).
#
# Example:
#
# ```
# status, result = Process.run(%w[echo hello]) do |process|
# process.output.gets_to_end
# end
# status # => Process::Status[0]
# result # => "hello\n"
# ```
@[Experimental]
def self.run(args : Enumerable(String), *, env : Env = nil, clear_env : Bool = false,
input : Stdio = Redirect::Pipe, output : Stdio = Redirect::Pipe, error : Stdio = Redirect::Pipe, chdir : Path | String? = nil, & : Process -> _)
process = new(args, env: env, clear_env: clear_env, input: input, output: output, error: error, chdir: chdir)
begin
value = yield process
process.close
status = process.wait
{status, value}
rescue ex
process.terminate
raise ex
end
end
# Executes a child process, yields the block, and then waits for it to finish.
#
# See `Process.new` for the meaning of the parameters.
#
# By default the process is configured to use pipes for input, output and error.
# These will be closed automatically at the end of the block.
#
# Returns the block's value.
#
# Raises `IO::Error` if the execution itself fails (for example because the
# executable does not exist or is not executable).
#
# Example:
#
# ```
# output = Process.run("echo", ["hello"]) do |process|
# process.output.gets_to_end
# end
# $? # => Process::Status[0]
# output # => "hello\n"
# ```
def self.run(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
input : Stdio = Redirect::Pipe, output : Stdio = Redirect::Pipe, error : Stdio = Redirect::Pipe, chdir : Path | String? = nil, &)
process = new(command, args, env, clear_env, shell, input, output, error, chdir)
begin
value = yield process
process.close
$? = process.wait
value
rescue ex
process.terminate
raise ex
end
end
# Replaces the current process with a new one. This function never returns.
#
# Raises `IO::Error` if executing the command fails (for example if the executable doesn't exist).
def self.exec(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
input : ExecStdio = Redirect::Inherit, output : ExecStdio = Redirect::Inherit, error : ExecStdio = Redirect::Inherit, chdir : Path | String? = nil) : NoReturn
input = exec_stdio_to_fd(input, for: STDIN)
output = exec_stdio_to_fd(output, for: STDOUT)
error = exec_stdio_to_fd(error, for: STDERR)
Crystal::System::Process.replace(command, args, shell, env, clear_env, input, output, error, chdir)
end
private def self.exec_stdio_to_fd(stdio : ExecStdio, for dst_io : IO::FileDescriptor) : IO::FileDescriptor
case stdio
when IO::FileDescriptor
stdio
when Redirect::Pipe
raise "Cannot use Process::Redirect::Pipe for Process.exec"
when Redirect::Inherit
dst_io
when Redirect::Close
if dst_io == STDIN
File.open(File::NULL, "r")
else
File.open(File::NULL, "w")
end
else
raise "BUG: Impossible type in ExecStdio #{stdio.class}"
end
end
# Returns the process identifier of this process.
def pid : Int64
@process_info.pid.to_i64
end
# A pipe to this process' input. Raises if a pipe wasn't asked when creating the process.
getter! input : IO::FileDescriptor
# A pipe to this process' output. Raises if a pipe wasn't asked when creating the process.
getter! output : IO::FileDescriptor
# A pipe to this process' error. Raises if a pipe wasn't asked when creating the process.
getter! error : IO::FileDescriptor
@process_info : Crystal::System::Process
@wait_count = 0
# Creates and executes a child process.
#
# This starts a new process for the command given in *args[0]*.
#
# The command is either a path to the executable to run, or the name of an
# executable which is then looked up by the operating system.
# The lookup uses the `PATH` variable of the current process environment
# (i.e. `ENV["PATH"]).
# In order to resolve to a specific executable, provide a path instead of
# only a command name. `Process.find_executable` can help with looking up a
# command in a custom `PATH`.
#
# The following arguments in *args* are passed as arguments to the child process.
#
# Raises `IO::Error` if executing *args[0]* fails, for example because the
# executable doesn't exist or is not executable.
#
# *env* provides a mapping of environment variables for the child process.
# If *clear_env* is `true`, only these explicit variables are used; if `false`,
# the child inherits the parent's environment with *env* merged.
#
# *input*, *output*, *error* configure the child process's standard streams.
# * `Redirect::Close` passes the null device
# * `Redirect::Pipe` creates a pipe that's accessible via `#input`, `#output`
# or `#error`.
# * `Redirect::Inherit` to share the parent's streams (`STDIN`, `STDOUT`, `STDERR`).
# * An `IO` instance creates a pipe that reads/writes into the given IO.
#
# *chdir* changes the working directory of the child process. If `nil`, uses
# the current working directory of the parent process.
#
# Example:
#
# ```
# process = Process.new(["echo", "Hello"], output: Process::Redirect::Pipe)
# process.output.gets_to_end # => "Hello\n"
# process.wait # => Process::Status[0]
# ```
#
# Similar methods:
#
# * `Process.run` is a convenient short cut if you just want to run a command
# and wait for it to finish.
# * `Process.exec` replaces the current process.
@[Experimental]
def self.new(args : Enumerable(String), *, env : Env = nil, clear_env : Bool = false,
input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil)
new(args, env: env, clear_env: clear_env, input: input, output: output, error: error, chdir: chdir) do |error, command|
raise ::File::Error.from_os_error("Error executing process", error, file: command)
end
end
# :nodoc:
protected def initialize(args : Enumerable(String), *, env : Env = nil, clear_env : Bool = false,
input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil, &)
raise File::NotFoundError.new("Error executing process: No command", file: "") if args.empty?
fork_input = stdio_to_fd(input, for: STDIN)
fork_output = stdio_to_fd(output, for: STDOUT)
fork_error = stdio_to_fd(error, for: STDERR)
prepared_args = Crystal::System::Process.prepare_args(args)
pid = Crystal::System::Process.spawn(prepared_args, false, env, clear_env, fork_input, fork_output, fork_error, chdir.try &.to_s) do |error, command|
yield error, command
end
@process_info = Crystal::System::Process.new(pid)
fork_input.close unless fork_input.in?(input, STDIN)
fork_output.close unless fork_output.in?(output, STDOUT)
fork_error.close unless fork_error.in?(error, STDERR)
end
# Creates and executes a child process.
#
# This starts a new process for `command`.
#
# ## `shell: false` (the default)
#
# *command* is either a path to the executable to run, or the name of an
# executable which is then looked up by the operating system.
# The lookup uses the `PATH` variable of the current process environment
# (i.e. `ENV["PATH"]).
# In order to resolve to a specific executable, provide a path instead of
# only a command name. `Process.find_executable` can help with looking up a
# command in a custom `PATH`.
#
# The arguments in *args* are passed as arguments to the child process.
#
# Raises `IO::Error` if executing *command* fails, for example because the
# executable doesn't exist or is not executable.
#
# ## `shell: true`
#
# *command* is a shell script executed in the system shell (`/bin/sh` on Unix
# systems, `cmd.exe` on Windows).
# Command names are looked up by the shell itself, using the `PATH` variable
# of the shell process (i.e. `env["PATH"]`).
#
# *args* is unsupported on Windows.
# On Unix it's passed as additional arguments to the shell process and can be
# used in the shell script with `"${@}"` to safely insert them there. If the
# script is a single command (no whitespace), `"${@}"` is appended implicitly.
#
# The returned instance represents the shell process, not the process executed
# for *command*.
#
# If executing *command* fails, for example because the executable doesn't
# exist or is not executable, it may raise `IO::Error` (on Windows) or return
# an unsuccessful exit status (on Unix).
#
# ## Shared parameters
#
# *env* provides a mapping of environment variables for the child process.
# If *clear_env* is `true`, only these explicit variables are used; if `false`,
# the child inherits the parent's environment with *env* merged.
#
# *input*, *output*, *error* configure the child process's standard streams.
# * `Redirect::Close` passes the null device
# * `Redirect::Pipe` creates a pipe that's accessible via `#input`, `#output`
# or `#error`.
# * `Redirect::Inherit` to share the parent's streams (`STDIN`, `STDOUT`, `STDERR`).
# * An `IO` instance creates a pipe that reads/writes into the given IO.
#
# *chdir* changes the working directory of the child process. If `nil`, uses
# the current working directory of the parent process.
#
# Example:
#
# ```
# process = Process.new("echo", ["Hello"], output: Process::Redirect::Pipe)
# process.output.gets_to_end # => "Hello\n"
# process.wait # => Process::Status[0]
# ```
#
# Similar methods:
#
# * `Process.run` is a convenient short cut if you just want to run a command
# and wait for it to finish.
# * `Process.exec` replaces the current process.
def initialize(command : String, args : Enumerable(String)? = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false,
input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String? = nil)
fork_input = stdio_to_fd(input, for: STDIN)
fork_output = stdio_to_fd(output, for: STDOUT)
fork_error = stdio_to_fd(error, for: STDERR)
prepared_args = Crystal::System::Process.prepare_args(command, args, shell)
pid = Crystal::System::Process.spawn(prepared_args, shell, env, clear_env, fork_input, fork_output, fork_error, chdir.try &.to_s) do |error, command|
raise ::File::Error.from_os_error("Error executing process", error, file: command)
end
@process_info = Crystal::System::Process.new(pid)
fork_input.close unless fork_input.in?(input, STDIN)
fork_output.close unless fork_output.in?(output, STDOUT)
fork_error.close unless fork_error.in?(error, STDERR)
end
def finalize : Nil
@process_info.release
end
private def stdio_to_fd(stdio : Stdio, for dst_io : IO::FileDescriptor) : IO::FileDescriptor
case stdio
in IO::FileDescriptor
# on Windows, only async pipes can be passed to child processes, async
# regular files will report an error and those require a separate pipe
# (https://github.com/crystal-lang/crystal/pull/13362#issuecomment-1519082712)
{% if flag?(:win32) %}
unless stdio.system_blocking? || stdio.info.type.pipe?
return io_to_fd(stdio, for: dst_io)
end
{% end %}
stdio
in IO
io_to_fd(stdio, for: dst_io)
in Redirect::Pipe
case dst_io
when STDIN
fork_io, @input = IO.pipe(read_blocking: true)
when STDOUT
@output, fork_io = IO.pipe(write_blocking: true)
when STDERR
@error, fork_io = IO.pipe(write_blocking: true)
else
raise "BUG: Unknown destination io #{dst_io}"
end
fork_io
in Redirect::Inherit
dst_io
in Redirect::Close
if dst_io == STDIN
File.open(File::NULL, "r")
else
File.open(File::NULL, "w")
end
end
end
private def io_to_fd(stdio : Stdio, for dst_io : IO::FileDescriptor) : IO::FileDescriptor
if stdio.closed?
if dst_io == STDIN
return File.open(File::NULL, "r").tap(&.close)
else
return File.open(File::NULL, "w").tap(&.close)
end
end
if dst_io == STDIN
fork_io, process_io = IO.pipe(read_blocking: true)
@wait_count += 1
ensure_channel
spawn { copy_io(stdio, process_io, channel, close_dst: true) }
else
process_io, fork_io = IO.pipe(write_blocking: true)
@wait_count += 1
ensure_channel
spawn { copy_io(process_io, stdio, channel, close_src: true) }
end
fork_io
end
{% unless flag?(:interpreted) %}
# :nodoc:
def initialize(pid : LibC::PidT)
@process_info = Crystal::System::Process.new(pid)
end
{% end %}
# Sends *signal* to this process.
#
# NOTE: `#terminate` is preferred over `signal(Signal::TERM)` and
# `signal(Signal::KILL)` as a portable alternative which also works on
# Windows.
def signal(signal : Signal) : Nil
Crystal::System::Process.signal(@process_info.pid, signal)
end
# Waits for this process to complete and closes any pipes.
def wait : Process::Status
@wait_count.times do
ex = channel.receive
raise ex if ex
end
@wait_count = 0
Process::Status.new(@process_info.wait)
ensure
close
@process_info.release
end
# Whether the process is still registered in the system.
# Note that this returns `true` for processes in the zombie or similar state.
def exists? : Bool
@process_info.exists?
end
# Whether this process is already terminated.
def terminated? : Bool
!exists?
end
# Closes any system resources (e.g. pipes) held for the child process.
def close : Nil
close_io @input
close_io @output
close_io @error
end
# Asks this process to terminate.
#
# If *graceful* is true, prefers graceful termination over abrupt termination
# if supported by the system.
#
# * On Unix-like systems, this causes `Signal::TERM` to be sent to the process
# instead of `Signal::KILL`.
# * On Windows, this parameter has no effect and graceful termination is
# unavailable. The terminated process has an exit status of 1.
def terminate(*, graceful : Bool = true) : Nil
@process_info.terminate(graceful: graceful)
end
private def channel
if channel = @channel
channel
else
raise "BUG: Notification channel was not initialized for this process"
end
end
private def ensure_channel
@channel ||= Channel(Exception?).new
end
private def copy_io(src, dst, channel, close_src = false, close_dst = false)
return unless src.is_a?(IO) && dst.is_a?(IO)
begin
IO.copy(src, dst)
# close is called here to trigger exceptions
# close must be called before channel.send or the process may deadlock
src.close if close_src
close_src = false
dst.close if close_dst
close_dst = false
channel.send nil
rescue ex
channel.send ex
ensure
# any exceptions are silently ignored because of spawn
src.close if close_src
dst.close if close_dst
end
end
private def close_io(io)
io.close if io
end
# Changes the root directory and the current working directory for the current
# process.
#
# Available only on Unix-like operating systems.
#
# Security: `chroot` on its own is not an effective means of mitigation. At minimum
# the process needs to also drop privileges as soon as feasible after the `chroot`.
# Changes to the directory hierarchy or file descriptors passed via `recvmsg(2)` from
# outside the `chroot` jail may allow a restricted process to escape, even if it is
# unprivileged.
#
# ```
# Process.chroot("/var/empty")
# ```
def self.chroot(path : String) : Nil
Crystal::System::Process.chroot(path)
end
end
# Executes the given command in a subshell.
# Standard input, output and error are inherited.
# Returns `true` if the command gives zero exit code, `false` otherwise.
# The special `$?` variable is set to a `Process::Status` associated with this execution.
#
# If *command* contains no spaces and *args* is given, it will become
# its argument list.
#
# If *command* contains spaces and *args* is given, *command* must include
# `"${@}"` (including the quotes) to receive the argument list.
#
# No shell interpretation is done in *args*.
#
# Example:
#
# ```
# system("echo *")
# ```
#
# Produces:
#
# ```text
# LICENSE shard.yml Readme.md spec src
# ```
def system(command : String, args : Enumerable(String)? = nil) : Bool
status = Process.run(command, args, shell: true, input: Process::Redirect::Inherit, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit)
$? = status
status.success?
end
# Returns the standard output of executing *command* in a subshell.
# Standard input, and error are inherited.
# The special `$?` variable is set to a `Process::Status` associated with this execution.
#
# It is impossible to call this method with any regular call syntax. There is an associated literal type which calls the method with the literal content as command:
#
# ```
# `echo hi` # => "hi\n"
# $?.success? # => true
# ```
#
# See [`Command` literals](https://crystal-lang.org/reference/syntax_and_semantics/literals/command.html) in the language reference.
def `(command : String) : String
process = Process.new(command, shell: true, input: Process::Redirect::Inherit, output: Process::Redirect::Pipe, error: Process::Redirect::Inherit)
output = process.output.gets_to_end
status = process.wait
$? = status
output
end
require "./process/*"