Startup and product initialisation
Description
What happens on Zope startup, and how do Zope 2 products and constructors work?
What happens on Zope startup?
A startup script (e.g. bin/instance fg
) calls Zope 2’s run.py
in an
appropriate interpreter context (i.e. one that has the necessary packages on
sys.path
). This invokes a subclass of ZopeStarter
from
Zope2.Startup
:
import Zope2.Startup
starter = Zope2.Startup.get_starter()
opts = _setconfig()
starter.setConfiguration(opts.configroot)
starter.prepare()
starter.run()
There are various variants that allow different ways to supply configuration.
There are two versions of the starter, one for Unix and one for Windows. It
performs a number of actions during the prepare()
phase:
def prepare(self):
self.setupInitialLogging()
self.setupLocale()
self.setupSecurityOptions()
self.setupPublisher()
# Start ZServer servers before we drop privileges so we can bind to
# "low" ports:
self.setupZServer()
self.setupServers()
# drop privileges after setting up servers
self.dropPrivileges()
self.setupFinalLogging()
self.makeLockFile()
self.makePidFile()
self.setupInterpreter()
self.startZope()
self.serverListen()
from App.config import getConfiguration
config = getConfiguration()
self.registerSignals()
# emit a "ready" message in order to prevent the kinds of emails
# to the Zope maillist in which people claim that Zope has "frozen"
# after it has emitted ZServer messages.
logger.info('Ready to handle requests')
self.sendEvents()
Mostly, this is about using information from the configuration (read using
ZConfig
from a configuration file, or taken from the global defaults) to
set various module level variables and options.
The startZope()
call ends up in Zope2.App.startup.startup()
, which
performs a number of startup tasks:
Importing products (
OFS.Application.import_products()
)Creating a ZODB for the chosen storage (as set in the
ZConfig
configuration). This is stored in bothGlobals.DB
andZope2.DB
, and is configured using adbtab
(mount points) read from the configuration file. When this is done, the eventzope.processlifetime.DatabaseOpened
is notified.Setting the
ClassFactory
on the ZODB instance toZope2.App.ClassFactory.ClassFactory
. This is a function that will attempt to import a class, and will returnOFS.Uninstalled.Broken
if the class cannot be imported for whatever reason. This allows for somewhat graceful recovery if symbols that are persistently referenced in the ZODB disappear.Loading ZCML configuration from
site.zcml
. This in turn loads ZCML for all installed products in theProducts.*
namespace, and ZCML slugs. Theload_zcml()
call also sets up aZope2VocabularyRegistry
.Creating the
app
object, an instance ofApp.ZApplication.ZApplicationWrapper
that wraps aOFS.Application.Application
. The purpose of the wrapper is to:Create an instance of the application object at the root of the ZODB on
__init__()
if not there already. The name by default isApplication
.Implement traversal over this wrapper (
__bobo_traverse__
) to open a ZODB connection before continuing traversal, and closing it at the end of the request.Return the persistent instance of the true application root object when called.
The wrapper is set as
Zope2.bobo_application
, which is used when the publisher publishes theZope2
module - more on publication later.Initialising the application object using
OFS.Application.initialize()
. This defensively creates a number of items:def initialize(self): # make sure to preserve relative ordering of calls below. self.install_cp_and_products() self.install_tempfolder_and_sdc() self.install_session_data_manager() self.install_browser_id_manager() self.install_required_roles() self.install_inituser() self.install_errorlog() self.install_products() self.install_standards() self.install_virtual_hosting()
Notfiying the event
zope.processlifetime.DatabaseOpenedWithRoot
Setting a number of ZPublisher hooks:
Zope2.zpublisher_transactions_manager = TransactionsManager() Zope2.zpublisher_exception_hook = zpublisher_exception_hook Zope2.zpublisher_validated_hook = validated_hook Zope2.__bobo_before__ = noSecurityManager
The run()
method of the ZopeStarter
then runs the main startup loop
(note: this is not applicable for WSGI startup using make_wsgi_app()
in
run.py
, where the WSGI server is responsible for the event loop):
def run(self):
# the mainloop.
try:
from App.config import getConfiguration
config = getConfiguration()
import ZServer
import Lifetime
Lifetime.loop()
sys.exit(ZServer.exit_code)
finally:
self.shutdown()
The Lifetime
module uses asyncore
to poll for connected sockets until
shutdown is initiated, either through a signal or an explicit changing of the
flag Lifetime._shutdown_phase
, which is checked for each iteraton of the
loop.
Sockets are created when new connections are received on a defined server. When
using the built-in ZServer (i.e. not WSGI), the default HTTP server is defined
in ZServer.HTTPServer.zhttp_server
, which derives from
ZServer.medusa.http_server
, which in turn is an asyncore.dispatcher
.
Servers are created in ZopeStarter.setupServers()
, which loops over the
ZConfig
-defined server factories and call their create()
method. The
server factories are defined in ZServer.datatypes
. (The word datatypes
refers to ZConfig
data types.)
Note also that some of the configuration data is mutated in the prepare()
method of the server instance, which is called from
Zope2.startup.handlers.root_handler()
during the configuration phase. These
handlers are registered with a call to Zope2.startup.handlers.handleConfig()
during the _setconfig()
call in run.py
.
How are products installed?
During application initialisation, the method install_products()
will call
the method OFS.Application.install_products()
. This will record products
in the Control_Panel
if this is enabled in zope.conf
, and call the
initialize()
function for any product that has one with a product context
that allows the product to register constructors for the Zope runtime.
install_products()
loops over all product directories (configured via
zope.conf
and kept in Products.__path___
by
Zope2.startup.handlers.root_handler()
) and scans these for product
directories with an __init__.py
. For each, it calls
OFS.Application.install_product
. This will:
Import the product as a Python package
Look for an attribute
misc_
at the product root, which is used to store things like icons. If it is a dict, wrap it in anOFS.misc_.Misc_
object, which is just a simple, security-aware class. Then store a copy of it as an attribute on the objectApplication.misc_
. The attribute name is the product name. This allows traversal to themisc_
resources.As an example of the use of the use of
misc_
, consider this dict set up inProducts/CMFPlone/__init__.py
:misc_ = {'plone_icon': ImageFile( os.path.join('skins', 'plone_images', 'logoIcon.png'), cmfplone_globals)}
This can now be traversed to as
/misc_/CMFPlone/plone_icon
by virtue of themisc_
attribute on the application root.Next, create an
App.ProductContext.ProductContext
to be used during product initialisation. This is passed aproduct
object, a handle to the application root, and the product’s package.There are two ways to obtain the
product
object:If persistent product installation (in the
Control_Panel
) is enabled inzope.conf
, callApp.Product.initializeProduct
. This will create aApp.Product.Product
object and save it persistently inApp.Control_Panel.Products
. It also reads the fileversion.txt
from the product to determine a version number, and will change the persistent object (at Zope startup) if the version has changed. TheProduct
object is initialised with a product name and title and is used to store basic information about the product. TheProduct
object is then returned.If persistent product installation is disabled (the default), simply instantiate a
FactoryDispatcher.Product
object (which is a simpler, duck-typing-equivalent ofApp.Product.Product
) with the product name.If the product has an
initialize()
method at its root, call it with the product context as an argument.
Once old-style products are initialised, any packages outside the Products.*
namespace that want to be initialised are processed. The
<five:registerProduct />
ZCML directive stores a list of packages to be
processed and any referenced initialize()
method in the variable
OFS.metaconfigure._packages_to_initialize
, accessible via the function
get_packages_to_initialize()
in the same module. install_products()
loops over this list, calling install_package()
for each. This works very
much like install_product()
. When it is done, it calls the function
OFS.metaconfigure.package_initialized()
to remove the package from the
list of packages to initialise.
How do Zope 2 product constructors work?
Products can make constructors available to the Zope runtime. This is what
powers the Add
drop-down in the ZMI, for instance. They do so by calling
registerClass()
on the product context passed to the initialize()
function. This takes the following main arguments:
instance_class
The class of the object that will be created.
meta_type
A unique string representing kind of object being created, which appears in add lists. If not specified, then the class
meta_type
will be used.permission
The permission name for the constructors. If not specified, a permission name generated from the meta type (
"Add <meta_type>"
) will be used.constructors
A list of constructor methods. An element in the list can be a callable object with a
__name__
attribute giving the name the method should have in the product, or a tuple consisting of a name and a callable object. The first method will be used as the initial method called when creating an object through the web (in the ZMI).It is quite common to pass in two constructor callables: one that is a
DTMLMethod
orPageTemplateFile
that renders an add form and one that is a method that actually creates and adds an instance. A typical example fromProducts.MailHost
is:manage_addMailHostForm = DTMLFile('dtml/addMailHost_form', globals()) def manage_addMailHost(self, id, title='', smtp_host='localhost', localhost='localhost', smtp_port=25, timeout=1.0, REQUEST=None, ): """ Add a MailHost into the system. """ i = MailHost(id, title, smtp_host, smtp_port) self._setObject(id, i) if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')
These are then referenced in
initialize()
:def initialize(context): context.registerClass( MailHost.MailHost, permission='Add MailHost objects', constructors=(MailHost.manage_addMailHostForm, MailHost.manage_addMailHost), icon='www/MailHost_icon.gif', )
The form will be called with a path like
/<container>/manage_addProduct/MailHost/manage_addMailHostForm
. The<form />
on this page has a relative URLaction="manage_addMailHost"
, which means that when the form is submitted, themanage_addMailHost()
function is called.id
,title
and the other variables are passed as request parameters and marshalled (bymapply()
- see below) into function arguments, and theREQUEST
is implicitly passed (again bymapply()
).icon
The name of an image file in the package to be used for instances. The class
icon
attribute will be set automagically if an icon is provided.permissions
Additional permissions to be registered.
visibility
The string
"Global"
if the object is globally visible, orNone
otherwise.interfaces
A list of the interfaces the object supports. These can be used to filter addable meta-types later.
container_filter
A function that is called with an
ObjectManager
object as the only parameter, which should return a truth value if the object is happy to be created in that container. The filter is called before showingObjectManager
’sAdd
list, and before pasting (after object copy or cut), but not before calling an object’s constructor.
The main aims of this method are to register some new permissions, store
some information about the class in the variable Products.meta_types
, and
create a FactoryDispatcher
that allow traversal to the constructor method.
If an
icon
andinstance_class
are supplied, set anicon
attribute oninstance_class
to a path likemisc_/<productname>/<iconfilename>
.Register any
permissions
by callingAccessControl.Permission.registerPermissions()
(described later).If there is no
permission
provided, generate a permission name as the string “Add <meta_type>”, defaulting to being granted toManager
only. Register this permission as well.Grab the name of the first constructor passed in the
constructors
tuple. This can either be the function’s__name__
, or a name can be provided explicitly by passing as the first list element a tuple of(name, function)
.Try to obtain the value of the symbol
__FactoryDispatcher__
in the package root (__init__.py
) if set. If not, create a class on the fly with this name by deriving fromApp.FactoryDispatcher.FactoryDispatcher
and set this onto the product package as an attribute named__FactoryDispatcher__
.Set an attribute
_m
in the package root if it does not exist to an instance ofAttrDict
wrapped around the factory dispatcher. This is a bizzarre construction best described by its implementation:class AttrDict: def __init__(self, ob): self.ob = ob def __setitem__(self, name, v): setattr(self.ob, name, v)
If no
interfaces
were passed in explicitly, obtain the interfaces implemented by theinstance_class
, if provided.Record information about the primary constructor in the tuple
Products.meta_types
by appending a dict with keys:name
The
meta_type
passed in or obtained from theinstance_class
.action
A path segment like
manage_addProduct/<productname>/<constructorname>
. for the initial (first) constructor. More onmanage_addProduct
below.product
The name of the product, without the
Product.
prefix.permission
The add permission passed in or generated.
visibility
Either
"Global"
orNone
as passed in to the method.interfaces
The list of interfaces passed in or obtained from
instance_class
.instance
The
instance_class
as passed in to the method.container_filter
The
container_filter
as passed in to the method.
Next, put the initial constructor and any further constructors passed in onto the
_m
pseudo-dictionary (which really just means setting them as attributes on theFactoryDispatcher
-subclass). The appropriate<methodname>__roles__
attribute is set to aPermissionRole
describing the add permission as well.If an
icon
filename was passed in, construct anImageFile
to read the icon file from the package and stash it in theOFS.misc_.misc_
class so that it can be traversed to later.
Note that previously, the approach taken was to inject factory methods into
the class OFS.ObjectManager.ObjectManager
, which is the base class for most
folderish types in Zope. This is still supported for backwards compatibility,
by providing a legacy
tuple of function objects, but is deprecated.
Products.meta_types
is used in various places, most notably in
OFS.ObjectManager.ObjectManager
in the methods all_meta_types()
and
filtered_meta_types()
.
The former returns all of Products.meta_types
(plus possibly some legacy
entries in _product_meta_types
on the application root object, used to
support through-the-web defined products via
App.ProductRegistry.ProductRegistry
), applying the container_filter
if
available and optionally filtering by interfaces
.
The latter is used to power the Add
widget in the ZMI by creating a
<select />
box for all meta_types
the user is allowed to add by checking
the add permission of each of the items returned by all_meta_types()
. The
action
stored in the meta_types
list is then used to traverse to and
invoke a constructor.
Note that subclasses of ObjectManager
may sometimes override
all_meta_types()
to set a more restrictive list of addable types. They may
also add to the list of the default implementation by setting a meta_types
class or instance variable containing further entries in the same format as
Products.meta_types
.
Finally, let us consider the manage_addProduct
method seen in the action
used to traverse to a registered constructor callable (e.g. an add form) using
a path such as /<container>/manage_addProduct/<productname>/<constructname>
.
It is set on OFS.ObjectManager.ObjectManager
, and is actually an instance of
App.FactoryDispatcher.ProductDispatcher
. This is an implicit-acquisition
capable object that implements __bobo_traverse__
as follows:
Attempt to obtain a
__FactoryDispatcher__
attribute from the product package (from the name being traversed to), defaulting to the standardFactoryDispatcher
class in the same module.Find a persistent
App.Product.Product
if there is one, or create a simpleApp.FactoryDispatcher.Product
wrapper if persistent product installation has not taken place.Create an instance of the factory dispatcher on the fly, passing in the product descriptor and the parent object (i.e. the container).
Return this, acquisition-wrapped in
self
, to allow traversal to continue.
Traversal then continues over the FactoryDispatcher
. In the version of
this created by registerClass()
, each constructor is set as an attribute
on the product-specific dispatcher, with appropriate roles, so traversal will be
able to obtain the constructor callable.
There is also a fallback __getattr__()
implementation in the base
FactoryDispatcher
class, which will inspect the _m
attribute on the
product package for an appropriate constructor, and is also able to obtain
constructor information from a persistent Product
instance (from
Control_Panel
if there was one. This supports a (legacy) approach where
instead of calling registerClass()
to register constructors, constructors
are set in a dict called``_m`` at the root of the product.