diff --git a/mycroft/configuration/mycroft.conf b/mycroft/configuration/mycroft.conf index 072fe6e267b3..e73420ffaa05 100644 --- a/mycroft/configuration/mycroft.conf +++ b/mycroft/configuration/mycroft.conf @@ -119,7 +119,6 @@ // this config value is not present in mycroft-core ()internet is required) // mycroft-lib expects that some instances will be running fully offline "wait_for_internet": true, - "msm": { // Relative to "data_dir" "directory": "skills", @@ -283,8 +282,10 @@ "host": "0.0.0.0", "base_port": 18181, "route": "/gui", - "ssl": false + "ssl": false, + "run_gui_file_server": false }, + "remote-server": "", // URIs to use for testing network connection. "network_tests": { diff --git a/mycroft/gui/__init__.py b/mycroft/gui/__init__.py index 995242ae26c9..8707b87dbb9d 100644 --- a/mycroft/gui/__init__.py +++ b/mycroft/gui/__init__.py @@ -53,6 +53,9 @@ def __init__(self, skill): self.skill = skill self.on_gui_changed_callback = None self.config = Configuration.get() + self.base_skill_dir = self.config["skills"]["directory"] + self.serving_http = self.config["gui_websocket"].get("run_gui_file_server", + False) @property def bus(self): @@ -186,13 +189,19 @@ def _pages2uri(self, page_names): page_urls = [] for name in page_names: if name.startswith("SYSTEM"): - page = resolve_resource_file(join('ui', name)) + if self.serving_http: + page = f"{self.remote_url}/system/ui/{name}" + else: + page = resolve_resource_file(join('ui', name)) else: page = self.skill.find_resource(name, 'ui') + if self.serving_http: + page = page.replace(self.base_skill_dir, + join(self.remote_url, "skills")) if page: if self.remote_url: page_urls.append(self.remote_url + "/" + page) - elif page.startswith("file://"): + elif "://" in page: page_urls.append(page) else: page_urls.append("file://" + page) diff --git a/mycroft/skills/__main__.py b/mycroft/skills/__main__.py index 0179624375f8..1a0db79dd661 100644 --- a/mycroft/skills/__main__.py +++ b/mycroft/skills/__main__.py @@ -229,6 +229,7 @@ def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, event_scheduler.start() SkillApi.connect_bus(bus) skill_manager = _initialize_skill_manager(bus, watchdog) + http_server = None status.bind(bus) status.set_alive() @@ -236,6 +237,10 @@ def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, if config["skills"].get("wait_for_internet", True): _wait_for_internet_connection() + if config["gui_websocket"].get("run_gui_file_server"): + from mycroft.util.qml_file_server import start_qml_http_server + http_server = start_qml_http_server(config["skills"]["directory"]) + if skill_manager is None: skill_manager = _initialize_skill_manager(bus, watchdog) @@ -251,7 +256,7 @@ def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, wait_for_exit_signal() status.set_stopping() - shutdown(skill_manager, event_scheduler) + shutdown(skill_manager, event_scheduler, http_server) def _register_intent_services(bus): @@ -296,7 +301,7 @@ def _wait_for_internet_connection(): time.sleep(1) -def shutdown(skill_manager, event_scheduler): +def shutdown(skill_manager, event_scheduler, http_server): LOG.info('Shutting down Skills service') if event_scheduler is not None: event_scheduler.shutdown() @@ -304,6 +309,8 @@ def shutdown(skill_manager, event_scheduler): if skill_manager is not None: skill_manager.stop() skill_manager.join() + if http_server is not None: + http_server.shutdown() LOG.info('Skills service shutdown complete!') diff --git a/mycroft/util/qml_file_server.py b/mycroft/util/qml_file_server.py new file mode 100644 index 000000000000..bf9035e66619 --- /dev/null +++ b/mycroft/util/qml_file_server.py @@ -0,0 +1,84 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +import socketserver +import http.server + +from tempfile import gettempdir +from os.path import isdir, join, dirname +from threading import Thread, Event + +_HTTP_SERVER = None + + +class QmlFileHandler(http.server.SimpleHTTPRequestHandler): + def end_headers(self) -> None: + mimetype = self.guess_type(self.path) + is_file = not self.path.endswith('/') + if is_file and any([mimetype.startswith(prefix) for + prefix in ("text/", "application/octet-stream")]): + self.send_header('Content-Type', "text/plain") + self.send_header('Content-Disposition', 'inline') + super().end_headers() + + +def start_qml_http_server(skills_dir: str, port: int = 8000): + if not isdir(skills_dir): + os.makedirs(skills_dir) + system_dir = join(dirname(dirname(__file__)), "res") + + qml_dir = join(gettempdir(), "neon", "qml") + os.makedirs(qml_dir, exist_ok=True) + + served_skills_dir = join(qml_dir, "skills") + served_system_dir = join(qml_dir, "system") + if os.path.exists(served_skills_dir): + os.remove(served_skills_dir) + if os.path.exists(served_system_dir): + os.remove(served_system_dir) + + os.symlink(skills_dir, join(qml_dir, "skills")) + os.symlink(system_dir, join(qml_dir, "system")) + started_event = Event() + http_daemon = Thread(target=_initialize_http_server, + args=(started_event, qml_dir, port), + daemon=True) + http_daemon.start() + started_event.wait(30) + return _HTTP_SERVER + + +def _initialize_http_server(started: Event, directory: str, port: int): + global _HTTP_SERVER + os.chdir(directory) + handler = QmlFileHandler + http_server = socketserver.TCPServer(("", port), handler) + _HTTP_SERVER = http_server + started.set() + http_server.serve_forever()