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/*"