1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Carla bridge for LV2 modguis
5# Copyright (C) 2015-2020 Filipe Coelho <falktx@falktx.com>
6#
7# This program is free software; you can redistribute it and/or
8# modify it under the terms of the GNU General Public License as
9# published by the Free Software Foundation; either version 2 of
10# the License, or 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 General Public License for more details.
16#
17# For a full copy of the GNU General Public License see the doc/GPL.txt file.
18
19# ------------------------------------------------------------------------------------------------------------
20# Imports (Global)
21
22import os
23
24from PyQt5.QtCore import pyqtSignal, QThread
25
26# ------------------------------------------------------------------------------------------------------------
27# Generate a random port number between 9000 and 18000
28
29from random import random
30
31PORTn = 8998 + int(random()*9000)
32
33# ------------------------------------------------------------------------------------------------------------
34# Imports (asyncio)
35
36try:
37    from asyncio import new_event_loop, set_event_loop
38    haveAsyncIO = True
39except:
40    haveAsyncIO = False
41
42# ------------------------------------------------------------------------------------------------------------
43# Imports (tornado)
44
45from tornado.log import enable_pretty_logging
46from tornado.ioloop import IOLoop
47from tornado.util import unicode_type
48from tornado.web import HTTPError
49from tornado.web import Application, RequestHandler, StaticFileHandler
50
51# ------------------------------------------------------------------------------------------------------------
52# Set up environment for the webserver
53
54PORT     = str(PORTn)
55ROOT     = "/usr/share/mod"
56DATA_DIR = os.path.expanduser("~/.local/share/mod-data/")
57HTML_DIR = os.path.join(ROOT, "html")
58
59os.environ['MOD_DEV_HOST'] = "1"
60os.environ['MOD_DEV_HMI']  = "1"
61os.environ['MOD_DESKTOP']  = "1"
62
63os.environ['MOD_DATA_DIR']           = DATA_DIR
64os.environ['MOD_HTML_DIR']           = HTML_DIR
65os.environ['MOD_KEY_PATH']           = os.path.join(DATA_DIR, "keys")
66os.environ['MOD_CLOUD_PUB']          = os.path.join(ROOT, "keys", "cloud_key.pub")
67os.environ['MOD_PLUGIN_LIBRARY_DIR'] = os.path.join(DATA_DIR, "lib")
68
69os.environ['MOD_PHANTOM_BINARY']        = "/usr/bin/phantomjs"
70os.environ['MOD_SCREENSHOT_JS']         = os.path.join(ROOT, "screenshot.js")
71os.environ['MOD_DEVICE_WEBSERVER_PORT'] = PORT
72
73# ------------------------------------------------------------------------------------------------------------
74# Imports (MOD)
75
76from modtools.utils import get_plugin_info, get_plugin_gui, get_plugin_gui_mini
77
78# ------------------------------------------------------------------------------------------------------------
79# MOD related classes
80
81class JsonRequestHandler(RequestHandler):
82    def write(self, data):
83        if isinstance(data, (bytes, unicode_type, dict)):
84            RequestHandler.write(self, data)
85            self.finish()
86            return
87
88        elif data is True:
89            data = "true"
90            self.set_header("Content-Type", "application/json; charset=UTF-8")
91
92        elif data is False:
93            data = "false"
94            self.set_header("Content-Type", "application/json; charset=UTF-8")
95
96        else:
97            data = json.dumps(data)
98            self.set_header("Content-Type", "application/json; charset=UTF-8")
99
100        RequestHandler.write(self, data)
101        self.finish()
102
103class EffectGet(JsonRequestHandler):
104    def get(self):
105        uri = self.get_argument('uri')
106
107        try:
108            data = get_plugin_info(uri)
109        except:
110            print("ERROR: get_plugin_info for '%s' failed" % uri)
111            raise HTTPError(404)
112
113        self.write(data)
114
115class EffectFile(StaticFileHandler):
116    def initialize(self):
117        # return custom type directly. The browser will do the parsing
118        self.custom_type = None
119
120        uri = self.get_argument('uri')
121
122        try:
123            self.modgui = get_plugin_gui(uri)
124        except:
125            raise HTTPError(404)
126
127        try:
128            root = self.modgui['resourcesDirectory']
129        except:
130            raise HTTPError(404)
131
132        return StaticFileHandler.initialize(self, root)
133
134    def parse_url_path(self, prop):
135        try:
136            path = self.modgui[prop]
137        except:
138            raise HTTPError(404)
139
140        if prop in ("iconTemplate", "settingsTemplate", "stylesheet", "javascript"):
141            self.custom_type = "text/plain"
142
143        return path
144
145    def get_content_type(self):
146        if self.custom_type is not None:
147            return self.custom_type
148        return StaticFileHandler.get_content_type(self)
149
150class EffectResource(StaticFileHandler):
151
152    def initialize(self):
153        # Overrides StaticFileHandler initialize
154        pass
155
156    def get(self, path):
157        try:
158            uri = self.get_argument('uri')
159        except:
160            return self.shared_resource(path)
161
162        try:
163            modgui = get_plugin_gui_mini(uri)
164        except:
165            raise HTTPError(404)
166
167        try:
168            root = modgui['resourcesDirectory']
169        except:
170            raise HTTPError(404)
171
172        try:
173            super(EffectResource, self).initialize(root)
174            return super(EffectResource, self).get(path)
175        except HTTPError as e:
176            if e.status_code != 404:
177                raise e
178            return self.shared_resource(path)
179        except IOError:
180            raise HTTPError(404)
181
182    def shared_resource(self, path):
183        super(EffectResource, self).initialize(os.path.join(HTML_DIR, 'resources'))
184        return super(EffectResource, self).get(path)
185
186# ------------------------------------------------------------------------------------------------------------
187# WebServer Thread
188
189class WebServerThread(QThread):
190    # signals
191    running = pyqtSignal()
192
193    def __init__(self, parent=None):
194        QThread.__init__(self, parent)
195
196        self.fApplication = Application(
197            [
198                (r"/effect/get/?", EffectGet),
199                (r"/effect/file/(.*)", EffectFile),
200                (r"/resources/(.*)", EffectResource),
201                (r"/(.*)", StaticFileHandler, {"path": HTML_DIR}),
202            ],
203            debug=True)
204
205        self.fPrepareWasCalled = False
206        self.fEventLoop = None
207
208    def run(self):
209        if not self.fPrepareWasCalled:
210            self.fPrepareWasCalled = True
211            if haveAsyncIO:
212                self.fEventLoop = new_event_loop()
213                set_event_loop(self.fEventLoop)
214            self.fApplication.listen(PORT, address="0.0.0.0")
215            if int(os.getenv("MOD_LOG", "0")):
216                enable_pretty_logging()
217
218        self.running.emit()
219        IOLoop.instance().start()
220
221    def stopWait(self):
222        IOLoop.instance().stop()
223        if self.fEventLoop is not None:
224            self.fEventLoop.call_soon_threadsafe(self.fEventLoop.stop)
225        return self.wait(5000)
226