Skip to content

Commit 11ccd39

Browse files
committed
Use SSH in the shell to support user ~/.ssh/config
This also forwards STDIN back to the mix task so that if SSH needs a passcode for the certificate or authentication, then the user can input it
1 parent 9ded72b commit 11ccd39

File tree

1 file changed

+77
-81
lines changed

1 file changed

+77
-81
lines changed

lib/mix/tasks/upload.ex

Lines changed: 77 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -65,39 +65,88 @@ defmodule Mix.Tasks.Upload do
6565
Uploading to #{ip}...
6666
""")
6767

68+
user = Process.whereis(:user)
69+
Process.unregister(:user)
70+
71+
# Take over STDIN in case SSH requires inputting password
72+
stdin_port = Port.open({:spawn, "tty_sl -c -e"}, [:binary, :eof, :stream, :in])
73+
_ = Application.stop(:logger)
74+
75+
shell = System.get_env("SHELL")
76+
6877
# Options:
6978
#
7079
# ConnectTimeout - don't wait forever to connect
71-
# PreferredAuthentications=publickey - since keyboard interactivity doesn't
72-
# work, don't try password entry options.
73-
# -T - No pseudoterminals since they're not needed for firmware updates
74-
opts = [
75-
:stream,
76-
:binary,
77-
:exit_status,
78-
:hide,
79-
:use_stdio,
80-
{:args,
81-
[
82-
"-o",
83-
"ConnectTimeout=3",
84-
"-o",
85-
"PreferredAuthentications=publickey",
86-
"-T",
87-
"-s",
88-
ip,
89-
"fwup"
90-
]}
91-
]
92-
93-
port = Port.open({:spawn_executable, ssh_path()}, opts)
94-
95-
fd = File.open!(firmware_path, [:read])
96-
80+
command = "cat #{firmware_path} | #{ssh_path()} -o ConnectTimeout=3 -s #{ip} fwup"
81+
82+
port =
83+
Port.open({:spawn, ~s(script -q /dev/null #{shell} -c "#{command}")}, [
84+
:binary,
85+
:exit_status,
86+
:stream,
87+
:stderr_to_stdout,
88+
{
89+
:env,
90+
# pass the whole user env
91+
for({k, v} <- System.get_env(), do: {to_charlist(k), to_charlist(v)})
92+
}
93+
])
94+
95+
Process.register(user, :user)
9796
Process.flag(:trap_exit, true)
9897

99-
sender_pid = spawn_link(fn -> send_data(port, fd) end)
100-
port_read(port, sender_pid)
98+
shell_loop(stdin_port, port)
99+
100+
# Close the ports if they are still around
101+
if Port.info(stdin_port), do: Port.close(stdin_port)
102+
if Port.info(port), do: Port.close(port)
103+
104+
:ok
105+
end
106+
107+
defp shell_loop(stdin_port, ssh_port) do
108+
receive do
109+
# Route input from stdin to the command port
110+
{^stdin_port, {:data, data}} ->
111+
Port.command(ssh_port, data)
112+
shell_loop(stdin_port, ssh_port)
113+
114+
# Route output from the command port to stdout
115+
{^ssh_port, {:data, data}} ->
116+
IO.write(data)
117+
shell_loop(stdin_port, ssh_port)
118+
119+
# If any of the ports get closed, break out of the loop
120+
{^ssh_port, :eof} ->
121+
:ok
122+
123+
{^ssh_port, {:exit_status, 0}} ->
124+
:ok
125+
126+
{_port, {:exit_status, status}} ->
127+
Mix.raise("ssh failed with status #{status}")
128+
129+
{:EXIT, ^ssh_port, reason} ->
130+
Mix.raise("""
131+
Unexpected exit from ssh (#{inspect(reason)})
132+
133+
This is known to happen when ssh interactively prompts you for a
134+
passphrase. The following are workarounds:
135+
136+
1. Load your private key identity into the ssh agent by running
137+
`ssh-add`
138+
139+
2. Use the `upload.sh` script. Create one by running
140+
`mix firmware.gen.script`.
141+
""")
142+
143+
other ->
144+
Mix.raise("""
145+
Unexpected message received: #{inspect(other)}
146+
147+
Please open an issue so that we can fix this.
148+
""")
149+
end
101150
end
102151

103152
defp firmware(opts) do
@@ -142,59 +191,6 @@ defmodule Mix.Tasks.Upload do
142191
end
143192
end
144193

145-
defp port_read(port, sender_pid) do
146-
receive do
147-
{^port, {:data, data}} ->
148-
IO.write(data)
149-
port_read(port, sender_pid)
150-
151-
{^port, {:exit_status, 0}} ->
152-
:ok
153-
154-
{^port, {:exit_status, status}} ->
155-
Mix.raise("ssh failed with status #{status}")
156-
157-
{:EXIT, ^sender_pid, :normal} ->
158-
# All data has been sent
159-
port_read(port, sender_pid)
160-
161-
{:EXIT, ^port, reason} ->
162-
Mix.raise("""
163-
Unexpected exit from ssh (#{inspect(reason)})
164-
165-
This is known to happen when ssh interactively prompts you for a
166-
passphrase. The following are workarounds:
167-
168-
1. Load your private key identity into the ssh agent by running
169-
`ssh-add`
170-
171-
2. Use the `upload.sh` script. Create one by running
172-
`mix firmware.gen.script`.
173-
""")
174-
175-
other ->
176-
Mix.raise("""
177-
Unexpected message received: #{inspect(other)}
178-
179-
Please open an issue so that we can fix this.
180-
""")
181-
end
182-
end
183-
184-
defp send_data(port, fd) do
185-
case IO.binread(fd, 16384) do
186-
:eof ->
187-
:ok
188-
189-
{:error, _reason} ->
190-
exit(:read_failed)
191-
192-
data ->
193-
Port.command(port, data)
194-
send_data(port, fd)
195-
end
196-
end
197-
198194
defp target_ip_address_or_name_msg() do
199195
~S"""
200196
mix upload expects a target IP address or hostname

0 commit comments

Comments
 (0)