diff options
author | Nick Vatamaniuc <vatamane@apache.org> | 2022-10-26 02:03:44 -0400 |
---|---|---|
committer | Nick Vatamaniuc <nickva@users.noreply.github.com> | 2022-10-26 10:01:26 -0400 |
commit | 8a71e3db8a43d8f7e916cbe5f7e5b8be507501c2 (patch) | |
tree | fcad8d6e9790901c1139956d746f75d6fddec534 | |
parent | 4e9c5588d765b84742784de5aafa146d14eed11f (diff) | |
download | couchdb-8a71e3db8a43d8f7e916cbe5f7e5b8be507501c2.tar.gz |
Integrate config app into main repo
As per consensus in ML discussion https://lists.apache.org/thread/9dphqb6mjh1v234v15rcft7mfpjx9223
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | rebar.config.script | 2 | ||||
-rw-r--r-- | src/config/LICENSE | 202 | ||||
-rw-r--r-- | src/config/src/config.app.src.script | 32 | ||||
-rw-r--r-- | src/config/src/config.erl | 629 | ||||
-rw-r--r-- | src/config/src/config_app.erl | 66 | ||||
-rw-r--r-- | src/config/src/config_listener.erl | 75 | ||||
-rw-r--r-- | src/config/src/config_listener_mon.erl | 80 | ||||
-rw-r--r-- | src/config/src/config_notifier.erl | 79 | ||||
-rw-r--r-- | src/config/src/config_sup.erl | 39 | ||||
-rw-r--r-- | src/config/src/config_util.erl | 75 | ||||
-rw-r--r-- | src/config/src/config_writer.erl | 73 | ||||
-rw-r--r-- | src/config/test/config_tests.erl | 830 | ||||
-rw-r--r-- | src/config/test/fixtures/config_default_test.ini | 23 | ||||
-rw-r--r-- | src/config/test/fixtures/config_tests_1.ini | 22 | ||||
-rw-r--r-- | src/config/test/fixtures/config_tests_2.ini | 22 | ||||
-rw-r--r-- | src/config/test/fixtures/default.d/extra.ini | 19 | ||||
-rw-r--r-- | src/config/test/fixtures/local.d/extra.ini | 19 |
18 files changed, 2287 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore index 518ec7b19..acf98e5c0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ debian/ log apache-couchdb-*/ bin/ -config.erl +/config.erl *.tar.gz *.tar.bz2 dev/*.beam @@ -42,7 +42,6 @@ share/www src/b64url/ src/bear/ src/certifi/ -src/config/ src/couch/priv/couch_js/**/config.h src/couch/priv/couchjs src/couch/priv/couchspawnkillable diff --git a/rebar.config.script b/rebar.config.script index bfae0c85f..3b91431f5 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -108,6 +108,7 @@ os:putenv("COUCHDB_APPS_CONFIG_DIR", filename:join([COUCHDB_ROOT, "rel/apps"])). SubDirs = [ %% must be compiled first as it has a custom behavior "src/couch_epi", + "src/config", "src/couch_log", "src/chttpd", "src/couch", @@ -141,7 +142,6 @@ SubDirs = [ DepDescs = [ %% Independent Apps -{config, "config", {tag, "2.2.1"}}, {b64url, "b64url", {tag, "1.0.3"}}, {ets_lru, "ets-lru", {tag, "1.1.0"}}, {khash, "khash", {tag, "1.1.0"}}, diff --git a/src/config/LICENSE b/src/config/LICENSE new file mode 100644 index 000000000..f6cd2bc80 --- /dev/null +++ b/src/config/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/src/config/src/config.app.src.script b/src/config/src/config.app.src.script new file mode 100644 index 000000000..e4faf276f --- /dev/null +++ b/src/config/src/config.app.src.script @@ -0,0 +1,32 @@ +% 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. + +ConfigPath = filename:join([os:getenv("COUCHDB_APPS_CONFIG_DIR"), "config.config"]), +AppEnv = case filelib:is_file(ConfigPath) of + true -> + {ok, Result} = file:consult(ConfigPath), + Result; + false -> + [] +end. + +{application, config, [ + {description, "INI file configuration system for Apache CouchDB"}, + {vsn, git}, + {registered, [ + config, + config_event + ]}, + {applications, [kernel, stdlib]}, + {mod, {config_app, []}}, + {env, AppEnv} +]}. diff --git a/src/config/src/config.erl b/src/config/src/config.erl new file mode 100644 index 000000000..58e3a40d9 --- /dev/null +++ b/src/config/src/config.erl @@ -0,0 +1,629 @@ +% 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. + +% Reads CouchDB's ini file and gets queried for configuration parameters. +% This module is initialized with a list of ini files that it consecutively +% reads Key/Value pairs from and saves them in an ets table. If more an one +% ini file is specified, the last one is used to write changes that are made +% with store/2 back to that ini file. + +-module(config). +-behaviour(gen_server). +-vsn(1). + +-export([start_link/1, stop/0, reload/0]). + +-export([all/0]). +-export([get/1, get/2, get/3]). +-export([set/3, set/4, set/5]). +-export([delete/2, delete/3, delete/4]). + +-export([get_integer/3, set_integer/3, set_integer/4]). +-export([get_float/3, set_float/3, set_float/4]). +-export([get_boolean/3, set_boolean/3, set_boolean/4]). + +-export([features/0, enable_feature/1, disable_feature/1]). + +-export([listen_for_changes/2]). +-export([subscribe_for_changes/1]). +-export([parse_ini_file/1]). + +-export([init/1, terminate/2, code_change/3]). +-export([handle_call/3, handle_cast/2, handle_info/2]). + +-export([is_sensitive/2]). + +-define(FEATURES, "features"). + +-define(TIMEOUT, 30000). +-define(INVALID_SECTION, <<"Invalid configuration section">>). +-define(INVALID_KEY, <<"Invalid configuration key">>). +-define(INVALID_VALUE, <<"Invalid configuration value">>). + +-record(config, { + notify_funs = [], + ini_files = undefined, + write_filename = undefined +}). + +start_link(IniFiles) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, IniFiles, []). + +stop() -> + gen_server:cast(?MODULE, stop). + +reload() -> + gen_server:call(?MODULE, reload, ?TIMEOUT). + +all() -> + lists:sort(gen_server:call(?MODULE, all, infinity)). + +get_integer(Section, Key, Default) when is_integer(Default) -> + try + to_integer(get(Section, Key, Default)) + catch + error:badarg -> + Default + end. + +set_integer(Section, Key, Value) -> + set_integer(Section, Key, Value, true). + +set_integer(Section, Key, Value, Persist) when is_integer(Value) -> + set(Section, Key, integer_to_list(Value), Persist); +set_integer(_, _, _, _) -> + error(badarg). + +to_integer(List) when is_list(List) -> + list_to_integer(List); +to_integer(Int) when is_integer(Int) -> + Int; +to_integer(Bin) when is_binary(Bin) -> + list_to_integer(binary_to_list(Bin)). + +get_float(Section, Key, Default) when is_float(Default) -> + try + to_float(get(Section, Key, Default)) + catch + error:badarg -> + Default + end. + +set_float(Section, Key, Value) -> + set_float(Section, Key, Value, true). + +set_float(Section, Key, Value, Persist) when is_float(Value) -> + set(Section, Key, float_to_list(Value), Persist); +set_float(_, _, _, _) -> + error(badarg). + +to_float(List) when is_list(List) -> + list_to_float(List); +to_float(Float) when is_float(Float) -> + Float; +to_float(Int) when is_integer(Int) -> + list_to_float(integer_to_list(Int) ++ ".0"); +to_float(Bin) when is_binary(Bin) -> + list_to_float(binary_to_list(Bin)). + +get_boolean(Section, Key, Default) when is_boolean(Default) -> + try + to_boolean(get(Section, Key, Default)) + catch + error:badarg -> + Default + end. + +set_boolean(Section, Key, Value) -> + set_boolean(Section, Key, Value, true). + +set_boolean(Section, Key, true, Persist) -> + set(Section, Key, "true", Persist); +set_boolean(Section, Key, false, Persist) -> + set(Section, Key, "false", Persist); +set_boolean(_, _, _, _) -> + error(badarg). + +to_boolean(List) when is_list(List) -> + case list_to_existing_atom(List) of + true -> + true; + false -> + false; + _ -> + error(badarg) + end; +to_boolean(Bool) when is_boolean(Bool) -> + Bool. + +get(Section) when is_binary(Section) -> + ?MODULE:get(binary_to_list(Section)); +get(Section) when is_list(Section) -> + Matches = ets:match(?MODULE, {{Section, '$1'}, '$2'}), + [{Key, Value} || [Key, Value] <- Matches]. + +get(Section, Key) -> + ?MODULE:get(Section, Key, undefined). + +get(Section, Key, Default) when is_binary(Section) and is_binary(Key) -> + ?MODULE:get(binary_to_list(Section), binary_to_list(Key), Default); +get(Section, Key, Default) when is_list(Section), is_list(Key) -> + case ets:lookup(?MODULE, {Section, Key}) of + [] when Default == undefined -> Default; + [] when is_boolean(Default) -> Default; + [] when is_float(Default) -> Default; + [] when is_integer(Default) -> Default; + [] when is_list(Default) -> Default; + [] when is_atom(Default) -> Default; + [] -> error(badarg); + [{_, Match}] -> Match + end. + +set(Section, Key, Value) -> + ?MODULE:set(Section, Key, Value, true, nil). + +set(Sec, Key, Val, Opts) when is_binary(Sec) and is_binary(Key) -> + ?MODULE:set(binary_to_list(Sec), binary_to_list(Key), Val, Opts); +set(Section, Key, Value, Persist) when is_boolean(Persist) -> + ?MODULE:set(Section, Key, Value, #{persist => Persist}); +set(Section, Key, Value, #{} = Opts) when + is_list(Section), is_list(Key), is_list(Value) +-> + gen_server:call(?MODULE, {set, Section, Key, Value, Opts}, ?TIMEOUT); +set(Section, Key, Value, Reason) when + is_list(Section), is_list(Key), is_list(Value) +-> + ?MODULE:set(Section, Key, Value, #{persist => true, reason => Reason}); +set(_Sec, _Key, _Val, _Options) -> + error(badarg). + +set(Section, Key, Value, Persist, Reason) when + is_list(Section), is_list(Key), is_list(Value) +-> + ?MODULE:set(Section, Key, Value, #{persist => Persist, reason => Reason}). + +delete(Section, Key) when is_binary(Section) and is_binary(Key) -> + delete(binary_to_list(Section), binary_to_list(Key)); +delete(Section, Key) -> + delete(Section, Key, true, nil). + +delete(Section, Key, Persist) when is_boolean(Persist) -> + delete(Section, Key, Persist, nil); +delete(Section, Key, Reason) -> + delete(Section, Key, true, Reason). + +delete(Sec, Key, Persist, Reason) when is_binary(Sec) and is_binary(Key) -> + delete(binary_to_list(Sec), binary_to_list(Key), Persist, Reason); +delete(Section, Key, Persist, Reason) when is_list(Section), is_list(Key) -> + gen_server:call( + ?MODULE, + {delete, Section, Key, Persist, Reason}, + ?TIMEOUT + ). + +features() -> + application:get_env(config, enabled_features, []). + +enable_feature(Feature) when is_atom(Feature) -> + application:set_env( + config, + enabled_features, + lists:usort([Feature | features()]), + [{persistent, true}] + ). + +disable_feature(Feature) when is_atom(Feature) -> + application:set_env( + config, + enabled_features, + features() -- [Feature], + [{persistent, true}] + ). + +listen_for_changes(CallbackModule, InitialState) -> + config_listener_mon:subscribe(CallbackModule, InitialState). + +subscribe_for_changes(Subscription) -> + config_notifier:subscribe(Subscription). + +init(IniFiles) -> + ets:new(?MODULE, [named_table, set, protected, {read_concurrency, true}]), + lists:map( + fun(IniFile) -> + {ok, ParsedIniValues} = parse_ini_file(IniFile), + ets:insert(?MODULE, ParsedIniValues) + end, + IniFiles + ), + WriteFile = + case IniFiles of + [_ | _] -> lists:last(IniFiles); + _ -> undefined + end, + debug_config(), + {ok, #config{ini_files = IniFiles, write_filename = WriteFile}}. + +terminate(_Reason, _State) -> + ok. + +handle_call(all, _From, Config) -> + Resp = lists:sort((ets:tab2list(?MODULE))), + {reply, Resp, Config}; +handle_call({set, Sec, Key, Val, Opts}, _From, Config) -> + Persist = maps:get(persist, Opts, true), + Reason = maps:get(reason, Opts, nil), + IsSensitive = is_sensitive(Sec, Key), + case validate_config_update(Sec, Key, Val) of + {error, ValidationError} when IsSensitive -> + couch_log:error( + "~p: [~s] ~s = '****' rejected for reason ~p", + [?MODULE, Sec, Key, Reason] + ), + {reply, {error, ValidationError}, Config}; + {error, ValidationError} -> + couch_log:error( + "~p: [~s] ~s = '~s' rejected for reason ~p", + [?MODULE, Sec, Key, Val, Reason] + ), + {reply, {error, ValidationError}, Config}; + ok -> + true = ets:insert(?MODULE, {{Sec, Key}, Val}), + case IsSensitive of + false -> + couch_log:notice( + "~p: [~s] ~s set to ~s for reason ~p", + [?MODULE, Sec, Key, Val, Reason] + ); + true -> + couch_log:notice( + "~p: [~s] ~s set to '****' for reason ~p", + [?MODULE, Sec, Key, Reason] + ) + end, + ConfigWriteReturn = + case {Persist, Config#config.write_filename} of + {true, undefined} -> + ok; + {true, FileName} -> + config_writer:save_to_file({{Sec, Key}, Val}, FileName); + _ -> + ok + end, + case ConfigWriteReturn of + ok -> + Event = {config_change, Sec, Key, Val, Persist}, + gen_event:sync_notify(config_event, Event), + {reply, ok, Config}; + {error, Else} -> + {reply, {error, Else}, Config} + end + end; +handle_call({delete, Sec, Key, Persist, Reason}, _From, Config) -> + true = ets:delete(?MODULE, {Sec, Key}), + couch_log:notice( + "~p: [~s] ~s deleted for reason ~p", + [?MODULE, Sec, Key, Reason] + ), + ConfigDeleteReturn = + case {Persist, Config#config.write_filename} of + {true, undefined} -> + ok; + {true, FileName} -> + config_writer:save_to_file({{Sec, Key}, ""}, FileName); + _ -> + ok + end, + case ConfigDeleteReturn of + ok -> + Event = {config_change, Sec, Key, deleted, Persist}, + gen_event:sync_notify(config_event, Event), + {reply, ok, Config}; + Else -> + {reply, Else, Config} + end; +handle_call(reload, _From, Config) -> + DiskKVs = lists:foldl( + fun(IniFile, DiskKVs0) -> + {ok, ParsedIniValues} = parse_ini_file(IniFile), + lists:foldl( + fun({K, V}, DiskKVs1) -> + dict:store(K, V, DiskKVs1) + end, + DiskKVs0, + ParsedIniValues + ) + end, + dict:new(), + Config#config.ini_files + ), + % Update ets with anything we just read + % from disk + dict:fold( + fun({Sec, Key} = K, V, _) -> + VExisting = get(Sec, Key, V), + ets:insert(?MODULE, {K, V}), + case V =:= VExisting of + true -> + ok; + false -> + case is_sensitive(Sec, Key) of + false -> + couch_log:notice( + "Reload detected config change ~s.~s = ~p", + [Sec, Key, V] + ); + true -> + couch_log:notice( + "Reload detected config change ~s.~s = '****'", + [Sec, Key] + ) + end, + Event = {config_change, Sec, Key, V, true}, + gen_event:sync_notify(config_event, Event) + end + end, + nil, + DiskKVs + ), + % And remove anything in ets that wasn't + % on disk. + ets:foldl( + fun({{Sec, Key} = K, _}, _) -> + case dict:is_key(K, DiskKVs) of + true -> + ok; + false -> + couch_log:notice("Reload deleting in-memory config ~s.~s", [Sec, Key]), + ets:delete(?MODULE, K), + Event = {config_change, Sec, Key, deleted, true}, + gen_event:sync_notify(config_event, Event) + end + end, + nil, + ?MODULE + ), + {reply, ok, Config}. + +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(Info, State) -> + couch_log:error("config:handle_info Info: ~p~n", [Info]), + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +is_sensitive(Section, Key) -> + Sensitive = application:get_env(config, sensitive, #{}), + case maps:get(Section, Sensitive, false) of + all -> true; + Fields when is_list(Fields) -> lists:member(Key, Fields); + _ -> false + end. + +parse_ini_file(IniFile) -> + IniFilename = config_util:abs_pathname(IniFile), + IniBin = + case file:read_file(IniFilename) of + {ok, IniBin0} -> + IniBin0; + {error, enoent} -> + Fmt = "Couldn't find server configuration file ~s.", + Msg = list_to_binary(io_lib:format(Fmt, [IniFilename])), + couch_log:error("~s~n", [Msg]), + throw({startup_error, Msg}) + end, + + Lines = re:split(IniBin, "\r\n|\n|\r|\032", [{return, list}]), + {_, ParsedIniValues} = + lists:foldl( + fun(Line, {AccSectionName, AccValues}) -> + case string:strip(Line) of + "[" ++ Rest -> + case re:split(Rest, "\\]", [{return, list}]) of + [NewSectionName, ""] -> + {NewSectionName, AccValues}; + % end bracket not at end, ignore this line + _Else -> + {AccSectionName, AccValues} + end; + ";" ++ _Comment -> + {AccSectionName, AccValues}; + Line2 -> + case re:split(Line2, "\s?=\s?", [{return, list}]) of + [Value] -> + MultiLineValuePart = + case re:run(Line, "^ \\S", []) of + {match, _} -> + true; + _ -> + false + end, + case {MultiLineValuePart, AccValues} of + {true, [{{_, ValueName}, PrevValue} | AccValuesRest]} -> + % remove comment + case re:split(Value, " ;|\t;", [{return, list}]) of + [[]] -> + % empty line + {AccSectionName, AccValues}; + [LineValue | _Rest] -> + E = { + {AccSectionName, ValueName}, + PrevValue ++ " " ++ LineValue + }, + {AccSectionName, [E | AccValuesRest]} + end; + _ -> + {AccSectionName, AccValues} + end; + % line begins with "=", ignore + ["" | _LineValues] -> + {AccSectionName, AccValues}; + % yeehaw, got a line! + [ValueName | LineValues] -> + RemainingLine = config_util:implode(LineValues, "="), + % removes comments + case re:split(RemainingLine, " ;|\t;", [{return, list}]) of + [[]] -> + % empty line means delete this key + ets:delete(?MODULE, {AccSectionName, ValueName}), + {AccSectionName, AccValues}; + [LineValue | _Rest] -> + LineValueWithoutLeadTrailWS = string:trim(LineValue), + {AccSectionName, [ + { + {AccSectionName, ValueName}, + LineValueWithoutLeadTrailWS + } + | AccValues + ]} + end + end + end + end, + {"", []}, + Lines + ), + {ok, ParsedIniValues}. + +debug_config() -> + case ?MODULE:get("log", "level") of + "debug" -> + io:format("Configuration Settings:~n", []), + lists:foreach( + fun({{Mod, Key}, Val}) -> + io:format(" [~s] ~s=~p~n", [Mod, Key, Val]) + end, + lists:sort(ets:tab2list(?MODULE)) + ); + _ -> + ok + end. + +validate_config_update(Sec, Key, Val) -> + %% See https://erlang.org/doc/man/re.html & + %% https://pcre.org/original/doc/html/pcrepattern.html + %% + %% only characters that are actually screen-visible are allowed + %% tabs and spaces are allowed + %% no [ ] explicitly to avoid section header bypass + {ok, Forbidden} = re:compile("[\]\[]+", [dollar_endonly, unicode]), + %% Values are permitted [ ] characters as we use these in + %% places like mochiweb socket option lists + %% Values may also be empty to delete manual configuration + {ok, Printable} = re:compile( + "^[[:graph:]\t\s]*$", + [dollar_endonly, unicode] + ), + case + { + re:run(Sec, Printable), + re:run(Sec, Forbidden), + re:run(Key, Printable), + re:run(Key, Forbidden), + re:run(Val, Printable) + } + of + {{match, _}, nomatch, {match, _}, nomatch, {match, _}} -> ok; + {nomatch, _, _, _, _} -> {error, ?INVALID_SECTION}; + {_, {match, _}, _, _, _} -> {error, ?INVALID_SECTION}; + {_, _, nomatch, _, _} -> {error, ?INVALID_KEY}; + {_, _, _, {match, _}, _} -> {error, ?INVALID_KEY}; + {_, _, _, _, nomatch} -> {error, ?INVALID_VALUE} + end. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +to_integer_test() -> + ?assertEqual(1, to_integer(1)), + ?assertEqual(1, to_integer(<<"1">>)), + ?assertEqual(1, to_integer("1")), + ?assertEqual(-1, to_integer("-01")), + ?assertEqual(0, to_integer("-0")), + ?assertEqual(0, to_integer("+0")), + ok. + +to_float_test() -> + ?assertEqual(1.0, to_float(1)), + ?assertEqual(1.0, to_float(<<"1.0">>)), + ?assertEqual(1.0, to_float("1.0")), + ?assertEqual(-1.1, to_float("-01.1")), + ?assertEqual(0.0, to_float("-0.0")), + ?assertEqual(0.0, to_float("+0.0")), + ok. + +validation_test() -> + ?assertEqual(ok, validate_config_update("section", "key", "value")), + ?assertEqual(ok, validate_config_update("delete", "empty_value", "")), + ?assertEqual( + {error, ?INVALID_SECTION}, + validate_config_update("sect[ion", "key", "value") + ), + ?assertEqual( + {error, ?INVALID_SECTION}, + validate_config_update("sect]ion", "key", "value") + ), + ?assertEqual( + {error, ?INVALID_SECTION}, + validate_config_update("section\n", "key", "value") + ), + ?assertEqual( + {error, ?INVALID_SECTION}, + validate_config_update("section\r", "key", "value") + ), + ?assertEqual( + {error, ?INVALID_SECTION}, + validate_config_update("section\r\n", "key", "value") + ), + ?assertEqual( + {error, ?INVALID_KEY}, + validate_config_update("section", "key\n", "value") + ), + ?assertEqual( + {error, ?INVALID_KEY}, + validate_config_update("section", "key\r", "value") + ), + ?assertEqual( + {error, ?INVALID_KEY}, + validate_config_update("section", "key\r\n", "value") + ), + ?assertEqual( + {error, ?INVALID_VALUE}, + validate_config_update("section", "key", "value\n") + ), + ?assertEqual( + {error, ?INVALID_VALUE}, + validate_config_update("section", "key", "value\r") + ), + ?assertEqual( + {error, ?INVALID_VALUE}, + validate_config_update("section", "key", "value\r\n") + ), + ?assertEqual( + {error, ?INVALID_KEY}, + validate_config_update("section", "k[ey", "value") + ), + ?assertEqual( + {error, ?INVALID_KEY}, + validate_config_update("section", "[key", "value") + ), + ?assertEqual( + {error, ?INVALID_KEY}, + validate_config_update("section", "key]", "value") + ), + ok. + +-endif. diff --git a/src/config/src/config_app.erl b/src/config/src/config_app.erl new file mode 100644 index 000000000..8ba58e6d2 --- /dev/null +++ b/src/config/src/config_app.erl @@ -0,0 +1,66 @@ +% 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(config_app). + +-behaviour(application). + +%% Application callbacks +-export([start/2, stop/1]). + +%% =================================================================== +%% Application callbacks +%% =================================================================== + +start(_StartType, _StartArgs) -> + config_sup:start_link(get_ini_files()). + +stop(_State) -> + ok. + +get_ini_files() -> + IniFiles = hd([L || L <- [command_line(), env(), default()], L =/= skip]), + lists:flatmap(fun expand_dirs/1, IniFiles). + +env() -> + case application:get_env(config, ini_files) of + undefined -> + skip; + {ok, IniFiles} -> + IniFiles + end. + +command_line() -> + case init:get_argument(couch_ini) of + error -> + skip; + {ok, [IniFiles]} -> + IniFiles + end. + +default() -> + Etc = filename:join(code:root_dir(), "etc"), + Default = [ + filename:join(Etc, "default.ini"), + filename:join(Etc, "default.d"), + filename:join(Etc, "local.ini"), + filename:join(Etc, "local.d") + ], + lists:filter(fun filelib:is_file/1, Default). + +expand_dirs(File) -> + case filelib:is_dir(File) of + true -> + lists:sort(filelib:wildcard(File ++ "/*.ini")); + false -> + [File] + end. diff --git a/src/config/src/config_listener.erl b/src/config/src/config_listener.erl new file mode 100644 index 000000000..dede2e35e --- /dev/null +++ b/src/config/src/config_listener.erl @@ -0,0 +1,75 @@ +% 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(config_listener). + +-behaviour(gen_event). +-vsn(2). + +%% Public interface +-export([start/2]). +-export([start/3]). + +%% Required gen_event interface +-export([ + init/1, + handle_event/2, + handle_call/2, + handle_info/2, + terminate/2, + code_change/3 +]). + +-callback handle_config_change( + Sec :: string(), + Key :: string(), + Value :: string(), + Persist :: boolean(), + State :: term() +) -> + {ok, term()} | remove_handler. + +-callback handle_config_terminate( + Subscriber :: pid(), + Reason :: term(), + State :: term() +) -> + term(). + +start(Module, State) -> + start(Module, Module, State). + +start(Module, Id, State) -> + gen_event:add_sup_handler(config_event, {?MODULE, Id}, {Module, State}). + +init({Module, State}) -> + {ok, {Module, State}}. + +handle_event({config_change, Sec, Key, Value, Persist}, {Module, {From, State}}) -> + case Module:handle_config_change(Sec, Key, Value, Persist, State) of + {ok, NewState} -> + {ok, {Module, {From, NewState}}}; + remove_handler -> + remove_handler + end. + +handle_call(_Request, St) -> + {ok, ignored, St}. + +handle_info(_Info, St) -> + {ok, St}. + +terminate(Reason, {Module, {Subscriber, State}}) -> + Module:handle_config_terminate(Subscriber, Reason, State). + +code_change(_OldVsn, St, _Extra) -> + {ok, St}. diff --git a/src/config/src/config_listener_mon.erl b/src/config/src/config_listener_mon.erl new file mode 100644 index 000000000..02cfd9502 --- /dev/null +++ b/src/config/src/config_listener_mon.erl @@ -0,0 +1,80 @@ +% 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(config_listener_mon). +-behaviour(gen_server). +-vsn(1). + +-export([ + subscribe/2, + start_link/2 +]). + +-export([ + init/1, + terminate/2, + handle_call/3, + handle_cast/2, + handle_info/2, + code_change/3 +]). + +-record(st, { + pid, + ref +}). + +start_link(Module, InitSt) -> + proc_lib:start_link(?MODULE, init, [{self(), Module, InitSt}]). + +subscribe(Module, InitSt) -> + case proc_lib:start(?MODULE, init, [{self(), Module, InitSt}]) of + {ok, _} -> ok; + Else -> Else + end. + +init({Pid, Mod, InitSt}) -> + Ref = erlang:monitor(process, Pid), + case config_listener:start(Mod, {Mod, Pid}, {Pid, InitSt}) of + ok -> + proc_lib:init_ack({ok, self()}), + gen_server:enter_loop(?MODULE, [], #st{pid = Pid, ref = Ref}); + Else -> + proc_lib:init_ack(Else) + end. + +terminate(_Reason, _St) -> + ok. + +handle_call(_Message, _From, St) -> + {reply, ignored, St}. + +handle_cast(_Message, St) -> + {noreply, St}. + +handle_info({'DOWN', Ref, _, _, _}, #st{ref = Ref} = St) -> + {stop, normal, St}; +handle_info({gen_event_EXIT, {config_listener, Module}, Reason}, St) -> + Level = + case Reason of + normal -> debug; + shutdown -> debug; + _ -> error + end, + Fmt = "config_listener(~p) for ~p stopped with reason: ~r~n", + couch_log:Level(Fmt, [Module, St#st.pid, Reason]), + {stop, shutdown, St}; +handle_info(_, St) -> + {noreply, St}. + +code_change(_OldVsn, St, _Extra) -> + {ok, St}. diff --git a/src/config/src/config_notifier.erl b/src/config/src/config_notifier.erl new file mode 100644 index 000000000..d09dd4fbc --- /dev/null +++ b/src/config/src/config_notifier.erl @@ -0,0 +1,79 @@ +% 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(config_notifier). + +-behaviour(gen_event). +-vsn(1). + +%% Public interface +-export([subscribe/1]). +-export([subscribe/2]). + +%% gen_event interface +-export([ + init/1, + handle_event/2, + handle_call/2, + handle_info/2, + terminate/2, + code_change/3 +]). + +subscribe(Subscription) -> + subscribe(self(), Subscription). + +subscribe(Subscriber, Subscription) -> + case lists:member(Subscriber, handlers()) of + true -> + ok; + false -> + gen_event:add_sup_handler( + config_event, {?MODULE, Subscriber}, {Subscriber, Subscription} + ) + end. + +init({Subscriber, Subscription}) -> + {ok, {Subscriber, Subscription}}. + +handle_event({config_change, _, _, _, _} = Event, {Subscriber, Subscription}) -> + maybe_notify(Event, Subscriber, Subscription), + {ok, {Subscriber, Subscription}}. + +handle_call(_Request, St) -> + {ok, ignored, St}. + +handle_info(_Info, St) -> + {ok, St}. + +terminate(_Reason, {_Subscriber, _Subscription}) -> + ok. + +code_change(_OldVsn, St, _Extra) -> + {ok, St}. + +maybe_notify(Event, Subscriber, all) -> + Subscriber ! Event; +maybe_notify({config_change, Sec, Key, _, _} = Event, Subscriber, Subscription) -> + case should_notify(Sec, Key, Subscription) of + true -> + Subscriber ! Event; + false -> + ok + end. + +should_notify(Sec, Key, Subscription) -> + lists:any(fun(S) -> S =:= Sec orelse S =:= {Sec, Key} end, Subscription). + +handlers() -> + AllHandlers = gen_event:which_handlers(config_event), + [Id || {?MODULE, Id} <- AllHandlers]. diff --git a/src/config/src/config_sup.erl b/src/config/src/config_sup.erl new file mode 100644 index 000000000..64e1e03bb --- /dev/null +++ b/src/config/src/config_sup.erl @@ -0,0 +1,39 @@ +% 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(config_sup). +-behaviour(supervisor). + +%% API +-export([start_link/1]). + +%% Supervisor callbacks +-export([init/1]). + +%% =================================================================== +%% API functions +%% =================================================================== + +start_link(IniFiles) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, IniFiles). + +%% =================================================================== +%% Supervisor callbacks +%% =================================================================== + +init(IniFiles) -> + Children = [ + {config, {config, start_link, [IniFiles]}, permanent, 5000, worker, [config]}, + {config_event, {gen_event, start_link, [{local, config_event}]}, permanent, 5000, worker, + dynamic} + ], + {ok, {{one_for_one, 5, 10}, Children}}. diff --git a/src/config/src/config_util.erl b/src/config/src/config_util.erl new file mode 100644 index 000000000..d71117ab0 --- /dev/null +++ b/src/config/src/config_util.erl @@ -0,0 +1,75 @@ +% 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(config_util). + +-export([abs_pathname/1]). +-export([abs_pathname/2]). +-export([implode/2]). + +% given a pathname "../foo/bar/" it gives back the fully qualified +% absolute pathname. +abs_pathname(" " ++ Filename) -> + % strip leading whitspace + abs_pathname(Filename); +abs_pathname([$/ | _] = Filename) -> + Filename; +abs_pathname(Filename) -> + {ok, Cwd} = file:get_cwd(), + {Filename2, Args} = separate_cmd_args(Filename, ""), + abs_pathname(Filename2, Cwd) ++ Args. + +abs_pathname(Filename, Dir) -> + Name = filename:absname(Filename, Dir ++ "/"), + OutFilename = filename:join(fix_path_list(filename:split(Name), [])), + % If the filename is a dir (last char slash, put back end slash + case string:right(Filename, 1) of + "/" -> + OutFilename ++ "/"; + "\\" -> + OutFilename ++ "/"; + _Else -> + OutFilename + end. + +implode(List, Sep) -> + implode(List, Sep, []). + +implode([], _Sep, Acc) -> + lists:flatten(lists:reverse(Acc)); +implode([H], Sep, Acc) -> + implode([], Sep, [H | Acc]); +implode([H | T], Sep, Acc) -> + implode(T, Sep, [Sep, H | Acc]). + +% if this as an executable with arguments, seperate out the arguments +% ""./foo\ bar.sh -baz=blah" -> {"./foo\ bar.sh", " -baz=blah"} +separate_cmd_args("", CmdAcc) -> + {lists:reverse(CmdAcc), ""}; +% handle skipped value +separate_cmd_args("\\ " ++ Rest, CmdAcc) -> + separate_cmd_args(Rest, " \\" ++ CmdAcc); +separate_cmd_args(" " ++ Rest, CmdAcc) -> + {lists:reverse(CmdAcc), " " ++ Rest}; +separate_cmd_args([Char | Rest], CmdAcc) -> + separate_cmd_args(Rest, [Char | CmdAcc]). + +% takes a heirarchical list of dirs and removes the dots ".", double dots +% ".." and the corresponding parent dirs. +fix_path_list([], Acc) -> + lists:reverse(Acc); +fix_path_list([".." | Rest], [_PrevAcc | RestAcc]) -> + fix_path_list(Rest, RestAcc); +fix_path_list(["." | Rest], Acc) -> + fix_path_list(Rest, Acc); +fix_path_list([Dir | Rest], Acc) -> + fix_path_list(Rest, [Dir | Acc]). diff --git a/src/config/src/config_writer.erl b/src/config/src/config_writer.erl new file mode 100644 index 000000000..2417416c3 --- /dev/null +++ b/src/config/src/config_writer.erl @@ -0,0 +1,73 @@ +% 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. + +%% @doc Saves a Key/Value pair to a ini file. The Key consists of a Section +%% and Option combination. If that combination is found in the ini file +%% the new value replaces the old value. If only the Section is found the +%% Option and value combination is appended to the Section. If the Section +%% does not yet exist in the ini file, it is added and the Option/Value +%% pair is appended. +%% @see config + +-module(config_writer). + +-export([save_to_file/2]). + +%% @spec save_to_file( +%% Config::{{Section::string(), Option::string()}, Value::string()}, +%% File::filename()) -> ok +%% @doc Saves a Section/Key/Value triple to the ini file File::filename() +save_to_file({{Section, Key}, Value}, File) -> + {ok, OldFileContents} = file:read_file(File), + Lines = re:split(OldFileContents, "\r\n|\n|\r|\032", [{return, list}]), + + SectionLine = "[" ++ Section ++ "]", + {ok, Pattern} = re:compile(["^(\\Q", Key, "\\E\\s*=)|\\[[a-zA-Z0-9\.\_-]*\\]"]), + + NewLines = process_file_lines(Lines, [], SectionLine, Pattern, Key, Value), + NewFileContents = reverse_and_add_newline(strip_empty_lines(NewLines), []), + file:write_file(File, NewFileContents). + +process_file_lines([Section | Rest], SeenLines, Section, Pattern, Key, Value) -> + process_section_lines(Rest, [Section | SeenLines], Pattern, Key, Value); +process_file_lines([Line | Rest], SeenLines, Section, Pattern, Key, Value) -> + process_file_lines(Rest, [Line | SeenLines], Section, Pattern, Key, Value); +process_file_lines([], SeenLines, Section, _Pattern, Key, Value) -> + % Section wasn't found. Append it with the option here. + [Key ++ " = " ++ Value, Section, "" | strip_empty_lines(SeenLines)]. + +process_section_lines([Line | Rest], SeenLines, Pattern, Key, Value) -> + case re:run(Line, Pattern, [{capture, all_but_first}]) of + % Found nothing interesting. Move on. + nomatch -> + process_section_lines(Rest, [Line | SeenLines], Pattern, Key, Value); + % Found another section. Append the option here. + {match, []} -> + lists:reverse(Rest) ++ + [Line, "", Key ++ " = " ++ Value | strip_empty_lines(SeenLines)]; + % Found the option itself. Replace it. + {match, _} -> + lists:reverse(Rest) ++ [Key ++ " = " ++ Value | SeenLines] + end; +process_section_lines([], SeenLines, _Pattern, Key, Value) -> + % Found end of file within the section. Append the option here. + [Key ++ " = " ++ Value | strip_empty_lines(SeenLines)]. + +reverse_and_add_newline([Line | Rest], Content) -> + reverse_and_add_newline(Rest, [Line, "\n", Content]); +reverse_and_add_newline([], Content) -> + Content. + +strip_empty_lines(["" | Rest]) -> + strip_empty_lines(Rest); +strip_empty_lines(All) -> + All. diff --git a/src/config/test/config_tests.erl b/src/config/test/config_tests.erl new file mode 100644 index 000000000..3d3ee9c94 --- /dev/null +++ b/src/config/test/config_tests.erl @@ -0,0 +1,830 @@ +% 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(config_tests). +-behaviour(config_listener). + +-export([ + handle_config_change/5, + handle_config_terminate/3 +]). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). + +-define(TIMEOUT, 4000). +-define(RESTART_TIMEOUT_IN_MILLISEC, 3000). + +-define(CONFIG_FIXTURESDIR, + filename:join([?BUILDDIR(), "src", "config", "test", "fixtures"]) +). + +-define(CONFIG_DEFAULT_TEST, + filename:join([?CONFIG_FIXTURESDIR, "config_default_test.ini"]) +). + +-define(CONFIG_FIXTURE_1, + filename:join([?CONFIG_FIXTURESDIR, "config_tests_1.ini"]) +). + +-define(CONFIG_FIXTURE_2, + filename:join([?CONFIG_FIXTURESDIR, "config_tests_2.ini"]) +). + +-define(CONFIG_DEFAULT_D, + filename:join([?CONFIG_FIXTURESDIR, "default.d"]) +). + +-define(CONFIG_LOCAL_D, + filename:join([?CONFIG_FIXTURESDIR, "local.d"]) +). + +-define(CONFIG_FIXTURE_TEMP, begin + FileName = filename:join([?TEMPDIR, "config_temp.ini"]), + {ok, Fd} = file:open(FileName, write), + ok = file:truncate(Fd), + ok = file:close(Fd), + FileName +end). + +-define(T(F), {erlang:fun_to_list(F), F}). +-define(FEXT(F), fun(_, _) -> F() end). + +setup() -> + setup(?CONFIG_CHAIN). + +setup({temporary, Chain}) -> + setup(Chain); +setup({persistent, Chain}) -> + setup(Chain ++ [?CONFIG_FIXTURE_TEMP]); +setup(Chain) -> + meck:new(couch_log), + meck:expect(couch_log, error, fun(_, _) -> ok end), + meck:expect(couch_log, notice, fun(_, _) -> ok end), + meck:expect(couch_log, debug, fun(_, _) -> ok end), + ok = application:set_env(config, ini_files, Chain), + test_util:start_applications([config]). + +setup_empty() -> + setup([]). + +setup_config_listener() -> + Apps = setup(), + Pid = spawn_config_listener(), + {Apps, Pid}. + +setup_config_notifier(Subscription) -> + Apps = setup(), + Pid = spawn_config_notifier(Subscription), + {Apps, Pid}. + +teardown({Apps, Pid}) when is_pid(Pid) -> + catch exit(Pid, kill), + teardown(Apps); +teardown(Apps) when is_list(Apps) -> + meck:unload(), + test_util:stop_applications(Apps). + +teardown(_, {Apps, Pid}) when is_pid(Pid) -> + catch exit(Pid, kill), + teardown(Apps); +teardown(_, Apps) -> + teardown(Apps). + +handle_config_change("remove_handler", _Key, _Value, _Persist, {_Pid, _State}) -> + remove_handler; +handle_config_change("update_state", Key, Value, Persist, {Pid, State}) -> + Pid ! {config_msg, {{"update_state", Key, Value, Persist}, State}}, + {ok, {Pid, Key}}; +handle_config_change("throw_error", _Key, _Value, _Persist, {_Pid, _State}) -> + throw(this_is_an_error); +handle_config_change(Section, Key, Value, Persist, {Pid, State}) -> + Pid ! {config_msg, {{Section, Key, Value, Persist}, State}}, + {ok, {Pid, State}}. + +handle_config_terminate(Self, Reason, {Pid, State}) -> + Pid ! {config_msg, {Self, Reason, State}}, + ok. + +config_get_test_() -> + { + "Config get tests", + { + foreach, + fun setup/0, + fun teardown/1, + [ + fun should_load_all_configs/0, + fun should_return_undefined_atom_on_missed_section/0, + fun should_return_undefined_atom_on_missed_option/0, + fun should_return_custom_default_value_on_missed_option/0, + fun should_only_return_default_on_missed_option/0, + fun should_fail_to_get_binary_value/0, + fun should_return_any_supported_default/0 + ] + } + }. + +config_set_test_() -> + { + "Config set tests", + { + foreach, + fun setup/0, + fun teardown/1, + [ + fun should_update_option/0, + fun should_create_new_section/0, + fun should_fail_to_set_binary_value/0 + ] + } + }. + +config_del_test_() -> + { + "Config deletion tests", + { + foreach, + fun setup/0, + fun teardown/1, + [ + fun should_return_undefined_atom_after_option_deletion/0, + fun should_be_ok_on_deleting_unknown_options/0 + ] + } + }. + +config_features_test_() -> + { + "Config features tests", + { + foreach, + fun setup/0, + fun teardown/1, + [ + {"enable", fun should_enable_features/0}, + {"disable", fun should_disable_features/0}, + {"restart config", fun should_keep_features_on_config_restart/0} + ] + } + }. + +config_override_test_() -> + { + "Configs overide tests", + { + foreachx, + fun setup/1, + fun teardown/2, + [ + {{temporary, [?CONFIG_DEFAULT_TEST]}, fun should_ensure_in_defaults/2}, + { + {temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_FIXTURE_1]}, + fun should_override_options/2 + }, + { + {temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_FIXTURE_2]}, + fun should_create_new_sections_on_override/2 + }, + { + {temporary, [ + ?CONFIG_DEFAULT_TEST, + ?CONFIG_FIXTURE_1, + ?CONFIG_FIXTURE_2 + ]}, + fun should_win_last_in_chain/2 + }, + { + {temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_DEFAULT_D]}, + fun should_read_default_d/2 + }, + {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_LOCAL_D]}, fun should_read_local_d/2}, + { + {temporary, [ + ?CONFIG_DEFAULT_TEST, + ?CONFIG_DEFAULT_D, + ?CONFIG_LOCAL_D + ]}, + fun should_read_default_and_local_d/2 + } + ] + } + }. + +config_persistent_changes_test_() -> + { + "Config persistent changes", + { + foreachx, + fun setup/1, + fun teardown/2, + [ + {{persistent, [?CONFIG_DEFAULT_TEST]}, fun should_write_changes/2}, + {{temporary, [?CONFIG_DEFAULT_TEST]}, fun should_ensure_default_wasnt_modified/2}, + { + {temporary, [?CONFIG_FIXTURE_TEMP]}, + fun should_ensure_written_to_last_config_in_chain/2 + } + ] + } + }. + +config_no_files_test_() -> + { + "Test config with no files", + { + foreach, + fun setup_empty/0, + fun teardown/1, + [ + fun should_ensure_that_no_ini_files_loaded/0, + fun should_create_non_persistent_option/0, + fun should_create_persistent_option/0 + ] + } + }. + +config_listener_behaviour_test_() -> + { + "Test config_listener behaviour", + { + foreach, + local, + fun setup_config_listener/0, + fun teardown/1, + [ + fun should_handle_value_change/1, + fun should_pass_correct_state_to_handle_config_change/1, + fun should_pass_correct_state_to_handle_config_terminate/1, + fun should_pass_subscriber_pid_to_handle_config_terminate/1, + fun should_not_call_handle_config_after_related_process_death/1, + fun should_remove_handler_when_requested/1, + fun should_remove_handler_when_pid_exits/1, + fun should_stop_monitor_on_error/1 + ] + } + }. + +config_notifier_behaviour_test_() -> + { + "Test config_notifier behaviour", + { + foreachx, + local, + fun setup_config_notifier/1, + fun teardown/2, + [ + {all, fun should_notify/2}, + {["section_foo"], fun should_notify/2}, + {[{"section_foo", "key_bar"}], fun should_notify/2}, + {["section_foo"], fun should_not_notify/2}, + {[{"section_foo", "key_bar"}], fun should_not_notify/2}, + {all, fun should_unsubscribe_when_subscriber_gone/2}, + {all, fun should_not_add_duplicate/2}, + {all, fun should_notify_on_config_reload/2}, + {all, fun should_notify_on_config_reload_flush/2} + ] + } + }. + +config_key_has_regex_test_() -> + { + "Test key with regex can be compiled and written to file", + { + foreach, + fun setup/0, + fun teardown/1, + [ + fun should_handle_regex_patterns_in_key/0 + ] + } + }. + +config_access_right_test_() -> + { + "Test config file access right", + { + foreach, + fun setup/0, + fun teardown/1, + [ + fun should_write_config_to_file/0, + fun should_delete_config_from_file/0, + fun should_not_write_config_to_file/0, + fun should_not_delete_config_from_file/0 + ] + } + }. + +should_write_config_to_file() -> + ?assertEqual(ok, config:set("admins", "foo", "500", true)). + +should_handle_regex_patterns_in_key() -> + ?assertEqual(ok, config:set("sect1", "pat||*", "true", true)), + ?assertEqual([{"pat||*", "true"}], config:get("sect1")). + +should_delete_config_from_file() -> + ?assertEqual(ok, config:delete("admins", "foo", true)). + +should_not_write_config_to_file() -> + meck:new(config_writer), + meck:expect(config_writer, save_to_file, fun(_, _) -> {error, eacces} end), + ?assertEqual({error, eacces}, config:set("admins", "foo", "500", true)), + meck:unload(config_writer). + +should_not_delete_config_from_file() -> + meck:new(config_writer), + meck:expect(config_writer, save_to_file, fun(_, _) -> {error, eacces} end), + ?assertEqual({error, eacces}, config:delete("admins", "foo", true)), + meck:unload(config_writer). + +should_load_all_configs() -> + ?assert(length(config:all()) > 0). + +should_return_undefined_atom_on_missed_section() -> + ?assertEqual(undefined, config:get("foo", "bar")). + +should_return_undefined_atom_on_missed_option() -> + ?assertEqual(undefined, config:get("httpd", "foo")). + +should_return_custom_default_value_on_missed_option() -> + ?assertEqual("bar", config:get("httpd", "foo", "bar")). + +should_only_return_default_on_missed_option() -> + ?assertEqual("0", config:get("httpd", "port", "bar")). + +should_fail_to_get_binary_value() -> + ?assertException(error, badarg, config:get(<<"a">>, <<"b">>, <<"c">>)). + +should_return_any_supported_default() -> + Values = [undefined, "list", true, false, 0.1, 1], + lists:map( + fun(V) -> + ?assertEqual(V, config:get(<<"foo">>, <<"bar">>, V)) + end, + Values + ). + +should_update_option() -> + ok = config:set("mock_log", "level", "severe", false), + ?assertEqual("severe", config:get("mock_log", "level")). + +should_create_new_section() -> + ?assertEqual(undefined, config:get("new_section", "bizzle")), + ?assertEqual(ok, config:set("new_section", "bizzle", "bang", false)), + ?assertEqual("bang", config:get("new_section", "bizzle")). + +should_fail_to_set_binary_value() -> + ?assertException( + error, + badarg, + config:set(<<"a">>, <<"b">>, <<"c">>, false) + ). + +should_return_undefined_atom_after_option_deletion() -> + ?assertEqual(ok, config:delete("mock_log", "level", false)), + ?assertEqual(undefined, config:get("mock_log", "level")). + +should_be_ok_on_deleting_unknown_options() -> + ?assertEqual(ok, config:delete("zoo", "boo", false)). + +should_ensure_in_defaults(_, _) -> + ?_test(begin + ?assertEqual("500", config:get("couchdb", "max_dbs_open")), + ?assertEqual("5986", config:get("httpd", "port")), + ?assertEqual(undefined, config:get("fizbang", "unicode")) + end). + +should_override_options(_, _) -> + ?_test(begin + ?assertEqual("10", config:get("couchdb", "max_dbs_open")), + ?assertEqual("4895", config:get("httpd", "port")) + end). + +should_read_default_d(_, _) -> + ?_test(begin + ?assertEqual("11", config:get("couchdb", "max_dbs_open")) + end). + +should_read_local_d(_, _) -> + ?_test(begin + ?assertEqual("12", config:get("couchdb", "max_dbs_open")) + end). + +should_read_default_and_local_d(_, _) -> + ?_test(begin + ?assertEqual("12", config:get("couchdb", "max_dbs_open")) + end). + +should_create_new_sections_on_override(_, _) -> + ?_test(begin + ?assertEqual("80", config:get("httpd", "port")), + ?assertEqual("normalized", config:get("fizbang", "unicode")) + end). + +should_win_last_in_chain(_, _) -> + ?_test(begin + ?assertEqual("80", config:get("httpd", "port")) + end). + +should_write_changes(_, _) -> + ?_test(begin + ?assertEqual("5986", config:get("httpd", "port")), + ?assertEqual(ok, config:set("httpd", "port", "8080")), + ?assertEqual("8080", config:get("httpd", "port")), + ?assertEqual(ok, config:delete("httpd", "bind_address", "8080")), + ?assertEqual(undefined, config:get("httpd", "bind_address")) + end). + +should_ensure_default_wasnt_modified(_, _) -> + ?_test(begin + ?assertEqual("5986", config:get("httpd", "port")), + ?assertEqual("127.0.0.1", config:get("httpd", "bind_address")) + end). + +should_ensure_written_to_last_config_in_chain(_, _) -> + ?_test(begin + ?assertEqual("8080", config:get("httpd", "port")), + ?assertEqual(undefined, config:get("httpd", "bind_address")) + end). + +should_ensure_that_no_ini_files_loaded() -> + ?assertEqual(0, length(config:all())). + +should_create_non_persistent_option() -> + ?_test(begin + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + ?assertEqual("80", config:get("httpd", "port")) + end). + +should_create_persistent_option() -> + ?_test(begin + ?assertEqual(ok, config:set("httpd", "bind_address", "127.0.0.1")), + ?assertEqual("127.0.0.1", config:get("httpd", "bind_address")) + end). + +should_handle_value_change({_Apps, Pid}) -> + ?_test(begin + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + ?assertMatch({{"httpd", "port", "80", false}, _}, getmsg(Pid)) + end). + +should_pass_correct_state_to_handle_config_change({_Apps, Pid}) -> + ?_test(begin + ?assertEqual(ok, config:set("update_state", "foo", "any", false)), + ?assertMatch({_, undefined}, getmsg(Pid)), + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + ?assertMatch({_, "foo"}, getmsg(Pid)) + end). + +should_pass_correct_state_to_handle_config_terminate({_Apps, Pid}) -> + ?_test(begin + ?assertEqual(ok, config:set("update_state", "foo", "any", false)), + ?assertMatch({_, undefined}, getmsg(Pid)), + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + ?assertMatch({_, "foo"}, getmsg(Pid)), + ?assertEqual(ok, config:set("remove_handler", "any", "any", false)), + ?assertEqual({Pid, remove_handler, "foo"}, getmsg(Pid)) + end). + +should_pass_subscriber_pid_to_handle_config_terminate({_Apps, Pid}) -> + ?_test(begin + ?assertEqual(ok, config:set("remove_handler", "any", "any", false)), + ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)) + end). + +should_not_call_handle_config_after_related_process_death({_Apps, Pid}) -> + ?_test(begin + ?assertEqual(ok, config:set("remove_handler", "any", "any", false)), + ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)), + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + Event = + receive + {config_msg, _} -> got_msg + after 250 -> no_msg + end, + ?assertEqual(no_msg, Event) + end). + +should_remove_handler_when_requested({_Apps, Pid}) -> + ?_test(begin + ?assertEqual(1, n_handlers()), + ?assertEqual(ok, config:set("remove_handler", "any", "any", false)), + ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)), + ?assertEqual(0, n_handlers()) + end). + +should_remove_handler_when_pid_exits({_Apps, Pid}) -> + ?_test(begin + ?assertEqual(1, n_handlers()), + + % Monitor the config_listener_mon process + {monitored_by, [Mon]} = process_info(Pid, monitored_by), + MonRef = erlang:monitor(process, Mon), + + % Kill the process synchronously + PidRef = erlang:monitor(process, Pid), + exit(Pid, kill), + receive + {'DOWN', PidRef, _, _, _} -> ok + after ?TIMEOUT -> + erlang:error({timeout, config_listener_death}) + end, + + % Wait for the config_listener_mon process to + % exit to indicate the handler has been removed. + receive + {'DOWN', MonRef, _, _, normal} -> ok + after ?TIMEOUT -> + erlang:error({timeout, config_listener_mon_death}) + end, + + ?assertEqual(0, n_handlers()) + end). + +should_stop_monitor_on_error({_Apps, Pid}) -> + ?_test(begin + ?assertEqual(1, n_handlers()), + + % Monitor the config_listener_mon process + {monitored_by, [Mon]} = process_info(Pid, monitored_by), + MonRef = erlang:monitor(process, Mon), + + % Have the process throw an error + ?assertEqual(ok, config:set("throw_error", "foo", "bar", false)), + + % Make sure handle_config_terminate is called + ?assertEqual({Pid, {error, this_is_an_error}, undefined}, getmsg(Pid)), + + % Wait for the config_listener_mon process to + % exit to indicate the handler has been removed + % due to an error + receive + {'DOWN', MonRef, _, _, shutdown} -> ok + after ?TIMEOUT -> + erlang:error({timeout, config_listener_mon_shutdown}) + end, + + ?assertEqual(0, n_handlers()) + end). + +should_notify(Subscription, {_Apps, Pid}) -> + { + to_string(Subscription), + ?_test(begin + ?assertEqual(ok, config:set("section_foo", "key_bar", "any", false)), + ?assertEqual({config_change, "section_foo", "key_bar", "any", false}, getmsg(Pid)), + ok + end) + }. + +should_not_notify([{Section, _}] = Subscription, {_Apps, Pid}) -> + { + to_string(Subscription), + ?_test(begin + ?assertEqual(ok, config:set(Section, "any", "any", false)), + ?assertError({timeout, config_msg}, getmsg(Pid)), + ok + end) + }; +should_not_notify(Subscription, {_Apps, Pid}) -> + { + to_string(Subscription), + ?_test(begin + ?assertEqual(ok, config:set("any", "any", "any", false)), + ?assertError({timeout, config_msg}, getmsg(Pid)), + ok + end) + }. + +should_unsubscribe_when_subscriber_gone(_Subscription, {_Apps, Pid}) -> + ?_test(begin + ?assertEqual(1, n_notifiers()), + + ?assert(is_process_alive(Pid)), + + % Monitor subscriber process + MonRef = erlang:monitor(process, Pid), + + exit(Pid, kill), + + % Wait for the subscriber process to exit + receive + {'DOWN', MonRef, _, _, _} -> ok + after ?TIMEOUT -> + erlang:error({timeout, config_notifier_shutdown}) + end, + + ?assertNot(is_process_alive(Pid)), + + ?assertEqual(0, n_notifiers()), + ok + end). + +should_not_add_duplicate(_, _) -> + ?_test(begin + %% spawned from setup + ?assertEqual(1, n_notifiers()), + + ?assertMatch(ok, config:subscribe_for_changes(all)), + + ?assertEqual(2, n_notifiers()), + + ?assertMatch(ok, config:subscribe_for_changes(all)), + + ?assertEqual(2, n_notifiers()), + ok + end). + +should_enable_features() -> + [config:disable_feature(F) || F <- config:features()], + ?assertEqual([], config:features()), + + ?assertEqual(ok, config:enable_feature(snek)), + ?assertEqual([snek], config:features()), + + ?assertEqual(ok, config:enable_feature(snek)), + ?assertEqual([snek], config:features()), + + ?assertEqual(ok, config:enable_feature(dogo)), + ?assertEqual([dogo, snek], config:features()). + +should_disable_features() -> + [config:disable_feature(F) || F <- config:features()], + ?assertEqual([], config:features()), + + config:enable_feature(snek), + ?assertEqual([snek], config:features()), + + ?assertEqual(ok, config:disable_feature(snek)), + ?assertEqual([], config:features()), + + ?assertEqual(ok, config:disable_feature(snek)), + ?assertEqual([], config:features()). + +should_keep_features_on_config_restart() -> + [config:disable_feature(F) || F <- config:features()], + ?assertEqual([], config:features()), + + config:enable_feature(snek), + ?assertEqual([snek], config:features()), + with_process_restart(config), + ?assertEqual([snek], config:features()). + +should_notify_on_config_reload(Subscription, {_Apps, Pid}) -> + { + to_string(Subscription), + ?_test(begin + ?assertEqual(ok, config:set("section_foo", "key_bar", "any", true)), + ?assertEqual({config_change, "section_foo", "key_bar", "any", true}, getmsg(Pid)), + ?assertEqual(ok, config:set("section_foo", "key_bar", "not_any", false)), + ?assertEqual({config_change, "section_foo", "key_bar", "not_any", false}, getmsg(Pid)), + ?assertEqual(ok, config:reload()), + ?assertEqual({config_change, "section_foo", "key_bar", "any", true}, getmsg(Pid)), + ok + end) + }. + +should_notify_on_config_reload_flush(Subscription, {_Apps, Pid}) -> + { + to_string(Subscription), + ?_test(begin + ?assertEqual(ok, config:set("section_foo_temp", "key_bar", "any", false)), + ?assertEqual({config_change, "section_foo_temp", "key_bar", "any", false}, getmsg(Pid)), + ?assertEqual(ok, config:reload()), + ?assertEqual( + {config_change, "section_foo_temp", "key_bar", deleted, true}, getmsg(Pid) + ), + ok + end) + }. + +spawn_config_listener() -> + Self = self(), + Pid = erlang:spawn(fun() -> + ok = config:listen_for_changes(?MODULE, {self(), undefined}), + Self ! registered, + loop(undefined) + end), + receive + registered -> ok + after ?TIMEOUT -> + erlang:error({timeout, config_handler_register}) + end, + Pid. + +spawn_config_notifier(Subscription) -> + Self = self(), + Pid = erlang:spawn(fun() -> + ok = config:subscribe_for_changes(Subscription), + Self ! registered, + loop(undefined) + end), + receive + registered -> ok + after ?TIMEOUT -> + erlang:error({timeout, config_handler_register}) + end, + Pid. + +loop(undefined) -> + receive + {config_msg, _} = Msg -> + loop(Msg); + {config_change, _, _, _, _} = Msg -> + loop({config_msg, Msg}); + {get_msg, _, _} = Msg -> + loop(Msg); + Msg -> + erlang:error({invalid_message, Msg}) + end; +loop({get_msg, From, Ref}) -> + receive + {config_msg, _} = Msg -> + From ! {Ref, Msg}; + {config_change, _, _, _, _} = Msg -> + From ! {Ref, Msg}; + Msg -> + erlang:error({invalid_message, Msg}) + end, + loop(undefined); +loop({config_msg, _} = Msg) -> + receive + {get_msg, From, Ref} -> + From ! {Ref, Msg}; + Msg -> + erlang:error({invalid_message, Msg}) + end, + loop(undefined). + +getmsg(Pid) -> + Ref = erlang:make_ref(), + Pid ! {get_msg, self(), Ref}, + receive + {Ref, {config_msg, Msg}} -> Msg + after ?TIMEOUT -> + erlang:error({timeout, config_msg}) + end. + +n_handlers() -> + Handlers = gen_event:which_handlers(config_event), + length([Pid || {config_listener, {?MODULE, Pid}} <- Handlers]). + +n_notifiers() -> + Handlers = gen_event:which_handlers(config_event), + length([Pid || {config_notifier, Pid} <- Handlers]). + +to_string(Term) -> + lists:flatten(io_lib:format("~p", [Term])). + +with_process_restart(Name) -> + ok = stop_sync(whereis(Name), ?TIMEOUT), + Now = now_us(), + wait_process_restart( + Name, ?RESTART_TIMEOUT_IN_MILLISEC * 1000, 50, Now, Now + ). + +wait_process_restart(_Name, Timeout, _Delay, Started, Prev) when + Prev - Started > Timeout +-> + timeout; +wait_process_restart(Name, Timeout, Delay, Started, _Prev) -> + case whereis(Name) of + undefined -> + ok = timer:sleep(Delay), + wait_process_restart(Name, Timeout, Delay, Started, now_us()); + Pid -> + Pid + end. + +stop_sync(Pid, Timeout) when is_pid(Pid) -> + MRef = erlang:monitor(process, Pid), + try + begin + catch unlink(Pid), + exit(Pid, kill), + receive + {'DOWN', MRef, _, _, _} -> + ok + after Timeout -> + timeout + end + end + after + erlang:demonitor(MRef, [flush]) + end; +stop_sync(_, _) -> + error(badarg). + +now_us() -> + {MegaSecs, Secs, MicroSecs} = os:timestamp(), + (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs. diff --git a/src/config/test/fixtures/config_default_test.ini b/src/config/test/fixtures/config_default_test.ini new file mode 100644 index 000000000..bac4405e5 --- /dev/null +++ b/src/config/test/fixtures/config_default_test.ini @@ -0,0 +1,23 @@ +; 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. + +[couchdb] +max_dbs_open = 500 + +[httpd] +port = 5986 +bind_address = 127.0.0.1 diff --git a/src/config/test/fixtures/config_tests_1.ini b/src/config/test/fixtures/config_tests_1.ini new file mode 100644 index 000000000..55451dade --- /dev/null +++ b/src/config/test/fixtures/config_tests_1.ini @@ -0,0 +1,22 @@ +; 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. + +[couchdb] +max_dbs_open=10 + +[httpd] +port=4895 diff --git a/src/config/test/fixtures/config_tests_2.ini b/src/config/test/fixtures/config_tests_2.ini new file mode 100644 index 000000000..5f46357f5 --- /dev/null +++ b/src/config/test/fixtures/config_tests_2.ini @@ -0,0 +1,22 @@ +; 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. + +[httpd] +port = 80 + +[fizbang] +unicode = normalized diff --git a/src/config/test/fixtures/default.d/extra.ini b/src/config/test/fixtures/default.d/extra.ini new file mode 100644 index 000000000..fda68b32c --- /dev/null +++ b/src/config/test/fixtures/default.d/extra.ini @@ -0,0 +1,19 @@ +; 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. + +[couchdb] +max_dbs_open=11 diff --git a/src/config/test/fixtures/local.d/extra.ini b/src/config/test/fixtures/local.d/extra.ini new file mode 100644 index 000000000..d6a6d4661 --- /dev/null +++ b/src/config/test/fixtures/local.d/extra.ini @@ -0,0 +1,19 @@ +; 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. + +[couchdb] +max_dbs_open=12 |