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
JavaScript
;
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