this repo has no description
1import { app, nativeTheme } from "electron";
2import * as path from "node:path";
3import * as fs from "node:fs/promises";
4import * as fsSync from "node:fs";
5import { parseTarGzip } from "nanotar";
6
7const logger = moonlightHost.getLogger("nativeFixes/host");
8const enabledFeatures = app.commandLine.getSwitchValue("enable-features").split(",");
9
10moonlightHost.events.on("window-created", function (browserWindow) {
11 if (moonlightHost.getConfigOption<boolean>("nativeFixes", "devtoolsThemeFix") ?? true) {
12 browserWindow.webContents.on("devtools-opened", () => {
13 if (!nativeTheme.shouldUseDarkColors) return;
14 nativeTheme.themeSource = "light";
15 setTimeout(() => {
16 nativeTheme.themeSource = "dark";
17 }, 100);
18 });
19 }
20});
21
22if (moonlightHost.getConfigOption<boolean>("nativeFixes", "disableRendererBackgrounding") ?? true) {
23 // Discord already disables UseEcoQoSForBackgroundProcess and some other
24 // related features
25 app.commandLine.appendSwitch("disable-renderer-backgrounding");
26 app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
27
28 // already added on Windows, but not on other operating systems
29 app.commandLine.appendSwitch("disable-background-timer-throttling");
30}
31
32if (moonlightHost.getConfigOption<boolean>("nativeFixes", "vulkan") ?? false) {
33 enabledFeatures.push("Vulkan", "DefaultANGLEVulkan", "VulkanFromANGLE");
34}
35
36if (process.platform === "linux") {
37 if (moonlightHost.getConfigOption<boolean>("nativeFixes", "linuxAutoscroll") ?? false) {
38 app.commandLine.appendSwitch("enable-blink-features", "MiddleClickAutoscroll");
39 }
40
41 if (moonlightHost.getConfigOption<boolean>("nativeFixes", "linuxSpeechDispatcher") ?? true) {
42 app.commandLine.appendSwitch("enable-speech-dispatcher");
43 }
44
45 if (moonlightHost.getConfigOption<boolean>("nativeFixes", "linuxHevcSupport") ?? true) {
46 enabledFeatures.push("PlatformHEVCDecoderSupport");
47 }
48}
49
50// NOTE: Only tested if this appears on Windows, it should appear on all when
51// hardware acceleration is disabled
52const noAccel = app.commandLine.hasSwitch("disable-gpu-compositing");
53if ((moonlightHost.getConfigOption<boolean>("nativeFixes", "vaapi") ?? true) && !noAccel) {
54 if (process.platform === "linux") {
55 // These will eventually be renamed https://source.chromium.org/chromium/chromium/src/+/5482210941a94d70406b8da962426e4faca7fce4
56 enabledFeatures.push("VaapiVideoEncoder", "VaapiVideoDecoder", "VaapiVideoDecodeLinuxGL");
57
58 if (moonlightHost.getConfigOption<boolean>("nativeFixes", "vaapiIgnoreDriverChecks") ?? false)
59 enabledFeatures.push("VaapiIgnoreDriverChecks");
60 }
61}
62
63app.commandLine.appendSwitch("enable-features", [...new Set(enabledFeatures)].join(","));
64
65if (process.platform === "linux" && moonlightHost.getConfigOption<boolean>("nativeFixes", "linuxUpdater")) {
66 const exePath = app.getPath("exe");
67 const appName = path.basename(exePath);
68 const targetDir = path.dirname(exePath);
69 const { releaseChannel }: { releaseChannel: string } = JSON.parse(
70 fsSync.readFileSync(path.join(targetDir, "resources", "build_info.json"), "utf8")
71 );
72
73 const updaterModule = require(path.join(moonlightHost.asarPath, "app_bootstrap", "hostUpdater.js"));
74 const updater = updaterModule.constructor;
75
76 async function doUpdate(cb: (percent: number) => void) {
77 logger.debug("Extracting to", targetDir);
78
79 const exists = (path: string) =>
80 fs
81 .stat(path)
82 .then(() => true)
83 .catch(() => false);
84
85 const url = `https://discord.com/api/download/${releaseChannel}?platform=linux&format=tar.gz`;
86 const resp = await fetch(url, {
87 cache: "no-store"
88 });
89
90 const reader = resp.body!.getReader();
91 const contentLength = parseInt(resp.headers.get("Content-Length") ?? "0");
92 logger.info(`Expecting ${contentLength} bytes for the update`);
93 const bytes = new Uint8Array(contentLength);
94 let pos = 0;
95 let lastPercent = 0;
96
97 while (true) {
98 const { done, value } = await reader.read();
99 if (done) {
100 break;
101 } else {
102 bytes.set(value, pos);
103 pos += value.length;
104
105 const newPercent = Math.floor((pos / contentLength) * 100);
106 if (lastPercent !== newPercent) {
107 lastPercent = newPercent;
108 cb(newPercent);
109 }
110 }
111 }
112
113 const files = await parseTarGzip(bytes);
114
115 for (const file of files) {
116 if (!file.data) continue;
117 // @ts-expect-error What do you mean their own types are wrong
118 if (file.type !== "file") continue;
119
120 // Discord update files are inside of a main "Discord(PTB|Canary)" folder
121 const filePath = file.name.replace(`${appName}/`, "");
122 logger.info("Extracting", filePath);
123
124 let targetFilePath = path.join(targetDir, filePath);
125 if (filePath === "resources/app.asar") {
126 // You tried
127 targetFilePath = path.join(targetDir, "resources", "_app.asar");
128 } else if (filePath === appName || filePath === "chrome_crashpad_handler") {
129 // Can't write over the executable? Just move it! 4head
130 if (await exists(targetFilePath)) {
131 await fs.rename(targetFilePath, targetFilePath + ".bak");
132 await fs.unlink(targetFilePath + ".bak");
133 }
134 }
135 const targetFileDir = path.dirname(targetFilePath);
136
137 if (!(await exists(targetFileDir))) await fs.mkdir(targetFileDir, { recursive: true });
138 await fs.writeFile(targetFilePath, file.data);
139
140 const mode = file.attrs?.mode;
141 if (mode != null) {
142 // Not sure why this slice is needed
143 await fs.chmod(targetFilePath, mode.slice(-3));
144 }
145 }
146
147 logger.debug("Done updating");
148 }
149
150 const realEmit = updater.prototype.emit;
151 updater.prototype.emit = function (event: string, ...args: any[]) {
152 // Arrow functions don't bind `this` :D
153 const call = (event: string, ...args: any[]) => realEmit.call(this, event, ...args);
154
155 if (event === "update-manually") {
156 const latestVerStr: string = args[0];
157 logger.debug("update-manually called, intercepting", latestVerStr);
158 call("update-available");
159
160 (async () => {
161 try {
162 await doUpdate((progress) => {
163 call("update-progress", progress);
164 });
165 // Copied from the win32 updater
166 this.updateVersion = latestVerStr;
167 call(
168 "update-downloaded",
169 {},
170 releaseChannel,
171 latestVerStr,
172 new Date(),
173 this.updateUrl,
174 this.quitAndInstall.bind(this)
175 );
176 } catch (e) {
177 logger.error("Error updating", e);
178 }
179 })();
180
181 return this;
182 } else {
183 return realEmit.call(this, event, ...args);
184 }
185 };
186}