From 6c5fcc4119d05389bbc71bd3e52090f6fd29b10c Mon Sep 17 00:00:00 2001 From: Ven Date: Fri, 13 Jan 2023 17:52:28 +0100 Subject: [PATCH] Use GUI installer for pnpm inject/uninject (#407) * Use GUI installer for pnpm inject/uninject * Run Installer in DevMode --- package.json | 4 +- scripts/patcher/common.js | 357 ----------------------------------- scripts/patcher/install.js | 219 --------------------- scripts/patcher/uninstall.js | 116 ------------ scripts/runInstaller.mjs | 97 ++++++++++ 5 files changed, 99 insertions(+), 694 deletions(-) delete mode 100644 scripts/patcher/common.js delete mode 100755 scripts/patcher/install.js delete mode 100755 scripts/patcher/uninstall.js create mode 100644 scripts/runInstaller.mjs diff --git a/package.json b/package.json index 51d384db..224a480f 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,13 @@ "scripts": { "build": "node scripts/build/build.mjs", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", - "inject": "node scripts/patcher/install.js", + "inject": "node scripts/runInstaller.mjs", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint:fix": "pnpm lint --fix", "test": "pnpm lint && pnpm build && pnpm testTsc", "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc", "testTsc": "tsc --noEmit", - "uninject": "node scripts/patcher/uninstall.js", + "uninject": "node scripts/runInstaller.mjs", "watch": "node scripts/build/build.mjs --watch" }, "dependencies": { diff --git a/scripts/patcher/common.js b/scripts/patcher/common.js deleted file mode 100644 index 05523e5a..00000000 --- a/scripts/patcher/common.js +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -const path = require("path"); -const readline = require("readline"); -const fs = require("fs"); -const menu = require("console-menu"); - -function pathToBranch(dir) { - dir = dir.toLowerCase(); - if (dir.endsWith("development")) { - return "development"; - } - if (dir.endsWith("canary")) { - return "canary"; - } - if (dir.endsWith("ptb")) { - return "ptb"; - } - return "stable"; -} - -const BRANCH_NAMES = [ - "Discord", - "DiscordPTB", - "DiscordCanary", - "DiscordDevelopment", - "discord", - "discordptb", - "discordcanary", - "discorddevelopment", - "discord-ptb", - "discord-canary", - "discord-development", - // Flatpak - "com.discordapp.Discord", - "com.discordapp.DiscordPTB", - "com.discordapp.DiscordCanary", - "com.discordapp.DiscordDevelopment", -]; - -const MACOS_DISCORD_DIRS = [ - "Discord.app", - "Discord PTB.app", - "Discord Canary.app", - "Discord Development.app", -]; - -if (process.platform === "linux" && process.env.SUDO_USER) { - process.env.HOME = fs - .readFileSync("/etc/passwd", "utf-8") - .match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0] - .split(":")[5]; -} - -const LINUX_DISCORD_DIRS = [ - "/usr/share", - "/usr/lib64", - "/opt", - `${process.env.HOME}/.local/share`, - `${process.env.HOME}/.dvm`, - "/var/lib/flatpak/app", - `${process.env.HOME}/.local/share/flatpak/app`, -]; - -const FLATPAK_NAME_MAPPING = { - DiscordCanary: "discord-canary", - DiscordPTB: "discord-ptb", - DiscordDevelopment: "discord-development", - Discord: "discord", -}; - -const ENTRYPOINT = path - .join(process.cwd(), "dist", "patcher.js") - .replace(/\\/g, "/"); - -function question(question) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: false, - }); - - return new Promise(resolve => { - rl.question(question, answer => { - rl.close(); - resolve(answer); - }); - }); -} - -async function getMenuItem(installations) { - const menuItems = installations.map(info => ({ - title: info.patched ? "[MODIFIED] " + info.location : info.location, - info, - })); - - const result = await menu( - [ - ...menuItems, - { title: "Specify custom path", info: "custom" }, - { title: "Exit without patching", exit: true } - ], - { - header: "Select a Discord installation to patch:", - border: true, - helpMessage: - "Use the up/down arrow keys to select an option. " + - "Press ENTER to confirm.", - } - ); - - if (!result || !result.info || result.exit) { - console.log("No installation selected."); - process.exit(0); - } - - if (result.info === "custom") { - const customPath = await question("Please enter the path: "); - if (!customPath || !fs.existsSync(customPath)) { - console.log("No such Path or not specifed."); - process.exit(); - } - - const resourceDir = path.join(customPath, "resources"); - if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { - console.log("Unsupported Install. resources/app.asar not found"); - process.exit(); - } - - const appDir = path.join(resourceDir, "app"); - result.info = { - branch: "unknown", - patched: fs.existsSync(appDir), - location: customPath, - versions: [{ - path: appDir, - name: null - }], - arch: process.platform === "linux" ? "linux" : "win32", - isFlatpak: false, - }; - } - - if (result.info.patched) { - const answer = await question( - "This installation has already been modified. Overwrite? [Y/n]: " - ); - - if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) { - console.log("Not patching."); - process.exit(0); - } - } - - return result.info; -} - -function getWindowsDirs() { - const dirs = []; - for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) { - if (!BRANCH_NAMES.includes(dir)) continue; - - const location = path.join(process.env.LOCALAPPDATA, dir); - if (!fs.statSync(location).isDirectory()) continue; - - const appDirs = fs - .readdirSync(location, { withFileTypes: true }) - .filter(file => file.isDirectory()) - .filter(file => file.name.startsWith("app-")) - .map(file => path.join(location, file.name)); - - const versions = []; - let patched = false; - - for (const fqAppDir of appDirs) { - const resourceDir = path.join(fqAppDir, "resources"); - if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { - continue; - } - const appDir = path.join(resourceDir, "app"); - if (fs.existsSync(appDir)) { - patched = true; - } - versions.push({ - path: appDir, - name: /app-([0-9.]+)/.exec(fqAppDir)[1], - }); - } - - if (appDirs.length) { - dirs.push({ - branch: dir, - patched, - location, - versions, - arch: "win32", - flatpak: false, - }); - } - } - return dirs; -} - -function getDarwinDirs() { - const dirs = []; - for (const dir of fs.readdirSync("/Applications")) { - if (!MACOS_DISCORD_DIRS.includes(dir)) continue; - - const location = path.join("/Applications", dir, "Contents"); - if (!fs.existsSync(location)) continue; - if (!fs.statSync(location).isDirectory()) continue; - - const appDirs = fs - .readdirSync(location, { withFileTypes: true }) - .filter(file => file.isDirectory()) - .filter(file => file.name.startsWith("Resources")) - .map(file => path.join(location, file.name)); - - const versions = []; - let patched = false; - - for (const resourceDir of appDirs) { - if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { - continue; - } - const appDir = path.join(resourceDir, "app"); - if (fs.existsSync(appDir)) { - patched = true; - } - - versions.push({ - path: appDir, - name: null, // MacOS installs have no version number - }); - } - - if (appDirs.length) { - dirs.push({ - branch: dir, - patched, - location, - versions, - arch: "win32", - }); - } - } - return dirs; -} - -function getLinuxDirs() { - const dirs = []; - for (const dir of LINUX_DISCORD_DIRS) { - if (!fs.existsSync(dir)) continue; - for (const branch of fs.readdirSync(dir)) { - if (!BRANCH_NAMES.includes(branch)) continue; - - const location = path.join(dir, branch); - if (!fs.statSync(location).isDirectory()) continue; - - const isFlatpak = location.includes("/flatpak/"); - - let appDirs = []; - - if (isFlatpak) { - const fqDir = path.join(location, "current", "active", "files"); - if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue; - const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1]; - if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) { - continue; - } - const appDir = path.join( - fqDir, - FLATPAK_NAME_MAPPING[branchName] - ); - - if (!fs.existsSync(appDir)) continue; - if (!fs.statSync(appDir).isDirectory()) continue; - - const resourceDir = path.join(appDir, "resources"); - - appDirs.push(resourceDir); - } else { - appDirs = fs - .readdirSync(location, { withFileTypes: true }) - .filter(file => file.isDirectory()) - .filter( - file => - file.name.startsWith("app-") || - file.name === "resources" - ) - .map(file => path.join(location, file.name)); - } - - const versions = []; - let patched = false; - - for (const resourceDir of appDirs) { - if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { - continue; - } - const appDir = path.join(resourceDir, "app"); - if (fs.existsSync(appDir)) { - patched = true; - } - - const version = /app-([0-9.]+)/.exec(resourceDir); - - versions.push({ - path: appDir, - name: version && version.length > 1 ? version[1] : null, - }); - } - - if (appDirs.length) { - dirs.push({ - branch, - patched, - location, - versions, - arch: "linux", - isFlatpak, - }); - } - } - } - return dirs; -} - -module.exports = { - pathToBranch, - BRANCH_NAMES, - MACOS_DISCORD_DIRS, - LINUX_DISCORD_DIRS, - FLATPAK_NAME_MAPPING, - ENTRYPOINT, - question, - getMenuItem, - getWindowsDirs, - getDarwinDirs, - getLinuxDirs, -}; diff --git a/scripts/patcher/install.js b/scripts/patcher/install.js deleted file mode 100755 index 3d744a67..00000000 --- a/scripts/patcher/install.js +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/node -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -const path = require("path"); -const fs = require("fs"); -const { execSync } = require("child_process"); - -console.log("\nVencord Installer\n"); - -if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) { - console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile"); - process.exit(1); -} - -if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) { - console.log("You need to build the project first. Run:", "pnpm build"); - process.exit(1); -} - -const { - getMenuItem, - getWindowsDirs, - getDarwinDirs, - getLinuxDirs, - ENTRYPOINT, - question, - pathToBranch -} = require("./common"); - -switch (process.platform) { - case "win32": - install(getWindowsDirs()); - break; - case "darwin": - install(getDarwinDirs()); - break; - case "linux": - install(getLinuxDirs()); - break; - default: - console.log("Unknown OS"); - break; -} - -async function install(installations) { - const selected = await getMenuItem(installations); - - // Attempt to give flatpak perms - if (selected.isFlatpak) { - try { - const cwd = process.cwd(); - const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`; - const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`; - const cmd = selected.location.startsWith("/home") - ? userCmd - : globalCmd; - execSync(cmd); - console.log("Gave write perms to Discord Flatpak."); - } catch (e) { - console.log("Failed to give write perms to Discord Flatpak."); - console.log( - "Try running this script as an administrator:", - "sudo pnpm inject" - ); - process.exit(1); - } - - const answer = await question( - `Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` + - "This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" + - "Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" + - "[y/N]: " - ); - - if (["y", "yes", "yeah"].includes(answer.toLowerCase())) { - try { - const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`; - const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`; - const cmd = selected.location.startsWith("/home") - ? userCmd - : globalCmd; - execSync(cmd); - console.log("Sucessfully gave talk permission"); - } catch (err) { - console.error("Failed to give talk permission\n", err); - } - } else { - console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`); - } - } - - const useNewMethod = pathToBranch(selected.branch) !== "stable"; - - for (const version of selected.versions) { - - const dir = useNewMethod ? path.join(version.path, "..") : version.path; - - // Check if we have write perms to the install directory... - try { - fs.accessSync(selected.location, fs.constants.W_OK); - } catch (e) { - console.error("No write access to", selected.location); - console.error( - "Make sure Discord isn't running. If that doesn't work,", - "try running this script as an administrator:", - "sudo pnpm inject" - ); - process.exit(1); - } - if (useNewMethod) { - const appAsar = path.join(dir, "app.asar"); - const _appAsar = path.join(dir, "_app.asar"); - - if (fs.existsSync(_appAsar) && fs.existsSync(appAsar)) { - console.log("This copy of Discord already seems to be patched..."); - console.log("Try running `pnpm uninject` first."); - process.exit(1); - } - - try { - fs.renameSync(appAsar, _appAsar); - } catch (err) { - if (err.code === "EBUSY") { - console.error(selected.branch, "is still running. Make sure you fully close it before running this script."); - process.exit(1); - } - console.error("Failed to rename app.asar to _app.asar"); - throw err; - } - - try { - fs.mkdirSync(appAsar); - } catch (err) { - if (err.code === "EBUSY") { - console.error(selected.branch, "is still running. Make sure you fully close it before running this script."); - process.exit(1); - } - console.error("Failed to create app.asar folder"); - throw err; - } - - fs.writeFileSync( - path.join(appAsar, "index.js"), - `require("${ENTRYPOINT}");` - ); - fs.writeFileSync( - path.join(appAsar, "package.json"), - JSON.stringify({ - name: "discord", - main: "index.js", - }) - ); - - const requiredFiles = ["index.js", "package.json"]; - - if (requiredFiles.every(f => fs.existsSync(path.join(appAsar, f)))) { - console.log( - "Successfully patched", - version.name - ? `${selected.branch} ${version.name}` - : selected.branch - ); - } else { - console.log("Failed to patch", dir); - console.log("Files in directory:", fs.readdirSync(appAsar)); - } - - return; - } - if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) { - fs.rmSync(dir, { recursive: true }); - } - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.writeFileSync( - path.join(dir, "index.js"), - `require("${ENTRYPOINT}");` - ); - fs.writeFileSync( - path.join(dir, "package.json"), - JSON.stringify({ - name: "discord", - main: "index.js", - }) - ); - - const requiredFiles = ["index.js", "package.json"]; - - if (requiredFiles.every(f => fs.existsSync(path.join(dir, f)))) { - console.log( - "Successfully patched", - version.name - ? `${selected.branch} ${version.name}` - : selected.branch - ); - } else { - console.log("Failed to patch", dir); - console.log("Files in directory:", fs.readdirSync(dir)); - } - } -} diff --git a/scripts/patcher/uninstall.js b/scripts/patcher/uninstall.js deleted file mode 100755 index ded6cf9c..00000000 --- a/scripts/patcher/uninstall.js +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/node -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -const path = require("path"); -const fs = require("fs"); - -console.log("\nVencord Uninstaller\n"); - -if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) { - console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile"); - process.exit(1); -} - -const { - getMenuItem, - getWindowsDirs, - getDarwinDirs, - getLinuxDirs, - pathToBranch, -} = require("./common"); - -switch (process.platform) { - case "win32": - uninstall(getWindowsDirs()); - break; - case "darwin": - uninstall(getDarwinDirs()); - break; - case "linux": - uninstall(getLinuxDirs()); - break; - default: - console.log("Unknown OS"); - break; -} - -async function uninstall(installations) { - const selected = await getMenuItem(installations); - - const useNewMethod = pathToBranch(selected.branch) !== "stable"; - - for (const version of selected.versions) { - const dir = useNewMethod ? path.join(version.path, "..") : version.path; - - // Check if we have write perms to the install directory... - try { - fs.accessSync(selected.location, fs.constants.W_OK); - } catch (e) { - console.error("No write access to", selected.location); - console.error( - "Make sure Discord isn't running. If that doesn't work,", - "try running this script as an administrator:", - "sudo pnpm uninject" - ); - process.exit(1); - } - if (useNewMethod) { - if (!fs.existsSync(path.join(dir, "_app.asar"))) { - console.error( - "Original app.asar (_app.asar) doesn't exist.", - "Is your Discord installation corrupt? Try reinstalling Discord." - ); - process.exit(1); - } - if (fs.existsSync(path.join(dir, "app.asar"))) { - try { - fs.rmSync(path.join(dir, "app.asar"), { force: true, recursive: true }); - } catch (err) { - console.error("Failed to delete app.asar folder"); - throw err; - } - } - try { - fs.renameSync( - path.join(dir, "_app.asar"), - path.join(dir, "app.asar") - ); - } catch (err) { - console.error("Failed to rename _app.asar to app.asar"); - throw err; - } - console.log( - "Successfully unpatched", - version.name - ? `${selected.branch} ${version.name}` - : selected.branch - ); - return; - } - if (fs.existsSync(dir)) { - fs.rmSync(dir, { recursive: true }); - } - console.log( - "Successfully unpatched", - version.name - ? `${selected.branch} ${version.name}` - : selected.branch - ); - } -} diff --git a/scripts/runInstaller.mjs b/scripts/runInstaller.mjs new file mode 100644 index 00000000..a74d01fa --- /dev/null +++ b/scripts/runInstaller.mjs @@ -0,0 +1,97 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { execFileSync } from "child_process"; +import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { dirname, join } from "path"; +import { Readable } from "stream"; +import { finished } from "stream/promises"; +import { fileURLToPath } from "url"; + +const BASE_URL = "https://github.com/Vencord/Installer/releases/latest/download/"; + +const DIST_DIR = join(dirname(fileURLToPath(import.meta.url)), ".."); +const FILE_DIR = join(DIST_DIR, "dist", "Installer"); +const ETAG_FILE = join(FILE_DIR, "etag.txt"); + +function getFilename() { + switch (process.platform) { + case "win32": + return "VencordInstaller.exe"; + case "darwin": + // return "VencordInstaller.MacOS.zip"; + throw new Error("PR Mac support if you want it. Or use a better OS that doesn't suck"); + case "linux": + return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11"); + default: + throw new Error("Unsupported platform: " + process.platform); + } +} + +async function ensureBinary() { + const filename = getFilename(); + console.log("Downloading " + filename); + + mkdirSync(FILE_DIR, { recursive: true }); + + const installerFile = join(FILE_DIR, filename); + const etag = existsSync(installerFile) && existsSync(ETAG_FILE) ? readFileSync(ETAG_FILE, "utf-8") : null; + + const res = await fetch(BASE_URL + filename, { + headers: { + "User-Agent": "Vencord (https://github.com/Vendicated/Vencord)", + "If-None-Match": etag + } + }); + if (res.status === 304) { + console.log("Up to date, not redownloading!"); + return installerFile; + } + + if (!res.ok) { + throw new Error(`Failed to download installer: ${res.status} ${res.statusText}`); + } + + const newEtag = res.headers.get("etag"); + writeFileSync(ETAG_FILE, newEtag); + + // WHY DOES NODE FETCH RETURN A WEB STREAM OH MY GOD + const body = Readable.fromWeb(res.body); + await finished(body.pipe(createWriteStream(installerFile, { + mode: 0o755, + autoClose: true + }))); + + console.log("Finished downloading!"); + + return installerFile; +} + + +console.log("Now running Installer..."); + +const installerBin = await ensureBinary(); + +execFileSync(installerBin, { + stdio: "inherit", + env: { + ...process.env, + VENCORD_USER_DATA_DIR: DIST_DIR, + VENCORD_DEV_INSTALL: "1" + } +});