From 77b1acda57798a7aae9315c7bed1d810b637577a Mon Sep 17 00:00:00 2001 From: NotNite Date: Mon, 20 Jan 2025 14:52:00 -0500 Subject: [PATCH] nativeFixes: Linux updater --- .../core-extensions/src/nativeFixes/host.ts | 128 ++++++++++++++++++ .../src/nativeFixes/manifest.json | 9 +- 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/packages/core-extensions/src/nativeFixes/host.ts b/packages/core-extensions/src/nativeFixes/host.ts index ddbcc054..749e0944 100644 --- a/packages/core-extensions/src/nativeFixes/host.ts +++ b/packages/core-extensions/src/nativeFixes/host.ts @@ -1,5 +1,10 @@ import { app, nativeTheme } from "electron"; +import * as path from "node:path"; +import * as fs from "node:fs/promises"; +import * as fsSync from "node:fs"; +import { parseTarGzip } from "nanotar"; +const logger = moonlightHost.getLogger("nativeFixes/host"); const enabledFeatures = app.commandLine.getSwitchValue("enable-features").split(","); moonlightHost.events.on("window-created", function (browserWindow) { @@ -44,3 +49,126 @@ if ((moonlightHost.getConfigOption("nativeFixes", "vaapi") ?? true) && } app.commandLine.appendSwitch("enable-features", [...new Set(enabledFeatures)].join(",")); + +if (process.platform === "linux" && moonlightHost.getConfigOption("nativeFixes", "linuxUpdater")) { + const exePath = app.getPath("exe"); + const appName = path.basename(exePath); + const targetDir = path.dirname(exePath); + const { releaseChannel }: { releaseChannel: string } = JSON.parse( + fsSync.readFileSync(path.join(targetDir, "resources", "build_info.json"), "utf8") + ); + + const updaterModule = require(path.join(moonlightHost.asarPath, "app_bootstrap", "hostUpdater.js")); + const updater = updaterModule.constructor; + + async function doUpdate(cb: (percent: number) => void) { + logger.debug("Extracting to", targetDir); + + const exists = (path: string) => + fs + .stat(path) + .then(() => true) + .catch(() => false); + + const url = `https://discord.com/api/download/${releaseChannel}?platform=linux&format=tar.gz`; + const resp = await fetch(url, { + cache: "no-store" + }); + + const reader = resp.body!.getReader(); + const contentLength = parseInt(resp.headers.get("Content-Length") ?? "0"); + logger.info(`Expecting ${contentLength} bytes for the update`); + const bytes = new Uint8Array(contentLength); + let pos = 0; + let lastPercent = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } else { + bytes.set(value, pos); + pos += value.length; + + const newPercent = Math.floor((pos / contentLength) * 100); + if (lastPercent !== newPercent) { + lastPercent = newPercent; + cb(newPercent); + } + } + } + + const files = await parseTarGzip(bytes); + + for (const file of files) { + if (!file.data) continue; + // @ts-expect-error What do you mean their own types are wrong + if (file.type !== "file") continue; + + // Discord update files are inside of a main "Discord(PTB|Canary)" folder + const filePath = file.name.replace(`${appName}/`, ""); + logger.info("Extracting", filePath); + + let targetFilePath = path.join(targetDir, filePath); + if (filePath === "resources/app.asar") { + // You tried + targetFilePath = path.join(targetDir, "resources", "_app.asar"); + } else if (filePath === appName) { + // Can't write over the executable? Just move it! 4head + if (await exists(targetFilePath)) { + await fs.rename(targetFilePath, targetFilePath + ".bak"); + await fs.unlink(targetFilePath + ".bak"); + } + } + const targetFileDir = path.dirname(targetFilePath); + + if (!(await exists(targetFileDir))) await fs.mkdir(targetFileDir, { recursive: true }); + await fs.writeFile(targetFilePath, file.data); + + const mode = file.attrs?.mode; + if (mode != null) { + // Not sure why this slice is needed + await fs.chmod(targetFilePath, mode.slice(-3)); + } + } + + logger.debug("Done updating"); + } + + const realEmit = updater.prototype.emit; + updater.prototype.emit = function (event: string, ...args: any[]) { + // Arrow functions don't bind `this` :D + const call = (event: string, ...args: any[]) => realEmit.call(this, event, ...args); + + if (event === "update-manually") { + const latestVerStr: string = args[0]; + logger.debug("update-manually called, intercepting", latestVerStr); + call("update-available"); + + (async () => { + try { + await doUpdate((progress) => { + call("update-progress", progress); + }); + // Copied from the win32 updater + this.updateVersion = latestVerStr; + call( + "update-downloaded", + {}, + releaseChannel, + latestVerStr, + new Date(), + this.updateUrl, + this.quitAndInstall.bind(this) + ); + } catch (e) { + logger.error("Error updating", e); + } + })(); + + return this; + } else { + return realEmit.call(this, event, ...args); + } + }; +} diff --git a/packages/core-extensions/src/nativeFixes/manifest.json b/packages/core-extensions/src/nativeFixes/manifest.json index f3f1321b..a17ca7e9 100644 --- a/packages/core-extensions/src/nativeFixes/manifest.json +++ b/packages/core-extensions/src/nativeFixes/manifest.json @@ -4,7 +4,7 @@ "meta": { "name": "Native Fixes", "tagline": "Various configurable fixes for Discord and Electron", - "authors": ["Cynosphere", "adryd"], + "authors": ["Cynosphere", "adryd", "NotNite"], "tags": ["fixes"] }, "environment": "desktop", @@ -43,6 +43,13 @@ "description": "Provides hardware accelerated video encode and decode. Has no effect on other operating systems", "type": "boolean", "default": true + }, + "linuxUpdater": { + "advice": "restart", + "displayName": "Linux Updater", + "description": "Actually implements updating Discord on Linux. Has no effect on other operating systems", + "type": "boolean", + "default": false } }, "apiLevel": 2