UNPKG

snips-sam

Version:

The Snips Assistant Manager

1,047 lines (1,043 loc) 50.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const fsExtra = require("fs-extra"); const models_1 = require("../models"); const utils_1 = require("../utils"); const assets_1 = require("../assets"); const cli_1 = require("../cli"); const nodeSsh = require("node-ssh"); const shelljs = require("shelljs"); const os = require("os"); const path = require("path"); const fs = require("fs"); exports.snipsRemoteAuthorizedKeysFilename = '~/.ssh/authorized_keys'; const snipsRSAKeyName = 'id_rsa_snips'; const snipsRSAKeyFilePath = path.join(os.homedir(), '.ssh', snipsRSAKeyName); var SnipsServices; (function (SnipsServices) { SnipsServices["analytics"] = "snips-analytics"; SnipsServices["asr"] = "snips-asr"; SnipsServices["audioServer"] = "snips-audio-server"; SnipsServices["dialogue"] = "snips-dialogue"; SnipsServices["hotword"] = "snips-hotword"; SnipsServices["nlu"] = "snips-nlu"; SnipsServices["skillServer"] = "snips-skill-server"; SnipsServices["template"] = "snips-template"; SnipsServices["tts"] = "snips-tts"; SnipsServices["watch"] = "snips-watch"; })(SnipsServices = exports.SnipsServices || (exports.SnipsServices = {})); exports.availableSnipsServices = [ SnipsServices.asr, SnipsServices.audioServer, SnipsServices.dialogue, SnipsServices.hotword, SnipsServices.nlu, SnipsServices.skillServer, SnipsServices.tts, ]; class SSHService { constructor() { this.ssh = new nodeSsh(); } connect() { return tslib_1.__awaiter(this, void 0, void 0, function* () { const credentials = cli_1.cli.config.credentials; if (credentials === undefined || credentials.hostname === null) { throw new Error('No credentials found'); } return this.connectInternal(credentials) .catch(e => { this.disconnect(); throw utils_1.newError(`Connection failed, is your device turned on? \nError: ${e.message}`); }); }); } retryConnect(tick, limit = 5) { return tslib_1.__awaiter(this, void 0, void 0, function* () { let isConnected = false; let numberOfRetries = 0; while (isConnected === false && limit > numberOfRetries) { yield utils_1.timeout(5000); tick(numberOfRetries); yield this.connect() .then(_ => isConnected = true) .catch(_ => { }); numberOfRetries += 1; } return isConnected; }); } newConnection(credentials, password, onConnected, onSSHCopyId, onCredentialsSaved, onSSHKeygenExeNotFound) { return tslib_1.__awaiter(this, void 0, void 0, function* () { yield this.connectInternal(credentials, password) .then(_ => onConnected()) .catch(e => { throw new Error(`Error connecting to ${credentials.hostname}: ${e.message}`); }); yield this.sshCopyId(onSSHKeygenExeNotFound) .then(_ => onSSHCopyId()) .catch(e => { this.disconnect(); throw new Error(`Failed to copy SSH public key: ${e.message}`); }); cli_1.cli.config.credentials = this.credentials; yield cli_1.cli.config.save() .then(_ => onCredentialsSaved()) .catch(e => { this.disconnect(); throw new Error(`Failed to save credentials: ${e.message}`); }); }); } connectInternal(credentials, password) { return tslib_1.__awaiter(this, void 0, void 0, function* () { this.credentials = credentials; if (password != null) { return this.ssh.connect({ password, host: this.credentials.hostname, username: this.credentials.username, }); } return this.ssh.connect({ host: this.credentials.hostname, username: this.credentials.username, privateKey: snipsRSAKeyFilePath, }); }); } disconnect() { return tslib_1.__awaiter(this, void 0, void 0, function* () { yield this.ssh.dispose(); }); } reboot() { return tslib_1.__awaiter(this, void 0, void 0, function* () { this.ssh.exec('sudo reboot'); }); } writeFile(contents, remotePath) { const tmpDir = path.join(os.tmpdir(), 'sam'); if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir); } const tmpPath = path.join(tmpDir, path.basename(remotePath)); fs.writeFileSync(tmpPath, contents, { encoding: 'utf8' }); return this.ssh.putFile(tmpPath, remotePath); } writeFileAsSnipsSKills(contents, remotePath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const tmpRemotePath = `/tmp/${path.basename(remotePath)}`; yield this.writeFile(contents, tmpRemotePath); return this.ssh.exec(`sudo -u _snips-skills bash -c 'cp "${tmpRemotePath}" "${remotePath}"'`) .then(_ => this.removeFile(tmpRemotePath)); }); } writeFileAsRoot(contents, remotePath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const tmpRemotePath = `/tmp/${path.basename(remotePath)}`; yield this.writeFile(contents, tmpRemotePath); return this.ssh.exec(`cat "${tmpRemotePath}" | sudo tee "${remotePath}" > /dev/null && rm "${tmpRemotePath}"`); }); } isAssistantInstalled() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('if [ -d /usr/share/snips/assistant ]; then echo 1; else echo 0; fi') .then(result => result === '1'); }); } getInstalledAssistant() { return tslib_1.__awaiter(this, void 0, void 0, function* () { const assistantFilename = '/usr/share/snips/assistant/assistant.json'; return this.ssh.exec(`if [ -f ${assistantFilename} ]; then cat ${assistantFilename}; else echo ""; fi`) .then(assistantJSON => { if (assistantJSON === '') throw new Error('No assistant installed'); return new models_1.AssistantFile(assistantJSON); }); }); } getOSVersion() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('cat /etc/os-release') .then(success => { const osVersion = success.split('\n')[0].replace('PRETTY_NAME="', '').replace('"', ''); if (osVersion === null || osVersion.length === 0) { return 'Unknown'; } return osVersion; }) .catch(_ => { return 'Unknown'; }); }); } installSnipsPlatformDemo(listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.aptGetY(['snips-platform-demo'], listener); }); } aptGetY(packageNames, listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.sshCommandLog(`sudo apt-get update; sudo apt-get install -y ${packageNames.join(' ')}`, listener); }); } isNetcatInstalled() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('if [ -x "$(command -v nc)" ]; then echo 1; else echo 0; fi') .then(result => result === '1') .then(result => { if (!result) { return this.dpkgCheckIfPackageIsInstalled('netcat'); } return result; }); }); } speakerTest() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('sudo speaker-test -c2 -twav'); }); } stopSpeakerTest() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('pkill speaker-test'); }); } setHostname(newHostname) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const oldHostname = yield this.ssh.exec('cat /etc/hostname') .catch(_ => { throw new Error('You device hostname was not readable'); }); return this.ssh.exec(`sudo sed -i "s/${oldHostname}/${newHostname}/g" /etc/hosts; sudo sed -i "s/${oldHostname}/${newHostname}/g" /etc/hostname; sudo reboot`) .then(_ => { this.credentials.hostname = `${newHostname}.local`; cli_1.cli.config.credentials = this.credentials; return cli_1.cli.config.save(); }) .catch(_ => { this.credentials.hostname = `${newHostname}.local`; cli_1.cli.config.credentials = this.credentials; return cli_1.cli.config.save(); }); }); } static fetchBootHostname(ips) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return yield Promise.all(ips.map((ip) => tslib_1.__awaiter(this, void 0, void 0, function* () { const client = new nodeSsh(); const hostnames = yield client.connect({ host: ip, username: 'pi', password: 'raspberry', }) .then((_) => tslib_1.__awaiter(this, void 0, void 0, function* () { const snipsHostname = yield client.exec('if [ -f /boot/alias ]; then echo 1; else echo 0; fi') .then(result => { if (result === '1') { return client.exec('cat /boot/alias').catch(_ => ''); } return ''; }); const hostname = yield client.exec('cat /etc/hostname').catch(_ => ''); return { hostname, snipsHostname }; })) .catch(_ => { return { snipsHostname: '', hostname: '' }; }); return { ip, hostname: hostnames.hostname, snipsHostname: hostnames.snipsHostname }; }))); }); } getCaptureLevel(card = 1) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`amixer -c ${card} cget name='Mic Capture Volume'`); }); } setCaptureLevel(card = 1, level = 80) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`amixer -c ${card} cset name='Mic Capture Volume' ${level}%`); }); } getVolumeLevel(card = 0) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`amixer -c ${card} cget name='PCM Playback Volume'`); }); } setVolumeLevel(card = 0, level = 90) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`amixer -c ${card} cset name='PCM Playback Volume' ${level}%`); }); } recordAudio(name) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`arecord -f cd ${name}.wav`); }); } stopRecording() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('pkill arecord'); }); } testingtesting() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('sudo apt-get install '); }); } listCaptureDevices() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec("arecord -l | grep -E '^[^ ][^**]'") .then(success => { if (success.length === 0) { throw new Error('No capture devices found. Check if your mic is correctly plugged.'); } const devices = String(success).split('\n'); if (devices.length === 0) { throw new Error('No capture devices found. Check if your mic is correctly plugged.'); } try { return devices.map(device => new utils_1.AudioDevice(device)); } catch (e) { throw new Error(`Error reading the information of the audio device: ${e.message}`); } }); }); } listOuputDevices() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec("aplay -l | grep -E '^[^ ][^**]'") .then(success => { const devices = String(success).split('\n'); if (devices.length === 0) { throw new Error('No output devices found. This should not happen, on your device, run \narecord -l'); } try { return devices.map(device => new utils_1.AudioDevice(device)); } catch (e) { throw new Error(`Error reading the information of the audio device: ${e.message}`); } }); }); } launchSnipsWatch(listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.sshCommandLog(`${SnipsServices.watch} -vvv`, listener); }); } journalctlLogs(services, listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (services !== undefined && services.length > 0) { const s = services.map(service => `-u ${service}`).join(' '); return this.sshCommandLog(`journalctl -f ${s}`, listener); } return this.sshCommandLog('journalctl -f -u "snips-*"', listener); }); } snipsServicesSystemctlStatus() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return Promise.all(exports.availableSnipsServices .filter(service => service !== SnipsServices.template && service !== SnipsServices.watch) .map((serviceName) => tslib_1.__awaiter(this, void 0, void 0, function* () { const active = yield this.systemctlStatus(serviceName); return { active, name: serviceName }; }))); }); } systemctlStatus(service) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return yield this.ssh.exec(`systemctl is-active ${service}.service >/dev/null 2>&1 && echo 'active' || echo 'inactive'`) .then(result => result === 'active'); }); } playAudio(name) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`aplay ${name}.wav`); }); } removeAudioFile(name) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`rm ${name}.wav`); }); } setupAsoundConf(outputDevice, captureDevice) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const dest = '/etc/asound.conf'; const contents = `pcm.!default { type asym playback.pcm { type plug slave.pcm "hw:${outputDevice.card},${outputDevice.subdevice}" } capture.pcm { type plug slave.pcm "hw:${captureDevice.card},${captureDevice.subdevice}" } }`; return this.writeFileAsRoot(contents, dest); }); } getAssistantSnipsfile() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.getFile('/usr/share/snips/assistant/Snipsfile.yaml') .then(snipsfile => new models_1.Snipsfile(snipsfile)); }); } copySnipsfile(snipsfile) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`echo "${snipsfile}" | sudo tee ~/Snipsfile > /dev/null`); }); } stopService(service) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`sudo systemctl stop ${service}.service`); }); } stopServices(services) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (services !== undefined && services.length > 0) { const restart = services.map(service => `sudo systemctl stop ${service}`).join(' ; '); return this.ssh.exec(restart); } return this.ssh.exec("sudo systemctl stop 'snips*'"); }); } relaunchService(service) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`sudo systemctl restart ${service}.service`); }); } startServices(services) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (services !== undefined && services.length > 0) { const restart = services.map(service => `sudo systemctl start ${service}`).join(' ; '); return this.ssh.exec(restart); } return this.ssh.exec('sudo systemctl start snips-analytics snips-asr snips-audio-server snips-dialogue snips-nlu snips-tts snips-skill-server snips-hotword'); }); } relaunchServices(services) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (services !== undefined && services.length > 0) { const restart = services.map(service => `sudo systemctl restart ${service}`).join(' ; '); return this.ssh.exec(restart); } return this.ssh.exec("sudo systemctl restart 'snips-*'"); }); } updateSnips(listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.sshCommandLog(`sudo apt-get update; sudo apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" upgrade -y snips-platform-voice ${SnipsServices.template} ${SnipsServices.skillServer}`, listener); }); } installSnips(listener, installSteps) { return tslib_1.__awaiter(this, void 0, void 0, function* () { installSteps('Updating aptitude repository'); return this.sshCommandLog('sudo apt-get update', listener) .then(_ => { installSteps('Installing dirmngr'); return this.sshCommandLog('sudo apt-get install -y dirmngr', listener) .catch(_ => { throw new Error("Couldn't install dirmngr"); }); }) .then(_ => { installSteps('Adding Snips APT source list'); return this.sshCommandLog(`sudo bash -c 'echo "deb https://raspbian.snips.ai/$(lsb_release -cs) stable main" > /etc/apt/sources.list.d/snips.list'`, listener) .catch(_ => { throw new Error("Couldn't update Snips apt sources on the device"); }); }) .then(_ => { installSteps('Adding Snips PGP public key'); const tmpRemotePGPkeyPath = '/tmp/snips-pgp-key.asc'; return this.ssh.exec(`(echo "${assets_1.Assets.pgpKey()}" > ${tmpRemotePGPkeyPath}) && sudo apt-key add ${tmpRemotePGPkeyPath}; rm ${tmpRemotePGPkeyPath}`) .catch(_ => { }); }) .then(_ => installSteps('Updating aptitude repository')) .then(_ => this.sshCommandLog('sudo apt-get update', listener)) .then(_ => installSteps('Installing Snips Platform, this could take a while, grab a cup of tea & relax')) .then(_ => { return this.sshCommandLog(`sudo apt-get install -y snips-platform-voice ${SnipsServices.watch} ${SnipsServices.template} ${SnipsServices.skillServer}`, listener) .catch(err => { throw new Error(`There was a problem during Snips Platform installation ${err}`); }); }) .then(_ => installSteps('Adding current user to snips-skills-admin group for security')) .then(_ => this.addCurrentUserToSnipsSkillsAdminGroup()) .then((_) => tslib_1.__awaiter(this, void 0, void 0, function* () { installSteps('Disabling Mosquitto persistence'); yield this.stopService('mosquitto') .then(_ => this.toggleMQTTLogPersistance(false)) .catch(_ => { }); yield this.relaunchService('mosquitto').catch(_ => { }); return this.relaunchServices(); })); }); } packagesVersion() { return tslib_1.__awaiter(this, void 0, void 0, function* () { const formatOutput = (dpkgOutput) => { return dpkgOutput .split('\n') .map(line => { const split = line.trim().split(' '); if (split.length >= 2) return { name: split[0], version: split[split.length - 1] }; return { name: '', version: '' }; }) .filter(v => v.name !== ''); }; return this.ssh.exec(`dpkg -l | grep snips | awk '{printf("%- 30s %- 10s\\n", $2,$3)}'`) .then(output => formatOutput(output).filter(v => exports.availableSnipsServices.includes(v.name))); }); } updateWifi(name, passphrase) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`(echo ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev; \ echo update_config=1; \ echo ; \ wpa_passphrase \"${name}\" \"${passphrase}\") \ | sudo tee /etc/wpa_supplicant/wpa_supplicant.conf > /dev/null`) .then(_ => { return this.ssh.exec('sudo wpa_cli reconfigure'); }); }); } statusWifi() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('sudo wpa_cli status') .then(response => { let ssid; let status; response.split('\n') .map(line => line.split('=')) .forEach(lineComponents => { const key = lineComponents[0]; const value = lineComponents[1]; if (key === 'ssid') { ssid = value; } else if (key === 'wpa_state') { status = value; } }); return [ssid, status]; }); }); } reconnectWifi() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('sudo wpa_cli reconnect'); }); } disconnectWifi() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('sudo wpa_cli disconnect'); }); } putFile(localPath, remotePath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.putFile(localPath, remotePath); }); } putString(string, remotePath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`sudo sh -c "echo '${string}' > ${remotePath}"`); }); } getFile(remotePath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`cat ${remotePath}`); }); } putDirectory(localDirectory, remoteDirectory) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return yield this.ssh.putDirectory(localDirectory, remoteDirectory, { recursive: true, concurrency: 1, validate: itemPath => { const baseName = path.basename(itemPath); return baseName.substr(0, 1) !== '.'; }, }); }); } removeFile(remotePath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`sudo rm "${remotePath}"`); }); } removeDirectory(remotePath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`sudo rm -rf "${remotePath}"`); }); } unzipFile(filePath, destinationPath = '.') { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`unzip -o "${filePath}" -d "${destinationPath}"`); }); } installAssistant(assistantPath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const assistantFilename = path.basename(assistantPath); const tmpDirPath = '/tmp/assistant/'; return this.removeDirectory('/usr/share/snips/assistant') .then(_ => { if (assistantFilename.includes('zip')) { return this.removeDirectory(tmpDirPath) .then(_ => this.ssh.exec(`mkdir ${tmpDirPath}`)) .then(_ => this.putFile(assistantPath, tmpDirPath + assistantFilename)) .then(_ => this.unzipFile(tmpDirPath + assistantFilename, tmpDirPath)) .then(_ => this.removeFile(tmpDirPath + assistantFilename)); } return this.putDirectory(assistantPath, tmpDirPath) .then(status => { if (!status) { throw new Error('Some files failed to transfer, aborting'); } return 'success'; }); }) .then(_ => this.ssh.exec(`sudo mv ${tmpDirPath}* /usr/share/snips`)); }); } dpkgCheckIfPackageIsInstalled(packageName) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`if dpkg-query -W -f='\${db:Status-abbrev}' "${packageName}" 2>/dev/null | grep -q '^i'; then echo 1; else echo 0; fi`) .then(result => result === '1'); }); } isFileExecutable(file) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`if [ -x "${file}" ]; then echo 1; else echo 0; fi`) .then(result => result === '1'); }); } checkCommandExists(command) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`if [ -x "$(command -v ${command})" ]; then echo 1; else echo 0; fi`) .then(result => result === '1'); }); } installRespeakerMicHat(listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.sshCommandLog('cd /tmp/; git clone https://github.com/respeaker/seeed-voicecard', listener) .then(_ => this.sshCommandLog('cd /tmp/seeed-voicecard; sudo ./install.sh; rm -rf /tmp/seeed-voicecard', listener)); }); } run(command) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(command); }); } sshCommandLog(command, listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.execCommand(command, { onStdout(chunk) { listener(chunk.toString('utf8')); }, onStderr(chunk) { listener(chunk.toString('utf8')); }, }); }); } snipsTemplateRender(listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.sshCommandLog(`sudo -u _snips-skills bash -c '${SnipsServices.template} render'`, listener); }); } hasSnipsSkillsServerGroup() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('if grep -q snips-skills-admin /etc/group; then echo 1; else echo 0; fi') .then(result => result === '1'); }); } addCurrentUserToSnipsSkillsAdminGroup() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.hasSnipsSkillsServerGroup() .then(hasSnipsSKillsGroup => { if (!hasSnipsSKillsGroup) return 'failure'; return this.ssh.exec('if id -nG "$USER" | grep -qw "snips-skills-admin"; then echo 1; else echo 0; fi') .then(result => result === '1') .then(isInGroup => { if (!isInGroup) return this.ssh.exec('sudo usermod -a -G snips-skills-admin $USER'); return 'done'; }); }); }); } detectConfigIniParameters() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.run('find /var/lib/snips/skills -name config.ini') .then((configs) => { return configs.split('\n') .map(line => path.basename(line).replace(/\.[^/.]+$/, '')); }); }); } gitClone(repository, branch, listener, permissionError) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const gitFolderName = path.basename(repository).replace(/\.[^/.]+$/, ''); let gitClone = `sudo -u _snips-skills bash -c 'git clone --recursive "${repository}"'`; if (branch !== undefined && branch.length > 0) { gitClone = `sudo -u _snips-skills bash -c 'git clone -b ${branch} --recursive "${repository}"'`; } return this.sshCommandLog(`cd /var/lib/snips/skills/ if [ ! -d "${gitFolderName}" ]; then ${gitClone} else cd "${gitFolderName}" if [ $(git config --get remote.origin.url) = ${repository} ]; then sudo -u _snips-skills bash -c 'git pull' else cd .. sudo -u _snips-skills bash -c 'rm -rf "${gitFolderName}"' ${gitClone} fi fi`, listener) .then(_ => this.ssh.exec(`find /var/lib/snips/skills/${gitFolderName} -name "action*" ! -executable`)) .then((actionsNotExecutable) => { if (actionsNotExecutable.length > 0) { if (actionsNotExecutable.length > 1) { permissionError(`Warning: Some actions are not executable. Snips-skill-server won't be able to run them. Files: \n${actionsNotExecutable}`); } else { permissionError(`Warning: The action ${actionsNotExecutable} is not executable. Snips-skill-server won't be able to run it.`); } listener('To change the permission on your device, run:'); const actions = actionsNotExecutable.split('\n').join(' '); listener(`chmod +x ${actions}\n`); } }); }); } runSkillSetupSH(skillFolder) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`cd /var/lib/snips/skills/${skillFolder}; sudo -u _snips-skills bash -c 'sh setup.sh'`); }); } getSnippets() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('cd /usr/share/snips/assistant/snippets; ls -d */*/*') .then((snippets) => { if (snippets === undefined) throw new Error('No snippets found'); if (snippets.length === 0) throw new Error('No snippets found'); return snippets.split('\n') .map(line => { const parts = line.split('/'); if (parts.length === 0 || parts.length < 3) throw new Error('No snippets found'); return { name: parts[0], type: parts[1], snippet: parts[2].replace(/.snippet/g, ''), }; }) .reduce((previous, next) => { let subArray = previous.get(next.name); if (subArray !== undefined) { subArray.snippets.push(next.snippet); } else { subArray = { type: next.type, snippets: [next.snippet] }; } previous.set(next.name, subArray); return previous; }, new Map()); }); }); } getSkillConfig(skillFolder) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const dest = `/var/lib/snips/skills/${skillFolder}/config.ini`; const exists = yield this.fileExists(dest); return exists ? this.ssh.exec(`cat ${dest}`) : null; }); } getSkillConfigNew(skillFolder) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const dest = `/var/lib/snips/skills/${skillFolder}/config.ini.new`; const exists = yield this.fileExists(dest); return exists ? this.ssh.exec(`cat ${dest}`) : null; }); } updateSkillConfig(config, skillFolder) { const dest = `/var/lib/snips/skills/${skillFolder}/config.ini`; return this.writeFileAsSnipsSKills(config, dest); } getGeneratedSkills() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('cd /var/lib/snips/skills; ls -1d ./*/') .then((skillPath) => { if (skillPath.length === 0) return undefined; return skillPath.split('\n') .map(line => path.basename(line)); }); }); } toggleMQTTLogPersistance(toggle) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const toggleString = Boolean(toggle).toString(); return this.ssh.exec(`sudo sed -i "s#.*persistence .*#persistence ${toggleString}#g" /etc/mosquitto/mosquitto.conf`); }); } deleteMosquittoDB() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('sudo rm /var/lib/mosquitto/mosquitto.db'); }); } tuneEchoCancel(source, sink, listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { yield this.sshCommandLog(`sudo sed -i.bak 's/^;\\?\\s*default-sink.*/default-sink = ${sink}.echo-cancel/' /etc/pulse/client.conf`, listener); yield this.sshCommandLog(`sudo sed -i.bak 's/^;\\?\\s*default-source.*/default-source = ${source}.echo-cancel/' /etc/pulse/client.conf`, listener); yield this.sshCommandLog(`sudo sed -i.bak 's/^mic=".*"/mic="${source}"/' /usr/local/bin/pulse-aec.sh`, listener); yield this.sshCommandLog(`sudo sed -i.bak 's/^speaker=".*"/speaker="${sink}"/' /usr/local/bin/pulse-aec.sh`, listener); yield this.sshCommandLog('sudo systemctl stop snips-audio-server.service', listener); yield this.sshCommandLog('sudo systemctl stop pulseaudio.service', listener); yield this.sshCommandLog('sudo systemctl restart pulseaudio.service', listener); yield this.sshCommandLog('sudo systemctl restart pulseaudio-aec.service', listener); yield this.sshCommandLog('sudo systemctl restart snips-audio-server.service', listener); yield this.sshCommandLog('sudo systemctl restart snips-audio-server.service', listener); }); } setupEchoCancel(source, sink, listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { yield this.sshCommandLog('sudo apt-get update', listener); yield this.sshCommandLog('sudo apt-get install -y --reinstall pulseaudio', listener); yield this.sshCommandLog('sudo systemctl stop pulseaudio.service', listener); yield this.sshCommandLog('sudo systemctl stop snips-audio-server.service', listener); yield this.sshCommandLog('pulseaudio -k', listener); yield this.sshCommandLog('sudo killall pulseaudio', listener); yield this.writeFileAsRoot(assets_1.Assets.pulseaudioService(), '/lib/systemd/system/pulseaudio.service'); yield this.sshCommandLog("sudo sed -i.bak 's/^\\(.*asound\\.conf.*\\)/# \\1/' /usr/bin/seeed-voicecard", listener).catch(() => { }); yield this.sshCommandLog('sudo rm /etc/asound.conf || true', listener); yield this.writeFileAsRoot(assets_1.Assets.pulseaudioAsoundConf(), '/etc/asound.conf'); yield this.sshCommandLog('sudo usermod -a -G pulse-access _snips', listener); yield this.sshCommandLog('sudo usermod -a -G pulse-access pi', listener); yield this.sshCommandLog('sudo usermod -a -G pulse-access root', listener); yield this.sshCommandLog("sudo sed -i.bak 's/^;\\?\\s*default-sample-rate.*/default-sample-rate = 48000/' /etc/pulse/daemon.conf", listener); const pulseScript = `#!/bin/bash # name of the mic and speaker to use mic="${source}" speaker="${sink}" # wait for the source to exist for (( ; ; )) do sleep 1 found=\`pactl list sources | grep Name: | grep -v ".monitor" | grep \$\{mic} | wc -l \` if [ "$found" == "1" ] then break fi done sleep 2 # load the echo-cancel module pactl load-module \\ module-echo-cancel \\ source_master=\$\{mic} \\ sink_master=\$\{speaker} \\ aec_method=webrtc \\ use_master_format=1 \\ aec_args='"high_pass_filter=1 noise_suppression=0 analog_gain_control=0"' `; yield this.writeFileAsRoot(pulseScript, '/usr/local/bin/pulse-aec.sh'); yield this.sshCommandLog('sudo chmod +x /usr/local/bin/pulse-aec.sh', listener); yield this.sshCommandLog('sudo chown pulse:pulse /usr/local/bin/pulse-aec.sh', listener); yield this.writeFileAsRoot(assets_1.Assets.pulseaudioAecService(), '/lib/systemd/system/pulseaudio-aec.service'); this.writeFileAsRoot(assets_1.Assets.pulseaudioSnipsAudioServerService(), '/etc/systemd/system/multi-user.target.wants/snips-audio-server.service'); yield this.sshCommandLog(`sudo sed -i.bak 's/^;\\?\\s*default-sink.*/default-sink = ${sink}.echo-cancel/' /etc/pulse/client.conf`, listener); yield this.sshCommandLog(`sudo sed -i.bak 's/^;\\?\\s*default-source.*/default-source = ${source}.echo-cancel/' /etc/pulse/client.conf`, listener); yield this.sshCommandLog('sudo systemctl daemon-reload', listener); yield this.sshCommandLog('systemctl --user disable pulseaudio', listener); yield this.sshCommandLog('sudo systemctl enable pulseaudio.service', listener); yield this.sshCommandLog('sudo systemctl enable pulseaudio-aec.service', listener); yield this.sshCommandLog('sudo systemctl enable snips-audio-server.service', listener); yield this.sshCommandLog('sudo systemctl restart pulseaudio.service', listener); yield this.sshCommandLog('sudo systemctl restart pulseaudio-aec.service', listener); yield this.sshCommandLog('sudo systemctl restart snips-audio-server.service', listener); }); } listPulseSources() { return tslib_1.__awaiter(this, void 0, void 0, function* () { const res = yield this.ssh.execCommand("LC_ALL=C.UTF8 ; pactl list sources | grep '^\\s*\\(Name\\|Description\\)' | sed 's/\\s*\\S*:\\s//' - | xargs -n2 -d'\\n' | grep -v '.monitor' | grep -v '.echo-cancel'"); const str = res.stdout; return str.split('\n').map(s => { return { name: s.substr(0, s.indexOf(' ')), description: s.substr(s.indexOf(' ') + 1), }; }); }); } listPulseSinks() { return tslib_1.__awaiter(this, void 0, void 0, function* () { const res = yield this.ssh.execCommand("LC_ALL=C.UTF8 ; pactl list sinks | grep '^\\s*\\(Name\\|Description\\)' | sed 's/\\s*\\S*:\\s//' - | xargs -n2 -d'\\n' | grep -v '.monitor' | grep -v '.echo-cancel'"); const str = res.stdout; return str.split('\n').map(s => { return { name: s.substr(0, s.indexOf(' ')), description: s.substr(s.indexOf(' ') + 1), }; }); }); } moveSkillsToHomeAssistantFolder(pythonFolder, skillNames) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.folderExists(pythonFolder) .then(pythonFolderExists => { if (!pythonFolderExists) return this.ssh.exec(`sudo mkdir ${pythonFolder}`); return ''; }) .then((_) => tslib_1.__awaiter(this, void 0, void 0, function* () { const moveFolders = skillNames.map(name => `/var/lib/snips/skills/${name}/*.py`).join(' '); return this.ssh.exec(`sudo mv -t ${pythonFolder} ${moveFolders}`); })); }); } readHASSConfig() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(''); }); } backupHassConfiguration(hassConfigurationPath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`cd ${hassConfigurationPath}; sudo cp configuration.yaml configuration.yaml.bak-$(date +%-Y%-m%-d)-$(date +%-T)`); }); } folderExists(remoteFilePath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`if [ -d "${remoteFilePath}" ]; then echo 1; else echo 0; fi`) .then(result => result === '1'); }); } fileExists(remoteFilePath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return (yield this.ssh.exec(`if [ -f "${remoteFilePath}" ]; then echo 1; else echo 0; fi`)) === '1'; }); } restartHomeAssistant() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('sudo systemctl restart home-assistant@homeassistant.service'); }); } updateHomeAssistant(listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.sshCommandLog(this.hassBashCmd('cd /srv/homeassistant; source bin/activate; pip3 install --upgrade homeassistant'), listener); }); } installPythonAndPip(listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.sshCommandLog('sudo apt-get install -y python3-pip python3-venv python-pip; sudo pip install virtualenv', listener); }); } installHomeAssistant(listener, installStep) { return tslib_1.__awaiter(this, void 0, void 0, function* () { installStep('Updating APT repositories'); return this.sshCommandLog('sudo apt-get update', listener) .then(_ => installStep('Installing python3-pip, python3-venv')) .then(_ => this.sshCommandLog('sudo apt-get install -y python3-pip python3-venv', listener)) .then(_ => installStep('Adding homeassistant user')) .then(_ => this.sshCommandLog('sudo useradd -rm homeassistant', listener)) .then(_ => this.sshCommandLog('cd /srv; sudo mkdir homeassistant; sudo chown homeassistant:homeassistant homeassistant', listener)) .then(_ => installStep('Installing Home Assistant')) .then(_ => this.sshCommandLog(this.hassBashCmd('cd /srv/homeassistant; python3 -m venv .; source bin/activate; python3 -m pip install wheel; pip3 install homeassistant'), listener)) .then(_ => installStep('Adding Hass systemd service to enable autostart')) .then(_ => this.sshCommandLog(`echo '${assets_1.Assets.homeAssistantPythonSystemdService()}' | sudo tee /etc/systemd/system/home-assistant@homeassistant.service > /dev/null`, listener)) .then(_ => this.ssh.exec('sudo systemctl --system daemon-reload; sudo systemctl enable home-assistant@homeassistant.service') .catch(_ => { })) .then(_ => installStep(`Launching Hass, it can take a while the first time. Check http://${this.credentials.hostname}:8123 to see when it's up.`)) .then(_ => this.ssh.exec('sudo systemctl start home-assistant@homeassistant.service')); }); } hassBashCmd(command) { return `sudo su -c '${command}' -s /bin/bash homeassistant`; } playWAVtoASR(remoteWAVPath) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`cat ${remoteWAVPath} | nc localhost 1234`); }); } journalctlLog(service, listener) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.sshCommandLog(`journalctl -S now -o cat -f -u ${service}.service`, listener); }); } editAudioServerService() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`sudo sed -i "s#.*ExecStart=/usr/bin/${SnipsServices.audioServer}.*#ExecStart=/usr/bin/${SnipsServices.audioServer} --hijack localhost:1234 --nomike#g" /lib/systemd/system/${SnipsServices.audioServer}.service; \ sudo systemctl daemon-reload; \ sudo systemctl restart ${SnipsServices.audioServer}.service`); }); } resetAudioServerService() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`sudo sed -i "s#.*ExecStart=/usr/bin/${SnipsServices.audioServer}.*#ExecStart=/usr/bin/${SnipsServices.audioServer}#g" /lib/systemd/system/${SnipsServices.audioServer}.service; \ sudo systemctl daemon-reload; \ sudo systemctl restart ${SnipsServices.audioServer}.service`); }); } editASRService(maxActive, minActive) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`sudo sed -i "s#.*ExecStart=/usr/bin/${SnipsServices.asr}.*#ExecStart=/usr/bin/${SnipsServices.asr} --max_active ${maxActive} --min_active ${minActive}#g" /lib/systemd/system/${SnipsServices.asr}.service; \ sudo systemctl daemon-reload; \ sudo systemctl restart ${SnipsServices.asr}.service`); }); } resetASRService() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`sudo sed -i "s#.*ExecStart=/usr/bin/${SnipsServices.asr}.*#ExecStart=/usr/bin/${SnipsServices.asr}#g" /lib/systemd/system/${SnipsServices.asr}.service; \ sudo systemctl daemon-reload; \ sudo systemctl restart ${SnipsServices.asr}.service`); }); } publishMQTTStartListening() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec(`mosquitto_pub -h 127.0.0.1 -t 'hermes/asr/startListening' -m '{ "siteId":"default", "sessionId": "some_session" }'`); }); } killNCServer() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.ssh.exec('pkill nc'); }); } sshCopyId(onSSHKeygenExeNotFound) { return tslib_1.__awaiter(this, void 0, void 0, function* () { let sshPubKeyFile; if (!fsExtra.existsSync(snipsRSAKeyFilePath)) { sshPubKeyFile = yield this.sshKeygen(onSSHKeygenExeNotFound); } else { sshPubKeyFile = fsExtra.readFileSync(`${snipsRSAKeyFilePath}.pub`, 'utf8'); } const command = `echo "${sshPubKeyFile}" | exec sh -c 'cd ; umask 077 ; mkdir -p .ssh && cat >> ${exports.snipsRemoteAuthorizedKeysFilename} || exit 1 ; if type restorecon >/dev/null 2>&1 ; then restorecon -F .ssh ${exports.snipsRemoteAuthorizedKeysFilename} ; fi'`; return this.ssh.exec(command); }); } sshKeygen(onSSHKeygenExeNotFound) { return tslib_1.__awaiter(this, void 0, void 0, function* () { let command = `ssh-keygen -m PEM -t rsa -b 4096 -C "Snips RSA key" -N "" -f "${snipsRSAKeyFilePath}"`; if (process.platform === 'win32') { let sshKeygenPath; switch (process.arch) { case 'ia32': case 'x64': sshKeygenPath = 'C:\\Program Files\\Git\\usr\\bin\\ssh-keygen.exe'; break; default: throw new Error('Unsupported platform'); } let pathValid = false; while (!pathValid) { try { fsExtra.accessSync(sshKeygenPath, fsExtra.constants.R_OK | fsExtra.constants.W_OK); pathValid =