emqx_plugins.erl 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2017-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. -module(emqx_plugins).
  17. -include_lib("emqx/include/emqx.hrl").
  18. -include_lib("emqx/include/logger.hrl").
  19. -export([ ensure_installed/1
  20. , ensure_uninstalled/1
  21. , ensure_enabled/1
  22. , ensure_enabled/2
  23. , ensure_disabled/1
  24. , purge/1
  25. , delete_package/1
  26. ]).
  27. -export([ ensure_started/0
  28. , ensure_started/1
  29. , ensure_stopped/0
  30. , ensure_stopped/1
  31. , restart/1
  32. , list/0
  33. , describe/1
  34. , parse_name_vsn/1
  35. ]).
  36. -export([ get_config/2
  37. , put_config/2
  38. ]).
  39. %% internal
  40. -export([ do_ensure_started/1
  41. ]).
  42. -export([
  43. install_dir/0
  44. ]).
  45. -ifdef(TEST).
  46. -compile(export_all).
  47. -compile(nowarn_export_all).
  48. -endif.
  49. -include_lib("emqx/include/emqx.hrl").
  50. -include_lib("emqx/include/logger.hrl").
  51. -include("emqx_plugins.hrl").
  52. -type name_vsn() :: binary() | string(). %% "my_plugin-0.1.0"
  53. -type plugin() :: map(). %% the parse result of the JSON info file
  54. -type position() :: no_move | front | rear | {before, name_vsn()} | {behind, name_vsn()}.
  55. %%--------------------------------------------------------------------
  56. %% APIs
  57. %%--------------------------------------------------------------------
  58. %% @doc Describe a plugin.
  59. -spec describe(name_vsn()) -> {ok, plugin()} | {error, any()}.
  60. describe(NameVsn) -> read_plugin(NameVsn, #{fill_readme => true}).
  61. %% @doc Install a .tar.gz package placed in install_dir.
  62. -spec ensure_installed(name_vsn()) -> ok | {error, any()}.
  63. ensure_installed(NameVsn) ->
  64. case read_plugin(NameVsn, #{}) of
  65. {ok, _} ->
  66. ok;
  67. {error, _} ->
  68. ok = purge(NameVsn),
  69. do_ensure_installed(NameVsn)
  70. end.
  71. do_ensure_installed(NameVsn) ->
  72. TarGz = pkg_file(NameVsn),
  73. case erl_tar:extract(TarGz, [{cwd, install_dir()}, compressed]) of
  74. ok ->
  75. case read_plugin(NameVsn, #{}) of
  76. {ok, _} -> ok;
  77. {error, Reason} ->
  78. ?SLOG(warning, Reason#{msg => "failed_to_read_after_install"}),
  79. _ = ensure_uninstalled(NameVsn),
  80. {error, Reason}
  81. end;
  82. {error, {_, enoent}} ->
  83. {error, #{ reason => "failed_to_extract_plugin_package"
  84. , path => TarGz
  85. , return => not_found
  86. }};
  87. {error, Reason} ->
  88. {error, #{ reason => "bad_plugin_package"
  89. , path => TarGz
  90. , return => Reason
  91. }}
  92. end.
  93. %% @doc Ensure files and directories for the given plugin are delete.
  94. %% If a plugin is running, or enabled, error is returned.
  95. -spec ensure_uninstalled(name_vsn()) -> ok | {error, any()}.
  96. ensure_uninstalled(NameVsn) ->
  97. case read_plugin(NameVsn, #{}) of
  98. {ok, #{running_status := RunningSt}} when RunningSt =/= stopped ->
  99. {error, #{reason => "bad_plugin_running_status",
  100. hint => "stop_the_plugin_first"
  101. }};
  102. {ok, #{config_status := enabled}} ->
  103. {error, #{reason => "bad_plugin_config_status",
  104. hint => "disable_the_plugin_first"
  105. }};
  106. _ ->
  107. purge(NameVsn)
  108. end.
  109. %% @doc Ensure a plugin is enabled to the end of the plugins list.
  110. -spec ensure_enabled(name_vsn()) -> ok | {error, any()}.
  111. ensure_enabled(NameVsn) ->
  112. ensure_enabled(NameVsn, no_move).
  113. %% @doc Ensure a plugin is enabled at the given position of the plugin list.
  114. -spec ensure_enabled(name_vsn(), position()) -> ok | {error, any()}.
  115. ensure_enabled(NameVsn, Position) ->
  116. ensure_state(NameVsn, Position, true).
  117. %% @doc Ensure a plugin is disabled.
  118. -spec ensure_disabled(name_vsn()) -> ok | {error, any()}.
  119. ensure_disabled(NameVsn) ->
  120. ensure_state(NameVsn, no_move, false).
  121. ensure_state(NameVsn, Position, State) when is_binary(NameVsn) ->
  122. ensure_state(binary_to_list(NameVsn), Position, State);
  123. ensure_state(NameVsn, Position, State) ->
  124. case read_plugin(NameVsn, #{}) of
  125. {ok, _} ->
  126. Item = #{ name_vsn => NameVsn
  127. , enable => State
  128. },
  129. tryit("ensure_state", fun() -> ensure_configured(Item, Position) end);
  130. {error, Reason} ->
  131. {error, Reason}
  132. end.
  133. ensure_configured(#{name_vsn := NameVsn} = Item, Position) ->
  134. Configured = configured(),
  135. SplitFun = fun(#{name_vsn := Nv}) -> bin(Nv) =/= bin(NameVsn) end,
  136. {Front, Rear} = lists:splitwith(SplitFun, Configured),
  137. NewConfigured =
  138. case Rear of
  139. [_ | More] when Position =:= no_move ->
  140. Front ++ [Item | More];
  141. [_ | More] ->
  142. add_new_configured(Front ++ More, Position, Item);
  143. [] ->
  144. add_new_configured(Configured, Position, Item)
  145. end,
  146. ok = put_configured(NewConfigured).
  147. add_new_configured(Configured, no_move, Item) ->
  148. %% default to rear
  149. add_new_configured(Configured, rear, Item);
  150. add_new_configured(Configured, front, Item) ->
  151. [Item | Configured];
  152. add_new_configured(Configured, rear, Item) ->
  153. Configured ++ [Item];
  154. add_new_configured(Configured, {Action, NameVsn}, Item) ->
  155. SplitFun = fun(#{name_vsn := Nv}) -> bin(Nv) =/= bin(NameVsn) end,
  156. {Front, Rear} = lists:splitwith(SplitFun, Configured),
  157. Rear =:= [] andalso
  158. throw(#{error => "position_anchor_plugin_not_configured",
  159. hint => "maybe_install_and_configure",
  160. name_vsn => NameVsn
  161. }),
  162. case Action of
  163. before -> Front ++ [Item | Rear];
  164. behind ->
  165. [Anchor | Rear0] = Rear,
  166. Front ++ [Anchor, Item | Rear0]
  167. end.
  168. %% @doc Delete the package file.
  169. -spec delete_package(name_vsn()) -> ok.
  170. delete_package(NameVsn) ->
  171. File = pkg_file(NameVsn),
  172. case file:delete(File) of
  173. ok ->
  174. ?SLOG(info, #{msg => "purged_plugin_dir", path => File}),
  175. ok;
  176. {error, enoent} ->
  177. ok;
  178. {error, Reason} ->
  179. ?SLOG(error, #{msg => "failed_to_delete_package_file",
  180. path => File,
  181. reason => Reason}),
  182. {error, Reason}
  183. end.
  184. %% @doc Delete extracted dir
  185. %% In case one lib is shared by multiple plugins.
  186. %% it might be the case that purging one plugin's install dir
  187. %% will cause deletion of loaded beams.
  188. %% It should not be a problem, because shared lib should
  189. %% reside in all the plugin install dirs.
  190. -spec purge(name_vsn()) -> ok.
  191. purge(NameVsn) ->
  192. Dir = dir(NameVsn),
  193. case file:del_dir_r(Dir) of
  194. ok ->
  195. ?SLOG(info, #{msg => "purged_plugin_dir", dir => Dir});
  196. {error, enoent} ->
  197. ok;
  198. {error, Reason} ->
  199. ?SLOG(error, #{msg => "failed_to_purge_plugin_dir",
  200. dir => Dir,
  201. reason => Reason}),
  202. {error, Reason}
  203. end.
  204. %% @doc Start all configured plugins are started.
  205. -spec ensure_started() -> ok.
  206. ensure_started() ->
  207. ok = for_plugins(fun ?MODULE:do_ensure_started/1).
  208. %% @doc Start a plugin from Management API or CLI.
  209. %% the input is a <name>-<vsn> string.
  210. -spec ensure_started(name_vsn()) -> ok | {error, term()}.
  211. ensure_started(NameVsn) ->
  212. case do_ensure_started(NameVsn) of
  213. ok -> ok;
  214. {error, Reason} ->
  215. ?SLOG(alert, #{msg => "failed_to_start_plugin",
  216. reason => Reason}),
  217. {error, Reason}
  218. end.
  219. %% @doc Stop all plugins before broker stops.
  220. -spec ensure_stopped() -> ok.
  221. ensure_stopped() ->
  222. for_plugins(fun ?MODULE:ensure_stopped/1).
  223. %% @doc Stop a plugin from Management API or CLI.
  224. -spec ensure_stopped(name_vsn()) -> ok | {error, term()}.
  225. ensure_stopped(NameVsn) ->
  226. tryit("stop_plugin",
  227. fun() ->
  228. Plugin = do_read_plugin(NameVsn),
  229. ensure_apps_stopped(Plugin)
  230. end).
  231. %% @doc Stop and then start the plugin.
  232. restart(NameVsn) ->
  233. case ensure_stopped(NameVsn) of
  234. ok -> ensure_started(NameVsn);
  235. {error, Reason} -> {error, Reason}
  236. end.
  237. %% @doc List all installed plugins.
  238. %% Including the ones that are installed, but not enabled in config.
  239. -spec list() -> [plugin()].
  240. list() ->
  241. Pattern = filename:join([install_dir(), "*", "release.json"]),
  242. All = lists:filtermap(
  243. fun(JsonFile) ->
  244. case read_plugin({file, JsonFile}, #{}) of
  245. {ok, Info} ->
  246. {true, Info};
  247. {error, Reason} ->
  248. ?SLOG(warning, Reason),
  249. false
  250. end
  251. end, filelib:wildcard(Pattern)),
  252. list(configured(), All).
  253. %% Make sure configured ones are ordered in front.
  254. list([], All) -> All;
  255. list([#{name_vsn := NameVsn} | Rest], All) ->
  256. SplitF = fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) ->
  257. bin([Name, "-", Vsn]) =/= bin(NameVsn)
  258. end,
  259. case lists:splitwith(SplitF, All) of
  260. {_, []} ->
  261. ?SLOG(warning, #{msg => "configured_plugin_not_installed",
  262. name_vsn => NameVsn
  263. }),
  264. list(Rest, All);
  265. {Front, [I | Rear]} ->
  266. [I | list(Rest, Front ++ Rear)]
  267. end.
  268. do_ensure_started(NameVsn) ->
  269. tryit("start_plugins",
  270. fun() ->
  271. Plugin = do_read_plugin(NameVsn),
  272. ok = load_code_start_apps(NameVsn, Plugin)
  273. end).
  274. %% try the function, catch 'throw' exceptions as normal 'error' return
  275. %% other exceptions with stacktrace returned.
  276. tryit(WhichOp, F) ->
  277. try
  278. F()
  279. catch
  280. throw : Reason ->
  281. %% thrown exceptions are known errors
  282. %% translate to a return value without stacktrace
  283. {error, Reason};
  284. error : Reason : Stacktrace ->
  285. %% unexpected errors, log stacktrace
  286. ?SLOG(warning, #{ msg => "plugin_op_failed"
  287. , which_op => WhichOp
  288. , exception => Reason
  289. , stacktrace => Stacktrace
  290. }),
  291. {error, {failed, WhichOp}}
  292. end.
  293. %% read plugin info from the JSON file
  294. %% returns {ok, Info} or {error, Reason}
  295. read_plugin(NameVsn, Options) ->
  296. tryit("read_plugin_info",
  297. fun() -> {ok, do_read_plugin(NameVsn, Options)} end).
  298. do_read_plugin(Plugin) -> do_read_plugin(Plugin, #{}).
  299. do_read_plugin({file, InfoFile}, Options) ->
  300. [_, NameVsn | _] = lists:reverse(filename:split(InfoFile)),
  301. case hocon:load(InfoFile, #{format => richmap}) of
  302. {ok, RichMap} ->
  303. Info0 = check_plugin(hocon_maps:ensure_plain(RichMap), NameVsn, InfoFile),
  304. Info1 = plugins_readme(NameVsn, Options, Info0),
  305. plugin_status(NameVsn, Info1);
  306. {error, Reason} ->
  307. throw(#{error => "bad_info_file",
  308. path => InfoFile,
  309. return => Reason
  310. })
  311. end;
  312. do_read_plugin(NameVsn, Options) ->
  313. do_read_plugin({file, info_file(NameVsn)}, Options).
  314. plugins_readme(NameVsn, #{fill_readme := true}, Info) ->
  315. case file:read_file(readme_file(NameVsn)) of
  316. {ok, Bin} -> Info#{readme => Bin};
  317. _ -> Info#{readme => <<>>}
  318. end;
  319. plugins_readme(_NameVsn, _Options, Info) -> Info.
  320. plugin_status(NameVsn, Info) ->
  321. {AppName, _AppVsn} = parse_name_vsn(NameVsn),
  322. RunningSt =
  323. case application:get_key(AppName, vsn) of
  324. {ok, _} ->
  325. case lists:keyfind(AppName, 1, running_apps()) of
  326. {AppName, _} -> running;
  327. _ -> loaded
  328. end;
  329. undefined ->
  330. stopped
  331. end,
  332. Configured = lists:filtermap(
  333. fun(#{name_vsn := Nv, enable := St}) ->
  334. case bin(Nv) =:= bin(NameVsn) of
  335. true -> {true, St};
  336. false -> false
  337. end
  338. end, configured()),
  339. ConfSt = case Configured of
  340. [] -> not_configured;
  341. [true] -> enabled;
  342. [false] -> disabled
  343. end,
  344. Info#{ running_status => RunningSt
  345. , config_status => ConfSt
  346. }.
  347. bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
  348. bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8);
  349. bin(B) when is_binary(B) -> B.
  350. check_plugin(#{ <<"name">> := Name
  351. , <<"rel_vsn">> := Vsn
  352. , <<"rel_apps">> := Apps
  353. , <<"description">> := _
  354. } = Info, NameVsn, File) ->
  355. case bin(NameVsn) =:= bin([Name, "-", Vsn]) of
  356. true ->
  357. try
  358. [_ | _ ] = Apps, %% assert
  359. %% validate if the list is all <app>-<vsn> strings
  360. lists:foreach(fun parse_name_vsn/1, Apps)
  361. catch
  362. _ : _ ->
  363. throw(#{ error => "bad_rel_apps"
  364. , rel_apps => Apps
  365. , hint => "A non-empty string list of app_name-app_vsn format"
  366. })
  367. end,
  368. Info;
  369. false ->
  370. throw(#{ error => "name_vsn_mismatch"
  371. , name_vsn => NameVsn
  372. , path => File
  373. , name => Name
  374. , rel_vsn => Vsn
  375. })
  376. end;
  377. check_plugin(_What, NameVsn, File) ->
  378. throw(#{ error => "bad_info_file_content"
  379. , mandatory_fields => [rel_vsn, name, rel_apps, description]
  380. , name_vsn => NameVsn
  381. , path => File
  382. }).
  383. load_code_start_apps(RelNameVsn, #{<<"rel_apps">> := Apps}) ->
  384. LibDir = filename:join([install_dir(), RelNameVsn]),
  385. RunningApps = running_apps(),
  386. %% load plugin apps and beam code
  387. AppNames =
  388. lists:map(fun(AppNameVsn) ->
  389. {AppName, AppVsn} = parse_name_vsn(AppNameVsn),
  390. EbinDir = filename:join([LibDir, AppNameVsn, "ebin"]),
  391. ok = load_plugin_app(AppName, AppVsn, EbinDir, RunningApps),
  392. AppName
  393. end, Apps),
  394. lists:foreach(fun start_app/1, AppNames).
  395. load_plugin_app(AppName, AppVsn, Ebin, RunningApps) ->
  396. case lists:keyfind(AppName, 1, RunningApps) of
  397. false -> do_load_plugin_app(AppName, Ebin);
  398. {_, Vsn} ->
  399. case bin(Vsn) =:= bin(AppVsn) of
  400. true ->
  401. %% already started on the exact version
  402. ok;
  403. false ->
  404. %% running but a different version
  405. ?SLOG(warning, #{msg => "plugin_app_already_running", name => AppName,
  406. running_vsn => Vsn,
  407. loading_vsn => AppVsn
  408. })
  409. end
  410. end.
  411. do_load_plugin_app(AppName, Ebin) when is_binary(Ebin) ->
  412. do_load_plugin_app(AppName, binary_to_list(Ebin));
  413. do_load_plugin_app(AppName, Ebin) ->
  414. _ = code:add_patha(Ebin),
  415. Modules = filelib:wildcard(filename:join([Ebin, "*.beam"])),
  416. lists:foreach(
  417. fun(BeamFile) ->
  418. Module = list_to_atom(filename:basename(BeamFile, ".beam")),
  419. case code:load_file(Module) of
  420. {module, _} -> ok;
  421. {error, Reason} -> throw(#{error => "failed_to_load_plugin_beam",
  422. path => BeamFile,
  423. reason => Reason
  424. })
  425. end
  426. end, Modules),
  427. case application:load(AppName) of
  428. ok -> ok;
  429. {error, {already_loaded, _}} -> ok;
  430. {error, Reason} -> throw(#{error => "failed_to_load_plugin_app",
  431. name => AppName,
  432. reason => Reason})
  433. end.
  434. start_app(App) ->
  435. case application:ensure_all_started(App) of
  436. {ok, Started} ->
  437. case Started =/= [] of
  438. true -> ?SLOG(debug, #{msg => "started_plugin_apps", apps => Started});
  439. false -> ok
  440. end,
  441. ?SLOG(debug, #{msg => "started_plugin_app", app => App}),
  442. ok;
  443. {error, {ErrApp, Reason}} ->
  444. throw(#{error => "failed_to_start_plugin_app",
  445. app => App,
  446. err_app => ErrApp,
  447. reason => Reason
  448. })
  449. end.
  450. %% Stop all apps installed by the plugin package,
  451. %% but not the ones shared with others.
  452. ensure_apps_stopped(#{<<"rel_apps">> := Apps}) ->
  453. %% load plugin apps and beam code
  454. AppsToStop =
  455. lists:map(fun(NameVsn) ->
  456. {AppName, _AppVsn} = parse_name_vsn(NameVsn),
  457. AppName
  458. end, Apps),
  459. case tryit("stop_apps", fun() -> stop_apps(AppsToStop) end) of
  460. {ok, []} ->
  461. %% all apps stopped
  462. ok;
  463. {ok, Left} ->
  464. ?SLOG(warning, #{msg => "unabled_to_stop_plugin_apps",
  465. apps => Left
  466. }),
  467. ok;
  468. {error, Reason} ->
  469. {error, Reason}
  470. end.
  471. stop_apps(Apps) ->
  472. RunningApps = running_apps(),
  473. case do_stop_apps(Apps, [], RunningApps) of
  474. {ok, []} -> {ok, []}; %% all stopped
  475. {ok, Remain} when Remain =:= Apps -> {ok, Apps}; %% no progress
  476. {ok, Remain} -> stop_apps(Remain) %% try again
  477. end.
  478. do_stop_apps([], Remain, _AllApps) ->
  479. {ok, lists:reverse(Remain)};
  480. do_stop_apps([App | Apps], Remain, RunningApps) ->
  481. case is_needed_by_any(App, RunningApps) of
  482. true ->
  483. do_stop_apps(Apps, [App | Remain], RunningApps);
  484. false ->
  485. ok = stop_app(App),
  486. do_stop_apps(Apps, Remain, RunningApps)
  487. end.
  488. stop_app(App) ->
  489. case application:stop(App) of
  490. ok ->
  491. ?SLOG(debug, #{msg => "stop_plugin_successfully", app => App}),
  492. ok = unload_moudle_and_app(App);
  493. {error, {not_started, App}} ->
  494. ?SLOG(debug, #{msg => "plugin_not_started", app => App}),
  495. ok = unload_moudle_and_app(App);
  496. {error, Reason} ->
  497. throw(#{error => "failed_to_stop_app", app => App, reason => Reason})
  498. end.
  499. unload_moudle_and_app(App) ->
  500. case application:get_key(App, modules) of
  501. {ok, Modules} -> lists:foreach(fun code:soft_purge/1, Modules);
  502. _ -> ok
  503. end,
  504. _ = application:unload(App),
  505. ok.
  506. is_needed_by_any(AppToStop, RunningApps) ->
  507. lists:any(fun({RunningApp, _RunningAppVsn}) ->
  508. is_needed_by(AppToStop, RunningApp)
  509. end, RunningApps).
  510. is_needed_by(AppToStop, AppToStop) -> false;
  511. is_needed_by(AppToStop, RunningApp) ->
  512. case application:get_key(RunningApp, applications) of
  513. {ok, Deps} -> lists:member(AppToStop, Deps);
  514. undefined -> false
  515. end.
  516. put_config(Key, Value) when is_atom(Key) ->
  517. put_config([Key], Value);
  518. put_config(Path, Values) when is_list(Path) ->
  519. Opts = #{rawconf_with_defaults => true, override_to => cluster},
  520. case emqx:update_config([?CONF_ROOT | Path], bin_key(Values), Opts) of
  521. {ok, _} -> ok;
  522. Error -> Error
  523. end.
  524. bin_key(Map) when is_map(Map) ->
  525. maps:fold(fun(K, V, Acc) -> Acc#{bin(K) => V} end, #{}, Map);
  526. bin_key(List = [#{} | _]) ->
  527. lists:map(fun(M) -> bin_key(M) end, List);
  528. bin_key(Term) -> Term.
  529. get_config(Key, Default) when is_atom(Key) ->
  530. get_config([Key], Default);
  531. get_config(Path, Default) ->
  532. emqx_conf:get([?CONF_ROOT | Path], Default).
  533. install_dir() -> get_config(install_dir, "").
  534. put_configured(Configured) ->
  535. ok = put_config(states, bin_key(Configured)).
  536. configured() ->
  537. get_config(states, []).
  538. for_plugins(ActionFun) ->
  539. case lists:flatmap(fun(I) -> for_plugin(I, ActionFun) end, configured()) of
  540. [] -> ok;
  541. Errors -> erlang:error(#{function => ActionFun, errors => Errors})
  542. end.
  543. for_plugin(#{name_vsn := NameVsn, enable := true}, Fun) ->
  544. case Fun(NameVsn) of
  545. ok -> [];
  546. {error, Reason} -> [{NameVsn, Reason}]
  547. end;
  548. for_plugin(#{name_vsn := NameVsn, enable := false}, _Fun) ->
  549. ?SLOG(debug, #{msg => "plugin_disabled",
  550. name_vsn => NameVsn}),
  551. [].
  552. parse_name_vsn(NameVsn) when is_binary(NameVsn) ->
  553. parse_name_vsn(binary_to_list(NameVsn));
  554. parse_name_vsn(NameVsn) when is_list(NameVsn) ->
  555. {AppName, [$- | Vsn]} = lists:splitwith(fun(X) -> X =/= $- end, NameVsn),
  556. {list_to_atom(AppName), Vsn}.
  557. pkg_file(NameVsn) ->
  558. filename:join([install_dir(), bin([NameVsn, ".tar.gz"])]).
  559. dir(NameVsn) ->
  560. filename:join([install_dir(), NameVsn]).
  561. info_file(NameVsn) ->
  562. filename:join([dir(NameVsn), "release.json"]).
  563. readme_file(NameVsn) ->
  564. filename:join([dir(NameVsn), "README.md"]).
  565. running_apps() ->
  566. lists:map(fun({N, _, V}) ->
  567. {N, V}
  568. end, application:which_applications(infinity)).