%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2025, %%% @doc %%% %%% @end %%% Created : 07. 5月 2025 15:47 %%%------------------------------------------------------------------- -module(efka_inetd_task). -author("anlicheng"). -include("efka_tables.hrl"). -behaviour(gen_server). %% API -export([start_link/4]). -export([deploy/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(SERVER, ?MODULE). -record(state, { root_dir :: string(), task_id :: integer(), service_id :: binary(), tar_url :: binary() }). %%%=================================================================== %%% API %%%=================================================================== -spec deploy(Pid :: pid()) -> no_return(). deploy(Pid) when is_pid(Pid) -> gen_server:cast(Pid, deploy). %% @doc Spawns the server and registers the local name (unique) -spec(start_link(TaskId :: integer(), RootDir :: string(), ServiceId :: binary(), TarUrl :: binary()) -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). start_link(TaskId, RootDir, ServiceId, TarUrl) when is_integer(TaskId), is_list(RootDir), is_binary(ServiceId), is_binary(TarUrl) -> gen_server:start_link(?MODULE, [TaskId, RootDir, ServiceId, TarUrl], []). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== %% @private %% @doc Initializes the server -spec(init(Args :: term()) -> {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {stop, Reason :: term()} | ignore). init([TaskId, RootDir, ServiceId, TarUrl]) -> {ok, #state{task_id = TaskId, root_dir = RootDir, service_id = ServiceId, tar_url = TarUrl}}. %% @private %% @doc Handling call messages -spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, State :: #state{}) -> {reply, Reply :: term(), NewState :: #state{}} | {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | {stop, Reason :: term(), NewState :: #state{}}). handle_call(_Request, _From, State = #state{}) -> {reply, ok, State}. %% @private %% @doc Handling cast messages -spec(handle_cast(Request :: term(), State :: #state{}) -> {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). handle_cast(deploy, State = #state{task_id = TaskId, root_dir = RootDir, service_id = ServiceId, tar_url = TarUrl}) -> %% 创建目录 {ok, ServiceRootDir} = ensure_dirs(RootDir, ServiceId), case check_lock(ServiceRootDir, TarUrl) of true -> {stop, normal, State}; false -> case check_download_url(TarUrl) of ok -> case download(binary_to_list(TarUrl), ServiceRootDir) of {ok, TarFile} -> efka_agent:feedback_phase(TaskId, efka_util:timestamp(), <<"download completed">>), {ok, WorkDir} = make_work_dir(ServiceRootDir), %% 清理目录下的文件 Result = delete_directory(WorkDir), lager:debug("delete_directory result is: ~p", [Result]), case tar_extract(TarFile, WorkDir) of ok -> %% 创建lock文件 touch_lock(ServiceRootDir, TarUrl), %% 更新数据 ok = service_model:insert(#micro_service{ service_id = ServiceId, tar_url = TarUrl, %% 工作目录 root_dir = ServiceRootDir, params = <<"">>, metrics = <<"">>, %% 状态: 0: 停止, 1: 运行中 status = 0 }), efka_agent:feedback_phase(TaskId, efka_util:timestamp(), <<"deploy success">>), {stop, normal, State}; {error, Reason} -> efka_agent:feedback_phase(TaskId, efka_util:timestamp(), <<"tar decompression error">>), {stop, {error, Reason}, State} end; {error, Reason} -> efka_agent:feedback_phase(TaskId, efka_util:timestamp(), <<"download error">>), {stop, {error, Reason}, State} end; {error, Reason} -> lager:debug("[efka_inetd] check_download_url: ~p, get error: ~p", [TarUrl, Reason]), efka_agent:feedback_phase(TaskId, efka_util:timestamp(), <<"download url error">>), {stop, {error, <<"download url error">>}, State} end end. %% @private %% @doc Handling all non call/cast messages -spec(handle_info(Info :: timeout() | term(), State :: #state{}) -> {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). handle_info(_Info, State = #state{}) -> {noreply, State}. %% @private %% @doc This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), State :: #state{}) -> term()). terminate(_Reason, _State = #state{}) -> ok. %% @private %% @doc Convert process state when code is changed -spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{}, Extra :: term()) -> {ok, NewState :: #state{}} | {error, Reason :: term()}). code_change(_OldVsn, State = #state{}, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec ensure_dirs(RootDir :: string(), ServerId :: binary()) -> {ok, ServerRootDir :: string()}. ensure_dirs(RootDir, ServerId) when is_list(RootDir), is_binary(ServerId) -> %% 根目录 ServiceRootDir = RootDir ++ "/" ++ binary_to_list(ServerId) ++ "/", ok = filelib:ensure_dir(ServiceRootDir), {ok, ServiceRootDir}. %% 工作逻辑,压缩文件需要解压到工作目录 -spec make_work_dir(ServiceRootDir :: string()) -> {ok, WorkDir :: string()}. make_work_dir(ServiceRootDir) when is_list(ServiceRootDir) -> %% 工作逻辑,压缩文件需要解压到工作目录 WorkDir = ServiceRootDir ++ "/work_dir/", ok = filelib:ensure_dir(WorkDir), {ok, WorkDir}. %% 递归删除目录下的问题 -spec delete_directory(Dir :: string()) -> ok | {error, Reason :: any()}. delete_directory(Dir) when is_list(Dir) -> % 递归删除目录内容 case file:list_dir(Dir) of {ok, Files} -> lists:foreach(fun(File) -> FullPath = filename:join(Dir, File), case filelib:is_dir(FullPath) of true -> delete_directory(FullPath); false -> file:delete(FullPath) end end, Files), % 删除空目录 file:del_dir(Dir); {error, enoent} -> ok; {error, Reason} -> {error, Reason} end. -spec check_lock(DirName :: string(), TarUrl :: binary()) -> boolean(). check_lock(DirName, TarUrl) when is_list(DirName), is_binary(TarUrl) -> FileName = DirName ++ ".efka.lock", case filelib:is_file(FileName) of true -> {ok, Content} = file:read_file(FileName), Content =:= TarUrl; false -> false end. -spec touch_lock(DirName :: string(), TarUrl :: binary()) -> boolean(). touch_lock(DirName, TarUrl) when is_list(DirName), is_binary(TarUrl) -> FileName = DirName ++ ".efka.lock", filelib:is_file(FileName) andalso file:delete(FileName), case file:write_file(FileName, TarUrl) of ok -> true; {error, _} -> false end. %% 通过head请求先判定下载地址是否正确 check_download_url(Url) when is_binary(Url) -> check_download_url(binary_to_list(Url)); check_download_url(Url) when is_list(Url) -> SslOpts = [ {ssl, [ % 完全禁用证书验证 {verify, verify_none} ]} ], case httpc:request(head, {Url, []}, SslOpts, [{sync, true}]) of {ok, {{_, 200, "OK"}, _Headers, _}} -> ok; {error, Reason} -> {error, Reason} end. %% 解压文件到指定目录 -spec tar_extract(TarFile :: string(), TargetDir :: string()) -> ok | {error, Reason :: term()}. tar_extract(TarFile, TargetDir) when is_list(TarFile), is_list(TargetDir) -> %% 判断文件的后缀名来判断 Ext = filename:extension(TarFile), case Ext of ".tar" -> erl_tar:extract(TarFile, [{cwd, TargetDir}, verbose]); ".gz" -> erl_tar:extract(TarFile, [compressed, {cwd, TargetDir}, verbose]) end. %% 下载文件 -spec download(Url :: string(), TargetDir :: string()) -> {ok, TarFile :: string()} | {error, Reason :: any()}. download(Url, TargetDir) when is_list(Url), is_list(TargetDir) -> SslOpts = [ {ssl, [ % 完全禁用证书验证 {verify, verify_none} ]} ], TargetFile = get_filename_from_url(Url), FullFilename = TargetDir ++ TargetFile, StartTs = os:timestamp(), case httpc:request(get, {Url, []}, SslOpts, [{sync, false}, {stream, self}]) of {ok, RequestId} -> case receive_data(RequestId, FullFilename) of ok -> EndTs = os:timestamp(), %% 计算操作的时间,单位为毫秒 CostMs = timer:now_diff(EndTs, StartTs) div 1000, lager:debug("[efka_downloader] download url: ~p, cost: ~p(ms)", [Url, CostMs]), {ok, FullFilename}; {error, Reason} -> %% 出错需要删除掉文件 file:delete(FullFilename), {error, Reason} end; {error, Reason} -> {error, Reason} end. %% 处理头部信息, 解析可能的文件名 receive_data(RequestId, FullFilename) -> receive {http, {RequestId, stream_start, _Headers}} -> {ok, File} = file:open(FullFilename, [write, binary]), receive_data0(RequestId, File); {http, {RequestId, {{_, 404, Status}, _Headers, Body}}} -> lager:debug("[efka_downloader] http_status: ~p, body: ~p", [Status, Body]), {error, Status} end. %% 接受文件数据 receive_data0(RequestId, File) -> receive {http, {RequestId, {error, Reason}}} -> ok = file:close(File), {error, Reason}; {http, {RequestId, stream_end, _Headers}} -> ok = file:close(File), ok; {http, {RequestId, stream, Data}} -> file:write(File, Data), receive_data0(RequestId, File) end. -spec get_filename_from_url(Url :: string()) -> string(). get_filename_from_url(Url) when is_list(Url) -> URIMap = uri_string:parse(Url), Path = maps:get(path, URIMap), filename:basename(Path).