Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUI File Server and Alternate GUI Framework Support #9

Merged
merged 29 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c0a5b08
feat/qml_server
JarbasAl May 1, 2023
b3c8027
Address review feedback
NeonDaniel Jul 4, 2023
bd8e66e
Make `_framework` an instance variable instead of class
NeonDaniel Jul 4, 2023
9c0207e
Add type annotations and resolve reference errors from refactor
NeonDaniel Jul 4, 2023
f03cf60
Refactor `name` to `skill_id` in unit test
NeonDaniel Jul 4, 2023
9549c45
Fix typo in WebSocketHandler init
NeonDaniel Jul 5, 2023
64101c5
Fix handling of uploaded GUI pages
NeonDaniel Jul 5, 2023
70d339f
Fix typo in GUI file decode
NeonDaniel Jul 5, 2023
5c08676
Fix uploaded binary file handling
NeonDaniel Jul 5, 2023
40a3b44
Update resource path handling
NeonDaniel Jul 5, 2023
593a04f
Fix resource output path
NeonDaniel Jul 5, 2023
15c7e16
Refactor NamespaceManager to use `page.id` instead of `page.url` to a…
NeonDaniel Jul 6, 2023
a5bff81
Fix syntax error in type annotation
NeonDaniel Jul 6, 2023
0d567d4
Troubleshoot served GUI file resolution
NeonDaniel Jul 6, 2023
a5e8680
Troubleshoot served GUI file resolution
NeonDaniel Jul 6, 2023
5cbea01
Fix typo in file extension handling
NeonDaniel Jul 6, 2023
ecda815
Minor adjustments to GuiWebsocketHandler with added unit tests
NeonDaniel Jul 6, 2023
f8193ca
patch `Application.listen` for GHA unit test support
NeonDaniel Jul 6, 2023
59b20ce
Update default qt version handling to support config
NeonDaniel Jul 6, 2023
1cca7c6
Refactor changes
NeonDaniel Jul 6, 2023
faa8f8e
Fix missed references from refactor
NeonDaniel Jul 6, 2023
2cf3c1e
Cleanup logging
NeonDaniel Jul 6, 2023
d563bb4
Add new configuration options to README.md
NeonDaniel Jul 6, 2023
7aef588
Refactor system GUI resources
NeonDaniel Jul 6, 2023
637b78f
Handle overwriting existing served system resource files with unit test
NeonDaniel Jul 6, 2023
c5a37e8
Update `SYSTEM` resource handling with updated unit tests
NeonDaniel Jul 7, 2023
9786ca5
Keep namespace unchanged when requesting `SYSTEM` resources
NeonDaniel Jul 7, 2023
f2845da
Add handling for GUI pages made available after client connection and…
NeonDaniel Jul 7, 2023
4fcc070
Prevent handling upload requests for other skills
NeonDaniel Jul 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 64 additions & 20 deletions ovos_gui/bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@
"""
import asyncio
import json
import os.path
from threading import Lock

from ovos_bus_client import Message, GUIMessage
from ovos_config.config import Configuration
# from ovos_gui.namespace import NamespaceManager
from ovos_utils import create_daemon
from ovos_utils.log import LOG
from tornado import ioloop
from tornado.options import parse_command_line
from tornado.web import Application
from tornado.websocket import WebSocketHandler
# from ovos_gui.namespace import NamespaceManager

_write_lock = Lock()

Expand All @@ -50,10 +51,10 @@ def get_gui_websocket_config() -> dict:
return websocket_config


def create_gui_service(enclosure) -> Application:
def create_gui_service(nsmanager) -> Application:
"""
Initiate a websocket for communicating with the GUI service.
@param enclosure: NamespaceManager instance
@param nsmanager: NamespaceManager instance
"""
LOG.info('Starting message bus for GUI...')
websocket_config = get_gui_websocket_config()
Expand All @@ -62,9 +63,7 @@ def create_gui_service(enclosure) -> Application:

routes = [(websocket_config['route'], GUIWebsocketHandler)]
application = Application(routes)
# TODO: Is the NamespaceManager used by `application`, or can it be a
NeonDaniel marked this conversation as resolved.
Show resolved Hide resolved
# GUIWebsocketHandler class variable
application.enclosure = enclosure
application.nsmanager = nsmanager
application.listen(
websocket_config['base_port'], websocket_config['host']
)
Expand Down Expand Up @@ -96,6 +95,7 @@ def determine_if_gui_connected() -> bool:
class GUIWebsocketHandler(WebSocketHandler):
"""Defines the websocket pipeline between the GUI and Mycroft."""
clients = []
_framework = "qt5"
NeonDaniel marked this conversation as resolved.
Show resolved Hide resolved

def open(self):
"""
Expand All @@ -112,42 +112,71 @@ def on_close(self):
LOG.info('Closing {}'.format(id(self)))
GUIWebsocketHandler.clients.remove(self)

def get_client_pages(self, namespace):
nsmanager = self.application.nsmanager
skill_id = namespace.skill_id

client_pages = []

for page in namespace.pages:

if not page.url.startswith('http') and nsmanager.qml_server:
p = os.path.join(nsmanager.gui_file_path, skill_id, self.framework, page)
if os.path.isfile(p):
LOG.info(f"serving qml file {page.url} via {p}")
client_pages.append(p)
continue
p = os.path.join(nsmanager.gui_file_path, skill_id, self.framework, page)
if os.path.isfile(p):
LOG.info(f"serving qml file {page.url} via {p}")
client_pages.append(p)
continue

