How to run local commands
Complete installation and initialize the local runtime before running these examples.
Run a command in a temporary machine
Choose an unused machine name. Pass commands as vectors of strings:
(require ' [ol.smolvm :as smol])
(with-open [machine (smol/create {:name "command-example"
:cpus 1
:memory-mib 256 })]
(smol/exec machine ["echo" "hello" ]))
The result includes :exit-code, :stdout, :stderr, and byte arrays
for both output streams. Check :exit-code for success. A nonzero exit
code does not throw.
Closing a handle from create deletes the machine and its storage.
The example deletes both when with-open ends, including after an exception.
Copy any required files out before leaving the block.
|
Commands do not run through a shell. For pipes or redirection, explicitly run a shell available in the guest:
(with-open [machine (smol/create {:name "shell-example" })]
(smol/exec machine ["sh" "-c" "printf hello > /tmp/message; cat /tmp/message" ]
{:timeout-ms 5000 }))
exec buffers all output in memory. For large output, use
streaming output.
Transfer a file
Use absolute guest paths. file-write creates parent directories and
replaces existing content:
(with-open [machine (smol/create {:name "file-example" })]
(smol/file-write machine "/workspace/message" "hello" )
(String. ^bytes (smol/file-read machine "/workspace/message" ) "UTF-8" ))
The returned string is "hello".
For binary data, pass a byte array and use the returned byte array directly.
If a consumer expects an input stream, use file-read-stream:
(with-open [machine (smol/create {:name "file-stream-example" })]
(smol/file-write machine "/workspace/message" "hello" )
(with-open [input (smol/file-read-stream machine "/workspace/message" )]
(slurp input :encoding "UTF-8" )))
This also returns "hello". The entire file is buffered before the stream
is returned. It is not a bounded-memory transfer. Read errors occur during
file-read-stream, not during later stream reads. Close the returned stream.
Use an existing machine
List machines, then connect by name:
(smol/list)
(with-open [machine (smol/connect "existing-machine" )]
{:name (smol/machine-name machine)
:result (smol/exec machine ["echo" "hello" ])})
Replace "existing-machine" with a name from the list.
connect starts the machine if needed. Closing this handle does not stop
or delete the machine. machine-name reads the handle without a native call.
Local IDs equal machine names. This accessor does not check whether the
machine still exists.
To stop a machine but keep its disk, call stop before closing the handle.
To delete the machine and its storage, call delete.
Stream command output
Use process to consume command output through Java streams.
It returns a process record with live output streams and a CompletableFuture
in :result. Waiting on that future needs no extra thread.
Copy stdout as the command runs
Capture stderr separately so it cannot fill an unread Java pipe:
(require ' [clojure.java.io :as io])
(with-open [machine (smol/create {:name "stream-example" })]
(let [process (smol/process machine ["echo" "hello" ]
{:err :string :timeout-ms 5000 })]
(try
(with-open [^java.io.InputStream output (:out process)]
(io/copy output System/out))
(deref (:result process) 10000 ::timeout )
(finally
(smol/destroy process)))))
The command prints hello. The result future supports timed dereference on
both Clojure and Babashka. Failures throw ExecutionException. Use ex-cause
to inspect the original failure.
Timed dereference waits up to ten seconds for
{:exit n :out value :err value}, then returns ::timeout if no result is
available. A wait timeout does not cancel the command. The finally block
requests cancellation if the process is still running. Use smol/destroy,
not future-cancel, to stop the guest command.
This wait limit does not interrupt the preceding io/copy.
The separate :timeout-ms option limits guest execution to five seconds.
It does not limit time spent writing to an output stream.
Close the returned input streams after use. The process record is not
Closeable, so do not put it in with-open.
If both outputs use the default :stream mode, drain them concurrently.
Do not wait for the process before draining its live output streams.
Forward both outputs
If you do not need to read the output in Clojure, pass output streams:
(with-open [machine (smol/create {:name "forward-example" })]
(let [process (smol/process machine ["echo" "hello" ]
{:out System/out :err System/err
:timeout-ms 5000 })]
(try
(deref (:result process) 10000 ::timeout )
(finally
(smol/destroy process)
(.flush System/out)
(.flush System/err)))))
The library does not close or flush supplied output streams.
For short output, choose :string or :bytes instead.
Consume native events
Use stream-open when you need separate stdout, stderr and exit events.
Use one reader and close the handle even after an exit event:
(with-open [machine (smol/create {:name "event-example" })
stream (smol/stream-open machine ["echo" "hello" ]
{:timeout-ms 5000 })]
(loop []
(when-let [event (smol/stream-next stream)]
(case (:kind event)
:stdout (.write System/out ^bytes (:data event))
:stderr (.write System/err ^bytes (:data event))
:exit (println "Exit:" (:exit-code event))
:error (throw (ex-info (:message event) event)))
(recur)))
(.flush System/out)
(.flush System/err))
Output events carry bytes. A UTF-8 character can cross event chunks, so
use a streaming decoder if you need text.
Use stream-kill to cancel a native stream command. It does not close the handle.
Do not pass process records to native stream functions.
Native output queues are unbounded. Consume output promptly. Streaming stdin is not supported.
For staged mounts, networking and images, see How to configure local workloads. For branches, checkpoints and packing, see How to reuse and save machine state.
See Local API reference for image configuration, mounts, ports, stream and process options, and cancellation. For smol cloud file streams, see How to run smol cloud commands.