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 QML file server for Container implementations #38

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
3 changes: 2 additions & 1 deletion mycroft/configuration/mycroft.conf
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
// 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,

"run_gui_file_server": false,
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved
"msm": {
// Relative to "data_dir"
"directory": "skills",
Expand Down Expand Up @@ -285,6 +285,7 @@
"route": "/gui",
"ssl": false
},
"remote-server": "http://localhost:8000",
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved

// URIs to use for testing network connection.
"network_tests": {
Expand Down
13 changes: 11 additions & 2 deletions mycroft/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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["skills"].get("run_gui_file_server",
False)

@property
def bus(self):
Expand Down Expand Up @@ -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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this not conflict with remote, if self.remote_url = "https://127.0.0.1/" , elif will also catch "://" ? maybe this should depend on if the inbult http server is enabled in addition to "://"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If self.remote_url is defined, this clause will never be parsed. I added this because I think :// will always relate to an absolute path, so some absolute path like smb:// or http:// could be defined without self.remote_url (i.e. I want to reference an exact QML file I host without leaving it up to a file resolver method)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it still run the startswith or would that be some slow regex stuff?

page_urls.append(page)
else:
page_urls.append("file://" + page)
Expand Down
11 changes: 9 additions & 2 deletions mycroft/skills/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,18 @@ 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()

if config["skills"].get("wait_for_internet", True):
_wait_for_internet_connection()

if config["skills"].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)

Expand All @@ -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):
Expand Down Expand Up @@ -296,14 +301,16 @@ 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()
# Terminate all running threads that update skills
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!')


Expand Down
84 changes: 84 additions & 0 deletions mycroft/util/qml_file_server.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if using port 8000 is a wise thing todo as it is being used as alternative http port and many other programs already use it as well. This might run into conflicts a lot.

Perhaps choose another port as default, just like the 18181 for the websocket?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, many demo apps use 8000 ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or make it a config variable just like the gui websocket port and if not set, make it gui websocket +1

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, thanks for catching this. I meant to put this in a config option too

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()