Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Enable CouchDB to manage OS process daemons.

This is a simple feature that allows useres to configure CouchDB
so that it maintains a given OS level process alive. If the process
dies for any reason, CouchDB will restart it. If the process restarts
too often, then CouchDB will mark it has halted and not attempt
to restart it. The default max restart rate is three times in the
last five seconds. These parameters are adjustable.

Commands that are started in this manner will have access to a simple
API over stdio to request configuration parameters or to add log
statements to CouchDB's logs.

To configure an OS process as a CouchDB os_daemon, create a section
in your local.ini like such:

    [os_daemons]
    daemon_name = /path/to/command -with args

This will make CouchDB bring up the command and attempt to keep it
alive. To request a configuration parameter, an os_daemon can write
a simple JSON message to stdout like such:

    ["get", "os_daemons"]\n

which would return:

    {"daemon_name": "/path/to/command -with args"}

Or:

    ["get", "os_daemons", "daemon_name"]\n

which would return:

    "/path/to/command -with args"

There's no restriction on what configuration variables are visible.
There's also no method for altering the configuration.

If you would like your OS daemon to be restarted in the event that
the configuration changes, you can send the following messages:

    ["register", $(SECTION)]\n

When anyting in that section changes, your OS process will be
rebooted so it can pick up the new configuration settings. If you
want to listen for changes on a specific key, you can send something
like:

    ["register", $(SECTION), $(KEY)]\n

In this case, CouchDB will only restart your daemon if that exact
section/key pair changes, instead of anything in that entire section.

Logging commands look like:

    ["log", $(JSON_MESSAGE)]\n

Where $(JSON_MESSAGE) is arbitrary JSON data. These messages are
logged at the 'info' level. If you want to log at a different level
you can pass messages like such:

    ["log", $(JSON_MESSAGE), {"level": $(LEVEL)}]\n

Where $(LEVEL) is one of "debug", "info", or "error".

When implementing a daemon process to be managed by CouchDB you
should remember to use a method like checking the parent process
id or if stdin has been closed. These flags can tell you if
your daemon process has been orphaned so you can exit cleanly.



git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@1031875 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information...
commit c8785113522b8486fab0ba53f2043d94e9b1507f 1 parent c82960c
Paul J. Davis authored November 05, 2010
18  etc/couchdb/default.ini.tpl.in
@@ -48,11 +48,6 @@ javascript = %bindir%/%couchjs_command_name% %localbuilddatadir%/server/main.js
48 48
 reduce_limit = true
49 49
 os_process_limit = 25
50 50
 
51  
-; enable external as an httpd handler, then link it with commands here.
52  
-; note, this api is still under consideration.
53  
-; [external]
54  
-; mykey = /path/to/mycommand
55  
-
56 51
 [daemons]
57 52
 view_manager={couch_view, start_link, []}
58 53
 external_manager={couch_external_manager, start_link, []}
@@ -65,6 +60,7 @@ uuids={couch_uuids, start, []}
65 60
 auth_cache={couch_auth_cache, start_link, []}
66 61
 rep_db_changes_listener={couch_rep_db_listener, start_link, []}
67 62
 vhosts={couch_httpd_vhost, start_link, []}
  63
+os_daemons={couch_os_daemons, start_link, []}
68 64
 
69 65
 [httpd_global_handlers]
70 66
 / = {couch_httpd_misc_handlers, handle_welcome_req, <<"Welcome">>}
@@ -103,6 +99,18 @@ _info = {couch_httpd_db,   handle_design_info_req}
103 99
 _rewrite = {couch_httpd_rewrite, handle_rewrite_req}
104 100
 _update = {couch_httpd_show, handle_doc_update_req}
105 101
 
  102
+; enable external as an httpd handler, then link it with commands here.
  103
+; note, this api is still under consideration.
  104
+; [external]
  105
+; mykey = /path/to/mycommand
  106
+
  107
+; Here you can setup commands for CouchDB to manage
  108
+; while it is alive. It will attempt to keep each command
  109
+; alive if it exits.
  110
+; [os_daemons]
  111
+; some_daemon_name = /path/to/script -with args
  112
+
  113
+
106 114
 [uuids]
107 115
 ; Known algorithms:
108 116
 ;   random - 128 bits of random awesome
6  etc/couchdb/local.ini
@@ -33,6 +33,12 @@
33 33
 ; enable SSL support by uncommenting the following line and supply the PEM's below.
34 34
 ; httpsd = {couch_httpd, start_link, [https]}
35 35
 
  36
+[os_daemons]
  37
+; For any commands listed here, CouchDB will attempt to ensure that
  38
+; the process remains alive while CouchDB runs as well as shut them
  39
+; down when CouchDB exits.
  40
+;foo = /path/to/command -with args
  41
+
36 42
 [ssl]
37 43
 ;cert_file = /full/path/to/server_cert.pem
38 44
 ;key_file = /full/path/to/server_key.pem
2  src/couchdb/Makefile.am
@@ -56,6 +56,7 @@ source_files = \
56 56
     couch_key_tree.erl \
57 57
     couch_log.erl \
58 58
     couch_native_process.erl \
  59
+    couch_os_daemons.erl \
59 60
     couch_os_process.erl \
60 61
     couch_query_servers.erl \
61 62
     couch_ref_counter.erl \
@@ -116,6 +117,7 @@ compiled_files = \
116 117
     couch_key_tree.beam \
117 118
     couch_log.beam \
118 119
     couch_native_process.beam \
  120
+    couch_os_daemons.beam \
119 121
     couch_os_process.beam \
120 122
     couch_query_servers.beam \
121 123
     couch_ref_counter.beam \
359  src/couchdb/couch_os_daemons.erl
... ...
@@ -0,0 +1,359 @@
  1
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  2
+% use this file except in compliance with the License. You may obtain a copy of
  3
+% the License at
  4
+%
  5
+%   http://www.apache.org/licenses/LICENSE-2.0
  6
+%
  7
+% Unless required by applicable law or agreed to in writing, software
  8
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10
+% License for the specific language governing permissions and limitations under
  11
+% the License.
  12
+-module(couch_os_daemons).
  13
+-behaviour(gen_server).
  14
+
  15
+-export([start_link/0, info/0, info/1, config_change/2]).
  16
+
  17
+-export([init/1, terminate/2, code_change/3]).
  18
