emqx_plugins.erl 21 KB

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