UNPKG

homebridge-appletv-enhanced

Version:

Plugin that exposes the Apple TV to HomeKit with much richer features than the vanilla Apple TV implementation of HomeKit.

339 lines 15.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const utils_1 = require("./utils"); const PrefixLogger_1 = __importDefault(require("./PrefixLogger")); const axios_1 = __importDefault(require("axios")); const compare_versions_1 = require("compare-versions"); const os_1 = __importDefault(require("os")); const supportedPythonVersions = [ '3.10', '3.11', '3.12', '3.13', ]; const MIN_OPENSSL_VERSION = '3.0.0'; const UID = os_1.default.userInfo().uid; const GID = os_1.default.userInfo().gid; class PythonChecker { customPythonExecutable; log; pluginDirPath; pythonExecutable = 'python3'; pythonRequirementsPath = path_1.default.join(__dirname, '..', 'requirements.txt'); venvAtvremoteExecutable; venvAtvscriptExecutable; venvConfigPath; venvPath; venvPipExecutable; venvPythonExecutable; constructor(logger, storagePath, customPythonExecutable) { this.log = new PrefixLogger_1.default(logger, 'Python check'); this.customPythonExecutable = customPythonExecutable; this.pluginDirPath = path_1.default.join(storagePath, 'appletv-enhanced'); this.venvPath = path_1.default.join(this.pluginDirPath, '.venv'); this.venvPythonExecutable = path_1.default.join(this.venvPath, 'bin', 'python3'); this.venvPipExecutable = path_1.default.join(this.venvPath, 'bin', 'pip3'); this.venvConfigPath = path_1.default.join(this.venvPath, 'pyvenv.cfg'); this.venvAtvremoteExecutable = path_1.default.join(this.venvPath, 'bin', 'atvremote'); this.venvAtvscriptExecutable = path_1.default.join(this.venvPath, 'bin', 'atvscript'); } async allInOne(forceVenvRecreate = false) { this.log.info('Starting python check.'); this.pythonExecutable = this.getPythonExecutable('python3', this.customPythonExecutable); this.log.info(`Using "${this.pythonExecutable}" as the python executable.`); this.ensurePluginDir(); await this.ensurePythonVersion(); await this.ensureVenvCreated(forceVenvRecreate); await this.ensureVenvUsesCorrectPython(); await this.ensureVenvPipUpToDate(); await this.ensureOpenSSLVersion(); await this.ensureVenvRequirementsSatisfied(); this.log.success('Finished'); } async areRequirementsSatisfied() { const [freezeStdout] = await (0, utils_1.runCommand)(this.log, this.venvPipExecutable, ['freeze'], undefined, true); const freeze = this.freezeStringToObject(freezeStdout); const requirements = this.freezeStringToObject(fs_1.default.readFileSync(this.pythonRequirementsPath).toString()); for (const pkg in requirements) { if (freeze[pkg] !== requirements[pkg]) { return false; } } return true; } async createVenv() { const [stdout] = await (0, utils_1.runCommand)(this.log, this.pythonExecutable, ['-m', 'venv', this.venvPath, '--clear'], undefined, true); if (stdout.includes('not created successfully') || !this.isVenvCreated()) { while (true) { this.log.error('virtualenv python module is not installed. If you have installed homebridge via the apt package manager, \ update the homebridge apt package to 1.1.4 or above (this applies for installations based on the Raspberry Pi OS image as well). When \ using the official docker image, update the image to version 2023-11-28 or above. Otherwise install the python virtualenv module \ manually.'); await (0, utils_1.delay)(300000); } } else if (stdout.trim() !== '') { this.log.warn(stdout); } this.log.success('Virtual python environment (re)created'); } async ensureOpenSSLVersion() { const [openSSLVersionString] = await (0, utils_1.runCommand)(this.log, this.venvPythonExecutable, ['-c', 'import ssl; print(ssl.OPENSSL_VERSION);'], undefined, true); const r = openSSLVersionString.match(/\d+\.\d+\.\d+/); if (r !== null && (0, compare_versions_1.compareVersions)(MIN_OPENSSL_VERSION, r[0]) !== 1) { this.log.success(`Python was compiled using a compatible OpenSSL version (${r[0]}).`); return; } if (r === null) { this.log.warn(`Could not verify that the correct OpenSSL version is installed. The plugin will continue to start but errors \ can occur if the OpenSSL version is older than ${MIN_OPENSSL_VERSION}.`); } else { while (true) { this.log.error(`You are using OpenSSL ${r[0]}. However, OpenSSL ${MIN_OPENSSL_VERSION} or later is required for AppleTV \ Enhanced. This has been a requirement for a long time. Up until now the plugin was starting in a "legacy openssl mode" if that \ requirement was not met. TvOS 18.4 requires a fix for pyatv which is only available in the newest version of pyatv that requires \ OpenSSL ${MIN_OPENSSL_VERSION}. Thus, the legacy mode cannot be provided any longer as it requires an older version of pyatv. If you \ wonder why this fix is required, please refer to https://github.com/maxileith/homebridge-appletv-enhanced/issues/953.`); await (0, utils_1.delay)(300000); } } } ensurePluginDir() { if (!fs_1.default.existsSync(this.pluginDirPath)) { this.log.info('creating plugin dir ...'); fs_1.default.mkdirSync(this.pluginDirPath); this.log.success('plugin dir created'); } else { this.log.info('plugin dir exists.'); } } async ensurePythonVersion() { const version = await this.getSystemPythonVersion(); if (supportedPythonVersions.findIndex((e) => version.startsWith(e)) === -1) { while (true) { this.log.error(`Python ${version} is installed. However, only Python \ ${supportedPythonVersions[0]} to ${supportedPythonVersions[supportedPythonVersions.length - 1]} is supported.`); await (0, utils_1.delay)(300000); } } else { this.log.success(`Python ${version} is installed and supported by the plugin.`); } } async ensureVenvCreated(forceVenvRecreate) { if (forceVenvRecreate) { this.log.warn('Forcing the python virtual environment to be recreated ...'); await this.createVenv(); } else if (this.isVenvCreated() === false) { this.log.info('Virtual python environment is not present. Creating now ...'); await this.createVenv(); } else if (this.isVenvExecutable() === false) { while (true) { this.log.error(`The current user ${UID}:${GID} does not have the permissions to execute the virtual python environment. \ Make sure the user has the permissions to execute the above mentioned files. \`chmod +x ./appletv-enhanced/.venv/bin/*\` should do the \ trick. Restart the plugin after fixing the permissions.`); await (0, utils_1.delay)(300000); } } else { this.log.info('Virtual environment already exists.'); } } async ensureVenvPipUpToDate() { const venvPipVersion = await this.getVenvPipVersion(); this.log.info(`Venv pip version: ${venvPipVersion}`); this.log.info('Checking if there is an update for venv pip ...'); if (venvPipVersion === await this.getMostRecentPipVersion()) { this.log.info('Venv pip is up to date'); } else { this.log.warn('Venv pip is outdated. Updating now ...'); const success = await this.updatePip(); if (success === true) { this.log.success('Venv pip successfully updated.'); } else { this.log.warn('Failed to update venv pip. Continuing anyhow ...'); } } } async ensureVenvRequirementsSatisfied() { if (await this.areRequirementsSatisfied()) { this.log.info('Python requirements are satisfied.'); } else { this.log.warn('Python requirements are not satisfied. Installing them now ...'); const success = await this.installRequirements(); if (success === true) { this.log.success('Python requirements successfully installed.'); } else { while (true) { this.log.error('There was an error installing the python dependencies. Cannot proceed!'); await (0, utils_1.delay)(300000); } } } if (this.isPyatvExecutable() === false) { while (true) { this.log.error(`The current user ${UID}:${GID} does not have the permissions to execute the PyATV scripts. \ Make sure the user has the permissions to execute the above mentioned files. \`chmod +x ./appletv-enhanced/.venv/bin/*\` should do the \ trick. Restart the plugin after fixing the permissions.`); await (0, utils_1.delay)(300000); } } } async ensureVenvUsesCorrectPython() { const systemPythonHome = await this.getPythonHome(this.pythonExecutable); this.log.debug(`System python home: ${systemPythonHome}`); const venvPythonHome = await this.getPythonHome(this.venvPythonExecutable); this.log.debug(`Venv python home: ${venvPythonHome}`); if (venvPythonHome !== systemPythonHome) { this.log.warn('The virtual environment does not use the configured python environment. Recreating virtual environment ...'); await this.createVenv(); return; } const systemPythonVersion = await this.getSystemPythonVersion(); this.log.debug(`System python version: ${systemPythonVersion}`); const venvPythonVersion = this.getVenvPythonVersion(); this.log.debug(`Venv python version: ${venvPythonVersion}`); if (systemPythonVersion !== venvPythonVersion) { this.log.warn('The virtual environment does not use the configured python environment. Recreating virtual environment ...'); await this.createVenv(); return; } this.log.info('Virtual environment is using the configured python environment. Continuing ...'); } freezeStringToObject(value) { const lines = value.trim().split('\n'); const packages = {}; for (const line of lines) { const [pkg, , version] = line.split(/(==|@)/); packages[pkg.replaceAll('_', '-')] = version; } return packages; } async getMostRecentPipVersion() { try { const response = await axios_1.default.get('https://pypi.org/pypi/pip/json'); return response.data.info.version; } catch (e) { this.log.error(e); return 'error'; } } getPythonExecutable(defaultsTo, customPythonExecutable) { if (customPythonExecutable === undefined) { this.log.debug('Using the systems default python installation since there is no custom python installation specified.'); return defaultsTo; } const pythonCandidate = (0, utils_1.normalizePath)(customPythonExecutable); if (pythonCandidate === undefined) { this.log.warn(`Could not normalize the python executable path ${pythonCandidate}. Falling back to the systems default python \ installation.`); return defaultsTo; } if (fs_1.default.existsSync(pythonCandidate)) { this.log.debug(`The specified python installation "${pythonCandidate}" does exist.`); try { fs_1.default.accessSync(pythonCandidate, fs_1.default.constants.X_OK); this.log.debug(`The current user ${UID}:${GID} has the permissions to execute "${pythonCandidate}".`); } catch { this.log.warn(`The current user ${UID}:${GID} does not have the permissions to execute "${pythonCandidate}". Falling back \ to the systems default python installation.`); return defaultsTo; } this.log.debug(`Using the specified python installation "${pythonCandidate}".`); return pythonCandidate; } this.log.warn(`The python executable "${pythonCandidate}" set in the configuration does not exist. Falling back to the \ systems default python installation.`); return defaultsTo; } async getPythonHome(executable) { const [venvPythonHome] = await (0, utils_1.runCommand)(this.log, executable, ['-c', 'import os, sys; print(os.path.realpath(sys.executable));'], undefined, true); return venvPythonHome.trim(); } async getSystemPythonVersion() { const [version] = await (0, utils_1.runCommand)(this.log, this.pythonExecutable, ['--version'], undefined, true); return version.trim().replace('Python ', ''); } async getVenvPipVersion() { const [version] = await (0, utils_1.runCommand)(this.log, this.venvPipExecutable, ['--version'], undefined, true); return version.trim().replace('pip ', '').split(' ')[0]; } getVenvPythonVersion() { const pyvenvcfgContent = fs_1.default.readFileSync(this.venvConfigPath, 'utf-8'); const lines = pyvenvcfgContent.split('\n'); const versionLine = lines.find((e) => e.startsWith('version')); if (versionLine === undefined) { return '?'; } return versionLine.replace('version', '').replace('=', '').trim(); } async installRequirements() { return (await (0, utils_1.runCommand)(this.log, this.venvPipExecutable, ['install', '-r', this.pythonRequirementsPath])).at(2) === 0; } isPyatvExecutable() { let success = true; for (const f of [ this.venvAtvremoteExecutable, this.venvAtvscriptExecutable, ]) { try { fs_1.default.accessSync(f, fs_1.default.constants.X_OK); } catch { this.log.warn(`The current user ${UID}:${GID} does not have the permissions to execute "${f}".`); success = false; } } return success; } isVenvCreated() { let success = true; for (const f of [ this.venvConfigPath, this.venvPythonExecutable, this.venvPipExecutable, ]) { if (fs_1.default.existsSync(f) === false) { this.log.debug(`${f} does not exist --> venv is not present`); success = false; } } return success; } isVenvExecutable() { let success = true; for (const f of [ this.venvPythonExecutable, this.venvPipExecutable, ]) { try { fs_1.default.accessSync(f, fs_1.default.constants.X_OK); } catch { this.log.warn(`The current user ${UID}:${GID} does not have the permissions to execute "${f}".`); success = false; } } return success; } async updatePip() { return (await (0, utils_1.runCommand)(this.log, this.venvPipExecutable, ['install', '--upgrade', 'pip'])).at(2) === 0; } } exports.default = PythonChecker; //# sourceMappingURL=PythonChecker.js.map