Skip to content

Instantly share code, notes, and snippets.

@sasa1977
Last active November 14, 2024 22:11
Show Gist options
  • Save sasa1977/3bf1753675a77f18805a to your computer and use it in GitHub Desktop.
Save sasa1977/3bf1753675a77f18805a to your computer and use it in GitHub Desktop.
defmodule RubyServer do
use GenServer
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, nil, opts)
end
def cast(server, cmd) do
GenServer.cast(server, {:cast, cmd})
end
def call(server, cmd) do
GenServer.call(server, {:call, cmd})
end
def init(_) do
{:ok, %{port: start_port, next_id: 1, awaiting: %{}}}
end
def handle_cast({:cast, cmd}, state) do
{_id, state} = send_request(state, {:cast, cmd})
{:noreply, state}
end
def handle_call({:call, cmd}, from, state) do
{id, state} = send_request(state, {:call, cmd})
{:noreply, %{state | awaiting: Map.put(state.awaiting, id, from)}}
end
def handle_info({port, {:data, response}}, %{port: port} = state) do
{id, result} = :erlang.binary_to_term(response)
case state.awaiting[id] do
nil -> {:noreply, state}
caller ->
GenServer.reply(caller, result)
{:noreply, %{state | awaiting: Map.delete(state.awaiting, id)}}
end
end
def handle_info({port, {:exit_status, status}}, %{port: port}) do
:erlang.error({:port_exit, status})
end
def handle_info(_, state), do: {:noreply, state}
defp start_port do
Port.open({:spawn, code}, [:binary, {:packet, 4}, :nouse_stdio, :exit_status])
end
defp send_request(state, command) do
id = state.next_id
Port.command(state.port, :erlang.term_to_binary({id, command}))
{id, %{state | next_id: id + 1}}
end
defp code do
~S"""
ruby -e '
require "bundler"
require "erlang/etf"
require "stringio"
@input = IO.new(3)
@output = IO.new(4)
@output.sync = true
def receive_input
encoded_length = @input.read(4)
return nil unless encoded_length
length = encoded_length.unpack("N").first
@request_id, cmd = Erlang.binary_to_term(@input.read(length))
cmd
end
def send_response(value)
response = Erlang.term_to_binary(Erlang::Tuple[@request_id, value])
@output.write([response.bytesize].pack("N"))
@output.write(response)
true
end
context = binding
while (cmd = receive_input) do
if [:call, :cast].include?(cmd[0])
puts "Ruby: #{cmd[1]}\r"
res = eval(cmd[1], context)
puts "Ruby: #{res.inspect}\n\r"
send_response(res) if cmd[0] == :call
end
end
puts "Ruby: exiting"
'
"""
end
end
{:ok, server} = RubyServer.start_link
RubyServer.cast(server, "a = 1")
RubyServer.cast(server, ~S"""
while a < 10 do
a *= 3
end
""")
RubyServer.call(server, "Erlang::Tuple[:response, a]")
|> IO.inspect
{:ok, another_server} = RubyServer.start_link
RubyServer.cast(another_server, "a = 42")
RubyServer.call(another_server, "Erlang::Tuple[:response, a]")
|> IO.inspect
RubyServer.call(server, "Erlang::Tuple[:response, a]")
|> IO.inspect
RubyServer.call(server, "1/0")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment