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