tested django-newsletter

This commit is contained in:
Esther Kleinhenz
2018-10-26 15:21:17 +02:00
parent 7ed667d043
commit 811b7c5453
2352 changed files with 448169 additions and 1 deletions
@@ -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
&lt;eventlog&gt; 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: &lt;runner&gt; defines options unique
zdctl.py and zdrun.py, and &lt;eventlog&gt; 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()