% Copyright (C) 2007-2019 Olivier Boudeville % % This file is part of the Ceylan-Myriad library. % % This library is free software: you can redistribute it and/or modify % it under the terms of the GNU Lesser General Public License or % the GNU General Public License, as they are published by the Free Software % Foundation, either version 3 of these Licenses, or (at your option) % any later version. % You can also redistribute it and/or modify it under the terms of the % Mozilla Public License, version 1.1 or later. % % This library is distributed in the hope that it will be useful, % but WITHOUT ANY WARRANTY; without even the implied warranty of % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the % GNU Lesser General Public License and the GNU General Public License % for more details. % % You should have received a copy of the GNU Lesser General Public % License, of the GNU General Public License and of the Mozilla Public License % along with this library. % If not, see and % . % % Authors: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com] % Samuel Thiriot [samuel (dot) thiriot (at) edf (dot) fr] % % Creation date: July 1, 2007. % Gathering of various convenient net-related facilities. % % See net_utils_test.erl for the corresponding test. % -module(net_utils). % Host-related functions: -export([ ping/1, localhost/0, localhost/1, get_local_ip_addresses/0, get_local_ip_address/0, reverse_lookup/1 ]). % Node-related functions: -export([ localnode/0, get_all_connected_nodes/0, check_node_availability/1, check_node_availability/2, get_node_naming_mode/0, get_naming_compliant_hostname/2, generate_valid_node_name_from/1, get_fully_qualified_node_name/3, launch_epmd/0, launch_epmd/1, enable_distribution/2, get_cookie/0, set_cookie/1, set_cookie/2, shutdown_node/0, shutdown_node/1 ]). % Net-related command line options: -export([ get_cookie_option/0, get_epmd_environment/1, get_node_name_option/2, get_tcp_port_range_option/1, get_basic_node_launching_command/5 ]). % Net-related transfers: -export([ send_file/2, receive_file/1, receive_file/2, receive_file/3 ]). % Server-related functions: -export([ is_service_running_at/1 ]). % Address-related functions: -export([ is_routable/1 ]). % Stringifications: -export([ ipv4_to_string/1, ipv4_to_string/2, ipv6_to_string/1, ipv6_to_string/2, host_to_string/1, url_info_to_string/1 ]). % Destringifications: -export([ string_to_url_info/1 ]). % Exported for convenience: -export([ wait_unavailable/3 ]). % Type declarations. -type ip_v4_address() :: { byte(), byte(), byte(), byte() }. -type ip_v6_address() :: { byte(), byte(), byte(), byte(), byte(), byte() }. -type ip_address() :: ip_v4_address() | ip_v6_address(). % We tend to favor atom-based node names (usual in Erlang) to string-based ones: -type atom_node_name() :: node(). -type string_node_name() :: nonempty_string(). -type node_name() :: atom_node_name() | string_node_name(). % See net_kernel:monitor_nodes/2 for more information: -type node_type() :: 'visible' | 'hidden' | 'all'. -type atom_host_name() :: atom(). -type string_host_name() :: nonempty_string(). -type host_name() :: atom_host_name() | string_host_name(). -type host_identifier() :: string_host_name() | ip_address(). -type check_duration() :: non_neg_integer(). -type check_node_timing() :: check_duration() | 'immediate' | 'with_waiting'. -type node_naming_mode() :: 'long_name' | 'short_name'. -type cookie() :: atom(). -type net_port() :: non_neg_integer(). -type tcp_port() :: net_port(). -type udp_port() :: net_port(). -type tcp_port_range() :: { tcp_port(), tcp_port() }. -type udp_port_range() :: { udp_port(), udp_port() }. -type tcp_port_restriction() :: 'no_restriction' | tcp_port_range(). % The possible protocols for an URL: -type protocol_type() :: 'http' | 'https' | 'ftp'. % Path of an URL (ex: 'access/login'): -type path() :: string(). -include("net_utils.hrl"). % Full information about an URL: -type url_info() :: #url_info{}. % An URL: -type url() :: string(). -export_type([ ip_v4_address/0, ip_v6_address/0, ip_address/0, atom_node_name/0, string_node_name/0, node_name/0, node_type/0, atom_host_name/0, string_host_name/0, host_name/0, host_identifier/0, check_duration/0, check_node_timing/0, node_naming_mode/0, cookie/0, net_port/0, tcp_port/0, udp_port/0, tcp_port_range/0, udp_port_range/0, tcp_port_restriction/0, protocol_type/0, path/0, url_info/0, url/0 ]). % For the file_info record: -include_lib("kernel/include/file.hrl"). % Host-related functions. % Pings specified hostname, and returns true iff it could be ping'd. % % Note: command-line based call, used that way as there is no ICMP stack. % % A port could be used also. % -spec ping( string_host_name() ) -> boolean(). ping( Hostname ) when is_list( Hostname ) -> Command = "/bin/ping " ++ Hostname ++ " -q -c 1 ", %trace_utils:debug_fmt( "Ping command: ~s.", [ Command ] ), case system_utils:run_executable( Command ) of { _ExitCode=0, _Output } -> true; { _ExitCode, _Output } -> false end. % Returns an appropriate DNS name for the local host (as a string), or throws an % exception. % % Tries to collect a FQDN (Fully Qualified Domain Name). % -spec localhost() -> string_host_name(). localhost() -> localhost( fqdn ). % Returns an appropriate DNS name (either a FQDN - Fully Qualified Domain Name - % or a short host name) for the local host (as a string), or throws an % exception. % -spec localhost( 'fqdn' | 'short' ) -> string_host_name(). localhost( fqdn ) -> % Depending on the node being launched with either: % % - no network name or a short name % - a long name % net_adm:localhost() may return respectively "XXX.domain.com" or % "XXX.localdomain", both of which are not proper hostnames. % % On the other hand, "hostname -f" might return 'localhost.localdomain' or % even "hostname: Name or service not known" if there are issues in terms of % name resolution. % Most reliable: case system_utils:run_executable( "hostname -f" ) of { _ExitCode=0, _Output="localhost" } -> localhost_last_resort(); { _ExitCode=0, _Output="localhost.localdomain" } -> localhost_last_resort(); { _ExitCode=0, Output } -> % Must be legit: Output; { _ExitCode, _Output } -> localhost_last_resort() end; % Returns the host name by itself (at least attempts to do so): localhost( short ) -> FQDN = localhost( fqdn ), % So that for example "tesla.esperide.com" becomes "tesla": hd( string:tokens( FQDN, "." ) ). % (helper) -spec localhost_last_resort() -> string_host_name(). localhost_last_resort() -> case system_utils:run_executable( "hostname" ) of { _ExitCode=0, _Output="localhost" } -> throw( could_not_determine_localhost ); { _ExitCode=0, _Output="localhost.localdomain" } -> throw( could_not_determine_localhost ); { _ExitCode=0, Output } -> % Must be legit: Output; { ExitCode, Output } -> throw( { could_not_determine_localhost, ExitCode, Output } ) end. % Returns a list of the potentially usable non-local network interfaces on this % host, trying to put in first position the "main" one, if any. % % Note: IPv6 support should be added. % -spec get_local_ip_addresses() -> [ ip_v4_address() ]. get_local_ip_addresses() -> IfList = case inet:getifaddrs() of { ok, List } -> List; { error, Reason } -> throw( { local_ip_look_up_failed, Reason } ) end, %trace_utils:debug_fmt( "Interface list:~n~p", [ IfList ] ), % Rules: put non-routable (network-local) interfaces last (including % loopback, i.e. "lo", which must be the very last one), try to put routable % "ethX"-like interfaces first, virtual interfaces (ex: "vmnetX") % last. Keeps only the actual address (addr). % More convenient than a queue: filter_interfaces( IfList, _FirstIfs=[], _LastIfs=[], _Loopback=undefined ). % (helper) filter_interfaces( _IfList=[], FirstIfs, LastIfs, _Loopback=undefined ) -> % No loopback here; quite surprising: filter_routable_first( FirstIfs ) ++ filter_routable_first( LastIfs ); filter_interfaces( _IfList=[], FirstIfs, LastIfs, Loopback ) -> % We need loopback never to take precedence over any other interface: filter_routable_first( FirstIfs ) ++ filter_routable_first( LastIfs ) ++ [ Loopback ]; filter_interfaces( _IfList=[ _If={ Name, Options } | T ], FirstIfs, LastIfs, Loopback ) -> %trace_utils:debug_fmt( "Examining interface named '~p', with options ~p.", % [ Name, Options ] ), case proplists:get_value( _K=addr, Options ) of % Ex: wlan0 might not have a configured address if down: undefined -> filter_interfaces( T, FirstIfs, LastIfs, Loopback ); Address -> case Name of % Assuming up to one loopback, replacing any previous one: "lo" -> filter_interfaces( T, FirstIfs, LastIfs, Address ); % For example, eth1: "eth" ++ _ -> filter_interfaces( T, [ Address | FirstIfs ], LastIfs, Loopback ); % For example, enp0s25: "enp" ++ _ -> filter_interfaces( T, [ Address | FirstIfs ], LastIfs, Loopback ); % Ex: vmnetX, etc. _ -> filter_interfaces( T, FirstIfs, [ Address | LastIfs ], Loopback ) end end. % (helper) filter_routable_first( IfList ) -> filter_routable_first( IfList, _RoutableAddrs=[], _NonRoutableAddrs=[] ). filter_routable_first( _IfList=[], RoutableAddrs, NonRoutableAddrs ) -> RoutableAddrs ++ NonRoutableAddrs; filter_routable_first( _IfList= [ If | T ], RoutableAddrs, NonRoutableAddrs ) -> case is_routable( If ) of true -> filter_routable_first( T, [ If | RoutableAddrs ], NonRoutableAddrs ); false -> filter_routable_first( T, RoutableAddrs, [ If | NonRoutableAddrs ] ) end. % Returns the "main" potentially usable non-local network interface on this % host. % -spec get_local_ip_address() -> ip_v4_address(). get_local_ip_address() -> case get_local_ip_addresses() of [] -> throw( no_local_ip_address_established ); % In some cases, the user wants to select another network interface than % the selected one: %[ _FirstAddr, SecondAddr | _T ] -> % SecondAddr; [ Addr | _T ] -> Addr end. % Returns a string specifying the DNS name corresponding to the specified IPv4 % address { N1, N2, N3, N4 }. % -spec reverse_lookup( ip_v4_address() ) -> string_host_name() | 'unknown_dns'. reverse_lookup( IPAddress ) -> % Note that the 'host' command is not available on all systems ('dig', % 'drill', 'nslookup') might be: % case executable_utils:lookup_executable( "host" ) of false -> case executable_utils:lookup_executable( "drill" ) of false -> case executable_utils:lookup_executable( "dig" ) of false -> throw( { executables_not_found, [ "host", "drill", "dig" ] } ); DigPath -> reverse_lookup_with_dig_like( IPAddress, DigPath ) end; DrillPath -> reverse_lookup_with_dig_like( IPAddress, DrillPath ) end; HostPath -> reverse_lookup_with_host( IPAddress, HostPath ) end. % (helper using dig-like commands, i.e. the 'drill'/'dig' ones) reverse_lookup_with_dig_like( IPAddress, DigLikeCmd ) -> % We remove empty lines and comments (lines starting with ';;') and extract % the host name: % Cmd = DigLikeCmd ++ " -x " ++ ipv4_to_string( IPAddress ) ++ " | grep -v '^;;' | grep PTR | sed 's|.*PTR\t||1'" ++ " | sed 's|\.$||1' 2>/dev/null", % Following could let non-PTR answers with '900 IN SOA' slip through: % %Cmd = DigLikeCmd ++ " -x " ++ ipv4_to_string( IPAddress ) % ++ " | grep . | grep -v '^;;' | sed 's|.*PTR\t||1' | " % ++ "sed 's|\.$||1' 2>/dev/null", % Alternatively, could have along the lines of: % % case system_utils:run_executable( Cmd ) of % % CleanedResult = text_utils:remove_whitespaces( Output ), % % case string:tokens( CleanedResult, "PTR" ) of % % [ _Prefix, DomainPlusDot ] -> % % There is a trailing dot: % text_utils:remove_last_characters( DomainPlusDot, % _Count=1 ); % % _Other -> % unknown_dns % % end; % % (however was not really elegant and a leading tabulation was remaining at % least in some cases) case system_utils:run_executable( Cmd ) of { _ExitCode=0, _Output="" } -> unknown_dns; { _ExitCode=0, Output } -> Output; { _ExitCode, _ErrorOutput } -> %throw( { reverse_lookup_failed, IPAddress, ExitCode, % ErrorOutput } ) unknown_dns end. % (helper using the 'host' command) reverse_lookup_with_host( IPAddress, HostCmd ) -> Cmd = HostCmd ++ " -W 1 " ++ ipv4_to_string( IPAddress ) ++ " 2>/dev/null", case system_utils:run_executable( Cmd ) of { _ExitCode=0, Output } -> %trace_utils:debug_fmt( "'host' command: ~s, result: ~s.", % [ Cmd, Output ] ), case string:tokens( Output, " " ) of [ _ArpaString, "domain", "name", "pointer", Domain ] -> % There is a trailing dot: text_utils:remove_last_characters( Domain, _Count=1 ); _Other -> unknown_dns end; { _ExitCode, _ErrorOutput } -> %throw( { reverse_lookup_failed, IPAddress, ExitCode, % ErrorOutput } ) unknown_dns end. % Node-related functions. % Returns the name of the local node, as an atom. % % It is either a specific node name, or the atom 'local_node' (preferred to % 'nonode@nohost') - which unfortunately are both atoms... % -spec localnode() -> atom_node_name() | 'local_node'. localnode() -> case node() of nonode@nohost -> local_node; OtherNodeName -> % Could be 'XX@myhost.example.com': OtherNodeName end. % Returns the list of all connected nodes (each being designated by an atom, % like 'foo@bar.org'), including the local node. % -spec get_all_connected_nodes() -> [ atom_node_name() ]. get_all_connected_nodes() -> [ node() | nodes() ]. % Returns immediately whether the specified Erlang node is found available. % % Nodename can be an atom or a string. % -spec check_node_availability( node_name() ) -> boolean(). check_node_availability( Nodename ) when is_list( Nodename ) -> check_node_availability( list_to_atom( Nodename ) ); check_node_availability( Nodename ) when is_atom( Nodename ) -> % Useful to troubleshoot longer ping durations: % (apparently this may come from badly configured DNS) %trace_utils:debug_fmt( "Pinging node '~s'...", [ Nodename ] ), case net_adm:ping( Nodename ) of pong -> %trace_utils:debug_fmt( % "... node '~s' found available from node '~s'.", % [ Nodename, node() ] ), true ; pang -> %trace_utils:debug_fmt( % "... node '~s' found NOT available from node '~s'.", % [ Nodename, node() ] ), false end. % Defining initial and upper bound to waiting durations for node look-up: -define( check_node_first_waiting_step, 20 ). -define( check_node_max_waiting_step, 2000 ). % Tells whether the specified Erlang node is available: returns % {IsAvailable,Duration} where IsAvailable is a boolean and Duration is the % number of milliseconds that was used to determine it. % % Parameters: % % - Nodename is an atom or a string corresponding to the name of the target node % % - Timing is either 'immediate', 'with_waiting' or a positive number of % attempts with exponential back-off: % % - if 'immediate', the target node will be deemed available or not, as soon % as the first and only ping attempted returns a result % % - if 'with_waiting', a fixed, default number of attempts with some % exponential waiting in-between will be performed, for a standard duration % % - if it is an integer, it will be used as a duration, i.e. the number of % milliseconds to be waited for, based on look-ups to be made with exponential % waiting in-between until a threshold duration is reached; this checking % should last no less than the specified duration, and not much more % % This is useful so that, if the node is being launched in the background, it is % waited for while returning as soon as possible. % -spec check_node_availability( node_name(), check_node_timing() ) -> { boolean(), check_duration() }. check_node_availability( Nodename, Timing ) when is_list( Nodename ) -> check_node_availability( list_to_atom( Nodename ), Timing ) ; check_node_availability( Nodename, _Timing=immediate ) -> IsAvailable = check_node_availability( Nodename ), { IsAvailable, _Duration=0 }; check_node_availability( Nodename, _Timing=with_waiting ) when is_atom( Nodename ) -> %trace_utils:debug_fmt( "check_node_availability of node '~s' with " % "default waiting.", [ Nodename ] ), % 3 seconds is a good default: check_node_availability( Nodename, _Duration=3000 ); check_node_availability( Nodename, Duration ) -> % In all cases, start with one immediate look-up: %trace_utils:debug_fmt( "Pinging '~s' (case A) now...", [ Nodename ] ), case net_adm:ping( Nodename ) of pong -> %trace_utils:debug_fmt( " - node '~s' found directly available.", % [ Nodename ] ), { true, 0 } ; pang -> %trace_utils:debug_fmt( " - node '~s' not yet found available.", % [ Nodename ] ), % Hopefully too early, let's retry later: check_node_availability( Nodename, _CurrentDurationStep=?check_node_first_waiting_step, _ElapsedDuration=0, _SpecifiedMaxDuration=Duration ) end. % Helper function for the actual waiting: check_node_availability( Nodename, CurrentDurationStep, ElapsedDuration, SpecifiedMaxDuration ) when ElapsedDuration < SpecifiedMaxDuration -> % Still on time here, apparently. % Avoid going past the deadline: RemainingDuration = SpecifiedMaxDuration - ElapsedDuration, ActualDurationStep = erlang:min( CurrentDurationStep, RemainingDuration ), %trace_utils:debug_fmt( "check_node_availability: actual step is ~B ms, " % "elapsed is ~B ms, for a specified duration of ~B ms.", % [ ActualDurationStep, ElapsedDuration, SpecifiedMaxDuration ] ), % By design we are directly following a ping attempt: timer:sleep( ActualDurationStep ), NewElapsedDuration = ElapsedDuration + ActualDurationStep, %trace_utils:debug_fmt( "Pinging '~s' (case B) now...", [ Nodename ] ), case net_adm:ping( Nodename ) of pong -> %trace_utils:debug_fmt( " - node '~s' found available after ~B ms.", % [ Nodename, NewElapsedDuration ] ), { true, NewElapsedDuration } ; pang -> %trace_utils:debug_fmt( " - node '~s' NOT found available after " % "~B ms.", [ Nodename, NewElapsedDuration ] ), % Too early, let's retry later: NewCurrentDurationStep = erlang:min( 2 * CurrentDurationStep, ?check_node_max_waiting_step ), check_node_availability( Nodename, NewCurrentDurationStep, NewElapsedDuration, SpecifiedMaxDuration ) end; % Already too late here (ElapsedDuration >= SpecifiedMaxDuration): check_node_availability( _Nodename, _CurrentDurationStep, ElapsedDuration, _SpecifiedMaxDuration ) -> %trace_utils:debug_fmt( " - node '~s' found NOT available, after ~B ms.", % [ Nodename, ElapsedDuration ] ), { false, ElapsedDuration }. % Returns the naming mode of this node, either 'short_name' or 'long_name'. -spec get_node_naming_mode() -> node_naming_mode(). get_node_naming_mode() -> % We determine the mode based on the returned node name: % (ex: 'foo@bar' vs 'foo@bar.baz.org') % [ _Node, Host ] = string:tokens( atom_to_list( node() ), "@" ), case length( string:tokens( Host, "." ) ) of 1 -> short_name; TwoOrMore when TwoOrMore > 1 -> long_name end. % Returns a transformed version (as a string) of the specified hostname (itself % specified as a string) so that it is compliant with the specified node naming % convention. % % For example, if the short_name convention is specified, then a "bar.baz.org" % hostname will result into "bar". % -spec get_naming_compliant_hostname( string_host_name(), node_naming_mode() ) -> string_host_name(). get_naming_compliant_hostname( Hostname, short_name ) -> hd( string:tokens( Hostname, "." ) ); get_naming_compliant_hostname( Hostname, long_name ) -> Hostname. % Returns a name (as a string) that is a legal name for an Erlang node, forged % from the specified name. % -spec generate_valid_node_name_from( iolist() ) -> string_node_name(). generate_valid_node_name_from( Name ) when is_list( Name ) -> % Replaces each series of spaces (' '), lower than ('<'), greater than % ('>'), comma (','), left ('(') and right (')') parentheses, single (''') % and double ('"') quotes, forward ('/') and backward ('\') slashes, % ampersand ('&'), tilde ('~'), sharp ('#'), at sign ('@'), all other kinds % of brackets ('{', '}', '[', ']'), pipe ('|'), dollar ('$'), star ('*'), % marks ('?' and '!'), plus ('+'), other punctation signs (';', '.' and ':') % by exactly one underscore: % % (see also: file_utils:convert_to_filename/1) re:replace( lists:flatten( Name ), "( |<|>|,|\\(|\\)|'|\"|/|\\\\|\&|~|" "#|@|{|}|\\[|\\]|\\||\\$|\\*|\\?|!|\\+|;|\\.|:)+", "_", [ global, { return, list } ] ). % Returns the full name of a node (as a string), which has to be used to target % it from another node, with respect to the specified node naming conventions. % % Ex: for a node name "foo", a hostname "bar.org", with short names, we may % specify "foo@bar" to target the corresponding node with these conventions (not % a mere "foo", neither "foo@bar.org"). % -spec get_fully_qualified_node_name( string_node_name(), string_host_name(), node_naming_mode() ) -> atom_node_name(). get_fully_qualified_node_name( NodeName, Hostname, NodeNamingMode ) -> StringNodeName = NodeName ++ "@" ++ get_naming_compliant_hostname( Hostname, NodeNamingMode ), text_utils:string_to_atom( StringNodeName ). % Launches as a daemon (in the background) an EPMD instance on the Erlang % standard port, if needed. % % If an EPMD instance is already launched for that port, no extra instance will % be launched, the former one remaining the active one; there is up to one EPMD % instance per port. % -spec launch_epmd() -> void(). launch_epmd() -> launch_epmd( _StandardPort=4369 ). % Launches as a daemon (in the background) an EPMD instance on the specified % port, if needed. % % If an EPMD instance is already launched for that port, no extra instance will % be launched, the former one remaining the active one; there is up to one EPMD % instance per port. % -spec launch_epmd( net_port() ) -> void(). launch_epmd( Port ) when is_integer( Port ) -> case executable_utils:lookup_executable( "epmd" ) of false -> throw( { unable_to_launch_epmd, executable_not_found } ); EPMDPath -> % Better through command line than using the environment to specify % the port: EpmdCmd = text_utils:format( "~s -port ~B", [ EPMDPath, Port ] ), %trace_utils:debug_fmt( "Launching EPMD thanks to '~s'.", % [ EpmdCmd ] ), system_utils:run_background_executable( EpmdCmd ) end. % Enables the distribution on the current node, supposedly not already % distributed (otherwise the operation will fail). % % Note: an EPMD instance is expected to be already running; see % launch_epmd/{0,1} in this module for that; apparently no race condition % happens, hence initially no need for a wait-and-retry mechanism was seen here. % % Otherwise following messages might be output: % - 'Protocol: "inet_tcp": register/listen error: econnrefused' % - '{distribution_enabling_failed,foobar,long_name,{{shutdown, % {failed_to_start_child,net_kernel,{'EXIT',nodistribution}}},... % % In some cases yet (first time an Erlang program is run after boot?), a % distribution_enabling_failed exception is raised, like in: % % {"init terminating in do_boot",{{nocatch,{distribution_enabling_failed, % 'A_NODE_NAME',long_name,{{{shutdown,{failed_to_start_child,net_kernel, % {'EXIT',nodistribution}}},{child,undefined,net_sup_dynamic, % {erl_distribution,start_link,[['A_NODE_NAME',longnames],false]},permanent, % 1000,supervisor,[erl_distribution]}},'nonode@nohost'}}},[...] % % This does not seem to be linked to a race condition with EPMD, as killing EPMD % and re-running the program does not fail anymore. % % So a (tiny) second-chance mechanism has been introduced. % -spec enable_distribution( node_name(), node_naming_mode() ) -> void(). enable_distribution( NodeName, NamingMode ) when is_list( NodeName ) -> AtomNodeName = text_utils:string_to_atom( lists:flatten( NodeName ) ), enable_distribution( AtomNodeName, NamingMode ); enable_distribution( NodeName, NamingMode=long_name ) when is_atom( NodeName ) -> enable_distribution_helper( NodeName, longnames, NamingMode, _RemainingAttempts=5 ); enable_distribution( NodeName, NamingMode=short_name ) when is_atom( NodeName ) -> enable_distribution_helper( NodeName, shortnames, NamingMode, _RemainingAttempts=5 ). % NamingMode kept for error message. % % (helper) % enable_distribution_helper( NodeName, NameType, NamingMode, RemainingAttempts ) -> %trace_utils:debug_fmt( "Starting distribution for node name ~s, as '~w'.", % [ NodeName, NameType ] ), case net_kernel:start( [ NodeName, NameType ] ) of { error, Reason } -> case RemainingAttempts of 0 -> ExtraReason = case net_kernel:stop() of ok -> { was_distributed, node(), Reason }; { error, not_allowed } -> { not_allowed, node(), Reason }; { error, not_found } -> { extra_reason, node(), Reason } end, throw( { distribution_enabling_failed, NodeName, NamingMode, ExtraReason } ); N -> trace_utils:warning_fmt( "(attempt of enabling ~p distribution " "for node '~s' failed, retrying...)", [ NamingMode, NodeName ] ), timer:sleep( 300 ), enable_distribution_helper( NodeName, NameType, NamingMode, N - 1 ) end; %{ ok, _NetKernelPid } -> R -> R end. % Returns the Erlang cookie of the current node if that node is alive, otherwise % the 'nocookie' atom. % -spec get_cookie() -> cookie() | 'nocookie'. get_cookie() -> erlang:get_cookie(). % Sets the Erlang cookie for the current node, as well as for the one of all % unknown nodes. % -spec set_cookie( cookie() ) -> void(). set_cookie( Cookie ) -> case erlang:is_alive() of true -> set_cookie( Cookie, node() ); false -> throw( local_node_not_alive ) end. % Sets the Erlang cookie for the specified node. -spec set_cookie( cookie(), atom_node_name() ) -> void(). set_cookie( Cookie, Node ) -> erlang:set_cookie( Node, Cookie ). % Shutdowns current node, and never returns (unlike init:stop/0): it is a % reliable and synchronous operation. % % Throws an exception if not able to terminate it. % -spec shutdown_node() -> no_return(). shutdown_node() -> init:stop(), timer:sleep( 5000 ), shutdown_node(). % Shutdowns specified node (specified as a string or an atom), and returns only % when it cannot be ping'ed anymore: it is a reliable and synchronous operation. % % Throws an exception if not able to terminate it. % -spec shutdown_node( node_name() ) -> void(). shutdown_node( Nodename ) when is_list( Nodename ) -> shutdown_node( list_to_atom( Nodename ) ); shutdown_node( Nodename ) when is_atom( Nodename ) -> %trace_utils:debug_fmt( "Request to shut down node '~s' from node '~s'.", % [ Nodename, node() ] ), case lists:member( Nodename, nodes() ) of true -> try %trace_utils:debug_fmt( "Sending shutdown command for '~s'.", % [ Nodename ] ) %rpc:cast( Nodename, erlang, halt, [] ) % Longer yet smoother: rpc:cast( Nodename, init, stop, [] ) catch _T:E -> trace_utils:error_fmt( "Error while shutting down node '~s': ~p.", [ Nodename, E ] ) end, wait_unavailable( Nodename, _AttemptCount=10, _Duration=150 ); %ok; false -> %trace_utils:debug_fmt( "Node '~s' apparently not connected.", % [ Nodename ] ), ok end. wait_unavailable( Nodename, _AttemptCount=0, _Duration ) -> throw( { node_not_terminating, Nodename } ); wait_unavailable( Nodename, AttemptCount, Duration ) -> % We used to rely on net_adm:ping/1 (see below), but apparently % 'noconnection' can be raised and does not seem to be catchable. % % So we finally just wait until the target node disappears from the list % returned by nodes(): % case lists:member( Nodename, nodes() ) of true -> timer:sleep( Duration ), wait_unavailable( Nodename, AttemptCount-1, 2*Duration ); false -> % Additional safety delay to ensure the node had time to fully shut % down and to unregister from everything: % timer:sleep( 200 ) end. %try net_adm:ping( Nodename ) of % pong -> % timer:sleep( Duration ), % wait_unavailable( Nodename, AttemptCount-1, 2*Duration ); % pang -> % Safety delay to ensure the node had time to fully shut down and to % unregister from everything: % timer:sleep( 200 ), % ok %catch % _T:E -> % trace_utils:debug_fmt( "Error while pinging node '~s': " % "exception '~p'.", [ Nodename, E ] ) %end. % Net-related command line options. % Returns the command-line option (a plain string) to be used to run a new % Erlang node with the same cookie as the current node, whether or not it is % alive. % -spec get_cookie_option() -> string(). get_cookie_option() -> case erlang:get_cookie() of nocookie -> ""; Cookie -> "-setcookie \"" ++ atom_to_list( Cookie ) ++ "\"" end. % Returns the command-line option (a plain string) to be used to run a new % Erlang node with the specified EPMD port specification, which can be either % the 'undefined' atom or the TCP port number. % % Note that if a non-default EPMD port is specified for a new node, this implies % that the current node usually has to itself respect the same non-standard % convention (ex: see the FIREWALL_OPT make option in myriad/GNUmakevars.inc), % otherwise available nodes will not be found. % -spec get_epmd_environment( maybe( tcp_port() ) ) -> system_utils:environment(). get_epmd_environment( undefined ) -> []; get_epmd_environment( EpmdPort ) when is_integer( EpmdPort ) -> % Flatten needed: PortString = text_utils:format( "~B", [ EpmdPort ] ), [ { "ERL_EPMD_PORT", PortString } ]. % Returns the command-line option (a plain string) to be used to run a new % Erlang node with the node name (specified as a string) and node naming mode % (short or long name, specified thanks to atoms). -spec get_node_name_option( string_node_name(), node_naming_mode() ) -> string(). get_node_name_option( NodeName, NodeNamingMode ) -> NodeNameOption = case NodeNamingMode of short_name -> "-sname"; long_name -> "-name" end, NodeNameOption ++ " " ++ NodeName. % Returns the command-line option (a plain string) to be used to run a new % Erlang node with the specified TCP port restriction, which can be either the % 'no_restriction' atom or a pair of integers {MinTCPPort,MaxTCPPort}. % % If using a specific TCP/IP port range for a new node, the current node may % have to respect this constraint as well (see the FIREWALL_OPT make option in % myriad/GNUmakevars.inc), otherwise inter-node communication could fail. % -spec get_tcp_port_range_option( 'no_restriction' | tcp_port_range() ) -> string(). get_tcp_port_range_option( no_restriction ) -> ""; get_tcp_port_range_option( { MinTCPPort, MaxTCPPort } ) when is_integer( MinTCPPort ) andalso is_integer( MaxTCPPort ) andalso MinTCPPort < MaxTCPPort -> %trace_utils:debug_fmt( "Enforcing following TCP range: [~B,~B].", % [ MinTCPPort, MaxTCPPort ] ), io_lib:format( " -kernel inet_dist_listen_min ~B inet_dist_listen_max ~B ", [ MinTCPPort, MaxTCPPort ] ). % Returns a basic command line (as a plain string) and its related environment % in order to launch an Erlang node (interpreter) with the specified settings. % -spec get_basic_node_launching_command( string_node_name(), node_naming_mode(), maybe( tcp_port() ), 'no_restriction' | tcp_port_range(), string() ) -> { system_utils:command(), system_utils:environment() }. get_basic_node_launching_command( NodeName, NodeNamingMode, EpmdSettings, TCPSettings, AdditionalOptions ) -> % May end up with a command-line option similar to: % 'erl -setcookie 'foobar' -sname hello % -kernel inet_dist_listen_min 10000 inet_dist_listen_max 14000 % -noshell -smp auto +K true +A 8 +P 400000' with an environment with % ERL_EPMD_PORT=754 Command = text_utils:join( _Separator=" ", [ executable_utils:get_default_erlang_interpreter_name(), get_cookie_option(), get_node_name_option( NodeName, NodeNamingMode ), get_tcp_port_range_option( TCPSettings ), AdditionalOptions ] ), % Note that specifying the environment that way would work locally only % (i.e. of course SSH does not propagate environment) - hence the export in % the previous command, that works locally and through SSH: % EpmdEnv = get_epmd_environment( EpmdSettings ), { Command, EpmdEnv }. % Net-related transfers. % % They are not through a dedicated TCP/IP socket pair, using sendfile. % % For proper operation, a sufficient number of async threads should be % available. % % The recipient acts as a server, while the emitter acts as a client. % % The sender is to use send_file/2 while the recipient is to use one of the % receive_file/{1,2,3}. As they synchronize through messages, no specific order % of these two calls matters (the first will wait for the second). % We use an ephemeral port number by default: -define( default_send_file_port, 0 ). % Sends specified file (probably over the network) to specified recipient PID, % supposed to have already called on of the receive_file/{1,2,3}. % -spec send_file( file_utils:file_name(), pid() ) -> void(). send_file( Filename, RecipientPid ) -> case file_utils:is_existing_file( Filename ) of true -> ok; false -> throw( { file_to_send_not_found, Filename } ) end, Permissions = case file:read_file_info( Filename ) of { ok, #file_info{ mode=Mode } } -> Mode; { error, ReadInfoReason } -> throw( { read_file_info_failed, ReadInfoReason } ) end, % Strip the directories, keeps only the filename: BinFilename = text_utils:string_to_binary( filename:basename( Filename ) ), % Notifies the recipient so that it can receive the content: % (note: we mimic the WOOPER conventions here) % RecipientPid ! { sendFile, [ BinFilename, Permissions, self() ] }, receive { sendFileAcknowledged, [ BinFilename, RemoteIP, Port ] } -> % We used to rely on hostnames: % Hostname = text_utils:binary_to_string( BinHostname ), %trace_utils:debug_fmt( "~w connecting to ~s:~B to send '~s'.", % [ self(), ipv4_to_string( RemoteIP ), Port, Filename ] ), DataSocket = case gen_tcp:connect( RemoteIP, Port, [ binary, { packet, 0 }, { active, false } ] ) of { ok, Socket } -> Socket; % Typically, 'econnrefused', 'etimedout' or 'enetunreach': { error, Error } -> throw( { send_file_connection_failed, ipv4_to_string( RemoteIP, Port ), Error } ) end, % Two possibilities: % First, basic reading and sending: % case file:read_file( Filename ) of % { ok, BinFileContent } -> % %trace_utils:debug_fmt( "Sending ~B elements.", % % [ size( BinFileContent ) ] ), % case gen_tcp:send( DataSocket, BinFileContent ) of % ok -> % ok; % { error, SendReason } -> % throw( { sending_failed, SendReason } ) % end; % { error, ReadReason } -> % throw( { reading_failed, ReadReason } ) % end, % Second, more efficient, is using the sendfile kernel % function. Moreover even very large files may be transferred this % way (whereas the previous approach would fail with 'enomem', % trying to load their full content in RAM before their sending), so % it is definitively the best solution: % %trace_utils:debug_fmt( "~w performing sendfile, using data " % "socket ~p.", [ self(), DataSocket ] ), case file:sendfile( Filename, DataSocket ) of { ok, _SentByteCount } -> %trace_utils:debug_fmt( "~w sent file.", [ self() ] ), ok; { error, Reason } -> throw( { sendfile_failed, Reason } ) end, ok = gen_tcp:close( DataSocket ) end. % Receives specified file out of band (through a dedicated TCP socket, not % thanks to Erlang messages), the emitter being supposed to use send_file/2. % % The file will be written in current directory, and the default TCP port will % be used. % % Returns the full path to the received file. % -spec receive_file( pid() ) -> file_utils:file_name(). receive_file( EmitterPid ) -> receive_file( EmitterPid, file_utils:get_current_directory() ). % Receives specified file out of band (through a dedicated TCP socket, not % thanks to Erlang messages) into specified pre-existing directory, the emitter % being supposed to use send_file/2. % % The default TCP port will be used. % % Returns the full path to the received file. % -spec receive_file( pid(), file_utils:directory_name() ) -> void(). receive_file( EmitterPid, TargetDir ) -> receive_file( EmitterPid, TargetDir, ?default_send_file_port ). % Receives specified file out of band (through a dedicated TCP socket, not % thanks to Erlang messages) into specified pre-existing directory, the emitter % being supposed to use send_file/2. % % The default TCP port will be used. % -spec receive_file( pid(), file_utils:directory_name(), tcp_port() ) -> file_utils:file_name(). receive_file( EmitterPid, TargetDir, Port ) -> % We prefer relying on IP addresses rather than hostnames, as a surprisingly % high number of systems have no usable DNS service: % % BinHostname = text_utils:string_to_binary( localhost() ), LocalIP = get_local_ip_address(), %trace_utils:debug_fmt( "~w (on ~w) determined its local IP: ~w.", % [ self(), node(), LocalIP ] ), receive { sendFile, [ BinFilename, Permissions, EmitterPid ] } -> case gen_tcp:listen( Port, [ binary, { active, false }, { packet,0 } ] ) of { ok, ListenSock } -> % An ephemeral port (0) may have been specified: { ok, ActualPort } = inet:port( ListenSock ), EmitterPid ! { sendFileAcknowledged, [ BinFilename, LocalIP, ActualPort ] }, Filename = file_utils:join( TargetDir, text_utils:binary_to_string( BinFilename ) ), %trace_utils:debug_fmt( "Writing received file in '~s'.", % [ Filename ] ), % Do not know the units for { delayed_write, Size, Delay }: OutputFile = file_utils:open( Filename, [ write, raw, binary, delayed_write ] ), % Mono-client, yet using a separate socket for actual % sending: % case gen_tcp:accept( ListenSock ) of { ok, DataSocket } -> receive_file_chunk( DataSocket, OutputFile ), ok = gen_tcp:close( ListenSock ); Other -> throw( { accept_failed, Other } ) end, case file:write_file_info( Filename, #file_info{ mode=Permissions } ) of ok -> Filename; { error, WriteInfoReason } -> throw( { write_file_info_failed, WriteInfoReason } ) end; { error, Reason } -> throw( { listen_failed, Reason } ) end end. % Reads next chunk of transferred file. % % (helper) % receive_file_chunk( DataSocket, OutputFile ) -> inet:setopts( DataSocket, [ { active, once } ] ), receive { tcp, DataSocket, Data } -> %trace_utils:debug_fmt( "Received chunk of ~B elements.", % [ size( Data ) ] ), file_utils:write( OutputFile, Data ), receive_file_chunk( DataSocket, OutputFile ); { tcp_closed, DataSocket } -> %trace_utils:debug( "Connection closed." ), ok = gen_tcp:close( DataSocket ), file_utils:close( OutputFile ) end. % Tells whether a service (socket) is running on the local host at specified % TCP port. % -spec is_service_running_at( tcp_port() ) -> boolean(). is_service_running_at( TCPPort ) -> %trace_utils:debug_fmt( "Testing local service availability at port #~B...", % [ TCPPort ] ), % Presumably a lot quicker than attempting to connect: case gen_tcp:listen( TCPPort, _Opts=[] ) of { ok, Socket } -> gen_tcp:close( Socket ), false; { error, _Error=eaddrinuse } -> true; { error, Error } -> trace_utils:error_fmt( "Error when testing service availability " "at local TCP port #~B: ~p", [ TCPPort, Error ] ), throw( { unexpected_error, Error, TCPPort } ) end. % Address-related functions. % Tells whether the specified IPv4 address is routable. % % Note: the loopback ({127,0,0,1}, or {0,0,0,0,0,0,0,1}) is deemed routable. % -spec is_routable( ip_v4_address() ) -> boolean(). is_routable( { 10, _, _, _ } ) -> false; is_routable( { 172, N, _, _ } ) when N >= 16 andalso N < 32 -> false; is_routable( { 192, 168, _, _ } ) -> false; is_routable( _ ) -> true. % Returns a string describing the specified IPv4 address. -spec ipv4_to_string( ip_v4_address() ) -> string(). ipv4_to_string( { N1, N2, N3, N4 } ) -> text_utils:format( "~B.~B.~B.~B", [ N1, N2, N3, N4 ] ). % Returns a string describing the specified IPv4 address and port. -spec ipv4_to_string( ip_v4_address(), net_port() ) -> string(). ipv4_to_string( { N1, N2, N3, N4 }, Port ) -> text_utils:format( "~B.~B.~B.~B:~B", [ N1, N2, N3, N4, Port ] ). % Returns a string describing the specified IPv6 address. -spec ipv6_to_string( ip_v6_address() ) -> string(). ipv6_to_string( { N1, N2, N3, N4, N5, N6 } ) -> text_utils:format( "~B.~B.~B.~B", [ N1, N2, N3, N4, N5, N6 ] ). % Returns a string describing the specified IPv6 address and port. -spec ipv6_to_string( ip_v6_address(), net_port() ) -> string(). ipv6_to_string( Ipv6={ _N1, _N2, _N3, _N4, _N5, _N6 }, Port ) -> text_utils:format( "~s:~B", [ ipv6_to_string( Ipv6 ), Port ] ). % Returns a string describing the specified host. -spec host_to_string( host_identifier() ) -> string(). host_to_string( IPv4={ _N1, _N2, _N3, _N4 } ) -> ipv4_to_string( IPv4 ); host_to_string( IPv6={ _N1, _N2, _N3, _N4, _N5, _N6 } ) -> ipv6_to_string( IPv6 ); host_to_string( Address ) -> Address. % Returns a string describing the specified URL information. -spec url_info_to_string( url_info() ) -> string(). url_info_to_string( #url_info{ protocol=Protocol, host_identifier=Host, port=Port, path=Path } ) -> text_utils:format( "~s://~s:~B/~s", [ Protocol, host_to_string( Host ), Port, Path ] ). % Decodes specified string in an url_info, by extracting protocol, host, port % and path information. % % Note that other information (query, user) will be ignored and lost. % -spec string_to_url_info( string() ) -> url_info(). string_to_url_info( String ) -> case http_uri:parse( String ) of { ok, { Scheme, _UserInfo, Host, Port, Path, _Query } } -> #url_info{ protocol=Scheme, host_identifier=Host, port=Port, path=Path }; { ok, { Scheme, _UserInfo, Host, Port, Path, _Query, _Fragment } } -> #url_info{ protocol=Scheme, host_identifier=Host, port=Port, path=Path }; { error, Reason } -> throw( { url_info_parsing_failed, String, Reason } ) end.