IO.read(:stdio, num) broken in 1.2.6?

I observe a strangeness in an escript I’m writing.

I’m trying to communicate with it over a port with a 2-byte length prefix.

So, I do:

IO.read(:stdio, 2)

But as result I do not get a binary of length 2, I get the whole 58 byte of the encoded term I just sent. Further reads on the device block as it basically returned the complete input from the stdin in the last call.

Same result with IO.getn as well.

While I can work with the input I get here it is decidedly weird and not as expected.

I do not have a 1.2 at hands but only tested with 1.3.4, and it worked as expected:

iex(1)> IO.read(:stdio, 2)
asd
"as"

But the implementation on elixir-side is the same in you version and mine, the only difference is that newer elixir is guarded by is_integer and >= 0 while older ones only compare to 0 (and thus theoretically allow floats).

In any case, this function only wraps :io.get_chars/3, so maybe there is not an issue in elixir but in your used erlang version?

Hello, NobbZ.

I tested it with an Erlang shell. Problem doesn’t exist.
I tested it with an iex shell. Problem doesn’t exist.

BUT: When I pipe into the program with a port from elixir side, I get this behavior.

So, in this case, I have one node starting an elixir script as port (as testing dummy standin for the more complex C port). The application started as port does read correctly if doing so from a Linux shell with piped input like echo "bla" | ./my_escript, but when the port from another elixir application transmits data IO.read/2 returns more bytes than requested.

I’m stumped.

When piping into a BEAM instance you need to make sure that your process actually waits for input before you send the data. Done h expected things might happen…

(sleep 1; echo foo) | ./your_script should be sufficient to prove or disprove Mr assumption.

I will try that out tomorrow. I have no access tonight.

I successfully stimulated my program like this:

[code]defmodule Tester do

def main(_args) do
port = Port.open({:spawn, “…/tested/tested”}, [:binary, :use_stdio, packet: 2])

Port.command(port, {:command1, "127.0.0.1", "7777", "127.0.0.1", "8888"} |> :erlang.term_to_binary)
receive do
    {^port, {:data, result}} ->
    IO.puts("Elixir got: #{inspect (result |> :erlang.binary_to_term)}")
end

Port.command(port, {:command2, 9999} |> :erlang.term_to_binary)
receive do
    {^port, {:data, result}} ->
    IO.puts("Elixir got: #{inspect (result |> :erlang.binary_to_term)}")
end

Port.close(port)

end

end[/code]

This works in spite of the IO.read(2) in the code…

I will try to implement the test program a bit differently so that I can send have that sleep on the input pipe. It expects 2 byte header right now.

1 Like

$ (sleep 1; echo foo) | ./tested
syscom_receiver: waiting for new input
syscom_receiver: received “foo\n”
syscom_receiver: received unknown command “foo\n”
syscom_receiver: waiting for new input
syscom_receiver: received :eof
syscom_receiver: exiting
$ echo foo | ./tested
syscom_receiver: waiting for new input
syscom_receiver: received “foo\n”
syscom_receiver: received unknown command “foo\n”
syscom_receiver: waiting for new input
syscom_receiver: received :eof
syscom_receiver: exiting

IO.read(:line) is being used for reading input on this one.

1 Like

It gets even weirder.

I made an Erlang program for testing the port weirdness:

[code]#!/usr/bin/env escript

main(_args) ->
Port = open_port({spawn, “./tested.esh”}, [binary, use_stdio, {packet, 2}]),

Command1 = {init, << "127.0.0.1" >>, << "7777" >>, << "127.0.0.1" >>, << "8888" >>},
port_command(Port, term_to_binary(Command1)),
receive
    {Port, {data, Result}} -> io:format("~p~n", [binary_to_term(Result)])
end,

Command2 = {eu_user_register, 5555},
port_command(Port, term_to_binary(Command2)),
receive
    {Port, {data, Result2}} -> io:format("~p~n", [binary_to_term(Result2)])
end,

port_close(Port).

[/code]

I then proceeded to test it against first an Erlang script doing what is expected:

[code]#!/usr/bin/env escript

main(_args) ->
loop().

loop() ->
Input = io:get_chars(standard_io, “”, 2),
case Input of
eof -> ok;
_any ->
<< Length:16 >> = iolist_to_binary(Input),
Command = io:get_chars(standard_io, “”, Length),
Decoded = binary_to_term(iolist_to_binary(Command)),
io:format(standard_error, “got: ~p~n”, [Decoded]),
Response = term_to_binary(ok),
ResponseLen = byte_size(Response),
BinResponse = << ResponseLen:16, Response/binary >>,
io:put_chars(standard_io, binary_to_list(BinResponse)),
loop()
end.[/code]

I then test it against my elixir program that has IO.read(2) (and no other reads on :stdio). It works as well.

When using the elixir escript, IO.read/1and :io.get_chars/2both behave incorrect.

In the Erlang scripts based on the same OTP, in the iex shell, and the erl shell, the respective functions work as expected.

The Erlang samples prove that if Erlang is driving the port everything works fine. If Erlang is the application running inside the port the IO works fine as well, so it is not the Beam VM and whether it is being piped into.

This issue is strictly elixir. Not Beam VM. Not the underlying OTP.

1 Like

PS - according to the website for the 1.2.6 release, IO.read/2 is implemented as follows:

def read(device, count) when count >= 0 do :io.get_chars(map_dev(device), '', count) end

The outcome of :io.get_chars/3 is an iolist, not a binary. But IO.read/2 seems to return binaries (with “” quotes), not iolists. When I apply << >> operator for matching on the result, or when I apply binary_part/3 both work, and is_binary/1 returns true. But the Erlang function does not return a binary.

Is there an implicit conversion of iolists involved? Because the code I see doesn’t show any conversion.

1 Like

Currently there seems to be an DNS error, and I am not able to look it up correctly, but IIRC io:get_chars/3 does either return a list of bytes for devices which are raw or a binary with unicode data if the device is unicode. This might be were the magic happens that gives you the binary back.

But I can’t help you with that other problem anymore, except to ask if you can reproduce the behaviour with other combinations of OTP/Elixir versions and I think it might help if you could not only give that small programs here, but a git repository which exactly shows the various test programs you tried in different subfolders.

1 Like

Or rather you are probably just getting it as a binary because the remote program sent that information within a single flush. As I recall :io.get_chars returns (in all cases that I can recall when in raw mode) a list of binaries, or just a binary if no others flushed between calls.

Thank you, OvermindDL1.

I don’t get the same behavior from my escript in pure Erlang when I run it as the port application within my Elixir application.

The io:get_chars from within the Erlang escript also returns then an iolist even though the application driving the port does so in binary mode with the same parameters as in the other case…

erlang documentation for stdlib is almost always available via

erl --man io

on unix installs of erlang. On Windows the web page versions are generally installed.

I was on a mobile and didn’t even had erlang installed.

Bbense noreply@elixirforum.com schrieb am So., 27. Nov. 2016 20:29: