Commit 2987b932 authored by Chris Withers's avatar Chris Withers

Merge of my recent changes from the 2.12 branch:

- Ignore pyd's when they're built in place.
- add sphinx section to buildout and make.bat for Windows user
- de-cruft of tree
- revamp of docs
- rework of Windows service stuff to make zopeservice.py in instances unnecessary
- make zopectl work properly on Windows
parents 1b53d8d0 ea03022f
Place files containing code for External Method objects in this
directory.
......@@ -7,6 +7,7 @@ parts =
alltests
allpy
dependencies
docs
extends = versions.cfg
unzip = true
......@@ -28,7 +29,6 @@ eggs = Zope2
interpreter = zopepy
scripts = zopepy
[alltests]
recipe = zc.recipe.testrunner
eggs =
......@@ -122,3 +122,7 @@ exclude =
ClientForm
docutils
mechanize
[docs]
recipe = zc.recipe.egg
eggs = sphinx
Credits
=======
The Zope software receives contributions from far and wide. Here's
the Zope Hall of Fame:
- Stephen Purcell allows us to distribute his PyUnit unit testing
framework with Zope.
- Jeff Bauer is Zope Dude Number One. Jeff took over PCGI and
kept pushing it forward through the years.
- Sam Rushing worked with us at Digital Creations to make Medusa
the publishing platform for ZServer and the concurrency of Zope2.
- A subset of windows guru Mark Hammond's win32 extensions are
bundled with win32 binary distributions of Zope.
- Martijn Pieters and Brian Hooper contributed the #in reverse
attribute.
- Phillip Eby contributed the DTML 'let' tag and many
other useful ideas, including the inspiration for the DTML
'call', 'with' and 'return'
tags.
- The DateTime module was based on work from Ted Horst.
- Jordan Baker contributed the 'try' tag, something we've wanted
for a long, long time.
- Martijn Pieters chipped in with a safe range function.
- Michael Hauser came up with the name "Zope".
- Eric Kidd from Userland contributed to ZPublisher's support for
XML-RPC.
- Andrew M. Kuchling wrote the initial version of mod_pcgi, making
him extremely cool in our book.
- Oleg Broytmann has taken up the standard of mod_pcgi and moving
it to be a really amazing thing, and ready for prime time.
- Jephte CLAIN made some patches to European ZopeTime.
- Thanks to Gregor Hoffleit for his work in getting Zope into the
Debian distribution.
- All the other Zopistas far and wide that stuck with us during
the Bobo/Principia days and politely push us to make the best damn
app server on this or any other planet.
- Of course the list of credits would be quite incomplete without
mentioning Guido van Rossum, benevolent dictator of Python and
long-time friend of Digital Creations. Zope Power is Python
Power.
- Special thanks to Richard Stallman and the Free Software
Foundation for their assistance and feedback on the
GPL-compatible 2.0 version of the Zope Public License.
========================================
Building and installing Zope from source
========================================
============================
Building and Installing Zope
============================
Welcome to Zope! This document describes building and installing
Zope on UNIX and Linux.
.. highlight:: bash
See ``doc/WINDOWS.rst`` for information about Windows.
This document descibes how to get going with Zope.
Prerequisites
-------------
=============
System requirements when building from source
In order to use Zope, you must have the following pre-requisites
available:
- A supported version of Python, including the development support if
installed from system-level packages. Supported versions include:
......@@ -24,55 +24,100 @@ System requirements when building from source
headers installed which correspond to your system's ``zlib``.
- A C compiler capable of building extension modules for your Python
(gcc recommended).
(gcc recommended). This is not necessary for Windows as binary
releases of the parts that would need compiling are always made
available.
- If you wish to install Zope as a Service on Windows, you will need
to have the `pywin32`__ package installed.
Building Zope using zc.buildout
-------------------------------
__ https://sourceforge.net/projects/pywin32/
Zope is built using the ``zc.buildout`` library, which needs to be
"bootstrapped" with your Python version. E.g.::
Installing Zope
===============
$ cd /path/to/zope
$ /path/to/your/python bootstrap/bootstrap.py
Unless using buildout to build a zope instance as described
:ref:`below <buildout-instances>`, you will need to install Zope
separately. If you want to create a buildout-based Zope instance,
please skip directly to that section.
Installing Zope using virtualenv
--------------------------------
Zope can be installed within a virtualized Python environment using
``virtualenv`` as follows::
$ virtualenv --no-site-packages my_zope
$ cd my_zope
$ source bin/activate
$ bin/easy_install -i http://download.zope.org/Zope2/index/<Zope version> Zope2
Using ``virtualenv`` is **highly recommended**. Otherwise, you may encounter
unexpected conflicts with packages that have already been installed.
Once you've installed Zope, you'll need to :ref:`create an instance <classic-instances>`.
Installing Zope using zc.buildout
---------------------------------
Unless you are `developing zope`__, you most likely
want to be creating a :ref:`buildout-based Zope instance <buildout-instances>` rather
that installing using buildout as described in this section.
The bootstrap script creates a ``buildout`` script in ``bin``; run this
script to finish building Zope::
__ http://docs.zope.org/developer/
However, if you really just want to create Zope instances using the
classic ``mkzopeinstance`` but with the software installed by buildout,
then you need to do the following:
- Download the Zope 2 source distribution from `PyPI`__
__ http://pypi.python.org/pypi/Zope2
- Bootstrap the buildout
- Run the buildout
On Linux, this can be done as follows::
$ wget http://pypi.python.org/packages/source/Z/Zope2/Zope2-<Zope version>.tar.gz
$ tar xfvz Zope2-2.12.0.tar.gz
$ cd Zope2-2.12.0
$ /path/to/your/python bootstrap/bootstrap.py
$ bin/buildout
Once you've installed Zope, you'll need to :ref:`create an instance <classic-instances>`.
Installing Zope using easy_install
----------------------------------
Zope can be installed using ``easy_install`` either using a global
easy_install installation or within a virtualized Python environment
(using ``virtualenv``)::
Zope can be installed using ``easy_install``, but it is recommended to
use ``virtualenv`` as described above to avoid unexpected conflicts
with other packages installed directly in your python installation.
$ virtualenv --no-site-packages my_zope
$ cd my_zope
$ source bin/activate
$ bin/easy_install -i http://download.zope.org/Zope2/index/<Zope version> Zope2
However, if you want to use easy_install globally, all you need to do
is::
This will create the related scripts like ``mkzopeinstance`` within the
``bin`` folder of your global or virtualized Python environment.
$ easy_install -i http://download.zope.org/Zope2/index/<Zope version> Zope2
Using ``virtualenv`` is **highly recommended**. Otherwise you may encounter
unexpected conflicts with already installed packages.
This will create the related scripts such as ``mkzopeinstance`` within the
scripts folder of your python installation. You can then use them to
create instances as described below.
.. _classic-instances:
Creating a Zope Instance
------------------------
Creating a classic Zope Instance
================================
Once you've performed the install step, to begin actually using
Zope, you will need to create an "instance home", which is a
directory that contains configuration and data for a Zope server
process. The instance home is created using the ``mkzopeinstance``
script::
Once you've installed Zope, you will need to create an "instance
home". This is a directory that contains configuration and data for a
Zope server process. The instance home is created using the
``mkzopeinstance`` script::
$ bin/mkzopeinstance
If you use Zope from SVN, you will need to specify the Python interpreter
to use for the instance explicitly::
You can specify the Python interpreter to use for the instance
explicitly::
$ bin/mkzopeinstance --python=$PWD/bin/zopepy
......@@ -82,8 +127,97 @@ command-line options, run the script with the ``--help`` option::
$ bin/mkzopeinstance --help
.. note::
The traditional "inplace" build is no longer supported. If using
``mkzopeinstance``, always do so outside the buildout/virtualenv
environment. If you wish to manage your Zope instance using
buildout, please see the section below.
Starting Zope as a Daemon
.. _buildout-instances:
Creating a buildout-based Zope Instance
=======================================
If you wish to use buildout to manage your Zope instance, then the
instance is created as follows:
* Create a directory for your instance. In this directory, create a
``etc``, ``logs`` and ``var`` subdirectories.
* Download the following file into your instance directory:
`http://svn.zope.org/*checkout*/zc.buildout/trunk/bootstrap/bootstrap.py`__
__ http://svn.zope.org/*checkout*/zc.buildout/trunk/bootstrap/bootstrap.py
.. highlight:: none
* Create a buildout configuration as follows:
.. topic:: buildout.cfg
:class: file
::
[buildout]
parts = instance
extends = http://svn.zope.org/*checkout*/Zope/tags/<Zope version>/versions.cfg
[instance]
recipe = zc.recipe.egg
eggs = Zope2
interpreter = py
scripts = runzope zopectl
initialization =
import sys
sys.argv[1:1] = ['-C',r'${buildout:directory}/etc/zope.conf']
This is the minimum but all the usual buildout techniques can be
used.
* Create a Zope configuration file starting as follows:
.. topic:: etc/zope.cfg
:class: file
::
%define INSTANCE <path to your instance directory>
python $INSTANCE/bin/py[.exe on Windows]
instancehome $INSTANCE
.. highlight:: bash
* Now, run the following commands::
$ /path/to/your/python bootstrap.py
$ bin/buildout
In the ``bin`` subdirectory of your instance directory, you will
find ``runzope`` and ``zopectl`` scripts that can be used as
normal.
Using your Zope instance
========================
There are various ways to run Zope from your newly created
instance. They are all described below.
Running Zope in the Foreground
------------------------------
To run Zope without detaching from the console, use the ``fg``
command (short for ``foreground``)::
$ /path/to/zope/instance/bin/zopectl fg
In this mode, Zope emits its log messages to the console, and does not
detach from terminal.
Running Zope as a Daemon
-------------------------
Once an instance home has been created, the Zope server can now be
......@@ -95,23 +229,47 @@ During start, zope emits log messages into ./log/event.log
You can examine it with the usual tools (cat, more, tail)
and see if there are any errors preventing zope from starting.
.. highlight:: none
.. note::
Running Zope in the Foreground
------------------------------
For this to work on Windows, the Zope instance must be installed as
a Service. This is done with::
By default, ``zopectl start`` will start a background process (a
"daemon) that manages Zope. ``zopectl stop`` will stop the background
process. To run Zope without detaching from the console, use the ``fg``
command (short for ``foreground``)::
bin\zopectl install
$ /path/to/zope/instance/bin/zopectl fg
If you later want to remove this Service, do the following::
In this mode, Zope emits its log messages to the console, and does not
detach from terminal.
bin\zopectl remove
For the full list of options available for setting up Zope as a
Windows Service, do::
bin\zopectl install --help
.. highlight:: bash
Integrating with System Startup
-------------------------------
zopectl can be linked as rc-script in the usual start directories
on linux or other System V unix variants.
You can use ``zopectl`` interactively as a command shell by just
calling it without any arguments. Try ``help`` there and ``help <command>``
to find out about additionally commands of zopectl. These commands
also work at the command line.
.. note::
On Windows, a Service can be installed and set to start
automatically with the following:
.. code-block:: none
bin\zopectl install --startup=auto
Configuring Zope
----------------
================
Your Zope instance is configured through a file, either found by
default::
......@@ -127,8 +285,8 @@ or passed explicitly on the commandline::
Config file: /tmp/other.conf
When starting Zope, if you see errors indicating that an address is in
use, then you will have to supply arguments to runzope to change the ports
used for HTTP or FTP. The default HTTP and FTP ports used by Zope are
use, then you may have to change the ports Zope uses for HTTP or FTP.
The default HTTP and FTP ports used by Zope are
8080 and 8021 respectively. You can change the ports used by
editing ./etc/zope.conf appropriately.
......@@ -143,21 +301,8 @@ The section in the configuration file looks like this::
The address can just be a port number as shown, or a host:port
pair to bind only to a specific interface.
Integrating with System Startup
-------------------------------
zopectl can be linked as rc-script in the usual start directories
on linux or other System V unix variants.
You can use ``zopectl`` interactively as a command shell by just
calling it without any arguments. Try ``help`` there and ``help <command>``
to find out about additionally commands of zopectl. These commands
also work at the command line.
Logging In To Zope
------------------
==================
Once you've started Zope, you can then connect to the Zope webserver
by directing your browser to::
......@@ -172,6 +317,11 @@ You will be prompted for a user name and password. Use the user name
and password you provided in response to the prompts issued during
the "make instance" process.
If you are using a buildout-based Zope instance, you will need to
create a user as follows::
$ bin/zopectl adduser username password
Now you're off and running! You should be looking at the Zope
management screen which is divided into two frames. On the left you
can navigate between Zope objects and on the right you can edit them
......@@ -180,12 +330,10 @@ of the frame.
If you haven't used Zope before, you should head to the Zope web
site and read some documentation. The Zope Documentation section is
a good place to start. You can access it at:
http://docs.zope.org/
a good place to start. You can access it at http://docs.zope.org/
Troubleshooting
---------------
===============
- This version of Zope requires Python 2.5.4 or better, including
2.6.x. It will *not* run with Python 3.x.
......@@ -201,4 +349,4 @@ Troubleshooting
you built Python from source all the configuration information
should already be available.
- See ``doc/CHANGES.rst`` for important notes on this version of Zope.
- See the :doc:`CHANGES` for important notes on this version of Zope.
Setting the initial user name and password
==========================================
Filesytem Permissions
=====================
Because Zope is managed through the web, user names and passwords must be
used to assure that only authorized people can make changes to a Zope
installation.
Some user name and password is needed to "bootstrap" the creation of
normal managers of your Zope site. This is accomplished through the
use of the file 'inituser'. The first time Zope starts, it will detect
that no users have been defined in the root user folder. It will search
for the 'inituser' file and, if it exists, will add the user defined
in the file to the root user folder.
Normally, 'inituser' is created by the Zope install scripts. Either
the installer prompts for the password or a randomly generated
password is created and displayed at the end of the build script.
You can use the 'zpasswd.py' script to create 'inituser' yourself.
Execute 'zpasswd.py' like this::
python zpasswd.py inituser
The script will prompt you for the name, password, and allowed
domains. The default is to encode the password with SHA, so please
remember this password as there is no way to recover it (although
'zpasswd.py' lets you reset it.)
In some situations you may need to bypass normal security controls
because you have lost your password or because the security settings
have been mixed up. Zope provides a facility called an "emergency
user" so that you can reset passwords and correct security
settings.
The emergency user password must be defined outside the application
user interface. It is defined in the 'access' file located
in the Zope directory. It should be readable only by the user
as which your web server runs.
To create the emergency user, use 'zpasswd.py' to create the
'access' file like this::
python zpasswd.py access
In order to provide a somewhat higher level of security, various
encoding schemes are supported which provide access to either SHA-1
encryption or the standard UNIX crypt facility if it has been compiled
into Python. Unless you have some special requirements (see below),
you should use the SHA-1 facility, which is the default.
Format of 'inituser' and 'access'
---------------------------------
A password file should consist of a single line of the form::
name:password
Note that you may also add an optional third component to the line in the
access file to restrict access by domain. For example, the line::
mario:nintendoRules:*.mydomain.com
in your 'access' file will only allow permit emergency user access
from `*.mydomain.com` machines. Attempts to access the system from
other domains will fail, even if the correct emergency user name
and password are used.
Please note that if you use the ZServer monitor capability, you will
need to run with a clear text password.
Setting permissions on the var directory
----------------------------------------
You need to set permissions on the Zope var directory.
Zope needs to read and write data from its var directory. Before
You need to set permissions on the directory Zope uses to store its
data. This will normally be the `var` directory in the instance home.
Zope needs to read and write data to this directory. Before
running Zope you should ensure that you give adequate permissions
to the Zope var directory for the userid Zope will run under.
to this directory for the userid Zope will run under.
Depending on how you choose to run Zope you will need to give
different permissions to the var directory. If you use Zope with an
different permissions to the directory. If you use Zope with an
existing web server, it will probably run Zope as 'nobody'. In this
case 'nobody' needs read and write permissions to the var directory.
If you change the way you run Zope you may need to modify the permissions
of the var directory and the files in it to allow Zope to read and write
If you change the way you run Zope, you may need to modify the permissions
of the directory and the files in it to allow Zope to read and write
under its changed userid.
......@@ -2,9 +2,9 @@ Zope effective user support
===========================
.. note::
It is best practice running Zope behind a reverse proxy like
Apache, Squid or Varnish. In this case you do not need to run
or install Zope with root privileges since the reverse proxy
It is best practice to run Zope behind a reverse proxy like
Apache, Squid or Varnish. In this case, you do not need to run
or install Zope with root privileges, since the reverse proxy
will bind to port 80 and proxy back all request to Zope running
on an unpriviledged port.
......
Special Users
=============
Because Zope is managed through the web, user names and passwords must be
used to assure that only authorized people can make changes to a Zope
installation.
Adding Managers
---------------
If you need to add a Manager to an existing Zope instance, you can do
this using `zopectl` as follows::
zopectl adduser `name` `password`
The Initial User
----------------
An initial username and password is needed to "bootstrap" the creation of
normal managers of your Zope site. This is accomplished through the
use of the 'inituser' file in the directory specified as the instance
home.
The first time Zope starts, it will detect
that no users have been defined in the root user folder. It will search
for the 'inituser' file and, if it exists, will add the user defined
in the file to the root user folder.
Normally, 'inituser' is created by the Zope install scripts. Either
the installer prompts for the password or a randomly generated
password is created and displayed at the end of the build script.
You can use the 'zpasswd.py' script to create 'inituser' yourself.
Execute 'zpasswd.py' like this::
python zpasswd.py inituser
The script will prompt you for the name, password, and allowed
domains. The default is to encode the password with SHA, so please
remember this password as there is no way to recover it (although
'zpasswd.py' lets you reset it.)
The Emergency User
------------------
In some situations you may need to bypass normal security controls
because you have lost your password or because the security settings
have been mixed up. Zope provides a facility called an "emergency
user" so that you can reset passwords and correct security
settings.
The emergency user password must be defined outside the application
user interface. It is defined in the 'access' file located
in the Zope directory. It should be readable only by the user
as which your web server runs.
To create the emergency user, use 'zpasswd.py' to create the
'access' file like this::
python zpasswd.py access
In order to provide a somewhat higher level of security, various
encoding schemes are supported which provide access to either SHA-1
encryption or the standard UNIX crypt facility if it has been compiled
into Python. Unless you have some special requirements (see below),
you should use the SHA-1 facility, which is the default.
Format of 'inituser' and 'access'
---------------------------------
A password file should consist of a single line of the form::
name:password
Note that you may also add an optional third component to the line in the
access file to restrict access by domain. For example, the line::
mario:nintendoRules:*.mydomain.com
in your 'access' file will only allow permit emergency user access
from `*.mydomain.com` machines. Attempts to access the system from
other domains will fail, even if the correct emergency user name
and password are used.
Please note that if you use the ZServer monitor capability, you will
need to run with a clear text password.
How to build and install Zope from source code on Windows.
----------------------------------------------------------
* Ensure you have the correct MSVC version installed for the
version of Python you will be using.
* Install (or build from sources) Python
http://www.python.org
* Install (or build from sources) the Python for Windows extensions
http://sourceforge.net/projects/pywin32/
* Unpack the Zope source distribution. Change to that directory.
* Execute:
% python.exe inst\configure.py
It should say something like:
>
> - Zope top-level binary directory will be c:\Zope-2.13.
> - Makefile written.
>
> Next, run the Visual C++ batch file "VCVARS32.bat" and then "nmake".
(run 'configure.py --help' to see how to change things)
* 'makefile' will have ben created. As instructed, execute 'nmake'.
If the build succeeds, the last message printed should be:
> Zope built. Next, do 'nmake install'.
* As instructed, execute 'nmake install'. A few warnings will be generated,
but they can be ignored. The last message in the build process should be:
> Zope binaries installed successfully.
* Zope itself has now been installed. We need to create an instance. Run:
% python.exe {install_path}\bin\mkzopeinstance.py
We will be prompted, via the console, for the instance directory and
username/password for the admin user.
* We are now ready to start zope. Run:
% {zope_instance}\bin\runzope.bat
Zope should start with nice log messages being printed to
stdout. When Zope is ready, you should see:
> ------
> 2004-10-13T12:27:58 INFO(0) Zope Ready to handle requests
Press Ctrl+C to stop this instance of the server.
* Optionally, install as a Windows service. Execute:
% python {zope_instance}\bin\zopeservice.py
to see the valid options. You may want something like:
% python {zope_instance}\bin\zopeservice.py --startup=auto install
Once installed, it can be started any number of ways:
- % {zope_instance}\bin\zopectl.bat start
- % python {zope_instance}\bin\zopeservice.py start
- Control Panel
- % net start service_short_name (eg, `net start Zope_-1227678699`)
......@@ -8,12 +8,11 @@ Contents:
:maxdepth: 2
WHATSNEW.rst
CHANGES.rst
INSTALL.rst
USERS.rst
SECURITY.rst
ZOPE3.rst
SETUID.rst
SIGNALS.rst
DEBUGGING.rst
CREDITS.rst
CHANGES.rst
@ECHO OFF
REM Command file for Sphinx documentation
set SPHINXBUILD=..\bin\sphinx-build
set ALLSPHINXOPTS=-d .build/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (.build\*) do rmdir /q /s %%i
del /q /s .build\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% .build/html
echo.
echo.Build finished. The HTML pages are in .build/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% .build/dirhtml
echo.
echo.Build finished. The HTML pages are in .build/dirhtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% .build/pickle
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% .build/json
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% .build/htmlhelp
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in .build/htmlhelp.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% .build/latex
echo.
echo.Build finished; the LaTeX files are in .build/latex.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% .build/changes
echo.
echo.The overview file is in .build/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% .build/linkcheck
echo.
echo.Link check complete; look for any errors in the above output ^
or in .build/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% .build/doctest
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in .build/doctest/output.txt.
goto end
)
:end
##############################################################################
#
# 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.
#
##############################################################################
"""
Compatibility stub (Zope 2.7's up to b1 used to keep all nt service-related
files here; they've since moved to ntservice_utils)
"""
from nt_svcutils import service
import win32serviceutil
# this is a class which instance services subclass
ZopeService = service.Service
if __name__=='__main__':
win32serviceutil.HandleCommandLine(ZopeService)
""" Placeholder module file """
......@@ -51,10 +51,56 @@ from ZConfig.datatypes import existing_dirpath
WIN = False
if sys.platform[:3].lower() == "win":
WIN = True
import win32serviceutil
from nt_svcutils import service
def do_windows(command):
def inner(self,arg):
INSTANCE_HOME = self.options.directory
name = 'Zope'+str(hash(INSTANCE_HOME.lower()))
display_name = 'Zope instance at '+INSTANCE_HOME
# This class exists only so we can take advantage of
# win32serviceutil.HandleCommandLine, it is never
# instantiated.
class InstanceService(service.Service):
_svc_name_ = name
_svc_display_name_ = display_name
_svc_description_ = "A Zope application instance running as a service"
# getopt sucks :-(
argv = [sys.argv[0]]
argv.extend(arg.split())
argv.append(command)
# we need to supply this manually as HandleCommandLine guesses wrong
serviceClassName = os.path.splitext(service.__file__)[0]+'.Service'
err = win32serviceutil.HandleCommandLine(
InstanceService,
serviceClassName,
argv=argv,
)
return err,InstanceService
return inner
def string_list(arg):
return arg.split()
def quote_command(command):
print " ".join(command)
# Quote the program name, so it works even if it contains spaces
command = " ".join(['"%s"' % x for x in command])
if WIN:
# odd, but true: the windows cmd processor can't handle more than
# one quoted item per string unless you add quotes around the
# whole line.
command = '"%s"' % command
return command
class ZopeCtlOptions(ZDOptions):
# Zope controller options.
#
......@@ -132,11 +178,6 @@ class ZopeCtlOptions(ZDOptions):
self.python = os.environ.get('PYTHON', config.python) or sys.executable
self.zdrun = os.path.join(os.path.dirname(zdaemon.__file__),
"zdrun.py")
if WIN:
# Add the path to the zopeservice.py script, which is needed for
# some of the Windows specific commands
servicescript = os.path.join(self.directory, 'bin', 'zopeservice.py')
self.servicescript = '"%s" %s' % (self.python, servicescript)
self.exitcodes = [0, 2]
if self.logfile is None and config.eventlog is not None:
......@@ -171,6 +212,13 @@ class ZopeCmd(ZDCmd):
args = [opt, svalue]
return args
def do_start(self, arg):
# signal to Zope that it is being managed
# (to indicate it's web-restartable)
os.putenv('ZMANAGED', '1')
## START OF WINDOWS ONLY STUFF
if WIN:
def get_status(self):
# get_status from zdaemon relies on *nix specific socket handling.
......@@ -182,52 +230,48 @@ class ZopeCmd(ZDCmd):
self.zd_status = None
return
def do_stop(self, arg):
# Stop the Windows service
program = "%s stop" % self.options.servicescript
print program
os.system(program)
def do_restart(self, arg):
# Restart the Windows service
program = "%s restart" % self.options.servicescript
print program
os.system(program)
do_start = do_windows('start')
do_stop = do_windows('stop')
do_restart = do_windows('restart')
# Add extra commands to install and remove the Windows service
def do_install(self, arg):
program = "%s install" % self.options.servicescript
print program
os.system(program)
def do_install(self,arg):
err,InstanceClass = do_windows('install')(self,arg)
if not err:
# If we installed successfully, put info in registry for the
# real Service class to use:
command = '"%s" -C "%s"' % (
# This gives us the instance script for buildout instances
# and the install script for classic instances.
os.path.join(os.path.split(sys.argv[0])[0],'runzope'),
self.options.configfile
)
InstanceClass.setReg('command',command)
# This is unfortunately needed because runzope.exe is a setuptools
# generated .exe that spawns off a sub process, so pid would give us
# the wrong event name.
InstanceClass.setReg('pid_filename',self.options.configroot.pid_filename)
def help_install(self):
print "install -- Installs Zope as a Windows service."
def do_remove(self, arg):
program = "%s remove" % self.options.servicescript
print program
os.system(program)
do_remove = do_windows('remove')
def help_remove(self):
print "remove -- Removes the Zope Windows service."
def do_start(self, arg):
# signal to Zope that it is being managed
# (to indicate it's web-restartable)
os.putenv('ZMANAGED', '1')
if WIN:
# On Windows start the service, this fails with a reasonable
# error message as long as the service is not installed
program = "%s start" % self.options.servicescript
print program
os.system(program)
else:
ZDCmd.do_start(self, arg)
do_windebug = do_windows('debug')
def help_windebug(self):
print "windebug -- Runs the Zope Windows service in the foreground, in debug mode."
## END OF WINDOWS ONLY STUFF
def get_startup_cmd(self, python, more):
cmdline = ( '%s -c "from Zope2 import configure;'
'configure(\'%s\');' %
'configure(%r);' %
(python, self.options.configfile)
)
return cmdline + more + '\"'
......@@ -240,18 +284,19 @@ class ZopeCmd(ZDCmd):
os.system(cmdline)
def do_foreground(self, arg):
if WIN:
# Adding arguments to the program is not supported on Windows
# and the runzope script doesn't put you in debug-mode either
ZDCmd.do_foreground(self, arg)
else:
self.options.program[1:1] = ["-X", "debug-mode=on"]
try:
ZDCmd.do_foreground(self, arg)
finally:
self.options.program.remove("-X")
self.options.program.remove("debug-mode=on")
program = self.options.program
local_additions = []
if not program.count('-X'):
local_additions += ['-X']
if not program.count('debug-mode=on'):
local_additions += ['debug-mode=on']
program[1:1] = local_additions
command = quote_command(program)
try:
return os.system(command)
finally:
for addition in local_additions: program.remove(addition)
def help_debug(self):
print "debug -- run the Zope debugger to inspect your database"
print " manually using a Python interactive shell"
......
##############################################################################
#
# Copyright (c) 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
#
##############################################################################
"""
A Zope Windows NT service frontend.
Usage:
Installation
The Zope service should be installed by the Zope Windows
installer. You can manually install, uninstall the service from
the commandline.
ntservice.py [options] install|update|remove|start [...]
|stop|restart [...]|debug [...]
Options for 'install' and 'update' commands only:
--username domain\username : The Username the service is to run
under
--password password : The password for the username
--startup [manual|auto|disabled] : How the service starts,
default = manual
Commands
install : Installs the service
update : Updates the service. Use this if you change any
configuration settings and need the service to be
re-registered.
remove : Removes the service
start : Starts the service, this can also be done from the
services control panel
stop : Stops the service, this can also be done from the
services control panel
restart : Restarts the service
debug : Runs the service in debug mode
You can view the usage options by running this module without any
arguments.
Starting Zope
Start Zope by clicking the 'start' button in the services control
panel. You can set Zope to automatically start at boot time by
choosing 'Auto' startup by clicking the 'statup' button.
Stopping Zope
Stop Zope by clicking the 'stop' button in the services control
panel. You can also stop Zope through the web by going to the
Zope control panel and by clicking 'Shutdown'.
Event logging
Service related events (such as startup, shutdown, or errors executing
the Zope process) are logged to the NT application event log. Use the
event viewer to see these events.
Zope Events are still written to the Zope event logs.
"""
import sys, os
# these are replacements from mkzopeinstance
INSTANCE_HOME = r'<<INSTANCE_HOME>>'
ZOPE_SCRIPTS = r'<<ZOPE_SCRIPTS>>'
ZOPE2PATH = r'<<ZOPE2PATH>>'
ZOPE_RUN = os.path.join(ZOPE_SCRIPTS, 'runzope')
CONFIG_FILE = os.path.join(INSTANCE_HOME, 'etc', 'zope.conf')
PYTHONSERVICE_EXE = os.path.join(ZOPE_SCRIPTS, 'PythonService.exe')
os.environ["INSTANCE_HOME"] = INSTANCE_HOME
# XXX: we need to find nt_svcutils.service
sys.path[0:0] = [ZOPE2PATH]
from nt_svcutils.service import Service
servicename = 'Zope_%s' % str(hash(INSTANCE_HOME.lower()))
class InstanceService(Service):
_svc_name_ = servicename
_svc_display_name_ = 'Zope instance at %s' % INSTANCE_HOME
# _svc_description_ can also be set (but what to say isn't clear!)
# If the exe we expect is not there, let the service framework search
# for it. This will be true for people running from source builds and
# relying on pre-installed pythonservice.exe.
# Note this is only used at install time, not runtime.
if os.path.isfile(PYTHONSERVICE_EXE):
_exe_name_ = PYTHONSERVICE_EXE
process_runner = ZOPE_RUN
process_args = '-C "%s"' % CONFIG_FILE
if __name__ == '__main__':
import win32serviceutil
win32serviceutil.HandleCommandLine(InstanceService)
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# Copyright (c) 2003-2009 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
......@@ -50,104 +50,37 @@ class Service(win32serviceutil.ServiceFramework):
should be created in the instance home.
"""
# The PythonService model requires that an actual on-disk class declaration
# represent a single service. Thus, the definitions below for the instance
# must be overridden in a subclass in a file within the instance home for
# each instance.
# The values below are just examples.
_svc_name_ = r'Zope-Instance'
_svc_display_name_ = r'Zope instance at C:\Zope-Instance'
process_runner = r'C:\Program Files\Zope-2.7.0-a1\bin\python.exe'
process_args = r'{path_to}\run.py -C {path_to}\zope.conf'
evtlog_name = 'Zope'
def __init__(self, args):
# We get passed in the service name
self._svc_name_ = args[0]
# ...and from that, we can look up the other needed bits
# from the registry:
self._svc_display_name_ = self.getReg('DisplayName')
self._svc_command_ = self.getReg('command',keyname='PythonClass')
win32serviceutil.ServiceFramework.__init__(self, args)
# Just say "Zope", instead of "Zope_-xxxxx"
try:
servicemanager.SetEventSourceName(self.evtlog_name)
except AttributeError:
# old pywin32 - that's ok.
pass
# Don't use the service name as the event source name:
servicemanager.SetEventSourceName(self.evtlog_name)
# Create an event which we will use to wait on.
# The "service stop" request will set this event.
# We create it inheritable so we can pass it to the child process, so
# it too can act on the stop event.
sa = win32security.SECURITY_ATTRIBUTES()
sa.bInheritHandle = True
self.hWaitStop = win32event.CreateEvent(sa, 0, 0, None)
self.redirect_thread = None
def SvcStop(self):
# Before we do anything, tell the SCM we are starting the stop process.
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
self.onStop()
# Set the stop event - the main loop takes care of termination.
win32event.SetEvent(self.hWaitStop)
# SvcStop only gets triggered when the user explictly stops (or restarts)
# the service. To shut the service down cleanly when Windows is shutting
# down, we also need to hook SvcShutdown.
SvcShutdown = SvcStop
def onStop(self):
# A hook for subclasses to override
pass
def createProcess(self, cmd):
self.start_time = time.time()
return self.createProcessCaptureIO(cmd)
def logmsg(self, event):
# log a service event using servicemanager.LogMsg
try:
servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
event,
(self._svc_name_,
" (%s)" % self._svc_display_name_))
except win32api.error, details:
# Failed to write a log entry - most likely problem is
# that the event log is full. We don't want this to kill us
try:
print "FAILED to write INFO event", event, ":", details
except IOError:
pass
def _dolog(self, func, msg):
try:
fullmsg = "%s (%s): %s" % \
(self._svc_name_, self._svc_display_name_, msg)
func(fullmsg)
except win32api.error, details:
# Failed to write a log entry - most likely problem is
# that the event log is full. We don't want this to kill us
try:
print "FAILED to write event log entry:", details
print msg
except IOError:
# And if running as a service, its likely our sys.stdout
# is invalid
pass
def info(self, s):
self._dolog(servicemanager.LogInfoMsg, s)
def warning(self, s):
self._dolog(servicemanager.LogWarningMsg, s)
def error(self, s):
self._dolog(servicemanager.LogErrorMsg, s)
### ServiceFramework methods
def SvcDoRun(self):
# indicate to Zope that the process is daemon managed (restartable)
os.environ['ZMANAGED'] = '1'
# XXX the restart behavior is different here than it is for
# zdaemon.zdrun. we should probably do the same thing in both
# places.
# daemon behavior: we want to to restart the process if it
# dies, but if it dies too many times, we need to give up.
......@@ -165,78 +98,35 @@ class Service(win32serviceutil.ServiceFramework):
# the cumulative backoff seconds counter
self.backoff_cumulative = 0
self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
self.logmsg(servicemanager.PYS_SERVICE_STARTED)
while 1:
# We pass *this* file and the handle as the first 2 params, then
# the 'normal' startup args.
# See the bottom of this script for how that is handled.
cmd = '"%s" %s' % (self.process_runner, self.process_args)
info = self.createProcess(cmd)
# info is (hProcess, hThread, pid, tid)
self.hZope = info[0] # process handle
# XXX why the test before the log message?
if self.backoff_interval > BACKOFF_INITIAL_INTERVAL:
self.info("created process")
if not (self.run() and self.checkRestart()):
self.hZope, hThread, pid, tid = self.createProcess(self._svc_command_)
self.ReportServiceStatus(win32service.SERVICE_RUNNING)
keep_running = self.run()
if not keep_running:
# The daemon process has asked to stop
break
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# Stop the child process by opening the special named event.
# We give it 90 seconds to shutdown normally. If that doesn't
# stop things, we give it 30 seconds to do a "fast" shutdown.
# After that, we just knock it on the head.
winver = sys.getwindowsversion()
for sig, timeout in ((signal.SIGINT, 30), (signal.SIGTERM, 10)):
event_name = "Zope-%d-%d" % (info[2], sig)
# sys.getwindowsversion() -> major, minor, build, platform_id, ver_string
# for platform_id, 2==VER_PLATFORM_WIN32_NT
if winver[0] >= 5 and winver[3] == 2:
event_name = "Global\\" + event_name
try:
he = win32event.OpenEvent(win32event.EVENT_MODIFY_STATE, 0,
event_name)
except win32event.error, details:
if details[0] == winerror.ERROR_FILE_NOT_FOUND:
# process already dead!
break
# no other expected error - report it.
self.warning("Failed to open child shutdown event %s"
% (event_name,))
continue
win32event.SetEvent(he)
# It should be shutting down now - wait for termination, reporting
# progress as we go.
for i in range(timeout):
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
rc = win32event.WaitForSingleObject(self.hZope, 3000)
if rc == win32event.WAIT_OBJECT_0:
break
# Process terminated - no need to try harder.
if rc == win32event.WAIT_OBJECT_0:
# should we attempt a restart?
if not self.checkRestart():
# No, we should not
break
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# If necessary, kill it
if win32process.GetExitCodeProcess(self.hZope)==win32con.STILL_ACTIVE:
win32api.TerminateProcess(self.hZope, 3)
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# Wait for the redirect thread - it should have died as the remote
# process terminated.
# As we are shutting down, we do the join with a little more care,
# reporting progress as we wait (even though we never will <wink>)
if self.redirect_thread is not None:
for i in range(5):
self.redirect_thread.join(1)
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
if not self.redirect_thread.isAlive():
break
else:
self.warning("Redirect thread did not stop!")
self.stop(pid)
self.ReportServiceStatus(win32service.SERVICE_STOPPED)
self.logmsg(servicemanager.PYS_SERVICE_STOPPED)
def SvcStop(self):
# Set the stop event - the main loop takes care of termination.
win32event.SetEvent(self.hWaitStop)
# SvcStop only gets triggered when the user explictly stops (or restarts)
# the service. To shut the service down cleanly when Windows is shutting
# down, we also need to hook SvcShutdown.
SvcShutdown = SvcStop
### Helper methods
def run(self):
"""Monitor the daemon process.
......@@ -250,24 +140,19 @@ class Service(win32serviceutil.ServiceFramework):
0, # bWaitAll
win32event.INFINITE)
if rc == win32event.WAIT_OBJECT_0:
# user sent a stop service request
# a stop service request was recieved
keep_running = False
elif rc == win32event.WAIT_OBJECT_0 + 1:
# user did not send a service stop request, but
# the process died; this may be an error condition
status = win32process.GetExitCodeProcess(self.hZope)
# exit status 0 means the user caused a clean shutdown,
# presumably via the web interface. Any other status
# is an error that gets written to the event log.
if status != 0:
# This should never block - the child process terminating
# has closed the redirection pipe, so our thread dies.
self.redirect_thread.join(5)
if self.redirect_thread.isAlive():
self.warning("Redirect thread did not stop!")
self.warning("process terminated with exit code %d.\n%s" \
% (status, "".join(self.captured_blocks)))
# exit status 0 means a clean shutdown,
# presumably via the web interface.
keep_running = status != 0
if keep_running:
# Any other status is an error so we write it and
# any output to the event log
self.warning("Process terminated with exit code %d.\n%s" \
% (status, self.getCapturedOutput()))
else:
# No other valid return codes.
assert 0, rc
......@@ -276,10 +161,13 @@ class Service(win32serviceutil.ServiceFramework):
def checkRestart(self):
# this was an abormal shutdown.
if self.backoff_cumulative > BACKOFF_MAX:
self.error("restarting too frequently; quit")
self.error("Attempted restarting more than %s times, aborting."
% BACKOFF_MAX)
return False
self.warning("sleep %s to avoid rapid restarts"
% self.backoff_interval)
self.warning(
"Process died unexpectedly, will attempt restart after %s seconds."
% self.backoff_interval
)
if time.time() - self.start_time > BACKOFF_CLEAR_TIME:
self.backoff_interval = BACKOFF_INITIAL_INTERVAL
self.backoff_cumulative = 0
......@@ -292,7 +180,9 @@ class Service(win32serviceutil.ServiceFramework):
self.backoff_interval *= 2
return True
def createProcessCaptureIO(self, cmd):
def createProcess(self, cmd):
self.start_time = time.time()
hInputRead, hInputWriteTemp = self.newPipe()
hOutReadTemp, hOutWrite = self.newPipe()
pid = win32api.GetCurrentProcess()
......@@ -328,18 +218,19 @@ class Service(win32serviceutil.ServiceFramework):
hInputWrite.Close()
# start a thread collecting output
t = threading.Thread(target=self.redirectCaptureThread,
args = (hOutRead,))
t = threading.Thread(
target=self.outputCaptureThread,
args = (hOutRead,)
)
t.start()
self.redirect_thread = t
self.output_thread = t
return info
def redirectCaptureThread(self, handle):
def outputCaptureThread(self, handle):
# Only one of these running at a time, and handling both stdout and
# stderr on a single handle. The read data is never referenced until
# the thread dies - so no need for locks around self.captured_blocks.
self.captured_blocks = []
#self.info("Redirect thread starting")
while 1:
try:
ec, data = win32file.ReadFile(handle, CHILDCAPTURE_BLOCK_SIZE)
......@@ -352,7 +243,12 @@ class Service(win32serviceutil.ServiceFramework):
self.captured_blocks.append(data)
del self.captured_blocks[CHILDCAPTURE_MAX_BLOCKS:]
handle.Close()
#self.info("Redirect capture thread terminating")
def getCapturedOutput(self):
self.output_thread.join(5)
if self.output_thread.isAlive():
self.warning("Output capturing thread failed to terminate!")
return "".join(self.captured_blocks)
def newPipe(self):
sa = win32security.SECURITY_ATTRIBUTES()
......@@ -369,8 +265,134 @@ class Service(win32serviceutil.ServiceFramework):
pipe.Close()
return dup
# Real __main__ bootstrap code is in the instance's service module.
if __name__ == '__main__':
print "This is a framework module - you don't run it directly."
print "See your installation directory for the service script."
sys.exit(1)
def stop(self,pid):
# call the method that any subclasses out there may implement:
self.onStop()
winver = sys.getwindowsversion()
# This is unfortunately needed because runzope.exe is a setuptools
# generated .exe that spawns off a sub process, so pid would give us
# the wrong event name.
child_pid = int(
open(self.getReg('pid_filename',keyname='PythonClass')).read()
)
# Stop the child process by sending signals to the special named event.
for sig, timeout in (
(signal.SIGINT, 30), # We give it 90 seconds to shutdown normally.
(signal.SIGTERM, 10) # If that doesn't stop things, we give it 30
# seconds to do a "fast" shutdown.
):
# See the Signals.WinSignalHandler module for
# the source of this event name
event_name = "Zope-%d-%d" % (child_pid,sig)
# sys.getwindowsversion() -> major, minor, build, platform_id, ver_string
# for platform_id, 2==VER_PLATFORM_WIN32_NT
if winver[0] >= 5 and winver[3] == 2:
event_name = "Global\\" + event_name
try:
he = win32event.OpenEvent(win32event.EVENT_MODIFY_STATE, 0,
event_name)
except win32event.error, details:
# no other expected error - report it.
self.warning("Failed to open child shutdown event %s"
% (event_name,))
continue
win32event.SetEvent(he)
# It should be shutting down now - wait for termination, reporting
# progress as we go.
for i in range(timeout):
# wait for one second
rc = win32event.WaitForSingleObject(self.hZope, 1000)
if rc == win32event.WAIT_OBJECT_0:
break
# Process terminated - no need to try harder.
if rc == win32event.WAIT_OBJECT_0:
break
if win32process.GetExitCodeProcess(self.hZope)==win32con.STILL_ACTIVE:
# None of the signals worked, so kill the process
self.warning(
"Terminating process as it could not be gracefully ended"
)
win32api.TerminateProcess(self.hZope, 3)
output = self.getCapturedOutput()
if output:
self.info("Process terminated with output:\n"+output)
### Overridable subclass methods
def onStop(self):
# A hook for subclasses to override.
# Called just before the service is stopped.
pass
### Registry interaction methods
@classmethod
def openKey(cls,serviceName,keyname=None):
keypath = "System\\CurrentControlSet\\Services\\"+serviceName
if keyname:
keypath += ('\\'+keyname)
return win32api.RegOpenKey(
win32con.HKEY_LOCAL_MACHINE,keypath,0,win32con.KEY_ALL_ACCESS
)
@classmethod
def setReg(cls,name,value,serviceName=None,keyname='PythonClass'):
if not serviceName:
serviceName = cls._svc_name_
key = cls.openKey(serviceName,keyname)
try:
win32api.RegSetValueEx(key, name, 0, win32con.REG_SZ, value)
finally:
win32api.RegCloseKey(key)
def getReg(self,name,keyname=None):
key = self.openKey(self._svc_name_,keyname)
return win32api.RegQueryValueEx(key,name)[0]
### Logging methods
def logmsg(self, event):
# log a service event using servicemanager.LogMsg
try:
servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
event,
(self._svc_name_,
" (%s)" % self._svc_display_name_))
except win32api.error, details:
# Failed to write a log entry - most likely problem is
# that the event log is full. We don't want this to kill us
try:
print "FAILED to write INFO event", event, ":", details
except IOError:
pass
def _dolog(self, func, msg):
try:
fullmsg = "%s (%s): %s" % \
(self._svc_name_, self._svc_display_name_, msg)
func(fullmsg)
except win32api.error, details:
# Failed to write a log entry - most likely problem is
# that the event log is full. We don't want this to kill us
try:
print "FAILED to write event log entry:", details
print msg
except IOError:
# And if running as a service, its likely our sys.stdout
# is invalid
pass
def info(self, s):
self._dolog(servicemanager.LogInfoMsg, s)
def warning(self, s):
self._dolog(servicemanager.LogWarningMsg, s)
def error(self, s):
self._dolog(servicemanager.LogErrorMsg, s)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment