Class: Bsdkrun::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/bsdkrun/client.rb

Overview

A client that talks to a remote bsdkrund daemon's GraphQL API directly — HTTP for queries/mutations, a hand-rolled graphql-transport-ws socket for subscriptions — instead of shelling out to a local bsdkrun binary the way Sandbox does.

The wire contract (URL/header shape, error mapping, subscription protocol, field names) is locked to match the other bsdkrun SDKs (TypeScript/Python/Elixir/Gleam) and the web frontend's web/src/lib/graphql.ts byte-for-byte — see that file for the reference implementation this one mirrors.

Examples:

client = Bsdkrun::Client.from_env
client.list.each { |m| puts m.id }
result = client.exec(id, ["uname", "-a"])
puts result.output

Constant Summary collapse

URL_ENV =
"BSDKRUN_URL"
TOKEN_ENV =
"BSDKRUN_TOKEN"
MACHINE_FIELDS =

The Machine field selection shared by machines / machine — mirrors +web/src/lib/api.ts+'s MACHINE_FIELDS fragment exactly.

<<~GQL.freeze
  id name image kind command status running exitCode pid detached
  cpus mem volume stateDir createdAt finishedAt network netIp origin
  ports { bind host guest }
GQL
SNAPSHOT_FIELDS =

The Snapshot selection, likewise shared by every snapshot document.

<<~GQL.freeze
  id name machineId machineName kind image path parent description
  cpus mem size createdAt ports { bind host guest }
GQL
AI_AGENT_FIELDS =

---- ai agents -------------------------------------------------------------

A sandbox is a machine, so its terminal is the ordinary #shell with the argv #ai_shell_command returns.

"id label flavor description installed running"
AI_SESSION_FIELDS =
"id name agent running workspace createdAt"
DOCKER_STATUS_FIELDS =

---- docker --------------------------------------------------------------

bsdkrun runs one docker:dind microVM and serves its API on a host unix socket, so these drive the same engine the host's docker CLI does.

<<~GQL.freeze
  running machineId machineRunning socket socketReady apiPort version
  containers images mounts disk diskSize
GQL
DOCKER_CONTAINER_FIELDS =
"id name image command state status ports created"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url:, token:) ⇒ Client

Returns a new instance of Client.

Parameters:



54
55
56
57
58
59
# File 'lib/bsdkrun/client.rb', line 54

def initialize(url:, token:)
  @url = self.class.normalize_url(url)
  @token = token.to_s
  @ws_mutex = Mutex.new
  @ws = nil
end

Instance Attribute Details

#urlString (readonly)

Returns the GraphQL endpoint URL (normalized).

Returns:

  • (String)

    the GraphQL endpoint URL (normalized).



49
50
51
# File 'lib/bsdkrun/client.rb', line 49

def url
  @url
end

Class Method Details

.from_envClient

Build a client from BSDKRUN_URL / BSDKRUN_TOKEN.

A host set without a token is an error, not a silent fallback — mirrors +daemon/src/client.rs+'s RemoteConfig::from_env (which uses +BSDKRUN_HOST+/+BSDKRUN_TOKEN+ for the gRPC client; these are GraphQL-specific env vars with a different URL shape, not aliases).

Returns:

Raises:

  • (Error)

    if BSDKRUN_URL is unset, or set without BSDKRUN_TOKEN.



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/bsdkrun/client.rb', line 70

def self.from_env
  url = ENV[URL_ENV]
  raise Error, "#{URL_ENV} is not set; nothing to connect to" if url.nil? || url.strip.empty?

  token = ENV[TOKEN_ENV]
  if token.nil? || token.strip.empty?
    raise Error, "#{URL_ENV} is set but #{TOKEN_ENV} is not"
  end

  new(url: url, token: token)
end

.normalize_url(input) ⇒ String

Normalize a user-supplied URL into a full GraphQL endpoint: trim, add http:// if no scheme was given, strip trailing slashes, append /graphql unless the path already ends with it. Mirrors +web/src/lib/connection.ts+'s normalizeUrl exactly.

Parameters:

  • input (String)

Returns:

  • (String)


89
90
91
92
93
94
95
96
97
# File 'lib/bsdkrun/client.rb', line 89

