%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2025, %%% @doc %%% 用于管理manifest.json配置文件 %%% @end %%% Created : 05. 5月 2025 22:39 %%%------------------------------------------------------------------- -module(efka_manifest). -author("anlicheng"). -record(manifest, { work_dir = "" :: string(), id = <<"">> :: binary(), exec = <<"">>:: binary(), args = [], health_check = <<"">> }). -type manifest() :: #manifest{}. -export_type([manifest/0]). %% API -export([new/1, startup/1]). -spec new(WorkDir :: string()) -> {ok, #manifest{}} | {error, Reason :: binary()}. new(WorkDir) when is_list(WorkDir) -> case file:read_file(WorkDir ++ "manifest.json") of {ok, ManifestInfo} -> Settings = catch jiffy:decode(ManifestInfo, [return_maps]), case check_manifest(Settings) of {ok, Manifest} -> {ok, Manifest#manifest{work_dir = WorkDir}}; {error, Reason} -> {error, Reason} end; {error, Reason} -> {error, Reason} end. -spec startup(Manifest :: #manifest{}) -> {ok, Port :: port()} | {error, Reason :: binary()}. startup(#manifest{work_dir = WorkDir, exec = ExecCmd0, args = Args0}) -> PortSettings = [ {cd, WorkDir}, {args, [binary_to_list(A) || A <- Args0]}, exit_status ], ExecCmd = binary_to_list(ExecCmd0), RealExecCmd = filename:absname_join(WorkDir, ExecCmd), case catch erlang:open_port({spawn_executable, RealExecCmd}, PortSettings) of Port when is_port(Port) -> {ok, Port}; _Other -> {error, <<"exec command startup failed">>} end. %% 检查配置是否合法 -spec check_manifest(Manifest :: map()) -> {ok, #manifest{}} | {error, Reason :: binary()}. check_manifest(Manifest) when is_map(Manifest) -> RequiredKeys = [<<"id">>, <<"exec">>, <<"args">>, <<"health_check">>], check_manifest0(RequiredKeys, Manifest, #manifest{}); check_manifest(_Manifest) -> {error, <<"invalid manifest json">>}. check_manifest0([], _Settings, Manifest) -> {ok, Manifest}; check_manifest0([<<"id">>|T], Settings, Manifest) -> case maps:find(<<"id">>, Settings) of error -> {error, <<"miss service_id">>}; {ok, Id} when is_binary(Id) -> check_manifest0(T, Settings, Manifest#manifest{id = Id}); {ok, _} -> {error, <<"service_id is not string">>} end; check_manifest0([<<"health_check">>|T], Settings, Manifest) -> case maps:find(<<"health_check">>, Settings) of error -> {error, <<"miss health_check">>}; {ok, Url} when is_binary(Url) -> case is_url(Url) of true -> check_manifest0(T, Settings, Manifest#manifest{health_check = Url}); false -> {error, <<"health_check is not a invalid url">>} end; {ok, _} -> {error, <<"health_check is not string">>} end; check_manifest0([<<"exec">>|T], Settings, Manifest) -> case maps:find(<<"exec">>, Settings) of error -> {error, <<"miss start">>}; {ok, Exec} when is_binary(Exec) -> %% 不能包含空格 case binary:match(Exec, <<" ">>) of nomatch -> check_manifest0(T, Settings, Manifest#manifest{exec = Exec}); _ -> {error, <<"start cmd cannot contain args">>} end end; check_manifest0([<<"args">>|T], Settings, Manifest) -> case maps:find(<<"args">>, Settings) of error -> check_manifest0(T, Settings, Manifest#manifest{args = []}); %% 对参数项目不进行检查 {ok, Args} when is_list(Args) -> check_manifest0(T, Settings, Manifest#manifest{args = Args}); {ok, _} -> {error, <<"args must be list">>} end. -spec is_url(binary()) -> boolean(). is_url(Input) when is_binary(Input) -> try uri_string:parse(Input), true catch _:_ -> false end.