%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2002-2010. 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% %% %% -module(httpc_manager). -behaviour(gen_server). -include("httpc_internal.hrl"). -include("http_internal.hrl"). %% Internal Application API -export([ start_link/3, request/2, cancel_request/2, request_canceled/2, retry_request/2, redirect_request/2, insert_session/2, delete_session/2, set_options/2, store_cookies/3, which_cookies/1, which_cookies/2, reset_cookies/1, session_type/1, info/1 ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, { cancel = [], % [{RequestId, HandlerPid, ClientPid}] handler_db, % ets() - Entry: #handler_info{} cookie_db, % cookie_db() session_db, % ets() - Entry: #tcp_session{} profile_name, % atom() options = #options{} }). -record(handler_info, { id, % Id of the request: request_id() starter, % Pid of the handler starter process (temp): pid() handler, % Pid of the handler process: pid() from, % From for the request: from() state % State of the handler: initiating | operational | canceled }). %% Entries in the handler / request cross-ref table %% -record(request_info, %% { %% id, % Id of the request %% handler, % Pid of the handler process %% from, % The From value for the caller %% mref % Monitor ref for the caller %% }). %%==================================================================== %% Internal Application API %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link(ProfileName, CookieDir, ManagedHow) -> {ok, Pid} %% %% ProfileName - httpc_manager_ %% CookieDir - directory() %% ManagedHow - stand_alone | inets %% %% Description: Starts the http request manager process. %% (If ManagedHow = inets then started by the inets supervisor.) %%-------------------------------------------------------------------- start_link(Profile, CookieDir, stand_alone) -> ProfileName = httpc:profile_name("stand_alone_", Profile), Args = [ProfileName, CookieDir], Opts = [], %% Opts = [{debug, [log, statistics]}], gen_server:start_link(?MODULE, Args, Opts); start_link(Profile, CookieDir, _) -> ProfileName = httpc:profile_name(Profile), Server = {local, ProfileName}, Args = [ProfileName, CookieDir], Opts = [], %% Opts = [{debug, [log, statistics]}], gen_server:start_link(Server, ?MODULE, Args, Opts). %%-------------------------------------------------------------------- %% Function: request(Request, ProfileName) -> %% {ok, Requestid} | {error, Reason} %% Request = #request{} %% ProfileName = atom() %% %% Description: Sends a request to the httpc manager process. %%-------------------------------------------------------------------- request(Request, ProfileName) -> call(ProfileName, {request, Request}). %%-------------------------------------------------------------------- %% Function: retry_request(Request, ProfileName) -> _ %% Request = #request{} %% ProfileName = atom() %% %% Description: Resends a request to the httpc manager process, intended %% to be called by the httpc handler process if it has to terminate with %% a non empty pipeline. %%-------------------------------------------------------------------- retry_request(Request, ProfileName) -> cast(ProfileName, {retry_or_redirect_request, Request}). %%-------------------------------------------------------------------- %% Function: redirect_request(Request, ProfileName) -> _ %% Request = #request{} %% ProfileName = atom() %% %% Description: Sends an atoumatic redirect request to the httpc %% manager process, intended to be called by the httpc handler process %% when the automatic redirect option is set. %%-------------------------------------------------------------------- redirect_request(Request, ProfileName) -> cast(ProfileName, {retry_or_redirect_request, Request}). %%-------------------------------------------------------------------- %% Function: cancel_request(RequestId, ProfileName) -> ok %% RequestId - ref() %% ProfileName = atom() %% %% Description: Cancels the request with . %%-------------------------------------------------------------------- cancel_request(RequestId, ProfileName) -> call(ProfileName, {cancel_request, RequestId}). %%-------------------------------------------------------------------- %% Function: request_canceled(RequestId, ProfileName) -> ok %% RequestId - ref() %% ProfileName = atom() %% %% Description: Confirms that a request has been canceld. Intended to %% be called by the httpc handler process. %%-------------------------------------------------------------------- request_canceled(RequestId, ProfileName) -> cast(ProfileName, {request_canceled, RequestId}). %%-------------------------------------------------------------------- %% Function: insert_session(Session, ProfileName) -> _ %% Session - #tcp_session{} %% ProfileName - atom() %% %% Description: Inserts session information into the httpc manager %% table _session_db. Intended to be called by %% the httpc request handler process. %%-------------------------------------------------------------------- insert_session(Session, ProfileName) -> SessionDbName = session_db_name(ProfileName), ?hcrt("insert session", [{session, Session}, {profile, ProfileName}]), ets:insert(SessionDbName, Session). %%-------------------------------------------------------------------- %% Function: delete_session(SessionId, ProfileName) -> _ %% SessionId - {{Host, Port}, HandlerPid} %% ProfileName - atom() %% %% Description: Deletes session information from the httpc manager %% table httpc_manager_session_db_. Intended to be called by %% the httpc request handler process. %%-------------------------------------------------------------------- delete_session(SessionId, ProfileName) -> SessionDbName = session_db_name(ProfileName), ?hcrt("delete session", [{session_is, SessionId}, {profile, ProfileName}]), ets:delete(SessionDbName, SessionId). %%-------------------------------------------------------------------- %% Function: set_options(Options, ProfileName) -> ok %% %% Options = [Option] %% Option = {proxy, {Proxy, [NoProxy]}} %% | {max_pipeline_length, integer()} | %% {max_sessions, integer()} | {pipeline_timeout, integer()} %% Proxy = {Host, Port} %% NoProxy - [Domain | HostName | IPAddress] %% Max - integer() %% ProfileName = atom() %% %% Description: Sets the options to be used by the client. %%-------------------------------------------------------------------- set_options(Options, ProfileName) -> cast(ProfileName, {set_options, Options}). %%-------------------------------------------------------------------- %% Function: store_cookies(Cookies, Address, ProfileName) -> ok %% %% Cookies = [Cookie] %% Cookie = #http_cookie{} %% ProfileName = atom() %% %% Description: Stores cookies from the server. %%-------------------------------------------------------------------- store_cookies([], _, _) -> ok; store_cookies(Cookies, Address, ProfileName) -> cast(ProfileName, {store_cookies, {Cookies, Address}}). %%-------------------------------------------------------------------- %% Function: reset_cookies(ProfileName) -> void() %% %% Url = string() %% ProfileName = atom() %% %% Description: Resets the cookie database %%-------------------------------------------------------------------- reset_cookies(ProfileName) -> call(ProfileName, reset_cookies). %%-------------------------------------------------------------------- %% Function: which_cookies(Url, ProfileName) -> [cookie()] %% %% Url = string() %% ProfileName = atom() %% %% Description: Retrieves the cookies that would be sent when %% requesting . %%-------------------------------------------------------------------- which_cookies(ProfileName) -> call(ProfileName, which_cookies). which_cookies(Url, ProfileName) -> call(ProfileName, {which_cookies, Url}). %%-------------------------------------------------------------------- %% Function: info(ProfileName) -> list() %% %% ProfileName = atom() %% %% Description: Retrieves various info about the manager and the %% handlers it manages %%-------------------------------------------------------------------- info(ProfileName) -> call(ProfileName, info). %%-------------------------------------------------------------------- %% Function: session_type(Options) -> ok %% %% Options = #options{} %% %% Description: Determines if to use pipelined sessions or not. %%-------------------------------------------------------------------- session_type(#options{pipeline_timeout = 0}) -> keep_alive; session_type(_) -> pipeline. %%==================================================================== %% gen_server callback functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init([ProfileName, CookiesConf]) -> {ok, State} | %% {ok, State, Timeout} | ignore |{stop, Reason} %% Description: Initiates the httpc_manger process %%-------------------------------------------------------------------- init([ProfileName, CookiesDir]) -> process_flag(trap_exit, true), ?hcrv("starting", [{profile, ProfileName}]), case (catch do_init(ProfileName, CookiesDir)) of {ok, _} = OK -> ?hcrd("started", [OK]), OK; {error, Reason} -> {stop, Reason}; Crap -> {stop, Crap} end. do_init(ProfileName, CookiesDir) -> %% Create session db ?hcrt("create session db", []), SessionDbName = session_db_name(ProfileName), ets:new(SessionDbName, [public, set, named_table, {keypos, #tcp_session.id}]), %% Create handler db ?hcrt("create handler/request db", []), HandlerDbName = handler_db_name(ProfileName), ets:new(HandlerDbName, [protected, set, named_table, {keypos, #handler_info.id}]), %% Cookie DB ?hcrt("create cookie db", []), SessionCookieDbName = session_cookie_db_name(ProfileName), CookieDbName = cookie_db_name(ProfileName), CookieDb = httpc_cookie:open_db(CookieDbName, CookiesDir, SessionCookieDbName), State = #state{handler_db = HandlerDbName, cookie_db = CookieDb, session_db = SessionDbName, profile_name = ProfileName}, {ok, State}. %%-------------------------------------------------------------------- %% Function: handle_call(Request, From, State) -> {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %% Description: Handling call messages %%-------------------------------------------------------------------- handle_call({request, Request}, _From, State) -> ?hcrv("request", [{request, Request}]), case (catch handle_request(Request, State)) of {ok, ReqId, NewState} -> {reply, {ok, ReqId}, NewState}; Error -> NewError = {error, {failed_process_request, Error}}, {reply, NewError, State} end; handle_call({cancel_request, RequestId}, From, #state{handler_db = HandlerDb} = State) -> ?hcrv("cancel_request", [{request_id, RequestId}]), case ets:lookup(State#state.handler_db, RequestId) of [] -> ?hcrd("nothing to cancel", []), Reply = ok, %% Nothing to cancel {reply, Reply, State}; [#handler_info{handler = Pid}] when is_pid(Pid) -> ?hcrd("found operational handler for this request", [{handler, Pid}]), httpc_handler:cancel(RequestId, Pid), {noreply, State#state{cancel = [{RequestId, Pid, From} | State#state.cancel]}}; [#handler_info{starter = Pid, state = HandlerState}] when is_pid(Pid) -> ?hcri("found *initiating* handler for this request", [{starter, Pid}, {state, HandlerState}]), ets:update_element(HandlerDb, RequestId, {#handler_info.state, canceled}), {noreply, State#state{cancel = [{RequestId, Pid, From} | State#state.cancel]}} end; handle_call(reset_cookies, _, #state{cookie_db = CookieDb} = State) -> ?hcrv("reset cookies", []), httpc_cookie:reset_db(CookieDb), {reply, ok, State}; handle_call(which_cookies, _, #state{cookie_db = CookieDb} = State) -> ?hcrv("which cookies", []), CookieHeaders = httpc_cookie:which_cookies(CookieDb), {reply, CookieHeaders, State}; handle_call({which_cookies, Url}, _, #state{cookie_db = CookieDb} = State) -> ?hcrv("which cookies", [{url, Url}]), case http_uri:parse(Url) of {Scheme, _, Host, Port, Path, _} -> CookieHeaders = httpc_cookie:header(CookieDb, Scheme, {Host, Port}, Path), {reply, CookieHeaders, State}; Msg -> {reply, Msg, State} end; handle_call(info, _, State) -> ?hcrv("info", []), Info = get_manager_info(State), {reply, Info, State}; handle_call(Req, From, #state{profile_name = ProfileName} = State) -> error_report(ProfileName, "received unkown request" "~n Req: ~p" "~n From: ~p", [Req, From]), {reply, {error, 'API_violation'}, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %% Description: Handling cast messages %%-------------------------------------------------------------------- handle_cast({retry_or_redirect_request, {Time, Request}}, #state{profile_name = ProfileName} = State) -> ?hcrv("retry or redirect request", [{time, Time}, {request, Request}]), case timer:apply_after(Time, ?MODULE, retry_request, [Request, ProfileName]) of {ok, _} -> {noreply, State}; {error, Reason} -> error_report(ProfileName, "failed scheduling retry/redirect request" "~n Time: ~p" "~n Request: ~p" "~n Reason: ~p", [Time, Request, Reason]), {noreply, State} end; handle_cast({retry_or_redirect_request, Request}, #state{profile_name = Profile, handler_db = HandlerDb} = State) -> ?hcrv("retry or redirect request", [{request, Request}]), case (catch handle_request(Request, State)) of {ok, _, NewState} -> {noreply, NewState}; Error -> ReqId = Request#request.id, error_report(Profile, "failed to retry or redirect request ~p" "~n Error: ~p", [ReqId, Error]), case ets:lookup(HandlerDb, ReqId) of [#handler_info{from = From}] -> Error2 = httpc_response:error(Request, Error), httpc_response:send(From, Error2), ok; _ -> ok end, {noreply, State} end; handle_cast({request_canceled, RequestId}, State) -> ?hcrv("request canceled", [{request_id, RequestId}]), ets:delete(State#state.handler_db, RequestId), case lists:keysearch(RequestId, 1, State#state.cancel) of {value, Entry = {RequestId, _, From}} -> ?hcrt("found in cancel", [{from, From}]), gen_server:reply(From, ok), {noreply, State#state{cancel = lists:delete(Entry, State#state.cancel)}}; Else -> ?hcrt("not found in cancel", [{else, Else}]), {noreply, State} end; handle_cast({set_options, Options}, State = #state{options = OldOptions}) -> ?hcrv("set options", [{options, Options}, {old_options, OldOptions}]), NewOptions = #options{proxy = get_proxy(Options, OldOptions), pipeline_timeout = get_pipeline_timeout(Options, OldOptions), max_pipeline_length = get_max_pipeline_length(Options, OldOptions), max_keep_alive_length = get_max_keep_alive_length(Options, OldOptions), keep_alive_timeout = get_keep_alive_timeout(Options, OldOptions), max_sessions = get_max_sessions(Options, OldOptions), cookies = get_cookies(Options, OldOptions), ipfamily = get_ipfamily(Options, OldOptions), ip = get_ip(Options, OldOptions), port = get_port(Options, OldOptions), verbose = get_verbose(Options, OldOptions), socket_opts = get_socket_opts(Options, OldOptions) }, case {OldOptions#options.verbose, NewOptions#options.verbose} of {Same, Same} -> ok; {_, false} -> dbg:stop(); {false, Level} -> dbg:tracer(), handle_verbose(Level); {_, Level} -> dbg:stop(), dbg:tracer(), handle_verbose(Level) end, {noreply, State#state{options = NewOptions}}; handle_cast({store_cookies, _}, State = #state{options = #options{cookies = disabled}}) -> {noreply, State}; handle_cast({store_cookies, {Cookies, _}}, State) -> ok = do_store_cookies(Cookies, State), {noreply, State}; handle_cast(Msg, #state{profile_name = ProfileName} = State) -> error_report(ProfileName, "recived unknown message" "~n Msg: ~p", [Msg]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %% Description: Handling all non call/cast messages %%--------------------------------------------------------- handle_info({connect_and_send, StarterPid, ReqId, HandlerPid, Res}, State) -> handle_connect_and_send(StarterPid, ReqId, HandlerPid, Res, State), {noreply, State}; handle_info({failed_starting_handler, StarterPid, ReqId, Res}, State) -> handle_failed_starting_handler(StarterPid, ReqId, Res, State), {noreply, State}; handle_info({'EXIT', Pid, Reason}, #state{handler_db = HandlerDb} = State) -> maybe_handle_terminating_starter(Pid, Reason, HandlerDb), {noreply, State}; handle_info({'DOWN', _, _, Pid, _}, State) -> %% %% Check what happens to waiting requests! Chall we not send a reply? %% Pattern = #handler_info{handler = Pid, _ = '_'}, ets:match_delete(State#state.handler_db, Pattern), %% If there where any canceled request, handled by the %% the process that now has terminated, the %% cancelation can be viewed as sucessfull! NewCanceledList = lists:foldl(fun({_, HandlerPid, From} = Entry, Acc) -> case HandlerPid of Pid -> gen_server:reply(From, ok), lists:delete(Entry, Acc); _ -> Acc end end, State#state.cancel, State#state.cancel), {noreply, State#state{cancel = NewCanceledList}}; handle_info(Info, #state{profile_name = ProfileName} = State) -> error_report(ProfileName, "received unknown info" "~n Info: ~p", [Info]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) -> _ (ignored by gen_server) %% Description: Shutdown the httpc_handler %%-------------------------------------------------------------------- terminate(_, State) -> httpc_cookie:close_db(State#state.cookie_db), ets:delete(State#state.session_db), ets:delete(State#state.handler_db). %%-------------------------------------------------------------------- %% Func: code_change(_OldVsn, State, Extra) -> {ok, NewState} %% Purpose: Convert process state when code is changed %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- get_manager_info(#state{handler_db = HDB, cookie_db = CDB} = _State) -> HandlerInfo = get_handler_info(HDB), CookieInfo = httpc_cookie:which_cookies(CDB), [{handlers, HandlerInfo}, {cookies, CookieInfo}]. get_handler_info(Tab) -> Pattern = #handler_info{handler = '$1', state = '$2', _ = '_'}, Handlers1 = [{Pid, State} || [Pid, State] <- ets:match(Tab, Pattern)], F = fun({Pid, State} = Elem, Acc) when State =/= canceled -> case lists:keymember(Pid, 1, Acc) of true -> Acc; false -> [Elem | Acc] end; (_, Acc) -> Acc end, Handlers2 = lists:foldl(F, [], Handlers1), Handlers3 = [{Pid, State, httpc_handler:info(Pid)} || {Pid, State} <- Handlers2], Handlers3. %% %% The request handler process is started asynchronously by a %% "starter process". When that process terminates it sends %% one of two messages. These ara handled by the two functions %% below. %% handle_connect_and_send(_StarterPid, ReqId, HandlerPid, Result, #state{profile_name = Profile, handler_db = HandlerDb}) -> case ets:lookup(HandlerDb, ReqId) of [#handler_info{state = initiating} = HandlerInfo] when Result =:= ok -> ?hcri("received connect-and-send ack for initiating handler", []), HandlerInfo2 = HandlerInfo#handler_info{starter = undefined, handler = HandlerPid, state = operational}, ets:insert(HandlerDb, HandlerInfo2), ok; [#handler_info{state = canceled} = HandlerInfo] when Result =:= ok -> ?hcri("received connect-and-send ack for canceled handler", []), httpc_handler:cancel(ReqId, HandlerPid), HandlerInfo2 = HandlerInfo#handler_info{starter = undefined, handler = HandlerPid}, ets:insert(HandlerDb, HandlerInfo2), ok; [#handler_info{from = From}] -> error_report(Profile, "handler (~p) failed to connect and/or " "send request ~p" "~n Error: ~p", [HandlerPid, ReqId, Result]), ?hcri("received connect-and-send error", [{result, Result}]), Reason2 = case Result of {error, Reason} -> {failed_connecting, Reason}; _ -> {failed_connecting, Result} end, DummyReq = #request{id = ReqId}, httpc_response:send(From, httpc_response:error(DummyReq, Reason2)), %% gen_server:reply(From, Error), ets:delete(HandlerDb, ReqId), ok; [] -> error_report(Profile, "handler successfully (~p) started for unknown request ~p", [HandlerPid, ReqId]), httpc_handler:cancel(ReqId, HandlerPid) end. handle_failed_starting_handler(_StarterPid, ReqId, Error, #state{profile_name = Profile, handler_db = HandlerDb}) -> case ets:lookup(HandlerDb, ReqId) of [#handler_info{state = canceled, from = From}] -> error_report(Profile, "failed starting handler for request ~p" "~n Error: ~p", [ReqId, Error]), request_canceled(Profile, ReqId), % Fake signal from handler gen_server:reply(From, Error), ets:delete(HandlerDb, ReqId), ok; [#handler_info{from = From}] -> error_report(Profile, "failed starting handler for request ~p" "~n Error: ~p", [ReqId, Error]), gen_server:reply(From, Error), ets:delete(HandlerDb, ReqId), ok; [] -> error_report(Profile, "failed starting handler for unknown request ~p" "~n Error: ~p", [ReqId, Error]), ok end. maybe_handle_terminating_starter(MeybeStarterPid, Reason, HandlerDb) -> Pattern = #handler_info{starter = MeybeStarterPid, _ = '_'}, case ets:match_object(HandlerDb, Pattern) of [#handler_info{id = ReqId, from = From, state = initiating}] -> Error = {error, {failed_starting_request_handler, Reason}}, gen_server:reply(From, Error), ets:delete(HandlerDb, ReqId), ok; _ -> ok end. %% ----- %% Act as an HTTP/0.9 client that does not know anything %% about persistent connections handle_request(#request{settings = #http_options{version = "HTTP/0.9"}} = Request0, State) -> Request1 = handle_cookies(generate_request_id(Request0), State), Hdrs0 = Request1#request.headers, Hdrs1 = Hdrs0#http_request_h{connection = undefined}, Request2 = Request1#request{headers = Hdrs1}, create_handler_starter(Request2, State), {ok, Request2#request.id, State}; %% ----- %% Act as an HTTP/1.0 client that does not %% use persistent connections handle_request(#request{settings = #http_options{version = "HTTP/1.0"}} = Request0, State) -> Request1 = handle_cookies(generate_request_id(Request0), State), Hdrs0 = Request1#request.headers, Hdrs1 = Hdrs0#http_request_h{connection = "close"}, Request2 = Request1#request{headers = Hdrs1}, create_handler_starter(Request2, State), {ok, Request2#request.id, State}; %% ----- handle_request(#request{method = Method, address = Address, scheme = Scheme} = Request0, #state{options = Opts} = State) -> Request1 = handle_cookies(generate_request_id(Request0), State), SessionType = session_type(Opts), case select_session(Method, Address, Scheme, SessionType, State) of {ok, HandlerPid} -> pipeline_or_keep_alive(Request1, HandlerPid, State); no_connection -> create_handler_starter(Request1, State); {no_session, OpenSessions} when OpenSessions < Opts#options.max_sessions -> create_handler_starter(Request1, State); {no_session, _} -> %% Do not start any more persistent connections %% towards this server. Hdrs0 = Request1#request.headers, Hdrs1 = Hdrs0#http_request_h{connection = "close"}, Request2 = Request1#request{headers = Hdrs1}, create_handler_starter(Request2, State) end, {ok, Request1#request.id, State}. select_session(Method, HostPort, Scheme, SessionType, #state{options = #options{max_pipeline_length = MaxPipe, max_keep_alive_length = MaxKeepAlive}, session_db = SessionDb}) -> ?hcrd("select session", [{session_type, SessionType}, {max_pipeline_length, MaxPipe}, {max_keep_alive_length, MaxKeepAlive}]), case httpc_request:is_idempotent(Method) orelse (SessionType =:= keep_alive) of true -> %% Look for handlers connecting to this host (HostPort) %% tcp_session with record name field (tcp_session) and %% socket fields ignored. The fields id (part of: HostPort), %% client_close, scheme and type specified. %% The fields id (part of: HandlerPid) and queue_length %% specified. Pattern = #tcp_session{id = {HostPort, '$1'}, client_close = false, scheme = Scheme, socket = '_', queue_length = '$2', type = SessionType}, %% {'_', {HostPort, '$1'}, false, Scheme, '_', '$2', SessionTyp}, Candidates = ets:match(SessionDb, Pattern), ?hcrd("select session", [{host_port, HostPort}, {scheme, Scheme}, {type, SessionType}, {candidates, Candidates}]), select_session(Candidates, MaxKeepAlive, MaxPipe, SessionType); false -> no_connection end. select_session(Candidates, Max, _, keep_alive) -> select_session(Candidates, Max); select_session(Candidates, _, Max, pipeline) -> select_session(Candidates, Max). select_session([] = _Candidates, _Max) -> ?hcrd("select session - no candicate", []), no_connection; select_session(Candidates, Max) -> NewCandidates = [{Pid, Length} || [Pid, Length] <- Candidates, Length =< Max], case lists:keysort(2, NewCandidates) of [] -> {no_session, length(Candidates)}; [{HandlerPid, _} | _] -> ?hcrd("select session - found one", [{handler, HandlerPid}]), {ok, HandlerPid} end. pipeline_or_keep_alive(#request{id = Id} = Request, HandlerPid, State) -> ?hcrd("pipeline of keep-alive", [{id, Id}, {handler, HandlerPid}]), case (catch httpc_handler:send(Request, HandlerPid)) of ok -> ?hcrd("pipeline or keep-alive - successfully sent", []), Entry = #handler_info{id = Id, handler = HandlerPid, state = operational}, ets:insert(State#state.handler_db, Entry); _ -> %% timeout pipelining failed ?hcrd("pipeline or keep-alive - failed sending -> " "start a new handler", []), create_handler_starter(Request, State) end. create_handler_starter(#request{socket_opts = SocketOpts} = Request, #state{options = Options} = State) when is_list(SocketOpts) -> %% The user provided us with (override) socket options ?hcrt("create handler starter", [{socket_opts, SocketOpts}, {options, Options}]), Options2 = Options#options{socket_opts = SocketOpts}, create_handler_starter(Request#request{socket_opts = undefined}, State#state{options = Options2}); create_handler_starter(#request{id = Id, from = From} = Request, #state{profile_name = ProfileName, options = Options, handler_db = HandlerDb} = _State) -> ?hcrv("create handler starter", [{id, Id}, {profile, ProfileName}]), IsInetsManager = is_inets_manager(), ManagerPid = self(), StarterFun = fun() -> ?hcrd("handler starter - start", [{id, Id}, {profile, ProfileName}, {inets_manager, IsInetsManager}]), Result1 = case IsInetsManager of true -> httpc_handler_sup:start_child(Options, ProfileName); false -> httpc_handler:start_link(Options, ProfileName) end, ?hcrd("handler starter - maybe connect and send", [{id, Id}, {profile, ProfileName}, {result, Result1}]), case Result1 of {ok, HandlerPid} -> Result2 = httpc_handler:connect_and_send(Request, HandlerPid), ?hcrd("handler starter - connected and sent", [{id, Id}, {profile, ProfileName}, {handler, HandlerPid}, {result, Result2}]), ConnAndSendMessage = {connect_and_send, self(), Id, HandlerPid, Result2}, ManagerPid ! ConnAndSendMessage; {error, Reason} -> StartFailureMessage = {failed_starting_handler, self(), Id, Reason}, ManagerPid ! StartFailureMessage; _ -> StartFailureMessage = {failed_starting_handler, self(), Id, Result1}, ManagerPid ! StartFailureMessage end end, Starter = erlang:spawn_link(StarterFun), ?hcrd("create handler starter - started", [{id, Id}, {starter, Starter}]), Entry = #handler_info{id = Id, starter = Starter, from = From, state = initiating}, ets:insert(HandlerDb, Entry), ok. is_inets_manager() -> case get('$ancestors') of [httpc_profile_sup | _] -> true; _ -> false end. generate_request_id(Request) -> case Request#request.id of undefined -> RequestId = make_ref(), Request#request{id = RequestId}; _ -> %% This is an automatic redirect or a retryed pipelined request %% => keep the old id. Request end. handle_cookies(Request, #state{options = #options{cookies = disabled}}) -> Request; handle_cookies( #request{scheme = Scheme, address = Address, path = Path, headers = #http_request_h{other = Other} = Hdrs} = Request, #state{cookie_db = CookieDb}) -> case httpc_cookie:header(CookieDb, Scheme, Address, Path) of {"cookie", ""} -> Request; CookieHeader -> NewHeaders = Hdrs#http_request_h{other = [CookieHeader | Other]}, Request#request{headers = NewHeaders} end. do_store_cookies([], _) -> ok; do_store_cookies([Cookie | Cookies], #state{cookie_db = CookieDb} = State) -> ok = httpc_cookie:insert(CookieDb, Cookie), do_store_cookies(Cookies, State). session_db_name(ProfileName) -> make_db_name(ProfileName, "__session_db"). cookie_db_name(ProfileName) -> make_db_name(ProfileName, "__cookie_db"). session_cookie_db_name(ProfileName) -> make_db_name(ProfileName, "__session_cookie_db"). handler_db_name(ProfileName) -> make_db_name(ProfileName, "__handler_db"). make_db_name(ProfileName, Post) -> list_to_atom(atom_to_list(ProfileName) ++ Post). call(ProfileName, Msg) -> Timeout = infinity, call(ProfileName, Msg, Timeout). call(ProfileName, Msg, Timeout) -> gen_server:call(ProfileName, Msg, Timeout). cast(ProfileName, Msg) -> gen_server:cast(ProfileName, Msg). get_proxy(Opts, #options{proxy = Default}) -> proplists:get_value(proxy, Opts, Default). get_pipeline_timeout(Opts, #options{pipeline_timeout = Default}) -> proplists:get_value(pipeline_timeout, Opts, Default). get_max_pipeline_length(Opts, #options{max_pipeline_length = Default}) -> proplists:get_value(max_pipeline_length, Opts, Default). get_max_keep_alive_length(Opts, #options{max_keep_alive_length = Default}) -> proplists:get_value(max_keep_alive_length, Opts, Default). get_keep_alive_timeout(Opts, #options{keep_alive_timeout = Default}) -> proplists:get_value(keep_alive_timeout, Opts, Default). get_max_sessions(Opts, #options{max_sessions = Default}) -> proplists:get_value(max_sessions, Opts, Default). get_cookies(Opts, #options{cookies = Default}) -> proplists:get_value(cookies, Opts, Default). get_ipfamily(Opts, #options{ipfamily = IpFamily}) -> case lists:keysearch(ipfamily, 1, Opts) of false -> case proplists:get_value(ipv6, Opts) of enabled -> inet6fb4; disabled -> inet; _ -> IpFamily end; {value, {_, Value}} -> Value end. get_ip(Opts, #options{ip = Default}) -> proplists:get_value(ip, Opts, Default). get_port(Opts, #options{port = Default}) -> proplists:get_value(port, Opts, Default). get_verbose(Opts, #options{verbose = Default}) -> proplists:get_value(verbose, Opts, Default). get_socket_opts(Opts, #options{socket_opts = Default}) -> proplists:get_value(socket_opts, Opts, Default). handle_verbose(debug) -> dbg:p(self(), [call]), dbg:tp(?MODULE, [{'_', [], [{return_trace}]}]); handle_verbose(trace) -> dbg:p(self(), [call]), dbg:tpl(?MODULE, [{'_', [], [{return_trace}]}]); handle_verbose(_) -> ok. error_report(Profile, F, A) -> Report = io_lib:format("HTTPC-MANAGER<~p> " ++ F ++ "~n", [Profile | A]), error_logger:error_report(Report). %% d(F) -> %% d(F, []). %% d(F, A) -> %% d(get(dbg), F, A). %% d(true, F, A) -> %% io:format(user, "~w:~w:" ++ F ++ "~n", [self(), ?MODULE | A]); %% d(_, _, _) -> %% ok.