bsdkrun 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe9ff0a423b1c2c2a7e2c61517396210b49e375dcb476f0fa33ab256e2d4a615
4
- data.tar.gz: cb19ed673cc77aeca248eda87d51cba7d8952f3be4396a797d625e6a1e644b8f
3
+ metadata.gz: e78b89f68872f94abe500dba9bc0d69bd2b8ff0488745bb1f7e07ef2658f8a28
4
+ data.tar.gz: 1effefa03711b90030219d9456edbb6df361591184aa9bb7f01c06e87ad582e2
5
5
  SHA512:
6
- metadata.gz: da14cb176e1f9aacb27c7689d267a7f551f3aa94dd261fb4a5b4d0c1935c8e07c76bf9f6e9bf52600642a96f76bb8664e3e932a29105f22bf9dca15ca3b8bdd7
7
- data.tar.gz: 7ae4a5af1c196da907d593aa20f7a4db241250186ab7609e5494e8328876bf1e47383472421da12ea8c4c9dda90e9687a8bb76ff202798beb56989bc7690d65e
6
+ metadata.gz: ce3f123068f4c08775e7308513b727182ece1ce14737de2cc874ed050be8e19979e82c902703202f72a162222d0b12363a6aefc5b9441eeab34038976ed28a1c
7
+ data.tar.gz: 8eb660e5db46bf1fc617b250f76c55f41f75943f2bc71026994790e812c000ae90aa63384956938e7a5bda406d8f59912ab36ac7f5a5029a615738a1ccb4dca3
data/README.md CHANGED
@@ -9,14 +9,14 @@ dependencies** — just the Ruby standard library (`open3`, `json`, `pathname`).
9
9
  ```ruby
10
10
  require "bsdkrun"
11
11
 
12
- box = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
12
+ sbx = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
13
13
 
14
14
  # exec argv directly, with env / stdin / a PTY / a working dir:
15
- puts box.exec(["uname", "-a"]).text
16
- box.exec(["apk", "add", "curl"], throw_on_error: true)
17
- box.run_command("curl", ["-fsSL", "https://example.com"])
15
+ puts sbx.exec(["uname", "-a"]).text
16
+ sbx.exec(["apk", "add", "curl"], throw_on_error: true)
17
+ sbx.run_command("curl", ["-fsSL", "https://example.com"])
18
18
 
19
- box.stop
19
+ sbx.stop
20
20
  ```
21
21
 
22
22
  ## Install
