Switch to WSGI API
issue4646 review20541003
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Switch to WSGI API
|
||||
* Limit the login size in LoginAttempt
|
||||
* Remove LocalDict from tools
|
||||
* Add LRUDictTransaction
|
||||
|
||||
+38
-68
@@ -1,80 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import glob
|
||||
|
||||
from werkzeug.serving import run_simple
|
||||
|
||||
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
|
||||
'..', '..', 'trytond')))
|
||||
if os.path.isdir(DIR):
|
||||
sys.path.insert(0, os.path.dirname(DIR))
|
||||
|
||||
from trytond import __version__
|
||||
from trytond import server
|
||||
import trytond.commandline as commandline
|
||||
from trytond.config import config, split_netloc
|
||||
from trytond.application import app
|
||||
from trytond.pool import Pool
|
||||
from trytond.modules import get_module_list, get_module_info
|
||||
|
||||
|
||||
def parse_commandline():
|
||||
options = {}
|
||||
parser = commandline.get_parser_daemon()
|
||||
options = parser.parse_args()
|
||||
commandline.config_log(options)
|
||||
config.update_etc(options.configfile)
|
||||
|
||||
parser = argparse.ArgumentParser(prog='trytond')
|
||||
|
||||
parser.add_argument('--version', action='version',
|
||||
version='%(prog)s ' + __version__)
|
||||
parser.add_argument("-c", "--config", dest="configfile", metavar='FILE',
|
||||
default=os.environ.get('TRYTOND_CONFIG'), help="specify config file")
|
||||
parser.add_argument('--dev', dest='dev', action='store_true',
|
||||
help='enable development mode')
|
||||
parser.add_argument("-v", "--verbose", action="store_true",
|
||||
dest="verbose", help="enable verbose mode")
|
||||
|
||||
parser.add_argument("-d", "--database", dest="database_names", nargs='+',
|
||||
default=[], metavar='DATABASE', help="specify the database name")
|
||||
parser.add_argument("-u", "--update", dest="update", nargs='+', default=[],
|
||||
metavar='MODULE', help="update a module")
|
||||
parser.add_argument("--all", dest="update", action="append_const",
|
||||
const="ir", help="update all installed modules")
|
||||
|
||||
parser.add_argument("--pidfile", dest="pidfile", metavar='FILE',
|
||||
help="file where the server pid will be stored")
|
||||
parser.add_argument("--logconf", dest="logconf", metavar='FILE',
|
||||
help="logging configuration file (ConfigParser format)")
|
||||
parser.add_argument("--cron", dest="cron", action="store_true",
|
||||
help="enable cron")
|
||||
|
||||
parser.epilog = ('The first time a database is initialized admin '
|
||||
'password is read from file defined by TRYTONPASSFILE '
|
||||
'environment variable or interactively ask user.\n'
|
||||
'The config file can be specified in the TRYTOND_CONFIG '
|
||||
'environment variable.\n'
|
||||
'The database URI can be specified in the TRYTOND_DATABASE_URI '
|
||||
'environment variable.')
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
if not options.database_names and options.update:
|
||||
parser.error('Missing database option')
|
||||
return options
|
||||
|
||||
|
||||
if '--profile' in sys.argv:
|
||||
import profile
|
||||
import pstats
|
||||
import tempfile
|
||||
sys.argv.remove('--profile')
|
||||
|
||||
options = parse_commandline()
|
||||
statfile = tempfile.mkstemp(".stat", "trytond-")[1]
|
||||
profile.run('server.TrytonServer(options).run()', statfile)
|
||||
s = pstats.Stats(statfile)
|
||||
s.sort_stats('cumulative').print_stats()
|
||||
s.sort_stats('call').print_stats()
|
||||
s.sort_stats('time').print_stats()
|
||||
s.sort_stats('time')
|
||||
s.print_callers()
|
||||
s.print_callees()
|
||||
|
||||
os.remove(statfile)
|
||||
else:
|
||||
options = parse_commandline()
|
||||
server.TrytonServer(options).run()
|
||||
with commandline.pidfile(options):
|
||||
for name in options.database_names:
|
||||
Pool(name).init()
|
||||
hostname, port = split_netloc(config.get('web', 'listen'))
|
||||
static_files = {
|
||||
'/': config.get('web', 'root'),
|
||||
}
|
||||
certificate = config.get('ssl', 'certificate')
|
||||
privatekey = config.get('ssl', 'privatekey')
|
||||
if certificate or privatekey:
|
||||
ssl_context = (certificate, privatekey)
|
||||
else:
|
||||
ssl_context = None
|
||||
extra_files = [options.configfile]
|
||||
for module in get_module_list():
|
||||
info = get_module_info(module)
|
||||
path = os.path.join(info['directory'], 'view', '*.xml')
|
||||
extra_files.extend(glob.glob(path))
|
||||
run_simple(hostname, port, app,
|
||||
threaded=True,
|
||||
extra_files=extra_files,
|
||||
static_files=static_files,
|
||||
ssl_context=ssl_context,
|
||||
use_reloader=options.dev)
|
||||
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import sys
|
||||
import os
|
||||
|
||||
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
|
||||
'..', '..', 'trytond')))
|
||||
if os.path.isdir(DIR):
|
||||
sys.path.insert(0, os.path.dirname(DIR))
|
||||
|
||||
import trytond.commandline as commandline
|
||||
from trytond.config import config
|
||||
import trytond.admin as admin
|
||||
|
||||
parser = commandline.get_parser_admin()
|
||||
options = parser.parse_args()
|
||||
config.update_etc(options.configfile)
|
||||
commandline.config_log(options)
|
||||
|
||||
admin.run(options)
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
|
||||
'..', '..', 'trytond')))
|
||||
if os.path.isdir(DIR):
|
||||
sys.path.insert(0, os.path.dirname(DIR))
|
||||
|
||||
import trytond.commandline as commandline
|
||||
from trytond.config import config
|
||||
import trytond.cron as cron
|
||||
|
||||
parser = commandline.get_parser_daemon()
|
||||
options = parser.parse_args()
|
||||
config.update_etc(options.configfile)
|
||||
commandline.config_log(options)
|
||||
|
||||
with commandline.pidfile(options):
|
||||
while True:
|
||||
cron.run(options)
|
||||
time.sleep(60)
|
||||
@@ -27,6 +27,7 @@ First steps
|
||||
:ref:`Installation <topics-install>` |
|
||||
:ref:`Configuration <topics-configuration>` |
|
||||
:ref:`Setup a database <topics-setup-database>`
|
||||
:ref:`Start the server <topics-start-server>`
|
||||
|
||||
The model layer
|
||||
===============
|
||||
|
||||
@@ -29,47 +29,31 @@ possible values.
|
||||
Some modules could request the usage of other sections for which the guideline
|
||||
asks them to be named like their module.
|
||||
|
||||
jsonrpc
|
||||
-------
|
||||
web
|
||||
---
|
||||
|
||||
Defines the behavior of the JSON-RPC_ network interface.
|
||||
Defines the behavior of the web interface.
|
||||
|
||||
listen
|
||||
~~~~~~
|
||||
|
||||
Defines a comma separated list of couples of host (or IP address) and port
|
||||
number separated by a colon to listen on.
|
||||
Defines the couple of host (or IP address) and port number separated by a colon
|
||||
to listen on.
|
||||
|
||||
Default `localhost:8000`
|
||||
|
||||
hostname
|
||||
~~~~~~~~
|
||||
|
||||
Defines the hostname for this network interface.
|
||||
Defines the hostname.
|
||||
|
||||
data
|
||||
root
|
||||
~~~~
|
||||
|
||||
Defines the root path to retrieve data for `GET` requests.
|
||||
Defines the root path served by `GET` requests.
|
||||
|
||||
Default: `/var/www/localhost/tryton`
|
||||
|
||||
xmlrpc
|
||||
------
|
||||
|
||||
Defines the behavior of the XML-RPC_ network interface.
|
||||
|
||||
listen
|
||||
~~~~~~
|
||||
|
||||
Same as for `jsonrpc` except it has no default value.
|
||||
|
||||
|
||||
listen
|
||||
~~~~~~
|
||||
|
||||
Same as for `jsonrpc` except it has no default value.
|
||||
|
||||
database
|
||||
--------
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ Introduction to all the key parts of trytond:
|
||||
configuration
|
||||
setup_database
|
||||
logs
|
||||
start_server
|
||||
models/index
|
||||
models/fields_default_value
|
||||
models/fields_on_change
|
||||
|
||||
@@ -8,6 +8,8 @@ Prerequisites
|
||||
=============
|
||||
|
||||
* Python 2.7 or later (http://www.python.org/)
|
||||
* Werkzeug (http://werkzeug.pocoo.org/)
|
||||
* wrapt (https://github.com/GrahamDumpleton/wrapt)
|
||||
* lxml 2.0 or later (http://lxml.de/)
|
||||
* relatorio 0.2.0 or later (http://code.google.com/p/python-relatorio/)
|
||||
* Genshi (http://genshi.edgewall.org/)
|
||||
|
||||
@@ -20,7 +20,7 @@ Initialize a database
|
||||
|
||||
A database can be initialized using this command line::
|
||||
|
||||
trytond -c <config file> -d <database name> --all
|
||||
trytond-admin -c <config file> -d <database name> --all
|
||||
|
||||
At the end of the process, `trytond` will ask to set the password for the
|
||||
At the end of the process, `trytond-admin` will ask to set the password for the
|
||||
`admin` user.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
.. _topics-start-server:
|
||||
|
||||
=======================
|
||||
How to start the server
|
||||
=======================
|
||||
|
||||
Web service
|
||||
===========
|
||||
|
||||
You can start the default web server bundled in Tryton with this command line::
|
||||
|
||||
trytond -c <config file>
|
||||
|
||||
The server will wait for client connections on the interface defined in the
|
||||
`web` section of the :ref:`configuration <topics-configuration>`.
|
||||
|
||||
WSGI server
|
||||
-----------
|
||||
|
||||
If you prefer to run Tryton inside your own WSGI server instead of the simple
|
||||
server of Werkzeug, you can use the application `trytond.application.app` and
|
||||
set the environment variable `TRYTOND_CONFIG` to point to the
|
||||
:ref:`configuration <topics-configuration>`.
|
||||
|
||||
.. warning:: You must manage to serve the static files from the web root.
|
||||
|
||||
Cron service
|
||||
============
|
||||
|
||||
If you want to run some scheduled actions, you must also run the cron server
|
||||
with this command line::
|
||||
|
||||
trytond-cron -c <config file> -d <database>
|
||||
|
||||
The server will wake up every minutes and preform the scheduled actions defined
|
||||
in the `database`.
|
||||
|
||||
Services options
|
||||
================
|
||||
|
||||
You will find more options for those services by using `--help` arguments.
|
||||
@@ -59,7 +59,7 @@ setup(name=name,
|
||||
'trytond.res': ['tryton.cfg', '*.xml', 'view/*.xml', 'locale/*.po'],
|
||||
'trytond.tests': ['tryton.cfg', '*.xml'],
|
||||
},
|
||||
scripts=['bin/trytond'],
|
||||
scripts=['bin/trytond', 'bin/trytond-admin', 'bin/trytond-cron'],
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: No Input/Output (Daemon)',
|
||||
@@ -97,6 +97,8 @@ setup(name=name,
|
||||
'python-dateutil',
|
||||
'polib',
|
||||
'python-sql >= 0.4',
|
||||
'werkzeug',
|
||||
'wrapt',
|
||||
],
|
||||
extras_require={
|
||||
'PostgreSQL': pg_require,
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from email import charset
|
||||
|
||||
__version__ = "3.9.0"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
os.environ['TZ'] = 'UTC'
|
||||
if hasattr(time, 'tzset'):
|
||||
time.tzset()
|
||||
|
||||
if time.tzname[0] != 'UTC':
|
||||
logger.error('Timezone must be set to UTC instead of %s', time.tzname[0])
|
||||
|
||||
# set email encoding for utf-8 to 'quoted-printable'
|
||||
charset.add_charset('utf-8', charset.QP, charset.QP)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from getpass import getpass
|
||||
|
||||
from sql import Table
|
||||
|
||||
from trytond.transaction import Transaction
|
||||
from trytond import backend
|
||||
from trytond.pool import Pool
|
||||
|
||||
__all__ = ['run']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run(options):
|
||||
Database = backend.get('Database')
|
||||
init = {}
|
||||
for db_name in options.database_names:
|
||||
init[db_name] = False
|
||||
with Transaction().start(db_name, 0):
|
||||
database = Database(db_name)
|
||||
database.connect()
|
||||
if options.update:
|
||||
if not database.test():
|
||||
logger.info("init db")
|
||||
database.init()
|
||||
init[db_name] = True
|
||||
elif not database.test():
|
||||
raise Exception("'%s' is not a Tryton database!" % db_name)
|
||||
|
||||
for db_name in options.database_names:
|
||||
if options.update:
|
||||
with Transaction().start(db_name, 0) as transaction,\
|
||||
transaction.connection.cursor() as cursor:
|
||||
database = Database(db_name)
|
||||
database.connect()
|
||||
if not database.test():
|
||||
raise Exception("'%s' is not a Tryton database!" % db_name)
|
||||
lang = Table('ir_lang')
|
||||
cursor.execute(*lang.select(lang.code,
|
||||
where=lang.translatable == True))
|
||||
lang = [x[0] for x in cursor.fetchall()]
|
||||
else:
|
||||
lang = None
|
||||
Pool(db_name).init(update=options.update, lang=lang)
|
||||
|
||||
for db_name in options.database_names:
|
||||
if init[db_name]:
|
||||
# try to read password from environment variable
|
||||
# TRYTONPASSFILE, empty TRYTONPASSFILE ignored
|
||||
passpath = os.getenv('TRYTONPASSFILE')
|
||||
password = ''
|
||||
if passpath:
|
||||
try:
|
||||
with open(passpath) as passfile:
|
||||
password = passfile.readline()[:-1]
|
||||
except Exception, err:
|
||||
sys.stderr.write('Can not read password '
|
||||
'from "%s": "%s"\n' % (passpath, err))
|
||||
|
||||
if not password:
|
||||
while True:
|
||||
password = getpass('Admin Password for %s: ' % db_name)
|
||||
password2 = getpass('Admin Password Confirmation: ')
|
||||
if password != password2:
|
||||
sys.stderr.write('Admin Password Confirmation '
|
||||
'doesn\'t match Admin Password!\n')
|
||||
continue
|
||||
if not password:
|
||||
sys.stderr.write('Admin Password is required!\n')
|
||||
continue
|
||||
break
|
||||
|
||||
with Transaction().start(db_name, 0) as transaction:
|
||||
pool = Pool()
|
||||
User = pool.get('res.user')
|
||||
admin, = User.search([('login', '=', 'admin')])
|
||||
User.write([admin], {
|
||||
'password': password,
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from trytond.pool import Pool
|
||||
from trytond.wsgi import app
|
||||
|
||||
__all__ = ['app']
|
||||
|
||||
Pool.start()
|
||||
import trytond.protocols.dispatcher
|
||||
@@ -0,0 +1,88 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import argparse
|
||||
import os
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
from contextlib import contextmanager
|
||||
|
||||
from trytond import __version__
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_parser():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument('--version', action='version',
|
||||
version='%(prog)s ' + __version__)
|
||||
parser.add_argument("-c", "--config", dest="configfile", metavar='FILE',
|
||||
default=os.environ.get('TRYTOND_CONFIG'), help="specify config file")
|
||||
parser.add_argument("-v", "--verbose", action="store_true",
|
||||
dest="verbose", help="enable verbose mode")
|
||||
parser.add_argument('--dev', dest='dev', action='store_true',
|
||||
help='enable development mode')
|
||||
|
||||
parser.add_argument("-d", "--database", dest="database_names", nargs='+',
|
||||
default=[], metavar='DATABASE', help="specify the database name")
|
||||
parser.add_argument("--logconf", dest="logconf", metavar='FILE',
|
||||
help="logging configuration file (ConfigParser format)")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def get_parser_daemon():
|
||||
parser = get_parser()
|
||||
parser.add_argument("--pidfile", dest="pidfile", metavar='FILE',
|
||||
help="file where the server pid will be stored")
|
||||
return parser
|
||||
|
||||
|
||||
def get_parser_admin():
|
||||
parser = get_parser()
|
||||
|
||||
parser.add_argument("-u", "--update", dest="update", nargs='+', default=[],
|
||||
metavar='MODULE', help="update a module")
|
||||
parser.add_argument("--all", dest="update", action="append_const",
|
||||
const="ir", help="update all installed modules")
|
||||
|
||||
parser.epilog = ('The first time a database is initialized admin '
|
||||
'password is read from file defined by TRYTONPASSFILE '
|
||||
'environment variable or interactively ask user.\n'
|
||||
'The config file can be specified in the TRYTOND_CONFIG '
|
||||
'environment variable.\n'
|
||||
'The database URI can be specified in the TRYTOND_DATABASE_URI '
|
||||
'environment variable.')
|
||||
return parser
|
||||
|
||||
|
||||
def config_log(options):
|
||||
if options.logconf:
|
||||
logging.config.fileConfig(options.logconf)
|
||||
logging.getLogger('server').info('using %s as logging '
|
||||
'configuration file', options.logconf)
|
||||
else:
|
||||
logformat = ('%(process)s %(thread)s [%(asctime)s] '
|
||||
'%(levelname)s %(name)s %(message)s')
|
||||
if options.verbose:
|
||||
if options.dev:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.INFO
|
||||
else:
|
||||
level = logging.ERROR
|
||||
logging.basicConfig(level=level, format=logformat)
|
||||
logging.captureWarnings(True)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def pidfile(options):
|
||||
path = options.pidfile
|
||||
if not path:
|
||||
yield
|
||||
else:
|
||||
with open(path, 'w') as fd:
|
||||
fd.write('%d' % os.getpid())
|
||||
yield
|
||||
os.unlink(path)
|
||||
+7
-4
@@ -3,9 +3,11 @@
|
||||
import os
|
||||
import ConfigParser
|
||||
import urlparse
|
||||
import logging
|
||||
|
||||
__all__ = ['config', 'get_hostname', 'get_port', 'split_netloc',
|
||||
'parse_listen', 'parse_uri']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_hostname(netloc):
|
||||
@@ -39,10 +41,9 @@ class TrytonConfigParser(ConfigParser.RawConfigParser):
|
||||
|
||||
def __init__(self):
|
||||
ConfigParser.RawConfigParser.__init__(self)
|
||||
self.add_section('jsonrpc')
|
||||
self.set('jsonrpc', 'listen', 'localhost:8000')
|
||||
self.set('jsonrpc', 'data', '/var/www/localhost/tryton')
|
||||
self.add_section('xmlrpc')
|
||||
self.add_section('web')
|
||||
self.set('web', 'listen', 'localhost:8000')
|
||||
self.set('web', 'root', '/var/www/localhost/tryton')
|
||||
self.add_section('database')
|
||||
self.set('database', 'uri',
|
||||
os.environ.get('TRYTOND_DATABASE_URI', 'sqlite://'))
|
||||
@@ -62,10 +63,12 @@ class TrytonConfigParser(ConfigParser.RawConfigParser):
|
||||
self.add_section('report')
|
||||
self.set('report', 'unoconv',
|
||||
'pipe,name=trytond;urp;StarOffice.ComponentContext')
|
||||
self.update_etc()
|
||||
|
||||
def update_etc(self, configfile=os.environ.get('TRYTOND_CONFIG')):
|
||||
if not configfile:
|
||||
return
|
||||
logger.info('using %s as configuration file', configfile)
|
||||
self.read(configfile)
|
||||
|
||||
def get(self, section, option, *args, **kwargs):
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import threading
|
||||
import logging
|
||||
|
||||
from trytond.pool import Pool
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
__all__ = ['run']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run(options):
|
||||
database_list = Pool.database_list()
|
||||
for dbname in options.database_names:
|
||||
thread = _threads.get(dbname)
|
||||
if thread and thread.is_alive():
|
||||
logger.info('skip "%s" as previous cron still running', dbname)
|
||||
continue
|
||||
pool = Pool(dbname)
|
||||
if dbname not in database_list:
|
||||
with Transaction().start(dbname, 0, readonly=True):
|
||||
pool.init()
|
||||
if not pool.lock.acquire(0):
|
||||
logger.warning('can not acquire lock on "%s"', dbname)
|
||||
continue
|
||||
try:
|
||||
try:
|
||||
Cron = pool.get('ir.cron')
|
||||
except KeyError:
|
||||
logger.error(
|
||||
'missing "ir.cron" on "%s"', dbname, exc_info=True)
|
||||
continue
|
||||
finally:
|
||||
pool.lock.release()
|
||||
thread = threading.Thread(
|
||||
target=Cron.run,
|
||||
args=(dbname,), kwargs={})
|
||||
logger.info('start thread for "%s"', dbname)
|
||||
thread.start()
|
||||
_threads[dbname] = thread
|
||||
_threads = {}
|
||||
Pool.start()
|
||||
@@ -2,7 +2,11 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
|
||||
class UserError(Exception):
|
||||
class TrytonException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserError(TrytonException):
|
||||
|
||||
def __init__(self, message, description=''):
|
||||
super(UserError, self).__init__('UserError', (message, description))
|
||||
@@ -11,7 +15,7 @@ class UserError(Exception):
|
||||
self.code = 1
|
||||
|
||||
|
||||
class UserWarning(Exception):
|
||||
class UserWarning(TrytonException):
|
||||
|
||||
def __init__(self, name, message, description=''):
|
||||
super(UserWarning, self).__init__('UserWarning', (name, message,
|
||||
@@ -22,14 +26,14 @@ class UserWarning(Exception):
|
||||
self.code = 2
|
||||
|
||||
|
||||
class NotLogged(Exception):
|
||||
class NotLogged(TrytonException):
|
||||
|
||||
def __init__(self):
|
||||
super(NotLogged, self).__init__('NotLogged')
|
||||
self.code = 3
|
||||
|
||||
|
||||
class ConcurrencyException(Exception):
|
||||
class ConcurrencyException(TrytonException):
|
||||
|
||||
def __init__(self, message):
|
||||
super(ConcurrencyException, self).__init__('ConcurrencyException',
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from threading import Lock
|
||||
from trytond.modules import get_module_list
|
||||
|
||||
_LOCK = Lock()
|
||||
_TIMES = {}
|
||||
_MODULES = None
|
||||
|
||||
|
||||
def _modified(path):
|
||||
_LOCK.acquire()
|
||||
try:
|
||||
try:
|
||||
if not os.path.isfile(path):
|
||||
return path in _TIMES
|
||||
|
||||
mtime = os.stat(path).st_mtime
|
||||
if path not in _TIMES:
|
||||
_TIMES[path] = mtime
|
||||
|
||||
if mtime != _TIMES[path]:
|
||||
_TIMES[path] = mtime
|
||||
return True
|
||||
except Exception:
|
||||
return True
|
||||
finally:
|
||||
_LOCK.release()
|
||||
return False
|
||||
|
||||
|
||||
def monitor(files):
|
||||
'''
|
||||
Monitor files and module files for change
|
||||
|
||||
:return: True if at least one file has changed
|
||||
'''
|
||||
global _MODULES
|
||||
modified = False
|
||||
for file_ in files:
|
||||
if _modified(file_):
|
||||
modified = True
|
||||
directories = set()
|
||||
for module in sys.modules.keys():
|
||||
if not module.startswith('trytond.'):
|
||||
continue
|
||||
if not hasattr(sys.modules[module], '__file__'):
|
||||
continue
|
||||
path = getattr(sys.modules[module], '__file__')
|
||||
if not path:
|
||||
continue
|
||||
if os.path.splitext(path)[1] in ['.pyc', '.pyo', '.pyd']:
|
||||
path = path[:-1]
|
||||
if _modified(path):
|
||||
if subprocess.call((sys.executable, '-c', 'import %s' % module),
|
||||
cwd=os.path.dirname(os.path.abspath(os.path.normpath(
|
||||
os.path.join(__file__, '..'))))):
|
||||
modified = False
|
||||
break
|
||||
modified = True
|
||||
|
||||
# Check view XML
|
||||
directory = os.path.dirname(path)
|
||||
if directory not in directories:
|
||||
directories.add(directory)
|
||||
view_dir = os.path.join(directory, 'view')
|
||||
if os.path.isdir(view_dir):
|
||||
for view in os.listdir(view_dir):
|
||||
view = os.path.join(view_dir, view)
|
||||
if os.path.splitext(view)[1] == '.xml':
|
||||
if _modified(view):
|
||||
modified = True
|
||||
|
||||
modules = set(get_module_list())
|
||||
if _MODULES is None:
|
||||
_MODULES = modules
|
||||
for module in modules.difference(_MODULES):
|
||||
if subprocess.call((sys.executable, '-c',
|
||||
'import trytond.modules.%s' % module)):
|
||||
modified = False
|
||||
break
|
||||
modified = True
|
||||
_MODULES = modules
|
||||
return modified
|
||||
@@ -1,53 +0,0 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import errno
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
from SocketServer import StreamRequestHandler
|
||||
|
||||
|
||||
def endsocket(sock):
|
||||
if os.name != 'nt':
|
||||
try:
|
||||
sock.shutdown(getattr(socket, 'SHUT_RDWR', 2))
|
||||
except socket.error, e:
|
||||
if e.errno != errno.ENOTCONN:
|
||||
raise
|
||||
sock.close()
|
||||
|
||||
|
||||
class daemon(threading.Thread):
|
||||
def __init__(self, interface, port, secure, name=None):
|
||||
threading.Thread.__init__(self, name=name)
|
||||
self.secure = secure
|
||||
self.ipv6 = False
|
||||
for family, _, _, _, _ in socket.getaddrinfo(interface or None, port,
|
||||
socket.AF_UNSPEC, socket.SOCK_STREAM):
|
||||
if family == socket.AF_INET6:
|
||||
self.ipv6 = True
|
||||
break
|
||||
|
||||
def stop(self):
|
||||
self.server.shutdown()
|
||||
self.server.socket.shutdown(socket.SHUT_RDWR)
|
||||
self.server.server_close()
|
||||
return
|
||||
|
||||
def run(self):
|
||||
self.server.serve_forever()
|
||||
return True
|
||||
|
||||
|
||||
class RegisterHandlerMixin:
|
||||
|
||||
def setup(self):
|
||||
self.server.handlers.add(self)
|
||||
StreamRequestHandler.setup(self)
|
||||
|
||||
def finish(self):
|
||||
StreamRequestHandler.finish(self)
|
||||
try:
|
||||
self.server.handlers.remove(self)
|
||||
except KeyError:
|
||||
pass
|
||||
+165
-124
@@ -4,7 +4,9 @@
|
||||
import logging
|
||||
import time
|
||||
import pydoc
|
||||
from functools import wraps
|
||||
|
||||
from werkzeug.utils import redirect
|
||||
from sql import Table
|
||||
|
||||
from trytond.pool import Pool
|
||||
@@ -17,6 +19,7 @@ from trytond.cache import Cache
|
||||
from trytond.exceptions import UserError, UserWarning, NotLogged, \
|
||||
ConcurrencyException
|
||||
from trytond.tools import is_instance_method
|
||||
from trytond.wsgi import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,131 +29,169 @@ ir_module = Table('ir_module')
|
||||
res_user = Table('res_user')
|
||||
|
||||
|
||||
def dispatch(host, port, protocol, database_name, user, session, object_type,
|
||||
object_name, method, *args, **kwargs):
|
||||
Database = backend.get('Database')
|
||||
DatabaseOperationalError = backend.get('DatabaseOperationalError')
|
||||
if object_type == 'common':
|
||||
if method == 'login':
|
||||
try:
|
||||
database = Database(database_name).connect()
|
||||
cursor = database.get_connection().cursor()
|
||||
cursor.close()
|
||||
except Exception:
|
||||
return False
|
||||
res = security.login(database_name, user, session)
|
||||
with Transaction().start(database_name, 0):
|
||||
Cache.clean(database_name)
|
||||
Cache.resets(database_name)
|
||||
msg = res and 'successful login' or 'bad login or password'
|
||||
logger.info('%s \'%s\' from %s:%d using %s on database \'%s\'',
|
||||
msg, user, host, port, protocol, database_name)
|
||||
return res or False
|
||||
elif method == 'logout':
|
||||
name = security.logout(database_name, user, session)
|
||||
logger.info('logout \'%s\' from %s:%d '
|
||||
'using %s on database \'%s\'',
|
||||
name, host, port, protocol, database_name)
|
||||
return True
|
||||
elif method == 'version':
|
||||
return __version__
|
||||
elif method == 'list_lang':
|
||||
return [
|
||||
('bg_BG', 'Български'),
|
||||
('ca_ES', 'Català'),
|
||||
('cs_CZ', 'Čeština'),
|
||||
('de_DE', 'Deutsch'),
|
||||
('en_US', 'English'),
|
||||
('es_AR', 'Español (Argentina)'),
|
||||
('es_EC', 'Español (Ecuador)'),
|
||||
('es_ES', 'Español (España)'),
|
||||
('es_CO', 'Español (Colombia)'),
|
||||
('es_MX', 'Español (México)'),
|
||||
('fr_FR', 'Français'),
|
||||
('hu_HU', 'Magyar'),
|
||||
('it_IT', 'Italiano'),
|
||||
('lt_LT', 'Lietuvių'),
|
||||
('nl_NL', 'Nederlands'),
|
||||
('pt_BR', 'Português (Brasil)'),
|
||||
('ru_RU', 'Russian'),
|
||||
('sl_SI', 'Slovenščina'),
|
||||
]
|
||||
elif method == 'db_exist':
|
||||
try:
|
||||
database = Database(*args, **kwargs).connect()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
elif method == 'list':
|
||||
if not config.getboolean('database', 'list'):
|
||||
raise Exception('AccessDenied')
|
||||
with Transaction().start(None, 0, close=True) as transaction:
|
||||
return transaction.database.list()
|
||||
elif method == 'create':
|
||||
return create(*args, **kwargs)
|
||||
elif method == 'restore':
|
||||
return restore(*args, **kwargs)
|
||||
elif method == 'drop':
|
||||
return drop(*args, **kwargs)
|
||||
elif method == 'dump':
|
||||
return dump(*args, **kwargs)
|
||||
return
|
||||
elif object_type == 'system':
|
||||
database = Database(database_name).connect()
|
||||
def with_pool(func):
|
||||
@wraps(func)
|
||||
def wrapper(request, database_name, *args, **kwargs):
|
||||
database_list = Pool.database_list()
|
||||
pool = Pool(database_name)
|
||||
if database_name not in database_list:
|
||||
pool.init()
|
||||
if method == 'listMethods':
|
||||
res = []
|
||||
for type in ('model', 'wizard', 'report'):
|
||||
for object_name, obj in pool.iterobject(type=type):
|
||||
for method in obj.__rpc__:
|
||||
res.append(type + '.' + object_name + '.' + method)
|
||||
return res
|
||||
elif method == 'methodSignature':
|
||||
return 'signatures not supported'
|
||||
elif method == 'methodHelp':
|
||||
res = []
|
||||
args_list = args[0].split('.')
|
||||
object_type = args_list[0]
|
||||
object_name = '.'.join(args_list[1:-1])
|
||||
method = args_list[-1]
|
||||
obj = pool.get(object_name, type=object_type)
|
||||
return pydoc.getdoc(getattr(obj, method))
|
||||
with Transaction().start(
|
||||
database_name, request.user_id, readonly=True):
|
||||
pool.init()
|
||||
return func(request, pool, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
for count in range(config.getint('database', 'retry'), -1, -1):
|
||||
try:
|
||||
user = security.check(database_name, user, session)
|
||||
except DatabaseOperationalError:
|
||||
if count:
|
||||
continue
|
||||
raise
|
||||
break
|
||||
|
||||
database_list = Pool.database_list()
|
||||
pool = Pool(database_name)
|
||||
if database_name not in database_list:
|
||||
with Transaction().start(database_name, user,
|
||||
readonly=True) as transaction:
|
||||
pool.init()
|
||||
obj = pool.get(object_name, type=object_type)
|
||||
@app.route('/<string:database_name>/', methods=['POST'])
|
||||
def rpc(request, database_name):
|
||||
methods = {
|
||||
'common.db.login': login,
|
||||
'common.db.logout': logout,
|
||||
'common.db.db_exist': db_exist,
|
||||
'common.db.create': create,
|
||||
'common.db.restore': restore,
|
||||
'common.db.drop': drop,
|
||||
'common.db.dump': dump,
|
||||
'system.listMethods': list_method,
|
||||
'system.methodHelp': help_method,
|
||||
'system.methodSignature': lambda *a: 'signatures not supported',
|
||||
}
|
||||
return methods.get(request.method, _dispatch)(
|
||||
request, database_name, *request.params)
|
||||
|
||||
|
||||
def login(request, database_name, user, password):
|
||||
Database = backend.get('Database')
|
||||
try:
|
||||
database = Database(database_name).connect()
|
||||
cursor = database.get_connection().cursor()
|
||||
cursor.close()
|
||||
except Exception:
|
||||
logger.error('fail to connect to %s', database_name, exc_info=True)
|
||||
return False
|
||||
session = security.login(database_name, user, password)
|
||||
with Transaction().start(database_name, 0):
|
||||
Cache.clean(database_name)
|
||||
Cache.resets(database_name)
|
||||
msg = 'successful login' if session else 'bad login or password'
|
||||
logger.info('%s \'%s\' from %s using %s on database \'%s\'',
|
||||
msg, user, request.remote_addr, request.scheme, database_name)
|
||||
return session
|
||||
|
||||
|
||||
@app.auth_required
|
||||
def logout(request, database_name):
|
||||
auth = request.authorization
|
||||
name = security.logout(
|
||||
database_name, auth.get('userid'), auth.get('session'))
|
||||
logger.info('logout \'%s\' from %s using %s on database \'%s\'',
|
||||
name, request.remote_addr, request.scheme, database_name)
|
||||
return True
|
||||
|
||||
|
||||
@app.route('/', methods=['POST'])
|
||||
def root(request, *args):
|
||||
methods = {
|
||||
'common.server.version': lambda *a: __version__,
|
||||
'common.db.list_lang': list_lang,
|
||||
'common.db.list': db_list,
|
||||
}
|
||||
return methods[request.method](request, *request.params)
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def home(request):
|
||||
return redirect('/index.html') # XXX find a better way
|
||||
|
||||
|
||||
def list_lang(*args):
|
||||
return [
|
||||
('bg_BG', 'Български'),
|
||||
('ca_ES', 'Català'),
|
||||
('cs_CZ', 'Čeština'),
|
||||
('de_DE', 'Deutsch'),
|
||||
('en_US', 'English'),
|
||||
('es_AR', 'Español (Argentina)'),
|
||||
('es_EC', 'Español (Ecuador)'),
|
||||
('es_ES', 'Español (España)'),
|
||||
('es_CO', 'Español (Colombia)'),
|
||||
('es_MX', 'Español (México)'),
|
||||
('fr_FR', 'Français'),
|
||||
('hu_HU', 'Magyar'),
|
||||
('it_IT', 'Italiano'),
|
||||
('lt_LT', 'Lietuvių'),
|
||||
('nl_NL', 'Nederlands'),
|
||||
('pt_BR', 'Português (Brasil)'),
|
||||
('ru_RU', 'Russian'),
|
||||
('sl_SI', 'Slovenščina'),
|
||||
]
|
||||
|
||||
|
||||
def db_exist(request, database_name):
|
||||
Database = backend.get('Database')
|
||||
try:
|
||||
Database(database_name).connect()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def db_list(*args):
|
||||
if not config.getboolean('database', 'list'):
|
||||
raise Exception('AccessDenied')
|
||||
with Transaction().start(None, 0, close=True) as transaction:
|
||||
return transaction.database.list()
|
||||
|
||||
|
||||
@app.auth_required
|
||||
@with_pool
|
||||
def list_method(request, pool):
|
||||
methods = []
|
||||
for type in ('model', 'wizard', 'report'):
|
||||
for object_name, obj in pool.iterobject(type=type):
|
||||
for method in obj.__rpc__:
|
||||
methods.append(type + '.' + object_name + '.' + method)
|
||||
return methods
|
||||
|
||||
|
||||
def get_object_method(request, pool):
|
||||
method = request.method
|
||||
type, _ = method.split('.', 1)
|
||||
name = '.'.join(method.split('.')[1:-1])
|
||||
method = method.split('.')[-1]
|
||||
return pool.get(name, type=type), method
|
||||
|
||||
|
||||
@app.auth_required
|
||||
@with_pool
|
||||
def help_method(request, pool):
|
||||
obj, method = get_object_method(request, pool)
|
||||
return pydoc.getdoc(getattr(obj, method))
|
||||
|
||||
|
||||
@app.auth_required
|
||||
@with_pool
|
||||
def _dispatch(request, pool, *args, **kwargs):
|
||||
DatabaseOperationalError = backend.get('DatabaseOperationalError')
|
||||
|
||||
obj, method = get_object_method(request, pool)
|
||||
if method in obj.__rpc__:
|
||||
rpc = obj.__rpc__[method]
|
||||
else:
|
||||
raise UserError('Calling method %s on %s %s is not allowed!'
|
||||
% (method, object_type, object_name))
|
||||
|
||||
log_message = '%s.%s.%s(*%s, **%s) from %s@%s:%d/%s'
|
||||
log_args = (object_type, object_name, method, args, kwargs,
|
||||
user, host, port, database_name)
|
||||
raise UserError('Calling method %s on %s is not allowed'
|
||||
% (method, obj))
|
||||
|
||||
log_message = '%s.%s(*%s, **%s) from %s@%s/%s'
|
||||
log_args = (obj, method, args, kwargs,
|
||||
request.authorization.username, request.remote_addr, request.path)
|
||||
logger.info(log_message, *log_args)
|
||||
|
||||
user = request.user_id
|
||||
|
||||
for count in range(config.getint('database', 'retry'), -1, -1):
|
||||
with Transaction().start(database_name, user,
|
||||
with Transaction().start(pool.database_name, user,
|
||||
readonly=rpc.readonly) as transaction:
|
||||
Cache.clean(database_name)
|
||||
Cache.clean(pool.database_name)
|
||||
try:
|
||||
c_args, c_kwargs, transaction.context, transaction.timestamp \
|
||||
= rpc.convert(obj, *args, **kwargs)
|
||||
@@ -177,19 +218,19 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
|
||||
except Exception:
|
||||
logger.error(log_message, *log_args, exc_info=True)
|
||||
raise
|
||||
Cache.resets(database_name)
|
||||
try:
|
||||
with Transaction().start(database_name, 0) as transaction:
|
||||
pool = Pool(database_name)
|
||||
Session = pool.get('ir.session')
|
||||
Session.reset(session)
|
||||
except DatabaseOperationalError:
|
||||
logger.debug('Reset session failed', exc_info=True)
|
||||
Cache.resets(pool.database_name)
|
||||
if request.authorization.type == 'session':
|
||||
try:
|
||||
with Transaction().start(pool.database_name, 0) as transaction:
|
||||
Session = pool.get('ir.session')
|
||||
Session.reset(request.authorization.get('session'))
|
||||
except DatabaseOperationalError:
|
||||
logger.debug('Reset session failed', exc_info=True)
|
||||
logger.debug('Result: %s', result)
|
||||
return result
|
||||
|
||||
|
||||
def create(database_name, password, lang, admin_password):
|
||||
def create(request, database_name, password, lang, admin_password):
|
||||
'''
|
||||
Create a database
|
||||
|
||||
@@ -242,7 +283,7 @@ def create(database_name, password, lang, admin_password):
|
||||
return res
|
||||
|
||||
|
||||
def drop(database_name, password):
|
||||
def drop(request, database_name, password):
|
||||
Database = backend.get('Database')
|
||||
security.check_super(password)
|
||||
database = Database(database_name)
|
||||
@@ -264,7 +305,7 @@ def drop(database_name, password):
|
||||
return True
|
||||
|
||||
|
||||
def dump(database_name, password):
|
||||
def dump(request, database_name, password):
|
||||
Database = backend.get('Database')
|
||||
security.check_super(password)
|
||||
Database(database_name).close()
|
||||
@@ -279,7 +320,7 @@ def dump(database_name, password):
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def restore(database_name, password, data, update=False):
|
||||
def restore(request, database_name, password, data, update=False):
|
||||
Database = backend.get('Database')
|
||||
security.check_super(password)
|
||||
try:
|
||||
|
||||
+43
-275
@@ -1,25 +1,7 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from trytond.protocols.sslsocket import SSLSocket
|
||||
from trytond.protocols.dispatcher import dispatch
|
||||
from trytond.config import config
|
||||
from trytond.protocols.common import daemon, RegisterHandlerMixin
|
||||
from trytond.exceptions import UserError, UserWarning, NotLogged, \
|
||||
ConcurrencyException
|
||||
|
||||
import SimpleXMLRPCServer
|
||||
import SimpleHTTPServer
|
||||
import SocketServer
|
||||
import traceback
|
||||
import socket
|
||||
import sys
|
||||
import os
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
fcntl = None
|
||||
import posixpath
|
||||
import urllib
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
try:
|
||||
@@ -27,8 +9,13 @@ try:
|
||||
except ImportError:
|
||||
import json
|
||||
import base64
|
||||
import encodings
|
||||
from io import BytesIO
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug.utils import cached_property
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from trytond.protocols.wrappers import Request
|
||||
from trytond.exceptions import TrytonException
|
||||
|
||||
|
||||
class JSONDecoder(object):
|
||||
@@ -127,269 +114,50 @@ JSONEncoder.register(Decimal,
|
||||
})
|
||||
|
||||
|
||||
class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
|
||||
"""Mix-in class that dispatches JSON-RPC requests.
|
||||
class JSONRequest(Request):
|
||||
parsed_content_type = 'json'
|
||||
|
||||
This class is used to register JSON-RPC method handlers
|
||||
and then to dispatch them. There should never be any
|
||||
reason to instantiate this class directly.
|
||||
"""
|
||||
@cached_property
|
||||
def parsed_data(self):
|
||||
if self.parsed_content_type in self.environ.get('CONTENT_TYPE', ''):
|
||||
try:
|
||||
return json.loads(self.decoded_data, object_hook=JSONDecoder())
|
||||
except Exception:
|
||||
raise BadRequest('Unable to read JSON request')
|
||||
else:
|
||||
raise BadRequest('Not a JSON request')
|
||||
|
||||
def _marshaled_dispatch(self, data, dispatch_method=None, path=None):
|
||||
"""Dispatches an JSON-RPC method from marshalled (JSON) data.
|
||||
@cached_property
|
||||
def method(self):
|
||||
return self.parsed_data['method']
|
||||
|
||||
JSON-RPC methods are dispatched from the marshalled (JSON) data
|
||||
using the _dispatch method and the result is returned as
|
||||
marshalled data. For backwards compatibility, a dispatch
|
||||
function can be provided as an argument (see comment in
|
||||
SimpleJSONRPCRequestHandler.do_POST) but overriding the
|
||||
existing method through subclassing is the prefered means
|
||||
of changing method dispatch behavior.
|
||||
"""
|
||||
rawreq = json.loads(data.decode('utf-8'), object_hook=JSONDecoder())
|
||||
@cached_property
|
||||
def params(self):
|
||||
return self.parsed_data['params']
|
||||
|
||||
req_id = rawreq.get('id', 0)
|
||||
method = rawreq['method']
|
||||
params = rawreq.get('params', [])
|
||||
|
||||
response = {'id': req_id}
|
||||
class JSONProtocol:
|
||||
content_type = 'json'
|
||||
|
||||
try:
|
||||
# generate response
|
||||
if dispatch_method is not None:
|
||||
response['result'] = dispatch_method(method, params)
|
||||
else:
|
||||
response['result'] = self._dispatch(method, params)
|
||||
except (UserError, UserWarning, NotLogged,
|
||||
ConcurrencyException), exception:
|
||||
response['error'] = exception.args
|
||||
except Exception:
|
||||
@classmethod
|
||||
def request(cls, environ):
|
||||
return JSONRequest(environ)
|
||||
|
||||
@classmethod
|
||||
def response(cls, data, request):
|
||||
if isinstance(request, JSONRequest):
|
||||
response = {'id': request.parsed_data.get('id', 0)}
|
||||
else:
|
||||
response = {}
|
||||
if isinstance(data, TrytonException):
|
||||
response['error'] = data.args
|
||||
elif isinstance(data, Exception):
|
||||
tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
|
||||
for path in sys.path:
|
||||
tb_s = tb_s.replace(path, '')
|
||||
# report exception back to server
|
||||
response['error'] = (str(sys.exc_value), tb_s)
|
||||
|
||||
return json.dumps(response, cls=JSONEncoder).encode('utf-8')
|
||||
|
||||
|
||||
class GenericJSONRPCRequestHandler:
|
||||
|
||||
def _dispatch(self, method, params):
|
||||
host, port = self.client_address[:2]
|
||||
database_name = self.path[1:]
|
||||
if database_name.startswith('sao/'):
|
||||
database_name = database_name[4:]
|
||||
method_list = method.split('.')
|
||||
object_type = method_list[0]
|
||||
object_name = '.'.join(method_list[1:-1])
|
||||
method = method_list[-1]
|
||||
args = (host, port, 'JSON-RPC', database_name, params[0], params[1],
|
||||
object_type, object_name, method) + tuple(params[2:])
|
||||
res = dispatch(*args)
|
||||
return res
|
||||
|
||||
|
||||
class SimpleJSONRPCRequestHandler(RegisterHandlerMixin,
|
||||
GenericJSONRPCRequestHandler,
|
||||
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler,
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
"""Simple JSON-RPC request handler class.
|
||||
|
||||
Handles all HTTP POST requests and attempts to decode them as
|
||||
JSON-RPC requests.
|
||||
"""
|
||||
protocol_version = "HTTP/1.1"
|
||||
rpc_paths = None
|
||||
encode_threshold = 1400 # common MTU
|
||||
|
||||
def send_header(self, keyword, value):
|
||||
if keyword == 'Content-type' and value == 'text/xml':
|
||||
value = 'application/json-rpc'
|
||||
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.send_header(self,
|
||||
keyword, value)
|
||||
|
||||
def do_GET(self):
|
||||
if self.is_tryton_url(self.path):
|
||||
self.send_tryton_url(self.path)
|
||||
return
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
|
||||
|
||||
def do_HEAD(self):
|
||||
if self.is_tryton_url(self.path):
|
||||
self.send_tryton_url(self.path)
|
||||
return
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler.do_HEAD(self)
|
||||
|
||||
def translate_path(self, path):
|
||||
"""Translate a /-separated PATH to the local filename syntax.
|
||||
|
||||
Components that mean special things to the local file system
|
||||
(e.g. drive or directory names) are ignored. (XXX They should
|
||||
probably be diagnosed.)
|
||||
|
||||
"""
|
||||
# abandon query parameters
|
||||
path = path.split('?', 1)[0]
|
||||
path = path.split('#', 1)[0]
|
||||
path = posixpath.normpath(urllib.unquote(path))
|
||||
words = path.split('/')
|
||||
words = filter(None, words)
|
||||
path = config.get('jsonrpc', 'data')
|
||||
for word in words:
|
||||
drive, word = os.path.splitdrive(word)
|
||||
head, word = os.path.split(word)
|
||||
if word in (os.curdir, os.pardir):
|
||||
continue
|
||||
path = os.path.join(path, word)
|
||||
return path
|
||||
|
||||
def is_tryton_url(self, path):
|
||||
words = path.split('/')
|
||||
try:
|
||||
return words[2] in ('model', 'wizard', 'report')
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
def send_tryton_url(self, path):
|
||||
self.send_response(300)
|
||||
hostname = (config.get('jsonrpc', 'hostname')
|
||||
or unicode(socket.getfqdn(), 'utf8'))
|
||||
hostname = '.'.join(encodings.idna.ToASCII(part) for part in
|
||||
hostname.split('.'))
|
||||
values = {
|
||||
'hostname': hostname,
|
||||
'path': path,
|
||||
}
|
||||
content = BytesIO()
|
||||
|
||||
def write(str_):
|
||||
content.write(str_.encode('utf-8'))
|
||||
write('<html')
|
||||
write('<head>')
|
||||
write('<meta http-equiv="Refresh" '
|
||||
'content="0;url=tryton://%(hostname)s%(path)s"/>' % values)
|
||||
write('<title>Moved</title>')
|
||||
write('</head>')
|
||||
write('<body>')
|
||||
write('<h1>Moved</h1>')
|
||||
write('<p>This page has moved to '
|
||||
'<a href="tryton://%(hostname)s%(path)s">'
|
||||
'tryton://%(hostname)s%(path)s</a>.</p>' % values)
|
||||
write('</body>')
|
||||
write('</html>')
|
||||
length = content.tell()
|
||||
content.seek(0)
|
||||
self.send_header('Location', 'tryton://%(hostname)s%(path)s' % values)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.send_header('Content-Length', str(length))
|
||||
self.end_headers()
|
||||
self.copyfile(content, self.wfile)
|
||||
content.close()
|
||||
|
||||
SimpleJSONRPCRequestHandler.extensions_map.update({
|
||||
'.svg': 'image/svg+xml',
|
||||
})
|
||||
|
||||
|
||||
class SecureJSONRPCRequestHandler(SimpleJSONRPCRequestHandler):
|
||||
|
||||
def setup(self):
|
||||
self.request = SSLSocket(self.request)
|
||||
SimpleJSONRPCRequestHandler.setup(self)
|
||||
|
||||
|
||||
class SimpleJSONRPCServer(SocketServer.TCPServer,
|
||||
SimpleJSONRPCDispatcher):
|
||||
"""Simple JSON-RPC server.
|
||||
|
||||
Simple JSON-RPC server that allows functions and a single instance
|
||||
to be installed to handle requests. The default implementation
|
||||
attempts to dispatch JSON-RPC calls to the functions or instance
|
||||
installed in the server. Override the _dispatch method inhereted
|
||||
from SimpleJSONRPCDispatcher to change this behavior.
|
||||
"""
|
||||
|
||||
allow_reuse_address = True
|
||||
|
||||
# Warning: this is for debugging purposes only! Never set this to True in
|
||||
# production code, as will be sending out sensitive information (exception
|
||||
# and stack trace details) when exceptions are raised inside
|
||||
# SimpleJSONRPCRequestHandler.do_POST
|
||||
_send_traceback_header = False
|
||||
|
||||
def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
|
||||
logRequests=True, allow_none=False, encoding=None,
|
||||
bind_and_activate=True):
|
||||
self.handlers = set()
|
||||
self.logRequests = logRequests
|
||||
|
||||
SimpleJSONRPCDispatcher.__init__(self, allow_none, encoding)
|
||||
try:
|
||||
SocketServer.TCPServer.__init__(self, addr, requestHandler,
|
||||
bind_and_activate)
|
||||
except TypeError:
|
||||
SocketServer.TCPServer.__init__(self, addr, requestHandler)
|
||||
|
||||
# [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 server_close(self):
|
||||
SocketServer.TCPServer.server_close(self)
|
||||
for handler in self.handlers.copy():
|
||||
self.shutdown_request(handler.request)
|
||||
|
||||
|
||||
class SimpleThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
|
||||
SimpleJSONRPCServer):
|
||||
timeout = 1
|
||||
daemon_threads = True
|
||||
disable_nagle_algorithm = True
|
||||
|
||||
def server_bind(self):
|
||||
self.socket.setsockopt(socket.SOL_SOCKET,
|
||||
socket.SO_REUSEADDR, 1)
|
||||
self.socket.setsockopt(socket.SOL_SOCKET,
|
||||
socket.SO_KEEPALIVE, 1)
|
||||
SimpleJSONRPCServer.server_bind(self)
|
||||
|
||||
|
||||
class SimpleThreadedJSONRPCServer6(SimpleThreadedJSONRPCServer):
|
||||
address_family = socket.AF_INET6
|
||||
|
||||
|
||||
class SecureThreadedJSONRPCServer(SimpleThreadedJSONRPCServer):
|
||||
|
||||
def __init__(self, server_address, HandlerClass, logRequests=1):
|
||||
SimpleThreadedJSONRPCServer.__init__(self, server_address,
|
||||
HandlerClass, logRequests)
|
||||
self.socket = socket.socket(self.address_family,
|
||||
self.socket_type)
|
||||
self.server_bind()
|
||||
self.server_activate()
|
||||
|
||||
|
||||
class SecureThreadedJSONRPCServer6(SecureThreadedJSONRPCServer):
|
||||
address_family = socket.AF_INET6
|
||||
|
||||
|
||||
class JSONRPCDaemon(daemon):
|
||||
|
||||
def __init__(self, interface, port, secure=False):
|
||||
daemon.__init__(self, interface, port, secure, name='JSONRPCDaemon')
|
||||
if self.secure:
|
||||
handler_class = SecureJSONRPCRequestHandler
|
||||
server_class = SecureThreadedJSONRPCServer
|
||||
if self.ipv6:
|
||||
server_class = SecureThreadedJSONRPCServer6
|
||||
response['error'] = (str(data), tb_s)
|
||||
else:
|
||||
handler_class = SimpleJSONRPCRequestHandler
|
||||
server_class = SimpleThreadedJSONRPCServer
|
||||
if self.ipv6:
|
||||
server_class = SimpleThreadedJSONRPCServer6
|
||||
self.server = server_class((interface, port), handler_class, 0)
|
||||
response['result'] = data
|
||||
return Response(json.dumps(response, cls=JSONEncoder),
|
||||
content_type='application/json')
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from trytond.config import config
|
||||
|
||||
|
||||
def SSLSocket(socket):
|
||||
# Let the import error raise only when used
|
||||
import ssl
|
||||
return ssl.wrap_socket(socket,
|
||||
server_side=True,
|
||||
certfile=config.get('ssl', 'certificate'),
|
||||
keyfile=config.get('ssl', 'privatekey'),
|
||||
ssl_version=ssl.PROTOCOL_SSLv23)
|
||||
@@ -0,0 +1,87 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import base64
|
||||
import gzip
|
||||
from io import BytesIO
|
||||
|
||||
from werkzeug.wrappers import Request as _Request
|
||||
from werkzeug.utils import cached_property
|
||||
from werkzeug.http import wsgi_to_bytes, bytes_to_wsgi
|
||||
from werkzeug.datastructures import Authorization
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from trytond import security
|
||||
|
||||
|
||||
class Request(_Request):
|
||||
|
||||
view_args = None
|
||||
|
||||
@property
|
||||
def decoded_data(self):
|
||||
if self.content_encoding == 'gzip':
|
||||
zipfile = gzip.GzipFile(fileobj=BytesIO(self.data), mode='rb')
|
||||
return zipfile.read()
|
||||
else:
|
||||
return self.data
|
||||
|
||||
@property
|
||||
def parsed_data(self):
|
||||
return self.data
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
return
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
return
|
||||
|
||||
@cached_property
|
||||
def authorization(self):
|
||||
authorization = super(Request, self).authorization
|
||||
if authorization is None:
|
||||
header = self.environ.get('HTTP_AUTHORIZATION')
|
||||
return parse_authorization_header(header)
|
||||
return authorization
|
||||
|
||||
@cached_property
|
||||
def user_id(self):
|
||||
database_name = self.view_args['database_name']
|
||||
auth = self.authorization
|
||||
if not auth:
|
||||
abort(401)
|
||||
if auth.type == 'session':
|
||||
user_id = security.check(
|
||||
database_name, auth.get('userid'), auth.get('session'))
|
||||
if not user_id:
|
||||
abort(403)
|
||||
else:
|
||||
user_id = security.login(
|
||||
database_name, auth.username, auth.password, cache=False)
|
||||
if not user_id:
|
||||
abort(401)
|
||||
return user_id
|
||||
|
||||
|
||||
def parse_authorization_header(value):
|
||||
if not value:
|
||||
return
|
||||
value = wsgi_to_bytes(value)
|
||||
try:
|
||||
auth_type, auth_info = value.split(None, 1)
|
||||
auth_type = auth_type.lower()
|
||||
except ValueError:
|
||||
return
|
||||
if auth_type == b'session':
|
||||
try:
|
||||
username, userid, session = base64.b64decode(auth_info).split(
|
||||
b':', 3)
|
||||
userid = int(userid)
|
||||
except Exception:
|
||||
return
|
||||
return Authorization('session', {
|
||||
'username': bytes_to_wsgi(username),
|
||||
'userid': userid,
|
||||
'session': bytes_to_wsgi(session),
|
||||
})
|
||||
+45
-138
@@ -1,22 +1,19 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from trytond.protocols.sslsocket import SSLSocket
|
||||
from trytond.protocols.dispatcher import dispatch
|
||||
from trytond.protocols.common import daemon, RegisterHandlerMixin
|
||||
from trytond.exceptions import UserError, UserWarning, NotLogged, \
|
||||
ConcurrencyException
|
||||
from trytond import security
|
||||
import SimpleXMLRPCServer
|
||||
import SocketServer
|
||||
import xmlrpclib as client
|
||||
import socket
|
||||
import base64
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
# convert decimal to float before marshalling:
|
||||
from decimal import Decimal
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug.utils import cached_property
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from trytond.protocols.wrappers import Request
|
||||
from trytond.exceptions import TrytonException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -138,134 +135,44 @@ if bytes == str:
|
||||
client.Unmarshaller.dispatch['base64'] = _end_base64
|
||||
|
||||
|
||||
class GenericXMLRPCRequestHandler:
|
||||
class XMLRequest(Request):
|
||||
parsed_content_type = 'xml'
|
||||
|
||||
def _dispatch(self, method, params):
|
||||
host, port = self.client_address[:2]
|
||||
database_name = self.path[1:]
|
||||
user = self.tryton['user']
|
||||
session = self.tryton['session']
|
||||
exception_message = 'Exception calling %s%s' % (method, params)
|
||||
try:
|
||||
@cached_property
|
||||
def parsed_data(self):
|
||||
if self.parsed_content_type in self.environ.get('CONTENT_TYPE', ''):
|
||||
try:
|
||||
method_list = method.split('.')
|
||||
object_type = method_list[0]
|
||||
object_name = '.'.join(method_list[1:-1])
|
||||
method = method_list[-1]
|
||||
if object_type == 'system' and method == 'getCapabilities':
|
||||
return {
|
||||
'introspect': {
|
||||
'specUrl': ('http://xmlrpc-c.sourceforge.net/'
|
||||
'xmlrpc-c/introspection.html'),
|
||||
'specVersion': 1,
|
||||
},
|
||||
}
|
||||
return dispatch(host, port, 'XML-RPC', database_name, user,
|
||||
session, object_type, object_name, method, *params)
|
||||
except (NotLogged, ConcurrencyException), exception:
|
||||
logger.debug(exception_message, exc_info=True)
|
||||
raise client.Fault(exception.code, str(exception))
|
||||
except (UserError, UserWarning), exception:
|
||||
logger.debug(exception_message, exc_info=True)
|
||||
error, description = exception.args
|
||||
raise client.Fault(exception.code, str(exception))
|
||||
except Exception, exception:
|
||||
logger.error(exception_message, exc_info=True)
|
||||
raise client.Fault(255, str(exception))
|
||||
finally:
|
||||
security.logout(database_name, user, session)
|
||||
|
||||
|
||||
class SimpleXMLRPCRequestHandler(RegisterHandlerMixin,
|
||||
GenericXMLRPCRequestHandler,
|
||||
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
rpc_paths = None
|
||||
encode_threshold = 1400 # common MTU
|
||||
|
||||
def parse_request(self):
|
||||
res = SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.parse_request(self)
|
||||
if not res:
|
||||
return res
|
||||
database_name = self.path[1:]
|
||||
if not database_name:
|
||||
self.tryton = {'user': None, 'session': None}
|
||||
return res
|
||||
try:
|
||||
method, up64 = self.headers['Authorization'].split(None, 1)
|
||||
if method.strip().lower() == 'basic':
|
||||
user, password = base64.decodestring(up64).split(':', 1)
|
||||
user_id, session = security.login(database_name, user,
|
||||
password)
|
||||
self.tryton = {'user': user_id, 'session': session}
|
||||
return res
|
||||
except Exception:
|
||||
pass
|
||||
self.send_error(401, 'Unauthorized')
|
||||
self.send_header("WWW-Authenticate", 'Basic realm="Tryton"')
|
||||
return False
|
||||
|
||||
|
||||
class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
|
||||
|
||||
def setup(self):
|
||||
self.request = SSLSocket(self.request)
|
||||
SimpleXMLRPCRequestHandler.setup(self)
|
||||
|
||||
|
||||
class SimpleThreadedXMLRPCServer(SocketServer.ThreadingMixIn,
|
||||
SimpleXMLRPCServer.SimpleXMLRPCServer):
|
||||
timeout = 1
|
||||
daemon_threads = True
|
||||
|
||||
def __init__(self, server_address, HandlerClass, logRequests=1):
|
||||
self.handlers = set()
|
||||
SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, server_address,
|
||||
HandlerClass, logRequests)
|
||||
|
||||
def server_bind(self):
|
||||
self.socket.setsockopt(socket.SOL_SOCKET,
|
||||
socket.SO_REUSEADDR, 1)
|
||||
self.socket.setsockopt(socket.SOL_SOCKET,
|
||||
socket.SO_KEEPALIVE, 1)
|
||||
SimpleXMLRPCServer.SimpleXMLRPCServer.server_bind(self)
|
||||
|
||||
def server_close(self):
|
||||
SimpleXMLRPCServer.SimpleXMLRPCServer.server_close(self)
|
||||
for handler in self.handlers.copy():
|
||||
self.shutdown_request(handler.request)
|
||||
|
||||
|
||||
class SimpleThreadedXMLRPCServer6(SimpleThreadedXMLRPCServer):
|
||||
address_family = socket.AF_INET6
|
||||
|
||||
|
||||
class SecureThreadedXMLRPCServer(SimpleThreadedXMLRPCServer):
|
||||
|
||||
def __init__(self, server_address, HandlerClass, logRequests=1):
|
||||
SimpleThreadedXMLRPCServer.__init__(self, server_address, HandlerClass,
|
||||
logRequests)
|
||||
self.socket = socket.socket(self.address_family, self.socket_type)
|
||||
self.server_bind()
|
||||
self.server_activate()
|
||||
|
||||
|
||||
class SecureThreadedXMLRPCServer6(SecureThreadedXMLRPCServer):
|
||||
address_family = socket.AF_INET6
|
||||
|
||||
|
||||
class XMLRPCDaemon(daemon):
|
||||
|
||||
def __init__(self, interface, port, secure=False):
|
||||
daemon.__init__(self, interface, port, secure, name='XMLRPCDaemon')
|
||||
if self.secure:
|
||||
handler_class = SecureXMLRPCRequestHandler
|
||||
server_class = SecureThreadedXMLRPCServer
|
||||
if self.ipv6:
|
||||
server_class = SecureThreadedXMLRPCServer6
|
||||
# TODO replace by own loads
|
||||
return client.loads(self.decoded_data)
|
||||
except Exception:
|
||||
raise BadRequest('Unable to read XMl request')
|
||||
else:
|
||||
handler_class = SimpleXMLRPCRequestHandler
|
||||
server_class = SimpleThreadedXMLRPCServer
|
||||
if self.ipv6:
|
||||
server_class = SimpleThreadedXMLRPCServer6
|
||||
self.server = server_class((interface, port), handler_class, 0)
|
||||
raise BadRequest('Not an XML request')
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
return self.parsed_data[1]
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
return self.parsed_data[0]
|
||||
|
||||
|
||||
class XMLProtocol:
|
||||
content_type = 'xml'
|
||||
|
||||
@classmethod
|
||||
def request(cls, environ):
|
||||
return XMLRequest(environ)
|
||||
|
||||
@classmethod
|
||||
def response(cls, data, request):
|
||||
if isinstance(data, TrytonException):
|
||||
data = client.Fault(data.code, str(data))
|
||||
elif isinstance(data, Exception):
|
||||
data = client.Fault(255, str(data))
|
||||
else:
|
||||
data = (data,)
|
||||
return Response(client.dumps(
|
||||
data, methodresponse=True, allow_none=True),
|
||||
content_type='text/xml')
|
||||
|
||||
+18
-16
@@ -8,7 +8,7 @@ except ImportError:
|
||||
from trytond.pool import Pool
|
||||
from trytond.config import config
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.exceptions import NotLogged
|
||||
from trytond import backend
|
||||
|
||||
|
||||
def _get_pool(dbname):
|
||||
@@ -31,7 +31,7 @@ def login(dbname, loginname, password, cache=True):
|
||||
Session = pool.get('ir.session')
|
||||
session, = Session.create([{}])
|
||||
return user_id, session.key
|
||||
return False
|
||||
return
|
||||
|
||||
|
||||
def logout(dbname, user, session):
|
||||
@@ -57,17 +57,19 @@ def check_super(passwd):
|
||||
|
||||
|
||||
def check(dbname, user, session):
|
||||
if user == 0:
|
||||
raise Exception('AccessDenied')
|
||||
if not user:
|
||||
raise NotLogged()
|
||||
with Transaction().start(dbname, user) as transaction:
|
||||
pool = _get_pool(dbname)
|
||||
Session = pool.get('ir.session')
|
||||
try:
|
||||
if not Session.check(user, session):
|
||||
raise NotLogged()
|
||||
else:
|
||||
return user
|
||||
finally:
|
||||
transaction.commit()
|
||||
DatabaseOperationalError = backend.get('DatabaseOperationalError')
|
||||
for count in range(config.getint('database', 'retry'), -1, -1):
|
||||
with Transaction().start(dbname, user) as transaction:
|
||||
pool = _get_pool(dbname)
|
||||
Session = pool.get('ir.session')
|
||||
try:
|
||||
if not Session.check(user, session):
|
||||
return
|
||||
else:
|
||||
return user
|
||||
except DatabaseOperationalError:
|
||||
if count:
|
||||
continue
|
||||
raise
|
||||
finally:
|
||||
transaction.commit()
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
"""
|
||||
%prog [options]
|
||||
"""
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
from getpass import getpass
|
||||
import threading
|
||||
|
||||
from sql import Table
|
||||
|
||||
from trytond.config import config, parse_listen
|
||||
from trytond import backend
|
||||
from trytond.pool import Pool
|
||||
from trytond.monitor import monitor
|
||||
from .transaction import Transaction
|
||||
|
||||
|
||||
class TrytonServer(object):
|
||||
|
||||
def __init__(self, options):
|
||||
|
||||
config.update_etc(options.configfile)
|
||||
|
||||
if options.logconf:
|
||||
logging.config.fileConfig(options.logconf)
|
||||
logging.getLogger('server').info('using %s as logging '
|
||||
'configuration file', options.logconf)
|
||||
else:
|
||||
logformat = ('%(process)s %(thread)s [%(asctime)s] '
|
||||
'%(levelname)s %(name)s %(message)s')
|
||||
if options.verbose:
|
||||
if options.dev:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.INFO
|
||||
else:
|
||||
level = logging.ERROR
|
||||
logging.basicConfig(level=level, format=logformat)
|
||||
logging.captureWarnings(True)
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
if options.configfile:
|
||||
self.logger.info('using %s as configuration file',
|
||||
options.configfile)
|
||||
else:
|
||||
self.logger.info('using default configuration')
|
||||
self.logger.info('initialising distributed objects services')
|
||||
self.xmlrpcd = []
|
||||
self.jsonrpcd = []
|
||||
self.options = options
|
||||
|
||||
if time.tzname[0] != 'UTC':
|
||||
self.logger.error('timezone is not set to UTC')
|
||||
|
||||
def run(self):
|
||||
"Run the server and never return"
|
||||
init = {}
|
||||
|
||||
signal.signal(signal.SIGINT, lambda *a: self.stop())
|
||||
signal.signal(signal.SIGTERM, lambda *a: self.stop())
|
||||
if hasattr(signal, 'SIGQUIT'):
|
||||
signal.signal(signal.SIGQUIT, lambda *a: self.stop())
|
||||
if hasattr(signal, 'SIGUSR1'):
|
||||
signal.signal(signal.SIGUSR1, lambda *a: self.restart())
|
||||
|
||||
if self.options.pidfile:
|
||||
with open(self.options.pidfile, 'w') as fd_pid:
|
||||
fd_pid.write("%d" % (os.getpid()))
|
||||
|
||||
if not self.options.update:
|
||||
self.start_servers()
|
||||
|
||||
Database = backend.get('Database')
|
||||
for db_name in self.options.database_names:
|
||||
init[db_name] = False
|
||||
try:
|
||||
with Transaction().start(db_name, 0) as transaction:
|
||||
database = Database(db_name)
|
||||
database.connect()
|
||||
if self.options.update:
|
||||
if not database.test():
|
||||
self.logger.info("init db")
|
||||
database.init()
|
||||
init[db_name] = True
|
||||
elif not database.test():
|
||||
raise Exception("'%s' is not a Tryton database!" %
|
||||
db_name)
|
||||
except Exception:
|
||||
self.stop(False)
|
||||
raise
|
||||
|
||||
for db_name in self.options.database_names:
|
||||
if self.options.update:
|
||||
with Transaction().start(db_name, 0) as transaction,\
|
||||
transaction.connection.cursor() as cursor:
|
||||
database = Database(db_name)
|
||||
database.connect()
|
||||
if not database.test():
|
||||
raise Exception("'%s' is not a Tryton database!"
|
||||
% db_name)
|
||||
lang = Table('ir_lang')
|
||||
cursor.execute(*lang.select(lang.code,
|
||||
where=lang.translatable == True))
|
||||
lang = [x[0] for x in cursor.fetchall()]
|
||||
else:
|
||||
lang = None
|
||||
Pool(db_name).init(update=self.options.update, lang=lang)
|
||||
|
||||
for db_name in self.options.database_names:
|
||||
if init[db_name]:
|
||||
# try to read password from environment variable
|
||||
# TRYTONPASSFILE, empty TRYTONPASSFILE ignored
|
||||
passpath = os.getenv('TRYTONPASSFILE')
|
||||
password = ''
|
||||
if passpath:
|
||||
try:
|
||||
with open(passpath) as passfile:
|
||||
password = passfile.readline()[:-1]
|
||||
except Exception, err:
|
||||
sys.stderr.write('Can not read password '
|
||||
'from "%s": "%s"\n' % (passpath, err))
|
||||
|
||||
if not password:
|
||||
while True:
|
||||
password = getpass('Admin Password for %s: ' % db_name)
|
||||
password2 = getpass('Admin Password Confirmation: ')
|
||||
if password != password2:
|
||||
sys.stderr.write('Admin Password Confirmation '
|
||||
'doesn\'t match Admin Password!\n')
|
||||
continue
|
||||
if not password:
|
||||
sys.stderr.write('Admin Password is required!\n')
|
||||
continue
|
||||
break
|
||||
|
||||
with Transaction().start(db_name, 0) as transaction:
|
||||
pool = Pool()
|
||||
User = pool.get('res.user')
|
||||
admin, = User.search([('login', '=', 'admin')])
|
||||
User.write([admin], {
|
||||
'password': password,
|
||||
})
|
||||
|
||||
if self.options.update:
|
||||
self.logger.info('Update/Init succeed!')
|
||||
logging.shutdown()
|
||||
sys.exit(0)
|
||||
|
||||
threads = {}
|
||||
while True:
|
||||
if self.options.cron:
|
||||
for dbname in Pool.database_list():
|
||||
thread = threads.get(dbname)
|
||||
if thread and thread.is_alive():
|
||||
continue
|
||||
pool = Pool(dbname)
|
||||
if not pool.lock.acquire(0):
|
||||
continue
|
||||
try:
|
||||
try:
|
||||
Cron = pool.get('ir.cron')
|
||||
except KeyError:
|
||||
continue
|
||||
finally:
|
||||
pool.lock.release()
|
||||
thread = threading.Thread(
|
||||
target=Cron.run,
|
||||
args=(dbname,), kwargs={})
|
||||
thread.start()
|
||||
threads[dbname] = thread
|
||||
if self.options.dev:
|
||||
for _ in range(60):
|
||||
if monitor([self.options.configfile]
|
||||
if self.options.configfile else []):
|
||||
self.restart()
|
||||
time.sleep(1)
|
||||
else:
|
||||
time.sleep(60)
|
||||
|
||||
def start_servers(self):
|
||||
ssl = config.get('ssl', 'privatekey')
|
||||
# Launch Server
|
||||
if config.get('jsonrpc', 'listen'):
|
||||
from trytond.protocols.jsonrpc import JSONRPCDaemon
|
||||
for hostname, port in parse_listen(
|
||||
config.get('jsonrpc', 'listen')):
|
||||
self.jsonrpcd.append(JSONRPCDaemon(hostname, port, ssl))
|
||||
self.logger.info("starting JSON-RPC%s protocol on %s:%d",
|
||||
ssl and ' SSL' or '', hostname or '*', port)
|
||||
|
||||
if config.get('xmlrpc', 'listen'):
|
||||
from trytond.protocols.xmlrpc import XMLRPCDaemon
|
||||
for hostname, port in parse_listen(
|
||||
config.get('xmlrpc', 'listen')):
|
||||
self.xmlrpcd.append(XMLRPCDaemon(hostname, port, ssl))
|
||||
self.logger.info("starting XML-RPC%s protocol on %s:%d",
|
||||
ssl and ' SSL' or '', hostname or '*', port)
|
||||
|
||||
for servers in (self.xmlrpcd, self.jsonrpcd):
|
||||
for server in servers:
|
||||
server.start()
|
||||
|
||||
def stop(self, exit=True):
|
||||
for servers in (self.xmlrpcd, self.jsonrpcd):
|
||||
for server in servers:
|
||||
server.stop()
|
||||
server.join()
|
||||
if exit:
|
||||
if self.options.pidfile:
|
||||
os.unlink(self.options.pidfile)
|
||||
logging.getLogger('server').info('stopped')
|
||||
logging.shutdown()
|
||||
sys.exit(0)
|
||||
|
||||
def restart(self):
|
||||
self.stop(False)
|
||||
args = ([sys.executable] + ['-W%s' % o for o in sys.warnoptions]
|
||||
+ sys.argv)
|
||||
if sys.platform == "win32":
|
||||
args = ['"%s"' % arg for arg in args]
|
||||
os.execv(sys.executable, args)
|
||||
@@ -280,12 +280,12 @@ def db_exist():
|
||||
|
||||
def create_db():
|
||||
if not db_exist():
|
||||
create(DB_NAME, None, 'en_US', USER_PASSWORD)
|
||||
create(None, DB_NAME, None, 'en_US', USER_PASSWORD)
|
||||
|
||||
|
||||
def drop_db():
|
||||
if db_exist():
|
||||
drop(DB_NAME, None)
|
||||
drop(None, DB_NAME, None)
|
||||
|
||||
|
||||
def drop_create():
|
||||
|
||||
@@ -120,7 +120,7 @@ class Transaction(object):
|
||||
self.delete = None
|
||||
self.timestamp = None
|
||||
current_instance = transactions.pop()
|
||||
assert current_instance is self
|
||||
assert current_instance is self, transactions
|
||||
|
||||
def set_context(self, context=None, **kwargs):
|
||||
if context is None:
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ from trytond.transaction import Transaction
|
||||
|
||||
__all__ = ['URLMixin']
|
||||
|
||||
HOSTNAME = (config.get('jsonrpc', 'hostname')
|
||||
HOSTNAME = (config.get('web', 'hostname')
|
||||
or socket.getfqdn())
|
||||
HOSTNAME = '.'.join(encodings.idna.ToASCII(part).decode('ascii')
|
||||
if part else '' for part in HOSTNAME.split('.'))
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import logging
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug.routing import Map, Rule
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
import wrapt
|
||||
|
||||
from trytond.protocols.wrappers import Request
|
||||
from trytond.protocols.jsonrpc import JSONProtocol
|
||||
from trytond.protocols.xmlrpc import XMLProtocol
|
||||
|
||||
__all__ = ['TrytondWSGI', 'app']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrytondWSGI(object):
|
||||
|
||||
def __init__(self):
|
||||
self.url_map = Map([])
|
||||
self.protocols = [JSONProtocol, XMLProtocol]
|
||||
self.error_handlers = []
|
||||
|
||||
def route(self, string, methods=None):
|
||||
def decorator(func):
|
||||
self.url_map.add(Rule(string, endpoint=func, methods=methods))
|
||||
return func
|
||||
return decorator
|
||||
|
||||
@wrapt.decorator
|
||||
def auth_required(self, wrapped, instance, args, kwargs):
|
||||
request = args[0]
|
||||
if request.user_id:
|
||||
return wrapped(*args, **kwargs)
|
||||
else:
|
||||
abort(303)
|
||||
|
||||
def dispatch_request(self, request):
|
||||
adapter = self.url_map.bind_to_environ(request.environ)
|
||||
try:
|
||||
endpoint, request.view_args = adapter.match()
|
||||
return endpoint(request, **request.view_args)
|
||||
except Exception, e:
|
||||
response = e
|
||||
for error_handler in self.error_handlers:
|
||||
rv = error_handler(e)
|
||||
if isinstance(rv, Response):
|
||||
response = rv
|
||||
return response
|
||||
|
||||
def wsgi_app(self, environ, start_response):
|
||||
for cls in self.protocols:
|
||||
if cls.content_type in environ.get('CONTENT_TYPE', ''):
|
||||
request = cls.request(environ)
|
||||
break
|
||||
else:
|
||||
request = Request(environ)
|
||||
data = self.dispatch_request(request)
|
||||
if not isinstance(data, Response):
|
||||
for cls in self.protocols:
|
||||
for mimetype in request.accept_mimetypes:
|
||||
if cls.content_type in mimetype:
|
||||
response = cls.response(data, request)
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
for cls in self.protocols:
|
||||
if cls.content_type in environ.get('CONTENT_TYPE', ''):
|
||||
response = cls.response(data, request)
|
||||
break
|
||||
else:
|
||||
response = Response(data)
|
||||
else:
|
||||
response = data
|
||||
# TODO custom process response
|
||||
return response(environ, start_response)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.wsgi_app(environ, start_response)
|
||||
|
||||
app = TrytondWSGI()
|
||||
Reference in New Issue
Block a user