cVisor — Ruby SDK #
A fiddle FFI wrapper over the libcvisor C ABI. Linux-only.
Usage #
require "cvisor"
sb = Cvisor::Sandbox.new
out = sb.run("echo hello")
puts out.stdout # "hello\n"
puts out.stderr # ""
puts out.exit_code # 0
Sandbox#run(cmd, timeout_ms: nil) blocks until the sandboxed command exits and
returns an Output with #stdout / #stderr (String), #stdout_bytes /
#stderr_bytes, and #exit_code (Integer, shell convention: status, or
128+signo when killed by a signal).
Pass a positive timeout_ms: to SIGKILL the guest after that many
milliseconds; a timed-out run reports exit code 137:
sb.run("sleep 30", timeout_ms: 300).exit_code # 137
Sandbox#set_allow_network(allow) toggles outbound INET/INET6 networking for
the sandbox (allowed by default):
sb.set_allow_network(false) # deny outbound networking
Sandbox#set_allow_listen(allow) toggles inbound TCP servers (listening
sockets) inside the sandbox (denied by default):
sb.set_allow_listen(true) # allow inbound TCP servers
sb.set_env("TOKEN", "xyz") # environment variable for the guest
Files #
Sandbox#write_file(path, data) seeds a file into the sandbox's persistent
overlay at an absolute path; it is visible to later #run calls of the same
Sandbox instance. Sandbox#read_file(path) returns the guest's view of an
absolute path (the overlay copy if present, else the real host file for cow
paths) as a binary String ("" for an empty or missing file). Paths must be
absolute; /proc and passthrough paths are not writable.
sb.write_file("/tmp/data.txt", "seeded\n")
sb.run("grep seeded /tmp/data.txt").stdout # "seeded\n"
sb.run("echo from-run > /tmp/out.txt")
sb.read_file("/tmp/out.txt") # "from-run\n"
Copying files and directories #
Sandbox#copy_into(host_path, guest_path) copies a host file or directory tree
into the sandbox overlay at guest_path, visible to later #run calls.
Directory copies are recursive and honor .gitignore/.dockerignore.
Sandbox#copy_out(guest_path, host_path) copies the guest's view of
guest_path (file or directory) back out to the host. Both raise on error.
sb.copy_into("./project", "/work")
sb.run("ls /work").stdout
sb.copy_out("/work/build", "./build")
Cache #
Sandbox#cache_save(sandbox_path, key, backend: "", format: "gzip") archives
the sandbox directory sandbox_path under key in a cache backend.
Sandbox#cache_restore(sandbox_path, key, backend: "", format: "gzip") unpacks
a stored archive back into the sandbox overlay at sandbox_path. Both raise on
error. Directory archives honor .gitignore/.dockerignore.
sb.cache_save("/work/node_modules", "deps")
# ... later, in a fresh sandbox ...
sb2.cache_restore("/work/node_modules", "deps")
backend:""or"disk"(default),"disk:/path"for a specific directory, or an"s3://bucket/prefix?..."URL. S3 requireslibcvisorbuilt with thes3feature; the bundled library is disk-only.format:"gzip"(default),"estargz","none", or"zstd".zstdrequireslibcvisorbuilt with that feature; the bundled library does not include it.
Streaming sessions #
Sandbox#run_streaming(command, on_stdout:, on_stderr:, poll_ms: 15) starts a
non-PTY session and streams output to the callbacks (each receives a UTF-8
String) as it arrives, blocking until the command exits and returning its
exit code:
code = sb.run_streaming("for i in 1 2 3; do echo line$i; sleep 0.1; done",
on_stdout: ->(s) { print s },
on_stderr: ->(s) { warn s })
puts code # 0
Interactive PTY shell #
Sandbox#shell(on_output: nil, poll_ms: 15) starts an interactive PTY session
(/bin/sh -i) and returns a Cvisor::Session. A PTY merges stdout and stderr,
so all output arrives via #read_stdout (and the on_output: callback):
buf = []
sh = sb.shell(on_output: ->(s) { buf << s })
sh.write_stdin("echo hello\n") # bytes written (Integer)
sh.resize(40, 120) # rows, cols
sh.write_stdin("exit 0\n")
code = sh.wait # blocks -> exit code
sh.close # free the session (idempotent)
puts buf.join
Cvisor::Session methods:
#read_stdout/#read_stderr->String: drain any new bytes (""when none). PTY sessions deliver everything on stdout.#write_stdin(data)->Integer: writedata(aString) to the PTY, returns bytes written (PTY sessions only).#resize(rows, cols): resize the PTY window.#exit_code->Integerornil: non-blocking; the exit code once finished, elsenil.#wait->Integer: block until exit, returning the exit code.#kill: SIGKILL the session's process.#close: free the session (idempotent).
Remote daemon (GraphQL) — works on macOS #
The Sandbox above needs the native libcvisor (Linux only). To use cVisor
from any platform, including macOS, talk to a running
cvisord over its GraphQL API. This path is pure
stdlib (net/http + json) — no native library — and require "cvisor" never
loads the .so:
require "cvisor"
remote = Cvisor::RemoteSandbox.new("http://127.0.0.1:8080/graphql", token)
out = remote.run("echo hello")
puts out["stdout"] # "hello\n"
sb = remote.create_sandbox("my-box")
remote.write_file(sb["id"], "/app/x", "hi") # base64 handled for you
remote.read_file(sb["id"], "/app/x") # "hi"
snap = remote.snapshot(sb["id"])
remote.fork(sb["id"], "clone")
remote.free_sandbox(sb["id"])
RemoteSandbox mirrors the daemon surface: #run, #create_sandbox /
#list_sandboxes / #free_sandbox / #configure, #write_file / #read_file,
#cache_save / #cache_restore / #cache_list, #snapshot / #rollback /
#branch / #fork / #snapshots / #delete_snapshot, and #health. For raw
documents, drop to the client:
gql = Cvisor::GraphQL.new("http://127.0.0.1:8080/graphql", token)
data = gql.query("{ sandboxes { id name } }")
gql.mutate("mutation($c:String!){ run(command:$c){ stdout } }", { "c" => "uname -a" })
The daemon prints its bearer token on startup (or set CVISOR_TOKEN). On a
non-Linux host, constructing the FFI Cvisor::Sandbox raises a clear
"Linux-only" error pointing you here.
Interactive console #
Launch an IRB session with a live sandbox preloaded:
bin/console
cVisor interactive console
sb -> a Sandbox instance
sh("cmd") -> run a shell command in the sandbox, printing stdout/stderr
Cvisor::Sandbox.new -> create your own
irb(main):001> sh("echo hello; uname -n")
hello
cvisor
Development #
The SDK loads libcvisor.so. Build it from the repo root (cargo xtask ffi),
which drops a copy into native/, or point the SDK at one via the CVISOR_LIB
environment variable.