[MERGE] sync with latest trunk

bzr revid: odo@openerp.com-20110927165133-uwl7px6bxl6eu7us
This commit is contained in:
Olivier Dony 2011-09-27 18:51:33 +02:00
commit 209390d627
81 changed files with 2067 additions and 3189 deletions

View File

@ -1,18 +1,12 @@
include rpminstall_sh.txt # TODO do we need this file ?
include README
include LICENSE
include MANIFEST.in
include setup.nsi
include setup.cfg
#include openerp/server.cert
#include openerp/server.pkey
#include openerp/gpl.txt
include man/openerp-server.1
include man/openerp_serverrc.5
recursive-include pixmaps *bmp *ico *png
include setup_rpm.sh
recursive-include win32 *.py *.bat
recursive-include openerp *css *csv *html *png *po *pot
recursive-include openerp *rml *rng *sql *sxw *xml *xsl *yml
recursive-include openerp *css *csv *html *png *po *pot *rml *rng *sql *sxw *xml *xsl *yml
graft install
graft debian
graft doc
global-exclude *pyc *~ # Exclude possible garbage from previous graft.

155
README
View File

@ -1,17 +1,138 @@
About OpenERP
---------------
OpenERP is a free Enterprise Resource Planning and Customer Relationship
Management software. It is mainly developed to meet changing needs.
The main functional features are: CRM & SRM, analytic and financial accounting,
double-entry stock management, sales and purchases management, tasks automation,
help desk, marketing campaign, ... and vertical modules for very specific
businesses.
Technical features include a distributed server, flexible workflows, an object
database, dynamic GUIs, customizable reports, NET-RPC and XML-RPC interfaces, ...
For more information, please visit:
http://www.openerp.com
About OpenERP
-------------
OpenERP is a free Enterprise Resource Planning and Customer Relationship
Management software. It is mainly developed to meet changing needs.
The main functional features are: CRM & SRM, analytic and financial accounting,
double-entry stock management, sales and purchases management, tasks automation,
help desk, marketing campaign, ... and vertical modules for very specific
businesses.
Technical features include a distributed server, flexible workflows, an object
database, dynamic GUIs, customizable reports, NET-RPC and XML-RPC interfaces, ...
For more information, please visit:
http://www.openerp.com
OpenERP Quick Installation Guide
---------------------------------
This file contains a quick guide to configure and install the OpenERP server.
Required dependencies:
---------------------
You need the following software installed:
* Python 2.5 or 2.6
* Postgresql 8.2 or above
* Psycopg2 python module
* Reportlab pdf generation library for python
* lxml python module
* pytz python module
* PyYaml python module (install with: easy_install PyYaml)
Some dependencies are only required for specific purposes:
for rendering workflows graphs, you need:
* graphviz
* pyparsing
For Luxembourg localization, you also need:
* pdftk (http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/)
for generating reports using non .jpg images, you need:
* Python Imaging Library for python
For Debian-based distributions, the required packages can be installed with the
following command:
#> apt-get install -y postgresql graphviz python-psycopg2 python-lxml python-tz python-imaging
For Fedora
if they are not installed, install:
python and postgresql
uses yum or you can recover required packages on fedora web site in "core" or "extra" repository :
postgresql-python
python-lxml
python-imaging
python-psycopg2
python-reportlab
graphviz
You can find pyparsing at http://pyparsing.sourceforge.net/
1. Check that all the required dependencies are installed.
2. Launch the program "python ./bin/openerp-server.py -r db_user -w db_password --db_host 127.0.0.1".
See the man page for more information about options.
3. Connect to the server using the GUI client. And follow the instructions to create a new database.
Installation Steps
------------------
1. Check that all the required dependencies are installed.
2. Create a postgresql database.
The default database name is "terp". If you want to use another name, you
will need to provide it when launching the server (by using the commandline
option --database).
To create a postgresql database named "terp" using the following command:
$ createdb --encoding=UNICODE terp
If it is the first time you use postgresql you might need to create a new user
to the postgres system using the following commands (where myusername is your
unix user name):
$ su -
# su - postgres
$ createuser openerp
Shall the new user be allowed to create databases? (y/n) y
Shall the new user be allowed to create more new users? (y/n) y
CREATE USER
$ logout
# logout
3. Launch service daemon by "service openerp-server start".
The first time it is run, the server will initialise the database with all the default values.
4. Connect to the server using the GUI client.
There are two accounts by default:
* login: admin, password:admin
* login: demo, password:demo
Some instructions to use setup.py for a user-install.
This file should/will be moved on a proper documentation place later.
- Possibly clean any left-over of the previous build.
> rm -rf dist openerp_server.egg-info
- Possibly copy the addons in the server if we want them to be packaged
together:
> rsync -av --delete \
--exclude .bzr/ \
--exclude .bzrignore \
--exclude /__init__.py \
--exclude /base \
--exclude /base_quality_interrogation.py \
<path-to-addons> openerp/addons
- Create the user-local directory where we want the package to be installed:
> mkdir -p /home/openerp/openerp-tmp/lib/python2.6/site-packages/
- Use --prefix to specify where the package is installed and include that
place in PYTHONPATH:
> PYTHONPATH=/home/openerp/openerp-tmp/lib/python2.6/site-packages/ \
python setup.py install --prefix=/home/openerp/openerp-tmp
- Run the main script, again specifying the PYTHONPATH:
> PYTHONPATH=/home/openerp/openerp-tmp/lib/python2.6/site-packages/ \
/home/openerp/openerp-tmp/bin/openerp-server

17
gunicorn.conf.py Normal file
View File

@ -0,0 +1,17 @@
import openerp
# Standard OpenERP XML-RPC port.
bind = '127.0.0.1:8069'
pidfile = '.gunicorn.pid'
# This is the big TODO: safely use more than a single worker.
workers = 1
# Some application-wide initialization is needed.
on_starting = openerp.wsgi.on_starting
when_ready = openerp.wsgi.when_ready
timeout = 240 # openerp request-response cycle can be quite long
# Setting openerp.conf.xxx will be better than setting
# openerp.tools.config['xxx']
conf = openerp.tools.config
conf['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc'
conf['static_http_document_root'] = '/tmp'
#conf['log_level'] = 10 # 10 is DEBUG

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -96,14 +96,17 @@ def preload_registry(dbname):
def run_test_file(dbname, test_file):
""" Preload a registry, possibly run a test file, and start the cron."""
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
try:
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
cr = db.cursor()
logger = logging.getLogger('server')
logger.info('loading test file %s', test_file)
openerp.tools.convert_yaml_import(cr, 'base', file(test_file), {}, 'test', True)
cr.rollback()
cr.close()
except Exception:
logging.exception('Failed to initialize database `%s` and run test file `%s`.', dbname, test_file)
cr = db.cursor()
logger = logging.getLogger('server')
logger.info('loading test file %s', test_file)
openerp.tools.convert_yaml_import(cr, 'base', file(test_file), {}, 'test', True)
cr.rollback()
cr.close()
def export_translation():
config = openerp.tools.config
@ -139,27 +142,6 @@ def import_translation():
cr.commit()
cr.close()
def start_services():
http_server = openerp.service.http_server
netrpc_server = openerp.service.netrpc_server
# Instantiate local services (this is a legacy design).
openerp.osv.osv.start_object_proxy()
# Export (for RPC) services.
openerp.service.web_services.start_web_services()
# Initialize the HTTP stack.
http_server.init_servers()
http_server.init_xmlrpc()
http_server.init_static_http()
netrpc_server.init_servers()
# Start the main cron thread.
openerp.netsvc.start_agent()
# Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC).
openerp.netsvc.Server.startAll()
# Variable keeping track of the number of calls to the signal handler defined
# below. This variable is monitored by ``quit_on_signals()``.
quit_signals_received = 0
@ -211,26 +193,10 @@ def quit_on_signals():
while quit_signals_received == 0:
time.sleep(60)
openerp.netsvc.Agent.quit()
openerp.netsvc.Server.quitAll()
config = openerp.tools.config
if config['pidfile']:
os.unlink(config['pidfile'])
logger = logging.getLogger('server')
logger.info("Initiating shutdown")
logger.info("Hit CTRL-C again or send a second signal to force the shutdown.")
logging.shutdown()
# manually join() all threads before calling sys.exit() to allow a second signal
# to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
# threading.Thread.join() should not mask signals (at least in python 2.5)
for thread in threading.enumerate():
if thread != threading.currentThread() and not thread.isDaemon():
while thread.isAlive():
# need a busyloop here as thread.join() masks signals
# and would present the forced shutdown
thread.join(0.05)
time.sleep(0.05)
openerp.service.stop_services()
sys.exit(0)
if __name__ == "__main__":
@ -262,7 +228,7 @@ if __name__ == "__main__":
if not config["stop_after_init"]:
# Some module register themselves when they are loaded so we need the
# services to be running before loading any registry.
start_services()
openerp.service.start_services()
if config['db_name']:
for dbname in config['db_name'].split(','):
@ -271,6 +237,16 @@ if __name__ == "__main__":
if config["stop_after_init"]:
sys.exit(0)
for m in openerp.conf.server_wide_modules:
try:
__import__(m)
# Call any post_load hook.
info = openerp.modules.module.load_information_from_description_file(m)
if info['post_load']:
getattr(sys.modules[m], info['post_load'])()
except Exception:
logging.exception('Failed to load server-wide module `%s`', m)
setup_pid_file()
logger = logging.getLogger('server')
logger.info('OpenERP server is running, waiting for connections...')

View File

@ -43,6 +43,7 @@ import tiny_socket
import tools
import wizard
import workflow
import wsgi
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -22,10 +22,15 @@
""" Addons module.
This module only serves to contain OpenERP addons. For the code to
manage those addons, see openerp.modules. This module conveniently
reexports some symbols from openerp.modules. Importing them from here
is deprecated.
This module serves to contain all OpenERP addons, across all configured addons
paths. For the code to manage those addons, see openerp.modules.
Addons are made available here (i.e. under openerp.addons) after
openerp.tools.config.parse_config() is called (so that the addons paths
are known).
This module also conveniently reexports some symbols from openerp.modules.
Importing them from here is deprecated.
"""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -245,9 +245,7 @@ class res_company(osv.osv):
return False
def _get_logo(self, cr, uid, ids):
return open(os.path.join(
tools.config['root_path'], '..', 'pixmaps', 'your_logo.png'),
'rb') .read().encode('base64')
return open(os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'res_company_logo.png'), 'rb') .read().encode('base64')
_header = """
<header>

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -700,7 +700,7 @@ class users_view(osv.osv):
self._process_values_groups(cr, uid, values, context)
return super(users_view, self).write(cr, uid, ids, values, context)
def read(self, cr, uid, ids, fields, context=None, load='_classic_read'):
def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
if not fields:
group_fields, fields = [], self.fields_get(cr, uid, context).keys()
else:

View File

@ -28,8 +28,21 @@ parsing, configuration file loading and saving, ...) in this module
and provide real Python variables, e.g. addons_paths is really a list
of paths.
To initialize properly this module, openerp.tools.config.parse_config()
must be used.
"""
import deprecation
# Paths to search for OpenERP addons.
addons_paths = []
# List of server-wide modules to load. Those modules are supposed to provide
# features not necessarily tied to a particular database. This is in contrast
# to modules that are always bound to a specific database when they are
# installed (i.e. the majority of OpenERP addons). This is set with the --load
# command-line option.
server_wide_modules = []
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -58,16 +58,16 @@ class Graph(dict):
"""
def add_node(self, name, deps):
def add_node(self, name, info):
max_depth, father = 0, None
for n in [Node(x, self) for x in deps]:
for n in [Node(x, self, None) for x in info['depends']]:
if n.depth >= max_depth:
father = n
max_depth = n.depth
if father:
return father.add_child(name)
return father.add_child(name, info)
else:
return Node(name, self)
return Node(name, self, info)
def update_from_db(self, cr):
if not len(self):
@ -120,7 +120,7 @@ class Graph(dict):
continue
later.clear()
current.remove(package)
node = self.add_node(package, deps)
node = self.add_node(package, info)
node.data = info
for kind in ('init', 'demo', 'update'):
if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
@ -154,12 +154,13 @@ class Graph(dict):
class Singleton(object):
def __new__(cls, name, graph):
def __new__(cls, name, graph, info):
if name in graph:
inst = graph[name]
else:
inst = object.__new__(cls)
inst.name = name
inst.info = info
graph[name] = inst
return inst
@ -167,19 +168,21 @@ class Singleton(object):
class Node(Singleton):
""" One module in the modules dependency graph.
Node acts as a per-module singleton.
Node acts as a per-module singleton. A node is constructed via
Graph.add_module() or Graph.add_modules(). Some of its fields are from
ir_module_module (setted by Graph.update_from_db()).
"""
def __init__(self, name, graph):
def __init__(self, name, graph, info):
self.graph = graph
if not hasattr(self, 'children'):
self.children = []
if not hasattr(self, 'depth'):
self.depth = 0
def add_child(self, name):
node = Node(name, self.graph)
def add_child(self, name, info):
node = Node(name, self.graph, info)
node.depth = self.depth + 1
if node not in self.children:
self.children.append(node)

View File

@ -158,7 +158,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
logger.info('module %s: loading objects', package.name)
migrations.migrate_module(package, 'pre')
register_module_classes(package.name)
models = pool.instanciate(package.name, cr)
models = pool.load(cr, package)
loaded_modules.append(package.name)
if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
init_module_models(cr, package.name, models)
@ -273,8 +273,6 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
# This is a brand new pool, just created in pooler.get_db_and_pool()
pool = pooler.get_pool(cr.dbname)
processed_modules = [] # for cleanup step after install
loaded_modules = [] # to avoid double loading
report = tools.assertion_report()
if 'base' in tools.config['update'] or 'all' in tools.config['update']:
cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
@ -285,8 +283,10 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
if not graph:
logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
loaded, processed = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
processed_modules.extend(processed)
# processed_modules: for cleanup step after install
# loaded_modules: to avoid double loading
loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
if tools.config['load_language']:
for lang in tools.config['load_language'].split(','):
@ -347,7 +347,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
for (model, name) in cr.fetchall():
model_obj = pool.get(model)
if model_obj.is_transient():
if model_obj and model_obj.is_transient():
logger.notifyChannel('init', netsvc.LOG_WARNING, 'The transient model %s (%s) should not have explicit access rules!' % (model, name))
cr.execute("SELECT model from ir_model")

View File

@ -31,7 +31,6 @@ import openerp.osv as osv
import openerp.tools as tools
import openerp.tools.osutil as osutil
from openerp.tools.safe_eval import safe_eval as eval
import openerp.pooler as pooler
from openerp.tools.translate import _
import openerp.netsvc as netsvc
@ -58,6 +57,11 @@ loaded = []
logger = netsvc.Logger()
def initialize_sys_path():
""" Add all addons paths in sys.path.
This ensures something like ``import crm`` works even if the addons are
not in the PYTHONPATH.
"""
global ad_paths
if ad_paths:
@ -250,6 +254,8 @@ def load_information_from_description_file(module):
info['license'] = info.get('license') or 'AGPL-3'
info.setdefault('installable', True)
info.setdefault('active', False)
# If the following is provided, it is called after the module is --loaded.
info.setdefault('post_load', None)
for kind in ['data', 'demo', 'test',
'init_xml', 'update_xml', 'demo_xml']:
info.setdefault(kind, [])
@ -289,23 +295,22 @@ def init_module_models(cr, module_name, obj_list):
t[1](cr, *t[2])
cr.commit()
# Import hook to write a addon m in both sys.modules['m'] and
# sys.modules['openerp.addons.m']. Otherwise it could be loaded twice
# if imported twice using different names.
#class MyImportHook(object):
# def find_module(self, module_name, package_path):
# print ">>>", module_name, package_path
# def load_module(self, module_name):
# raise ImportError("Restricted")
def load_module(module_name):
""" Load a Python module found on the addons paths."""
fm = imp.find_module(module_name, ad_paths)
try:
imp.load_module(module_name, *fm)
finally:
if fm[0]:
fm[0].close()
#sys.meta_path.append(MyImportHook())
def register_module_classes(m):
""" Register module named m, if not already registered.
This will load the module and register all of its models. (Actually, the
explicit constructor call of each of the models inside the module will
register them.)
This loads the module and register all of its models, thanks to either
the MetaModel metaclass, or the explicit instantiation of the model.
"""
@ -325,7 +330,7 @@ def register_module_classes(m):
try:
zip_mod_path = mod_path + '.zip'
if not os.path.isfile(zip_mod_path):
load_module(m)
__import__(m)
else:
zimp = zipimport.zipimporter(zip_mod_path)
zimp.load_module(m)

View File

@ -77,14 +77,21 @@ class Registry(object):
""" Return a model for a given name or raise KeyError if it doesn't exist."""
return self.models[model_name]
def instanciate(self, module, cr):
""" Instanciate all the classes of a given module for a particular db."""
def load(self, cr, module):
""" Load a given module in the registry.
At the Python level, the modules are already loaded, but not yet on a
per-registry level. This method populates a registry with the given
modules, i.e. it instanciates all the classes of a the given module
and registers them in the registry.
"""
res = []
# Instantiate registered classes (via the MetaModel automatic discovery
# or via explicit constructor call), and add them to the pool.
for cls in openerp.osv.orm.MetaModel.module_to_models.get(module, []):
for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
res.append(cls.create_instance(self, cr))
return res

View File

@ -60,20 +60,21 @@ def close_socket(sock):
#.apidoc title: Common Services: netsvc
#.apidoc module-mods: member-order: bysource
def abort_response(error, description, origin, details):
if not tools.config['debug_mode']:
raise Exception("%s -- %s\n\n%s"%(origin, description, details))
else:
raise
class Service(object):
""" Base class for *Local* services
Functionality here is trusted, no authentication.
"""
_services = {}
def __init__(self, name, audience=''):
def __init__(self, name):
Service._services[name] = self
self.__name = name
self._methods = {}
def joinGroup(self, name):
raise Exception("No group for local services")
#GROUPS.setdefault(name, {})[self.__name] = self
@classmethod
def exists(cls, name):
@ -84,35 +85,13 @@ class Service(object):
if cls.exists(name):
cls._services.pop(name)
def exportMethod(self, method):
if callable(method):
self._methods[method.__name__] = method
def LocalService(name):
# Special case for addons support, will be removed in a few days when addons
# are updated to directly use openerp.osv.osv.service.
if name == 'object_proxy':
return openerp.osv.osv.service
def abortResponse(self, error, description, origin, details):
if not tools.config['debug_mode']:
raise Exception("%s -- %s\n\n%s"%(origin, description, details))
else:
raise
class LocalService(object):
""" Proxy for local services.
Any instance of this class will behave like the single instance
of Service(name)
"""
__logger = logging.getLogger('service')
def __init__(self, name):
self.__name = name
try:
self._service = Service._services[name]
for method_name, method_definition in self._service._methods.items():
setattr(self, method_name, method_definition)
except KeyError, keyError:
self.__logger.error('This service does not exist: %s' % (str(keyError),) )
raise
def __call__(self, method, *params):
return getattr(self, method)(*params)
return Service._services[name]
class ExportService(object):
""" Proxy for exported services.
@ -126,33 +105,22 @@ class ExportService(object):
"""
_services = {}
_groups = {}
_logger = logging.getLogger('web-services')
def __init__(self, name, audience=''):
def __init__(self, name):
ExportService._services[name] = self
self.__name = name
self._logger.debug("Registered an exported service: %s" % name)
def joinGroup(self, name):
ExportService._groups.setdefault(name, {})[self.__name] = self
@classmethod
def getService(cls,name):
return cls._services[name]
# Dispatch a RPC call w.r.t. the method name. The dispatching
# w.r.t. the service (this class) is done by OpenERPDispatcher.
def dispatch(self, method, auth, params):
raise Exception("stub dispatch at %s" % self.__name)
def new_dispatch(self,method,auth,params):
raise Exception("stub dispatch at %s" % self.__name)
def abortResponse(self, error, description, origin, details):
if not tools.config['debug_mode']:
raise Exception("%s -- %s\n\n%s"%(origin, description, details))
else:
raise
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
#The background is set with 40 plus the number of the color, and the foreground with 30
#These are the sequences need to get colored ouput
@ -425,33 +393,36 @@ def log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
logger.log(channel, indent+line)
indent=indent_after
class OpenERPDispatcher:
def log(self, title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
def dispatch_rpc(service_name, method, params, auth):
""" Handle a RPC call.
This is pure Python code, the actual marshalling (from/to XML-RPC or
NET-RPC) is done in a upper layer.
"""
def _log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
log(title, msg, channel=channel, depth=depth, fn=fn)
def dispatch(self, service_name, method, params):
try:
auth = getattr(self, 'auth_provider', None)
logger = logging.getLogger('result')
start_time = end_time = 0
if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
self.log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method))
if logger.isEnabledFor(logging.DEBUG_RPC):
start_time = time.time()
result = ExportService.getService(service_name).dispatch(method, auth, params)
if logger.isEnabledFor(logging.DEBUG_RPC):
end_time = time.time()
if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
self.log('service (%.3fs)' % (end_time - start_time), tuple(replace_request_password(params)), depth=1, fn='%s.%s'%(service_name,method))
self.log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER)
self.log('result', result, channel=logging.DEBUG_RPC_ANSWER)
return result
except Exception, e:
self.log('exception', tools.exception_to_unicode(e))
tb = getattr(e, 'traceback', sys.exc_info())
tb_s = "".join(traceback.format_exception(*tb))
if tools.config['debug_mode'] and isinstance(tb[2], types.TracebackType):
import pdb
pdb.post_mortem(tb[2])
raise OpenERPDispatcherException(e, tb_s)
try:
logger = logging.getLogger('result')
start_time = end_time = 0
if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
_log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method))
if logger.isEnabledFor(logging.DEBUG_RPC):
start_time = time.time()
result = ExportService.getService(service_name).dispatch(method, auth, params)
if logger.isEnabledFor(logging.DEBUG_RPC):
end_time = time.time()
if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
_log('service (%.3fs)' % (end_time - start_time), tuple(replace_request_password(params)), depth=1, fn='%s.%s'%(service_name,method))
_log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER)
_log('result', result, channel=logging.DEBUG_RPC_ANSWER)
return result
except Exception, e:
_log('exception', tools.exception_to_unicode(e))
tb = getattr(e, 'traceback', sys.exc_info())
tb_s = "".join(traceback.format_exception(*tb))
if tools.config['debug_mode'] and isinstance(tb[2], types.TracebackType):
import pdb
pdb.post_mortem(tb[2])
raise OpenERPDispatcherException(e, tb_s)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -1126,17 +1126,6 @@ class BaseModel(object):
"""
Import given data in given module
:param cr: database cursor
:param uid: current user id
:param fields: list of fields
:param data: data to import
:param mode: 'init' or 'update' for record creation
:param current_module: module name
:param noupdate: flag for record creation
:param context: context arguments, like lang, time zone,
:param filename: optional file to store partial import state for recovery
:rtype: tuple
This method is used when importing data via client menu.
Example of fields to import for a sale.order::
@ -1149,6 +1138,22 @@ class BaseModel(object):
order_line/price_unit,
order_line/product_uom_qty,
order_line/product_uom/id (=xml_id)
This method returns a 4-tuple with the following structure:
* The first item is a return code, it returns either ``-1`` in case o
:param cr: database cursor
:param uid: current user id
:param fields: list of fields
:param data: data to import
:param mode: 'init' or 'update' for record creation
:param current_module: module name
:param noupdate: flag for record creation
:param context: context arguments, like lang, time zone,
:param filename: optional file to store partial import state for recovery
:returns: 4-tuple of a return code, an errored resource, an error message and ???
:rtype: (int, dict|0, str|0, ''|0)
"""
if not context:
context = {}

View File

@ -40,13 +40,13 @@ class except_osv(Exception):
self.value = value
self.args = (exc_type, name)
service = None
class object_proxy(netsvc.Service):
class object_proxy():
def __init__(self):
self.logger = logging.getLogger('web-services')
netsvc.Service.__init__(self, 'object_proxy', audience='')
self.exportMethod(self.exec_workflow)
self.exportMethod(self.execute)
global service
service = self
def check(f):
@wraps(f)
@ -120,14 +120,14 @@ class object_proxy(netsvc.Service):
except orm.except_orm, inst:
if inst.name == 'AccessError':
self.logger.debug("AccessError", exc_info=True)
self.abortResponse(1, inst.name, 'warning', inst.value)
netsvc.abort_response(1, inst.name, 'warning', inst.value)
except except_osv, inst:
self.abortResponse(1, inst.name, inst.exc_type, inst.value)
netsvc.abort_response(1, inst.name, inst.exc_type, inst.value)
except IntegrityError, inst:
osv_pool = pooler.get_pool(dbname)
for key in osv_pool._sql_error.keys():
if key in inst[0]:
self.abortResponse(1, _('Constraint Error'), 'warning',
netsvc.abort_response(1, _('Constraint Error'), 'warning',
tr(osv_pool._sql_error[key], 'sql_constraint') or inst[0])
if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set')
@ -148,9 +148,9 @@ class object_proxy(netsvc.Service):
msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
except Exception:
pass
self.abortResponse(1, _('Integrity Error'), 'warning', msg)
netsvc.abort_response(1, _('Integrity Error'), 'warning', msg)
else:
self.abortResponse(1, _('Integrity Error'), 'warning', inst[0])
netsvc.abort_response(1, _('Integrity Error'), 'warning', inst[0])
except Exception:
self.logger.exception("Uncaught exception")
raise

View File

@ -136,16 +136,15 @@ class report_custom(report_int):
ids = self.pool.get(report.model_id.model).search(cr, uid, [])
datas['ids'] = ids
service = netsvc.LocalService("object_proxy")
report_id = datas['report_id']
report = service.execute(cr.dbname, uid, 'ir.report.custom', 'read', [report_id], context=context)[0]
fields = service.execute(cr.dbname, uid, 'ir.report.custom.fields', 'read', report['fields_child0'], context=context)
report = self.pool.get('ir.report.custom').read(cr, uid, [report_id], context=context)[0]
fields = self.pool.get('ir.report.custom.fields').read(cr, uid, report['fields_child0'], context=context)
fields.sort(lambda x,y : x['sequence'] - y['sequence'])
if report['field_parent']:
parent_field = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [report['field_parent'][0]],['model'])
model_name = service.execute(cr.dbname, uid, 'ir.model', 'read', [report['model_id'][0]], ['model'],context=context)[0]['model']
parent_field = self.pool.get('ir.model.fields').read(cr, uid, [report['field_parent'][0]], ['model'])
model_name = self.pool.get('ir.model').read(cr, uid, [report['model_id'][0]], ['model'], context=context)[0]['model']
fct = {}
fct['id'] = lambda x : x
@ -160,9 +159,7 @@ class report_custom(report_int):
field_child = f['field_child'+str(i)]
if field_child:
row.append(
service.execute(cr.dbname, uid,
'ir.model.fields', 'read', [field_child[0]],
['name'], context=context)[0]['name']
self.pool.get('ir.model.fields').read(cr, uid, [field_child[0]], ['name'], context=context)[0]['name']
)
if f['fc'+str(i)+'_operande']:
fct_name = 'id'
@ -346,7 +343,7 @@ class report_custom(report_int):
def _create_lines(self, cr, uid, ids, report, fields, results, context):
service = netsvc.LocalService("object_proxy")
pool = pooler.get_pool(cr.dbname)
pdf_string = cStringIO.StringIO()
can = canvas.init(fname=pdf_string, format='pdf')
@ -376,7 +373,7 @@ class report_custom(report_int):
for f in fields:
field_id = (f['field_child3'] and f['field_child3'][0]) or (f['field_child2'] and f['field_child2'][0]) or (f['field_child1'] and f['field_child1'][0]) or (f['field_child0'] and f['field_child0'][0])
if field_id:
type = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [field_id],['ttype'])
type = pool.get('ir.model.fields').read(cr, uid, [field_id],['ttype'])
if type[0]['ttype'] == 'date':
date_idx = idx
fct[idx] = process_date[report['frequency']]
@ -449,7 +446,7 @@ class report_custom(report_int):
def _create_bars(self, cr, uid, ids, report, fields, results, context):
service = netsvc.LocalService("object_proxy")
pool = pooler.get_pool(cr.dbname)
pdf_string = cStringIO.StringIO()
can = canvas.init(fname=pdf_string, format='pdf')
@ -475,7 +472,7 @@ class report_custom(report_int):
for f in fields:
field_id = (f['field_child3'] and f['field_child3'][0]) or (f['field_child2'] and f['field_child2'][0]) or (f['field_child1'] and f['field_child1'][0]) or (f['field_child0'] and f['field_child0'][0])
if field_id:
type = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [field_id],['ttype'])
type = pool.get('ir.model.fields').read(cr, uid, [field_id],['ttype'])
if type[0]['ttype'] == 'date':
date_idx = idx
fct[idx] = process_date[report['frequency']]

View File

@ -41,9 +41,9 @@ def toxml(value):
return unicode_value.replace('&', '&amp;').replace('<','&lt;').replace('>','&gt;')
class report_int(netsvc.Service):
def __init__(self, name, audience='*'):
def __init__(self, name):
assert not self.exists(name), 'The report "%s" already exists!' % name
super(report_int, self).__init__(name, audience)
super(report_int, self).__init__(name)
if name[0:7]<>'report.':
raise Exception, 'ConceptionError, bad report name, should start with "report."'
self.name = name
@ -51,8 +51,6 @@ class report_int(netsvc.Service):
self.name2 = '.'.join(name.split('.')[1:])
# TODO the reports have methods with a 'title' kwarg that is redundant with this attribute
self.title = None
#self.joinGroup('report')
self.exportMethod(self.create)
def create(self, cr, uid, ids, datas, context=None):
return False

View File

@ -137,9 +137,8 @@ class document(object):
value = self.get_value(browser, attrs['name'])
service = netsvc.LocalService("object_proxy")
ids = service.execute(self.cr.dbname, self.uid, 'ir.attachment', 'search', [('res_model','=',model),('res_id','=',int(value))])
datas = service.execute(self.cr.dbname, self.uid, 'ir.attachment', 'read', ids)
ids = self.pool.get('ir.attachment').search(self.cr, self.uid, [('res_model','=',model),('res_id','=',int(value))])
datas = self.pool.get('ir.attachment').read(self.cr, self.uid, ids)
if len(datas):
# if there are several, pick first

View File

