From 65d26dc8190602f91f98c67ff88cb731411cd3ff Mon Sep 17 00:00:00 2001 From: silentrald Date: Thu, 9 Jan 2025 23:42:56 +0800 Subject: [PATCH 1/3] [bugfix-733] manually set WINEPREFIX when executing IPA.exe --- .eslintrc.js | 1 + src/__tests__/unit/os.test.ts | 9 ++-- src/main/helpers/os.helpers.ts | 47 +++++++++++-------- .../bs-launcher/abstract-launcher.service.ts | 4 +- src/main/services/linux.service.ts | 35 +++++++++----- .../services/mods/bs-mods-manager.service.ts | 26 ++++++---- 6 files changed, 77 insertions(+), 45 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index f06e496db..7cfacf2f4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -61,6 +61,7 @@ module.exports = { "react/button-has-type": "off", "max-classes-per-file": "off", "jest/no-standalone-expect": "off", + "no-bitwise": "off", }, parserOptions: { ecmaVersion: 2020, diff --git a/src/__tests__/unit/os.test.ts b/src/__tests__/unit/os.test.ts index 616b14b63..06f0d93be 100644 --- a/src/__tests__/unit/os.test.ts +++ b/src/__tests__/unit/os.test.ts @@ -1,4 +1,5 @@ import { + BsmShellLog, bsmSpawn, // bsmExec, isProcessRunning, @@ -76,7 +77,7 @@ describe("Test os.helpers bsmSpawn", () => { it("Simple spawn command with logging", () => { bsmSpawn("mkdir", { args: ["new_folder"], - log: true, + log: BsmShellLog.Command, }); expect(spawnSpy).toHaveBeenCalledTimes(1); expect(spawnSpy).toHaveBeenCalledWith("mkdir new_folder", expect.anything()); @@ -86,7 +87,7 @@ describe("Test os.helpers bsmSpawn", () => { it("Complex spawn command call (Mods install)", () => { bsmSpawn(`"./BSIPA.exe" "./Beat Saber.exe" -n`, { - log: true, + log: BsmShellLog.Command, linux: { prefix: `"./wine64"` }, }); @@ -109,7 +110,7 @@ describe("Test os.helpers bsmSpawn", () => { detached: true, env: BS_ENV, }, - log: true, + log: BsmShellLog.Command, linux: { prefix: `"./proton" run` }, }); @@ -152,7 +153,7 @@ describe("Test os.helpers bsmSpawn", () => { detached: true, env: newEnv, }, - log: true, + log: BsmShellLog.Command, linux: { prefix: `"./proton" run` }, flatpak: { host: true, diff --git a/src/main/helpers/os.helpers.ts b/src/main/helpers/os.helpers.ts index 99a122587..cdafd0ac0 100644 --- a/src/main/helpers/os.helpers.ts +++ b/src/main/helpers/os.helpers.ts @@ -18,22 +18,23 @@ type FlatpakOptions = { env?: string[]; }; -export type BsmSpawnOptions = { - args?: string[]; - options?: cp.SpawnOptions; - log?: boolean; - linux?: LinuxOptions; - flatpak?: FlatpakOptions; +export enum BsmShellLog { + Command = 0x0000_0001, + EnvVariables = 0x0000_0002, }; -export type BsmExecOptions = { +interface BsmShellOptions { args?: string[]; - options?: cp.ExecOptions; - log?: boolean; + options?: OptionsType; + // Look into BsmShellLog values + log?: number; linux?: LinuxOptions; flatpak?: FlatpakOptions; }; +export type BsmSpawnOptions = BsmShellOptions; +export type BsmExecOptions = BsmShellOptions; + function updateCommand(command: string, options: BsmSpawnOptions) { if (options?.args) { command += ` ${options.args.join(" ")}`; @@ -63,14 +64,25 @@ function updateCommand(command: string, options: BsmSpawnOptions) { return command; } +function logValues(shell: "spawn" | "exec", command: string, options?: BsmShellOptions) { + const platform = process.platform === "win32" ? "Windows" : "Linux"; + const optionsLog = options?.log || 0; + + if ((optionsLog & BsmShellLog.EnvVariables) > 0) { + log.info(platform, shell, "env", options?.options?.env); + } + + if ((optionsLog & BsmShellLog.Command) > 0) { + log.info(platform, shell, "command\n>", command); + } +} + export function bsmSpawn(command: string, options?: BsmSpawnOptions) { options = options || {}; options.options = options.options || {}; command = updateCommand(command, options); - if (options?.log) { - log.info(process.platform === "win32" ? "Windows" : "Linux", "spawn command\n>", command); - } + logValues("spawn", command, options); return cp.spawn(command, options.options); } @@ -83,12 +95,7 @@ export function bsmExec(command: string, options?: BsmExecOptions): Promise<{ options.options = options.options || {}; command = updateCommand(command, options); - if (options?.log) { - log.info( - process.platform === "win32" ? "Windows" : "Linux", - "exec command\n>", command - ); - } + logValues("exec", command, options); return new Promise((resolve, reject) => { cp.exec(command, options?.options || {}, (error: Error, stdout: string, stderr: string) => { @@ -111,7 +118,7 @@ async function isProcessRunningLinux(name: string): Promise { try { const processName = transformProcessNameForPS(name); const { stdout: count } = await bsmExec(`ps awwxo args | grep -c "${processName}"`, { - log: true, + log: BsmShellLog.Command, flatpak: { host: IS_FLATPAK }, }); @@ -157,7 +164,7 @@ async function getProcessIdLinux(name: string): Promise { try { const processName = transformProcessNameForPS(name); const { stdout } = await bsmExec(`ps awwxo pid,args | grep "${processName}"`, { - log: true, + log: BsmShellLog.Command, flatpak: { host: IS_FLATPAK }, }); diff --git a/src/main/services/bs-launcher/abstract-launcher.service.ts b/src/main/services/bs-launcher/abstract-launcher.service.ts index 9f6d8a849..cf4ee3655 100644 --- a/src/main/services/bs-launcher/abstract-launcher.service.ts +++ b/src/main/services/bs-launcher/abstract-launcher.service.ts @@ -5,7 +5,7 @@ import path from "path"; import log from "electron-log"; import { sToMs } from "../../../shared/helpers/time.helpers"; import { LinuxService } from "../linux.service"; -import { bsmSpawn } from "main/helpers/os.helpers"; +import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers"; import { IS_FLATPAK } from "main/constants"; import { LaunchMods } from "shared/models/bs-launch/launch-option.interface"; @@ -56,7 +56,7 @@ export abstract class AbstractLauncherService { spawnOptions.shell = true; // For windows to spawn properly return bsmSpawn(`"${bsExePath}"`, { - args, options: spawnOptions, log: true, + args, options: spawnOptions, log: BsmShellLog.Command, linux: { prefix: this.linux.getProtonPrefix() }, flatpak: { host: IS_FLATPAK, diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts index 0dba7c0f4..497860812 100644 --- a/src/main/services/linux.service.ts +++ b/src/main/services/linux.service.ts @@ -3,9 +3,10 @@ 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 { StaticConfigurationService } from "./static-configuration.service"; +import { SteamService } from "./steam.service"; import { CustomError } from "shared/models/exceptions/custom-error.class"; import { BSLaunchError, LaunchOption } from "shared/models/bs-launch"; -import { bsmExec } from "main/helpers/os.helpers"; +import { BsmShellLog, bsmExec } from "main/helpers/os.helpers"; import { LaunchMods } from "shared/models/bs-launch/launch-option.interface"; export class LinuxService { @@ -19,16 +20,31 @@ export class LinuxService { } private readonly staticConfig: StaticConfigurationService; + private readonly steamService: SteamService; private protonPrefix = ""; private nixOS: boolean | undefined; private constructor() { this.staticConfig = StaticConfigurationService.getInstance(); + this.steamService = SteamService.getInstance(); } // === Launching === // + private getCompatDataPath(steamPath: 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 = path.join(steamPath, "steamapps", "compatdata", BS_APP_ID); + if (!fs.existsSync(compatDataPath)) { + log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`); + fs.mkdirSync(compatDataPath); + } + return compatDataPath; + } + public async setupLaunch( launchOptions: LaunchOption, steamPath: string, @@ -40,15 +56,7 @@ export class LinuxService { 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 = `${steamPath}/steamapps/compatdata/${BS_APP_ID}`; - if (!fs.existsSync(compatDataPath)) { - log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`); - fs.mkdirSync(compatDataPath); - } + const compatDataPath = this.getCompatDataPath(steamPath); if (!this.staticConfig.has("proton-folder")) { throw CustomError.fromError( @@ -120,6 +128,11 @@ export class LinuxService { return winePath; } + public async getWinePrefixPath(): Promise { + const compatDataPath = this.getCompatDataPath(await this.steamService.getSteamPath()); + return path.join(compatDataPath, "pfx"); + } + public getProtonPrefix(): string { // Set in setupLaunch return this.protonPrefix; @@ -134,7 +147,7 @@ export class LinuxService { try { await bsmExec("nixos-version", { - log: true, + log: BsmShellLog.Command, flatpak: { host: IS_FLATPAK }, }); this.nixOS = true; diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts index df83de17e..60983370a 100644 --- a/src/main/services/mods/bs-mods-manager.service.ts +++ b/src/main/services/mods/bs-mods-manager.service.ts @@ -17,7 +17,7 @@ import { LinuxService } from "../linux.service"; import { tryit } from "shared/helpers/error.helpers"; import crypto from "crypto"; import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class"; -import { bsmSpawn } from "main/helpers/os.helpers"; +import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers"; import { BbmFullMod, BbmModVersion, ExternalMod } from "../../../shared/models/mods/mod.interface"; export class BsModsManagerService { @@ -139,25 +139,35 @@ export class BsModsManagerService { return false; } + const env: Record = {}; const cmd = `"${ipaPath}" "${bsExePath}" ${args.join(" ")}`; let winePath: string = ""; if (process.platform === "linux") { - const { error, result } = tryit(() => this.linuxService.getWinePath()); - if (error) { - log.error(error); + const { error: winePathError, result: winePathResult } = + tryit(() => this.linuxService.getWinePath()); + if (winePathError) { + log.error(winePathError); return false; } - winePath = `"${result}"`; + winePath = `"${winePathResult}"`; + + const { error: winePrefixError, result: winePrefixResult } = + await tryit(async () => this.linuxService.getWinePrefixPath()); + if (winePrefixError) { + log.warn("Could not get WINEPREFIX value", winePrefixError); + } else { + env.WINEPREFIX = winePrefixResult; + } } return new Promise(resolve => { - log.info("START IPA PROCESS", cmd); const processIPA = bsmSpawn(cmd, { - log: true, + log: BsmShellLog.Command | BsmShellLog.EnvVariables, options: { cwd: versionPath, detached: true, - shell: true + shell: true, + env }, linux: { prefix: winePath }, }); From f2ad6c15317c005e348366d4d1a59fa78b56e117 Mon Sep 17 00:00:00 2001 From: silentrald Date: Thu, 9 Jan 2025 23:55:17 +0800 Subject: [PATCH 2/3] [bugfix-733] fix issue with unit tests --- src/__tests__/unit/os.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/unit/os.test.ts b/src/__tests__/unit/os.test.ts index 06f0d93be..ec4a89bc1 100644 --- a/src/__tests__/unit/os.test.ts +++ b/src/__tests__/unit/os.test.ts @@ -24,7 +24,7 @@ jest.mock("electron-log", () => ({ info: jest.fn(), error: jest.fn(), })); -jest.mock("ps-list", () => () => []); +jest.mock("ps-list", (): (() => any[]) => () => []); const IS_WINDOWS = process.platform === "win32"; const IS_LINUX = process.platform === "linux"; From fb7f68cd04162f1e742c18e54ac08032cca71782 Mon Sep 17 00:00:00 2001 From: silentrald Date: Fri, 10 Jan 2025 09:53:22 +0800 Subject: [PATCH 3/3] [bugfix-733] point to the correct compat folder --- src/main/services/linux.service.ts | 10 +++++----- src/main/services/steam.service.ts | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts index 497860812..5498a0d0f 100644 --- a/src/main/services/linux.service.ts +++ b/src/main/services/linux.service.ts @@ -32,12 +32,13 @@ export class LinuxService { // === Launching === // - private getCompatDataPath(steamPath: string) { + private async getCompatDataPath() { // 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 = path.join(steamPath, "steamapps", "compatdata", BS_APP_ID); + const commonFolder = await this.steamService.getGameFolder(BS_APP_ID); + const compatDataPath = path.resolve(commonFolder, "..", "compatdata", BS_APP_ID); if (!fs.existsSync(compatDataPath)) { log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`); fs.mkdirSync(compatDataPath); @@ -56,7 +57,7 @@ export class LinuxService { launchOptions.admin = false; } - const compatDataPath = this.getCompatDataPath(steamPath); + const compatDataPath = this.getCompatDataPath(); if (!this.staticConfig.has("proton-folder")) { throw CustomError.fromError( @@ -129,8 +130,7 @@ export class LinuxService { } public async getWinePrefixPath(): Promise { - const compatDataPath = this.getCompatDataPath(await this.steamService.getSteamPath()); - return path.join(compatDataPath, "pfx"); + return path.join(await this.getCompatDataPath(), "pfx"); } public getProtonPrefix(): string { diff --git a/src/main/services/steam.service.ts b/src/main/services/steam.service.ts index 68cc012d3..9298f8c86 100644 --- a/src/main/services/steam.service.ts +++ b/src/main/services/steam.service.ts @@ -119,7 +119,8 @@ export class SteamService { } if (libraryFolders[libKey].apps[gameId] != null) { - return path.join(libraryFolders[libKey].path, "steamapps", "common", gameFolder); + const commonFolder = path.join(libraryFolders[libKey].path, "steamapps", "common"); + return gameFolder ? path.join(commonFolder, gameFolder) : commonFolder; } }