aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ssh
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ssh')
-rw-r--r--lib/ssh/doc/src/ssh.xml5
-rw-r--r--lib/ssh/src/ssh.erl13
-rw-r--r--lib/ssh/src/ssh_connection_manager.erl66
-rw-r--r--lib/ssh/test/ssh_basic_SUITE.erl25
4 files changed, 102 insertions, 7 deletions
diff --git a/lib/ssh/doc/src/ssh.xml b/lib/ssh/doc/src/ssh.xml
index 9de1dd1dad..aac4b462a2 100644
--- a/lib/ssh/doc/src/ssh.xml
+++ b/lib/ssh/doc/src/ssh.xml
@@ -193,7 +193,10 @@
(simply passed on to the transport protocol).</p></item>
<tag><c><![CDATA[{ip_v6_disabled, boolean()}]]></c></tag>
<item>
- <p>Determines if SSH shall use IPv6 or not.</p></item>
+ <p>Determines if SSH shall use IPv6 or not.</p></item>
+ <tag><c><![CDATA[{idle_time, timeout()}]]></c></tag>
+ <item>
+ <p>Sets a timeout on connection when no channels are active, default is infinity</p></item>
</taglist>
</desc>
</func>
diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl
index 7d68b1d7bd..a569298056 100644
--- a/lib/ssh/src/ssh.erl
+++ b/lib/ssh/src/ssh.erl
@@ -79,7 +79,7 @@ connect(Host, Port, Options, Timeout) ->
DisableIpv6 = proplists:get_value(ip_v6_disabled, SshOptions, false),
Inet = inetopt(DisableIpv6),
do_connect(Host, Port, [Inet | SocketOptions],
- [{user_pid, self()}, {host, Host} | SshOptions], Timeout, DisableIpv6)
+ [{user_pid, self()}, {host, Host} | fix_idle_time(SshOptions)], Timeout, DisableIpv6)
end.
do_connect(Host, Port, SocketOptions, SshOptions, Timeout, DisableIpv6) ->
@@ -246,6 +246,13 @@ shell(Host, Port, Options) ->
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
+fix_idle_time(SshOptions) ->
+ case proplists:get_value(idle_time, SshOptions) of
+ undefined ->
+ [{idle_time, infinity}|SshOptions];
+ _ ->
+ SshOptions
+ end.
start_daemon(Host, Port, Options, Inet) ->
case handle_options(Options) of
{error, _Reason} = Error ->
@@ -355,6 +362,8 @@ handle_option([{pref_public_key_algs, _} = Opt | Rest], SocketOptions, SshOption
handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
handle_option([{quiet_mode, _} = Opt|Rest], SocketOptions, SshOptions) ->
handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
+handle_option([{idle_time, _} = Opt | Rest], SocketOptions, SshOptions) ->
+ handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
handle_option([Opt | Rest], SocketOptions, SshOptions) ->
handle_option(Rest, [handle_inet_option(Opt) | SocketOptions], SshOptions).
@@ -430,6 +439,8 @@ handle_ssh_option({shell, Value} = Opt) when is_function(Value) ->
handle_ssh_option({quiet_mode, Value} = Opt) when Value == true;
Value == false ->
Opt;
+handle_ssh_option({idle_time, Value} = Opt) when is_integer(Value), Value > 0 ->
+ Opt;
handle_ssh_option(Opt) ->
throw({error, {eoptions, Opt}}).
diff --git a/lib/ssh/src/ssh_connection_manager.erl b/lib/ssh/src/ssh_connection_manager.erl
index 513b0f86c7..0c1eee5186 100644
--- a/lib/ssh/src/ssh_connection_manager.erl
+++ b/lib/ssh/src/ssh_connection_manager.erl
@@ -62,6 +62,7 @@
latest_channel_id = 0,
opts,
channel_args,
+ idle_timer_ref, % timerref
connected
}).
@@ -203,6 +204,8 @@ init([client, Opts]) ->
ChannelPid = proplists:get_value(channel_pid, Opts),
self() !
{start_connection, client, [Parent, Address, Port, SocketOpts, Options]},
+ TimerRef = get_idle_time(Options),
+
{ok, #state{role = client,
client = ChannelPid,
connection_state = #connection{channel_cache = Cache,
@@ -211,6 +214,7 @@ init([client, Opts]) ->
connection_supervisor = Parent,
requests = []},
opts = Opts,
+ idle_timer_ref = TimerRef,
connected = false}}.
%%--------------------------------------------------------------------
@@ -230,6 +234,13 @@ handle_call({request, ChannelPid, ChannelId, Type, Data}, From, State0) ->
%% channel is sent later when reply arrives from the connection
%% handler.
lists:foreach(fun send_msg/1, Replies),
+ SshOpts = proplists:get_value(ssh_opts, State0#state.opts),
+ case proplists:get_value(idle_time, SshOpts) of
+ infinity ->
+ ok;
+ _IdleTime ->
+ erlang:send_after(5000, self(), {check_cache, [], []})
+ end,
{noreply, State};
handle_call({request, ChannelId, Type, Data}, From, State0) ->
@@ -358,7 +369,7 @@ handle_call({open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data},
recv_packet_size = MaxPacketSize},
ssh_channel:cache_update(Cache, Channel),
State = add_request(true, ChannelId, From, State1),
- {noreply, State};
+ {noreply, remove_timer_ref(State)};
handle_call({send_window, ChannelId}, _From,
#state{connection_state =
@@ -403,6 +414,13 @@ handle_call({close, ChannelId}, _,
send_msg({connection_reply, Pid,
ssh_connection:channel_close_msg(Id)}),
ssh_channel:cache_update(Cache, Channel#channel{sent_close = true}),
+ SshOpts = proplists:get_value(ssh_opts, State#state.opts),
+ case proplists:get_value(idle_time, SshOpts) of
+ infinity ->
+ ok;
+ _IdleTime ->
+ erlang:send_after(5000, self(), {check_cache, [], []})
+ end,
{reply, ok, State};
undefined ->
{reply, ok, State}
@@ -523,7 +541,10 @@ handle_info({start_connection, client,
Pid ! {self(), not_connected, Reason},
{stop, {shutdown, normal}, State}
end;
-
+handle_info({check_cache, _ , _},
+ #state{connection_state =
+ #connection{channel_cache = Cache}} = State) ->
+ {noreply, check_cache(State, Cache)};
handle_info({ssh_cm, _Sender, Msg}, State0) ->
%% Backwards compatibility!
State = cm_message(Msg, State0),
@@ -621,6 +642,45 @@ code_change(_OldVsn, State, _Extra) ->
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
+get_idle_time(SshOptions) ->
+ case proplists:get_value(idle_time, SshOptions) of
+ infinity ->
+ infinity;
+ _IdleTime -> %% We dont want to set the timeout on first connect
+ undefined
+ end.
+check_cache(State, Cache) ->
+ %% Check the number of entries in Cache
+ case proplists:get_value(size, ets:info(Cache)) of
+ 0 ->
+ Opts = proplists:get_value(ssh_opts, State#state.opts),
+ case proplists:get_value(idle_time, Opts) of
+ infinity ->
+ State;
+ undefined ->
+ State;
+ Time ->
+ case State#state.idle_timer_ref of
+ undefined ->
+ TimerRef = erlang:send_after(Time, self(), {'EXIT', [], "Timeout"}),
+ State#state{idle_timer_ref=TimerRef};
+ _ ->
+ State
+ end
+ end;
+ _ ->
+ State
+ end.
+remove_timer_ref(State) ->
+ case State#state.idle_timer_ref of
+ infinity -> %% If the timer is not activated
+ State;
+ undefined -> %% If we already has cancelled the timer
+ State;
+ TimerRef -> %% Timer is active
+ erlang:cancel_timer(TimerRef),
+ State#state{idle_timer_ref = undefined}
+ end.
channel_data(Id, Type, Data, Connection0, ConnectionPid, From, State) ->
case ssh_connection:channel_data(Id, Type, Data, Connection0,
ConnectionPid, From) of
@@ -718,7 +778,7 @@ handle_channel_down(ChannelPid, #state{connection_state =
(_,Acc) ->
Acc
end, [], Cache),
- {{replies, []}, State}.
+ {{replies, []}, check_cache(State, Cache)}.
update_sys(Cache, Channel, Type, ChannelPid) ->
ssh_channel:cache_update(Cache,
diff --git a/lib/ssh/test/ssh_basic_SUITE.erl b/lib/ssh/test/ssh_basic_SUITE.erl
index 7a641c92c1..5fec7f0cd7 100644
--- a/lib/ssh/test/ssh_basic_SUITE.erl
+++ b/lib/ssh/test/ssh_basic_SUITE.erl
@@ -42,14 +42,15 @@ all() ->
{group, dsa_pass_key},
{group, rsa_pass_key},
{group, internal_error},
+ {group, idle_time},
daemon_already_started,
server_password_option,
server_userpassword_option,
close].
groups() ->
- [{dsa_key, [], [send, exec, exec_compressed, shell, known_hosts]},
- {rsa_key, [], [send, exec, exec_compressed, shell, known_hosts]},
+ [{dsa_key, [], [send, exec, exec_compressed, shell, known_hosts, idle_time]},
+ {rsa_key, [], [send, exec, exec_compressed, shell, known_hosts, idle_time]},
{dsa_pass_key, [], [pass_phrase]},
{rsa_pass_key, [], [pass_phrase]},
{internal_error, [], [internal_error]}
@@ -234,7 +235,27 @@ exec_compressed(Config) when is_list(Config) ->
ssh:stop_daemon(Pid).
%%--------------------------------------------------------------------
+idle_time(doc) ->
+ ["Idle timeout test"];
+idle_time(Config) ->
+ SystemDir = filename:join(?config(priv_dir, Config), system),
+ UserDir = ?config(priv_dir, Config),
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},
+ {user_dir, UserDir},
+ {failfun, fun ssh_test_lib:failfun/2}]),
+ ConnectionRef =
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user_dir, UserDir},
+ {user_interaction, false}]),
+ {ok, Id} = ssh_connection:session_channel(ConnectionRef, 1000),
+ ssh_connection:close(ConnectionRef, Id),
+ receive
+ after 10000 ->
+ {error,channel_closed} = ssh_connection:session_channel(ConnectionRef, 1000)
+ end,
+ ssh:stop_daemon(Pid).
+%%--------------------------------------------------------------------
shell(doc) ->
["Test that ssh:shell/2 works"];
shell(Config) when is_list(Config) ->