2016-05-26 16:16:13 +00:00
|
|
|
# Copyright 2016 Mycroft AI, Inc.
|
|
|
|
#
|
|
|
|
# This file is part of Mycroft Core.
|
|
|
|
#
|
|
|
|
# Mycroft Core is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# Mycroft Core is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
2016-05-20 14:16:01 +00:00
|
|
|
import json
|
2017-04-17 17:25:27 +00:00
|
|
|
import os
|
2017-04-25 19:15:19 +00:00
|
|
|
import subprocess
|
2017-01-28 00:36:19 +00:00
|
|
|
import sys
|
2017-02-21 05:43:50 +00:00
|
|
|
import time
|
2017-04-06 01:01:40 +00:00
|
|
|
from os.path import exists, join
|
2017-04-17 17:25:27 +00:00
|
|
|
from threading import Timer
|
2016-05-20 14:16:01 +00:00
|
|
|
|
2017-04-05 22:11:45 +00:00
|
|
|
from mycroft import MYCROFT_ROOT_PATH
|
2016-06-17 18:47:25 +00:00
|
|
|
from mycroft.configuration import ConfigurationManager
|
2017-04-17 17:25:27 +00:00
|
|
|
from mycroft.lock import Lock # Creates PID file for single instance
|
2016-05-20 14:16:01 +00:00
|
|
|
from mycroft.messagebus.client.ws import WebsocketClient
|
2017-04-06 23:08:23 +00:00
|
|
|
from mycroft.messagebus.message import Message
|
2017-04-06 01:01:40 +00:00
|
|
|
from mycroft.skills.core import load_skill, create_skill_descriptor, \
|
2017-07-14 15:08:34 +00:00
|
|
|
MainModule, SKILLS_DIR, FallbackSkill
|
2017-04-14 08:53:29 +00:00
|
|
|
from mycroft.skills.intent_service import IntentService
|
2017-04-25 22:18:43 +00:00
|
|
|
from mycroft.util import connected
|
2016-05-20 14:16:01 +00:00
|
|
|
from mycroft.util.log import getLogger
|
2017-06-07 10:00:29 +00:00
|
|
|
from mycroft.api import is_paired
|
2017-05-09 03:30:02 +00:00
|
|
|
import mycroft.dialog
|
2016-09-17 02:08:53 +00:00
|
|
|
|
2016-05-20 14:16:01 +00:00
|
|
|
logger = getLogger("Skills")
|
|
|
|
|
|
|
|
__author__ = 'seanfitz'
|
|
|
|
|
2016-12-17 19:53:22 +00:00
|
|
|
ws = None
|
2017-02-21 05:43:50 +00:00
|
|
|
loaded_skills = {}
|
|
|
|
last_modified_skill = 0
|
|
|
|
skills_directories = []
|
2017-02-22 00:00:11 +00:00
|
|
|
skill_reload_thread = None
|
2017-04-06 23:30:58 +00:00
|
|
|
skills_manager_timer = None
|
2016-05-20 14:16:01 +00:00
|
|
|
|
2017-04-27 22:04:16 +00:00
|
|
|
installer_config = ConfigurationManager.instance().get("SkillInstallerSkill")
|
|
|
|
MSM_BIN = installer_config.get("path", join(MYCROFT_ROOT_PATH, 'msm', 'msm'))
|
2017-04-05 22:11:45 +00:00
|
|
|
|
2017-03-14 16:12:34 +00:00
|
|
|
|
2017-02-21 05:43:50 +00:00
|
|
|
def connect():
|
2016-12-17 19:53:22 +00:00
|
|
|
global ws
|
2017-02-21 05:43:50 +00:00
|
|
|
ws.run_forever()
|
|
|
|
|
|
|
|
|
2017-04-25 19:15:19 +00:00
|
|
|
def install_default_skills(speak=True):
|
2017-05-23 05:47:06 +00:00
|
|
|
"""
|
|
|
|
Install default skill set using msm.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
speak (optional): Enable response for success. Default True
|
|
|
|
"""
|
2017-04-25 19:15:19 +00:00
|
|
|
if exists(MSM_BIN):
|
2017-07-05 10:10:04 +00:00
|
|
|
p = subprocess.Popen(MSM_BIN + " default", stderr=subprocess.STDOUT,
|
|
|
|
stdout=subprocess.PIPE, shell=True)
|
|
|
|
(output, err) = p.communicate()
|
|
|
|
res = p.returncode
|
2017-05-23 05:47:06 +00:00
|
|
|
if res == 0 and speak:
|
2017-06-07 10:00:29 +00:00
|
|
|
# ws.emit(Message("speak", {
|
|
|
|
# 'utterance': mycroft.dialog.get("skills updated")}))
|
|
|
|
pass
|
2017-04-28 00:32:48 +00:00
|
|
|
elif not connected():
|
2017-07-05 10:10:04 +00:00
|
|
|
logger.error('msm failed, network connection is not available')
|
2017-04-28 00:32:48 +00:00
|
|
|
ws.emit(Message("speak", {
|
2017-05-09 03:30:02 +00:00
|
|
|
'utterance': mycroft.dialog.get("no network connection")}))
|
2017-05-23 05:47:06 +00:00
|
|
|
elif res != 0:
|
2017-07-05 10:10:04 +00:00
|
|
|
logger.error('msm failed with error {}: {}'.format(res, output))
|
2017-05-23 05:47:06 +00:00
|
|
|
ws.emit(Message("speak", {
|
|
|
|
'utterance': mycroft.dialog.get(
|
|
|
|
"sorry I couldn't install default skills")}))
|
2017-04-25 19:15:19 +00:00
|
|
|
|
|
|
|
else:
|
|
|
|
logger.error("Unable to invoke Mycroft Skill Manager: " + MSM_BIN)
|
|
|
|
|
|
|
|
|
2017-04-06 23:08:23 +00:00
|
|
|
def skills_manager(message):
|
2017-04-06 23:30:58 +00:00
|
|
|
global skills_manager_timer, ws
|
2017-04-14 08:53:29 +00:00
|
|
|
|
2017-05-09 03:30:02 +00:00
|
|
|
if connected():
|
|
|
|
if skills_manager_timer is None:
|
2017-06-07 10:00:29 +00:00
|
|
|
pass
|
|
|
|
# ws.emit(
|
|
|
|
# Message("speak", {'utterance':
|
|
|
|
# mycroft.dialog.get("checking for updates")}))
|
2017-04-14 08:53:29 +00:00
|
|
|
|
2017-05-09 03:30:02 +00:00
|
|
|
# Install default skills and look for updates via Github
|
|
|
|
logger.debug("==== Invoking Mycroft Skill Manager: " + MSM_BIN)
|
|
|
|
install_default_skills(False)
|
2017-04-14 08:53:29 +00:00
|
|
|
|
|
|
|
# Perform check again once and hour
|
2017-04-25 19:15:19 +00:00
|
|
|
skills_manager_timer = Timer(3600, _skills_manager_dispatch)
|
2017-04-06 23:08:23 +00:00
|
|
|
skills_manager_timer.daemon = True
|
|
|
|
skills_manager_timer.start()
|
|
|
|
|
|
|
|
|
2017-04-14 08:53:29 +00:00
|
|
|
def _skills_manager_dispatch():
|
2017-04-06 23:08:23 +00:00
|
|
|
ws.emit(Message("skill_manager", {}))
|
|
|
|
|
|
|
|
|
2017-04-14 20:50:30 +00:00
|
|
|
def _load_skills():
|
2017-02-22 00:00:11 +00:00
|
|
|
global ws, loaded_skills, last_modified_skill, skills_directories, \
|
|
|
|
skill_reload_thread
|
2017-02-21 05:43:50 +00:00
|
|
|
|
2017-04-25 22:18:43 +00:00
|
|
|
check_connection()
|
|
|
|
|
2017-07-14 15:08:34 +00:00
|
|
|
ws.on('intent_failure', FallbackSkill.make_intent_failure_handler(ws))
|
2017-06-16 21:40:12 +00:00
|
|
|
|
2017-04-14 08:53:29 +00:00
|
|
|
# Create skill_manager listener and invoke the first time
|
2017-04-06 23:08:23 +00:00
|
|
|
ws.on('skill_manager', skills_manager)
|
2017-04-25 22:18:43 +00:00
|
|
|
ws.on('mycroft.internet.connected', install_default_skills)
|
2017-04-25 19:18:02 +00:00
|
|
|
ws.emit(Message('skill_manager', {}))
|
2017-04-06 23:08:23 +00:00
|
|
|
|
2017-04-14 08:53:29 +00:00
|
|
|
# Create the Intent manager, which converts utterances to intents
|
|
|
|
# This is the heart of the voice invoked skill system
|
|
|
|
IntentService(ws)
|
|
|
|
|
|
|
|
# Create a thread that monitors the loaded skills, looking for updates
|
|
|
|
skill_reload_thread = Timer(0, _watch_skills)
|
2017-02-22 00:00:11 +00:00
|
|
|
skill_reload_thread.daemon = True
|
|
|
|
skill_reload_thread.start()
|
2016-12-20 21:19:22 +00:00
|
|
|
|
2016-05-20 14:16:01 +00:00
|
|
|
|
2017-04-25 22:18:43 +00:00
|
|
|
def check_connection():
|
|
|
|
if connected():
|
|
|
|
ws.emit(Message('mycroft.internet.connected'))
|
2017-06-07 10:00:29 +00:00
|
|
|
# check for pairing, if not automatically start pairing
|
|
|
|
if not is_paired():
|
|
|
|
# begin the process
|
|
|
|
payload = {
|
2017-06-08 00:40:34 +00:00
|
|
|
'utterances': ["pair my device"],
|
2017-06-07 10:00:29 +00:00
|
|
|
'lang': "en-us"
|
|
|
|
}
|
|
|
|
ws.emit(Message("recognizer_loop:utterance", payload))
|
2017-04-25 22:18:43 +00:00
|
|
|
else:
|
|
|
|
thread = Timer(1, check_connection)
|
|
|
|
thread.daemon = True
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
|
2017-04-19 00:46:02 +00:00
|
|
|
def _get_last_modified_date(path):
|
2017-04-20 00:55:37 +00:00
|
|
|
last_date = 0
|
2017-04-19 00:46:02 +00:00
|
|
|
# getting all recursive paths
|
|
|
|
for root, _, _ in os.walk(path):
|
|
|
|
f = root.replace(path, "")
|
|
|
|
# checking if is a hidden path
|
|
|
|
if not f.startswith(".") and not f.startswith("/."):
|
2017-04-20 00:55:37 +00:00
|
|
|
last_date = max(last_date, os.path.getmtime(path + f))
|
|
|
|
|
|
|
|
return last_date
|
2017-04-19 00:46:02 +00:00
|
|
|
|
|
|
|
|
2017-04-14 08:53:29 +00:00
|
|
|
def _watch_skills():
|
2017-04-06 01:01:40 +00:00
|
|
|
global ws, loaded_skills, last_modified_skill, \
|
2017-04-05 00:11:33 +00:00
|
|
|
id_counter
|
2017-03-14 16:12:34 +00:00
|
|
|
|
2017-04-14 08:53:29 +00:00
|
|
|
# Scan the file folder that contains Skills. If a Skill is updated,
|
|
|
|
# unload the existing version from memory and reload from the disk.
|
2017-02-23 18:02:11 +00:00
|
|
|
while True:
|
2017-04-06 01:01:40 +00:00
|
|
|
if exists(SKILLS_DIR):
|
2017-04-19 00:46:02 +00:00
|
|
|
# checking skills dir and getting all skills there
|
2017-04-06 18:59:51 +00:00
|
|
|
list = filter(lambda x: os.path.isdir(
|
|
|
|
os.path.join(SKILLS_DIR, x)), os.listdir(SKILLS_DIR))
|
2017-04-19 00:46:02 +00:00
|
|
|
|
2017-04-06 01:01:40 +00:00
|
|
|
for skill_folder in list:
|
|
|
|
if skill_folder not in loaded_skills:
|
|
|
|
loaded_skills[skill_folder] = {}
|
|
|
|
skill = loaded_skills.get(skill_folder)
|
|
|
|
skill["path"] = os.path.join(SKILLS_DIR, skill_folder)
|
2017-04-19 00:46:02 +00:00
|
|
|
# checking if is a skill
|
2017-04-06 01:01:40 +00:00
|
|
|
if not MainModule + ".py" in os.listdir(skill["path"]):
|
|
|
|
continue
|
2017-04-19 00:46:02 +00:00
|
|
|
# getting the newest modified date of skill
|
|
|
|
skill["last_modified"] = _get_last_modified_date(skill["path"])
|
2017-04-06 01:01:40 +00:00
|
|
|
modified = skill.get("last_modified", 0)
|
2017-04-19 00:46:02 +00:00
|
|
|
# checking if skill is loaded and wasn't modified
|
2017-04-06 01:01:40 +00:00
|
|
|
if skill.get(
|
|
|
|
"loaded") and modified <= last_modified_skill:
|
|
|
|
continue
|
2017-04-19 00:46:02 +00:00
|
|
|
# checking if skill was modified
|
2017-04-06 01:01:40 +00:00
|
|
|
elif skill.get(
|
|
|
|
"instance") and modified > last_modified_skill:
|
2017-04-19 00:46:02 +00:00
|
|
|
# checking if skill should be reloaded
|
2017-04-06 01:01:40 +00:00
|
|
|
if not skill["instance"].reload_skill:
|
2017-02-23 18:02:11 +00:00
|
|
|
continue
|
2017-04-06 01:01:40 +00:00
|
|
|
logger.debug("Reloading Skill: " + skill_folder)
|
2017-04-19 00:46:02 +00:00
|
|
|
# removing listeners and stopping threads
|
2017-04-06 01:01:40 +00:00
|
|
|
skill["instance"].shutdown()
|
|
|
|
del skill["instance"]
|
|
|
|
skill["loaded"] = True
|
|
|
|
skill["instance"] = load_skill(
|
|
|
|
create_skill_descriptor(skill["path"]), ws)
|
2017-04-19 00:46:02 +00:00
|
|
|
# get the last modified skill
|
2017-04-17 18:27:07 +00:00
|
|
|
modified_dates = map(lambda x: x.get("last_modified"),
|
|
|
|
loaded_skills.values())
|
|
|
|
if len(modified_dates) > 0:
|
|
|
|
last_modified_skill = max(modified_dates)
|
2017-04-14 08:53:29 +00:00
|
|
|
|
|
|
|
# Pause briefly before beginning next scan
|
2017-02-23 18:02:11 +00:00
|
|
|
time.sleep(2)
|
2016-05-20 14:16:01 +00:00
|
|
|
|
|
|
|
|
2017-07-11 02:49:37 +00:00
|
|
|
def _starting_up():
|
2017-07-11 04:22:29 +00:00
|
|
|
# Startup: Kick off loading of skills
|
2017-07-11 02:49:37 +00:00
|
|
|
_load_skills()
|
|
|
|
|
|
|
|
|
2016-05-20 14:16:01 +00:00
|
|
|
def main():
|
2016-12-17 19:53:22 +00:00
|
|
|
global ws
|
2017-04-14 08:53:29 +00:00
|
|
|
lock = Lock('skills') # prevent multiple instances of this service
|
|
|
|
|
|
|
|
# Connect this Skill management process to the websocket
|
2016-12-17 19:53:22 +00:00
|
|
|
ws = WebsocketClient()
|
|
|
|
ConfigurationManager.init(ws)
|
2016-05-20 14:16:01 +00:00
|
|
|
|
2017-04-14 08:53:29 +00:00
|
|
|
ignore_logs = ConfigurationManager.instance().get("ignore_logs")
|
2017-04-06 17:45:28 +00:00
|
|
|
|
2017-04-14 08:53:29 +00:00
|
|
|
# Listen for messages and echo them for logging
|
|
|
|
def _echo(message):
|
2016-05-20 14:16:01 +00:00
|
|
|
try:
|
|
|
|
_message = json.loads(message)
|
|
|
|
|
2017-04-06 17:45:28 +00:00
|
|
|
if _message.get("type") in ignore_logs:
|
|
|
|
return
|
|
|
|
|
2016-09-04 00:59:39 +00:00
|
|
|
if _message.get("type") == "registration":
|
2016-05-20 14:16:01 +00:00
|
|
|
# do not log tokens from registration messages
|
2016-09-04 01:24:18 +00:00
|
|
|
_message["data"]["token"] = None
|
2016-05-20 14:16:01 +00:00
|
|
|
message = json.dumps(_message)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
logger.debug(message)
|
2017-04-17 17:25:27 +00:00
|
|
|
|
2017-04-14 08:53:29 +00:00
|
|
|
ws.on('message', _echo)
|
2016-05-20 14:16:01 +00:00
|
|
|
|
2017-07-11 02:49:37 +00:00
|
|
|
# Startup will be called after websocket is full live
|
|
|
|
ws.once('open', _starting_up)
|
2016-12-17 19:53:22 +00:00
|
|
|
ws.run_forever()
|
2016-05-20 14:16:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2017-01-27 03:00:37 +00:00
|
|
|
try:
|
|
|
|
main()
|
|
|
|
except KeyboardInterrupt:
|
2017-04-05 22:11:45 +00:00
|
|
|
skills_manager_timer.cancel()
|
2017-02-21 05:43:50 +00:00
|
|
|
for skill in loaded_skills:
|
2017-01-27 18:32:20 +00:00
|
|
|
skill.shutdown()
|
2017-02-22 00:00:11 +00:00
|
|
|
if skill_reload_thread:
|
|
|
|
skill_reload_thread.cancel()
|
|
|
|
|
2017-01-28 00:36:19 +00:00
|
|
|
finally:
|
|
|
|
sys.exit()
|