snips-sam
Version:
The Snips Assistant Manager
1,047 lines (1,043 loc) • 50.8 kB
JavaScript
"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 =