diff --git a/electron-builder.config.js b/electron-builder.config.js index 4775cb77..465ba255 100644 --- a/electron-builder.config.js +++ b/electron-builder.config.js @@ -63,7 +63,7 @@ const config = { // Read/write home directory access "--filesystem=~/BSManager:create", // Default BSManager installation folder "--filesystem=~/.steam/steam/steamapps:ro", // for the libraryfolders.vdf - "--filesystem=~/.steam/steam/steamapps/common:ro", // Steam game folder + "--filesystem=~/.steam/steam/steamapps/common:create", // Steam game folder "--filesystem=~/.steam/steam/steamapps/common/Beat Saber:create", // For installing mods/maps to original Beat Saber version // Allow communication with network "--share=network", diff --git a/src/__tests__/unit/os.test.ts b/src/__tests__/unit/os.test.ts index 00dd25db..616b14b6 100644 --- a/src/__tests__/unit/os.test.ts +++ b/src/__tests__/unit/os.test.ts @@ -196,8 +196,14 @@ ifDescribe(IS_LINUX)("Test os.helpers isProcessRunning", () => { const running = await isProcessRunning(`bs-manager-${crypto.randomUUID()}`); expect(running).toBe(false); - // No errors received - expect(logSpy).toHaveBeenCalledTimes(0); + // Throws because grep couldn't find any process with that name + expect(logSpy).toHaveBeenCalledTimes(1); }); + + it("Empty process name", async () => { + const running = await isProcessRunning(""); + expect(running).toBe(false); + expect(logSpy).toHaveBeenCalledTimes(0); + }) }); diff --git a/src/main/helpers/os.helpers.ts b/src/main/helpers/os.helpers.ts index 1e5a9638..6f22ff4f 100644 --- a/src/main/helpers/os.helpers.ts +++ b/src/main/helpers/os.helpers.ts @@ -3,9 +3,6 @@ import log from "electron-log"; import psList from "ps-list"; import { IS_FLATPAK } from "main/constants"; -// There are 2 erroneous lines ps | grep which is both the ps and grep calls themselves -const MIN_PROCESS_COUNT_LINUX = 2; - type LinuxOptions = { // Add the prefix to the command // eg. command - "./Beat Saber.exe" --no-yeet, prefix - "path/to/proton" run @@ -101,14 +98,24 @@ export function bsmExec(command: string, options?: BsmExecOptions): Promise<{ }); } +// Transform command from "steam" to "[s]team" +// NOTE: Can add an option to isProcessRunning/getProcessId to ignore this transformation +// in the future if needed +const transformProcessNameForPS = (name: string) => `[${name.at(0)}]${name.substring(1)}`; + async function isProcessRunningLinux(name: string): Promise { + if (!name) { + return false; + } + try { - const { stdout: count } = await bsmExec(`ps awwxo args | grep -c "${name}"`, { + const processName = transformProcessNameForPS(name); + const { stdout: count } = await bsmExec(`ps awwxo args | grep -c "${processName}"`, { log: true, flatpak: { host: IS_FLATPAK }, }); - return +count.trim() > MIN_PROCESS_COUNT_LINUX; + return +count.trim() > 0; } catch(error) { log.error(error); return false; @@ -143,14 +150,22 @@ async function isProcessRunningWindows(name: string): Promise { } async function getProcessIdLinux(name: string): Promise { + if (!name) { + return null; + } + try { - const { stdout } = await bsmExec(`ps awwxo pid,args | grep "${name}"`, { + const processName = transformProcessNameForPS(name); + const { stdout } = await bsmExec(`ps awwxo pid,args | grep "${processName}"`, { log: true, flatpak: { host: IS_FLATPAK }, }); + if (!stdout) { + return null; + } + const line = stdout.split("\n") - .slice(0, -MIN_PROCESS_COUNT_LINUX) .map(line => line.trimStart()) .find(line => line.includes(name) && !line.includes("grep")); return line ? +line.split(" ").at(0) : null; diff --git a/src/main/services/bs-launcher/abstract-launcher.service.ts b/src/main/services/bs-launcher/abstract-launcher.service.ts index 347c17eb..6ed46121 100644 --- a/src/main/services/bs-launcher/abstract-launcher.service.ts +++ b/src/main/services/bs-launcher/abstract-launcher.service.ts @@ -52,7 +52,7 @@ export abstract class AbstractLauncherService { spawnOptions.shell = true; // For windows to spawn properly return bsmSpawn(`"${bsExePath}"`, { args, options: spawnOptions, log: true, - linux: { prefix: this.linux.getProtonCommand() }, + linux: { prefix: this.linux.getProtonPrefix() }, flatpak: { host: IS_FLATPAK, env: [ diff --git a/src/main/services/bs-launcher/steam-launcher.service.ts b/src/main/services/bs-launcher/steam-launcher.service.ts index 57075038..8b8816cf 100644 --- a/src/main/services/bs-launcher/steam-launcher.service.ts +++ b/src/main/services/bs-launcher/steam-launcher.service.ts @@ -106,7 +106,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto // Linux setup if (process.platform === "linux") { - this.linux.setupLaunch(launchOptions, steamPath, bsFolderPath, env); + await this.linux.setupLaunch(launchOptions, steamPath, bsFolderPath, env); } obs.next({type: BSLaunchEvent.BS_LAUNCHING}); diff --git a/src/main/services/bs-version-lib.service.ts b/src/main/services/bs-version-lib.service.ts index eac8d0f1..a8ca4ce8 100644 --- a/src/main/services/bs-version-lib.service.ts +++ b/src/main/services/bs-version-lib.service.ts @@ -3,10 +3,11 @@ import path from "path"; import { writeFileSync } from "fs"; import { BSVersion } from "shared/bs-version.interface"; import { RequestService } from "./request.service"; -import { pathExistsSync, readJSON } from "fs-extra"; +import { readJSON } from "fs-extra"; import { allSettled } from "../../shared/helpers/promise.helpers"; import { LinuxService } from "./linux.service"; import { IS_FLATPAK } from "main/constants"; +import { StaticConfigurationService } from "./static-configuration.service"; export class BSVersionLibService { private readonly REMOTE_BS_VERSIONS_URL: string = "https://raw.githubusercontent.com/Zagrios/bs-manager/master/assets/jsons/bs-versions.json"; @@ -14,9 +15,10 @@ export class BSVersionLibService { private static instance: BSVersionLibService; - private linuxService: LinuxService; - private utilsService: UtilsService; - private requestService: RequestService; + private readonly linuxService: LinuxService; + private readonly utilsService: UtilsService; + private readonly requestService: RequestService; + private readonly configService: StaticConfigurationService; private bsVersions: BSVersion[]; @@ -24,6 +26,7 @@ export class BSVersionLibService { this.linuxService = LinuxService.getInstance(); this.utilsService = UtilsService.getInstance(); this.requestService = RequestService.getInstance(); + this.configService = StaticConfigurationService.getInstance(); } public static getInstance(): BSVersionLibService { @@ -37,25 +40,32 @@ export class BSVersionLibService { return this.requestService.getJSON(this.REMOTE_BS_VERSIONS_URL).then(res => res.data); } + private async shouldLoadFromConfig(): Promise { + // Some special cases of readonly memory installations + return IS_FLATPAK || this.linuxService.isNixOS(); + } + private async getLocalVersions(): Promise { - if (IS_FLATPAK) { - const flatpakVersionsPath = path.join(this.linuxService.getFlatpakLocalVersionFolder(), this.VERSIONS_FILE); - if (pathExistsSync(flatpakVersionsPath)) { - return readJSON(flatpakVersionsPath); - } + const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE); + + if (!(await this.shouldLoadFromConfig())) { + return readJSON(localVersionsPath); } - const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE); - return readJSON(localVersionsPath); + let versions = this.configService.get("versions"); + if (!versions) { + versions = await readJSON(localVersionsPath) + } + return versions; } private async updateLocalVersions(versions: BSVersion[]): Promise { - const localVersionsPath = path.join( - IS_FLATPAK - ? this.linuxService.getFlatpakLocalVersionFolder() - : this.utilsService.getAssestsJsonsPath(), - this.VERSIONS_FILE - ); + if (await this.shouldLoadFromConfig()) { + this.configService.set("versions", versions); + return; + } + + const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE); writeFileSync(localVersionsPath, JSON.stringify(versions, null, "\t"), { encoding: "utf-8", flag: "w" }); } diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts index f08fba3d..88b22df3 100644 --- a/src/main/services/linux.service.ts +++ b/src/main/services/linux.service.ts @@ -1,12 +1,11 @@ import fs from "fs-extra"; import log from "electron-log"; import path from "path"; -import { BS_APP_ID, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants"; +import { BS_APP_ID, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants"; import { StaticConfigurationService } from "./static-configuration.service"; import { CustomError } from "shared/models/exceptions/custom-error.class"; import { BSLaunchError, LaunchOption } from "shared/models/bs-launch"; -import { app } from "electron"; -import config from "../../../electron-builder.config"; +import { bsmExec } from "main/helpers/os.helpers"; export class LinuxService { private static instance: LinuxService; @@ -19,7 +18,9 @@ export class LinuxService { } private readonly staticConfig: StaticConfigurationService; - private protonCommand = ""; + private protonPrefix = ""; + + private nixOS: boolean | undefined; private constructor() { this.staticConfig = StaticConfigurationService.getInstance(); @@ -27,7 +28,7 @@ export class LinuxService { // === Launching === // - public setupLaunch( + public async setupLaunch( launchOptions: LaunchOption, steamPath: string, bsFolderPath: string, @@ -64,7 +65,10 @@ export class LinuxService { BSLaunchError.PROTON_NOT_FOUND ); } - this.protonCommand = `"${protonPath}" run`; + + this.protonPrefix = await this.isNixOS() + ? `steam-run "${protonPath}" run` + : `"${protonPath}" run`; // Setup Proton environment variables Object.assign(env, { @@ -111,19 +115,29 @@ export class LinuxService { return winePath; } - public getProtonCommand(): string { + public getProtonPrefix(): string { // Set in setupLaunch - return this.protonCommand; + return this.protonPrefix; } - // === Flatpak Specific === // + // === NixOS Specific === // - public getFlatpakLocalVersionFolder(): string { - return path.join( - app.getPath("home"), - ".var", "app", config.appId, - "resources", "assets", "jsons" - ); + public async isNixOS(): Promise { + if (this.nixOS !== undefined) { + return this.nixOS; + } + + try { + await bsmExec("nixos-version", { + log: true, + flatpak: { host: IS_FLATPAK }, + }); + this.nixOS = true; + } catch (error) { + log.info("Not NixOS", error); + this.nixOS = false; + } + + return this.nixOS; } - } diff --git a/src/main/services/static-configuration.service.ts b/src/main/services/static-configuration.service.ts index c1386130..1748b42d 100644 --- a/src/main/services/static-configuration.service.ts +++ b/src/main/services/static-configuration.service.ts @@ -4,6 +4,7 @@ import path from "path"; import { PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants"; import { Observable, Subject } from "rxjs"; import { CustomError } from "shared/models/exceptions/custom-error.class"; +import { BSVersion } from "shared/bs-version.interface"; export class StaticConfigurationService { private static instance: StaticConfigurationService; @@ -90,6 +91,7 @@ export interface StaticConfigKeyValues { // Linux Specific static configs "proton-folder": string; + "versions": BSVersion[]; }; export type StaticConfigKeys = keyof StaticConfigKeyValues;