src/zotonic_filewatcher_sup.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2015-2020 Marc Worrell
%% @doc Check for changed files, notify the zotonic_filehandler of any changes

%% Copyright 2015-2020 Marc Worrell
%%
%% Licensed 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.

-module(zotonic_filewatcher_sup).
-author('Marc Worrell <marc@worrell.nl>').
-behaviour(supervisor).

%% External exports
-export([
    start_link/0,
    init/1,
    start_watchers/0,
    restart_watchers/0,
    watch_dirs/0,
    watch_dirs_expanded/0
    ]).

-include_lib("kernel/include/file.hrl").
-include_lib("kernel/include/logger.hrl").


%% @doc API for starting the site supervisor.
start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

%% @doc Return the filewatcher gen_server(s) to be used.
init([]) ->
    Children = [
        {zotonic_filewatcher_handler,
          {zotonic_filewatcher_handler, start_link, []},
          permanent, 5000, worker, [zotonic_filewatcher_handler]}
    ],
    RestartStrategy = {one_for_all, 5, 10},
    {ok, {RestartStrategy, Children}}.

%% @doc Restart watchers because of a new application. This is because of new
%%      symlinks, the filewatcher_monitor resolves symlinks itself, so doesn't
%%      need to be restarted.
restart_watchers() ->
    case z_config:get(filewatcher_enabled) of
        true ->
            ?LOG_INFO("Restarting filewatchers"),
            zotonic_filewatcher_fswatch:restart(),
            zotonic_filewatcher_inotify:restart(),
            ok;
        false ->
            ok
    end.

start_watchers() ->
    case z_config:get(filewatcher_enabled) of
        true ->
            Children = watcher_children(z_config:get(filewatcher_enabled)),
            lists:foreach(
                fun(ChildSpec) ->
                    supervisor:start_child(?MODULE, ChildSpec)
                end,
                Children);
        false ->
            ok
    end.

watcher_children(true) ->
    Watchers = [
        zotonic_filewatcher_fswatch,
        zotonic_filewatcher_inotify
    ],
    which_watcher(Watchers);
watcher_children(false) ->
    ?LOG_DEBUG("zotonic_filewatcher: disabled"),
    [
        {zotonic_filewatcher_beam_reloader,
          {zotonic_filewatcher_beam_reloader, start_link, [false]},
          permanent, 5000, worker, [zotonic_filewatcher_beam_reloader]}
    ].

which_watcher([]) ->
    IsScannerEnabled = z_config:get(filewatcher_scanner_enabled),
    case IsScannerEnabled of
        true ->
            ?LOG_WARNING("zotonic_filewatcher: please install fswatch or inotify-tools to improve automatic loading of changed files");
        false ->
            ?LOG_WARNING("zotonic_filewatcher: please install fswatch or inotify-tools to automatically load changed files")
    end,
    % Start the filewatcher process and the beam reloader.
    % If the scanner is enabled then the beam reloader will tell the monitor which
    % directories need to be watched.
    MonitorOpts = [
        {interval, z_config:get(filewatcher_scanner_interval)}
    ],
    [
        {zotonic_filewatcher_monitor,
          {zotonic_filewatcher_monitor, start_link, [ MonitorOpts ]},
          permanent, 5000, worker, [zotonic_filewatcher_monitor]},
        {zotonic_filewatcher_beam_reloader,
          {zotonic_filewatcher_beam_reloader, start_link, [ IsScannerEnabled ]},
          permanent, 5000, worker, [zotonic_filewatcher_beam_reloader]}
    ];
which_watcher([M|Ms]) ->
    case M:is_installed() of
        true ->
            [
                {zotonic_filewatcher_beam_reloader,
                  {zotonic_filewatcher_beam_reloader, start_link, [false]},
                  permanent, 5000, worker, [zotonic_filewatcher_beam_reloader]},
                {M,
                  {M, start_link, []},
                  permanent, 5000, worker, [M]}
            ];
        false ->
            which_watcher(Ms)
    end.

%% @doc Return the list of all directories to watch
%% @todo Add a non recursive watch on zotonic_apps, _checkouts and the lib dir.
%%       To see if new applications are added (or removed).
-spec watch_dirs() -> list(string()).
watch_dirs() ->
    ZotonicDirs = [
        filename:join([ z_path:get_path(), "_checkouts" ]),
        build_lib_dir()
    ],
    lists:filter(fun(Dir) -> filelib:is_dir(Dir) end, ZotonicDirs).

%% @doc We expand all watch dirs, so that symbolic links to src, include, and priv are followed
-spec watch_dirs_expanded() -> list(string()).
watch_dirs_expanded() ->
    lists:foldl(
        fun(Dir, Acc) ->
            symlinks(Dir) ++ [ Dir | Acc ]
        end,
        [],
        watch_dirs()).

symlinks(Dir) ->
    All =  filelib:wildcard(filename:join([ Dir, "*" ]))
        ++ filelib:wildcard(filename:join([ Dir, "*", "{src,priv,include}" ])),
    lists:filter(
        fun(D) ->
            case filelib:is_file(D) of
                true ->
                    case file:read_link_info(D) of
                        {ok, #file_info{ type = symlink }} -> true;
                        _ -> false
                    end;
                false ->
                    false
            end
        end,
        All).

%% @doc Return the _build/default/lib directory
-spec build_lib_dir() -> file:filename().
build_lib_dir() ->
    filename:dirname(code:lib_dir(zotonic_filewatcher)).