@@ -77,24 +77,45 @@ Bsdkrun::Sandbox.create(os: "kernel", kernel: "netbsd", format: "elf", disk: "ro
77
77
 
78
78
  Every `create` runs the machine **detached** and returns a `Sandbox` handle.
79
79
 
80
+ ### Environment variables
81
+
82
+ `env:` sets the guest environment for the machine's entrypoint. It is merged
83
+ over the image's own config, so a key the image already defines is replaced
84
+ rather than duplicated.
85
+
86
+ ```ruby
87
+ sbx = Bsdkrun::Sandbox.create(
88
+ os: "linux",
89
+ image: "node:22",
90
+ env: { "NODE_ENV" => "production", "PORT" => "3000" },
91
+ command: ["node", "server.js"]
92
+ )
93
+ ```
94
+
95
+ Linux guests only — BSD guests boot their own init, so there is no generated
96
+ init to export into; set those from `exec` after boot. For a single command
97
+ rather than the whole machine, `exec` takes its own `env:`.
98
+
80
99
  ## Running commands
81
100
 
82
101
  `exec` is the primary programmatic entrypoint. No shell parsing — pass an argv
83
102
  array (or a program name plus `args:`).
84
103
 
85
104
  ```ruby
86
- box.exec(["ls", "-la", "/etc"])
105
+ sbx.exec(["ls", "-la", "/etc"])
87
106
 
88
- box.exec("ruby",
107
+ sbx.exec("ruby",
89
108
  args: ["-e", "puts ENV['X']"],
90
109
  env: { "X" => "hi" },
91
110
  cwd: "/app",
92
111
  stdin: "data on stdin",
93
112
  tty: true, # allocate a PTY
113
+ on_stdout: ->(chunk) { $stdout.write(chunk) },
114
+ on_stderr: ->(chunk) { $stderr.write(chunk) },
94
115
  throw_on_error: true) # raise on non-zero exit (default: false)
95
116
 
96
117
  # Vercel-Sandbox-style alias:
97
- result = box.run_command("uname", ["-a"])
118
+ result = sbx.run_command("uname", ["-a"])
98
119
  result.stdout # raw stdout
99
120
  result.text # stdout, trailing newlines trimmed
100
121
  result.exit_code
@@ -105,21 +126,78 @@ result.lines # non-empty stdout lines
105
126
  `exec` returns a `Bsdkrun::Result`. It only raises `CommandFailed` when you pass
106
127
  `throw_on_error: true` (or call `result.throw_if_failed!`).
107
128
 
129
+ The callbacks run as chunks arrive, and the same bytes remain buffered in the
130
+ returned result. They do not require `tty`; a PTY changes command semantics and
131
+ may merge stderr into stdout.
132
+
133
+ ## Caching
134
+
135
+ `sbx.cache` saves a guest directory under a key and restores it later, so a
136
+ rebuild can pick up where the last one left off. **A miss is not an error** —
137
+ check `restored` rather than rescuing.
138
+
139
+ ```ruby
140
+ key = "deps-#{lock_hash}"
141
+ hit = sbx.cache.restore(key: key, restore_keys: ["deps-"])
142
+ unless hit.restored
143
+ sbx.exec(["npm", "ci"])
144
+ sbx.cache.save("/app/node_modules", key: key, compression: "zstd")
145
+ end
146
+
147
+ Bsdkrun::Caches.ls # every stored entry, newest first
148
+ Bsdkrun::Caches.rm([key]) # or Bsdkrun::Caches.rm(all: true)
149
+ ```
150
+
151
+ `restore_keys` are prefixes tried in order when the exact key misses; within a
152
+ prefix the newest matching entry wins, and `hit.key` says which one was used.
153
+ Formats are `gzip` (default), `zstd`, `estargz` and `none`.
154
+
155
+ Where entries live is host configuration, not an SDK concern: the default is
156
+ this host's disk, and `BSDKRUN_CACHE_BACKEND=s3` + `BSDKRUN_CACHE_S3_*` (or
157
+ `~/.config/bsdkrun/cache.toml`) points them at a bucket instead.
158
+
159
+ ## Files
160
+
161
+ `sbx.fs` reads and writes files in the guest. Parent directories are created
162
+ for you, and everything is byte-exact — `read_file` returns a binary string.
163
+
164
+ ```ruby
165
+ sbx.fs.write_file("/app/main.py", "print('hi')")
166
+ sbx.fs.write_file("/app/logo.png", png_bytes)
167
+
168
+ text = sbx.fs.read_text("/app/out.json")
169
+ bytes = sbx.fs.read_file("/app/logo.png")
170
+
171
+ sbx.fs.upload("./src", "/app/src") # file or directory
172
+ sbx.fs.download("/app/dist", "./dist", recursive: true)
173
+ ```
174
+
175
+ `upload` looks at the local path to decide whether to recurse; `download` cannot
176
+ (the path is in the guest), so say so for a directory. A directory's *contents*
177
+ land in the destination: uploading `./src` to `/app/src` leaves the guest's
178
+ `/app/src` holding what `./src` holds.
179
+
180
+ Failures raise `Bsdkrun::FileTransferFailed`, which carries the offending `path`.
181
+
182
+ > Transfers ride the same in-guest agent as `exec`, so the sandbox must be
183
+ > running. A directory copy also needs `tar` in the guest; single files need
184
+ > only the shell every bootable image already has.
185
+
108
186
  ## Lifecycle & inventory
109
187
 
110
188
  ```ruby
111
- box = Bsdkrun::Sandbox.create(os: "linux", image: "alpine", command: ["sleep", "300"])
112
- same = Bsdkrun::Sandbox.get(box.id) # reconnect (prefix ok)
189
+ sbx = Bsdkrun::Sandbox.create(os: "linux", image: "alpine", command: ["sleep", "300"])
190
+ same = Bsdkrun::Sandbox.get(sbx.id) # reconnect (prefix ok)
113
191
  all = Bsdkrun::Sandbox.list(all: true) # Array<SandboxInfo>
114
192
 
115
- box.status # SandboxInfo | nil
116
- box.running? # true / false
117
- box.logs # console log (String)
118
- box.shell # interactive shell (inherits the terminal)
119
- box.stop # BSD guests clean-poweroff; Linux SIGTERM
120
- box.start # restart in place — resumes its own disk/rootfs (data persists)
121
- box.update(cpus: 4, mem: 2048) # applies on next start
122
- box.remove(force: true)
193
+ sbx.status # SandboxInfo | nil
194
+ sbx.running? # true / false
195
+ sbx.logs # console log (String)
196
+ sbx.shell # interactive shell (inherits the terminal)
197
+ sbx.stop # BSD guests clean-poweroff; Linux SIGTERM
198
+ sbx.start # restart in place — resumes its own disk/rootfs (data persists)
199
+ sbx.update(cpus: 4, mem: 2048) # applies on next start
200
+ sbx.remove(force: true)
123
201
  ```
124
202
 
125
203
  Host-level namespaces:
@@ -141,11 +219,11 @@ Bsdkrun::System.versions("netbsd") # Array<String>
141
219
  Bsdkrun::Sandbox.create(os: "linux", image: "alpine", net: { ports: ["2222:22"] })
142
220
 
143
221
  # agent-managed key-based SSH
144
- box.ssh_setup # install local ~/.ssh/*.pub keys
145
- box.ssh_setup(user: "tsiry", key: "~/.ssh/work.pub")
222
+ sbx.ssh_setup # install local ~/.ssh/*.pub keys
223
+ sbx.ssh_setup(user: "tsiry", key: "~/.ssh/work.pub")
146
224
 
147
225
  # put a guest on your tailnet
148
- box.tailscale_up(authkey: "tskey-auth-...", hostname: "web")
226
+ sbx.tailscale_up(authkey: "tskey-auth-...", hostname: "web")
149
227
  ```
150
228
 
151
229
  ### Global networks — reach machines by name
data/lib/bsdkrun/args.rb CHANGED
@@ -28,6 +28,17 @@ module Bsdkrun
28
28
  # +--name+ flag if a name is set.
29
29
  # @param o [Hash]
30
30
  # @return [Array<String>]
31
+ # +-e K=V+ per entry, sorted by key.
32
+ #
33
+ # A hash has no argv order of its own, so sorting is what makes the command
34
+ # line — and the tests that assert on it — deterministic. The guest sees the
35
+ # same environment either way.
36
+ def env_args(env)
37
+ return [] if env.nil? || env.empty?
38
+
39
+ env.to_h.sort_by { |k, _| k.to_s }.flat_map { |k, v| ["-e", "#{k}=#{v}"] }
40
+ end
41
+
31
42
  def name_args(o)
32
43
  o[:name] ? ["--name", o[:name].to_s] : []
33
44
  end
@@ -81,7 +92,9 @@ module Bsdkrun
81
92
  a.push("--initramfs") if opts[:initramfs]
82
93
  a.push("-v", opts[:volume]) if opts[:volume]
83
94
  Array(opts[:mounts]).each { |m| a.push("--mount", m) }
95
+ Array(opts[:attach_disk]).each { |d| a.push("--attach-disk", d) }
84
96
  a.push("--entrypoint", opts[:entrypoint]) if opts[:entrypoint]
97
+ a.concat(env_args(opts[:env]))
85
98
  a.push("--console", opts[:console]) if opts[:console]
86
99
  a.concat(net_args(opts[:net])).concat(name_args(opts)).concat(vm_args(opts))
87
100
  cmd = opts[:command]
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Bsdkrun
6
+ # A stored cache entry, as +cache ls+ reports it.
7
+ CacheEntry = Struct.new(:key, :path, :compression, :size, :created, :digest, keyword_init: true) do
8
+ def self.from(row)
9
+ new(
10
+ key: row["key"].to_s,
11
+ path: row["path"].to_s,
12
+ compression: row["compression"].to_s,
13
+ size: row["size"].to_i,
14
+ created: row["created"].to_i,
15
+ digest: row["digest"].to_s
16
+ )
17
+ end
18
+ end
19
+
20
+ # What a restore did. A miss is not an error — check +restored+.
21
+ RestoreResult = Struct.new(
22
+ :restored, :requested_key, :key, :path, :size, :compression, :created,
23
+ keyword_init: true
24
+ )
25
+
26
+ # Save and restore guest directories under a key, reached as {Sandbox#cache}.
27
+ #
28
+ # Entries are keyed, so a rebuild can pick up where the last one left off:
29
+ #
30
+ # hit = sbx.cache.restore(key: key, restore_keys: ["deps-"])
31
+ # unless hit.restored
32
+ # sbx.exec(["npm", "ci"])
33
+ # sbx.cache.save("/app/node_modules", key: key)
34
+ # end
35
+ #
36
+ # Where entries live — host disk or S3 — is host configuration, not an SDK
37
+ # concern: set +BSDKRUN_CACHE_BACKEND+ / +BSDKRUN_CACHE_S3_*+, or write
38
+ # +~/.config/bsdkrun/cache.toml+.
39
+ class Cache
40
+ # @param id [String] the machine's id.
41
+ def initialize(id)
42
+ @id = id
43
+ end
44
+
45
+ # Archive the guest directory at +path+ under +key+.
46
+ #
47
+ # @param path [String] absolute path in the guest.
48
+ # @param key [String] key to store under.
49
+ # @param compression [String] gzip (default), zstd, estargz or none.
50
+ # @param force [Boolean] replace an entry that already has this key.
51
+ # @return [CacheEntry]
52
+ def save(path, key:, compression: "gzip", force: false)
53
+ args = ["cache", "save", "#{@id}:#{path}", "--key", key, "--json"]
54
+ args += ["--compression", compression] unless compression == "gzip"
55
+ args << "--force" if force
56
+ CacheEntry.from(json(args, "bsdkrun cache save"))
57
+ end
58
+
59
+ # Restore a stored tree.
60
+ #
61
+ # @param key [String]
62
+ # @param path [String, nil] defaults to where the entry was saved from.
63
+ # @param restore_keys [Array<String>] prefixes tried in order on a miss.
64
+ # @return [RestoreResult]
65
+ def restore(key:, path: nil, restore_keys: [])
66
+ target = path ? "#{@id}:#{path}" : @id
67
+ args = ["cache", "restore", target, "--key", key, "--json"]
68
+ args += ["--restore-keys", *restore_keys] unless restore_keys.empty?
69
+ row = json(args, "bsdkrun cache restore")
70
+ RestoreResult.new(
71
+ restored: !!row["restored"],
72
+ requested_key: row["requested_key"].to_s,
73
+ key: row["key"],
74
+ path: row["path"],
75
+ size: row["size"],
76
+ compression: row["compression"],
77
+ created: row["created"]
78
+ )
79
+ end
80
+
81
+ private
82
+
83
+ def json(args, label)
84
+ out = Process.run!(args, label: label).stdout
85
+ JSON.parse(out.strip.empty? ? "{}" : out)
86
+ end
87
+ end
88
+
89
+ # Host-level cache operations, mirroring {Bsdkrun.volumes}.
90
+ module Caches
91
+ module_function
92
+
93
+ # @return [Array<CacheEntry>] every stored entry, newest first.
94
+ def ls
95
+ out = Process.run!(["cache", "ls", "--json"], label: "bsdkrun cache ls").stdout
96
+ JSON.parse(out.strip.empty? ? "[]" : out).map { |row| CacheEntry.from(row) }
97
+ end
98
+
99
+ # Remove entries by key, or every one with <tt>all: true</tt>.
100
+ # @return [void]
101
+ def rm(keys = [], all: false)
102
+ args = ["cache", "rm"]
103
+ if all
104
+ args << "--all"
105
+ else
106
+ args += Array(keys)
107
+ end
108
+ Process.run!(args, label: "bsdkrun cache rm")
109
+ nil
110
+ end
111
+ end
112
+ end
@@ -293,6 +293,7 @@ module Bsdkrun
293
293
  net: net_input(o[:net]),
294
294
  volume: o[:volume],
295
295
  mounts: o[:mounts] || [],
296
+ attachDisk: o[:attach_disk] || [],
296
297
  env: o[:env] || [],
297
298
  entrypoint: o[:entrypoint],
298
299
  initramfs: o[:initramfs] || false,
@@ -50,6 +50,19 @@ module Bsdkrun
50
50
  end
51
51
  end
52
52
 
53
+ # A guest filesystem operation was refused (see {FileSystem}).
54
+ class FileTransferFailed < Error
55
+ # @return [String] the path that could not be transferred.
56
+ attr_reader :path
57
+
58
+ # @param message [String]
59
+ # @param path [String]
60
+ def initialize(message, path)
61
+ @path = path
62
+ super(message)
63
+ end
64
+ end
65
+
53
66
  # A GraphQL request to a remote +bsdkrund+ daemon failed — a transport
54
67
  # failure, a non-JSON response, or a +body["errors"]+ entry that was not an
55
68
  # auth failure. Mirrors +web/src/lib/graphql.ts+'s +GraphQLError+.
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bsdkrun
4
+ # Files in a running sandbox, reached as {Sandbox#fs}.
5
+ #
6
+ # Every call goes through the guest's exec agent, so the sandbox has to be
7
+ # running — there is no offline write.
8
+ #
9
+ # @example
10
+ # sbx.fs.write_file("/app/main.py", "print('hi')")
11
+ # sbx.fs.read_text("/app/out.json")
12
+ # sbx.fs.upload("./src", "/app/src")
13
+ # sbx.fs.download("/app/dist", "./dist", recursive: true)
14
+ class FileSystem
15
+ # @param id [String] the machine's id.
16
+ def initialize(id)
17
+ @id = id
18
+ end
19
+
20
+ # Write +data+ to +path+ in the guest, creating parent directories.
21
+ #
22
+ # @param path [String] absolute path in the guest.
23
+ # @param data [String] text or binary content.
24
+ # @return [void]
25
+ # @raise [FileTransferFailed]
26
+ def write_file(path, data)
27
+ res = Process.run(["cp", "-", "#{@id}:#{path}"], stdin: data, binary: true)
28
+ check!(res, path)
29
+ nil
30
+ end
31
+
32
+ # Read +path+ from the guest as bytes (ASCII-8BIT).
33
+ #
34
+ # @param path [String]
35
+ # @return [String] binary string.
36
+ # @raise [FileTransferFailed]
37
+ def read_file(path)
38
+ res = Process.run(["cp", "#{@id}:#{path}", "-"], binary: true)
39
+ check!(res, path)
40
+ res.stdout
41
+ end
42
+
43
+ # Read +path+ from the guest and tag it with +encoding+.
44
+ #
45
+ # @param path [String]
46
+ # @param encoding [String]
47
+ # @return [String]
48
+ def read_text(path, encoding: "UTF-8")
49
+ read_file(path).force_encoding(encoding)
50
+ end
51
+
52
+ # Copy a host file or directory into the guest.
53
+ #
54
+ # A directory's *contents* land in +remote_path+, so
55
+ # <tt>upload("./src", "/app/src")</tt> leaves the guest's +/app/src+ holding
56
+ # what +./src+ holds. Whether it recurses is decided by looking at the local
57
+ # path, so callers do not have to say which kind of thing it is.
58
+ #
59
+ # @param local_path [String]
60
+ # @param remote_path [String]
61
+ # @return [void]
62
+ # @raise [FileTransferFailed]
63
+ def upload(local_path, remote_path)
64
+ unless File.exist?(local_path)
65
+ raise FileTransferFailed.new("cannot upload #{local_path}: no such file or directory",
66
+ local_path)
67
+ end
68
+ args = ["cp"]
69
+ args << "-r" if File.directory?(local_path)
70
+ args += [local_path.to_s, "#{@id}:#{remote_path}"]
71
+ check!(Process.run(args), local_path)
72
+ nil
73
+ end
74
+
75
+ # Copy a file or directory out of the guest onto the host.
76
+ #
77
+ # Pass <tt>recursive: true</tt> for a directory; unlike {#upload} it cannot
78
+ # be detected here, because the path lives in the guest and answering would
79
+ # cost an extra round trip.
80
+ #
81
+ # @param remote_path [String]
82
+ # @param local_path [String]
83
+ # @param recursive [Boolean]
84
+ # @return [void]
85
+ # @raise [FileTransferFailed]
86
+ def download(remote_path, local_path, recursive: false)
87
+ args = ["cp"]
88
+ args << "-r" if recursive
89
+ args += ["#{@id}:#{remote_path}", local_path.to_s]
90
+ check!(Process.run(args), remote_path)
91
+ nil
92
+ end
93
+
94
+ private
95
+
96
+ def check!(res, path)
97
+ return if res.exit_code.zero?
98
+
99
+ # The CLI already explains these well; strip its "Error: " prefix.
100
+ text = res.stderr.to_s.strip.sub(/\AError:\s*/, "")
101
+ text = "file transfer failed for #{path}" if text.empty?
102
+ raise FileTransferFailed.new(text, path)
103
+ end
104
+ end
105
+ end
@@ -26,12 +26,39 @@ module Bsdkrun
26
26
  # @param env [Hash] extra environment merged onto the process env.
27
27
  # @param stdin [String, nil] data piped to the child's stdin.
28
28
  # @param log_level [Integer] bsdkrun global log level (0=off .. 5=trace).
29
+ # @param binary [Boolean] keep stdout as bytes (ASCII-8BIT) instead of text.
30
+ # Needed by {FileSystem#read_file}: appending a chunk of arbitrary bytes to
31
+ # a UTF-8 buffer raises Encoding::CompatibilityError, so a PNG read back
32
+ # out of a guest would blow up mid-transfer rather than return.
29
33
  # @return [RawResult]
30
- def run(args, env: {}, stdin: nil, log_level: 0)
34
+ def run(args, env: {}, stdin: nil, log_level: 0, on_stdout: nil, on_stderr: nil, binary: false)
31
35
  bin = Binary.resolve
32
36
  full = ["--log-level", log_level.to_s, *args]
33
37
  merged_env = env.to_h.transform_keys(&:to_s).transform_values(&:to_s)
34
- out, err, status = Open3.capture3(merged_env, bin, *full, stdin_data: stdin || "")
38
+ out = binary ? (+"").b : +""
39
+ err = +""
40
+ status = nil
41
+ Open3.popen3(merged_env, bin, *full) do |child_in, child_out, child_err, wait|
42
+ if binary
43
+ child_in.binmode
44
+ child_out.binmode
45
+ end
46
+ writer = Thread.new { child_in.write(stdin) if stdin; child_in.close }
47
+ stdout_reader = Thread.new do
48
+ while (chunk = child_out.readpartial(8192) rescue nil)
49
+ out << chunk
50
+ on_stdout&.call(chunk)
51
+ end
52
+ end
53
+ stderr_reader = Thread.new do
54
+ while (chunk = child_err.readpartial(8192) rescue nil)
55
+ err << chunk
56
+ on_stderr&.call(chunk)
57
+ end
58
+ end
59
+ [writer, stdout_reader, stderr_reader].each(&:join)
60
+ status = wait.value
61
+ end
35
62
  RawResult.new(stdout: out, stderr: err, exit_code: status.exitstatus || 0)
36
63
  end
37
64
 
@@ -9,9 +9,9 @@ module Bsdkrun
9
9
  # with {Sandbox.list}.
10
10
  #
11
11
  # @example
12
- # box = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
13
- # box.exec(["uname", "-a"]).text
14
- # box.stop
12
+ # sbx = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
13
+ # sbx.exec(["uname", "-a"]).text
14
+ # sbx.stop
15
15
  class Sandbox
16
16
  ID_RE = /\A[0-9a-f]{6,}\z/
17
17
  SSH_PORT_RE = /ssh -p (\d+)/
@@ -32,6 +32,18 @@ module Bsdkrun
32
32
  @ssh_port = ssh_port
33
33
  end
34
34
 
35
+ # Read and write files in the guest.
36
+ # @return [FileSystem]
37
+ def fs
38
+ @fs ||= FileSystem.new(@id)
39
+ end
40
+
41
+ # Save and restore guest directories under a key.
42
+ # @return [Cache]
43
+ def cache
44
+ @cache ||= Cache.new(@id)
45
+ end
46
+
35
47
  class << self
36
48
  # Boot a new microVM and return a handle to it.
37
49
  #
@@ -109,9 +121,11 @@ module Bsdkrun
109
121
  # @param cwd [String, nil] working directory (emulated via +sh -c 'cd …'+).
110
122
  # @param throw_on_error [Boolean] raise {CommandFailed} on a non-zero exit.
111
123
  # @param log_level [Integer] per-command bsdkrun log level.
124
+ # @param on_stdout [Proc, nil] called with each stdout chunk as it arrives.
125
+ # @param on_stderr [Proc, nil] called with each stderr chunk as it arrives.
112
126
  # @return [Result]
113
127
  def exec(command, args: [], env: {}, tty: false, stdin: nil, cwd: nil,
114
- throw_on_error: false, log_level: 0)
128
+ throw_on_error: false, log_level: 0, on_stdout: nil, on_stderr: nil)
115
129
  argv = command.is_a?(Array) ? command.dup : [command, *args]
