UNPKG

homebridge-config-ui-x

Version:

A web based management, configuration and control platform for Homebridge

514 lines • 25.3 kB
"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}@latest`], 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}@latest`], 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}@latest`], 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