@ -19,9 +19,19 @@
#
##############################################################################
import logging
import threading
import time
import http_server
import netrpc_server
import web_services
import websrv_lib
import openerp.netsvc
import openerp.osv
import openerp.tools
import openerp.wsgi
#.apidoc title: RPC Services
@ -34,5 +44,56 @@ import web_services
low-level behavior of the wire.
"""
def start_services():
""" Start all services.
Services include the different servers and cron threads.
"""
# Instantiate local services (this is a legacy design).
openerp.osv.osv.start_object_proxy()
# Export (for RPC) services.
web_services.start_web_services()
# Initialize the HTTP stack.
#http_server.init_servers()
#http_server.init_xmlrpc()
#http_server.init_static_http()
netrpc_server.init_servers()
# Start the main cron thread.
openerp.netsvc.start_agent()
# Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC).
openerp.netsvc.Server.startAll()
# Start the WSGI server.
openerp.wsgi.start_server()
def stop_services():
""" Stop all services. """
openerp.netsvc.Agent.quit()
openerp.netsvc.Server.quitAll()
openerp.wsgi.stop_server()
config = openerp.tools.config
logger = logging.getLogger('server')
logger.info("Initiating shutdown")
logger.info("Hit CTRL-C again or send a second signal to force the shutdown.")
logging.shutdown()
# Manually join() all threads before calling sys.exit() to allow a second signal
# to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
# threading.Thread.join() should not mask signals (at least in python 2.5).
for thread in threading.enumerate():
if thread != threading.currentThread() and not thread.isDaemon():
while thread.isAlive():
# Need a busyloop here as thread.join() masks signals
# and would prevent the forced shutdown.
thread.join(0.05)
time.sleep(0.05)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -154,7 +154,6 @@ class BaseHttpDaemon(threading.Thread, netsvc.Server):
try:
self.server = ThreadedHTTPServer((interface, port), handler, proto=self._RealProto)
self.server.vdirs = []
self.server.logRequests = True
self.server.timeout = self._busywait_timeout
logging.getLogger("web-services").info(
@ -191,29 +190,8 @@ class BaseHttpDaemon(threading.Thread, netsvc.Server):
res += ", %d threads" % (self.server.numThreads,)
return res
def append_svc(self, service):
if not isinstance(service, HTTPDir):
raise Exception("Wrong class for http service")
pos = len(self.server.vdirs)
lastpos = pos
while pos > 0:
pos -= 1
if self.server.vdirs[pos].matches(service.path):
lastpos = pos
# we won't break here, but search all way to the top, to
# ensure there is no lesser entry that will shadow the one
# we are inserting.
self.server.vdirs.insert(lastpos, service)
def list_services(self):
ret = []
for svc in self.server.vdirs:
ret.append( ( svc.path, str(svc.handler)) )
return ret
# No need for these two classes: init_server() below can initialize correctly
# directly the BaseHttpDaemon class.
class HttpDaemon(BaseHttpDaemon):
_RealProto = 'HTTP'
def __init__(self, interface, port):
@ -244,33 +222,8 @@ def init_servers():
httpsd = HttpSDaemon(tools.config.get('xmlrpcs_interface', ''),
int(tools.config.get('xmlrpcs_port', 8071)))
def reg_http_service(hts, secure_only = False):
""" Register some handler to httpd.
hts must be an HTTPDir
"""
global httpd, httpsd
if httpd and not secure_only:
httpd.append_svc(hts)
if httpsd:
httpsd.append_svc(hts)
if (not httpd) and (not httpsd):
logging.getLogger('httpd').warning("No httpd available to register service %s" % hts.path)
return
def list_http_services(protocol=None):
global httpd, httpsd
if httpd and (protocol == 'http' or protocol == None):
return httpd.list_services()
elif httpsd and (protocol == 'https' or protocol == None):
return httpsd.list_services()
else:
raise Exception("Incorrect protocol or no http services")
import SimpleXMLRPCServer
class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
class XMLRPCRequestHandler(FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
rpc_paths = []
protocol_version = 'HTTP/1.1'
_logger = logging.getLogger('xmlrpc')
@ -278,7 +231,8 @@ class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,
def _dispatch(self, method, params):
try:
service_name = self.path.split("/")[-1]
return self.dispatch(service_name, method, params)
auth = getattr(self, 'auth_provider', None)
return netsvc.dispatch_rpc(service_name, method, params, auth)
except netsvc.OpenERPDispatcherException, e:
raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback)
@ -296,14 +250,14 @@ class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,
def init_xmlrpc():
if tools.config.get('xmlrpc', False):
# Example of http file serving:
# reg_http_service(HTTPDir('/test/',HTTPHandler))
reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler))
# reg_http_service('/test/', HTTPHandler)
reg_http_service('/xmlrpc/', XMLRPCRequestHandler)
logging.getLogger("web-services").info("Registered XML-RPC over HTTP")
if tools.config.get('xmlrpcs', False) \
and not tools.config.get('xmlrpc', False):
# only register at the secure server
reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler), True)
reg_http_service('/xmlrpc/', XMLRPCRequestHandler, secure_only=True)
logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only")
class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler):
@ -345,65 +299,21 @@ def init_static_http():
base_path = tools.config.get('static_http_url_prefix', '/')
reg_http_service(HTTPDir(base_path,StaticHTTPHandler))
reg_http_service(base_path, StaticHTTPHandler)
logging.getLogger("web-services").info("Registered HTTP dir %s for %s" % \
(document_root, base_path))
class OerpAuthProxy(AuthProxy):
""" Require basic authentication..
import security
This is a copy of the BasicAuthProxy, which however checks/caches the db
as well.
"""
def __init__(self,provider):
AuthProxy.__init__(self,provider)
class OpenERPAuthProvider(AuthProvider):
""" Require basic authentication."""
def __init__(self,realm='OpenERP User'):
self.realm = realm
self.auth_creds = {}
self.auth_tries = 0
self.last_auth = None
def checkRequest(self,handler,path, db=False):
auth_str = handler.headers.get('Authorization',False)
try:
if not db:
db = handler.get_db_from_path(path)
except Exception:
if path.startswith('/'):
path = path[1:]
psp= path.split('/')
if len(psp)>1:
db = psp[0]
else:
#FIXME!
self.provider.log("Wrong path: %s, failing auth" %path)
raise AuthRejectedExc("Authorization failed. Wrong sub-path.")
if self.auth_creds.get(db):
return True
if auth_str and auth_str.startswith('Basic '):
auth_str=auth_str[len('Basic '):]
(user,passwd) = base64.decodestring(auth_str).split(':')
self.provider.log("Found user=\"%s\", passwd=\"***\" for db=\"%s\"" %(user,db))
acd = self.provider.authenticate(db,user,passwd,handler.client_address)
if acd != False:
self.auth_creds[db] = acd
self.last_auth = db
return True
if self.auth_tries > 5:
self.provider.log("Failing authorization after 5 requests w/o password")
raise AuthRejectedExc("Authorization failed.")
self.auth_tries += 1
raise AuthRequiredExc(atype='Basic', realm=self.provider.realm)
import security
class OpenERPAuthProvider(AuthProvider):
def __init__(self,realm='OpenERP User'):
self.realm = realm
def setupAuth(self, multi, handler):
if not multi.sec_realms.has_key(self.realm):
multi.sec_realms[self.realm] = OerpAuthProxy(self)
handler.auth_proxy = multi.sec_realms[self.realm]
def authenticate(self, db, user, passwd, client_address):
try:
uid = security.login(db,user,passwd)
@ -417,4 +327,36 @@ class OpenERPAuthProvider(AuthProvider):
def log(self, msg, lvl=logging.INFO):
logging.getLogger("auth").log(lvl,msg)
def checkRequest(self,handler,path, db=False):
auth_str = handler.headers.get('Authorization',False)
try:
if not db:
db = handler.get_db_from_path(path)
except Exception:
if path.startswith('/'):
path = path[1:]
psp= path.split('/')
if len(psp)>1:
db = psp[0]
else:
#FIXME!
self.log("Wrong path: %s, failing auth" %path)
raise AuthRejectedExc("Authorization failed. Wrong sub-path.")
if self.auth_creds.get(db):
return True
if auth_str and auth_str.startswith('Basic '):
auth_str=auth_str[len('Basic '):]
(user,passwd) = base64.decodestring(auth_str).split(':')
self.log("Found user=\"%s\", passwd=\"***\" for db=\"%s\"" %(user,db))
acd = self.authenticate(db,user,passwd,handler.client_address)
if acd != False:
self.auth_creds[db] = acd
self.last_auth = db
return True
if self.auth_tries > 5:
self.log("Failing authorization after 5 requests w/o password")
raise AuthRejectedExc("Authorization failed.")
self.auth_tries += 1
raise AuthRequiredExc(atype='Basic', realm=self.realm)
#eof

View File

@ -36,7 +36,7 @@ import openerp.netsvc as netsvc
import openerp.tiny_socket as tiny_socket
import openerp.tools as tools
class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher):
class TinySocketClientThread(threading.Thread):
def __init__(self, sock, threads):
spn = sock and sock.getpeername()
spn = 'netrpc-client-%s:%s' % spn[0:2]
@ -59,7 +59,8 @@ class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher):
while self.running:
try:
msg = ts.myreceive()
result = self.dispatch(msg[0], msg[1], msg[2:])
auth = getattr(self, 'auth_provider', None)
result = netsvc.dispatch_rpc(msg[0], msg[1], msg[2:], auth)
ts.mysend(result)
except socket.timeout:
#terminate this channel because other endpoint is gone
@ -107,7 +108,7 @@ class TinySocketServerThread(threading.Thread,netsvc.Server):
self.socket.listen(5)
self.threads = []
netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO,
"starting NET-RPC service at %s port %d" % (interface or '0.0.0.0', port,))
"starting NET-RPC service on %s:%s" % (interface or '0.0.0.0', port,))
def run(self):
try:

View File

@ -87,7 +87,6 @@ def _initialize_db(serv, id, db_name, demo, lang, user_password):
class db(netsvc.ExportService):
def __init__(self, name="db"):
netsvc.ExportService.__init__(self, name)
self.joinGroup("web-services")
self.actions = {}
self.id = 0
self.id_protect = threading.Semaphore()
@ -97,7 +96,8 @@ class db(netsvc.ExportService):
def dispatch(self, method, auth, params):
if method in [ 'create', 'get_progress', 'drop', 'dump',
'restore', 'rename',
'change_admin_password', 'migrate_databases' ]:
'change_admin_password', 'migrate_databases',
'create_database' ]:
passwd = params[0]
params = params[1:]
security.check_super(passwd)
@ -110,8 +110,6 @@ class db(netsvc.ExportService):
fn = getattr(self, 'exp_'+method)
return fn(*params)
def new_dispatch(self,method,auth,params):
pass
def _create_empty_database(self, name):
db = sql_db.db_connect('template1')
cr = db.cursor()
@ -138,6 +136,20 @@ class db(netsvc.ExportService):
self.actions[id]['thread'] = create_thread
return id
def exp_create_database(self, db_name, demo, lang, user_password='admin'):
""" Similar to exp_create but blocking."""
self.id_protect.acquire()
self.id += 1
id = self.id
self.id_protect.release()
self.actions[id] = {'clean': False}
logging.getLogger('db.create').info('CREATE DATABASE %s', db_name.lower())
self._create_empty_database(db_name)
_initialize_db(self, id, db_name, demo, lang, user_password)
return True
def exp_get_progress(self, id):
if self.actions[id]['thread'].isAlive():
# return openerp.modules.init_progress[db_name]
@ -342,9 +354,9 @@ class db(netsvc.ExportService):
tools.config['update']['base'] = True
pooler.restart_pool(db, force_demo=False, update_module=True)
except except_orm, inst:
self.abortResponse(1, inst.name, 'warning', inst.value)
netsvc.abort_response(1, inst.name, 'warning', inst.value)
except except_osv, inst:
self.abortResponse(1, inst.name, inst.exc_type, inst.value)
netsvc.abort_response(1, inst.name, inst.exc_type, inst.value)
except Exception:
import traceback
tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
@ -352,24 +364,9 @@ class db(netsvc.ExportService):
raise
return True
class _ObjectService(netsvc.ExportService):
"A common base class for those who have fn(db, uid, password,...) "
def common_dispatch(self, method, auth, params):
(db, uid, passwd ) = params[0:3]
params = params[3:]
security.check(db,uid,passwd)
cr = pooler.get_db(db).cursor()
fn = getattr(self, 'exp_'+method)
res = fn(cr, uid, *params)
cr.commit()
cr.close()
return res
class common(_ObjectService):
class common(netsvc.ExportService):
def __init__(self,name="common"):
_ObjectService.__init__(self,name)
self.joinGroup("web-services")
netsvc.ExportService.__init__(self,name)
def dispatch(self, method, auth, params):
logger = netsvc.Logger()
@ -382,7 +379,7 @@ class common(_ObjectService):
return res or False
elif method == 'logout':
if auth:
auth.logout(params[1])
auth.logout(params[1]) # TODO I didn't see any AuthProxy implementing this method.
logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
return True
elif method in ['about', 'timezone_get', 'get_server_environment',
@ -399,10 +396,6 @@ class common(_ObjectService):
fn = getattr(self, 'exp_'+method)
return fn(*params)
def new_dispatch(self,method,auth,params):
pass
def exp_about(self, extended=False):
"""Return information about the OpenERP Server.
@ -436,7 +429,7 @@ GNU Public Licence.
return rc.get_available_updates(rc.id, openerp.modules.get_modules_with_version())
except tm.RemoteContractException, e:
self.abortResponse(1, 'Migration Error', 'warning', str(e))
netsvc.abort_response(1, 'Migration Error', 'warning', str(e))
def exp_get_migration_scripts(self, contract_id, contract_password):
@ -504,7 +497,7 @@ GNU Public Licence.
return True
except tm.RemoteContractException, e:
self.abortResponse(1, 'Migration Error', 'warning', str(e))
netsvc.abort_response(1, 'Migration Error', 'warning', str(e))
except Exception, e:
import traceback
tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
@ -568,7 +561,6 @@ GNU Public Licence.
class objects_proxy(netsvc.ExportService):
def __init__(self, name="object"):
netsvc.ExportService.__init__(self,name)
self.joinGroup('web-services')
def dispatch(self, method, auth, params):
(db, uid, passwd ) = params[0:3]
@ -578,16 +570,12 @@ class objects_proxy(netsvc.ExportService):
if method not in ['execute','exec_workflow']:
raise NameError("Method not available %s" % method)
security.check(db,uid,passwd)
ls = netsvc.LocalService('object_proxy')
fn = getattr(ls, method)
assert openerp.osv.osv.service, "The object_proxy class must be started with start_object_proxy."
fn = getattr(openerp.osv.osv.service, method)
res = fn(db, uid, *params)
return res
def new_dispatch(self,method,auth,params):
pass
#
# Wizard ID: 1
# - None = end of wizard
@ -602,7 +590,6 @@ class objects_proxy(netsvc.ExportService):
class wizard(netsvc.ExportService):
def __init__(self, name='wizard'):
netsvc.ExportService.__init__(self,name)
self.joinGroup('web-services')
self.id = 0
self.wiz_datas = {}
self.wiz_name = {}
@ -618,9 +605,6 @@ class wizard(netsvc.ExportService):
res = fn(db, uid, *params)
return res
def new_dispatch(self,method,auth,params):
pass
def _execute(self, db, uid, wiz_id, datas, action, context):
self.wiz_datas[wiz_id].update(datas)
wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
@ -664,7 +648,6 @@ class ExceptionWithTraceback(Exception):
class report_spool(netsvc.ExportService):
def __init__(self, name='report'):
netsvc.ExportService.__init__(self, name)
self.joinGroup('web-services')
self._reports = {}
self.id = 0
self.id_protect = threading.Semaphore()
@ -672,16 +655,54 @@ class report_spool(netsvc.ExportService):
def dispatch(self, method, auth, params):
(db, uid, passwd ) = params[0:3]
params = params[3:]
if method not in ['report','report_get']:
if method not in ['report', 'report_get', 'render_report']:
raise KeyError("Method not supported %s" % method)
security.check(db,uid,passwd)
fn = getattr(self, 'exp_' + method)
res = fn(db, uid, *params)
return res
def exp_render_report(self, db, uid, object, ids, datas=None, context=None):
if not datas:
datas={}
if not context:
context={}
def new_dispatch(self,method,auth,params):
pass
self.id_protect.acquire()
self.id += 1
id = self.id
self.id_protect.release()
self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
cr = pooler.get_db(db).cursor()
import traceback
import sys
try:
obj = netsvc.LocalService('report.'+object)
(result, format) = obj.create(cr, uid, ids, datas, context)
if not result:
tb = sys.exc_info()
self._reports[id]['exception'] = ExceptionWithTraceback('RML is not available at specified location or not enough data to print!', tb)
self._reports[id]['result'] = result
self._reports[id]['format'] = format
self._reports[id]['state'] = True
except Exception, exception:
tb = sys.exc_info()
tb_s = "".join(traceback.format_exception(*tb))
logger = netsvc.Logger()
logger.notifyChannel('web-services', netsvc.LOG_ERROR,
'Exception: %s\n%s' % (str(exception), tb_s))
if hasattr(exception, 'name') and hasattr(exception, 'value'):
self._reports[id]['exception'] = ExceptionWithTraceback(tools.ustr(exception.name), tools.ustr(exception.value))
else:
self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
self._reports[id]['state'] = True
cr.commit()
cr.close()
return self._check_report(id)
def exp_report(self, db, uid, object, ids, datas=None, context=None):
if not datas:
@ -732,7 +753,7 @@ class report_spool(netsvc.ExportService):
result = self._reports[report_id]
exc = result['exception']
if exc:
self.abortResponse(exc, exc.message, 'warning', exc.traceback)
netsvc.abort_response(exc, exc.message, 'warning', exc.traceback)
res = {'state': result['state']}
if res['state']:
if tools.config['reportgz']:

