Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Z
ZODB
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kirill Smelkov
ZODB
Commits
399c6ba3
Commit
399c6ba3
authored
Jan 24, 2005
by
Tim Peters
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Stitch in a current version of zdaemon; several
bugfixes have piled up since this was last done.
parent
71a3a673
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
2691 additions
and
0 deletions
+2691
-0
branches/3.3/src/zdaemon/DEPENDENCIES.cfg
branches/3.3/src/zdaemon/DEPENDENCIES.cfg
+1
-0
branches/3.3/src/zdaemon/SETUP.cfg
branches/3.3/src/zdaemon/SETUP.cfg
+2
-0
branches/3.3/src/zdaemon/__init__.py
branches/3.3/src/zdaemon/__init__.py
+14
-0
branches/3.3/src/zdaemon/component.xml
branches/3.3/src/zdaemon/component.xml
+275
-0
branches/3.3/src/zdaemon/sample.conf
branches/3.3/src/zdaemon/sample.conf
+24
-0
branches/3.3/src/zdaemon/schema.xml
branches/3.3/src/zdaemon/schema.xml
+26
-0
branches/3.3/src/zdaemon/tests/__init__.py
branches/3.3/src/zdaemon/tests/__init__.py
+1
-0
branches/3.3/src/zdaemon/tests/donothing.sh
branches/3.3/src/zdaemon/tests/donothing.sh
+6
-0
branches/3.3/src/zdaemon/tests/nokill.py
branches/3.3/src/zdaemon/tests/nokill.py
+8
-0
branches/3.3/src/zdaemon/tests/parent.py
branches/3.3/src/zdaemon/tests/parent.py
+32
-0
branches/3.3/src/zdaemon/tests/testzdoptions.py
branches/3.3/src/zdaemon/tests/testzdoptions.py
+294
-0
branches/3.3/src/zdaemon/tests/testzdrun.py
branches/3.3/src/zdaemon/tests/testzdrun.py
+294
-0
branches/3.3/src/zdaemon/zdctl.py
branches/3.3/src/zdaemon/zdctl.py
+584
-0
branches/3.3/src/zdaemon/zdoptions.py
branches/3.3/src/zdaemon/zdoptions.py
+411
-0
branches/3.3/src/zdaemon/zdrun.py
branches/3.3/src/zdaemon/zdrun.py
+719
-0
No files found.
branches/3.3/src/zdaemon/DEPENDENCIES.cfg
0 → 100644
View file @
399c6ba3
ZConfig
branches/3.3/src/zdaemon/SETUP.cfg
0 → 100644
View file @
399c6ba3
script zdctl.py
script zdrun.py
branches/3.3/src/zdaemon/__init__.py
0 → 100644
View file @
399c6ba3
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation 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."""
branches/3.3/src/zdaemon/component.xml
0 → 100644
View file @
399c6ba3
<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=
"existing-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=
"false"
>
<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=
"existing-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=
"user"
datatype=
"string"
required=
"no"
>
<description>
Command-line option: -u or --user.
When zdrun.py is started by root, this option specifies the
user as who the the zdrun.py process (and hence the daemon
subprocess) will run. This can be a user name or a numeric
user id. Both the user and the group are set from the
corresponding password entry, using setuid() and setgid().
This is done before zdrun.py does anything else besides
parsing its command line arguments.
NOTE: when zdrun.py is not started by root, specifying this
option is an error. (XXX This may be a mistake.)
XXX The zdrun.py event log file may be opened *before*
setuid() is called. Is this good or bad?
</description>
</key>
<key
name=
"umask"
datatype=
"zdaemon.zdoptions.octal_type"
required=
"no"
default=
"022"
>
<description>
Command-line option: -m or --umask.
When daemon mode is used, this option specifies the octal umask
of the subprocess.
</description>
</key>
<key
name=
"hang-around"
datatype=
"boolean"
required=
"no"
default=
"false"
>
<description>
If this option is true, the zdrun.py process will remain even
when the daemon subprocess is stopped. In this case, zdctl.py
will restart zdrun.py as necessary. If this option is false,
zdrun.py will exit when the daemon subprocess is stopped
(unless zdrun.py intends to restart it).
</description>
</key>
<key
name=
"default-to-interactive"
datatype=
"boolean"
required=
"no"
default=
"true"
>
<description>
If this option is true, zdctl.py enters interactive mode
when it is invoked without a positional command argument. If
it is false, you must use the -i or --interactive command line
option to zdctl.py to enter interactive mode.
</description>
</key>
<key
name=
"logfile"
datatype=
"existing-dirpath"
required=
"no"
>
<description>
This option specifies a log file that is the default target of
the "logtail" zdctl.py command.
NOTE: This is NOT the log file to which zdrun.py writes its
logging messages! That log file is specified by the
<
eventlog
>
section.
</description>
</key>
<key
name=
"transcript"
datatype=
"existing-dirpath"
required=
"no"
>
<description>
The name of a file in which a transcript of all output from
the command being run will be written to when daemonized.
If not specified, output from the command will be discarded.
This only takes effect when the "daemon" option is enabled.
</description>
</key>
<key
name=
"prompt"
datatype=
"string"
required=
"no"
>
<description>
The prompt shown by the controller program. The default must
be provided by the application.
</description>
</key>
</sectiontype>
</component>
branches/3.3/src/zdaemon/sample.conf
0 → 100644
View file @
399c6ba3
# 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
>
branches/3.3/src/zdaemon/schema.xml
0 → 100644
View file @
399c6ba3
<schema>
<description>
This schema describes various options that control zdctl.py and
zdrun.py. zdrun.py is the "daemon process manager"; it runs a
subprocess in the background and restarts it when it crashes.
zdctl.py is the user interface to zdrun.py; it can tell zdrun.py
to start, stop or restart the subprocess, send it a signal, etc.
There are two sections:
<
runner
>
defines options unique
zdctl.py and zdrun.py, and
<
eventlog
>
defines a standard
event logging section used by zdrun.py.
More information about zdctl.py and zdrun.py can be found in the
file Doc/zdctl.txt. This all is specific to Unix/Linux.
</description>
<import
package=
"ZConfig.components.logger"
/>
<import
package=
"zdaemon"
/>
<section
name=
"*"
type=
"runner"
attribute=
"runner"
required=
"yes"
/>
<section
name=
"*"
type=
"eventlog"
attribute=
"eventlog"
required=
"no"
/>
</schema>
branches/3.3/src/zdaemon/tests/__init__.py
0 → 100644
View file @
399c6ba3
# This file is needed to make this a package.
branches/3.3/src/zdaemon/tests/donothing.sh
0 → 100755
View file @
399c6ba3
#!/bin/sh
while
[
"1"
-ne
"2"
]
;
do
sleep
10
done
branches/3.3/src/zdaemon/tests/nokill.py
0 → 100755
View file @
399c6ba3
#! /usr/bin/env python
import
signal
signal
.
signal
(
signal
.
SIGTERM
,
signal
.
SIG_IGN
)
while
1
:
signal
.
pause
()
branches/3.3/src/zdaemon/tests/parent.py
0 → 100644
View file @
399c6ba3
import
time
import
os
import
sys
def
main
():
# dummy zdctl startup of zdrun
shutup
()
file
=
os
.
path
.
normpath
(
os
.
path
.
abspath
(
sys
.
argv
[
0
]))
dir
=
os
.
path
.
dirname
(
file
)
zctldir
=
os
.
path
.
dirname
(
dir
)
zdrun
=
os
.
path
.
join
(
zctldir
,
'zdrun.py'
)
args
=
[
sys
.
executable
,
zdrun
]
args
+=
[
'-d'
,
'-b'
,
'10'
,
'-s'
,
os
.
path
.
join
(
dir
,
'testsock'
),
'-x'
,
'0,2'
,
'-z'
,
dir
,
os
.
path
.
join
(
dir
,
'donothing.sh'
)]
flag
=
os
.
P_NOWAIT
#cmd = ' '.join([sys.executable] + args)
#print cmd
os
.
spawnvp
(
flag
,
args
[
0
],
args
)
while
1
:
# wait to be signaled
time
.
sleep
(
1
)
def
shutup
():
os
.
close
(
0
)
sys
.
stdin
=
sys
.
__stdin__
=
open
(
"/dev/null"
)
os
.
close
(
1
)
sys
.
stdout
=
sys
.
__stdout__
=
open
(
"/dev/null"
,
"w"
)
os
.
close
(
2
)
sys
.
stderr
=
sys
.
__stderr__
=
open
(
"/dev/null"
,
"w"
)
if
__name__
==
'__main__'
:
main
()
branches/3.3/src/zdaemon/tests/testzdoptions.py
0 → 100644
View file @
399c6ba3
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation 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
unittest
from
StringIO
import
StringIO
import
ZConfig
import
zdaemon
from
zdaemon.zdoptions
import
ZDOptions
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
):
save_sys_stderr
=
sys
.
stderr
try
:
sys
.
stderr
=
StringIO
()
try
:
options
.
realize
(
args
)
except
SystemExit
,
err
:
self
.
assertEqual
(
err
.
code
,
2
)
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
)
def
test_help
(
self
):
for
arg
in
"-h"
,
"--h"
,
"--help"
:
options
=
self
.
OptionsClass
()
try
:
self
.
save_streams
()
try
:
options
.
realize
([
arg
])
finally
:
self
.
restore_streams
()
except
SystemExit
,
err
:
self
.
assertEqual
(
err
.
code
,
0
)
else
:
self
.
fail
(
"%s didn't call sys.exit()"
%
repr
(
arg
))
def
test_unrecognized
(
self
):
# Check that we get an error for an unrecognized option
self
.
check_exit_code
(
self
.
OptionsClass
(),
[
"-/"
])
class
TestBasicFunctionality
(
TestZDOptions
):
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_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
.
assert_
(
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_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'
])
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
,
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"
])
def
test_suite
():
suite
=
unittest
.
TestSuite
()
for
cls
in
[
TestBasicFunctionality
,
TestZDOptionsEnvironment
,
TestCommandLineOverrides
]:
suite
.
addTest
(
unittest
.
makeSuite
(
cls
))
return
suite
if
__name__
==
"__main__"
:
unittest
.
main
(
defaultTest
=
'test_suite'
)
branches/3.3/src/zdaemon/tests/testzdrun.py
0 → 100644
View file @
399c6ba3
"""Test suite for zdrun.py."""
import
os
import
sys
import
time
import
signal
import
tempfile
import
unittest
import
socket
from
StringIO
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
):
if
type
(
args
)
is
type
(
""
):
args
=
args
.
split
()
try
:
zdctl
.
main
([
"-s"
,
self
.
zdsock
]
+
args
,
cmdclass
=
cmdclass
)
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 testInvoke(self):
## self._run("echo -n")
## self.expect = ""
## def testControl(self):
## self.rundaemon(["sleep", "1000"])
## time.sleep(1)
## self._run("stop")
## time.sleep(1)
## self._run("exit")
## self.expect = "Sent SIGTERM\nExiting now\n"
## def testStop(self):
## self.rundaemon([self.python, self.nokill])
## time.sleep(1)
## self._run("stop")
## time.sleep(1)
## self._run("exit")
## self.expect = "Sent SIGTERM\nSent SIGTERM; will exit later\n"
def
testHelp
(
self
):
self
.
_run
(
"-h"
)
import
__main__
self
.
expect
=
__main__
.
__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
,
err
:
self
.
failIf
(
err
.
code
)
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).
zdrun_socket
=
os
.
path
.
join
(
self
.
here
,
'testsock'
)
zdctlpid
=
os
.
spawnvp
(
os
.
P_NOWAIT
,
sys
.
executable
,
[
sys
.
executable
,
os
.
path
.
join
(
self
.
here
,
'parent.py'
)]
)
# 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
.
assert_
(
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
)
self
.
assert_
(
response
is
not
None
and
'
\
n
'
in
response
)
# Kill the process.
send_action
(
'exit
\
n
'
,
zdrun_socket
)
def
testUmask
(
self
):
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
.
assert_
(
os
.
path
.
exists
(
path
))
self
.
assert_
(
not
os
.
access
(
path
,
os
.
W_OK
))
finally
:
if
os
.
path
.
exists
(
path
):
os
.
remove
(
path
)
def
send_action
(
action
,
sockname
):
"""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
+
"
\
n
"
)
sock
.
shutdown
(
1
)
# We're not writing any more
response
=
""
while
1
:
data
=
sock
.
recv
(
1000
)
if
not
data
:
break
response
+=
data
sock
.
close
()
return
response
except
socket
.
error
,
msg
:
return
None
def
test_suite
():
suite
=
unittest
.
TestSuite
()
if
os
.
name
==
"posix"
:
suite
.
addTest
(
unittest
.
makeSuite
(
ZDaemonTests
))
return
suite
if
__name__
==
'__main__'
:
__file__
=
sys
.
argv
[
0
]
unittest
.
main
(
defaultTest
=
'test_suite'
)
branches/3.3/src/zdaemon/zdctl.py
0 → 100755
View file @
399c6ba3
#!python
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation 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:
-C/--configure URL -- configuration file or URL
-S/--schema XML Schema -- XML schema for configuration file
-h/--help -- print usage message and exit
-b/--backoff-limit SECONDS -- set backoff limit to SECONDS (default 10)
-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
-i/--interactive -- start an interactive shell after executing commands
-l/--logfile -- log file to be read by logtail command
-p/--program PROGRAM -- the program to run
-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")
-z/--directory DIRECTORY -- directory to chdir to when using -d (default off)
action [arguments] -- see below
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.
"""
import
os
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
))
from
zdaemon.zdoptions
import
RunnerOptions
def
string_list
(
arg
):
return
arg
.
split
()
class
ZDCtlOptions
(
RunnerOptions
):
positional_args_allowed
=
1
def
__init__
(
self
):
RunnerOptions
.
__init__
(
self
)
self
.
add
(
"schemafile"
,
short
=
"S:"
,
long
=
"schema="
,
default
=
"schema.xml"
,
handler
=
self
.
set_schemafile
)
self
.
add
(
"interactive"
,
None
,
"i"
,
"interactive"
,
flag
=
1
)
self
.
add
(
"default_to_interactive"
,
"runner.default_to_interactive"
,
default
=
1
)
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
(
"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
and
not
self
.
interactive
:
if
not
self
.
default_to_interactive
:
self
.
usage
(
"either -i or an action argument is required"
)
self
.
interactive
=
1
# Where's python?
if
not
self
.
python
:
self
.
python
=
sys
.
executable
# Where's zdrun?
if
not
self
.
zdrun
:
if
__name__
==
"__main__"
:
file
=
sys
.
argv
[
0
]
else
:
file
=
__file__
file
=
os
.
path
.
normpath
(
os
.
path
.
abspath
(
file
))
dir
=
os
.
path
.
dirname
(
file
)
self
.
zdrun
=
os
.
path
.
join
(
dir
,
"zdrun.py"
)
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__"
:
{}})
if
args
!=
self
.
options
.
program
:
print
"WARNING! zdrun is managing a different program!"
print
"our program ="
,
self
.
options
.
program
print
"daemon's args ="
,
args
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
+
"
\
n
"
)
sock
.
shutdown
(
1
)
# We're not writing any more
response
=
""
while
1
:
data
=
sock
.
recv
(
1000
)
if
not
data
:
break
response
+=
data
sock
.
close
()
return
response
except
socket
.
error
,
msg
:
return
None
def
get_status
(
self
):
self
.
zd_up
=
0
self
.
zd_pid
=
0
self
.
zd_status
=
None
resp
=
self
.
send_action
(
"status"
)
if
not
resp
:
return
m
=
re
.
search
(
"(?m)^application=(
\
d+)$
"
, resp)
if not m:
return
self.zd_up = 1
self.zd_pid = int(m.group(1))
self.zd_status = resp
def awhile(self, cond, msg):
try:
self.get_status()
while not cond():
sys.stdout.write("
.
")
sys.stdout.flush()
time.sleep(1)
self.get_status()
except KeyboardInterrupt:
print "
^
C
"
else:
print msg % self.__dict__
def help_help(self):
print "
help
--
Print
a
list
of
available
actions
.
"
print "
help
<
action
>
--
Print
help
for
<
action
>
.
"
def do_EOF(self, arg):
print
return 1
def help_EOF(self):
print "
To
quit
,
type
^
D
or
use
the
quit
command
.
"
def do_start(self, arg):
self.get_status()
if not self.zd_up:
args = [
self.options.python,
self.options.zdrun,
]
args += self._get_override("
-
S
", "
schemafile
")
args += self._get_override("
-
C
", "
configfile
")
args += self._get_override("
-
b", "
backofflimit
")
args += self._get_override("
-
d
", "
daemon
", flag=1)
args += self._get_override("
-
f", "
forever
", flag=1)
args += self._get_override("
-
s
", "
sockname
")
args += self._get_override("
-
u", "
user
")
args += self._get_override("
-
m
", "
umask
")
args += self._get_override(
"
-
x
", "
exitcodes
", "
,
".join(map(str, self.options.exitcodes)))
args += self._get_override("
-
z
", "
directory
")
args.extend(self.options.program)
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
self.awhile(lambda: self.zd_pid,
"
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:
print "
daemon
process
not
running
"
else:
self.send_action("
stop
")
self.awhile(lambda: 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_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: self.zd_pid not in (0, pid),
"
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:
sig = signal.SIGTERM
else:
try:
sig = int(arg)
except: # int() can raise any number of exceptions
print "
invalid
signal
number
", `arg`
return
self.get_status()
if not self.zd_pid:
print "
daemon
process
not
running
"
return
print "
kill
(
%
d
,
%
d
)
" % (self.zd_pid, sig)
try:
os.kill(self.zd_pid, sig)
except os.error, msg:
print "
Error
:
", msg
else:
print "
signal
%
d
sent
to
process
%
d
" % (sig, 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: 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=""):
if arg not in ["", "
-
l
"]:
print "
status
argument
must
be
absent
or
-
l
"
return
self.get_status()
if not self.zd_up:
print "
daemon
manager
not
running
"
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
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, 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 "
interactive
:
", repr(self.options.interactive)
print "
default_to_interactive
:
",
print repr(self.options.default_to_interactive)
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)
print "
umask
:
", oct(self.options.umask)
print "
directory
:
", repr(self.options.directory)
print "
logfile
:
", repr(self.options.logfile)
print "
hang_around
:
", repr(self.options.hang_around)
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 complete_show(self, text, *ignored):
options = ["
options
", "
python
", "
all
"]
return [x for x in options if x.startswith(text)]
def do_logreopen(self, arg):
self.do_kill(str(signal.SIGUSR2))
def help_logreopen(self):
print "
logreopen
--
Send
a
SIGUSR2
signal
to
the
daemon
process
.
"
print "
This
is
designed
to
reopen
the
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, msg:
print msg
except OSError, 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_shell(self, arg):
if not arg:
arg = os.getenv("
SHELL
") or "
/
bin
/
sh
"
try:
os.system(arg)
except KeyboardInterrupt:
print
def help_shell(self):
print "
shell
[
command
]
--
Execute
a
shell
command
.
"
print "
Without
a
command
,
start
an
interactive
sh
.
"
print "
An
alias
for
this
command
is
!
[
command
]
"
def do_reload(self, arg):
if arg:
args = arg.split()
if self.options.configfile:
args = ["
-
C
", self.options.configfile] + args
else:
args = None
options = ZDCtlOptions()
options.positional_args_allowed = 0
try:
options.realize(args)
except SystemExit:
print "
Configuration
not
reloaded
"
else:
self.options = options
if self.options.configfile:
print "
Configuration
reloaded
from
", self.options.configfile
else:
print "
Configuration
reloaded
without
a
config
file
"
def help_reload(self):
print "
reload
[
options
]
--
Reload
the
configuration
.
"
print "
Without
options
,
this
reparses
the
command
line
.
"
print "
With
options
,
this
substitutes
'options'
for
the
"
print "
command
line
,
except
that
if
no
-
C
option
is
given
,
"
print "
the
last
configuration
file
is
used
.
"
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 = "
".join(self.options.program)
print program
try:
os.system(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 do_quit(self, arg):
self.get_status()
if not self.zd_up:
print "
daemon
manager
not
running
"
elif not self.zd_pid:
print "
daemon
process
not
running
;
stopping
daemon
manager
"
self.send_action("
exit
")
self.awhile(lambda: not self.zd_up, "
daemon
manager
stopped
")
else:
print "
daemon
process
and
daemon
manager
still
running
"
return 1
def help_quit(self):
print "
quit
--
Exit
the
zdctl
shell
.
"
print "
If
the
daemon
process
is
not
running
,
"
print "
stop
the
daemon
manager
.
"
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 options is None:
options = ZDCtlOptions()
options.realize(args)
c = cmdclass(options)
if options.args:
c.onecmd("
".join(options.args))
if options.interactive:
try:
import readline
except ImportError:
pass
print "
program
:
", "
".join(options.program)
c.do_status()
c.cmdloop()
if __name__ == "
__main__
":
main()
branches/3.3/src/zdaemon/zdoptions.py
0 → 100644
View file @
399c6ba3
##############################################################################
#
# Copyright (c) 2003 Zope Corporation 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."""
import
os
import
sys
import
getopt
import
ZConfig
class
ZDOptions
:
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
.
add
(
None
,
None
,
"h"
,
"help"
,
self
.
help
)
self
.
add
(
"configfile"
,
None
,
"C:"
,
"configure="
)
self
.
add
(
None
,
None
,
"X:"
,
handler
=
self
.
zconfig_options
.
append
)
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
doc
.
find
(
"%s"
)
>
0
:
doc
=
doc
.
replace
(
"%s"
,
self
.
progname
)
print
doc
,
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
self
.
default_map
.
has_key
(
name
):
del
self
.
default_map
[
name
]
if
self
.
required_map
.
has_key
(
name
):
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
self
.
options_map
.
has_key
(
key
):
del
self
.
options_map
[
key
]
if
long
:
key
=
"--"
+
long
if
key
[
-
1
]
==
"="
:
key
=
key
[:
-
1
]
if
self
.
options_map
.
has_key
(
key
):
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
self
.
options_map
.
has_key
(
key
):
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
self
.
options_map
.
has_key
(
key
):
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 __main__.__doc__)
"""
# Provide dynamic default method arguments
if
args
is
None
:
try
:
args
=
sys
.
argv
[
1
:]
except
AttributeError
:
args
=
()
if
progname
is
None
:
try
:
progname
=
sys
.
argv
[
0
]
except
(
AttributeError
,
IndexError
):
progname
=
'zope'
if
doc
is
None
:
import
__main__
doc
=
__main__
.
__doc__
self
.
progname
=
progname
self
.
doc
=
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
,
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
,
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
:
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
os
.
environ
.
has_key
(
envvar
):
value
=
os
.
environ
[
envvar
]
if
handler
is
not
None
:
try
:
value
=
handler
(
value
)
except
ValueError
,
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
,
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
==
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
=
0
)
self
.
add
(
"forever"
,
"runner.forever"
,
"f"
,
"forever"
,
flag
=
1
,
default
=
0
)
self
.
add
(
"sockname"
,
"runner.socket_name"
,
"s:"
,
"socket-name="
,
ZConfig
.
datatypes
.
existing_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
=
022
)
self
.
add
(
"directory"
,
"runner.directory"
,
"z:"
,
"directory="
,
ZConfig
.
datatypes
.
existing_directory
)
self
.
add
(
"hang_around"
,
"runner.hang_around"
,
default
=
0
)
def
realize
(
self
,
*
args
,
**
kwds
):
ZDOptions
.
realize
(
self
,
*
args
,
**
kwds
)
# Additional checking of user option; set uid and gid
if
self
.
user
is
not
None
:
import
pwd
try
:
uid
=
int
(
self
.
user
)
except
ValueError
:
try
:
pwrec
=
pwd
.
getpwnam
(
self
.
user
)
except
KeyError
:
self
.
usage
(
"username %r not found"
%
self
.
user
)
uid
=
pwrec
[
2
]
else
:
try
:
pwrec
=
pwd
.
getpwuid
(
uid
)
except
KeyError
:
self
.
usage
(
"uid %r not found"
%
self
.
user
)
gid
=
pwrec
[
3
]
self
.
uid
=
uid
self
.
gid
=
gid
# ZConfig datatype
def
list_of_ints
(
arg
):
if
not
arg
:
return
[]
else
:
return
map
(
int
,
arg
.
split
(
","
))
def
octal_type
(
arg
):
return
int
(
arg
,
8
)
def
_test
():
# 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
()
branches/3.3/src/zdaemon/zdrun.py
0 → 100755
View file @
399c6ba3
#!python
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation 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]
Options:
-C/--configure URL -- configuration file or URL
-S/--schema XML Schema -- XML schema for configuration file
-b/--backoff-limit SECONDS -- set backoff limit to SECONDS (default 10)
-d/--daemon -- run as a proper daemon; fork a subprocess, setsid(), etc.
-f/--forever -- run forever (by default, exit when backoff limit is exceeded)
-h/--help -- print this usage message and exit
-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)
-t/--transcript FILE -- transript of output from daemon-mode program
-x/--exit-codes LIST -- list of fatal exit codes (default "0,2")
-z/--directory DIRECTORY -- directory to chdir to when using -d (default off)
program [program-arguments] -- an arbitrary application to run
This daemon manager has two purposes: it restarts the application when
it dies, and (when requested to do so with the -d option) it runs the
application in the background, detached from the foreground tty
session that started it (if any).
Exit codes: if at any point the application exits with an exit status
listed by the -x option, it is not restarted. Any other form of
termination (either being killed by a signal or exiting with an exit
status not listed in the -x option) causes it to be restarted.
Backoff limit: when the application exits (nearly) immediately after a
restart, the daemon manager starts slowing down by delaying between
restarts. The delay starts at 1 second and is increased by one on
each restart up to the backoff limit given by the -b option; it is
reset when the application runs for more than the backoff limit
seconds. By default, when the delay reaches the backoff limit, the
daemon manager exits (under the assumption that the application has a
persistent fault). The -f (forever) option prevents this exit; use it
when you expect that a temporary external problem (such as a network
outage or an overfull disk) may prevent the application from starting
but you want the daemon manager to keep trying.
"""
"""
XXX TO DO
- Finish OO design -- use multiple classes rather than folding
everything into one class.
- Add unit tests.
- Add doc strings.
"""
import
os
import
sys
import
time
import
errno
import
logging
import
socket
import
select
import
signal
from
stat
import
ST_MODE
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
))
from
zdaemon.zdoptions
import
RunnerOptions
class
ZDRunOptions
(
RunnerOptions
):
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
(
"transcript"
,
"runner.transcript"
,
"t:"
,
"transcript="
,
default
=
"/dev/null"
)
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
.
_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
&
0111
:
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
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
self
.
options
.
logger
.
info
(
"spawned process pid=%d"
%
pid
)
return
pid
else
:
# 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
,
err
:
sys
.
stderr
.
write
(
"can't exec %r: %s
\
n
"
%
(
self
.
filename
,
err
))
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
,
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
.
set_uid
()
self
.
run
()
def
set_uid
(
self
):
if
self
.
options
.
uid
is
None
:
return
uid
=
os
.
geteuid
()
if
uid
!=
0
and
uid
!=
self
.
options
.
uid
:
self
.
options
.
usage
(
"only root can use -u USER to change users"
)
os
.
setgid
(
self
.
options
.
gid
)
os
.
setuid
(
self
.
options
.
uid
)
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
,
0700
)
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
"
)
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
)
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
(
"status
\
n
"
)
data
=
s
.
recv
(
1000
)
s
.
close
()
except
socket
.
error
:
pass
else
:
while
data
.
endswith
(
"
\
n
"
):
data
=
data
[:
-
1
]
msg
=
(
"Another zrdun is already up using socket %r:
\
n
%s"
%
(
self
.
options
.
sockname
,
data
))
sys
.
stderr
.
write
(
msg
+
"
\
n
"
)
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
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
:
# 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
,
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"
)
os
.
close
(
1
)
sys
.
stdout
=
sys
.
__stdout__
=
open
(
self
.
options
.
transcript
,
"a"
,
0
)
os
.
close
(
2
)
sys
.
stderr
=
sys
.
__stderr__
=
open
(
self
.
options
.
transcript
,
"a"
,
0
)
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.
mood
=
1
# 1: up, 0: down, -1: suicidal
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
):
self
.
logger
.
info
(
"daemon manager started"
)
min_mood
=
not
self
.
options
.
hang_around
while
self
.
mood
>=
min_mood
or
self
.
proc
.
pid
:
if
self
.
mood
>
0
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
],
[],
[]
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
,
err
:
if
err
[
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
,
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
,
msg
:
self
.
logger
.
exception
(
"socket.error in doaccept(): %s"
%
str
(
msg
))
self
.
commandsocket
=
None
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
()
self
.
commandbuffer
=
""
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
"
\
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
(
"
\
n
"
)
args
=
lines
[
0
].
split
()
if
not
args
:
self
.
sendreply
(
"Empty command"
)
return
command
=
args
[
0
]
methodname
=
"cmd_"
+
command
method
=
getattr
(
self
,
methodname
,
None
)
if
method
:
method
(
args
)
else
:
self
.
sendreply
(
"Unknown command %r; 'help' for a list"
%
args
[
0
])
def
cmd_start
(
self
,
args
):
self
.
mood
=
1
# Up
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
.
mood
=
0
# Down
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
self
.
delay
=
time
.
time
()
+
self
.
options
.
backofflimit
else
:
self
.
sendreply
(
"Application already stopped"
)
def
cmd_restart
(
self
,
args
):
self
.
mood
=
1
# Up
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
self
.
delay
=
time
.
time
()
+
self
.
options
.
backofflimit
else
:
self
.
proc
.
spawn
()
self
.
sendreply
(
"Application started"
)
def
cmd_exit
(
self
,
args
):
self
.
mood
=
-
1
# Suicidal
self
.
backoff
=
0
self
.
delay
=
0
self
.
killing
=
0
if
self
.
proc
.
pid
:
self
.
proc
.
kill
(
signal
.
SIGTERM
)
self
.
sendreply
(
"Sent SIGTERM; will exit later"
)
self
.
killing
=
1
self
.
delay
=
time
.
time
()
+
self
.
options
.
backofflimit
else
:
self
.
sendreply
(
"Exiting now"
)
self
.
logger
.
info
(
"Exiting"
)
sys
.
exit
(
0
)
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
()
+
"mood=%d
\
n
"
%
self
.
mood
+
"delay=%r
\
n
"
%
self
.
delay
+
"backoff=%r
\
n
"
%
self
.
backoff
+
"lasttime=%r
\
n
"
%
self
.
proc
.
lasttime
+
"application=%r
\
n
"
%
self
.
proc
.
pid
+
"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_help
(
self
,
args
):
self
.
sendreply
(
"Available commands:
\
n
"
" help -- return command help
\
n
"
" status -- report application status (default command)
\
n
"
" kill [signal] -- send a signal to the application
\
n
"
" (default signal is SIGTERM)
\
n
"
" start -- start the application if not already running
\
n
"
" stop -- stop the application if running
\
n
"
" (the daemon manager keeps running)
\
n
"
" restart -- stop followed by start
\
n
"
" exit -- stop the application and exit
\
n
"
)
def
sendreply
(
self
,
msg
):
try
:
if
not
msg
.
endswith
(
"
\
n
"
):
msg
=
msg
+
"
\
n
"
if
hasattr
(
self
.
commandsocket
,
"sendall"
):
self
.
commandsocket
.
sendall
(
msg
)
else
:
# This is quadratic, but msg is rarely more than 100 bytes :-)
while
msg
:
sent
=
self
.
commandsocket
.
send
(
msg
)
msg
=
msg
[
sent
:]
except
socket
.
error
,
msg
:
self
.
logger
.
warn
(
"Error sending reply: %s"
%
str
(
msg
))
# 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
:
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
os
.
environ
.
has_key
(
"PATH"
):
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
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment