homebridge-config-ui-x
Version:
A web based management, configuration and control platform for Homebridge
514 lines • 25.3 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@nestjs/common");
const os = require("os");
const _ = require("lodash");
const path = require("path");
const fs = require("fs-extra");
const child_process = require("child_process");
const semver = require("semver");
const rp = require("request-promise-native");
const color = require("bash-color");
const pty = require("node-pty-prebuilt-multiarch");
const logger_service_1 = require("../../core/logger/logger.service");
const config_service_1 = require("../../core/config/config.service");
let PluginsService = class PluginsService {
constructor(configService, logger) {
this.configService = configService;
this.logger = logger;
this.npm = this.getNpmPath();
this.paths = this.getBasePaths();
this.rp = rp.defaults({
json: true,
headers: {
'User-Agent': this.configService.package.name,
},
timeout: 5000,
});
}
getInstalledPlugins() {
return __awaiter(this, void 0, void 0, function* () {
const plugins = [];
const modules = yield this.getInstalledModules();
const homebridgePlugins = modules
.filter(module => (module.name.indexOf('homebridge-') === 0))
.filter((module) => __awaiter(this, void 0, void 0, function* () { return (yield fs.pathExists(path.join(module.installPath, 'package.json')).catch(x => null)); }))
.filter(x => x);
yield Promise.all(homebridgePlugins.map((pkg) => __awaiter(this, void 0, void 0, function* () {
try {
const pjson = yield fs.readJson(path.join(pkg.installPath, 'package.json'));
if (pjson.keywords && pjson.keywords.includes('homebridge-plugin')) {
const plugin = yield this.parsePackageJson(pjson, pkg.path);
if (!plugins.find(x => plugin.name === x.name)) {
plugins.push(plugin);
}
else if (!plugin.globalInstall && plugins.find(x => plugin.name === x.name && x.globalInstall === true)) {
const index = plugins.findIndex(x => plugin.name === x.name && x.globalInstall === true);
plugins[index] = plugin;
}
}
}
catch (e) {
this.logger.error(`Failed to parse plugin "${pkg.name}": ${e.message}`);
}
})));
this.installedPlugins = plugins;
return _.orderBy(plugins, ['updateAvailable', 'name'], ['desc', 'asc']);
});
}
getOutOfDatePlugins() {
return __awaiter(this, void 0, void 0, function* () {
const plugins = yield this.getInstalledPlugins();
return plugins.filter(x => x.updateAvailable);
});
}
searchNpmRegistry(query) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.installedPlugins) {
yield this.getInstalledPlugins();
}
const q = ((!query || !query.length) ? '' : query + '+') + 'keywords:homebridge-plugin+not:deprecated&size=30';
const searchResults = yield this.rp.get(`https://registry.npmjs.org/-/v1/search?text=${q}`);
const result = searchResults.objects
.filter(x => x.package.name.indexOf('homebridge-') === 0)
.map((pkg) => {
let plugin = {
name: pkg.package.name,
};
const isInstalled = this.installedPlugins.find(x => x.name === plugin.name);
if (isInstalled) {
plugin = isInstalled;
return plugin;
}
plugin.publicPackage = true;
plugin.installedVersion = null;
plugin.latestVersion = pkg.package.version;
plugin.lastUpdated = pkg.package.date;
plugin.description = (pkg.package.description) ?
pkg.package.description.replace(/(?:https?|ftp):\/\/[\n\S]+/g, '').trim() : pkg.package.name;
plugin.links = pkg.package.links;
plugin.author = (pkg.package.publisher) ? pkg.package.publisher.username : null;
plugin.certifiedPlugin = (pkg.package.name.indexOf('@homebridge/homebridge-') === 0);
return plugin;
});
if (!result.length && query.indexOf('homebridge-') === 0) {
return yield this.searchNpmRegistrySingle(query);
}
return result;
});
}
searchNpmRegistrySingle(query) {
return __awaiter(this, void 0, void 0, function* () {
try {
const pkg = yield this.rp.get(`https://registry.npmjs.org/${encodeURIComponent(query).replace('%40', '@')}`);
if (!pkg.keywords || !pkg.keywords.includes('homebridge-plugin')) {
return [];
}
let plugin;
const isInstalled = this.installedPlugins.find(x => x.name === pkg.name);
if (isInstalled) {
plugin = isInstalled;
return [plugin];
}
plugin = {
name: pkg.name,
description: (pkg.description) ?
pkg.description.replace(/(?:https?|ftp):\/\/[\n\S]+/g, '').trim() : pkg.name,
certifiedPlugin: (pkg.name.indexOf('@homebridge/homebridge-') === 0),
};
plugin.publicPackage = true;
plugin.latestVersion = pkg['dist-tags'].latest;
plugin.lastUpdated = pkg.package.date;
plugin.updateAvailable = false;
plugin.links = {
npm: `https://www.npmjs.com/package/${plugin.name}`,
homepage: pkg.homepage,
bugs: (pkg.bugs) ? pkg.bugs.url : null,
};
plugin.author = (pkg.maintainers.length) ? pkg.maintainers[0].name : null;
plugin.certifiedPlugin = (pkg.name.indexOf('@homebridge/homebridge-') === 0);
return [plugin];
}
catch (e) {
if (e.statusCode !== 404) {
this.logger.error('Failed to search npm registry');
this.logger.error(e.message);
}
return [];
}
});
}
installPlugin(pluginName, client) {
return __awaiter(this, void 0, void 0, function* () {
yield this.getInstalledPlugins();
let installPath = (this.configService.customPluginPath) ?
this.configService.customPluginPath : this.installedPlugins.find(x => x.name === this.configService.name).installPath;
const installOptions = [];
if (installPath === this.configService.customPluginPath && (yield fs.pathExists(path.resolve(installPath, '../package.json')))) {
installOptions.push('--save');
}
installPath = path.resolve(installPath, '../');
yield this.runNpmCommand([...this.npm, 'install', '--unsafe-perm', ...installOptions, `${pluginName}`], installPath, client);
return true;
});
}
uninstallPlugin(pluginName, client) {
return __awaiter(this, void 0, void 0, function* () {
yield this.getInstalledPlugins();
const plugin = this.installedPlugins.find(x => x.name === pluginName);
if (!plugin) {
throw new Error(`Plugin "${pluginName}" Not Found`);
}
let installPath = plugin.installPath;
const installOptions = [];
if (installPath === this.configService.customPluginPath && (yield fs.pathExists(path.resolve(installPath, '../package.json')))) {
installOptions.push('--save');
}
installPath = path.resolve(installPath, '../');
yield this.runNpmCommand([...this.npm, 'uninstall', '--unsafe-perm', ...installOptions, pluginName], installPath, client);
yield this.ensureCustomPluginDirExists();
return true;
});
}
updatePlugin(pluginName, client) {
return __awaiter(this, void 0, void 0, function* () {
if (pluginName === this.configService.name && this.configService.dockerOfflineUpdate) {
yield this.updateSelfOffline(client);
return true;
}
yield this.getInstalledPlugins();
const plugin = this.installedPlugins.find(x => x.name === pluginName);
if (!plugin) {
throw new Error(`Plugin "${pluginName}" Not Found`);
}
let installPath = plugin.installPath;
const installOptions = [];
if (installPath === this.configService.customPluginPath && (yield fs.pathExists(path.resolve(installPath, '../package.json')))) {
installOptions.push('--save');
}
installPath = path.resolve(installPath, '../');
yield this.runNpmCommand([...this.npm, 'install', '--unsafe-perm', ...installOptions, `${pluginName}`], installPath, client);
return true;
});
}
getHomebridgePackage() {
return __awaiter(this, void 0, void 0, function* () {
if (this.configService.ui.homebridgePackagePath) {
const pjsonPath = path.join(this.configService.ui.homebridgePackagePath, 'package.json');
if (yield fs.pathExists(pjsonPath)) {
return yield this.parsePackageJson(yield fs.readJson(pjsonPath), this.configService.ui.homebridgePackagePath);
}
else {
this.logger.error(`"homebridgePath" (${this.configService.ui.homebridgePackagePath}) does not exist`);
}
}
const modules = yield this.getInstalledModules();
const homebridgeInstalls = modules.filter(x => x.name === 'homebridge');
if (homebridgeInstalls.length > 1) {
this.logger.warn('Multiple Instances Of Homebridge Found Installed');
homebridgeInstalls.forEach((instance) => {
this.logger.warn(instance.installPath);
});
}
if (!homebridgeInstalls.length) {
this.logger.error('Unable To Find Homebridge Installation');
throw new Error('Unable To Find Homebridge Installation');
}
const homebridgeModule = homebridgeInstalls[0];
const pjson = yield fs.readJson(path.join(homebridgeModule.installPath, 'package.json'));
const homebridge = yield this.parsePackageJson(pjson, homebridgeModule.path);
return homebridge;
});
}
updateHomebridgePackage(client) {
return __awaiter(this, void 0, void 0, function* () {
const homebridge = yield this.getHomebridgePackage();
let installPath = homebridge.installPath;
const installOptions = [];
if (installPath === this.configService.customPluginPath && (yield fs.pathExists(path.resolve(installPath, '../package.json')))) {
installOptions.push('--save');
}
installPath = path.resolve(installPath, '../');
yield this.runNpmCommand([...this.npm, 'install', '--unsafe-perm', ...installOptions, `${homebridge.name}`], installPath, client);
return true;
});
}
updateSelfOffline(client) {
return __awaiter(this, void 0, void 0, function* () {
client.emit('stdout', color.yellow(`${this.configService.name} has been scheduled to update on the next container restart.\n\r\n\r`));
yield new Promise(resolve => setTimeout(resolve, 800));
client.emit('stdout', color.yellow(`The Docker container will now try and restart.\n\r\n\r`));
yield new Promise(resolve => setTimeout(resolve, 800));
client.emit('stdout', color.yellow(`If you have not started the Docker container with `) +
color.red('--restart=always') + color.yellow(` you may\n\rneed to manually start the container again.\n\r\n\r`));
yield new Promise(resolve => setTimeout(resolve, 800));
client.emit('stdout', color.yellow(`This process may take several minutes. Please be patient.\n\r`));
yield new Promise(resolve => setTimeout(resolve, 10000));
yield fs.createFile('/homebridge/.uix-upgrade-on-restart');
});
}
getPluginConfigSchema(pluginName) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.installedPlugins)
yield this.getInstalledPlugins();
const plugin = this.installedPlugins.find(x => x.name === pluginName);
if (!plugin) {
throw new common_1.NotFoundException();
}
if (!plugin.settingsSchema) {
throw new common_1.NotFoundException();
}
const schemaPath = path.resolve(plugin.installPath, pluginName, 'config.schema.json');
const configSchema = yield fs.readJson(schemaPath);
if (pluginName === this.configService.name) {
configSchema.schema.properties.port.default = this.configService.ui.port;
}
if (pluginName === 'homebridge-alexa') {
configSchema.schema.properties.pin.default = this.configService.homebridgeConfig.bridge.pin;
}
return configSchema;
});
}
getPluginChangeLog(pluginName) {
return __awaiter(this, void 0, void 0, function* () {
yield this.getInstalledPlugins();
const plugin = this.installedPlugins.find(x => x.name === pluginName);
if (!plugin) {
throw new common_1.NotFoundException();
}
const changeLog = path.resolve(plugin.installPath, plugin.name, 'CHANGELOG.md');
if (yield fs.pathExists(changeLog)) {
return {
changelog: yield fs.readFile(changeLog, 'utf8'),
};
}
else {
throw new common_1.NotFoundException();
}
});
}
getPluginRelease(pluginName) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.installedPlugins)
yield this.getInstalledPlugins();
const plugin = this.installedPlugins.find(x => x.name === pluginName);
if (!plugin) {
throw new common_1.NotFoundException();
}
if (!plugin.links.homepage) {
throw new common_1.NotFoundException();
}
if (!plugin.links.homepage.match(/https:\/\/github.com/)) {
throw new common_1.NotFoundException();
}
try {
const repo = plugin.links.homepage.split('https://github.com/')[1].split('#readme')[0];
const release = yield this.rp.get(`https://api.github.com/repos/${repo}/releases/latest`);
return {
name: release.name,
changelog: release.body,
};
}
catch (e) {
throw new common_1.NotFoundException();
}
});
}
getInstalledModules() {
return __awaiter(this, void 0, void 0, function* () {
const allModules = [];
for (const requiredPath of this.paths) {
const modules = yield fs.readdir(requiredPath);
for (const module of modules) {
allModules.push({
name: module,
installPath: path.join(requiredPath, module),
path: requiredPath,
});
}
}
return allModules;
});
}
getNpmPath() {
if (os.platform() === 'win32') {
const windowsNpmPath = [
path.join(process.env.APPDATA, 'npm/npm.cmd'),
path.join(process.env.ProgramFiles, 'nodejs/npm.cmd'),
]
.filter(fs.existsSync);
if (windowsNpmPath.length) {
return [windowsNpmPath[0], '--no-update-notifier'];
}
else {
this.logger.error(`ERROR: Cannot find npm binary. You will not be able to manage plugins or update homebridge.`);
this.logger.error(`ERROR: You might be able to fix this problem by running: npm install -g npm`);
}
}
return ['npm', '--no-update-notifier'];
}
getBasePaths() {
let paths = [];
paths = paths.concat(eval('require').main.paths);
if (this.configService.customPluginPath) {
paths.unshift(this.configService.customPluginPath);
}
if (process.env.NODE_PATH) {
paths = process.env.NODE_PATH.split(path.delimiter)
.filter((p) => !!p)
.concat(paths);
}
else {
if ((os.platform() === 'win32')) {
paths.push(path.join(process.env.APPDATA, 'npm/node_modules'));
}
else {
paths.push('/usr/local/lib/node_modules');
paths.push('/usr/lib/node_modules');
paths.push(child_process.execSync('/bin/echo -n "$(npm --no-update-notifier -g prefix)/lib/node_modules"').toString('utf8'));
}
}
return _.uniq(paths).filter((requiredPath) => {
return fs.existsSync(requiredPath);
});
}
parsePackageJson(pjson, installPath) {
return __awaiter(this, void 0, void 0, function* () {
const plugin = {
name: pjson.name,
displayName: pjson.displayName,
description: (pjson.description) ?
pjson.description.replace(/(?:https?|ftp):\/\/[\n\S]+/g, '').trim() : pjson.name,
certifiedPlugin: (pjson.name.indexOf('@homebridge/homebridge-') === 0),
installedVersion: installPath ? (pjson.version || '0.0.1') : null,
globalInstall: (installPath !== this.configService.customPluginPath),
settingsSchema: yield fs.pathExists(path.resolve(installPath, pjson.name, 'config.schema.json')),
installPath,
};
return this.getPluginFromNpm(plugin);
});
}
getPluginFromNpm(plugin) {
return __awaiter(this, void 0, void 0, function* () {
try {
const pkg = yield this.rp.get(`https://registry.npmjs.org/${encodeURIComponent(plugin.name).replace('%40', '@')}`);
plugin.publicPackage = true;
plugin.latestVersion = pkg['dist-tags'].latest;
plugin.updateAvailable = semver.lt(plugin.installedVersion, plugin.latestVersion);
plugin.links = {
npm: `https://www.npmjs.com/package/${plugin.name}`,
homepage: pkg.homepage,
bugs: (pkg.bugs) ? pkg.bugs.url : null,
};
plugin.author = (pkg.maintainers.length) ? pkg.maintainers[0].name : null;
}
catch (e) {
if (e.statusCode !== 404) {
this.logger.error(`[${plugin.name}] ${e.message}`);
}
plugin.publicPackage = false;
plugin.latestVersion = null;
plugin.updateAvailable = false;
plugin.links = {};
}
return plugin;
});
}
runNpmCommand(command, cwd, client) {
return __awaiter(this, void 0, void 0, function* () {
let timeoutTimer;
command = command.filter(x => x.length);
if (this.configService.ui.sudo) {
command.unshift('sudo', '-E', '-n');
}
else {
try {
yield fs.access(cwd, fs.constants.W_OK);
}
catch (e) {
client.emit('stdout', color.yellow(`The user "${os.userInfo().username}" does not have write access to the target directory:\n\r\n\r`));
client.emit('stdout', `${cwd}\n\r\n\r`);
client.emit('stdout', color.yellow(`This may cause the operation to fail.\n\r`));
client.emit('stdout', color.yellow(`See the docs for details on how to enable sudo mode:\n\r`));
client.emit('stdout', color.yellow(`https://github.com/oznu/homebridge-config-ui-x#sudo-mode\n\r\n\r`));
}
}
this.logger.log(`Running Command: ${command.join(' ')}`);
if (!semver.satisfies(process.version, `>=${this.configService.minimumNodeVersion}`)) {
client.emit('stdout', color.yellow(`Node.js v${this.configService.minimumNodeVersion} higher is required for ${this.configService.name}.\n\r`));
client.emit('stdout', color.yellow(`You may experience issues while running on Node.js ${process.version}.\n\r\n\r`));
}
client.emit('stdout', color.cyan(`USER: ${os.userInfo().username}\n\r`));
client.emit('stdout', color.cyan(`DIR: ${cwd}\n\r`));
client.emit('stdout', color.cyan(`CMD: ${command.join(' ')}\n\r\n\r`));
yield new Promise((resolve, reject) => {
const term = pty.spawn(command.shift(), command, {
name: 'xterm-color',
cols: 80,
rows: 30,
cwd,
env: process.env,
});
term.on('data', (data) => {
client.emit('stdout', data);
});
term.on('exit', (code) => {
if (code === 0) {
clearTimeout(timeoutTimer);
client.emit('stdout', color.green(`\n\rCommand succeeded!.\n\r`));
resolve();
}
else {
clearTimeout(timeoutTimer);
reject('Command failed. Please review log for details.');
}
});
timeoutTimer = setTimeout(() => {
term.kill('SIGTERM');
}, 300000);
});
});
}
ensureCustomPluginDirExists() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.configService.customPluginPath) {
return;
}
if (!(yield fs.pathExists(this.configService.customPluginPath))) {
this.logger.warn(`Custom plugin directory was removed. Re-creating: ${this.configService.customPluginPath}`);
try {
yield fs.ensureDir(this.configService.customPluginPath);
}
catch (e) {
this.logger.error(`Failed to recreate custom plugin directory`);
this.logger.error(e.message);
}
}
});
}
};
PluginsService = __decorate([
common_1.Injectable(),
__metadata("design:paramtypes", [config_service_1.ConfigService,
logger_service_1.Logger])
], PluginsService);
exports.PluginsService = PluginsService;
//# sourceMappingURL=plugins.service.js.map