diff --git a/CHANGELOG b/CHANGELOG index c93645bb21..74a5486766 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Handle URL * Add communication between boards * Added domain inversion feature * Handle loading attribute on fields diff --git a/doc/usage.rst b/doc/usage.rst index 8129b9331a..0212d10414 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -17,7 +17,7 @@ Synopsis :: - tryton [options] + tryton [options] [url] After startup, there raises the `login dialog`__ and optionally a `tips dialog`__. @@ -48,6 +48,30 @@ Options -s SERVER, --server=SERVER Specify the server hostname +URL +*** + +When an url is passed, the client will try to find already running client that +could handle it and send to this one to open the url. If it doesn't find one +then it will start the GUI and open the url itself. + +The url schemes are: + + `tryton://[:]//model/[/][;parameters]` + + `tryton://[:]//wizard/[;parameters]` + + `tryton://[:]//report/[;parameters]` + + +where `parameters` are the corresponding fields of actions encoded in +`JSON`_. + +.. _JSON: http://en.wikipedia.org/wiki/Json +.. Note:: `model` is for `act_window` +.. Note:: `report` must have at least a data parameter with `ids`, `id` and + `model name` + Overview ******** diff --git a/setup.nsi b/setup.nsi index 3920e70c0b..17e05c59fe 100644 --- a/setup.nsi +++ b/setup.nsi @@ -98,6 +98,12 @@ SectionIn 1 2 RO SetOutPath "$INSTDIR\doc" File /r "doc\*" + ;Register URL protocol + WriteRegStr HKCR "tryton" "" "URL:Tryton Protocol" + WriteRegStr HKCR "tryton" "URL Protocol" "" + WriteRegStr HKCR "tryton\DefaultIcon" "" "$INSTDIR\tryton.exe,1" + WriteRegStr HKCR "tryton\shell\open\command" "" '$INSTDIR\tryton.exe "%1"' + ;Write the installation path into the registry WriteRegStr HKCU "Software\tryton-${VERSION}" "" $INSTDIR WriteRegStr HKLM "Software\tryton-${VERSION}" "" $INSTDIR diff --git a/tryton/client.py b/tryton/client.py index 47d2ea7c23..338f25bab3 100644 --- a/tryton/client.py +++ b/tryton/client.py @@ -12,6 +12,7 @@ if not (os.name in ('nt', 'mac') or \ (hasattr(os, 'uname') and os.uname()[0] == 'Darwin')): gtk.gdk.threads_init() import logging +from urlparse import urlparse from tryton import version from tryton import config @@ -19,6 +20,7 @@ from tryton.config import CONFIG, CURRENT_DIR, PREFIX, PIXMAPS_DIR, \ TRYTON_ICON, get_config_dir from tryton import translate from tryton import gui +from tryton.ipc import Client as IPCClient import traceback import time import signal @@ -29,6 +31,19 @@ class TrytonClient(object): def __init__(self): CONFIG.parse() + if CONFIG.arguments: + url, = CONFIG.arguments + urlp = urlparse(url) + if urlp.scheme == 'tryton': + urlp = urlparse('http' + url[6:]) + hostname, port = (urlp.netloc.split(':', 1) + + [CONFIG.defaults['login.port']])[:2] + database, _ = (urlp.path[1:].split('/', 1) + [None])[:2] + if IPCClient(hostname, port, database).write(url): + sys.exit(0) + CONFIG['login.server'] = hostname + CONFIG['login.port'] = port + CONFIG['login.db'] = database logging.basicConfig() translate.set_language_direction(CONFIG['client.language_direction']) translate.setlang(CONFIG['client.lang']) diff --git a/tryton/config.py b/tryton/config.py index 06c054a3be..92ee696b30 100644 --- a/tryton/config.py +++ b/tryton/config.py @@ -90,9 +90,11 @@ class ConfigManager(object): self.options = { 'login.host': True } + self.arguments = [] def parse(self): - parser = optparse.OptionParser(version=("Tryton %s" % VERSION)) + parser = optparse.OptionParser(version=("Tryton %s" % VERSION), + usage="Usage: %prog [options] [url]") parser.add_option("-c", "--config", dest="config", help=_("specify alternate config file")) parser.add_option("-v", "--verbose", action="store_true", @@ -109,8 +111,10 @@ class ConfigManager(object): help=_("specify the server port")) parser.add_option("-s", "--server", dest="server", help=_("specify the server hostname")) - opt = parser.parse_args()[0] + opt, self.arguments = parser.parse_args() + if len(self.arguments) > 1: + raise Exception(_('Too much arguments')) if opt.config and not os.path.isfile(opt.config): raise Exception(_('File "%s" not found') % (opt.config,)) diff --git a/tryton/gui/main.py b/tryton/gui/main.py index 5f7537d9d1..a659de626f 100644 --- a/tryton/gui/main.py +++ b/tryton/gui/main.py @@ -3,14 +3,21 @@ import os import sys import gettext -import urlparse +from urlparse import urlparse +import urllib import gobject import gtk +if sys.version_info < (2, 6): + import simplejson as json +else: + import json +import webbrowser import tryton.rpc as rpc from tryton.config import CONFIG, TRYTON_ICON, PIXMAPS_DIR, DATA_DIR, \ get_config_dir import tryton.common as common from tryton.action import Action +from tryton.wizard import Wizard from tryton.gui.window import Window from tryton.gui.window.preference import Preference from tryton.gui.window import Limit @@ -1186,6 +1193,9 @@ class Main(object): self.menuitem_form.set_sensitive(True) self.menuitem_plugins.set_sensitive(True) self.notebook.grab_focus() + if CONFIG.arguments: + url = CONFIG.arguments.pop() + self.open_url(url) return True def sig_logout(self, widget, disconnect=True): @@ -1616,3 +1626,121 @@ class Main(object): else: rpc.logout() Main.get_main().refresh_ssl() + + def _open_url(self, url): + url = urllib.unquote(url) + urlp = urlparse(url) + if not urlp.scheme == 'tryton': + return + urlp = urlparse('http' + url[6:]) + hostname, port = (urlp.netloc.split(':', 1) + + [CONFIG.defaults['login.port']])[:2] + database, path = (urlp.path[1:].split('/', 1) + [None])[:2] + if (not path or + hostname != rpc._SOCK.hostname or + int(port) != rpc._SOCK.port or + database != rpc._DATABASE): + return + type_, path = (path.split('/', 1) + [''])[:2] + params = {} + if urlp.params: + try: + params = dict(param.split('=', 1) + for param in urlp.params.split('&')) + except Exception: + return + + def open_model(path): + model, path = (path.split('/', 1) + [''])[:2] + if not model: + return + res_id = False + mode = None + view_type = params.get('view_type', 'form') + try: + view_ids = json.loads(params.get('views', 'false')) + limit = json.loads(params.get('limit', 'null')) + auto_refresh = json.loads(params.get('auto_refresh', 'false')) + name = json.loads(params.get('window_name', 'false')) + search_value = json.loads(params.get('search_value', '{}')) + domain = json.loads(params.get('domain', '[]')) + context = json.loads(params.get('context', '{}')) + except ValueError: + return + if path: + try: + res_id = int(path) + except ValueError: + return + mode = ['form', 'tree'] + try: + Window.create(view_ids, model, res_id=res_id, domain=domain, + view_type=view_type, window=self.window, + context=context, mode=mode, name=name, limit=limit, + auto_refresh=auto_refresh, search_value=search_value) + except Exception: + return + + def open_wizard(wizard): + if not wizard: + return + try: + data = json.loads(params.get('data', '{}')) + direct_print = json.loads(params.get('direct_print', 'false')) + email_print = json.loads(params.get('email_print', 'false')) + email = json.loads(params.get('email', 'null')) + name = json.loads(params.get('name', 'false')) + window = json.loads(params.get('window', 'false')) + context = json.loads(params.get('context', '{}')) + except ValueError: + return + try: + if window: + Window.create_wizard(wizard, data, self.window, + direct_print=direct_print, email_print=email_print, + email=email, name=name, context=context) + else: + Wizard.execute(wizard, data, self.window, + direct_print=direct_print, email_print=email_print, + email=email, context=context) + except Exception: + return + + def open_report(report): + if not report: + return + try: + data = json.loads(params.get('data')) + direct_print = json.loads(params.get('direct_print', 'false')) + email_print = json.loads(params.get('email_print', 'false')) + email = json.loads(params.get('email', 'null')) + name = json.loads(params.get('name', 'false')) + context = json.loads(params.get('context', '{}')) + except ValueError: + return + try: + Action.exec_report(report, data, self.window, + direct_print=direct_print, email_print=email_print, + email=email, context=context) + except Exception: + return + + def open_url(): + try: + url = json.loads(params.get('url', 'false')) + except ValueError: + return + if url: + webbrowser.open(url, new=2) + + if type_ == 'model': + open_model(path) + elif type_ == 'wizard': + open_wizard(path) + elif type_ == 'report': + open_report(path) + elif type_ == 'url': + open_url() + + def open_url(self, url): + gobject.idle_add(self._open_url, url) diff --git a/tryton/ipc.py b/tryton/ipc.py new file mode 100644 index 0000000000..85d0cae441 --- /dev/null +++ b/tryton/ipc.py @@ -0,0 +1,178 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. +""" +Inter-Process Communication +""" +import tempfile +import os +import threading +import select +import time +from tryton.config import get_config_dir + +__all__ = ['Server', 'Client'] + +class IPCServer(object): + + thread = None + running = None + instance = None + + def __init__(self, hostname, port, database): + self.hostname = hostname + self.port = port + self.database = database + self.config = os.path.join(get_config_dir(), '%s@%s@%s' % + (self.hostname, self.port, self.database)) + self.tmpdir = tempfile.mkdtemp(prefix='.tryton') + Server.instance = self + + def setup(self): + raise NotImplemented + + def run(self): + self.setup() + self.running = True + self.thread = threading.Thread(target=self._read) + self.thread.start() + + def clean(self): + raise NotImplemented + + def stop(self): + self.running = False + self.thread.join() + self.thread = None + self.clean() + Server.instance = None + + def _read(self): + raise NotImplemented + + +class FileServer(IPCServer): + + def setup(self): + config = open(self.config, 'w') + print >> config, self.tmpdir + + def clean(self): + try: + os.remove(self.config) + os.rmdir(self.tmpdir) + except OSError: + pass + + def _read(self): + to_remove = set() + while self.running: + for filename in os.listdir(self.tmpdir): + path = os.path.join(self.tmpdir, filename) + if not os.path.isfile(path): + continue + if path in to_remove: + continue + try: + data = open(path, 'r').readline() + except IOError: + pass + if data and data[-1] != '\n': + continue + to_remove.add(path) + if data: + from tryton.gui.main import Main + Main.get_main().open_url(data[:-1]) + if not os.path.exists(self.config): + self.setup() + for path in to_remove.copy(): + try: + os.remove(path) + except Exception: + continue + to_remove.remove(path) + time.sleep(1) + + +class FIFOServer(IPCServer): + + def setup(self): + self.filename = os.path.join(self.tmpdir, 'Socket') + os.mkfifo(self.filename, 0600) + if os.path.lexists(self.config): + os.remove(self.config) + os.symlink(self.filename, self.config) + + def clean(self): + try: + os.remove(self.config) + os.rmdir(self.tmpdir) + except OSError: + pass + + def _read(self): + fifo = os.fdopen(os.open(self.filename, os.O_RDONLY|os.O_NONBLOCK)) + data = '' + while self.running: + rlist, _, _ = select.select([fifo], [], [], 1) + if rlist: + try: + data += fifo.readline() + except IOError: + pass + if data and data[-1] != '\n': + continue + if data: + from tryton.gui.main import Main + Main.get_main().open_url(data.strip()) + data = '' + if not os.path.lexists(self.config): + os.symlink(self.filename, self.config) + + +class IPCClient(object): + + def __init__(self, hostname, port, database): + self.hostname = hostname + self.port = port + self.database = database + self.filename = os.path.join(get_config_dir(), + '%s@%s@%s' % (hostname, port, database)) + + def write(self, message): + raise NotImplemented + + +class FileClient(IPCClient): + + def __init__(self, hostname, port, database): + super(FileClient, self).__init__(hostname, port, database) + + def write(self, message): + if not os.path.exists(self.filename): + return False + tmpdir = open(self.filename, 'r').readline().strip() + _, tmpfile = tempfile.mkstemp(dir=tmpdir, text=True) + tmpfile = open(tmpfile, 'w') + print >> tmpfile, message + tmpfile.close() + return True + + +class FIFOClient(IPCClient): + + def __init__(self, hostname, port, database): + super(FIFOClient, self).__init__(hostname, port, database) + + def write(self, message): + if not os.path.lexists(self.filename): + return False + fifo = open(self.filename, 'w') + print >> fifo, message + return True + +if hasattr(os, 'mkfifo'): + Server = FIFOServer + Client = FIFOClient +else: + Server = FileServer + Client = FileClient diff --git a/tryton/rpc.py b/tryton/rpc.py index 892222c695..7e14218a61 100644 --- a/tryton/rpc.py +++ b/tryton/rpc.py @@ -7,6 +7,7 @@ import os from threading import Semaphore from tryton.fingerprints import Fingerprints from tryton.config import get_config_dir +from tryton.ipc import Server as IPCServer _SOCK = None _USER = None @@ -154,10 +155,13 @@ def login(username, password, host, port, database): _DATABASE = database SECURE = _SOCK.ssl context_reload() + IPCServer(host, port, database).run() return 1 def logout(): global _SOCK, _USER, _USERNAME, _SESSION, _DATABASE, _VIEW_CACHE, SECURE + if IPCServer.instance: + IPCServer.instance.stop() if _SOCK and _USER: try: _SEMAPHORE.acquire()