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

Add option to use host paths in containers in addition to http URLs #25

Merged
merged 12 commits into from
Sep 29, 2023
Merged
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ under mycroft.conf
// Optional file server support for remote clients
// "gui_file_server": true,
// "file_server_port": 8000,


// Optional support for collecting GUI files for container support
// The ovos-gui container path for these files will be {XDG_CACHE_HOME}/ovos_gui_file_server.
// With the below configuration, the GUI client will have files prefixed with the configured host path,
// so the example below describes a situation where `{XDG_CACHE_HOME}/ovos_gui_file_server` maps
// to `/tmp/gui_files` on the filesystem where the GUI client is running.
// "gui_file_host_path": "/tmp/gui_files",

// Optionally specify a default qt version for connected clients that don't report it
"default_qt_version": 5
},
Expand Down
6 changes: 4 additions & 2 deletions ovos_gui/bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ def get_client_pages(self, namespace):
"""
client_pages = []
server_url = self.ns_manager.gui_file_server.url if \
self.ns_manager.gui_file_server else None
self.ns_manager.gui_file_server else \
self.ns_manager.gui_file_host_path
for page in namespace.pages:
uri = page.get_uri(self.framework, server_url)
client_pages.append(uri)
Expand Down Expand Up @@ -260,7 +261,8 @@ def send_gui_pages(self, pages: List[GuiPage], namespace: str,
@param position: position to insert pages at
"""
server_url = self.ns_manager.gui_file_server.url if \
self.ns_manager.gui_file_server else None
self.ns_manager.gui_file_server else \
self.ns_manager.gui_file_host_path
framework = self.framework

