Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LL&C 2: Electric Boogaloo #348

Draft
wants to merge 9 commits into
base: development
Choose a base branch
from
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"productName": "BetterDiscord Installer",
"description": "A simple standalone program which automates the installation, removal and maintenance of BetterDiscord.",
"author": "BetterDiscord",
"version": "1.3.0",
"version": "1.3.1",
"license": "MIT",
"scripts": {
"dev": "electron-webpack dev",
Expand All @@ -15,25 +15,26 @@
"dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null"
},
"dependencies": {
"source-map-support": "^0.5.16"
"source-map-support": "^0.5.21"
},
"devDependencies": {
"electron": "^13.6.9",
"electron-builder": "^23.6.0",
"electron-webpack": "^2.8.2",
"eslint": "^7.21.0",
"eslint-plugin-svelte3": "^3.1.2",
"eslint": "^8.22.0",
"eslint-plugin-svelte3": "^4.0.0",
"find-process": "https://github.com/BetterDiscord/find-process",
"focus-visible": "^5.2.0",
"phin": "^3.7.0",
"rimraf": "^3.0.2",
"semver": "^7.3.8",
"svelte": "^3.38.2",
"svelte": "^3.49.0",
"svelte-loader": "^3.1.7",
"svelte-spa-router": "^3.1.0",
"svelte-spa-router": "^3.3.0",
"tmp": "^0.2.1",
"tree-kill": "^1.2.2",
"webpack": "~5.76.0",
"webpack-bundle-analyzer": "^4.4.0"
"webpack-bundle-analyzer": "^4.6.0"
},
"build": {
"appId": "app.betterdiscord.installer",
Expand Down
4 changes: 2 additions & 2 deletions src/main/update_installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default async function () {

const result = await dialog.showMessageBox({
title: "New Installer Version Available",
message: `A new version of the BetterDiscord installer is available. Click "Download" to download the newest version.`,
message: `A new version of the BetterDiscord installer is available.\n\nClick "Download" to get the latest version.`,
buttons: ["Download", "Later"],
defaultId: 0,
cancelId: 1
Expand All @@ -35,7 +35,7 @@ export default async function () {
await shell.openExternal(latestRelease.html_url);
process.exit(0);
}

}
else {
console.info(`The installer is up to date.`);
Expand Down
110 changes: 81 additions & 29 deletions src/renderer/actions/install.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {progress, status} from "../stores/installation";
import {remote, shell} from "electron";
import {promises as fs} from "fs";
import {promises as fs, copyFileSync} from "fs";
import path from "path";
import phin from "phin";
const tmp = require("tmp");

import {log, lognewline} from "./utils/log";
import succeed from "./utils/succeed";
Expand All @@ -20,27 +21,21 @@ const RESTART_DISCORD_PROGRESS = 100;

const RELEASE_API = "https://api.github.com/repos/BetterDiscord/BetterDiscord/releases";

const bdFolder = path.join(remote.app.getPath("appData"), "BetterDiscord");
const bdDataFolder = path.join(bdFolder, "data");
const bdPluginsFolder = path.join(bdFolder, "plugins");
const bdThemesFolder = path.join(bdFolder, "themes");


async function makeDirectories(...folders) {
const progressPerLoop = (MAKE_DIR_PROGRESS - progress.value) / folders.length;
for (const folder of folders) {
if (await exists(folder)) {
log(`✅ Directory exists: ${folder}`);
log(`✅ Directory exists: ${folder}.`);
progress.set(progress.value + progressPerLoop);
continue;
}
try {
await fs.mkdir(folder);
progress.set(progress.value + progressPerLoop);
log(`✅ Directory created: ${folder}`);
log(`✅ Directory created: ${folder}.`);
}
catch (err) {
log(`❌ Failed to create directory: ${folder}`);
log(`❌ Failed to create directory: ${folder}.`);
log(`❌ ${err.message}`);
return err;
}
Expand Down Expand Up @@ -100,8 +95,9 @@ async function downloadAsar() {
}
}

const asarPath = path.join(bdDataFolder, "betterdiscord.asar");
async function installAsar(fileContent) {
async function installAsar(fileContent, bdFolder) {
const bdDataFolder = path.join(bdFolder, "data");
const asarPath = path.join(bdDataFolder, "betterdiscord.asar");
try {
const originalFs = require("original-fs").promises; // because electron doesn't like writing asar files
await originalFs.writeFile(asarPath, fileContent);
Expand All @@ -113,27 +109,45 @@ async function installAsar(fileContent) {
}
}

async function downloadAndInstallAsar() {
async function downloadAndInstallAsar(bdFolders) {
try {
const fileContent = await downloadAsar();
await installAsar(fileContent);
for (const bdFolder of bdFolders) {
await installAsar(fileContent, bdFolder);
}
}
catch (error) {
return error;
}
}

async function injectShims(paths) {
async function injectShims(paths, bdFolders) {
// * Mac and Windows: All Discord installations share a single, absolute
// folder path where BetterDiscord is installed (in "bdFolders[0]").
// * Linux: Every Discord installation has its own BetterDiscord folder,
// and uses relative paths to load them, so we don't need "bdFolders".
// Flatpaks/Snaps have individual per-Discord-channel BetterDiscord folders,
// since every sandboxed installation has their own xdg-config structure,
// whereas all native installations share the `~/.config/BetterDiscord` folder.
const progressPerLoop = (INJECT_SHIM_PROGRESS - progress.value) / paths.length;
for (const discordPath of paths) {
log("Injecting into: " + discordPath);
log(`Injecting into: ${discordPath}.`);
try {
await fs.writeFile(path.join(discordPath, "index.js"), `require("${asarPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}");\nmodule.exports = require("./core.asar");`);
log("✅ Injection successful");
if (process.platform === "win32" || process.platform === "darwin") { // Windows and Mac.
const asarPath = path.join(bdFolders[0], "data", "betterdiscord.asar");
await fs.writeFile(path.join(discordPath, "index.js"), `require("${asarPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}");\nmodule.exports = require("./core.asar");`);
}
else { // Linux.
await fs.writeFile(
path.join(discordPath, "index.js"),
"require('../../../../BetterDiscord/data/betterdiscord.asar');\nmodule.exports = require('./core.asar');"
);
}
log("✅ Injection successful.");
progress.set(progress.value + progressPerLoop);
}
catch (err) {
log(`❌ Could not inject shims to ${discordPath}`);
log(`❌ Could not inject loader for ${discordPath}.`);
log(`❌ ${err.message}`);
return err;
}
Expand All @@ -146,36 +160,74 @@ export default async function(config) {
const sane = doSanityCheck(config);
if (!sane) return fail();


// Installation channels (such as "stable", "canary") and their latest version's paths.
const channels = Object.keys(config);
const paths = Object.values(config);

// Latest BD paths. This is where we'll install BetterDiscord's ASAR package.
const bdFolders = [];
if (process.platform === "win32" || process.platform === "darwin") {
// Windows and Mac: BetterDiscord stored in a single, shared location.
bdFolders.push(path.join(remote.app.getPath("appData"), "BetterDiscord"));
}
else { // Linux.
// NOTE: On Linux, we install BetterDiscord into every Discord instance's
// individual xdg-config directory, to be able to support Flatpaks and Snaps,
// and to avoid conflicts if multiple sandboxed Discord versions run at the
// same time. Only the sandboxes have truly unique "per-installation" BD paths.
for (const discordPath of paths) {
// Jump out of the latest Discord version's subdirectory, to the Discord
// installation's main xdg-config directory, and install BetterDiscord there.
// Before: "/home/foo/.var/app/com.discordapp.Discord/config/discord/0.0.19/modules/discord_desktop_core".
// After: "/home/foo/.var/app/com.discordapp.Discord/config/BetterDiscord".
// NOTE: Installation paths on Linux always contain those 4 suffix parts.
// NOTE: The requirement that BetterDiscord is installed in xdg-config
// is hardcoded into betterdiscord.asar, so we can't change these paths.
bdFolders.push(path.join(discordPath, "../../../..", "BetterDiscord"));
}
}

if (bdFolders.length < 1) {
lognewline("No Discord paths provided.");
return fail();
}


// Create BetterDiscord's directory hierarchies.
lognewline("Creating required directories...");
const makeDirErr = await makeDirectories(bdFolder, bdDataFolder, bdThemesFolder, bdPluginsFolder);
if (makeDirErr) return fail();
log("✅ Directories created");
for (const bdFolder of bdFolders) {
const makeDirErr = await makeDirectories(
bdFolder,
path.join(bdFolder, "data"),
path.join(bdFolder, "plugins"),
path.join(bdFolder, "themes")
);
if (makeDirErr) return fail();
}
log("✅ Directories created.");
progress.set(MAKE_DIR_PROGRESS);


lognewline("Downloading asar file");
const downloadErr = await downloadAndInstallAsar();
const downloadErr = await downloadAndInstallAsar(bdFolders);
if (downloadErr) return fail();
log("✅ Package downloaded");
log("✅ Package downloaded.");
progress.set(DOWNLOAD_PACKAGE_PROGRESS);


lognewline("Injecting shims...");
// Add BetterDiscord to each Discord version's loader-script.
lognewline("Injecting into Discord...");
const injectErr = await injectShims(paths);
if (injectErr) return fail();
log("✅ Shims injected");
log("✅ Loader scripts injected.");
progress.set(INJECT_SHIM_PROGRESS);


// Automatically restart Discord clients if they are running.
lognewline("Restarting Discord...");
const killErr = await kill(channels, (RESTART_DISCORD_PROGRESS - progress.value) / channels.length);
if (killErr) showRestartNotice(); // No need to bail out and show failed
else log("✅ Discord restarted");
if (killErr) showRestartNotice(); // No need to bail out if we failed, just tell user to restart manually.
else log("✅ Discord restarted.");
progress.set(RESTART_DISCORD_PROGRESS);


Expand Down
Loading