%% Licensed to the Apache Software Foundation (ASF) under one %% or more contributor license agreements. See the NOTICE file %% distributed with this work for additional information %% regarding copyright ownership. The ASF licenses this file %% to you under the Apache License, Version 2.0 (the %% "License"); you may not use this file except in compliance %% with the License. You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, %% software distributed under the License is distributed on an %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY %% KIND, either express or implied. See the License for the %% specific language governing permissions and limitations %% under the License. %% %% @doc %% Functions for interacting with `systemd' features. %% %% @since 0.1.0 %% @end -module(systemd). -type state() :: ready | stopping | reloading | {status, unicode:chardata()} | {errno, non_neg_integer()} | {buserror, unicode:chardata()} | {extend_timeout, {non_neg_integer(), erlang:time_unit()}} | {unicode:chardata() | atom(), unicode:chardata()}. -type sd_timeout() :: pos_integer(). -type fd() :: integer() | {integer(), unicode:chardata()}. -export_type([ state/0, fd/0, sd_timeout/0 ]). -include_lib("kernel/include/file.hrl"). -include("systemd_internal.hrl"). -export([ unset_env/1, notify/1, ready/0, ready/1, reload/0, reload/1, set_status/1, watchdog/1, listen_fds/0, store_fds/1, clear_fds/1, booted/0, is_journal/1 ]). -export([notify_spawn/2]). %% @doc %% Unset environment variables for given subsystem. %% %% Most environment variables will be cleaned on startup by default. To prevent %% such behaviour set `unset_env' application variable to `false'. %% %%
%%
`unset_env(notify)'
%%
Unset variables used by {@link notify/1. `notify/1'}. This call will %% be done automatically when the `unset_env' application option is set %% (default). It is highly encouraged to unset these variables to prevent %% them from being passed to subprocesses.
%%
`unset_env(watchdog)'
%%
Unset variables used by {@link watchdog/1. `watchdog/1'}. This call %% will be done automatically when the `unset_env' application option is %% set (default). It is highly encouraged to unset these variables %% to prevent them from being passed to subprocesses.
%%
`unset_env(listen_fds)'
%%
Unset variables used by {@link listen_fds/0. `listen_fds/0'}. After %% that all subsequent calls to `listen_fds' will return empty list. It is %% highly encouraged to unset these variables to prevent them from being %% passed to the subprocesses.
%%
%% %% @since 0.4.0 %% @end -spec unset_env(Subsystem) -> ok when Subsystem :: notify | watchdog | listen_fds. unset_env(notify) -> os:unsetenv(?NOTIFY_SOCKET), ok; unset_env(watchdog) -> os:unsetenv(?WATCHDOG_PID), os:unsetenv(?WATCHDOG_TIMEOUT), ok; unset_env(listen_fds) -> os:unsetenv(?LISTEN_PID), os:unsetenv(?LISTEN_FDS), os:unsetenv(?LISTEN_FDNAMES), ok. %% ---------------------------------------------------------------------------- %% @doc %% Send notification to the `systemd' socket. %% %% == Arguments == %% %%
%%
`notify(ready)'
%%
Notify that application is ready for work. If used with %% `Type=notify' in `systemd.service(5)' file then it will block %% `systemctl start' until this is called.
%%
`notify(stopping)'
%%
Notify that application has already started shutting down, but is %% not yet ready to stop. For example when you want to do connection %% draining or you have to do some cleaning before fully stopping. %% %% This will be automatically set for you in `systemd''s application %% `prep_stop/1' step, so user do not need to call it manually.
%%
`notify(reloading)'
%%
Notify that application is reloading. It is left up to user what %% is considered reloading and handle this call manually.
%%
`notify({errno, Errno :: integer()})'
%%
Notify that application encountered `Errno' in C's `errno' %% format. %% %% Implemented only for feature parity.
%%
`notify({buserror, Error :: unicode:chardata()})'
%%
Notify about DBus error.
%%
`notify({extend_timeout, {integer(), erlang:time_unit()}})'
%%
Request extension of timeout for sending `notify(ready)'. This is %% useful in case of setups that are taking more time than originally %% expected, for example because of retries in connecting to external %% service. %% %% This message must be sent within original timeout.
%%
%% %% == Return value == %% %% Returns `ok' on success or if the message was ignored because there is no %% systemd notify socket. Only error that can be returned as `0.6.0' is when %% the file descriptors are passed and at least one of them is not correct file %% descriptor (for example it is closed), then this call will return `{error, %% bad_descriptor}'. %% %% @since 0.1.0 %% @end -spec notify(State :: state() | [state()]) -> ok. notify(States) when is_list(States) -> systemd_socket:send([normalize_state(State) || State <- States], 0, []); notify(State) -> notify([State]). normalize_state(ready) -> {ready, "1"}; normalize_state(stopping) -> {stopping, "1"}; normalize_state(reloading) -> {reloading, "1"}; normalize_state({errno, Errno}) when is_integer(Errno) -> {"ERRNO", integer_to_binary(Errno)}; normalize_state({buserror, Error}) -> {"BUSERROR", Error}; normalize_state({extend_timeout, {Time, Unit}}) when is_integer(Time) -> Microsecs = erlang:convert_time_unit(Time, Unit, microsecond), {"EXTEND_TIMEOUT_USEC", integer_to_binary(Microsecs)}; normalize_state({_, _} = Msg) -> Msg. %% @doc %% Returns child spec for task that will inform systemd that application is %% ready. %% %% This is helper function that will return `supervisor:child_spec/0' map that %% contains temporary job that will notify systemd about application readiness. %% This is meant to be inserted into your supervison tree when application is %% ready (usually at the end). %% @end ready() -> ready(undefined). ready(undefined) -> Up = [{"MAINPID", os:getpid()}, ready], set_status(#{up => Up}); ready(Status) -> Up = [{"MAINPID", os:getpid()}, ready, {status, Status}], set_status(#{up => Up}). -spec set_status(Statuses) -> supervisor:child_spec() when Statuses :: MapStatuses | ListStatuses, ListStatuses :: [{up | down, state()}], MapStatuses :: #{up := state(), down := state()}. set_status(List) when is_list(List) -> set_status(maps:from_list(List)); set_status(Map) -> Up = maps:get(up, Map, undefined), Down = maps:get(down, Map, undefined), #{ id => {?MODULE, {Up, Down}}, start => {?MODULE, notify_spawn, [Up, Down]}, restart => temporary }. %% @private helper for `ready/0' function that spawn process that notify about %% application readiness. notify_spawn(Up, Down) -> Parent = self(), Pid = spawn_link(fun() -> notify_init(Parent, Up, Down) end), {ok, Pid}. notify_init(Parent, Up, Down) -> process_flag(trap_exit, true), try_notify(Up), notify_loop(Parent, Down). notify_loop(Parent, Down) -> receive {'EXIT', Parent, _Reason} -> try_notify(Down), ok; _ -> notify_loop(Parent, Down) end. try_notify(undefined) -> ok; try_notify(Message) -> notify(Message). %% @equiv reload([]) reload() -> reload([]). %% @doc Restart VM with informing the systemd about reload. %% %% This is esentially the same as `init:restart(Opts)' with the difference that %% it will inform the systemd that the system is reloading instead of shutting %% down. If you use `init:restart/{0,1}' anywhere in your application you need %% to change it to this function otherwise your application may be forced to %% exit by systemd on reloads. %% @end reload(Opts) -> persistent_term:put({?MODULE, shutdown}, reloading), init:restart(Opts). %% ---------------------------------------------------------------------------- %% @doc %% Manage watchdog process. %% %% By default `systemd' will handle Watchdog process automatically for you. %% %% == Arguments == %% %%
%%
`watchdog(state) -> sd_timeout()'
%%
Returns state of the Watchdog process. Which either be integer %% representing timeout in microseconds or `false' if Watchdog process is %% disabled.
%%
`watchdog(enable) -> ok'
%%
Enable Watchdog process. Watchdog process is automatically enabled %% when needed after startup, so this will be only needed if user manually %% called `watchdog(disable)'.
%%
`watchdog(disable) -> ok'
%%
Disable Watchdog process. This will cause no keep-alive messages to %% be sent.
%%
`watchdog(trigger) -> ok'
%%
Manually send keep-alive message to the Watchdog. It will not reset %% timer and will not disturb regular process pinging.
%%
%% %% == Options == %% %%
%%
`watchdog_scale'
%%
Divider of the timeout to send messages more often than this is %% required to prevent any jitter. %% %% Defaults to `2' which will send messages twice as often as needed.
%%
`watchdog_check'
%%
Function (specified as 0-ary function or `mfa()') that will be ran %% before pinging watchdog process. Such function should return `true' if %% the application is in functioning state and `false' otherwise. %% %% Defaults to function that always return `true'.
%%
%% %% @since 0.1.0 %% @end -spec watchdog (state) -> sd_timeout(); (trigger) -> ok; (enable) -> ok; (disable) -> ok; (ping) -> ok. watchdog(ping) -> systemd:notify({watchdog, "1"}); watchdog(trigger) -> systemd:notify({watchdog, "trigger"}); watchdog(state) -> gen_server:call(?WATCHDOG, state); watchdog(enable) -> gen_server:call(?WATCHDOG, enable); watchdog(disable) -> gen_server:call(?WATCHDOG, disable). %% ---------------------------------------------------------------------------- %% @doc %% Check if system was booted with systemd. %% %% Note that all other functions in this module are safe to call even on %% non-systemd boots (even on non-systemd platforms). You should NOT %% protect them with a call to this function. Also note that this checks wheter %% the system, not the user session, is controlled by systemd. However other %% functions will work for both - user and system services. %% %% @returns `{ok, true}' if system was booted with systemd, `{ok,false}' if not, %% and `{error, Reason}' on error. %% %% @since 0.1.0 %% @end -spec booted() -> {ok, boolean()} | {error, file:posix()}. booted() -> case file:read_file_info("/run/systemd/system/", [{time, posix}]) of {ok, _} -> {ok, true}; {error, enoent} -> {ok, false}; Error -> Error end. is_journal(Type) -> case get_journal_stream() of {Dev, Inode} -> case file_info(Type) of {ok, #file_info{major_device = Dev, inode = Inode}} -> true; _ -> false end; _ -> false end. get_journal_stream() -> Env = os:getenv("JOURNAL_STREAM", ""), case string:split(Env, ":") of [DevStr, InodeStr | _] -> try {list_to_integer(DevStr), list_to_integer(InodeStr)} catch error:badarg:_ -> error end; _ -> error end. file_info(standard_io) -> file:read_file_info("/dev/stdout"); file_info(standard_error) -> file:read_file_info("/dev/stderr"); file_info(_) -> {error, unknown_device}. %% ---------------------------------------------------------------------------- %% @doc %% Returns list of file descriptors passed to the application by systemd. %% %% @returns List of passed file descriptors. If descriptor have name defined %% then it will be returned as 2nd value in tuple. Order of returned descriptors %% is the same as passed in environment. %% %% @since 0.2.0 %% @end -spec listen_fds() -> [fd()]. listen_fds() -> case check_listen_pid() of true -> Count = listen_fds_count(), Names = listen_names(), generate_fds(Count, Names); false -> [] end. %% @doc %% Send given file descriptors to supervisor for storage between VM restarts. %% %% == Warning == %% This currently assumes that the currently used ABI is using 32-bit `int's. %% This is true for Linux at least for IA-32, x86-64, AArch64, AArch32, SPARC, %% OpenRISC, and RISC-V (required by POSIX). So while that shouldn't cause any %% problems, beware, that currently it may fail on some exotic platforms. %% %% @returns `ok' on success, `{error, bad_descriptor}' if any file descriptor in %% the passed list is invalid (isn't file desciptor or is closed). %% %% @since 0.6.0 %% @end -spec store_fds([fd()]) -> ok | {error, term()}. store_fds(List) when is_list(List) -> {Names, Fds} = build_fds(List), systemd_socket:send( [ {"FDSTORE", "1"}, {"FDNAMES", lists:join($:, Names)} ], 0, Fds ). %% @doc %% Removes given named filedescriptors from the store. %% %% @since 0.6.0 %% @end -spec clear_fds([unicode:chardata()]) -> ok. clear_fds(Names) -> systemd_socket:send( [ {"FDSTOREREMOVE", "1"}, {"FDNAMES", lists:join($:, Names)} ], 0, [] ). check_listen_pid() -> os:getenv(?LISTEN_PID) == os:getpid(). listen_fds_count() -> case os:getenv(?LISTEN_FDS) of false -> 0; Env -> case string:to_integer(Env) of {Value, ""} -> Value; _ -> 0 end end. listen_names() -> case os:getenv(?LISTEN_FDNAMES) of false -> []; Env -> string:split(Env, ":", all) end. generate_fds(Count, Names) -> generate_fds(?LISTEN_FDS_START, Count, Names, []). generate_fds(_, 0, _, Agg) -> lists:reverse(Agg); generate_fds(Fd, Count, [Name | Names], Agg) -> generate_fds(Fd + 1, Count - 1, Names, [decode_fd(Fd, Name) | Agg]); generate_fds(Fd, Count, [], Agg) -> generate_fds(Fd + 1, Count - 1, [], [Fd | Agg]). decode_fd(Fd, "") -> Fd; decode_fd(Fd, Name) -> {Fd, Name}. build_fds(List) -> {Names, FDs} = build_fds(List, {[], []}), {lists:reverse(Names), lists:reverse(FDs)}. build_fds([], Acc) -> Acc; build_fds([{Fd, Name} | Rest], {ANames, AFds}) when is_integer(Fd) -> build_fds(Rest, {[Name | ANames], [Fd | AFds]}); build_fds([Fd | Rest], {ANames, AFds}) when is_integer(Fd) -> build_fds(Rest, {["" | ANames], [Fd | AFds]}).