+-export([handle_call/3, handle_cast/2, handle_info/2]).
  19
+
  20
+-include("couch_db.hrl").
  21
+
  22
+-record(daemon, {
  23
+    port,
  24
+    name,
  25
+    cmd,
  26
+    kill,
  27
+    status=running,
  28
+    cfg_patterns=[],
  29
+    errors=[],
  30
+    buf=[]
  31
+}).
  32
+
  33
+-define(PORT_OPTIONS, [stream, {line, 1024}, binary, exit_status, hide]).
  34
+-define(TIMEOUT, 5000).
  35
+
  36
+start_link() ->
  37
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
  38
+
  39
+info() ->
  40
+    info([]).
  41
+
  42
+info(Options) ->
  43
+    gen_server:call(?MODULE, {daemon_info, Options}).
  44
+
  45
+config_change(Section, Key) ->
  46
+    gen_server:cast(?MODULE, {config_change, Section, Key}).
  47
+
  48
+init(_) ->
  49
+    process_flag(trap_exit, true),
  50
+    ok = couch_config:register(fun couch_os_daemons:config_change/2),
  51
+    Table = ets:new(?MODULE, [protected, set, {keypos, #daemon.port}]),
  52
+    reload_daemons(Table),
  53
+    {ok, Table}.
  54
+
  55
+terminate(_Reason, Table) ->
  56
+    [stop_port(D) || D <- ets:tab2list(Table)],
  57
+    ok.
  58
+
  59
+handle_call({daemon_info, Options}, _From, Table) when is_list(Options) ->
  60
+    case lists:member(table, Options) of
  61
+        true ->
  62
+            {reply, {ok, ets:tab2list(Table)}, Table};
  63
+        _ ->
  64
+            {reply, {ok, Table}, Table}
  65
+    end;
  66
+handle_call(Msg, From, Table) ->
  67
+    ?LOG_ERROR("Unknown call message to ~p from ~p: ~p", [?MODULE, From, Msg]),
  68
+    {stop, error, Table}.
  69
+
  70
+handle_cast({config_change, Sect, Key}, Table) ->
  71
+    restart_daemons(Table, Sect, Key),
  72
+    case Sect of
  73
+        "os_daemons" -> reload_daemons(Table);
  74
+        _ -> ok
  75
+    end,
  76
+    {noreply, Table};
  77
+handle_cast(stop, Table) ->
  78
+    {stop, normal, Table};
  79
+handle_cast(Msg, Table) ->
  80
+    ?LOG_ERROR("Unknown cast message to ~p: ~p", [?MODULE, Msg]),
  81
+    {stop, error, Table}.
  82
+
  83
+handle_info({'EXIT', Port, Reason}, Table) ->
  84
+    case ets:lookup(Table, Port) of
  85
+        [] ->
  86
+            ?LOG_INFO("Port ~p exited after stopping: ~p~n", [Port, Reason]);
  87
+        [#daemon{status=stopping}] ->
  88
+            true = ets:delete(Table, Port);
  89
+        [#daemon{name=Name, status=restarting, errors=Errs}=D] ->
  90
+            ?LOG_INFO("Daemon ~P restarting after config change.", [Name]),
  91
+            true = ets:delete(Table, Port),
  92
+            {ok, Port2} = start_port(D#daemon.cmd),
  93
+            true = ets:insert(Table, D#daemon{
  94
+                port=Port2, status=running, kill=undefined, errors=Errs, buf=[]
  95
+            });
  96
+        [#daemon{name=Name, status=halted}] ->
  97
+            ?LOG_ERROR("Halted daemon process: ~p", [Name]);
  98
+        [D] ->
  99
+            ?LOG_ERROR("Invalid port state at exit: ~p", [D])
  100
+    end,
  101
+    {noreply, Table};
  102
+handle_info({Port, closed}, Table) ->
  103
+    handle_info({Port, {exit_status, closed}}, Table);
  104
+handle_info({Port, {exit_status, Status}}, Table) ->
  105
+    case ets:lookup(Table, Port) of
  106
+        [] ->
  107
+            ?LOG_ERROR("Unknown port ~p exiting ~p", [Port, Status]),
  108
+            {stop, {error, unknown_port_died, Status}, Table};
  109
+        [#daemon{name=Name, status=restarting, errors=Errors}=D] ->
  110
+            ?LOG_INFO("Daemon ~P restarting after config change.", [Name]),
  111
+            true = ets:delete(Table, Port),
  112
+            {ok, Port2} = start_port(D#daemon.cmd),
  113
+            true = ets:insert(Table, D#daemon{
  114
+                port=Port2, kill=undefined, errors=Errors, buf=[]
  115
+            }),
  116
+            {noreply, Table};
  117
+        [#daemon{status=stopping}=D] ->
  118
+            % The configuration changed and this daemon is no
  119
+            % longer needed.
  120
+            ?LOG_DEBUG("Port ~p shut down.", [D#daemon.name]),
  121
+            true = ets:delete(Table, Port),
  122
+            {noreply, Table};
  123
+        [D] ->
  124
+            % Port died for unknown reason. Check to see if it's
  125
+            % died too many times or if we should boot it back up.
  126
+            case should_halt([now() | D#daemon.errors]) of
  127
+                {true, _} ->
  128
+                    % Halting the process. We won't try and reboot
  129
+                    % until the configuration changes.
  130
+                    Fmt = "Daemon ~p halted with exit_status ~p",
  131
+                    ?LOG_ERROR(Fmt, [D#daemon.name, Status]),
  132
+                    D2 = D#daemon{status=halted, errors=nil, buf=nil},
  133
+                    true = ets:insert(Table, D2),
  134
+                    {noreply, Table};
  135
+                {false, Errors} ->
  136
+                    % We're guessing it was a random error, this daemon
  137
+                    % has behaved so we'll give it another chance.
  138
+                    Fmt = "Daemon ~p is being rebooted after exit_status ~p",
  139
+                    ?LOG_INFO(Fmt, [D#daemon.name, Status]),
  140
+                    true = ets:delete(Table, Port),
  141
+                    {ok, Port2} = start_port(D#daemon.cmd),
  142
+                    true = ets:insert(Table, D#daemon{
  143
+                        port=Port2, kill=undefined, errors=Errors, buf=[]
  144
+                    }),
  145
+                    {noreply, Table}
  146
+            end;
  147
+        _Else ->
  148
+            throw(error)
  149
+    end;
  150
+handle_info({Port, {data, {noeol, Data}}}, Table) ->
  151
+    [#daemon{buf=Buf}=D] = ets:lookup(Table, Port),
  152
+    true = ets:insert(Table, D#daemon{buf=[Data | Buf]}),
  153
+    {noreply, Table};
  154
+handle_info({Port, {data, {eol, Data}}}, Table) ->
  155
+    [#daemon{buf=Buf}=D] = ets:lookup(Table, Port),
  156
+    Line = lists:reverse(Buf, Data),
  157
+    % The first line echoed back is the kill command
  158
+    % for when we go to get rid of the port. Lines after
  159
+    % that are considered part of the stdio API.
  160
+    case D#daemon.kill of
  161
+        undefined ->
  162
+            true = ets:insert(Table, D#daemon{kill=?b2l(Line), buf=[]});
  163
+        _Else ->
  164
+            D2 = case (catch ?JSON_DECODE(Line)) of
  165
+                {invalid_json, Rejected} ->
  166
+                    ?LOG_ERROR("Ignoring OS daemon request: ~p", [Rejected]),
  167
+                    D;
  168
+                JSON ->
  169
+                    {ok, D3} = handle_port_message(D, JSON),
  170
+                    D3
  171
+            end,
  172
+            true = ets:insert(Table, D2#daemon{buf=[]})
  173
+    end,
  174
+    {noreply, Table};
  175
+handle_info({Port, Error}, Table) ->
  176
+    ?LOG_ERROR("Unexpectd message from port ~p: ~p", [Port, Error]),
  177
+    stop_port(Port),
  178
+    [D] = ets:lookup(Table, Port),
  179
+    true = ets:insert(Table, D#daemon{status=restarting, buf=nil}),
  180
+    {noreply, Table};
  181
+handle_info(Msg, Table) ->
  182
+    ?LOG_ERROR("Unexpected info message to ~p: ~p", [?MODULE, Msg]),
  183
+    {stop, error, Table}.
  184
+
  185
+code_change(_OldVsn, State, _Extra) ->
  186
+    {ok, State}.
  187
+
  188
+% Internal API
  189
+
  190
+%
  191
+% Port management helpers
  192
+%
  193
+
  194
+start_port(Command) ->
  195
+    PrivDir = couch_util:priv_dir(),
  196
+    Spawnkiller = filename:join(PrivDir, "couchspawnkillable"),
  197
+    Port = open_port({spawn, Spawnkiller ++ " " ++ Command}, ?PORT_OPTIONS),
  198
+    {ok, Port}.
  199
+
  200
+
  201
+stop_port(#daemon{port=Port, kill=undefined}=D) ->
  202
+    ?LOG_ERROR("Stopping daemon without a kill command: ~p", [D#daemon.name]),
  203
+    catch port_close(Port);
  204
+stop_port(#daemon{port=Port}=D) ->
  205
+    ?LOG_DEBUG("Stopping daemon: ~p", [D#daemon.name]),
  206
+    os:cmd(D#daemon.kill),
  207
+    catch port_close(Port).
  208
+
  209
+
  210
+handle_port_message(#daemon{port=Port}=Daemon, [<<"get">>, Section]) ->
  211
+    KVs = couch_config:get(Section),
  212
+    Data = lists:map(fun({K, V}) -> {?l2b(K), ?l2b(V)} end, KVs),
  213
+    JsonData = ?JSON_ENCODE({Data}),
  214
+    port_command(Port, JsonData ++ "\n"),
  215
+    {ok, Daemon};
  216
+handle_port_message(#daemon{port=Port}=Daemon, [<<"get">>, Section, Key]) ->
  217
+    Value = couch_config:get(Section, Key, null),
  218
+    port_command(Port, ?JSON_ENCODE(?l2b(Value)) ++ "\n"),
  219
+    {ok, Daemon};
  220
+handle_port_message(Daemon, [<<"register">>, Sec]) when is_binary(Sec) ->
  221
+    Patterns = lists:usort(Daemon#daemon.cfg_patterns ++ [{?b2l(Sec)}]),
  222
+    {ok, Daemon#daemon{cfg_patterns=Patterns}};
  223
+handle_port_message(Daemon, [<<"register">>, Sec, Key])
  224
+                        when is_binary(Sec) andalso is_binary(Key) ->
  225
+    Pattern = {?b2l(Sec), ?b2l(Key)},
  226
+    Patterns = lists:usort(Daemon#daemon.cfg_patterns ++ [Pattern]),
  227
+    {ok, Daemon#daemon{cfg_patterns=Patterns}};
  228
+handle_port_message(#daemon{name=Name}=Daemon, [<<"log">>, Msg]) ->
  229
+    handle_log_message(Name, Msg, <<"info">>),
  230
+    {ok, Daemon};
  231
+handle_port_message(#daemon{name=Name}=Daemon, [<<"log">>, Msg, {Opts}]) ->
  232
+    Level = couch_util:get_value(<<"level">>, Opts, <<"info">>),
  233
+    handle_log_message(Name, Msg, Level),
  234
+    {ok, Daemon};
  235
+handle_port_message(#daemon{name=Name}=Daemon, Else) ->
  236
+    ?LOG_ERROR("Daemon ~p made invalid request: ~p", [Name, Else]),
  237
+    {ok, Daemon}.
  238
+
  239
+
  240
+handle_log_message(Name, Msg, _Level) when not is_binary(Msg) ->
  241
+    ?LOG_ERROR("Invalid log message from daemon ~p: ~p", [Name, Msg]);
  242
+handle_log_message(Name, Msg, <<"debug">>) ->
  243
+    ?LOG_DEBUG("Daemon ~p :: ~s", [Name, ?b2l(Msg)]);
  244
+handle_log_message(Name, Msg, <<"info">>) ->
  245
+    ?LOG_INFO("Daemon ~p :: ~s", [Name, ?b2l(Msg)]);
  246
+handle_log_message(Name, Msg, <<"error">>) ->
  247
+    ?LOG_ERROR("Daemon: ~p :: ~s", [Name, ?b2l(Msg)]);
  248
+handle_log_message(Name, Msg, Level) ->
  249
+    ?LOG_ERROR("Invalid log level from daemon: ~p", [Level]),
  250
+    ?LOG_INFO("Daemon: ~p :: ~s", [Name, ?b2l(Msg)]).
  251
+
  252
+%
  253
+% Daemon management helpers
  254
+%
  255
+
  256
+reload_daemons(Table) ->
  257
+    % List of daemons we want to have running.
  258
+    Configured = lists:sort(couch_config:get("os_daemons")),
  259
+    
  260
+    % Remove records for daemons that were halted.
  261
+    MSpecHalted = #daemon{name='$1', cmd='$2', status=halted, _='_'},
  262
+    Halted = lists:sort([{N, C} || [N, C] <- ets:match(Table, MSpecHalted)]),
  263
+    ok = stop_os_daemons(Table, find_to_stop(Configured, Halted, [])),
  264
+    
  265
+    % Stop daemons that are running
  266
+    % Start newly configured daemons
  267
+    MSpecRunning = #daemon{name='$1', cmd='$2', status=running, _='_'},
  268
+    Running = lists:sort([{N, C} || [N, C] <- ets:match(Table, MSpecRunning)]),
  269
+    ok = stop_os_daemons(Table, find_to_stop(Configured, Running, [])),
  270
+    ok = boot_os_daemons(Table, find_to_boot(Configured, Running, [])),
  271
+    ok.
  272
+
  273
+
  274
+restart_daemons(Table, Sect, Key) ->
  275
+    restart_daemons(Table, Sect, Key, ets:first(Table)).
  276
+
  277
+restart_daemons(_, _, _, '$end_of_table') ->
  278
+    ok;
  279
+restart_daemons(Table, Sect, Key, Port) ->
  280
+    [D] = ets:lookup(Table, Port),
  281
+    HasSect = lists:member({Sect}, D#daemon.cfg_patterns),
  282
+    HasKey = lists:member({Sect, Key}, D#daemon.cfg_patterns),
  283
+    case HasSect or HasKey of
  284
+        true ->
  285
+            stop_port(D),
  286
+            D2 = D#daemon{status=restarting, buf=nil},
  287
+            true = ets:insert(Table, D2);
  288
+        _ ->
  289
+            ok
  290
+    end,
  291
+    restart_daemons(Table, Sect, Key, ets:next(Table, Port)).
  292
+    
  293
+
  294
+stop_os_daemons(_Table, []) ->
  295
+    ok;
  296
+stop_os_daemons(Table, [{Name, Cmd} | Rest]) ->
  297
+    [[Port]] = ets:match(Table, #daemon{port='$1', name=Name, cmd=Cmd, _='_'}),
  298
+    [D] = ets:lookup(Table, Port),
  299
+    case D#daemon.status of
  300
+        halted ->
  301
+            ets:delete(Table, Port);
  302
+        _ ->
  303
+            stop_port(D),
  304
+            D2 = D#daemon{status=stopping, errors=nil, buf=nil},
  305
+            true = ets:insert(Table, D2)
  306
+    end,
  307
+    stop_os_daemons(Table, Rest).
  308
+    
  309
+boot_os_daemons(_Table, []) ->
  310
+    ok;
  311
+boot_os_daemons(Table, [{Name, Cmd} | Rest]) ->
  312
+    {ok, Port} = start_port(Cmd),
  313
+    true = ets:insert(Table, #daemon{port=Port, name=Name, cmd=Cmd}),
  314
+    boot_os_daemons(Table, Rest).
  315
+    
  316
+% Elements unique to the configured set need to be booted.
  317
+find_to_boot([], _Rest, Acc) ->
  318
+    % Nothing else configured.
  319
+    Acc;
  320
+find_to_boot([D | R1], [D | R2], Acc) ->
  321
+    % Elements are equal, daemon already running.
  322
+    find_to_boot(R1, R2, Acc);
  323
+find_to_boot([D1 | R1], [D2 | _]=A2, Acc) when D1 < D2 ->
  324
+    find_to_boot(R1, A2, [D1 | Acc]);
  325
+find_to_boot(A1, [_ | R2], Acc) ->
  326
+    find_to_boot(A1, R2, Acc);
  327
+find_to_boot(Rest, [], Acc) ->
  328
+    % No more candidates for already running. Boot all.
  329
+    Rest ++ Acc.
  330
+
  331
+% Elements unique to the running set need to be killed.
  332
+find_to_stop([], Rest, Acc) ->
  333
+    % The rest haven't been found, so they must all
  334
+    % be ready to die.
  335
+    Rest ++ Acc;
  336
+find_to_stop([D | R1], [D | R2], Acc) ->
  337
+    % Elements are equal, daemon already running.
  338
+    find_to_stop(R1, R2, Acc);
  339
+find_to_stop([D1 | R1], [D2 | _]=A2, Acc) when D1 < D2 ->
  340
+    find_to_stop(R1, A2, Acc);
  341
+find_to_stop(A1, [D2 | R2], Acc) ->
  342
+    find_to_stop(A1, R2, [D2 | Acc]);
  343
+find_to_stop(_, [], Acc) ->
  344
+    % No more running daemons to worry about.
  345
+    Acc.
  346
+
  347
+should_halt(Errors) ->
  348
+    RetryTimeCfg = couch_config:get("os_daemon_settings", "retry_time", "5"),
  349
+    RetryTime = list_to_integer(RetryTimeCfg),
  350
+
  351
+    Now = now(),
  352
+    RecentErrors = lists:filter(fun(Time) ->
  353
+        timer:now_diff(Now, Time) =< RetryTime * 1000000
  354
+    end, Errors),
  355
+
  356
+    RetryCfg = couch_config:get("os_daemon_settings", "max_retries", "3"),
  357
+    Retries = list_to_integer(RetryCfg),
  358
+
  359
+    {length(RecentErrors) >= Retries, RecentErrors}.
26  test/etap/170-os-daemons.es
... ...
@@ -0,0 +1,26 @@
  1
+#! /usr/bin/env escript
  2
+
  3
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  4
+% use this file except in compliance with the License. You may obtain a copy of
  5
+% the License at
  6
+%
  7
+%   http://www.apache.org/licenses/LICENSE-2.0
  8
+%
  9
+% Unless required by applicable law or agreed to in writing, software
  10
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12
+% License for the specific language governing permissions and limitations under
  13
+% the License.
  14
+
  15
+loop() ->
  16
+    loop(io:read("")).
  17
+
  18
+loop({ok, _}) ->
  19
+    loop(io:read(""));
  20
+loop(eof) ->
  21
+    stop;
  22
+loop({error, Reason}) ->
  23
+    throw({error, Reason}).
  24
+
  25
+main([]) ->
  26
+    loop().
114  test/etap/170-os-daemons.t
... ...
@@ -0,0 +1,114 @@
  1
+#!/usr/bin/env escript
  2
+%% -*- erlang -*-
  3
+
  4
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5
+% use this file except in compliance with the License.  You may obtain a copy of
  6
+% the License at
  7
+%
  8
+%   http://www.apache.org/licenses/LICENSE-2.0
  9
+%
  10
+% Unless required by applicable law or agreed to in writing, software
  11
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
  13
+% License for the specific language governing permissions and limitations under
  14
+% the License.
  15
+
  16
+-record(daemon, {
  17
+    port,
  18
+    name,
  19
+    cmd,
  20
+    kill,
  21
+    status=running,
  22
+    cfg_patterns=[],
  23
+    errors=[],
  24
+    buf=[]
  25
+}).
  26
+
  27
+config_files() ->
  28
+    lists:map(fun test_util:build_file/1, [
  29
+        "etc/couchdb/default_dev.ini"
  30
+    ]).
  31
+
  32
+daemon_cmd() ->
  33
+    test_util:source_file("test/etap/170-os-daemons.es").
  34
+
  35
+main(_) ->
  36
+    test_util:init_code_path(),
  37
+
  38
+    etap:plan(49),
  39
+    case (catch test()) of
  40
+        ok ->
  41
+            etap:end_tests();
  42
+        Other ->
  43
+            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
  44
+            etap:bail(Other)
  45
+    end,
  46
+    ok.
  47
+
  48
+test() ->
  49
+    couch_config:start_link(config_files()),
  50
+    couch_os_daemons:start_link(),
  51
+
  52
+    etap:diag("Daemons boot after configuration added."),
  53
+    couch_config:set("os_daemons", "foo", daemon_cmd(), false),
  54
+    timer:sleep(1000),
  55
+    
  56
+    {ok, [D1]} = couch_os_daemons:info([table]),
  57
+    check_daemon(D1, "foo"),
  58
+
  59
+    % Check table form
  60
+    {ok, Tab1} = couch_os_daemons:info(),
  61
+    [T1] = ets:tab2list(Tab1),
  62
+    check_daemon(T1, "foo"),
  63
+
  64
+    etap:diag("Daemons stop after configuration removed."),
  65
+    couch_config:delete("os_daemons", "foo", false),
  66
+    timer:sleep(500),
  67
+    
  68
+    {ok, []} = couch_os_daemons:info([table]),
  69
+    {ok, Tab2} = couch_os_daemons:info(),
  70
+    etap:is(ets:tab2list(Tab2), [], "As table returns empty table."),
  71
+    
  72
+    etap:diag("Adding multiple daemons causes both to boot."),
  73
+    couch_config:set("os_daemons", "bar", daemon_cmd(), false),
  74
+    couch_config:set("os_daemons", "baz", daemon_cmd(), false),
  75
+    timer:sleep(500),
  76
+    {ok, Daemons} = couch_os_daemons:info([table]),
  77
+    lists:foreach(fun(D) ->
  78
+        check_daemon(D)
  79
+    end, Daemons),
  80
+
  81
+    {ok, Tab3} = couch_os_daemons:info(),
  82
+    lists:foreach(fun(D) ->
  83
+        check_daemon(D)
  84
+    end, ets:tab2list(Tab3)),
  85
+    
  86
+    etap:diag("Removing one daemon leaves the other alive."),
  87
+    couch_config:delete("os_daemons", "bar", false),
  88
+    timer:sleep(500),
  89
+    
  90
+    {ok, [D2]} = couch_os_daemons:info([table]),
  91
+    check_daemon(D2, "baz"),
  92
+    
  93
+    % Check table version
  94
+    {ok, Tab4} = couch_os_daemons:info(),
  95
+    [T4] = ets:tab2list(Tab4),
  96
+    check_daemon(T4, "baz"),
  97
+    
  98
+    ok.
  99
+
  100
+check_daemon(D) ->
  101
+    check_daemon(D, D#daemon.name).
  102
+
  103
+check_daemon(D, Name) ->
  104
+    BaseName = "170-os-daemons.es",
  105
+    BaseLen = length(BaseName),
  106
+    CmdLen = length(D#daemon.cmd),
  107
+    CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
  108
+
  109
+    etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
  110
+    etap:is(D#daemon.name, Name, "Daemon name was set correctly."),
  111
+    etap:is(CmdName, BaseName, "Command name was set correctly."),
  112
+    etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
  113
+    etap:is(D#daemon.errors, [], "No errors occurred while booting."),
  114
+    etap:is(D#daemon.buf, [], "No extra data left in the buffer.").
78  test/etap/171-os-daemons-config.es
... ...
@@ -0,0 +1,78 @@
  1
+#! /usr/bin/env escript
  2
+
  3
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  4
+% use this file except in compliance with the License. You may obtain a copy of
  5
+% the License at
  6
+%
  7
+%   http://www.apache.org/licenses/LICENSE-2.0
  8
+%
  9
+% Unless required by applicable law or agreed to in writing, software
  10
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12
+% License for the specific language governing permissions and limitations under
  13
+% the License.
  14
+
  15
+filename() ->
  16
+    list_to_binary(test_util:source_file("test/etap/171-os-daemons-config.es")).
  17
+
  18
+read() ->
  19
+    case io:get_line('') of
  20
+        eof ->
  21
+            stop;
  22
+        Data ->
  23
+            couch_util:json_decode(Data)
  24
+    end.
  25
+
  26
+write(Mesg) ->
  27
+    Data = iolist_to_binary(couch_util:json_encode(Mesg)),
  28
+    io:format(binary_to_list(Data) ++ "\n", []).
  29
+
  30
+get_cfg(Section) ->
  31
+    write([<<"get">>, Section]),
  32
+    read().
  33
+
  34
+get_cfg(Section, Name) ->
  35
+    write([<<"get">>, Section, Name]),
  36
+    read().
  37
+
  38
+log(Mesg) ->
  39
+    write([<<"log">>, Mesg]).
  40
+
  41
+log(Mesg, Level) ->
  42
+    write([<<"log">>, Mesg, {[{<<"level">>, Level}]}]).
  43
+
  44
+test_get_cfg1() ->
  45
+    FileName = filename(),
  46
+    {[{<<"foo">>, FileName}]} = get_cfg(<<"os_daemons">>).
  47
+
  48
+test_get_cfg2() ->
  49
+    FileName = filename(),
  50
+    FileName = get_cfg(<<"os_daemons">>, <<"foo">>),
  51
+    <<"sequential">> = get_cfg(<<"uuids">>, <<"algorithm">>).
  52
+
  53
+test_log() ->
  54
+    log(<<"foobar!">>),
  55
+    log(<<"some stuff!">>, <<"debug">>),
  56
+    log(2),
  57
+    log(true),
  58
+    write([<<"log">>, <<"stuff">>, 2]),
  59
+    write([<<"log">>, 3, null]),
  60
+    write([<<"log">>, [1, 2], {[{<<"level">>, <<"debug">>}]}]),
  61
+    write([<<"log">>, <<"true">>, {[]}]).
  62
+
  63
+do_tests() ->
  64
+    test_get_cfg1(),
  65
+    test_get_cfg2(),
  66
+    test_log(),
  67
+    loop(io:read("")).
  68
+
  69
+loop({ok, _}) ->
  70
+    loop(io:read(""));
  71
+loop(eof) ->
  72
+    init:stop();
  73
+loop({error, _Reason}) ->
  74
+    init:stop().
  75
+
  76
+main([]) ->
  77
+    test_util:init_code_path(),
  78
+    do_tests().
74  test/etap/171-os-daemons-config.t
... ...
@@ -0,0 +1,74 @@
  1
+#!/usr/bin/env escript
  2
+%% -*- erlang -*-
  3
+
  4
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5
+% use this file except in compliance with the License.  You may obtain a copy of
  6
+% the License at
  7
+%
  8
+%   http://www.apache.org/licenses/LICENSE-2.0
  9
+%
  10
+% Unless required by applicable law or agreed to in writing, software
  11
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
  13
+% License for the specific language governing permissions and limitations under
  14
+% the License.
  15
+
  16
+-record(daemon, {
  17
+    port,
  18
+    name,
  19
+    cmd,
  20
+    kill,
  21
+    status=running,
  22
+    cfg_patterns=[],
  23
+    errors=[],
  24
+    buf=[]
  25
+}).
  26
+
  27
+config_files() ->
  28
+    lists:map(fun test_util:build_file/1, [
  29
+        "etc/couchdb/default_dev.ini"
  30
+    ]).
  31
+
  32
+daemon_cmd() ->
  33
+    test_util:source_file("test/etap/171-os-daemons-config.es").
  34
+
  35
+main(_) ->
  36
+    test_util:init_code_path(),
  37
+
  38
+    etap:plan(6),
  39
+    case (catch test()) of
  40
+        ok ->
  41
+            etap:end_tests();
  42
+        Other ->
  43
+            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
  44
+            etap:bail(Other)
  45
+    end,
  46
+    ok.
  47
+
  48
+test() ->
  49
+    couch_config:start_link(config_files()),
  50
+    couch_config:set("log", "level", "debug", false),
  51
+    couch_log:start_link(),
  52
+    couch_os_daemons:start_link(),
  53
+
  54
+    % "foo" is a required name by this test.
  55
+    couch_config:set("os_daemons", "foo", daemon_cmd(), false),
  56
+    timer:sleep(1000),
  57
+    
  58
+    {ok, [D1]} = couch_os_daemons:info([table]),
  59
+    check_daemon(D1, "foo"),
  60
+    
  61
+    ok.
  62
+
  63
+check_daemon(D, Name) ->
  64
+    BaseName = "171-os-daemons-config.es",
  65
+    BaseLen = length(BaseName),
  66
+    CmdLen = length(D#daemon.cmd),
  67
+    CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
  68
+
  69
+    etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
  70
+    etap:is(D#daemon.name, Name, "Daemon name was set correctly."),
  71
+    etap:is(CmdName, BaseName, "Command name was set correctly."),
  72
+    etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
  73
+    etap:is(D#daemon.errors, [], "No errors occurred while booting."),
  74
+    etap:is(D#daemon.buf, [], "No extra data left in the buffer.").
22  test/etap/172-os-daemon-errors.1.es
... ...
@@ -0,0 +1,22 @@
  1
+#! /usr/bin/env escript
  2
+
  3
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  4
+% use this file except in compliance with the License. You may obtain a copy of
  5
+% the License at
  6
+%
  7
+%   http://www.apache.org/licenses/LICENSE-2.0
  8
+%
  9
+% Unless required by applicable law or agreed to in writing, software
  10
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12
+% License for the specific language governing permissions and limitations under
  13
+% the License.
  14
+
  15
+% Please do not make this file executable as that's the error being tested.
  16
+
  17
+loop() ->
  18
+    timer:sleep(5000),
  19
+    loop().
  20
+
  21
+main([]) ->
  22
+    loop().
16  test/etap/172-os-daemon-errors.2.es
... ...
@@ -0,0 +1,16 @@
  1
+#! /usr/bin/env escript
  2
+
  3
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  4
+% use this file except in compliance with the License. You may obtain a copy of
  5
+% the License at
  6
+%
  7
+%   http://www.apache.org/licenses/LICENSE-2.0
  8
+%
  9
+% Unless required by applicable law or agreed to in writing, software
  10
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12
+% License for the specific language governing permissions and limitations under
  13
+% the License.
  14
+
  15
+main([]) ->
  16
+    init:stop().
17  test/etap/172-os-daemon-errors.3.es
... ...
@@ -0,0 +1,17 @@
  1
+#! /usr/bin/env escript
  2
+
  3
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  4
+% use this file except in compliance with the License. You may obtain a copy of
  5
+% the License at
  6
+%
  7
+%   http://www.apache.org/licenses/LICENSE-2.0
  8
+%
  9
+% Unless required by applicable law or agreed to in writing, software
  10
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12
+% License for the specific language governing permissions and limitations under
  13
+% the License.
  14
+
  15
+main([]) ->
  16
+    timer:sleep(1000),
  17
+    init:stop().
17  test/etap/172-os-daemon-errors.4.es
... ...
@@ -0,0 +1,17 @@
  1
+#! /usr/bin/env escript
  2
+
  3
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  4
+% use this file except in compliance with the License. You may obtain a copy of
  5
+% the License at
  6
+%
  7
+%   http://www.apache.org/licenses/LICENSE-2.0
  8
+%
  9
+% Unless required by applicable law or agreed to in writing, software
  10
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12
+% License for the specific language governing permissions and limitations under
  13
+% the License.
  14
+
  15
+main([]) ->
  16
+    timer:sleep(2000),
  17
+    init:stop().
126  test/etap/172-os-daemon-errors.t
... ...
@@ -0,0 +1,126 @@
  1
+#!/usr/bin/env escript
  2
+%% -*- erlang -*-
  3
+
  4
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5
+% use this file except in compliance with the License.  You may obtain a copy of
  6
+% the License at
  7
+%
  8
+%   http://www.apache.org/licenses/LICENSE-2.0
  9
+%
  10
+% Unless required by applicable law or agreed to in writing, software
  11
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
  13
+% License for the specific language governing permissions and limitations under
  14
+% the License.
  15
+
  16
+-record(daemon, {
  17
+    port,
  18
+    name,
  19
+    cmd,
  20
+    kill,
  21
+    status=running,
  22
+    cfg_patterns=[],
  23
+    errors=[],
  24
+    buf=[]
  25
+}).
  26
+
  27
+config_files() ->
  28
+    lists:map(fun test_util:build_file/1, [
  29
+        "etc/couchdb/default_dev.ini"
  30
+    ]).
  31
+
  32
+bad_perms() ->
  33
+    test_util:source_file("test/etap/172-os-daemon-errors.1.es").
  34
+
  35
+die_on_boot() ->
  36
+    test_util:source_file("test/etap/172-os-daemon-errors.2.es").
  37
+
  38
+die_quickly() ->
  39
+    test_util:source_file("test/etap/172-os-daemon-errors.3.es").
  40
+
  41
+can_reboot() ->
  42
+    test_util:source_file("test/etap/172-os-daemon-errors.4.es").
  43
+
  44
+main(_) ->
  45
+    test_util:init_code_path(),
  46
+
  47
+    etap:plan(36),
  48
+    case (catch test()) of
  49
+        ok ->
  50
+            etap:end_tests();
  51
+        Other ->
  52
+            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
  53
+            etap:bail(Other)
  54
+    end,
  55
+    ok.
  56
+
  57
+test() ->
  58
+    couch_config:start_link(config_files()),
  59
+    couch_os_daemons:start_link(),
  60
+
  61
+    etap:diag("Daemon not executable."),
  62
+    test_halts("foo", bad_perms(), 1000),
  63
+
  64
+    etap:diag("Daemon dies on boot."),
  65
+    test_halts("bar", die_on_boot(), 1000),
  66
+
  67
+    etap:diag("Daemon dies quickly after boot."),
  68
+    test_halts("baz", die_quickly(), 4000),
  69
+    
  70
+    etap:diag("Daemon dies, but not quickly enough to be halted."),
  71
+    test_runs("bam", can_reboot()),
  72
+    
  73
+    ok.
  74
+
  75
+test_halts(Name, Cmd, Time) ->
  76
+    couch_config:set("os_daemons", Name, Cmd ++ " 2> /dev/null", false),
  77
+    timer:sleep(Time),
  78
+    {ok, [D]} = couch_os_daemons:info([table]),
  79
+    check_dead(D, Name, Cmd),
  80
+    couch_config:delete("os_daemons", Name, false).
  81
+
  82
+test_runs(Name, Cmd) ->
  83
+    couch_config:set("os_daemons", Name, Cmd, false),
  84
+
  85
+    timer:sleep(1000),
  86
+    {ok, [D1]} = couch_os_daemons:info([table]),
  87
+    check_daemon(D1, Name, Cmd, 0),
  88
+    
  89
+    % Should reboot every two seconds. We're at 1s, so wait
  90
+    % utnil 3s to be in the middle of the next invocation's
  91
+    % life span.
  92
+    timer:sleep(2000),
  93
+    {ok, [D2]} = couch_os_daemons:info([table]),
  94
+    check_daemon(D2, Name, Cmd, 1),
  95
+    
  96
+    % If the kill command changed, that means we rebooted the process.
  97
+    etap:isnt(D1#daemon.kill, D2#daemon.kill, "Kill command changed.").
  98
+
  99
+check_dead(D, Name, Cmd) ->
  100
+    BaseName = filename:basename(Cmd) ++ " 2> /dev/null",
  101
+    BaseLen = length(BaseName),
  102
+    CmdLen = length(D#daemon.cmd),
  103
+    CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
  104
+
  105
+    etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
  106
+    etap:is(D#daemon.name, Name, "Daemon name was set correctly."),
  107
+    etap:is(CmdName, BaseName, "Command name was set correctly."),
  108
+    etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
  109
+    etap:is(D#daemon.status, halted, "Daemon has been halted."),
  110
+    etap:is(D#daemon.errors, nil, "Errors have been disabled."),
  111
+    etap:is(D#daemon.buf, nil, "Buffer has been switched off.").
  112
+
  113
+check_daemon(D, Name, Cmd, Errs) ->
  114
+    BaseName = filename:basename(Cmd),
  115
+    BaseLen = length(BaseName),
  116
+    CmdLen = length(D#daemon.cmd),
  117
+    CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
  118
+
  119
+    etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
  120
+    etap:is(D#daemon.name, Name, "Daemon name was set correctly."),
  121
+    etap:is(CmdName, BaseName, "Command name was set correctly."),
  122
+    etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
  123
+    etap:is(D#daemon.status, running, "Daemon still running."),
  124
+    etap:is(length(D#daemon.errors), Errs, "Found expected number of errors."),
  125
+    etap:is(D#daemon.buf, [], "No extra data left in the buffer.").
  126
+
35  test/etap/173-os-daemon-cfg-register.es
... ...
@@ -0,0 +1,35 @@
  1
+#! /usr/bin/env escript
  2
+
  3
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  4
+% use this file except in compliance with the License. You may obtain a copy of
  5
+% the License at
  6
+%
  7
+%   http://www.apache.org/licenses/LICENSE-2.0
  8
+%
  9
+% Unless required by applicable law or agreed to in writing, software
  10
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12
+% License for the specific language governing permissions and limitations under
  13
+% the License.
  14
+
  15
+write(Mesg) ->
  16
+    Data = iolist_to_binary(couch_util:json_encode(Mesg)),
  17
+    io:format(binary_to_list(Data) ++ "\n", []).
  18
+
  19
+cfg_register(Section) ->
  20
+    write([<<"register">>, Section]).
  21
+
  22
+cfg_register(Section, Key) ->
  23
+    write([<<"register">>, Section, Key]).
  24
+
  25
+wait(_) ->
  26
+    init:stop().
  27
+
  28
+do_tests() ->
  29
+    cfg_register(<<"s1">>),
  30
+    cfg_register(<<"s2">>, <<"k">>),
  31
+    wait(io:read("")).
  32
+
  33
+main([]) ->
  34
+    test_util:init_code_path(),
  35
+    do_tests().
98  test/etap/173-os-daemon-cfg-register.t
... ...
@@ -0,0 +1,98 @@
  1
+#!/usr/bin/env escript
  2
+%% -*- erlang -*-
  3
+
  4
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5
+% use this file except in compliance with the License.  You may obtain a copy of
  6
+% the License at
  7
+%
  8
+%   http://www.apache.org/licenses/LICENSE-2.0
  9
+%
  10
+% Unless required by applicable law or agreed to in writing, software
  11
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
  13
+% License for the specific language governing permissions and limitations under
  14
+% the License.
  15
+
  16
+-record(daemon, {
  17
+    port,
  18
+    name,
  19
+    cmd,
  20
+    kill,
  21
+    status=running,
  22
+    cfg_patterns=[],
  23
+    errors=[],
  24
+    buf=[]
  25
+}).
  26
+
  27
+config_files() ->
  28
+    lists:map(fun test_util:build_file/1, [
  29
+        "etc/couchdb/default_dev.ini"
  30
+    ]).
  31
+
  32
+daemon_name() ->
  33
+    "wheee".
  34
+
  35
+daemon_cmd() ->
  36
+    test_util:source_file("test/etap/173-os-daemon-cfg-register.es").
  37
+
  38
+main(_) ->
  39
+    test_util:init_code_path(),
  40
+
  41
+    etap:plan(27),
  42
+    case (catch test()) of
  43
+        ok ->
  44
+            etap:end_tests();
  45
+        Other ->
  46
+            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
  47
+            etap:bail(Other)
  48
+    end,
  49
+    ok.
  50
+
  51
+test() ->
  52
+    couch_config:start_link(config_files()),
  53
+    couch_os_daemons:start_link(),
  54
+    
  55
+    DaemonCmd = daemon_cmd() ++ " 2> /dev/null",
  56
+    
  57
+    etap:diag("Booting the daemon"),
  58
+    couch_config:set("os_daemons", daemon_name(), DaemonCmd, false),
  59
+    timer:sleep(1000),
  60
+    {ok, [D1]} = couch_os_daemons:info([table]),
  61
+    check_daemon(D1, running),
  62
+    
  63
+    etap:diag("Daemon restarts when section changes."),
  64
+    couch_config:set("s1", "k", "foo", false),
  65
+    timer:sleep(1000),
  66
+    {ok, [D2]} = couch_os_daemons:info([table]),
  67
+    check_daemon(D2, running),
  68
+    etap:isnt(D2#daemon.kill, D1#daemon.kill, "Kill command shows restart."),
  69
+
  70
+    etap:diag("Daemon doesn't restart for ignored section key."),
  71
+    couch_config:set("s2", "k2", "baz", false),
  72
+    timer:sleep(1000),
  73
+    {ok, [D3]} = couch_os_daemons:info([table]),
  74
+    etap:is(D3, D2, "Same daemon info after ignored config change."),
  75
+    
  76
+    etap:diag("Daemon restarts for specific section/key pairs."),
  77
+    couch_config:set("s2", "k", "bingo", false),
  78
+    timer:sleep(1000),
  79
+    {ok, [D4]} = couch_os_daemons:info([table]),
  80
+    check_daemon(D4, running),
  81
+    etap:isnt(D4#daemon.kill, D3#daemon.kill, "Kill command changed again."),
  82
+    
  83
+    ok.
  84
+
  85
+check_daemon(D, Status) ->
  86
+    BaseName = filename:basename(daemon_cmd()) ++ " 2> /dev/null",
  87
+    BaseLen = length(BaseName),
  88
+    CmdLen = length(D#daemon.cmd),
  89
+    CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
  90
+
  91
+    etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
  92
+    etap:is(D#daemon.name, daemon_name(), "Daemon name was set correctly."),
  93
+    etap:is(CmdName, BaseName, "Command name was set correctly."),
  94
+    etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
  95
+    etap:is(D#daemon.status, Status, "Daemon status is correct."),
  96
+    etap:is(D#daemon.cfg_patterns, [{"s1"}, {"s2", "k"}], "Cfg patterns set"),
  97
+    etap:is(D#daemon.errors, [], "No errors have occurred."),
  98
+    etap:isnt(D#daemon.buf, nil, "Buffer is active.").
13  test/etap/Makefile.am
@@ -66,4 +66,15 @@ EXTRA_DIST = \
66 66
     130-attachments-md5.t \
67 67
     140-attachment-comp.t \
68 68
     150-invalid-view-seq.t \
69  
-    160-vhosts.t
  69
+    160-vhosts.t \
  70
+    170-os-daemons.es \
  71
+	170-os-daemons.t \
  72
+    171-os-daemons-config.es \
  73
+    171-os-daemons-config.t \
  74
+    172-os-daemon-errors.1.es \
  75
+    172-os-daemon-errors.2.es \
  76
+    172-os-daemon-errors.3.es \
  77
+    172-os-daemon-errors.4.es \
  78
+    172-os-daemon-errors.t \
  79
+	173-os-daemon-cfg-register.es \
  80
+	173-os-daemon-cfg-register.t

0 notes on commit c878511

Please sign in to comment.
Something went wrong with that request. Please try again.