Current section
Files
Jump to
Current section
Files
lib/miosa/exec.ex
defmodule Miosa.Exec do
@moduledoc """
Execute commands and scripts inside a running MIOSA computer.
Both Bash and Python execution are supported. Commands run with the default
user's environment inside the VM.
## Example
{:ok, result} = Miosa.Exec.bash(client, computer_id, "ls -la /home/user")
IO.puts(result.output)
IO.puts("Exit code: \#{result.exit_code}")
{:ok, result} = Miosa.Exec.python(client, computer_id, \"\"\"
import json
data = {"hello": "world"}
print(json.dumps(data))
\"\"\")
"""
alias Miosa.{Client, Types}
@doc """
Executes a Bash command inside the computer.
## Options
* `:timeout` — Execution timeout in milliseconds. Defaults to `30_000`.
* `:working_dir` — Working directory for the command. Defaults to `"/home/user"`.
* `:env` — Map of additional environment variables.
## Returns
`{:ok, result}` with an `ExecResult` containing `:output`, `:stdout`,
`:stderr`, and `:exit_code`. On API error, returns `{:error, reason}`.
"""
@spec bash(Client.t(), String.t(), String.t(), keyword()) ::
Client.result(Types.ExecResult.t())
def bash(%Client{} = client, computer_id, command, opts \\ [])
when is_binary(command) do
body = build_body(%{command: command}, opts)
client
|> Client.post("/computers/#{computer_id}/exec", body)
|> unwrap_result()
end
@doc """
Executes a Python script inside the computer.
The script runs with the system Python 3 interpreter. Common packages
(requests, numpy, pandas, etc.) may be pre-installed depending on the
computer template.
## Options
* `:timeout` — Execution timeout in milliseconds. Defaults to `30_000`.
* `:working_dir` — Working directory. Defaults to `"/home/user"`.
* `:env` — Map of additional environment variables.
"""
@spec python(Client.t(), String.t(), String.t(), keyword()) ::
Client.result(Types.ExecResult.t())
def python(%Client{} = client, computer_id, code, opts \\ [])
when is_binary(code) do
body = build_body(%{code: code}, opts)
client
|> Client.post("/computers/#{computer_id}/exec/python", body)
|> unwrap_result()
end
@doc """
Convenience wrapper: runs bash and raises `Miosa.Error` on failure.
Returns the `ExecResult` directly (not wrapped in `{:ok, _}`).
"""
@spec bash!(Client.t(), String.t(), String.t(), keyword()) :: Types.ExecResult.t()
def bash!(%Client{} = client, computer_id, command, opts \\ []) do
case bash(client, computer_id, command, opts) do
{:ok, result} -> result
{:error, err} -> raise err
end
end
@doc """
Convenience wrapper: runs python and raises `Miosa.Error` on failure.
Returns the `ExecResult` directly (not wrapped in `{:ok, _}`).
"""
@spec python!(Client.t(), String.t(), String.t(), keyword()) :: Types.ExecResult.t()
def python!(%Client{} = client, computer_id, code, opts \\ []) do
case python(client, computer_id, code, opts) do
{:ok, result} -> result
{:error, err} -> raise err
end
end
@doc """
Spawns an interactive command session over WebSocket and returns a
`Miosa.Exec.Command` GenServer PID.
The returned PID represents a live WebSocket connection to the computer.
Use `Miosa.Exec.Command.send_stdin/2`, `close_stdin/1`, `resize/3`, and
`await/2` to interact with the running process.
The GenServer owns the WebSocket connection and terminates it on shutdown.
Callers should always call `Miosa.Exec.Command.await/2` to drain the session
and ensure the underlying connection is cleaned up.
## Options
* `:pty` — Allocate a pseudo-terminal. Required for interactive programs
like shells. Defaults to `false`.
## Example
{:ok, cmd} = Miosa.Exec.spawn(client, computer_id, "bash", pty: true)
:ok = Miosa.Exec.Command.send_stdin(cmd, "echo hello\\n")
:ok = Miosa.Exec.Command.send_stdin(cmd, "exit\\n")
{:ok, 0} = Miosa.Exec.Command.await(cmd, 5_000)
"""
@spec spawn(Client.t(), String.t(), String.t(), keyword()) ::
{:ok, Miosa.Exec.Command.t()} | {:error, term()}
def spawn(%Client{} = client, computer_id, command, opts \\ [])
when is_binary(computer_id) and is_binary(command) do
Miosa.Exec.Command.start_link(
client: client,
computer_id: computer_id,
command: command,
pty: Keyword.get(opts, :pty, false)
)
end
# ---------------------------------------------------------------------------
# Private
# ---------------------------------------------------------------------------
defp build_body(base, opts) do
base
|> maybe_put(:timeout, Keyword.get(opts, :timeout))
|> maybe_put(:working_dir, Keyword.get(opts, :working_dir))
|> maybe_put(:env, Keyword.get(opts, :env))
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp unwrap_result({:ok, body}) do
result = body |> get_resource() |> Types.ExecResult.from_map()
{:ok, result}
end
defp unwrap_result({:error, _} = err), do: err
defp get_resource(%{"data" => data}) when is_map(data), do: data
defp get_resource(%{"result" => data}) when is_map(data), do: data
defp get_resource(map) when is_map(map), do: map
end