Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal
JavaScript seems to be disabled. Functionality is limited.

process/spawn.fz


# This file is part of the Fuzion language implementation.
#
# The Fuzion language implementation is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, version 3 of the License.
#
# The Fuzion language implementation is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License along with The
# Fuzion language implementation.  If not, see <https://www.gnu.org/licenses/>.


# -----------------------------------------------------------------------
#
#  Tokiwa Software GmbH, Germany
#
#  Source code of Fuzion standard library feature process.spawn
#
# -----------------------------------------------------------------------

private:public Process(id i64, std_in option i64, std_out, std_err i64) ref is

  # NYI use this to register open resource
  uid := unique_id


  # how many bytes are read at a time
  # POSIX.1 requires PIPE_BUF to be at least 512 bytes
  # NYI move this to effect?
  pipe_buffer_size := 4096


  # read bytes from standard out
  #
  read_bytes choice (array u8) io.end_of_file error
  =>
    arr := array u8 pipe_buffer_size i->0
    res := fuzion.sys.pipe.read std_out arr.internal_array.data arr.count
    if res = -1
      error "error reading from stdout."
    else if res = 0
      io.end_of_file
    else
      arr
        .slice 0 res
        .as_array


  # write bytes to stdin of child process
  #
  public write_bytes (bytes Sequence u8) outcome i32
  =>
    match std_in
      nil =>
        error "This process does not have a standard in."
      n i64 =>
        write_bytes n bytes


  # helper function
  #
  write_bytes (desc i64, bytes Sequence u8) outcome i32
  pre bytes.count > 0
  =>
    bytes
      .chunk pipe_buffer_size
      .reduce_or_error 0 ((r, t) ->
        arr := t.as_array
        bw := fuzion.sys.pipe.write desc arr.internal_array.data arr.count
        if bw = -1
          abort (outcome i32) (error "error while writing. wrote $r bytes already.")
        else
          r+bw
      )


  # read up to n bytes of stdout to string
  #
  public read_string(n i32) outcome String =>

    rp :=
      ref : io.Read_Provider
        read(count i32) choice (array u8) io.end_of_file error =>
          read_bytes

    (io.buffered.reader rp pipe_buffer_size [])
      .with (()->io.buffered.read_string n)


  # read string, up to 1MB from stdout
  #
  public read_string outcome String =>
    read_string 1E9


  # write string to stdin of child process
  #
  public write_string (s String) outcome i32 =>
    write_bytes s.utf8


  # wait for process to finish
  #
  public wait outcome u32 =>
    # NYI make sure std_in can not be used anymore
    if std_in!! || fuzion.sys.pipe.close std_in.get = 0
      r := fuzion.sys.process.wait id
      if r<0
        # NYI can we be more specific here?
        error "Waiting for process with id $id was unsuccessful."
      else
        # NYI make sure process is not used anymore
        r.as_u32
    else
      # NYI can we be more specific here?
      error "Standard-in of process could not be closed successfully."


  # dispose process, cleanup
  #
  module dispose outcome unit =>
    # NYI make sure std_out, std_err can not be used anymore
    if fuzion.sys.pipe.close std_out = -1
      error "standard out could not be closed successfully."
    else if (fuzion.sys.pipe.close std_err = -1)
      error "standard error could not be closed successfully."
    # NYI cleanup process handle
    # fuzion.sys.process.close id
    else
      unit


  # pipe this processes output to new process
  #
  public infix | (process_and_args array String) outcome Process ! env_vars
  pre
    (process_and_args ∀ str -> str.is_ascii)
    !process_and_args[0].contains_whitespace
  =>
    (spawn0 process_and_args).bind Process tup->
      (pid, std_in, std_out, std_err) := tup

      # NYI make sure std_out, std_err are not used anymore
      p := Process pid nil std_out std_err

      # NYI should wire pipe directly to process
      # NYI this works only when pipes buffer is large enough for
      # what is being written/read to/from pipes
      # thread to pipe from one process to other
      concur.thread.spawn ()->
        for p1rb := read_bytes
        while
          match p1rb
            s array u8 => (write_bytes std_in s).ok
            * => false

        # NYI error handling
        fuzion.sys.pipe.close std_in
        fuzion.sys.pipe.close Process.this.std_out
        _ := fuzion.sys.pipe.close Process.this.std_err

      p


# spawn a new process
#
# NOTE: environment variables that should be passed to the process
# will be taken from effect process.env_vars
#
public spawn(process_and_args array String) outcome Process ! env_vars
pre
  (process_and_args ∀ str -> str.is_ascii)
  !process_and_args[0].contains_whitespace
=>
  (spawn0 process_and_args).bind Process (tup)->
    (id, std_in, std_out, std_err) := tup
    Process id std_in std_out std_err


# spawn process with option to pass environment variables
#
# helper feature for spawn and `infix |`
#
spawn0(process_and_args array String) outcome (tuple i64 i64 i64 i64)
pre process_and_args.length > 0
# NYI allow utf-8?
    process_and_args ∀ (x -> x.as_codepoints ∀ (y -> y.is_ascii))
=>

  sys => fuzion.sys
  NULL := [u8 0].internal_array.data

  # posix_spawn needs last arg to be NULL
  arg_data := array fuzion.sys.Pointer process_and_args.count+1 (i -> if i<process_and_args.length then sys.c_string process_and_args[i] else NULL)

  env_var_strings := env_vars
    .items
    .map (x ->
      (k, v) := x
      "$k=$v")
    .as_array

  # posix_spawn needs last arg to be NULL
  env_data := array fuzion.sys.Pointer env_var_strings.count+1 (i -> if i<env_var_strings.count then sys.c_string env_var_strings[i] else NULL)

  # args as string for windows to avoid malloc in backend
  args_str := sys.c_string (String.type.join process_and_args " ")

  # environment variables for windows to avoid malloc in backend
  # NULL terminates each environment variable
  # NULL is also used to terminate the environment variables data structure.
  env_str := sys.c_string ((String.type.join env_var_strings (codepoint 0)) + (codepoint 0))

  res_data := array i64 4 i->0
  if (sys.process.create arg_data.internal_array.data arg_data.count env_data.internal_array.data env_data.count res_data.internal_array.data args_str env_str) = -1
    error "*** error creating process ***"
  else
    (res_data[0], res_data[1], res_data[2], res_data[3])