Current section
Files
Jump to
Current section
Files
lib/pi/project_eval.ex
defmodule Pi.ProjectEval do
@moduledoc "Runs eval snippets in a separate target-project VM without loading pi_bridge there."
alias Pi.Protocol.Tool.Eval, as: EvalPayload
alias Pi.Protocol.Tool.OutputPart
@marker "__PI_ELIXIR_EVAL_RESULT__"
def run(code, opts \\ []) when is_binary(code) do
case run_external(code, opts) do
{:ok, %{text: text}} -> {:ok, text}
{:error, reason} -> {:error, reason}
end
end
def run_structured(code, opts \\ []) when is_binary(code) do
case run_external(code, opts) do
{:ok, %{text: text, io: io, success?: true}} ->
parts =
[]
|> maybe_io_part(io)
|> Kernel.++([
OutputPart.inspect(text,
language: :elixir,
data: Pi.Syntax.metadata(text, language: :elixir)
)
])
{:ok, %EvalPayload{io: io, result: text, text: text, parts: parts}}
{:ok, %{text: text, io: io, success?: false}} ->
parts = [] |> maybe_io_part(io) |> Kernel.++([OutputPart.error(text)])
{:error, %EvalPayload{io: io, error: text, text: text, parts: parts}}
{:error, reason} ->
{:error, reason}
end
end
def available? do
project_cwd() not in [nil, ""]
end
defp run_external(code, opts) do
cwd = project_cwd()
timeout = Keyword.get(opts, :timeout, 30_000)
try do
if is_binary(cwd) and File.exists?(Path.join(cwd, "mix.exs")) do
encoded = Base.encode64(code)
wrapper = wrapper(encoded)
case System.cmd("mix", ["run", "-e", wrapper],
cd: cwd,
stderr_to_stdout: true,
env: mix_env()
) do
{output, 0} -> decode_output(output)
{output, code} -> {:error, "mix run exited with code #{code}\n\n#{output}"}
end
else
{:error, "No target Mix project configured for isolated project eval."}
end
catch
:exit, {:timeout, _} -> {:error, "Evaluation timed out after #{timeout}ms"}
end
end
defp project_cwd, do: System.get_env("PI_ELIXIR_PROJECT_CWD")
defp mix_env do
[
{"MIX_ENV", System.get_env("MIX_ENV") || "dev"},
{"PI_ELIXIR_PROJECT_EVAL", "1"}
]
end
defp decode_output(output) do
case String.split(output, @marker, parts: 2) do
[_before, encoded] ->
encoded
|> String.trim()
|> Base.decode64!()
|> :erlang.binary_to_term([:safe])
_ ->
{:error, output}
end
rescue
exception in [ArgumentError] ->
{:error,
"Could not decode project eval result: #{Exception.message(exception)}\n\n#{output}"}
end
defp wrapper(encoded) do
escaped = inspect(encoded)
marker = inspect(@marker)
"""
code = Base.decode64!(#{escaped})
old_gl = Process.group_leader()
{:ok, capture} = StringIO.open("")
Process.group_leader(self(), capture)
result =
try do
{value, _binding} = Code.eval_string(code, [arguments: []], __ENV__)
{:ok, inspect(value, pretty: true, limit: 50, printable_limit: 4096), ""}
catch
kind, reason ->
{:error, Exception.format(kind, reason, __STACKTRACE__), ""}
after
Process.group_leader(self(), old_gl)
end
{_input, io} = StringIO.contents(capture)
payload =
case result do
{:ok, text, _} -> {:ok, %{success?: true, text: text, io: io}}
{:error, text, _} -> {:ok, %{success?: false, text: text, io: io}}
end
IO.puts(#{marker} <> Base.encode64(:erlang.term_to_binary(payload)))
"""
end
defp maybe_io_part(parts, ""), do: parts
defp maybe_io_part(parts, io), do: parts ++ [OutputPart.text(io)]
end