UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

303 lines (277 loc) 8.29 kB
import { detectPackageManager, PackageManager } from "@alcalzone/pak"; import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core"; import { getErrorMessage } from "@zwave-js/shared"; import { isObject } from "alcalzone-shared/typeguards"; import axios from "axios"; import execa from "execa"; import fs from "fs-extra"; import os from "os"; import * as path from "path"; import * as lockfile from "proper-lockfile"; import * as semver from "semver"; import type { Readable } from "stream"; /** * Checks whether there is a compatible update for the currently installed config package. * Returns the new version if there is an update, `undefined` otherwise. * Throws if the update check failed. */ export async function checkForConfigUpdates( currentVersion: string, ): Promise<string | undefined> { let registry: Record<string, unknown>; try { registry = ( await axios.get<any>("https://registry.npmjs.org/@zwave-js/config") ).data; } catch (e) { throw new ZWaveError( `Could not check for config updates: Failed to download package information!`, ZWaveErrorCodes.Config_Update_RegistryError, ); } if (!isObject(registry) || !isObject(registry.versions)) { throw new ZWaveError( `Could not check for config updates: Downloaded package information does not contain version information!`, ZWaveErrorCodes.Config_Update_RegistryError, ); } // Find the highest possible prepatch update (e.g. 7.2.4 -> 7.2.5-20200424) const allVersions = Object.keys(registry.versions) .filter((v) => !!semver.valid(v)) .filter((v) => /\-\d{8}$/.test(v)); const updateRange = `>${currentVersion} <${semver.inc( currentVersion, "patch", )}`; const updateVersion = semver.maxSatisfying(allVersions, updateRange, { includePrerelease: true, }); if (updateVersion) return updateVersion; } /** * Installs the update for @zwave-js/config with the given version. */ export async function installConfigUpdate(newVersion: string): Promise<void> { // Check which package manager to use for the update let pak: PackageManager; try { pak = await detectPackageManager({ cwd: __dirname, requireLockfile: false, setCwdToPackageRoot: true, }); } catch { throw new ZWaveError( `Config update failed: No package manager detected or package.json not found!`, ZWaveErrorCodes.Config_Update_PackageManagerNotFound, ); } const packageJsonPath = path.join(pak.cwd, "package.json"); try { await lockfile.lock(packageJsonPath, { onCompromised: () => { // do nothing }, }); } catch { throw new ZWaveError( `Config update failed: Another installation is already in progress!`, ZWaveErrorCodes.Config_Update_InstallFailed, ); } // And install it const result = await pak.overrideDependencies({ "@zwave-js/config": newVersion, }); // Free the lock try { if (await lockfile.check(packageJsonPath)) await lockfile.unlock(packageJsonPath); } catch { // whatever - just don't crash } if (result.success) return; throw new ZWaveError( `Config update failed: Package manager exited with code ${result.exitCode} ${result.stderr}`, ZWaveErrorCodes.Config_Update_InstallFailed, ); } /** * Installs the update for @zwave-js/config with the given version. * Version for Docker images that does not mess up the container if there's no yarn cache */ export async function installConfigUpdateInDocker( newVersion: string, external?: { configDir: string; cacheDir: string; }, ): Promise<void> { let registryInfo: any; try { registryInfo = ( await axios.get( `https://registry.npmjs.org/@zwave-js/config/${newVersion}`, ) ).data; } catch { throw new ZWaveError( `Config update failed: Could not fetch package info from npm registry!`, ZWaveErrorCodes.Config_Update_InstallFailed, ); } const url = registryInfo?.dist?.tarball; if (typeof url !== "string") { throw new ZWaveError( `Config update failed: Could not fetch package tarball URL from npm registry!`, ZWaveErrorCodes.Config_Update_InstallFailed, ); } let lockfilePath: string; let lockfileOptions: lockfile.LockOptions; if (external) { lockfilePath = external.cacheDir; lockfileOptions = { lockfilePath: path.join(external.cacheDir, "config-update.lock"), }; } else { // Acquire a lock so the installation doesn't run twice let pak: PackageManager; try { pak = await detectPackageManager({ cwd: __dirname, requireLockfile: false, setCwdToPackageRoot: true, }); } catch { throw new ZWaveError( `Config update failed: No package manager detected or package.json not found!`, ZWaveErrorCodes.Config_Update_PackageManagerNotFound, ); } lockfilePath = path.join(pak.cwd, "package.json"); lockfileOptions = {}; } try { await lockfile.lock(lockfilePath, { ...lockfileOptions, onCompromised: () => { // do nothing }, }); } catch (e) { throw new ZWaveError( `Config update failed: Another installation is already in progress!`, ZWaveErrorCodes.Config_Update_InstallFailed, ); } const freeLock = async () => { try { if (await lockfile.check(lockfilePath, lockfileOptions)) await lockfile.unlock(lockfilePath, lockfileOptions); } catch { // whatever - just don't crash } }; // Download tarball to a temporary directory const tmpDir = path.join(os.tmpdir(), "zjs-config-update"); const tarFilename = path.join(tmpDir, "zjs-config-update.tgz"); const configModuleDir = path.dirname( require.resolve("@zwave-js/config/package.json"), ); const extractedDir = path.join(tmpDir, "extracted"); try { await fs.ensureDir(tmpDir); const fstream = fs.createWriteStream(tarFilename, { autoClose: true }); const response = await axios({ method: "GET", url, responseType: "stream", }); const rstream = response.data as Readable; rstream.pipe(fstream); await new Promise((resolve, reject) => { rstream.on("error", reject); rstream.on("end", resolve); }); } catch (e) { await freeLock(); throw new ZWaveError( `Config update failed: Could not download tarball. Reason: ${getErrorMessage( e, )}`, ZWaveErrorCodes.Config_Update_InstallFailed, ); } // This should not be necessary in Docker. Leaving it here anyways in case // we want to use this method on Windows at some point function normalizeToUnixStyle(path: string): string { path = path.replace(/:/g, ""); path = path.replace(/\\/g, "/"); if (!path.startsWith("/")) path = `/${path}`; return path; } // Extract it into a temporary folder, then overwrite the config node_modules with it try { await fs.emptyDir(extractedDir); await execa("tar", [ "--strip-components=1", "-xzf", normalizeToUnixStyle(tarFilename), "-C", normalizeToUnixStyle(extractedDir), ]); // How we install now depends on whether we're installing into the external config dir. // If we are, we just need to copy the `devices` subdirectory. If not, copy the entire extracted dir if (external) { await fs.emptyDir(external.configDir); await fs.copy( path.join(extractedDir, "config"), external.configDir, { filter: async (src: string) => { if (!(await fs.stat(src)).isFile()) return true; return src.endsWith(".json"); }, }, ); const externalVersionFilename = path.join( external.configDir, "version", ); await fs.writeFile(externalVersionFilename, newVersion, "utf8"); } else { await fs.remove(configModuleDir); await fs.rename(extractedDir, configModuleDir); } } catch (e) { await freeLock(); throw new ZWaveError( `Config update failed: Could not extract tarball`, ZWaveErrorCodes.Config_Update_InstallFailed, ); } // Try to update our own package.json if we're working with the internal structure if (!external) { try { const packageJsonPath = require.resolve("zwave-js/package.json"); const json = await fs.readJSON(packageJsonPath, { encoding: "utf8", }); json.dependencies["@zwave-js/config"] = newVersion; await fs.writeJSON(packageJsonPath, json, { encoding: "utf8", spaces: 2, }); } catch { // ignore } } // Clean up the temp dir and ignore errors void fs.remove(tmpDir).catch(() => { // ignore }); // Free the lock await freeLock(); }