mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge pull request #586 from silentrald/feat/578
[feat-578] remove dotnet and screen dependency on linux
This commit is contained in:
@@ -16,3 +16,9 @@ export const CACHE_PATH = path.join(app.getPath("userData"), "CachedData");
|
||||
export const IMAGE_CACHE_PATH = path.join(CACHE_PATH, "imagescache");
|
||||
|
||||
export const HTTP_STATUS_CODES = constants;
|
||||
|
||||
// Linux related stuff
|
||||
|
||||
export const PROTON_BINARY_PREFIX = "proton";
|
||||
export const WINE_BINARY_PREFIX = path.join("files", "bin", "wine64");
|
||||
|
||||
|
||||
@@ -14,3 +14,4 @@ import "./model-saber.ipcs";
|
||||
import "./bs-model-ipcs";
|
||||
import "./bs-version-download/bs-download-ipcs";
|
||||
import "./static-configuration.ipcs";
|
||||
import "./linux.ipcs.ts";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LinuxService } from "main/services/linux.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { of } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on("linux.verify-proton-folder", (_, reply) => {
|
||||
const linuxService = LinuxService.getInstance();
|
||||
reply(of(linuxService.verifyProtonPath()));
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { shell, dialog, app, BrowserWindow } from "electron";
|
||||
import { shell, dialog, app, BrowserWindow, OpenDialogOptions } from "electron";
|
||||
import { NotificationService } from "../services/notification.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from, of } from "rxjs";
|
||||
import { readFileSync } from "fs-extra";
|
||||
import log from "electron-log";
|
||||
import path from "path";
|
||||
|
||||
// TODO IMPROVE WINDOW CONTROL BY USING WINDOW SERVICE
|
||||
|
||||
@@ -19,7 +20,20 @@ ipc.on("open-dialog", (args, reply) => {
|
||||
})
|
||||
|
||||
ipc.on("choose-folder", (args, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: args ?? "" })));
|
||||
const options: OpenDialogOptions = {
|
||||
properties: ["openDirectory"],
|
||||
defaultPath: args?.defaultPath ?? ""
|
||||
};
|
||||
|
||||
if (args?.showHidden) {
|
||||
options.properties.push("showHiddenFiles");
|
||||
}
|
||||
|
||||
if (args?.parent) {
|
||||
options.defaultPath = path.join(app.getPath(args.parent), options.defaultPath);
|
||||
}
|
||||
|
||||
reply(from(dialog.showOpenDialog(options)));
|
||||
});
|
||||
|
||||
ipc.on("choose-file", async (args, reply) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { of } from "rxjs";
|
||||
import { from, of } from "rxjs";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { StaticConfigurationService } from "../services/static-configuration.service";
|
||||
|
||||
@@ -10,5 +10,5 @@ ipc.on("static-configuration.get", (args, reply) => {
|
||||
});
|
||||
|
||||
ipc.on("static-configuration.set", (args, reply) => {
|
||||
reply(of(staticConfig.set(args.key, args.value)));
|
||||
reply(from(staticConfig.set(args.key, args.value)));
|
||||
});
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { BSLaunchError, BSLaunchEvent, BSLaunchEventData, BSLaunchWarning, LaunchOption } from "../../../shared/models/bs-launch";
|
||||
import { StoreLauncherInterface } from "./store-launcher.interface";
|
||||
import { pathExists, rename } from "fs-extra";
|
||||
import { pathExists, pathExistsSync, rename } from "fs-extra";
|
||||
import { SteamService } from "../steam.service";
|
||||
import path from "path";
|
||||
import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants";
|
||||
import { BS_APP_ID, BS_EXECUTABLE, PROTON_BINARY_PREFIX, 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{
|
||||
|
||||
@@ -23,11 +24,13 @@ 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();
|
||||
}
|
||||
@@ -128,9 +131,16 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
`${exePath}`,
|
||||
...launchArgs,
|
||||
];
|
||||
exePath = launchOptions.protonPath;
|
||||
if (!exePath) {
|
||||
throw CustomError.fromError(new Error("Proton path not set"), BSLaunchError.PROTON_NOT_SET);
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import path from "path";
|
||||
import { pathExistsSync } from "fs-extra";
|
||||
import { PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||
import { StaticConfigurationService } from "./static-configuration.service";
|
||||
|
||||
export class LinuxService {
|
||||
private static instance: LinuxService;
|
||||
|
||||
public static getInstance(): LinuxService {
|
||||
if (!LinuxService.instance) {
|
||||
LinuxService.instance = new LinuxService();
|
||||
}
|
||||
return LinuxService.instance;
|
||||
}
|
||||
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
|
||||
private constructor() {
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
}
|
||||
|
||||
public verifyProtonPath(protonFolder: string = ""): boolean {
|
||||
if (protonFolder === "") {
|
||||
if (!this.staticConfig.has("proton-folder")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protonFolder = this.staticConfig.get("proton-folder");
|
||||
}
|
||||
|
||||
const protonPath = path.join(protonFolder, PROTON_BINARY_PREFIX);
|
||||
const winePath = path.join(protonFolder, WINE_BINARY_PREFIX);
|
||||
return pathExistsSync(protonPath) && pathExistsSync(winePath);
|
||||
}
|
||||
|
||||
public getWinePath(): string {
|
||||
if (!this.staticConfig.has("proton-folder")) {
|
||||
throw new Error("proton-folder variable not set");
|
||||
}
|
||||
|
||||
const winePath = path.join(
|
||||
this.staticConfig.get("proton-folder"),
|
||||
WINE_BINARY_PREFIX
|
||||
);
|
||||
if (!pathExistsSync(winePath)) {
|
||||
throw new Error(`"${winePath}" binary file not found`);
|
||||
}
|
||||
|
||||
return winePath;
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,15 @@ import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { ensureDir, pathExistsSync } from "fs-extra";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { popElement } from "shared/helpers/array.helpers";
|
||||
import { LinuxService } from "../linux.service";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
|
||||
private readonly beatModsApi: BeatModsApiService;
|
||||
private readonly bsLocalService: BSLocalVersionService;
|
||||
private readonly linuxService: LinuxService;
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
private manifestMatches: Mod[];
|
||||
@@ -37,6 +40,7 @@ export class BsModsManagerService {
|
||||
private constructor() {
|
||||
this.beatModsApi = BeatModsApiService.getInstance();
|
||||
this.bsLocalService = BSLocalVersionService.getInstance();
|
||||
this.linuxService = LinuxService.getInstance();
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
@@ -140,25 +144,34 @@ export class BsModsManagerService {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
const cmd = process.platform === 'linux'
|
||||
? `screen -dmS "BSIPA" dotnet "${ipaPath}" ${args.join(" ")}` // Must run through screen, otherwise BSIPA tries to move console cursor and crashes.
|
||||
: `"${ipaPath}" ${args.join(" ")}`;
|
||||
let cmd = `"${ipaPath}" "${bsExePath}" ${args.join(" ")}`;
|
||||
if (process.platform === "linux") {
|
||||
const { error, result: winePath } = tryit(() => this.linuxService.getWinePath());
|
||||
if (error) {
|
||||
log.error(error);
|
||||
return false;
|
||||
}
|
||||
cmd = `"${winePath}" ${cmd}`;
|
||||
}
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
log.info("START IPA PROCESS", cmd);
|
||||
const processIPA = spawn(cmd, { cwd: versionPath, detached: true, shell: true });
|
||||
|
||||
const timemout = setTimeout(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
log.info("Ipa process timeout");
|
||||
resolve(false)
|
||||
}, sToMs(30));
|
||||
|
||||
processIPA.stdout.on("data", data => {
|
||||
log.info("IPA process stdout", data.toString());
|
||||
});
|
||||
processIPA.stderr.on("data", data => {
|
||||
log.error("IPA process stderr", data.toString());
|
||||
})
|
||||
|
||||
processIPA.once("exit", code => {
|
||||
clearTimeout(timemout);
|
||||
clearTimeout(timeout);
|
||||
if (code === 0) {
|
||||
log.info("Ipa process exist with code 0");
|
||||
return resolve(true);
|
||||
@@ -195,7 +208,7 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
const crypto = require("crypto");
|
||||
const files = await zip.files;
|
||||
const { files } = zip;
|
||||
|
||||
const checkedEntries = (
|
||||
await Promise.all(
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import ElectronStore from "electron-store";
|
||||
import { pathExistsSync } from "fs-extra";
|
||||
import path from "path";
|
||||
import { PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class StaticConfigurationService {
|
||||
private static instance: StaticConfigurationService;
|
||||
@@ -34,7 +38,17 @@ export class StaticConfigurationService {
|
||||
cb(this.get(key));
|
||||
}
|
||||
|
||||
public set<K extends StaticConfigKeys>(key: K, value: StaticConfigKeyValues[K]): void {
|
||||
public async set<K extends StaticConfigKeys>(key: K, value: StaticConfigKeyValues[K]): Promise<void> {
|
||||
// Validate the setters
|
||||
switch (key) {
|
||||
case "proton-folder":
|
||||
this.validateProtonFolder(value as string);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.store.set(key, value);
|
||||
|
||||
if (this.watchers[key]) {
|
||||
@@ -42,6 +56,16 @@ export class StaticConfigurationService {
|
||||
}
|
||||
}
|
||||
|
||||
// Setters with validation
|
||||
|
||||
private validateProtonFolder(protonFolder: string): void {
|
||||
const protonPath = path.join(protonFolder, PROTON_BINARY_PREFIX);
|
||||
const winePath = path.join(protonFolder, WINE_BINARY_PREFIX);
|
||||
if (!pathExistsSync(protonPath) || !pathExistsSync(winePath)) {
|
||||
throw new CustomError("Invalid proton folder path", "invalid-folder");
|
||||
}
|
||||
}
|
||||
|
||||
public delete<K extends StaticConfigKeys>(key: K): void {
|
||||
this.store.delete(key);
|
||||
}
|
||||
@@ -60,12 +84,15 @@ export class StaticConfigurationService {
|
||||
}
|
||||
|
||||
export interface StaticConfigKeyValues {
|
||||
"versions": BSVersion[];
|
||||
"installation-folder": string;
|
||||
"song-details-cache-etag": string;
|
||||
"disable-hadware-acceleration": boolean;
|
||||
"use-symlinks": boolean;
|
||||
}
|
||||
|
||||
// Linux Specific static configs
|
||||
"versions": BSVersion[];
|
||||
"proton-folder": string;
|
||||
};
|
||||
|
||||
export type StaticConfigKeys = keyof StaticConfigKeyValues;
|
||||
|
||||
@@ -77,5 +104,5 @@ export type StaticConfigGetIpcRequestResponse<K extends StaticConfigKeys> = {
|
||||
export type StaticConfigSetIpcRequest<K extends StaticConfigKeys> = {
|
||||
request: { key: K, value: StaticConfigKeyValues[K] };
|
||||
response: void;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user