View File

@ -52,61 +52,17 @@ class AuthProvider:
def __init__(self,realm):
self.realm = realm
def setupAuth(self, multi,handler):
""" Attach an AuthProxy object to handler
"""
pass
def authenticate(self, user, passwd, client_address):
return False
def log(self, msg):
print msg
class BasicAuthProvider(AuthProvider):
def setupAuth(self, multi, handler):
if not multi.sec_realms.has_key(self.realm):
multi.sec_realms[self.realm] = BasicAuthProxy(self)
class AuthProxy:
""" This class will hold authentication information for a handler,
i.e. a connection
"""
def __init__(self, provider):
self.provider = provider
def checkRequest(self,handler,path = '/'):
""" Check if we are allowed to process that request
"""
pass
class BasicAuthProxy(AuthProxy):
""" Require basic authentication..
"""
def __init__(self,provider):
AuthProxy.__init__(self,provider)
self.auth_creds = None
self.auth_tries = 0
def checkRequest(self,handler,path = '/'):
if self.auth_creds:
return True
auth_str = handler.headers.get('Authorization',False)
if auth_str and auth_str.startswith('Basic '):
auth_str=auth_str[len('Basic '):]
(user,passwd) = base64.decodestring(auth_str).split(':')
self.provider.log("Found user=\"%s\", passwd=\"%s\"" %(user,passwd))
self.auth_creds = self.provider.authenticate(user,passwd,handler.client_address)
if self.auth_creds:
return True
if self.auth_tries > 5:
self.provider.log("Failing authorization after 5 requests w/o password")
raise AuthRejectedExc("Authorization failed.")
self.auth_tries += 1
raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm)
class HTTPHandler(SimpleHTTPRequestHandler):
def __init__(self,request, client_address, server):
SimpleHTTPRequestHandler.__init__(self,request,client_address,server)
@ -125,13 +81,17 @@ class HTTPHandler(SimpleHTTPRequestHandler):
def setup(self):
pass
# A list of HTTPDir.
handlers = []
class HTTPDir:
""" A dispatcher class, like a virtual folder in httpd
"""
def __init__(self,path,handler, auth_provider = None):
def __init__(self, path, handler, auth_provider=None, secure_only=False):
self.path = path
self.handler = handler
self.auth_provider = auth_provider
self.secure_only = secure_only
def matches(self, request):
""" Test if some request matches us. If so, return
@ -140,6 +100,48 @@ class HTTPDir:
return self.path
return False
def instanciate_handler(self, request, client_address, server):
handler = self.handler(noconnection(request), client_address, server)
if self.auth_provider:
handler.auth_provider = self.auth_provider()
return handler
def reg_http_service(path, handler, auth_provider=None, secure_only=False):
""" Register a HTTP handler at a given path.
The auth_provider will be instanciated and set on the handler instances.
"""
global handlers
service = HTTPDir(path, handler, auth_provider, secure_only)
pos = len(handlers)
lastpos = pos
while pos > 0:
pos -= 1
if handlers[pos].matches(service.path):
lastpos = pos
# we won't break here, but search all way to the top, to
# ensure there is no lesser entry that will shadow the one
# we are inserting.
handlers.insert(lastpos, service)
def list_http_services(protocol=None):
global handlers
ret = []
for svc in handlers:
if protocol is None or protocol == 'http' or svc.secure_only:
ret.append((svc.path, str(svc.handler)))
return ret
def find_http_service(path, secure=False):
global handlers
for vdir in handlers:
p = vdir.matches(path)
if p == False or (vdir.secure_only and not secure):
continue
return vdir
return None
class noconnection(object):
""" a class to use instead of the real connection
"""
@ -246,11 +248,10 @@ class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
self.in_handlers = {}
self.sec_realms = {}
SocketServer.StreamRequestHandler.__init__(self,request,client_address,server)
self.log_message("MultiHttpHandler init for %s" %(str(client_address)))
def _handle_one_foreign(self,fore, path, auth_provider):
def _handle_one_foreign(self, fore, path):
""" This method overrides the handle_one_request for *children*
handlers. It is required, since the first line should not be
read again..
@ -266,9 +267,9 @@ class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler):
return
self.request_version = fore.request_version
if auth_provider and auth_provider.realm:
if hasattr(fore, 'auth_provider'):
try:
self.sec_realms[auth_provider.realm].checkRequest(fore,path)
fore.auth_provider.checkRequest(fore,path)
except AuthRequiredExc,ae:
# Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
# authorisation features of HTTP/1.1
@ -408,35 +409,29 @@ class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler):
return
self.do_OPTIONS()
return
for vdir in self.server.vdirs:
p = vdir.matches(self.path)
if p == False:
continue
vdir = find_http_service(self.path, self.server.proto == 'HTTPS')
if vdir:
p = vdir.path
npath = self.path[len(p):]
if not npath.startswith('/'):
npath = '/' + npath
if not self.in_handlers.has_key(p):
self.in_handlers[p] = vdir.handler(noconnection(self.request),self.client_address,self.server)
if vdir.auth_provider:
vdir.auth_provider.setupAuth(self, self.in_handlers[p])
self.in_handlers[p] = vdir.instanciate_handler(noconnection(self.request),self.client_address,self.server)
hnd = self.in_handlers[p]
hnd.rfile = self.rfile
hnd.wfile = self.wfile
self.rlpath = self.raw_requestline
try:
self._handle_one_foreign(hnd,npath, vdir.auth_provider)
self._handle_one_foreign(hnd, npath)
except IOError, e:
if e.errno == errno.EPIPE:
self.log_message("Could not complete request %s," \
"client closed connection", self.rlpath.rstrip())
else:
raise
return
# if no match:
self.send_error(404, "Path not found: %s" % self.path)
return
else: # no match:
self.send_error(404, "Path not found: %s" % self.path)
def _get_ignore_body(self,fore):
if not fore.headers.has_key("content-length"):

View File

@ -24,6 +24,7 @@ import optparse
import os
import sys
import openerp
import openerp.conf
import openerp.loglevels as loglevels
import logging
import openerp.release as release
@ -101,8 +102,10 @@ class configmanager(object):
group.add_option("-P", "--import-partial", dest="import_partial", my_default='',
help="Use this for big data importation, if it crashes you will be able to continue at the current state. Provide a filename to store intermediate importation states.")
group.add_option("--pidfile", dest="pidfile", help="file where the server pid will be stored")
group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules")
parser.add_option_group(group)
# XML-RPC / HTTP
group = optparse.OptionGroup(parser, "XML-RPC Configuration")
group.add_option("--xmlrpc-interface", dest="xmlrpc_interface", my_default='',
help="Specify the TCP IP address for the XML-RPC protocol. The empty string binds to all interfaces.")
@ -112,6 +115,7 @@ class configmanager(object):
help="disable the XML-RPC protocol")
parser.add_option_group(group)
# XML-RPC / HTTPS
title = "XML-RPC Secure Configuration"
if not self.has_ssl:
title += " (disabled as ssl is unavailable)"
@ -139,6 +143,13 @@ class configmanager(object):
help="disable the NETRPC protocol")
parser.add_option_group(group)
# WEB
# TODO move to web addons after MetaOption merge
group = optparse.OptionGroup(parser, "Web interface Configuration")
group.add_option("--db-filter", dest="dbfilter", default='.*',
help="Filter listed database", metavar="REGEXP")
parser.add_option_group(group)
# Static HTTP
group = optparse.OptionGroup(parser, "Static HTTP service")
group.add_option("--static-http-enable", dest="static_http_enable", action="store_true", my_default=False, help="enable static HTTP service for serving plain HTML files")
@ -260,9 +271,26 @@ class configmanager(object):
self.options[option.dest] = option.my_default
self.casts[option.dest] = option
self.parse_config()
self.parse_config(None, False)
def parse_config(self, args=None):
def parse_config(self, args=None, complete=True):
""" Parse the configuration file (if any) and the command-line
arguments.
This method initializes openerp.tools.config and openerp.conf (the
former should be removed in the furture) with library-wide
configuration values.
This method must be called before proper usage of this library can be
made.
Typical usage of this method:
openerp.tools.config.parse_config(sys.argv[1:])
:param complete: this is a hack used in __init__(), leave it to True.
"""
if args is None:
args = []
opt, args = self.parser.parse_args(args)
@ -319,8 +347,8 @@ class configmanager(object):
'netrpc_interface', 'netrpc_port', 'db_maxconn', 'import_partial', 'addons_path',
'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone',
'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs',
'secure_cert_file', 'secure_pkey_file',
'static_http_enable', 'static_http_document_root', 'static_http_url_prefix'
'static_http_enable', 'static_http_document_root', 'static_http_url_prefix',
'secure_cert_file', 'secure_pkey_file', 'dbfilter'
]
for arg in keys:
@ -419,6 +447,17 @@ class configmanager(object):
if opt.save:
self.save()
openerp.conf.addons_paths = self.options['addons_path'].split(',')
openerp.conf.server_wide_modules = \
map(lambda m: m.strip(), opt.server_wide_modules.split(',')) if \
opt.server_wide_modules else []
if complete:
openerp.modules.module.initialize_sys_path()
openerp.modules.loading.open_openerp_namespace()
# openerp.addons.__path__.extend(openerp.conf.addons_paths) # This
# is not compatible with initialize_sys_path(): import crm and
# import openerp.addons.crm load twice the module.
def _generate_pgpassfile(self):
"""
Generate the pgpass file with the parameters from the command line (db_host, db_user,

View File

@ -1,10 +1,12 @@
# -*- encoding: utf-8 -*-
import threading
import types
import time # used to eval time.strftime expressions
from datetime import datetime, timedelta
import logging
import openerp.pooler as pooler
import openerp.sql_db as sql_db
import misc
from config import config
import yaml_tag
@ -331,6 +333,7 @@ class YamlInterpreter(object):
def _create_record(self, model, fields):
record_dict = {}
fields = fields or {}
for field_name, expression in fields.items():
field_value = self._eval_field(model, field_name, expression)
record_dict[field_name] = field_value
@ -800,4 +803,19 @@ def yaml_import(cr, module, yamlfile, idref=None, mode='init', noupdate=False):
# keeps convention of convert.py
convert_yaml_import = yaml_import
def threaded_yaml_import(db_name, module_name, file_name, delay=0):
def f():
time.sleep(delay)
cr = None
fp = None
try:
cr = sql_db.db_connect(db_name).cursor()
fp = misc.file_open(file_name)
convert_yaml_import(cr, module_name, fp, {}, 'update', True)
finally:
if cr: cr.close()
if fp: fp.close()
threading.Thread(target=f).start()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -44,7 +44,6 @@ class interface(netsvc.Service):
def __init__(self, name):
assert not self.exists('wizard.'+name), 'The wizard "%s" already exists!' % (name,)
super(interface, self).__init__('wizard.'+name)
self.exportMethod(self.execute)
self.wiz_name = name
def translate_view(self, cr, node, state, lang):
@ -156,7 +155,7 @@ class interface(netsvc.Service):
if isinstance(e, except_wizard) \
or isinstance(e, except_osv) \
or isinstance(e, except_orm):
self.abortResponse(2, e.name, 'warning', e.value)
netsvc.abort_response(2, e.name, 'warning', e.value)
else:
import traceback
tb_s = reduce(lambda x, y: x+y, traceback.format_exception(

View File

@ -36,15 +36,9 @@ class workflow_service(netsvc.Service):
>>> wf_service = netsvc.LocalService("workflow")
"""
def __init__(self, name='workflow', audience='*'):
netsvc.Service.__init__(self, name, audience)
self.exportMethod(self.trg_write)
self.exportMethod(self.trg_delete)
self.exportMethod(self.trg_create)
self.exportMethod(self.trg_validate)
self.exportMethod(self.trg_redirect)
self.exportMethod(self.trg_trigger)
self.exportMethod(self.clear_cache)
def __init__(self, name='workflow'):
netsvc.Service.__init__(self, name)
self.wkf_on_create_cache={}
def clear_cache(self, cr, uid):

375
openerp/wsgi.py Normal file
View File

