[MERGE] sync with latest trunk
bzr revid: odo@openerp.com-20110927165133-uwl7px6bxl6eu7us
12
MANIFEST.in
|
@ -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
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
@ -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...')
|
||||
|
|
|
@ -43,6 +43,7 @@ import tiny_socket
|
|||
import tools
|
||||
import wizard
|
||||
import workflow
|
||||
import wsgi
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']]
|
||||
|
|
|
@ -41,9 +41,9 @@ def toxml(value):
|
|||
return unicode_value.replace('&', '&').replace('<','<').replace('>','>')
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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']:
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
Before Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 139 KiB |
|
@ -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("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
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()
|
|
@ -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()
|
|
@ -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)
|
27
setup.README
|
@ -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
|
|
@ -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
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import test_xmlrpc
|
|
@ -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()
|
||||
|