[MERGE] merged trunk.
bzr revid: vmt@openerp.com-20110930151533-7mhnk5bbtwx0rjjq
This commit is contained in:
commit
106e8d48da
|
@ -89,15 +89,17 @@ def setup_pid_file():
|
|||
def preload_registry(dbname):
|
||||
""" Preload a registry, and start the cron."""
|
||||
try:
|
||||
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
|
||||
pool.get('ir.cron').restart(db.dbname)
|
||||
db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
|
||||
|
||||
# jobs will start to be processed later, when openerp.cron.start_master_thread() is called by openerp.service.start_services()
|
||||
registry.schedule_cron_jobs()
|
||||
except Exception:
|
||||
logging.exception('Failed to initialize database `%s`.', dbname)
|
||||
|
||||
def run_test_file(dbname, test_file):
|
||||
""" Preload a registry, possibly run a test file, and start the cron."""
|
||||
try:
|
||||
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
|
||||
db, registry = 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)
|
||||
|
|
|
@ -24,6 +24,7 @@ import module
|
|||
import res
|
||||
import publisher_warranty
|
||||
import report
|
||||
import test
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -92,6 +92,10 @@
|
|||
'test/test_osv_expression.yml',
|
||||
'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules.
|
||||
'test/test_ir_values.yml',
|
||||
# Commented because this takes some time.
|
||||
# This must be (un)commented with the corresponding import statement
|
||||
# in test/__init__.py.
|
||||
# 'test/test_ir_cron.yml', # <-- These tests perform a roolback.
|
||||
],
|
||||
'installable': True,
|
||||
'active': True,
|
||||
|
|
|
@ -290,6 +290,7 @@ CREATE TABLE ir_module_module (
|
|||
state character varying(16),
|
||||
latest_version character varying(64),
|
||||
shortdesc character varying(256),
|
||||
complexity character varying(32),
|
||||
category_id integer REFERENCES ir_module_category ON DELETE SET NULL,
|
||||
certificate character varying(64),
|
||||
description text,
|
||||
|
|
|
@ -1002,7 +1002,7 @@
|
|||
</record>
|
||||
|
||||
<record id="main_partner" model="res.partner">
|
||||
<field name="name">Company Name</field>
|
||||
<field name="name">Your Company</field>
|
||||
<!-- Address and Company ID will be set later -->
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="company_id" eval="None"/>
|
||||
|
@ -1010,14 +1010,7 @@
|
|||
</record>
|
||||
<record id="main_address" model="res.partner.address">
|
||||
<field name="partner_id" ref="main_partner"/>
|
||||
<field name="name">Company contact name</field>
|
||||
<field name="street">Company street, number</field>
|
||||
<field name="zip">Company zip</field>
|
||||
<field name="city">Company city</field>
|
||||
<field name="phone">+1-212-555-12345</field>
|
||||
<field name="type">default</field>
|
||||
<field model="res.country" name="country_id" ref="us"/>
|
||||
<!-- Company ID will be set later -->
|
||||
<field name="company_id" eval="None"/>
|
||||
</record>
|
||||
|
||||
|
@ -1038,7 +1031,7 @@
|
|||
|
||||
<!-- Basic Company -->
|
||||
<record id="main_company" model="res.company">
|
||||
<field name="name">Company Name</field>
|
||||
<field name="name">Your Company</field>
|
||||
<field name="partner_id" ref="main_partner"/>
|
||||
<field name="rml_header1">Company business slogan</field>
|
||||
<field name="rml_footer1">Web: www.companyname.com - Tel: +1-212-555-12345</field>
|
||||
|
|
|
@ -209,7 +209,7 @@
|
|||
<group colspan="4" col="6">
|
||||
<group colspan="4" col="4">
|
||||
<field name="name"/>
|
||||
<field name="partner_id" readonly="1" required="0"/>
|
||||
<field name="partner_id" readonly="1" required="0" groups="base.group_extended"/>
|
||||
<field name="parent_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group colspan="2" col="2">
|
||||
|
@ -225,18 +225,17 @@
|
|||
<field name="city"/>
|
||||
<field name="country_id"/>
|
||||
<field name="state_id"/>
|
||||
<field name="phone"/>
|
||||
<field name="email"/>
|
||||
<field name="fax"/>
|
||||
<field name="website"/>
|
||||
<field name="vat"/>
|
||||
<field name="company_registry"/>
|
||||
<field name="phone" on_change="on_change_header(phone, email, fax, website, vat, company_registry)"/>
|
||||
<field name="email" on_change="on_change_header(phone, email, fax, website, vat, company_registry)"/>
|
||||
<field name="fax" on_change="on_change_header(phone, email, fax, website, vat, company_registry)"/>
|
||||
<field name="website" on_change="on_change_header(phone, email, fax, website, vat, company_registry)"/>
|
||||
<field name="vat" on_change="on_change_header(phone, email, fax, website, vat, company_registry)"/>
|
||||
<field name="company_registry" on_change="on_change_header(phone, email, fax, website, vat, company_registry)"/>
|
||||
<separator string="Header/Footer of Reports" colspan="4"/>
|
||||
<group colspan="4" col="3">
|
||||
<field name="rml_header1" colspan="3"/>
|
||||
<newline/>
|
||||
<field name="rml_footer1" colspan="2"/>
|
||||
<button name="generate_header" string="Generate" type="object" icon="gtk-go-forward"/>
|
||||
<field name="rml_footer1" colspan="3" groups="base.group_extended"/>
|
||||
<newline/>
|
||||
<field name="rml_footer2" colspan="2"/>
|
||||
<button name="%(bank_account_update)d" string="Set Bank Accounts" type="action" icon="gtk-go-forward"/>
|
||||
|
@ -245,10 +244,10 @@
|
|||
<button name="%(preview_report)d" string="Preview Header" type="action" icon="gtk-print"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="External Template" groups="base.group_extended">
|
||||
<page string="Header/Footer" groups="base.group_extended">
|
||||
<field colspan="4" name="rml_header" nolabel="1"/>
|
||||
</page>
|
||||
<page string="Internal Template" groups="base.group_extended">
|
||||
<page string="Internal Header/Footer" groups="base.group_extended">
|
||||
<separator string="Portrait" colspan="2"/>
|
||||
<separator string="Landscape" colspan="2"/>
|
||||
<field colspan="2" name="rml_header2" nolabel="1"/>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: OpenERP Server 5.0.0\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2011-01-11 11:14+0000\n"
|
||||
"PO-Revision-Date: 2011-09-27 16:28+0000\n"
|
||||
"PO-Revision-Date: 2011-09-29 15:26+0000\n"
|
||||
"Last-Translator: Walter Cheuk <wwycheuk@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2011-09-28 05:19+0000\n"
|
||||
"X-Generator: Launchpad (build 14049)\n"
|
||||
"X-Launchpad-Export-Date: 2011-09-30 04:37+0000\n"
|
||||
"X-Generator: Launchpad (build 14071)\n"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.filters:0
|
||||
|
@ -2795,7 +2795,7 @@ msgstr "向用戶提供目標視窗之額外說明文字,如其用法及用途
|
|||
#. module: base
|
||||
#: model:res.country,name:base.va
|
||||
msgid "Holy See (Vatican City State)"
|
||||
msgstr "教廷(梵蒂岡)"
|
||||
msgstr "教廷 (梵蒂岡)"
|
||||
|
||||
#. module: base
|
||||
#: field:base.module.import,module_file:0
|
||||
|
@ -5754,7 +5754,7 @@ msgstr "一年中的星期:%(woy)s"
|
|||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_14
|
||||
msgid "Bad customers"
|
||||
msgstr "差客戶"
|
||||
msgstr "壞客戶"
|
||||
|
||||
#. module: base
|
||||
#: report:ir.module.reference.graph:0
|
||||
|
|
|
@ -21,13 +21,20 @@
|
|||
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
import psycopg2
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
import netsvc
|
||||
import tools
|
||||
from tools.safe_eval import safe_eval as eval
|
||||
import openerp
|
||||
import pooler
|
||||
import tools
|
||||
from openerp.cron import WAKE_UP_NOW
|
||||
from osv import fields, osv
|
||||
from tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from tools.safe_eval import safe_eval as eval
|
||||
from tools.translate import _
|
||||
|
||||
def str2tuple(s):
|
||||
return eval('tuple(%s)' % (s or ''))
|
||||
|
@ -41,10 +48,15 @@ _intervalTypes = {
|
|||
'minutes': lambda interval: relativedelta(minutes=interval),
|
||||
}
|
||||
|
||||
class ir_cron(osv.osv, netsvc.Agent):
|
||||
""" This is the ORM object that periodically executes actions.
|
||||
Note that we use the netsvc.Agent()._logger member.
|
||||
class ir_cron(osv.osv):
|
||||
""" Model describing cron jobs (also called actions or tasks).
|
||||
"""
|
||||
|
||||
# TODO: perhaps in the future we could consider a flag on ir.cron jobs
|
||||
# that would cause database wake-up even if the database has not been
|
||||
# loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something)
|
||||
# See also openerp.cron
|
||||
|
||||
_name = "ir.cron"
|
||||
_order = 'name'
|
||||
_columns = {
|
||||
|
@ -54,17 +66,17 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
'interval_number': fields.integer('Interval Number',help="Repeat every x."),
|
||||
'interval_type': fields.selection( [('minutes', 'Minutes'),
|
||||
('hours', 'Hours'), ('work_days','Work Days'), ('days', 'Days'),('weeks', 'Weeks'), ('months', 'Months')], 'Interval Unit'),
|
||||
'numbercall': fields.integer('Number of Calls', help='Number of time the function is called,\na negative number indicates no limit'),
|
||||
'doall' : fields.boolean('Repeat Missed', help="Enable this if you want to execute missed occurences as soon as the server restarts."),
|
||||
'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this scheduler"),
|
||||
'model': fields.char('Object', size=64, help="Name of object whose function will be called when this scheduler will run. e.g. 'res.partener'"),
|
||||
'function': fields.char('Function', size=64, help="Name of the method to be called on the object when this scheduler is executed."),
|
||||
'args': fields.text('Arguments', help="Arguments to be passed to the method. e.g. (uid,)"),
|
||||
'priority': fields.integer('Priority', help='0=Very Urgent\n10=Not urgent')
|
||||
'numbercall': fields.integer('Number of Calls', help='How many times the method is called,\na negative number indicates no limit.'),
|
||||
'doall' : fields.boolean('Repeat Missed', help="Specify if missed occurrences should be executed when the server restarts."),
|
||||
'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this job."),
|
||||
'model': fields.char('Object', size=64, help="Model name on which the method to be called is located, e.g. 'res.partner'."),
|
||||
'function': fields.char('Method', size=64, help="Name of the method to be called when this job is processed."),
|
||||
'args': fields.text('Arguments', help="Arguments to be passed to the method, e.g. (uid,)."),
|
||||
'priority': fields.integer('Priority', help='The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.')
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'nextcall' : lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'nextcall' : lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
'priority' : lambda *a: 5,
|
||||
'user_id' : lambda obj,cr,uid,context: uid,
|
||||
'interval_number' : lambda *a: 1,
|
||||
|
@ -74,6 +86,8 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
'doall' : lambda *a: 1
|
||||
}
|
||||
|
||||
_logger = logging.getLogger('cron')
|
||||
|
||||
def _check_args(self, cr, uid, ids, context=None):
|
||||
try:
|
||||
for this in self.browse(cr, uid, ids, context):
|
||||
|
@ -86,68 +100,164 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
(_check_args, 'Invalid arguments', ['args']),
|
||||
]
|
||||
|
||||
def _handle_callback_exception(self, cr, uid, model, func, args, job_id, job_exception):
|
||||
cr.rollback()
|
||||
logger=logging.getLogger('cron')
|
||||
logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model, func, args, job_id))
|
||||
def _handle_callback_exception(self, cr, uid, model_name, method_name, args, job_id, job_exception):
|
||||
""" Method called when an exception is raised by a job.
|
||||
|
||||
def _callback(self, cr, uid, model, func, args, job_id):
|
||||
Simply logs the exception and rollback the transaction.
|
||||
|
||||
:param model_name: model name on which the job method is located.
|
||||
:param method_name: name of the method to call when this job is processed.
|
||||
:param args: arguments of the method (without the usual self, cr, uid).
|
||||
:param job_id: job id.
|
||||
:param job_exception: exception raised by the job.
|
||||
|
||||
"""
|
||||
cr.rollback()
|
||||
self._logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model_name, method_name, args, job_id))
|
||||
|
||||
def _callback(self, cr, uid, model_name, method_name, args, job_id):
|
||||
""" Run the method associated to a given job
|
||||
|
||||
It takes care of logging and exception handling.
|
||||
|
||||
:param model_name: model name on which the job method is located.
|
||||
:param method_name: name of the method to call when this job is processed.
|
||||
:param args: arguments of the method (without the usual self, cr, uid).
|
||||
:param job_id: job id.
|
||||
"""
|
||||
args = str2tuple(args)
|
||||
m = self.pool.get(model)
|
||||
if m and hasattr(m, func):
|
||||
f = getattr(m, func)
|
||||
model = self.pool.get(model_name)
|
||||
if model and hasattr(model, method_name):
|
||||
method = getattr(model, method_name)
|
||||
try:
|
||||
netsvc.log('cron', (cr.dbname,uid,'*',model,func)+tuple(args), channel=logging.DEBUG,
|
||||
netsvc.log('cron', (cr.dbname,uid,'*',model_name,method_name)+tuple(args), channel=logging.DEBUG,
|
||||
depth=(None if self._logger.isEnabledFor(logging.DEBUG_RPC_ANSWER) else 1), fn='object.execute')
|
||||
logger = logging.getLogger('execution time')
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
start_time = time.time()
|
||||
f(cr, uid, *args)
|
||||
method(cr, uid, *args)
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
end_time = time.time()
|
||||
logger.log(logging.DEBUG, '%.3fs (%s, %s)' % (end_time - start_time, model, func))
|
||||
logger.log(logging.DEBUG, '%.3fs (%s, %s)' % (end_time - start_time, model_name, method_name))
|
||||
except Exception, e:
|
||||
self._handle_callback_exception(cr, uid, model, func, args, job_id, e)
|
||||
self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
|
||||
|
||||
def _poolJobs(self, db_name, check=False):
|
||||
def _run_job(self, cr, job, now):
|
||||
""" Run a given job taking care of the repetition.
|
||||
|
||||
The cursor has a lock on the job (aquired by _run_jobs_multithread()) and this
|
||||
method is run in a worker thread (spawned by _run_jobs_multithread())).
|
||||
|
||||
:param job: job to be run (as a dictionary).
|
||||
:param now: timestamp (result of datetime.now(), no need to call it multiple time).
|
||||
|
||||
"""
|
||||
try:
|
||||
db, pool = pooler.get_db_and_pool(db_name)
|
||||
except:
|
||||
return False
|
||||
nextcall = datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
numbercall = job['numbercall']
|
||||
|
||||
ok = False
|
||||
while nextcall < now and numbercall:
|
||||
if numbercall > 0:
|
||||
numbercall -= 1
|
||||
if not ok or job['doall']:
|
||||
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
|
||||
if numbercall:
|
||||
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
|
||||
ok = True
|
||||
addsql = ''
|
||||
if not numbercall:
|
||||
addsql = ', active=False'
|
||||
cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
|
||||
(nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
|
||||
|
||||
if numbercall:
|
||||
# Reschedule our own main cron thread if necessary.
|
||||
# This is really needed if this job runs longer than its rescheduling period.
|
||||
nextcall = time.mktime(nextcall.timetuple())
|
||||
openerp.cron.schedule_wakeup(nextcall, cr.dbname)
|
||||
finally:
|
||||
cr.commit()
|
||||
cr.close()
|
||||
openerp.cron.release_thread_slot()
|
||||
|
||||
def _run_jobs_multithread(self):
|
||||
# TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py
|
||||
""" Process the cron jobs by spawning worker threads.
|
||||
|
||||
This selects in database all the jobs that should be processed. It then
|
||||
tries to lock each of them and, if it succeeds, spawns a thread to run
|
||||
the cron job (if it doesn't succeed, it means the job was already
|
||||
locked to be taken care of by another thread).
|
||||
|
||||
The cursor used to lock the job in database is given to the worker
|
||||
thread (which has to close it itself).
|
||||
|
||||
"""
|
||||
db = self.pool.db
|
||||
cr = db.cursor()
|
||||
db_name = db.dbname
|
||||
try:
|
||||
if not pool._init:
|
||||
now = datetime.now()
|
||||
cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority')
|
||||
for job in cr.dictfetchall():
|
||||
nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S')
|
||||
numbercall = job['numbercall']
|
||||
jobs = {} # mapping job ids to jobs for all jobs being processed.
|
||||
now = datetime.now()
|
||||
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
|
||||
cr.execute("""SELECT * FROM ir_cron
|
||||
WHERE numbercall != 0
|
||||
AND active AND nextcall <= (now() at time zone 'UTC')
|
||||
ORDER BY priority""")
|
||||
for job in cr.dictfetchall():
|
||||
if not openerp.cron.get_thread_slots():
|
||||
break
|
||||
jobs[job['id']] = job
|
||||
|
||||
ok = False
|
||||
while nextcall < now and numbercall:
|
||||
if numbercall > 0:
|
||||
numbercall -= 1
|
||||
if not ok or job['doall']:
|
||||
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
|
||||
if numbercall:
|
||||
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
|
||||
ok = True
|
||||
addsql = ''
|
||||
if not numbercall:
|
||||
addsql = ', active=False'
|
||||
cr.execute("update ir_cron set nextcall=%s, numbercall=%s"+addsql+" where id=%s", (nextcall.strftime('%Y-%m-%d %H:%M:%S'), numbercall, job['id']))
|
||||
cr.commit()
|
||||
task_cr = db.cursor()
|
||||
try:
|
||||
# Try to grab an exclusive lock on the job row from within the task transaction
|
||||
acquired_lock = False
|
||||
task_cr.execute("""SELECT *
|
||||
FROM ir_cron
|
||||
WHERE id=%s
|
||||
FOR UPDATE NOWAIT""",
|
||||
(job['id'],), log_exceptions=False)
|
||||
acquired_lock = True
|
||||
except psycopg2.OperationalError, e:
|
||||
if e.pgcode == '55P03':
|
||||
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
|
||||
self._logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['name'])
|
||||
continue
|
||||
else:
|
||||
# Unexpected OperationalError
|
||||
raise
|
||||
finally:
|
||||
if not acquired_lock:
|
||||
# we're exiting due to an exception while acquiring the lot
|
||||
task_cr.close()
|
||||
|
||||
# Got the lock on the job row, now spawn a thread to execute it in the transaction with the lock
|
||||
task_thread = threading.Thread(target=self._run_job, name=job['name'], args=(task_cr, job, now))
|
||||
# force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
|
||||
task_thread.setDaemon(False)
|
||||
openerp.cron.take_thread_slot()
|
||||
task_thread.start()
|
||||
self._logger.debug('Cron execution thread for job `%s` spawned', job['name'])
|
||||
|
||||
cr.execute('select min(nextcall) as min_next_call from ir_cron where numbercall<>0 and active')
|
||||
next_call = cr.dictfetchone()['min_next_call']
|
||||
if next_call:
|
||||
next_call = time.mktime(time.strptime(next_call, '%Y-%m-%d %H:%M:%S'))
|
||||
# Find next earliest job ignoring currently processed jobs (by this and other cron threads)
|
||||
find_next_time_query = """SELECT min(nextcall) AS min_next_call
|
||||
FROM ir_cron WHERE numbercall != 0 AND active"""
|
||||
if jobs:
|
||||
cr.execute(find_next_time_query + " AND id NOT IN %s", (tuple(jobs.keys()),))
|
||||
else:
|
||||
next_call = int(time.time()) + 3600 # if do not find active cron job from database, it will run again after 1 day
|
||||
cr.execute(find_next_time_query)
|
||||
next_call = cr.dictfetchone()['min_next_call']
|
||||
|
||||
if not check:
|
||||
self.setAlarm(self._poolJobs, next_call, db_name, db_name)
|
||||
if next_call:
|
||||
next_call = time.mktime(time.strptime(next_call, DEFAULT_SERVER_DATETIME_FORMAT))
|
||||
else:
|
||||
# no matching cron job found in database, re-schedule arbitrarily in 1 day,
|
||||
# this delay will likely be modified when running jobs complete their tasks
|
||||
next_call = time.time() + (24*3600)
|
||||
|
||||
openerp.cron.schedule_wakeup(next_call, db_name)
|
||||
|
||||
except Exception, ex:
|
||||
self._logger.warning('Exception in cron:', exc_info=True)
|
||||
|
@ -156,12 +266,8 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
cr.commit()
|
||||
cr.close()
|
||||
|
||||
def restart(self, dbname):
|
||||
self.cancel(dbname)
|
||||
# Reschedule cron processing job asap, but not in the current thread
|
||||
self.setAlarm(self._poolJobs, time.time(), dbname, dbname)
|
||||
|
||||
def update_running_cron(self, cr):
|
||||
""" Schedule as soon as possible a wake-up for this database. """
|
||||
# Verify whether the server is already started and thus whether we need to commit
|
||||
# immediately our changes and restart the cron agent in order to apply the change
|
||||
# immediately. The commit() is needed because as soon as the cron is (re)started it
|
||||
|
@ -171,23 +277,37 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
# when the server is only starting or loading modules (hence the test on pool._init).
|
||||
if not self.pool._init:
|
||||
cr.commit()
|
||||
self.restart(cr.dbname)
|
||||
openerp.cron.schedule_wakeup(WAKE_UP_NOW, self.pool.db.dbname)
|
||||
|
||||
def _try_lock(self, cr, uid, ids, context=None):
|
||||
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
|
||||
to make sure a following write() or unlink() will not block due
|
||||
to a process currently executing those cron tasks"""
|
||||
try:
|
||||
cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
|
||||
(tuple(ids),), log_exceptions=False)
|
||||
except psycopg2.OperationalError:
|
||||
cr.rollback() # early rollback to allow translations to work for the user feedback
|
||||
raise osv.except_osv(_("Record cannot be modified right now"),
|
||||
_("This cron task is currently being executed and may not be modified, "
|
||||
"please try again in a few minutes"))
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
res = super(ir_cron, self).create(cr, uid, vals, context=context)
|
||||
self.update_running_cron(cr)
|
||||
return res
|
||||
|
||||
def write(self, cr, user, ids, vals, context=None):
|
||||
res = super(ir_cron, self).write(cr, user, ids, vals, context=context)
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
self._try_lock(cr, uid, ids, context)
|
||||
res = super(ir_cron, self).write(cr, uid, ids, vals, context=context)
|
||||
self.update_running_cron(cr)
|
||||
return res
|
||||
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
self._try_lock(cr, uid, ids, context)
|
||||
res = super(ir_cron, self).unlink(cr, uid, ids, context=context)
|
||||
self.update_running_cron(cr)
|
||||
return res
|
||||
ir_cron()
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -88,6 +88,18 @@ class res_company(osv.osv):
|
|||
result[company.id][field] = address[field] or False
|
||||
return result
|
||||
|
||||
|
||||
def _get_bank_data(self, cr, uid, ids, field_names, arg, context=None):
|
||||
""" Read the 'address' functional fields. """
|
||||
result = {}
|
||||
for company in self.browse(cr, uid, ids, context=context):
|
||||
r = []
|
||||
for bank in company.bank_ids:
|
||||
if bank.footer:
|
||||
r.append(bank.name_get(context=context)[0][1])
|
||||
result[company.id] = ' | '.join(r)
|
||||
return result
|
||||
|
||||
def _set_address_data(self, cr, uid, company_id, name, value, arg, context=None):
|
||||
""" Write the 'address' functional fields. """
|
||||
company = self.browse(cr, uid, company_id, context=context)
|
||||
|
@ -102,19 +114,9 @@ class res_company(osv.osv):
|
|||
address_obj.create(cr, uid, {name: value or False, 'partner_id': company.partner_id.id}, context=context)
|
||||
return True
|
||||
|
||||
def _get_bank_data(self, cr, uid, ids, field_names, arg, context=None):
|
||||
""" Read the 'address' functional fields. """
|
||||
result = {}
|
||||
for company in self.browse(cr, uid, ids, context=context):
|
||||
r = []
|
||||
for bank in company.bank_ids:
|
||||
if bank.footer:
|
||||
r.append(bank.name_get(context=context)[0][1])
|
||||
result[company.id] = ' | '.join(r)
|
||||
return result
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Company Name', size=64, required=True),
|
||||
'name': fields.related('partner_id', 'name', string='Company Name', size=64, required=True, store=True, type='char'),
|
||||
'parent_id': fields.many2one('res.company', 'Parent Company', select=True),
|
||||
'child_ids': fields.one2many('res.company', 'parent_id', 'Child Companies'),
|
||||
'partner_id': fields.many2one('res.partner', 'Partner', required=True),
|
||||
|
@ -146,6 +148,15 @@ class res_company(osv.osv):
|
|||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', 'The company name must be unique !')
|
||||
]
|
||||
def on_change_header(self, cr, uid, ids, phone, email, fax, website, vat, reg=False, context={}):
|
||||
val = []
|
||||
if phone: val.append(_('Phone: ')+phone)
|
||||
if fax: val.append(_('Fax: ')+fax)
|
||||
if website: val.append(_('Website: ')+website)
|
||||
if vat: val.append(_('VAT: ')+vat)
|
||||
if reg: val.append(_('Reg: ')+reg)
|
||||
return {'value': {'rml_footer1':' | '.join(val)}}
|
||||
|
||||
|
||||
def _search(self, cr, uid, args, offset=0, limit=None, order=None,
|
||||
context=None, count=False, access_rights_uid=None):
|
||||
|
@ -228,16 +239,6 @@ class res_company(osv.osv):
|
|||
self.cache_restart(cr)
|
||||
return super(res_company, self).write(cr, *args, **argv)
|
||||
|
||||
def generate_header(self, cr, uid, ids, context=None):
|
||||
for c in self.browse(cr, uid, ids, context=context):
|
||||
val = []
|
||||
if c.phone: val.append(_('Phone: ')+c.phone)
|
||||
if c.fax: val.append(_('Fax: ')+c.fax)
|
||||
if c.website: val.append(_('Website: ')+c.website)
|
||||
if c.vat: val.append(_('VAT: ')+c.vat)
|
||||
if c.company_registry: val.append(_('Reg: ')+c.company_registry)
|
||||
self.write(cr,uid, [c.id], {'rml_footer1':' | '.join(val)}, context)
|
||||
|
||||
def _get_euro(self, cr, uid, context={}):
|
||||
try:
|
||||
return self.pool.get('res.currency').search(cr, uid, [])[0]
|
||||
|
|
|
@ -131,12 +131,15 @@ class res_partner(osv.osv):
|
|||
'customer': fields.boolean('Customer', help="Check this box if the partner is a customer."),
|
||||
'supplier': fields.boolean('Supplier', help="Check this box if the partner is a supplier. If it's not checked, purchase people will not see it when encoding a purchase order."),
|
||||
'city': fields.related('address', 'city', type='char', string='City'),
|
||||
'function': fields.related('address', 'function', type='char', string='function'),
|
||||
'subname': fields.related('address', 'name', type='char', string='Contact Name'),
|
||||
'phone': fields.related('address', 'phone', type='char', string='Phone'),
|
||||
'mobile': fields.related('address', 'mobile', type='char', string='Mobile'),
|
||||
'country': fields.related('address', 'country_id', type='many2one', relation='res.country', string='Country'),
|
||||
'employee': fields.boolean('Employee', help="Check this box if the partner is an Employee."),
|
||||
'email': fields.related('address', 'email', type='char', size=240, string='E-mail'),
|
||||
'company_id': fields.many2one('res.company', 'Company', select=1),
|
||||
'color': fields.integer('Color Index'),
|
||||
}
|
||||
|
||||
def _default_category(self, cr, uid, context={}):
|
||||
|
@ -150,6 +153,7 @@ class res_partner(osv.osv):
|
|||
'address': [{'type': 'default'}],
|
||||
'category_id': _default_category,
|
||||
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'res.partner', context=c),
|
||||
'color': 0,
|
||||
}
|
||||
|
||||
def copy(self, cr, uid, id, default={}, context={}):
|
||||
|
@ -300,6 +304,7 @@ class res_partner_address(osv.osv):
|
|||
'active': fields.boolean('Active', help="Uncheck the active field to hide the contact."),
|
||||
# 'company_id': fields.related('partner_id','company_id',type='many2one',relation='res.company',string='Company', store=True),
|
||||
'company_id': fields.many2one('res.company', 'Company',select=1),
|
||||
'color': fields.integer('Color Index'),
|
||||
}
|
||||
_defaults = {
|
||||
'active': lambda *a: 1,
|
||||
|
|
|
@ -105,11 +105,80 @@
|
|||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="contacts_kanban_view">
|
||||
<field name="name">res.partner.address.kanban</field>
|
||||
<field name="model">res.partner.address</field>
|
||||
<field name="type">kanban</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban >
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<t t-set="color" t-value="kanban_color(record.color.raw_value || record.name.raw_value)"/>
|
||||
<div t-att-class="color + (record.color.raw_value == 1 ? ' oe_kanban_color_alert' : '')">
|
||||
<div class="oe_kanban_box oe_kanban_color_border">
|
||||
<div class="oe_kanban_box_header oe_kanban_color_bgdark oe_kanban_color_border oe_kanban_draghandle">
|
||||
<table class="oe_kanban_table">
|
||||
<tr>
|
||||
<td class="oe_kanban_title1" align="left" valign="middle">
|
||||
<field name="name"/>
|
||||
</td>
|
||||
<td valign="top" width="22">
|
||||
<img t-att-src="kanban_gravatar(record.email.value, 22)" class="oe_kanban_gravatar"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="oe_kanban_box_content oe_kanban_color_bglight oe_kanban_box_show_onclick_trigger oe_kanban_color_border">
|
||||
<table class="oe_kanban_table">
|
||||
<tr>
|
||||
<td valign="top" width="22" align="left">
|
||||
<img src="/web/static/src/img/persons.png"/>
|
||||
</td>
|
||||
<td valign="top" align="left">
|
||||
<div class="oe_kanban_title2">
|
||||
<field name="title"/>
|
||||
<t t-if="record.title.raw_value && record.function.raw_value">,</t>
|
||||
<field name="function"/>
|
||||
</div>
|
||||
<div class="oe_kanban_title3">
|
||||
<field name="partner_id"/>
|
||||
<t t-if="record.partner_id.raw_value && record.country_id.raw_value">,</t>
|
||||
<field name="country_id"/>
|
||||
</div>
|
||||
<div class="oe_kanban_title3">
|
||||
<i><field name="email"/>
|
||||
<t t-if="record.phone.raw_value && record.email.raw_value">,</t>
|
||||
<field name="phone"/></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="oe_kanban_buttons_set oe_kanban_color_border oe_kanban_color_bglight oe_kanban_box_show_onclick">
|
||||
<div class="oe_kanban_left">
|
||||
<a string="Edit" icon="gtk-edit" type="edit"/>
|
||||
<a string="Change Color" icon="color-picker" type="color" name="color"/>
|
||||
<a title="Mail" t-att-href="'mailto:'+record.email.value" style="text-decoration: none;" >
|
||||
<img src="/web/static/src/img/icons/terp-mail-message-new.png" border="0" width="16" height="16"/>
|
||||
</a>
|
||||
</div>
|
||||
<br class="oe_kanban_clear"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_partner_address_form" model="ir.actions.act_window">
|
||||
<field name="name">Addresses</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.partner.address</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
<field name="context">{"search_default_customer":1}</field>
|
||||
<field name="search_view_id" ref="view_res_partner_address_filter"/>
|
||||
<field name="help">Customers (also called Partners in other areas of the system) helps you manage your address book of companies whether they are prospects, customers and/or suppliers. The partner form allows you to track and record all the necessary information to interact with your partners from the company address to their contacts as well as pricelists, and much more. If you installed the CRM, with the history tab, you can track all the interactions with a partner such as opportunities, emails, or sales orders issued.</field>
|
||||
|
@ -353,12 +422,81 @@
|
|||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Partner Kanban View -->
|
||||
<record model="ir.ui.view" id="res_partner_kanban_view">
|
||||
<field name="name">RES - PARTNER KANBAN</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="type">kanban</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<t t-set="color" t-value="kanban_color(record.color.raw_value || record.name.raw_value)"/>
|
||||
<div t-att-class="color + (record.color.raw_value == 1 ? ' oe_kanban_color_alert' : '')">
|
||||
<div class="oe_kanban_box oe_kanban_color_border">
|
||||
<div class="oe_kanban_box_header oe_kanban_color_bgdark oe_kanban_color_border oe_kanban_draghandle">
|
||||
<table class="oe_kanban_table">
|
||||
<tr>
|
||||
<td class="oe_kanban_title1" align="left" valign="middle">
|
||||
<field name="name"/>
|
||||
</td>
|
||||
<td valign="top" width="22">
|
||||
<img t-att-src="kanban_gravatar(record.email.value, 22)" class="oe_kanban_gravatar"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="oe_kanban_box_content oe_kanban_color_bglight oe_kanban_box_show_onclick_trigger oe_kanban_color_border">
|
||||
<table class="oe_kanban_table">
|
||||
<tr>
|
||||
<td valign="top" width="22" align="left">
|
||||
<img src="/web/static/src/img/partner.png"/>
|
||||
</td>
|
||||
<td valign="top" align="left">
|
||||
<div class="oe_kanban_title2">
|
||||
<field name="title"/>
|
||||
<t t-if="record.title.raw_value && record.country.raw_value">,</t>
|
||||
<field name="country"/>
|
||||
</div>
|
||||
<div class="oe_kanban_title3">
|
||||
<field name="subname"/>
|
||||
<t t-if="record.subname.raw_value && record.function.raw_value">,</t>
|
||||
<field name="function"/>
|
||||
</div>
|
||||
<div class="oe_kanban_title3">
|
||||
<i><field name="email"/>
|
||||
<t t-if="record.phone.raw_value && record.email.raw_value">,</t>
|
||||
<field name="phone"/></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="oe_kanban_buttons_set oe_kanban_color_border oe_kanban_color_bglight oe_kanban_box_show_onclick">
|
||||
<div class="oe_kanban_left">
|
||||
<a string="Edit" icon="gtk-edit" type="edit"/>
|
||||
<a string="Change Color" icon="color-picker" type="color" name="color"/>
|
||||
<a title="Mail" t-att-href="'mailto:'+record.email.value" style="text-decoration: none;" >
|
||||
<img src="/web/static/src/img/icons/terp-mail-message-new.png" border="0" width="16" height="16"/>
|
||||
</a>
|
||||
</div>
|
||||
<br class="oe_kanban_clear"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_partner_form" model="ir.actions.act_window">
|
||||
<field name="name">Customers</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.partner</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="context">{"search_default_customer":1}</field>
|
||||
<field name="search_view_id" ref="view_res_partner_filter"/>
|
||||
<field name="help">A customer is an entity you do business with, like a company or an organization. A customer can have several contacts or addresses which are the people working for this company. You can use the history tab, to follow all transactions related to a customer: sales order, emails, opportunities, claims, etc. If you use the email gateway, the Outlook or the Thunderbird plugin, don't forget to register emails to each contact so that the gateway will automatically attach incoming emails to the right partner.</field>
|
||||
|
|
|
@ -35,6 +35,7 @@ from osv import fields,osv
|
|||
from osv.orm import browse_record
|
||||
from service import security
|
||||
from tools.translate import _
|
||||
import openerp.exceptions
|
||||
|
||||
class groups(osv.osv):
|
||||
_name = "res.groups"
|
||||
|
@ -437,14 +438,14 @@ class users(osv.osv):
|
|||
if passwd == tools.config['admin_passwd']:
|
||||
return True
|
||||
else:
|
||||
raise security.ExceptionNoTb('AccessDenied')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
|
||||
def check(self, db, uid, passwd):
|
||||
"""Verifies that the given (uid, password) pair is authorized for the database ``db`` and
|
||||
raise an exception if it is not."""
|
||||
if not passwd:
|
||||
# empty passwords disallowed for obvious security reasons
|
||||
raise security.ExceptionNoTb('AccessDenied')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
if self._uid_cache.get(db, {}).get(uid) == passwd:
|
||||
return
|
||||
cr = pooler.get_db(db).cursor()
|
||||
|
@ -453,7 +454,7 @@ class users(osv.osv):
|
|||
(int(uid), passwd, True))
|
||||
res = cr.fetchone()[0]
|
||||
if not res:
|
||||
raise security.ExceptionNoTb('AccessDenied')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
if self._uid_cache.has_key(db):
|
||||
ulist = self._uid_cache[db]
|
||||
ulist[uid] = passwd
|
||||
|
@ -470,7 +471,7 @@ class users(osv.osv):
|
|||
cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
|
||||
res = cr.fetchone()
|
||||
if not res:
|
||||
raise security.ExceptionNoTb('Bad username or password')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
return res[0]
|
||||
finally:
|
||||
cr.close()
|
||||
|
@ -481,7 +482,7 @@ class users(osv.osv):
|
|||
password is not used to authenticate requests.
|
||||
|
||||
:return: True
|
||||
:raise: security.ExceptionNoTb when old password is wrong
|
||||
:raise: openerp.exceptions.AccessDenied when old password is wrong
|
||||
:raise: except_osv when new password is not set or empty
|
||||
"""
|
||||
self.check(cr.dbname, uid, old_passwd)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2011-TODAY OpenERP S.A. <http://www.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/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Useful for manual testing of cron jobs scheduling.
|
||||
# This must be (un)commented with the corresponding yml file
|
||||
# in ../__openerp__.py.
|
||||
# import test_ir_cron
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,116 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2011-TODAY OpenERP S.A. <http://www.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/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
import openerp
|
||||
|
||||
JOB = {
|
||||
'function': u'_0_seconds',
|
||||
'interval_type': u'minutes',
|
||||
'user_id': 1,
|
||||
'name': u'test',
|
||||
'args': False,
|
||||
'numbercall': 1,
|
||||
'nextcall': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'priority': 5,
|
||||
'doall': True,
|
||||
'active': True,
|
||||
'interval_number': 1,
|
||||
'model': u'ir.cron'
|
||||
}
|
||||
|
||||
class test_ir_cron(openerp.osv.osv.osv):
|
||||
""" Add a few handy methods to test cron jobs scheduling. """
|
||||
_inherit = "ir.cron"
|
||||
|
||||
def _0_seconds(a, b, c):
|
||||
print ">>> _0_seconds"
|
||||
|
||||
def _20_seconds(self, cr, uid):
|
||||
print ">>> in _20_seconds"
|
||||
time.sleep(20)
|
||||
print ">>> out _20_seconds"
|
||||
|
||||
def _80_seconds(self, cr, uid):
|
||||
print ">>> in _80_seconds"
|
||||
time.sleep(80)
|
||||
print ">>> out _80_seconds"
|
||||
|
||||
def test_0(self, cr, uid):
|
||||
now = datetime.now()
|
||||
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.create(cr, uid, dict(JOB, name='test_0 _20_seconds A', function='_20_seconds', nextcall=t1))
|
||||
self.create(cr, uid, dict(JOB, name='test_0 _20_seconds B', function='_20_seconds', nextcall=t2))
|
||||
self.create(cr, uid, dict(JOB, name='test_0 _20_seconds C', function='_20_seconds', nextcall=t3))
|
||||
|
||||
def test_1(self, cr, uid):
|
||||
now = datetime.now()
|
||||
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.create(cr, uid, dict(JOB, name='test_1 _20_seconds * 3', function='_20_seconds', nextcall=t1, numbercall=3))
|
||||
|
||||
def test_2(self, cr, uid):
|
||||
now = datetime.now()
|
||||
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.create(cr, uid, dict(JOB, name='test_2 _80_seconds * 2', function='_80_seconds', nextcall=t1, numbercall=2))
|
||||
|
||||
def test_3(self, cr, uid):
|
||||
now = datetime.now()
|
||||
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.create(cr, uid, dict(JOB, name='test_3 _80_seconds A', function='_80_seconds', nextcall=t1))
|
||||
self.create(cr, uid, dict(JOB, name='test_3 _20_seconds B', function='_20_seconds', nextcall=t2))
|
||||
self.create(cr, uid, dict(JOB, name='test_3 _20_seconds C', function='_20_seconds', nextcall=t3))
|
||||
|
||||
# This test assumes 4 cron threads.
|
||||
def test_00(self, cr, uid):
|
||||
self.test_00_set = set()
|
||||
now = datetime.now()
|
||||
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.create(cr, uid, dict(JOB, name='test_00 _20_seconds_A', function='_20_seconds_A', nextcall=t1))
|
||||
self.create(cr, uid, dict(JOB, name='test_00 _20_seconds_B', function='_20_seconds_B', nextcall=t2))
|
||||
self.create(cr, uid, dict(JOB, name='test_00 _20_seconds_C', function='_20_seconds_C', nextcall=t3))
|
||||
|
||||
def _expect(self, cr, uid, to_add, to_sleep, to_expect_in, to_expect_out):
|
||||
assert self.test_00_set == to_expect_in
|
||||
self.test_00_set.add(to_add)
|
||||
time.sleep(to_sleep)
|
||||
self.test_00_set.discard(to_add)
|
||||
assert self.test_00_set == to_expect_out
|
||||
|
||||
def _20_seconds_A(self, cr, uid):
|
||||
self._expect(cr, uid, 'A', 20, set(), set(['B', 'C']))
|
||||
|
||||
def _20_seconds_B(self, cr, uid):
|
||||
self._expect(cr, uid, 'B', 20, set('A'), set('C'))
|
||||
|
||||
def _20_seconds_C(self, cr, uid):
|
||||
self._expect(cr, uid, 'C', 20, set(['A', 'B']), set())
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
-
|
||||
Test the cron jobs scheduling.
|
||||
-
|
||||
Disable the existing cron jobs if any during the tests.
|
||||
-
|
||||
!python {model: ir.cron }: |
|
||||
# For this test to work, as it involves multiple database cursors,
|
||||
# we have to commit changes. But YAML tests must be rollbacked, so
|
||||
# the final database state is left untouched. So we have to be a bit
|
||||
# ugly here: use our own cursor, commit, and clean after ourselves.
|
||||
# We also pass around some ids using setattr/delattr, and we have to
|
||||
# rollback the previous tests otherwise we won't be able to touch the
|
||||
# db.
|
||||
# Well, this should probably be a standalone, or regular unit test,
|
||||
# instead of using the YAML infrastructure.
|
||||
cr.rollback()
|
||||
our_cr = self.pool.db.cursor()
|
||||
try:
|
||||
ids = self.search(our_cr, uid, [], {})
|
||||
setattr(self, 'saved_ids', ids)
|
||||
self.write(our_cr, uid, ids, {'active': False}, {})
|
||||
our_cr.commit()
|
||||
finally:
|
||||
our_cr.close()
|
||||
-
|
||||
Three concurrent jobs started with a slight time gap. Assume 4 cron threads.
|
||||
This will take about 2 minutes.
|
||||
-
|
||||
!python {model: ir.cron }: |
|
||||
# Pretend initialization is already done. We the use a try/finally
|
||||
# to reset _init correctly.
|
||||
self.pool._init = False
|
||||
our_cr = self.pool.db.cursor()
|
||||
try:
|
||||
self.test_00(our_cr, uid) # this will commit using the passed cursor
|
||||
import openerp.cron
|
||||
openerp.cron._thread_slots = 4
|
||||
# Wake up this db as soon as the master cron thread starts.
|
||||
openerp.cron.schedule_wakeup(1, self.pool.db.dbname)
|
||||
# Pretend to be the master thread, for 4 iterations.
|
||||
openerp.cron.runner_body()
|
||||
openerp.cron.runner_body()
|
||||
openerp.cron.runner_body()
|
||||
openerp.cron.runner_body()
|
||||
finally:
|
||||
self.pool._init = True
|
||||
our_cr.close()
|
||||
-
|
||||
Clean after ourselves.
|
||||
-
|
||||
!python {model: ir.cron }: |
|
||||
our_cr = self.pool.db.cursor()
|
||||
try:
|
||||
ids = [x for x in self.search(our_cr, uid, ['|', ('active', '=', True), ('active', '=', False)], {}) if x not in self.saved_ids]
|
||||
self.unlink(our_cr, uid, ids, {})
|
||||
ids = self.saved_ids
|
||||
delattr(self, 'saved_ids')
|
||||
self.write(our_cr, uid, ids, {'active': True}, {})
|
||||
our_cr.commit()
|
||||
finally:
|
||||
our_cr.close()
|
|
@ -35,6 +35,10 @@ must be used.
|
|||
|
||||
import deprecation
|
||||
|
||||
# Maximum number of threads processing concurrently cron jobs.
|
||||
max_cron_threads = 4 # Actually the default value here is meaningless,
|
||||
# look at tools.config for the default value.
|
||||
|
||||
# Paths to search for OpenERP addons.
|
||||
addons_paths = []
|
||||
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP SA (<http://www.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/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
""" Cron jobs scheduling
|
||||
|
||||
Cron jobs are defined in the ir_cron table/model. This module deals with all
|
||||
cron jobs, for all databases of a single OpenERP server instance.
|
||||
|
||||
It defines a single master thread that will spawn (a bounded number of)
|
||||
threads to process individual cron jobs.
|
||||
|
||||
The thread runs forever, checking every 60 seconds for new
|
||||
'database wake-ups'. It maintains a heapq of database wake-ups. At each
|
||||
wake-up, it will call ir_cron._run_jobs_multithread() for the given database. _run_jobs_multithread
|
||||
will check the jobs defined in the ir_cron table and spawn accordingly threads
|
||||
to process them.
|
||||
|
||||
This module's behavior depends on the following configuration variable:
|
||||
openerp.conf.max_cron_threads.
|
||||
|
||||
"""
|
||||
|
||||
import heapq
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import openerp
|
||||
import tools
|
||||
|
||||
# Heapq of database wake-ups. Note that 'database wake-up' meaning is in
|
||||
# the context of the cron management. This is not originally about loading
|
||||
# a database, although having the database name in the queue will
|
||||
# cause it to be loaded when the schedule time is reached, even if it was
|
||||
# unloaded in the mean time. Normally a database's wake-up is cancelled by
|
||||
# the RegistryManager when the database is unloaded - so this should not
|
||||
# cause it to be reloaded.
|
||||
#
|
||||
# TODO: perhaps in the future we could consider a flag on ir.cron jobs
|
||||
# that would cause database wake-up even if the database has not been
|
||||
# loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something)
|
||||
#
|
||||
# Each element is a triple (timestamp, database-name, boolean). The boolean
|
||||
# specifies if the wake-up is canceled (so a wake-up can be canceled without
|
||||
# relying on the heapq implementation detail; no need to remove the job from
|
||||
# the heapq).
|
||||
_wakeups = []
|
||||
|
||||
# Mapping of database names to the wake-up defined in the heapq,
|
||||
# so that we can cancel the wake-up without messing with the heapq
|
||||
# invariant: lookup the wake-up by database-name, then set
|
||||
# its third element to True.
|
||||
_wakeup_by_db = {}
|
||||
|
||||
# Re-entrant lock to protect the above _wakeups and _wakeup_by_db variables.
|
||||
# We could use a simple (non-reentrant) lock if the runner function below
|
||||
# was more fine-grained, but we are fine with the loop owning the lock
|
||||
# while spawning a few threads.
|
||||
_wakeups_lock = threading.RLock()
|
||||
|
||||
# Maximum number of threads allowed to process cron jobs concurrently. This
|
||||
# variable is set by start_master_thread using openerp.conf.max_cron_threads.
|
||||
_thread_slots = None
|
||||
|
||||
# A (non re-entrant) lock to protect the above _thread_slots variable.
|
||||
_thread_slots_lock = threading.Lock()
|
||||
|
||||
_logger = logging.getLogger('cron')
|
||||
|
||||
# Sleep duration limits - must not loop too quickly, but can't sleep too long
|
||||
# either, because a new job might be inserted in ir_cron with a much sooner
|
||||
# execution date than current known ones. We won't see it until we wake!
|
||||
MAX_SLEEP = 60 # 1 min
|
||||
MIN_SLEEP = 1 # 1 sec
|
||||
|
||||
# Dummy wake-up timestamp that can be used to force a database wake-up asap
|
||||
WAKE_UP_NOW = 1
|
||||
|
||||
def get_thread_slots():
|
||||
""" Return the number of available thread slots """
|
||||
return _thread_slots
|
||||
|
||||
|
||||
def release_thread_slot():
|
||||
""" Increment the number of available thread slots """
|
||||
global _thread_slots
|
||||
with _thread_slots_lock:
|
||||
_thread_slots += 1
|
||||
|
||||
|
||||
def take_thread_slot():
|
||||
""" Decrement the number of available thread slots """
|
||||
global _thread_slots
|
||||
with _thread_slots_lock:
|
||||
_thread_slots -= 1
|
||||
|
||||
|
||||
def cancel(db_name):
|
||||
""" Cancel the next wake-up of a given database, if any.
|
||||
|
||||
:param db_name: database name for which the wake-up is canceled.
|
||||
|
||||
"""
|
||||
_logger.debug("Cancel next wake-up for database '%s'.", db_name)
|
||||
with _wakeups_lock:
|
||||
if db_name in _wakeup_by_db:
|
||||
_wakeup_by_db[db_name][2] = True
|
||||
|
||||
|
||||
def cancel_all():
|
||||
""" Cancel all database wake-ups. """
|
||||
_logger.debug("Cancel all database wake-ups")
|
||||
global _wakeups
|
||||
global _wakeup_by_db
|
||||
with _wakeups_lock:
|
||||
_wakeups = []
|
||||
_wakeup_by_db = {}
|
||||
|
||||
|
||||
def schedule_wakeup(timestamp, db_name):
|
||||
""" Schedule a new wake-up for a database.
|
||||
|
||||
If an earlier wake-up is already defined, the new wake-up is discarded.
|
||||
If another wake-up is defined, that wake-up is discarded and the new one
|
||||
is scheduled.
|
||||
|
||||
:param db_name: database name for which a new wake-up is scheduled.
|
||||
:param timestamp: when the wake-up is scheduled.
|
||||
|
||||
"""
|
||||
if not timestamp:
|
||||
return
|
||||
with _wakeups_lock:
|
||||
if db_name in _wakeup_by_db:
|
||||
task = _wakeup_by_db[db_name]
|
||||
if not task[2] and timestamp > task[0]:
|
||||
# existing wakeup is valid and occurs earlier than new one
|
||||
return
|
||||
task[2] = True # cancel existing task
|
||||
task = [timestamp, db_name, False]
|
||||
heapq.heappush(_wakeups, task)
|
||||
_wakeup_by_db[db_name] = task
|
||||
_logger.debug("Wake-up scheduled for database '%s' @ %s", db_name,
|
||||
'NOW' if timestamp == WAKE_UP_NOW else timestamp)
|
||||
|
||||
def runner():
|
||||
"""Neverending function (intended to be run in a dedicated thread) that
|
||||
checks every 60 seconds the next database wake-up. TODO: make configurable
|
||||
"""
|
||||
while True:
|
||||
runner_body()
|
||||
|
||||
def runner_body():
|
||||
with _wakeups_lock:
|
||||
while _wakeups and _wakeups[0][0] < time.time() and get_thread_slots():
|
||||
task = heapq.heappop(_wakeups)
|
||||
timestamp, db_name, canceled = task
|
||||
if canceled:
|
||||
continue
|
||||
del _wakeup_by_db[db_name]
|
||||
registry = openerp.pooler.get_pool(db_name)
|
||||
if not registry._init:
|
||||
_logger.debug("Database '%s' wake-up! Firing multi-threaded cron job processing", db_name)
|
||||
registry['ir.cron']._run_jobs_multithread()
|
||||
amount = MAX_SLEEP
|
||||
with _wakeups_lock:
|
||||
# Sleep less than MAX_SLEEP if the next known wake-up will happen before that.
|
||||
if _wakeups and get_thread_slots():
|
||||
amount = min(MAX_SLEEP, max(MIN_SLEEP, _wakeups[0][0] - time.time()))
|
||||
_logger.debug("Going to sleep for %ss", amount)
|
||||
time.sleep(amount)
|
||||
|
||||
def start_master_thread():
|
||||
""" Start the above runner function in a daemon thread.
|
||||
|
||||
The thread is a typical daemon thread: it will never quit and must be
|
||||
terminated when the main process exits - with no consequence (the processing
|
||||
threads it spawns are not marked daemon).
|
||||
|
||||
"""
|
||||
global _thread_slots
|
||||
_thread_slots = openerp.conf.max_cron_threads
|
||||
db_maxconn = tools.config['db_maxconn']
|
||||
if _thread_slots >= tools.config.get('db_maxconn', 64):
|
||||
_logger.warning("Connection pool size (%s) is set lower than max number of cron threads (%s), "
|
||||
"this may cause trouble if you reach that number of parallel cron tasks.",
|
||||
db_maxconn, _thread_slots)
|
||||
t = threading.Thread(target=runner, name="openerp.cron.master_thread")
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
_logger.debug("Master cron daemon started!")
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,57 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
""" OpenERP core exceptions.
|
||||
|
||||
This module defines a few exception types. Those types are understood by the
|
||||
RPC layer. Any other exception type bubbling until the RPC layer will be
|
||||
treated as a 'Server error'.
|
||||
|
||||
"""
|
||||
|
||||
class Warning(Exception):
|
||||
pass
|
||||
|
||||
class AccessDenied(Exception):
|
||||
""" Login/password error. No message, no traceback. """
|
||||
def __init__(self):
|
||||
super(AccessDenied, self).__init__('AccessDenied.')
|
||||
self.traceback = ('', '', '')
|
||||
|
||||
class AccessError(Exception):
|
||||
""" Access rights error. """
|
||||
|
||||
class DeferredException(Exception):
|
||||
""" Exception object holding a traceback for asynchronous reporting.
|
||||
|
||||
Some RPC calls (database creation and report generation) happen with
|
||||
an initial request followed by multiple, polling requests. This class
|
||||
is used to store the possible exception occuring in the thread serving
|
||||
the first request, and is then sent to a polling request.
|
||||
|
||||
('Traceback' is misleading, this is really a exc_info() triple.)
|
||||
"""
|
||||
def __init__(self, msg, tb):
|
||||
self.message = msg
|
||||
self.traceback = tb
|
||||
self.args = (msg, tb)
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -75,13 +75,14 @@ def initialize(cr):
|
|||
|
||||
cr.execute('INSERT INTO ir_module_module \
|
||||
(author, website, name, shortdesc, description, \
|
||||
category_id, state, certificate, web, license) \
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
|
||||
category_id, state, certificate, web, license, complexity) \
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
|
||||
info['author'],
|
||||
info['website'], i, info['name'],
|
||||
info['description'], category_id, state, info['certificate'],
|
||||
info['web'],
|
||||
info['license']))
|
||||
info['license'],
|
||||
info['complexity']))
|
||||
id = cr.fetchone()[0]
|
||||
cr.execute('INSERT INTO ir_model_data \
|
||||
(name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', (
|
||||
|
|
|
@ -249,6 +249,7 @@ def load_information_from_description_file(module):
|
|||
info.setdefault('website', '')
|
||||
info.setdefault('name', False)
|
||||
info.setdefault('description', '')
|
||||
info.setdefault('complexity', False)
|
||||
info['certificate'] = info.get('certificate') or None
|
||||
info['web'] = info.get('web') or False
|
||||
info['license'] = info.get('license') or 'AGPL-3'
|
||||
|
|
|
@ -28,6 +28,8 @@ import logging
|
|||
|
||||
import openerp.sql_db
|
||||
import openerp.osv.orm
|
||||
import openerp.cron
|
||||
import openerp.tools
|
||||
import openerp.modules.db
|
||||
import openerp.tools.config
|
||||
|
||||
|
@ -96,9 +98,17 @@ class Registry(object):
|
|||
|
||||
return res
|
||||
|
||||
def schedule_cron_jobs(self):
|
||||
""" Make the cron thread care about this registry/database jobs.
|
||||
This will initiate the cron thread to check for any pending jobs for
|
||||
this registry/database as soon as possible. Then it will continuously
|
||||
monitor the ir.cron model for future jobs. See openerp.cron for
|
||||
details.
|
||||
"""
|
||||
openerp.cron.schedule_wakeup(openerp.cron.WAKE_UP_NOW, self.db.dbname)
|
||||
|
||||
def clear_caches(self):
|
||||
""" Clear the caches
|
||||
|
||||
This clears the caches associated to methods decorated with
|
||||
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
|
||||
"""
|
||||
|
@ -112,25 +122,20 @@ class RegistryManager(object):
|
|||
registries (essentially database connection/model registry pairs).
|
||||
|
||||
"""
|
||||
|
||||
# Mapping between db name and model registry.
|
||||
# Accessed through the methods below.
|
||||
registries = {}
|
||||
registries_lock = threading.RLock()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get(cls, db_name, force_demo=False, status=None, update_module=False,
|
||||
pooljobs=True):
|
||||
""" Return a registry for a given database name."""
|
||||
with cls.registries_lock:
|
||||
if db_name in cls.registries:
|
||||
registry = cls.registries[db_name]
|
||||
else:
|
||||
registry = cls.new(db_name, force_demo, status,
|
||||
update_module, pooljobs)
|
||||
return registry
|
||||
|
||||
try:
|
||||
return cls.registries[db_name]
|
||||
except KeyError:
|
||||
return cls.new(db_name, force_demo, status,
|
||||
update_module, pooljobs)
|
||||
|
||||
@classmethod
|
||||
def new(cls, db_name, force_demo=False, status=None,
|
||||
|
@ -165,23 +170,39 @@ class RegistryManager(object):
|
|||
finally:
|
||||
cr.close()
|
||||
|
||||
if pooljobs:
|
||||
registry.get('ir.cron').restart(registry.db.dbname)
|
||||
|
||||
return registry
|
||||
if pooljobs:
|
||||
registry.schedule_cron_jobs()
|
||||
|
||||
return registry
|
||||
|
||||
@classmethod
|
||||
def delete(cls, db_name):
|
||||
""" Delete the registry linked to a given database. """
|
||||
"""Delete the registry linked to a given database.
|
||||
|
||||
This also cleans the associated caches. For good measure this also
|
||||
cancels the associated cron job. But please note that the cron job can
|
||||
be running and take some time before ending, and that you should not
|
||||
remove a registry if it can still be used by some thread. So it might
|
||||
be necessary to call yourself openerp.cron.Agent.cancel(db_name) and
|
||||
and join (i.e. wait for) the thread.
|
||||
"""
|
||||
with cls.registries_lock:
|
||||
if db_name in cls.registries:
|
||||
cls.registries[db_name].clear_caches()
|
||||
del cls.registries[db_name]
|
||||
openerp.cron.cancel(db_name)
|
||||
|
||||
|
||||
@classmethod
|
||||
def delete_all(cls):
|
||||
"""Delete all the registries. """
|
||||
with cls.registries_lock:
|
||||
for db_name in cls.registries.keys():
|
||||
cls.delete(db_name)
|
||||
|
||||
@classmethod
|
||||
def clear_caches(cls, db_name):
|
||||
""" Clear the caches
|
||||
"""Clear caches
|
||||
|
||||
This clears the caches associated to methods decorated with
|
||||
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
##############################################################################
|
||||
|
||||
import errno
|
||||
import heapq
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
|
@ -31,12 +30,14 @@ import socket
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import types
|
||||
from pprint import pformat
|
||||
|
||||
# TODO modules that import netsvc only for things from loglevels must be changed to use loglevels.
|
||||
from loglevels import *
|
||||
import tools
|
||||
import openerp
|
||||
|
||||
def close_socket(sock):
|
||||
""" Closes a socket instance cleanly
|
||||
|
@ -60,11 +61,12 @@ 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))
|
||||
def abort_response(dummy_1, description, dummy_2, details):
|
||||
# TODO Replace except_{osv,orm} with these directly.
|
||||
if description == 'AccessError':
|
||||
raise openerp.exceptions.AccessError(details)
|
||||
else:
|
||||
raise
|
||||
raise openerp.exceptions.Warning(details)
|
||||
|
||||
class Service(object):
|
||||
""" Base class for *Local* services
|
||||
|
@ -96,12 +98,9 @@ def LocalService(name):
|
|||
class ExportService(object):
|
||||
""" Proxy for exported services.
|
||||
|
||||
All methods here should take an AuthProxy as their first parameter. It
|
||||
will be appended by the calling framework.
|
||||
|
||||
Note that this class has no direct proxy, capable of calling
|
||||
eservice.method(). Rather, the proxy should call
|
||||
dispatch(method,auth,params)
|
||||
dispatch(method, params)
|
||||
"""
|
||||
|
||||
_services = {}
|
||||
|
@ -118,7 +117,7 @@ class ExportService(object):
|
|||
|
||||
# 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):
|
||||
def dispatch(self, method, params):
|
||||
raise Exception("stub dispatch at %s" % self.__name)
|
||||
|
||||
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
|
||||
|
@ -212,83 +211,6 @@ def init_alternative_logger():
|
|||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
class Agent(object):
|
||||
""" Singleton that keeps track of cancellable tasks to run at a given
|
||||
timestamp.
|
||||
|
||||
The tasks are characterised by:
|
||||
|
||||
* a timestamp
|
||||
* the database on which the task run
|
||||
* the function to call
|
||||
* the arguments and keyword arguments to pass to the function
|
||||
|
||||
Implementation details:
|
||||
|
||||
- Tasks are stored as list, allowing the cancellation by setting
|
||||
the timestamp to 0.
|
||||
- A heapq is used to store tasks, so we don't need to sort
|
||||
tasks ourself.
|
||||
"""
|
||||
__tasks = []
|
||||
__tasks_by_db = {}
|
||||
_logger = logging.getLogger('netsvc.agent')
|
||||
|
||||
@classmethod
|
||||
def setAlarm(cls, function, timestamp, db_name, *args, **kwargs):
|
||||
task = [timestamp, db_name, function, args, kwargs]
|
||||
heapq.heappush(cls.__tasks, task)
|
||||
cls.__tasks_by_db.setdefault(db_name, []).append(task)
|
||||
|
||||
@classmethod
|
||||
def cancel(cls, db_name):
|
||||
"""Cancel all tasks for a given database. If None is passed, all tasks are cancelled"""
|
||||
cls._logger.debug("Cancel timers for %s db", db_name or 'all')
|
||||
if db_name is None:
|
||||
cls.__tasks, cls.__tasks_by_db = [], {}
|
||||
else:
|
||||
if db_name in cls.__tasks_by_db:
|
||||
for task in cls.__tasks_by_db[db_name]:
|
||||
task[0] = 0
|
||||
|
||||
@classmethod
|
||||
def quit(cls):
|
||||
cls.cancel(None)
|
||||
|
||||
@classmethod
|
||||
def runner(cls):
|
||||
"""Neverending function (intended to be ran in a dedicated thread) that
|
||||
checks every 60 seconds tasks to run. TODO: make configurable
|
||||
"""
|
||||
current_thread = threading.currentThread()
|
||||
while True:
|
||||
while cls.__tasks and cls.__tasks[0][0] < time.time():
|
||||
task = heapq.heappop(cls.__tasks)
|
||||
timestamp, dbname, function, args, kwargs = task
|
||||
cls.__tasks_by_db[dbname].remove(task)
|
||||
if not timestamp:
|
||||
# null timestamp -> cancelled task
|
||||
continue
|
||||
current_thread.dbname = dbname # hack hack
|
||||
cls._logger.debug("Run %s.%s(*%s, **%s)", function.im_class.__name__, function.func_name, args, kwargs)
|
||||
delattr(current_thread, 'dbname')
|
||||
task_thread = threading.Thread(target=function, name='netsvc.Agent.task', args=args, kwargs=kwargs)
|
||||
# force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
|
||||
task_thread.setDaemon(False)
|
||||
task_thread.start()
|
||||
time.sleep(1)
|
||||
time.sleep(60)
|
||||
|
||||
def start_agent():
|
||||
agent_runner = threading.Thread(target=Agent.runner, name="netsvc.Agent.runner")
|
||||
# the agent runner is a typical daemon thread, that will never quit and must be
|
||||
# terminated when the main process exits - with no consequence (the processing
|
||||
# threads it spawns are not marked daemon)
|
||||
agent_runner.setDaemon(True)
|
||||
agent_runner.start()
|
||||
|
||||
import traceback
|
||||
|
||||
class Server:
|
||||
""" Generic interface for all servers with an event loop etc.
|
||||
Override this to impement http, net-rpc etc. servers.
|
||||
|
@ -371,11 +293,6 @@ class Server:
|
|||
def _close_socket(self):
|
||||
close_socket(self.socket)
|
||||
|
||||
class OpenERPDispatcherException(Exception):
|
||||
def __init__(self, exception, traceback):
|
||||
self.exception = exception
|
||||
self.traceback = traceback
|
||||
|
||||
def replace_request_password(args):
|
||||
# password is always 3rd argument in a request, we replace it in RPC logs
|
||||
# so it's easier to forward logs for diagnostics/debugging purposes...
|
||||
|
@ -393,7 +310,7 @@ def log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
|
|||
logger.log(channel, indent+line)
|
||||
indent=indent_after
|
||||
|
||||
def dispatch_rpc(service_name, method, params, auth):
|
||||
def dispatch_rpc(service_name, method, params):
|
||||
""" Handle a RPC call.
|
||||
|
||||
This is pure Python code, the actual marshalling (from/to XML-RPC or
|
||||
|
@ -408,7 +325,7 @@ def dispatch_rpc(service_name, method, params, auth):
|
|||
_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)
|
||||
result = ExportService.getService(service_name).dispatch(method, params)
|
||||
if logger.isEnabledFor(logging.DEBUG_RPC):
|
||||
end_time = time.time()
|
||||
if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
|
||||
|
@ -416,13 +333,24 @@ def dispatch_rpc(service_name, method, params, auth):
|
|||
_log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER)
|
||||
_log('result', result, channel=logging.DEBUG_RPC_ANSWER)
|
||||
return result
|
||||
except openerp.exceptions.AccessError:
|
||||
raise
|
||||
except openerp.exceptions.AccessDenied:
|
||||
raise
|
||||
except openerp.exceptions.Warning:
|
||||
raise
|
||||
except openerp.exceptions.DeferredException, e:
|
||||
_log('exception', tools.exception_to_unicode(e))
|
||||
post_mortem(e.traceback)
|
||||
raise
|
||||
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)
|
||||
post_mortem(sys.exc_info())
|
||||
raise
|
||||
|
||||
def post_mortem(info):
|
||||
if tools.config['debug_mode'] and isinstance(info[2], types.TracebackType):
|
||||
import pdb
|
||||
pdb.post_mortem(info[2])
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -1285,7 +1285,7 @@ class property(function):
|
|||
self.field_id = {}
|
||||
|
||||
|
||||
def field_to_dict(self, cr, user, context, field):
|
||||
def field_to_dict(model, cr, user, field, context=None):
|
||||
""" Return a dictionary representation of a field.
|
||||
|
||||
The string, help, and selection attributes (if any) are untranslated. This
|
||||
|
@ -1308,8 +1308,9 @@ def field_to_dict(self, cr, user, context, field):
|
|||
res['fnct_inv_arg'] = field._fnct_inv_arg or False
|
||||
res['func_obj'] = field._obj or False
|
||||
if isinstance(field, many2many):
|
||||
res['related_columns'] = list((field._id1, field._id2))
|
||||
res['third_table'] = field._rel
|
||||
(table, col1, col2) = field._sql_names(model)
|
||||
res['related_columns'] = [col1, col2]
|
||||
res['third_table'] = table
|
||||
for arg in ('string', 'readonly', 'states', 'size', 'required', 'group_operator',
|
||||
'change_default', 'translate', 'help', 'select', 'selectable'):
|
||||
if getattr(field, arg):
|
||||
|
@ -1328,7 +1329,7 @@ def field_to_dict(self, cr, user, context, field):
|
|||
res['selection'] = field.selection
|
||||
else:
|
||||
# call the 'dynamic selection' function
|
||||
res['selection'] = field.selection(self, cr, user, context)
|
||||
res['selection'] = field.selection(model, cr, user, context)
|
||||
if res['type'] in ('one2many', 'many2many', 'many2one', 'one2one'):
|
||||
res['relation'] = field._obj
|
||||
res['domain'] = field._domain
|
||||
|
|
|
@ -2069,8 +2069,9 @@ class BaseModel(object):
|
|||
context)
|
||||
resprint = map(clean, resprint)
|
||||
resaction = map(clean, resaction)
|
||||
resaction = filter(lambda x: not x.get('multi', False), resaction)
|
||||
resprint = filter(lambda x: not x.get('multi', False), resprint)
|
||||
if view_type != 'tree':
|
||||
resaction = filter(lambda x: not x.get('multi'), resaction)
|
||||
resprint = filter(lambda x: not x.get('multi'), resprint)
|
||||
resrelate = map(lambda x: x[2], resrelate)
|
||||
|
||||
for x in resprint + resaction + resrelate:
|
||||
|
@ -3135,7 +3136,7 @@ class BaseModel(object):
|
|||
if allfields and f not in allfields:
|
||||
continue
|
||||
|
||||
res[f] = fields.field_to_dict(self, cr, user, context, field)
|
||||
res[f] = fields.field_to_dict(self, cr, user, field, context=context)
|
||||
|
||||
if not write_access:
|
||||
res[f]['readonly'] = True
|
||||
|
@ -3469,7 +3470,7 @@ class BaseModel(object):
|
|||
WHERE id IN %%s""" % self._table, (tuple(ids),))
|
||||
uids = [x[0] for x in cr.fetchall()]
|
||||
if len(uids) != 1 or uids[0] != uid:
|
||||
raise orm.except_orm(_('AccessError'), '%s access is '
|
||||
raise except_orm(_('AccessError'), '%s access is '
|
||||
'restricted to your own records for transient models '
|
||||
'(except for the super-user).' % operation.capitalize())
|
||||
else:
|
||||
|
|
|
@ -32,17 +32,14 @@ import openerp.sql_db as sql_db
|
|||
from openerp.tools.func import wraps
|
||||
from openerp.tools.translate import translate
|
||||
from openerp.osv.orm import MetaModel, Model, TransientModel, AbstractModel
|
||||
import openerp.exceptions
|
||||
|
||||
class except_osv(Exception):
|
||||
def __init__(self, name, value, exc_type='warning'):
|
||||
self.name = name
|
||||
self.exc_type = exc_type
|
||||
self.value = value
|
||||
self.args = (exc_type, name)
|
||||
# For backward compatibility
|
||||
except_osv = openerp.exceptions.Warning
|
||||
|
||||
service = None
|
||||
|
||||
class object_proxy():
|
||||
class object_proxy(object):
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('web-services')
|
||||
global service
|
||||
|
@ -121,8 +118,8 @@ class object_proxy():
|
|||
if inst.name == 'AccessError':
|
||||
self.logger.debug("AccessError", exc_info=True)
|
||||
netsvc.abort_response(1, inst.name, 'warning', inst.value)
|
||||
except except_osv, inst:
|
||||
netsvc.abort_response(1, inst.name, inst.exc_type, inst.value)
|
||||
except except_osv:
|
||||
raise
|
||||
except IntegrityError, inst:
|
||||
osv_pool = pooler.get_pool(dbname)
|
||||
for key in osv_pool._sql_error.keys():
|
||||
|
|
|
@ -34,11 +34,6 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False,
|
|||
return registry.db, registry
|
||||
|
||||
|
||||
def delete_pool(db_name):
|
||||
"""Delete an existing registry."""
|
||||
RegistryManager.delete(db_name)
|
||||
|
||||
|
||||
def restart_pool(db_name, force_demo=False, status=None, update_module=False):
|
||||
"""Delete an existing registry and return a database connection and a newly initialized registry."""
|
||||
registry = RegistryManager.new(db_name, force_demo, status, update_module, True)
|
||||
|
|
|
@ -28,6 +28,8 @@ import netrpc_server
|
|||
import web_services
|
||||
import websrv_lib
|
||||
|
||||
import openerp.cron
|
||||
import openerp.modules
|
||||
import openerp.netsvc
|
||||
import openerp.osv
|
||||
import openerp.tools
|
||||
|
@ -57,24 +59,24 @@ def start_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()
|
||||
openerp.cron.start_master_thread()
|
||||
|
||||
# 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()
|
||||
# stop scheduling new jobs; we will have to wait for the jobs to complete below
|
||||
openerp.cron.cancel_all()
|
||||
|
||||
openerp.netsvc.Server.quitAll()
|
||||
openerp.wsgi.stop_server()
|
||||
config = openerp.tools.config
|
||||
|
@ -94,6 +96,8 @@ def stop_services():
|
|||
thread.join(0.05)
|
||||
time.sleep(0.05)
|
||||
|
||||
openerp.modules.registry.RegistryManager.delete_all()
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -64,53 +64,6 @@ try:
|
|||
except ImportError:
|
||||
class SSLError(Exception): pass
|
||||
|
||||
class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer):
|
||||
""" A threaded httpd server, with all the necessary functionality for us.
|
||||
|
||||
It also inherits the xml-rpc dispatcher, so that some xml-rpc functions
|
||||
will be available to the request handler
|
||||
"""
|
||||
encoding = None
|
||||
allow_none = False
|
||||
allow_reuse_address = 1
|
||||
_send_traceback_header = False
|
||||
i = 0
|
||||
|
||||
def __init__(self, addr, requestHandler, proto='http',
|
||||
logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
|
||||
self.logRequests = logRequests
|
||||
|
||||
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
|
||||
HTTPServer.__init__(self, addr, requestHandler)
|
||||
|
||||
self.numThreads = 0
|
||||
self.proto = proto
|
||||
self.__threadno = 0
|
||||
|
||||
# [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)
|
||||
|
||||
def handle_error(self, request, client_address):
|
||||
""" Override the error handler
|
||||
"""
|
||||
|
||||
logging.getLogger("init").exception("Server error in request from %s:" % (client_address,))
|
||||
|
||||
def _mark_start(self, thread):
|
||||
self.numThreads += 1
|
||||
|
||||
def _mark_end(self, thread):
|
||||
self.numThreads -= 1
|
||||
|
||||
|
||||
def _get_next_name(self):
|
||||
self.__threadno += 1
|
||||
return 'http-client-%d' % self.__threadno
|
||||
class HttpLogHandler:
|
||||
""" helper class for uniform log handling
|
||||
Please define self._logger at each class that is derived from this
|
||||
|
@ -129,136 +82,6 @@ class HttpLogHandler:
|
|||
def log_request(self, code='-', size='-'):
|
||||
self._logger.log(netsvc.logging.DEBUG_RPC, '"%s" %s %s',
|
||||
self.requestline, str(code), str(size))
|
||||
|
||||
class MultiHandler2(HttpLogHandler, MultiHTTPHandler):
|
||||
_logger = logging.getLogger('http')
|
||||
|
||||
|
||||
class SecureMultiHandler2(HttpLogHandler, SecureMultiHTTPHandler):
|
||||
_logger = logging.getLogger('https')
|
||||
|
||||
def getcert_fnames(self):
|
||||
tc = tools.config
|
||||
fcert = tc.get('secure_cert_file', 'server.cert')
|
||||
fkey = tc.get('secure_pkey_file', 'server.key')
|
||||
return (fcert,fkey)
|
||||
|
||||
class BaseHttpDaemon(threading.Thread, netsvc.Server):
|
||||
_RealProto = '??'
|
||||
|
||||
def __init__(self, interface, port, handler):
|
||||
threading.Thread.__init__(self, name='%sDaemon-%d'%(self._RealProto, port))
|
||||
netsvc.Server.__init__(self)
|
||||
self.__port = port
|
||||
self.__interface = interface
|
||||
|
||||
try:
|
||||
self.server = ThreadedHTTPServer((interface, port), handler, proto=self._RealProto)
|
||||
self.server.logRequests = True
|
||||
self.server.timeout = self._busywait_timeout
|
||||
logging.getLogger("web-services").info(
|
||||
"starting %s service at %s port %d" %
|
||||
(self._RealProto, interface or '0.0.0.0', port,))
|
||||
except Exception, e:
|
||||
logging.getLogger("httpd").exception("Error occured when starting the server daemon.")
|
||||
raise
|
||||
|
||||
@property
|
||||
def socket(self):
|
||||
return self.server.socket
|
||||
|
||||
def attach(self, path, gw):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
self._close_socket()
|
||||
|
||||
def run(self):
|
||||
self.running = True
|
||||
while self.running:
|
||||
try:
|
||||
self.server.handle_request()
|
||||
except (socket.error, select.error), e:
|
||||
if self.running or e.args[0] != errno.EBADF:
|
||||
raise
|
||||
return True
|
||||
|
||||
def stats(self):
|
||||
res = "%sd: " % self._RealProto + ((self.running and "running") or "stopped")
|
||||
if self.server:
|
||||
res += ", %d threads" % (self.server.numThreads,)
|
||||
return res
|
||||
|
||||
# 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):
|
||||
super(HttpDaemon, self).__init__(interface, port,
|
||||
handler=MultiHandler2)
|
||||
|
||||
class HttpSDaemon(BaseHttpDaemon):
|
||||
_RealProto = 'HTTPS'
|
||||
def __init__(self, interface, port):
|
||||
try:
|
||||
super(HttpSDaemon, self).__init__(interface, port,
|
||||
handler=SecureMultiHandler2)
|
||||
except SSLError, e:
|
||||
logging.getLogger('httpsd').exception( \
|
||||
"Can not load the certificate and/or the private key files")
|
||||
raise
|
||||
|
||||
httpd = None
|
||||
httpsd = None
|
||||
|
||||
def init_servers():
|
||||
global httpd, httpsd
|
||||
if tools.config.get('xmlrpc'):
|
||||
httpd = HttpDaemon(tools.config.get('xmlrpc_interface', ''),
|
||||
int(tools.config.get('xmlrpc_port', 8069)))
|
||||
|
||||
if tools.config.get('xmlrpcs'):
|
||||
httpsd = HttpSDaemon(tools.config.get('xmlrpcs_interface', ''),
|
||||
int(tools.config.get('xmlrpcs_port', 8071)))
|
||||
|
||||
import SimpleXMLRPCServer
|
||||
class XMLRPCRequestHandler(FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
|
||||
rpc_paths = []
|
||||
protocol_version = 'HTTP/1.1'
|
||||
_logger = logging.getLogger('xmlrpc')
|
||||
|
||||
def _dispatch(self, method, params):
|
||||
try:
|
||||
service_name = self.path.split("/")[-1]
|
||||
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)
|
||||
|
||||
def handle(self):
|
||||
pass
|
||||
|
||||
def finish(self):
|
||||
pass
|
||||
|
||||
def setup(self):
|
||||
self.connection = dummyconn()
|
||||
self.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys())
|
||||
|
||||
|
||||
def init_xmlrpc():
|
||||
if tools.config.get('xmlrpc', False):
|
||||
# Example of http file serving:
|
||||
# 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('/xmlrpc/', XMLRPCRequestHandler, secure_only=True)
|
||||
logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only")
|
||||
|
||||
class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler):
|
||||
_logger = logging.getLogger('httpd')
|
||||
|
|
|
@ -59,27 +59,18 @@ class TinySocketClientThread(threading.Thread):
|
|||
while self.running:
|
||||
try:
|
||||
msg = ts.myreceive()
|
||||
auth = getattr(self, 'auth_provider', None)
|
||||
result = netsvc.dispatch_rpc(msg[0], msg[1], msg[2:], auth)
|
||||
result = netsvc.dispatch_rpc(msg[0], msg[1], msg[2:])
|
||||
ts.mysend(result)
|
||||
except socket.timeout:
|
||||
#terminate this channel because other endpoint is gone
|
||||
break
|
||||
except netsvc.OpenERPDispatcherException, e:
|
||||
try:
|
||||
new_e = Exception(tools.exception_to_unicode(e.exception)) # avoid problems of pickeling
|
||||
logging.getLogger('web-services').debug("netrpc: rpc-dispatching exception", exc_info=True)
|
||||
ts.mysend(new_e, exception=True, traceback=e.traceback)
|
||||
except Exception:
|
||||
#terminate this channel if we can't properly send back the error
|
||||
logging.getLogger('web-services').exception("netrpc: cannot deliver exception message to client")
|
||||
break
|
||||
except Exception, e:
|
||||
try:
|
||||
new_e = Exception(tools.exception_to_unicode(e)) # avoid problems of pickeling
|
||||
tb = getattr(e, 'traceback', sys.exc_info())
|
||||
tb_s = "".join(traceback.format_exception(*tb))
|
||||
logging.getLogger('web-services').debug("netrpc: communication-level exception", exc_info=True)
|
||||
ts.mysend(e, exception=True, traceback=tb_s)
|
||||
ts.mysend(new_e, exception=True, traceback=tb_s)
|
||||
break
|
||||
except Exception, ex:
|
||||
#terminate this channel if we can't properly send back the error
|
||||
|
|
|
@ -19,18 +19,12 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
import openerp.exceptions
|
||||
import openerp.pooler as pooler
|
||||
import openerp.tools as tools
|
||||
|
||||
#.apidoc title: Authentication helpers
|
||||
|
||||
class ExceptionNoTb(Exception):
|
||||
""" When rejecting a password, hide the traceback
|
||||
"""
|
||||
def __init__(self, msg):
|
||||
super(ExceptionNoTb, self).__init__(msg)
|
||||
self.traceback = ('','','')
|
||||
|
||||
def login(db, login, password):
|
||||
pool = pooler.get_pool(db)
|
||||
user_obj = pool.get('res.users')
|
||||
|
@ -40,7 +34,7 @@ def check_super(passwd):
|
|||
if passwd == tools.config['admin_passwd']:
|
||||
return True
|
||||
else:
|
||||
raise ExceptionNoTb('AccessDenied: Invalid super administrator password.')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
|
||||
def check(db, uid, passwd):
|
||||
pool = pooler.get_pool(db)
|
||||
|
|
|
@ -38,6 +38,7 @@ import openerp.release as release
|
|||
import openerp.sql_db as sql_db
|
||||
import openerp.tools as tools
|
||||
import openerp.modules
|
||||
import openerp.exceptions
|
||||
|
||||
#.apidoc title: Exported Service methods
|
||||
#.apidoc module-mods: member-order: bysource
|
||||
|
@ -93,7 +94,7 @@ class db(netsvc.ExportService):
|
|||
|
||||
self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
|
||||
|
||||
def dispatch(self, method, auth, params):
|
||||
def dispatch(self, method, params):
|
||||
if method in [ 'create', 'get_progress', 'drop', 'dump',
|
||||
'restore', 'rename',
|
||||
'change_admin_password', 'migrate_databases',
|
||||
|
@ -161,14 +162,13 @@ class db(netsvc.ExportService):
|
|||
self.actions.pop(id)
|
||||
return (1.0, users)
|
||||
else:
|
||||
e = self.actions[id]['exception']
|
||||
e = self.actions[id]['exception'] # TODO this seems wrong: actions[id]['traceback'] is set, but not 'exception'.
|
||||
self.actions.pop(id)
|
||||
raise Exception, e
|
||||
|
||||
def exp_drop(self, db_name):
|
||||
openerp.modules.registry.RegistryManager.delete(db_name)
|
||||
sql_db.close_db(db_name)
|
||||
openerp.modules.registry.RegistryManager.clear_caches(db_name)
|
||||
openerp.netsvc.Agent.cancel(db_name)
|
||||
logger = netsvc.Logger()
|
||||
|
||||
db = sql_db.db_connect('template1')
|
||||
|
@ -270,9 +270,8 @@ class db(netsvc.ExportService):
|
|||
return True
|
||||
|
||||
def exp_rename(self, old_name, new_name):
|
||||
openerp.modules.registry.RegistryManager.delete(old_name)
|
||||
sql_db.close_db(old_name)
|
||||
openerp.modules.registry.RegistryManager.clear_caches(old_name)
|
||||
openerp.netsvc.Agent.cancel(old_name)
|
||||
logger = netsvc.Logger()
|
||||
|
||||
db = sql_db.db_connect('template1')
|
||||
|
@ -302,7 +301,7 @@ class db(netsvc.ExportService):
|
|||
|
||||
def exp_list(self, document=False):
|
||||
if not tools.config['list_db'] and not document:
|
||||
raise Exception('AccessDenied')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
|
||||
db = sql_db.db_connect('template1')
|
||||
cr = db.cursor()
|
||||
|
@ -356,7 +355,7 @@ class db(netsvc.ExportService):
|
|||
except except_orm, inst:
|
||||
netsvc.abort_response(1, inst.name, 'warning', inst.value)
|
||||
except except_osv, inst:
|
||||
netsvc.abort_response(1, inst.name, inst.exc_type, inst.value)
|
||||
netsvc.abort_response(1, inst.name, 'warning', 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))
|
||||
|
@ -368,20 +367,14 @@ class common(netsvc.ExportService):
|
|||
def __init__(self,name="common"):
|
||||
netsvc.ExportService.__init__(self,name)
|
||||
|
||||
def dispatch(self, method, auth, params):
|
||||
def dispatch(self, method, params):
|
||||
logger = netsvc.Logger()
|
||||
if method == 'login':
|
||||
# At this old dispatcher, we do NOT update the auth proxy
|
||||
res = security.login(params[0], params[1], params[2])
|
||||
msg = res and 'successful login' or 'bad login or password'
|
||||
# TODO log the client ip address..
|
||||
logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, params[1], params[0].lower()))
|
||||
return res or False
|
||||
elif method == 'logout':
|
||||
if auth:
|
||||
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',
|
||||
'login_message','get_stats', 'check_connectivity',
|
||||
'list_http_services']:
|
||||
|
@ -562,7 +555,7 @@ class objects_proxy(netsvc.ExportService):
|
|||
def __init__(self, name="object"):
|
||||
netsvc.ExportService.__init__(self,name)
|
||||
|
||||
def dispatch(self, method, auth, params):
|
||||
def dispatch(self, method, params):
|
||||
(db, uid, passwd ) = params[0:3]
|
||||
params = params[3:]
|
||||
if method == 'obj_list':
|
||||
|
@ -595,7 +588,7 @@ class wizard(netsvc.ExportService):
|
|||
self.wiz_name = {}
|
||||
self.wiz_uid = {}
|
||||
|
||||
def dispatch(self, method, auth, params):
|
||||
def dispatch(self, method, params):
|
||||
(db, uid, passwd ) = params[0:3]
|
||||
params = params[3:]
|
||||
if method not in ['execute','create']:
|
||||
|
@ -628,9 +621,9 @@ class wizard(netsvc.ExportService):
|
|||
if self.wiz_uid[wiz_id] == uid:
|
||||
return self._execute(db, uid, wiz_id, datas, action, context)
|
||||
else:
|
||||
raise Exception, 'AccessDenied'
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
else:
|
||||
raise Exception, 'WizardNotFound'
|
||||
raise openerp.exceptions.Warning('Wizard not found.')
|
||||
|
||||
#
|
||||
# TODO: set a maximum report number per user to avoid DOS attacks
|
||||
|
@ -639,12 +632,6 @@ class wizard(netsvc.ExportService):
|
|||
# False -> True
|
||||
#
|
||||
|
||||
class ExceptionWithTraceback(Exception):
|
||||
def __init__(self, msg, tb):
|
||||
self.message = msg
|
||||
self.traceback = tb
|
||||
self.args = (msg, tb)
|
||||
|
||||
class report_spool(netsvc.ExportService):
|
||||
def __init__(self, name='report'):
|
||||
netsvc.ExportService.__init__(self, name)
|
||||
|
@ -652,7 +639,7 @@ class report_spool(netsvc.ExportService):
|
|||
self.id = 0
|
||||
self.id_protect = threading.Semaphore()
|
||||
|
||||
def dispatch(self, method, auth, params):
|
||||
def dispatch(self, method, params):
|
||||
(db, uid, passwd ) = params[0:3]
|
||||
params = params[3:]
|
||||
if method not in ['report', 'report_get', 'render_report']:
|
||||
|
@ -683,7 +670,7 @@ class report_spool(netsvc.ExportService):
|
|||
(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]['exception'] = openerp.exceptions.DeferredException('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
|
||||
|
@ -695,9 +682,9 @@ class report_spool(netsvc.ExportService):
|
|||
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))
|
||||
self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.ustr(exception.name), tools.ustr(exception.value))
|
||||
else:
|
||||
self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
|
||||
self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.exception_to_unicode(exception), tb)
|
||||
self._reports[id]['state'] = True
|
||||
cr.commit()
|
||||
cr.close()
|
||||
|
@ -726,7 +713,7 @@ class report_spool(netsvc.ExportService):
|
|||
(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]['exception'] = openerp.exceptions.DeferredException('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
|
||||
|
@ -738,9 +725,9 @@ class report_spool(netsvc.ExportService):
|
|||
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))
|
||||
self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.ustr(exception.name), tools.ustr(exception.value))
|
||||
else:
|
||||
self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
|
||||
self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.exception_to_unicode(exception), tb)
|
||||
self._reports[id]['state'] = True
|
||||
cr.commit()
|
||||
cr.close()
|
||||
|
|
|
@ -232,305 +232,3 @@ class HttpOptions:
|
|||
"""
|
||||
return opts
|
||||
|
||||
class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler):
|
||||
""" this is a multiple handler, that will dispatch each request
|
||||
to a nested handler, iff it matches
|
||||
|
||||
The handler will also have *one* dict of authentication proxies,
|
||||
groupped by their realm.
|
||||
"""
|
||||
|
||||
protocol_version = "HTTP/1.1"
|
||||
default_request_version = "HTTP/0.9" # compatibility with py2.5
|
||||
|
||||
auth_required_msg = """ <html><head><title>Authorization required</title></head>
|
||||
<body>You must authenticate to use this service</body><html>\r\r"""
|
||||
|
||||
def __init__(self, request, client_address, server):
|
||||
self.in_handlers = {}
|
||||
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):
|
||||
""" This method overrides the handle_one_request for *children*
|
||||
handlers. It is required, since the first line should not be
|
||||
read again..
|
||||
|
||||
"""
|
||||
fore.raw_requestline = "%s %s %s\n" % (self.command, path, self.version)
|
||||
if not fore.parse_request(): # An error code has been sent, just exit
|
||||
return
|
||||
if fore.headers.status:
|
||||
self.log_error("Parse error at headers: %s", fore.headers.status)
|
||||
self.close_connection = 1
|
||||
self.send_error(400,"Parse error at HTTP headers")
|
||||
return
|
||||
|
||||
self.request_version = fore.request_version
|
||||
if hasattr(fore, 'auth_provider'):
|
||||
try:
|
||||
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
|
||||
if self.request_version != 'HTTP/1.1' and ('Darwin/9.' not in fore.headers.get('User-Agent', '')):
|
||||
self.log_error("Cannot require auth at %s", self.request_version)
|
||||
self.send_error(403)
|
||||
return
|
||||
self._get_ignore_body(fore) # consume any body that came, not loose sync with input
|
||||
self.send_response(401,'Authorization required')
|
||||
self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm))
|
||||
self.send_header('Connection', 'keep-alive')
|
||||
self.send_header('Content-Type','text/html')
|
||||
self.send_header('Content-Length',len(self.auth_required_msg))
|
||||
self.end_headers()
|
||||
self.wfile.write(self.auth_required_msg)
|
||||
return
|
||||
except AuthRejectedExc,e:
|
||||
self.log_error("Rejected auth: %s" % e.args[0])
|
||||
self.send_error(403,e.args[0])
|
||||
self.close_connection = 1
|
||||
return
|
||||
mname = 'do_' + fore.command
|
||||
if not hasattr(fore, mname):
|
||||
if fore.command == 'OPTIONS':
|
||||
self.do_OPTIONS()
|
||||
return
|
||||
self.send_error(501, "Unsupported method (%r)" % fore.command)
|
||||
return
|
||||
fore.close_connection = 0
|
||||
method = getattr(fore, mname)
|
||||
try:
|
||||
method()
|
||||
except (AuthRejectedExc, AuthRequiredExc):
|
||||
raise
|
||||
except Exception, e:
|
||||
if hasattr(self, 'log_exception'):
|
||||
self.log_exception("Could not run %s", mname)
|
||||
else:
|
||||
self.log_error("Could not run %s: %s", mname, e)
|
||||
self.send_error(500, "Internal error")
|
||||
# may not work if method has already sent data
|
||||
fore.close_connection = 1
|
||||
self.close_connection = 1
|
||||
if hasattr(fore, '_flush'):
|
||||
fore._flush()
|
||||
return
|
||||
|
||||
if fore.close_connection:
|
||||
# print "Closing connection because of handler"
|
||||
self.close_connection = fore.close_connection
|
||||
if hasattr(fore, '_flush'):
|
||||
fore._flush()
|
||||
|
||||
|
||||
def parse_rawline(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):
|
||||
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.log_error("Junk http request: %s", self.raw_requestline)
|
||||
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.request_version = version
|
||||
self.command, self.path, self.version = command, path, version
|
||||
return True
|
||||
|
||||
def handle_one_request(self):
|
||||
"""Handle a single HTTP request.
|
||||
Dispatch to the correct handler.
|
||||
"""
|
||||
self.request.setblocking(True)
|
||||
self.raw_requestline = self.rfile.readline()
|
||||
if not self.raw_requestline:
|
||||
self.close_connection = 1
|
||||
# self.log_message("no requestline, connection closed?")
|
||||
return
|
||||
if not self.parse_rawline():
|
||||
self.log_message("Could not parse rawline.")
|
||||
return
|
||||
# self.parse_request(): # Do NOT parse here. the first line should be the only
|
||||
|
||||
if self.path == '*' and self.command == 'OPTIONS':
|
||||
# special handling of path='*', must not use any vdir at all.
|
||||
if not self.parse_request():
|
||||
return
|
||||
self.do_OPTIONS()
|
||||
return
|
||||
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.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)
|
||||
except IOError, e:
|
||||
if e.errno == errno.EPIPE:
|
||||
self.log_message("Could not complete request %s," \
|
||||
"client closed connection", self.rlpath.rstrip())
|
||||
else:
|
||||
raise
|
||||
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"):
|
||||
return
|
||||
max_chunk_size = 10*1024*1024
|
||||
size_remaining = int(fore.headers["content-length"])
|
||||
got = ''
|
||||
while size_remaining:
|
||||
chunk_size = min(size_remaining, max_chunk_size)
|
||||
got = fore.rfile.read(chunk_size)
|
||||
size_remaining -= len(got)
|
||||
|
||||
|
||||
class SecureMultiHTTPHandler(MultiHTTPHandler):
|
||||
def getcert_fnames(self):
|
||||
""" Return a pair with the filenames of ssl cert,key
|
||||
|
||||
Override this to direct to other filenames
|
||||
"""
|
||||
return ('server.cert','server.key')
|
||||
|
||||
def setup(self):
|
||||
import ssl
|
||||
certfile, keyfile = self.getcert_fnames()
|
||||
try:
|
||||
self.connection = ssl.wrap_socket(self.request,
|
||||
server_side=True,
|
||||
certfile=certfile,
|
||||
keyfile=keyfile,
|
||||
ssl_version=ssl.PROTOCOL_SSLv23)
|
||||
self.rfile = self.connection.makefile('rb', self.rbufsize)
|
||||
self.wfile = self.connection.makefile('wb', self.wbufsize)
|
||||
self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address)
|
||||
except Exception:
|
||||
self.request.shutdown(socket.SHUT_RDWR)
|
||||
raise
|
||||
|
||||
def finish(self):
|
||||
# With ssl connections, closing the filehandlers alone may not
|
||||
# work because of ref counting. We explicitly tell the socket
|
||||
# to shutdown.
|
||||
MultiHTTPHandler.finish(self)
|
||||
try:
|
||||
self.connection.shutdown(socket.SHUT_RDWR)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import threading
|
||||
class ConnThreadingMixIn:
|
||||
"""Mix-in class to handle each _connection_ in a new thread.
|
||||
|
||||
This is necessary for persistent connections, where multiple
|
||||
requests should be handled synchronously at each connection, but
|
||||
multiple connections can run in parallel.
|
||||
"""
|
||||
|
||||
# Decides how threads will act upon termination of the
|
||||
# main process
|
||||
daemon_threads = False
|
||||
|
||||
def _get_next_name(self):
|
||||
return None
|
||||
|
||||
def _handle_request_noblock(self):
|
||||
"""Start a new thread to process the request."""
|
||||
if not threading: # happens while quitting python
|
||||
return
|
||||
t = threading.Thread(name=self._get_next_name(), target=self._handle_request2)
|
||||
if self.daemon_threads:
|
||||
t.setDaemon (1)
|
||||
t.start()
|
||||
|
||||
def _mark_start(self, thread):
|
||||
""" Mark the start of a request thread """
|
||||
pass
|
||||
|
||||
def _mark_end(self, thread):
|
||||
""" Mark the end of a request thread """
|
||||
pass
|
||||
|
||||
def _handle_request2(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:
|
||||
self._mark_start(threading.currentThread())
|
||||
request, client_address = self.get_request()
|
||||
if self.verify_request(request, client_address):
|
||||
try:
|
||||
self.process_request(request, client_address)
|
||||
except Exception:
|
||||
self.handle_error(request, client_address)
|
||||
self.close_request(request)
|
||||
except socket.error:
|
||||
return
|
||||
finally:
|
||||
self._mark_end(threading.currentThread())
|
||||
|
||||
#eof
|
||||
|
|
|
@ -214,11 +214,11 @@ class Cursor(object):
|
|||
params = params or None
|
||||
res = self._obj.execute(query, params)
|
||||
except psycopg2.ProgrammingError, pe:
|
||||
if self._default_log_exceptions or log_exceptions:
|
||||
if (self._default_log_exceptions if log_exceptions is None else log_exceptions):
|
||||
self.__logger.error("Programming error: %s, in query %s", pe, query)
|
||||
raise
|
||||
except Exception:
|
||||
if self._default_log_exceptions or log_exceptions:
|
||||
if (self._default_log_exceptions if log_exceptions is None else log_exceptions):
|
||||
self.__logger.exception("bad query: %s", self._obj.query or query)
|
||||
raise
|
||||
|
||||
|
@ -504,7 +504,7 @@ def db_connect(db_name):
|
|||
return Connection(_Pool, db_name)
|
||||
|
||||
def close_db(db_name):
|
||||
""" You might want to call openerp.netsvc.Agent.cancel(db_name) along this function."""
|
||||
""" You might want to call openerp.modules.registry.RegistryManager.delete(db_name) along this function."""
|
||||
_Pool.close_all(dsn(db_name))
|
||||
ct = currentThread()
|
||||
if hasattr(ct, 'dbname'):
|
||||
|
|
|
@ -261,8 +261,12 @@ class configmanager(object):
|
|||
"osv_memory tables. This is a decimal value expressed in hours, "
|
||||
"and the default is 1 hour.",
|
||||
type="float")
|
||||
group.add_option("--max-cron-threads", dest="max_cron_threads", my_default=4,
|
||||
help="Maximum number of threads processing concurrently cron jobs.",
|
||||
type="int")
|
||||
group.add_option("--unaccent", dest="unaccent", my_default=False, action="store_true",
|
||||
help="Use the unaccent function provided by the database when available.")
|
||||
|
||||
parser.add_option_group(group)
|
||||
|
||||
# Copy all optparse options (i.e. MyOption) into self.options.
|
||||
|
@ -365,7 +369,7 @@ class configmanager(object):
|
|||
'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog',
|
||||
'list_db', 'xmlrpcs',
|
||||
'test_file', 'test_disable', 'test_commit', 'test_report_directory',
|
||||
'osv_memory_count_limit', 'osv_memory_age_limit', 'unaccent',
|
||||
'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent',
|
||||
]
|
||||
|
||||
for arg in keys:
|
||||
|
@ -447,6 +451,8 @@ class configmanager(object):
|
|||
if opt.save:
|
||||
self.save()
|
||||
|
||||
openerp.conf.max_cron_threads = self.options['max_cron_threads']
|
||||
|
||||
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 \
|
||||
|
|
|
@ -36,62 +36,98 @@ import signal
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import openerp
|
||||
import openerp.modules
|
||||
import openerp.tools.config as config
|
||||
import service.websrv_lib as websrv_lib
|
||||
|
||||
# XML-RPC fault codes. Some care must be taken when changing these: the
|
||||
# constants are also defined client-side and must remain in sync.
|
||||
# User code must use the exceptions defined in ``openerp.exceptions`` (not
|
||||
# create directly ``xmlrpclib.Fault`` objects).
|
||||
XML_RPC_FAULT_CODE_APPLICATION_ERROR = 1
|
||||
XML_RPC_FAULT_CODE_DEFERRED_APPLICATION_ERROR = 2
|
||||
XML_RPC_FAULT_CODE_ACCESS_DENIED = 3
|
||||
XML_RPC_FAULT_CODE_ACCESS_ERROR = 4
|
||||
XML_RPC_FAULT_CODE_WARNING = 5
|
||||
|
||||
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.
|
||||
"""
|
||||
Helper to call a service's method with some params, using a wsgi-supplied
|
||||
``start_response`` callback.
|
||||
|
||||
This is the place to look at to see the mapping between core exceptions
|
||||
and XML-RPC fault codes.
|
||||
"""
|
||||
# Map OpenERP core exceptions to XML-RPC fault codes. Specific exceptions
|
||||
# defined in ``openerp.exceptions`` are mapped to specific fault codes;
|
||||
# all the other exceptions are mapped to the generic
|
||||
# XML_RPC_FAULT_CODE_APPLICATION_ERROR value.
|
||||
# This also mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for
|
||||
# exception handling.
|
||||
try:
|
||||
result = openerp.netsvc.dispatch_rpc(service, method, params, None) # TODO auth
|
||||
result = openerp.netsvc.dispatch_rpc(service, method, params)
|
||||
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)
|
||||
except openerp.exceptions.Warning, e:
|
||||
fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_WARNING, str(e))
|
||||
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))
|
||||
except openerp.exceptions.AccessError, e:
|
||||
fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_ACCESS_ERROR, str(e))
|
||||
response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
|
||||
except openerp.exceptions.AccessDenied, e:
|
||||
fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_ACCESS_DENIED, str(e))
|
||||
response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
|
||||
except openerp.exceptions.DeferredException, e:
|
||||
info = e.traceback
|
||||
# Which one is the best ?
|
||||
formatted_info = "".join(traceback.format_exception(*info))
|
||||
#formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
|
||||
fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_DEFERRED_APPLICATION_ERROR, formatted_info)
|
||||
response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
|
||||
except Exception, e:
|
||||
info = sys.exc_info()
|
||||
# Which one is the best ?
|
||||
formatted_info = "".join(traceback.format_exception(*info))
|
||||
#formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
|
||||
fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
|
||||
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'):
|
||||
if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/openerp/6.1/xmlrpc'):
|
||||
length = int(environ['CONTENT_LENGTH'])
|
||||
data = environ['wsgi.input'].read(length)
|
||||
|
||||
params, method = xmlrpclib.loads(data)
|
||||
|
||||
path = environ['PATH_INFO'][len('/openerp/xmlrpc'):]
|
||||
path = environ['PATH_INFO'][len('/openerp/6.1/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.
|
||||
# All routes are hard-coded.
|
||||
|
||||
# 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)
|
||||
if method in ('server_version',):
|
||||
service = 'db'
|
||||
return xmlrpc_return(start_response, service, 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)
|
||||
service = 'object'
|
||||
return xmlrpc_return(start_response, service, method, params)
|
||||
|
||||
# TODO the body has been read, need to raise an exception (not return None).
|
||||
|
||||
|
@ -108,17 +144,18 @@ def wsgi_jsonrpc(environ, start_response):
|
|||
pass
|
||||
|
||||
def wsgi_webdav(environ, start_response):
|
||||
if environ['REQUEST_METHOD'] == 'OPTIONS' and environ['PATH_INFO'] == '*':
|
||||
pi = environ['PATH_INFO']
|
||||
if environ['REQUEST_METHOD'] == 'OPTIONS' and pi in ['*','/']:
|
||||
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)
|
||||
elif pi.startswith('/webdav'):
|
||||
http_dir = websrv_lib.find_http_service(pi)
|
||||
if http_dir:
|
||||
path = pi[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
|
||||
|
|
|
@ -20,6 +20,10 @@ common_proxy_60 = None
|
|||
db_proxy_60 = None
|
||||
object_proxy_60 = None
|
||||
|
||||
common_proxy_61 = None
|
||||
db_proxy_61 = None
|
||||
model_proxy_61 = None
|
||||
|
||||
def setUpModule():
|
||||
"""
|
||||
Start the OpenERP server similary to the openerp-server script and
|
||||
|
@ -39,6 +43,17 @@ def setUpModule():
|
|||
db_proxy_60 = xmlrpclib.ServerProxy(url + 'db')
|
||||
object_proxy_60 = xmlrpclib.ServerProxy(url + 'object')
|
||||
|
||||
global common_proxy_61
|
||||
global db_proxy_61
|
||||
global model_proxy_61
|
||||
|
||||
# Use the new (6.1) API.
|
||||
url = 'http://%s:%d/openerp/6.1/xmlrpc/' % (HOST, PORT)
|
||||
common_proxy_61 = xmlrpclib.ServerProxy(url + 'common')
|
||||
db_proxy_61 = xmlrpclib.ServerProxy(url + 'db')
|
||||
model_proxy_61 = xmlrpclib.ServerProxy(url + 'model/' + DB)
|
||||
|
||||
|
||||
# Ugly way to ensure the server is listening.
|
||||
time.sleep(2)
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ tearDownModule = common.tearDownModule
|
|||
|
||||
class test_xmlrpc(unittest2.TestCase):
|
||||
|
||||
def test_xmlrpc_create_database_polling(self):
|
||||
def test_00_xmlrpc_create_database_polling(self):
|
||||
"""
|
||||
Simulate a OpenERP client requesting the creation of a database and
|
||||
polling the server until the creation is complete.
|
||||
|
@ -52,6 +52,13 @@ class test_xmlrpc(unittest2.TestCase):
|
|||
'ir.model', 'search', [], {})
|
||||
assert ids
|
||||
|
||||
def test_xmlrpc_61_ir_model_search(self):
|
||||
""" Try a search on the object service. """
|
||||
ids = common.model_proxy_61.execute(ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', [])
|
||||
assert ids
|
||||
ids = common.model_proxy_61.execute(ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', [], {})
|
||||
assert ids
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest2.main()
|
||||
|
||||
|
|
Loading…
Reference in New Issue