@ -0,0 +1,375 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
""" WSGI stuffs (proof of concept for now)
This module offers a WSGI interface to OpenERP.
"""
import httplib
import urllib
import xmlrpclib
import StringIO
import logging
import os
import signal
import sys
import threading
import time
import openerp
import openerp.tools.config as config
import service.websrv_lib as websrv_lib
def xmlrpc_return(start_response, service, method, params):
""" Helper to call a service's method with some params, using a
wsgi-supplied ``start_response`` callback."""
# This mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for exception
# handling.
try:
result = openerp.netsvc.dispatch_rpc(service, method, params, None) # TODO auth
response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None)
except openerp.netsvc.OpenERPDispatcherException, e:
fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e.exception), e.traceback)
response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
except:
exc_type, exc_value, exc_tb = sys.exc_info()
fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value))
response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
return [response]
def wsgi_xmlrpc(environ, start_response):
""" The main OpenERP WSGI handler."""
if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/openerp/xmlrpc'):
length = int(environ['CONTENT_LENGTH'])
data = environ['wsgi.input'].read(length)
params, method = xmlrpclib.loads(data)
path = environ['PATH_INFO'][len('/openerp/xmlrpc'):]
if path.startswith('/'): path = path[1:]
if path.endswith('/'): p = path[:-1]
path = path.split('/')
# All routes are hard-coded. Need a way to register addons-supplied handlers.
# No need for a db segment.
if len(path) == 1:
service = path[0]
if service == 'common':
if method in ('create_database', 'list', 'server_version'):
return xmlrpc_return(start_response, 'db', method, params)
else:
return xmlrpc_return(start_response, 'common', method, params)
# A db segment must be given.
elif len(path) == 2:
service, db_name = path
params = (db_name,) + params
if service == 'model':
return xmlrpc_return(start_response, 'object', method, params)
elif service == 'report':
return xmlrpc_return(start_response, 'report', method, params)
# TODO the body has been read, need to raise an exception (not return None).
def legacy_wsgi_xmlrpc(environ, start_response):
if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'):
length = int(environ['CONTENT_LENGTH'])
data = environ['wsgi.input'].read(length)
path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ...
params, method = xmlrpclib.loads(data)
return xmlrpc_return(start_response, path, method, params)
def wsgi_jsonrpc(environ, start_response):
pass
def wsgi_webdav(environ, start_response):
if environ['REQUEST_METHOD'] == 'OPTIONS' and environ['PATH_INFO'] == '*':
return return_options(environ, start_response)
http_dir = websrv_lib.find_http_service(environ['PATH_INFO'])
if http_dir:
path = environ['PATH_INFO'][len(http_dir.path):]
if path.startswith('/'):
environ['PATH_INFO'] = path
else:
environ['PATH_INFO'] = '/' + path
return http_to_wsgi(http_dir)(environ, start_response)
def return_options(environ, start_response):
# Microsoft specific header, see
# http://www.ibm.com/developerworks/rational/library/2089.html
if 'Microsoft' in environ.get('User-Agent', ''):
option = [('MS-Author-Via', 'DAV')]
else:
option = []
options += [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')]
start_response("200 OK", [('Content-Length', str(0))] + options)
return []
def http_to_wsgi(http_dir):
"""
Turn a BaseHTTPRequestHandler into a WSGI entry point.
Actually the argument is not a bare BaseHTTPRequestHandler but is wrapped
(as a class, so it needs to be instanciated) in a HTTPDir.
This code is adapted from wbsrv_lib.MultiHTTPHandler._handle_one_foreign().
It is a temporary solution: the HTTP sub-handlers (in particular the
document_webdav addon) have to be WSGIfied.
"""
def wsgi_handler(environ, start_response):
# Extract from the WSGI environment the necessary data.
scheme = environ['wsgi.url_scheme']
headers = {}
for key, value in environ.items():
if key.startswith('HTTP_'):
key = key[5:].replace('_', '-').title()
headers[key] = value
if key == 'CONTENT_LENGTH':
key = key.replace('_', '-').title()
headers[key] = value
if environ.get('Content-Type'):
headers['Content-Type'] = environ['Content-Type']
path = urllib.quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
path += '?' + environ['QUERY_STRING']
request_version = 'HTTP/1.1' # TODO
request_line = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version)
class Dummy(object):
pass
# Let's pretend we have a server to hand to the handler.
server = Dummy()
server.server_name = environ['SERVER_NAME']
server.server_port = int(environ['SERVER_PORT'])
# Initialize the underlying handler and associated auth. provider.
con = openerp.service.websrv_lib.noconnection(environ['wsgi.input'])
handler = http_dir.instanciate_handler(con, environ['REMOTE_ADDR'], server)
# Populate the handler as if it is called by a regular HTTP server
# and the request is already parsed.
handler.wfile = StringIO.StringIO()
handler.rfile = environ['wsgi.input']
handler.headers = headers
handler.command = environ['REQUEST_METHOD']
handler.path = path
handler.request_version = request_version
handler.close_connection = 1
handler.raw_requestline = request_line
handler.requestline = request_line
# Handle authentication if there is an auth. provider associated to
# the handler.
if hasattr(handler, 'auth_provider'):
try:
handler.auth_provider.checkRequest(handler, path)
except websrv_lib.AuthRequiredExc, ae:
# Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
# authorisation features of HTTP/1.1
if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')):
start_response("403 Forbidden", [])
return []
start_response("401 Authorization required", [
('WWW-Authenticate', '%s realm="%s"' % (ae.atype,ae.realm)),
# ('Connection', 'keep-alive'),
('Content-Type', 'text/html'),
('Content-Length', 4), # len(self.auth_required_msg)
])
return ['Blah'] # self.auth_required_msg
except websrv_lib.AuthRejectedExc,e:
start_response("403 %s" % (e.args[0],), [])
return []
method_name = 'do_' + handler.command
# Support the OPTIONS method even when not provided directly by the
# handler. TODO I would prefer to remove it and fix the handler if
# needed.
if not hasattr(handler, method_name):
if handler.command == 'OPTIONS':
return return_options(environ, start_response)
start_response("501 Unsupported method (%r)" % handler.command, [])
return []
# Finally, call the handler's method.
try:
method = getattr(handler, method_name)
method()
# The DAV handler buffers its output and provides a _flush()
# method.
getattr(handler, '_flush', lambda: None)()
response = parse_http_response(handler.wfile.getvalue())
response_headers = response.getheaders()
body = response.read()
start_response(str(response.status) + ' ' + response.reason, response_headers)
return [body]
except (websrv_lib.AuthRejectedExc, websrv_lib.AuthRequiredExc):
raise
except Exception, e:
start_response("500 Internal error", [])
return []
return wsgi_handler
def parse_http_response(s):
""" Turn a HTTP response string into a httplib.HTTPResponse object."""
class DummySocket(StringIO.StringIO):
"""
This is used to provide a StringIO to httplib.HTTPResponse
which, instead of taking a file object, expects a socket and
uses its makefile() method.
"""
def makefile(self, *args, **kw):
return self
response = httplib.HTTPResponse(DummySocket(s))
response.begin()
return response
# WSGI handlers registered through the register_wsgi_handler() function below.
module_handlers = []
def register_wsgi_handler(handler):
""" Register a WSGI handler.
Handlers are tried in the order they are added. We might provide a way to
register a handler for specific routes later.
"""
module_handlers.append(handler)
def application(environ, start_response):
""" WSGI entry point."""
# Try all handlers until one returns some result (i.e. not None).
wsgi_handlers = [
wsgi_xmlrpc,
wsgi_jsonrpc,
legacy_wsgi_xmlrpc,
wsgi_webdav
] + module_handlers
for handler in wsgi_handlers:
result = handler(environ, start_response)
if result is None:
continue
return result
# We never returned from the loop.
response = 'No handler found.\n'
start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
return [response]
# The WSGI server, started by start_server(), stopped by stop_server().
httpd = None
def serve():
""" Serve HTTP requests via werkzeug development server.
If werkzeug can not be imported, we fall back to wsgiref's simple_server.
Calling this function is blocking, you might want to call it in its own
thread.
"""
global httpd
# TODO Change the xmlrpc_* options to http_*
interface = config['xmlrpc_interface'] or '0.0.0.0'
port = config['xmlrpc_port']
try:
import werkzeug.serving
httpd = werkzeug.serving.make_server(interface, port, application, threaded=True)
logging.getLogger('wsgi').info('HTTP service (werkzeug) running on %s:%s', interface, port)
except ImportError, e:
import wsgiref.simple_server
logging.getLogger('wsgi').warn('Werkzeug module unavailable, falling back to wsgiref.')
httpd = wsgiref.simple_server.make_server(interface, port, application)
logging.getLogger('wsgi').info('HTTP service (wsgiref) running on %s:%s', interface, port)
httpd.serve_forever()
def start_server():
""" Call serve() in its own thread.
The WSGI server can be shutdown with stop_server() below.
"""
threading.Thread(target=openerp.wsgi.serve).start()
def stop_server():
""" Initiate the shutdown of the WSGI server.
The server is supposed to have been started by start_server() above.
"""
if httpd:
httpd.shutdown()
# Master process id, can be used for signaling.
arbiter_pid = None
# Application setup before we can spawn any worker process.
# This is suitable for e.g. gunicorn's on_starting hook.
def on_starting(server):
global arbiter_pid
arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable
config = openerp.tools.config
#openerp.tools.cache = kill_workers_cache
openerp.netsvc.init_logger()
openerp.osv.osv.start_object_proxy()
openerp.service.web_services.start_web_services()
# Install our own signal handler on the master process.
def when_ready(server):
# Hijack gunicorn's SIGWINCH handling; we can choose another one.
signal.signal(signal.SIGWINCH, make_winch_handler(server))
# Our signal handler will signal a SGIQUIT to all workers.
def make_winch_handler(server):
def handle_winch(sig, fram):
server.kill_workers(signal.SIGQUIT) # This is gunicorn specific.
return handle_winch
# Kill gracefuly the workers (e.g. because we want to clear their cache).
# This is done by signaling a SIGWINCH to the master process, so it can be
# called by the workers themselves.
def kill_workers():
try:
os.kill(arbiter_pid, signal.SIGWINCH)
except OSError, e:
if e.errno == errno.ESRCH: # no such pid
return
raise
class kill_workers_cache(openerp.tools.ormcache):
def clear(self, dbname, *args, **kwargs):
kill_workers()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

View File

