[IMP] better connection pool
[odoo/odoo.git] / bin / service / web_services.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #    
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.     
19 #
20 ##############################################################################
21
22 import base64
23 import logging
24 import os
25 import security
26 import string
27 import thread
28 import threading
29 import time
30 import sys
31 import platform
32 from tools.translate import _
33 import addons
34 import ir
35 import netsvc
36 import pooler
37 import release
38 import sql_db
39 import tools
40 import locale
41 logging.basicConfig()
42
43 class db(netsvc.Service):
44     def __init__(self, name="db"):
45         netsvc.Service.__init__(self, name)
46         self.joinGroup("web-services")
47         self.exportMethod(self.create)
48         self.exportMethod(self.get_progress)
49         self.exportMethod(self.drop)
50         self.exportMethod(self.dump)
51         self.exportMethod(self.restore)
52         self.exportMethod(self.rename)
53         self.exportMethod(self.list)
54         self.exportMethod(self.list_lang)
55         self.exportMethod(self.change_admin_password)
56         self.exportMethod(self.server_version)
57         self.exportMethod(self.migrate_databases)
58         self.actions = {}
59         self.id = 0
60         self.id_protect = threading.Semaphore()
61
62         self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
63
64     def create(self, password, db_name, demo, lang, user_password='admin'):
65         security.check_super(password)
66         self.id_protect.acquire()
67         self.id += 1
68         id = self.id
69         self.id_protect.release()
70
71         self.actions[id] = {'clean': False}
72
73         db = sql_db.db_connect('template1')
74         cr = db.cursor()
75         try:
76             cr.autocommit(True) # avoid transaction block
77             cr.execute('CREATE DATABASE "%s" ENCODING \'unicode\'' % db_name)
78         finally:
79             cr.close()
80             del db
81
82         class DBInitialize(object):
83             def __call__(self, serv, id, db_name, demo, lang, user_password='admin'):
84                 cr = None
85                 try:
86                     serv.actions[id]['progress'] = 0
87                     clean = False
88                     cr = sql_db.db_connect(db_name).cursor()
89                     tools.init_db(cr)
90                     cr.commit()
91                     cr.close()
92                     cr = None
93                     pool = pooler.restart_pool(db_name, demo, serv.actions[id],
94                             update_module=True)[1]
95
96                     cr = sql_db.db_connect(db_name).cursor()
97
98                     if lang:
99                         modobj = pool.get('ir.module.module')
100                         mids = modobj.search(cr, 1, [('state', '=', 'installed')])
101                         modobj.update_translations(cr, 1, mids, lang)
102
103                     cr.execute('UPDATE res_users SET password=%s, context_lang=%s, active=True WHERE login=%s', (
104                         user_password, lang, 'admin'))
105                     cr.execute('SELECT login, password, name ' \
106                                '  FROM res_users ' \
107                                ' ORDER BY login')
108                     serv.actions[id]['users'] = cr.dictfetchall()
109                     serv.actions[id]['clean'] = True
110                     cr.commit()
111                     cr.close()
112                 except Exception, e:
113                     serv.actions[id]['clean'] = False
114                     serv.actions[id]['exception'] = e
115                     from cStringIO import StringIO
116                     import traceback
117                     e_str = StringIO()
118                     traceback.print_exc(file=e_str)
119                     traceback_str = e_str.getvalue()
120                     e_str.close()
121                     netsvc.Logger().notifyChannel('web-services', netsvc.LOG_ERROR, 'CREATE DATABASE\n%s' % (traceback_str))
122                     serv.actions[id]['traceback'] = traceback_str
123                     if cr:
124                         cr.close()
125         logger = netsvc.Logger()
126         logger.notifyChannel("web-services", netsvc.LOG_INFO, 'CREATE DATABASE: %s' % (db_name.lower()))
127         dbi = DBInitialize()
128         create_thread = threading.Thread(target=dbi,
129                 args=(self, id, db_name, demo, lang, user_password))
130         create_thread.start()
131         self.actions[id]['thread'] = create_thread
132         return id
133
134     def get_progress(self, password, id):
135         security.check_super(password)
136         if self.actions[id]['thread'].isAlive():
137 #           return addons.init_progress[db_name]
138             return (min(self.actions[id].get('progress', 0),0.95), [])
139         else:
140             clean = self.actions[id]['clean']
141             if clean:
142                 users = self.actions[id]['users']
143                 del self.actions[id]
144                 return (1.0, users)
145             else:
146                 e = self.actions[id]['exception']
147                 del self.actions[id]
148                 raise Exception, e
149
150     def drop(self, password, db_name):
151         security.check_super(password)
152         sql_db.close_db(db_name)
153         logger = netsvc.Logger()
154
155         db = sql_db.db_connect('template1')
156         cr = db.cursor()
157         cr.autocommit(True) # avoid transaction block
158         try:
159             try:
160                 cr.execute('DROP DATABASE "%s"' % db_name)
161             except Exception, e:
162                 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
163                         'DROP DB: %s failed:\n%s' % (db_name, e))
164                 raise Exception("Couldn't drop database %s: %s" % (db_name, e))
165             else:
166                 logger.notifyChannel("web-services", netsvc.LOG_INFO,
167                     'DROP DB: %s' % (db_name))
168         finally:
169             cr.close()
170         return True
171
172     def _set_pg_psw_env_var(self):
173         if os.name == 'nt' and not os.environ.get('PGPASSWORD', ''):
174             os.environ['PGPASSWORD'] = tools.config['db_password']
175             self._pg_psw_env_var_is_set = True
176
177     def _unset_pg_psw_env_var(self):
178         if os.name == 'nt' and self._pg_psw_env_var_is_set:
179             os.environ['PGPASSWORD'] = ''
180
181     def dump(self, password, db_name):
182         security.check_super(password)
183         logger = netsvc.Logger()
184
185         self._set_pg_psw_env_var()
186
187         cmd = ['pg_dump', '--format=c', '--no-owner']
188         if tools.config['db_user']:
189             cmd.append('--username=' + tools.config['db_user'])
190         if tools.config['db_host']:
191             cmd.append('--host=' + tools.config['db_host'])
192         if tools.config['db_port']:
193             cmd.append('--port=' + str(tools.config['db_port']))
194         cmd.append(db_name)
195
196         stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
197         stdin.close()
198         data = stdout.read()
199         res = stdout.close()
200         if res:
201             logger.notifyChannel("web-services", netsvc.LOG_ERROR,
202                     'DUMP DB: %s failed\n%s' % (db_name, data))
203             raise Exception, "Couldn't dump database"
204         logger.notifyChannel("web-services", netsvc.LOG_INFO,
205                 'DUMP DB: %s' % (db_name))
206
207         self._unset_pg_psw_env_var()
208
209         return base64.encodestring(data)
210
211     def restore(self, password, db_name, data):
212         security.check_super(password)
213         logger = netsvc.Logger()
214
215         self._set_pg_psw_env_var()
216
217         if self.db_exist(db_name):
218             logger.notifyChannel("web-services", netsvc.LOG_WARNING,
219                     'RESTORE DB: %s already exists' % (db_name,))
220             raise Exception, "Database already exists"
221
222         db = sql_db.db_connect('template1')
223         cr = db.cursor()
224         cr.autocommit(True) # avoid transaction block
225         try:
226             cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "template0" """ % db_name)
227         finally:
228             cr.close()
229             del db
230
231         cmd = ['pg_restore', '--no-owner']
232         if tools.config['db_user']:
233             cmd.append('--username=' + tools.config['db_user'])
234         if tools.config['db_host']:
235             cmd.append('--host=' + tools.config['db_host'])
236         if tools.config['db_port']:
237             cmd.append('--port=' + str(tools.config['db_port']))
238         cmd.append('--dbname=' + db_name)
239         args2 = tuple(cmd)
240
241         buf=base64.decodestring(data)
242         if os.name == "nt":
243             tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
244             file(tmpfile, 'wb').write(buf)
245             args2=list(args2)
246             args2.append(' ' + tmpfile)
247             args2=tuple(args2)
248         stdin, stdout = tools.exec_pg_command_pipe(*args2)
249         if not os.name == "nt":
250             stdin.write(base64.decodestring(data))
251         stdin.close()
252         res = stdout.close()
253         if res:
254             raise Exception, "Couldn't restore database"
255         logger.notifyChannel("web-services", netsvc.LOG_INFO,
256                 'RESTORE DB: %s' % (db_name))
257
258         self._unset_pg_psw_env_var()
259
260         return True
261
262     def rename(self, password, old_name, new_name):
263         security.check_super(password)
264         sql_db.close_db(old_name)
265         logger = netsvc.Logger()
266
267         db = sql_db.db_connect('template1')
268         cr = db.cursor()
269         try:
270             try:
271                 cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
272             except Exception, e:
273                 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
274                         'RENAME DB: %s -> %s failed:\n%s' % (old_name, new_name, e))
275                 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
276             else:
277                 fs = os.path.join(tools.config['root_path'], 'filestore')
278                 if os.path.exists(os.path.join(fs, old_name)):
279                     os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
280
281                 logger.notifyChannel("web-services", netsvc.LOG_INFO,
282                     'RENAME DB: %s -> %s' % (old_name, new_name))
283         finally:
284             cr.close()
285         return True
286
287     def db_exist(self, db_name):
288         try:
289             db = sql_db.db_connect(db_name)
290             return True
291         except:
292             return False
293
294     def list(self):
295         db = sql_db.db_connect('template1')
296         cr = db.cursor()
297         try:
298             list_db = tools.config["list_db"]
299             if list_db == 'False':
300                 return []
301             try:
302                 db_user = tools.config["db_user"]
303                 if not db_user and os.name == 'posix':
304                     import pwd
305                     db_user = pwd.getpwuid(os.getuid())[0]
306                 if not db_user:
307                     cr.execute("select decode(usename, 'escape') from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
308                     res = cr.fetchone()
309                     db_user = res and str(res[0])
310                 if db_user:
311                     cr.execute("select decode(datname, 'escape') from pg_database where datdba=(select usesysid from pg_user where usename=%s) and datname not in ('template0', 'template1', 'postgres') order by datname", (db_user,))
312                 else:
313                     cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres') order by datname")
314                 res = [str(name) for (name,) in cr.fetchall()]
315             except:
316                 res = []
317         finally:
318             cr.close()
319         res.sort()
320         return res
321
322     def change_admin_password(self, old_password, new_password):
323         security.check_super(old_password)
324         tools.config['admin_passwd'] = new_password
325         tools.config.save()
326         return True
327
328     def list_lang(self):
329         return tools.scan_languages()
330
331     def server_version(self):
332         """ Return the version of the server
333             Used by the client to verify the compatibility with its own version
334         """
335         return release.version
336
337     def migrate_databases(self, password, databases):
338
339         from osv.orm import except_orm
340         from osv.osv import except_osv
341
342         security.check_super(password)
343         l = netsvc.Logger()
344         for db in databases:
345             try:
346                 l.notifyChannel('migration', netsvc.LOG_INFO, 'migrate database %s' % (db,))
347                 tools.config['update']['base'] = True
348                 pooler.restart_pool(db, force_demo=False, update_module=True)
349             except except_orm, inst:
350                 self.abortResponse(1, inst.name, 'warning', inst.value)
351             except except_osv, inst:
352                 self.abortResponse(1, inst.name, inst.exc_type, inst.value)
353             except Exception, e:
354                 import traceback
355                 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
356                 l.notifyChannel('web-services', netsvc.LOG_ERROR, tb_s)
357                 raise
358         return True
359 db()
360
361 class common(netsvc.Service):
362     def __init__(self,name="common"):
363         netsvc.Service.__init__(self,name)
364         self.joinGroup("web-services")
365         self.exportMethod(self.ir_get)
366         self.exportMethod(self.ir_set)
367         self.exportMethod(self.ir_del)
368         self.exportMethod(self.about)
369         self.exportMethod(self.login)
370         self.exportMethod(self.logout)
371         self.exportMethod(self.timezone_get)
372         self.exportMethod(self.get_available_updates)
373         self.exportMethod(self.get_migration_scripts)
374         self.exportMethod(self.get_server_environment)
375         self.exportMethod(self.login_message)
376
377     def ir_set(self, db, uid, password, keys, args, name, value, replace=True, isobject=False):
378         security.check(db, uid, password)
379         cr = pooler.get_db(db).cursor()
380         res = ir.ir_set(cr,uid, keys, args, name, value, replace, isobject)
381         cr.commit()
382         cr.close()
383         return res
384
385     def ir_del(self, db, uid, password, id):
386         security.check(db, uid, password)
387         cr = pooler.get_db(db).cursor()
388         res = ir.ir_del(cr,uid, id)
389         cr.commit()
390         cr.close()
391         return res
392
393     def ir_get(self, db, uid, password, keys, args=None, meta=None, context=None):
394         if not args:
395             args=[]
396         if not context:
397             context={}
398         security.check(db, uid, password)
399         cr = pooler.get_db(db).cursor()
400         res = ir.ir_get(cr,uid, keys, args, meta, context)
401         cr.commit()
402         cr.close()
403         return res
404
405     def login(self, db, login, password):
406         res = security.login(db, login, password)
407         logger = netsvc.Logger()
408         msg = res and 'successful login' or 'bad login or password'
409         logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, login, db.lower()))
410         return res or False
411
412     def logout(self, db, login, password):
413         logger = netsvc.Logger()
414         logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
415         return True
416
417     def about(self, extended=False):
418         """Return information about the OpenERP Server.
419
420         @param extended: if True then return version info
421         @return string if extended is False else tuple
422         """
423
424         info = _('''
425
426 OpenERP is an ERP+CRM program for small and medium businesses.
427
428 The whole source code is distributed under the terms of the
429 GNU Public Licence.
430
431 (c) 2003-TODAY, Fabien Pinckaers - Tiny sprl''')
432
433         if extended:
434             return info, release.version
435         return info
436
437     def timezone_get(self, db, login, password):
438         return time.tzname[0]
439
440
441     def get_available_updates(self, password, contract_id, contract_password):
442         security.check_super(password)
443         import tools.maintenance as tm
444         try:
445             rc = tm.remote_contract(contract_id, contract_password)
446             if not rc.id:
447                 raise tm.RemoteContractException('This contract does not exist or is not active')
448
449             return rc.get_available_updates(rc.id, addons.get_modules_with_version())
450
451         except tm.RemoteContractException, e:
452             self.abortResponse(1, 'Migration Error', 'warning', str(e))
453
454
455     def get_migration_scripts(self, password, contract_id, contract_password):
456         security.check_super(password)
457         l = netsvc.Logger()
458         import tools.maintenance as tm
459         try:
460             rc = tm.remote_contract(contract_id, contract_password)
461             if not rc.id:
462                 raise tm.RemoteContractException('This contract does not exist or is not active')
463             if rc.status != 'full':
464                 raise tm.RemoteContractException('Can not get updates for a partial contract')
465
466             l.notifyChannel('migration', netsvc.LOG_INFO, 'starting migration with contract %s' % (rc.name,))
467
468             zips = rc.retrieve_updates(rc.id, addons.get_modules_with_version())
469
470             from shutil import rmtree, copytree, copy
471
472             backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
473             if zips and not os.path.isdir(backup_directory):
474                 l.notifyChannel('migration', netsvc.LOG_INFO, 'create a new backup directory to \
475                                 store the old modules: %s' % (backup_directory,))
476                 os.makedirs(backup_directory)
477
478             for module in zips:
479                 l.notifyChannel('migration', netsvc.LOG_INFO, 'upgrade module %s' % (module,))
480                 mp = addons.get_module_path(module)
481                 if mp:
482                     if os.path.isdir(mp):
483                         copytree(mp, os.path.join(backup_directory, module))
484                         if os.path.islink(mp):
485                             os.unlink(mp)
486                         else:
487                             rmtree(mp)
488                     else:
489                         copy(mp + 'zip', backup_directory)
490                         os.unlink(mp + '.zip')
491
492                 try:
493                     try:
494                         base64_decoded = base64.decodestring(zips[module])
495                     except:
496                         l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to read the module %s' % (module,))
497                         raise
498
499                     zip_contents = cStringIO.StringIO(base64_decoded)
500                     zip_contents.seek(0)
501                     try:
502                         try:
503                             tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
504                         except:
505                             l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to extract the module %s' % (module, ))
506                             rmtree(module)
507                             raise
508                     finally:
509                         zip_contents.close()
510                 except:
511                     l.notifyChannel('migration', netsvc.LOG_ERROR, 'restore the previous version of the module %s' % (module, ))
512                     nmp = os.path.join(backup_directory, module)
513                     if os.path.isdir(nmp):
514                         copytree(nmp, tools.config['addons_path'])
515                     else:
516                         copy(nmp+'.zip', tools.config['addons_path'])
517                     raise
518
519             return True
520         except tm.RemoteContractException, e:
521             self.abortResponse(1, 'Migration Error', 'warning', str(e))
522         except Exception, e:
523             import traceback
524             tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
525             l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s)
526             raise
527
528     def get_server_environment(self):
529         try:
530             rev_id = os.popen('bzr revision-info').read()
531         except Exception,e:
532              rev_id = 'Exception: %s\n' % (tools.ustr(e))
533
534         os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
535         if not os_lang:
536             os_lang = 'NOT SET'
537         environment = '\nEnvironment Information : \n' \
538                      'System : %s\n' \
539                      'OS Name : %s\n' \
540                      %(platform.platform(), platform.os.name)
541         if os.name == 'posix':
542           if platform.system() == 'Linux':
543              lsbinfo = os.popen('lsb_release -a').read()
544              environment += '%s'%(lsbinfo)
545           else:
546              environment += 'Your System is not lsb compliant\n'
547         environment += 'Operating System Release : %s\n' \
548                     'Operating System Version : %s\n' \
549                     'Operating System Architecture : %s\n' \
550                     'Operating System Locale : %s\n'\
551                     'Python Version : %s\n'\
552                     'OpenERP-Server Version : %s\n'\
553                     'Last revision No. & ID : %s'\
554                     %(platform.release(), platform.version(), platform.architecture()[0],
555                       os_lang, platform.python_version(),release.version,rev_id)
556         return environment
557     
558
559     def login_message(self):
560         return tools.config.get('login_message', False)
561
562 common()
563
564 class objects_proxy(netsvc.Service):
565     def __init__(self, name="object"):
566         netsvc.Service.__init__(self,name)
567         self.joinGroup('web-services')
568         self.exportMethod(self.execute)
569         self.exportMethod(self.exec_workflow)
570         self.exportMethod(self.obj_list)
571
572     def exec_workflow(self, db, uid, passwd, object, method, id):
573         security.check(db, uid, passwd)
574         service = netsvc.LocalService("object_proxy")
575         res = service.exec_workflow(db, uid, object, method, id)
576         return res
577
578     def execute(self, db, uid, passwd, object, method, *args):
579         security.check(db, uid, passwd)
580         service = netsvc.LocalService("object_proxy")
581         res = service.execute(db, uid, object, method, *args)
582         return res
583
584     def obj_list(self, db, uid, passwd):
585         security.check(db, uid, passwd)
586         service = netsvc.LocalService("object_proxy")
587         res = service.obj_list()
588         return res
589 objects_proxy()
590
591
592 #
593 # Wizard ID: 1
594 #    - None = end of wizard
595 #
596 # Wizard Type: 'form'
597 #    - form
598 #    - print
599 #
600 # Wizard datas: {}
601 # TODO: change local request to OSE request/reply pattern
602 #
603 class wizard(netsvc.Service):
604     def __init__(self, name='wizard'):
605         netsvc.Service.__init__(self,name)
606         self.joinGroup('web-services')
607         self.exportMethod(self.execute)
608         self.exportMethod(self.create)
609         self.id = 0
610         self.wiz_datas = {}
611         self.wiz_name = {}
612         self.wiz_uid = {}
613
614     def _execute(self, db, uid, wiz_id, datas, action, context):
615         self.wiz_datas[wiz_id].update(datas)
616         wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
617         return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
618
619     def create(self, db, uid, passwd, wiz_name, datas=None):
620         if not datas:
621             datas={}
622         security.check(db, uid, passwd)
623 #FIXME: this is not thread-safe
624         self.id += 1
625         self.wiz_datas[self.id] = {}
626         self.wiz_name[self.id] = wiz_name
627         self.wiz_uid[self.id] = uid
628         return self.id
629
630     def execute(self, db, uid, passwd, wiz_id, datas, action='init', context=None):
631         if not context:
632             context={}
633         security.check(db, uid, passwd)
634
635         if wiz_id in self.wiz_uid:
636             if self.wiz_uid[wiz_id] == uid:
637                 return self._execute(db, uid, wiz_id, datas, action, context)
638             else:
639                 raise Exception, 'AccessDenied'
640         else:
641             raise Exception, 'WizardNotFound'
642 wizard()
643
644 #
645 # TODO: set a maximum report number per user to avoid DOS attacks
646 #
647 # Report state:
648 #     False -> True
649 #
650
651 class ExceptionWithTraceback(Exception):
652     def __init__(self, msg, tb):
653         self.message = msg
654         self.traceback = tb
655         self.args = (msg, tb)
656
657 class report_spool(netsvc.Service):
658     def __init__(self, name='report'):
659         netsvc.Service.__init__(self, name)
660         self.joinGroup('web-services')
661         self.exportMethod(self.report)
662         self.exportMethod(self.report_get)
663         self._reports = {}
664         self.id = 0
665         self.id_protect = threading.Semaphore()
666
667     def report(self, db, uid, passwd, object, ids, datas=None, context=None):
668         if not datas:
669             datas={}
670         if not context:
671             context={}
672         security.check(db, uid, passwd)
673
674         self.id_protect.acquire()
675         self.id += 1
676         id = self.id
677         self.id_protect.release()
678
679         self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
680
681         def go(id, uid, ids, datas, context):
682             cr = pooler.get_db(db).cursor()
683             try:
684                 obj = netsvc.LocalService('report.'+object)
685                 (result, format) = obj.create(cr, uid, ids, datas, context)
686                 self._reports[id]['result'] = result
687                 self._reports[id]['format'] = format
688                 self._reports[id]['state'] = True
689             except Exception, exception:
690                 import traceback
691                 import sys
692                 tb = sys.exc_info()
693                 tb_s = "".join(traceback.format_exception(*tb))
694                 logger = netsvc.Logger()
695                 logger.notifyChannel('web-services', netsvc.LOG_ERROR,
696                         'Exception: %s\n%s' % (str(exception), tb_s))
697                 self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
698                 self._reports[id]['state'] = True
699             cr.commit()
700             cr.close()
701             return True
702
703         thread.start_new_thread(go, (id, uid, ids, datas, context))
704         return id
705
706     def _check_report(self, report_id):
707         result = self._reports[report_id]
708         if result['exception']:
709             raise result['exception']
710         res = {'state': result['state']}
711         if res['state']:
712             if tools.config['reportgz']:
713                 import zlib
714                 res2 = zlib.compress(result['result'])
715                 res['code'] = 'zlib'
716             else:
717                 #CHECKME: why is this needed???
718                 if isinstance(result['result'], unicode):
719                     res2 = result['result'].encode('latin1', 'replace')
720                 else:
721                     res2 = result['result']
722             if res2:
723                 res['result'] = base64.encodestring(res2)
724             res['format'] = result['format']
725             del self._reports[report_id]
726         return res
727
728     def report_get(self, db, uid, passwd, report_id):
729         security.check(db, uid, passwd)
730
731         if report_id in self._reports:
732             if self._reports[report_id]['uid'] == uid:
733                 return self._check_report(report_id)
734             else:
735                 raise Exception, 'AccessDenied'
736         else:
737             raise Exception, 'ReportNotFound'
738
739 report_spool()
740
741
742 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
743