lib/mix/nerves/shell.ex

defmodule Mix.Nerves.Shell do
  def open(command, initial_input \\ []) do
    # We need to get raw binary access to the stdout file descriptor
    # so we can directly pass through control characters output by the command
    stdout_port = Port.open({:fd, 0, 1}, [:binary, :eof, :stream, :out])

    # We use the tty_sl driver for input because it handles tty geometry and
    # streaming mode.
    stdin_port = Port.open({:spawn, "tty_sl -c -e"}, [:binary, :eof, :stream, :in])
    _ = Application.stop(:logger)
    # We run the command through the script command to emulate a pty
    cmd =
      "script -q /dev/null " <>
        case Nerves.Env.host_os() do
          "linux" ->
            "-c \"#{command}\""

          _ ->
            "#{command}"
        end

    cmd_port =
      Port.open({:spawn, cmd}, [
        :binary,
        :eof,
        :stream,
        :stderr_to_stdout,
        {:env, [{'PATH', Mix.Nerves.Utils.sanitize_path() |> to_charlist()}]}
      ])

    # Tell the script command about the terminal dimensions
    {w, h} = get_tty_geometry(stdin_port)

    Port.command(cmd_port, """
    stty sane rows #{h} cols #{w}; stty -echo
    export PS1=""; export PS2=""
    start() {
    echo -e "\\e[25F\\e[0J\\e[1;7m\n  Preparing Nerves Shell  \\e[0m"
    echo -e "\\e]0;Nerves Shell\\a"
    export PS1="\\e[1;7m Nerves \\e[0;1m \\W > \\e[0m"
    export PS2="\\e[1;7m Nerves \\e[0;1m \\W ..\\e[0m"
    #{Enum.join(initial_input, "\n")}
    stty echo
    }; start
    """)

    shell_loop(stdin_port, stdout_port, cmd_port)
  end

  defp shell_loop(stdin_port, stdout_port, cmd_port) do
    receive do
      # Route input from stdin to the command port
      {^stdin_port, {:data, data}} ->
        Port.command(cmd_port, data)
        shell_loop(stdin_port, stdout_port, cmd_port)

      # Route output from the command port to stdout
      {^cmd_port, {:data, data}} ->
        Port.command(stdout_port, data)
        shell_loop(stdin_port, stdout_port, cmd_port)

      # If any of the ports get closed, break out of the loop
      {_port, :eof} ->
        :ok

      # Ignore other messages
      _message ->
        shell_loop(stdin_port, stdout_port, cmd_port)
    end
  end

  # Starting in OTP 21.3.0, the CTRL_OP_GET_WINSIZE changes to be
  #
  # define(ERTS_TTYSL_DRV_CONTROL_MAGIC_NUMBER, 16#018b0900).
  # define(CTRL_OP_GET_WINSIZE, (100 + ?ERTS_TTYSL_DRV_CONTROL_MAGIC_NUMBER)).
  #
  # See https://github.com/erlang/otp/commit/ad5822c6b1401111bbdbc5e77fe097a3f1b2b3cb

  @erts_ttysl_drv_control_magic_number 0x018B0900
  @ctrl_op_get_winsize 100
  @ctrl_op_get_winsize_otp_21_3 @ctrl_op_get_winsize + @erts_ttysl_drv_control_magic_number

  defp get_tty_geometry(tty_port) do
    geometry =
      try do
        :erlang.port_control(tty_port, @ctrl_op_get_winsize, [])
      rescue
        _e in ArgumentError ->
          :erlang.port_control(tty_port, @ctrl_op_get_winsize_otp_21_3, [])
      end
      |> :erlang.list_to_binary()

    <<w::native-integer-size(32), h::native-integer-size(32)>> = geometry
    {w, h}
  end
end