@ -1,587 +0,0 @@
"""HTTP server base class.
Note: the class in this module doesn't implement any HTTP request; see
SimpleHTTPServer for simple implementations of GET, HEAD and POST
(including CGI scripts). It does, however, optionally implement HTTP/1.1
persistent connections, as of version 0.3.
Contents:
- BaseHTTPRequestHandler: HTTP request handler base class
- test: test function
XXX To do:
- log requests even later (to capture byte count)
- log user-agent header and other interesting goodies
- send error log to separate file
"""
# See also:
#
# HTTP Working Group T. Berners-Lee
# INTERNET-DRAFT R. T. Fielding
# <draft-ietf-http-v10-spec-00.txt> H. Frystyk Nielsen
# Expires September 8, 1995 March 8, 1995
#
# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt
#
# and
#
# Network Working Group R. Fielding
# Request for Comments: 2616 et al
# Obsoletes: 2068 June 1999
# Category: Standards Track
#
# URL: http://www.faqs.org/rfcs/rfc2616.html
# Log files
# ---------
#
# Here's a quote from the NCSA httpd docs about log file format.
#
# | The logfile format is as follows. Each line consists of:
# |
# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb
# |
# | host: Either the DNS name or the IP number of the remote client
# | rfc931: Any information returned by identd for this person,
# | - otherwise.
# | authuser: If user sent a userid for authentication, the user name,
# | - otherwise.
# | DD: Day
# | Mon: Month (calendar name)
# | YYYY: Year
# | hh: hour (24-hour format, the machine's timezone)
# | mm: minutes
# | ss: seconds
# | request: The first line of the HTTP request as sent by the client.
# | ddd: the status code returned by the server, - if not available.
# | bbbb: the total number of bytes sent,
# | *not including the HTTP/1.0 header*, - if not available
# |
# | You can determine the name of the file accessed through request.
#
# (Actually, the latter is only true if you know the server configuration
# at the time the request was made!)
__version__ = "0.3"
__all__ = ["HTTPServer", "BaseHTTPRequestHandler"]
import sys
import time
import socket # For gethostbyaddr()
import mimetools
import SocketServer
# Default error message template
DEFAULT_ERROR_MESSAGE = """\
<head>
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code %(code)d.
<p>Message: %(message)s.
<p>Error code explanation: %(code)s = %(explain)s.
</body>
"""
DEFAULT_ERROR_CONTENT_TYPE = "text/html"
def _quote_html(html):
return html.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
class HTTPServer(SocketServer.TCPServer):
allow_reuse_address = 1 # Seems to make sense in testing environment
def server_bind(self):
"""Override server_bind to store the server name."""
SocketServer.TCPServer.server_bind(self)
host, port = self.socket.getsockname()[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
class BaseHTTPRequestHandler(SocketServer.StreamRequestHandler):
"""HTTP request handler base class.
The following explanation of HTTP serves to guide you through the
code as well as to expose any misunderstandings I may have about
HTTP (so you don't need to read the code to figure out I'm wrong
:-).
HTTP (HyperText Transfer Protocol) is an extensible protocol on
top of a reliable stream transport (e.g. TCP/IP). The protocol
recognizes three parts to a request:
1. One line identifying the request type and path
2. An optional set of RFC-822-style headers
3. An optional data part
The headers and data are separated by a blank line.
The first line of the request has the form
<command> <path> <version>
where <command> is a (case-sensitive) keyword such as GET or POST,
<path> is a string containing path information for the request,
and <version> should be the string "HTTP/1.0" or "HTTP/1.1".
<path> is encoded using the URL encoding scheme (using %xx to signify
the ASCII character with hex code xx).
The specification specifies that lines are separated by CRLF but
for compatibility with the widest range of clients recommends
servers also handle LF. Similarly, whitespace in the request line
is treated sensibly (allowing multiple spaces between components
and allowing trailing whitespace).
Similarly, for output, lines ought to be separated by CRLF pairs
but most clients grok LF characters just fine.
If the first line of the request has the form
<command> <path>
(i.e. <version> is left out) then this is assumed to be an HTTP
0.9 request; this form has no optional headers and data part and
the reply consists of just the data.
The reply form of the HTTP 1.x protocol again has three parts:
1. One line giving the response code
2. An optional set of RFC-822-style headers
3. The data
Again, the headers and data are separated by a blank line.
The response code line has the form
<version> <responsecode> <responsestring>
where <version> is the protocol version ("HTTP/1.0" or "HTTP/1.1"),
<responsecode> is a 3-digit response code indicating success or
failure of the request, and <responsestring> is an optional
human-readable string explaining what the response code means.
This server parses the request and the headers, and then calls a
function specific to the request type (<command>). Specifically,
a request SPAM will be handled by a method do_SPAM(). If no
such method exists the server sends an error response to the
client. If it exists, it is called with no arguments:
do_SPAM()
Note that the request name is case sensitive (i.e. SPAM and spam
are different requests).
The various request details are stored in instance variables:
- client_address is the client IP address in the form (host,
port);
- command, path and version are the broken-down request line;
- headers is an instance of mimetools.Message (or a derived
class) containing the header information;
- rfile is a file object open for reading positioned at the
start of the optional input data part;
- wfile is a file object open for writing.
IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING!
The first thing to be written must be the response line. Then
follow 0 or more header lines, then a blank line, and then the
actual data (if any). The meaning of the header lines depends on
the command executed by the server; in most cases, when data is
returned, there should be at least one header line of the form
Content-type: <type>/<subtype>
where <type> and <subtype> should be registered MIME types,
e.g. "text/html" or "text/plain".
"""
# The Python system version, truncated to its first component.
sys_version = "Python/" + sys.version.split()[0]
# The server software version. You may want to override this.
# The format is multiple whitespace-separated strings,
# where each string is of the form name[/version].
server_version = "BaseHTTP/" + __version__
# The default request version. This only affects responses up until
# the point where the request line is parsed, so it mainly decides what
# the client gets back when sending a malformed request line.
# Most web servers default to HTTP 0.9, i.e. don't send a status line.
default_request_version = "HTTP/0.9"
def parse_request(self):
"""Parse a request (internal).
The request should be stored in self.raw_requestline; the results
are in self.command, self.path, self.request_version and
self.headers.
Return True for success, False for failure; on failure, an
error is sent back.
"""
self.command = None # set in case of error on the first line
self.request_version = version = self.default_request_version
self.close_connection = 1
requestline = self.raw_requestline
if requestline[-2:] == '\r\n':
requestline = requestline[:-2]
elif requestline[-1:] == '\n':
requestline = requestline[:-1]
self.requestline = requestline
words = requestline.split()
if len(words) == 3:
[command, path, version] = words
if version[:5] != 'HTTP/':
self.send_error(400, "Bad request version (%r)" % version)
return False
try:
base_version_number = version.split('/', 1)[1]
version_number = base_version_number.split(".")
# RFC 2145 section 3.1 says there can be only one "." and
# - major and minor numbers MUST be treated as
# separate integers;
# - HTTP/2.4 is a lower version than HTTP/2.13, which in
# turn is lower than HTTP/12.3;
# - Leading zeros MUST be ignored by recipients.
if len(version_number) != 2:
raise ValueError
version_number = int(version_number[0]), int(version_number[1])
except (ValueError, IndexError):
self.send_error(400, "Bad request version (%r)" % version)
return False
if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1":
self.close_connection = 0
if version_number >= (2, 0):
self.send_error(505,
"Invalid HTTP Version (%s)" % base_version_number)
return False
elif len(words) == 2:
[command, path] = words
self.close_connection = 1
if command != 'GET':
self.send_error(400,
"Bad HTTP/0.9 request type (%r)" % command)
return False
elif not words:
return False
else:
self.send_error(400, "Bad request syntax (%r)" % requestline)
return False
self.command, self.path, self.request_version = command, path, version
# Examine the headers and look for a Connection directive
self.headers = self.MessageClass(self.rfile, 0)
conntype = self.headers.get('Connection', "")
if conntype.lower() == 'close':
self.close_connection = 1
elif (conntype.lower() == 'keep-alive' and
self.protocol_version >= "HTTP/1.1"):
self.close_connection = 0
return True
def handle_one_request(self):
"""Handle a single HTTP request.
You normally don't need to override this method; see the class
__doc__ string for information on how to handle specific HTTP
commands such as GET and POST.
"""
self.raw_requestline = self.rfile.readline()
if not self.raw_requestline:
self.close_connection = 1
return
if not self.parse_request(): # An error code has been sent, just exit
return
mname = 'do_' + self.command
if not hasattr(self, mname):
self.send_error(501, "Unsupported method (%r)" % self.command)
return
method = getattr(self, mname)
method()
def handle(self):
"""Handle multiple requests if necessary."""
self.close_connection = 1
self.handle_one_request()
while not self.close_connection:
self.handle_one_request()
def send_error(self, code, message=None):
"""Send and log an error reply.
Arguments are the error code, and a detailed message.
The detailed message defaults to the short entry matching the
response code.
This sends an error response (so it must be called before any
output has been generated), logs the error, and finally sends
a piece of HTML explaining the error to the user.
"""
try:
short, long = self.responses[code]
except KeyError:
short, long = '???', '???'
if message is None:
message = short
explain = long
self.log_error("code %d, message %s", code, message)
# using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
content = (self.error_message_format %
{'code': code, 'message': _quote_html(message), 'explain': explain})
self.send_response(code, message)
self.send_header("Content-Type", self.error_content_type)
self.send_header('Connection', 'close')
self.end_headers()
if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
self.wfile.write(content)
error_message_format = DEFAULT_ERROR_MESSAGE
error_content_type = DEFAULT_ERROR_CONTENT_TYPE
def send_response(self, code, message=None):
"""Send the response header and log the response code.
Also send two standard headers with the server software
version and the current date.
"""
self.log_request(code)
if message is None:
if code in self.responses:
message = self.responses[code][0]
else:
message = ''
if self.request_version != 'HTTP/0.9':
self.wfile.write("%s %d %s\r\n" %
(self.protocol_version, code, message))
# print (self.protocol_version, code, message)
self.send_header('Server', self.version_string())
self.send_header('Date', self.date_time_string())
def send_header(self, keyword, value):
"""Send a MIME header."""
if self.request_version != 'HTTP/0.9':
self.wfile.write("%s: %s\r\n" % (keyword, value))
if keyword.lower() == 'connection':
if value.lower() == 'close':
self.close_connection = 1
elif value.lower() == 'keep-alive':
self.close_connection = 0
def end_headers(self):
"""Send the blank line ending the MIME headers."""
if self.request_version != 'HTTP/0.9':
self.wfile.write("\r\n")
def log_request(self, code='-', size='-'):
"""Log an accepted request.
This is called by send_response().
"""
self.log_message('"%s" %s %s',
self.requestline, str(code), str(size))
def log_error(self, format, *args):
"""Log an error.
This is called when a request cannot be fulfilled. By
default it passes the message on to log_message().
Arguments are the same as for log_message().
XXX This should go to the separate error log.
"""
self.log_message(format, *args)
def log_message(self, format, *args):
"""Log an arbitrary message.
This is used by all other logging functions. Override
it if you have specific logging wishes.
The first argument, FORMAT, is a format string for the
message to be logged. If the format string contains
any % escapes requiring parameters, they should be
specified as subsequent arguments (it's just like
printf!).
The client host and current date/time are prefixed to
every message.
"""
sys.stderr.write("%s - - [%s] %s\n" %
(self.address_string(),
self.log_date_time_string(),
format%args))
def version_string(self):
"""Return the server software version string."""
return self.server_version + ' ' + self.sys_version
def date_time_string(self, timestamp=None):
"""Return the current date and time formatted for a message header."""
if timestamp is None:
timestamp = time.time()
year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
self.weekdayname[wd],
day, self.monthname[month], year,
hh, mm, ss)
return s
def log_date_time_string(self):
"""Return the current time formatted for logging."""
now = time.time()
year, month, day, hh, mm, ss, x, y, z = time.localtime(now)
s = "%02d/%3s/%04d %02d:%02d:%02d" % (
day, self.monthname[month], year, hh, mm, ss)
return s
weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
monthname = [None,
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
def address_string(self):
"""Return the client address formatted for logging.
This version looks up the full hostname using gethostbyaddr(),
and tries to find a name that contains at least one dot.
"""
host, port = self.client_address[:2]
return socket.getfqdn(host)
# Essentially static class variables
# The version of the HTTP protocol we support.
# Set this to HTTP/1.1 to enable automatic keepalive
protocol_version = "HTTP/1.0"
# The Message-like class used to parse headers
MessageClass = mimetools.Message
# Table mapping response codes to messages; entries have the
# form {code: (shortmessage, longmessage)}.
# See RFC 2616.
responses = {
100: ('Continue', 'Request received, please continue'),
101: ('Switching Protocols',
'Switching to new protocol; obey Upgrade header'),
200: ('OK', 'Request fulfilled, document follows'),
201: ('Created', 'Document created, URL follows'),
202: ('Accepted',
'Request accepted, processing continues off-line'),
203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
204: ('No Content', 'Request fulfilled, nothing follows'),
205: ('Reset Content', 'Clear input form for further input.'),
206: ('Partial Content', 'Partial content follows.'),
300: ('Multiple Choices',
'Object has several resources -- see URI list'),
301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
302: ('Found', 'Object moved temporarily -- see URI list'),
303: ('See Other', 'Object moved -- see Method and URL list'),
304: ('Not Modified',
'Document has not changed since given time'),
305: ('Use Proxy',
'You must use proxy specified in Location to access this '
'resource.'),
307: ('Temporary Redirect',
'Object moved temporarily -- see URI list'),
400: ('Bad Request',
'Bad request syntax or unsupported method'),
401: ('Unauthorized',
'No permission -- see authorization schemes'),
402: ('Payment Required',
'No payment -- see charging schemes'),
403: ('Forbidden',
'Request forbidden -- authorization will not help'),
404: ('Not Found', 'Nothing matches the given URI'),
405: ('Method Not Allowed',
'Specified method is invalid for this server.'),
406: ('Not Acceptable', 'URI not available in preferred format.'),
407: ('Proxy Authentication Required', 'You must authenticate with '
'this proxy before proceeding.'),
408: ('Request Timeout', 'Request timed out; try again later.'),
409: ('Conflict', 'Request conflict.'),
410: ('Gone',
'URI no longer exists and has been permanently removed.'),
411: ('Length Required', 'Client must specify Content-Length.'),
412: ('Precondition Failed', 'Precondition in headers is false.'),
413: ('Request Entity Too Large', 'Entity is too large.'),
414: ('Request-URI Too Long', 'URI is too long.'),
415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
416: ('Requested Range Not Satisfiable',
'Cannot satisfy request range.'),
417: ('Expectation Failed',
'Expect condition could not be satisfied.'),
500: ('Internal Server Error', 'Server got itself in trouble'),
501: ('Not Implemented',
'Server does not support this operation'),
502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
503: ('Service Unavailable',
'The server cannot process the request due to a high load'),
504: ('Gateway Timeout',
'The gateway server did not receive a timely response'),
505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
}
def test(HandlerClass = BaseHTTPRequestHandler,
ServerClass = HTTPServer, protocol="HTTP/1.0"):
"""Test the HTTP request handler class.
This runs an HTTP server on port 8000 (or the first command line
argument).
"""
if sys.argv[1:]:
port = int(sys.argv[1])
else:
port = 8000
server_address = ('', port)
HandlerClass.protocol_version = protocol
httpd = ServerClass(server_address, HandlerClass)
sa = httpd.socket.getsockname()
print "Serving HTTP on", sa[0], "port", sa[1], "..."
httpd.serve_forever()
if __name__ == '__main__':
test()

View File

@ -1,611 +0,0 @@
"""Simple XML-RPC Server.
This module can be used to create simple XML-RPC servers
by creating a server and either installing functions, a
class instance, or by extending the SimpleXMLRPCServer
class.
It can also be used to handle XML-RPC requests in a CGI
environment using CGIXMLRPCRequestHandler.
A list of possible usage patterns follows:
1. Install functions:
server = SimpleXMLRPCServer(("localhost", 8000))
server.register_function(pow)
server.register_function(lambda x,y: x+y, 'add')
server.serve_forever()
2. Install an instance:
class MyFuncs:
def __init__(self):
# make all of the string functions available through
# string.func_name
import string
self.string = string
def _listMethods(self):
# implement this method so that system.listMethods
# knows to advertise the strings methods
return list_public_methods(self) + \
['string.' + method for method in list_public_methods(self.string)]
def pow(self, x, y): return pow(x, y)
def add(self, x, y) : return x + y
server = SimpleXMLRPCServer(("localhost", 8000))
server.register_introspection_functions()
server.register_instance(MyFuncs())
server.serve_forever()
3. Install an instance with custom dispatch method:
class Math:
def _listMethods(self):
# this method must be present for system.listMethods
# to work
return ['add', 'pow']
def _methodHelp(self, method):
# this method must be present for system.methodHelp
# to work
if method == 'add':
return "add(2,3) => 5"
elif method == 'pow':
return "pow(x, y[, z]) => number"
else:
# By convention, return empty
# string if no help is available
return ""
def _dispatch(self, method, params):
if method == 'pow':
return pow(*params)
elif method == 'add':
return params[0] + params[1]
else:
raise 'bad method'
server = SimpleXMLRPCServer(("localhost", 8000))
server.register_introspection_functions()
server.register_instance(Math())
server.serve_forever()
4. Subclass SimpleXMLRPCServer:
class MathServer(SimpleXMLRPCServer):
def _dispatch(self, method, params):
try:
# We are forcing the 'export_' prefix on methods that are
# callable through XML-RPC to prevent potential security
# problems
func = getattr(self, 'export_' + method)
except AttributeError:
raise Exception('method "%s" is not supported' % method)
else:
return func(*params)
def export_add(self, x, y):
return x + y
server = MathServer(("localhost", 8000))
server.serve_forever()
5. CGI script:
server = CGIXMLRPCRequestHandler()
server.register_function(pow)
server.handle_request()
"""
# Written by Brian Quinlan (brian@sweetapp.com).
# Based on code written by Fredrik Lundh.
import xmlrpclib
from xmlrpclib import Fault
import SocketServer
import BaseHTTPServer
import sys
import os
import traceback
try:
import fcntl
except ImportError:
fcntl = None
def resolve_dotted_attribute(obj, attr, allow_dotted_names=True):
"""resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d
Resolves a dotted attribute name to an object. Raises
an AttributeError if any attribute in the chain starts with a '_'.
If the optional allow_dotted_names argument is false, dots are not
supported and this function operates similar to getattr(obj, attr).
"""
if allow_dotted_names:
attrs = attr.split('.')
else:
attrs = [attr]
for i in attrs:
if i.startswith('_'):
raise AttributeError(
'attempt to access private attribute "%s"' % i
)
else:
obj = getattr(obj,i)
return obj
def list_public_methods(obj):
"""Returns a list of attribute strings, found in the specified
object, which represent callable attributes"""
return [member for member in dir(obj)
if not member.startswith('_') and
hasattr(getattr(obj, member), '__call__')]
def remove_duplicates(lst):
"""remove_duplicates([2,2,2,1,3,3]) => [3,1,2]
Returns a copy of a list without duplicates. Every list
item must be hashable and the order of the items in the
resulting list is not defined.
"""
u = {}
for x in lst:
u[x] = 1
return u.keys()
class SimpleXMLRPCDispatcher:
"""Mix-in class that dispatches XML-RPC requests.
This class is used to register XML-RPC method handlers
and then to dispatch them. There should never be any
reason to instantiate this class directly.
"""
def __init__(self, allow_none, encoding):
self.funcs = {}
self.instance = None
self.allow_none = allow_none
self.encoding = encoding
def register_instance(self, instance, allow_dotted_names=False):
"""Registers an instance to respond to XML-RPC requests.
Only one instance can be installed at a time.
If the registered instance has a _dispatch method then that
method will be called with the name of the XML-RPC method and
its parameters as a tuple
e.g. instance._dispatch('add',(2,3))
If the registered instance does not have a _dispatch method
then the instance will be searched to find a matching method
and, if found, will be called. Methods beginning with an '_'
are considered private and will not be called by
SimpleXMLRPCServer.
If a registered function matches a XML-RPC request, then it
will be called instead of the registered instance.
If the optional allow_dotted_names argument is true and the
instance does not have a _dispatch method, method names
containing dots are supported and resolved, as long as none of
the name segments start with an '_'.
*** SECURITY WARNING: ***
Enabling the allow_dotted_names options allows intruders
to access your module's global variables and may allow
intruders to execute arbitrary code on your machine. Only
use this option on a secure, closed network.
"""
self.instance = instance
self.allow_dotted_names = allow_dotted_names
def register_function(self, function, name = None):
"""Registers a function to respond to XML-RPC requests.
The optional name argument can be used to set a Unicode name
for the function.
"""
if name is None:
name = function.__name__
self.funcs[name] = function
def register_introspection_functions(self):
"""Registers the XML-RPC introspection methods in the system
namespace.
see http://xmlrpc.usefulinc.com/doc/reserved.html
"""
self.funcs.update({'system.listMethods' : self.system_listMethods,
'system.methodSignature' : self.system_methodSignature,
'system.methodHelp' : self.system_methodHelp})
def register_multicall_functions(self):
"""Registers the XML-RPC multicall method in the system
namespace.
see http://www.xmlrpc.com/discuss/msgReader$1208"""
self.funcs.update({'system.multicall' : self.system_multicall})
def _marshaled_dispatch(self, data, dispatch_method = None):
"""Dispatches an XML-RPC method from marshalled (XML) data.
XML-RPC methods are dispatched from the marshalled (XML) data
using the _dispatch method and the result is returned as
marshalled data. For backwards compatibility, a dispatch
function can be provided as an argument (see comment in
SimpleXMLRPCRequestHandler.do_POST) but overriding the
existing method through subclassing is the prefered means
of changing method dispatch behavior.
"""
try:
params, method = xmlrpclib.loads(data)
# generate response
if dispatch_method is not None:
response = dispatch_method(method, params)
else:
response = self._dispatch(method, params)
# wrap response in a singleton tuple
response = (response,)
response = xmlrpclib.dumps(response, methodresponse=1,
allow_none=self.allow_none, encoding=self.encoding)
except Fault, fault:
response = xmlrpclib.dumps(fault, allow_none=self.allow_none,
encoding=self.encoding)
except:
# report exception back to server
exc_type, exc_value, exc_tb = sys.exc_info()
response = xmlrpclib.dumps(
xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
encoding=self.encoding, allow_none=self.allow_none,
)
return response
def system_listMethods(self):
"""system.listMethods() => ['add', 'subtract', 'multiple']
Returns a list of the methods supported by the server."""
methods = self.funcs.keys()
if self.instance is not None:
# Instance can implement _listMethod to return a list of
# methods
if hasattr(self.instance, '_listMethods'):
methods = remove_duplicates(
methods + self.instance._listMethods()
)
# if the instance has a _dispatch method then we
# don't have enough information to provide a list
# of methods
elif not hasattr(self.instance, '_dispatch'):
methods = remove_duplicates(
methods + list_public_methods(self.instance)
)
methods.sort()
return methods
def system_methodSignature(self, method_name):
"""system.methodSignature('add') => [double, int, int]
Returns a list describing the signature of the method. In the
above example, the add method takes two integers as arguments
and returns a double result.
This server does NOT support system.methodSignature."""
# See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html
return 'signatures not supported'
def system_methodHelp(self, method_name):
"""system.methodHelp('add') => "Adds two integers together"
Returns a string containing documentation for the specified method."""
method = None
if method_name in self.funcs:
method = self.funcs[method_name]
elif self.instance is not None:
# Instance can implement _methodHelp to return help for a method
if hasattr(self.instance, '_methodHelp'):
return self.instance._methodHelp(method_name)
# if the instance has a _dispatch method then we
# don't have enough information to provide help
elif not hasattr(self.instance, '_dispatch'):
try:
method = resolve_dotted_attribute(
self.instance,
method_name,
self.allow_dotted_names
)
except AttributeError:
pass
# Note that we aren't checking that the method actually
# be a callable object of some kind
if method is None:
return ""
else:
import pydoc
return pydoc.getdoc(method)
def system_multicall(self, call_list):
"""system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \
[[4], ...]
Allows the caller to package multiple XML-RPC calls into a single
request.
See http://www.xmlrpc.com/discuss/msgReader$1208
"""
results = []
for call in call_list:
method_name = call['methodName']
params = call['params']
try:
# XXX A marshalling error in any response will fail the entire
# multicall. If someone cares they should fix this.
results.append([self._dispatch(method_name, params)])
except Fault, fault:
results.append(
{'faultCode' : fault.faultCode,
'faultString' : fault.faultString}
)
except:
exc_type, exc_value, exc_tb = sys.exc_info()
results.append(
{'faultCode' : 1,
'faultString' : "%s:%s" % (exc_type, exc_value)}
)
return results
def _dispatch(self, method, params):
"""Dispatches the XML-RPC method.
XML-RPC calls are forwarded to a registered function that
matches the called XML-RPC method name. If no such function
exists then the call is forwarded to the registered instance,
if available.
If the registered instance has a _dispatch method then that
method will be called with the name of the XML-RPC method and
its parameters as a tuple
e.g. instance._dispatch('add',(2,3))
If the registered instance does not have a _dispatch method
then the instance will be searched to find a matching method
and, if found, will be called.
Methods beginning with an '_' are considered private and will
not be called.
"""
func = None
try:
# check to see if a matching function has been registered
func = self.funcs[method]
except KeyError:
if self.instance is not None:
# check for a _dispatch method
if hasattr(self.instance, '_dispatch'):
return self.instance._dispatch(method, params)
else:
# call instance method directly
try:
func = resolve_dotted_attribute(
self.instance,
method,
self.allow_dotted_names
)
except AttributeError:
pass
if func is not None:
return func(*params)
else:
raise Exception('method "%s" is not supported' % method)
class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""Simple XML-RPC request handler class.
Handles all HTTP POST requests and attempts to decode them as
XML-RPC requests.
"""
# Class attribute listing the accessible path components;
# paths not on this list will result in a 404 error.
rpc_paths = ('/', '/RPC2')
def is_rpc_path_valid(self):
if self.rpc_paths:
return self.path in self.rpc_paths
else:
# If .rpc_paths is empty, just assume all paths are legal
return True
def do_POST(self):
"""Handles the HTTP POST request.
Attempts to interpret all HTTP POST requests as XML-RPC calls,
which are forwarded to the server's _dispatch method for handling.
"""
# Check that the path is legal
if not self.is_rpc_path_valid():
self.report_404()
return
try:
# Get arguments by reading body of request.
# We read this in chunks to avoid straining
# socket.read(); around the 10 or 15Mb mark, some platforms
# begin to have problems (bug #792570).
max_chunk_size = 10*1024*1024
size_remaining = int(self.headers["content-length"])
L = []
while size_remaining:
chunk_size = min(size_remaining, max_chunk_size)
L.append(self.rfile.read(chunk_size))
size_remaining -= len(L[-1])
data = ''.join(L)
# In previous versions of SimpleXMLRPCServer, _dispatch
# could be overridden in this class, instead of in
# SimpleXMLRPCDispatcher. To maintain backwards compatibility,
# check to see if a subclass implements _dispatch and dispatch
# using that method if present.
response = self.server._marshaled_dispatch(
data, getattr(self, '_dispatch', None)
)
except Exception, e: # This should only happen if the module is buggy
# internal error, report as HTTP server error
self.send_response(500)
# Send information about the exception if requested
if hasattr(self.server, '_send_traceback_header') and \
self.server._send_traceback_header:
self.send_header("X-exception", str(e))
self.send_header("X-traceback", traceback.format_exc())
self.end_headers()
else:
# got a valid XML RPC response
self.send_response(200)
self.send_header("Content-type", "text/xml")
self.send_header("Content-length", str(len(response)))
self.end_headers()
self.wfile.write(response)
# shut down the connection
self.wfile.flush()
self.connection.shutdown(1)
def report_404 (self):
# Report a 404 error
self.send_response(404)
response = 'No such page'
self.send_header("Content-type", "text/plain")
self.send_header("Content-length", str(len(response)))
self.end_headers()
self.wfile.write(response)
# shut down the connection
self.wfile.flush()
self.connection.shutdown(1)
def log_request(self, code='-', size='-'):
"""Selectively log an accepted request."""
if self.server.logRequests:
BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
class SimpleXMLRPCServer(SocketServer.TCPServer,
SimpleXMLRPCDispatcher):
"""Simple XML-RPC server.
Simple XML-RPC server that allows functions and a single instance
to be installed to handle requests. The default implementation
attempts to dispatch XML-RPC calls to the functions or instance
installed in the server. Override the _dispatch method inhereted
from SimpleXMLRPCDispatcher to change this behavior.
"""
allow_reuse_address = True
# Warning: this is for debugging purposes only! Never set this to True in
# production code, as will be sending out sensitive information (exception
# and stack trace details) when exceptions are raised inside
# SimpleXMLRPCRequestHandler.do_POST
_send_traceback_header = False
def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
self.logRequests = logRequests
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
SocketServer.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
# [Bug #1222790] If possible, set close-on-exec flag; if a
# method spawns a subprocess, the subprocess shouldn't have
# the listening socket open.
if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
flags |= fcntl.FD_CLOEXEC
fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher):
"""Simple handler for XML-RPC data passed through CGI."""
def __init__(self, allow_none=False, encoding=None):
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
def handle_xmlrpc(self, request_text):
"""Handle a single XML-RPC request"""
response = self._marshaled_dispatch(request_text)
print 'Content-Type: text/xml'
print 'Content-Length: %d' % len(response)
print
sys.stdout.write(response)
def handle_get(self):
"""Handle a single HTTP GET request.
Default implementation indicates an error because
XML-RPC uses the POST method.
"""
code = 400
message, explain = \
BaseHTTPServer.BaseHTTPRequestHandler.responses[code]
response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \
{
'code' : code,
'message' : message,
'explain' : explain
}
print 'Status: %d %s' % (code, message)
print 'Content-Type: text/html'
print 'Content-Length: %d' % len(response)
print
sys.stdout.write(response)
def handle_request(self, request_text = None):
"""Handle a single XML-RPC request passed through a CGI post method.
If no XML data is given then it is read from stdin. The resulting
XML-RPC response is printed to stdout along with the correct HTTP
headers.
"""
if request_text is None and \
os.environ.get('REQUEST_METHOD', None) == 'GET':
self.handle_get()
else:
# POST data is normally available through stdin
if request_text is None:
request_text = sys.stdin.read()
self.handle_xmlrpc(request_text)
if __name__ == '__main__':
print 'Running XML-RPC server on port 8000'
server = SimpleXMLRPCServer(("localhost", 8000))
server.register_function(pow)
server.register_function(lambda x,y: x+y, 'add')
server.serve_forever()

View File

@ -1,681 +0,0 @@
"""Generic socket server classes.
This module tries to capture the various aspects of defining a server:
For socket-based servers:
- address family:
- AF_INET{,6}: IP (Internet Protocol) sockets (default)
- AF_UNIX: Unix domain sockets
- others, e.g. AF_DECNET are conceivable (see <socket.h>
- socket type:
- SOCK_STREAM (reliable stream, e.g. TCP)
- SOCK_DGRAM (datagrams, e.g. UDP)
For request-based servers (including socket-based):
- client address verification before further looking at the request
(This is actually a hook for any processing that needs to look
at the request before anything else, e.g. logging)
- how to handle multiple requests:
- synchronous (one request is handled at a time)
- forking (each request is handled by a new process)
- threading (each request is handled by a new thread)
The classes in this module favor the server type that is simplest to
write: a synchronous TCP/IP server. This is bad class design, but
save some typing. (There's also the issue that a deep class hierarchy
slows down method lookups.)
There are five classes in an inheritance diagram, four of which represent
synchronous servers of four types:
+------------+
| BaseServer |
+------------+
|
v
+-----------+ +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+ +------------------+
|
v
+-----------+ +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+ +--------------------+
Note that UnixDatagramServer derives from UDPServer, not from
UnixStreamServer -- the only difference between an IP and a Unix
stream server is the address family, which is simply repeated in both
unix server classes.
Forking and threading versions of each type of server can be created
using the ForkingMixIn and ThreadingMixIn mix-in classes. For
instance, a threading UDP server class is created as follows:
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
The Mix-in class must come first, since it overrides a method defined
in UDPServer! Setting the various member variables also changes
the behavior of the underlying server mechanism.
To implement a service, you must derive a class from
BaseRequestHandler and redefine its handle() method. You can then run
various versions of the service by combining one of the server classes
with your request handler class.
The request handler class must be different for datagram or stream
services. This can be hidden by using the request handler
subclasses StreamRequestHandler or DatagramRequestHandler.
Of course, you still have to use your head!
For instance, it makes no sense to use a forking server if the service
contains state in memory that can be modified by requests (since the
modifications in the child process would never reach the initial state
kept in the parent process and passed to each child). In this case,
you can use a threading server, but you will probably have to use
locks to avoid two requests that come in nearly simultaneous to apply
conflicting changes to the server state.
On the other hand, if you are building e.g. an HTTP server, where all
data is stored externally (e.g. in the file system), a synchronous
class will essentially render the service "deaf" while one request is
being handled -- which may be for a very long time if a client is slow
to reqd all the data it has requested. Here a threading or forking
server is appropriate.
In some cases, it may be appropriate to process part of a request
synchronously, but to finish processing in a forked child depending on
the request data. This can be implemented by using a synchronous
server and doing an explicit fork in the request handler class
handle() method.
Another approach to handling multiple simultaneous requests in an
environment that supports neither threads nor fork (or where these are
too expensive or inappropriate for the service) is to maintain an
explicit table of partially finished requests and to use select() to
decide which request to work on next (or whether to handle a new
incoming request). This is particularly important for stream services
where each client can potentially be connected for a long time (if
threads or subprocesses cannot be used).
Future work:
- Standard classes for Sun RPC (which uses either UDP or TCP)
- Standard mix-in classes to implement various authentication
and encryption schemes
- Standard framework for select-based multiplexing
XXX Open problems:
- What to do with out-of-band data?
BaseServer:
- split generic "request" functionality out into BaseServer class.
Copyright (C) 2000 Luke Kenneth Casson Leighton <lkcl@samba.org>
example: read entries from a SQL database (requires overriding
get_request() to return a table entry from the database).
entry is processed by a RequestHandlerClass.
"""
# Author of the BaseServer patch: Luke Kenneth Casson Leighton
# XXX Warning!
# There is a test suite for this module, but it cannot be run by the
# standard regression test.
# To run it manually, run Lib/test/test_socketserver.py.
__version__ = "0.4"
import socket
import select
import sys
import os
try:
import threading
except ImportError:
import dummy_threading as threading
__all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer",
"ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler",
"StreamRequestHandler","DatagramRequestHandler",
"ThreadingMixIn", "ForkingMixIn"]
if hasattr(socket, "AF_UNIX"):
__all__.extend(["UnixStreamServer","UnixDatagramServer",
"ThreadingUnixStreamServer",
"ThreadingUnixDatagramServer"])
class BaseServer:
"""Base class for server classes.
Methods for the caller:
- __init__(server_address, RequestHandlerClass)
- serve_forever(poll_interval=0.5)
- shutdown()
- handle_request() # if you do not use serve_forever()
- fileno() -> int # for select()
Methods that may be overridden:
- server_bind()
- server_activate()
- get_request() -> request, client_address
- handle_timeout()
- verify_request(request, client_address)
- server_close()
- process_request(request, client_address)
- close_request(request)
- handle_error()
Methods for derived classes:
- finish_request(request, client_address)
Class variables that may be overridden by derived classes or
instances:
- timeout
- address_family
- socket_type
- allow_reuse_address
Instance variables:
- RequestHandlerClass
- socket
"""
timeout = None
def __init__(self, server_address, RequestHandlerClass):
"""Constructor. May be extended, do not override."""
self.server_address = server_address
self.RequestHandlerClass = RequestHandlerClass
self.__is_shut_down = threading.Event()
self.__serving = False
def server_activate(self):
"""Called by constructor to activate the server.
May be overridden.
"""
pass
def serve_forever(self, poll_interval=0.5):
"""Handle one request at a time until shutdown.
Polls for shutdown every poll_interval seconds. Ignores
self.timeout. If you need to do periodic tasks, do them in
another thread.
"""
self.__serving = True
self.__is_shut_down.clear()
while self.__serving:
# XXX: Consider using another file descriptor or
# connecting to the socket to wake this up instead of
# polling. Polling reduces our responsiveness to a
# shutdown request and wastes cpu at all other times.
r, w, e = select.select([self], [], [], poll_interval)
if r:
self._handle_request_noblock()
self.__is_shut_down.set()
def shutdown(self):
"""Stops the serve_forever loop.
Blocks until the loop has finished. This must be called while
serve_forever() is running in another thread, or it will
deadlock.
"""
self.__serving = False
self.__is_shut_down.wait()
# The distinction between handling, getting, processing and
# finishing a request is fairly arbitrary. Remember:
#
# - handle_request() is the top-level call. It calls
# select, get_request(), verify_request() and process_request()
# - get_request() is different for stream or datagram sockets
# - process_request() is the place that may fork a new process
# or create a new thread to finish the request
# - finish_request() instantiates the request handler class;
# this constructor will handle the request all by itself
def handle_request(self):
"""Handle one request, possibly blocking.
Respects self.timeout.
"""
# Support people who used socket.settimeout() to escape
# handle_request before self.timeout was available.
timeout = self.socket.gettimeout()
if timeout is None:
timeout = self.timeout
elif self.timeout is not None:
timeout = min(timeout, self.timeout)
fd_sets = select.select([self], [], [], timeout)
if not fd_sets[0]:
self.handle_timeout()
return
self._handle_request_noblock()
def _handle_request_noblock(self):
"""Handle one request, without blocking.
I assume that select.select has returned that the socket is
readable before this function was called, so there should be
no risk of blocking in get_request().
"""
try:
request, client_address = self.get_request()
except socket.error:
return
if self.verify_request(request, client_address):
try:
self.process_request(request, client_address)
except:
self.handle_error(request, client_address)
self.close_request(request)
def handle_timeout(self):
"""Called if no new request arrives within self.timeout.
Overridden by ForkingMixIn.
"""
pass
def verify_request(self, request, client_address):
"""Verify the request. May be overridden.
Return True if we should proceed with this request.
"""
return True
def process_request(self, request, client_address):
"""Call finish_request.
Overridden by ForkingMixIn and ThreadingMixIn.
"""
self.finish_request(request, client_address)
self.close_request(request)
def server_close(self):
"""Called to clean-up the server.
May be overridden.
"""
pass
def finish_request(self, request, client_address):
"""Finish one request by instantiating RequestHandlerClass."""
self.RequestHandlerClass(request, client_address, self)
def close_request(self, request):
"""Called to clean up an individual request."""
pass
def handle_error(self, request, client_address):
"""Handle an error gracefully. May be overridden.
The default is to print a traceback and continue.
"""
print '-'*40
print 'Exception happened during processing of request from',
print client_address
import traceback
traceback.print_exc() # XXX But this goes to stderr!
print '-'*40
class TCPServer(BaseServer):
"""Base class for various socket-based server classes.
Defaults to synchronous IP stream (i.e., TCP).
Methods for the caller:
- __init__(server_address, RequestHandlerClass, bind_and_activate=True)
- serve_forever(poll_interval=0.5)
- shutdown()
- handle_request() # if you don't use serve_forever()
- fileno() -> int # for select()
Methods that may be overridden:
- server_bind()
- server_activate()
- get_request() -> request, client_address
- handle_timeout()
- verify_request(request, client_address)
- process_request(request, client_address)
- close_request(request)
- handle_error()
Methods for derived classes:
- finish_request(request, client_address)
Class variables that may be overridden by derived classes or
instances:
- timeout
- address_family
- socket_type
- request_queue_size (only for stream sockets)
- allow_reuse_address
Instance variables:
- server_address
- RequestHandlerClass
- socket
"""
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
request_queue_size = 5
allow_reuse_address = False
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
"""Constructor. May be extended, do not override."""
BaseServer.__init__(self, server_address, RequestHandlerClass)
self.socket = socket.socket(self.address_family,
self.socket_type)
if bind_and_activate:
self.server_bind()
self.server_activate()
def server_bind(self):
"""Called by constructor to bind the socket.
May be overridden.
"""
if self.allow_reuse_address:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(self.server_address)
self.server_address = self.socket.getsockname()
def server_activate(self):
"""Called by constructor to activate the server.
May be overridden.
"""
self.socket.listen(self.request_queue_size)
def server_close(self):
"""Called to clean-up the server.
May be overridden.
"""
self.socket.close()
def fileno(self):
"""Return socket file number.
Interface required by select().
"""
return self.socket.fileno()
def get_request(self):
"""Get the request and client address from the socket.
May be overridden.
"""
return self.socket.accept()
def close_request(self, request):
"""Called to clean up an individual request."""
request.close()
class UDPServer(TCPServer):
"""UDP server class."""
allow_reuse_address = False
socket_type = socket.SOCK_DGRAM
max_packet_size = 8192
def get_request(self):
data, client_addr = self.socket.recvfrom(self.max_packet_size)
return (data, self.socket), client_addr
def server_activate(self):
# No need to call listen() for UDP.
pass
def close_request(self, request):
# No need to close anything.
pass
class ForkingMixIn:
"""Mix-in class to handle each request in a new process."""
timeout = 300
active_children = None
max_children = 40
def collect_children(self):
"""Internal routine to wait for children that have exited."""
if self.active_children is None: return
while len(self.active_children) >= self.max_children:
# XXX: This will wait for any child process, not just ones
# spawned by this library. This could confuse other
# libraries that expect to be able to wait for their own
# children.
try:
pid, status = os.waitpid(0, options=0)
except os.error:
pid = None
if pid not in self.active_children: continue
self.active_children.remove(pid)
# XXX: This loop runs more system calls than it ought
# to. There should be a way to put the active_children into a
# process group and then use os.waitpid(-pgid) to wait for any
# of that set, but I couldn't find a way to allocate pgids
# that couldn't collide.
for child in self.active_children:
try:
pid, status = os.waitpid(child, os.WNOHANG)
except os.error:
pid = None
if not pid: continue
try:
self.active_children.remove(pid)
except ValueError, e:
raise ValueError('%s. x=%d and list=%r' % (e.message, pid,
self.active_children))
def handle_timeout(self):
"""Wait for zombies after self.timeout seconds of inactivity.
May be extended, do not override.
"""
self.collect_children()
def process_request(self, request, client_address):
"""Fork a new subprocess to process the request."""
self.collect_children()
pid = os.fork()
if pid:
# Parent process
if self.active_children is None:
self.active_children = []
self.active_children.append(pid)
self.close_request(request)
return
else:
# Child process.
# This must never return, hence os._exit()!
try:
self.finish_request(request, client_address)
os._exit(0)
except:
try:
self.handle_error(request, client_address)
finally:
os._exit(1)
class ThreadingMixIn:
"""Mix-in class to handle each request in a new thread."""
# Decides how threads will act upon termination of the
# main process
daemon_threads = False
def process_request_thread(self, request, client_address):
"""Same as in BaseServer but as a thread.
In addition, exception handling is done here.
"""
try:
self.finish_request(request, client_address)
self.close_request(request)
except:
self.handle_error(request, client_address)
self.close_request(request)
def process_request(self, request, client_address):
"""Start a new thread to process the request."""
t = threading.Thread(target = self.process_request_thread,
args = (request, client_address))
if self.daemon_threads:
t.setDaemon (1)
t.start()
class ForkingUDPServer(ForkingMixIn, UDPServer): pass
class ForkingTCPServer(ForkingMixIn, TCPServer): pass
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
if hasattr(socket, 'AF_UNIX'):
class UnixStreamServer(TCPServer):
address_family = socket.AF_UNIX
class UnixDatagramServer(UDPServer):
address_family = socket.AF_UNIX
class ThreadingUnixStreamServer(ThreadingMixIn, UnixStreamServer): pass
class ThreadingUnixDatagramServer(ThreadingMixIn, UnixDatagramServer): pass
class BaseRequestHandler:
"""Base class for request handler classes.
This class is instantiated for each request to be handled. The
constructor sets the instance variables request, client_address
and server, and then calls the handle() method. To implement a
specific service, all you need to do is to derive a class which
defines a handle() method.
The handle() method can find the request as self.request, the
client address as self.client_address, and the server (in case it
needs access to per-server information) as self.server. Since a
separate instance is created for each request, the handle() method
can define arbitrary other instance variariables.
"""
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
try:
self.setup()
self.handle()
self.finish()
finally:
sys.exc_traceback = None # Help garbage collection
def setup(self):
pass
def handle(self):
pass
def finish(self):
pass
# The following two classes make it possible to use the same service
# class for stream or datagram servers.
# Each class sets up these instance variables:
# - rfile: a file object from which receives the request is read
# - wfile: a file object to which the reply is written
# When the handle() method returns, wfile is flushed properly
class StreamRequestHandler(BaseRequestHandler):
"""Define self.rfile and self.wfile for stream sockets."""
# Default buffer sizes for rfile, wfile.
# We default rfile to buffered because otherwise it could be
# really slow for large data (a getc() call per byte); we make
# wfile unbuffered because (a) often after a write() we want to
# read and we need to flush the line; (b) big writes to unbuffered
# files are typically optimized by stdio even when big reads
# aren't.
rbufsize = -1
wbufsize = 0
def setup(self):
self.connection = self.request
self.rfile = self.connection.makefile('rb', self.rbufsize)
self.wfile = self.connection.makefile('wb', self.wbufsize)
def finish(self):
if not self.wfile.closed:
self.wfile.flush()
self.wfile.close()
self.rfile.close()
class DatagramRequestHandler(BaseRequestHandler):
# XXX Regrettably, I cannot get this working on Linux;
# s.recvfrom() doesn't return a meaningful client address.
"""Define self.rfile and self.wfile for datagram sockets."""
def setup(self):
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
self.packet, self.socket = self.request
self.rfile = StringIO(self.packet)
self.wfile = StringIO()
def finish(self):
self.socket.sendto(self.wfile.getvalue(), self.client_address)

View File

@ -1,27 +0,0 @@
Some instructions to use setup.py for a user-install.
This file should/will be moved on a proper documentation place later.
- Possibly clean any left-over of the previous build.
> rm -rf dist openerp_server.egg-info
- Possibly copy the addons in the server if we want them to be packaged
together:
> rsync -av --delete \
--exclude .bzr/ \
--exclude .bzrignore \
--exclude /__init__.py \
--exclude /base \
--exclude /base_quality_interrogation.py \
<path-to-addons> openerp/addons
- Create the user-local directory where we want the package to be installed:
> mkdir -p /home/openerp/openerp-tmp/lib/python2.6/site-packages/
- Use --prefix to specify where the package is installed and include that
place in PYTHONPATH:
> PYTHONPATH=/home/openerp/openerp-tmp/lib/python2.6/site-packages/ \
python setup.py install --prefix=/home/openerp/openerp-tmp
- Run the main script, again specifying the PYTHONPATH:
> PYTHONPATH=/home/openerp/openerp-tmp/lib/python2.6/site-packages/ \
/home/openerp/openerp-tmp/bin/openerp-server

View File

@ -17,4 +17,4 @@ requires=python >= 2.5
# Need to overwrite the install-part of the RPM to patch
# the filenames of the man pages.
install_script=rpminstall_sh.txt
install_script=setup_rpm.sh

113
setup.py
View File

@ -20,57 +20,12 @@
#
##############################################################################
# setup from TinERP
# taken from straw http://www.nongnu.org/straw/index.html
# taken from gnomolicious http://www.nongnu.org/gnomolicious/
# adapted by Nicolas Évrard <nicoe@altern.org>
#
# doc/migrate is not included since about 6.1-dev
# doc/tests is not included
# python25-compat/*py should be in the openerp (and imported appropriately)
import sys
import os
import glob, os, re, setuptools, sys
from os.path import join, isfile
import glob
from setuptools import setup, find_packages
# Backports os.walk with followlinks from python 2.6.
# Needed to add all addons files to data_files for Windows packaging.
def walk_followlinks(top, topdown=True, onerror=None, followlinks=False):
from os.path import join, isdir, islink
from os import listdir, error
try:
names = listdir(top)
except error, err:
if onerror is not None:
onerror(err)
return
dirs, nondirs = [], []
for name in names:
if isdir(join(top, name)):
dirs.append(name)
else:
nondirs.append(name)
if topdown:
yield top, dirs, nondirs
for name in dirs:
path = join(top, name)
if followlinks or not islink(path):
for x in walk_followlinks(path, topdown, onerror, followlinks):
yield x
if not topdown:
yield top, dirs, nondirs
if sys.version_info < (2, 6):
os.walk = walk_followlinks
execfile(join('openerp', 'release.py'))
py2exe_keywords = {}
py2exe_data_files = []
if os.name == 'nt':
import py2exe
py2exe_keywords['console'] = [
@ -94,37 +49,30 @@ if os.name == 'nt':
"excludes" : ["Tkconstants","Tkinter","tcl"],
}
}
# TODO is it still necessary now that we don't use the library.zip file?
def data_files():
'''For Windows, we consider all the addons as data files.
It seems also that package_data below isn't honored by py2exe.'''
files = []
os.chdir('openerp')
for (dp, dn, names) in os.walk('addons'):
files.append((join('openerp',dp), map(lambda x: join('openerp', dp, x), names)))
os.chdir('..')
files.append(('openerp', [join('openerp', 'import_xml.rng'),]))
# copy pytz/timzeone
# TODO check if we have to also copy dateutil's timezone data.
import pytz
# Make sure the layout of pytz hasn't changed
assert (pytz.__file__.endswith('__init__.pyc') or
pytz.__file__.endswith('__init__.py')), pytz.__file__
pytz_dir = os.path.dirname(pytz.__file__)
# List all data files
def data():
files = []
for root, dirnames, filenames in os.walk('openerp'):
for filename in filenames:
if not re.match(r'.*(\.pyc|\.pyo|\~)$',filename):
files.append(os.path.join(root, filename))
d = {}
for v in files:
k=os.path.dirname(v)
if k in d:
d[k].append(v)
else:
d[k]=[v]
r = d.items()
return r
saved_dir = os.getcwd()
os.chdir(pytz_dir)
for dp, dn, names in os.walk('zoneinfo'):
files.append((join('pytz',dp), map(lambda x: join(pytz_dir, dp, x), names)))
os.chdir(saved_dir)
def gen_manifest():
file_list="\n".join(data())
open('MANIFEST','w').write(file_list)
return files
py2exe_data_files = data_files()
execfile(join('openerp', 'release.py'))
setup(name = name,
setuptools.setup(
name = name,
version = version,
description = description,
long_description = long_desc,
@ -133,18 +81,10 @@ setup(name = name,
author_email = author_email,
classifiers = filter(None, classifiers.split("\n")),
license = license,
data_files = [
(join('man', 'man1'), ['man/openerp-server.1']),
(join('man', 'man5'), ['man/openerp_serverrc.5']),
('doc', filter(isfile, glob.glob('doc/*'))),
] + py2exe_data_files,
scripts = ['openerp-server'],
packages = find_packages(),
include_package_data = True,
package_data = {
'': ['*.yml', '*.xml', '*.po', '*.pot', '*.csv'],
},
dependency_links = ['http://download.gna.org/pychart/'],
data_files = data(),
packages = setuptools.find_packages(),
#include_package_data = True,
install_requires = [
# We require the same version as caldav for lxml.
'lxml==2.1.5',
@ -156,6 +96,7 @@ setup(name = name,
# It is probably safe to move to PyChart 1.39 (the latest one).
# (Let setup.py choose the latest one, and we should check we can remove pychart from
# our tree.)
# http://download.gna.org/pychart/
'pychart',
'pydot',
'pytz',

2
tests/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
import test_xmlrpc

85
tests/test_xmlrpc.py Normal file
View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# Run with one of these commands:
# > OPENERP_ADDONS_PATH='../../addons/trunk' OPENERP_PORT=8069 \
# OPENERP_DATABASE=yy PYTHONPATH=. python tests/test_xmlrpc.py
# > OPENERP_ADDONS_PATH='../../addons/trunk' OPENERP_PORT=8069 \
# OPENERP_DATABASE=yy nosetests tests/test_xmlrpc.py
# > OPENERP_ADDONS_PATH='../../../addons/trunk' OPENERP_PORT=8069 \
# OPENERP_DATABASE=yy PYTHONPATH=../:. unit2 test_xmlrpc
import os
import time
import unittest2
import xmlrpclib
import openerp
ADDONS_PATH = os.environ['OPENERP_ADDONS_PATH']
PORT = int(os.environ['OPENERP_PORT'])
DB = os.environ['OPENERP_DATABASE']
HOST = '127.0.0.1'
ADMIN_USER = 'admin'
ADMIN_USER_ID = 1
ADMIN_PASSWORD = 'admin'
common_proxy_60 = None
db_proxy_60 = None
object_proxy_60 = None
def setUpModule():
"""
Start the OpenERP server similary to the openerp-server script and
setup some xmlrpclib proxies.
"""
openerp.tools.config['addons_path'] = ADDONS_PATH
openerp.tools.config['xmlrpc_port'] = PORT
openerp.service.start_services()
global common_proxy_60
global db_proxy_60
global object_proxy_60
# Use the old (pre 6.1) API.
url = 'http://%s:%d/xmlrpc/' % (HOST, PORT)
common_proxy_60 = xmlrpclib.ServerProxy(url + 'common')
db_proxy_60 = xmlrpclib.ServerProxy(url + 'db')
object_proxy_60 = xmlrpclib.ServerProxy(url + 'object')
def tearDownModule():
""" Shutdown the OpenERP server similarly to a single ctrl-c. """
openerp.service.stop_services()
class test_xmlrpc(unittest2.TestCase):
def test_xmlrpc_create_database_polling(self):
"""
Simulate a OpenERP client requesting the creation of a database and
polling the server until the creation is complete.
"""
progress_id = db_proxy_60.create(ADMIN_PASSWORD, DB, True, False,
ADMIN_PASSWORD)
while True:
time.sleep(1)
progress, users = db_proxy_60.get_progress(ADMIN_PASSWORD,
progress_id)
if progress == 1.0:
break
def test_xmlrpc_login(self):
""" Try to login on the common service. """
uid = common_proxy_60.login(DB, ADMIN_USER, ADMIN_PASSWORD)
assert uid == ADMIN_USER_ID
def test_xmlrpc_ir_model_search(self):
""" Try a search on the object service. """
ids = object_proxy_60.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD,
'ir.model', 'search', [])
assert ids
ids = object_proxy_60.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD,
'ir.model', 'search', [], {})
assert ids
if __name__ == '__main__':
unittest2.main()