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