def _getSkillUpdateVersion(self, skillName: str) -> Optional[tuple]: versionMapping = self._skillStoreData.get(skillName, dict()).get( 'versionMapping', dict()) userUpdatePref = self.ConfigManager.getAliceConfigByName( 'skillsUpdateChannel') skillUpdateVersion = (Version(), 'master') aliceVersion = Version.fromString(constants.VERSION) for aliceMinVersion, repoVersion in versionMapping.items(): aliceMinVersion = Version.fromString(aliceMinVersion) repoVersion = Version.fromString(repoVersion) if not repoVersion.isVersionNumber or not aliceMinVersion.isVersionNumber or aliceMinVersion > aliceVersion: continue releaseType = repoVersion.releaseType if userUpdatePref == 'master' and releaseType in {'rc', 'b', 'a'} \ or userUpdatePref == 'rc' and releaseType in {'b', 'a'} \ or userUpdatePref == 'beta' and releaseType == 'a': continue if repoVersion > skillUpdateVersion[0]: skillUpdateVersion = ( repoVersion, f'{str(repoVersion)}_{str(aliceMinVersion)}') if not skillUpdateVersion[0].isVersionNumber: raise GithubNotFound return skillUpdateVersion
def updateProjectAlice(self): self._logger.logInfo('Checking for satellite updates') req = requests.get(url=f'{constants.GITHUB_API_URL}/ProjectAliceSatellite/branches', auth=SuperManager.getInstance().configManager.getGithubAuth()) if req.status_code != 200: self._logger.logWarning('Failed checking for updates') return userUpdatePref = SuperManager.getInstance().configManager.getAliceConfigByName('aliceUpdateChannel') if userUpdatePref == 'master': candidate = 'master' else: candidate = Version.fromString(constants.VERSION) for branch in req.json(): repoVersion = Version.fromString(branch['name']) if not repoVersion.isVersionNumber: continue releaseType = repoVersion.releaseType if userUpdatePref == 'rc' and releaseType in {'b', 'a'} or userUpdatePref == 'beta' and releaseType == 'a': continue if repoVersion > candidate: candidate = repoVersion self._logger.logInfo(f'Checking on "{str(candidate)}" update channel') commons = SuperManager.getInstance().commons commons.runSystemCommand(['git', '-C', commons.rootDir(), 'stash']) commons.runSystemCommand(['git', '-C', commons.rootDir(), 'clean', '-df']) commons.runSystemCommand(['git', '-C', commons.rootDir(), 'checkout', str(candidate)]) commons.runSystemCommand(['git', '-C', commons.rootDir(), 'pull'])
def loadStoreData(self): self.SkillStoreManager.refreshStoreData() skillStoreData = self.SkillStoreManager.skillStoreData activeLanguage = self.LanguageManager.activeLanguage.lower() aliceVersion = Version.fromString(constants.VERSION) supportedSkillStoreData = dict() for skillName, skillInfo in skillStoreData.items(): if self.SkillManager.getSkillInstance(skillName=skillName, silent=True) \ or ('lang' in skillInfo['conditions'] and activeLanguage not in skillInfo['conditions']['lang']): continue version = Version() for aliceMinVersion, skillVersion in skillInfo[ 'versionMapping'].items(): if Version.fromString(aliceMinVersion) > aliceVersion: continue skillRepoVersion = Version.fromString(skillVersion) if skillRepoVersion > version: version = skillRepoVersion skillInfo['version'] = str(version) supportedSkillStoreData[skillName] = skillInfo return supportedSkillStoreData
def checkDependencies(self) -> bool: self.logInfo('Checking dependencies') for dep in { **self.DEPENDENCIES.get('internal', dict()), **self.DEPENDENCIES.get('external', dict()) }: result = self.Commons.runRootSystemCommand( ['dpkg-query', '-l', dep]) if result.returncode: self.logWarning(f'Found missing dependency: {dep}') return False for dep in self.DEPENDENCIES['pip']: match = re.match(r'^([a-zA-Z0-9-_]*)(?:([=><]{0,2})([\d.]*)$)', dep) if not match: continue packageName, operator, version = match.groups() if not packageName: self.logWarning('Wrongly declared PIP requirement') continue try: installedVersion = packageVersion(packageName) except PackageNotFoundError: self.logWarning(f'Found missing dependencies: {packageName}') return False if not installedVersion or not operator or not version: continue version = Version.fromString(version) installedVersion = Version.fromString(installedVersion) if (operator == '==' and version != installedVersion) or \ (operator == '>=' and installedVersion < version) or \ (operator == '>' and (installedVersion < version or installedVersion == version)) or \ (operator == '<' and (installedVersion > version or installedVersion == version)): self.logWarning( f'Dependency "{packageName}" is not conform with version requirements' ) return False for dep in self.DEPENDENCIES['system']: result = self.Commons.runRootSystemCommand( ['dpkg-query', '-l', dep]) if result.returncode: self.logWarning(f'Found missing dependency: {dep}') return False return True
def getSkillScenarioVersion(self, skillName: str) -> Version: if skillName not in self._skillList: return Version.fromString('0.0.0') else: # noinspection SqlResolve query = 'SELECT * FROM :__table__ WHERE skillName = :skillName' data = self.DatabaseManager.fetch(tableName='skills', query=query, values={'skillName': skillName}, callerName=self.name) if not data: return Version.fromString('0.0.0') return Version.fromString(data[0]['scenarioVersion'])
def checkSkillConditions(self, installer: dict = None) -> bool: conditions = { 'aliceMinVersion': installer['aliceMinVersion'], **installer.get('conditions', dict()) } notCompliant = 'Skill is not compliant' if 'aliceMinVersion' in conditions and \ Version.fromString(conditions['aliceMinVersion']) > Version.fromString(constants.VERSION): raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition='Alice minimum version', conditionValue=conditions['aliceMinVersion']) for conditionName, conditionValue in conditions.items(): if conditionName == 'lang' and self.LanguageManager.activeLanguage not in conditionValue: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) elif conditionName == 'online': if conditionValue and self.ConfigManager.getAliceConfigByName('stayCompletelyOffline') \ or not conditionValue and not self.ConfigManager.getAliceConfigByName('stayCompletelyOffline'): raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) elif conditionName == 'skill': for requiredSkill in conditionValue: if requiredSkill in self._skillList and not self._skillList[requiredSkill]['active']: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) elif requiredSkill not in self._skillList: self.logInfo(f'Skill {installer["name"]} has another skill as dependency, adding download') if not self.downloadInstallTicket(requiredSkill): raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) elif conditionName == 'notSkill': for excludedSkill in conditionValue: author, name = excludedSkill.split('/') if name in self._skillList and self._skillList[name]['active']: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) elif conditionName == 'asrArbitraryCapture': if conditionValue and not self.ASRManager.asr.capableOfArbitraryCapture: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) elif conditionName == 'activeManager': for manager in conditionValue: if not manager: continue man = SuperManager.getInstance().getManager(manager) if not man or not man.isActive: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) return True
def getUpdateSource(self, definedSource: str) -> str: updateSource = 'master' if definedSource in {'master', 'release'}: return updateSource try: import requests except: self.setServiceFileTo('system') subprocess.run(['sudo', 'systemctl', 'restart', 'ProjectAlice']) self.informUser() exit(0) # noinspection PyUnboundLocalVariable req = requests.get('https://api.github.com/repos/project-alice-assistant/ProjectAlice/branches') result = req.json() versions = list() from core.base.model.Version import Version for branch in result: repoVersion = Version.fromString(branch['name']) releaseType = repoVersion.releaseType if not repoVersion.isVersionNumber \ or definedSource == 'rc' and releaseType in {'b', 'a'} \ or definedSource == 'beta' and releaseType == 'a': continue versions.append(repoVersion) if versions: versions.sort(reverse=True) updateSource = versions[0] return str(updateSource)
def __init__(self, supportedIntents: Iterable = None, databaseSchema: dict = None, **kwargs): super().__init__(**kwargs) try: self._skillPath = Path(inspect.getfile(self.__class__)).parent self._installFile = Path(inspect.getfile( self.__class__)).with_suffix('.install') self._installer = json.loads(self._installFile.read_text()) except FileNotFoundError: raise SkillStartingFailed( skillName=constants.UNKNOWN, error=f'[{type(self).__name__}] Cannot find install file') except Exception as e: raise SkillStartingFailed( skillName=constants.UNKNOWN, error=f'[{type(self).__name__}] Failed loading skill: {e}') instructionsFile = self.getResource( f'instructions/{self.LanguageManager.activeLanguage}.md') if not instructionsFile.exists(): instructionsFile = self.getResource(f'instructions/en.md') self._instructions = instructionsFile.read_text( ) if instructionsFile.exists() else '' self._name = self._installer['name'] self._author = self._installer.get('author', constants.UNKNOWN) self._version = self._installer.get('version', '0.0.1') self._icon = self._installer.get('icon', 'fas fa-biohazard') self._aliceMinVersion = Version.fromString( self._installer.get('aliceMinVersion', '1.0.0-b4')) self._maintainers = self._installer.get('maintainers', list()) self._description = self._installer.get('desc', '') self._category = self._installer.get('category', constants.UNKNOWN) self._conditions = self._installer.get('conditions', dict()) self._updateAvailable = False self._modified = False self._active = False self._delayed = False self._required = False self._failedStarting = False self._databaseSchema = databaseSchema self._widgets = list() self._widgetTemplates = dict() self._deviceTypes = list() self._intentsDefinitions = dict() self._scenarioPackageName = '' self._scenarioPackageVersion = Version(mainVersion=0, updateVersion=0, hotfix=0) self._supportedIntents: Dict[str, Intent] = self.buildIntentList( supportedIntents) self.loadIntentsDefinition() self._utteranceSlotCleaner = re.compile('{(.+?):=>.+?}') self._myDevicesTemplates = dict() self._myDevices: Dict[str, Device] = dict()
def getUpdateSource(definedSource: str) -> str: updateSource = 'master' if definedSource == 'master': return updateSource req = requests.get('https://api.github.com/repos/project-alice-assistant/ProjectAlice/branches') result = req.json() versions = list() for branch in result: repoVersion = Version.fromString(branch['name']) releaseType = repoVersion.releaseType if not repoVersion.isVersionNumber \ or definedSource == 'rc' and releaseType in {'b', 'a'} \ or definedSource == 'beta' and releaseType == 'a': continue versions.append(repoVersion) if versions: versions.sort(reverse=True) updateSource = versions[0] return str(updateSource)
def injectSkillNodes(self): package = Path('../.node-red/package.json') if not package.exists(): #self.logWarning('Package json file for Node Red is missing. Is Node Red even installed?') return for skillName, tup in self.SkillManager.allScenarioNodes().items(): scenarioNodeName, scenarioNodeVersion, scenarioNodePath = tup path = Path('../.node-red/node_modules', scenarioNodeName, 'package.json') if not path.exists(): self.logInfo('New scenario node found') install = self.Commons.runSystemCommand( f'cd ~/.node-red && npm install {scenarioNodePath}', shell=True) if install.returncode == 1: self.logWarning( f'Something went wrong installing new node: {install.stderr}' ) continue with path.open('r') as fp: data = json.load(fp) version = Version.fromString(data['version']) if version < scenarioNodeVersion: self.logInfo('New scenario node update found') install = self.Commons.runSystemCommand( f'cd ~/.node-red && npm install {scenarioNodePath}', shell=True) if install.returncode == 1: self.logWarning( f'Something went wrong updating node: {install.stderr}' )
def checkForSkillUpdates(self, skillToCheck: str = None) -> bool: """ Check all installed skills for availability of updates. Includes failed skills but not inactive. :param skillToCheck: :return: """ self.logInfo('Checking for skill updates') updateCount = 0 for skillName, data in self._skillList.items(): if not data['active']: continue try: if skillToCheck and skillName != skillToCheck: continue remoteVersion = self.SkillStoreManager.getSkillUpdateVersion(skillName) localVersion = Version.fromString(self._skillList[skillName]['installer']['version']) if localVersion < remoteVersion: updateCount += 1 self.WebUINotificationManager.newNotification( typ=UINotificationType.INFO, notification='skillUpdateAvailable', key='skillUpdate_{}'.format(skillName), replaceBody=[skillName, str(remoteVersion)] ) if data.get('modified', False): self.allSkills[skillName].updateAvailable = True self.logInfo(f'![blue]({skillName}) - Version {self._skillList[skillName]["installer"]["version"]} < {str(remoteVersion)} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")} - ![blue](LOCKED) for local changes!') continue self.logInfo(f'![yellow]({skillName}) - Version {self._skillList[skillName]["installer"]["version"]} < {str(remoteVersion)} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}') if not self.ConfigManager.getAliceConfigByName('skillAutoUpdate'): if skillName in self.allSkills: self.allSkills[skillName].updateAvailable = True else: if not self.downloadInstallTicket(skillName, isUpdate=True): raise Exception else: if data.get('modified', False): self.logInfo(f'![blue]({skillName}) - Version {self._skillList[skillName]["installer"]["version"]} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")} - ![blue](LOCKED) for local changes!') else: self.logInfo(f'![green]({skillName}) - Version {self._skillList[skillName]["installer"]["version"]} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}') except GithubNotFound: self.logInfo(f'![red](Skill **{skillName}**) is not available on Github. Deprecated or is it a dev skill?') except Exception as e: self.logError(f'Error checking updates for skill **{skillName}**: {e}') self.logInfo(f'Found {updateCount} skill update', plural='update') return updateCount > 0
def checkForSkillUpdates(self, skillToCheck: str = None) -> List[str]: """ Checks all installed skills for availability of updates. Includes failed skills but not inactive. :param skillToCheck: :return: """ self.logInfo('Checking for skill updates') skillsToUpdate = list() for skillName in self._skillList: try: if skillToCheck and skillName != skillToCheck: continue installer = json.loads(self.getSkillInstallFilePath(skillName=skillName).read_text()) remoteVersion = self.SkillStoreManager.getSkillUpdateVersion(skillName) localVersion = Version.fromString(installer['version']) if localVersion < remoteVersion: self.WebUINotificationManager.newNotification( typ=UINotificationType.INFO, notification='skillUpdateAvailable', key='skillUpdate_{}'.format(skillName), replaceBody=[skillName, str(remoteVersion)] ) if self.isSkillUserModified(skillName=skillName) and self.ConfigManager.getAliceConfigByName('devMode'): if skillName in self.allSkills: self.allSkills[skillName].updateAvailable = True self.logInfo(f'![blue]({skillName}) - Version {installer["version"]} < {str(remoteVersion)} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")} - Locked for local changes!') continue self.logInfo(f'![yellow]({skillName}) - Version {installer["version"]} < {str(remoteVersion)} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}') if not self.ConfigManager.getAliceConfigByName('skillAutoUpdate'): if skillName in self.allSkills: self.allSkills[skillName].updateAvailable = True else: skillsToUpdate.append(skillName) else: if self.isSkillUserModified(skillName=skillName) and self.ConfigManager.getAliceConfigByName('devMode'): self.logInfo(f'![blue]({skillName}) - Version {installer["version"]} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")} - Locked for local changes!') else: self.logInfo(f'![green]({skillName}) - Version {installer["version"]} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}') except GithubNotFound: self.logInfo(f'![red](Skill **{skillName}**) is not available on Github. Deprecated or is it a dev skill?') except Exception as e: self.logError(f'Error checking updates for skill **{skillName}**: {e}') self.logInfo(f'Found {len(skillsToUpdate)} skill update', plural='update') return skillsToUpdate
def _getSkillUpdateVersion(self, skillName: str) -> Optional[tuple]: """ Get the highest skill version number a user can install. This is based on the user preferences, dependending on the current Alice version and the user's selected update channel for skills In case nothing is found, DO NOT FALLBACK TO MASTER :param skillName: The skill to look for :return: tuple """ versionMapping = self._skillStoreData.get(skillName, dict()).get( 'versionMapping', dict()) if not versionMapping: raise GithubNotFound userUpdatePref = self.ConfigManager.getAliceConfigByName( 'skillsUpdateChannel') skillUpdateVersion = (Version(), '') aliceVersion = Version.fromString(constants.VERSION) for aliceMinVersion, repoVersion in versionMapping.items(): aliceMinVersion = Version.fromString(aliceMinVersion) repoVersion = Version.fromString(repoVersion) if not repoVersion.isVersionNumber or not aliceMinVersion.isVersionNumber or aliceMinVersion > aliceVersion: continue releaseType = repoVersion.releaseType if userUpdatePref == 'master' and releaseType in {'rc', 'b', 'a'} \ or userUpdatePref == 'rc' and releaseType in {'b', 'a'} \ or userUpdatePref == 'beta' and releaseType == 'a': continue if repoVersion > skillUpdateVersion[0]: skillUpdateVersion = ( repoVersion, f'{str(repoVersion)}_{str(aliceMinVersion)}') if not skillUpdateVersion[0].isVersionNumber: raise GithubNotFound return skillUpdateVersion
def __init__(self, installer: dict): self._installer = installer self._updateAvailable = False self._name = installer['name'] self._icon = self._installer.get('icon', 'fas fa-biohazard') self._aliceMinVersion = Version.fromString( self._installer.get('aliceMinVersion', '1.0.0-b4')) self._maintainers = self._installer.get('maintainers', list()) self._description = self._installer.get('desc', '') self._category = self._installer.get('category', constants.UNKNOWN) self._conditions = self._installer.get('conditions', dict()) super().__init__()
def loadScenarioNodes(self): path = self.getResource('scenarioNodes/package.json') if not path.exists(): return try: with path.open('r') as fp: data = json.load(fp) self._scenarioNodeName = data['name'] self._scenarioNodeVersion = Version.fromString(data['version']) except Exception as e: self.logWarning(f'Failed to load scenario nodes: {e}')
def test_fromString(self): self.assertEqual(Version.fromString('1.2.3-a4'), Version(1, 2, 3, 'a', 4)) self.assertEqual(Version.fromString('1.2.3'), Version(1, 2, 3, 'release', 1)) self.assertEqual(Version.fromString('1.2'), Version(1, 2, 0, 'release', 1)) self.assertEqual(Version.fromString('test'), Version(0, 0, 0, '', 0)) self.assertFalse(Version.fromString('test').isVersionNumber) self.assertTrue(Version.fromString('1.2').isVersionNumber)
def __init__(self, installer: dict): self._installer = installer self._updateAvailable = False self._name = installer['name'] self._icon = self._installer.get('icon', 'fas fa-biohazard') self._aliceMinVersion = Version.fromString( self._installer.get('aliceMinVersion', '1.0.0-b4')) self._maintainers = self._installer.get('maintainers', list()) self._description = self._installer.get('desc', '') self._category = self._installer.get('category', constants.UNKNOWN) self._conditions = self._installer.get('conditions', dict()) self._skillPath = Path('skills') / self._name self._repository = Repository(directory=self._skillPath, init=True, raiseIfExisting=False) super().__init__()
def loadScenarioNodes(self) -> None: """ Load the scenario nodes (folder scenarioNodes) for Node-Red and store them in _scenarioPackageName :return: """ path = self.getResource('scenarioNodes/package.json') if not path.exists(): return try: with path.open('r') as fp: data = json.load(fp) self._scenarioPackageName = data['name'] self._scenarioPackageVersion = Version.fromString( data['version']) except Exception as e: self.logWarning(f'Failed to load scenario nodes: {e}')
def checkForSkillUpdates(self, skillToCheck: str = None) -> bool: self.logInfo('Checking for skill updates') updateCount = 0 for skillName, data in self._skillList.items(): if not data['active']: continue try: if skillToCheck and skillName != skillToCheck: continue remoteVersion = self.SkillStoreManager.getSkillUpdateVersion( skillName) localVersion = Version.fromString( self._skillList[skillName]['installer']['version']) if localVersion < remoteVersion: updateCount += 1 self.logInfo( f'![yellow]({skillName}) - Version {self._skillList[skillName]["installer"]["version"]} < {str(remoteVersion)} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}' ) if not self.ConfigManager.getAliceConfigByName( 'skillAutoUpdate'): if skillName in self._activeSkills: self._activeSkills[ skillName].updateAvailable = True else: if not self.downloadInstallTicket(skillName): raise Exception else: self.logInfo( f'![green]({skillName}) - Version {self._skillList[skillName]["installer"]["version"]} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}' ) except GithubNotFound: self.logInfo( f'![red](Skill **{skillName}**) is not available on Github. Deprecated or is it a dev skill?' ) except Exception as e: self.logError( f'Error checking updates for skill **{skillName}**: {e}') self.logInfo(f'Found {updateCount} skill update', plural='update') return updateCount > 0
def checkForSkillUpdates(self, skillToCheck: str = None) -> bool: self.logInfo('Checking for skill updates') availableSkills = self.ConfigManager.skillsConfigurations updateCount = 0 for skillName in availableSkills: try: if skillToCheck and skillName != skillToCheck: continue remoteVersion = self.SkillStoreManager.getSkillUpdateVersion(skillName) localVersion = Version.fromString(availableSkills[skillName]['version']) if localVersion < remoteVersion: updateCount += 1 self.logInfo(f'❌ {skillName} - Version {availableSkills[skillName]["version"]} < {str(remoteVersion)} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}') if not self.ConfigManager.getAliceConfigByName('skillAutoUpdate'): if skillName in self._activeSkills: self._activeSkills[skillName].updateAvailable = True elif skillName in self._deactivatedSkills: self._deactivatedSkills[skillName].updateAvailable = True else: if not self.downloadInstallTicket(skillName): raise Exception if skillName in self._failedSkills: del self._failedSkills[skillName] else: self.logInfo(f'✔ {skillName} - Version {availableSkills[skillName]["version"]} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}') except GithubNotFound: self.logInfo(f'❓ Skill "{skillName}" is not available on Github. Deprecated or is it a dev skill?') except Exception as e: self.logError(f'❗ Error checking updates for skill "{skillName}": {e}') self.logInfo(f'Found {updateCount} skill update(s)') return updateCount > 0
def _installSkills(self, skills: list) -> dict: root = Path(self.Commons.rootDir(), constants.SKILL_INSTALL_TICKET_PATH) availableSkills = self.ConfigManager.skillsConfigurations skillsToBoot = dict() self.MqttManager.mqttBroadcast(topic='hermes/leds/systemUpdate', payload={'sticky': True}) for file in skills: skillName = Path(file).with_suffix('') self.logInfo(f'Now taking care of skill {skillName.stem}') res = root / file try: updating = False installFile = json.loads(res.read_text()) skillName = installFile['name'] path = Path(installFile['author'], skillName) if not skillName: self.logError('Skill name to install not found, aborting to avoid casualties!') continue directory = Path(self.Commons.rootDir()) / 'skills' / skillName conditions = { 'aliceMinVersion': installFile['aliceMinVersion'], **installFile.get('conditions', dict()) } self.checkSkillConditions(skillName, conditions, availableSkills) if skillName in availableSkills: installedVersion = Version.fromString(availableSkills[skillName]['version']) remoteVersion = Version.fromString(installFile['version']) localVersionIsLatest: bool = \ directory.is_dir() and \ 'version' in availableSkills[skillName] and \ installedVersion >= remoteVersion if localVersionIsLatest: self.logWarning(f'Skill "{skillName}" is already installed, skipping') self.Commons.runRootSystemCommand(['rm', res]) continue else: self.logWarning(f'Skill "{skillName}" needs updating') updating = True if skillName in self._activeSkills: try: self._activeSkills[skillName].onStop() except Exception as e: self.logError(f'Error stopping "{skillName}" for update: {e}') raise gitCloner = GithubCloner(baseUrl=f'{constants.GITHUB_URL}/skill_{skillName}.git', path=path, dest=directory) try: gitCloner.clone(skillName=skillName) self.logInfo('Skill successfully downloaded') self._installSkill(res) skillsToBoot[skillName] = { 'update': updating } except (GithubTokenFailed, GithubRateLimit): self.logError('Failed cloning skill') raise except GithubNotFound: if self.ConfigManager.getAliceConfigByName('devMode'): if not Path(f'{self.Commons.rootDir}/skills/{skillName}').exists() or not \ Path(f'{self.Commons.rootDir}/skills/{skillName}/{skillName.py}').exists() or not \ Path(f'{self.Commons.rootDir}/skills/{skillName}/dialogTemplate').exists() or not \ Path(f'{self.Commons.rootDir}/skills/{skillName}/talks').exists(): self.logWarning(f'Skill "{skillName}" cannot be installed in dev mode due to missing base files') else: self._installSkill(res) skillsToBoot[skillName] = { 'update': updating } continue else: self.logWarning(f'Skill "{skillName}" is not available on Github, cannot install') raise except SkillNotConditionCompliant as e: self.logInfo(f'Skill "{skillName}" does not comply to "{e.condition}" condition, required "{e.conditionValue}"') if res.exists(): res.unlink() self.broadcast( method=constants.EVENT_SKILL_INSTALL_FAILED, exceptions=self._name, skill=skillName ) except Exception as e: self.logError(f'Failed installing skill "{skillName}": {e}') if res.exists(): res.unlink() self.broadcast( method=constants.EVENT_SKILL_INSTALL_FAILED, exceptions=self.name, skill=skillName ) raise return skillsToBoot
def checkSkillConditions(self, installer: dict = None, checkOnly=False) -> Union[bool, List[Dict[str, str]]]: """ Checks if the given skill is compliant to its conditions :param installer: :param checkOnly: Do not perform any other action (download other skill, etc.) but checking conditions :return: """ conditions = { 'aliceMinVersion': installer['aliceMinVersion'], **installer.get('conditions', dict()) } notCompliant = 'Skill is not compliant' notCompliantRules = list() if 'aliceMinVersion' in conditions and Version.fromString(conditions['aliceMinVersion']) > Version.fromString(constants.VERSION): if not checkOnly: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition='Alice minimum version', conditionValue=conditions['aliceMinVersion']) else: notCompliantRules.append({'Alice version': conditions['aliceMinVersion']}) for conditionName, conditionValue in conditions.items(): if conditionName == 'lang' and self.LanguageManager.activeLanguage not in conditionValue: if not checkOnly: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) else: notCompliantRules.append({conditionName: conditionValue}) elif conditionName == 'online': if conditionValue and self.ConfigManager.getAliceConfigByName('stayCompletelyOffline') or not conditionValue and not self.ConfigManager.getAliceConfigByName('stayCompletelyOffline'): if not checkOnly: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) else: notCompliantRules.append({conditionName: conditionValue}) elif conditionName == 'skill': for requiredSkill in conditionValue: if requiredSkill in self._skillList and not self.isSkillActive(skillName=installer['name']): if not checkOnly: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) else: notCompliantRules.append({conditionName: conditionValue}) elif requiredSkill not in self._skillList: if not checkOnly: self.logInfo(f'Skill {installer["name"]} has another skill as dependency, adding download') try: self.downloadSkills(skills=requiredSkill) except: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) else: notCompliantRules.append({conditionName: conditionValue}) elif conditionName == 'notSkill': for excludedSkill in conditionValue: author, name = excludedSkill.split('/') if name in self._skillList and self.isSkillActive(skillName=installer['name']): if not checkOnly: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) else: notCompliantRules.append({conditionName: conditionValue}) elif conditionName == 'asrArbitraryCapture': if conditionValue and self.ASRManager.asr and not self.ASRManager.asr.capableOfArbitraryCapture: if not checkOnly: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) else: notCompliantRules.append({conditionName: conditionValue}) elif conditionName == 'activeManager': for manager in conditionValue: if not manager: continue man = SuperManager.getInstance().getManager(manager) if not man or not man.isActive: if not checkOnly: raise SkillNotConditionCompliant(message=notCompliant, skillName=installer['name'], condition=conditionName, conditionValue=conditionValue) else: notCompliantRules.append({conditionName: conditionValue}) return True if not checkOnly else notCompliantRules
def updateProjectAlice(self): self._logger.logInfo('Checking for core updates') self._isUpdating = True req = requests.get( url=f'{constants.GITHUB_API_URL}/ProjectAlice/branches', auth=SuperManager.getInstance().configManager.getGithubAuth()) if req.status_code != 200: self._logger.logWarning('Failed checking for updates') return userUpdatePref = SuperManager.getInstance( ).configManager.getAliceConfigByName('aliceUpdateChannel') if userUpdatePref == 'master': candidate = 'master' else: candidate = Version.fromString(constants.VERSION) for branch in req.json(): repoVersion = Version.fromString(branch['name']) if not repoVersion.isVersionNumber: continue releaseType = repoVersion.releaseType if userUpdatePref == 'rc' and releaseType in { 'b', 'a' } or userUpdatePref == 'beta' and releaseType == 'a': continue if repoVersion > candidate: candidate = repoVersion self._logger.logInfo(f'Checking on "{str(candidate)}" update channel') commons = SuperManager.getInstance().commons currentHash = subprocess.check_output( ['git', 'rev-parse', '--short HEAD']) commons.runSystemCommand(['git', '-C', commons.rootDir(), 'stash']) commons.runSystemCommand( ['git', '-C', commons.rootDir(), 'clean', '-df']) commons.runSystemCommand( ['git', '-C', commons.rootDir(), 'checkout', str(candidate)]) commons.runSystemCommand(['git', '-C', commons.rootDir(), 'pull']) newHash = subprocess.check_output(['git', 'rev-parse', '--short HEAD']) # Remove install tickets [ file.unlink() for file in Path( commons.rootDir(), 'system/skillInstallTickets').glob('*') if file.is_file() ] if currentHash != newHash: self._logger.logWarning( 'New Alice version installed, need to restart...') self.doRestart() self._isUpdating = False
def checkForSkillUpdates(self, skillToCheck: str = None) -> bool: self.logInfo('Checking for skill updates') availableSkills = self.ConfigManager.skillsConfigurations updateCount = 0 for skillName in availableSkills: try: if skillToCheck and skillName != skillToCheck: continue remoteVersion = self.SkillStoreManager.getSkillUpdateVersion( skillName) localVersion = Version.fromString( availableSkills[skillName]['version']) if localVersion < remoteVersion: updateCount += 1 self.logInfo( f'❌ {skillName} - Version {availableSkills[skillName]["version"]} < {str(remoteVersion)} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}' ) if not self.ConfigManager.getAliceConfigByName( 'skillAutoUpdate'): if skillName in self._activeSkills: self._activeSkills[ skillName].updateAvailable = True elif skillName in self._deactivatedSkills: self._deactivatedSkills[ skillName].updateAvailable = True else: req = requests.get( f'{constants.GITHUB_RAW_URL}/skill_{skillName}/{self.SkillStoreManager.getSkillUpdateTag(skillName)}/{skillName}.install' ) if req.status_code == 404: raise GithubNotFound remoteFile = req.json() if not remoteFile: raise Exception skillFile = Path(self.Commons.rootDir(), constants.SKILL_INSTALL_TICKET_PATH, skillName + '.install') skillFile.write_text(json.dumps(remoteFile)) if skillName in self._failedSkills: del self._failedSkills[skillName] else: self.logInfo( f'✔ {skillName} - Version {availableSkills[skillName]["version"]} in {self.ConfigManager.getAliceConfigByName("skillsUpdateChannel")}' ) except GithubNotFound: self.logInfo( f'❓ Skill "{skillName}" is not available on Github. Deprecated or is it a dev skill?' ) except Exception as e: self.logError( f'❗ Error checking updates for skill "{skillName}": {e}') self.logInfo(f'Found {updateCount} skill update(s)') return updateCount > 0
def checkSkillConditions(self, skillName: str, conditions: dict, availableSkills: dict) -> bool: notCompliant = 'Skill is not compliant' if 'aliceMinVersion' in conditions and \ Version.fromString(conditions['aliceMinVersion']) > Version.fromString(constants.VERSION): raise SkillNotConditionCompliant( message=notCompliant, skillName=skillName, condition='Alice minimum version', conditionValue=conditions['aliceMinVersion']) for conditionName, conditionValue in conditions.items(): if conditionName == 'lang' and self.LanguageManager.activeLanguage not in conditionValue: raise SkillNotConditionCompliant(message=notCompliant, skillName=skillName, condition=conditionName, conditionValue=conditionValue) elif conditionName == 'online': if conditionValue and self.ConfigManager.getAliceConfigByName('stayCompletlyOffline') \ or not conditionValue and not self.ConfigManager.getAliceConfigByName('stayCompletlyOffline'): raise SkillNotConditionCompliant( message=notCompliant, skillName=skillName, condition=conditionName, conditionValue=conditionValue) elif conditionName == 'skill': for requiredSkill in conditionValue: if requiredSkill[ 'name'] in availableSkills and not availableSkills[ requiredSkill['name']]['active']: raise SkillNotConditionCompliant( message=notCompliant, skillName=skillName, condition=conditionName, conditionValue=conditionValue) elif requiredSkill['name'] not in availableSkills: self.logInfo( f'Skill {skillName} has another skill as dependency, adding download' ) if not self.Commons.downloadFile( requiredSkill['url'], Path( self.Commons.rootDir(), f"system/skillInstallTickets/{requiredSkill['name']}.install" )): raise SkillNotConditionCompliant( message=notCompliant, skillName=skillName, condition=conditionName, conditionValue=conditionValue) elif conditionName == 'notSkill': for excludedSkill in conditionValue: author, name = excludedSkill.split('/') if name in availableSkills and availableSkills[name][ 'author'] == author and availableSkills[name][ 'active']: raise SkillNotConditionCompliant( message=notCompliant, skillName=skillName, condition=conditionName, conditionValue=conditionValue) elif conditionName == 'asrArbitraryCapture': if conditionValue and not self.ASRManager.asr.capableOfArbitraryCapture: raise SkillNotConditionCompliant( message=notCompliant, skillName=skillName, condition=conditionName, conditionValue=conditionValue) elif conditionName == 'activeManager': for manager in conditionValue: if not manager: continue man = SuperManager.getInstance().getManager(manager) if not man or not man.isActive: raise SkillNotConditionCompliant( message=notCompliant, skillName=skillName, condition=conditionName, conditionValue=conditionValue) return True
def updateProjectAlice(self): self._logger.logInfo('Checking for core updates') STATE = 'projectalice.core.updating' state = self._superManager.stateManager.getState(STATE) if not state: self._superManager.stateManager.register( STATE, initialState=StateType.RUNNING) elif state.currentState == StateType.RUNNING: self._logger.logInfo('Update cancelled, already running') return self._superManager.stateManager.setState(STATE, newState=StateType.RUNNING) self._isUpdating = True req = requests.get( url=f'{constants.GITHUB_API_URL}/ProjectAlice/branches', auth=SuperManager.getInstance().configManager.githubAuth) if req.status_code != 200: self._logger.logWarning('Failed checking for updates') self._superManager.stateManager.setState(STATE, newState=StateType.ERROR) return userUpdatePref = SuperManager.getInstance( ).configManager.getAliceConfigByName('aliceUpdateChannel') if userUpdatePref == 'master': candidate = 'master' else: candidate = Version.fromString(constants.VERSION) for branch in req.json(): if 'dependabot' in branch['name']: continue repoVersion = Version.fromString(branch['name']) if not repoVersion.isVersionNumber: continue releaseType = repoVersion.releaseType if userUpdatePref == 'rc' and releaseType in { 'b', 'a' } or userUpdatePref == 'beta' and releaseType == 'a': continue if repoVersion > candidate: candidate = repoVersion self._logger.logInfo(f'Checking on "{str(candidate)}" update channel') commons = SuperManager.getInstance().commons currentHash = subprocess.check_output( ['git', 'rev-parse', '--short HEAD']) commons.runSystemCommand(['git', '-C', commons.rootDir(), 'stash']) commons.runSystemCommand( ['git', '-C', commons.rootDir(), 'clean', '-df']) commons.runSystemCommand( ['git', '-C', commons.rootDir(), 'checkout', str(candidate)]) commons.runSystemCommand(['git', '-C', commons.rootDir(), 'pull']) commons.runSystemCommand( ['git', '-C', commons.rootDir(), 'submodule', 'init']) commons.runSystemCommand( ['git', '-C', commons.rootDir(), 'submodule', 'update']) commons.runSystemCommand([ 'git', '-C', commons.rootDir(), 'submodule', 'foreach', 'git', 'checkout', f'builds_{str(candidate)}' ]) commons.runSystemCommand([ 'git', '-C', commons.rootDir(), 'submodule', 'foreach', 'git', 'pull' ]) newHash = subprocess.check_output(['git', 'rev-parse', '--short HEAD']) # Remove install tickets [ file.unlink() for file in Path( commons.rootDir(), 'system/skillInstallTickets').glob('*') if file.is_file() ] self._superManager.stateManager.setState(STATE, newState=StateType.FINISHED) if currentHash != newHash: self._logger.logWarning( 'New Alice version installed, need to restart...') self._superManager.webUINotificationManager.newNotification( typ=UINotificationType.INFO, notification='aliceUpdated') self.doRestart() self._logger.logInfo('Update checks completed.') self._isUpdating = False