message = {
Expand Down
6 changes: 6 additions & 0 deletions ovos_gui/gui_file_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def end_headers(self) -> None:


def start_gui_http_server(qml_path: str, port: int = None):
"""
Start an http server to host GUI Resources
@param qml_path: Local file path to server
@param port: Host port to run file server on
@return: Initialized HTTP Server
"""
port = port or Configuration().get("gui", {}).get("file_server_port", 8089)

if os.path.exists(qml_path):
Expand Down
37 changes: 25 additions & 12 deletions ovos_gui/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

from ovos_bus_client import Message, MessageBusClient
from ovos_config.config import Configuration
from ovos_utils.log import LOG
from ovos_utils.log import LOG, log_deprecation

from ovos_gui.bus import (
create_gui_service,
Expand Down Expand Up @@ -458,25 +458,36 @@ def __init__(self, core_bus: MessageBusClient):
self._system_res_dir = join(dirname(__file__), "res", "gui")
self._ready_event = Event()
self.gui_file_server = None
self.gui_file_path = None
self.gui_file_path = None # HTTP Server local path
self.gui_file_host_path = None # Docker host path
self._connected_frameworks: List[str] = list()
self._init_gui_server()
self._init_gui_file_share()
self._define_message_handlers()

@property
def _active_homescreen(self) -> str:
return Configuration().get('gui', {}).get('idle_display_skill')

def _init_gui_server(self):
def _init_gui_file_share(self):
"""
Initialize a GUI HTTP file server if enabled in configuration
Initialize optional GUI file collection. if `gui_file_path` is
defined, resources are assumed to be referenced outside this container.
If `gui_file_server` is defined, resources will be served via HTTP
"""
config = Configuration().get("gui", {})
if config.get("gui_file_server", False):
from ovos_utils.file_utils import get_temp_path
self.gui_file_host_path = config.get("gui_file_host_path")

# Check for GUI file sharing via HTTP server or mounted host path
if config.get("gui_file_server") or self.gui_file_host_path:
from ovos_utils.xdg_utils import xdg_cache_home
if config.get("server_path"):
log_deprecation("`server_path` configuration is deprecated. "
"Files will always be saved to "
"XDG_CACHE_HOME/ovos_gui_file_server", "0.1.0")
self.gui_file_path = config.get("server_path") or \
get_temp_path("ovos_gui_file_server")
self.gui_file_server = start_gui_http_server(self.gui_file_path)
join(xdg_cache_home(), "ovos_gui_file_server")
if config.get("gui_file_server"):
self.gui_file_server = start_gui_http_server(self.gui_file_path)
self._upload_system_resources()

def _define_message_handlers(self):
Expand Down Expand Up @@ -507,8 +518,8 @@ def handle_gui_pages_available(self, message: Message):
GUI framework.
@param message: `gui.volunteer_page_upload` message
"""
if not self.gui_file_path:
LOG.debug("No GUI file server running")
if not any((self.gui_file_host_path, self.gui_file_server)):
LOG.debug("No GUI file server running or host path configured")
return

LOG.debug(f"Requesting resources for {self._connected_frameworks}")
Expand Down Expand Up @@ -915,16 +926,18 @@ def handle_client_connected(self, message: Message):
dict(port=port, gui_id=gui_id))
self.core_bus.emit(message)

if self.gui_file_path:
if self.gui_file_path or self.gui_file_host_path:
if not self._ready_event.wait(90):
LOG.warning("Not reported ready after 90s")
if framework not in self._connected_frameworks:
LOG.debug(f"Requesting page upload for {framework}")
self.core_bus.emit(Message("gui.request_page_upload",
{'framework': framework},
{"source": "gui",
"destination": ["skills", "PHAL"]}))

if framework not in self._connected_frameworks:
LOG.debug(f"Connecting framework: {framework}")
self._connected_frameworks.append(framework)

def handle_page_interaction(self, message: Message):
Expand Down
11 changes: 8 additions & 3 deletions ovos_gui/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def get_uri(self, framework: str = "qt5", server_url: str = None) -> str:
"""
Get a valid URI for this Page.
@param framework: String GUI framework to get resources for
@param server_url: String server URL if available
@param server_url: String server URL if available; this could be for a
web server (http://), or a container host path (file://)
@return: Absolute path to the requested resource
"""
if self.url:
Expand All @@ -62,8 +63,12 @@ def get_uri(self, framework: str = "qt5", server_url: str = None) -> str:
self.namespace
if server_url:
if "://" not in server_url:
LOG.debug(f"No schema in server_url, assuming 'http'")
server_url = f"http://{server_url}"
if server_url.startswith("/"):
LOG.debug(f"No schema in server_url, assuming 'file'")
server_url = f"file://{server_url}"
else:
LOG.debug(f"No schema in server_url, assuming 'http'")
server_url = f"http://{server_url}"
path = f"{server_url}/{res_namespace}/{framework}/{res_filename}"
LOG.info(f"Resolved server URI: {path}")
return path
Expand Down
21 changes: 21 additions & 0 deletions test/unittests/test_bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,24 @@ def test_get_client_pages(self):
page_2.get_uri.return_value = "page_2_uri"
test_namespace.pages = [page_1, page_2]

# Specify no host path mapping
self.handler.ns_manager.gui_file_host_path = None

# Test no server_url
self.handler.ns_manager.gui_file_server = None
pages = self.handler.get_client_pages(test_namespace)
page_1.get_uri.assert_called_once_with(self.handler.framework, None)
page_2.get_uri.assert_called_once_with(self.handler.framework, None)
self.assertEqual(pages, ["page_1_uri", "page_2_uri"])

# Test host path mapping
test_path = "/test/ovos-gui-file-server"
self.handler.ns_manager.gui_file_host_path = test_path
pages = self.handler.get_client_pages(test_namespace)
page_1.get_uri.assert_called_with(self.handler.framework, test_path)
page_2.get_uri.assert_called_with(self.handler.framework, test_path)
self.assertEqual(pages, ["page_1_uri", "page_2_uri"])

# Test with server_url
self.handler.ns_manager.gui_file_server = Mock()
self.handler.ns_manager.gui_file_server.url = "server_url"
Expand Down Expand Up @@ -129,6 +140,9 @@ def test_send_gui_pages(self):
page_2 = GuiPage(None, "", False, False)
page_2.get_uri = Mock(return_value="page_2")

# Specify no host path mapping
self.handler.ns_manager.gui_file_host_path = None

# Test no server_url
self.handler.ns_manager.gui_file_server = None
self.handler._framework = "qt5"
Expand All @@ -141,6 +155,13 @@ def test_send_gui_pages(self):
"position": test_pos,
"data": [{"url": "page_1"}, {"url": "page_2"}]})

# Test host path mapping
test_path = "/test/ovos-gui-file-server"
self.handler.ns_manager.gui_file_host_path = test_path
self.handler.send_gui_pages([page_1, page_2], test_ns, test_pos)
page_1.get_uri.assert_called_with(self.handler.framework, test_path)
page_2.get_uri.assert_called_with(self.handler.framework, test_path)

# Test with server_url
self.handler.ns_manager.gui_file_server = Mock()
self.handler.ns_manager.gui_file_server.url = "server_url"
Expand Down
6 changes: 5 additions & 1 deletion test/unittests/test_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class TestNamespace(TestCase):
def setUp(self):
self.namespace = Namespace("foo")

def test_init_gui_file_share(self):
# TODO: Test init with/without server and host config
pass

def test_add(self):
add_namespace_message = dict(
type="mycroft.session.list.insert",
Expand Down Expand Up @@ -202,7 +206,7 @@ def setUp(self):
with mock.patch(PATCH_MODULE + ".create_gui_service"):
self.namespace_manager = NamespaceManager(FakeBus())

def test_init_gui_server(self):
def test_init_gui_file_share(self):
# TODO
pass

Expand Down
16 changes: 16 additions & 0 deletions test/unittests/test_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ def test_gui_page_from_server(self):
self.assertEqual(qt6,
f"https://files.local/{namespace}/qt5/{page_id}.qml")

def test_gui_page_from_mapped_path(self):
name = "test_page"
persistent = False
duration = 60
page_id = "test_page"
namespace = "skill.test"

page = GuiPage(None, name, persistent, duration, page_id, namespace)
qt5 = page.get_uri(server_url="/path/for/gui/client")
self.assertEqual(qt5,
f"file:///path/for/gui/client/{namespace}/qt5/{page_id}.qml")

qt6 = page.get_uri(server_url="/path/for/gui/client")
self.assertEqual(qt6,
f"file:///path/for/gui/client/{namespace}/qt5/{page_id}.qml")

def test_gui_page_from_local_path(self):
name = "test"
persistent = True
Expand Down
Loading