Blocking IO.gets / IO.getn

Not sure if this is an Erlang or Elixir question. So I have an escript written in Elixir that launches some diagnostic processes on a target VM. The escript then enters a recursive function that accepts user input with IO.gets, which blocks until someone enters something on the keyboard. Now the issue is that I want the escript to accept messages from the diagnostic processes in the VM to do things like change prompt, update the display etc; but as I said it is blocked by IO.gets, so the mailbox will slowly fill up with unread messages.

The solution I found is rather hacky and I’m sure there must be a better way:

##
## This is in the escript
##
defp do_start_work(state) do
  
  ## Some code goes here

  ## This is where the hack happens
  message_proxy()

  ## Get input from stdio
  wait_for_user_events(state)
end

defp wait_for_user_events(%{target_node: target_node} = state) do
    prompt = make_prompt(state)
    case IO.gets(prompt) |> String.trim() do

      "start" ->
        # Sends a message to the process on the target node/VM
        Diagnostic.start_checks(target_node)
        wait_for_user_events(state)

      "reset" ->
        Diagnostic.reset_checks(target_node)
        wait_for_user_events(state)

        ......... snip .......

      message ->
        case handle_mailbox(message) do
           # These are all Erlang/Elixir messages
           :diagnostic_started -> 
               started_work()
               wait_for_user_events(state)

          {:state_update, new_state} ->
              wait_for_user_events(new_state)
   
              ..... snip .....
 
         _ -> 
             wait_for_user_events(state)
     end
   end
end

##
## Gets actual messages from its mailbox
##
defp handle_mailbox("message_waiting_in_mailbox") do
  receive do
      msg -> msg
    after
      2000 -> :ok
    end
end

##
## The hack
##
def message_proxy() do
    main_pid = self()
    spawn(fn ->
      # Other processes send to this registered name
      Process.register(self(), :message_proxy_handler)

      # Find the group leader port that IO.gets is blocked on
      [port] = Process.info(Process.group_leader())[:links] |>  Enum.filter(&is_port(&1))
      message_proxy_loop(port, main_pid)
    end)
  end

  defp message_proxy_loop(port, main_pid) do
    receive do
      message ->
        # Got a message to be proxied, send it to the main process stuck on IO.gets 
        # in wait_for_user_events/1
        send(main_pid, message)
        # Send a unique string to the group_leader port that matches that in handle_mailbox/1
        send(Process.group_leader(), {port, {:data, "message_waiting_in_mailbox\n"}})
        message_proxy_loop(port, main_pid)
    end
  end
``

Can you create a separate process for reading the IO, that exclusively blocks on the IO, and doesn’t do anything else?

I thought that, but I want to do things like change the prompt. It seems to work well, I just feel dirty writing it.

I see. In that case, you can send a message to the IO processes to change its prompt. The message will sit on its inbox but that should not be an issue because the new prompt will only have an effect the next time you call gets anyway.

In any case, if you really want to make it async, the IO message protocol in Erlang is well documented. For example, here is how the Logger.Backend.Console does async_io:

And here is how we created our own “IO device” with StringIO:

So you could build on top of the message protocol your own async IO stuff.

1 Like

Great, thanks