116
130
 
117
131
  if cwd
@@ -124,7 +138,8 @@ module Bsdkrun
124
138
  env.each { |k, v| cli.push("-e", "#{k}=#{v}") }
125
139
  cli.push(@id, *argv)
126
140
 
127
- res = Process.run(cli, stdin: stdin, log_level: log_level)
141
+ res = Process.run(cli, stdin: stdin, log_level: log_level,
142
+ on_stdout: on_stdout, on_stderr: on_stderr)
128
143
  result = Result.new(
129
144
  stdout: res.stdout, stderr: res.stderr, exit_code: res.exit_code,
130
145
  command: "exec #{argv.join(' ')}"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Bsdkrun
4
4
  # The bsdkrun Ruby SDK version.
5
- VERSION = "0.3.1"
5
+ VERSION = "0.4.0"
6
6
  end
data/lib/bsdkrun.rb CHANGED
@@ -5,6 +5,8 @@ require_relative "bsdkrun/errors"
5
5
  require_relative "bsdkrun/binary"
6
6
  require_relative "bsdkrun/process"
7
7
  require_relative "bsdkrun/args"
8
+ require_relative "bsdkrun/cache"
9
+ require_relative "bsdkrun/filesystem"
8
10
  require_relative "bsdkrun/types"
9
11
  require_relative "bsdkrun/sandbox"
10
12
  require_relative "bsdkrun/images"
@@ -23,9 +25,9 @@ require_relative "bsdkrun/client"
23
25
  # @example
24
26
  # require "bsdkrun"
25
27
  #
26
- # box = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
27
- # puts box.exec(["uname", "-a"]).text
28
- # box.stop
28
+ # sbx = Bsdkrun::Sandbox.create(os: "linux", image: "alpine")
29
+ # puts sbx.exec(["uname", "-a"]).text
30
+ # sbx.stop
29
31
  module Bsdkrun
30
32
  class << self
31
33
  # Force the SDK to use a specific +bsdkrun+ binary, bypassing discovery.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bsdkrun
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tsiry Sandratraina
@@ -51,8 +51,10 @@ files:
51
51
  - lib/bsdkrun.rb
52
52
  - lib/bsdkrun/args.rb
53
53
  - lib/bsdkrun/binary.rb
54
+ - lib/bsdkrun/cache.rb
54
55
  - lib/bsdkrun/client.rb
55
56
  - lib/bsdkrun/errors.rb
57
+ - lib/bsdkrun/filesystem.rb
56
58
  - lib/bsdkrun/images.rb
57
59
  - lib/bsdkrun/networks.rb
58
60
  - lib/bsdkrun/process.rb