client_pages.append(page.url)

return client_pages

def synchronize(self):
"""
Upload namespaces, pages and data to the last connected client.
"""
namespace_pos = 0
enclosure = self.application.enclosure
nsmanager = self.application.nsmanager

for namespace in enclosure.active_namespaces:
LOG.info(f'Sync {namespace.name}')
for namespace in nsmanager.active_namespaces:
LOG.info(f'Sync {namespace.skill_id}')
# Insert namespace
self.send({"type": "mycroft.session.list.insert",
"namespace": "mycroft.system.active_skills",
"position": namespace_pos,
"data": [{"skill_id": namespace.name}]
"data": [{"skill_id": namespace.skill_id}]
})
# Insert pages
self.send({"type": "mycroft.gui.list.insert",
"namespace": namespace.name,
"namespace": namespace.skill_id,
"position": 0,
"data": [{"url": p.url} for p in namespace.pages]
"data": [{"url": url} for url in self.get_client_pages(namespace)]
})
# Insert data
for key, value in namespace.data.items():
self.send({"type": "mycroft.session.set",
"namespace": namespace.name,
"namespace": namespace.skill_id,
"data": {key: value}
})
namespace_pos += 1

@property
def framework(self):
return self._framework or "qt5"

def on_message(self, message: str):
"""
Handle a message on the GUI websocket. Deserialize the message, map
message types to valid equivalents for the core messagebus and emit
on the core messagebus.
@param message: Serialized Message
"""
LOG.info(f"Received: {message}")
parsed_message = GUIMessage.deserialize(message)
LOG.debug(f"Received: {parsed_message.msg_type}|{parsed_message.data}")

Expand Down Expand Up @@ -179,18 +208,36 @@ def on_message(self, message: str):
msg_data = parsed_message.data['data']
elif parsed_message.msg_type == 'mycroft.gui.connected':
# new client connected to GUI

# NOTE: mycroft-gui clients do this directly in core bus, dont send it to gui bus
# in those cases framework is always QT5 (backwards compat)
# new GUIs MUST send this message via gui websocket
# this means QT6 version of mycroft-gui WILL NOT WORK for now
NeonDaniel marked this conversation as resolved.
Show resolved Hide resolved
# TODO: Move default framework to configuration
msg_type = parsed_message.msg_type
msg_data = parsed_message.data

framework = msg_data.get("framework") # new api
if framework is None:
qt = msg_data.get("qt_version", 5) # mycroft-gui api
if int(qt) == 6:
framework = "qt6"
else:
framework = "qt5"

self._framework = framework
else:
# message not in spec
# https://github.com/MycroftAI/mycroft-gui/blob/master/transportProtocol.md
LOG.error(f"unknown GUI protocol message type, ignoring: "
f"{parsed_message}")
f"{parsed_message.msg_type}")
return

parsed_message.context["gui_framework"] = self.framework
message = Message(msg_type, msg_data, parsed_message.context)
self.application.enclosure.core_bus.emit(message)
LOG.debug('Forwarded to core bus')
LOG.info('Forwarding to core bus...')
self.application.nsmanager.core_bus.emit(message)
LOG.info('Done!')

def write_message(self, *arg, **kwarg):
"""
Expand All @@ -214,8 +261,5 @@ def send(self, data: dict):
self.write_message(s)

def check_origin(self, origin):
"""
Disable origin check to make js connections work.
"""
# TODO: Should this be implemented or deprecated
"""Disable origin check to make js connections work."""
return True
51 changes: 51 additions & 0 deletions ovos_gui/gui_file_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import http.server
import os
import shutil
import socketserver
from threading import Thread, Event

from ovos_config import Configuration
from ovos_utils.file_utils import get_temp_path
from ovos_utils.log import LOG

_HTTP_SERVER = None


class GuiFileHandler(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_gui_http_server(qml_path: str, port: int = None):
port = port or Configuration().get("gui", {}).get("qml_server_port", 8089)

if os.path.exists(qml_path):
shutil.rmtree(qml_path, ignore_errors=True)
os.makedirs(qml_path, exist_ok=True)

started_event = Event()
http_daemon = Thread(target=_initialize_http_server,
args=(started_event, qml_path, 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 = GuiFileHandler
http_server = socketserver.TCPServer(("", port), handler)
_HTTP_SERVER = http_server
_HTTP_SERVER.qml_path = directory
_HTTP_SERVER.url = f"{_HTTP_SERVER.server_address[0]}:{_HTTP_SERVER.server_address[1]}"
LOG.info(f"QML file server started: {_HTTP_SERVER.url}")
started.set()
http_server.serve_forever()
4 changes: 2 additions & 2 deletions ovos_gui/homescreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ def get_active_homescreen(self) -> Optional[dict]:
Get the active homescreen according to configuration if it is loaded
@return: Loaded homescreen with an ID matching configuration
"""
enclosure_config = Configuration().get("gui") or {}
active_homescreen = enclosure_config.get("idle_display_skill")
gui_config = Configuration().get("gui") or {}
active_homescreen = gui_config.get("idle_display_skill")
LOG.debug(f"Homescreen Manager: Active Homescreen {active_homescreen}")
for h in self.homescreens:
if h["id"] == active_homescreen:
Expand Down
Loading