tested django-newsletter
This commit is contained in:
@@ -0,0 +1,526 @@
|
||||
===============
|
||||
Using zdaemon
|
||||
===============
|
||||
|
||||
zdaemon provides a script, zdaemon, that can be used to run other
|
||||
programs as POSIX (Unix) daemons. (Of course, it is only usable on
|
||||
POSIX-complient systems.)
|
||||
|
||||
Using zdaemon requires specifying a number of options, which can be
|
||||
given in a configuration file, or as command-line options. It also
|
||||
accepts commands teling it what do do. The commands are:
|
||||
|
||||
start
|
||||
Start a process as a daemon
|
||||
|
||||
stop
|
||||
Stop a running daemon process
|
||||
|
||||
restart
|
||||
Stop and then restart a program
|
||||
|
||||
status
|
||||
Find out if the program is running
|
||||
|
||||
foreground or fg
|
||||
Run a program
|
||||
|
||||
kill signal
|
||||
Send a signal to the daemon process
|
||||
|
||||
reopen_transcript
|
||||
Reopen the transcript log. See the discussion of the transcript
|
||||
log below.
|
||||
|
||||
help command
|
||||
Get help on a command
|
||||
|
||||
|
||||
Commands can be given on a command line, or can be given using an
|
||||
interactive interpreter.
|
||||
|
||||
Let's start with a simple example. We'll use command-line options to
|
||||
run the echo command:
|
||||
|
||||
sh> ./zdaemon -p 'echo hello world' fg
|
||||
echo hello world
|
||||
hello world
|
||||
|
||||
|
||||
Here we used the -p option to specify a program to run. We can
|
||||
specify a program name and command-line options in the program
|
||||
command. Note, however, that the command-line parsing is pretty
|
||||
primitive. Quotes and spaces aren't handled correctly. Let's look at
|
||||
a slightly more complex example. We'll run the sleep command as a
|
||||
daemon :)
|
||||
|
||||
sh> ./zdaemon -p 'sleep 100' start
|
||||
. .
|
||||
daemon process started, pid=819
|
||||
|
||||
This ran the sleep daemon. We can check whether it ran with the
|
||||
status command:
|
||||
|
||||
sh> ./zdaemon -p 'sleep 100' status
|
||||
program running; pid=819
|
||||
|
||||
We can stop it with the stop command:
|
||||
|
||||
sh> ./zdaemon -p 'sleep 100' stop
|
||||
. .
|
||||
daemon process stopped
|
||||
|
||||
sh> ./zdaemon -p 'sleep 100' status
|
||||
daemon manager not running
|
||||
Failed: 3
|
||||
|
||||
Normally, we control zdaemon using a configuration file. Let's create
|
||||
a typical configuration file::
|
||||
|
||||
<runner>
|
||||
program sleep 100
|
||||
</runner>
|
||||
|
||||
.. -> text
|
||||
|
||||
>>> with open('conf', 'w') as file:
|
||||
... _ = file.write(text)
|
||||
|
||||
Now, we can run with the -C option to read the configuration file:
|
||||
|
||||
sh> ./zdaemon -Cconf start
|
||||
. .
|
||||
daemon process started, pid=1136
|
||||
|
||||
If we list the directory:
|
||||
|
||||
sh> ls
|
||||
conf
|
||||
zdaemon
|
||||
zdsock
|
||||
|
||||
We'll see that a file, zdsock, was created. This is a unix-domain
|
||||
socket used internally by ZDaemon. We'll normally want to control
|
||||
where this goes.
|
||||
|
||||
sh> ./zdaemon -Cconf stop
|
||||
. .
|
||||
daemon process stopped
|
||||
|
||||
Here's an updated configuration::
|
||||
|
||||
<runner>
|
||||
program sleep 100
|
||||
socket-name /tmp/demo.zdsock
|
||||
</runner>
|
||||
|
||||
.. -> text
|
||||
|
||||
>>> with open('conf', 'w') as file:
|
||||
... _ = file.write(text.replace('/tmp', tmpdir))
|
||||
|
||||
Now, when we run zdaemon:
|
||||
|
||||
sh> ./zdaemon -Cconf start
|
||||
. .
|
||||
daemon process started, pid=1139
|
||||
|
||||
sh> ls
|
||||
conf
|
||||
zdaemon
|
||||
|
||||
.. test
|
||||
|
||||
>>> import os
|
||||
>>> os.path.exists("/tmp/demo.zdsock".replace('/tmp', tmpdir))
|
||||
True
|
||||
|
||||
The socket file is created in the given directory.
|
||||
|
||||
sh> ./zdaemon -Cconf stop
|
||||
. .
|
||||
daemon process stopped
|
||||
|
||||
In the example, we included a command-line argument in the program
|
||||
option. We can also provide options on the command line::
|
||||
|
||||
<runner>
|
||||
program sleep
|
||||
socket-name /tmp/demo.zdsock
|
||||
</runner>
|
||||
|
||||
.. -> text
|
||||
|
||||
>>> with open('conf', 'w') as file:
|
||||
... _ = file.write(text.replace('/tmp', tmpdir))
|
||||
|
||||
Then we can pass the program argument on the command line:
|
||||
|
||||
sh> ./zdaemon -Cconf start 100
|
||||
. .
|
||||
daemon process started, pid=1149
|
||||
|
||||
sh> ./zdaemon -Cconf status
|
||||
program running; pid=1149
|
||||
|
||||
sh> ./zdaemon -Cconf stop
|
||||
. .
|
||||
daemon process stopped
|
||||
|
||||
Environment Variables
|
||||
=====================
|
||||
|
||||
Sometimes, it is necessary to set environment variables before running
|
||||
a program. Perhaps the most common case for this is setting
|
||||
LD_LIBRARY_PATH so that dynamically loaded libraries can be found.
|
||||
|
||||
::
|
||||
|
||||
<runner>
|
||||
program env
|
||||
socket-name /tmp/demo.zdsock
|
||||
</runner>
|
||||
<environment>
|
||||
LD_LIBRARY_PATH /home/foo/lib
|
||||
HOME /home/foo
|
||||
</environment>
|
||||
|
||||
.. -> text
|
||||
|
||||
>>> with open('conf', 'w') as file:
|
||||
... _ = file.write(text.replace('/tmp', tmpdir))
|
||||
|
||||
Now, when we run the command, we'll see out environment settings reflected:
|
||||
|
||||
sh> ./zdaemon -Cconf fg
|
||||
env
|
||||
USER=jim
|
||||
HOME=/home/foo
|
||||
LOGNAME=jim
|
||||
USERNAME=jim
|
||||
TERM=dumb
|
||||
PATH=/home/jim/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin
|
||||
EMACS=t
|
||||
LANG=en_US.UTF-8
|
||||
SHELL=/bin/bash
|
||||
EDITOR=emacs
|
||||
LD_LIBRARY_PATH=/home/foo/lib
|
||||
|
||||
Transcript log
|
||||
==============
|
||||
|
||||
When zdaemon run a program in daemon mode, it disconnects the
|
||||
program's standard input, standard output, and standard error from the
|
||||
controlling terminal. It can optionally redirect the output to
|
||||
standard error and standard output to a file. This is done with the
|
||||
transcript option. This is, of course, useful for logging output from
|
||||
long-running applications.
|
||||
|
||||
Let's look at an example. We'll have a long-running process that
|
||||
simple tails a data file:
|
||||
|
||||
>>> f = open('data', 'w', 1)
|
||||
>>> import os
|
||||
>>> _ = f.write('rec 1\n'); f.flush(); os.fsync(f.fileno())
|
||||
|
||||
Now, here's out zdaemon configuration::
|
||||
|
||||
<runner>
|
||||
program tail -f data
|
||||
transcript log
|
||||
</runner>
|
||||
|
||||
.. -> text
|
||||
|
||||
>>> with open('conf', 'w') as file:
|
||||
... _ = file.write(text)
|
||||
|
||||
Now we'll start:
|
||||
|
||||
sh> ./zdaemon -Cconf start
|
||||
. .
|
||||
daemon process started, pid=7963
|
||||
|
||||
.. Wait a little bit to make sure tail has a chance to work
|
||||
|
||||
>>> import time
|
||||
>>> time.sleep(0.1)
|
||||
|
||||
After waiting a bit, if we look at the log file, it contains the tail output:
|
||||
|
||||
>>> with open('log') as file:
|
||||
... file.read()
|
||||
'rec 1\n'
|
||||
|
||||
We can rotate the transcript log by renaming it and telling zdaemon to
|
||||
reopen it:
|
||||
|
||||
>>> import os
|
||||
>>> os.rename('log', 'log.1')
|
||||
|
||||
If we generate more output:
|
||||
|
||||
>>> _ = f.write('rec 2\n'); f.flush(); os.fsync(f.fileno())
|
||||
|
||||
.. Wait a little bit to make sure tail has a chance to work
|
||||
|
||||
>>> time.sleep(1)
|
||||
|
||||
The output will appear in the old file, because zdaemon still has it
|
||||
open:
|
||||
|
||||
>>> with open('log.1') as file:
|
||||
... file.read()
|
||||
'rec 1\nrec 2\n'
|
||||
|
||||
Now, if we tell zdaemon to reopen the file:
|
||||
|
||||
sh> ./zdaemon -Cconf reopen_transcript
|
||||
|
||||
and generate some output:
|
||||
|
||||
>>> _ = f.write('rec 3\n'); f.flush(); os.fsync(f.fileno())
|
||||
|
||||
.. Wait a little bit to make sure tail has a chance to work
|
||||
|
||||
>>> time.sleep(1)
|
||||
|
||||
the output will show up in the new file, not the old:
|
||||
|
||||
>>> with open('log') as file:
|
||||
... file.read()
|
||||
'rec 3\n'
|
||||
|
||||
>>> with open('log.1') as file:
|
||||
... file.read()
|
||||
'rec 1\nrec 2\n'
|
||||
|
||||
Close files and clean up:
|
||||
|
||||
>>> f.close()
|
||||
|
||||
sh> ./zdaemon -Cconf stop
|
||||
. .
|
||||
daemon process stopped
|
||||
|
||||
|
||||
Start test program and timeout
|
||||
==============================
|
||||
|
||||
Normally, zdaemon considers a process to have started when the process
|
||||
itself has been created. A process may take a while before it is
|
||||
truly up and running. For example, a database server or a web server
|
||||
may take time before they're ready to accept requests.
|
||||
|
||||
You can optionally supply a test program, via the ``start-test-program``
|
||||
configuration option, that is called repeatedly until it returns a 0
|
||||
exit status or until a time limit, ``start-timeout``, has been reached.
|
||||
|
||||
Reference Documentation
|
||||
=======================
|
||||
|
||||
The following options are available for use in the runner section of
|
||||
configuration files and as command-line options.
|
||||
|
||||
program
|
||||
Command-line option: -p or --program
|
||||
|
||||
This option gives the command used to start the subprocess
|
||||
managed by zdaemon. This is currently a simple list of
|
||||
whitespace-delimited words. The first word is the program
|
||||
file, subsequent words are its command line arguments. If the
|
||||
program file contains no slashes, it is searched using $PATH.
|
||||
(Note that there is no way to to include whitespace in the program
|
||||
file or an argument, and under certain circumstances other
|
||||
shell metacharacters are also a problem.)
|
||||
|
||||
socket-name
|
||||
Command-line option: -s or --socket-name.
|
||||
|
||||
The pathname of the Unix domain socket used for communication
|
||||
between the zdaemon command-line tool and a daemon-management
|
||||
process. The default is relative to the current directory in
|
||||
which zdaemon is started. You want to specify
|
||||
an absolute pathname here.
|
||||
|
||||
This defaults to "zdsock", which is created in the directory
|
||||
in which zdrun is started.
|
||||
|
||||
daemon
|
||||
Command-line option: -d or --daemon.
|
||||
|
||||
If this option is true, zdaemon runs in the background as a
|
||||
true daemon. It forks a child process which becomes the
|
||||
subprocess manager, while the parent exits (making the shell
|
||||
that started it believe it is done). The child process also
|
||||
does the following:
|
||||
|
||||
- if the directory option is set, change into that directory
|
||||
|
||||
- redirect stdin, stdout and stderr to /dev/null
|
||||
|
||||
- call setsid() so it becomes a session leader
|
||||
|
||||
- call umask() with specified value
|
||||
|
||||
The default for this option is on by default. The
|
||||
command-line option therefore has no effect. To disable
|
||||
daemon mode, you must use a configuration file::
|
||||
|
||||
<runner>
|
||||
program sleep 1
|
||||
daemon off
|
||||
</runner>
|
||||
|
||||
directory
|
||||
Command-line option: -z or --directory.
|
||||
|
||||
If the daemon option is true (default), this option can
|
||||
specify a directory into which zdrun.py changes as part of the
|
||||
"daemonizing". If the daemon option is false, this option is
|
||||
ignored.
|
||||
|
||||
backoff-limit
|
||||
Command-line option: -b or --backoff-limit.
|
||||
|
||||
When the subprocess crashes, zdaemon inserts a one-second
|
||||
delay before it restarts it. When the subprocess crashes
|
||||
again right away, the delay is incremented by one second, and
|
||||
so on. What happens when the delay has reached the value of
|
||||
backoff-limit (in seconds), depends on the value of the
|
||||
forever option. If forever is false, zdaemon gives up at
|
||||
this point, and exits. An always-crashing subprocess will
|
||||
have been restarted exactly backoff-limit times in this case.
|
||||
If forever is true, zdaemon continues to attempt to restart
|
||||
the process, keeping the delay at backoff-limit seconds.
|
||||
|
||||
If the subprocess stays up for more than backoff-limit
|
||||
seconds, the delay is reset to 1 second.
|
||||
|
||||
This defaults to 10.
|
||||
|
||||
forever
|
||||
Command-line option: -f or --forever.
|
||||
|
||||
If this option is true, zdaemon will keep restarting a
|
||||
crashing subprocess forever. If it is false, it will give up
|
||||
after backoff-limit crashes in a row. See the description of
|
||||
backoff-limit for details.
|
||||
|
||||
This is disabled by default.
|
||||
|
||||
exit-codes
|
||||
Command-line option: -x or --exit-codes.
|
||||
|
||||
This defaults to 0,2.
|
||||
|
||||
If the subprocess exits with an exit status that is equal to
|
||||
one of the integers in this list, zdaemon will not restart
|
||||
it. The default list requires some explanation. Exit status
|
||||
0 is considered a willful successful exit; the ZEO and Zope
|
||||
server processes use this exit status when they want to stop
|
||||
without being restarted. (Including in response to a
|
||||
SIGTERM.) Exit status 2 is typically issued for command line
|
||||
syntax errors; in this case, restarting the program will not
|
||||
help!
|
||||
|
||||
NOTE: this mechanism overrides the backoff-limit and forever
|
||||
options; i.e. even if forever is true, a subprocess exit
|
||||
status code in this list makes zdaemon give up. To disable
|
||||
this, change the value to an empty list.
|
||||
|
||||
start-test-program
|
||||
A command that tests whether the program is up and running.
|
||||
The command should exit with a zero exit statis if the program
|
||||
is running and with a non-zero status otherwise.
|
||||
|
||||
start-timeout
|
||||
Command-line option: -T or --start-timeout.
|
||||
|
||||
If the program takes more than ``start-timeout`` seconds to
|
||||
start, then an error is printed and the control script will
|
||||
exit with a non-zero exit status.
|
||||
|
||||
stop-timeout
|
||||
This defaults to 300 seconds (5 minutes).
|
||||
|
||||
When a stop command is issued, a SIGTERM signal is sent to the
|
||||
process. zdaemon waits for stop-timeout seconds for the
|
||||
process to gracefully exit. If the process doesn't exit in
|
||||
that time, a SIGKILL signal is sent.
|
||||
|
||||
user
|
||||
Command-line option: -u or --user.
|
||||
|
||||
When zdaemon is started by root, this option specifies the
|
||||
user as who the the zdaemon process (and hence the daemon
|
||||
subprocess) will run. This can be a user name or a numeric
|
||||
user id. Both the user and the group are set from the
|
||||
corresponding password entry, using setuid() and setgid().
|
||||
This is done before zdaemon does anything else besides
|
||||
parsing its command line arguments.
|
||||
|
||||
NOTE: when zdaemon is not started by root, specifying this
|
||||
option is an error. (XXX This may be a mistake.)
|
||||
|
||||
XXX The zdaemon event log file may be opened *before*
|
||||
setuid() is called. Is this good or bad?
|
||||
|
||||
umask
|
||||
Command-line option: -m or --umask.
|
||||
|
||||
When daemon mode is used, this option specifies the octal umask
|
||||
of the subprocess.
|
||||
|
||||
default-to-interactive
|
||||
If this option is true, zdaemon enters interactive mode
|
||||
when it is invoked without a positional command argument. If
|
||||
it is false, you must use the -i or --interactive command line
|
||||
option to zdaemon to enter interactive mode.
|
||||
|
||||
This is enabled by default.
|
||||
|
||||
logfile
|
||||
Command-line option: -l or --logfile.
|
||||
|
||||
This option specifies a log file that is the default target of
|
||||
the "logtail" zdaemon command.
|
||||
|
||||
NOTE: This is NOT the log file to which zdaemon writes its
|
||||
logging messages! That log file is specified by the
|
||||
<eventlog> section described below.
|
||||
|
||||
transcript
|
||||
Command-line option: -t or --transcript.
|
||||
|
||||
The name of a file in which a transcript of all output from
|
||||
the command being run will be written to when daemonized.
|
||||
|
||||
If not specified, output from the command will be discarded.
|
||||
|
||||
This only takes effect when the "daemon" option is enabled.
|
||||
|
||||
prompt
|
||||
The prompt shown by the controller program. The default must
|
||||
be provided by the application.
|
||||
|
||||
(Note that a few other options are available to support old
|
||||
configuration files, but aren't needed any more and can generally be
|
||||
ignored.)
|
||||
|
||||
In addition to the runner section, you can use an eventlog section
|
||||
that specified one or more logfile subsections::
|
||||
|
||||
<eventlog>
|
||||
<logfile>
|
||||
path /var/log/foo/foo.log
|
||||
</logfile>
|
||||
|
||||
<logfile>
|
||||
path STDOUT
|
||||
</logfile>
|
||||
</eventlog>
|
||||
|
||||
In this example, log output is sent to a file and to standard out.
|
||||
Log output from zdaemon usually isn't very interesting but can be
|
||||
handy for debugging.
|
||||
@@ -0,0 +1,14 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""zdaemon -- a package to manage a daemon application."""
|
||||
@@ -0,0 +1,2 @@
|
||||
from zdaemon.zdctl import main
|
||||
main()
|
||||
@@ -0,0 +1,328 @@
|
||||
<component>
|
||||
|
||||
<!-- Note on logging configuration:
|
||||
|
||||
This schema component expects to use a section type named
|
||||
"eventlog"; this type needs to be provided by some other
|
||||
component that the top-level schema needs to import.
|
||||
|
||||
The import is not performed here to allow applications to
|
||||
load the type from different components.
|
||||
-->
|
||||
|
||||
<sectiontype name="runner">
|
||||
|
||||
<description>
|
||||
This section describes the options for zdctl.py and zdrun.py.
|
||||
The only required option is "program". Many other options have
|
||||
no default value specified in the schema; in some cases, the
|
||||
program calculates a dynamic default, in others, the feature
|
||||
associated with the option is disabled.
|
||||
|
||||
For those options that also have corresponding command-line
|
||||
options, the command line option (short and long form) are given
|
||||
here too.
|
||||
</description>
|
||||
|
||||
<section name="*" type="ZConfig.logger.log"
|
||||
attribute="eventlog"
|
||||
required="no">
|
||||
<description>
|
||||
Log configuration for zdctl.py and zdrun.py. These
|
||||
applications will normally use the eventlog section at the top
|
||||
level of the configuration, but will use this eventlog section
|
||||
if it exists.
|
||||
|
||||
(This is done so that the combined schema for the runner and
|
||||
the controlled application will write to the same logs by
|
||||
default, but a separation of logs can be achieved if desired.)
|
||||
</description>
|
||||
</section>
|
||||
|
||||
<key name="program" datatype="string-list"
|
||||
required="yes">
|
||||
<description>
|
||||
Command-line option: -p or --program (zdctl.py only).
|
||||
|
||||
This option gives the command used to start the subprocess
|
||||
managed by zdrun.py. This is currently a simple list of
|
||||
whitespace-delimited words. The first word is the program
|
||||
file, subsequent words are its command line arguments. If the
|
||||
program file contains no slashes, it is searched using $PATH.
|
||||
(XXX There is no way to to include whitespace in the program
|
||||
file or an argument, and under certain circumstances other
|
||||
shell metacharacters are also a problem, e.g. the "foreground"
|
||||
command of zdctl.py.)
|
||||
|
||||
NOTE: zdrun.py doesn't use this option; it uses its positional
|
||||
arguments. Rather, zdctl.py uses this option to determine the
|
||||
positional argument with which to invoke zdrun.py. (XXX This
|
||||
could be better.)
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="python" datatype="existing-path"
|
||||
required="no">
|
||||
<description>
|
||||
Path to the Python interpreter. Used by zdctl.py to start the
|
||||
zdrun.py process. Defaults to sys.executable.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="zdrun" datatype="existing-path"
|
||||
required="no">
|
||||
<description>
|
||||
Path to the zdrun.py script. Used by zdctl.py to start the
|
||||
zdrun.py process. Defaults to a file named "zdrun.py" in the
|
||||
same directory as zdctl.py.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="socket-name"
|
||||
datatype="zdaemon.zdoptions.existing_parent_dirpath"
|
||||
required="no"
|
||||
default="zdsock">
|
||||
<description>
|
||||
Command-line option: -s or --socket-name.
|
||||
|
||||
The pathname of the Unix domain socket used for communication
|
||||
between zdctl.py and zdrun.py. The default is relative to the
|
||||
current directory in which zdctl.py and zdrun.py are started.
|
||||
You want to specify an absolute pathname here.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="daemon" datatype="boolean"
|
||||
required="no"
|
||||
default="on">
|
||||
<description>
|
||||
Command-line option: -d or --daemon.
|
||||
|
||||
If this option is true, zdrun.py runs in the background as a
|
||||
true daemon. It forks a child process which becomes the
|
||||
subprocess manager, while the parent exits (making the shell
|
||||
that started it believe it is done). The child process also
|
||||
does the following:
|
||||
|
||||
- if the directory option is set, change into that directory
|
||||
|
||||
- redirect stdin, stdout and stderr to /dev/null
|
||||
|
||||
- call setsid() so it becomes a session leader
|
||||
|
||||
- call umask() with specified value
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="directory"
|
||||
datatype="zdaemon.zdoptions.existing_parent_directory"
|
||||
required="no">
|
||||
<description>
|
||||
Command-line option: -z or --directory.
|
||||
|
||||
If the daemon option is true, this option can specify a
|
||||
directory into which zdrun.py changes as part of the
|
||||
"daemonizing". If the daemon option is false, this option is
|
||||
ignored.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="backoff-limit" datatype="integer"
|
||||
required="no"
|
||||
default="10">
|
||||
<description>
|
||||
Command-line option: -b or --backoff-limit.
|
||||
|
||||
When the subprocess crashes, zdrun.py inserts a one-second
|
||||
delay before it restarts it. When the subprocess crashes
|
||||
again right away, the delay is incremented by one second, and
|
||||
so on. What happens when the delay has reached the value of
|
||||
backoff-limit (in seconds), depends on the value of the
|
||||
forever option. If forever is false, zdrun.py gives up at
|
||||
this point, and exits. An always-crashing subprocess will
|
||||
have been restarted exactly backoff-limit times in this case.
|
||||
If forever is true, zdrun.py continues to attempt to restart
|
||||
the process, keeping the delay at backoff-limit seconds.
|
||||
|
||||
If the subprocess stays up for more than backoff-limit
|
||||
seconds, the delay is reset to 1 second.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="forever" datatype="boolean"
|
||||
required="no"
|
||||
default="false">
|
||||
<description>
|
||||
Command-line option: -f or --forever.
|
||||
|
||||
If this option is true, zdrun.py will keep restarting a
|
||||
crashing subprocess forever. If it is false, it will give up
|
||||
after backoff-limit crashes in a row. See the description of
|
||||
backoff-limit for details.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="exit-codes" datatype="zdaemon.zdoptions.list_of_ints"
|
||||
required="no"
|
||||
default="0,2">
|
||||
<description>
|
||||
Command-line option: -x or --exit-codes.
|
||||
|
||||
If the subprocess exits with an exit status that is equal to
|
||||
one of the integers in this list, zdrun.py will not restart
|
||||
it. The default list requires some explanation. Exit status
|
||||
0 is considered a willful successful exit; the ZEO and Zope
|
||||
server processes use this exit status when they want to stop
|
||||
without being restarted. (Including in response to a
|
||||
SIGTERM.) Exit status 2 is typically issued for command line
|
||||
syntax errors; in this case, restarting the program will not
|
||||
help!
|
||||
|
||||
NOTE: this mechanism overrides the backoff-limit and forever
|
||||
options; i.e. even if forever is true, a subprocess exit
|
||||
status code in this list makes zdrun.py give up. To disable
|
||||
this, change the value to an empty list.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="start-test-program" datatype="string-list"
|
||||
required="no">
|
||||
<description>
|
||||
Command-line option: -p or --program (zdctl.py only).
|
||||
|
||||
This option gives the command used to start the subprocess
|
||||
managed by zdrun.py. This is currently a simple list of
|
||||
whitespace-delimited words. The first word is the program
|
||||
file, subsequent words are its command line arguments. If the
|
||||
program file contains no slashes, it is searched using $PATH.
|
||||
(XXX There is no way to to include whitespace in the program
|
||||
file or an argument, and under certain circumstances other
|
||||
shell metacharacters are also a problem, e.g. the "foreground"
|
||||
command of zdctl.py.)
|
||||
|
||||
NOTE: zdrun.py doesn't use this option; it uses its positional
|
||||
arguments. Rather, zdctl.py uses this option to determine the
|
||||
positional argument with which to invoke zdrun.py. (XXX This
|
||||
could be better.)
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="start-timeout" datatype="integer" required="no"
|
||||
default="300">
|
||||
<description>
|
||||
When a start-test-program is supplied, a process won't be
|
||||
considered to be started until the test program exits normally
|
||||
or until start-timout seconds have passed.
|
||||
|
||||
This defaults to 300 seconds (5 minutes).
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="stop-timeout" datatype="integer" required="no" default="300">
|
||||
<description>
|
||||
When a stop command is issued, a SIGTERM signal is sent to the
|
||||
process. zdaemon waits for stop-timeout seconds for the
|
||||
process to gracefully exit. If the process doesn't exit in
|
||||
that time, a SIGKILL signal is sent.
|
||||
|
||||
This defaults to 300 seconds (5 minutes).
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="user" datatype="string"
|
||||
required="no">
|
||||
<description>
|
||||
Command-line option: -u or --user.
|
||||
|
||||
When zdrun.py is started by root, this option specifies the
|
||||
user as who the the zdrun.py process (and hence the daemon
|
||||
subprocess) will run. This can be a user name or a numeric
|
||||
user id. Both the user and the group are set from the
|
||||
corresponding password entry, using setuid() and setgid().
|
||||
This is done before zdrun.py does anything else besides
|
||||
parsing its command line arguments.
|
||||
|
||||
NOTE: when zdrun.py is not started by root, specifying this
|
||||
option is an error. (XXX This may be a mistake.)
|
||||
|
||||
XXX The zdrun.py event log file may be opened *before*
|
||||
setuid() is called. Is this good or bad?
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="umask" datatype="zdaemon.zdoptions.octal_type"
|
||||
required="no"
|
||||
default="022">
|
||||
<description>
|
||||
Command-line option: -m or --umask.
|
||||
|
||||
When daemon mode is used, this option specifies the octal umask
|
||||
of the subprocess.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="hang-around" datatype="boolean"
|
||||
required="no"
|
||||
default="false">
|
||||
<description>
|
||||
If this option is true, the zdrun.py process will remain even
|
||||
when the daemon subprocess is stopped. In this case, zdctl.py
|
||||
will restart zdrun.py as necessary. If this option is false,
|
||||
zdrun.py will exit when the daemon subprocess is stopped
|
||||
(unless zdrun.py intends to restart it).
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="default-to-interactive" datatype="boolean"
|
||||
required="no"
|
||||
default="true">
|
||||
<description>
|
||||
If this option is true, zdctl.py enters interactive mode
|
||||
when it is invoked without a positional command argument. If
|
||||
it is false, you must use the -i or --interactive command line
|
||||
option to zdctl.py to enter interactive mode.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="logfile" datatype="existing-dirpath"
|
||||
required="no">
|
||||
<description>
|
||||
This option specifies a log file that is the default target of
|
||||
the "logtail" zdctl.py command.
|
||||
|
||||
NOTE: This is NOT the log file to which zdrun.py writes its
|
||||
logging messages! That log file is specified by the
|
||||
<eventlog> section.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="transcript" datatype="existing-dirpath"
|
||||
required="no">
|
||||
<description>
|
||||
The name of a file in which a transcript of all output from
|
||||
the command being run will be written to when daemonized.
|
||||
|
||||
If not specified, output from the command will be discarded.
|
||||
|
||||
This only takes effect when the "daemon" option is enabled.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="prompt" datatype="string"
|
||||
required="no">
|
||||
<description>
|
||||
The prompt shown by the controller program. The default must
|
||||
be provided by the application.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
</sectiontype>
|
||||
|
||||
<sectiontype name="environment" keytype="string">
|
||||
<key name="+"
|
||||
attribute="mapping"
|
||||
required="no"
|
||||
/>
|
||||
</sectiontype>
|
||||
|
||||
</component>
|
||||
@@ -0,0 +1,24 @@
|
||||
# Sample config file for zdctl.py and zdrun.py (which share a schema).
|
||||
|
||||
<runner>
|
||||
# Harmless example
|
||||
program sleep 100
|
||||
# Repeat the defaults
|
||||
backoff-limit 10
|
||||
daemon True
|
||||
forever True
|
||||
socket-name zdsock
|
||||
exit-codes 0,2
|
||||
# user has no default
|
||||
umask 022
|
||||
directory .
|
||||
default-to-interactive True
|
||||
hang-around False
|
||||
</runner>
|
||||
|
||||
<eventlog>
|
||||
level info
|
||||
<logfile>
|
||||
path /tmp/zdrun.log
|
||||
</logfile>
|
||||
</eventlog>
|
||||
@@ -0,0 +1,28 @@
|
||||
<schema>
|
||||
|
||||
<description>
|
||||
This schema describes various options that control zdctl.py and
|
||||
zdrun.py. zdrun.py is the "daemon process manager"; it runs a
|
||||
subprocess in the background and restarts it when it crashes.
|
||||
zdctl.py is the user interface to zdrun.py; it can tell zdrun.py
|
||||
to start, stop or restart the subprocess, send it a signal, etc.
|
||||
|
||||
There are two sections: <runner> defines options unique
|
||||
zdctl.py and zdrun.py, and <eventlog> defines a standard
|
||||
event logging section used by zdrun.py.
|
||||
|
||||
More information about zdctl.py and zdrun.py can be found in the
|
||||
file Doc/zdctl.txt. This all is specific to Unix/Linux.
|
||||
</description>
|
||||
|
||||
<import package="ZConfig.components.logger"/>
|
||||
|
||||
<import package="zdaemon"/>
|
||||
|
||||
<section name="*" type="runner" attribute="runner" required="yes" />
|
||||
|
||||
<section name="*" type="environment" attribute="environment" required="no" />
|
||||
|
||||
<section name="*" type="eventlog" attribute="eventlog" required="no" />
|
||||
|
||||
</schema>
|
||||
@@ -0,0 +1 @@
|
||||
# This file is needed to make this a package.
|
||||
@@ -0,0 +1,8 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
import signal
|
||||
|
||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||
|
||||
while 1:
|
||||
signal.pause()
|
||||
@@ -0,0 +1,58 @@
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
|
||||
donothing_contents = """\
|
||||
#!/bin/sh
|
||||
while [ "1" -ne "2" ]; do
|
||||
sleep 10
|
||||
done
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
# dummy zdctl startup of zdrun
|
||||
shutup()
|
||||
file = os.path.normpath(os.path.abspath(sys.argv[0]))
|
||||
tmp = sys.argv[1]
|
||||
dir = os.path.dirname(file)
|
||||
zctldir = os.path.dirname(dir)
|
||||
zdrun = os.path.join(zctldir, 'zdrun.py')
|
||||
donothing = os.path.join(tmp, 'donothing.sh')
|
||||
fd = os.open(donothing, os.O_WRONLY | os.O_CREAT, 0o700)
|
||||
os.write(fd, donothing_contents.encode())
|
||||
os.close(fd)
|
||||
args = [sys.executable, zdrun]
|
||||
args += ['-d', '-b', '10', '-s', os.path.join(tmp, 'testsock'),
|
||||
'-x', '0,2', '-z', dir, donothing]
|
||||
flag = os.P_NOWAIT
|
||||
os.spawnvpe(flag, args[0], args,
|
||||
dict(os.environ, PYTHONPATH=':'.join(sys.path)),
|
||||
)
|
||||
while 1:
|
||||
# wait to be signaled
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def shutup():
|
||||
os.close(0)
|
||||
sys.stdin = sys.__stdin__ = open("/dev/null")
|
||||
try: # PEP 446, Python >= 3.4
|
||||
os.set_inheritable(sys.stdin.fileno(), True)
|
||||
except AttributeError:
|
||||
pass
|
||||
os.close(1)
|
||||
sys.stdout = sys.__stdout__ = open("/dev/null", "w")
|
||||
try: # PEP 446, Python >= 3.4
|
||||
os.set_inheritable(sys.stdout.fileno(), True)
|
||||
except AttributeError:
|
||||
pass
|
||||
os.close(2)
|
||||
sys.stderr = sys.__stderr__ = open("/dev/null", "w")
|
||||
try: # PEP 446, Python >= 3.4
|
||||
os.set_inheritable(sys.stderr.fileno(), True)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,530 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2004 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
from __future__ import print_function
|
||||
|
||||
import doctest
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
|
||||
import ZConfig
|
||||
import manuel.capture
|
||||
import manuel.doctest
|
||||
import manuel.testing
|
||||
import zc.customdoctests
|
||||
import zdaemon
|
||||
from zope.testing import renormalizing
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
zdaemon_loc = pkg_resources.working_set.find(
|
||||
pkg_resources.Requirement.parse('zdaemon')).location
|
||||
zconfig_loc = pkg_resources.working_set.find(
|
||||
pkg_resources.Requirement.parse('ZConfig')).location
|
||||
except (ImportError, AttributeError):
|
||||
zdaemon_loc = os.path.dirname(os.path.dirname(zdaemon.__file__))
|
||||
zconfig_loc = os.path.dirname(os.path.dirname(ZConfig.__file__))
|
||||
|
||||
|
||||
def write(name, text):
|
||||
with open(name, 'w') as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def read(name):
|
||||
with open(name) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def make_sure_non_daemon_mode_doesnt_hang_when_program_exits():
|
||||
"""
|
||||
The whole awhile bit that waits for a program to start
|
||||
whouldn't be used on non-daemon mode.
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program sleep 1
|
||||
... daemon off
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> system("./zdaemon -Cconf start")
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def dont_hang_when_program_doesnt_start():
|
||||
"""
|
||||
If a program doesn't start, we don't want to wait for ever.
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program sleep
|
||||
... backoff-limit 2
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> system("./zdaemon -Cconf start")
|
||||
. .
|
||||
daemon manager not running
|
||||
Failed: 1
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def allow_duplicate_arguments():
|
||||
"""
|
||||
Wrapper scripts will often embed configuration arguments. This could
|
||||
cause a problem when zdaemon reinvokes itself, passing it's own set of
|
||||
configuration arguments. To deal with this, we'll allow duplicate
|
||||
arguments that have the same values.
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program sleep 10
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> system("./zdaemon -Cconf -Cconf -Cconf start")
|
||||
. .
|
||||
daemon process started, pid=21446
|
||||
|
||||
>>> system("./zdaemon -Cconf -Cconf -Cconf stop")
|
||||
. .
|
||||
daemon process stopped
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def test_stop_timeout():
|
||||
r"""
|
||||
|
||||
>>> write('t.py',
|
||||
... '''
|
||||
... import time, signal
|
||||
... signal.signal(signal.SIGTERM, lambda *a: None)
|
||||
... while 1: time.sleep(9)
|
||||
... ''')
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program %s t.py
|
||||
... stop-timeout 1
|
||||
... </runner>
|
||||
... ''' % sys.executable)
|
||||
|
||||
>>> system("./zdaemon -Cconf start")
|
||||
. .
|
||||
daemon process started, pid=21446
|
||||
|
||||
>>> import threading, time
|
||||
>>> thread = threading.Thread(
|
||||
... target=system, args=("./zdaemon -Cconf stop",),
|
||||
... kwargs=dict(quiet=True))
|
||||
>>> thread.start()
|
||||
>>> time.sleep(.2)
|
||||
|
||||
>>> system("./zdaemon -Cconf status")
|
||||
program running; pid=15372
|
||||
|
||||
>>> thread.join(2)
|
||||
|
||||
>>> system("./zdaemon -Cconf status")
|
||||
daemon manager not running
|
||||
Failed: 3
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def test_kill():
|
||||
"""
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program sleep 100
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> system("./zdaemon -Cconf start")
|
||||
. .
|
||||
daemon process started, pid=1234
|
||||
|
||||
>>> system("./zdaemon -Cconf kill ded")
|
||||
invalid signal 'ded'
|
||||
|
||||
>>> system("./zdaemon -Cconf kill CONT")
|
||||
kill(1234, 18)
|
||||
signal SIGCONT sent to process 1234
|
||||
|
||||
>>> system("./zdaemon -Cconf stop")
|
||||
. .
|
||||
daemon process stopped
|
||||
|
||||
>>> system("./zdaemon -Cconf kill")
|
||||
daemon process not running
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def test_logreopen():
|
||||
"""
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program sleep 100
|
||||
... transcript transcript.log
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> system("./zdaemon -Cconf start")
|
||||
. .
|
||||
daemon process started, pid=1234
|
||||
|
||||
>>> os.rename('transcript.log', 'transcript.log.1')
|
||||
|
||||
>>> system("./zdaemon -Cconf logreopen")
|
||||
kill(1234, 12)
|
||||
signal SIGUSR2 sent to process 1234
|
||||
|
||||
This also reopens the transcript.log:
|
||||
|
||||
>>> sorted(os.listdir('.'))
|
||||
['conf', 'transcript.log', 'transcript.log.1', 'zdaemon', 'zdsock']
|
||||
|
||||
>>> system("./zdaemon -Cconf stop")
|
||||
. .
|
||||
daemon process stopped
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def test_log_rotation():
|
||||
"""
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program sleep 100
|
||||
... transcript transcript.log
|
||||
... </runner>
|
||||
... <eventlog>
|
||||
... <logfile>
|
||||
... path event.log
|
||||
... </logfile>
|
||||
... </eventlog>
|
||||
... ''')
|
||||
|
||||
>>> system("./zdaemon -Cconf start")
|
||||
. .
|
||||
daemon process started, pid=1234
|
||||
|
||||
Pretend we did a logrotate:
|
||||
|
||||
>>> os.rename('transcript.log', 'transcript.log.1')
|
||||
>>> os.rename('event.log', 'event.log.1')
|
||||
|
||||
>>> system("./zdaemon -Cconf reopen_transcript") # or logreopen
|
||||
|
||||
This reopens both transcript.log and event.log:
|
||||
|
||||
>>> sorted(glob.glob('transcript.log*'))
|
||||
['transcript.log', 'transcript.log.1']
|
||||
|
||||
>>> sorted(glob.glob('event.log*'))
|
||||
['event.log', 'event.log.1']
|
||||
|
||||
>>> system("./zdaemon -Cconf stop")
|
||||
. .
|
||||
daemon process stopped
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def test_start_test_program():
|
||||
"""
|
||||
>>> write('t.py',
|
||||
... '''
|
||||
... import time
|
||||
... time.sleep(1)
|
||||
... open('x', 'w').close()
|
||||
... time.sleep(99)
|
||||
... ''')
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program %s t.py
|
||||
... start-test-program cat x
|
||||
... </runner>
|
||||
... ''' % sys.executable)
|
||||
|
||||
>>> import os
|
||||
|
||||
>>> system("./zdaemon -Cconf start")
|
||||
. .
|
||||
daemon process started, pid=21446
|
||||
|
||||
>>> os.path.exists('x')
|
||||
True
|
||||
>>> os.remove('x')
|
||||
|
||||
>>> system("./zdaemon -Cconf restart")
|
||||
. . .
|
||||
daemon process restarted, pid=19622
|
||||
>>> os.path.exists('x')
|
||||
True
|
||||
|
||||
>>> system("./zdaemon -Cconf stop")
|
||||
<BLANKLINE>
|
||||
daemon process stopped
|
||||
"""
|
||||
|
||||
|
||||
def test_start_timeout():
|
||||
"""
|
||||
>>> write('t.py',
|
||||
... '''
|
||||
... import time
|
||||
... time.sleep(9)
|
||||
... ''')
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program %s t.py
|
||||
... start-test-program cat x
|
||||
... start-timeout 1
|
||||
... </runner>
|
||||
... ''' % sys.executable)
|
||||
|
||||
>>> import time
|
||||
>>> start = time.time()
|
||||
|
||||
>>> system("./zdaemon -Cconf start")
|
||||
<BLANKLINE>
|
||||
Program took too long to start
|
||||
Failed: 1
|
||||
|
||||
>>> system("./zdaemon -Cconf stop")
|
||||
<BLANKLINE>
|
||||
daemon process stopped
|
||||
"""
|
||||
|
||||
|
||||
def DAEMON_MANAGER_MODE_leak():
|
||||
"""
|
||||
Zdaemon used an environment variable to flag that it's running in
|
||||
daemon-manager mode, as opposed to UI mode. If this environment
|
||||
variable is allowed to leak to the program, them the program will
|
||||
be unable to invoke zdaemon correctly.
|
||||
|
||||
>>> write('c', '''
|
||||
... <runner>
|
||||
... program env
|
||||
... transcript t
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> system('./zdaemon -b0 -T1 -Cc start', quiet=True)
|
||||
Failed: 1
|
||||
>>> 'DAEMON_MANAGER_MODE' not in read('t')
|
||||
True
|
||||
"""
|
||||
|
||||
|
||||
def nonzero_exit_on_program_failure():
|
||||
"""
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... backoff-limit 1
|
||||
... program nosuch
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> system("./zdaemon -Cconf start", echo=True) # doctest: +ELLIPSIS
|
||||
./zdaemon...
|
||||
daemon manager not running
|
||||
Failed: 1
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... backoff-limit 1
|
||||
... program cat nosuch
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> system("./zdaemon -Cconf start", echo=True) # doctest: +ELLIPSIS
|
||||
./zdaemon...
|
||||
daemon manager not running
|
||||
Failed: 1
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... backoff-limit 1
|
||||
... program pwd
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> system("./zdaemon -Cconf start", echo=True) # doctest: +ELLIPSIS
|
||||
./zdaemon...
|
||||
daemon manager not running
|
||||
Failed: 1
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def setUp(test):
|
||||
test.globs['_td'] = td = []
|
||||
here = os.getcwd()
|
||||
td.append(lambda: os.chdir(here))
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
td.append(lambda: shutil.rmtree(tmpdir))
|
||||
test.globs['tmpdir'] = tmpdir
|
||||
workspace = tempfile.mkdtemp()
|
||||
td.append(lambda: shutil.rmtree(workspace))
|
||||
os.chdir(workspace)
|
||||
write('zdaemon', zdaemon_template % dict(
|
||||
python=sys.executable,
|
||||
zdaemon=zdaemon_loc,
|
||||
ZConfig=zconfig_loc,
|
||||
))
|
||||
os.chmod('zdaemon', 0o755)
|
||||
test.globs['system'] = system
|
||||
|
||||
|
||||
def tearDown(test):
|
||||
for f in test.globs['_td']:
|
||||
f()
|
||||
|
||||
|
||||
class Timeout(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
@contextmanager
|
||||
def timeout(seconds):
|
||||
this_frame = sys._getframe()
|
||||
|
||||
def raiseTimeout(signal, frame):
|
||||
# the if statement here is meant to prevent an exception in the
|
||||
# finally: clause before clean up can take place
|
||||
if frame is not this_frame:
|
||||
raise Timeout('timed out after %s seconds' % seconds)
|
||||
|
||||
try:
|
||||
prev_handler = signal.signal(signal.SIGALRM, raiseTimeout)
|
||||
except ValueError:
|
||||
# signal only works in main thread
|
||||
# let's ignore the request for a timeout and hope the test doesn't hang
|
||||
yield
|
||||
else:
|
||||
try:
|
||||
signal.alarm(seconds)
|
||||
yield
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
signal.signal(signal.SIGALRM, prev_handler)
|
||||
|
||||
|
||||
def system(command, input='', quiet=False, echo=False):
|
||||
if echo:
|
||||
print(command)
|
||||
p = subprocess.Popen(
|
||||
command, shell=True,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
with timeout(60):
|
||||
data = p.communicate(input)[0]
|
||||
if not quiet:
|
||||
print(data.decode(), end='')
|
||||
r = p.wait()
|
||||
if r:
|
||||
print('Failed:', r)
|
||||
|
||||
|
||||
def checkenv(match):
|
||||
match = [a for a in match.group(1).split('\n')[:-1]
|
||||
if a.split('=')[0] in ('HOME', 'LD_LIBRARY_PATH')]
|
||||
match.sort()
|
||||
return '\n'.join(match) + '\n'
|
||||
|
||||
|
||||
zdaemon_template = """#!%(python)s
|
||||
|
||||
import sys
|
||||
sys.path[0:0] = [
|
||||
%(zdaemon)r,
|
||||
%(ZConfig)r,
|
||||
]
|
||||
|
||||
try:
|
||||
import coverage
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
coverage.process_startup()
|
||||
|
||||
import zdaemon.zdctl
|
||||
|
||||
if __name__ == '__main__':
|
||||
zdaemon.zdctl.main()
|
||||
"""
|
||||
|
||||
|
||||
def test_suite():
|
||||
README_checker = renormalizing.RENormalizing([
|
||||
(re.compile('pid=\d+'), 'pid=NNN'),
|
||||
(re.compile('(\. )+\.?'), '<BLANKLINE>'),
|
||||
(re.compile('^env\n((?:.*\n)+)$'), checkenv),
|
||||
])
|
||||
|
||||
return unittest.TestSuite((
|
||||
doctest.DocTestSuite(
|
||||
setUp=setUp, tearDown=tearDown,
|
||||
checker=renormalizing.RENormalizing([
|
||||
(re.compile('pid=\d+'), 'pid=NNN'),
|
||||
(re.compile('(\. )+\.?'), '<BLANKLINE>'),
|
||||
(re.compile('process \d+'), 'process NNN'),
|
||||
(re.compile('kill\(\d+,'), 'kill(NNN,'),
|
||||
])),
|
||||
manuel.testing.TestSuite(
|
||||
manuel.doctest.Manuel(
|
||||
parser=zc.customdoctests.DocTestParser(
|
||||
ps1='sh>',
|
||||
transform=lambda s: 'system("%s")\n' % s.rstrip()
|
||||
),
|
||||
checker=README_checker,
|
||||
) +
|
||||
manuel.doctest.Manuel(checker=README_checker) +
|
||||
manuel.capture.Manuel(),
|
||||
'../README.rst',
|
||||
setUp=setUp, tearDown=tearDown),
|
||||
))
|
||||
@@ -0,0 +1,144 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2010 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Test user and groups options
|
||||
|
||||
from zope.testing import setupstack
|
||||
import doctest
|
||||
import mock
|
||||
import os
|
||||
import sys
|
||||
import zdaemon.zdctl
|
||||
|
||||
|
||||
def write(name, text):
|
||||
with open(name, 'w') as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
class O:
|
||||
def __init__(self, **kw):
|
||||
self.__dict__.update(kw)
|
||||
|
||||
|
||||
def test_user_fails_when_not_root():
|
||||
"""
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program sleep 9
|
||||
... user zope
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> with mock.patch('os.geteuid') as geteuid:
|
||||
... with mock.patch('sys.stderr'):
|
||||
... sys.stderr = sys.stdout
|
||||
... geteuid.return_value = 42
|
||||
... try:
|
||||
... zdaemon.zdctl.main(['-C', 'conf', 'status'])
|
||||
... except SystemExit:
|
||||
... pass
|
||||
... else:
|
||||
... print('oops')
|
||||
... # doctest: +ELLIPSIS
|
||||
Error: only root can use -u USER to change users
|
||||
For help, use ... -h
|
||||
|
||||
>>> import pwd
|
||||
>>> pwd.getpwnam.assert_called_with('zope')
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def test_user_sets_supplemtary_groups():
|
||||
"""
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program sleep 9
|
||||
... user zope
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> import grp
|
||||
>>> grp.getgrall.return_value = [
|
||||
... O(gr_gid=8, gr_mem =['g', 'zope', ]),
|
||||
... O(gr_gid=1, gr_mem =['a', 'x', ]),
|
||||
... O(gr_gid=2, gr_mem =['b', 'x', 'zope']),
|
||||
... O(gr_gid=5, gr_mem =['c', 'x', ]),
|
||||
... O(gr_gid=4, gr_mem =['d', 'x', ]),
|
||||
... O(gr_gid=3, gr_mem =['e', 'x', 'zope', ]),
|
||||
... O(gr_gid=6, gr_mem =['f', ]),
|
||||
... O(gr_gid=7, gr_mem =['h', ]),
|
||||
... ]
|
||||
|
||||
>>> with mock.patch('sys.exit'):
|
||||
... zdaemon.zdctl.main(['-C', 'conf', 'status'])
|
||||
daemon manager not running
|
||||
|
||||
>>> import pwd, os
|
||||
>>> os.geteuid.assert_called_with()
|
||||
>>> pwd.getpwnam.assert_called_with('zope')
|
||||
>>> grp.getgrall.assert_called_with()
|
||||
>>> os.setuid.assert_called_with(99)
|
||||
>>> os.setgid.assert_called_with(5)
|
||||
>>> os.setgroups.assert_called_with([2, 3, 8])
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def test_do_nothing_if_effective_user_is_configured_user():
|
||||
"""
|
||||
|
||||
>>> write('conf',
|
||||
... '''
|
||||
... <runner>
|
||||
... program sleep 9
|
||||
... user zope
|
||||
... </runner>
|
||||
... ''')
|
||||
|
||||
>>> with mock.patch('os.geteuid') as geteuid:
|
||||
... with mock.patch('sys.exit'):
|
||||
... geteuid.return_value = 99
|
||||
... zdaemon.zdctl.main(['-C', 'conf', 'status'])
|
||||
... os.geteuid.assert_called_with()
|
||||
daemon manager not running
|
||||
|
||||
>>> import pwd, os, grp
|
||||
>>> pwd.getpwnam.assert_called_with('zope')
|
||||
>>> _ = grp.getgrall.assert_not_called()
|
||||
>>> _ = os.setuid.assert_not_called()
|
||||
>>> _ = os.setgid.assert_not_called()
|
||||
>>> _ = os.setgroups.assert_not_called()
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def setUp(test):
|
||||
setupstack.setUpDirectory(test)
|
||||
getpwname = setupstack.context_manager(test, mock.patch('pwd.getpwnam'))
|
||||
getpwname.return_value = O(pw_gid=5, pw_uid=99, pw_name='zope')
|
||||
setupstack.context_manager(test, mock.patch('os.geteuid')).return_value = 0
|
||||
setupstack.context_manager(test, mock.patch('grp.getgrall'))
|
||||
setupstack.context_manager(test, mock.patch('os.setgroups'))
|
||||
setupstack.context_manager(test, mock.patch('os.setuid'))
|
||||
setupstack.context_manager(test, mock.patch('os.setgid'))
|
||||
|
||||
|
||||
def test_suite():
|
||||
return doctest.DocTestSuite(setUp=setUp, tearDown=setupstack.tearDown)
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Test suite for zdctl.py."""
|
||||
|
||||
import doctest
|
||||
|
||||
from zdaemon import zdctl
|
||||
|
||||
|
||||
def run(args):
|
||||
options = zdctl.ZDCtlOptions()
|
||||
options.realize(['-p', 'true'] + args.split())
|
||||
cmd = zdctl.ZDCmd(options)
|
||||
cmd.onecmd(" ".join(options.args))
|
||||
|
||||
|
||||
def doctest_ZDCmd_help():
|
||||
"""Test for ZDCmd.help_xxx
|
||||
|
||||
>>> run("help")
|
||||
<BLANKLINE>
|
||||
Documented commands (type help <topic>):
|
||||
========================================
|
||||
fg help logreopen reopen_transcript show status wait
|
||||
foreground kill logtail restart start stop
|
||||
<BLANKLINE>
|
||||
|
||||
>>> run("help fg")
|
||||
foreground -- Run the program in the forground.
|
||||
fg -- an alias for foreground.
|
||||
|
||||
>>> run("help help")
|
||||
help -- Print a list of available actions.
|
||||
help <action> -- Print help for <action>.
|
||||
|
||||
>>> run("help kill")
|
||||
kill [sig] -- Send signal sig to the daemon process.
|
||||
The default signal is SIGTERM.
|
||||
|
||||
>>> run("help logreopen")
|
||||
logreopen -- Send a SIGUSR2 signal to the daemon process.
|
||||
This is designed to reopen the log file.
|
||||
Also reopens the transcript log file.
|
||||
|
||||
>>> run("help logtail")
|
||||
logtail [logfile] -- Run tail -f on the given logfile.
|
||||
A default file may exist.
|
||||
Hit ^C to exit this mode.
|
||||
|
||||
>>> run("help reopen_transcript")
|
||||
reopen_transcript -- Reopen the transcript log file.
|
||||
Use after log rotation.
|
||||
|
||||
>>> run("help restart")
|
||||
restart -- Stop and then start the daemon process.
|
||||
|
||||
>>> run("help show")
|
||||
show options -- show zdctl options
|
||||
show python -- show Python version and details
|
||||
show all -- show all of the above
|
||||
|
||||
>>> run("help start")
|
||||
start -- Start the daemon process.
|
||||
If it is already running, do nothing.
|
||||
|
||||
>>> run("help status")
|
||||
status [-l] -- Print status for the daemon process.
|
||||
With -l, show raw status output as well.
|
||||
|
||||
>>> run("help stop")
|
||||
stop -- Stop the daemon process.
|
||||
If it is not running, do nothing.
|
||||
|
||||
>>> run("help wait")
|
||||
wait -- Wait for the daemon process to exit.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def test_suite():
|
||||
return doctest.DocTestSuite(optionflags=doctest.NORMALIZE_WHITESPACE)
|
||||
@@ -0,0 +1,496 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Test suite for zdaemon.zdoptions."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
import unittest
|
||||
import doctest
|
||||
|
||||
import ZConfig
|
||||
import zdaemon
|
||||
from zdaemon.zdoptions import (
|
||||
ZDOptions, RunnerOptions, list_of_ints,
|
||||
existing_parent_directory, existing_parent_dirpath)
|
||||
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except:
|
||||
# Python 3 support.
|
||||
from io import StringIO
|
||||
|
||||
|
||||
class ZDOptionsTestBase(unittest.TestCase):
|
||||
|
||||
OptionsClass = ZDOptions
|
||||
|
||||
def save_streams(self):
|
||||
self.save_stdout = sys.stdout
|
||||
self.save_stderr = sys.stderr
|
||||
sys.stdout = self.stdout = StringIO()
|
||||
sys.stderr = self.stderr = StringIO()
|
||||
|
||||
def restore_streams(self):
|
||||
sys.stdout = self.save_stdout
|
||||
sys.stderr = self.save_stderr
|
||||
|
||||
def check_exit_code(self, options, args, exit_code=2):
|
||||
save_sys_stderr = sys.stderr
|
||||
try:
|
||||
sys.stderr = StringIO()
|
||||
try:
|
||||
options.realize(args)
|
||||
except SystemExit as err:
|
||||
self.assertEqual(err.code, exit_code)
|
||||
else:
|
||||
self.fail("SystemExit expected")
|
||||
finally:
|
||||
sys.stderr = save_sys_stderr
|
||||
|
||||
|
||||
class TestZDOptions(ZDOptionsTestBase):
|
||||
|
||||
input_args = ["arg1", "arg2"]
|
||||
output_opts = []
|
||||
output_args = ["arg1", "arg2"]
|
||||
|
||||
def test_basic(self):
|
||||
progname = "progname"
|
||||
doc = "doc"
|
||||
options = self.OptionsClass()
|
||||
options.positional_args_allowed = 1
|
||||
options.schemadir = os.path.dirname(zdaemon.__file__)
|
||||
options.realize(self.input_args, progname, doc)
|
||||
self.assertEqual(options.progname, "progname")
|
||||
self.assertEqual(options.doc, "doc")
|
||||
self.assertEqual(options.options, self.output_opts)
|
||||
self.assertEqual(options.args, self.output_args)
|
||||
|
||||
def test_configure(self):
|
||||
configfile = os.path.join(os.path.dirname(zdaemon.__file__),
|
||||
"sample.conf")
|
||||
for arg in "-C", "--c", "--configure":
|
||||
options = self.OptionsClass()
|
||||
options.realize([arg, configfile])
|
||||
self.assertEqual(options.configfile, configfile)
|
||||
|
||||
# The original intent was that the docstring of whatever module is
|
||||
# __main__ would be used as help documentation.
|
||||
# Because of the way buildout generates scripts, this will always
|
||||
# be an empty string.
|
||||
# So, we now use the __doc__ of the options class being used.
|
||||
|
||||
def help_test_helper(self, optionsclass, kw, expected):
|
||||
for arg in "-h", "--h", "--help":
|
||||
options = optionsclass()
|
||||
try:
|
||||
self.save_streams()
|
||||
try:
|
||||
options.realize([arg], **kw)
|
||||
finally:
|
||||
self.restore_streams()
|
||||
except SystemExit as err:
|
||||
self.assertEqual(err.code, 0)
|
||||
else:
|
||||
self.fail("%s didn't call sys.exit()" % repr(arg))
|
||||
helptext = self.stdout.getvalue()
|
||||
self.assertEqual(helptext, expected)
|
||||
|
||||
def test_default_help(self):
|
||||
# test what happens if OptionsClass is used directly.
|
||||
# Not sure this ever happens :-S
|
||||
self.help_test_helper(
|
||||
self.OptionsClass, {},
|
||||
self.OptionsClass.__doc__ or 'No help available.')
|
||||
|
||||
def test_default_subclass_help(self):
|
||||
# test what happens when the subclass doesn't do anything
|
||||
# with __doc__
|
||||
class SubClass(self.OptionsClass):
|
||||
pass
|
||||
# __doc__ isn't inherited :-(
|
||||
self.help_test_helper(SubClass, {}, 'No help available.')
|
||||
|
||||
def test_default_help_with_doc_kw(self):
|
||||
# test what happens when the subclass doesn't do anything
|
||||
# with __doc__, but doc is supplied to realize
|
||||
self.help_test_helper(self.OptionsClass,
|
||||
{'doc': 'Example help'},
|
||||
'Example help')
|
||||
|
||||
def test_no_help(self):
|
||||
# test what happens when the subclass has None for __doc__
|
||||
class NoHelp(self.OptionsClass):
|
||||
__doc__ = None
|
||||
self.help_test_helper(NoHelp, {}, 'No help available.')
|
||||
|
||||
def test_no_help_with_doc_kw(self):
|
||||
# test what happens when the subclass has None for __doc__,
|
||||
# but doc is supplied to realize
|
||||
class NoHelp(self.OptionsClass):
|
||||
__doc__ = None
|
||||
self.help_test_helper(NoHelp, {'doc': 'Example help'}, 'Example help')
|
||||
|
||||
def test_help(self):
|
||||
# test what happens when the subclass has None for __doc__
|
||||
class HasHelp(self.OptionsClass):
|
||||
__doc__ = 'Some help for %s'
|
||||
self.help_test_helper(HasHelp, {'progname': 'me'}, 'Some help for me')
|
||||
|
||||
def test_has_help_with_doc_kw(self):
|
||||
# test what happens when the subclass has something for __doc__,
|
||||
# and doc is also supplied to realize
|
||||
class HasHelp(self.OptionsClass):
|
||||
__doc__ = 'Some help'
|
||||
self.help_test_helper(HasHelp, {'doc': 'Example help'}, 'Example help')
|
||||
|
||||
def test_version(self):
|
||||
options = self.OptionsClass()
|
||||
options.version = '2.4.frog-knows'
|
||||
self.save_streams()
|
||||
try:
|
||||
self.check_exit_code(options, ['--version'], exit_code=0)
|
||||
finally:
|
||||
self.restore_streams()
|
||||
self.assertNotEqual(self.stdout.getvalue(), "2.4.frog-knows")
|
||||
|
||||
def test_unrecognized(self):
|
||||
# Check that we get an error for an unrecognized option
|
||||
self.check_exit_code(self.OptionsClass(), ["-/"])
|
||||
|
||||
|
||||
class TestBasicFunctionality(ZDOptionsTestBase):
|
||||
|
||||
def test_no_positional_args(self):
|
||||
# Check that we get an error for positional args when they
|
||||
# haven't been enabled.
|
||||
self.check_exit_code(self.OptionsClass(), ["A"])
|
||||
|
||||
def test_positional_args(self):
|
||||
options = self.OptionsClass()
|
||||
options.positional_args_allowed = 1
|
||||
options.realize(["A", "B"])
|
||||
self.assertEqual(options.args, ["A", "B"])
|
||||
|
||||
def test_positional_args_empty(self):
|
||||
options = self.OptionsClass()
|
||||
options.positional_args_allowed = 1
|
||||
options.realize([])
|
||||
self.assertEqual(options.args, [])
|
||||
|
||||
def test_positional_args_unknown_option(self):
|
||||
# Make sure an unknown option doesn't become a positional arg.
|
||||
options = self.OptionsClass()
|
||||
options.positional_args_allowed = 1
|
||||
self.check_exit_code(options, ["-o", "A", "B"])
|
||||
|
||||
def test_conflicting_flags(self):
|
||||
# Check that we get an error for flags which compete over the
|
||||
# same option setting.
|
||||
options = self.OptionsClass()
|
||||
options.add("setting", None, "a", flag=1)
|
||||
options.add("setting", None, "b", flag=2)
|
||||
self.check_exit_code(options, ["-a", "-b"])
|
||||
|
||||
def test_duplicate_flags(self):
|
||||
# Check that we don't get an error for flags which reinforce the
|
||||
# same option setting.
|
||||
options = self.OptionsClass()
|
||||
options.add("setting", None, "a", flag=1)
|
||||
options.realize(["-a", "-a"])
|
||||
|
||||
def test_handler_simple(self):
|
||||
# Test that a handler is called; use one that doesn't return None.
|
||||
options = self.OptionsClass()
|
||||
options.add("setting", None, "a:", handler=int)
|
||||
options.realize(["-a2"])
|
||||
self.assertEqual(options.setting, 2)
|
||||
|
||||
def test_handler_side_effect(self):
|
||||
# Test that a handler is called and conflicts are not
|
||||
# signalled when it returns None.
|
||||
options = self.OptionsClass()
|
||||
L = []
|
||||
options.add("setting", None, "a:", "append=", handler=L.append)
|
||||
options.realize(["-a2", "--append", "3"])
|
||||
self.assertTrue(options.setting is None)
|
||||
self.assertEqual(L, ["2", "3"])
|
||||
|
||||
def test_handler_with_bad_value(self):
|
||||
options = self.OptionsClass()
|
||||
options.add("setting", None, "a:", handler=int)
|
||||
self.check_exit_code(options, ["-afoo"])
|
||||
|
||||
def test_required_options(self):
|
||||
# Check that we get an error if a required option is not specified
|
||||
options = self.OptionsClass()
|
||||
options.add("setting", None, "a:", handler=int, required=True)
|
||||
self.check_exit_code(options, [])
|
||||
|
||||
def test_overrides_without_config_file(self):
|
||||
# Check that we get an error if we use -X without -C
|
||||
options = self.OptionsClass()
|
||||
self.check_exit_code(options, ["-Xfoo"])
|
||||
|
||||
def test_raise_getopt_errors(self):
|
||||
options = self.OptionsClass()
|
||||
# note that we do not add "a" to the list of options;
|
||||
# if raise_getopt_errors was true, this test would error
|
||||
options.realize(["-afoo"], raise_getopt_errs=False)
|
||||
# check_exit_code realizes the options with raise_getopt_errs=True
|
||||
self.check_exit_code(options, ['-afoo'])
|
||||
|
||||
def test_list_of_ints(self):
|
||||
self.assertEqual(list_of_ints(''), [])
|
||||
self.assertEqual(list_of_ints('42'), [42])
|
||||
self.assertEqual(list_of_ints('42,43'), [42, 43])
|
||||
self.assertEqual(list_of_ints('42, 43'), [42, 43])
|
||||
|
||||
|
||||
class TestOptionConfiguration(ZDOptionsTestBase):
|
||||
|
||||
def test_add_flag_or_handler_not_both(self):
|
||||
options = self.OptionsClass()
|
||||
self.assertRaises(ValueError, options.add, short="a", flag=1,
|
||||
handler=lambda x: None)
|
||||
|
||||
def test_flag_requires_command_line_flag(self):
|
||||
options = self.OptionsClass()
|
||||
self.assertRaises(ValueError, options.add, flag=1)
|
||||
|
||||
def test_flag_cannot_accept_arguments(self):
|
||||
options = self.OptionsClass()
|
||||
self.assertRaises(ValueError, options.add, short='a:', flag=1)
|
||||
self.assertRaises(ValueError, options.add, long='an-option=', flag=1)
|
||||
|
||||
def test_arguments_must_be_consistent(self):
|
||||
options = self.OptionsClass()
|
||||
self.assertRaises(ValueError, options.add, short='a:', long='an-option')
|
||||
self.assertRaises(ValueError, options.add, short='a', long='an-option=')
|
||||
|
||||
def test_short_cmdline_syntax(self):
|
||||
options = self.OptionsClass()
|
||||
self.assertRaises(ValueError, options.add, short='-a')
|
||||
self.assertRaises(ValueError, options.add, short='ab')
|
||||
self.assertRaises(ValueError, options.add, short='abc')
|
||||
|
||||
def test_long_cmdline_syntax(self):
|
||||
options = self.OptionsClass()
|
||||
self.assertRaises(ValueError, options.add, long='--an-option')
|
||||
self.assertRaises(ValueError, options.add, long='-an-option')
|
||||
|
||||
def test_duplicate_short_flags(self):
|
||||
options = self.OptionsClass()
|
||||
options.add(short='a')
|
||||
options.add(short='b')
|
||||
self.assertRaises(ValueError, options.add, short='a')
|
||||
|
||||
def test_duplicate_long_flags(self):
|
||||
options = self.OptionsClass()
|
||||
options.add(long='an-option')
|
||||
options.add(long='be-still-my-beating-heart')
|
||||
self.assertRaises(ValueError, options.add, long='an-option')
|
||||
|
||||
|
||||
class EnvironmentOptions(ZDOptionsTestBase):
|
||||
|
||||
saved_schema = None
|
||||
|
||||
class OptionsClass(ZDOptions):
|
||||
def __init__(self):
|
||||
ZDOptions.__init__(self)
|
||||
self.add("opt", "opt", "o:", "opt=",
|
||||
default=42, handler=int, env="OPT")
|
||||
|
||||
def load_schema(self):
|
||||
# Doing this here avoids needing a separate file for the schema:
|
||||
if self.schema is None:
|
||||
if EnvironmentOptions.saved_schema is None:
|
||||
schema = ZConfig.loadSchemaFile(StringIO("""\
|
||||
<schema>
|
||||
<key name='opt' datatype='integer' default='12'/>
|
||||
</schema>
|
||||
"""))
|
||||
EnvironmentOptions.saved_schema = schema
|
||||
self.schema = EnvironmentOptions.saved_schema
|
||||
|
||||
def load_configfile(self):
|
||||
if getattr(self, "configtext", None):
|
||||
self.configfile = tempfile.mktemp()
|
||||
f = open(self.configfile, 'w')
|
||||
f.write(self.configtext)
|
||||
f.close()
|
||||
try:
|
||||
ZDOptions.load_configfile(self)
|
||||
finally:
|
||||
os.unlink(self.configfile)
|
||||
else:
|
||||
ZDOptions.load_configfile(self)
|
||||
|
||||
# Save and restore the environment around each test:
|
||||
|
||||
def setUp(self):
|
||||
self._oldenv = os.environ
|
||||
env = {}
|
||||
for k, v in os.environ.items():
|
||||
env[k] = v
|
||||
os.environ = env
|
||||
|
||||
def tearDown(self):
|
||||
os.environ = self._oldenv
|
||||
|
||||
def create_with_config(self, text):
|
||||
options = self.OptionsClass()
|
||||
zdpkgdir = os.path.dirname(os.path.abspath(zdaemon.__file__))
|
||||
options.schemadir = os.path.join(zdpkgdir, 'tests')
|
||||
options.schemafile = "envtest.xml"
|
||||
# configfile must be set for ZDOptions to use ZConfig:
|
||||
if text:
|
||||
options.configfile = "not used"
|
||||
options.configtext = text
|
||||
return options
|
||||
|
||||
|
||||
class TestZDOptionsEnvironment(EnvironmentOptions):
|
||||
|
||||
def test_with_environment(self):
|
||||
os.environ["OPT"] = "2"
|
||||
self.check_from_command_line()
|
||||
options = self.OptionsClass()
|
||||
options.realize([])
|
||||
self.assertEqual(options.opt, 2)
|
||||
|
||||
def test_without_environment(self):
|
||||
self.check_from_command_line()
|
||||
options = self.OptionsClass()
|
||||
options.realize([])
|
||||
self.assertEqual(options.opt, 42)
|
||||
|
||||
def check_from_command_line(self):
|
||||
for args in (["-o1"], ["--opt", "1"]):
|
||||
options = self.OptionsClass()
|
||||
options.realize(args)
|
||||
self.assertEqual(options.opt, 1)
|
||||
|
||||
def test_with_bad_environment(self):
|
||||
os.environ["OPT"] = "Spooge!"
|
||||
# make sure the bad value is ignored if the command-line is used:
|
||||
self.check_from_command_line()
|
||||
options = self.OptionsClass()
|
||||
try:
|
||||
self.save_streams()
|
||||
try:
|
||||
options.realize([])
|
||||
finally:
|
||||
self.restore_streams()
|
||||
except SystemExit as e:
|
||||
self.assertEqual(e.code, 2)
|
||||
else:
|
||||
self.fail("expected SystemExit")
|
||||
|
||||
def test_environment_overrides_configfile(self):
|
||||
options = self.create_with_config("opt 3")
|
||||
options.realize([])
|
||||
self.assertEqual(options.opt, 3)
|
||||
|
||||
os.environ["OPT"] = "2"
|
||||
options = self.create_with_config("opt 3")
|
||||
options.realize([])
|
||||
self.assertEqual(options.opt, 2)
|
||||
|
||||
|
||||
class TestCommandLineOverrides(EnvironmentOptions):
|
||||
|
||||
def test_simple_override(self):
|
||||
options = self.create_with_config("# empty config")
|
||||
options.realize(["-X", "opt=-2"])
|
||||
self.assertEqual(options.opt, -2)
|
||||
|
||||
def test_error_propogation(self):
|
||||
self.check_exit_code(self.create_with_config("# empty"),
|
||||
["-Xopt=1", "-Xopt=2"])
|
||||
self.check_exit_code(self.create_with_config("# empty"),
|
||||
["-Xunknown=foo"])
|
||||
|
||||
|
||||
class TestRunnerDirectory(ZDOptionsTestBase):
|
||||
|
||||
OptionsClass = RunnerOptions
|
||||
|
||||
def setUp(self):
|
||||
super(TestRunnerDirectory, self).setUp()
|
||||
# Create temporary directory to work in
|
||||
self.root = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.root)
|
||||
super(TestRunnerDirectory, self).tearDown()
|
||||
|
||||
def test_not_existing_directory(self):
|
||||
options = self.OptionsClass()
|
||||
path = os.path.join(self.root, 'does-not-exist', 'really-not')
|
||||
self.check_exit_code(options, ["-z", path])
|
||||
socket = os.path.join(path, 'socket')
|
||||
self.check_exit_code(options, ["-s", socket])
|
||||
|
||||
def test_existing_directory(self):
|
||||
options = self.OptionsClass()
|
||||
options.realize(["-z", self.root])
|
||||
socket = os.path.join(self.root, 'socket')
|
||||
self.check_exit_code(options, ["-s", socket])
|
||||
|
||||
def test_parent_is_created(self):
|
||||
options = self.OptionsClass()
|
||||
path = os.path.join(self.root, 'will-be-created')
|
||||
options.realize(["-z", path])
|
||||
self.assertEqual(path, options.directory)
|
||||
socket = os.path.join(path, 'socket')
|
||||
options = self.OptionsClass()
|
||||
options.realize(["-s", socket])
|
||||
# Directory will be created when zdaemon runs, not when the
|
||||
# configuration is read
|
||||
self.assertFalse(os.path.exists(path))
|
||||
|
||||
def test_existing_parent_directory(self):
|
||||
self.assertTrue(existing_parent_directory(self.root))
|
||||
self.assertTrue(existing_parent_directory(
|
||||
os.path.join(self.root, 'not-there')))
|
||||
self.assertRaises(
|
||||
ValueError, existing_parent_directory,
|
||||
os.path.join(self.root, 'not-there', 'this-also-not'))
|
||||
|
||||
def test_existing_parent_dirpath(self):
|
||||
self.assertTrue(existing_parent_dirpath(
|
||||
os.path.join(self.root, 'sock')))
|
||||
self.assertTrue(existing_parent_dirpath(
|
||||
os.path.join(self.root, 'not-there', 'sock')))
|
||||
self.assertTrue(existing_parent_dirpath(
|
||||
os.path.join('not-there', 'sock')))
|
||||
self.assertRaises(
|
||||
ValueError, existing_parent_dirpath,
|
||||
os.path.join(self.root, 'not-there', 'this-also-not', 'sock'))
|
||||
|
||||
|
||||
def test_suite():
|
||||
return unittest.TestSuite([
|
||||
doctest.DocTestSuite('zdaemon.zdoptions'),
|
||||
unittest.defaultTestLoader.loadTestsFromName(__name__),
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,461 @@
|
||||
"""Test suite for zdrun.py."""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
import signal
|
||||
import tempfile
|
||||
import unittest
|
||||
import socket
|
||||
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except:
|
||||
# Python 3 support.
|
||||
from io import StringIO
|
||||
|
||||
import ZConfig
|
||||
|
||||
from zdaemon import zdrun, zdctl
|
||||
|
||||
|
||||
class ConfiguredOptions:
|
||||
"""Options class that loads configuration from a specified string.
|
||||
|
||||
This always loads from the string, regardless of any -C option
|
||||
that may be given.
|
||||
"""
|
||||
|
||||
def set_configuration(self, configuration):
|
||||
self.__configuration = configuration
|
||||
self.configfile = "<preloaded string>"
|
||||
|
||||
def load_configfile(self):
|
||||
sio = StringIO(self.__configuration)
|
||||
cfg = ZConfig.loadConfigFile(self.schema, sio, self.zconfig_options)
|
||||
self.configroot, self.confighandlers = cfg
|
||||
|
||||
|
||||
class ConfiguredZDRunOptions(ConfiguredOptions, zdrun.ZDRunOptions):
|
||||
|
||||
def __init__(self, configuration):
|
||||
zdrun.ZDRunOptions.__init__(self)
|
||||
self.set_configuration(configuration)
|
||||
|
||||
|
||||
class ZDaemonTests(unittest.TestCase):
|
||||
|
||||
python = os.path.abspath(sys.executable)
|
||||
assert os.path.exists(python)
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
assert os.path.isdir(here)
|
||||
nokill = os.path.join(here, "nokill.py")
|
||||
assert os.path.exists(nokill)
|
||||
parent = os.path.dirname(here)
|
||||
zdrun = os.path.join(parent, "zdrun.py")
|
||||
assert os.path.exists(zdrun)
|
||||
|
||||
ppath = os.pathsep.join(sys.path)
|
||||
|
||||
def setUp(self):
|
||||
self.zdsock = tempfile.mktemp()
|
||||
self.new_stdout = StringIO()
|
||||
self.save_stdout = sys.stdout
|
||||
sys.stdout = self.new_stdout
|
||||
self.expect = ""
|
||||
|
||||
def tearDown(self):
|
||||
sys.stdout = self.save_stdout
|
||||
for sig in (signal.SIGTERM,
|
||||
signal.SIGHUP,
|
||||
signal.SIGINT,
|
||||
signal.SIGCHLD):
|
||||
signal.signal(sig, signal.SIG_DFL)
|
||||
try:
|
||||
os.unlink(self.zdsock)
|
||||
except os.error:
|
||||
pass
|
||||
output = self.new_stdout.getvalue()
|
||||
self.assertEqual(self.expect, output)
|
||||
|
||||
def quoteargs(self, args):
|
||||
for i in range(len(args)):
|
||||
if " " in args[i]:
|
||||
args[i] = '"%s"' % args[i]
|
||||
return " ".join(args)
|
||||
|
||||
def rundaemon(self, args):
|
||||
# Add quotes, in case some pathname contains spaces (e.g. Mac OS X)
|
||||
args = self.quoteargs(args)
|
||||
cmd = ('PYTHONPATH="%s" "%s" "%s" -d -s "%s" %s' %
|
||||
(self.ppath, self.python, self.zdrun, self.zdsock, args))
|
||||
os.system(cmd)
|
||||
# When the daemon crashes, the following may help debug it:
|
||||
# os.system("PYTHONPATH=%s %s %s -s %s %s &" %
|
||||
# (self.ppath, self.python, self.zdrun, self.zdsock, args))
|
||||
|
||||
def _run(self, args, cmdclass=None, module=zdctl):
|
||||
if isinstance(args, str):
|
||||
args = args.split()
|
||||
kw = {}
|
||||
if cmdclass:
|
||||
kw['cmdclass'] = cmdclass
|
||||
try:
|
||||
module.main(["-s", self.zdsock] + args, **kw)
|
||||
except SystemExit:
|
||||
pass
|
||||
|
||||
def testCmdclassOverride(self):
|
||||
class MyCmd(zdctl.ZDCmd):
|
||||
def do_sproing(self, rest):
|
||||
print(rest)
|
||||
self._run("-p echo sproing expected", cmdclass=MyCmd)
|
||||
self.expect = "expected\n"
|
||||
|
||||
def testSystem(self):
|
||||
self.rundaemon(["echo", "-n"])
|
||||
self.expect = ""
|
||||
|
||||
def test_help_zdrun(self):
|
||||
self._run("-h", module=zdrun)
|
||||
self.expect = zdrun.__doc__
|
||||
|
||||
def test_help_zdctl(self):
|
||||
self._run("-h")
|
||||
self.expect = zdctl.__doc__
|
||||
|
||||
def testOptionsSysArgv(self):
|
||||
# Check that options are parsed from sys.argv by default
|
||||
options = zdrun.ZDRunOptions()
|
||||
save_sys_argv = sys.argv
|
||||
try:
|
||||
sys.argv = ["A", "B", "C"]
|
||||
options.realize()
|
||||
finally:
|
||||
sys.argv = save_sys_argv
|
||||
self.assertEqual(options.options, [])
|
||||
self.assertEqual(options.args, ["B", "C"])
|
||||
|
||||
def testOptionsBasic(self):
|
||||
# Check basic option parsing
|
||||
options = zdrun.ZDRunOptions()
|
||||
options.realize(["B", "C"], "foo")
|
||||
self.assertEqual(options.options, [])
|
||||
self.assertEqual(options.args, ["B", "C"])
|
||||
self.assertEqual(options.progname, "foo")
|
||||
|
||||
def testOptionsHelp(self):
|
||||
# Check that -h behaves properly
|
||||
options = zdrun.ZDRunOptions()
|
||||
try:
|
||||
options.realize(["-h"], doc=zdrun.__doc__)
|
||||
except SystemExit as err:
|
||||
self.assertEqual(err.code, 0)
|
||||
else:
|
||||
self.fail("SystemExit expected")
|
||||
self.expect = zdrun.__doc__
|
||||
|
||||
def testSubprocessBasic(self):
|
||||
# Check basic subprocess management: spawn, kill, wait
|
||||
options = zdrun.ZDRunOptions()
|
||||
options.realize(["sleep", "100"])
|
||||
proc = zdrun.Subprocess(options)
|
||||
self.assertEqual(proc.pid, 0)
|
||||
pid = proc.spawn()
|
||||
self.assertEqual(proc.pid, pid)
|
||||
msg = proc.kill(signal.SIGTERM)
|
||||
self.assertEqual(msg, None)
|
||||
wpid, wsts = os.waitpid(pid, 0)
|
||||
self.assertEqual(wpid, pid)
|
||||
self.assertEqual(os.WIFSIGNALED(wsts), 1)
|
||||
self.assertEqual(os.WTERMSIG(wsts), signal.SIGTERM)
|
||||
proc.setstatus(wsts)
|
||||
self.assertEqual(proc.pid, 0)
|
||||
|
||||
def testEventlogOverride(self):
|
||||
# Make sure runner.eventlog is used if it exists
|
||||
options = ConfiguredZDRunOptions("""\
|
||||
<runner>
|
||||
program /bin/true
|
||||
<eventlog>
|
||||
level 42
|
||||
</eventlog>
|
||||
</runner>
|
||||
|
||||
<eventlog>
|
||||
level 35
|
||||
</eventlog>
|
||||
""")
|
||||
options.realize(["/bin/true"])
|
||||
self.assertEqual(options.config_logger.level, 42)
|
||||
|
||||
def testEventlogWithoutOverride(self):
|
||||
# Make sure eventlog is used if runner.eventlog doesn't exist
|
||||
options = ConfiguredZDRunOptions("""\
|
||||
<runner>
|
||||
program /bin/true
|
||||
</runner>
|
||||
|
||||
<eventlog>
|
||||
level 35
|
||||
</eventlog>
|
||||
""")
|
||||
options.realize(["/bin/true"])
|
||||
self.assertEqual(options.config_logger.level, 35)
|
||||
|
||||
def testRunIgnoresParentSignals(self):
|
||||
# Spawn a process which will in turn spawn a zdrun process.
|
||||
# We make sure that the zdrun process is still running even if
|
||||
# its parent process receives an interrupt signal (it should
|
||||
# not be passed to zdrun).
|
||||
tmp = tempfile.mkdtemp()
|
||||
zdrun_socket = os.path.join(tmp, 'testsock')
|
||||
try:
|
||||
zdctlpid = os.spawnvpe(
|
||||
os.P_NOWAIT,
|
||||
sys.executable,
|
||||
[sys.executable, os.path.join(self.here, 'parent.py'), tmp],
|
||||
dict(os.environ,
|
||||
PYTHONPATH=":".join(sys.path),
|
||||
)
|
||||
)
|
||||
# Wait for it to start, but no longer than a minute.
|
||||
deadline = time.time() + 60
|
||||
is_started = False
|
||||
while time.time() < deadline:
|
||||
response = send_action('status\n', zdrun_socket)
|
||||
if response is None:
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
is_started = True
|
||||
break
|
||||
self.assertTrue(is_started,
|
||||
"spawned process failed to start in a minute")
|
||||
# Kill it, and wait a little to ensure it's dead.
|
||||
os.kill(zdctlpid, signal.SIGINT)
|
||||
time.sleep(0.25)
|
||||
# Make sure the child is still responsive.
|
||||
response = send_action('status\n', zdrun_socket,
|
||||
raise_on_error=True)
|
||||
self.assertTrue(b'\n' in response,
|
||||
'no newline in response: ' + repr(response))
|
||||
# Kill the process.
|
||||
send_action('stop\n', zdrun_socket)
|
||||
finally:
|
||||
# Remove the tmp directory.
|
||||
# Caution: this is delicate. The code here used to do
|
||||
# shutil.rmtree(tmp), but that suffers a sometimes-fatal
|
||||
# race with zdrun.py. The 'testsock' socket is created
|
||||
# by zdrun in the tmp directory, and zdrun tries to
|
||||
# unlink it. If shutil.rmtree sees 'testsock' too, it
|
||||
# will also try to unlink it, but zdrun may complete
|
||||
# unlinking it before shutil gets to it (there's more
|
||||
# than one process here). So, in effect, we code a
|
||||
# 1-level rmtree inline here, suppressing errors.
|
||||
for fname in os.listdir(tmp):
|
||||
try:
|
||||
os.unlink(os.path.join(tmp, fname))
|
||||
except os.error:
|
||||
pass
|
||||
os.rmdir(tmp)
|
||||
|
||||
def testUmask(self):
|
||||
# people have a strange tendency to run the tests as root
|
||||
if os.getuid() == 0:
|
||||
self.fail("""
|
||||
I am root!
|
||||
Do not run the tests as root.
|
||||
Testing proper umask handling cannot be done as root.
|
||||
Furthermore, it is not a good idea and strongly discouraged to run zope, the
|
||||
build system (configure, make) or the tests as root.
|
||||
In general do not run anything as root unless absolutely necessary.
|
||||
""")
|
||||
|
||||
path = tempfile.mktemp()
|
||||
# With umask 666, we should create a file that we aren't able
|
||||
# to write. If access says no, assume that umask works.
|
||||
try:
|
||||
touch_cmd = "/bin/touch"
|
||||
if not os.path.exists(touch_cmd):
|
||||
touch_cmd = "/usr/bin/touch" # Mac OS X
|
||||
self.rundaemon(["-m", "666", touch_cmd, path])
|
||||
for i in range(5):
|
||||
if not os.path.exists(path):
|
||||
time.sleep(0.1)
|
||||
self.assertTrue(os.path.exists(path))
|
||||
self.assertTrue(not os.access(path, os.W_OK))
|
||||
finally:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
class TestRunnerDirectory(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRunnerDirectory, self).setUp()
|
||||
self.root = tempfile.mkdtemp()
|
||||
self.save_stdout = sys.stdout
|
||||
self.save_stderr = sys.stdout
|
||||
sys.stdout = StringIO()
|
||||
sys.stderr = StringIO()
|
||||
self.expect = ''
|
||||
self.cmd = "/bin/true"
|
||||
if not os.path.exists(self.cmd):
|
||||
self.cmd = "/usr/bin/true" # Mac OS X
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.root)
|
||||
got = sys.stdout.getvalue()
|
||||
err = sys.stderr.getvalue()
|
||||
sys.stdout = self.save_stdout
|
||||
sys.stderr = self.save_stderr
|
||||
if err:
|
||||
print(err, end='', file=sys.stderr)
|
||||
self.assertEqual(self.expect, got)
|
||||
super(TestRunnerDirectory, self).tearDown()
|
||||
|
||||
def run_ctl(self, opts):
|
||||
options = zdctl.ZDCtlOptions()
|
||||
options.realize(opts + ['fg'])
|
||||
self.expect = self.cmd + '\n'
|
||||
proc = zdctl.ZDCmd(options)
|
||||
proc.onecmd(" ".join(options.args))
|
||||
|
||||
def testCtlRunDirectoryCreation(self):
|
||||
path = os.path.join(self.root, 'rundir')
|
||||
self.run_ctl(['-z', path, '-p', self.cmd])
|
||||
self.assertTrue(os.path.exists(path))
|
||||
|
||||
def testCtlRunDirectoryCreationFromConfigFile(self):
|
||||
path = os.path.join(self.root, 'rundir')
|
||||
options = ['directory ' + path,
|
||||
'program ' + self.cmd]
|
||||
config = self.writeConfig(
|
||||
'<runner>\n%s\n</runner>' % '\n'.join(options))
|
||||
self.run_ctl(['-C', config])
|
||||
self.assertTrue(os.path.exists(path))
|
||||
|
||||
def testCtlRunDirectoryCreationOnlyOne(self):
|
||||
path = os.path.join(self.root, 'rundir', 'not-created')
|
||||
self.assertRaises(SystemExit,
|
||||
self.run_ctl, ['-z', path, '-p', self.cmd])
|
||||
self.assertFalse(os.path.exists(path))
|
||||
got = sys.stderr.getvalue().strip()
|
||||
sys.stderr = StringIO()
|
||||
self.assertTrue(got.startswith('Error: invalid value for -z'))
|
||||
|
||||
def testCtlSocketDirectoryCreation(self):
|
||||
path = os.path.join(self.root, 'rundir', 'sock')
|
||||
self.run_ctl(['-s', path, '-p', self.cmd])
|
||||
self.assertTrue(os.path.exists(os.path.dirname(path)))
|
||||
|
||||
def testCtlSocketDirectoryCreationRelativePath(self):
|
||||
path = os.path.join('rundir', 'sock')
|
||||
self.run_ctl(['-s', path, '-p', self.cmd])
|
||||
self.assertTrue(
|
||||
os.path.exists(os.path.dirname(os.path.join(os.getcwd(), path))))
|
||||
|
||||
def testCtlSocketDirectoryCreationOnlyOne(self):
|
||||
path = os.path.join(self.root, 'rundir', 'not-created', 'sock')
|
||||
self.assertRaises(SystemExit,
|
||||
self.run_ctl, ['-s', path, '-p', self.cmd])
|
||||
self.assertFalse(os.path.exists(path))
|
||||
got = sys.stderr.getvalue().strip()
|
||||
sys.stderr = StringIO()
|
||||
self.assertTrue(got.startswith('Error: invalid value for -s'))
|
||||
|
||||
def testCtlSocketDirectoryCreationFromConfigFile(self):
|
||||
path = os.path.join(self.root, 'rundir')
|
||||
options = ['socket-name %s/sock' % path,
|
||||
'program ' + self.cmd]
|
||||
config = self.writeConfig(
|
||||
'<runner>\n%s\n</runner>' % '\n'.join(options))
|
||||
self.run_ctl(['-C', config])
|
||||
self.assertTrue(os.path.exists(path))
|
||||
|
||||
def testCtlSocketDirectoryCreationFromConfigFileRelativePath(self):
|
||||
path = 'rel-rundir'
|
||||
options = ['socket-name %s/sock' % path,
|
||||
'program ' + self.cmd]
|
||||
config = self.writeConfig(
|
||||
'<runner>\n%s\n</runner>' % '\n'.join(options))
|
||||
self.run_ctl(['-C', config])
|
||||
self.assertTrue(os.path.exists(os.path.join(os.getcwd(), path)))
|
||||
|
||||
def writeConfig(self, config):
|
||||
config_file = os.path.join(self.root, 'config')
|
||||
with open(config_file, 'w') as f:
|
||||
f.write(config)
|
||||
return config_file
|
||||
|
||||
def testDirectoryChown(self):
|
||||
path = os.path.join(self.root, 'foodir')
|
||||
options = zdctl.ZDCtlOptions()
|
||||
options.realize(['-p', self.cmd, 'status'])
|
||||
cmd = zdctl.ZDCmd(options)
|
||||
options.uid = 27
|
||||
options.gid = 28
|
||||
# Patch chown and geteuid, because we're not root
|
||||
chown = os.chown
|
||||
geteuid = os.geteuid
|
||||
calls = []
|
||||
|
||||
def my_chown(*args):
|
||||
calls.append(('chown',) + args)
|
||||
|
||||
def my_geteuid():
|
||||
return 0
|
||||
|
||||
try:
|
||||
os.chown = my_chown
|
||||
os.geteuid = my_geteuid
|
||||
cmd.create_directory(path)
|
||||
finally:
|
||||
os.chown = chown
|
||||
os.geteuid = geteuid
|
||||
self.assertEqual([('chown', path, 27, 28)], calls)
|
||||
|
||||
|
||||
def send_action(action, sockname, raise_on_error=False):
|
||||
"""Send an action to the zdrun server and return the response.
|
||||
|
||||
Return None if the server is not up or any other error happened.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(sockname)
|
||||
sock.send(action.encode() + b"\n")
|
||||
sock.shutdown(1) # We're not writing any more
|
||||
response = b""
|
||||
while 1:
|
||||
data = sock.recv(1000)
|
||||
if not data:
|
||||
break
|
||||
response += data
|
||||
sock.close()
|
||||
return response
|
||||
except socket.error as msg:
|
||||
if str(msg) == 'AF_UNIX path too long':
|
||||
# MacOS has apparent small limits on the length of a UNIX
|
||||
# domain socket filename, we want to make MacOS users aware
|
||||
# of the actual problem
|
||||
raise
|
||||
if raise_on_error:
|
||||
raise
|
||||
return None
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def test_suite():
|
||||
suite = unittest.TestSuite()
|
||||
if os.name == "posix":
|
||||
suite.addTest(unittest.makeSuite(ZDaemonTests))
|
||||
suite.addTest(unittest.makeSuite(TestRunnerDirectory))
|
||||
return suite
|
||||
|
||||
if __name__ == '__main__':
|
||||
__file__ = sys.argv[0]
|
||||
unittest.main(defaultTest='test_suite')
|
||||
@@ -0,0 +1,643 @@
|
||||
#!python
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""zdctl -- control an application run by zdaemon.
|
||||
|
||||
Usage: python zdctl.py [-C URL] [-S schema.xml] [-h] [-p PROGRAM]
|
||||
[zdrun-options] [action [arguments]]
|
||||
|
||||
Options:
|
||||
-b/--backoff-limit SECONDS -- set backoff limit to SECONDS (default 10)
|
||||
-C/--configure URL -- configuration file or URL
|
||||
-d/--daemon -- run as a proper daemon; fork a subprocess, close files etc.
|
||||
-f/--forever -- run forever (by default, exit when backoff limit is exceeded)
|
||||
-h/--help -- print this usage message and exit
|
||||
-t/--transcript FILE -- log file where to redirect stdout and stderr
|
||||
-l/--logfile -- log file to be read by logtail command
|
||||
-p/--program PROGRAM -- the program to run
|
||||
-S/--schema XML Schema -- XML schema for configuration file
|
||||
-T/--start-timeout SECONDS -- Start timeout when a test program is used
|
||||
-s/--socket-name SOCKET -- Unix socket name for client (default "zdsock")
|
||||
-u/--user USER -- run as this user (or numeric uid)
|
||||
-m/--umask UMASK -- use this umask for daemon subprocess (default is 022)
|
||||
-x/--exit-codes LIST -- list of fatal exit codes (default "0,2")
|
||||
--version -- print zdaemon version and exit
|
||||
-z/--directory DIRECTORY -- directory to chdir to when using -d (default off)
|
||||
action [arguments] -- see below
|
||||
|
||||
Actions are commands like "start", "stop" and "status". Use the
|
||||
action "help" to find out about available actions.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import cmd
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import socket
|
||||
import stat
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Add the parent of the script directory to the module search path
|
||||
# (but only when the script is run from inside the zdaemon package)
|
||||
from os.path import dirname, basename, abspath, normpath
|
||||
scriptdir = dirname(normpath(abspath(sys.argv[0])))
|
||||
if basename(scriptdir).lower() == "zdaemon":
|
||||
sys.path.append(dirname(scriptdir))
|
||||
here = os.path.dirname(os.path.realpath(__file__))
|
||||
swhome = os.path.dirname(here)
|
||||
for parts in [("src",), ("lib", "python"), ("Lib", "site-packages")]:
|
||||
d = os.path.join(swhome, *(parts + ("zdaemon",)))
|
||||
if os.path.isdir(d):
|
||||
d = os.path.join(swhome, *parts)
|
||||
sys.path.insert(0, d)
|
||||
break
|
||||
|
||||
from zdaemon.zdoptions import RunnerOptions, name2signal
|
||||
|
||||
|
||||
def string_list(arg):
|
||||
return arg.split()
|
||||
|
||||
|
||||
class ZDCtlOptions(RunnerOptions):
|
||||
|
||||
__doc__ = __doc__
|
||||
|
||||
positional_args_allowed = True
|
||||
|
||||
def __init__(self):
|
||||
RunnerOptions.__init__(self)
|
||||
self.add("schemafile", short="S:", long="schema=",
|
||||
default="schema.xml",
|
||||
handler=self.set_schemafile)
|
||||
self.add("program", "runner.program", "p:", "program=",
|
||||
handler=string_list,
|
||||
required="no program specified; use -p or -C")
|
||||
self.add("logfile", "runner.logfile", "l:", "logfile=")
|
||||
self.add("start_timeout", "runner.start_timeout",
|
||||
"T:", "start-timeout=", int, default=300)
|
||||
self.add("python", "runner.python")
|
||||
self.add("zdrun", "runner.zdrun")
|
||||
programname = os.path.basename(sys.argv[0])
|
||||
base, ext = os.path.splitext(programname)
|
||||
if ext == ".py":
|
||||
programname = base
|
||||
self.add("prompt", "runner.prompt", default=(programname + ">"))
|
||||
|
||||
def realize(self, *args, **kwds):
|
||||
|
||||
RunnerOptions.realize(self, *args, **kwds)
|
||||
|
||||
# Maybe the config file requires -i or positional args
|
||||
if not self.args:
|
||||
self.usage("an action argument is required")
|
||||
|
||||
# Where's python?
|
||||
if not self.python:
|
||||
self.python = sys.executable
|
||||
|
||||
def set_schemafile(self, file):
|
||||
self.schemafile = file
|
||||
|
||||
|
||||
class ZDCmd(cmd.Cmd):
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
self.prompt = self.options.prompt + ' '
|
||||
cmd.Cmd.__init__(self)
|
||||
self.get_status()
|
||||
if self.zd_status:
|
||||
m = re.search("(?m)^args=(.*)$", self.zd_status)
|
||||
if m:
|
||||
s = m.group(1)
|
||||
args = eval(s, {"__builtins__": {}})
|
||||
program = self.options.program
|
||||
if args[:len(program)] != program:
|
||||
print("WARNING! zdrun is managing a different program!")
|
||||
print("our program =", program)
|
||||
print("daemon's args =", args)
|
||||
|
||||
if options.configroot is not None:
|
||||
env = getattr(options.configroot, 'environment', None)
|
||||
if env is not None:
|
||||
if getattr(env, 'mapping', None) is not None:
|
||||
for k, v in env.mapping.items():
|
||||
os.environ[k] = v
|
||||
elif isinstance(env, dict):
|
||||
for k, v in env.items():
|
||||
os.environ[k] = v
|
||||
|
||||
self.create_rundir()
|
||||
self.create_socket_dir()
|
||||
self.set_uid()
|
||||
|
||||
def create_rundir(self):
|
||||
if self.options.directory is None:
|
||||
return
|
||||
self.create_directory(self.options.directory)
|
||||
|
||||
def create_socket_dir(self):
|
||||
dir = os.path.dirname(self.options.sockname)
|
||||
if not dir:
|
||||
return
|
||||
self.create_directory(dir)
|
||||
|
||||
def create_directory(self, directory):
|
||||
if os.path.isdir(directory):
|
||||
return
|
||||
os.mkdir(directory)
|
||||
uid = os.geteuid()
|
||||
if uid == 0 and uid != self.options.uid:
|
||||
# Change owner of directory to target
|
||||
os.chown(directory, self.options.uid, self.options.gid)
|
||||
|
||||
def set_uid(self):
|
||||
user = self.options.user
|
||||
if user is None:
|
||||
return
|
||||
|
||||
import pwd
|
||||
try:
|
||||
uid = int(user)
|
||||
except ValueError:
|
||||
try:
|
||||
pwrec = pwd.getpwnam(user)
|
||||
except KeyError:
|
||||
self.options.usage("username %r not found" % user)
|
||||
uid = pwrec.pw_uid
|
||||
else:
|
||||
try:
|
||||
pwrec = pwd.getpwuid(uid)
|
||||
except KeyError:
|
||||
self.options.usage("uid %r not found" % user)
|
||||
|
||||
# See if we're already that user:
|
||||
euid = os.geteuid()
|
||||
if euid != 0:
|
||||
if euid != uid:
|
||||
self.options.usage("only root can use -u USER to change users")
|
||||
return
|
||||
|
||||
# OK, we have to set user and groups:
|
||||
os.setgid(pwrec.pw_gid)
|
||||
|
||||
import grp
|
||||
user = pwrec.pw_name
|
||||
os.setgroups(
|
||||
sorted(g.gr_gid for g in grp.getgrall() # sort for tests
|
||||
if user in g.gr_mem)
|
||||
)
|
||||
os.setuid(uid)
|
||||
|
||||
def emptyline(self):
|
||||
# We don't want a blank line to repeat the last command.
|
||||
# Showing status is a nice alternative.
|
||||
self.do_status()
|
||||
|
||||
def send_action(self, action):
|
||||
"""Send an action to the zdrun server and return the response.
|
||||
|
||||
Return None if the server is not up or any other error happened.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(self.options.sockname)
|
||||
sock.send(action.encode() + b"\n")
|
||||
sock.shutdown(1) # We're not writing any more
|
||||
response = b""
|
||||
while 1:
|
||||
data = sock.recv(1000)
|
||||
if not data:
|
||||
break
|
||||
response += data
|
||||
return response.decode()
|
||||
except socket.error:
|
||||
return None
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
zd_testing = 0
|
||||
|
||||
def get_status(self):
|
||||
self.zd_up = 0
|
||||
self.zd_pid = 0
|
||||
self.zd_should_be_up = 0
|
||||
self.zd_status = None
|
||||
resp = self.send_action("status")
|
||||
if not resp:
|
||||
return resp
|
||||
m = re.search("(?m)^application=(\d+)$", resp)
|
||||
if not m:
|
||||
return resp
|
||||
self.zd_up = 1
|
||||
self.zd_pid = int(m.group(1))
|
||||
self.zd_status = resp
|
||||
m = re.search("(?m)^should_be_up=(\d+)$", resp)
|
||||
if m:
|
||||
self.zd_should_be_up = int(m.group(1))
|
||||
else:
|
||||
self.zd_should_be_up = 1
|
||||
m = re.search("(?m)^testing=(\d+)$", resp)
|
||||
if m:
|
||||
self.zd_testing = int(m.group(1))
|
||||
else:
|
||||
self.zd_testing = 0
|
||||
|
||||
return resp
|
||||
|
||||
def awhile(self, cond, msg):
|
||||
n = 0
|
||||
was_running = False
|
||||
try:
|
||||
if self.get_status():
|
||||
was_running = True
|
||||
|
||||
while not cond(n):
|
||||
sys.stdout.write(". ")
|
||||
sys.stdout.flush()
|
||||
time.sleep(1)
|
||||
n += 1
|
||||
if self.get_status():
|
||||
was_running = True
|
||||
elif (was_running or n > 10) and not cond(n):
|
||||
print("\ndaemon manager not running")
|
||||
return 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("^C")
|
||||
print("\n" + msg % self.__dict__)
|
||||
|
||||
def _start_cond(self, n):
|
||||
if (n > self.options.start_timeout):
|
||||
print('\nProgram took too long to start')
|
||||
sys.exit(1)
|
||||
return self.zd_pid and not self.zd_testing
|
||||
|
||||
def do_start(self, arg):
|
||||
self.get_status()
|
||||
if not self.zd_up:
|
||||
if self.options.zdrun:
|
||||
args = [self.options.python, self.options.zdrun]
|
||||
else:
|
||||
args = [self.options.python, sys.argv[0]]
|
||||
os.environ['DAEMON_MANAGER_MODE'] = '1'
|
||||
|
||||
args += self._get_override("-S", "schemafile")
|
||||
args += self._get_override("-C", "configfile")
|
||||
args += self._get_override("-b", "backofflimit")
|
||||
args += self._get_override("-f", "forever", flag=1)
|
||||
args += self._get_override("-s", "sockname")
|
||||
args += self._get_override("-u", "user")
|
||||
args += self._get_override("-t", "transcript")
|
||||
if self.options.umask:
|
||||
args += self._get_override("-m", "umask",
|
||||
oct(self.options.umask))
|
||||
args += self._get_override(
|
||||
"-x", "exitcodes", ",".join(map(str, self.options.exitcodes)))
|
||||
args += self._get_override("-z", "directory")
|
||||
args.extend(self.options.program)
|
||||
args.extend(self.options.args[1:])
|
||||
if self.options.daemon:
|
||||
flag = os.P_NOWAIT
|
||||
else:
|
||||
flag = os.P_WAIT
|
||||
os.spawnvp(flag, args[0], args)
|
||||
elif not self.zd_pid:
|
||||
self.send_action("start")
|
||||
else:
|
||||
print("daemon process already running; pid=%d" % self.zd_pid)
|
||||
return
|
||||
if self.options.daemon:
|
||||
return self.awhile(
|
||||
self._start_cond, "daemon process started, pid=%(zd_pid)d")
|
||||
|
||||
def _get_override(self, opt, name, svalue=None, flag=0):
|
||||
value = getattr(self.options, name)
|
||||
if value is None:
|
||||
return []
|
||||
configroot = self.options.configroot
|
||||
if configroot is not None:
|
||||
for n, cn in self.options.names_list:
|
||||
if n == name and cn:
|
||||
v = configroot
|
||||
for p in cn.split("."):
|
||||
v = getattr(v, p, None)
|
||||
if v is None:
|
||||
break
|
||||
if v == value: # We didn't override anything
|
||||
return []
|
||||
break
|
||||
if flag:
|
||||
if value:
|
||||
args = [opt]
|
||||
else:
|
||||
args = []
|
||||
else:
|
||||
if svalue is None:
|
||||
svalue = str(value)
|
||||
args = [opt, svalue]
|
||||
return args
|
||||
|
||||
def help_start(self):
|
||||
print("start -- Start the daemon process.")
|
||||
print(" If it is already running, do nothing.")
|
||||
|
||||
def do_stop(self, arg):
|
||||
self.get_status()
|
||||
if not self.zd_up:
|
||||
print("daemon manager not running")
|
||||
elif not self.zd_pid and not self.zd_should_be_up:
|
||||
print("daemon process not running")
|
||||
else:
|
||||
self.send_action("stop")
|
||||
self.awhile(lambda n: not self.zd_pid, "daemon process stopped")
|
||||
|
||||
def help_stop(self):
|
||||
print("stop -- Stop the daemon process.")
|
||||
print(" If it is not running, do nothing.")
|
||||
|
||||
def do_reopen_transcript(self, arg):
|
||||
if not self.zd_up:
|
||||
print("daemon manager not running")
|
||||
else:
|
||||
self.send_action("reopen_transcript")
|
||||
|
||||
def help_reopen_transcript(self):
|
||||
print("reopen_transcript -- Reopen the transcript log file.")
|
||||
print(" Use after log rotation.")
|
||||
|
||||
def do_restart(self, arg):
|
||||
self.get_status()
|
||||
pid = self.zd_pid
|
||||
if not pid:
|
||||
self.do_start(arg)
|
||||
else:
|
||||
self.send_action("restart")
|
||||
self.awhile(lambda n: (self.zd_pid != pid) and self._start_cond(n),
|
||||
"daemon process restarted, pid=%(zd_pid)d")
|
||||
|
||||
def help_restart(self):
|
||||
print("restart -- Stop and then start the daemon process.")
|
||||
|
||||
def do_kill(self, arg):
|
||||
if not arg:
|
||||
arg = 'SIGTERM'
|
||||
try:
|
||||
signame = name2signal(arg)
|
||||
except ValueError:
|
||||
print("invalid signal", repr(arg))
|
||||
return
|
||||
self.get_status()
|
||||
if not self.zd_pid:
|
||||
print("daemon process not running")
|
||||
return
|
||||
sig = getattr(signal, signame)
|
||||
print("kill(%d, %d)" % (self.zd_pid, sig))
|
||||
try:
|
||||
os.kill(self.zd_pid, sig)
|
||||
except os.error as msg:
|
||||
print("Error:", msg)
|
||||
else:
|
||||
print("signal %s sent to process %d" % (signame, self.zd_pid))
|
||||
|
||||
def help_kill(self):
|
||||
print("kill [sig] -- Send signal sig to the daemon process.")
|
||||
print(" The default signal is SIGTERM.")
|
||||
|
||||
def do_wait(self, arg):
|
||||
self.awhile(lambda n: not self.zd_pid, "daemon process stopped")
|
||||
self.do_status()
|
||||
|
||||
def help_wait(self):
|
||||
print("wait -- Wait for the daemon process to exit.")
|
||||
|
||||
def do_status(self, arg=""):
|
||||
status = 0
|
||||
if arg not in ["", "-l"]:
|
||||
print("status argument must be absent or -l")
|
||||
return 1
|
||||
self.get_status()
|
||||
if not self.zd_up:
|
||||
print("daemon manager not running")
|
||||
status = 3
|
||||
elif not self.zd_pid:
|
||||
print("daemon manager running; daemon process not running")
|
||||
else:
|
||||
print("program running; pid=%d" % self.zd_pid)
|
||||
if arg == "-l" and self.zd_status:
|
||||
print(self.zd_status)
|
||||
return status
|
||||
|
||||
def help_status(self):
|
||||
print("status [-l] -- Print status for the daemon process.")
|
||||
print(" With -l, show raw status output as well.")
|
||||
|
||||
def do_show(self, arg):
|
||||
if not arg:
|
||||
arg = "options"
|
||||
try:
|
||||
method = getattr(self, "show_" + arg)
|
||||
except AttributeError as err:
|
||||
print(err)
|
||||
self.help_show()
|
||||
return
|
||||
method()
|
||||
|
||||
def show_options(self):
|
||||
print("zdctl/zdrun options:")
|
||||
print("schemafile: ", repr(self.options.schemafile))
|
||||
print("configfile: ", repr(self.options.configfile))
|
||||
print("zdrun: ", repr(self.options.zdrun))
|
||||
print("python: ", repr(self.options.python))
|
||||
print("program: ", repr(self.options.program))
|
||||
print("backofflimit:", repr(self.options.backofflimit))
|
||||
print("daemon: ", repr(self.options.daemon))
|
||||
print("forever: ", repr(self.options.forever))
|
||||
print("sockname: ", repr(self.options.sockname))
|
||||
print("exitcodes: ", repr(self.options.exitcodes))
|
||||
print("user: ", repr(self.options.user))
|
||||
umask = self.options.umask
|
||||
if not umask:
|
||||
# Here we're just getting the current umask so we can report it:
|
||||
umask = os.umask(0o777)
|
||||
os.umask(umask)
|
||||
print("umask: ", oct(umask))
|
||||
print("directory: ", repr(self.options.directory))
|
||||
print("logfile: ", repr(self.options.logfile))
|
||||
print("transcript: ", repr(self.options.transcript))
|
||||
|
||||
def show_python(self):
|
||||
print("Python info:")
|
||||
version = sys.version.replace("\n", "\n ")
|
||||
print("Version: ", version)
|
||||
print("Platform: ", sys.platform)
|
||||
print("Executable: ", repr(sys.executable))
|
||||
print("Arguments: ", repr(sys.argv))
|
||||
print("Directory: ", repr(os.getcwd()))
|
||||
print("Path:")
|
||||
for dir in sys.path:
|
||||
print(" " + repr(dir))
|
||||
|
||||
def show_all(self):
|
||||
self.show_options()
|
||||
print()
|
||||
self.show_python()
|
||||
|
||||
def help_show(self):
|
||||
print("show options -- show zdctl options")
|
||||
print("show python -- show Python version and details")
|
||||
print("show all -- show all of the above")
|
||||
|
||||
def do_logreopen(self, arg):
|
||||
self.do_reopen_transcript('')
|
||||
self.do_kill('USR2')
|
||||
|
||||
def help_logreopen(self):
|
||||
print("logreopen -- Send a SIGUSR2 signal to the daemon process.")
|
||||
print(" This is designed to reopen the log file.")
|
||||
print(" Also reopens the transcript log file.")
|
||||
|
||||
def do_logtail(self, arg):
|
||||
if not arg:
|
||||
arg = self.options.logfile
|
||||
if not arg:
|
||||
print("No default log file specified; use logtail <logfile>")
|
||||
return
|
||||
try:
|
||||
helper = TailHelper(arg)
|
||||
helper.tailf()
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
except IOError as msg:
|
||||
print(msg)
|
||||
except OSError as msg:
|
||||
print(msg)
|
||||
|
||||
def help_logtail(self):
|
||||
print("logtail [logfile] -- Run tail -f on the given logfile.")
|
||||
print(" A default file may exist.")
|
||||
print(" Hit ^C to exit this mode.")
|
||||
|
||||
def do_foreground(self, arg):
|
||||
self.get_status()
|
||||
pid = self.zd_pid
|
||||
if pid:
|
||||
print(
|
||||
"To run the program in the foreground, please stop it first.")
|
||||
return
|
||||
|
||||
program = self.options.program + self.options.args[1:]
|
||||
print(" ".join(program))
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
os.spawnlp(os.P_WAIT, program[0], *program)
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
|
||||
def do_fg(self, arg):
|
||||
self.do_foreground(arg)
|
||||
|
||||
def help_foreground(self):
|
||||
print("foreground -- Run the program in the forground.")
|
||||
print("fg -- an alias for foreground.")
|
||||
|
||||
def help_fg(self):
|
||||
self.help_foreground()
|
||||
|
||||
def help_help(self):
|
||||
print("help -- Print a list of available actions.")
|
||||
print("help <action> -- Print help for <action>.")
|
||||
|
||||
|
||||
class TailHelper:
|
||||
|
||||
MAX_BUFFSIZE = 1024
|
||||
|
||||
def __init__(self, fname):
|
||||
self.f = open(fname, 'r')
|
||||
|
||||
def tailf(self):
|
||||
sz, lines = self.tail(10)
|
||||
for line in lines:
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
while 1:
|
||||
newsz = self.fsize()
|
||||
bytes_added = newsz - sz
|
||||
if bytes_added < 0:
|
||||
sz = 0
|
||||
print("==> File truncated <==")
|
||||
bytes_added = newsz
|
||||
if bytes_added > 0:
|
||||
self.f.seek(-bytes_added, 2)
|
||||
bytes = self.f.read(bytes_added)
|
||||
sys.stdout.write(bytes)
|
||||
sys.stdout.flush()
|
||||
sz = newsz
|
||||
time.sleep(1)
|
||||
|
||||
def tail(self, max=10):
|
||||
self.f.seek(0, 2)
|
||||
pos = sz = self.f.tell()
|
||||
|
||||
lines = []
|
||||
bytes = []
|
||||
num_bytes = 0
|
||||
|
||||
while 1:
|
||||
if pos == 0:
|
||||
break
|
||||
self.f.seek(pos)
|
||||
byte = self.f.read(1)
|
||||
if byte == '\n':
|
||||
if len(lines) == max:
|
||||
break
|
||||
bytes.reverse()
|
||||
line = ''.join(bytes)
|
||||
line and lines.append(line)
|
||||
bytes = []
|
||||
bytes.append(byte)
|
||||
num_bytes = num_bytes + 1
|
||||
if num_bytes > self.MAX_BUFFSIZE:
|
||||
break
|
||||
pos = pos - 1
|
||||
lines.reverse()
|
||||
return sz, lines
|
||||
|
||||
def fsize(self):
|
||||
return os.fstat(self.f.fileno())[stat.ST_SIZE]
|
||||
|
||||
|
||||
def main(args=None, options=None, cmdclass=ZDCmd):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
if os.environ.get('DAEMON_MANAGER_MODE'):
|
||||
del os.environ['DAEMON_MANAGER_MODE']
|
||||
import zdaemon.zdrun
|
||||
return zdaemon.zdrun.main(args)
|
||||
|
||||
if options is None:
|
||||
options = ZDCtlOptions()
|
||||
options.realize(args)
|
||||
c = cmdclass(options)
|
||||
sys.exit(c.onecmd(" ".join(options.args)))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,528 @@
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2003 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Option processing for zdaemon and related code."""
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import getopt
|
||||
import signal
|
||||
|
||||
import pkg_resources
|
||||
import ZConfig
|
||||
|
||||
|
||||
class ZDOptions:
|
||||
"""a zdaemon script.
|
||||
|
||||
Usage: python <script>.py [-C URL] [zdrun-options] [action [arguments]]
|
||||
|
||||
Options:
|
||||
-C/--configure URL -- configuration file or URL
|
||||
-h/--help -- print usage message and exit
|
||||
--version -- print zdaemon version and exit
|
||||
|
||||
Actions are commands like "start", "stop" and "status". If -i is
|
||||
specified or no action is specified on the command line, a "shell"
|
||||
interpreting actions typed interactively is started (unless the
|
||||
configuration option default_to_interactive is set to false). Use the
|
||||
action "help" to find out about available actions.
|
||||
"""
|
||||
|
||||
doc = None
|
||||
progname = None
|
||||
configfile = None
|
||||
schemadir = None
|
||||
schemafile = "schema.xml"
|
||||
schema = None
|
||||
confighandlers = None
|
||||
configroot = None
|
||||
|
||||
# Class variable to control automatic processing of an <eventlog>
|
||||
# section. This should be the (possibly dotted) name of something
|
||||
# accessible from configroot, typically "eventlog".
|
||||
logsectionname = None
|
||||
config_logger = None # The configured event logger, if any
|
||||
|
||||
# Class variable deciding whether positional arguments are allowed.
|
||||
# If you want positional arguments, set this to 1 in your subclass.
|
||||
positional_args_allowed = 0
|
||||
|
||||
def __init__(self):
|
||||
self.names_list = []
|
||||
self.short_options = []
|
||||
self.long_options = []
|
||||
self.options_map = {}
|
||||
self.default_map = {}
|
||||
self.required_map = {}
|
||||
self.environ_map = {}
|
||||
self.zconfig_options = []
|
||||
self.version = pkg_resources.get_distribution("zdaemon").version
|
||||
self.add(None, None, "h", "help", self.help)
|
||||
self.add(None, None, None, "version", self.print_version)
|
||||
self.add("configfile", None, "C:", "configure=")
|
||||
self.add(None, None, "X:", handler=self.zconfig_options.append)
|
||||
|
||||
def print_version(self, dummy):
|
||||
"""Print zdaemon version number to stdout and exit(0)."""
|
||||
print(self.version)
|
||||
sys.exit(0)
|
||||
|
||||
def help(self, dummy):
|
||||
"""Print a long help message (self.doc) to stdout and exit(0).
|
||||
|
||||
Occurrences of "%s" in self.doc are replaced by self.progname.
|
||||
"""
|
||||
doc = self.doc
|
||||
if not doc:
|
||||
doc = "No help available."
|
||||
elif doc.find("%s") > 0:
|
||||
doc = doc.replace("%s", self.progname)
|
||||
print(doc, end='')
|
||||
sys.exit(0)
|
||||
|
||||
def usage(self, msg):
|
||||
"""Print a brief error message to stderr and exit(2)."""
|
||||
sys.stderr.write("Error: %s\n" % str(msg))
|
||||
sys.stderr.write("For help, use %s -h\n" % self.progname)
|
||||
sys.exit(2)
|
||||
|
||||
def remove(self,
|
||||
name=None, # attribute name on self
|
||||
confname=None, # name in ZConfig (may be dotted)
|
||||
short=None, # short option name
|
||||
long=None, # long option name
|
||||
):
|
||||
"""Remove all traces of name, confname, short and/or long."""
|
||||
if name:
|
||||
for n, cn in self.names_list[:]:
|
||||
if n == name:
|
||||
self.names_list.remove((n, cn))
|
||||
if name in self.default_map:
|
||||
del self.default_map[name]
|
||||
if name in self.required_map:
|
||||
del self.required_map[name]
|
||||
if confname:
|
||||
for n, cn in self.names_list[:]:
|
||||
if cn == confname:
|
||||
self.names_list.remove((n, cn))
|
||||
if short:
|
||||
key = "-" + short[0]
|
||||
if key in self.options_map:
|
||||
del self.options_map[key]
|
||||
if long:
|
||||
key = "--" + long
|
||||
if key[-1] == "=":
|
||||
key = key[:-1]
|
||||
if key in self.options_map:
|
||||
del self.options_map[key]
|
||||
|
||||
def add(self,
|
||||
name=None, # attribute name on self
|
||||
confname=None, # name in ZConfig (may be dotted)
|
||||
short=None, # short option name
|
||||
long=None, # long option name
|
||||
handler=None, # handler (defaults to string)
|
||||
default=None, # default value
|
||||
required=None, # message if not provided
|
||||
flag=None, # if not None, flag value
|
||||
env=None, # if not None, environment variable
|
||||
):
|
||||
"""Add information about a configuration option.
|
||||
|
||||
This can take several forms:
|
||||
|
||||
add(name, confname)
|
||||
Configuration option 'confname' maps to attribute 'name'
|
||||
add(name, None, short, long)
|
||||
Command line option '-short' or '--long' maps to 'name'
|
||||
add(None, None, short, long, handler)
|
||||
Command line option calls handler
|
||||
add(name, None, short, long, handler)
|
||||
Assign handler return value to attribute 'name'
|
||||
|
||||
In addition, one of the following keyword arguments may be given:
|
||||
|
||||
default=... -- if not None, the default value
|
||||
required=... -- if nonempty, an error message if no value provided
|
||||
flag=... -- if not None, flag value for command line option
|
||||
env=... -- if not None, name of environment variable that
|
||||
overrides the configuration file or default
|
||||
"""
|
||||
|
||||
if flag is not None:
|
||||
if handler is not None:
|
||||
raise ValueError("use at most one of flag= and handler=")
|
||||
if not long and not short:
|
||||
raise ValueError("flag= requires a command line flag")
|
||||
if short and short.endswith(":"):
|
||||
raise ValueError("flag= requires a command line flag")
|
||||
if long and long.endswith("="):
|
||||
raise ValueError("flag= requires a command line flag")
|
||||
handler = lambda arg, flag=flag: flag
|
||||
|
||||
if short and long:
|
||||
if short.endswith(":") != long.endswith("="):
|
||||
raise ValueError("inconsistent short/long options: %r %r" % (
|
||||
short, long))
|
||||
|
||||
if short:
|
||||
if short[0] == "-":
|
||||
raise ValueError("short option should not start with '-'")
|
||||
key, rest = short[:1], short[1:]
|
||||
if rest not in ("", ":"):
|
||||
raise ValueError("short option should be 'x' or 'x:'")
|
||||
key = "-" + key
|
||||
if key in self.options_map:
|
||||
raise ValueError("duplicate short option key '%s'" % key)
|
||||
self.options_map[key] = (name, handler)
|
||||
self.short_options.append(short)
|
||||
|
||||
if long:
|
||||
if long[0] == "-":
|
||||
raise ValueError("long option should not start with '-'")
|
||||
key = long
|
||||
if key[-1] == "=":
|
||||
key = key[:-1]
|
||||
key = "--" + key
|
||||
if key in self.options_map:
|
||||
raise ValueError("duplicate long option key '%s'" % key)
|
||||
self.options_map[key] = (name, handler)
|
||||
self.long_options.append(long)
|
||||
|
||||
if env:
|
||||
self.environ_map[env] = (name, handler)
|
||||
|
||||
if name:
|
||||
if not hasattr(self, name):
|
||||
setattr(self, name, None)
|
||||
self.names_list.append((name, confname))
|
||||
if default is not None:
|
||||
self.default_map[name] = default
|
||||
if required:
|
||||
self.required_map[name] = required
|
||||
|
||||
def realize(self, args=None, progname=None, doc=None,
|
||||
raise_getopt_errs=True):
|
||||
"""Realize a configuration.
|
||||
|
||||
Optional arguments:
|
||||
|
||||
args -- the command line arguments, less the program name
|
||||
(default is sys.argv[1:])
|
||||
|
||||
progname -- the program name (default is sys.argv[0])
|
||||
|
||||
doc -- usage message (default is __doc__ of the options class)
|
||||
"""
|
||||
|
||||
# Provide dynamic default method arguments
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
if progname is None:
|
||||
progname = sys.argv[0]
|
||||
|
||||
self.progname = progname
|
||||
self.doc = doc or self.__doc__
|
||||
|
||||
self.options = []
|
||||
self.args = []
|
||||
|
||||
# Call getopt
|
||||
try:
|
||||
self.options, self.args = getopt.getopt(
|
||||
args, "".join(self.short_options), self.long_options)
|
||||
except getopt.error as msg:
|
||||
if raise_getopt_errs:
|
||||
self.usage(msg)
|
||||
|
||||
# Check for positional args
|
||||
if self.args and not self.positional_args_allowed:
|
||||
self.usage("positional arguments are not supported")
|
||||
|
||||
# Process options returned by getopt
|
||||
for opt, arg in self.options:
|
||||
name, handler = self.options_map[opt]
|
||||
if handler is not None:
|
||||
try:
|
||||
arg = handler(arg)
|
||||
except ValueError as msg:
|
||||
self.usage("invalid value for %s %r: %s" % (opt, arg, msg))
|
||||
if name and arg is not None:
|
||||
if getattr(self, name) is not None:
|
||||
if getattr(self, name) == arg:
|
||||
# Repeated option, but we don't mind because it
|
||||
# just reinforces what we have.
|
||||
continue
|
||||
self.usage("conflicting command line option %r" % opt)
|
||||
setattr(self, name, arg)
|
||||
|
||||
# Process environment variables
|
||||
for envvar in self.environ_map.keys():
|
||||
name, handler = self.environ_map[envvar]
|
||||
if name and getattr(self, name, None) is not None:
|
||||
continue
|
||||
if envvar in os.environ:
|
||||
value = os.environ[envvar]
|
||||
if handler is not None:
|
||||
try:
|
||||
value = handler(value)
|
||||
except ValueError as msg:
|
||||
self.usage("invalid environment value for %s %r: %s"
|
||||
% (envvar, value, msg))
|
||||
if name and value is not None:
|
||||
setattr(self, name, value)
|
||||
|
||||
if self.configfile is None:
|
||||
self.configfile = self.default_configfile()
|
||||
if self.zconfig_options and self.configfile is None:
|
||||
self.usage("configuration overrides (-X) cannot be used"
|
||||
" without a configuration file")
|
||||
if self.configfile is not None:
|
||||
# Process config file
|
||||
self.load_schema()
|
||||
try:
|
||||
self.load_configfile()
|
||||
except ZConfig.ConfigurationError as msg:
|
||||
self.usage(str(msg))
|
||||
|
||||
# Copy config options to attributes of self. This only fills
|
||||
# in options that aren't already set from the command line.
|
||||
for name, confname in self.names_list:
|
||||
if confname and getattr(self, name) is None:
|
||||
parts = confname.split(".")
|
||||
obj = self.configroot
|
||||
for part in parts:
|
||||
if obj is None:
|
||||
break
|
||||
# Here AttributeError is not a user error!
|
||||
obj = getattr(obj, part)
|
||||
setattr(self, name, obj)
|
||||
|
||||
# Process defaults
|
||||
for name, value in self.default_map.items():
|
||||
if getattr(self, name) is None:
|
||||
setattr(self, name, value)
|
||||
|
||||
# Process required options
|
||||
for name, message in self.required_map.items():
|
||||
if getattr(self, name) is None:
|
||||
self.usage(message)
|
||||
|
||||
if self.logsectionname:
|
||||
self.load_logconf(self.logsectionname)
|
||||
|
||||
def default_configfile(self):
|
||||
"""Return the name of the default config file, or None."""
|
||||
# This allows a default configuration file to be used without
|
||||
# affecting the -C command line option; setting self.configfile
|
||||
# before calling realize() makes the -C option unusable since
|
||||
# then realize() thinks it has already seen the option. If no
|
||||
# -C is used, realize() will call this method to try to locate
|
||||
# a configuration file.
|
||||
return None
|
||||
|
||||
def load_schema(self):
|
||||
if self.schema is None:
|
||||
# Load schema
|
||||
if self.schemadir is None:
|
||||
self.schemadir = os.path.dirname(__file__)
|
||||
self.schemafile = os.path.join(self.schemadir, self.schemafile)
|
||||
self.schema = ZConfig.loadSchema(self.schemafile)
|
||||
|
||||
def load_configfile(self):
|
||||
self.configroot, self.confighandlers = \
|
||||
ZConfig.loadConfig(self.schema, self.configfile,
|
||||
self.zconfig_options)
|
||||
|
||||
def load_logconf(self, sectname="eventlog"):
|
||||
parts = sectname.split(".")
|
||||
obj = self.configroot
|
||||
for p in parts:
|
||||
if obj is None:
|
||||
break
|
||||
obj = getattr(obj, p)
|
||||
self.config_logger = obj
|
||||
if obj is not None:
|
||||
obj.startup()
|
||||
|
||||
|
||||
class RunnerOptions(ZDOptions):
|
||||
|
||||
uid = gid = None
|
||||
|
||||
def __init__(self):
|
||||
ZDOptions.__init__(self)
|
||||
self.add("backofflimit", "runner.backoff_limit",
|
||||
"b:", "backoff-limit=", int, default=10)
|
||||
self.add("daemon", "runner.daemon", "d", "daemon", flag=1, default=1)
|
||||
self.add("forever", "runner.forever", "f", "forever",
|
||||
flag=1, default=0)
|
||||
self.add("sockname", "runner.socket_name", "s:", "socket-name=",
|
||||
existing_parent_dirpath, default="zdsock")
|
||||
self.add("exitcodes", "runner.exit_codes", "x:", "exit-codes=",
|
||||
list_of_ints, default=[0, 2])
|
||||
self.add("user", "runner.user", "u:", "user=")
|
||||
self.add("umask", "runner.umask", "m:", "umask=", octal_type,
|
||||
default=0o22)
|
||||
self.add("directory", "runner.directory", "z:", "directory=",
|
||||
existing_parent_directory)
|
||||
self.add("transcript", "runner.transcript", "t:", "transcript=",
|
||||
default="/dev/null")
|
||||
|
||||
|
||||
# ZConfig datatype
|
||||
|
||||
def list_of_ints(arg):
|
||||
if not arg:
|
||||
return []
|
||||
else:
|
||||
return list(map(int, arg.split(",")))
|
||||
|
||||
|
||||
def octal_type(arg):
|
||||
return int(arg, 8)
|
||||
|
||||
|
||||
def name2signal(string):
|
||||
"""Converts a signal name to canonical form.
|
||||
|
||||
Signal names are recognized without regard for case:
|
||||
|
||||
>>> name2signal('sighup')
|
||||
'SIGHUP'
|
||||
>>> name2signal('SigHup')
|
||||
'SIGHUP'
|
||||
>>> name2signal('SIGHUP')
|
||||
'SIGHUP'
|
||||
|
||||
The leading 'SIG' is not required::
|
||||
|
||||
>>> name2signal('hup')
|
||||
'SIGHUP'
|
||||
>>> name2signal('HUP')
|
||||
'SIGHUP'
|
||||
|
||||
Names that are not known cause an exception to be raised::
|
||||
|
||||
>>> name2signal('woohoo')
|
||||
Traceback (most recent call last):
|
||||
ValueError: could not convert 'woohoo' to signal name
|
||||
|
||||
>>> name2signal('sigwoohoo')
|
||||
Traceback (most recent call last):
|
||||
ValueError: could not convert 'sigwoohoo' to signal name
|
||||
|
||||
Numeric values are accepted to names as well::
|
||||
|
||||
>>> name2signal(str(signal.SIGHUP))
|
||||
'SIGHUP'
|
||||
|
||||
Numeric values that can't be matched to any signal known to Python
|
||||
are treated as errors::
|
||||
|
||||
>>> name2signal('-234')
|
||||
Traceback (most recent call last):
|
||||
ValueError: unsupported signal on this platform: -234
|
||||
|
||||
>>> name2signal(str(signal.NSIG)) #doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
ValueError: unsupported signal on this platform: ...
|
||||
|
||||
Non-signal attributes of the signal module are not mistakenly
|
||||
converted::
|
||||
|
||||
>>> name2signal('_ign')
|
||||
Traceback (most recent call last):
|
||||
ValueError: could not convert '_ign' to signal name
|
||||
|
||||
>>> name2signal('_DFL')
|
||||
Traceback (most recent call last):
|
||||
ValueError: could not convert '_DFL' to signal name
|
||||
|
||||
>>> name2signal('sig_ign')
|
||||
Traceback (most recent call last):
|
||||
ValueError: could not convert 'sig_ign' to signal name
|
||||
|
||||
>>> name2signal('SIG_DFL')
|
||||
Traceback (most recent call last):
|
||||
ValueError: could not convert 'SIG_DFL' to signal name
|
||||
|
||||
>>> name2signal('getsignal')
|
||||
Traceback (most recent call last):
|
||||
ValueError: could not convert 'getsignal' to signal name
|
||||
|
||||
"""
|
||||
try:
|
||||
v = int(string)
|
||||
except ValueError:
|
||||
if "_" in string:
|
||||
raise ValueError("could not convert %r to signal name" % string)
|
||||
if string.startswith('Signals.'): # py35 signals are an enum type
|
||||
string = string[len('Signals.'):]
|
||||
s = string.upper()
|
||||
if not s.startswith("SIG"):
|
||||
s = "SIG" + s
|
||||
v = getattr(signal, s, None)
|
||||
if isinstance(v, int):
|
||||
return s
|
||||
raise ValueError("could not convert %r to signal name" % string)
|
||||
if v >= signal.NSIG:
|
||||
raise ValueError("unsupported signal on this platform: %s" % string)
|
||||
for name in dir(signal):
|
||||
if "_" in name:
|
||||
continue
|
||||
if getattr(signal, name) == v:
|
||||
return name
|
||||
raise ValueError("unsupported signal on this platform: %s" % string)
|
||||
|
||||
|
||||
def existing_parent_directory(arg):
|
||||
path = os.path.expanduser(arg)
|
||||
if os.path.isdir(path):
|
||||
# If the directory exists, that's fine.
|
||||
return path
|
||||
parent, tail = os.path.split(path)
|
||||
if os.path.isdir(parent):
|
||||
return path
|
||||
raise ValueError('%s is not an existing directory' % arg)
|
||||
|
||||
|
||||
def existing_parent_dirpath(arg):
|
||||
path = os.path.expanduser(arg)
|
||||
dir = os.path.dirname(path)
|
||||
parent, tail = os.path.split(dir)
|
||||
if not parent:
|
||||
# relative pathname
|
||||
return path
|
||||
if os.path.isdir(parent):
|
||||
return path
|
||||
raise ValueError('The directory named as part of the path %s '
|
||||
'does not exist.' % arg)
|
||||
|
||||
|
||||
def _test(): # pragma: nocover
|
||||
# Stupid test program
|
||||
z = ZDOptions()
|
||||
z.add("program", "zdctl.program", "p:", "program=")
|
||||
print(z.names_list)
|
||||
z.realize()
|
||||
names = z.names_list[:]
|
||||
names.sort()
|
||||
for name, confname in names:
|
||||
print("%-20s = %.56r" % (name, getattr(z, name)))
|
||||
|
||||
if __name__ == "__main__":
|
||||
__file__ = sys.argv[0]
|
||||
_test()
|
||||
@@ -0,0 +1,734 @@
|
||||
#!python
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE
|
||||
#
|
||||
##############################################################################
|
||||
"""zrdun -- run an application as a daemon.
|
||||
|
||||
Usage: python zrdun.py [zrdun-options] program [program-arguments]
|
||||
"""
|
||||
|
||||
from stat import ST_MODE
|
||||
import errno
|
||||
import fcntl
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Add the parent of the script directory to the module search path
|
||||
# (but only when the script is run from inside the zdaemon package)
|
||||
from os.path import dirname, basename, abspath, normpath
|
||||
scriptdir = dirname(normpath(abspath(sys.argv[0])))
|
||||
if basename(scriptdir).lower() == "zdaemon":
|
||||
sys.path.append(dirname(scriptdir))
|
||||
here = os.path.dirname(os.path.realpath(__file__))
|
||||
swhome = os.path.dirname(here)
|
||||
for parts in [("src",), ("lib", "python"), ("Lib", "site-packages")]:
|
||||
d = os.path.join(swhome, *(parts + ("zdaemon",)))
|
||||
if os.path.isdir(d):
|
||||
d = os.path.join(swhome, *parts)
|
||||
sys.path.insert(0, d)
|
||||
break
|
||||
|
||||
from zdaemon.zdoptions import RunnerOptions
|
||||
from ZConfig.components.logger.loghandler import reopenFiles
|
||||
|
||||
|
||||
def string_list(arg):
|
||||
return arg.split()
|
||||
|
||||
|
||||
class ZDRunOptions(RunnerOptions):
|
||||
|
||||
__doc__ = __doc__
|
||||
|
||||
positional_args_allowed = 1
|
||||
logsectionname = "runner.eventlog"
|
||||
program = None
|
||||
|
||||
def __init__(self):
|
||||
RunnerOptions.__init__(self)
|
||||
self.add("schemafile", short="S:", long="schema=",
|
||||
default="schema.xml",
|
||||
handler=self.set_schemafile)
|
||||
self.add("stoptimeut", "runner.stop_timeout")
|
||||
self.add("starttestprogram", "runner.start_test_program")
|
||||
|
||||
def set_schemafile(self, file):
|
||||
self.schemafile = file
|
||||
|
||||
def realize(self, *args, **kwds):
|
||||
RunnerOptions.realize(self, *args, **kwds)
|
||||
if self.args:
|
||||
self.program = self.args
|
||||
if not self.program:
|
||||
self.usage("no program specified (use -C or positional args)")
|
||||
if self.sockname:
|
||||
# Convert socket name to absolute path
|
||||
self.sockname = os.path.abspath(self.sockname)
|
||||
if self.config_logger is None:
|
||||
# This doesn't perform any configuration of the logging
|
||||
# package, but that's reasonable in this case.
|
||||
self.logger = logging.getLogger()
|
||||
else:
|
||||
self.logger = self.config_logger()
|
||||
|
||||
def load_logconf(self, sectname):
|
||||
"""Load alternate eventlog if the specified section isn't present."""
|
||||
RunnerOptions.load_logconf(self, sectname)
|
||||
if self.config_logger is None and sectname != "eventlog":
|
||||
RunnerOptions.load_logconf(self, "eventlog")
|
||||
|
||||
|
||||
class Subprocess:
|
||||
|
||||
"""A class to manage a subprocess."""
|
||||
|
||||
# Initial state; overridden by instance variables
|
||||
pid = 0 # Subprocess pid; 0 when not running
|
||||
lasttime = 0 # Last time the subprocess was started; 0 if never
|
||||
|
||||
def __init__(self, options, args=None):
|
||||
"""Constructor.
|
||||
|
||||
Arguments are a ZDRunOptions instance and a list of program
|
||||
arguments; the latter's first item must be the program name.
|
||||
"""
|
||||
if args is None:
|
||||
args = options.args
|
||||
if not args:
|
||||
options.usage("missing 'program' argument")
|
||||
self.options = options
|
||||
self.args = args
|
||||
self.testing = set()
|
||||
self._set_filename(args[0])
|
||||
|
||||
def _set_filename(self, program):
|
||||
"""Internal: turn a program name into a file name, using $PATH."""
|
||||
if "/" in program:
|
||||
filename = program
|
||||
try:
|
||||
st = os.stat(filename)
|
||||
except os.error:
|
||||
self.options.usage("can't stat program %r" % program)
|
||||
else:
|
||||
path = get_path()
|
||||
for dir in path:
|
||||
filename = os.path.join(dir, program)
|
||||
try:
|
||||
st = os.stat(filename)
|
||||
except os.error:
|
||||
continue
|
||||
mode = st[ST_MODE]
|
||||
if mode & 0o111:
|
||||
break
|
||||
else:
|
||||
self.options.usage("can't find program %r on PATH %s" %
|
||||
(program, path))
|
||||
if not os.access(filename, os.X_OK):
|
||||
self.options.usage("no permission to run program %r" % filename)
|
||||
self.filename = filename
|
||||
|
||||
def test(self, pid):
|
||||
starttestprogram = self.options.starttestprogram
|
||||
try:
|
||||
while self.pid == pid:
|
||||
if not subprocess.call(starttestprogram):
|
||||
break
|
||||
time.sleep(1)
|
||||
finally:
|
||||
self.testing.remove(pid)
|
||||
|
||||
def spawn(self):
|
||||
"""Start the subprocess. It must not be running already.
|
||||
|
||||
Return the process id. If the fork() call fails, return 0.
|
||||
"""
|
||||
assert not self.pid
|
||||
self.lasttime = time.time()
|
||||
try:
|
||||
pid = os.fork()
|
||||
except os.error:
|
||||
return 0
|
||||
if pid != 0:
|
||||
# Parent
|
||||
self.pid = pid
|
||||
if self.options.starttestprogram:
|
||||
self.testing.add(pid)
|
||||
thread = threading.Thread(target=self.test, args=(pid,))
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
|
||||
self.options.logger.info("spawned process pid=%d" % pid)
|
||||
return pid
|
||||
else: # pragma: nocover
|
||||
# Child
|
||||
try:
|
||||
# Close file descriptors except std{in,out,err}.
|
||||
# XXX We don't know how many to close; hope 100 is plenty.
|
||||
for i in range(3, 100):
|
||||
try:
|
||||
os.close(i)
|
||||
except os.error:
|
||||
pass
|
||||
try:
|
||||
os.execv(self.filename, self.args)
|
||||
except os.error as err:
|
||||
sys.stderr.write("can't exec %r: %s\n" %
|
||||
(self.filename, err))
|
||||
sys.stderr.flush() # just in case
|
||||
finally:
|
||||
os._exit(127)
|
||||
# Does not return
|
||||
|
||||
def kill(self, sig):
|
||||
"""Send a signal to the subprocess. This may or may not kill it.
|
||||
|
||||
Return None if the signal was sent, or an error message string
|
||||
if an error occurred or if the subprocess is not running.
|
||||
"""
|
||||
if not self.pid:
|
||||
return "no subprocess running"
|
||||
try:
|
||||
os.kill(self.pid, sig)
|
||||
except os.error as msg:
|
||||
return str(msg)
|
||||
return None
|
||||
|
||||
def setstatus(self, sts):
|
||||
"""Set process status returned by wait() or waitpid().
|
||||
|
||||
This simply notes the fact that the subprocess is no longer
|
||||
running by setting self.pid to 0.
|
||||
"""
|
||||
self.pid = 0
|
||||
|
||||
|
||||
class Daemonizer:
|
||||
|
||||
def main(self, args=None):
|
||||
self.options = ZDRunOptions()
|
||||
self.options.realize(args)
|
||||
self.logger = self.options.logger
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
self.proc = Subprocess(self.options)
|
||||
self.opensocket()
|
||||
try:
|
||||
self.setsignals()
|
||||
if self.options.daemon:
|
||||
self.daemonize()
|
||||
self.runforever()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(self.options.sockname)
|
||||
except os.error:
|
||||
pass
|
||||
|
||||
mastersocket = None
|
||||
commandsocket = None
|
||||
|
||||
def opensocket(self):
|
||||
sockname = self.options.sockname
|
||||
tempname = "%s.%d" % (sockname, os.getpid())
|
||||
self.unlink_quietly(tempname)
|
||||
while 1:
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.bind(tempname)
|
||||
os.chmod(tempname, 0o700)
|
||||
try:
|
||||
os.link(tempname, sockname)
|
||||
break
|
||||
except os.error:
|
||||
# Lock contention, or stale socket.
|
||||
self.checkopen()
|
||||
# Stale socket -- delete, sleep, and try again.
|
||||
msg = "Unlinking stale socket %s; sleep 1" % sockname
|
||||
sys.stderr.write(msg + "\n")
|
||||
sys.stderr.flush() # just in case
|
||||
self.logger.warn(msg)
|
||||
self.unlink_quietly(sockname)
|
||||
sock.close()
|
||||
time.sleep(1)
|
||||
continue
|
||||
finally:
|
||||
self.unlink_quietly(tempname)
|
||||
sock.listen(1)
|
||||
sock.setblocking(0)
|
||||
try: # PEP 446, Python >= 3.4
|
||||
sock.set_inheritable(True)
|
||||
except AttributeError:
|
||||
pass
|
||||
self.mastersocket = sock
|
||||
|
||||
def unlink_quietly(self, filename):
|
||||
try:
|
||||
os.unlink(filename)
|
||||
except os.error:
|
||||
pass
|
||||
|
||||
def checkopen(self):
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.connect(self.options.sockname)
|
||||
s.send(b"status\n")
|
||||
data = s.recv(1000).decode()
|
||||
s.close()
|
||||
except socket.error:
|
||||
pass
|
||||
else:
|
||||
data = data.rstrip("\n")
|
||||
msg = ("Another zrdun is already up using socket %r:\n%s" %
|
||||
(self.options.sockname, data))
|
||||
sys.stderr.write(msg + "\n")
|
||||
sys.stderr.flush() # just in case
|
||||
self.logger.critical(msg)
|
||||
sys.exit(1)
|
||||
|
||||
def setsignals(self):
|
||||
signal.signal(signal.SIGTERM, self.sigexit)
|
||||
signal.signal(signal.SIGHUP, self.sigexit)
|
||||
signal.signal(signal.SIGINT, self.sigexit)
|
||||
signal.signal(signal.SIGCHLD, self.sigchild)
|
||||
|
||||
def sigexit(self, sig, frame):
|
||||
self.logger.critical("daemon manager killed by %s" % signame(sig))
|
||||
sys.exit(1)
|
||||
|
||||
waitstatus = None
|
||||
|
||||
def sigchild(self, sig, frame):
|
||||
try:
|
||||
pid, sts = os.waitpid(-1, os.WNOHANG)
|
||||
except os.error:
|
||||
return
|
||||
if pid:
|
||||
self.waitstatus = pid, sts
|
||||
|
||||
transcript = None
|
||||
|
||||
def daemonize(self):
|
||||
|
||||
# To daemonize, we need to become the leader of our own session
|
||||
# (process) group. If we do not, signals sent to our
|
||||
# parent process will also be sent to us. This might be bad because
|
||||
# signals such as SIGINT can be sent to our parent process during
|
||||
# normal (uninteresting) operations such as when we press Ctrl-C in the
|
||||
# parent terminal window to escape from a logtail command.
|
||||
# To disassociate ourselves from our parent's session group we use
|
||||
# os.setsid. It means "set session id", which has the effect of
|
||||
# disassociating a process from is current session and process group
|
||||
# and setting itself up as a new session leader.
|
||||
#
|
||||
# Unfortunately we cannot call setsid if we're already a session group
|
||||
# leader, so we use "fork" to make a copy of ourselves that is
|
||||
# guaranteed to not be a session group leader.
|
||||
#
|
||||
# We also change directories, set stderr and stdout to null, and
|
||||
# change our umask.
|
||||
#
|
||||
# This explanation was (gratefully) garnered from
|
||||
# http://www.hawklord.uklinux.net/system/daemons/d3.htm
|
||||
|
||||
pid = os.fork()
|
||||
if pid != 0: # pragma: nocover
|
||||
# Parent
|
||||
self.logger.debug("daemon manager forked; parent exiting")
|
||||
os._exit(0)
|
||||
# Child
|
||||
self.logger.info("daemonizing the process")
|
||||
if self.options.directory:
|
||||
try:
|
||||
os.chdir(self.options.directory)
|
||||
except os.error as err:
|
||||
self.logger.warn("can't chdir into %r: %s"
|
||||
% (self.options.directory, err))
|
||||
else:
|
||||
self.logger.info("set current directory: %r"
|
||||
% self.options.directory)
|
||||
os.close(0)
|
||||
sys.stdin = sys.__stdin__ = open("/dev/null")
|
||||
self.transcript = Transcript(self.options.transcript)
|
||||
os.setsid()
|
||||
os.umask(self.options.umask)
|
||||
# XXX Stevens, in his Advanced Unix book, section 13.3 (page
|
||||
# 417) recommends calling umask(0) and closing unused
|
||||
# file descriptors. In his Network Programming book, he
|
||||
# additionally recommends ignoring SIGHUP and forking again
|
||||
# after the setsid() call, for obscure SVR4 reasons.
|
||||
|
||||
should_be_up = True
|
||||
delay = 0 # If nonzero, delay starting or killing until this time
|
||||
killing = 0 # If true, send SIGKILL when delay expires
|
||||
proc = None # Subprocess instance
|
||||
|
||||
def runforever(self):
|
||||
sig_r, sig_w = os.pipe()
|
||||
fcntl.fcntl(sig_r, fcntl.F_SETFL, fcntl.fcntl(sig_r, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
fcntl.fcntl(sig_w, fcntl.F_SETFL, fcntl.fcntl(sig_w, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
signal.set_wakeup_fd(sig_w)
|
||||
self.logger.info("daemon manager started")
|
||||
while self.should_be_up or self.proc.pid:
|
||||
if self.should_be_up and not self.proc.pid and not self.delay:
|
||||
pid = self.proc.spawn()
|
||||
if not pid:
|
||||
# Can't fork. Try again later...
|
||||
self.delay = time.time() + self.backofflimit
|
||||
if self.waitstatus:
|
||||
self.reportstatus()
|
||||
r, w, x = [self.mastersocket, sig_r], [], []
|
||||
if self.commandsocket:
|
||||
r.append(self.commandsocket)
|
||||
timeout = self.options.backofflimit
|
||||
if self.delay:
|
||||
timeout = max(0, min(timeout, self.delay - time.time()))
|
||||
if timeout <= 0:
|
||||
self.delay = 0
|
||||
if self.killing and self.proc.pid:
|
||||
self.proc.kill(signal.SIGKILL)
|
||||
self.delay = time.time() + self.options.backofflimit
|
||||
try:
|
||||
r, w, x = select.select(r, w, x, timeout)
|
||||
except select.error as err:
|
||||
if err.args[0] != errno.EINTR:
|
||||
raise
|
||||
r = w = x = []
|
||||
if self.waitstatus:
|
||||
self.reportstatus()
|
||||
if self.commandsocket and self.commandsocket in r:
|
||||
try:
|
||||
self.dorecv()
|
||||
except socket.error as msg:
|
||||
self.logger.exception("socket.error in dorecv(): %s"
|
||||
% str(msg))
|
||||
self.commandsocket = None
|
||||
if self.mastersocket in r:
|
||||
try:
|
||||
self.doaccept()
|
||||
except socket.error as msg:
|
||||
self.logger.exception("socket.error in doaccept(): %s"
|
||||
% str(msg))
|
||||
self.commandsocket = None
|
||||
if sig_r in r:
|
||||
os.read(sig_r, 1) # don't let the buffer fill up
|
||||
self.logger.info("Exiting")
|
||||
sys.exit(0)
|
||||
|
||||
def reportstatus(self):
|
||||
pid, sts = self.waitstatus
|
||||
self.waitstatus = None
|
||||
es, msg = decode_wait_status(sts)
|
||||
msg = "pid %d: " % pid + msg
|
||||
if pid != self.proc.pid:
|
||||
msg = "unknown " + msg
|
||||
self.logger.warn(msg)
|
||||
else:
|
||||
killing = self.killing
|
||||
if killing:
|
||||
self.killing = 0
|
||||
self.delay = 0
|
||||
else:
|
||||
self.governor()
|
||||
self.proc.setstatus(sts)
|
||||
if es in self.options.exitcodes and not killing:
|
||||
msg = msg + "; exiting now"
|
||||
self.logger.info(msg)
|
||||
sys.exit(es)
|
||||
self.logger.info(msg)
|
||||
|
||||
backoff = 0
|
||||
|
||||
def governor(self):
|
||||
# Back off if respawning too frequently
|
||||
now = time.time()
|
||||
if not self.proc.lasttime:
|
||||
pass
|
||||
elif now - self.proc.lasttime < self.options.backofflimit:
|
||||
# Exited rather quickly; slow down the restarts
|
||||
self.backoff += 1
|
||||
if self.backoff >= self.options.backofflimit:
|
||||
if self.options.forever:
|
||||
self.backoff = self.options.backofflimit
|
||||
else:
|
||||
self.logger.critical("restarting too frequently; quit")
|
||||
sys.exit(1)
|
||||
self.logger.info("sleep %s to avoid rapid restarts" % self.backoff)
|
||||
self.delay = now + self.backoff
|
||||
else:
|
||||
# Reset the backoff timer
|
||||
self.backoff = 0
|
||||
self.delay = 0
|
||||
|
||||
def doaccept(self):
|
||||
if self.commandsocket:
|
||||
# Give up on previous command socket!
|
||||
self.sendreply("Command superseded by new command")
|
||||
self.commandsocket.close()
|
||||
self.commandsocket = None
|
||||
self.commandsocket, addr = self.mastersocket.accept()
|
||||
try: # PEP 446, Python >= 3.4
|
||||
self.commandsocket.set_inheritable(True)
|
||||
except AttributeError:
|
||||
pass
|
||||
self.commandbuffer = b""
|
||||
|
||||
def dorecv(self):
|
||||
data = self.commandsocket.recv(1000)
|
||||
if not data:
|
||||
self.sendreply("Command not terminated by newline")
|
||||
self.commandsocket.close()
|
||||
self.commandsocket = None
|
||||
self.commandbuffer += data
|
||||
if b"\n" in self.commandbuffer:
|
||||
self.docommand()
|
||||
self.commandsocket.close()
|
||||
self.commandsocket = None
|
||||
elif len(self.commandbuffer) > 10000:
|
||||
self.sendreply("Command exceeds 10 KB")
|
||||
self.commandsocket.close()
|
||||
self.commandsocket = None
|
||||
|
||||
def docommand(self):
|
||||
lines = self.commandbuffer.split(b"\n")
|
||||
args = lines[0].split()
|
||||
if not args:
|
||||
self.sendreply("Empty command")
|
||||
return
|
||||
command = args[0].decode()
|
||||
methodname = "cmd_" + command
|
||||
method = getattr(self, methodname, None)
|
||||
if method:
|
||||
method([a.decode() for a in args])
|
||||
else:
|
||||
self.sendreply("Unknown command %r; 'help' for a list" % command)
|
||||
|
||||
def cmd_start(self, args):
|
||||
self.should_be_up = True
|
||||
self.backoff = 0
|
||||
self.delay = 0
|
||||
self.killing = 0
|
||||
if not self.proc.pid:
|
||||
self.proc.spawn()
|
||||
self.sendreply("Application started")
|
||||
else:
|
||||
self.sendreply("Application already started")
|
||||
|
||||
def cmd_stop(self, args):
|
||||
self.should_be_up = False
|
||||
self.backoff = 0
|
||||
self.delay = 0
|
||||
self.killing = 0
|
||||
if self.proc.pid:
|
||||
self.proc.kill(signal.SIGTERM)
|
||||
self.sendreply("Sent SIGTERM")
|
||||
self.killing = 1
|
||||
if self.options.stoptimeut:
|
||||
self.delay = time.time() + self.options.stoptimeut
|
||||
else:
|
||||
self.sendreply("Application already stopped")
|
||||
|
||||
def cmd_restart(self, args):
|
||||
self.should_be_up = True
|
||||
self.backoff = 0
|
||||
self.delay = 0
|
||||
self.killing = 0
|
||||
if self.proc.pid:
|
||||
self.proc.kill(signal.SIGTERM)
|
||||
self.sendreply("Sent SIGTERM; will restart later")
|
||||
self.killing = 1
|
||||
if self.options.stoptimeut:
|
||||
self.delay = time.time() + self.options.stoptimeut
|
||||
else:
|
||||
self.proc.spawn()
|
||||
self.sendreply("Application started")
|
||||
|
||||
def cmd_kill(self, args):
|
||||
if args[1:]:
|
||||
try:
|
||||
sig = int(args[1])
|
||||
except:
|
||||
self.sendreply("Bad signal %r" % args[1])
|
||||
return
|
||||
else:
|
||||
sig = signal.SIGTERM
|
||||
if not self.proc.pid:
|
||||
self.sendreply("Application not running")
|
||||
else:
|
||||
msg = self.proc.kill(sig)
|
||||
if msg:
|
||||
self.sendreply("Kill %d failed: %s" % (sig, msg))
|
||||
else:
|
||||
self.sendreply("Signal %d sent" % sig)
|
||||
|
||||
def cmd_status(self, args):
|
||||
if not self.proc.pid:
|
||||
status = "stopped"
|
||||
else:
|
||||
status = "running"
|
||||
self.sendreply("status=%s\n" % status +
|
||||
"now=%r\n" % time.time() +
|
||||
"should_be_up=%d\n" % self.should_be_up +
|
||||
"delay=%r\n" % self.delay +
|
||||
"backoff=%r\n" % self.backoff +
|
||||
"lasttime=%r\n" % self.proc.lasttime +
|
||||
"application=%r\n" % self.proc.pid +
|
||||
"testing=%d\n" % bool(self.proc.testing) +
|
||||
"manager=%r\n" % os.getpid() +
|
||||
"backofflimit=%r\n" % self.options.backofflimit +
|
||||
"filename=%r\n" % self.proc.filename +
|
||||
"args=%r\n" % self.proc.args)
|
||||
|
||||
def cmd_reopen_transcript(self, args):
|
||||
reopenFiles()
|
||||
if self.transcript is not None:
|
||||
self.transcript.reopen()
|
||||
|
||||
def sendreply(self, msg):
|
||||
try:
|
||||
if not msg.endswith("\n"):
|
||||
msg = msg + "\n"
|
||||
msg = msg.encode()
|
||||
if hasattr(self.commandsocket, "sendall"):
|
||||
self.commandsocket.sendall(msg)
|
||||
else: # pragma: nocover
|
||||
# This is quadratic, but msg is rarely more than 100 bytes :-)
|
||||
while msg:
|
||||
sent = self.commandsocket.send(msg)
|
||||
msg = msg[sent:]
|
||||
except socket.error as msg:
|
||||
self.logger.warn("Error sending reply: %s" % str(msg))
|
||||
|
||||
|
||||
class Transcript:
|
||||
|
||||
def __init__(self, filename):
|
||||
self.read_from, w = os.pipe()
|
||||
os.dup2(w, 1)
|
||||
sys.stdout = sys.__stdout__ = os.fdopen(1, "w", 1)
|
||||
os.dup2(w, 2)
|
||||
sys.stderr = sys.__stderr__ = os.fdopen(2, "w", 1)
|
||||
self.filename = filename
|
||||
self.file = open(filename, 'ab', 0)
|
||||
self.write = self.file.write
|
||||
self.lock = threading.Lock()
|
||||
thread = threading.Thread(target=self.copy)
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
|
||||
def copy(self):
|
||||
try:
|
||||
lock = self.lock
|
||||
i = [self.read_from]
|
||||
o = e = []
|
||||
while 1:
|
||||
ii, oo, ee = select.select(i, o, e)
|
||||
with lock:
|
||||
for fd in ii:
|
||||
self.write(os.read(fd, 8192))
|
||||
finally:
|
||||
# since there's no reader from this pipe we want the other side to
|
||||
# get a SIGPIPE as soon as it tries to write to it, instead of
|
||||
# deadlocking when the pipe buffer becomes full.
|
||||
os.close(self.read_from)
|
||||
|
||||
def reopen(self):
|
||||
new_file = open(self.filename, 'ab', 0)
|
||||
with self.lock:
|
||||
self.file.close()
|
||||
self.file = new_file
|
||||
self.write = self.file.write
|
||||
|
||||
|
||||
# Helpers for dealing with signals and exit status
|
||||
|
||||
def decode_wait_status(sts):
|
||||
"""Decode the status returned by wait() or waitpid().
|
||||
|
||||
Return a tuple (exitstatus, message) where exitstatus is the exit
|
||||
status, or -1 if the process was killed by a signal; and message
|
||||
is a message telling what happened. It is the caller's
|
||||
responsibility to display the message.
|
||||
"""
|
||||
if os.WIFEXITED(sts):
|
||||
es = os.WEXITSTATUS(sts) & 0xffff
|
||||
msg = "exit status %s" % es
|
||||
return es, msg
|
||||
elif os.WIFSIGNALED(sts):
|
||||
sig = os.WTERMSIG(sts)
|
||||
msg = "terminated by %s" % signame(sig)
|
||||
if hasattr(os, "WCOREDUMP"):
|
||||
iscore = os.WCOREDUMP(sts)
|
||||
else:
|
||||
iscore = sts & 0x80
|
||||
if iscore:
|
||||
msg += " (core dumped)"
|
||||
return -1, msg
|
||||
else:
|
||||
msg = "unknown termination cause 0x%04x" % sts
|
||||
return -1, msg
|
||||
|
||||
_signames = None
|
||||
|
||||
|
||||
def signame(sig):
|
||||
"""Return a symbolic name for a signal.
|
||||
|
||||
Return "signal NNN" if there is no corresponding SIG name in the
|
||||
signal module.
|
||||
"""
|
||||
|
||||
if _signames is None:
|
||||
_init_signames()
|
||||
return _signames.get(sig) or "signal %d" % sig
|
||||
|
||||
|
||||
def _init_signames():
|
||||
global _signames
|
||||
d = {}
|
||||
for k, v in signal.__dict__.items():
|
||||
k_startswith = getattr(k, "startswith", None)
|
||||
if k_startswith is None: # pragma: nocover
|
||||
continue
|
||||
if k_startswith("SIG") and not k_startswith("SIG_"):
|
||||
d[v] = k
|
||||
_signames = d
|
||||
|
||||
|
||||
def get_path():
|
||||
"""Return a list corresponding to $PATH, or a default."""
|
||||
path = ["/bin", "/usr/bin", "/usr/local/bin"]
|
||||
if "PATH" in os.environ:
|
||||
p = os.environ["PATH"]
|
||||
if p:
|
||||
path = p.split(os.pathsep)
|
||||
return path
|
||||
|
||||
|
||||
# Main program
|
||||
|
||||
def main(args=None):
|
||||
assert os.name == "posix", "This code makes many Unix-specific assumptions"
|
||||
|
||||
d = Daemonizer()
|
||||
d.main(args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user