diff options
Diffstat (limited to 'lib/common_test/src/ct_telnet.erl')
-rw-r--r-- | lib/common_test/src/ct_telnet.erl | 1166 |
1 files changed, 1166 insertions, 0 deletions
diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl new file mode 100644 index 0000000000..c19d312f01 --- /dev/null +++ b/lib/common_test/src/ct_telnet.erl @@ -0,0 +1,1166 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2003-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%%% @doc Common Test specific layer on top of telnet client ct_telnet_client.erl +%%% +%%% <p>Use this module to set up telnet connections, send commands and +%%% perform string matching on the result. +%%% (See the <code>unix_telnet</code> manual page for information +%%% about how ct_telnet may be used specifically with unix hosts.)</p> +%%% <p>The following default values are defined in ct_telnet:</p> +%%% <pre> +%%% Connection timeout = 10 sec (time to wait for connection) +%%% Command timeout = 10 sec (time to wait for a command to return) +%%% Max no of reconnection attempts = 3 +%%% Reconnection interval = 5 sek (time to wait in between reconnection attempts) +%%% </pre> +%%% <p>These parameters can be altered by the user with the following +%%% configuration term:</p> +%%% <pre> +%%% {telnet_settings, [{connect_timeout,Millisec}, +%%% {command_timeout,Millisec}, +%%% {reconnection_attempts,N}, +%%% {reconnection_interval,Millisec}]}. +%%% </pre> +%%% <p><code>Millisec = integer(), N = integer()</code></p> +%%% <p>Enter the <code>telnet_settings</code> term in a configuration +%%% file included in the test and ct_telnet will retrieve the information +%%% automatically.</p> + +%%% @type connection_type() = telnet | ts1 | ts2 + +%%% @type connection() = handle() | +%%% {ct:target_name(),connection_type()} | ct:target_name() + +%%% @type handle() = ct_gen_conn:handle(). Handle for a +%%% specific telnet connection. + +%%% @type prompt_regexp() = string(). A regular expression which +%%% matches all possible prompts for a specific type of target. The +%%% regexp must not have any groups i.e. when matching, re:run/3 shall +%%% return a list with one single element. +%%% +%%% @see unix_telnet + +-module(ct_telnet). + +-compile(export_all). + +-export([open/1, open/2, open/3, open/4, close/1]). +-export([cmd/2, cmd/3, cmdf/3, cmdf/4, get_data/1, + send/2, sendf/3, expect/2, expect/3]). + +%% Callbacks +-export([init/3,handle_msg/2,reconnect/2,terminate/2]). + +%% Tool internals +-export([silent_teln_expect/5, teln_receive_until_prompt/3, + start_log/1, log/3, cont_log/2, end_log/0, + try_start_log/1, try_log/3, try_cont_log/2, try_end_log/0]). + + +-define(RECONNS,3). +-define(RECONN_TIMEOUT,5000). +-define(DEFAULT_TIMEOUT,10000). +-define(DEFAULT_PORT,23). + +-include("ct_util.hrl"). + +-record(state,{teln_pid, + prx, + type, + buffer=[], + prompt=false, + name, + target_mod,extra, + conn_to=?DEFAULT_TIMEOUT, + com_to=?DEFAULT_TIMEOUT, + reconns=?RECONNS, + reconn_int=?RECONN_TIMEOUT}). + +%%%----------------------------------------------------------------- +%%% @spec open(Name) -> {ok,Handle} | {error,Reason} +%%% @equiv open(Name,telnet) +open(Name) -> + open(Name,telnet). + +%%%----------------------------------------------------------------- +%%% @spec open(Name,ConnType) -> {ok,Handle} | {error,Reason} +%%% Name = target_name() +%%% ConnType = ct_telnet:connection_type() +%%% Handle = ct_telnet:handle() +%%% +%%% @doc Open a telnet connection to the specified target host. +open(Name,ConnType) -> + case ct_util:get_key_from_name(Name) of + {ok, unix} -> % unix host + open(Name, ConnType, unix_telnet, Name); + {ok, Key} -> % any other, e.g. interwatch (iw), etc. + open(Name, ConnType, Key, Name); + Error -> + Error + end. + +%%%----------------------------------------------------------------- +%%% @spec open(KeyOrName,ConnType,TargetMod) -> +%%% {ok,Handle} | {error,Reason} +%%% @equiv open(KeyOrName,ConnType,TargetMod,[]) +open(KeyOrName,ConnType,TargetMod) -> + open(KeyOrName,ConnType,TargetMod,KeyOrName). + +%%%----------------------------------------------------------------- +%%% @spec open(KeyOrName,ConnType,TargetMod,Extra) -> +%%% {ok,Handle} | {error,Reason} +%%% KeyOrName = Key | Name +%%% Key = atom() +%%% Name = ct:target_name() +%%% ConnType = connection_type() +%%% TargetMod = atom() +%%% Extra = term() +%%% Handle = handle() +%%% +%%% @doc Open a telnet connection to the specified target host. +%%% +%%% <p>The target data must exist in a configuration file. The connection +%%% may be associated with either <code>Name</code> and/or the returned +%%% <code>Handle</code>. To allocate a name for the target, +%%% use <code>ct:require/2</code> in a test case, or use a +%%% <code>require</code> statement in the suite info function +%%% (<code>suite/0</code>), or in a test case info function. +%%% If you want the connection to be associated with <code>Handle</code> only +%%% (in case you need to open multiple connections to a host for example), +%%% simply use <code>Key</code>, the configuration variable name, to +%%% specify the target. Note that a connection that has no associated target +%%% name can only be closed with the handle value.</p> +%%% +%%% <p><code>TargetMod</code> is a module which exports the functions +%%% <code>connect(Ip,Port,Extra)</code> and <code>get_prompt_regexp()</code> +%%% for the given <code>TargetType</code> (e.g. <code>unix_telnet</code>).</p> +open(KeyOrName,ConnType,TargetMod,Extra) -> + case ct:get_config({KeyOrName,ConnType}) of + undefined -> + log(heading(open,{KeyOrName,ConnType}),"Failed: ~p", + [{not_available,KeyOrName}]), + {error,{not_available,KeyOrName,ConnType}}; + Addr -> + Addr1 = + case Addr of + {_IP,_Port} -> + Addr; + IP -> + case ct:get_config({KeyOrName,port}) of + undefined -> IP; + P -> {IP,P} + end + end, + log(heading(open,{KeyOrName,ConnType}),"Opening connection to: ~p",[Addr1]), + ct_gen_conn:start(KeyOrName,full_addr(Addr1,ConnType), + {TargetMod,Extra},?MODULE) + end. + +%%%----------------------------------------------------------------- +%%% @spec close(Connection) -> ok | {error,Reason} +%%% Connection = ct_telnet:connection() +%%% +%%% @doc Close the telnet connection and stop the process managing it. +%%% +%%% <p>A connection may be associated with a target name and/or a handle. +%%% If <code>Connection</code> has no associated target name, it may only +%%% be closed with the handle value (see the <code>open/4</code> +%%% function).</p> +close(Connection) -> + case get_handle(Connection) of + {ok,Pid} -> + log("ct_telnet:close","Handle: ~p",[Pid]), + case ct_gen_conn:stop(Pid) of + {error,{process_down,Pid,noproc}} -> + {error,already_closed}; + Result -> + Result + end; + Error -> + Error + end. + +%%%================================================================= +%%% Test suite interface +%%%----------------------------------------------------------------- +%%% @spec cmd(Connection,Cmd) -> {ok,Data} | {error,Reason} +%%% @equiv cmd(Connection,Cmd,DefaultTimeout) +cmd(Connection,Cmd) -> + cmd(Connection,Cmd,default). +%%%----------------------------------------------------------------- +%%% @spec cmd(Connection,Cmd,Timeout) -> {ok,Data} | {error,Reason} +%%% Connection = ct_telnet:connection() +%%% Cmd = string() +%%% Timeout = integer() +%%% Data = [string()] +%%% @doc Send a command via telnet and wait for prompt. +cmd(Connection,Cmd,Timeout) -> + case get_handle(Connection) of + {ok,Pid} -> + call(Pid,{cmd,Cmd,Timeout}); + Error -> + Error + end. +%%%----------------------------------------------------------------- +%%% @spec cmdf(Connection,CmdFormat,Args) -> {ok,Data} | {error,Reason} +%%% @equiv cmdf(Connection,CmdFormat,Args,DefaultTimeout) +cmdf(Connection,CmdFormat,Args) -> + cmdf(Connection,CmdFormat,Args,default). +%%%----------------------------------------------------------------- +%%% @spec cmdf(Connection,CmdFormat,Args,Timeout) -> {ok,Data} | {error,Reason} +%%% Connection = ct_telnet:connection() +%%% CmdFormat = string() +%%% Args = list() +%%% Timeout = integer() +%%% Data = [string()] +%%% @doc Send a telnet command and wait for prompt +%%% (uses a format string and list of arguments to build the command). +%%%----------------------------------------------------------------- +cmdf(Connection,CmdFormat,Args,Timeout) when is_list(Args) -> + Cmd = lists:flatten(io_lib:format(CmdFormat,Args)), + cmd(Connection,Cmd,Timeout). + +%%%----------------------------------------------------------------- +%%% @spec get_data(Connection) -> {ok,Data} | {error,Reason} +%%% Connection = ct_telnet:connection() +%%% Data = [string()] +%%% @doc Get all data which has been received by the telnet client +%%% since last command was sent. +get_data(Connection) -> + case get_handle(Connection) of + {ok,Pid} -> + call(Pid,get_data); + Error -> + Error + end. + +%%%----------------------------------------------------------------- +%%% @spec send(Connection,Cmd) -> ok | {error,Reason} +%%% Connection = ct_telnet:connection() +%%% Cmd = string() +%%% @doc Send a telnet command and return immediately. +%%% +%%% <p>The resulting output from the command can be read with +%%% <code>get_data/1</code> or <code>expect/2/3</code>.</p> +send(Connection,Cmd) -> + case get_handle(Connection) of + {ok,Pid} -> + call(Pid,{send,Cmd}); + Error -> + Error + end. + +%%%----------------------------------------------------------------- +%%% @spec sendf(Connection,CmdFormat,Args) -> ok | {error,Reason} +%%% Connection = ct_telnet:connection() +%%% CmdFormat = string() +%%% Args = list() +%%% @doc Send a telnet command and return immediately (uses a format +%%% string and a list of arguments to build the command). +sendf(Connection,CmdFormat,Args) when is_list(Args) -> + Cmd = lists:flatten(io_lib:format(CmdFormat,Args)), + send(Connection,Cmd). + +%%%----------------------------------------------------------------- +%%% @spec expect(Connection,Patterns) -> term() +%%% @equiv expect(Connections,Patterns,[]) +expect(Connection,Patterns) -> + expect(Connection,Patterns,[]). + +%%%----------------------------------------------------------------- +%%% @spec expect(Connection,Patterns,Opts) -> {ok,Match} | +%%% {ok,MatchList,HaltReason} | +%%% {error,Reason} +%%% Connection = ct_telnet:connection() +%%% Patterns = Pattern | [Pattern] +%%% Pattern = string() | {Tag,string()} | prompt | {prompt,Prompt} +%%% Prompt = string() +%%% Tag = term() +%%% Opts = [Opt] +%%% Opt = {timeout,Timeout} | repeat | {repeat,N} | sequence | +%%% {halt,HaltPatterns} | ignore_prompt +%%% Timeout = integer() +%%% N = integer() +%%% HaltPatterns = Patterns +%%% MatchList = [Match] +%%% Match = RxMatch | {Tag,RxMatch} | {prompt,Prompt} +%%% RxMatch = [string()] +%%% HaltReason = done | Match +%%% Reason = timeout | {prompt,Prompt} +%%% +%%% @doc Get data from telnet and wait for the expected pattern. +%%% +%%% <p><code>Pattern</code> can be a POSIX regular expression. If more +%%% than one pattern is given, the function returns when the first +%%% match is found.</p> +%%% +%%% <p><code>RxMatch</code> is a list of matched strings. It looks +%%% like this: <code>[FullMatch, SubMatch1, SubMatch2, ...]</code> +%%% where <code>FullMatch</code> is the string matched by the whole +%%% regular expression and <code>SubMatchN</code> is the string that +%%% matched subexpression no <code>N</code>. Subexpressions are +%%% denoted with '(' ')' in the regular expression</p> +%%% +%%% <p>If a <code>Tag</code> is given, the returned <code>Match</code> +%%% will also include the matched <code>Tag</code>. Else, only +%%% <code>RxMatch</code> is returned.</p> +%%% +%%% <p>The function will always return when a prompt is found, unless +%%% the <code>ignore_prompt</code> options is used.</p> +%%% +%%% <p>The <code>timeout</code> option indicates that the function +%%% shall return if the telnet client is idle (i.e. if no data is +%%% received) for more than <code>Timeout</code> milliseconds. Default +%%% timeout is 10 seconds.</p> +%%% +%%% <p>The <code>repeat</code> option indicates that the pattern(s) +%%% shall be matched multiple times. If <code>N</code> is given, the +%%% pattern(s) will be matched <code>N</code> times, and the function +%%% will return with <code>HaltReason = done</code>.</p> +%%% +%%% <p>The <code>sequence</code> option indicates that all patterns +%%% shall be matched in a sequence. A match will not be concluded +%%% untill all patterns are matched.</p> +%%% +%%% <p>Both <code>repeat</code> and <code>sequence</code> can be +%%% interrupted by one or more <code>HaltPatterns</code>. When +%%% <code>sequence</code> or <code>repeat</code> is used, there will +%%% always be a <code>MatchList</code> returned, i.e. a list of +%%% <code>Match</code> instead of only one <code>Match</code>. There +%%% will also be a <code>HaltReason</code> returned.</p> +%%% +%%% <p><underline>Examples:</underline><br/> +%%% <code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}], +%%% [sequence,{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match +%%% "ABC" first and then "XYZ", but if "NNN" appears the function will +%%% return <code>{error,{nnn,["NNN"]}}</code>. If both "ABC" and "XYZ" +%%% are matched, the function will return +%%% <code>{ok,[AbcMatch,XyzMatch]}</code>.</p> +%%% +%%% <p><code>expect(Connection,[{abc,"ABC"},{xyz,"XYZ"}], +%%% [{repeat,2},{halt,[{nnn,"NNN"}]}]).</code><br/> will try to match +%%% "ABC" or "XYZ" twice. If "NNN" appears the function will return +%%% with <code>HaltReason = {nnn,["NNN"]}</code>.</p> +%%% +%%% <p>The <code>repeat</code> and <code>sequence</code> options can be +%%% combined in order to match a sequence multiple times.</p> +expect(Connection,Patterns,Opts) -> + case get_handle(Connection) of + {ok,Pid} -> + call(Pid,{expect,Patterns,Opts}); + Error -> + Error + end. + +%%%================================================================= +%%% Callback functions +%% @hidden +init(Name,{Ip,Port,Type},{TargetMod,Extra}) -> + S0 = case ct:get_config(telnet_settings) of + undefined -> + #state{}; + Settings -> + set_telnet_defaults(Settings,#state{}) + end, + case catch TargetMod:connect(Ip,Port,S0#state.conn_to,Extra) of + {ok,TelnPid} -> + log(heading(init,{Name,Type}), + "Opened telnet connection\n" + "IP: ~p\n" + "Port: ~p\n" + "Command timeout: ~p\n" + "Reconnection attempts: ~p\n" + "Reconnection interval: ~p\n" + "Connection timeout: ~p", + [Ip,Port,S0#state.com_to,S0#state.reconns, + S0#state.reconn_int,S0#state.conn_to]), + {ok,TelnPid,S0#state{teln_pid=TelnPid, + type=type(Type), + name={Name,Type}, + target_mod=TargetMod, + extra=Extra, + prx=TargetMod:get_prompt_regexp()}}; + {'EXIT',Reason} -> + {error,Reason}; + Error -> + Error + end. + +type(telnet) -> ip; +type(TS) when TS==ts1;TS==ts2 -> ts. + +set_telnet_defaults([{connect_timeout,CnTo}|Ss],S) -> + set_telnet_defaults(Ss,S#state{conn_to=CnTo}); +set_telnet_defaults([{command_timeout,CmTo}|Ss],S) -> + set_telnet_defaults(Ss,S#state{com_to=CmTo}); +set_telnet_defaults([{reconnection_attempts,Rs}|Ss],S) -> + set_telnet_defaults(Ss,S#state{reconns=Rs}); +set_telnet_defaults([{reconnection_interval,RInt}|Ss],S) -> + set_telnet_defaults(Ss,S#state{reconn_int=RInt}); +set_telnet_defaults([],S) -> + S. + +%% @hidden +handle_msg({cmd,Cmd,Timeout},State) -> + try_start_log(heading(cmd,State#state.name)), + try_cont_log("Cmd: ~p", [Cmd]), + debug_cont_log("Throwing Buffer:",[]), + debug_log_lines(State#state.buffer), + case {State#state.type,State#state.prompt} of + {ts,_} -> + silent_teln_expect(State#state.teln_pid, + State#state.buffer, + prompt, + State#state.prx, + [{timeout,2000}]); + {ip,false} -> + silent_teln_expect(State#state.teln_pid, + State#state.buffer, + prompt, + State#state.prx, + [{timeout,200}]); + {ip,true} -> + ok + end, + TO = if Timeout == default -> State#state.com_to; + true -> Timeout + end, + {Return,NewBuffer,Prompt} = + case teln_cmd(State#state.teln_pid, Cmd, State#state.prx, TO) of + {ok,Data,_PromptType,Rest} -> + try_cont_log("Return: ~p", [{ok,Data}]), + {{ok,Data},Rest,true}; + Error -> + Retry = {retry,{Error,State#state.name,State#state.teln_pid, + {cmd,Cmd,TO}}}, + try_cont_log("Return: ~p", [Error]), + {Retry,[],false} + end, + try_end_log(), + {Return,State#state{buffer=NewBuffer,prompt=Prompt}}; +handle_msg({send,Cmd},State) -> + try_log(heading(send,State#state.name),"Cmd: ~p",[Cmd]), + debug_cont_log("Throwing Buffer:",[]), + debug_log_lines(State#state.buffer), + case {State#state.type,State#state.prompt} of + {ts,_} -> + silent_teln_expect(State#state.teln_pid, + State#state.buffer, + prompt, + State#state.prx, + [{timeout,2000}]); + {ip,false} -> + silent_teln_expect(State#state.teln_pid, + State#state.buffer, + prompt, + State#state.prx, + [{timeout,200}]); + {ip,true} -> + ok + end, + ct_telnet_client:send_data(State#state.teln_pid,Cmd), + {ok,State#state{buffer=[],prompt=false}}; +handle_msg(get_data,State) -> + try_start_log(heading(get_data,State#state.name)), + {ok,Data,Buffer} = teln_get_all_data(State#state.teln_pid, + State#state.prx, + State#state.buffer, + [],[]), + try_cont_log("Return: ~p",[{ok,Data}]), + try_end_log(), + {{ok,Data},State#state{buffer=Buffer}}; +handle_msg({expect,Pattern,Opts},State) -> + try_start_log(heading(expect,State#state.name)), + try_cont_log("Expect: ~p\nOpts=~p\n",[Pattern,Opts]), + {Return,NewBuffer,Prompt} = + case teln_expect(State#state.teln_pid, + State#state.buffer, + Pattern, + State#state.prx, + Opts) of + {ok,Data,Rest} -> + P = check_if_prompt_was_reached(Data,[]), + {{ok,Data},Rest,P}; + {ok,Data,HaltReason,Rest} -> + force_cont_log("HaltReason: ~p", + [HaltReason]), + P = check_if_prompt_was_reached(Data,HaltReason), + {{ok,Data,HaltReason},Rest,P}; + {error,Reason,Rest} -> + force_cont_log("Expect failed\n~p",[{error,Reason}]), + P = check_if_prompt_was_reached([],Reason), + {{error,Reason},Rest,P}; + {error,Reason} -> + force_cont_log("Expect failed\n~p",[{error,Reason}]), + P = check_if_prompt_was_reached([],Reason), + {{error,Reason},[],P} + end, + try_end_log(), + Return1 = case Return of + {error,_} -> {retry,{Return,State#state.name, + State#state.teln_pid, + {expect,Pattern,Opts}}}; + _ -> Return + end, + {Return1,State#state{buffer=NewBuffer,prompt=Prompt}}. + + +%% @hidden +reconnect({Ip,Port,_Type},State) -> + reconnect(Ip,Port,State#state.reconns,State). +reconnect(Ip,Port,N,State=#state{target_mod=TargetMod, + extra=Extra, + conn_to=ConnTo, + reconn_int=ReconnInt}) -> + case TargetMod:connect(Ip,Port,ConnTo,Extra) of + {ok, NewPid} -> + {ok, NewPid, State#state{teln_pid=NewPid}}; + Error when N==0 -> + Error; + _Error -> + log("Reconnect failed!","Retries left: ~p",[N]), + timer:sleep(ReconnInt), + reconnect(Ip,Port,N-1,State) + end. + + +%% @hidden +terminate(TelnPid,State) -> + log(heading(terminate,State#state.name), + "Closing telnet connection.\nId: ~p", + [TelnPid]), + ct_telnet_client:close(TelnPid). + + +%%%================================================================= +%%% Internal function +get_handle(Pid) when is_pid(Pid) -> + {ok,Pid}; +get_handle({Name,Type}) when Type==telnet;Type==ts1;Type==ts2 -> + case ct_util:get_connections(Name,?MODULE) of + {ok,Conns} when Conns /= [] -> + case get_handle(Type,Conns) of + {ok,Pid} -> + {ok,Pid}; + _Error -> + case ct_util:get_key_from_name(Name) of + {ok,node} -> + open(Name,Type,ct_telnet_cello_node); + {ok,unix} -> % unix host + open(Name,Type,unix_telnet,Name); + {ok,Key} -> % any other, e.g. interwatch (iw) + open(Name,Type,Key,Name); + Error -> + Error + end + end; + {ok,[]} -> + {error,already_closed}; + Error -> + Error + end; +get_handle(Name) -> + get_handle({Name,telnet}). + +get_handle(Type,[{Pid,{_,_,Type}}|_]) -> + {ok,Pid}; +get_handle(Type,[_H|T]) -> + get_handle(Type,T); +get_handle(Type,[]) -> + {error,{no_such_connection,Type}}. + +full_addr({Ip,Port},Type) -> + {Ip,Port,Type}; +full_addr(Ip,Type) -> + {Ip,?DEFAULT_PORT,Type}. + +call(Pid,Msg) -> + ct_gen_conn:call(Pid,Msg). + +check_if_prompt_was_reached({prompt,_},_) -> + true; +check_if_prompt_was_reached(_,{prompt,_}) -> + true; +check_if_prompt_was_reached(Data,_) when is_list(Data) -> + lists:keymember(prompt,1,Data); +check_if_prompt_was_reached(_,_) -> + false. + +%tc(Fun) -> +% Before = erlang:now(), +% Val = Fun(), +% After = erlang:now(), +% {now_diff(After, Before), Val}. +%now_diff({A2, B2, C2}, {A1, B1, C1}) -> +% ((A2-A1)*1000000 + B2-B1)*1000000 + C2-C1. + +heading(Function,Name) -> + io_lib:format("~w:~w ~p",[?MODULE,Function,Name]). + +%%% @hidden +%% Functions for regular (unconditional) logging, to be +%% used during connect, reconnect, disconnect etc. +log(Heading,Str,Args) -> + ct_gen_conn:log(Heading,Str,Args). +%%% @hidden +start_log(Heading) -> + ct_gen_conn:start_log(Heading). +cont_log(Str,Args) -> + ct_gen_conn:cont_log(Str,Args). +end_log() -> + ct_gen_conn:end_log(). + +%%% @hidden +%% Functions for conditional logging, to be used by +%% cmd, send, receive, expect etc (this output may be +%% silenced by user). +try_start_log(Heading) -> + do_try_log(start_log,[Heading]). +%%% @hidden +try_end_log() -> + do_try_log(end_log,[]). + +%%% @hidden +try_log(Heading,Str,Args) -> + do_try_log(log,[Heading,Str,Args]). + +%%% @hidden +try_cont_log(Str,Args) -> + do_try_log(cont_log,[Str,Args]). + +%%% @hidden +do_try_log(Func,Args) -> + %% check if output is suppressed + case ct_util:is_silenced(telnet) of + true -> + ok; + false -> + apply(ct_gen_conn,Func,Args) + end. + +%%% @hidden +%% Functions that will force printout even if ct_telnet +%% output has been silenced, to be used for error printouts. +force_cont_log(Str,Args) -> + case ct_util:is_silenced(telnet) of + true -> + %% call log/3 now instead of cont_log/2 since + %% start_log/1 will not have been previously called + log("ct_telnet info",Str,Args); + false -> + cont_log(Str,Args) + end. + +%%% @hidden +%% Debug printouts. +debug_cont_log(Str,Args) -> + Old = put(silent,true), + cont_log(Str,Args), + put(silent,Old). + + + +%%%================================================================= +%%% Abstraction layer on top of ct_telnet_client.erl +teln_cmd(Pid,Cmd,Prx,Timeout) -> + ct_telnet_client:send_data(Pid,Cmd), + teln_receive_until_prompt(Pid,Prx,Timeout). + + +teln_get_all_data(Pid,Prx,Data,Acc,LastLine) -> + case check_for_prompt(Prx,lists:reverse(LastLine) ++ Data) of + {prompt,Lines,_PromptType,Rest} -> + teln_get_all_data(Pid,Prx,Rest,[Lines|Acc],[]); + {noprompt,Lines,LastLine1} -> + case ct_telnet_client:get_data(Pid) of + {ok,[]} -> + {ok,lists:reverse(lists:append([Lines|Acc])), + lists:reverse(LastLine1)}; + {ok,Data1} -> + teln_get_all_data(Pid,Prx,Data1,[Lines|Acc],LastLine1) + end + end. + +%% Expect options record +-record(eo,{teln_pid, + prx, + timeout, + haltpatterns=[], + seq=false, + repeat=false, + found_prompt=false}). + +%% @hidden +%% @doc Externally the silent_teln_expect function shall only be used +%% by the TargetModule, i.e. the target specific module which +%% implements connect/2 and get_prompt_regexp/0. +silent_teln_expect(Pid,Data,Pattern,Prx,Opts) -> + Old = put(silent,true), + try_cont_log("silent_teln_expect/5, Pattern = ~p",[Pattern]), + Result = teln_expect(Pid,Data,Pattern,Prx,Opts), + try_cont_log("silent_teln_expect -> ~p\n",[Result]), + put(silent,Old), + Result. + +%% teln_expect/5 +%% +%% This function implements the expect functionality over telnet. In +%% general there are three possible ways to go: +%% 1) Single: One or more patterns are given, and the function return +%% when one of the patterns are matched. +%% 2) Sequence: Several patterns are given, and they are matched in +%% the order they appear in the pattern list. +%% 3a) Repeat (single): 1) is repeated either N times or until a halt +%% condition is fullfilled. +%% 3b) Repeat (sequence): 2) is repeated either N times or until a +%% halt condition is fullfilled. +teln_expect(Pid,Data,Pattern0,Prx,Opts) -> HaltPatterns = case + get_ignore_prompt(Opts) of true -> get_haltpatterns(Opts); false + -> [prompt | get_haltpatterns(Opts)] end, + + Seq = get_seq(Opts), + Pattern = convert_pattern(Pattern0,Seq), + + Timeout = get_timeout(Opts), + + EO = #eo{teln_pid=Pid, + prx=Prx, + timeout=Timeout, + seq=Seq, + haltpatterns=HaltPatterns}, + + case get_repeat(Opts) of + false -> + case teln_expect1(Data,Pattern,[],EO) of + {ok,Matched,Rest} -> + {ok,Matched,Rest}; + {halt,Why,Rest} -> + {error,Why,Rest}; + {error,Reason} -> + {error,Reason} + end; + N -> + EO1 = EO#eo{repeat=N}, + repeat_expect(Data,Pattern,[],EO1) + end. + +convert_pattern(Pattern,Seq) + when is_list(Pattern) and not is_integer(hd(Pattern)) -> + case Seq of + true -> Pattern; + false -> rm_dupl(Pattern,[]) + end; +convert_pattern(Pattern,_Seq) -> + [Pattern]. + +rm_dupl([P|Ps],Acc) -> + case lists:member(P,Acc) of + true -> + rm_dupl(Ps,Acc); + false -> + rm_dupl(Ps,[P|Acc]) + end; +rm_dupl([],Acc) -> + lists:reverse(Acc). + +get_timeout(Opts) -> + case lists:keysearch(timeout,1,Opts) of + {value,{timeout,T}} -> T; + false -> ?DEFAULT_TIMEOUT + end. +get_repeat(Opts) -> + case lists:keysearch(repeat,1,Opts) of + {value,{repeat,N}} when is_integer(N) -> + N; + false -> + case lists:member(repeat,Opts) of + true -> + -1; + false -> + false + end + end. +get_seq(Opts) -> + lists:member(sequence,Opts). +get_haltpatterns(Opts) -> + case lists:keysearch(halt,1,Opts) of + {value,{halt,HaltPatterns}} -> + convert_pattern(HaltPatterns,false); + false -> + [] + end. +get_ignore_prompt(Opts) -> + lists:member(ignore_prompt,Opts). + +%% Repeat either single or sequence. All match results are accumulated +%% and returned when a halt condition is fulllfilled. +repeat_expect(Rest,_Pattern,Acc,#eo{repeat=0}) -> + {ok,lists:reverse(Acc),done,Rest}; +repeat_expect(Data,Pattern,Acc,EO) -> + case teln_expect1(Data,Pattern,[],EO) of + {ok,Matched,Rest} -> + EO1 = EO#eo{repeat=EO#eo.repeat-1}, + repeat_expect(Rest,Pattern,[Matched|Acc],EO1); + {halt,Why,Rest} -> + {ok,lists:reverse(Acc),Why,Rest}; + {error,Reason} -> + {error,Reason} + end. + +teln_expect1(Data,Pattern,Acc,EO) -> + ExpectFun = case EO#eo.seq of + true -> fun() -> seq_expect(Data,Pattern,Acc,EO) end; + false -> fun() -> one_expect(Data,Pattern,EO) end + end, + case ExpectFun() of + {match,Match,Rest} -> + {ok,Match,Rest}; + {halt,Why,Rest} -> + {halt,Why,Rest}; + NotFinished -> + %% Get more data + Fun = fun() -> get_data1(EO#eo.teln_pid) end, + case ct_gen_conn:do_within_time(Fun, EO#eo.timeout) of + {error,Reason} -> + %% A timeout will occur when the telnet connection + %% is idle for EO#eo.timeout milliseconds. + {error,Reason}; + {ok,Data1} -> + case NotFinished of + {nomatch,Rest} -> + %% One expect + teln_expect1(Rest++Data1,Pattern,[],EO); + {continue,Patterns1,Acc1,Rest} -> + %% Sequence + teln_expect1(Rest++Data1,Patterns1,Acc1,EO) + end + end + end. + +get_data1(Pid) -> + case ct_telnet_client:get_data(Pid) of + {ok,[]} -> + get_data1(Pid); + {ok,Data} -> + {ok,Data} + end. + +%% 1) Single expect. +%% First the whole data chunk is searched for a prompt (to avoid doing +%% a regexp match for the prompt at each line). +%% If we are searching for anyting else, the datachunk is split into +%% lines and each line is matched against each pattern. + +%% one_expect: split data chunk at prompts +one_expect(Data,Pattern,EO) -> + case match_prompt(Data,EO#eo.prx) of + {prompt,UptoPrompt,PromptType,Rest} -> + case Pattern of + [Prompt] when Prompt==prompt; Prompt=={prompt,PromptType} -> + %% Only searching for prompt + log_lines(UptoPrompt), + try_cont_log("<b>PROMPT:</b> ~s", [PromptType]), + {match,{prompt,PromptType},Rest}; + [{prompt,_OtherPromptType}] -> + %% Only searching for one specific prompt, not thisone + log_lines(UptoPrompt), + {nomatch,Rest}; + _ -> + one_expect1(UptoPrompt,Pattern,Rest, + EO#eo{found_prompt=PromptType}) + end; + noprompt -> + case Pattern of + [Prompt] when Prompt==prompt; element(1,Prompt)==prompt -> + %% Only searching for prompt + LastLine = log_lines_not_last(Data), + {nomatch,LastLine}; + _ -> + one_expect1(Data,Pattern,[],EO#eo{found_prompt=false}) + end + end. + +remove_zero(List) -> + [Ch || Ch <- List, Ch=/=0, Ch=/=13]. + +%% one_expect1: split data chunk at lines +one_expect1(Data,Pattern,Rest,EO) -> + case match_lines(Data,Pattern,EO) of + {match,Match,MatchRest} -> + {match,Match,MatchRest++Rest}; + {nomatch,prompt} -> + one_expect(Rest,Pattern,EO); + {nomatch,NoMatchRest} -> + {nomatch,NoMatchRest++Rest}; + {halt,Why,HaltRest} -> + {halt,Why,HaltRest++Rest} + end. + + +%% 2) Sequence. +%% First the whole data chunk is searched for a prompt (to avoid doing +%% a regexp match for the prompt at each line). +%% If we are searching for anyting else, the datachunk is split into +%% lines and each line is matched against the first pattern in the list. +%% When a match is found, the match result is accumulated, and we keep +%% searching for the next pattern in the list. + +%% seq_expect: Split data chunk at prompts +seq_expect(Data,[],Acc,_EO) -> + {match,lists:reverse(Acc),Data}; +seq_expect([],Patterns,Acc,_EO) -> + {continue,Patterns,lists:reverse(Acc),[]}; +seq_expect(Data,Patterns,Acc,EO) -> + case match_prompt(Data,EO#eo.prx) of + {prompt,UptoPrompt,PromptType,Rest} -> + seq_expect1(UptoPrompt,Patterns,Acc,Rest, + EO#eo{found_prompt=PromptType}); + noprompt -> + seq_expect1(Data,Patterns,Acc,[],EO#eo{found_prompt=false}) + end. + +%% seq_expect1: For one prompt-chunk, match each pattern - line by +%% line if it is other than the prompt we are seaching for. +seq_expect1(Data,[prompt|Patterns],Acc,Rest,EO) -> + case EO#eo.found_prompt of + false -> + LastLine = log_lines_not_last(Data), + %% Rest==[] because no prompt is found + {continue,[prompt|Patterns],Acc,LastLine}; + PromptType -> + log_lines(Data), + try_cont_log("<b>PROMPT:</b> ~s", [PromptType]), + seq_expect(Rest,Patterns,[{prompt,PromptType}|Acc],EO) + end; +seq_expect1(Data,[{prompt,PromptType}|Patterns],Acc,Rest,EO) -> + case EO#eo.found_prompt of + false -> + LastLine = log_lines_not_last(Data), + %% Rest==[] because no prompt is found + {continue,[{prompt,PromptType}|Patterns],Acc,LastLine}; + PromptType -> + log_lines(Data), + try_cont_log("<b>PROMPT:</b> ~s", [PromptType]), + seq_expect(Rest,Patterns,[{prompt,PromptType}|Acc],EO); + _OtherPromptType -> + log_lines(Data), + seq_expect(Rest,[{prompt,PromptType}|Patterns],Acc,EO) + end; +seq_expect1(Data,[Pattern|Patterns],Acc,Rest,EO) -> + case match_lines(Data,[Pattern],EO) of + {match,Match,MatchRest} -> + seq_expect1(MatchRest,Patterns,[Match|Acc],Rest,EO); + {nomatch,prompt} -> + seq_expect(Rest,[Pattern|Patterns],Acc,EO); + {nomatch,NoMatchRest} when Rest==[] -> + %% The data did not end with a prompt + {continue,[Pattern|Patterns],Acc,NoMatchRest}; + {halt,Why,HaltRest} -> + {halt,Why,HaltRest++Rest} + end; +seq_expect1(Data,[],Acc,Rest,_EO) -> + {match,lists:reverse(Acc),Data++Rest}. + +%% Split prompt-chunk at lines +match_lines(Data,Patterns,EO) -> + FoundPrompt = EO#eo.found_prompt, + case one_line(Data,[]) of + {noline,Rest} when FoundPrompt=/=false -> + %% This is the line including the prompt + case match_line(Rest,Patterns,FoundPrompt,EO) of + nomatch -> + {nomatch,prompt}; + {Tag,Match} -> + {Tag,Match,[]} + end; + {noline,Rest} -> + {nomatch,Rest}; + {Line,Rest} -> + case match_line(Line,Patterns,false,EO) of + nomatch -> + match_lines(Rest,Patterns,EO); + {Tag,Match} -> + {Tag,Match,Rest} + end + end. + + +%% For one line, match each pattern +match_line(Line,Patterns,FoundPrompt,EO) -> + match_line(Line,Patterns,FoundPrompt,EO,match). + +match_line(Line,[prompt|Patterns],false,EO,RetTag) -> + match_line(Line,Patterns,false,EO,RetTag); +match_line(Line,[prompt|_Patterns],FoundPrompt,_EO,RetTag) -> + try_cont_log(" ~s", [Line]), + try_cont_log("<b>PROMPT:</b> ~s", [FoundPrompt]), + {RetTag,{prompt,FoundPrompt}}; +match_line(Line,[{prompt,PromptType}|_Patterns],FoundPrompt,_EO,RetTag) + when PromptType==FoundPrompt -> + try_cont_log(" ~s", [Line]), + try_cont_log("<b>PROMPT:</b> ~s", [FoundPrompt]), + {RetTag,{prompt,FoundPrompt}}; +match_line(Line,[{prompt,PromptType}|Patterns],FoundPrompt,EO,RetTag) + when PromptType=/=FoundPrompt -> + match_line(Line,Patterns,FoundPrompt,EO,RetTag); +match_line(Line,[{Tag,Pattern}|Patterns],FoundPrompt,EO,RetTag) -> + case re:run(Line,Pattern,[{capture,all,list}]) of + nomatch -> + match_line(Line,Patterns,FoundPrompt,EO,RetTag); + {match,Match} -> + try_cont_log("<b>MATCH:</b> ~s", [Line]), + {RetTag,{Tag,Match}} + end; +match_line(Line,[Pattern|Patterns],FoundPrompt,EO,RetTag) -> + case re:run(Line,Pattern,[{capture,all,list}]) of + nomatch -> + match_line(Line,Patterns,FoundPrompt,EO,RetTag); + {match,Match} -> + try_cont_log("<b>MATCH:</b> ~s", [Line]), + {RetTag,Match} + end; +match_line(Line,[],FoundPrompt,EO,match) -> + match_line(Line,EO#eo.haltpatterns,FoundPrompt,EO,halt); +match_line(Line,[],_FoundPrompt,_EO,halt) -> + try_cont_log(" ~s", [Line]), + nomatch. + +one_line([$\n|Rest],Line) -> + {lists:reverse(Line),Rest}; +one_line([$\r|Rest],Line) -> + one_line(Rest,Line); +one_line([0|Rest],Line) -> + one_line(Rest,Line); +one_line([Char|Rest],Line) -> + one_line(Rest,[Char|Line]); +one_line([],Line) -> + {noline,lists:reverse(Line)}. + +debug_log_lines(String) -> + Old = put(silent,true), + log_lines(String), + put(silent,Old). + +log_lines(String) -> + case log_lines_not_last(String) of + [] -> + ok; + LastLine -> + try_cont_log(" ~s", [LastLine]) + end. + +log_lines_not_last(String) -> + case add_tabs(String,[],[]) of + {[],LastLine} -> + LastLine; + {String1,LastLine} -> + try_cont_log("~s",[String1]), + LastLine + end. + +add_tabs([0|Rest],Acc,LastLine) -> + add_tabs(Rest,Acc,LastLine); +add_tabs([$\r|Rest],Acc,LastLine) -> + add_tabs(Rest,Acc,LastLine); +add_tabs([$\n|Rest],Acc,LastLine) -> + add_tabs(Rest,[$\n|LastLine] ++ [$\s,$\s,$\s,$\s,$\s,$\s,$\s|Acc],[]); +add_tabs([Ch|Rest],Acc,LastLine) -> + add_tabs(Rest,Acc,[Ch|LastLine]); +add_tabs([],[$\n|Acc],LastLine) -> + {lists:reverse(Acc),lists:reverse(LastLine)}; +add_tabs([],[],LastLine) -> + {[],lists:reverse(LastLine)}. + + + + +%%% @hidden +teln_receive_until_prompt(Pid,Prx,Timeout) -> + Fun = fun() -> teln_receive_until_prompt(Pid,Prx,[],[]) end, + ct_gen_conn:do_within_time(Fun, Timeout). + +teln_receive_until_prompt(Pid,Prx,Acc,LastLine) -> + {ok,Data} = ct_telnet_client:get_data(Pid), + case check_for_prompt(Prx,LastLine ++ Data) of + {prompt,Lines,PromptType,Rest} -> + Return = lists:reverse(lists:append([Lines|Acc])), + {ok,Return,PromptType,Rest}; + {noprompt,Lines,LastLine1} -> + teln_receive_until_prompt(Pid,Prx,[Lines|Acc],LastLine1) + end. + +check_for_prompt(Prx,Data) -> + case match_prompt(Data,Prx) of + {prompt,UptoPrompt,PromptType,Rest} -> + {RevLines,LastLine} = split_lines(UptoPrompt), + {prompt,[LastLine|RevLines],PromptType,Rest}; + noprompt -> + {RevLines,Rest} = split_lines(Data), + {noprompt,RevLines,Rest} + end. + +split_lines(String) -> + split_lines(String,[],[]). +split_lines([$\n|Rest],Line,Lines) -> + split_lines(Rest,[],[lists:reverse(Line)|Lines]); +split_lines([$\r|Rest],Line,Lines) -> + split_lines(Rest,Line,Lines); +split_lines([0|Rest],Line,Lines) -> + split_lines(Rest,Line,Lines); +split_lines([Char|Rest],Line,Lines) -> + split_lines(Rest,[Char|Line],Lines); +split_lines([],Line,Lines) -> + {Lines,lists:reverse(Line)}. + + +match_prompt(Str,Prx) -> + match_prompt(Str,Prx,[]). +match_prompt(Str,Prx,Acc) -> + case re:run(Str,Prx) of + nomatch -> + noprompt; + {match,[{Start,Len}]} -> + case split_prompt_string(Str,Start+1,Start+Len,1,[],[]) of + {noprompt,Done,Rest} -> + match_prompt(Rest,Prx,Done); + {prompt,UptoPrompt,Prompt,Rest} -> + {prompt,lists:reverse(UptoPrompt++Acc), + lists:reverse(Prompt),Rest} + end + end. + +split_prompt_string([Ch|Str],Start,End,N,UptoPrompt,Prompt) when N<Start -> + split_prompt_string(Str,Start,End,N+1,[Ch|UptoPrompt],Prompt); +split_prompt_string([Ch|Str],Start,End,N,UptoPrompt,Prompt) + when N>=Start, N<End-> + split_prompt_string(Str,Start,End,N+1,UptoPrompt,[Ch|Prompt]); +split_prompt_string([Ch|Rest],_Start,End,N,UptoPrompt,Prompt) when N==End -> + case UptoPrompt of + [$",$=,$T,$P,$M,$O,$R,$P|_] -> + %% This is a line from "listenv", it is not a real prompt + {noprompt,[Ch|Prompt]++UptoPrompt,Rest}; + [$\s,$t,$s,$a|_] when Prompt==":nigol" -> + %% This is probably the "Last login:" statement which is + %% written when telnet connection is openend. + {noprompt,[Ch|Prompt]++UptoPrompt,Rest}; + _ -> + {prompt,[Ch|Prompt]++UptoPrompt,[Ch|Prompt],Rest} + end. |