Commit d8eaafb3 authored by Shane Hathaway's avatar Shane Hathaway

The Refresh product.

parent 1e9da134
......@@ -57,6 +57,10 @@ Zope changes
- DTML-In and DTML-Tree now have optional "prefix" attributes
that can be used to make friendlier tag variable names.
- Added product reloading capability, formerly provided by
the "Refresh" product. This enables developers to see the
effect of changes to their products without restarting Zope.
Zope 2.3.0 beta 3
Features Added
......
......@@ -116,6 +116,7 @@ from Factory import Factory
from Permission import PermissionManager
import ZClasses, ZClasses.ZClass
from HelpSys.HelpSys import ProductHelp
import RefreshFuncs
class ProductFolder(Folder):
......@@ -341,6 +342,129 @@ class Product(Folder, PermissionManager):
self._setObject('Help', ProductHelp('Help', self.id))
return self.Help
#
# Product refresh
#
_refresh_dtml = Globals.DTMLFile('dtml/refresh', globals())
def _readRefreshTxt(self, pid=None):
refresh_txt = None
if pid is None:
pid = self.id
for productDir in Products.__path__:
found = 0
for name in ('refresh.txt', 'REFRESH.txt', 'REFRESH.TXT'):
p = os.path.join(productDir, pid, name)
if os.path.exists(p):
found = 1
break
if found:
try:
file = open(p)
text = file.read()
file.close()
refresh_txt = text
break
except:
# Not found here.
pass
return refresh_txt
def manage_refresh(self, REQUEST, manage_tabs_message=None):
'''
Displays the refresh management screen.
'''
error_type = error_value = error_tb = None
exc = RefreshFuncs.getLastRefreshException(self.id)
if exc is not None:
error_type, error_value, error_tb = exc
exc = None
refresh_txt = self._readRefreshTxt()
# Read the persistent refresh information.
auto = RefreshFuncs.isAutoRefreshEnabled(self._p_jar, self.id)
deps = RefreshFuncs.getDependentProducts(self._p_jar, self.id)
# List all product modules.
mods = RefreshFuncs.listRefreshableModules(self.id)
loaded_modules = []
prefix = 'Products.%s' % self.id
prefixdot = prefix + '.'
lpdot = len(prefixdot)
for name, module in mods:
if name == prefix or name[:lpdot] == prefixdot:
name = name[lpdot:]
if not name:
name = '__init__'
loaded_modules.append(name)
all_auto = RefreshFuncs.listAutoRefreshableProducts(self._p_jar)
for pid in all_auto:
# Ignore products that don't have a refresh.txt.
if self._readRefreshTxt(pid) is None:
all_auto.remove(pid)
auto_other = filter(lambda productId, myId=self.id:
productId != myId, all_auto)
# Return rendered DTML.
return self._refresh_dtml(REQUEST,
id=self.id,
refresh_txt=refresh_txt,
error_type=error_type,
error_value=error_value,
error_tb=error_tb,
devel_mode=Globals.DevelopmentMode,
auto_refresh_enabled=auto,
auto_refresh_other=auto_other,
dependent_products=deps,
loaded_modules=loaded_modules,
manage_tabs_message=manage_tabs_message,
management_view='Refresh')
def manage_performRefresh(self, REQUEST=None):
'''
Attempts to perform a refresh operation.
'''
if self._readRefreshTxt() is None:
raise 'Unauthorized', 'refresh.txt not found'
message = None
if RefreshFuncs.performFullRefresh(self._p_jar, self.id):
from ZODB import Connection
Connection.updateCodeTimestamp() # Clears cache in next connection.
message = 'Product refreshed.'
else:
message = 'An exception occurred.'
if REQUEST is not None:
return self.manage_refresh(REQUEST, manage_tabs_message=message)
def manage_enableAutoRefresh(self, enable=0, REQUEST=None):
'''
Changes the auto refresh flag for this product.
'''
if self._readRefreshTxt() is None:
raise 'Unauthorized', 'refresh.txt not created'
RefreshFuncs.enableAutoRefresh(self._p_jar, self.id, enable)
if enable:
message = 'Enabled auto refresh.'
else:
message = 'Disabled auto refresh.'
if REQUEST is not None:
return self.manage_refresh(REQUEST, manage_tabs_message=message)
def manage_selectDependentProducts(self, selections=(), REQUEST=None):
'''
Selects which products to refresh simultaneously.
'''
if self._readRefreshTxt() is None:
raise 'Unauthorized', 'refresh.txt not created'
RefreshFuncs.setDependentProducts(self._p_jar, self.id, selections)
if REQUEST is not None:
return self.manage_refresh(REQUEST)
class CompressedOutputFile:
def __init__(self, rot):
self._c=zlib.compressobj()
......@@ -477,6 +601,17 @@ def initializeProduct(productp, name, home, app):
)
break
# Ensure this product has a refresh tab.
found = 0
for option in product.manage_options:
if option.get('label') == 'Refresh':
found = 1
break
if not found:
product.manage_options = product.manage_options + (
{'label':'Refresh', 'action':'manage_refresh',
'help': ('OFSP','Product_Refresh.stx')},)
if (os.environ.get('ZEO_CLIENT') and
not os.environ.get('FORCE_PRODUCT_LOAD')):
get_transaction().abort()
......
This diff is collapsed.
<dtml-comment>
Arguments for this method:
id, refresh_txt, error_type, error_value, error_tb, devel_mode,
auto_refresh_enabled, auto_refresh_other, dependent_products,
loaded_modules
</dtml-comment>
<dtml-let form_title="'Refresh product: ' + id">
<dtml-if manage_page_header>
<dtml-var manage_page_header>
<dtml-else>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html lang="en">
<head>
<title>&dtml-form_title;</title>
</head>
<body bgcolor="#FFFFFF" link="#000099" vlink="#555555">
<h3>&dtml-form_title;</h3>
</dtml-if>
</dtml-let>
<dtml-var manage_tabs>
<dtml-if expr="refresh_txt == _.None">
<p>The refresh function, designed to ease the development of Zope
products, is not currently enabled for this product.
To make it available, put a file named "refresh.txt" in the &dtml-id;
product directory. Please note that not all products are
compatible with the refresh function.</p>
<dtml-else>
<dtml-if error_type>
<p><b>An exception occurred during the last refresh.</b><br />
Exception type: <b>&dtml-error_type;</b> <br />
Exception value: <b>&dtml-error_value;</b>
</p>
<pre>&dtml-error_tb;</pre>
<hr />
</dtml-if>
<form action="&dtml-absolute_url;" method="POST">
<table border="0">
<tr>
<td valign="top">
<dtml-if expr="_.string.strip(refresh_txt)">
<p>
<b>Important information about refreshing this product:</b><br />
<dtml-var refresh_txt fmt="structured-text">
</p>
</dtml-if>
<div align="center"><input type="submit"
name="manage_performRefresh:method" value="Refresh this product" />
</div>
<p>
<dtml-if auto_refresh_enabled>
<dtml-if devel_mode>
Auto refresh is enabled. Zope will repeatedly scan for
changes to the Python modules that make up this product and
execute a refresh when needed.
<dtml-else>
Although auto refresh is enabled, Zope is not in development
mode so auto refresh is not available. Use the "-D" argument
when starting Zope to enable development mode.
</dtml-if>
<dtml-else>
Auto refresh is disabled. Enable auto refresh
to cause Zope to frequently scan this product for changes.
Note that auto refresh can slow down Zope considerably
if enabled for more than a few products.
</dtml-if>
<br />
<dtml-let checked="auto_refresh_enabled and 'checked' or ' '">
<input type="checkbox" name="enable" value="1" &dtml-checked; />
Auto refresh mode &nbsp;
<input type="submit" name="manage_enableAutoRefresh:method"
value="Change" />
</dtml-let>
</p>
<dtml-if auto_refresh_other>
<p>Select dependent auto-refreshable products to be refreshed
simultaneously.<br />
<dtml-in auto_refresh_other sort>
<dtml-let checked="(_['sequence-item'] in dependent_products) and
'checked' or ' '">
<input type="checkbox" name="selections:list"
value="&dtml-sequence-item;" &dtml-checked; />
<a href="../&dtml-sequence-item;/manage_refresh"
>&dtml-sequence-item;</a><br />
</dtml-let>
</dtml-in>
<input type="submit" name="manage_selectDependentProducts:method"
value="Change" />
</p>
</dtml-if>
</td>
<td valign="top" class="row-hilite">
<p><b>Refreshable product modules:</b></p>
<ul>
<dtml-in loaded_modules sort>
<li>&dtml-sequence-item;</li>
</dtml-in>
</ul>
</td>
</tr>
</table>
</form>
</dtml-if>
<dtml-if manage_page_footer>
<dtml-var manage_page_footer>
<dtml-else>
</body></html>
</dtml-if>
......@@ -85,8 +85,8 @@
__doc__='''Application support
$Id: Application.py,v 1.143 2001/04/07 16:37:07 jim Exp $'''
__version__='$Revision: 1.143 $'[11:-2]
$Id: Application.py,v 1.144 2001/05/17 18:35:09 shane Exp $'''
__version__='$Revision: 1.144 $'[11:-2]
import Globals,Folder,os,sys,App.Product, App.ProductRegistry, misc_
import time, traceback, os, string, Products
......@@ -495,16 +495,8 @@ def initialize(app):
get_transaction().abort()
def import_products(_st=type('')):
def import_products():
# Try to import each product, checking for and catching errors.
path_join=os.path.join
isdir=os.path.isdir
exists=os.path.exists
DictType=type({})
global_dict=globals()
silly=('__doc__',)
modules=sys.modules
have_module=modules.has_key
done={}
for product_dir in Products.__path__:
......@@ -516,12 +508,26 @@ def import_products(_st=type('')):
if done.has_key(product_name): continue
done[product_name]=1
import_product(product_dir, product_name)
def import_product(product_dir, product_name, raise_exc=0, log_exc=1):
path_join=os.path.join
isdir=os.path.isdir
exists=os.path.exists
_st=type('')
global_dict=globals()
silly=('__doc__',)
modules=sys.modules
have_module=modules.has_key
if 1: # Preserve indentation for diff :-)
try:
package_dir=path_join(product_dir, product_name)
if not isdir(package_dir): continue
if not isdir(package_dir): return
if not exists(path_join(package_dir, '__init__.py')):
if not exists(path_join(package_dir, '__init__.pyc')):
continue
return
pname="Products.%s" % product_name
try:
......@@ -532,35 +538,27 @@ def import_products(_st=type('')):
if type(v) is _st and have_module(v): v=modules[v]
modules[k]=v
except:
LOG('Zope',ERROR,'Couldn\'t import %s' % pname,
error=sys.exc_info())
exc = sys.exc_info()
if log_exc:
LOG('Zope', ERROR, 'Could not import %s' % pname,
error=exc)
f=StringIO()
traceback.print_exc(100,f)
f=f.getvalue()
try: modules[pname].__import_error__=f
except: pass
if raise_exc:
raise exc[0], exc[1], exc[2]
finally:
exc = None
def install_products(app):
# Install a list of products into the basic folder class, so
# that all folders know about top-level objects, aka products
path_join=os.path.join
isdir=os.path.isdir
exists=os.path.exists
DictType=type({})
from Folder import Folder
folder_permissions={}
for p in Folder.__ac_permissions__:
permission, names = p[:2]
folder_permissions[permission]=names
folder_permissions = get_folder_permissions()
meta_types=[]
global_dict=globals()
silly=('__doc__',)
done={}
get_transaction().note('Prior to product installs')
......@@ -579,13 +577,38 @@ def install_products(app):
if done.has_key(product_name):
continue
done[product_name]=1
install_product(app, product_dir, product_name, meta_types,
folder_permissions)
Products.meta_types=Products.meta_types+tuple(meta_types)
Globals.default__class_init__(Folder.Folder)
def get_folder_permissions():
folder_permissions={}
for p in Folder.Folder.__ac_permissions__:
permission, names = p[:2]
folder_permissions[permission]=names
return folder_permissions
def install_product(app, product_dir, product_name, meta_types,
folder_permissions, raise_exc=0, log_exc=1):
path_join=os.path.join
isdir=os.path.isdir
exists=os.path.exists
DictType=type({})
global_dict=globals()
silly=('__doc__',)
if 1: # Preserve indentation for diff :-)
package_dir=path_join(product_dir, product_name)
__traceback_info__=product_name
if not isdir(package_dir): continue
if not isdir(package_dir): return
if not exists(path_join(package_dir, '__init__.py')):
if not exists(path_join(package_dir, '__init__.pyc')):
continue
return
try:
product=__import__("Products.%s" % product_name,
global_dict, global_dict, silly)
......@@ -605,7 +628,7 @@ def install_products(app):
# should be associated with that product. Products are
# expected to implement a method named 'initialize' in
# their __init__.py that takes the ProductContext as an
# argument.
# argument.
productObject=App.Product.initializeProduct(
product, product_name, package_dir, app)
context=ProductContext(productObject, app, product)
......@@ -616,7 +639,7 @@ def install_products(app):
# build up enough information to do initialization manually.
initmethod=pgetattr(product, 'initialize', None)
if initmethod is not None:
initmethod(context)
initmethod(context)
# Support old-style product metadata. Older products may
# define attributes to name their permissions, meta_types,
......@@ -661,8 +684,8 @@ def install_products(app):
for permission, names in new_permissions:
folder_permissions[permission]=names
new_permissions.sort()
Folder.__dict__['__ac_permissions__']=tuple(
list(Folder.__ac_permissions__)+new_permissions)
Folder.Folder.__dict__['__ac_permissions__']=tuple(
list(Folder.Folder.__ac_permissions__)+new_permissions)
if (os.environ.get('ZEO_CLIENT') and
not os.environ.get('FORCE_PRODUCT_LOAD')):
......@@ -674,13 +697,58 @@ def install_products(app):
get_transaction().commit()
except:
LOG('Zope',ERROR,'Couldn\'t install %s' % product_name,
error=sys.exc_info())
if log_exc:
LOG('Zope',ERROR,'Couldn\'t install %s' % product_name,
error=sys.exc_info())
get_transaction().abort()
if raise_exc:
raise
def reinstall_product(app, product_name):
folder_permissions = get_folder_permissions()
meta_types=[]
get_transaction().note('Prior to product reinstall')
get_transaction().commit()
for product_dir in Products.__path__:
product_names=os.listdir(product_dir)
product_names.sort()
if product_name in product_names:
removeProductMetaTypes(product_name)
install_product(app, product_dir, product_name, meta_types,
folder_permissions, raise_exc=1, log_exc=0)
Products.meta_types=Products.meta_types+tuple(meta_types)
Globals.default__class_init__(Folder.Folder)
Globals.default__class_init__(Folder)
def reimport_product(product_name):
for product_dir in Products.__path__:
product_names=os.listdir(product_dir)
product_names.sort()
if product_name in product_names:
import_product(product_dir, product_name,
raise_exc=1, log_exc=0)
break
def removeProductMetaTypes(pid):
'''
Unregisters the meta types registered by a product.
'''
meta_types = Products.meta_types
new_mts = []
changed = 0
for meta_type in meta_types:
if meta_type.get('product', None) == pid:
# Remove this meta type.
changed = 1
else:
new_mts.append(meta_type)
if changed:
Products.meta_types = tuple(new_mts)
def pgetattr(product, name, default=install_products, __init__=0):
......
Product - Refresh: Reload a filesystem-based Python product.
Description
This view allows you to reload filesystem-based product code
without restarting Zope. This function is useful during
development of products.
To enable your product to be refreshed, it is required that you
put a file called 'refresh.txt' in your product directory.
It can optionally contain a warning for others who might use
the refresh function.
(Producting refreshing is not perfect. Some products, especially
"hotfix" style products which patch Zope, should not be refreshed.
That's why 'refresh.txt' is required. Most products are safe to
refresh, however.)
There are two modes of operation. You can visit your product's
'Refresh' tab and manually push the refresh button. Or you can
turn on "auto-refresh" mode, which causes Zope to periodically
scan the modification time of the Python files that make up your
product and execute a refresh operation in the background.
**NOTE**: Don't enable auto-refresh for too many products at once.
Scanning file modification times can take a lot of time per
request.
You can also select dependent refreshable products. If you have
a product that subclasses from a product you're working on,
you'll want to enable refresh for both products and add the
product that subclasses as a dependent of the product you're
modifying. This enables subclasses to be updated.
Controls
'Refresh this product' -- The manual refresh button.
'Auto refresh mode' -- Check the checkbox to enable auto-refresh.
'Dependent auto-refreshable products' -- A list of other products
which are auto-refreshable.
How it works
To execute a refresh, Zope looks in the sys.modules dictionary
for modules with names that start with the prefix for your product.
It tries to scan for dependencies between the modules that make
up your product then uses Python's reload() function for each
module in order. Then it sets a flag that will cause ZODB to dump
its cache on the next connection so that changes to persistent
classes will take effect.
To implement auto-refresh, Zope stores a PersistentMapping called
RefreshData on the database root object (below the Application
object). The contents of the PersistentMapping are examined at the
moment a database connection is opened by ZApplication. The
PersistentMapping contains a list of which products have auto-refresh
enabled. For each product with auto-refresh enabled, Zope compares
the file mod times with the last recorded times and executes a
refresh if there are any changes.
......@@ -142,13 +142,18 @@ app=bobo_application=ZODB.ZApplication.ZApplicationWrapper(
# Initialize products:
c=app()
OFS.Application.initialize(c)
if Globals.DevelopmentMode:
# Set up auto-refresh.
from App.RefreshFuncs import setupAutoRefresh
setupAutoRefresh(c._p_jar)
c._p_jar.close()
del c
# "Log off" as system user
AccessControl.SecurityManagement.noSecurityManager()
# This is sneaky, but we don't want to play with Main:
sys.modules['Main']=sys.modules['Zope']
......
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