%% Copyright (c) 2019-2026 Guilherme Andrade %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% %% locus is an independent project and has not been authorized, sponsored, %% or otherwise approved by MaxMind. %% @private -module(locus_cli). -ifdef(ESCRIPTIZING). %% ------------------------------------------------------------------ %% API Function Exports %% ------------------------------------------------------------------ -export([main/1]). %% ------------------------------------------------------------------ %% API Function Definitions %% ------------------------------------------------------------------ -spec main([string()]) -> ok | no_return(). main(Args) -> ensure_apps_are_started([locus, getopt]), case Args of ["check" | CmdArgs] -> handle_check_command(CmdArgs); ["analyze" | CmdArgs] -> locus_logger:log_warning("The `analyze` command is now called `check`", []), handle_check_command(CmdArgs); _ -> fall_from_grace( "~n" "Usage: locus [] []~n" "~n" "Available commands:~n" " check" ) end. %% ------------------------------------------------------------------ %% Internal Function Definitions - Utils %% ------------------------------------------------------------------ ensure_apps_are_started(Apps) -> lists:foreach( fun(App) -> {ok, _} = application:ensure_all_started(App) end, Apps ). prepare_database(DatabaseURL, LoadTimeout, SuccessHandler) -> DatabaseId = cli_check, BaseOpts = [{event_subscriber, self()}], DatabaseURLStr = unicode:characters_to_list(DatabaseURL), ExtraOpts = case locus_util:parse_absolute_http_url(DatabaseURLStr) of {ok, _} -> [no_cache]; {error, _} -> [] end, stderr_println("Loading database from \"~ts\"...", [DatabaseURL]), ok = locus:start_loader(DatabaseId, DatabaseURL, BaseOpts ++ ExtraOpts), wait_for_database_load(DatabaseId, LoadTimeout, SuccessHandler). wait_for_database_load(DatabaseId, LoadTimeout, SuccessHandler) -> receive {locus, DatabaseId, {load_attempt_finished, _, {ok, Version}}} -> stderr_println("Database version ~p successfully loaded", [Version]), SuccessHandler(DatabaseId); {locus, DatabaseId, {load_attempt_finished, _, {error, Reason}}} -> fall_from_grace("Failed to load database: ~p", [Reason]) after LoadTimeout -> fall_from_grace("Timeout loading the database") end. fall_from_grace() -> fall_from_grace("", []). fall_from_grace(MsgFmt) -> fall_from_grace(MsgFmt, []). fall_from_grace(MsgFmt, MsgArgs) -> _ = MsgFmt =/= "" andalso stderr_println("[ERROR] " ++ MsgFmt, MsgArgs), erlang:halt(1, [{flush, true}]). stderr_println(Fmt) -> stderr_println(Fmt, []). stderr_println(Fmt, Args) -> io:format(standard_error, Fmt ++ "~n", Args). %% ------------------------------------------------------------------ %% Internal Function Definitions - Check %% ------------------------------------------------------------------ handle_check_command(CmdArgs) -> OptSpecList = [ {load_timeout, undefined, "load-timeout", {integer, 30}, "Database load timeout (in seconds)"}, {log_level, undefined, "log-level", {string, "error"}, "debug | info | warning | error"}, {url, undefined, undefined, utf8_binary, "Database URL (local or remote)"}, {warnings_as_errors, undefined, "warnings-as-errors", {boolean, false}, "Treat check warnings as errors"} ], case getopt:parse_and_check(OptSpecList, CmdArgs) of {ok, {ParsedArgs, []}} -> {load_timeout, LoadTimeoutSecs} = lists:keyfind(load_timeout, 1, ParsedArgs), {log_level, StrLogLevel} = lists:keyfind(log_level, 1, ParsedArgs), {url, DatabaseURL} = lists:keyfind(url, 1, ParsedArgs), {warnings_as_errors, WarningsAsErrors} = lists:keyfind( warnings_as_errors, 1, ParsedArgs ), LoadTimeout = timer:seconds(LoadTimeoutSecs), LogLevel = list_to_atom(StrLogLevel), ok = locus_logger:set_loglevel(LogLevel), prepare_database( DatabaseURL, LoadTimeout, fun(DatabaseId) -> perform_check(DatabaseId, WarningsAsErrors) end ); _ -> getopt:usage(OptSpecList, "locus check"), fall_from_grace() end. perform_check(DatabaseId, WarningsAsErrors) -> stderr_println("Checking database for flaws..."), case locus:check(DatabaseId) of ok -> stderr_println("Database is wholesome."); {validation_warnings, Warnings} when not WarningsAsErrors -> stderr_println( "Database has a few quirks but is otherwise wholesome:~n" ++ lists:flatten(["* ~p~n" || _ <- Warnings]), WarningsAsErrors ); {validation_warnings, Warnings} when WarningsAsErrors -> stderr_println( "Database has a few quirks:~n" ++ lists:flatten(["* ~p~n" || _ <- Warnings]), WarningsAsErrors ); {validation_errors, Errors, [] = _Warnings} -> fall_from_grace( "Database is corrupt or incompatible:~n" ++ lists:flatten(["* ~p~n" || _ <- Errors]), Errors ); {validation_errors, Errors, Warnings} -> fall_from_grace( "Database is corrupt or incompatible:~n" ++ lists:flatten(["* ~p~n" || _ <- Errors]) ++ "~n...and also:~n" ++ lists:flatten(["* ~p~n" || _ <- Warnings]), Errors ++ Warnings ) end. -endif.