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
ZConfigconfiguration). This is stored in bothGlobals.DBandZope2.DB, and is configured using adbtab(mount points) read from the configuration file. When this is done, the eventzope.processlifetime.DatabaseOpenedis notified.Setting the
ClassFactoryon the ZODB instance toZope2.App.ClassFactory.ClassFactory. This is a function that will attempt to import a class, and will returnOFS.Uninstalled.Brokenif 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
appobject, an instance ofApp.ZApplication.ZApplicationWrapperthat 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 theZope2module - 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.DatabaseOpenedWithRootSetting 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_iconby virtue of themisc_attribute on the application root.Next, create an
App.ProductContext.ProductContextto be used during product initialisation. This is passed aproductobject, a handle to the application root, and the product’s package.There are two ways to obtain the
productobject:If persistent product installation (in the
Control_Panel) is enabled inzope.conf, callApp.Product.initializeProduct. This will create aApp.Product.Productobject and save it persistently inApp.Control_Panel.Products. It also reads the fileversion.txtfrom the product to determine a version number, and will change the persistent object (at Zope startup) if the version has changed. TheProductobject is initialised with a product name and title and is used to store basic information about the product. TheProductobject is then returned.If persistent product installation is disabled (the default), simply instantiate a
FactoryDispatcher.Productobject (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_classThe class of the object that will be created.
meta_typeA unique string representing kind of object being created, which appears in add lists. If not specified, then the class
meta_typewill be used.permissionThe permission name for the constructors. If not specified, a permission name generated from the meta type (
"Add <meta_type>") will be used.constructorsA 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
DTMLMethodorPageTemplateFilethat renders an add form and one that is a method that actually creates and adds an instance. A typical example fromProducts.MailHostis: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,titleand the other variables are passed as request parameters and marshalled (bymapply()- see below) into function arguments, and theREQUESTis implicitly passed (again bymapply()).iconThe name of an image file in the package to be used for instances. The class
iconattribute will be set automagically if an icon is provided.permissionsAdditional permissions to be registered.
visibilityThe string
"Global"if the object is globally visible, orNoneotherwise.interfacesA list of the interfaces the object supports. These can be used to filter addable meta-types later.
container_filterA function that is called with an
ObjectManagerobject 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’sAddlist, 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
iconandinstance_classare supplied, set aniconattribute oninstance_classto a path likemisc_/<productname>/<iconfilename>.Register any
permissionsby callingAccessControl.Permission.registerPermissions()(described later).If there is no
permissionprovided, generate a permission name as the string “Add <meta_type>”, defaulting to being granted toManageronly. Register this permission as well.Grab the name of the first constructor passed in the
constructorstuple. 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.FactoryDispatcherand set this onto the product package as an attribute named__FactoryDispatcher__.Set an attribute
_min the package root if it does not exist to an instance ofAttrDictwrapped 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
interfaceswere passed in explicitly, obtain the interfaces implemented by theinstance_class, if provided.Record information about the primary constructor in the tuple
Products.meta_typesby appending a dict with keys:nameThe
meta_typepassed in or obtained from theinstance_class.actionA path segment like
manage_addProduct/<productname>/<constructorname>. for the initial (first) constructor. More onmanage_addProductbelow.productThe name of the product, without the
Product.prefix.permissionThe add permission passed in or generated.
visibilityEither
"Global"orNoneas passed in to the method.interfacesThe list of interfaces passed in or obtained from
instance_class.instanceThe
instance_classas passed in to the method.container_filterThe
container_filteras passed in to the method.
Next, put the initial constructor and any further constructors passed in onto the
_mpseudo-dictionary (which really just means setting them as attributes on theFactoryDispatcher-subclass). The appropriate<methodname>__roles__attribute is set to aPermissionRoledescribing the add permission as well.If an
iconfilename was passed in, construct anImageFileto 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 standardFactoryDispatcherclass in the same module.Find a persistent
App.Product.Productif there is one, or create a simpleApp.FactoryDispatcher.Productwrapper 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.