let src_daemon = Logs.Src.create "happy-daemon" module Logd = (val Logs.src_log src_daemon : Logs.LOG) let src_client = Logs.Src.create "happy" module Logc = (val Logs.src_log src_client : Logs.LOG) let error_msgf fmt = Format.kasprintf (fun msg -> Error (`Msg msg)) fmt let pp_sockaddr ppf = function | Unix.ADDR_UNIX str -> Format.fprintf ppf "<%s>" str | Unix.ADDR_INET (inet_addr, port) -> Format.fprintf ppf "%s:%u" (Unix.string_of_inet_addr inet_addr) port let to_sockaddr (ipaddr, port) = Unix.ADDR_INET (Ipaddr_unix.to_inet_addr ipaddr, port) let clock = Mtime_clock.elapsed_ns [@@@warning "-30"] type state = ((Ipaddr.t * int) * Unix.file_descr) Miou.Computation.t and entry = Happy_eyeballs.id * attempt * [ `host ] Domain_name.t * addr and attempt = int and addr = Ipaddr.t * int and cancel = attempt * unit Miou.t and connect_ip = { aaaa_timeout : int64 option ; connect_delay : int64 option ; connect_timeout : int64 option ; state : state ; addrs : addr list } and connect = { aaaa_timeout : int64 option ; connect_delay : int64 option ; connect_timeout : int64 option ; resolve_timeout : int64 option ; resolve_retries : int option ; state : state ; host : [ `host ] Domain_name.t ; ports : int list } and action = [ `Connect_ip of connect_ip | `Connect of connect ] and connected = [ `Connected of entry * Miou_unix.Ownership.file_descr ] type event = [ connected | `Connection_failed of entry * string | `Resolution_v4 of [ `host ] Domain_name.t * (Ipaddr.V4.Set.t, [ `Msg of string ]) result | `Resolution_v6 of [ `host ] Domain_name.t * (Ipaddr.V6.Set.t, [ `Msg of string ]) result ] and getaddrinfo = [ `A | `AAAA ] -> [ `host ] Domain_name.t -> (Ipaddr.Set.t, [ `Msg of string ]) result [@@@warning "+30"] let getaddrinfo record domain_name = Logd.debug (fun m -> m "Resolve %a via the system's resolver" Domain_name.pp domain_name); let opt = match record with | `A -> [ Unix.AI_FAMILY Unix.PF_INET ] | `AAAA -> [ Unix.AI_FAMILY Unix.PF_INET6 ] in let opt = Unix.AI_SOCKTYPE Unix.SOCK_STREAM :: opt in match Unix.getaddrinfo (Domain_name.to_string domain_name) "" opt with | [] -> error_msgf "%a not found" Domain_name.pp domain_name | addrs -> let set = List.fold_left (fun set { Unix.ai_addr; _ } -> match ai_addr with | Unix.ADDR_INET (inet_addr, _) -> Ipaddr.Set.add (Ipaddr_unix.of_inet_addr inet_addr) set | Unix.ADDR_UNIX _ -> set) Ipaddr.Set.empty addrs in if Ipaddr.Set.is_empty set then error_msgf "%a not found as an inet service" Domain_name.pp domain_name else Ok set type t = { mutable cancel_connecting: cancel list Happy_eyeballs.Waiter_map.t ; mutable waiters: state Happy_eyeballs.Waiter_map.t ; condition: Miou.Condition.t ; mutex: Miou.Mutex.t ; queue: [ action | event ] Miou.Queue.t ; mutable set : bool ; mutable getaddrinfo: getaddrinfo ; timer_interval : float } let create timer_interval getaddrinfo = { cancel_connecting= Happy_eyeballs.Waiter_map.empty ; waiters= Happy_eyeballs.Waiter_map.empty ; condition= Miou.Condition.create () ; mutex= Miou.Mutex.create () ; queue= Miou.Queue.create () ; set= false ; getaddrinfo ; timer_interval } let try_connect t ~meta addr () = let id, attempt, _, _ = meta in let addr = to_sockaddr addr in Logd.debug (fun m -> m "connect to %a (%d:%d)" pp_sockaddr addr (Obj.magic id) attempt); let socket = match Unix.domain_of_sockaddr addr with | Unix.PF_UNIX -> let socket = Unix.socket ~cloexec:true Unix.PF_UNIX Unix.SOCK_STREAM 0 in Miou_unix.Ownership.of_file_descr socket | Unix.PF_INET -> Miou_unix.Ownership.tcpv4 () | Unix.PF_INET6 -> Miou_unix.Ownership.tcpv6 () in try Miou_unix.Ownership.connect socket addr; Logd.debug (fun m -> m "connected to %a (%d:%d)" pp_sockaddr addr (Obj.magic id) attempt); Miou.Ownership.transfer (Miou_unix.Ownership.resource socket); Miou.Mutex.protect t.mutex @@ fun () -> Miou.Queue.enqueue t.queue (`Connected (meta, socket)); Miou.Condition.signal t.condition with Unix.Unix_error (err, _, _) -> Logd.debug (fun m -> m "error connecting to %a: %s" pp_sockaddr addr (Unix.error_message err)); Miou_unix.Ownership.close socket; let msg = Fmt.str "error connecting to %a: %s" pp_sockaddr addr (Unix.error_message err) in Miou.Mutex.protect t.mutex @@ fun () -> Miou.Queue.enqueue t.queue (`Connection_failed (meta, msg)); Miou.Condition.signal t.condition let connect t ~prms:orphans host id attempt addr = let meta = (id, attempt, host, addr) in Logd.debug (fun m -> m "connect to %a (%d:%d)" Domain_name.pp host (Obj.magic id) attempt); let prm : unit Miou.t = Miou.async ~orphans (try_connect t ~meta addr) in let entry = (attempt, prm) in t.cancel_connecting <- Happy_eyeballs.Waiter_map.update id (function None -> Some [ entry ] | Some cs -> Some (entry :: cs)) t.cancel_connecting exception Connection_failed of [ `host ] Domain_name.t * string let empty_bt = Printexc.get_callstack 0 let connection_failed host reason = Connection_failed (host, reason), empty_bt let handle_one_action t ~prms action = match action with | Happy_eyeballs.Connect (host, id, attempt, addr) -> connect t ~prms host id attempt addr | Happy_eyeballs.Connect_failed (host, id, reason) -> Logd.debug (fun m -> m "connection to %a failed: %s" Domain_name.pp host reason); let cancel_connecting, others = Happy_eyeballs.Waiter_map.find_and_remove id t.cancel_connecting in t.cancel_connecting <- cancel_connecting; List.iter (fun (_, prm) -> Miou.cancel prm) (Option.value ~default:[] others); (* clean waiter *) let waiters, waiter = Happy_eyeballs.Waiter_map.find_and_remove id t.waiters in t.waiters <- waiters; let trans waiter = let err = connection_failed host reason in ignore (Miou.Computation.try_cancel waiter err) in Option.iter trans waiter | Happy_eyeballs.(Resolve_a host | Resolve_aaaa host) -> let record = match action with | Happy_eyeballs.Resolve_a _ -> `A | Happy_eyeballs.Resolve_aaaa _ -> `AAAA | _ -> assert false in let _ = Miou.async ~orphans:prms @@ fun () -> match t.getaddrinfo record host with | Ok result -> let ipv4, ipv6 = Ipaddr.Set.fold (fun ip (ipv4, ipv6) -> match ip with | Ipaddr.V4 v -> Ipaddr.V4.Set.add v ipv4, ipv6 | Ipaddr.V6 v -> ipv4, Ipaddr.V6.Set.add v ipv6) result Ipaddr.(V4.Set.empty, V6.Set.empty) in let result = match record with | `A -> if Ipaddr.V4.Set.is_empty ipv4 then `Resolution_v4 (host, error_msgf "%a unreachable via IPv4" Domain_name.pp host) else `Resolution_v4 (host, Ok ipv4) | `AAAA -> if Ipaddr.V6.Set.is_empty ipv6 then `Resolution_v6 (host, error_msgf "%a unreachable via IPv6" Domain_name.pp host) else `Resolution_v6 (host, Ok ipv6) in Miou.Mutex.protect t.mutex @@ fun () -> Miou.Queue.enqueue t.queue result; Miou.Condition.signal t.condition | Error err -> let result = match record with | `A -> `Resolution_v4 (host, Error err) | `AAAA -> `Resolution_v6 (host, Error err) in Miou.Mutex.protect t.mutex @@ fun () -> Miou.Queue.enqueue t.queue result; Miou.Condition.signal t.condition in () let to_event t = function | `Connection_failed ((id, attempt, host, addr), msg) -> let fold = function | None -> None | Some cs -> ( match List.filter (fun (att, _) -> not (att = attempt)) cs with | [] -> None | cs -> Some cs) in t.cancel_connecting <- Happy_eyeballs.Waiter_map.update id fold t.cancel_connecting; Happy_eyeballs.Connection_failed (host, id, addr, msg) | `Connected ((id, attempt, host, addr), fd) -> let cancel_connecting, others = Happy_eyeballs.Waiter_map.find_and_remove id t.cancel_connecting in t.cancel_connecting <- cancel_connecting; List.iter (fun (att, prm) -> if att <> attempt then Miou.cancel prm) (Option.value ~default:[] others); let waiters, waiter = Happy_eyeballs.Waiter_map.find_and_remove id t.waiters in t.waiters <- waiters; let () = match waiter with | None -> Logd.debug (fun m -> m "Loose a connected socket to %a (%a) (%d:%d)" Domain_name.pp host pp_sockaddr (to_sockaddr addr) (Obj.magic id) attempt); Miou_unix.Ownership.close fd | Some waiter -> (* NOTE(dinosaure): the task is suspended **before** [disown]. If a cancellation appear, [disown] is **not** executed and Miou properly close [fd]. If we are able to [disown], the responsability to close the [fd] falls to the person who requested the socket if we are able to transfer it. Otherwise, we just [Unix.close]. *) Miou.Ownership.disown (Miou_unix.Ownership.resource fd); let fd = Miou_unix.Ownership.to_file_descr fd in let set = Miou.Computation.try_return waiter (addr, fd) in Logd.debug (fun m -> m "file-descr transmitted? %b" set); if not set then Unix.close fd in Logd.debug (fun m -> m "connected to %a (%a) (%d:%d)" Domain_name.pp host pp_sockaddr (to_sockaddr addr) (Obj.magic id) attempt); Happy_eyeballs.Connected (host, id, addr) | `Resolution_v4 (host, Ok ips) -> Logd.debug (fun m -> m "%a resolved" Domain_name.pp host); Happy_eyeballs.Resolved_a (host, ips) | `Resolution_v4 (host, Error (`Msg msg)) -> Logd.debug (fun m -> m "impossible to resolve %a: %s" Domain_name.pp host msg); Happy_eyeballs.Resolved_a_failed (host, msg) | `Resolution_v6 (host, Ok ips) -> Logd.debug (fun m -> m "%a resolved" Domain_name.pp host); Happy_eyeballs.Resolved_aaaa (host, ips) | `Resolution_v6 (host, Error (`Msg msg)) -> Logd.debug (fun m -> m "impossible to resolve %a: %s" Domain_name.pp host msg); Happy_eyeballs.Resolved_aaaa_failed (host, msg) let to_actions t he user's_actions = let fold (he, actions) = function | `Connect_ip { aaaa_timeout ; connect_delay ; connect_timeout ; state ; addrs } -> let waiters, id = Happy_eyeballs.Waiter_map.register state t.waiters in t.waiters <- waiters; let he, actions' = Happy_eyeballs.connect_ip he (clock ()) ?aaaa_timeout ?connect_delay ?connect_timeout ~id addrs in (he, actions @ actions') | `Connect { aaaa_timeout ; connect_delay ; connect_timeout ; resolve_timeout ; resolve_retries ; state ; host ; ports } -> let waiters, id = Happy_eyeballs.Waiter_map.register state t.waiters in t.waiters <- waiters; let he, actions' = Happy_eyeballs.connect he (clock ()) ?aaaa_timeout ?connect_delay ?connect_timeout ?resolve_timeout ?resolve_retries ~id host ports in (he, actions @ actions') in List.fold_left fold (he, []) user's_actions let await_actions_or_events t () = Miou.Mutex.protect t.mutex @@ fun () -> while Miou.Queue.is_empty t.queue do Miou.Condition.wait t.condition t.mutex done exception Timeout let with_timeout ~timeout:ts fn = let timeout () = Miou_unix.sleep ts; raise Timeout in let prm1 = Miou.async timeout in let prm0 = Miou.async fn in Miou.await_first [ prm0; prm1 ] let continue t cont he = let fn () = match cont with | `Act -> with_timeout ~timeout:t.timer_interval (await_actions_or_events t) | `Suspend -> try Ok (await_actions_or_events t ()) with exn -> Error exn in match fn () with | Error Timeout -> (he, [], []) | Ok () -> let user's_actions_and_events = Miou.Queue.(to_list (transfer t.queue)) in Logd.debug (fun m -> m "got %d actions or events" (List.length user's_actions_and_events)); let user's_actions, events = List.partition_map (function | #action as action -> Either.Left action | #event as event -> Either.Right event) user's_actions_and_events in Logd.debug (fun m -> m "got %d actions and %d events" (List.length user's_actions) (List.length events)); let he, actions = to_actions t he user's_actions in (he, actions, events) | Error Miou.Cancelled -> (he, [], []) | Error exn -> Logd.err (fun m -> m "Got an unexpected exception (suspend): %s" (Printexc.to_string exn)); raise exn let rec clean_up prms = match Miou.care prms with | Some (Some prm) -> let _ = Miou.await prm in clean_up prms | Some None | None -> Miou.yield () let rec go t ~prms he () = Logd.debug (fun m -> m "daemon tick"); clean_up prms; let he, cont, actions = Happy_eyeballs.timer he (clock ()) in List.iter (handle_one_action ~prms t) actions; let he, actions, events = continue t cont he in Logd.debug (fun m -> m "got %d action(s) and %d event(s)" (List.length actions) (List.length events)); let he, actions = List.fold_left (fun (he, actions) event -> let he, actions' = Happy_eyeballs.event he (clock ()) (to_event t event) in (he, List.rev_append actions actions')) (he, actions) events in Logd.debug (fun m -> m "daemon handles %d action(s)" (List.length actions)); List.iter (handle_one_action ~prms t) actions; go t ~prms he () let launch_daemon t he () = let prms = Miou.orphans () in if Miou.Domain.available () > 0 then Miou.call (go t ~prms he) else Miou.async (go t ~prms he) let connect_ip ?aaaa_timeout ?connect_delay ?connect_timeout t addrs = let state = Miou.Computation.create () in Miou.Mutex.protect t.mutex @@ fun () -> let connect_ip = { aaaa_timeout ; connect_delay ; connect_timeout ; state ; addrs } in Miou.Queue.enqueue t.queue (`Connect_ip connect_ip); Miou.Condition.signal t.condition; state let connect_ip ?aaaa_timeout ?connect_delay ?connect_timeout t ips = try connect_ip ?aaaa_timeout ?connect_delay ?connect_timeout t ips with exn -> Logc.err (fun m -> m "Got an unexpected exception: %S" (Printexc.to_string exn)); raise exn type daemon = unit Miou.t let create ?happy_eyeballs:(he= Happy_eyeballs.create (clock ())) ?(getaddrinfo= getaddrinfo) ?(timer_interval= Duration.of_ms 10) () = let t = create (Duration.to_f timer_interval) getaddrinfo in launch_daemon t he (), t let error_injection = {text| It's impossible to hot-load a new getaddrinfo into the happy-eyeballs instance. |text} let inject t getaddrinfo = if t.set then invalid_arg error_injection; t.getaddrinfo <- getaddrinfo; t.set <- true let kill = Miou.cancel let connect_ip ?aaaa_timeout ?connect_delay ?connect_timeout t ips = let state = connect_ip ?aaaa_timeout ?connect_delay ?connect_timeout t ips in match Miou.Computation.await state with | Ok (addr, fd) -> Ok (addr, Miou_unix.of_file_descr fd) | Error (Connection_failed (host, reason), _) -> error_msgf "Impossible to connect to %a: %s" Domain_name.pp host reason | Error (exn, bt) -> Printexc.raise_with_backtrace exn bt let connect_host ?aaaa_timeout ?connect_delay ?connect_timeout ?resolve_timeout ?resolve_retries t host ports = let state = Miou.Computation.create () in let () = Miou.Mutex.protect t.mutex @@ fun () -> let connect = { aaaa_timeout ; connect_delay ; connect_timeout ; resolve_timeout ; resolve_retries ; state ; host ; ports } in Miou.Queue.enqueue t.queue (`Connect connect); Miou.Condition.signal t.condition in match Miou.Computation.await state with | Ok (addr, fd) -> Ok (addr, Miou_unix.of_file_descr fd) | Error (Connection_failed (host, reason), _) -> error_msgf "Impossible to connect to %a: %s" Domain_name.pp host reason | Error (exn, bt) -> Printexc.raise_with_backtrace exn bt let connect ?aaaa_timeout ?connect_delay ?connect_timeout ?resolve_timeout ?resolve_retries t str ports = match Ipaddr.of_string str with | Ok ipaddr -> connect_ip ?aaaa_timeout ?connect_delay ?connect_timeout t (List.map (fun port -> (ipaddr, port)) ports) | Error _ -> ( match Result.bind (Domain_name.of_string str) Domain_name.host with | Ok domain_name -> connect_host ?aaaa_timeout ?connect_delay ?connect_timeout ?resolve_timeout ?resolve_retries t domain_name ports | Error _ -> error_msgf "Invalid endpoint: %S" str)