Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

webserver.py 7.0KB

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