From 42000fc2871e97b71e9cbd61a781d214e435b3c3 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Fri, 26 Mar 2021 23:34:27 -0400 Subject: [PATCH 1/5] Add first pass installer actions --- package.json | 3 +- src/renderer/actions/install.js | 213 ++++++++++++++-------- src/renderer/actions/repair.js | 230 ++++++++++++++++++++++++ src/renderer/actions/uninstall.js | 145 +++++++++++++++ src/renderer/common/TextDisplay.svelte | 2 +- src/renderer/pages/License.svelte | 1 + src/renderer/pages/PerformAction.svelte | 11 +- webpack.renderer.js | 2 +- yarn.lock | 209 ++++++++++++++++++--- 9 files changed, 710 insertions(+), 106 deletions(-) create mode 100644 src/renderer/actions/repair.js create mode 100644 src/renderer/actions/uninstall.js diff --git a/package.json b/package.json index dfdb0cef..a21a9323 100644 --- a/package.json +++ b/package.json @@ -13,16 +13,17 @@ "dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null" }, "dependencies": { - "bent": "^7.3.12", "source-map-support": "^0.5.16" }, "devDependencies": { + "del": "^6.0.0", "electron": "9.4.0", "electron-builder": "^22.4.1", "electron-webpack": "^2.8.2", "eslint": "^7.21.0", "eslint-plugin-svelte3": "^3.1.2", "find-process": "^1.4.4", + "phin": "^3.5.1", "svelte": "^3.35.0", "svelte-loader": "^3.0.0", "svelte-spa-router": "^3.1.0", diff --git a/src/renderer/actions/install.js b/src/renderer/actions/install.js index 788c833f..7de9484a 100644 --- a/src/renderer/actions/install.js +++ b/src/renderer/actions/install.js @@ -3,11 +3,26 @@ import {progress, status} from "../stores/installation"; import {remote, shell} from "electron"; import fs from "fs"; import path from "path"; -import bent from "bent"; +import phin from "phin"; import kill from "tree-kill"; import findProcess from "find-process"; -const downloadFile = bent("buffer"); +const discordURL = "https://discord.gg/0Tmfo5ZbORCRqbAd"; + +const RC_OK = 0; +const RC_ERROR = 1; + +const MAKE_DIR_PROGRESS = 30; +const DOWNLOAD_PACKAGE_PROGRESS = 60; // 60 +const INJECT_SHIM_PROGRESS = 90; // 90 +const RESTART_DISCORD_PROGRESS = 100; // 100 +const MAX_PROGRESS = 100; // MAKE_DIR_PROGRESS + DOWNLOAD_PACKAGE_PROGRESS + INJECT_SHIM_PROGRESS + RESTART_DISCORD_PROGRESS; + +let progressCache = 0; +function setProgress(value) { + progressCache = value; + progress.set(value); +} function log(entry) { logs.update(a => { @@ -16,15 +31,7 @@ function log(entry) { }); } -let progressCache = 0; - -function addProgress(val) { - progressCache += val; - progress.set(progressCache); -} - -function failInstallation() { - const discordURL = "https://discord.gg/0Tmfo5ZbORCRqbAd"; +function fail() { log(""); log(`The installation seems to have failed. If this problem is recurring, join our discord community for support. ${discordURL}`); status.set("error"); @@ -35,85 +42,53 @@ const bdDataFolder = path.join(bdFolder, "data"); const bdPluginsFolder = path.join(bdFolder, "plugins"); const bdThemesFolder = path.join(bdFolder, "themes"); -async function makeDirectories() { - const folders = [bdFolder, bdDataFolder, bdThemesFolder, bdPluginsFolder]; +async function makeDirectories(...folders) { + const progressPerLoop = (MAKE_DIR_PROGRESS - progressCache) / folders.length; for (const folder of folders) { if (fs.existsSync(folder)) { log(`✅ Directory exists: ${folder}`); + setProgress(progressCache + progressPerLoop); continue; } try { fs.mkdirSync(folder); + setProgress(progressCache + progressPerLoop); log(`✅ Directory created: ${folder}`); } - catch { + catch (err) { log(`❌ Failed to create directory: ${folder}`); - failInstallation(); - return; + log(`❌ ${err.message}`); + return RC_ERROR; } } - progress.set(25); + return RC_OK; } -const downloadUrl = `https://bd.zerebos.com/betterdiscord.asar`; +const getJSON = phin.defaults({method: "GET", parse: "json", headers: {"User-Agent": "BetterDiscord Installer"}}); +const downloadFile = phin.defaults({method: "GET", followRedirects: true, headers: {"User-Agent": "BetterDiscord Installer", "Accept": "application/octet-stream"}}); const asarPath = path.join(bdDataFolder, "betterdiscord.asar"); async function downloadAsar() { - const buffer = await downloadFile(downloadUrl); - const originalFs = require("original-fs"); // because electron doesn't like when I write asar files - originalFs.writeFileSync(asarPath, buffer); -} - -async function restartDiscord() { - const results = await findProcess("name", "Discord", true); - if (!results || !results.length) return; - const parentPids = results.map(p => p.ppid); - const discordPid = results.find(p => parentPids.includes(p.pid)); - const bin = discordPid.bin; - kill(discordPid.pid); - shell.openExternal(bin); -} - - -export default async function(discordPaths) { - progress.set(0); - - log("Starting installation..."); - log(""); - log("Locating Discord paths..."); - - if (!discordPaths || !discordPaths.length) { - log("❌ Failed to locate required directories."); - failInstallation(); - return; - } - + let downloadUrl = "https://api.github.com/repos/rauenzi/BetterDiscordApp/releases"; try { - await makeDirectories(); + const response = await getJSON(downloadUrl); + const releases = response.body; + const asset = releases[0].assets.find(a => a.name === "betterdiscord.asar"); + downloadUrl = asset.url; + + const resp = await downloadFile(downloadUrl); + const originalFs = require("original-fs"); // because electron doesn't like when I write asar files + originalFs.writeFileSync(asarPath, resp.body); } catch (err) { - log(`❌ Failed to create directories - ${err.message}`); - failInstallation(); - return; - } - - - log(""); - log(`Downloading asar file from: ${downloadUrl}`); - - try { - await downloadAsar(); - log("✅ Package downloaded"); - progress.set(50); - } - catch (err) { - log(`❌ Failed to download package - ${err.message}`); - failInstallation(); - return; + log(`❌ Failed to download package ${downloadUrl}`); + log(`❌ ${err.message}`); + return RC_ERROR; } +} - log(""); - log("Injecting shims..."); - for (const discordPath of discordPaths) { +function injectShims(paths) { + const progressPerLoop = (INJECT_SHIM_PROGRESS - progressCache) / paths.length; + for (const discordPath of paths) { log("Injecting into: " + discordPath); const appPath = path.join(discordPath, "app"); const pkgFile = path.join(appPath, "package.json"); @@ -123,16 +98,108 @@ export default async function(discordPaths) { fs.writeFileSync(pkgFile, JSON.stringify({name: "betterdiscord", main: "index.js"})); fs.writeFileSync(indexFile, `require("${asarPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}");`); log("✅ Injection successful"); + setProgress(progressCache + progressPerLoop); } catch (err) { - log(`❌ Injection Error - ${err.message}`); - failInstallation(); - return; + log(`❌ Could not inject shims to ${discordPath}`); + log(`❌ ${err.message}`); + return RC_ERROR; } } +} + +const platforms = {stable: "Discord", ptb: "Discord PTB", canary: "Discord Canary"}; +async function restartProcesses(channels) { + const progressPerLoop = (RESTART_DISCORD_PROGRESS - progressCache) / channels.length; + for (const channel of channels) { + let processName = platforms[channel]; + if (process.platform === "win32") processName = platforms[channel].replace(" ", ""); + else if (process.platform === "darwin") processName = `${platforms[channel]}.app`; + else processName = platforms[channel].toLowerCase().replace(" ", "-"); + + try { + const results = await findProcess("name", processName, true); + if (!results || !results.length) { + log(`❌ ${processName} not running`); + continue; + } + + const parentPids = results.map(p => p.ppid); + const discordPid = results.find(p => parentPids.includes(p.pid)); + const bin = discordPid.bin; + kill(discordPid.pid); + shell.openExternal(bin); + setProgress(progressCache + progressPerLoop); + } + catch (err) { + log(`❌ Could not restart ${platforms[channel]}`); + log(`❌ ${err.message}`); + return RC_ERROR; + } + } +} + +function showRestartNotice() { + remote.dialog.showMessageBox({ + type: "info", + title: "Restart Discord", + message: "BetterDiscord could not restart Discord. Please restart it manually." + }); +} + + +export default async function(config) { + const channels = Object.keys(config); + const paths = Object.values(config); + + await new Promise(r => setTimeout(r, 500)); + + let rc = RC_OK; + setProgress(0); + + log("Starting installation..."); + + log(""); + log("Locating Discord paths..."); + if (!paths || !paths.length) { + log("❌ Something went wrong internally."); + return fail(); + } + + log(""); + log("Creating required directories..."); + rc = await makeDirectories(bdFolder, bdDataFolder, bdThemesFolder, bdPluginsFolder); + if (rc) return fail(); + log("✅ Directories created"); + setProgress(MAKE_DIR_PROGRESS); + + + log(""); + log("Downloading asar file"); + rc = await downloadAsar(); + if (rc) return fail(); + log("✅ Package downloaded"); + setProgress(DOWNLOAD_PACKAGE_PROGRESS); + + + log(""); + log("Injecting shims..."); + rc = injectShims(paths); + if (rc) return fail(); + log("✅ Shims injected"); + setProgress(INJECT_SHIM_PROGRESS); + + + log(""); + log("Restarting Discord..."); + rc = await restartProcesses(channels); + // if (rc) return fail(); // No need to bail out + if (rc) showRestartNotice(); + else log("✅ Discord restarted"); + setProgress(RESTART_DISCORD_PROGRESS); log("Installation completed!"); - progress.set(100); + setProgress(MAX_PROGRESS); status.set("success"); }; \ No newline at end of file diff --git a/src/renderer/actions/repair.js b/src/renderer/actions/repair.js new file mode 100644 index 00000000..2b90d204 --- /dev/null +++ b/src/renderer/actions/repair.js @@ -0,0 +1,230 @@ +import logs from "../stores/logs"; +import {progress, status, action} from "../stores/installation"; +import {remote, shell} from "electron"; +import fs from "fs"; +const fsp = fs.promises; +import del from "del"; +import path from "path"; +import kill from "tree-kill"; +import findProcess from "find-process"; +import {replace} from "svelte-spa-router"; + +const discordURL = "https://discord.gg/0Tmfo5ZbORCRqbAd"; + +const RC_OK = 0; +const RC_ERROR = 1; + +const KILL_DISCORD_PROGRESS = 20; +const DELETE_APP_DIRS_PROGRESS = 50; +const DELETE_MODULE_DIRS_PROGRESS = 100; +const START_DISCORD_PROGRESS = 100; +const MAX_PROGRESS = 100; + +let progressCache = 0; +function setProgress(value) { + progressCache = value; + progress.set(value); +} + +function log(entry) { + logs.update(a => { + a.push(entry); + return a; + }); +} + +function fail() { + log(""); + log(`The repair seems to have failed. If this problem is recurring, join our discord community for support. ${discordURL}`); + status.set("error"); +} + +async function exists(file) { + try { + await fsp.stat(file); + return true; + } + catch { + return false; + } +} + +async function deleteAppDirs(paths) { + const progressPerLoop = (DELETE_APP_DIRS_PROGRESS - progressCache) / paths.length; + for (const discordPath of paths) { + log("Removing " + discordPath); + const appPath = path.join(discordPath, "app"); + try { + if (await exists(appPath)) await del(appPath, {force: true}); + log("✅ Deletion successful"); + setProgress(progressCache + progressPerLoop); + } + catch (err) { + log(`❌ Could not delete folder ${appPath}`); + log(`❌ ${err.message}`); + return RC_ERROR; + } + } +} + +async function deleteModuleDirs(config) { + const size = Object.keys(config).length; + const progressPerLoop = (DELETE_MODULE_DIRS_PROGRESS - progressCache) / size; + for (const channel in config) { + const roaming = path.join(remote.app.getPath("userData"), "..", platforms[channel].replace(" ", "").toLowerCase()); + try { + const versionDir = fs.readdirSync(roaming).find(d => d.split(".").length > 2); + if (await exists(path.join(versionDir, "modules"))) await del(versionDir, {force: true}); + log("✅ Deletion successful"); + setProgress(progressCache + progressPerLoop); + } + catch (err) { + log(`❌ Could not delete modules in ${roaming}`); + log(`❌ ${err.message}`); + return RC_ERROR; + } + } +} + +const executables = {stable: "", ptb: "", canary: ""}; +const platforms = {stable: "Discord", ptb: "Discord PTB", canary: "Discord Canary"}; +async function killProcesses(channels) { + const progressPerLoop = (KILL_DISCORD_PROGRESS - progressCache) / channels.length; + for (const channel of channels) { + let processName = platforms[channel]; + if (process.platform === "win32") processName = platforms[channel].replace(" ", ""); + else if (process.platform === "darwin") processName = `${platforms[channel]}.app`; + else processName = platforms[channel].toLowerCase().replace(" ", "-"); + + try { + const results = await findProcess("name", processName, true); + if (!results || !results.length) { + log(`✅ ${processName} not running`); + setProgress(progressCache + progressPerLoop); + return RC_OK; + } + + const parentPids = results.map(p => p.ppid); + const discordPid = results.find(p => parentPids.includes(p.pid)); + const bin = discordPid.bin; + kill(discordPid.pid); + executables[channel] = bin; // shell.openExternal(bin); + setProgress(progressCache + progressPerLoop); + } + catch (err) { + log(`❌ Could not kill ${platforms[channel]}`); + log(`❌ ${err.message}`); + return RC_ERROR; + } + } +} + +// function startProcesses() { +// const progressPerLoop = (START_DISCORD_PROGRESS - progressCache) / executables.length; +// for (const channel in executables) { +// const exe = executables[channel]; +// try { +// shell.openExternal(exe); +// setProgress(progressCache + progressPerLoop); +// } +// catch (err) { +// log(`❌ Could not start ${platforms[channel]} (${exe})`); +// log(`❌ ${err.message}`); +// return RC_ERROR; +// } +// } +// } + +function showKillNotice() { + remote.dialog.showMessageBox({ + type: "info", + title: "Shutdown Discord", + message: "BetterDiscord could not shutdown Discord. Please make sure Discord is shut down, then run the installer again." + }); +} + +// function showRestartNotice() { +// remote.dialog.showMessageBox({ +// type: "info", +// title: "Restart Discord", +// message: "BetterDiscord could not restart Discord. Please restart it manually." +// }); +// } + +async function showInstallNotice() { + const confirmation = await remote.dialog.showMessageBox(remote.BrowserWindow.getFocusedWindow(), { + type: "info", + title: "Reinstall BetterDiscord", + message: "After repairing, you need to reinstall BetterDiscord. Do you want to reinstall now?", + noLink: true, + cancelId: 1, + buttons: ["Yes", "No"] + }); + + if (confirmation.response === 0) { + action.set("install"); + replace("/actions"); + } +} + + +export default async function(config) { + const channels = Object.keys(config); + const paths = Object.values(config); + + await new Promise(r => setTimeout(r, 500)); + + let rc = RC_OK; + setProgress(0); + + log("Starting Repair..."); + + log(""); + if (!paths || !paths.length) { + log("❌ Something went wrong internally."); + return fail(); + } + + log(""); + log("Killing Discord..."); + rc = await killProcesses(channels); + if (rc) { + showKillNotice(); + return fail(); + } + log("✅ Discord Killed"); + setProgress(KILL_DISCORD_PROGRESS); + + + log(""); + log("Deleting shims..."); + rc = await deleteAppDirs(paths); + if (rc) return fail(); + log("✅ Shims deleted"); + setProgress(DELETE_APP_DIRS_PROGRESS); + + + log(""); + log("Deleting discord modules..."); + rc = await deleteModuleDirs(config); + if (rc) return fail(); + log("✅ Shims deleted"); + setProgress(DELETE_MODULE_DIRS_PROGRESS); + + + // log(""); + // log("Killing Discord..."); + // rc = startProcesses(); + // // if (rc) return fail(); // No need to bail out + // if (rc) showRestartNotice(); + // else log("✅ Discord restarted"); + // log("✅ Shims injected"); + // setProgress(START_DISCORD_PROGRESS); + + + log("Repair completed!"); + setProgress(MAX_PROGRESS); + status.set("success"); + + showInstallNotice(); +}; \ No newline at end of file diff --git a/src/renderer/actions/uninstall.js b/src/renderer/actions/uninstall.js new file mode 100644 index 00000000..b9d4e579 --- /dev/null +++ b/src/renderer/actions/uninstall.js @@ -0,0 +1,145 @@ +import logs from "../stores/logs"; +import {progress, status} from "../stores/installation"; +import {remote, shell} from "electron"; +import fs from "fs"; +import del from "del"; +const fsp = fs.promises; +import path from "path"; +import kill from "tree-kill"; +import findProcess from "find-process"; + +const discordURL = "https://discord.gg/0Tmfo5ZbORCRqbAd"; + +const RC_OK = 0; +const RC_ERROR = 1; + +const DELETE_SHIM_PROGRESS = 85; +const RESTART_DISCORD_PROGRESS = 15; +const MAX_PROGRESS = 100; + +let progressCache = 0; +function setProgress(value) { + progressCache = value; + progress.set(value); +} + +function log(entry) { + logs.update(a => { + a.push(entry); + return a; + }); +} + +function fail() { + log(""); + log(`The uninstall seems to have failed. If this problem is recurring, join our discord community for support. ${discordURL}`); + status.set("error"); +} + +async function exists(file) { + try { + await fsp.stat(file); + return true; + } + catch { + return false; + } +} + +async function deleteShims(paths) { + const progressPerLoop = (DELETE_SHIM_PROGRESS - progressCache) / paths.length; + for (const discordPath of paths) { + log("Removing " + discordPath); + const appPath = path.join(discordPath, "app"); + try { + if (await exists(appPath)) await del(appPath, {force: true}); + log("✅ Deletion successful"); + setProgress(progressCache + progressPerLoop); + } + catch (err) { + log(`❌ Could not delete folder ${appPath}`); + log(`❌ ${err.message}`); + return RC_ERROR; + } + } +} + +const platforms = {stable: "Discord", ptb: "Discord PTB", canary: "Discord Canary"}; +async function restartProcesses(channels) { + const progressPerLoop = (RESTART_DISCORD_PROGRESS - progressCache) / channels.length; + for (const channel of channels) { + let processName = platforms[channel]; + if (process.platform === "win32") processName = platforms[channel].replace(" ", ""); + else if (process.platform === "darwin") processName = `${platforms[channel]}.app`; + else processName = platforms[channel].toLowerCase().replace(" ", "-"); + + try { + const results = await findProcess("name", processName, true); + if (!results || !results.length) { + log(`❌ Could not find process ${processName}`); + return RC_ERROR; + } + + const parentPids = results.map(p => p.ppid); + const discordPid = results.find(p => parentPids.includes(p.pid)); + const bin = discordPid.bin; + kill(discordPid.pid); + shell.openExternal(bin); + setProgress(progressCache + progressPerLoop); + } + catch (err) { + log(`❌ Could not restart ${platforms[channel]}`); + log(`❌ ${err.message}`); + return RC_ERROR; + } + } +} + +function showRestartNotice() { + remote.dialog.showMessageBox({ + type: "info", + title: "Restart Discord", + message: "BetterDiscord could not restart Discord. Please restart it manually." + }); +} + + +export default async function(config) { + const channels = Object.keys(config); + const paths = Object.values(config); + + await new Promise(r => setTimeout(r, 500)); + + let rc = RC_OK; + setProgress(0); + + log("Starting uninstall..."); + + log(""); + if (!paths || !paths.length) { + log("❌ Something went wrong internally."); + return fail(); + } + + + log(""); + log("Deleting shims..."); + rc = await deleteShims(paths); + if (rc) return fail(); + log("✅ Shims deleted"); + setProgress(DELETE_SHIM_PROGRESS); + + + log(""); + log("Killing Discord..."); + rc = await restartProcesses(channels); + // if (rc) return fail(); // No need to bail out + if (rc) showRestartNotice(); + else log("✅ Discord restarted"); + setProgress(RESTART_DISCORD_PROGRESS); + + + log("Uninstall completed!"); + setProgress(MAX_PROGRESS); + status.set("success"); +}; \ No newline at end of file diff --git a/src/renderer/common/TextDisplay.svelte b/src/renderer/common/TextDisplay.svelte index 16b65d72..d8f80194 100644 --- a/src/renderer/common/TextDisplay.svelte +++ b/src/renderer/common/TextDisplay.svelte @@ -40,7 +40,7 @@ try { setImmediate(() => scroller.scrollTop = scroller.scrollHeight); } - catch(e) {} + catch (e) {} } diff --git a/src/renderer/pages/License.svelte b/src/renderer/pages/License.svelte index 6f67551f..5b9bb318 100644 --- a/src/renderer/pages/License.svelte +++ b/src/renderer/pages/License.svelte @@ -38,6 +38,7 @@
+ diff --git a/src/renderer/pages/PerformAction.svelte b/src/renderer/pages/PerformAction.svelte index bd83d2fe..8ce9abb7 100644 --- a/src/renderer/pages/PerformAction.svelte +++ b/src/renderer/pages/PerformAction.svelte @@ -5,6 +5,8 @@ import TextDisplay from "../common/TextDisplay.svelte"; import logs from "../stores/logs"; import install from "../actions/install"; + import repair from "../actions/repair"; + import uninstall from "../actions/uninstall"; import debug from "../actions/debug"; import {canGoBack, canGoForward, nextPage} from "../stores/navigation"; import {action, paths, progress, platforms, status} from "../stores/installation"; @@ -32,17 +34,17 @@ // Run action scripts if (currentAction == "install") { - install(Object.values(installPaths)).then(() => { + pageIcon = ``; + install(installPaths).then(() => { nextPage.set(null); canGoForward.set(true); canGoBack.set(true); }); - pageIcon = ``; } else if (currentAction == "repair") { pageIcon = ``; // TODO: Finish repair script - debug(Object.values(installPaths)).then(() => { + repair(installPaths).then(() => { nextPage.set(null); canGoForward.set(true); canGoBack.set(true); @@ -50,8 +52,7 @@ } else if (currentAction == "uninstall") { pageIcon = ``; - // TODO: Finish uninstall script - debug(Object.values(installPaths)).then(() => { + uninstall(installPaths).then(() => { nextPage.set(null); canGoForward.set(true); canGoBack.set(true); diff --git a/webpack.renderer.js b/webpack.renderer.js index f321424b..c0497e1a 100644 --- a/webpack.renderer.js +++ b/webpack.renderer.js @@ -3,7 +3,7 @@ module.exports = { rules: [ { test: /\.(html|svelte)$/, - use: 'svelte-loader' + use: "svelte-loader" } ] } diff --git a/yarn.lock b/yarn.lock index dd9d4b01..e15afaaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -829,6 +829,27 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@nodelib/fs.scandir@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" + integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== + dependencies: + "@nodelib/fs.stat" "2.0.4" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" + integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== + dependencies: + "@nodelib/fs.scandir" "2.1.4" + fastq "^1.6.0" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -1320,6 +1341,11 @@ array-union@^1.0.1: dependencies: array-uniq "^1.0.1" +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -1442,15 +1468,6 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= -bent@^7.3.12: - version "7.3.12" - resolved "https://registry.yarnpkg.com/bent/-/bent-7.3.12.tgz#e0a2775d4425e7674c64b78b242af4f49da6b035" - integrity sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w== - dependencies: - bytesish "^0.4.1" - caseless "~0.12.0" - is-stream "^2.0.0" - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -1561,6 +1578,13 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -1707,11 +1731,6 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -bytesish@^0.4.1: - version "0.4.4" - resolved "https://registry.yarnpkg.com/bytesish/-/bytesish-0.4.4.tgz#f3b535a0f1153747427aee27256748cff92347e6" - integrity sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ== - cacache@^12.0.2: version "12.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" @@ -1808,10 +1827,10 @@ caniuse-lite@^1.0.30001038: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001039.tgz#b3814a1c38ffeb23567f8323500c09526a577bbe" integrity sha512-SezbWCTT34eyFoWHgx8UWso7YtvtM7oosmFoXbCkdC6qJzRfBTeTgE9REtKtiuKXuMwWTZEvdnFNGAyVMorv8Q== -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +centra@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/centra/-/centra-2.4.2.tgz#3bad51dbca2250dbecad84598dc7184973f4c4a9" + integrity sha512-f1RaP0V1HqVNEXfLfjNBthB2yy3KnSGnPCnOPCFLUk9e/Z4rNJ8nBaJNnghflnp88mi1IT8mfmW+HlMS1/H+bg== chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" @@ -2403,6 +2422,20 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" +del@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" + integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== + dependencies: + globby "^11.0.1" + graceful-fs "^4.2.4" + is-glob "^4.0.1" + is-path-cwd "^2.2.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" + slash "^3.0.0" + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -2440,6 +2473,13 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + dmg-builder@22.4.1: version "22.4.1" resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-22.4.1.tgz#ab80d3d6e4ed8a1d38beddbfe97c8f7a794dd932" @@ -3120,6 +3160,18 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== +fast-glob@^3.1.1: + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -3130,6 +3182,13 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastq@^1.6.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + dependencies: + reusify "^1.0.4" + faye-websocket@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" @@ -3186,6 +3245,13 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -3415,7 +3481,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0: +glob-parent@^5.0.0, glob-parent@^5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -3519,6 +3585,18 @@ globalthis@^1.0.1: dependencies: define-properties "^1.1.3" +globby@^11.0.1: + version "11.0.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" + integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -3552,6 +3630,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== +graceful-fs@^4.2.4: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -3847,6 +3930,11 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -4109,12 +4197,17 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-obj@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-path-cwd@^2.0.0: +is-path-cwd@^2.0.0, is-path-cwd@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== @@ -4138,6 +4231,11 @@ is-path-inside@^3.0.1: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -4162,11 +4260,6 @@ is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= -is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - is-symbol@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" @@ -4590,6 +4683,11 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -4614,6 +4712,14 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -5135,6 +5241,13 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-retry@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" @@ -5281,6 +5394,11 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -5297,6 +5415,18 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= +phin@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/phin/-/phin-3.5.1.tgz#41ce07aa7eb23b1f7ded89fc00a464addddac972" + integrity sha512-jgFO28IaiWAl0xk+zmqVx7neKVokWKU8YTQC5QlB45SZnEE53LH2saqJIcyIV557VX3Gk+TdR4rwWTc3P83DSA== + dependencies: + centra "^2.4.2" + +picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -5568,6 +5698,11 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -5864,6 +5999,11 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -5898,6 +6038,13 @@ roarr@^2.15.2: semver-compare "^1.0.0" sprintf-js "^1.1.2" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -6128,6 +6275,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -6684,6 +6836,13 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" From 0b29026ce0f76a4c2ff8eb77a8771713b81409d1 Mon Sep 17 00:00:00 2001 From: Tropical <42101043+Tropix126@users.noreply.github.com> Date: Fri, 26 Mar 2021 23:10:35 -0500 Subject: [PATCH 2/5] Make improvements to action scripts. - Move action success into it's own function. - Better integrate installation script into repair. - improvements to dialogs - run install script directly through installation panel - fix two newlines being created --- src/renderer/actions/install.js | 11 +++++--- src/renderer/actions/repair.js | 45 +++++++++++++++++++++---------- src/renderer/actions/uninstall.js | 15 ++++++----- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/renderer/actions/install.js b/src/renderer/actions/install.js index 7de9484a..f268a7cc 100644 --- a/src/renderer/actions/install.js +++ b/src/renderer/actions/install.js @@ -37,6 +37,13 @@ function fail() { status.set("error"); } +function succeed() { + log(""); + log("Installation completed!"); + setProgress(MAX_PROGRESS); + status.set("success"); +} + const bdFolder = path.join(remote.app.getPath("appData"), "BetterDiscord"); const bdDataFolder = path.join(bdFolder, "data"); const bdPluginsFolder = path.join(bdFolder, "plugins"); @@ -199,7 +206,5 @@ export default async function(config) { setProgress(RESTART_DISCORD_PROGRESS); - log("Installation completed!"); - setProgress(MAX_PROGRESS); - status.set("success"); + succeed(); }; \ No newline at end of file diff --git a/src/renderer/actions/repair.js b/src/renderer/actions/repair.js index 2b90d204..163dfb73 100644 --- a/src/renderer/actions/repair.js +++ b/src/renderer/actions/repair.js @@ -7,7 +7,9 @@ import del from "del"; import path from "path"; import kill from "tree-kill"; import findProcess from "find-process"; -import {replace} from "svelte-spa-router"; +import install from "./install.js"; + +let installPaths = {}; // Temporary installation config for setting up instalaltion script const discordURL = "https://discord.gg/0Tmfo5ZbORCRqbAd"; @@ -39,6 +41,13 @@ function fail() { status.set("error"); } +function succeed() { + log(""); + log("Repair completed!"); + setProgress(MAX_PROGRESS); + status.set("success"); +} + async function exists(file) { try { await fsp.stat(file); @@ -137,9 +146,9 @@ async function killProcesses(channels) { function showKillNotice() { remote.dialog.showMessageBox({ - type: "info", + type: "error", title: "Shutdown Discord", - message: "BetterDiscord could not shutdown Discord. Please make sure Discord is shut down, then run the installer again." + message: "BetterDiscord could not shut down Discord. Please make sure Discord is fully closed, then run the installer again." }); } @@ -153,17 +162,28 @@ function showKillNotice() { async function showInstallNotice() { const confirmation = await remote.dialog.showMessageBox(remote.BrowserWindow.getFocusedWindow(), { - type: "info", - title: "Reinstall BetterDiscord", - message: "After repairing, you need to reinstall BetterDiscord. Do you want to reinstall now?", + type: "question", + title: "Reinstall BetterDiscord?", + message: "After repairing, you need to reinstall BetterDiscord. Would you like to do that now?", noLink: true, cancelId: 1, buttons: ["Yes", "No"] }); if (confirmation.response === 0) { - action.set("install"); - replace("/actions"); + status.set(""); + setProgress(0); + log(""); + install(installPaths).then(() => { + remote.dialog.showMessageBox({ + type: "info", + title: "Reinstall Complete", + message: "Please relaunch discord manually to finish the repair." + }); + succeed(); + }); + } else { + succeed(); } } @@ -179,8 +199,8 @@ export default async function(config) { log("Starting Repair..."); - log(""); if (!paths || !paths.length) { + log(""); log("❌ Something went wrong internally."); return fail(); } @@ -221,10 +241,7 @@ export default async function(config) { // log("✅ Shims injected"); // setProgress(START_DISCORD_PROGRESS); - - log("Repair completed!"); - setProgress(MAX_PROGRESS); - status.set("success"); - showInstallNotice(); + + installPaths = config; // Pass our config to the installation script if user decides to reinstall }; \ No newline at end of file diff --git a/src/renderer/actions/uninstall.js b/src/renderer/actions/uninstall.js index b9d4e579..857a49ad 100644 --- a/src/renderer/actions/uninstall.js +++ b/src/renderer/actions/uninstall.js @@ -36,6 +36,13 @@ function fail() { status.set("error"); } +function succeed() { + log(""); + log("Uninstall completed!"); + setProgress(MAX_PROGRESS); + status.set("success"); +} + async function exists(file) { try { await fsp.stat(file); @@ -115,13 +122,12 @@ export default async function(config) { log("Starting uninstall..."); - log(""); if (!paths || !paths.length) { + log(""); log("❌ Something went wrong internally."); return fail(); } - log(""); log("Deleting shims..."); rc = await deleteShims(paths); @@ -138,8 +144,5 @@ export default async function(config) { else log("✅ Discord restarted"); setProgress(RESTART_DISCORD_PROGRESS); - - log("Uninstall completed!"); - setProgress(MAX_PROGRESS); - status.set("success"); + succeed(); }; \ No newline at end of file From 13b0096f148117cfd7fda0ee96d5edea27902474 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Sun, 28 Mar 2021 05:05:11 -0400 Subject: [PATCH 3/5] Clean up install functions and prepare for prod - Clean up logging in install functions - Abstract out common routine - Remove boilerplate - Prepare for production by introducing getStatic --- .eslintrc | 3 +- src/renderer/App.svelte | 2 +- src/renderer/actions/install.js | 174 +++++----------- src/renderer/actions/repair.js | 231 +++++----------------- src/renderer/actions/uninstall.js | 137 +++---------- src/renderer/actions/utils/exists.js | 11 ++ src/renderer/actions/utils/fail.js | 10 + src/renderer/actions/utils/kill.js | 37 ++++ src/renderer/actions/utils/log.js | 16 ++ src/renderer/actions/utils/notices.js | 17 ++ src/renderer/actions/utils/reset.js | 9 + src/renderer/actions/utils/sanity.js | 14 ++ src/renderer/actions/utils/succeed.js | 10 + src/renderer/getstatic.js | 11 ++ src/renderer/index.js | 7 +- src/renderer/pages/Platforms.svelte | 3 +- src/renderer/stores/installation.js | 6 +- src/renderer/stores/types/readwritable.js | 22 +++ 18 files changed, 297 insertions(+), 423 deletions(-) create mode 100644 src/renderer/actions/utils/exists.js create mode 100644 src/renderer/actions/utils/fail.js create mode 100644 src/renderer/actions/utils/kill.js create mode 100644 src/renderer/actions/utils/log.js create mode 100644 src/renderer/actions/utils/notices.js create mode 100644 src/renderer/actions/utils/reset.js create mode 100644 src/renderer/actions/utils/sanity.js create mode 100644 src/renderer/actions/utils/succeed.js create mode 100644 src/renderer/getstatic.js create mode 100644 src/renderer/stores/types/readwritable.js diff --git a/.eslintrc b/.eslintrc index f48be512..c793582c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -100,6 +100,7 @@ "DiscordNative": "readonly", "__non_webpack_require__": "readonly", "Symbol": "readonly", - "__static": "readonly" + "__static": "readonly", + "status": "off" } } \ No newline at end of file diff --git a/src/renderer/App.svelte b/src/renderer/App.svelte index ef18deda..5bd1dc00 100644 --- a/src/renderer/App.svelte +++ b/src/renderer/App.svelte @@ -121,7 +121,7 @@ left: 0; width: 100%; height: 100%; - background-image: url('images/background.png'); + background-image: var(--background); background-size: 60px; background-repeat: repeat; background-position: center; diff --git a/src/renderer/actions/install.js b/src/renderer/actions/install.js index f268a7cc..79bb2338 100644 --- a/src/renderer/actions/install.js +++ b/src/renderer/actions/install.js @@ -1,48 +1,23 @@ import logs from "../stores/logs"; import {progress, status} from "../stores/installation"; import {remote, shell} from "electron"; -import fs from "fs"; +import {promises as fs} from "fs"; import path from "path"; import phin from "phin"; -import kill from "tree-kill"; -import findProcess from "find-process"; -const discordURL = "https://discord.gg/0Tmfo5ZbORCRqbAd"; - -const RC_OK = 0; -const RC_ERROR = 1; +import {log, lognewline} from "./utils/log"; +import succeed from "./utils/succeed"; +import fail from "./utils/fail"; +import exists from "./utils/exists"; +import reset from "./utils/reset"; +import kill from "./utils/kill"; +import {showRestartNotice} from "./utils/notices"; +import doSanityCheck from "./utils/sanity"; const MAKE_DIR_PROGRESS = 30; -const DOWNLOAD_PACKAGE_PROGRESS = 60; // 60 -const INJECT_SHIM_PROGRESS = 90; // 90 -const RESTART_DISCORD_PROGRESS = 100; // 100 -const MAX_PROGRESS = 100; // MAKE_DIR_PROGRESS + DOWNLOAD_PACKAGE_PROGRESS + INJECT_SHIM_PROGRESS + RESTART_DISCORD_PROGRESS; - -let progressCache = 0; -function setProgress(value) { - progressCache = value; - progress.set(value); -} - -function log(entry) { - logs.update(a => { - a.push(entry); - return a; - }); -} - -function fail() { - log(""); - log(`The installation seems to have failed. If this problem is recurring, join our discord community for support. ${discordURL}`); - status.set("error"); -} - -function succeed() { - log(""); - log("Installation completed!"); - setProgress(MAX_PROGRESS); - status.set("success"); -} +const DOWNLOAD_PACKAGE_PROGRESS = 60; +const INJECT_SHIM_PROGRESS = 90; +const RESTART_DISCORD_PROGRESS = 100; const bdFolder = path.join(remote.app.getPath("appData"), "BetterDiscord"); const bdDataFolder = path.join(bdFolder, "data"); @@ -50,25 +25,24 @@ const bdPluginsFolder = path.join(bdFolder, "plugins"); const bdThemesFolder = path.join(bdFolder, "themes"); async function makeDirectories(...folders) { - const progressPerLoop = (MAKE_DIR_PROGRESS - progressCache) / folders.length; + const progressPerLoop = (MAKE_DIR_PROGRESS - progress.value) / folders.length; for (const folder of folders) { - if (fs.existsSync(folder)) { + if (await exists(folder)) { log(`✅ Directory exists: ${folder}`); - setProgress(progressCache + progressPerLoop); + progress.set(progress.value + progressPerLoop); continue; } try { - fs.mkdirSync(folder); - setProgress(progressCache + progressPerLoop); + await fs.mkdir(folder); + progress.set(progress.value + progressPerLoop); log(`✅ Directory created: ${folder}`); } catch (err) { log(`❌ Failed to create directory: ${folder}`); log(`❌ ${err.message}`); - return RC_ERROR; + return err; } } - return RC_OK; } const getJSON = phin.defaults({method: "GET", parse: "json", headers: {"User-Agent": "BetterDiscord Installer"}}); @@ -83,127 +57,75 @@ async function downloadAsar() { downloadUrl = asset.url; const resp = await downloadFile(downloadUrl); - const originalFs = require("original-fs"); // because electron doesn't like when I write asar files - originalFs.writeFileSync(asarPath, resp.body); + const originalFs = require("original-fs").promises; // because electron doesn't like when I write asar files + await originalFs.writeFile(asarPath, resp.body); } catch (err) { log(`❌ Failed to download package ${downloadUrl}`); log(`❌ ${err.message}`); - return RC_ERROR; + return err; } } -function injectShims(paths) { - const progressPerLoop = (INJECT_SHIM_PROGRESS - progressCache) / paths.length; +async function injectShims(paths) { + const progressPerLoop = (INJECT_SHIM_PROGRESS - progress.value) / paths.length; for (const discordPath of paths) { log("Injecting into: " + discordPath); const appPath = path.join(discordPath, "app"); const pkgFile = path.join(appPath, "package.json"); const indexFile = path.join(appPath, "index.js"); try { - if (!fs.existsSync(appPath)) fs.mkdirSync(appPath); - fs.writeFileSync(pkgFile, JSON.stringify({name: "betterdiscord", main: "index.js"})); - fs.writeFileSync(indexFile, `require("${asarPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}");`); + if (!(await exists(appPath))) await fs.mkdir(appPath); + await fs.writeFile(pkgFile, JSON.stringify({name: "betterdiscord", main: "index.js"})); + await fs.writeFile(indexFile, `require("${asarPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}");`); log("✅ Injection successful"); - setProgress(progressCache + progressPerLoop); + progress.set(progress.value + progressPerLoop); } catch (err) { log(`❌ Could not inject shims to ${discordPath}`); log(`❌ ${err.message}`); - return RC_ERROR; + return err; } } } -const platforms = {stable: "Discord", ptb: "Discord PTB", canary: "Discord Canary"}; -async function restartProcesses(channels) { - const progressPerLoop = (RESTART_DISCORD_PROGRESS - progressCache) / channels.length; - for (const channel of channels) { - let processName = platforms[channel]; - if (process.platform === "win32") processName = platforms[channel].replace(" ", ""); - else if (process.platform === "darwin") processName = `${platforms[channel]}.app`; - else processName = platforms[channel].toLowerCase().replace(" ", "-"); - try { - const results = await findProcess("name", processName, true); - if (!results || !results.length) { - log(`❌ ${processName} not running`); - continue; - } - - const parentPids = results.map(p => p.ppid); - const discordPid = results.find(p => parentPids.includes(p.pid)); - const bin = discordPid.bin; - kill(discordPid.pid); - shell.openExternal(bin); - setProgress(progressCache + progressPerLoop); - } - catch (err) { - log(`❌ Could not restart ${platforms[channel]}`); - log(`❌ ${err.message}`); - return RC_ERROR; - } - } -} - -function showRestartNotice() { - remote.dialog.showMessageBox({ - type: "info", - title: "Restart Discord", - message: "BetterDiscord could not restart Discord. Please restart it manually." - }); -} +export default async function(config) { + await reset(); + const sane = doSanityCheck(config); + if (!sane) return fail(); -export default async function(config) { const channels = Object.keys(config); const paths = Object.values(config); - await new Promise(r => setTimeout(r, 500)); - - let rc = RC_OK; - setProgress(0); - - log("Starting installation..."); - - log(""); - log("Locating Discord paths..."); - if (!paths || !paths.length) { - log("❌ Something went wrong internally."); - return fail(); - } - log(""); - log("Creating required directories..."); - rc = await makeDirectories(bdFolder, bdDataFolder, bdThemesFolder, bdPluginsFolder); - if (rc) return fail(); + lognewline("Creating required directories..."); + const makeDirErr = await makeDirectories(bdFolder, bdDataFolder, bdThemesFolder, bdPluginsFolder); + if (makeDirErr) return fail(); log("✅ Directories created"); - setProgress(MAKE_DIR_PROGRESS); + progress.set(MAKE_DIR_PROGRESS); - log(""); - log("Downloading asar file"); - rc = await downloadAsar(); - if (rc) return fail(); + lognewline("Downloading asar file"); + const downloadErr = await downloadAsar(); + if (downloadErr) return fail(); log("✅ Package downloaded"); - setProgress(DOWNLOAD_PACKAGE_PROGRESS); + progress.set(DOWNLOAD_PACKAGE_PROGRESS); - log(""); - log("Injecting shims..."); - rc = injectShims(paths); - if (rc) return fail(); + lognewline("Injecting shims..."); + const injectErr = await injectShims(paths); + if (injectErr) return fail(); log("✅ Shims injected"); - setProgress(INJECT_SHIM_PROGRESS); + progress.set(INJECT_SHIM_PROGRESS); - log(""); - log("Restarting Discord..."); - rc = await restartProcesses(channels); - // if (rc) return fail(); // No need to bail out - if (rc) showRestartNotice(); + 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"); - setProgress(RESTART_DISCORD_PROGRESS); + progress.set(RESTART_DISCORD_PROGRESS); succeed(); diff --git a/src/renderer/actions/repair.js b/src/renderer/actions/repair.js index 163dfb73..74b6471c 100644 --- a/src/renderer/actions/repair.js +++ b/src/renderer/actions/repair.js @@ -1,166 +1,62 @@ -import logs from "../stores/logs"; -import {progress, status, action} from "../stores/installation"; -import {remote, shell} from "electron"; -import fs from "fs"; -const fsp = fs.promises; + +import {progress, status} from "../stores/installation"; +import {remote} from "electron"; +import {promises as fs} from "fs"; import del from "del"; import path from "path"; -import kill from "tree-kill"; -import findProcess from "find-process"; import install from "./install.js"; - -let installPaths = {}; // Temporary installation config for setting up instalaltion script - -const discordURL = "https://discord.gg/0Tmfo5ZbORCRqbAd"; - -const RC_OK = 0; -const RC_ERROR = 1; +import {log, lognewline} from "./utils/log"; +import succeed from "./utils/succeed"; +import fail from "./utils/fail"; +import exists from "./utils/exists"; +import kill from "./utils/kill"; +import reset from "./utils/reset"; +import {showKillNotice} from "./utils/notices"; +import doSanityCheck from "./utils/sanity"; const KILL_DISCORD_PROGRESS = 20; const DELETE_APP_DIRS_PROGRESS = 50; const DELETE_MODULE_DIRS_PROGRESS = 100; -const START_DISCORD_PROGRESS = 100; -const MAX_PROGRESS = 100; - -let progressCache = 0; -function setProgress(value) { - progressCache = value; - progress.set(value); -} - -function log(entry) { - logs.update(a => { - a.push(entry); - return a; - }); -} - -function fail() { - log(""); - log(`The repair seems to have failed. If this problem is recurring, join our discord community for support. ${discordURL}`); - status.set("error"); -} - -function succeed() { - log(""); - log("Repair completed!"); - setProgress(MAX_PROGRESS); - status.set("success"); -} - -async function exists(file) { - try { - await fsp.stat(file); - return true; - } - catch { - return false; - } -} async function deleteAppDirs(paths) { - const progressPerLoop = (DELETE_APP_DIRS_PROGRESS - progressCache) / paths.length; + const progressPerLoop = (DELETE_APP_DIRS_PROGRESS - progress.value) / paths.length; for (const discordPath of paths) { log("Removing " + discordPath); const appPath = path.join(discordPath, "app"); try { if (await exists(appPath)) await del(appPath, {force: true}); log("✅ Deletion successful"); - setProgress(progressCache + progressPerLoop); + progress.set(progress.value + progressPerLoop); } catch (err) { - log(`❌ Could not delete folder ${appPath}`); + log(` Could not delete folder ${appPath}`); log(`❌ ${err.message}`); - return RC_ERROR; + return err; } } } +const platforms = {stable: "Discord", ptb: "Discord PTB", canary: "Discord Canary"}; async function deleteModuleDirs(config) { const size = Object.keys(config).length; - const progressPerLoop = (DELETE_MODULE_DIRS_PROGRESS - progressCache) / size; + const progressPerLoop = (DELETE_MODULE_DIRS_PROGRESS - progress.value) / size; for (const channel in config) { const roaming = path.join(remote.app.getPath("userData"), "..", platforms[channel].replace(" ", "").toLowerCase()); try { - const versionDir = fs.readdirSync(roaming).find(d => d.split(".").length > 2); + const versionDir = (await fs.readdir(roaming)).find(d => d.split(".").length > 2); if (await exists(path.join(versionDir, "modules"))) await del(versionDir, {force: true}); log("✅ Deletion successful"); - setProgress(progressCache + progressPerLoop); + progress.set(progress.value + progressPerLoop); } catch (err) { log(`❌ Could not delete modules in ${roaming}`); log(`❌ ${err.message}`); - return RC_ERROR; - } - } -} - -const executables = {stable: "", ptb: "", canary: ""}; -const platforms = {stable: "Discord", ptb: "Discord PTB", canary: "Discord Canary"}; -async function killProcesses(channels) { - const progressPerLoop = (KILL_DISCORD_PROGRESS - progressCache) / channels.length; - for (const channel of channels) { - let processName = platforms[channel]; - if (process.platform === "win32") processName = platforms[channel].replace(" ", ""); - else if (process.platform === "darwin") processName = `${platforms[channel]}.app`; - else processName = platforms[channel].toLowerCase().replace(" ", "-"); - - try { - const results = await findProcess("name", processName, true); - if (!results || !results.length) { - log(`✅ ${processName} not running`); - setProgress(progressCache + progressPerLoop); - return RC_OK; - } - - const parentPids = results.map(p => p.ppid); - const discordPid = results.find(p => parentPids.includes(p.pid)); - const bin = discordPid.bin; - kill(discordPid.pid); - executables[channel] = bin; // shell.openExternal(bin); - setProgress(progressCache + progressPerLoop); - } - catch (err) { - log(`❌ Could not kill ${platforms[channel]}`); - log(`❌ ${err.message}`); - return RC_ERROR; + return err; } } } -// function startProcesses() { -// const progressPerLoop = (START_DISCORD_PROGRESS - progressCache) / executables.length; -// for (const channel in executables) { -// const exe = executables[channel]; -// try { -// shell.openExternal(exe); -// setProgress(progressCache + progressPerLoop); -// } -// catch (err) { -// log(`❌ Could not start ${platforms[channel]} (${exe})`); -// log(`❌ ${err.message}`); -// return RC_ERROR; -// } -// } -// } - -function showKillNotice() { - remote.dialog.showMessageBox({ - type: "error", - title: "Shutdown Discord", - message: "BetterDiscord could not shut down Discord. Please make sure Discord is fully closed, then run the installer again." - }); -} - -// function showRestartNotice() { -// remote.dialog.showMessageBox({ -// type: "info", -// title: "Restart Discord", -// message: "BetterDiscord could not restart Discord. Please restart it manually." -// }); -// } - -async function showInstallNotice() { +async function showInstallNotice(config) { const confirmation = await remote.dialog.showMessageBox(remote.BrowserWindow.getFocusedWindow(), { type: "question", title: "Reinstall BetterDiscord?", @@ -170,78 +66,53 @@ async function showInstallNotice() { buttons: ["Yes", "No"] }); - if (confirmation.response === 0) { - status.set(""); - setProgress(0); - log(""); - install(installPaths).then(() => { - remote.dialog.showMessageBox({ - type: "info", - title: "Reinstall Complete", - message: "Please relaunch discord manually to finish the repair." - }); - succeed(); - }); - } else { - succeed(); - } + if (confirmation.response !== 0) return succeed(); + + await reset(); + await install(config); + remote.dialog.showMessageBox({ + type: "info", + title: "Reinstall Complete", + message: "Please relaunch discord manually to finish the repair." + }); } export default async function(config) { - const channels = Object.keys(config); - const paths = Object.values(config); - - await new Promise(r => setTimeout(r, 500)); + await reset(); + const sane = doSanityCheck(config); + if (!sane) return fail(); - let rc = RC_OK; - setProgress(0); - log("Starting Repair..."); + const channels = Object.keys(config); + const paths = Object.values(config); - if (!paths || !paths.length) { - log(""); - log("❌ Something went wrong internally."); - return fail(); - } - log(""); - log("Killing Discord..."); - rc = await killProcesses(channels); - if (rc) { + lognewline("Killing Discord..."); + const killErr = await kill(channels, (KILL_DISCORD_PROGRESS - progress.value) / channels.length, false); // await killProcesses(channels); + if (killErr) { showKillNotice(); return fail(); } log("✅ Discord Killed"); - setProgress(KILL_DISCORD_PROGRESS); + progress.set(KILL_DISCORD_PROGRESS); - log(""); - log("Deleting shims..."); - rc = await deleteAppDirs(paths); - if (rc) return fail(); + await new Promise(r => setTimeout(r, 200)); + lognewline("Deleting shims..."); + const deleteShimErr = await deleteAppDirs(paths); + if (deleteShimErr) return fail(); log("✅ Shims deleted"); - setProgress(DELETE_APP_DIRS_PROGRESS); + progress.set(DELETE_APP_DIRS_PROGRESS); - log(""); - log("Deleting discord modules..."); - rc = await deleteModuleDirs(config); - if (rc) return fail(); + await new Promise(r => setTimeout(r, 200)); + lognewline("Deleting discord modules..."); + const deleteModulesErr = await deleteModuleDirs(config); + if (deleteModulesErr) return fail(); log("✅ Shims deleted"); - setProgress(DELETE_MODULE_DIRS_PROGRESS); - - - // log(""); - // log("Killing Discord..."); - // rc = startProcesses(); - // // if (rc) return fail(); // No need to bail out - // if (rc) showRestartNotice(); - // else log("✅ Discord restarted"); - // log("✅ Shims injected"); - // setProgress(START_DISCORD_PROGRESS); + progress.set(DELETE_MODULE_DIRS_PROGRESS); - showInstallNotice(); - installPaths = config; // Pass our config to the installation script if user decides to reinstall + showInstallNotice(config); }; \ No newline at end of file diff --git a/src/renderer/actions/uninstall.js b/src/renderer/actions/uninstall.js index 857a49ad..33e672dc 100644 --- a/src/renderer/actions/uninstall.js +++ b/src/renderer/actions/uninstall.js @@ -1,148 +1,63 @@ -import logs from "../stores/logs"; -import {progress, status} from "../stores/installation"; -import {remote, shell} from "electron"; -import fs from "fs"; +import {progress} from "../stores/installation"; import del from "del"; -const fsp = fs.promises; import path from "path"; -import kill from "tree-kill"; -import findProcess from "find-process"; -const discordURL = "https://discord.gg/0Tmfo5ZbORCRqbAd"; -const RC_OK = 0; -const RC_ERROR = 1; +import {log, lognewline} from "./utils/log"; +import succeed from "./utils/succeed"; +import fail from "./utils/fail"; +import exists from "./utils/exists"; +import reset from "./utils/reset"; +import kill from "./utils/kill"; +import {showRestartNotice} from "./utils/notices"; +import doSanityCheck from "./utils/sanity"; -const DELETE_SHIM_PROGRESS = 85; -const RESTART_DISCORD_PROGRESS = 15; -const MAX_PROGRESS = 100; - -let progressCache = 0; -function setProgress(value) { - progressCache = value; - progress.set(value); -} - -function log(entry) { - logs.update(a => { - a.push(entry); - return a; - }); -} - -function fail() { - log(""); - log(`The uninstall seems to have failed. If this problem is recurring, join our discord community for support. ${discordURL}`); - status.set("error"); -} -function succeed() { - log(""); - log("Uninstall completed!"); - setProgress(MAX_PROGRESS); - status.set("success"); -} +const DELETE_SHIM_PROGRESS = 85; +const RESTART_DISCORD_PROGRESS = 100; -async function exists(file) { - try { - await fsp.stat(file); - return true; - } - catch { - return false; - } -} async function deleteShims(paths) { - const progressPerLoop = (DELETE_SHIM_PROGRESS - progressCache) / paths.length; + const progressPerLoop = (DELETE_SHIM_PROGRESS - progress.value) / paths.length; for (const discordPath of paths) { log("Removing " + discordPath); const appPath = path.join(discordPath, "app"); try { if (await exists(appPath)) await del(appPath, {force: true}); log("✅ Deletion successful"); - setProgress(progressCache + progressPerLoop); + progress.set(progress.value + progressPerLoop); } catch (err) { log(`❌ Could not delete folder ${appPath}`); log(`❌ ${err.message}`); - return RC_ERROR; + return err; } } } -const platforms = {stable: "Discord", ptb: "Discord PTB", canary: "Discord Canary"}; -async function restartProcesses(channels) { - const progressPerLoop = (RESTART_DISCORD_PROGRESS - progressCache) / channels.length; - for (const channel of channels) { - let processName = platforms[channel]; - if (process.platform === "win32") processName = platforms[channel].replace(" ", ""); - else if (process.platform === "darwin") processName = `${platforms[channel]}.app`; - else processName = platforms[channel].toLowerCase().replace(" ", "-"); - - try { - const results = await findProcess("name", processName, true); - if (!results || !results.length) { - log(`❌ Could not find process ${processName}`); - return RC_ERROR; - } - - const parentPids = results.map(p => p.ppid); - const discordPid = results.find(p => parentPids.includes(p.pid)); - const bin = discordPid.bin; - kill(discordPid.pid); - shell.openExternal(bin); - setProgress(progressCache + progressPerLoop); - } - catch (err) { - log(`❌ Could not restart ${platforms[channel]}`); - log(`❌ ${err.message}`); - return RC_ERROR; - } - } -} -function showRestartNotice() { - remote.dialog.showMessageBox({ - type: "info", - title: "Restart Discord", - message: "BetterDiscord could not restart Discord. Please restart it manually." - }); -} +export default async function(config) { + await reset(); + const sane = doSanityCheck(config); + if (!sane) return fail(); -export default async function(config) { const channels = Object.keys(config); const paths = Object.values(config); - await new Promise(r => setTimeout(r, 500)); - - let rc = RC_OK; - setProgress(0); - - log("Starting uninstall..."); - - if (!paths || !paths.length) { - log(""); - log("❌ Something went wrong internally."); - return fail(); - } - log(""); - log("Deleting shims..."); - rc = await deleteShims(paths); - if (rc) return fail(); + lognewline("Deleting shims..."); + const deleteErr = await deleteShims(paths); + if (deleteErr) return fail(); log("✅ Shims deleted"); - setProgress(DELETE_SHIM_PROGRESS); + progress.set(DELETE_SHIM_PROGRESS); - log(""); - log("Killing Discord..."); - rc = await restartProcesses(channels); - // if (rc) return fail(); // No need to bail out - if (rc) showRestartNotice(); + lognewline("Killing Discord..."); + const killErr = await kill(channels, (RESTART_DISCORD_PROGRESS - progress.value) / channels.length); + if (killErr) showRestartNotice(); // No need to bail out else log("✅ Discord restarted"); - setProgress(RESTART_DISCORD_PROGRESS); + progress.set(RESTART_DISCORD_PROGRESS); succeed(); }; \ No newline at end of file diff --git a/src/renderer/actions/utils/exists.js b/src/renderer/actions/utils/exists.js new file mode 100644 index 00000000..f37c244e --- /dev/null +++ b/src/renderer/actions/utils/exists.js @@ -0,0 +1,11 @@ +import {promises as fs} from "fs"; + +export default async function exists(file) { + try { + await fs.stat(file); + return true; + } + catch { + return false; + } +} \ No newline at end of file diff --git a/src/renderer/actions/utils/fail.js b/src/renderer/actions/utils/fail.js new file mode 100644 index 00000000..ec01a5e9 --- /dev/null +++ b/src/renderer/actions/utils/fail.js @@ -0,0 +1,10 @@ +import {log} from "./log"; +import {action, status} from "../../stores/installation"; + +const discordURL = "https://discord.gg/0Tmfo5ZbORCRqbAd"; + +export default function fail() { + log(""); + log(`The ${action.value} seems to have failed. If this problem is recurring, join our discord community for support. ${discordURL}`); + status.set("error"); +} \ No newline at end of file diff --git a/src/renderer/actions/utils/kill.js b/src/renderer/actions/utils/kill.js new file mode 100644 index 00000000..b8540428 --- /dev/null +++ b/src/renderer/actions/utils/kill.js @@ -0,0 +1,37 @@ +import findProcess from "find-process"; +import kill from "tree-kill"; +import {shell} from "electron"; +import {progress} from "../../stores/installation"; +import {log} from "./log"; + +const platforms = {stable: "Discord", ptb: "Discord PTB", canary: "Discord Canary"}; +export default async function killProcesses(channels, progressPerLoop, shouldRestart = true) { + for (const channel of channels) { + let processName = platforms[channel]; + if (process.platform === "darwin") processName = platforms[channel]; // Discord Canary and Discord PTB on Mac + else processName = platforms[channel].replace(" ", ""); // DiscordCanary and DiscordPTB on Windows/Linux + + log("Attempting to kill " + processName); + try { + const results = await findProcess("name", processName, true); + if (!results || !results.length) { + log(`✅ ${processName} not running`); + progress.set(progress.value + progressPerLoop); + continue; + } + + const parentPids = results.map(p => p.ppid); + const discordPid = results.find(p => parentPids.includes(p.pid)); + const bin = discordPid.bin; + kill(discordPid.pid); + if (shouldRestart) shell.openExternal(bin); + progress.set(progress.value + progressPerLoop); + } + catch (err) { + const symbol = shouldRestart ? "⚠️" : "❌"; + log(`${symbol} Could not kill ${platforms[channel]}`); + log(`${symbol} ${err.message}`); + return err; + } + } +} \ No newline at end of file diff --git a/src/renderer/actions/utils/log.js b/src/renderer/actions/utils/log.js new file mode 100644 index 00000000..a4360e3a --- /dev/null +++ b/src/renderer/actions/utils/log.js @@ -0,0 +1,16 @@ +import logs from "../../stores/logs"; + +export function log(entry) { + logs.update(a => { + a.push(entry); + return a; + }); +} + +export function lognewline(entry) { + logs.update(a => { + a.push(""); + a.push(entry); + return a; + }); +} \ No newline at end of file diff --git a/src/renderer/actions/utils/notices.js b/src/renderer/actions/utils/notices.js new file mode 100644 index 00000000..5ad17f7f --- /dev/null +++ b/src/renderer/actions/utils/notices.js @@ -0,0 +1,17 @@ +import {remote} from "electron"; + +export function showRestartNotice() { + remote.dialog.showMessageBox({ + type: "info", + title: "Restart Discord", + message: "BetterDiscord could not restart Discord. Please restart it manually." + }); +} + +export function showKillNotice() { + remote.dialog.showMessageBox({ + type: "error", + title: "Shutdown Discord", + message: "BetterDiscord could not shut down Discord. Please make sure Discord is fully closed, then run the installer again." + }); +} \ No newline at end of file diff --git a/src/renderer/actions/utils/reset.js b/src/renderer/actions/utils/reset.js new file mode 100644 index 00000000..908ed113 --- /dev/null +++ b/src/renderer/actions/utils/reset.js @@ -0,0 +1,9 @@ +import logs from "../../stores/logs"; +import {progress, status} from "../../stores/installation"; + +export default async function reset() { + logs.set([]); + progress.set(0); + status.set(""); + await new Promise(r => setTimeout(r, 500)); +} \ No newline at end of file diff --git a/src/renderer/actions/utils/sanity.js b/src/renderer/actions/utils/sanity.js new file mode 100644 index 00000000..18bca7ed --- /dev/null +++ b/src/renderer/actions/utils/sanity.js @@ -0,0 +1,14 @@ +import {log} from "./log"; +import {action} from "../../stores/installation"; + +export default function doSanityCheck(config) { + const paths = Object.values(config); + if (paths && paths.length) { + const name = action.value; + log(`Starting ${name.charAt(0).toUpperCase() + name.slice(1)}...`); + return true; + } + + log("❌ Something went wrong internally."); + return false; +} \ No newline at end of file diff --git a/src/renderer/actions/utils/succeed.js b/src/renderer/actions/utils/succeed.js new file mode 100644 index 00000000..4c02868a --- /dev/null +++ b/src/renderer/actions/utils/succeed.js @@ -0,0 +1,10 @@ +import {log} from "./log"; +import {action, progress, status} from "../../stores/installation"; + +export default function succeed() { + const name = action.value; + log(""); + log(`${name.charAt(0).toUpperCase() + name.slice(1)} completed!`); + progress.set(100); + status.set("success"); +} \ No newline at end of file diff --git a/src/renderer/getstatic.js b/src/renderer/getstatic.js new file mode 100644 index 00000000..f61d1178 --- /dev/null +++ b/src/renderer/getstatic.js @@ -0,0 +1,11 @@ +import path from "path"; +import * as url from "url"; + +const isDevelopment = process.env.NODE_ENV !== "production"; + +export default function getStatic(val) { + if (isDevelopment) { + return url.resolve(window.location.origin, val); + } + return path.resolve(__static, val); +} \ No newline at end of file diff --git a/src/renderer/index.js b/src/renderer/index.js index 5d08c69e..7ca354bc 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -1,9 +1,14 @@ import App from "./App.svelte"; +import getStatic from "./getstatic"; +const appElement = document.getElementById("app"); const app = new App({ - target: document.getElementById("app") + target: appElement }); +// Setup this in a var because otherwise it won't work in prod +appElement.style.setProperty("--background", `url('${getStatic("/images/background.png")}')`); + window.refresh = () => window.location.href = `http://${window.location.host}/`; diff --git a/src/renderer/pages/Platforms.svelte b/src/renderer/pages/Platforms.svelte index c3ba7775..3a11ec12 100644 --- a/src/renderer/pages/Platforms.svelte +++ b/src/renderer/pages/Platforms.svelte @@ -6,6 +6,7 @@ import {action, platforms, paths} from "../stores/installation"; import {platforms as platformLabels, validatePath, getBrowsePath} from "../actions/paths"; import {remote} from "electron"; + import getStatic from "../getstatic"; if (Object.values($platforms).some(r => r)) canGoForward.set(true); else canGoForward.set(false); @@ -50,7 +51,7 @@ {#each Object.entries(platformLabels) as [channel, label]} - Platform Icon + Platform Icon {label} {/each} diff --git a/src/renderer/stores/installation.js b/src/renderer/stores/installation.js index fbed09d9..273eb889 100644 --- a/src/renderer/stores/installation.js +++ b/src/renderer/stores/installation.js @@ -1,9 +1,11 @@ import {writable} from "svelte/store"; import {locations} from "../actions/paths"; +import readwritable from "./types/readwritable"; export const status = writable(""); export const hasAgreed = writable(false); -export const action = writable("install"); export const platforms = writable({stable: false, canary: false, ptb: false}); export const paths = writable({stable: locations.stable, canary: locations.canary, ptb: locations.ptb}); -export const progress = writable(0); \ No newline at end of file + +export const progress = readwritable(0); +export const action = readwritable("install"); \ No newline at end of file diff --git a/src/renderer/stores/types/readwritable.js b/src/renderer/stores/types/readwritable.js new file mode 100644 index 00000000..da0be003 --- /dev/null +++ b/src/renderer/stores/types/readwritable.js @@ -0,0 +1,22 @@ +import {writable} from "svelte/store"; + +export default function readWritable(initial) { + const {subscribe, set, update} = writable(initial); + + let cached = initial; + return { + subscribe, + update: fn => { + update(v => { + const retVal = fn(v); + cached = retVal; + return retVal; + }); + }, + set: val => { + cached = val; + set(val); + }, + get value() {return cached;} + }; +} \ No newline at end of file From 44b640f469a761457f3e3e109d70c2ad5e230779 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Sun, 28 Mar 2021 18:53:51 -0400 Subject: [PATCH 4/5] Move linux installation to discord_desktop_core --- src/renderer/actions/install.js | 11 ++++++++--- src/renderer/actions/paths.js | 29 ++++++++++++++++++++++------- src/renderer/actions/uninstall.js | 11 +++++++++-- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/renderer/actions/install.js b/src/renderer/actions/install.js index 79bb2338..f49be1d2 100644 --- a/src/renderer/actions/install.js +++ b/src/renderer/actions/install.js @@ -75,9 +75,14 @@ async function injectShims(paths) { const pkgFile = path.join(appPath, "package.json"); const indexFile = path.join(appPath, "index.js"); try { - if (!(await exists(appPath))) await fs.mkdir(appPath); - await fs.writeFile(pkgFile, JSON.stringify({name: "betterdiscord", main: "index.js"})); - await fs.writeFile(indexFile, `require("${asarPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}");`); + if (process.platform === "win32" || process.platform === "darwin") { + if (!(await exists(appPath))) await fs.mkdir(appPath); + await fs.writeFile(pkgFile, JSON.stringify({name: "betterdiscord", main: "index.js"})); + await fs.writeFile(indexFile, `require("${asarPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}");`); + } + else { + await fs.writeFile(path.join(discordPath, "index.js"), `require("${asarPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}");\nmodule.exports = require("./core.asar");`); + } log("✅ Injection successful"); progress.set(progress.value + progressPerLoop); } diff --git a/src/renderer/actions/paths.js b/src/renderer/actions/paths.js index 9c5f1d5f..cdccdb70 100644 --- a/src/renderer/actions/paths.js +++ b/src/renderer/actions/paths.js @@ -1,5 +1,6 @@ const fs = require("fs"); const path = require("path"); +import {remote} from "electron"; export const platforms = {stable: "Discord", ptb: "Discord PTB", canary: "Discord Canary"}; export const locations = {stable: "", ptb: "", canary: ""}; @@ -17,7 +18,11 @@ const getDiscordPath = function(releaseChannel) { resourcePath = path.join("/Applications", `${releaseChannel}.app`, "Contents", "Resources"); } else { - resourcePath = path.join("/usr", "share", releaseChannel.toLowerCase().replace(/ /g, "-"), "resources"); + const basedir = path.join(remote.app.getPath("userData"), "..", releaseChannel.toLowerCase().replace(" ", "")); + if (!fs.existsSync(basedir)) return ""; + const version = fs.readdirSync(basedir).filter(f => fs.lstatSync(path.join(basedir, f)).isDirectory() && f.split(".").length > 1).sort().reverse()[0]; + if (!version) return ""; + resourcePath = path.join(basedir, version, "modules", "discord_desktop_core"); } if (fs.existsSync(resourcePath)) return resourcePath; @@ -31,7 +36,7 @@ for (const channel in platforms) { export const getBrowsePath = function(channel) { if (process.platform === "win32") return path.join(process.env.LOCALAPPDATA, platforms[channel].replace(" ", "")); else if (process.platform === "darwin") return path.join("/Applications", `${platforms[channel]}.app`); - return path.join("/usr", "share", platforms[channel].toLowerCase().replace(" ", "-")); + return path.join(remote.app.getPath("userData"), "..", platforms[channel].toLowerCase().replace(" ", "")); }; export const validatePath = function(channel, proposedPath) { @@ -76,14 +81,24 @@ const validateMac = function(channel, proposedPath) { }; const validateLinux = function(channel, proposedPath) { - const channelName = platforms[channel].toLowerCase().replace(" ", "-"); + if (proposedPath.includes("/snap/")) { + remote.dialog.showErrorBox("BetterDiscord Incompatible", "BetterDiscord is currently incompatible with Snap installs of Discord. Support for snap installs is coming soon!"); + return ""; + } + const channelName = platforms[channel].toLowerCase().replace(" ", ""); let resourcePath = ""; const selected = path.basename(proposedPath); - if (selected === channelName) resourcePath = path.join(proposedPath, "resources"); - if (selected === "resources") resourcePath = proposedPath; + if (selected === channelName) { + const version = fs.readdirSync(proposedPath).filter(f => fs.lstatSync(path.join(proposedPath, f)).isDirectory() && f.split(".").length > 1).sort().reverse()[0]; + if (!version) return ""; + resourcePath = path.join(proposedPath, version, "modules", "discord_desktop_core"); + } + if (selected.split(".").length > 2) resourcePath = path.join(proposedPath, "modules", "discord_desktop_core"); + if (selected === "modules") resourcePath = path.join(proposedPath, "discord_desktop_core"); + if (selected === "discord_desktop_core") resourcePath = proposedPath; - const executablePath = path.join(resourcePath, "..", "MacOS", platforms[channel]); - if (fs.existsSync(executablePath)) return resourcePath; + const asarPath = path.join(resourcePath, "core.asar"); + if (fs.existsSync(asarPath)) return resourcePath; return ""; }; diff --git a/src/renderer/actions/uninstall.js b/src/renderer/actions/uninstall.js index 33e672dc..1e430ad3 100644 --- a/src/renderer/actions/uninstall.js +++ b/src/renderer/actions/uninstall.js @@ -1,7 +1,8 @@ -import {progress} from "../stores/installation"; +import {promises as fs} from "fs"; import del from "del"; import path from "path"; +import {progress} from "../stores/installation"; import {log, lognewline} from "./utils/log"; import succeed from "./utils/succeed"; @@ -22,8 +23,14 @@ async function deleteShims(paths) { for (const discordPath of paths) { log("Removing " + discordPath); const appPath = path.join(discordPath, "app"); + const indexFile = path.join(discordPath, "index.js"); try { - if (await exists(appPath)) await del(appPath, {force: true}); + if (process.platform === "win32" || process.platform === "darwin") { + if (await exists(appPath)) await del(appPath, {force: true}); + } + else { + if (await exists(indexFile)) await fs.writeFile(indexFile, `module.exports = require("./core.asar");`); + } log("✅ Deletion successful"); progress.set(progress.value + progressPerLoop); } From 06578e04566918a3d9cbde4b8aed950628653665 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Mon, 29 Mar 2021 16:32:02 -0400 Subject: [PATCH 5/5] Setup distributable options --- assets/icon.icns | Bin 0 -> 51010 bytes assets/icon.ico | Bin 0 -> 9223 bytes package.json | 31 ++++++++++++++++++++++++----- scripts/fixmac.js | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 assets/icon.icns create mode 100644 assets/icon.ico create mode 100644 scripts/fixmac.js diff --git a/assets/icon.icns b/assets/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..4c4cbdb2ac671ef93159d33e3984b6b6068287ad GIT binary patch literal 51010 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 dNr+p=KXHGS~z*J`IY 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 G7yknl7OGwV literal 0 HcmV?d00001 diff --git a/package.json b/package.json index a21a9323..154b8b33 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "betterdiscord", "productName": "BetterDiscord", - "description": "test", - "author": "Zerebos", + "description": "Installer for BetterDiscord.", + "author": "BetterDiscord", "version": "1.0.0", "license": "MIT", "scripts": { @@ -33,11 +33,32 @@ "build": { "appId": "app.betterdiscord.installer", "productName": "BetterDiscord", + "copyright": "Copyright © 2021 BetterDiscord", + "afterAllArtifactBuild": "scripts/fixmac.js", + "win": { + "icon": "assets/icon.ico", + "target": { + "target": "portable", + "arch": ["ia32"] + } + }, "mac": { - "category": "your.app.category.type" + "icon": "assets/icon.icns", + "category": "public.app-category.social-networking", + "target": { + "target": "zip", + "arch": ["x64"] + } }, - "win": { - "target": "portable" + "linux": { + "category": "Utility", + "target": { + "target": "AppImage", + "arch": ["x64"] + } + }, + "appImage": { + "license": "assets/license.txt" } }, "electronWebpack": { diff --git a/scripts/fixmac.js b/scripts/fixmac.js new file mode 100644 index 00000000..fb434b06 --- /dev/null +++ b/scripts/fixmac.js @@ -0,0 +1,49 @@ +// Exists due to https://github.com/electron-userland/electron-builder/issues/4299 +// Tempfix adapted from: https://gist.github.com/harshitsilly/a1bd5a405f93966aad20358ae6c4cec5 + +const path = require("path"); +const {execSync} = require("child_process"); +const fs = require("fs"); +const yaml = require("js-yaml"); +const {appBuilderPath} = require("app-builder-bin"); +const currentWorkingDirectory = process.cwd(); +const packageInfo = require(path.join(currentWorkingDirectory, "package.json")); + +const APP_NAME = packageInfo.build.productName; +const APP_VERSION = process.argv[2] ? process.argv[2] : packageInfo.version; +const APP_DIST_PATH = path.join(currentWorkingDirectory, "dist"); + + +/* eslint-disable no-console */ +module.exports = function(buildResult) { + if (!buildResult.artifactPaths.some(p => p.endsWith("mac.zip"))) return console.log("No Mac build detected"); + console.log("Zipping Started"); + + execSync( + `ditto -c -k --sequesterRsrc --keepParent --zlibCompressionLevel 9 "${APP_DIST_PATH}/mac/${APP_NAME}.app" "${APP_DIST_PATH}/${APP_NAME}-${APP_VERSION}-mac.zip"` + ); + + console.log("Zipping Completed"); + + const APP_GENERATED_BINARY_PATH = path.join(APP_DIST_PATH, `${APP_NAME}-${APP_VERSION}-mac.zip`); + try { + const output = execSync( + `${appBuilderPath} blockmap --input="${APP_GENERATED_BINARY_PATH}" --output="${APP_DIST_PATH}/${APP_NAME}-${APP_VERSION}-mac.zip.blockmap" --compression=gzip` + ); + const {sha512, size} = JSON.parse(output); + + const ymlPath = path.join(APP_DIST_PATH, "latest-mac.yml"); + const ymlData = yaml.safeLoad(fs.readFileSync(ymlPath, "utf8")); + // console.log(ymlData); + ymlData.sha512 = sha512; + ymlData.files[0].sha512 = sha512; + ymlData.files[0].size = size; + const yamlStr = yaml.safeDump(ymlData); + // console.log(yamlStr); + fs.writeFileSync(ymlPath, yamlStr, "utf8"); + console.log("Successfully updated YAML file and configurations with blockmap."); + } + catch (e) { + console.log("Error in updating YAML file and configurations with blockmap.", e); + } +}; \ No newline at end of file