├── .gitignore ├── LICENSE ├── README.md ├── conf ├── example.conf ├── example.conf_20111101_14_56_49.dump └── mgmt_conf.conf ├── include └── confetti.hrl ├── priv └── helo.txt ├── rebar.config └── src ├── confetti.app.src ├── confetti.erl ├── confetti_app.erl ├── confetti_mgmt.erl ├── confetti_mgmt_cmnds.erl ├── confetti_mgmt_sup.erl ├── confetti_reader.erl ├── confetti_sup.erl ├── confetti_sup_sup.erl ├── confetti_table_man.erl ├── confetti_table_man_sup.erl ├── confetti_utils.erl ├── confetti_writer.erl ├── example_srv.erl └── example_user_commands.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | priv 4 | *.o 5 | *.beam 6 | ebin/ 7 | conf_cache.db 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, jtendo.com 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Confetti 2 | ======== 3 | 4 | Confetti is configuration provider for your Erlang applications. 5 | 6 | Basically it's `application:get_env/2` on steroids. 7 | 8 | Features 9 | -------- 10 | 11 | ### Management console 12 | 13 | * Accessible via telnet - maintenance department **will** love you for this 14 | * Configuration reload in runtime (designated processes receive notifications on reload) 15 | * Easily extensible with your own management commands (plugins!) 16 | * (TODO) broadcast working configuration across the Erlang cluster 17 | 18 | ![Confetti management console](http://mtod.org/assets/c3/w92p4radk4wg8.png) 19 | 20 | ### Configuration supervision 21 | 22 | * Increase your system's uptime - previous working configuration is 23 | DETS-cached in case someone messes up the configuration files 24 | * Broken config for ``process_a`` can not break ``process_b`` 25 | 26 | ![Confetti supervision tree](http://mtod.org/assets/83/n4jtwvai8s4ck.png) 27 | 28 | ### Easy to use 29 | 30 | ```erlang 31 | application:start(confetti). 32 | ``` 33 | then 34 | 35 | ```erlang 36 | %% your process 37 | %% (...) 38 | init([]) -> 39 | confetti:use(my_foo), %% reads configuration terms 40 | %% from "conf/my_foo.conf", 41 | %% spawns new configuration provider 42 | %% if needed... 43 | 44 | confetti:fetch(my_foo), %% fetches the configuration terms 45 | {ok, #state{}}. 46 | 47 | %% (...) 48 | %% react to configuration changes 49 | handle_info({config_reloaded, NewConf}, State) -> (...) 50 | ``` 51 | 52 | ### Customizable 53 | 54 | * Write configuration validators and more: 55 | 56 | ```erlang 57 | confetti:use(foo, [ 58 | %% Specify config file location 59 | {location, {"conf/bar", "foo.cnf"}, 60 | 61 | %% Make sure it's more than just correct Erlang term 62 | %% or even transform the terms into something! 63 | %% Validator funs should accept Config and return {ok, NewConf} 64 | %% on success, error otherwise. 65 | {validators, [fun validate_foo_config/1]}, 66 | 67 | %% ignore notifications for current process 68 | {subscribe, false} 69 | ]). 70 | ``` 71 | 72 | * Expose any module via the management console: 73 | 74 | ```erlang 75 | -module(my_commands). 76 | export([foo/1, foo/3]). 77 | 78 | foo(help) -> 79 | "Foo does bar two times!". 80 | foo(Param1, Param2, Param3) -> 81 | %% perform command logic 82 | "bar bar". 83 | ``` 84 | 85 | Let confetti know about it: 86 | 87 | ```erlang 88 | %% conf/mgmt_conf.conf 89 | {port, 50000}. 90 | {plugins, [my_commands]}. 91 | ``` 92 | 93 | Assuming your application is already running, 94 | perform live management configruation reload: 95 | 96 | ``` 97 | $ telnet localhost 50000 98 | 99 | ... 100 | 101 | (nonode@nohost)> reload mgmt_conf 102 | ok 103 | ``` 104 | 105 | * Provide your own welcome screen to the management console, i.e.: 106 | 107 | ``` 108 | $ figlet MyApp > priv/helo.txt 109 | ``` 110 | 111 | Try it out quickly 112 | ------------------ 113 | 114 | 1. Obtain the source code 115 | 2. `rebar compile; erl -pa ebin -boot start_sasl -s confetti_app` 116 | 3. `1> example_srv:start_link().` 117 | 3. `telnet localhost 50000` 118 | 4. Type `help` for available commands, and `help COMMAND` for command usage 119 | details. 120 | 121 | 122 | License 123 | ------- 124 | 125 | BSD License. 126 | See `LICENSE` file for details. 127 | 128 | 129 | Authors 130 | ------- 131 | Adam Rutkowski `` 132 | 133 | 134 | Contribute! 135 | ----------- 136 | Feel encouraged to spot bugs/poor code and implement new sexy features. 137 | 138 | Also, make sure, you add yourself to the ``authors`` where appropriate! 139 | Thanks. 140 | 141 | 142 | -------------------------------------------------------------------------------- /conf/example.conf: -------------------------------------------------------------------------------- 1 | %% This is some comment 2 | ["hello"]. 3 | [ 4 | {foo, bar, blah, "ąęćź"}, 5 | {woo, hoo} % make sure woo exists! 6 | ]. 7 | 8 | [ 9 | {foo, bar, blah, "ąęćź"}, 10 | {woo, co_tam} % make sure woo exists! 11 | ]. 12 | 13 | {yo, foo}. 14 | {wussup}. 15 | 16 | [ 17 | {foo, bar, blah, "ąęćź"}, 18 | {woo, hoo} % make sure woo exists! 19 | ]. 20 | 21 | 22 | -------------------------------------------------------------------------------- /conf/example.conf_20111101_14_56_49.dump: -------------------------------------------------------------------------------- 1 | %% This is some comment 2 | ["hello"]. 3 | [ 4 | {foo, bar, blah, "ąęćź"}, 5 | {woo, hoo} % make sure woo exists! 6 | ]. 7 | 8 | [ 9 | {foo, bar, blah, "ąęćź"}, 10 | {woo, co_tam} % make sure woo exists! 11 | ]. 12 | 13 | {yo, foo}. 14 | {wussup}. 15 | 16 | [ 17 | {foo, bar, blah, "ąęćź"}, 18 | {woo, hoo} % make sure woo exists! 19 | ]. 20 | 21 | 22 | -------------------------------------------------------------------------------- /conf/mgmt_conf.conf: -------------------------------------------------------------------------------- 1 | %% listen socket 2 | {port, 50000}. % lets not break anything 3 | %% which module to take mgmt commands from 4 | {plugins, [example_user_commands]}. 5 | -------------------------------------------------------------------------------- /include/confetti.hrl: -------------------------------------------------------------------------------- 1 | -record(provider, { 2 | opts, 3 | raw_conf, 4 | conf 5 | }). 6 | 7 | -define(FETCH, fun(Provider, Key, Default) -> 8 | proplists:get_value(Key, confetti:fetch(Provider), Default) 9 | end). 10 | 11 | -define(SOCK(Msg), {tcp, _Port, Msg}). 12 | 13 | -define(LOAD, fun(D,F) -> 14 | P = filename:join(D, F), 15 | {ok, C} = file:read_file(P), 16 | binary_to_list(C) 17 | end). 18 | 19 | -define(HELO, 20 | fun() -> 21 | case filelib:is_file("priv/helo.txt") of 22 | true -> 23 | ?LOAD("priv", "helo.txt"); 24 | false -> 25 | case code:priv_dir(confetti) of 26 | {error, bad_name} -> 27 | "Could not read helofile"; 28 | Dir -> 29 | ?LOAD(Dir, "helo.txt") 30 | end 31 | end 32 | end). 33 | 34 | -define(PROMPT, 35 | fun() -> 36 | io_lib:format("(~w)> ", [node()]) 37 | end). 38 | -------------------------------------------------------------------------------- /priv/helo.txt: -------------------------------------------------------------------------------- 1 | __ _ _ _ 2 | ___ ___ _ __ / _| ___| |_| |_(_) 3 | / __/ _ \| '_ \| |_ / _ \ __| __| | 4 | | (_| (_) | | | | _| __/ |_| |_| | 5 | \___\___/|_| |_|_| \___|\__|\__|_| 6 | 7 | ------------------------------------- 8 | 9 | Type 'help' for assistance. 10 | Press Ctrl-D to quit. 11 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ts=4 sw=4 ft=erlang et 3 | 4 | %% == Core == 5 | 6 | %% Additional library directories to add to the code path 7 | {lib_dirs, []}. 8 | 9 | %% == Erlang Compiler == 10 | 11 | %% Erlang files to compile before the rest. Rebar automatically compiles 12 | %% parse_transforms and custom behaviours before anything other than the files 13 | %% in this list. 14 | 15 | %% Erlang compiler options 16 | %% Opts: fail_on_warning 17 | {erl_opts, [warn_unused_vars, warn_unused_import, warn_exported_vars, 18 | debug_info, {i, "include"}, {d, debug}]}. 19 | 20 | %% MIB Options? 21 | %{mib_opts, [{group_check, false}]}. 22 | {mib_opts, []}. 23 | 24 | %% SNMP mibs to compile first? 25 | {mib_first_files, []}. 26 | 27 | %% == EUnit == 28 | 29 | %% Options for eunit:test() 30 | {eunit_opts, []}. 31 | 32 | %% Additional compile options for eunit. erl_opts from above is also used 33 | %{eunit_compile_opts, [{src_dirs, ["test"]}]}. 34 | 35 | %% Whether to enable coverage reporting. Default is `false' 36 | {cover_enabled, false}. 37 | 38 | %% Whether to print coverage report to console. Default is `false' 39 | {cover_print_enabled, false}. 40 | 41 | %% == Dialyzer == 42 | 43 | %% Options for running the dialyzer, right now only `plt' is supported 44 | {dialyzer_opts, []}. 45 | 46 | %% == Cleanup == 47 | 48 | %% Which files to cleanup 49 | {clean_files, ["rel/erl_crash.dump"]}. 50 | 51 | %% == Reltool == 52 | 53 | %% Target directory for the release 54 | {target, "target"}. 55 | 56 | %% == OTP Applications == 57 | 58 | %% Binaries to link into the erlang path? 59 | {app_bin, []}. 60 | 61 | %% Enable validation of the OTP app module list. Default is 'true' 62 | {validate_app_modules, true}. 63 | 64 | %% == Subdirectories == 65 | 66 | %% Subdirectories? 67 | {sub_dirs, ["rel"]}. 68 | -------------------------------------------------------------------------------- /src/confetti.app.src: -------------------------------------------------------------------------------- 1 | {application, confetti, 2 | [ 3 | {description, ""}, 4 | {vsn, "0.1"}, 5 | {registered, [ 6 | confetti_sup_sup, 7 | confetti_sup, 8 | confetti, 9 | confetti_mgmt_sup, 10 | confetti_mgmt, 11 | confetti_mgmt_cmds, 12 | confetti_table_man_sup, 13 | confetti_table_man, 14 | confetti_utils, 15 | confetti_writer, 16 | confetti_reader 17 | ]}, 18 | {applications, [ 19 | kernel, 20 | stdlib, 21 | sasl 22 | ]}, 23 | {mod, { confetti_app, []}} 24 | ]}. 25 | -------------------------------------------------------------------------------- /src/confetti.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Confetti main module 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti). 9 | -behaviour(gen_server). 10 | -include("confetti.hrl"). 11 | 12 | %% API 13 | -export([start_link/2]). 14 | -export([use/1, use/2]). 15 | -export([fetch/1, reload/1]). 16 | 17 | %% gen_server callbacks 18 | -export([init/1, 19 | handle_call/3, 20 | handle_cast/2, 21 | handle_info/2, 22 | terminate/2, 23 | code_change/3]). 24 | 25 | %%%=================================================================== 26 | %%% API 27 | %%%=================================================================== 28 | 29 | -type opts() :: [ 30 | {location, {Filename :: string(), Directory :: string()}} | 31 | {validators, [fun( (Conf :: term()) -> 32 | {ok, NewConf :: term()} | {error, Reason :: any()} )]} | 33 | {subscribe, boolean()} 34 | ]. 35 | 36 | -spec use(ProviderName :: atom()) -> {ok, Pid :: pid()}. 37 | 38 | %% @doc 39 | %% Obtains configuration provider process with default settings. 40 | %% Reads configuration terms from "conf/ProviderName.conf". 41 | %% Subscribes caller to configuration reload notification. 42 | 43 | use(ProviderName) -> 44 | use(ProviderName, 45 | [{location, {atom_to_list(ProviderName) ++ ".conf", "conf"}}, 46 | {validators, []}, 47 | {subscribe, true} 48 | ]). 49 | 50 | -spec use(ProviderName :: atom(), Opts :: opts()) -> {ok, Pid :: pid()}. 51 | 52 | %% @doc 53 | %% Obtains configuration provider process 54 | %% @see opts() 55 | 56 | use(ProviderName, Opts) -> 57 | {ok, Pid} = confetti_sup:start_child(ProviderName, Opts), 58 | Subscribe = proplists:get_value(subscribe, Opts, true), 59 | case Subscribe of 60 | false -> 61 | {ok, Pid}; 62 | _ -> 63 | ok = gen_server:call(ProviderName, {subscribe, ProviderName}), 64 | {ok, Pid} 65 | end. 66 | 67 | -spec reload(ProviderName :: atom()) -> ok. 68 | 69 | %% @doc 70 | %% Reloads given provider's configuration from disk. 71 | %% Notifies subscribers with {config_reloaded, Conf} message on success. 72 | %% This function is not intented to be called directly. 73 | %% Confetti uses it via management console commands. 74 | 75 | reload(ProviderName) -> 76 | case is_provider(ProviderName) of 77 | true -> 78 | case gen_server:call(ProviderName, {reload_config, ProviderName}) of 79 | {ok, Conf} -> notify_subscribers(ProviderName, {config_reloaded, Conf}); 80 | Err -> Err 81 | end; 82 | false -> not_provider(ProviderName) 83 | end. 84 | 85 | -spec fetch(ProviderName :: atom()) -> Conf :: term(). 86 | 87 | %% @doc 88 | %% Fetch configuration terms from given provider process. 89 | %% User is responsible for implementing detailed 90 | %% config value getters. 91 | 92 | fetch(ProviderName) -> 93 | case is_provider(ProviderName) of 94 | true -> gen_server:call(ProviderName, {fetch_config}); 95 | false -> not_provider(ProviderName) 96 | end. 97 | 98 | %% @doc 99 | %% Starts the server, and pg2 group if needed. 100 | %% This function is called by confetti_sup module and 101 | %% there is probably no need to call it directly. 102 | 103 | start_link(ProviderName, Opts) when is_atom(ProviderName) -> 104 | pg2:create(ProviderName), 105 | gen_server:start_link({local, ProviderName}, ?MODULE, {ProviderName, Opts}, []). 106 | 107 | %%%=================================================================== 108 | %%% Gen Server Callbacks 109 | %%%=================================================================== 110 | 111 | %% @doc 112 | %% Tries to load configuration for given provider. 113 | %% If disk configuration loading fails (i.e. config terms are broken, 114 | %% or configuration file does not exists), init/1 will try 115 | %% to fetch the last working configuration from DETS. 116 | 117 | init(Subject = {ProviderName, Opts}) -> 118 | case confetti_reader:load_config(Subject) of 119 | {ok, RawConf, Conf} -> 120 | {ok, #provider{opts=Opts, conf=Conf, raw_conf=RawConf}}; 121 | Error -> 122 | case confetti_reader:last_working_config(ProviderName) of 123 | {ok, {Opts, PrevRawConf, PrevConf}} -> 124 | {ok, #provider{opts=Opts, conf=PrevConf, raw_conf=PrevRawConf}}; 125 | _Else -> Error 126 | end 127 | end. 128 | 129 | %% @private 130 | %% @doc 131 | %% Reload configuration for given provider. 132 | %% Dump previous configuration to disk on success. 133 | 134 | handle_call({reload_config, ProviderName}, _From, State) -> 135 | Opts = State#provider.opts, 136 | case confetti_reader:load_config({ProviderName, Opts}) of 137 | {ok, NewRawConf, NewConf} -> 138 | NewState = State#provider{raw_conf=NewRawConf, conf=NewConf}, 139 | Conf = State#provider.conf, 140 | RawConf = State#provider.raw_conf, 141 | confetti_writer:dump_config(proplists:get_value(location, Opts), Conf, RawConf), 142 | {reply, {ok, NewConf}, NewState}; 143 | Error -> 144 | {reply, Error, State} 145 | end; 146 | 147 | %% @private 148 | %% @doc 149 | %% Fetches the current provider's configuration from State. 150 | 151 | handle_call({fetch_config}, _From, State) -> 152 | {reply, State#provider.conf, State}; 153 | 154 | %% @private 155 | %% @doc 156 | %% Join subscribers group if not there already. 157 | 158 | handle_call({subscribe, ProviderName}, {Pid, _}, State) -> 159 | ok = join_group(ProviderName, Pid), 160 | {reply, ok, State}; 161 | 162 | handle_call(_Request, _From, State) -> 163 | {reply, ok, State}. 164 | 165 | handle_cast(_Msg, State) -> 166 | {noreply, State}. 167 | 168 | %% just for debug purposes 169 | %handle_info(calcbad, State) -> 170 | %1/0, 171 | %{noreply, State}; 172 | 173 | handle_info(_Info, State) -> 174 | {noreply, State}. 175 | 176 | terminate(_Reason, _State) -> 177 | ok. 178 | 179 | code_change(_OldVsn, State, _Extra) -> 180 | {ok, State}. 181 | 182 | %%%=================================================================== 183 | %%% Internal functions 184 | %%%=================================================================== 185 | 186 | %% don't know why pg2:join doesn't do it by default 187 | join_group(Group, Pid) -> 188 | case lists:member(Pid, pg2:get_local_members(Group)) of 189 | true -> ok; 190 | false -> 191 | pg2:join(Group, Pid) 192 | end. 193 | 194 | notify_subscribers(Group, Msg) -> 195 | lists:foreach(fun(Pid) -> 196 | Pid ! Msg 197 | end, pg2:get_local_members(Group)), 198 | ok. 199 | 200 | is_provider(ProviderName) -> 201 | case whereis(ProviderName) of 202 | Pid when is_pid(Pid) -> 203 | Providers = supervisor:which_children(confetti_sup), 204 | lists:member(Pid, [P || {_,P,_,_} <- Providers]); 205 | _ -> false 206 | end. 207 | 208 | not_provider(ProviderName) -> 209 | throw({unknown_provider, ProviderName}). 210 | -------------------------------------------------------------------------------- /src/confetti_app.erl: -------------------------------------------------------------------------------- 1 | -module(confetti_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/0, start/2, stop/1]). 7 | 8 | %% =================================================================== 9 | %% Application callbacks 10 | %% =================================================================== 11 | 12 | % useful for console starts 13 | % i.e. erl -pa apps/*/ebin -pa deps/*/ebin -boot start_sasl -s confetti_app 14 | start() -> 15 | application:start(confetti). 16 | 17 | start(_StartType, _StartArgs) -> 18 | confetti_sup_sup:start_link(). 19 | 20 | stop(_State) -> 21 | ok. 22 | -------------------------------------------------------------------------------- /src/confetti_mgmt.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Management console socket server and function execution engine 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_mgmt). 9 | -behaviour(gen_server). 10 | -export([start_link/1]). 11 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 12 | code_change/3, terminate/2]). 13 | -compile([export_all]). 14 | -include("confetti.hrl"). 15 | 16 | -record(state, {socket}). % the current socket 17 | 18 | % functions that should not be treated as management commands 19 | -define(PRIVATE_INTERFACE, [module_info]). 20 | 21 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 22 | % API % 23 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 24 | 25 | start_link(Socket) -> 26 | gen_server:start_link(?MODULE, Socket, []). 27 | 28 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 29 | % gen_server callbacks % 30 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 31 | 32 | init(Socket) -> 33 | gen_server:cast(self(), accept), 34 | confetti:use(mgmt_conf), 35 | lists:foreach(fun(M) -> {module, M} = code:ensure_loaded(M) end, 36 | all_cmd_modules()), 37 | {ok, #state{socket=Socket}}. 38 | 39 | handle_call(_, _, State) -> 40 | {noreply, State}. 41 | 42 | %% Accepting a connection 43 | handle_cast(accept, S = #state{socket=ListenSocket}) -> 44 | {ok, AcceptSocket} = gen_tcp:accept(ListenSocket), 45 | confetti_mgmt_sup:start_socket(), 46 | welcome(AcceptSocket), 47 | {noreply, S#state{socket=AcceptSocket}}; 48 | 49 | handle_cast(_, State) -> 50 | {noreply, State}. 51 | 52 | handle_info(?SOCK([4]), S = #state{socket=Socket}) -> 53 | send(Socket, "~nBye!", []), 54 | {stop, normal, S}; 55 | 56 | handle_info(?SOCK("\r\n"), S = #state{socket=Socket}) -> 57 | prompt(Socket), 58 | {noreply, S}; 59 | 60 | handle_info(?SOCK(E), S = #state{socket=Socket}) -> 61 | send(Socket, "~ts", [handle_command(cmd(E))]), 62 | prompt(Socket), 63 | {noreply, S}; 64 | 65 | handle_info({tcp_closed, _}, S) -> 66 | {stop, normal, S}; 67 | 68 | handle_info(_E,S) -> 69 | {noreply, S}. 70 | 71 | code_change(_OldVsn, State, _Extra) -> 72 | {ok, State}. 73 | 74 | terminate(_Reason, _State) -> 75 | ok. 76 | 77 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 78 | % management handlers % 79 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 80 | 81 | handle_command(["help"]) -> 82 | AvailableCommands = lists:map(fun get_plugin_help/1, all_cmd_modules()), 83 | string:join(AvailableCommands, "\n"); 84 | 85 | handle_command(["help"|Topic]) -> 86 | ErrMsg = io_lib:format("No help for ~s", [hd(Topic)]), 87 | try_execute(hd(Topic), [help], ErrMsg); 88 | 89 | handle_command([Cmd|Params]) -> 90 | try_execute(Cmd, Params). 91 | 92 | find_cmd_module({_, _}, []) -> 93 | {error, undefined}; 94 | 95 | find_cmd_module({F, Arity}, [M|Rest]) -> 96 | case erlang:function_exported(M, F, Arity) of 97 | true -> 98 | {found, {M, F}}; 99 | false -> 100 | find_cmd_module({F, Arity}, Rest) 101 | end. 102 | 103 | find_cmd_module({Cmd, Arity}) -> 104 | try 105 | F = list_to_existing_atom(Cmd), 106 | find_cmd_module({F, Arity}, all_cmd_modules()) 107 | catch _:_ -> 108 | {error, undefined} 109 | end. 110 | 111 | try_execute(F, A) -> 112 | try_execute(F, A, "Unknown command or syntax error"). 113 | 114 | try_execute(F, A, ErrMsg) -> 115 | case find_cmd_module({F, length(A)}) of 116 | {error, undefined} -> 117 | ErrMsg; 118 | {found, {Mod, Fun}} -> 119 | try apply(Mod, Fun, A) of 120 | Result -> Result 121 | catch Class:Error -> 122 | io_lib:format("Error (~p): ~p", [Class, Error]) 123 | end 124 | end. 125 | 126 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 127 | % helper functions % 128 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 129 | 130 | prompt(Socket) -> 131 | gen_tcp:send(Socket, ?PROMPT()), 132 | inet:setopts(Socket, [{active, once}]), 133 | ok. 134 | 135 | welcome(Socket) -> 136 | gen_tcp:send(Socket, ?HELO()), 137 | gen_tcp:send(Socket, ?PROMPT()), 138 | inet:setopts(Socket, [{active, once}]), 139 | ok. 140 | 141 | send(Socket, Str, Args) -> 142 | try io_lib:format(Str++"~n", Args) of 143 | FormattedResult -> 144 | gen_tcp:send(Socket, FormattedResult) 145 | catch _:_ -> 146 | gen_tcp:send(Socket, 147 | io_lib:format("Error: Could not format command output~n", [])) 148 | end, 149 | inet:setopts(Socket, [{active, once}]), 150 | ok. 151 | 152 | cmd(Str) when is_list(Str) -> 153 | string:tokens(hd(string:tokens(Str, "\r\n")), " "). 154 | 155 | all_cmd_modules() -> 156 | [confetti_mgmt_cmnds|?FETCH(mgmt_conf, plugins, [])]. 157 | 158 | get_plugin_help(Module) -> 159 | [{exports, Exports}|_] = Module:module_info(), 160 | UExports = proplists:get_keys(Exports), 161 | Mod = string:left(atom_to_list(Module), 30) ++ ":", 162 | lists:foldl(fun(F, Acc) -> 163 | case lists:member(F, ?PRIVATE_INTERFACE) of 164 | true -> Acc; 165 | false -> string:join([Acc, atom_to_list(F)], " ") 166 | end 167 | end, Mod, UExports). 168 | -------------------------------------------------------------------------------- /src/confetti_mgmt_cmnds.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Default management console commands bundle 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_mgmt_cmnds). 9 | -author('adam.rutkowski@jtendo.com'). 10 | 11 | -export([reload/1, reload/2]). 12 | -export([fetch/1]). 13 | -export([cluster/0, cluster/1]). 14 | -export([broadcast/1, broadcast/2]). 15 | -export([modules/0, modules/1]). 16 | 17 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 18 | % cluster % 19 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 20 | 21 | cluster(help) -> 22 | "Display node names available in cluster. Current node is bypassed.\n" 23 | "Usage:\n\n" 24 | "> cluster". 25 | 26 | cluster() -> 27 | case nodes() of 28 | [] -> 29 | "No neighbor nodes found."; 30 | Nodes -> 31 | string:join(lists:map(fun(N) -> atom_to_list(N) end, Nodes), ",") 32 | end. 33 | 34 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 35 | % fetch % 36 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 37 | 38 | fetch(help) -> 39 | "Fetch runtime module configuration\n" 40 | "Usage:\n\n" 41 | "> fetch mymodule"; 42 | 43 | fetch(Module) -> 44 | confetti_call(Module, fetch). 45 | 46 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 47 | % broadcast % 48 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 49 | 50 | broadcast(help) -> 51 | "Broadcast local node working configuration copy to nodes Nodes.\n" 52 | "Defaults to cluster if Nodes not provided. See: 'help cluster'.\n" 53 | "Usage:\n\n" 54 | "> broadcast node@host1,node@host2\n\n" 55 | "or\n" 56 | "> broadcast"; 57 | 58 | broadcast(Module) -> 59 | broadcast(Module, cluster()). 60 | 61 | broadcast(_, []) -> 62 | "No neighbor nodes found. Verify your cluster settings."; 63 | 64 | broadcast(Module, Nodes) -> 65 | % TODO implement me / rpc multicall 66 | io_lib:format("Broadcast ~p to ~p", [Module, Nodes]). 67 | 68 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 69 | % reload % 70 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 71 | 72 | reload(help) -> 73 | "Reload local node Module configuration.\n" 74 | "Usage:\n\n" 75 | "> reload mymodule"; 76 | 77 | reload(Module) -> 78 | confetti_call(Module, reload). 79 | 80 | reload(Module, _Nodes) -> 81 | confetti_call(Module, reload). 82 | 83 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 84 | % modules % 85 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 86 | 87 | modules(help) -> 88 | "List available configuration providers". 89 | 90 | modules() -> 91 | Title = string:left("Modules", 20) ++ ":", 92 | lists:foldl(fun({_,Pid,_,_}, Acc) -> 93 | N = proplists:get_value(registered_name, erlang:process_info(Pid)), 94 | string:join([Acc, atom_to_list(N)], " ") 95 | end, Title, supervisor:which_children(confetti_sup)). 96 | 97 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 98 | % internal functions % 99 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 100 | 101 | confetti_call(Module, Fun) -> 102 | try list_to_existing_atom(Module) of 103 | M -> 104 | io_lib:format("~p", [confetti:Fun(M)]) 105 | catch _C:E -> 106 | throw({mgmt_call_failed, Fun, Module, E}) 107 | end. 108 | 109 | -------------------------------------------------------------------------------- /src/confetti_mgmt_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Confetti Management server supervisor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_mgmt_sup). 9 | -behaviour(supervisor). 10 | 11 | -export([start_link/0, start_socket/0]). 12 | -export([init/1]). 13 | 14 | -include("confetti.hrl"). 15 | 16 | start_link() -> 17 | {ok, Pid} = supervisor:start_link({local, ?MODULE}, ?MODULE, []), 18 | start_socket(), 19 | {ok, Pid}. 20 | 21 | init([]) -> 22 | confetti:use(mgmt_conf, [ 23 | {location, {"mgmt_conf.conf", "conf"}}, 24 | {subscribe, false} 25 | ]), 26 | Port = ?FETCH(mgmt_conf, port, 50000), 27 | IpS = ?FETCH(mgmt_conf, ip, "127.0.0.1"), 28 | {ok, Ip} = inet_parse:ipv4_address(IpS), 29 | {ok, ListenSocket} = gen_tcp:listen(Port, [{active,once}, 30 | {reuseaddr, true}, {ip, Ip}]), 31 | {ok, {{simple_one_for_one, 60, 3600}, 32 | [{socket, 33 | {confetti_mgmt, start_link, [ListenSocket]}, 34 | temporary, 1000, worker, [confetti_mgmt]} 35 | ]}}. 36 | 37 | start_socket() -> 38 | supervisor:start_child(?MODULE, []). 39 | 40 | -------------------------------------------------------------------------------- /src/confetti_reader.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Configuration loader 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_reader). 9 | -author('adam.rutkowski@jtendo.com'). 10 | -export([load_config/1, last_working_config/1]). 11 | -include("confetti.hrl"). 12 | 13 | %%%=================================================================== 14 | %%% API 15 | %%%=================================================================== 16 | 17 | load_config({ProviderName, Opts}) -> 18 | File = confetti_utils:fname(proplists:get_value(location, Opts)), 19 | case file:read_file(File) of 20 | {ok, RawConfig} -> 21 | case confetti_utils:u_consult(File) of 22 | {ok, Terms} -> 23 | case proplists:get_value(validators, Opts, undefined) of 24 | undefined -> 25 | load_valid_config(ProviderName, Opts, RawConfig, 26 | Terms); 27 | Funs when is_list(Funs) -> 28 | case validate_config(Funs, Terms) of 29 | {ok, ValidTerms} -> 30 | load_valid_config(ProviderName, Opts, 31 | RawConfig, ValidTerms); 32 | Err -> 33 | Err 34 | end 35 | end; 36 | {error, Reason} -> 37 | confetti_utils:raise_alarm(ProviderName, Reason), 38 | handle_error({File, Reason}) 39 | end; 40 | {error, Err} -> 41 | handle_error({File, Err}) 42 | end. 43 | 44 | validate_config([], ValidTerms) -> 45 | {ok, ValidTerms}; 46 | 47 | validate_config([F|Rest], Terms) when is_function(F) -> 48 | try 49 | case apply(F, [Terms]) of 50 | {ok, ValidTerms} -> 51 | validate_config(Rest, ValidTerms); 52 | {error, Reason} -> 53 | {error, {invalid_config, Reason}} 54 | end 55 | catch _C:Error -> 56 | {error, {invalid_config, Error}} 57 | end. 58 | 59 | load_valid_config(ProviderName, Opts, RawConfig, Terms) -> 60 | confetti_utils:clear_alarm(ProviderName), 61 | confetti_writer:store_working_config(ProviderName, Opts, RawConfig, Terms), 62 | {ok, RawConfig, Terms}. 63 | 64 | last_working_config(ProviderName) -> 65 | case confetti_table_man:lookup(ProviderName) of 66 | [] -> {error, no_previous_config}; 67 | [{ProviderName, Opts, PrevConf, PrevRawConf}] -> 68 | {ok, {Opts, PrevConf, PrevRawConf}} 69 | end. 70 | 71 | %%%=================================================================== 72 | %%% Helpers 73 | %%%=================================================================== 74 | 75 | handle_error(Error = {File, enoent}) -> 76 | io:format("Failed to load ~s - no such file.~n", [File]), 77 | {error, Error}; 78 | 79 | handle_error(Error = {File, {StopLine, erl_parse, Reason}}) -> 80 | io:format("Failed to parse term in ~s close to line ~p: ~p~n", [File, StopLine, lists:flatten(Reason)]), 81 | {error, Error}; 82 | 83 | handle_error(Error = {File, _}) -> 84 | io:format("Unknown error loading ~s: ~p~n", [File, Error]), 85 | {error, Error}. 86 | 87 | -------------------------------------------------------------------------------- /src/confetti_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Configuration providers supervisor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_sup). 9 | -behaviour(supervisor). 10 | 11 | %-export([start_link/0, start/1, start/2, start/3, start/4]). 12 | -export([start_link/0,init/1]). 13 | -export([start_child/2]). 14 | 15 | start_link() -> 16 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 17 | 18 | init(_Args) -> 19 | {ok, {{simple_one_for_one, 60, 3600}, 20 | [{conf_server, 21 | {confetti, start_link, []}, 22 | transient, 1000, worker, [confetti]} 23 | ]}}. 24 | 25 | start_child(ProviderName, Options) -> 26 | case supervisor:start_child(?MODULE, [ProviderName, Options]) of 27 | {ok, Pid} -> {ok, Pid}; 28 | {error, {already_started, Pid}} -> {ok, Pid} 29 | end. 30 | -------------------------------------------------------------------------------- /src/confetti_sup_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Application main supervisor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_sup_sup). 9 | -author('adam.rutkowski@jtendo.com'). 10 | 11 | -behaviour(supervisor). 12 | 13 | %% API 14 | -export([start_link/0]). 15 | 16 | %% Supervisor callbacks 17 | -export([init/1]). 18 | 19 | %% Helper macro for declaring children of supervisor 20 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). 21 | 22 | %% =================================================================== 23 | %% API functions 24 | %% =================================================================== 25 | 26 | start_link() -> 27 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 28 | 29 | %% =================================================================== 30 | %% Supervisor callbacks 31 | %% =================================================================== 32 | 33 | init([]) -> 34 | Children = [ 35 | ?CHILD(confetti_table_man_sup, supervisor), 36 | ?CHILD(confetti_sup, supervisor), 37 | ?CHILD(confetti_mgmt_sup, supervisor) 38 | ], 39 | {ok, { {one_for_one, 5, 10}, Children} }. 40 | -------------------------------------------------------------------------------- /src/confetti_table_man.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% DETS Manager 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_table_man). 9 | 10 | -behaviour(gen_server). 11 | 12 | %% API 13 | -export([start_link/0]). 14 | -export([store/1, lookup/1]). 15 | 16 | %% gen_server callbacks 17 | -export([init/1, 18 | handle_call/3, 19 | handle_cast/2, 20 | handle_info/2, 21 | terminate/2, 22 | code_change/3]). 23 | 24 | -define(SERVER, ?MODULE). 25 | -define(CACHE, "conf_cache.db"). 26 | 27 | start_link() -> 28 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). 29 | 30 | init([]) -> 31 | dets:open_file(?CACHE, [{type, set}]). 32 | 33 | store(Data) -> 34 | gen_server:call(?SERVER, {store, Data}). 35 | 36 | lookup(Key) -> 37 | gen_server:call(?SERVER, {lookup, Key}). 38 | 39 | handle_call({store, Data}, _From, State) -> 40 | Reply = dets:insert(?CACHE, Data), 41 | {reply, Reply, State}; 42 | 43 | handle_call({lookup, Key}, _From, State) -> 44 | Reply = dets:lookup(?CACHE, Key), 45 | {reply, Reply, State}; 46 | 47 | handle_call(_Request, _From, State) -> 48 | Reply = ok, 49 | {reply, Reply, State}. 50 | 51 | handle_cast(_Msg, State) -> 52 | {noreply, State}. 53 | 54 | handle_info(_Info, State) -> 55 | {noreply, State}. 56 | 57 | terminate(_Reason, _State) -> 58 | ok. 59 | 60 | code_change(_OldVsn, State, _Extra) -> 61 | {ok, State}. 62 | -------------------------------------------------------------------------------- /src/confetti_table_man_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% DETS Manager supervisor 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_table_man_sup). 9 | 10 | -behaviour(supervisor). 11 | 12 | %% API 13 | -export([start_link/0]). 14 | 15 | %% Supervisor callbacks 16 | -export([init/1]). 17 | 18 | -define(SERVER, ?MODULE). 19 | 20 | %%%=================================================================== 21 | %%% API functions 22 | %%%=================================================================== 23 | 24 | start_link() -> 25 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 26 | 27 | %%%=================================================================== 28 | %%% Supervisor callbacks 29 | %%%=================================================================== 30 | 31 | init([]) -> 32 | RestartStrategy = one_for_one, 33 | MaxRestarts = 1000, 34 | MaxSecondsBetweenRestarts = 3600, 35 | SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, 36 | Restart = permanent, 37 | Shutdown = 2000, 38 | Type = worker, 39 | AChild = {confetti_table_man, {confetti_table_man, start_link, []}, 40 | Restart, Shutdown, Type, [confetti_table_man]}, 41 | {ok, {SupFlags, [AChild]}}. 42 | -------------------------------------------------------------------------------- /src/confetti_utils.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Confetti utlility module 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_utils). 9 | -author('adam.rutkowski@jtendo.com'). 10 | -export([u_consult/1, fname/1, fname/2]). 11 | -export([raise_alarm/2, clear_alarm/1]). 12 | -include("confetti.hrl"). 13 | 14 | 15 | %%%=================================================================== 16 | %%% API 17 | %%%=================================================================== 18 | 19 | fname(normal, {Filename, Directory}) -> 20 | ConfigFile = filename:join(Directory, Filename), 21 | ConfigFile; 22 | 23 | fname(dump, {Filename, Directory}) -> 24 | Marker = "_" ++ f_date(erlang:localtime()) ++ ".dump", 25 | ConfigFile = filename:join([Directory, "dump", Filename ++ Marker]), 26 | ConfigFile. 27 | 28 | fname(Location = {_, _}) -> 29 | fname(normal, Location). 30 | 31 | % same as consult, except for encoding flag 32 | u_consult(File) -> 33 | case file:open(File, [read, {encoding, unicode}]) of 34 | {ok, Fd} -> 35 | R = consult_stream(Fd), 36 | file:close(Fd), 37 | R; 38 | Error -> 39 | Error 40 | end. 41 | 42 | %%%=================================================================== 43 | %%% API (alarms) 44 | %%%=================================================================== 45 | 46 | raise_alarm(ProviderName, AlarmDesc) -> 47 | case application:get_application(sasl) of 48 | undefined -> 49 | io:format("Alarm! Could not start ~p!~n", [ProviderName]); 50 | {ok, sasl} -> 51 | Alarms = alarm_handler:get_alarms(), 52 | case proplists:get_value({confetti, ProviderName}, Alarms) of 53 | undefined -> 54 | alarm_handler:set_alarm({{confetti, ProviderName}, AlarmDesc}); 55 | _ -> 56 | ok 57 | end 58 | end. 59 | 60 | clear_alarm(ProviderName) -> 61 | case application:get_application(sasl) of 62 | {ok, sasl} -> 63 | Alarms = alarm_handler:get_alarms(), 64 | case proplists:get_value({confetti, ProviderName}, Alarms) of 65 | undefined -> ok; 66 | _ -> 67 | alarm_handler:clear_alarm({confetti, ProviderName}) 68 | end; 69 | _ -> ok 70 | end. 71 | 72 | %%%=================================================================== 73 | %%% Helpers 74 | %%%=================================================================== 75 | 76 | f_date(DateTime) -> 77 | {{Year,Month,Day},{Hour,Min,Sec}} = DateTime, 78 | io_lib:format("~4.10.0B~2.10.0B~2.10.0B_~2.10.0B_~2.10.0B_~2.10.0B", 79 | [Year, Month, Day, Hour, Min, Sec]). 80 | 81 | consult_stream(Fd) -> 82 | consult_stream(Fd, 1, []). 83 | 84 | consult_stream(Fd, Line, Acc) -> 85 | case io:read(Fd, '', Line) of 86 | {ok,Term,EndLine} -> 87 | consult_stream(Fd, EndLine, [Term|Acc]); 88 | {error, Error, _Line} -> 89 | {error, Error}; 90 | {eof,_Line} -> 91 | {ok,lists:reverse(Acc)} 92 | end. 93 | -------------------------------------------------------------------------------- /src/confetti_writer.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Configuration saver. 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(confetti_writer). 9 | -author('adam.rutkowski@jtendo.com'). 10 | -export([dump_config/3, store_working_config/4]). 11 | -include("confetti.hrl"). 12 | 13 | %%%=================================================================== 14 | %%% API 15 | %%%=================================================================== 16 | 17 | dump_config(Location = {_, _}, _, RawConf) -> 18 | dump(Location, RawConf). 19 | 20 | store_working_config(ProviderName, Opts, Conf, RawConf) -> 21 | confetti_table_man:store({ProviderName, Opts, Conf, RawConf}). 22 | 23 | %%%=================================================================== 24 | %%% Helpers 25 | %%%=================================================================== 26 | 27 | dump({_,D} = Location, OutConf) -> 28 | DumpDir = filename:join([D, "dump"]), 29 | case filelib:is_dir(DumpDir) of 30 | true -> ok; 31 | false -> 32 | file:make_dir(DumpDir) 33 | end, 34 | Fname = confetti_utils:fname(dump, Location), 35 | case file:write_file(Fname, OutConf) of 36 | ok -> {ok, Fname}; 37 | {error, Reason} -> {error, Reason} 38 | end. 39 | -------------------------------------------------------------------------------- /src/example_srv.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Example: using confetti configuration provider. 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(example_srv). 9 | 10 | -behaviour(gen_server). 11 | 12 | %% API 13 | -export([start_link/0]). 14 | 15 | %% gen_server callbacks 16 | -export([init/1, 17 | handle_call/3, 18 | handle_cast/2, 19 | handle_info/2, 20 | terminate/2, 21 | code_change/3]). 22 | 23 | -define(SERVER, ?MODULE). 24 | 25 | validate1(Conf) -> 26 | io:format("Config seems valid!~n"), 27 | io:format("Operating on ~p~n", [Conf]), 28 | {ok, {modified_conf, Conf}}. 29 | 30 | validate2(Conf = {modified_conf, _}) -> 31 | io:format("Another validator!~n"), 32 | io:format("Operating on ~p~n", [Conf]), 33 | {ok, {transofmed_conf, Conf}}. 34 | 35 | start_link() -> 36 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). 37 | 38 | init([]) -> 39 | {ok, ProviderPid} = confetti:use(example, [ 40 | {location, {"example.conf", "conf"}}, 41 | {validators, [fun validate1/1, fun validate2/1]} 42 | ]), 43 | io:format("~n~n~nUsing provider: ~p~n~n~n", [ProviderPid]), 44 | Conf = confetti:fetch(example), 45 | io:format("Hi, I was initialized with config: ~p~n", [Conf]), 46 | {ok, 1}. 47 | 48 | handle_call(_Request, _From, State) -> 49 | Reply = ok, 50 | {reply, Reply, State}. 51 | 52 | handle_cast(_Msg, State) -> 53 | {noreply, State}. 54 | 55 | handle_info(Info, State) -> 56 | io:format("confetti client got info ~p~n", [Info]), 57 | {noreply, State}. 58 | 59 | terminate(_Reason, _State) -> 60 | ok. 61 | 62 | code_change(_OldVsn, State, _Extra) -> 63 | {ok, State}. 64 | -------------------------------------------------------------------------------- /src/example_user_commands.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Adam Rutkowski 3 | %%% @copyright (C) 2011, jtendo 4 | %%% @doc 5 | %%% Example of management console plugin module. 6 | %%% @end 7 | %%%------------------------------------------------------------------- 8 | -module(example_user_commands). 9 | 10 | -export([say_hi/0, say_hi/1]). 11 | 12 | say_hi(help) -> 13 | "This commands greets you!". 14 | 15 | say_hi() -> 16 | "Oh Hai!". 17 | --------------------------------------------------------------------------------