def self.normalize_url(input)
  s = input.to_s.strip
  return s if s.empty?

  s = "http://#{s}" unless s.match?(%r{\Ahttps?://}i)
  s = s.sub(%r{/+\z}, "")
  s = "#{s}/graphql" unless s.match?(%r{/graphql\z}i)
  s
end

.ws_url(http_url) ⇒ String

Derive the websocket endpoint from the HTTP one: http:// -> ws://, https:// -> wss://, trailing slashes on the path stripped, /ws appended. Mirrors +web/src/lib/graphql.ts+'s wsUrl.

Parameters:

  • http_url (String)

Returns:

  • (String)


105
106
107
108
109
110
# File 'lib/bsdkrun/client.rb', line 105

def self.ws_url(http_url)
  uri = URI.parse(http_url)
  uri.scheme = uri.scheme == "https" ? "wss" : "ws"
  uri.path = "#{uri.path.to_s.sub(%r{/+\z}, "")}/ws"
  uri.to_s
end

Instance Method Details

#ai_agentsArray<AiAgent>

The coding agents, and whether each one's sandbox image is built.

Returns:



261
262
263
264
# File 'lib/bsdkrun/client.rb', line 261

def ai_agents
  data = request("{ aiAgents { #{AI_AGENT_FIELDS} } }")
  (data["aiAgents"] || []).map { |a| AiAgent.from_graphql(a) }
end

#ai_remove(agent, keep_home: false) ⇒ CommandResult

Remove an agent's sandboxes, and unless keep_home its saved login too.

Returns:



311
312
313
314
315
316
317
318
# File 'lib/bsdkrun/client.rb', line 311

def ai_remove(agent, keep_home: false)
  run_command_mutation(
    "aiRemove",
    "mutation($agent:String!,$keepHome:Boolean!){ " \
    "aiRemove(agent:$agent, keepHome:$keepHome){ exitCode stdout stderr } }",
    { agent: agent, keepHome: keep_home }
  )
end

#ai_sessionsArray<AiSession>

Agent sandboxes, newest first.

Returns:



268
269
270
271
# File 'lib/bsdkrun/client.rb', line 268

def ai_sessions
  data = request("{ aiSessions { #{AI_SESSION_FIELDS} } }")
  (data["aiSessions"] || []).map { |s| AiSession.from_graphql(s) }
end

#ai_shell_command(agent, machine_id) ⇒ Array<String>

The argv that starts the agent's TUI — pass it to #shell.

Returns:

  • (Array<String>)


290
291
292
293
294
295
296
297
# File 'lib/bsdkrun/client.rb', line 290

def ai_shell_command(agent, machine_id)
  data = request(
    "query($agent:String!,$machineId:String!){ " \
    "aiShellCommand(agent:$agent, machineId:$machineId) }",
    { agent: agent, machineId: machine_id }
  )
  Array(data["aiShellCommand"])
end

#ai_start(agent, cpus: nil, mem: nil, workspace: nil, new: false) ⇒ String

Start (or reuse) a sandbox; returns its machine id.

Parameters:

  • agent (String)

    claude, codex, ...

  • workspace (String, nil) (defaults to: nil)

    a directory on the engine's host.

  • new (Boolean) (defaults to: false)

    boot a second sandbox against the same saved login.

Returns:

  • (String)


279
280
281
282
283
284
285
286
# File 'lib/bsdkrun/client.rb', line 279

def ai_start(agent, cpus: nil, mem: nil, workspace: nil, new: false)
  data = request(
    "mutation($input:AiStartInput!){ aiStart(input:$input) }",
    { input: { agent: agent, cpus: cpus, mem: mem,
               workspace: workspace, new: new } }
  )
  data["aiStart"].to_s
end

#ai_stop(agent) ⇒ CommandResult

Stop an agent's sandboxes. Its saved login survives.

Returns:



301
302
303
304
305
306
307
# File 'lib/bsdkrun/client.rb', line 301

def ai_stop(agent)
  run_command_mutation(
    "aiStop",
    "mutation($agent:String!){ aiStop(agent:$agent){ exitCode stdout stderr } }",
    { agent: agent }
  )
end

#branch(snapshot, name: nil, cpus: nil, mem: nil, ports: [], no_ports: false) ⇒ String

Boot a NEW machine from a snapshot — or from a machine, which is snapshotted first — and return the new machine's id.

The state is cloned, never booted in place, so the source is untouched and one snapshot can be branched any number of times. With no ports, the snapshot's own forwards are inherited, with any host port that is already taken swapped for a free one.

Parameters:

  • snapshot (String)

    snapshot name/id, or a machine id.

  • name (String, nil) (defaults to: nil)
  • cpus (Integer, nil) (defaults to: nil)
  • mem (Integer, nil) (defaults to: nil)
  • ports (Array<String>) (defaults to: [])
  • no_ports (Boolean) (defaults to: false)

Returns:

  • (String)

    the new machine's id.



505
506
507
508
509
510
511
512
# File 'lib/bsdkrun/client.rb', line 505

def branch(snapshot, name: nil, cpus: nil, mem: nil, ports: [], no_ports: false)
  data = request(
    "mutation($input:BranchInput!){ branchSnapshot(input:$input) }",
    { input: { snapshot: snapshot, name: name, cpus: cpus, mem: mem,
               ports: Array(ports), noPorts: no_ports } }
  )
  data["branchSnapshot"].to_s
end

#commit(id, name, description: "") ⇒ CommandResult

Snapshot a machine into a named flavor, like docker commit.

Parameters:

  • id (String)
  • name (String)
  • description (String) (defaults to: "")

Returns:



242
243
244
245
246
247
248
249
# File 'lib/bsdkrun/client.rb', line 242

def commit(id, name, description: "")
  run_command_mutation(
    "commitMachine",
    "mutation($id:String!,$name:String!,$description:String!){ " \
    "commitMachine(id:$id, name:$name, description:$description){ exitCode stdout stderr } }",
    { id: id, name: name, description: description }
  )
end

#docker_container(action, ids) ⇒ CommandResult

start | stop | restart | kill | pause | unpause | rm.

Parameters:

  • action (String)
  • ids (String, Array<String>)

Returns:



387
388
389
390
391
392
393
394
# File 'lib/bsdkrun/client.rb', line 387

def docker_container(action, ids)
  run_command_mutation(
    "dockerContainer",
    "mutation($action:String!,$ids:[String!]!){ " \
    "dockerContainer(action:$action, ids:$ids){ exitCode stdout stderr } }",
    { action: action, ids: Array(ids) }
  )
end

#docker_containers(all: true) ⇒ Array<DockerContainer>

Containers in the engine.

Parameters:

  • all (Boolean) (defaults to: true)

    include stopped ones (default true).

Returns:



342
343
344
345
346
347
348
# File 'lib/bsdkrun/client.rb', line 342

def docker_containers(all: true)
  data = request(
    "query($all:Boolean!){ dockerContainers(all:$all){ #{DOCKER_CONTAINER_FIELDS} } }",
    { all: all }
  )
  (data["dockerContainers"] || []).map { |c| DockerContainer.from_graphql(c) }
end

#docker_logs(id, tail: 200) ⇒ String

One container's logs (stdout+stderr, most recent tail lines).

Parameters:

  • id (String)
  • tail (Integer) (defaults to: 200)

Returns:

  • (String)


400
401
402
403
404
405
406
# File 'lib/bsdkrun/client.rb', line 400

def docker_logs(id, tail: 200)
  data = request(
    "query($id:String!,$tail:Int!){ dockerContainerLogs(id:$id, tail:$tail) }",
    { id: id, tail: tail }
  )
  data["dockerContainerLogs"].to_s
end

#docker_start(cpus: nil, mem: nil, mounts: [], no_home: false, publish_bind: nil, disk_size: nil) ⇒ DockerStatus

Start (or resume) the engine, returning its status once it answers.

Idempotent: the VM has a fixed name, so this resumes the existing one rather than creating a second.

Parameters:

  • cpus (Integer, nil) (defaults to: nil)
  • mem (Integer, nil) (defaults to: nil)
  • mounts (Array<String>) (defaults to: [])

    host dirs to share, PATH or HOST:GUEST.

  • no_home (Boolean) (defaults to: false)

    do not share $HOME (shared by default).

  • publish_bind (String, nil) (defaults to: nil)

    mirror (default) or a fixed address.

  • disk_size (String, nil) (defaults to: nil)

    a dedicated image store, e.g. 60G.

Returns:



362
363
364
365
366
367
368
369
370
371
# File 'lib/bsdkrun/client.rb', line 362

def docker_start(cpus: nil, mem: nil, mounts: [], no_home: false,
                 publish_bind: nil, disk_size: nil)
  data = request(
    "mutation($input:DockerStartInput!){ dockerStart(input:$input){ " \
    "#{DOCKER_STATUS_FIELDS} } }",
    { input: { cpus: cpus, mem: mem, mounts: Array(mounts), noHome: no_home,
               publishBind: publish_bind, diskSize: disk_size } }
  )
  DockerStatus.from_graphql(data["dockerStart"])
end

#docker_statusDockerStatus

Is the Docker engine up, and where is its socket?

Returns:



334
335
336
337
# File 'lib/bsdkrun/client.rb', line 334

def docker_status
  data = request("{ dockerStatus { #{DOCKER_STATUS_FIELDS} } }")
  DockerStatus.from_graphql(data["dockerStatus"])
end

#docker_stopCommandResult

Stop the engine. Images and containers stay on its disk.

Returns:



375
376
377
378
379
380
381
# File 'lib/bsdkrun/client.rb', line 375

def docker_stop
  run_command_mutation(
    "dockerStop",
    "mutation{ dockerStop{ exitCode stdout stderr } }",
    {}
  )
end

#exec(id, command, env: nil) ⇒ ExecResult

Run a command to completion and collect its output. Implemented as the three-operation sequence daemon/README.md documents: openShell (with a command:, so the session runs it instead of a login shell), THEN subscribe to shellOutput (so nothing written in between is lost), THEN wait for an exit code. closeShell runs in an ensure so it happens whether the wait succeeded, failed, or raised.

Parameters:

  • id (String)

    machine id.

  • command (Array<String>)

    argv.

  • env (Hash, Array<String>, nil) (defaults to: nil)

    "K=V" pairs, or a Hash of them.

Returns:



702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
# File 'lib/bsdkrun/client.rb', line 702

def exec(id, command, env: nil)
  data = request(
    "mutation($m:String!,$c:[String!]!,$e:[String!]!,$r:Int!,$k:Int!){ " \
    "openShell(machineId:$m, command:$c, env:$e, rows:$r, cols:$k){ id } }",
    { m: id, c: Array(command), e: env_to_list(env), r: 24, k: 80 }
  )
  session_id = data["openShell"]["id"]

  output = +"".b
  exit_code = nil
  done = Queue.new

  unsubscribe = subscribe(
    "subscription($s:String!){ shellOutput(sessionId:$s){ dataBase64 exitCode } }",
    { s: session_id },
    on_next: lambda { |d|
      payload = d && d["shellOutput"]
      next unless payload

      output << Base64.decode64(payload["dataBase64"]) if payload["dataBase64"]
      unless payload["exitCode"].nil?
        exit_code = payload["exitCode"]
        done << :done
      end
    },
    on_error: ->(e) { done << e },
    on_complete: -> { done << :done }
  )

  begin
    result = done.pop
    raise result if result.is_a?(Exception)
  ensure
    unsubscribe.call
    begin
      request("mutation($s:String!){ closeShell(sessionId:$s) }", { s: session_id })
    rescue GraphQLError
      # closeShell is idempotent server-side; a request failure here
      # (already gone, machine removed, etc.) must not mask the actual
      # exec result/exception above — same as the other SDKs' Client#exec.
      nil
    end
  end

  ExecResult.new(exit_code: exit_code, output: output)
end

#follow_logs(id, follow: true, boot: false) {|data| ... } ⇒ Proc

Stream a machine's console log live.

Parameters:

  • id (String)
  • follow (Boolean) (defaults to: true)
  • boot (Boolean) (defaults to: false)

Yield Parameters:

  • data (String)

    binary-safe decoded chunk.

Returns:

  • (Proc)

    call to stop following.



530
531
532
533
534
535
536
537
538
539
540
541
542
# File 'lib/bsdkrun/client.rb', line 530

def follow_logs(id, follow: true, boot: false, &on_data)
  subscribe(
    "subscription($id:String!,$follow:Boolean!,$boot:Boolean!){ " \
    "machineLogs(id:$id, follow:$follow, boot:$boot){ dataBase64 exitCode } }",
    { id: id, follow: follow, boot: boot },
    on_next: lambda { |data|
      payload = data && data["machineLogs"]
      next unless payload && payload["dataBase64"]

      on_data&.call(Base64.decode64(payload["dataBase64"]))
    }
  )
end

#get(id) ⇒ SandboxInfo?

Returns nil if no such machine exists.

Parameters:

  • id (String)

    id, name, or unique id prefix.

Returns:

  • (SandboxInfo, nil)

    nil if no such machine exists.



188
189
190
191
192
# File 'lib/bsdkrun/client.rb', line 188

def get(id)
  data = request("query($id:String!){ machine(id:$id){ #{MACHINE_FIELDS} } }", { id: id })
  m = data["machine"]
  m && SandboxInfo.from_graphql(m)
end

#list(all: false) ⇒ Array<SandboxInfo>

Parameters:

  • all (Boolean) (defaults to: false)

    include stopped machines too.

Returns:



181
182
183
184
# File 'lib/bsdkrun/client.rb', line 181

def list(all: false)
  data = request("query($all:Boolean!){ machines(all:$all){ #{MACHINE_FIELDS} } }", { all: all })
  (data["machines"] || []).map { |m| SandboxInfo.from_graphql(m) }
end

#logs(id, boot: false) ⇒ String

One-shot console log fetch. Use #follow_logs to stream instead.

Parameters:

  • id (String)
  • boot (Boolean) (defaults to: false)

    bsdkrun's own boot log instead of the guest console.

Returns:

  • (String)


518
519
520
521
522
# File 'lib/bsdkrun/client.rb', line 518

def logs(id, boot: false)
  data = request("query($id:String!,$boot:Boolean!){ machineLogs(id:$id, boot:$boot) }",
                  { id: id, boot: boot })
  data["machineLogs"]
end

#remove(ids, force: false) ⇒ CommandResult

Parameters:

  • ids (String, Array<String>)
  • force (Boolean) (defaults to: false)

Returns:



217
218
219
220
221
222
223
# File 'lib/bsdkrun/client.rb', line 217

def remove(ids, force: false)
  run_command_mutation(
    "removeMachines",
    "mutation($ids:[String!]!,$force:Boolean!){ removeMachines(ids:$ids, force:$force){ exitCode stdout stderr } }",
    { ids: Array(ids), force: force }
  )
end

#remove_snapshots(names) ⇒ CommandResult

Delete snapshots and their data. Machines branched from them are unaffected.

Parameters:

  • names (String, Array<String>)

Returns:



447
448
449
450
451
452
453
# File 'lib/bsdkrun/client.rb', line 447

def remove_snapshots(names)
  run_command_mutation(
    "removeSnapshots",
    "mutation($names:[String!]!){ removeSnapshots(names:$names){ exitCode stdout stderr } }",
    { names: Array(names) }
  )
end

#request(query, variables = {}) ⇒ Hash

Run an arbitrary query or mutation. Every typed method on this class is implemented in terms of this — it exists as a public escape hatch for documents this SDK has no typed wrapper for yet.

Parameters:

  • query (String)

    a GraphQL document.

  • variables (Hash) (defaults to: {})

Returns:

  • (Hash)

    body["data"] (String-keyed, as parsed by JSON.parse).

Raises:

  • (AuthError)

    on HTTP 401, or a GraphQL error with extensions.code == "UNAUTHENTICATED".

  • (GraphQLError)

    on transport failure, a non-JSON response, or any other GraphQL error.



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/bsdkrun/client.rb', line 125

def request(query, variables = {})
  uri = URI.parse(@url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == "https"

  req = Net::HTTP::Post.new(uri.request_uri.empty? ? "/" : uri.request_uri)
  req["content-type"] = "application/json"
  req["authorization"] = "Bearer #{@token}"
  req.body = JSON.generate({ query: query, variables: variables })

  begin
    res = http.request(req)
  rescue StandardError => e
    raise GraphQLError, "cannot reach the bsdkrun daemon at #{@url}#{e.message}"
  end

  raise AuthError if res.code.to_i == 401

  body = begin
    JSON.parse(res.body.to_s)
  rescue JSON::ParserError
    nil
  end
  raise GraphQLError, "the daemon returned a non-JSON response (#{res.code})" if body.nil?

  errors = body["errors"]
  if errors.is_a?(Array) && !errors.empty?
    first = errors.first
    message = first["message"].to_s
    code = first.is_a?(Hash) ? first.dig("extensions", "code") : nil
    raise AuthError, message if code == "UNAUTHENTICATED"
    raise GraphQLError.new(message, code)
  end

  body["data"]
end

#restore(id, snapshot, force: true, backup: true) ⇒ CommandResult

Put a machine's disk state back to one of its snapshots.

force stops the machine first (it holds the very files being replaced); backup snapshots the state being overwritten, which is a CoW clone and therefore free. The machine is left stopped.

Parameters:

  • id (String)
  • snapshot (String)
  • force (Boolean) (defaults to: true)
  • backup (Boolean) (defaults to: true)

Returns:



466
467
468
469
470
471
472
473
474
# File 'lib/bsdkrun/client.rb', line 466

def restore(id, snapshot, force: true, backup: true)
  run_command_mutation(
    "restoreMachine",
    "mutation($id:String!,$snapshot:String!,$force:Boolean!,$backup:Boolean!){ " \
    "restoreMachine(id:$id, snapshot:$snapshot, force:$force, backup:$backup){ " \
    "exitCode stdout stderr } }",
    { id: id, snapshot: snapshot, force: force, backup: backup }
  )
end

#rollback(id, force: true, backup: true) ⇒ CommandResult

Restore a machine to its most recent snapshot.

Parameters:

  • id (String)
  • force (Boolean) (defaults to: true)
  • backup (Boolean) (defaults to: true)

Returns:



481
482
483
484
485
486
487
488
# File 'lib/bsdkrun/client.rb', line 481

def rollback(id, force: true, backup: true)
  run_command_mutation(
    "rollbackMachine",
    "mutation($id:String!,$force:Boolean!,$backup:Boolean!){ " \
    "rollbackMachine(id:$id, force:$force, backup:$backup){ exitCode stdout stderr } }",
    { id: id, force: force, backup: backup }
  )
end

#run_bsd(opts = {}, **kwargs) ⇒ String

Returns the new machine's id.

Returns:

  • (String)

    the new machine's id.



579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/bsdkrun/client.rb', line 579

def run_bsd(opts = {}, **kwargs)
  o = merge_opts(opts, kwargs)
  input = {
    os: bsd_os_enum(o.fetch(:os)),
    version: o[:version],
    cpus: o[:cpus],
    mem: o[:mem],
    net: net_input(o[:net]),
    volume: o[:volume],
    persist: o[:persist] || false,
    force: o[:force] || false,
    firmware: o[:firmware],
    attachDisk: o[:attach_disk] || [],
    diskSize: o[:disk_size],
    repo: o[:repo],
    command: o[:command] || []
  }
  request("mutation($i:RunBsdInput!){ runBsd(input:$i) }", { i: input })["runBsd"]
end

#run_flavor(opts = {}, **kwargs) ⇒ String

Returns the new machine's id.

Returns:

  • (String)

    the new machine's id.



676
677
678
679
680
681
682
683
684
685
686
687
# File 'lib/bsdkrun/client.rb', line 676

def run_flavor(opts = {}, **kwargs)
  o = merge_opts(opts, kwargs)
  input = {
    name: o.fetch(:name),
    cpus: o[:cpus],
    mem: o[:mem],
    ports: o[:ports] || [],
    volume: o[:volume],
    repo: o[:repo]
  }
  request("mutation($i:RunFlavorInput!){ runFlavor(input:$i) }", { i: input })["runFlavor"]
end

#run_linux(opts = {}, **kwargs) ⇒ String

Returns the new machine's id.

Returns:

  • (String)

    the new machine's id.



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/bsdkrun/client.rb', line 556

def run_linux(opts = {}, **kwargs)
  o = merge_opts(opts, kwargs)
  input = {
    image: o.fetch(:image),
    cpus: o[:cpus],
    mem: o[:mem],
    net: net_input(o[:net]),
    volume: o[:volume],
    mounts: o[:mounts] || [],
    attachDisk: o[:attach_disk] || [],
    env: o[:env] || [],
    entrypoint: o[:entrypoint],
    initramfs: o[:initramfs] || false,
    kernel: o[:kernel],
    kernelVersion: o[:kernel_version],
    console: o[:console],
    repo: o[:repo],
    command: o[:command] || []
  }
  request("mutation($i:RunLinuxInput!){ runLinux(input:$i) }", { i: input })["runLinux"]
end

#run_nanos(opts = {}, **kwargs) ⇒ String

No agent (no exec/shell), but it does have a root disk, so persist: is the one disk option it takes.

Returns:

  • (String)

    the new machine's id.



602
603
604
605
606
607
608
609
610
611
612
613
614
# File 'lib/bsdkrun/client.rb', line 602

def run_nanos(opts = {}, **kwargs)
  o = merge_opts(opts, kwargs)
  input = {
    image: o.fetch(:image),
    cpus: o[:cpus],
    mem: o[:mem],
    net: net_input(o[:net]),
    kernel: o[:kernel],
    cmdline: o[:cmdline],
    persist: o[:persist] || false
  }
  request("mutation($i:RunNanosInput!){ runNanos(input:$i) }", { i: input })["runNanos"]
end

#run_osv(opts = {}, **kwargs) ⇒ String

Like Nanos, no agent, but it does have a root filesystem, so the disk options apply — disk: in particular, how an x86_64 guest gets a filesystem (its loader ELF is kernel only).

Returns:

  • (String)

    the new machine's id.



657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/bsdkrun/client.rb', line 657

def run_osv(opts = {}, **kwargs)
  o = merge_opts(opts, kwargs)
  input = {
    image: o.fetch(:image),
    cpus: o[:cpus],
    mem: o[:mem],
    net: net_input(o[:net]),
    cmdline: o[:cmdline],
    disk: o[:disk],
    noDisk: o[:no_disk] || false,
    attachDisk: o[:attach_disk] || [],
    gic: o[:gic],
    persist: o[:persist] || false,
    volume: o[:volume]
  }
  request("mutation($i:RunOsvInput!){ runOsv(input:$i) }", { i: input })["runOsv"]
end

#run_solo5(opts = {}, **kwargs) ⇒ String

Solo5 (MirageOS): runs under the solo5-hvt tender rather than libkrun. The unikernel declares its own network and block devices in its MFT1 manifest note, so only what the host alone can know is asked for — block: backing files (+NAME=FILE+) and the unikernel's own args: (e.g. "--ipv4=10.0.0.2/24"). Like Unikraft, no disk and no agent, so no volume/persist/repo/command fields.

Returns:

  • (String)

    the new machine's id.



640
641
642
643
644
645
646
647
648
649
650
651
# File 'lib/bsdkrun/client.rb', line 640

def run_solo5(opts = {}, **kwargs)
  o = merge_opts(opts, kwargs)
  input = {
    path: o[:path],
    cpus: o[:cpus],
    mem: o[:mem],
    net: net_input(o[:net]),
    block: o[:block] || [],
    args: o[:args] || []
  }
  request("mutation($i:RunSolo5Input!){ runSolo5(input:$i) }", { i: input })["runSolo5"]
end

#run_unikraft(opts = {}, **kwargs) ⇒ String

A unikernel has no disk and no agent, so no volume/persist/repo/command fields — mounts: (virtio-fs shares) is the exception, needing neither.

Returns:

  • (String)

    the new machine's id.



619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/bsdkrun/client.rb', line 619

def run_unikraft(opts = {}, **kwargs)
  o = merge_opts(opts, kwargs)
  input = {
    path: o[:path],
    cpus: o[:cpus],
    mem: o[:mem],
    net: net_input(o[:net]),
    cmdline: o[:cmdline],
    initramfs: o[:initramfs],
    mounts: o[:mounts] || []
  }
  request("mutation($i:RunUnikraftInput!){ runUnikraft(input:$i) }", { i: input })["runUnikraft"]
end

#shell(id, command: nil, env: nil, rows: 24, cols: 80) ⇒ ShellSession

Open a live interactive session. Unlike #exec, this returns immediately with a handle whose ShellSession#on_output / ShellSession#on_exit callbacks fire as output arrives.

Parameters:

  • id (String)

    machine id.

  • command (Array<String>, nil) (defaults to: nil)

    nil opens a login shell.

  • env (Hash, Array<String>, nil) (defaults to: nil)
  • rows (Integer) (defaults to: 24)
  • cols (Integer) (defaults to: 80)

Returns:



759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
# File 'lib/bsdkrun/client.rb', line 759

def shell(id, command: nil, env: nil, rows: 24, cols: 80)
  data = request(
    "mutation($m:String!,$c:[String!]!,$e:[String!]!,$r:Int!,$k:Int!){ " \
    "openShell(machineId:$m, command:$c, env:$e, rows:$r, cols:$k){ id } }",
    { m: id, c: command.nil? ? [] : Array(command), e: env_to_list(env), r: rows, k: cols }
  )
  session_id = data["openShell"]["id"]
  session = ShellSession.new(client: self, id: session_id)

  unsubscribe = subscribe(
    "subscription($s:String!){ shellOutput(sessionId:$s){ dataBase64 exitCode } }",
    { s: session_id },
    on_next: lambda { |d|
      payload = d && d["shellOutput"]
      next unless payload

      session.deliver_output(Base64.decode64(payload["dataBase64"])) if payload["dataBase64"]
      session.deliver_exit(payload["exitCode"]) unless payload["exitCode"].nil?
    },
    on_error: ->(_e) { session.deliver_exit(nil) },
    on_complete: -> {}
  )
  session.unsubscribe = unsubscribe
  session
end

#snapshot(id, name: nil, description: "") ⇒ SnapshotInfo

Capture a machine's disk state.

A BSD guest is powered off first — a mounted UFS cannot be cloned consistently — so the machine is left stopped; #start brings it back.

Parameters:

  • id (String)
  • name (String, nil) (defaults to: nil)

    defaults to <machine>-<n>.

  • description (String) (defaults to: "")

Returns:



434
435
436
437
438
439
440
441
# File 'lib/bsdkrun/client.rb', line 434

def snapshot(id, name: nil, description: "")
  data = request(
    "mutation($id:String!,$name:String,$description:String!){ " \
    "snapshotMachine(id:$id, name:$name, description:$description){ #{SNAPSHOT_FIELDS} } }",
    { id: id, name: name, description: description }
  )
  SnapshotInfo.from_graphql(data["snapshotMachine"])
end

#snapshots(machine: nil) ⇒ Array<SnapshotInfo>

List snapshots, newest first.

Parameters:

  • machine (String, nil) (defaults to: nil)

    only this machine's, when given.

Returns:



417
418
419
420
421
422
423
# File 'lib/bsdkrun/client.rb', line 417

def snapshots(machine: nil)
  data = request(
    "query($machine:String){ snapshots(machine:$machine){ #{SNAPSHOT_FIELDS} } }",
    { machine: machine }
  )
  (data["snapshots"] || []).map { |s| SnapshotInfo.from_graphql(s) }
end

#start(id) ⇒ CommandResult

Parameters:

  • id (String)

Returns:



206
207
208
209
210
211
212
# File 'lib/bsdkrun/client.rb', line 206

def start(id)
  run_command_mutation(
    "startMachine",
    "mutation($id:String!){ startMachine(id:$id){ exitCode stdout stderr } }",
    { id: id }
  )
end

#stop(id) ⇒ CommandResult

Parameters:

  • id (String)

Returns:



196
197
198
199
200
201
202
# File 'lib/bsdkrun/client.rb', line 196

def stop(id)
  run_command_mutation(
    "stopMachine",
    "mutation($id:String!){ stopMachine(id:$id){ exitCode stdout stderr } }",
    { id: id }
  )
end

#subscribe(query, variables = {}, on_next:, on_error: nil, on_complete: nil) ⇒ Proc

Start a subscription over the shared websocket (opened lazily on first use). See WsClient#subscribe for the exact queueing/ack semantics.

Parameters:

  • query (String)
  • variables (Hash) (defaults to: {})
  • on_next (#call)
  • on_error (#call, nil) (defaults to: nil)
  • on_complete (#call, nil) (defaults to: nil)

Returns:

  • (Proc)

    call to unsubscribe.



173
174
175
# File 'lib/bsdkrun/client.rb', line 173

def subscribe(query, variables = {}, on_next:, on_error: nil, on_complete: nil)
  ws.subscribe(query, variables, on_next: on_next, on_error: on_error, on_complete: on_complete)
end

#update(id, cpus: nil, mem: nil) ⇒ CommandResult

Parameters:

  • id (String)
  • cpus (Integer, nil) (defaults to: nil)
  • mem (Integer, nil) (defaults to: nil)

Returns:



229
230
231
232
233
234
235
# File 'lib/bsdkrun/client.rb', line 229

def update(id, cpus: nil, mem: nil)
  run_command_mutation(
    "updateMachine",
    "mutation($id:String!,$cpus:Int,$mem:Int){ updateMachine(id:$id, cpus:$cpus, mem:$mem){ exitCode stdout stderr } }",
    { id: id, cpus: cpus, mem: mem }
  )
end