Current section

Files

Jump to
isabelle_elixir lib isabelle_client raw.ex
Raw

lib/isabelle_client/raw.ex

defmodule IsabelleClient.Raw do
@moduledoc """
Raw-socket Isabelle client.
This profile exposes the TCP socket and leaves session ids and asynchronous
task waiting explicit. It is useful for protocol-level control and for
understanding Isabelle server messages directly.
"""
alias IsabelleClient.Arguments
alias IsabelleClient.Protocol
alias IsabelleClient.Protocol.Response
alias IsabelleClient.Server
alias IsabelleClient.Session
alias IsabelleClient.Task
@default_host "127.0.0.1"
@default_port 9999
@timeout 30_000
@doc "Starts a local resident Isabelle server via `isabelle server`."
def new_server(name \\ "elixir", port \\ @default_port), do: Server.start(name, port)
@doc "Lists local resident Isabelle servers known to Isabelle."
def list_servers, do: Server.list()
@doc "Force-kills a local resident Isabelle server by name."
def kill_server(name), do: Server.kill(name)
@doc """
Connects to an Isabelle server and returns a raw TCP socket.
Use the server password plus host/port from `new_server/2`, `list_servers/0`,
or another Isabelle server process reachable on the network.
"""
def connect(password, host \\ @default_host, port \\ @default_port, timeout \\ @timeout) do
with {:ok, socket} <-
:gen_tcp.connect(
to_charlist(host),
port,
[:binary, active: false, nodelay: true],
timeout
),
:ok <- Protocol.send(socket, Protocol.line_message(password)),
{:ok, %Response{type: :ok}} <- Protocol.recv(socket, timeout) do
{:ok, socket}
else
{:ok, response} -> {:error, {:authentication_failed, response}}
{:error, _} = error -> error
end
end
@doc "Closes a raw TCP socket."
def close(socket) when is_port(socket), do: :gen_tcp.close(socket)
@doc "Receives one framed Isabelle server response from a raw socket."
def recv(socket, timeout \\ @timeout) when is_port(socket), do: Protocol.recv(socket, timeout)
@doc "Runs a synchronous Isabelle command and returns `{:ok, body}` or `{:error, body}`."
def command(socket, name, arg \\ nil, timeout \\ @timeout) when is_port(socket) do
with :ok <- Protocol.send(socket, Protocol.command(name, normalize_arg(arg))),
{:ok, response} <- recv_command_response(socket, deadline(timeout)) do
Protocol.ok_body(response)
end
end
@doc "Round-trips a JSON value through Isabelle's `echo` command."
def echo(socket, value), do: command(socket, "echo", value)
@doc "Returns the server command names supported by Isabelle."
def help(socket), do: command(socket, "help")
@doc "Asks the Isabelle server process to shut down."
def shutdown_server(socket), do: command(socket, "shutdown")
@doc "Requests cancellation of an Isabelle asynchronous task by task id."
def cancel_task(socket, task_id) when is_port(socket),
do: command(socket, "cancel", %{"task" => task_id}, @timeout)
@doc """
Sends an async command and returns a running `%IsabelleClient.Task{}`.
Use `await_task/3` to collect later `NOTE` messages and the final
`FINISHED` or `FAILED` response.
Pending `NOTE` frames from earlier tasks are skipped while waiting for the
command acknowledgement.
"""
def async_command(socket, name, arg, timeout \\ @timeout) when is_port(socket) do
with :ok <- Protocol.send(socket, Protocol.command(name, normalize_arg(arg))),
{:ok, response} <- recv_command_response(socket, deadline(timeout)),
{:ok, task_id} <- Protocol.task_id(response) do
{:ok, Task.new(task_id)}
end
end
@doc """
Waits for an asynchronous task to finish or fail.
Returns `{:ok, task}` for `FINISHED` and `{:error, task}` for `FAILED`.
Task notes received while waiting are stored in `task.notes`.
"""
def await_task(socket, task_or_id, timeout \\ :infinity)
def await_task(socket, %Task{id: id} = task, timeout) when is_port(socket) do
await_task(socket, id, timeout, task)
end
def await_task(socket, task_id, timeout) when is_port(socket) and is_binary(task_id) do
await_task(socket, task_id, timeout, Task.new(task_id))
end
defp await_task(socket, task_id, timeout, task) when is_binary(task_id) do
do_await_task(socket, task, deadline(timeout))
end
@doc "Starts a `session_build` task and returns its task handle."
def build_session(socket, args, timeout \\ @timeout) when is_port(socket),
do: async_command(socket, "session_build", args, timeout)
@doc """
Starts a `session_start` task and returns its task handle.
Arguments are forwarded to Isabelle. `session_start` accepts `session_build`
arguments (`:session`, `:preferences`, `:options`, `:dirs`,
`:include_sessions`, `:verbose`) plus `:print_mode`.
"""
def start_session(socket, args, timeout \\ @timeout) when is_port(socket),
do: async_command(socket, "session_start", args, timeout)
@doc "Starts a `session_stop` task for a session struct or session id."
def stop_session(socket, session_or_id, timeout \\ @timeout)
def stop_session(socket, %Session{id: session_id}, timeout) when is_port(socket),
do: stop_session(socket, session_id, timeout)
def stop_session(socket, session_id, timeout)
when is_port(socket) and is_binary(session_id),
do: async_command(socket, "session_stop", %{"session_id" => session_id}, timeout)
@doc """
Starts a `use_theories` task and returns its task handle.
Arguments are forwarded to Isabelle and may include `:session_id`,
`:theories`, `:master_dir`, `:pretty_margin`, `:unicode_symbols`,
`:export_pattern`, `:check_delay`, `:check_limit`, `:watchdog_timeout`, and
`:nodes_status_delay`.
"""
def use_theories(socket, args, timeout \\ @timeout) when is_port(socket),
do: async_command(socket, "use_theories", args, timeout)
@doc "Runs Isabelle's synchronous `purge_theories` command."
def purge_theories(socket, args, timeout \\ @timeout) when is_port(socket),
do: command(socket, "purge_theories", args, timeout)
@doc "Alias for `await_task/3`."
def poll_status(socket, task_or_id, timeout \\ :infinity),
do: await_task(socket, task_or_id, timeout)
defp do_await_task(socket, %Task{id: id, notes: notes} = task, deadline) do
with {:ok, response} <- Protocol.recv(socket, remaining(deadline)) do
response_task = task_id(response.body)
cond do
response.type == :note and response_task in [nil, id] ->
do_await_task(socket, %{task | notes: [response.body | notes]}, deadline)
response.type == :finished and response_task == id ->
{:ok, %{task | status: :finished, result: response.body, notes: Enum.reverse(notes)}}
response.type == :failed and response_task == id ->
{:error, %{task | status: :failed, result: response.body, notes: Enum.reverse(notes)}}
true ->
do_await_task(socket, task, deadline)
end
end
end
defp task_id(%{"task" => id}), do: id
defp task_id(_), do: nil
defp recv_command_response(socket, deadline) do
with {:ok, response} <- Protocol.recv(socket, remaining(deadline)) do
if response.type == :note do
recv_command_response(socket, deadline)
else
{:ok, response}
end
end
end
defp normalize_arg(nil), do: nil
defp normalize_arg(arg), do: Arguments.normalize(arg)
defp deadline(:infinity), do: :infinity
defp deadline(timeout), do: System.monotonic_time(:millisecond) + timeout
defp remaining(:infinity), do: :infinity
defp remaining(deadline) do
max(deadline - System.monotonic_time(:millisecond), 0)
end
end