Janet 1.38.0-73334f3 Documentation
(Other Versions: 1.37.1 1.36.0 1.35.0 1.34.0 1.31.0 1.29.1 1.28.0 1.27.0 1.26.0 1.25.1 1.24.0 1.23.0 1.22.0 1.21.0 1.20.0 1.19.0 1.18.1 1.17.1 1.16.1 1.15.0 1.13.1 1.12.2 1.11.1 1.10.1 1.9.1 1.8.1 1.7.0 1.6.0 1.5.1 1.5.0 1.4.0 1.3.1 )

Spawning a Process

Unlike os/execute, which returns an exit code (or errors) after the started subprocess completes, os/spawn returns a core/process value. This value represents a subprocess which may or may not have completed its execution. In contrast to os/execute, which "waits" for the subprocess, os/spawn does not and it is recommended to explicitly arrange for this "waiting" for resource management and other purposes ("waiting" will be covered in more detail below).

The arguments for os/spawn are the same as those of os/execute. Again, the required argument, args, is a tuple or array representing an invocation of a program with its arguments.

# prints: :salutations
# the returned value, proc, is a core/process value
(let [proc (os/spawn ["janet" "-e" "(pp :salutations)"] :p)]
  (type proc)) # => :core/process

core/process and os/proc-wait

Passing the core/process value to the os/proc-wait function causes Janet to "wait" for the subprocess to complete execution. This is sometimes referred to as "rejoining" the subprocess.

The main reason for "waiting" is so that the operating system can release resources. Not "waiting" appropriately can lead to resources remaining unreclaimed and this can become a problem for a running system because it may run out of usable resources.

Once "waiting" has completed, the exit code for the subprocess can be obtained via the :return-code key. Accessing this key before "waiting" will result in a nil value.

# prints: :relax
(def proc (os/spawn ["janet" "-e" "(pp :relax)"] :p))

(get proc :return-code) # => nil

(os/proc-wait proc) # => 0

(get proc :return-code) # => 0

The os/proc-wait function takes a single argument, proc, which should be a core/process value. The return value is the exit code of the subprocess. If os/proc-wait is called more than once for the same core/process value it will error.

# prints: :sit-up
(def proc (os/spawn ["janet" "-e" "(pp :sit-up)"] :p))

(os/proc-wait proc) # => 0

# prints: cannot wait twice on a process
(try
  (os/proc-wait proc)
  ([e]
    (eprint e))) # => nil

Note that the first call to os/proc-wait will cause the current fiber to pause execution until the subprocess completes.

core/process keys and the env argument

As seen above, once "waiting" for a subprocess has completed, the exit code of a subprocess becomes available via the :return-code key. Other information about a subprocess can also be accessed via the keys of an associated core/process value such as :in, :out, and :err. On UNIX-like platforms, there is an additional :pid key.

The :in, :out, and :err keys for a core/process value provide access to the standard input, output, and error of the associated subprocess provided that corresponding choices were made via the env dictionary (i.e. table or struct) argument to os/spawn. This means that, for example, if there is no key-value pair specified via :in in the env dictionary, then the standard input of the subprocess will not be programmatically accessible via the associated core/process's :in key.

The values associated with the :in, :out, and :err keys of the env argument can be core/file or core/stream values.

(def out-file (file/temp))

(type out-file) # => :core/file

(def proc
  (os/spawn ["janet" "-e" `(print "again") (flush)`]
            :p {:out out-file}))

# only standard output of the subprocess is accessible
(proc :in) # => nil
(type (proc :out)) # => :core/stream
(proc :err) # => nil

(os/proc-wait proc) # => 0

(file/seek out-file :set 0)

# prints: again
(print (file/read out-file :all))

(file/close out-file)

Note that in the example above, the standard input and error of the subprocess were not accessible -- attempts to access them resulted in nil values -- because corresponding keys for the env argument were not specified. In contrast, because a key-value pair for :out and out-file were included in the env struct, the standard output of the subprocess could be accessed programmatically.

:pipe and ev/gather

For each of the :in, :out, and :err keys of the env argument to os/spawn, the associated value can be the keyword :pipe. This causes core/stream values to be used that can be read from and written to for appropriate standard IO of the subprocess.

In order to avoid deadlocks (aka hanging), it's important for data to be transferred between processes in a timely manner. One example of how this can fail is if output redirected to pipes is not read from (sufficiently). In such a situation, pipe buffers might become full and this can prevent a process from completing its writing. That in turn can result in the writing process being unable to finish executing. To keep data flowing appropriately, apply the ev/gather macro.

In the following example, ev/write and os/proc-wait are both run in parallel via ev/gather. The ev/gather macro runs a number of fibers in parallel (in this case, two) on the event loop and returns the gathered results in an array.

(def proc
  (os/spawn ["janet" "-e" "(print (file/read stdin :all))"]
            :p {:in :pipe}))

(ev/gather
  (do
    # leads to printing via subprocess: know thyself
    (ev/write (proc :in) "know thyself")
    (ev/close (proc :in))
    :done)
  (os/proc-wait proc)) # => @[:done 0]

os/proc-close

The next example is a slightly modified version of the previous one. It involves adding a third fiber (to ev/gather) for reading standard output from the subprocess via ev/read.

(def proc
  (os/spawn ["janet" "-e" "(print (file/read stdin :all))"]
            :p {:in :pipe :out :pipe}))

(def buf @"")

(ev/gather
  (do
    (ev/write (proc :in) "know thyself")
    (ev/close (proc :in))
    :write-done)
  # hi! i'm new here :)
  (do
    (ev/read (proc :out) :all buf)
    :read-done)
  (os/proc-wait proc)) # => @[:write-done :read-done 0]

(os/proc-close proc) # => nil

buf # => @"know thyself\n"

Beware that if pipe streams are not closed soon enough, the process that created them (e.g. the janet executable) may run out of file descriptors or handles due to process limits enforced by an operating system.

Note the use of the function os/proc-close in the code above. This function takes a core/process value and closes all of the unclosed pipes that were created for it via os/spawn. In the example above, (proc :in) had already been closed explicitly, but (proc :out) had not and is thus closed by os/proc-close.

The os/proc-close function will also attempt to wait on the subprocess associated with the passed core/process value if it has not been waited on already. The function's return value is nil if waiting was not attempted and otherwise it is the exit code of the subprocess corresponding to the core/process value.

Effects of :x

If the flag keyword argument contains x for an invocation of os/spawn and the exit code of the subprocess is non-zero, calling a function such as os/proc-wait, os/proc-close, and os/proc-kill (if invoked to wait) with the subprocess' core/process value results in an error.

(def proc
  (os/spawn ["janet" "-e" "(os/exit 1)"] :px))

(defn invoke-an-error-raiser
  [proc]
  (def zero-one-or-two
    (-> (os/cryptorand 8)
        math/rng
        (math/rng-int 3)))
  # any of the following should raise an error
  (case zero-one-or-two
    0 (os/proc-wait proc)
    1 (os/proc-close proc)
    2 (os/proc-kill proc true)))

# prints: error
(try
  (invoke-an-error-raiser proc)
  ([_]
    (eprint "error")))