emqx_plugins.erl 24 KB

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