Description
Describe the bug
Since OTP-28, it is no longer possible to spawn an erlang process as a child process on Windows, as long as standard error is inherited. It seems like the new tty tries to set some console mode flags on the handles, which are not valid for this call when the process is run as a child process.
To Reproduce
Run the following code as escript echo.erl
on Windows, using OTP-28:
`echo.erl`
-module(echo).
-export([main/0, main/1]).
-mode(compile).
main() -> main([]).
main([]) -> parent();
main(["child"]) -> child().
parent() ->
Escript = os:find_executable("escript"),
Port = open_port({spawn_executable, Escript},
[{args, [escript:script_name(), "child"]}, {line, 1024}, hide]),
port_command(Port, "hello world\n"),
port_command(Port, []),
parent_loop(Port).
parent_loop(Port) ->
receive
{Port, {data, {eol, Response}}} ->
io:format("Parent received: ~s~n", [Response]),
parent_loop(Port);
{Port, {exit_status, Status}} ->
io:format("Child exited with status: ~p~n", [Status]),
port_close(Port);
{Port, closed} ->
io:format("Port closed~n")
after 5000 ->
io:format("Timeout - closing port~n"),
port_close(Port)
end.
child() ->
case io:get_line("") of
eof -> ok;
Line ->
io:format("~s~n", [string:uppercase(string:trim(Line))]),
child()
end.
Expected behaviour
The parent process is able to communicate with the child process using stdin and stdout, while stderr is inherited and forwarded to the console. The child process does not crash on startup. The behaviour is the same as using OTP-27.
Observed behaviour
The child process crashes with the following crash report:
Crash report
=ERROR REPORT==== 24-May-2025::13:32:42.490000 ===
Error in process <0.52.0> with exit value:
{{case_clause,{error,{'SetConsoleMode','The handle is invalid.\r\n'}}},
[{prim_tty,init_term,1,[{file,"prim_tty.erl"},{line,283}]},
{standard_error,server,0,[{file,"standard_error.erl"},{line,72}]}]}
=SUPERVISOR REPORT==== 24-May-2025::13:32:42.490000 ===
supervisor: {local,standard_error_sup}
errorContext: child_terminated
reason: {{case_clause,
{error,{'SetConsoleMode','The handle is invalid.\r\n'}}},
[{prim_tty,init_term,1,[{file,"prim_tty.erl"},{line,283}]},
{standard_error,server,0,
[{file,"standard_error.erl"},{line,72}]}]}
offender: [{pid,<0.52.0>},{mod,standard_error}]
=ERROR REPORT==== 24-May-2025::13:32:42.504000 ===
** Generic server standard_error_sup terminating
** Last message in was {'EXIT',<0.52.0>,
{{case_clause,
{error,
{'SetConsoleMode','The handle is invalid.\r\n'}}},
[{prim_tty,init_term,1,
[{file,"prim_tty.erl"},{line,283}]},
{standard_error,server,0,
[{file,"standard_error.erl"},{line,72}]}]}}
** When Server state == {state,standard_error,undefined,<0.52.0>,
{local,standard_error_sup}}
** Reason for termination ==
** {{case_clause,{error,{'SetConsoleMode','The handle is invalid.\r\n'}}},
[{prim_tty,init_term,1,[{file,"prim_tty.erl"},{line,283}]},
{standard_error,server,0,[{file,"standard_error.erl"},{line,72}]}]}
=CRASH REPORT==== 24-May-2025::13:32:42.504000 ===
crasher:
initial call: supervisor_bridge:standard_error/1
pid: <0.51.0>
registered_name: standard_error_sup
exception exit: {{case_clause,
{error,
{'SetConsoleMode','The handle is invalid.\r\n'}}},
[{prim_tty,init_term,1,
[{file,"prim_tty.erl"},{line,283}]},
{standard_error,server,0,
[{file,"standard_error.erl"},{line,72}]}]}
in function gen_server:handle_common_reply/5 (gen_server.erl:2562)
ancestors: [kernel_sup,<0.47.0>]
message_queue_len: 0
messages: []
links: [<0.49.0>]
dictionary: []
trap_exit: true
status: running
heap_size: 610
stack_size: 29
reductions: 792
neighbours:
=ERROR REPORT==== 24-May-2025::13:32:42.601000 ===
** State machine user_drv terminating
** When server state = {undefined,undefined}
** Reason for termination = error:{badmatch,{error,arguments}}
** Callback modules = [user_drv]
** Callback mode = state_functions
** Stacktrace =
** [{user_drv,init_standard_error,2,[{file,"user_drv.erl"},{line,212}]},
{user_drv,init,1,[{file,"user_drv.erl"},{line,188}]},
{gen_statem,init_it,6,[{file,"gen_statem.erl"},{line,3323}]},
{proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,333}]}]
=CRASH REPORT==== 24-May-2025::13:32:42.601000 ===
crasher:
initial call: user_drv:init/1
pid: <0.69.0>
registered_name: []
exception error: no match of right hand side value {error,arguments}
in function user_drv:init_standard_error/2 (user_drv.erl:212)
in call from user_drv:init/1 (user_drv.erl:188)
in call from gen_statem:init_it/6 (gen_statem.erl:3323)
ancestors: [<0.68.0>,kernel_sup,<0.47.0>]
message_queue_len: 0
messages: []
links: [<0.70.0>,<0.71.0>]
dictionary: []
trap_exit: true
status: running
heap_size: 610
stack_size: 29
reductions: 511
neighbours:
neighbour:
pid: <0.71.0>
registered_name: user_drv_reader
initial call: prim_tty:reader/1
current_function: {prim_tty,reader_loop,2}
ancestors: [user_drv,<0.68.0>,kernel_sup,<0.47.0>]
message_queue_len: 0
links: [<0.69.0>]
trap_exit: false
status: waiting
heap_size: 233
stack_size: 11
reductions: 36
current_stacktrace: [{prim_tty,reader_loop,2,[{file,"prim_tty.erl"},{line,553}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,333}]}]
neighbour:
pid: <0.70.0>
registered_name: user_drv_writer
initial call: prim_tty:writer/1
current_function: {prim_tty,writer_loop,2}
ancestors: [user_drv,<0.68.0>,kernel_sup,<0.47.0>]
message_queue_len: 0
links: [<0.69.0>]
trap_exit: false
status: waiting
heap_size: 233
stack_size: 7
reductions: 24
current_stacktrace: [{prim_tty,writer_loop,2,[{file,"prim_tty.erl"},{line,652}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,333}]}]
Affected versions
OTP-28.0
Additional context
We've ran into this issue in the Gleam compiler (see gleam-lang/gleam#4619) - the compiler uses Rusts std::process
module to spawn an escript as a child process to handle compilation of Erlang and Elixir files to bytecode. Similar to the escript above, it tries to capture stdin and stdout, while "inheriting" the stderr handle to pass all Erlang and Elixir compilation errors to the user.
In Rust, using a pipe instead of inheriting stderr and spawning a thread that continuously reads from it fixes the issue; in Erlang, a workaround is to pass stderr_to_stdout
. I've also tried some combinations of -noshell
and -noinput
, which did not seem to make a difference.