mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[bugfix-627] fixed issue with running the BS exe in flatpak
moved all launching logic to the LinuxService
This commit is contained in:
@@ -53,6 +53,25 @@ const config: Configuration = {
|
||||
deb: {
|
||||
fpm: ["--after-install=build/after-install.sh"],
|
||||
},
|
||||
flatpak: {
|
||||
finishArgs: [
|
||||
// Wayland/X11 Rendering
|
||||
"--socket=wayland",
|
||||
"--socket=x11",
|
||||
"--share=ipc",
|
||||
// Open GL
|
||||
"--device=dri",
|
||||
// Audio output
|
||||
"--socket=pulseaudio",
|
||||
// Read/write home directory access
|
||||
"--filesystem=home",
|
||||
// Allow communication with network
|
||||
"--share=network",
|
||||
// System notifications with libnotify
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
"--talk-name=org.freedesktop.Flatpak",
|
||||
]
|
||||
},
|
||||
directories: {
|
||||
app: "release/app",
|
||||
buildResources: "assets",
|
||||
|
||||
@@ -4,12 +4,15 @@ import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio, spawn } from
|
||||
import path from "path";
|
||||
import log from "electron-log";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { LinuxService } from "../linux.service";
|
||||
|
||||
export abstract class AbstractLauncherService {
|
||||
|
||||
protected readonly linux = LinuxService.getInstance();
|
||||
protected readonly localVersions = BSLocalVersionService.getInstance();
|
||||
|
||||
constructor(){
|
||||
this.linux = LinuxService.getInstance();
|
||||
this.localVersions = BSLocalVersionService.getInstance();
|
||||
}
|
||||
|
||||
@@ -44,10 +47,12 @@ export abstract class AbstractLauncherService {
|
||||
spawnOptions.windowsVerbatimArguments = true;
|
||||
}
|
||||
|
||||
log.info(`Launch BS exe at ${bsExePath} with args ${args?.join(" ")}`);
|
||||
if (process.platform === "linux") {
|
||||
return this.linux.spawnBsProcess(bsExePath, args, spawnOptions)
|
||||
}
|
||||
|
||||
log.info("Windows launch BS command\n>" ,bsExePath, args?.join(" "));
|
||||
return spawn(bsExePath, args, spawnOptions);
|
||||
|
||||
}
|
||||
|
||||
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { BSLaunchError, BSLaunchEvent, BSLaunchEventData, BSLaunchWarning, LaunchOption } from "../../../shared/models/bs-launch";
|
||||
import { StoreLauncherInterface } from "./store-launcher.interface";
|
||||
import { pathExists, pathExistsSync, rename } from "fs-extra";
|
||||
import { pathExists, rename } from "fs-extra";
|
||||
import { SteamService } from "../steam.service";
|
||||
import path from "path";
|
||||
import { BS_APP_ID, BS_EXECUTABLE, PROTON_BINARY_PREFIX, STEAMVR_APP_ID } from "../../constants";
|
||||
import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants";
|
||||
import log from "electron-log";
|
||||
import { AbstractLauncherService } from "./abstract-launcher.service";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import { exec } from "child_process";
|
||||
import fs from 'fs';
|
||||
import { StaticConfigurationService } from "../static-configuration.service";
|
||||
|
||||
export class SteamLauncherService extends AbstractLauncherService implements StoreLauncherInterface{
|
||||
|
||||
@@ -24,13 +22,11 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
return SteamLauncherService.instance;
|
||||
}
|
||||
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
private readonly steam: SteamService;
|
||||
private readonly util: UtilsService;
|
||||
|
||||
private constructor(){
|
||||
super();
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
this.steam = SteamService.getInstance();
|
||||
this.util = UtilsService.getInstance();
|
||||
}
|
||||
@@ -71,10 +67,10 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
return new Observable<BSLaunchEventData>(obs => {(async () => {
|
||||
|
||||
const bsFolderPath = await this.localVersions.getInstalledVersionPath(launchOptions.version);
|
||||
let exePath = path.join(bsFolderPath, BS_EXECUTABLE);
|
||||
const bsExePath = path.join(bsFolderPath, BS_EXECUTABLE);
|
||||
|
||||
if(!(await pathExists(exePath))){
|
||||
throw CustomError.fromError(new Error(`Path not exist : ${exePath}`), BSLaunchError.BS_NOT_FOUND);
|
||||
if(!(await pathExists(bsExePath))){
|
||||
throw CustomError.fromError(new Error(`Path not exist : ${bsExePath}`), BSLaunchError.BS_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Open Steam if not running
|
||||
@@ -98,7 +94,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
await this.restoreSteamVR().catch(log.error);
|
||||
}
|
||||
|
||||
let launchArgs = this.buildBsLaunchArgs(launchOptions);
|
||||
const launchArgs = this.buildBsLaunchArgs(launchOptions);
|
||||
const steamPath = await this.steam.getSteamPath();
|
||||
|
||||
const env = {
|
||||
@@ -110,52 +106,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
|
||||
// Linux setup
|
||||
if (process.platform === "linux") {
|
||||
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 = `${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);
|
||||
}
|
||||
|
||||
// proton run BeatSaber.exe
|
||||
launchArgs = [
|
||||
"run",
|
||||
`${exePath}`,
|
||||
...launchArgs,
|
||||
];
|
||||
|
||||
if (!this.staticConfig.has("proton-folder")) {
|
||||
throw CustomError.fromError(new Error("Proton folder not set"), BSLaunchError.PROTON_NOT_SET);
|
||||
}
|
||||
exePath = path.join(this.staticConfig.get("proton-folder"), PROTON_BINARY_PREFIX);
|
||||
if (!pathExistsSync(exePath)) {
|
||||
throw CustomError.fromError(
|
||||
new Error("Could not locate proton binary"),
|
||||
BSLaunchError.PROTON_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
// Setup Proton environment variables
|
||||
Object.assign(env, {
|
||||
"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",
|
||||
// Uncomment these to create a proton log file in the Beat Saber install directory.
|
||||
// "PROTON_LOG": 1,
|
||||
// "PROTON_LOG_DIR": bsFolderPath,
|
||||
});
|
||||
this.linux.setupLaunch(launchOptions, steamPath, bsFolderPath, env);
|
||||
}
|
||||
|
||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||
@@ -163,10 +114,10 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
const spawnOpts = { env, cwd: bsFolderPath };
|
||||
|
||||
const launchPromise = !launchOptions.admin ? (
|
||||
this.launchBs(exePath, launchArgs, spawnOpts).exit
|
||||
this.launchBs(bsExePath, launchArgs, spawnOpts).exit
|
||||
) : (
|
||||
new Promise<number>(resolve => {
|
||||
const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${exePath}" ${launchArgs.join(" ")}`, spawnOpts);
|
||||
const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${bsExePath}" ${launchArgs.join(" ")}`, spawnOpts);
|
||||
adminProcess.on("error", err => {
|
||||
log.error("Error while starting BS as Admin", err);
|
||||
resolve(-1)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from "fs-extra";
|
||||
import log from "electron-log";
|
||||
import path from "path";
|
||||
import { pathExistsSync } from "fs-extra";
|
||||
import { PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||
import { SpawnOptionsWithoutStdio, spawn } from "child_process";
|
||||
import { BS_APP_ID, 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";
|
||||
|
||||
export class LinuxService {
|
||||
private static instance: LinuxService;
|
||||
@@ -15,10 +19,104 @@ export class LinuxService {
|
||||
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
|
||||
public readonly isFlatpak = process.env.container === "flatpak";
|
||||
|
||||
private constructor() {
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
}
|
||||
|
||||
// === Launching === //
|
||||
|
||||
public setupLaunch(
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
bsFolderPath: 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 = `${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);
|
||||
}
|
||||
|
||||
if (!this.staticConfig.has("proton-folder")) {
|
||||
throw CustomError.fromError(
|
||||
new Error("Proton folder not set"),
|
||||
BSLaunchError.PROTON_NOT_SET
|
||||
);
|
||||
}
|
||||
const protonPath = path.join(this.staticConfig.get("proton-folder"), PROTON_BINARY_PREFIX);
|
||||
if (!fs.pathExistsSync(protonPath)) {
|
||||
throw CustomError.fromError(
|
||||
new Error("Could not locate proton binary"),
|
||||
BSLaunchError.PROTON_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
// Setup Proton environment variables
|
||||
Object.assign(env, {
|
||||
"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",
|
||||
// Uncomment these to create a proton log file in the Beat Saber install directory.
|
||||
// "PROTON_LOG": 1,
|
||||
// "PROTON_LOG_DIR": bsFolderPath,
|
||||
});
|
||||
}
|
||||
|
||||
public spawnBsProcess(bsExePath: string, args: string[], spawnOptions: SpawnOptionsWithoutStdio) {
|
||||
// Already checked in setupLaunch
|
||||
const protonPath = path.join(this.staticConfig.get("proton-folder"), PROTON_BINARY_PREFIX);
|
||||
|
||||
// "/bin/sh" does not see flatpak-spawn
|
||||
// Most Debian and Arch should also support "/bin/bash"
|
||||
spawnOptions.shell = "/bin/bash";
|
||||
|
||||
const command = this.isFlatpak
|
||||
? this.createFlatpakCommand(protonPath, bsExePath, args, spawnOptions)
|
||||
: `"${protonPath}" run "${bsExePath}" ${args.join(" ")}`;
|
||||
|
||||
log.info("Linux launch BS command\n>", command);
|
||||
return spawn(command, spawnOptions);
|
||||
}
|
||||
|
||||
private createFlatpakCommand(protonPath: string, bsExePath: string, args: string[], spawnOptions: SpawnOptionsWithoutStdio): string {
|
||||
|
||||
// DON'T REMOVE: Good for injecting commands while debugging with flatpak
|
||||
// return args.slice(1).join(" ");
|
||||
|
||||
// The env vars are hidden to flatpak-spawn, need to set them manually in --env arg
|
||||
// Minimal copy of the env, don't need to copy them all
|
||||
const envArgs = [
|
||||
"SteamAppId",
|
||||
"SteamOverlayGameId",
|
||||
"SteamGameId",
|
||||
"WINEDLLOVERRIDES",
|
||||
"STEAM_COMPAT_DATA_PATH",
|
||||
"STEAM_COMPAT_INSTALL_PATH",
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
|
||||
"STEAM_COMPAT_APP_ID",
|
||||
"SteamEnv",
|
||||
].map(envName => {
|
||||
return `--env=${envName}="${spawnOptions.env[envName]}"`;
|
||||
}).join(" ");
|
||||
|
||||
return `flatpak-spawn --host ${envArgs} "${protonPath}" run "${bsExePath}" ${args.join(" ")}`;
|
||||
}
|
||||
|
||||
public verifyProtonPath(protonFolder: string = ""): boolean {
|
||||
if (protonFolder === "") {
|
||||
if (!this.staticConfig.has("proton-folder")) {
|
||||
@@ -30,7 +128,7 @@ export class LinuxService {
|
||||
|
||||
const protonPath = path.join(protonFolder, PROTON_BINARY_PREFIX);
|
||||
const winePath = path.join(protonFolder, WINE_BINARY_PREFIX);
|
||||
return pathExistsSync(protonPath) && pathExistsSync(winePath);
|
||||
return fs.pathExistsSync(protonPath) && fs.pathExistsSync(winePath);
|
||||
}
|
||||
|
||||
public getWinePath(): string {
|
||||
@@ -42,7 +140,7 @@ export class LinuxService {
|
||||
this.staticConfig.get("proton-folder"),
|
||||
WINE_BINARY_PREFIX
|
||||
);
|
||||
if (!pathExistsSync(winePath)) {
|
||||
if (!fs.pathExistsSync(winePath)) {
|
||||
throw new Error(`"${winePath}" binary file not found`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user