[bugfix] refactored bs launch + launch bs version directly with shortcut

This commit is contained in:
silentrald
2025-01-15 23:25:40 +08:00
parent a47616086c
commit 350dff37cf
5 changed files with 108 additions and 67 deletions
@@ -46,7 +46,7 @@ export abstract class AbstractLauncherService {
return Array.from(new Set(launchArgs).values());
}
protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams {
protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): ChildProcessWithoutNullStreams {
const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), ...(options || {}) };
@@ -57,7 +57,7 @@ export abstract class AbstractLauncherService {
spawnOptions.shell = true; // For windows to spawn properly
return bsmSpawn(`"${bsExePath}"`, {
args, options: spawnOptions, log: BsmShellLog.Command,
linux: { prefix: this.linux.getProtonPrefix() },
linux: { prefix: options?.protonPrefix || "" },
flatpak: {
host: IS_FLATPAK,
env: [
@@ -80,7 +80,7 @@ export abstract class AbstractLauncherService {
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
const process = this.launchBSProcess(bsExePath, args, options);
let timoutId: NodeJS.Timeout;
let timeoutId: NodeJS.Timeout;
const exit = new Promise<number>((resolve, reject) => {
// Don't remove, useful for debugging!
@@ -103,7 +103,7 @@ export abstract class AbstractLauncherService {
const unrefAfter = options?.unrefAfter ?? sToMs(10);
timoutId = setTimeout(() => {
timeoutId = setTimeout(() => {
log.error("BS process unref after timeout", unrefAfter);
process.unref();
process.removeAllListeners();
@@ -111,7 +111,7 @@ export abstract class AbstractLauncherService {
}, unrefAfter);
}).finally(() => {
clearTimeout(timoutId);
clearTimeout(timeoutId);
});
return { process, exit };
@@ -119,5 +119,6 @@ export abstract class AbstractLauncherService {
}
export type SpawnBsProcessOptions = {
protonPrefix?: string;
unrefAfter?: number;
} & SpawnOptionsWithoutStdio;
@@ -24,6 +24,7 @@ import { LaunchMod, LaunchMods } from "shared/models/bs-launch/launch-option.int
import { StaticConfigurationService } from "../static-configuration.service";
import { SteamService } from "../steam.service";
import { tryit } from "shared/helpers/error.helpers";
import { LinuxService } from "../linux.service";
export class BSLauncherService {
private static instance: BSLauncherService;
@@ -37,6 +38,7 @@ export class BSLauncherService {
private readonly steam: SteamService;
private readonly oculusLauncher: OculusLauncherService;
private readonly staticConfig: StaticConfigurationService;
private readonly linux: LinuxService;
public static getInstance(): BSLauncherService {
if (!BSLauncherService.instance) {
@@ -55,6 +57,7 @@ export class BSLauncherService {
this.oculusLauncher = OculusLauncherService.getInstance();
this.staticConfig = StaticConfigurationService.getInstance();
this.steam = SteamService.getInstance();
this.linux = LinuxService.getInstance();
this.bsmProtocolService.on("launch", link => {
log.info("Launch from bsm protocol", link.toString());
@@ -230,12 +233,13 @@ export class BSLauncherService {
description: [shortcutName, launchOptions.version.color].join(" "), // <= Need color in description to help windows know that the shortcut is different
})
),
linux: async () => (
createDesktopUrlShortcut(path.join(app.getPath("desktop"), `${shortcutName}.desktop`), {
name: shortcutName,
url: shortcutUrl,
icon: await this.createShortcutPng(shortcutIconColor),
})
linux: async () => this.linux.createDesktopShortcut(
path.join(app.getPath("desktop"), `${shortcutName}.desktop`),
shortcutName,
await this.createShortcutPng(shortcutIconColor),
launchOptions,
await this.steam.getSteamPath(),
await this.localVersionService.getVersionPath(launchOptions.version)
)
})
@@ -282,26 +286,3 @@ type ShortcutParams = {
versionOculus?: string;
}
/**
* Create .desktop file for url shortcut (only for linux)
* @param {string} shortcutPath
* @param options
* @returns
*/
function createDesktopUrlShortcut(shortcutPath: string, options?: {
url: string
name: string,
icon: string
}): Promise<boolean> {
const { url, name, icon } = options || {};
const data = [
"[Desktop Entry]",
"Type=Link",
`Name=${name}`,
`Icon=${icon}`,
`URL=${url}`
].join("\n");
return writeFile(shortcutPath, data).then(() => true);
}
@@ -112,9 +112,14 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
"SteamGameId": BS_APP_ID,
};
let protonPrefix = "";
// Linux setup
if (process.platform === "linux") {
await this.linux.setupLaunch(launchOptions, steamPath, bsFolderPath, env);
const linuxSetup = await this.linux.setupLaunch(
launchOptions, steamPath, bsFolderPath
);
protonPrefix = linuxSetup.protonPrefix;
Object.assign(env, linuxSetup.env);
}
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
@@ -122,7 +127,10 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
const spawnOpts = { env, cwd: bsFolderPath };
const launchPromise = !launchOptions.admin ? (
this.launchBs(bsExePath, launchArgs, spawnOpts).exit
this.launchBs(bsExePath, launchArgs, {
...spawnOpts,
protonPrefix
}).exit
) : (
new Promise<number>(resolve => {
const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${bsExePath}" ${launchArgs.join(" ")} --log-path "${path.join(app.getPath("logs"), "bs-admin-start.log")}"`, spawnOpts);
+81 -30
View File
@@ -1,7 +1,7 @@
import fs from "fs-extra";
import log from "electron-log";
import path from "path";
import { BS_APP_ID, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
import { BS_APP_ID, BS_EXECUTABLE, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
import { InstallationLocationService } from "./installation-location.service";
import { StaticConfigurationService } from "./static-configuration.service";
import { CustomError } from "shared/models/exceptions/custom-error.class";
@@ -21,7 +21,6 @@ export class LinuxService {
private readonly installLocationService: InstallationLocationService;
private readonly staticConfig: StaticConfigurationService;
private protonPrefix = "";
private nixOS: boolean | undefined;
@@ -40,24 +39,16 @@ export class LinuxService {
public async setupLaunch(
launchOptions: LaunchOption,
steamPath: string,
bsFolderPath: string,
env: Record<string, string>
) {
bsFolderPath: string
): Promise<{
protonPrefix: string;
env: Record<string, string>;
}> {
if (launchOptions.admin) {
log.warn("Launching as admin is not supported on Linux! Starting the game as a normal user.");
launchOptions.admin = false;
}
// Create the compat data path if it doesn't exist.
// If the user never ran Beat Saber through steam before
// using bsmanager, it won't exist, and proton will fail
// to launch the game.
const compatDataPath = this.getCompatDataPath();
if (!fs.existsSync(compatDataPath)) {
log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`);
fs.mkdirSync(compatDataPath);
}
if (!this.staticConfig.has("proton-folder")) {
throw CustomError.fromError(
new Error("Proton folder not set"),
@@ -75,27 +66,46 @@ export class LinuxService {
);
}
this.protonPrefix = await this.isNixOS()
? `steam-run "${protonPath}" run`
: `"${protonPath}" run`;
return {
protonPrefix: await this.isNixOS()
? `steam-run "${protonPath}" run`
: `"${protonPath}" run`,
env: await this.prepareEnvVariables(launchOptions, steamPath, bsFolderPath)
};
}
private async prepareEnvVariables(
launchOptions: LaunchOption,
steamPath: string,
bsFolderPath: string
): Promise<Record<string, string>> {
// Create the compat data path if it doesn't exist.
// If the user never ran Beat Saber through steam before
// using bsmanager, it won't exist, and proton will fail
// to launch the game.
const compatDataPath = this.getCompatDataPath();
if (!fs.existsSync(compatDataPath)) {
log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`);
fs.mkdirSync(compatDataPath);
}
// Setup Proton environment variables
Object.assign(env, {
const envVars: Record<string, string> = {
"WINEDLLOVERRIDES": "winhttp=n,b", // Required for mods to work
"STEAM_COMPAT_DATA_PATH": compatDataPath,
"STEAM_COMPAT_INSTALL_PATH": bsFolderPath,
"STEAM_COMPAT_CLIENT_INSTALL_PATH": steamPath,
"STEAM_COMPAT_APP_ID": BS_APP_ID,
// Run game in steam environment; fixes #585 for unicode song titles
"SteamEnv": 1,
});
"SteamEnv": "1",
};
if (launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)) {
Object.assign(env, {
"PROTON_LOG": 1,
"PROTON_LOG_DIR": path.join(bsFolderPath, "Logs"),
});
envVars.PROTON_LOG = "1";
envVars.PROTON_LOG_DIR = path.join(bsFolderPath, "Logs");
}
return envVars;
}
public verifyProtonPath(protonFolder: string = ""): boolean {
@@ -134,11 +144,6 @@ export class LinuxService {
? path.join(compatDataPath, "pfx") : "";
}
public getProtonPrefix(): string {
// Set in setupLaunch
return this.protonPrefix;
}
// === NixOS Specific === //
public async isNixOS(): Promise<boolean> {
@@ -159,4 +164,50 @@ export class LinuxService {
return this.nixOS;
}
// === Shortcuts === //
public async createDesktopShortcut(
shortcutPath: string,
name: string,
icon: string,
launchOptions: LaunchOption,
steamPath: string,
bsFolderPath: string
): Promise<boolean> {
try {
const {
protonPrefix, env
} = await this.setupLaunch(launchOptions, steamPath, bsFolderPath);
Object.assign(env, {
"SteamAppId": BS_APP_ID,
"SteamOverlayGameId": BS_APP_ID,
"SteamGameId": BS_APP_ID,
});
const envString = Object.entries(env)
.map(([ key, value ]) => `${key}="${value}"`)
.join(" ");
const command = `${envString} ${protonPrefix} "${
path.join(bsFolderPath, BS_EXECUTABLE)
}"`;
const desktopEntry = [
"[Desktop Entry]",
"Type=Application",
`Name=${name}`,
`Icon=${icon}`,
`Path=${bsFolderPath}`,
`Exec=${command}`
].join("\n");
await fs.writeFile(shortcutPath, desktopEntry);
log.info("Created shorcut at ", `"${shortcutPath}/${name}"`);
return true;
} catch (error) {
log.error("Could not create shortcut", error);
return false;
}
}
}
+1 -1
View File
@@ -211,7 +211,7 @@ export class SteamService {
for (const userId of userIds) {
const shortcuts = await this.getShortcuts(userId).catch(e => {
log.warn("Error while reading shortcuts", e);
return [];
return [] as SteamShortcut[];
});
shortcuts.push(new SteamShortcut(shortcutData));