# # # add_dir "test/test1/scripts" # # add_file "test/test1/scripts/foobar.sh" # content [163fe8665a223821f28c4c4515b2cf5d4f1cf26d] # # patch "doc/documentation.html" # from [8fa162b604a4b395d0acba60c8b78dfd4ed88a57] # to [aa098f958289d699d949361a2fed998ae33a1b70] # # patch "src/administrator.cc" # from [1b15e642e907e823949d4f29e68bee03c3872d78] # to [ce537034af9af4b4985bdf5b52c44b3a22714317] # # patch "src/administrator.hh" # from [80100a85bb6fad55b37272f583cc423592c7d9ef] # to [c4f24daf6e734462911a998c1fd55b7288d6cc87] # # patch "test/run-tests.sh" # from [1eb46a921473826f1797551748752e69fa63b039] # to [2e4fda1e753add064643ce3aac932de0da58f799] # # patch "test/test1/script.txt" # from [33b921a8fc5d016e41c354563db7dafe75f838b2] # to [18da33dc8b0860cda8241385b62e5604c3300b1c] # # patch "test/test1/usher.conf" # from [242f16c8a5b3d125705a775647984ce8dd7a1020] # to [e36111d846aab6dca73eef803aa73a539d82ddc0] # ============================================================ --- src/administrator.cc 1b15e642e907e823949d4f29e68bee03c3872d78 +++ src/administrator.cc ce537034af9af4b4985bdf5b52c44b3a22714317 @@ -10,6 +10,13 @@ #include "administrator.hh" #include "io.hh" +#include // fcntl +#include // fcntl +#include // memcpy +#include // errno +#include // waitpid +#include // waitpid + #include #include using std::cerr; @@ -50,7 +57,8 @@ administrator::connection::connection(so administrator::connection::connection(sock & s) : authenticated(false), read_done(false), - response_done(false), the_sock(s) + response_done(false), the_sock(s), + script_fd(-1), script_pid(-1) { } @@ -227,6 +235,118 @@ administrator::process(connection & cs) } else if (cmd == "STARTUP") { manager.allow_connections(true); cs.outbound.put_string("ok\n"); + } else if (cmd == "SCRIPT") { + do { + string name; + iss >> name; + map >::iterator script_iter = scripts.find(name); + if (script_iter == scripts.end()) { + cs.outbound.put_string("\n127 script '" + name + "' not found\n"); + break; + } + int my_pipe[2]; + if (pipe(my_pipe) == -1) { + cs.outbound.put_string("\n127 cannot create pipe\n"); + break; + } + cs.script_fd = my_pipe[0]; + fcntl(cs.script_fd, F_SETFD, FD_CLOEXEC); + cs.script_pid = fork(); + if (cs.script_pid == -1) { + close(my_pipe[0]); + close(my_pipe[1]); + cs.script_fd = -1; + cs.outbound.put_string("\n127 fork failed\n"); + break; + } + if (cs.script_pid == 0) { + dup2(my_pipe[1], 1); + dup2(1, 2); + sock::close_all_socks(); + close(0); + + vector args = script_iter->second; + string cmd_line; + std::getline(iss, cmd_line); + { + bool have_arg = false; + string arg; + enum quote_type { q_none, q_single, q_double }; + quote_type quote = q_none; + for (string::iterator i = cmd_line.begin(); i != cmd_line.end(); ++i) { + switch(quote) { + case q_none: + if (std::isspace(*i)) { + if (have_arg) { + args.push_back(arg); + arg.clear(); + } + have_arg = false; + } else { + have_arg = true; + switch (*i) { + case '"': + quote = q_double; + break; + case '\'': + quote = q_single; + break; + case '\\': + ++i; + if (i != cmd_line.end()) { + if (*i == 'n') + arg += '\n'; + else + arg += *i; + } + break; + default: + arg += *i; + } + } + break; + case q_single: + if (*i == '\'') + quote = q_none; + else { + arg += *i; + } + break; + case q_double: + if (*i == '"') + quote = q_none; + else if (*i == '\\') { + ++i; + if (i != cmd_line.end()) { + if (*i == 'n') + arg += '\n'; + else + arg += *i; + } + } else { + arg += *i; + } + break; + } + } + if (have_arg) { + args.push_back(arg); + } + } + char ** argv = new char*[args.size() + 1]; + for (unsigned int i = 0; i < args.size(); ++i) { + argv[i] = new char[args[i].size()+1]; + memcpy(argv[i], args[i].c_str(), args[i].size()+1); + } + argv[args.size()] = 0; + execvp(argv[0], argv); + perror("execvp failed"); + exit(126); + } + close(my_pipe[1]); + + set_response = false; + } while(false); } else { cs.outbound.put_string("unknown command\n"); } @@ -289,6 +409,10 @@ administrator::add_to_select(int & maxfd } if (added) maxfd = max(maxfd, int(c)); + if (i->script_fd != -1 && i->outbound.canwrite()) { + FD_SET(i->script_fd, &rd); + maxfd = max(maxfd, (int)i->script_fd); + } } } @@ -312,27 +436,55 @@ administrator::process_selected(fd_set & i != conns.end(); ++i) { sock & connsock = i->the_sock; if (connsock <= 0) { + std::cerr<<"Admin connection died: bad socket.\n"; del.push_back(i); continue; } if (FD_ISSET(connsock, &rd)) { if (!connsock.read_to(i->inbound)) { + std::cerr<<"Admin connection died: read failed.\n"; del.push_back(i); } else if (!process(*i)) { + std::cerr<<"Admin connection died: processing failed.\n"; del.push_back(i); } } if (FD_ISSET(connsock, &wr)) { if (!connsock.write_from(i->outbound)) { + std::cerr<<"Admin connection died: write failed.\n"; del.push_back(i); continue; } - if (!i->outbound.canwrite() && i->response_done) { + if (!i->outbound.canread() && i->response_done) { del.push_back(i); } } + + if (i->script_fd != -1 && FD_ISSET(i->script_fd, &rd)) { + if (!i->script_fd.read_to(i->outbound)) { + i->script_fd = -1; + int r; + int status; + do { r = waitpid(i->script_pid, &status, 0); } while (r == -1 && errno == EINTR); + i->script_pid = -1; + if (WIFEXITED(status)) { + std::ostringstream oss; + oss << "\n"; + oss << WEXITSTATUS(status); + oss << " exited\n"; + i->outbound.put_string(oss.str()); + } else { + std::ostringstream oss; + oss << "\n"; + oss << WTERMSIG(status); + oss << " signalled\n"; + i->outbound.put_string(oss.str()); + } + i->response_done = true; + } + } } for (list::iterator>::iterator i = del.begin(); ============================================================ --- src/administrator.hh 80100a85bb6fad55b37272f583cc423592c7d9ef +++ src/administrator.hh c4f24daf6e734462911a998c1fd55b7288d6cc87 @@ -37,6 +37,8 @@ struct administrator buffer outbound; buffer inbound; sock the_sock; + sock script_fd; + int script_pid; connection(sock & s); }; list conns; ============================================================ --- test/run-tests.sh 1eb46a921473826f1797551748752e69fa63b039 +++ test/run-tests.sh 2e4fda1e753add064643ce3aac932de0da58f799 @@ -33,7 +33,7 @@ msg_usher() { # see usher.conf.head for address { echo "USERPASS user pass"; sleep $MSG_USHER_SLEEP; - echo "$@"; } | socat tcp:127.0.0.1:23345 stdio + echo "$@"; } | socat -t10 tcp:127.0.0.1:23345 stdio } serve() { @@ -51,6 +51,18 @@ client() { CLIENTS="$CLIENTS $!" } +sync() { + client sync "$@" +} + +multipull() { + count=$1 + pattern="$2" + for ((i=0; i<$count; ++i)); do + client pull multipull-$LINE-$i "$pattern" + done +} + check_match() { local host=$1 local pattern="$2" @@ -68,6 +80,20 @@ check_match() { fi } +script() { + local name="$1" + local args="$2" + local output="$(printf "$3")" + + local result="$(msg_usher SCRIPT $name "$args")" + if [ "$output" != "$result" ]; then + echo "script: expected '$output', got '$result'" + return 1 + else + echo "script: OK" + fi +} + EXIT_STATUS=0 for test_name in $(ls $SRCDIR/test/); do @@ -90,11 +116,20 @@ for test_name in $(ls $SRCDIR/test/); do cp databases/*.mtn ./ # see if it works - # 'cp' will preserve read-only-ness, which messes up distcheck - cat $SRCDIR/test/usher.conf.head > usher.conf.full + cp $SRCDIR/test/usher.conf.head usher.conf.full + chmod +w usher.conf.full sed '/^local/ s,$, "--confdir" "'$TESTDIR/confdir'",' \ < $TEST_SRC/usher.conf >> usher.conf.full + if [ -d $TEST_SRC/scripts ] + then + echo "Copying scripts..." + [ -d scripts ] && rm -rf scripts + cp -r $TEST_SRC/scripts/ scripts + chmod -R +wx scripts + find scripts + fi + ../../usher usher.conf.full & USHER=$! sleep 1 @@ -105,56 +140,35 @@ for test_name in $(ls $SRCDIR/test/); do sed -n 's/#.*$//; /./ p' <$TEST_SRC/script.txt | { SERVERS= CLIENTS= - while read cmd a1 a2 a3; do - echo "Testing: $cmd $a1 $a2 $a3" + while read -r cmd rest; do + echo "Testing: $cmd $rest" LINE=$(expr $LINE + 1) case $cmd in - serve) - database=$a1 - address=$a2 - serve $database $address - ;; - multipull) - count=$a1 - pattern="$a2" - for ((i=0; i<$count; ++i)); do - client pull multipull-$LINE-$i "$pattern" - done - ;; - sync) - database=$a1 - pattern="$a2" - client sync $database "$pattern" - ;; - check_match) - hostname="$a1" - pattern="$a2" - server="$a3" - if ! check_match "$hostname" "$pattern" "$server"; then - OK=false - fi - ;; - stop) - break - ;; - esac - #sleep 1 - done - echo "Reached end of script, waiting for clients to die..." - for c in $CLIENTS; do - echo "Waiting for $c..." - if ! wait $c; then - echo "Client died horribly." - OK=false + stop) + break + ;; + *) + if ! eval $cmd "$rest"; then + OK=false + fi + ;; + esac + done + echo "Reached end of script, waiting for clients to die..." + for c in $CLIENTS; do + echo "Waiting for $c..." + if ! wait $c; then + echo "Client died horribly." + OK=false + fi + done + if $OK; then + echo "PASS $test_name" >>$TESTDIR/status + else + echo "FAIL $test_name" >>$TESTDIR/status fi - done - if $OK; then - echo "PASS $test_name" >>$TESTDIR/status - else - echo "FAIL $test_name" >>$TESTDIR/status - fi - echo "Killing any independent servers (pids: $SERVERS)..." - [ "$SERVERS" ] && kill $SERVERS + echo "Killing any independent servers (pids: $SERVERS)..." + [ "$SERVERS" ] && kill $SERVERS } | tee testlog.log echo "Killing usher (pid: $USHER)..." kill $USHER ============================================================ --- test/test1/script.txt 33b921a8fc5d016e41c354563db7dafe75f838b2 +++ test/test1/script.txt 18da33dc8b0860cda8241385b62e5604c3300b1c @@ -4,16 +4,16 @@ multipull 3 net.prjek.{fnord,foobar} multipull 3 org.example -sync user1 net.prjek* +sync user1 net.prjek'*' # check_match # by hostname -check_match prjek.net * prjek -check_match prjek-other.prjek.net * prjek-s -check_match prjek-fnord.prjek.net * prjek -check_match xyzzy.prjek.net * - -check_match example.org * example +check_match prjek.net '*' prjek +check_match prjek-other.prjek.net '*' prjek-s +check_match prjek-fnord.prjek.net '*' prjek +check_match xyzzy.prjek.net '*' - +check_match example.org '*' example # by pattern check_match - net.prjek.foo prjek @@ -36,3 +36,6 @@ check_match mtn://example.org/ - example check_match mtn://example.org/prjek - prjek check_match mtn://prjek.net/foobar net.prjek - + +# script +script fooscript 'a "b c" d' 'foo\nxyzzy x z a b c d\nbar\n\n0 exited\n' ============================================================ --- test/test1/usher.conf 242f16c8a5b3d125705a775647984ce8dd7a1020 +++ test/test1/usher.conf e36111d846aab6dca73eef803aa73a539d82ddc0 @@ -1,3 +1,6 @@ + +script "fooscript" "scripts/foobar.sh" "xyzzy" "x z" + server "prjek-s" host "prjek-other" pattern "net.prjek.separate" ============================================================ --- doc/documentation.html 8fa162b604a4b395d0acba60c8b78dfd4ed88a57 +++ doc/documentation.html aa098f958289d699d949361a2fed998ae33a1b70 @@ -4,6 +4,7 @@ + Usher Documentation

Introduction

@@ -212,8 +213,16 @@ will be "ok", and will not be given unti
RELOAD
Reload the config file, the same as sending SIGHUP. The reply will be "ok", and will not be given until the config file has been -reloaded.
+reloaded.
RUN scriptname arg "a r g"
+
Run the named script, which was defined with the "script" config +file directive. Arguments given here are appended to the end of the +command line, after any arguments given in the "script" directive. +Arguments with spaces can be quoted with single or double quotes, and +the "\n", "\'", "\"", and "\\" escape sequences are recognized except +inside single quotes. You cannot use literal newlines even inside +quotes, but must use "\n" if you want a newline character.
+ \ No newline at end of file ============================================================ --- /dev/null +++ test/test1/scripts/foobar.sh 163fe8665a223821f28c4c4515b2cf5d4f1cf26d @@ -0,0 +1,7 @@ +#!/bin/sh + +echo foo +sleep 1 +echo "$@" +sleep 1 +echo bar