Compare commits

..

32 Commits

Author SHA1 Message Date
MathieuG-P 127a538509 Merge pull request #679 from beatmaps-io/signed-playlists
Pass query parameters to playlist downloader
2024-12-02 19:57:52 +01:00
Thomas Cheyney 043e300f42 Pass query parameters to playlist downloader 2024-12-02 18:17:56 +00:00
MathieuG-P 6e3d8852de update certificate sha 2024-12-01 19:59:12 +01:00
MathieuG-P e2c2aa7a84 update to 1.5.0-alpha.5 2024-12-01 19:53:33 +01:00
MathieuG-P 2a0de5f2ad update to 1.5.0-alpha.5 2024-12-01 19:52:55 +01:00
MathieuG-P 214f0494c1 Merge pull request #677 from Zagrios/bugfix/error-when-verifying-bs-files
[bugfix] Fix DepotDownloader error when verifying bs files
2024-12-01 19:48:58 +01:00
MathieuG-P 19f2a57c37 [bugfix] Fix DepotDownloader error when verifying bs files 2024-12-01 19:43:14 +01:00
MathieuG-P 922bcf30b4 Build flatpak in the build action 2024-12-01 14:54:09 +01:00
MathieuG-P f31ade5754 Change the app id 2024-12-01 13:09:58 +01:00
MathieuG-P c8e4d8c359 Merge pull request #676 from Zagrios/bugfix/mod-dependencies-are-not-always-installed
[bugfix] Mod dependencies are not always installed
2024-12-01 12:38:37 +01:00
MathieuG-P fc44dcba65 [bugfix] Mod dependencies are not always installed 2024-12-01 12:33:09 +01:00
MathieuG-P b02311456f Add "--publish never" for the package script 2024-12-01 10:22:54 +01:00
MathieuG-P 5462c8821e Merge pull request #629 from silentrald/bugfix/627
[bugfix-627] build BSM with deb and flatpak for linux
2024-11-30 20:02:27 +01:00
silentrald 4afad63753 [chore-627] added electron-builder.config.js to release-linux.yaml 2024-11-29 05:23:19 +08:00
silentrald 438611f4ca [bugfix-627] added original Beat Saber location in flatpak permissions 2024-11-29 05:23:19 +08:00
silentrald 1fdf1f0247 [bugfix-627] fix bsmSpawn for launching game on windows 2024-11-29 05:23:12 +08:00
silentrald 5721891e28 [chore-627] fixed actions with electron-builder 2024-11-22 00:57:31 +08:00
silentrald cba001dc97 [test-627] added test cases for os.helpers.ts 2024-11-22 00:57:31 +08:00
silentrald 38ea360ee6 [bugfix-627] support host calls for flatpak with spawn/exec
* created wrapper calls for spawn and exec for all os support
* refactored names for isProcessRunning, getProcessId, and isSteamRunning
2024-11-22 00:57:31 +08:00
silentrald 8096b3ec76 [chore-627] fix issue with chrome-sandbox permissions 2024-11-22 00:57:31 +08:00
MathieuG-P 8844e7cec3 [bugfix-627] Fix couldn't package and/or publish on windows 2024-11-22 00:57:31 +08:00
silentrald 403995612c [chore] added silentrald and Insprill in credits and linux installation in README file 2024-11-22 00:57:31 +08:00
silentrald 58656294df [bugfix-627] added finer permissions for flatpak
handled issue with write only directory with bs-versions.json
2024-11-22 00:57:31 +08:00
silentrald 84ea7a26b6 [bugfix-627] fixed issue with running the BS exe in flatpak
moved all launching logic to the LinuxService
2024-11-22 00:57:31 +08:00
silentrald 82a19c0c8b [docs-627] updated docs on review
- updated flatpak installation
- moved deb and AUR above flatpak so users with appropriate distros will install those first
- idiot-proofing on how to install flatpak on different distros
2024-11-22 00:57:31 +08:00
silentrald 15f8d83c8d [chore-627] drop rpm and pacman in favor of flatpak 2024-11-22 00:57:31 +08:00
silentrald 196bdc04f5 [docs-627] removed wine installation and updated proton setup 2024-11-22 00:57:31 +08:00
silentrald e6bdf56c6d [chore] move build config to electron-builder.ts for better typing 2024-11-22 00:57:31 +08:00
silentrald 7a85171cd3 [docs] added linux documentation 2024-11-22 00:57:31 +08:00
silentrald 38e0cc56d0 [chore] add write permissions to bs-versions.json after install 2024-11-22 00:57:31 +08:00
silentrald a345a3ee7c [bugfix-627] reverted #577 PR 2024-11-22 00:57:31 +08:00
silentrald cb121a9442 [chore] change target builds to deb, rpm, and pacman 2024-11-22 00:57:31 +08:00
32 changed files with 832 additions and 200 deletions
+1
View File
@@ -60,6 +60,7 @@ module.exports = {
"jsx-a11y/control-has-associated-label": "off",
"react/button-has-type": "off",
"max-classes-per-file": "off",
"jest/no-standalone-expect": "off",
},
parserOptions: {
ecmaVersion: 2020,
+19 -1
View File
@@ -22,11 +22,29 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.11.0
cache: "npm"
- run: npm ci
- run: npm run package
# Install and setup Flatpak for Ubuntu
- name: Install flatpak packages (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get install -y flatpak flatpak-builder
- name: Setup flatpak repo (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: |
flatpak remote-add --if-not-exists --user \
flathub https://flathub.org/repo/flathub.flatpakrepo
# Build Flatpak package
- name: Build Flatpak (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: |
env DEBUG="@malept/flatpak-bundler" \
npx electron-builder --config electron-builder.config.js --publish never --linux flatpak
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
+3 -3
View File
@@ -14,11 +14,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.11.0
cache: "npm"
- run: npm ci
- run: npm run lint
+3 -3
View File
@@ -18,11 +18,11 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.11.0
cache: "npm"
- run: npm ci
- run: npm run build
+16 -2
View File
@@ -20,13 +20,27 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install flatpak packages
run: sudo apt-get install -y flatpak flatpak-builder
- name: Setup flatpak repo
run: |
flatpak remote-add --if-not-exists --user \
flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.11.0
cache: "npm"
- run: npm ci
- run: npm run publish:linux
- run: npm run build
- name: Build deb
run: npx electron-builder --config electron-builder.config.js --publish always --linux deb --x64
- name: Build flatpak
run: env DEBUG="@malept/flatpak-bundler" npx electron-builder --config electron-builder.config.js --publish always --linux flatpak
- name: Upload artifact
uses: actions/upload-artifact@v4
+10
View File
@@ -226,6 +226,10 @@
-->
<div>
<h2><b>How to install?</b></h2>
</div>
<div>
<h3><b>Windows Installation</b></h3>
<ul>
<li>Download the <a href="https://github.com/Zagrios/bs-manager/releases/latest">latest release</a> from <a
href="https://github.com/Zagrios/bs-manager/releases">Releases</a>.</li>
@@ -239,6 +243,10 @@
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/4215384e-eb68-40da-884c-f21df491ef75" />
</div>
<div>
<h3><b>Linux installation</b></h3>
<p>Refer to <a href="https://github.com/Zagrios/bs-manager/wiki/Linux">Linux wiki</a></p>
</div>
<!--
----------------------------------------
@@ -550,6 +558,8 @@
<li><a href="https://github.com/Iluhadesu">Iluhadesu</a> - Co-Developer & Co-Founder, Discord Bot Developer.</li>
<li><a href="https://github.com/GaetanGrd">GaetanGrd</a> - Co-Developer & Co-Founder, Documentation Lead.</li>
<li><a href="https://github.com/cheddZy">cheddZy</a> - Icon Creator.</li>
<li><a href="https://github.com/Insprill">Insprill</a> - Co-Developer, Linux Developer, AUR Maintainer.</li>
<li><a href="https://github.com/silentrald">silentrald</a> - Co-Developer, Linux Developer, deb and flatpak Maintainer.</li>
</ul>
</div>
Binary file not shown.
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
# Add write permissions to anybody so that this can be sync with the
# github's bs-versions.json when starting bsmanager
/usr/bin/chmod +002 /opt/BSManager/resources/assets/jsons/bs-versions.json
# https://github.com/electron/electron/issues/42510
/usr/bin/chmod 4755 /opt/BSManager/chrome-sandbox
+86
View File
@@ -0,0 +1,86 @@
# Linux Guide
## Installation
Go to [Releases](https://github.com/Zagrios/bs-manager/releases) page and go to the latest release. Download the necessary build installer for your distro (see below).
### Ubuntu, Debian (deb)
Download the `.deb` file in the releases and run the following command:
```bash
dpkg -i ./bsmanager.deb
```
### Arch (AUR)
Refer to [bs-manager-git](https://aur.archlinux.org/packages/bs-manager-git).
To install AUR packages, you need to install [yay](https://github.com/Jguer/yay).
### Universal (flatpak)
This should work on any linux distribution. You are only required to have `flatpak` installed in your system. If it is not installed, then go to [flatpak](https://flatpak.org/setup/) to look for a guide on how to install it on your distro.
After installing, download the `.flatpak` file in the releases and run the following command:
```bash
flatpak install --user ./bsmanager.flatpak
```
If you are getting errors like packages not existing, run the command below so that it finds the correct packages.
```bash
sudo flatpak remote-add --if-not-exists --system flathub https://flathub.org/repo/flathub.flatpakrepo
# or
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
```
Flatpak also supports sandboxing which gives the minimal access to your machine. To configure this, you can download [Flatseal](https://flathub.org/apps/com.github.tchx84.Flatseal) which has a GUI to edit your permissions. You can also this with the `flatpak` executable but it will not be discussed here.
## Proton Setup
[Proton](https://github.com/ValveSoftware/Proton) is needed to run the Beat Saber executable under Linux. You need to download this from either from Steam or building it from their GitHub repo.
Once Proton is installed, when you open your BSManager application for the first time, it will ask you to link the _Proton Folder_. The _Proton Folder_ also verifies if the `proton` and `files/bin/wine64` binaries exists. Once set, you should be able to launch the Beat Saber (using `proton`) and install mods (using `files/bin/wine64`). You can still change the _Proton Folder_ in the **settings page** if any new version of Steam Proton is downloaded.
# Troubleshooting
## Permission denied on "bs-versions.json"
<pre>
Unhandled Exception UnhandledRejection Error: EACCES: permission denied, open '/opt/BSManager/resources/assets/jsons/bs-versions.json'
</pre>
To fix this issue, the current user must have write permissions to the "bs-versions.json". To correct the permissions do command below:
```bash
chmod +002 /opt/BSManager/resources/assets/jsons/bs-versions.json
# or
chown $(whoami) /opt/BSManager/resources/assets/jsons/bs-versions.json
```
## [deb] The SUID sandbox helper binary was found.
This is encountered when running the app file or executing the app in the terminal. This is due to a change to Ubuntu 24.04. In order to fix the issue, take a look into the path of "chrome-sandbox" described in the error log and give the correct permissions within the terminal, for example:
```bash
chmod 4755 /opt/BSManager/chrome-sandbox
```
ref: https://github.com/electron/electron/issues/42510
## [Flatpak] Steam Beat Saber version not showing / Proton not detected
Flatpak should have permissions with the steam games folder. By default, the minimum permissions are `~/.steam/steam/steamapps/common:ro` and `~/.steam/steam/steamapps/common:ro`. If you changed the steam installation path, add that path instead into the permissions.
## [Flatpak] Changing installation folder
To change the installation path of the **BSManager** folder, you have to edit the flatpak permissions to destination folder.
- In flatpak or Flatseal, add the destination folder with `:create` permissions.
- In BSM, move the folder to the destination folder.
- [Optional] In flatpak or Flatseal, remove the original folder permissions.
+94
View File
@@ -0,0 +1,94 @@
const config = {
extraResources: [
"./assets/jsons/bs-versions.json",
"./assets/jsons/patreons.json",
"./assets/proto/song_details_cache_v1.proto"
],
productName: "BSManager",
appId: "io.bsmanager.bsmanager",
asarUnpack: "**\\*.{node,dll}",
files: [
"dist/**/*",
"node_modules",
"package.json"
],
afterSign: ".erb/scripts/notarize.js",
afterPack: ".erb/scripts/after-pack.js",
win: {
signingHashAlgorithms: ["sha256"],
target: [
"nsis",
"nsis-web"
],
icon: "./build/icons/win/favicon.ico",
extraResources: [
"./build/icons/win",
"./assets/scripts/*.exe"
],
},
linux: {
target: [
"deb",
],
icon: "./build/icons/png",
category: "Utility;Game;",
extraResources: [
"./build/icons/png",
"./assets/scripts/DepotDownloader"
],
protocols: {
name: "BSManager",
schemes: [
"bsmanager",
"beatsaver",
"bsplaylist",
"modelsaber",
"web+bsmap",
],
},
},
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=~/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/Beat Saber:create", // For installing mods/maps to original Beat Saber version
// 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",
output: "release/build",
},
publish: {
provider: "github",
owner: "Zagrios",
},
fileAssociations: [
{
ext: "bplist",
description: "Beat Saber Playlist (BSManager)",
icon: "./assets/bsm_file.ico",
role: "Viewer",
},
],
};
module.exports = config;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.4",
"version": "1.5.0-alpha.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.0-alpha.4",
"version": "1.5.0-alpha.5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+5 -57
View File
@@ -2,7 +2,7 @@
"name": "bs-manager",
"description": "Manage maps, mods and more for Beat Saber",
"main": "./.erb/dll/main.bundle.dev.js",
"version": "1.5.0-alpha.4",
"version": "1.5.0-alpha.5",
"scripts": {
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
@@ -13,15 +13,15 @@
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
"prestart": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.dev.ts",
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll",
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --config electron-builder.config.js && npm run build:dll",
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
"start:main": "concurrently -k \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon .\"",
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"test": "jest",
"test:unit": "jest ./src/__tests__/unit",
"publish": "npm run build && electron-builder -c.win.certificateSha1=2164d6a7d641ecf6ad57852f665a518ca2bf960f --publish always --win --x64",
"publish:linux": "npm run build && electron-builder --publish always --linux --x64"
"publish": "npm run build && electron-builder -c.win.certificateSha1=206941d969c4fa8a0e04d9427def361e13b02fd0 --config electron-builder.config.js --publish always --win --x64",
"publish:linux": "npm run build && electron-builder --config electron-builder.config.js --publish never --linux --x64"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
@@ -38,63 +38,11 @@
]
},
"build": {
"extraResources": [
"./assets/jsons/bs-versions.json",
"./assets/jsons/patreons.json",
"./assets/proto/song_details_cache_v1.proto"
],
"productName": "BSManager",
"appId": "org.erb.BSManager",
"asarUnpack": "**\\*.{node,dll}",
"files": [
"dist/**/*",
"node_modules",
"package.json"
],
"afterSign": ".erb/scripts/notarize.js",
"afterPack": ".erb/scripts/after-pack.js",
"win": {
"signingHashAlgorithms": [
"sha256"
],
"target": [
"nsis",
"nsis-web"
],
"icon": "./build/icons/win/favicon.ico",
"extraResources": [
"./build/icons/win",
"./assets/scripts/*.exe"
]
},
"linux": {
"target": [
"AppImage"
],
"icon": "./build/icons/png",
"category": "Utility;Game;",
"extraResources": [
"./build/icons/png",
"./assets/scripts/DepotDownloader"
]
},
"directories": {
"app": "release/app",
"buildResources": "assets",
"output": "release/build"
},
"publish": {
"provider": "github",
"owner": "Zagrios"
},
"fileAssociations": [
{
"ext": "bplist",
"description": "Beat Saber Playlist (BSManager)",
"icon": "./assets/bsm_file.ico",
"role": "Viewer"
}
]
}
},
"repository": {
"type": "git",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.4",
"version": "1.5.0-alpha.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.0-alpha.4",
"version": "1.5.0-alpha.5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.4",
"version": "1.5.0-alpha.5",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
+203
View File
@@ -0,0 +1,203 @@
import {
bsmSpawn,
// bsmExec,
isProcessRunning,
// getProcessId,
} from "main/helpers/os.helpers";
import cp from "child_process";
import crypto from "crypto";
import log from "electron-log";
import { ifDescribe, ifIt } from "__tests__/utils";
import { BS_APP_ID } from "main/constants";
Object.defineProperty(global, "crypto", {
value: {
randomUUID: () => crypto.webcrypto.randomUUID(),
}
});
jest.mock("electron", () => ({
app: { getPath: () => "" },
}));
jest.mock("electron-log", () => ({
info: jest.fn(),
error: jest.fn(),
}));
jest.mock("ps-list", () => () => []);
const IS_WINDOWS = process.platform === "win32";
const IS_LINUX = process.platform === "linux";
describe("Test os.helpers bsmSpawn", () => {
const spawnSpy: jest.SpyInstance = jest.spyOn(cp, "spawn")
.mockImplementation();
const logSpy: jest.SpyInstance = jest.spyOn(log, "info");
const originalContainer = process.env.container;
const BS_ENV = {
SteamAppId: BS_APP_ID,
SteamOverlayGameId: BS_APP_ID,
SteamGameId: BS_APP_ID,
};
beforeAll(() => {
if (IS_LINUX) {
Object.assign(BS_ENV, {
WINEDLLOVERRIDES: "winhttp=n,b",
STEAM_COMPAT_DATA_PATH: "/compatdata",
STEAM_COMPAT_INSTALL_PATH: "/BSInstance",
STEAM_COMPAT_CLIENT_INSTALL_PATH: "/steam",
STEAM_COMPAT_APP_ID: BS_APP_ID,
SteamEnv: "1",
});
}
});
afterAll(() => {
spawnSpy.mockRestore();
});
afterEach(() => {
spawnSpy.mockClear();
logSpy.mockClear();
process.env.container = originalContainer;
});
it("Simple spawn command", () => {
bsmSpawn("cd", {
args: ["folder1", "folder2"],
});
expect(spawnSpy).toHaveBeenCalledTimes(1);
expect(spawnSpy).toHaveBeenCalledWith("cd folder1 folder2", expect.anything());
expect(logSpy).toHaveBeenCalledTimes(0);
});
it("Simple spawn command with logging", () => {
bsmSpawn("mkdir", {
args: ["new_folder"],
log: true,
});
expect(spawnSpy).toHaveBeenCalledTimes(1);
expect(spawnSpy).toHaveBeenCalledWith("mkdir new_folder", expect.anything());
expect(logSpy).toHaveBeenCalledTimes(1);
});
it("Complex spawn command call (Mods install)", () => {
bsmSpawn(`"./BSIPA.exe" "./Beat Saber.exe" -n`, {
log: true,
linux: { prefix: `"./wine64"` },
});
expect(spawnSpy).toHaveBeenCalledTimes(1);
expect(spawnSpy).toHaveBeenCalledWith(
process.platform === "win32"
? `"./BSIPA.exe" "./Beat Saber.exe" -n`
: `"./wine64" "./BSIPA.exe" "./Beat Saber.exe" -n`,
expect.anything()
);
expect(logSpy).toHaveBeenCalledTimes(1);
});
it("Complex spawn command call (BS launch)", () => {
bsmSpawn(`"./Beat Saber.exe"`, {
args: ["--no-yeet", "fpfc"],
options: {
cwd: "/",
detached: true,
env: BS_ENV,
},
log: true,
linux: { prefix: `"./proton" run` },
});
expect(spawnSpy).toHaveBeenCalledTimes(1);
expect(spawnSpy).toHaveBeenCalledWith(
IS_WINDOWS
? `"./Beat Saber.exe" --no-yeet fpfc`
: `"./proton" run "./Beat Saber.exe" --no-yeet fpfc`,
expect.objectContaining({
cwd: "/",
detached: true,
env: BS_ENV,
})
);
expect(logSpy).toHaveBeenCalledTimes(1);
});
ifIt(IS_LINUX)("Complex spawn command call (BS launch flatpak)", () => {
const flatpakEnv = [
"SteamAppId",
"SteamOverlayGameId",
"SteamGameId",
"WINEDLLOVERRIDES",
"STEAM_COMPAT_DATA_PATH",
"STEAM_COMPAT_INSTALL_PATH",
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
"STEAM_COMPAT_APP_ID",
"SteamEnv"
];
const newEnv = {
...BS_ENV,
something: "else",
more: "tests",
};
bsmSpawn(`"./Beat Saber.exe"`, {
args: ["--no-yeet", "fpfc"],
options: {
cwd: "/",
detached: true,
env: newEnv,
},
log: true,
linux: { prefix: `"./proton" run` },
flatpak: {
host: true,
env: flatpakEnv,
},
});
expect(spawnSpy).toHaveBeenCalledTimes(1);
const envArgs = flatpakEnv.map(argName =>
`--env=${argName}="${(BS_ENV as any)[argName]}"`
).join(" ");
expect(spawnSpy).toHaveBeenCalledWith(
`flatpak-spawn --host ${envArgs} "./proton" run "./Beat Saber.exe" --no-yeet fpfc`,
expect.objectContaining({
cwd: "/",
detached: true,
env: newEnv,
})
);
expect(logSpy).toHaveBeenCalledTimes(1);
});
});
ifDescribe(IS_LINUX)("Test os.helpers isProcessRunning", () => {
const logSpy: jest.SpyInstance = jest.spyOn(log, "error");
afterEach(() => {
logSpy.mockClear();
});
it("Process is running", async () => {
// There will always a node process running
const running = await isProcessRunning("node");
expect(running).toBe(true);
// No errors received
expect(logSpy).toHaveBeenCalledTimes(0);
});
it("Process is not running", async () => {
const running = await isProcessRunning(`bs-manager-${crypto.randomUUID()}`);
expect(running).toBe(false);
// No errors received
expect(logSpy).toHaveBeenCalledTimes(0);
});
});
+5
View File
@@ -0,0 +1,5 @@
export const ifDescribe = (condition: boolean) => condition ? describe : describe.skip;
export const ifIt = (condition: boolean) => condition ? it : it.skip;
+1
View File
@@ -21,4 +21,5 @@ export const HTTP_STATUS_CODES = constants;
export const PROTON_BINARY_PREFIX = "proton";
export const WINE_BINARY_PREFIX = path.join("files", "bin", "wine64");
export const IS_FLATPAK = process.env.container === "flatpak";
+151 -10
View File
@@ -1,25 +1,166 @@
import cp from "child_process";
import log from "electron-log";
import psList from "ps-list";
import { IS_FLATPAK } from "main/constants";
export async function taskRunning(task: string): Promise<boolean> {
// 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
// = "path/to/proton" run "./Beat Saber.exe" --no-yeet
prefix: string;
};
// Only applied if package as flatpak
type FlatpakOptions = {
// Force to use "flatpak-spawn --host" to run commands outside of the sandbox
host: boolean;
// Only copy the keys from options.env from bsmSpawn/bsmExec
env?: string[];
};
export type BsmSpawnOptions = {
args?: string[];
options?: cp.SpawnOptions;
log?: boolean;
linux?: LinuxOptions;
flatpak?: FlatpakOptions;
};
export type BsmExecOptions = {
args?: string[];
options?: cp.ExecOptions;
log?: boolean;
linux?: LinuxOptions;
flatpak?: FlatpakOptions;
};
function updateCommand(command: string, options: BsmSpawnOptions) {
if (options?.args) {
command += ` ${options.args.join(" ")}`;
}
if (process.platform === "linux") {
// "/bin/sh" does not see flatpak-spawn
// Most Debian and Arch should also support "/bin/bash"
options.options.shell = "/bin/bash";
if (options.linux?.prefix) {
command = `${options.linux.prefix} ${command}`;
}
if (options?.flatpak?.host) {
const envArgs = (options?.flatpak?.env && options?.options?.env)
&& options.flatpak.env
.filter(envName => options.options.env[envName])
.map(envName =>
`--env=${envName}="${options.options.env[envName]}"`
)
.join(" ");
command = `flatpak-spawn --host ${envArgs || ""} ${command}`;
}
}
return 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);
}
return cp.spawn(command, options.options);
}
export function bsmExec(command: string, options?: BsmExecOptions): Promise<{
stdout: string;
stderr: string;
}> {
options = options || {};
options.options = options.options || {};
command = updateCommand(command, options);
if (options?.log) {
log.info(
process.platform === "win32" ? "Windows" : "Linux",
"exec command\n>", command
);
}
return new Promise((resolve, reject) => {
cp.exec(command, options?.options || {}, (error: Error, stdout: string, stderr: string) => {
if (error) { return reject(error); }
resolve({ stdout, stderr });
});
});
}
async function isProcessRunningLinux(name: string): Promise<boolean> {
try {
const { stdout: count } = await bsmExec(`ps awwxo args | grep -c "${name}"`, {
log: true,
flatpak: { host: IS_FLATPAK },
});
return +count.trim() > MIN_PROCESS_COUNT_LINUX;
} catch(error) {
log.error(error);
return false;
};
}
async function getProcessIdWindows(name: string): Promise<number | null> {
try {
const processes = await psList();
return processes.some(process => process.name?.includes(task) || process.cmd?.includes(task));
const process = processes.find(process => process.name?.includes(name) || process.cmd?.includes(name));
return process?.pid;
} catch (error) {
log.error(error);
return null;
}
catch(error){
}
export const isProcessRunning = process.platform === "win32"
? isProcessRunningWindows
: isProcessRunningLinux;
async function isProcessRunningWindows(name: string): Promise<boolean> {
try {
const processes = await psList();
return processes.some(process =>
process.name?.includes(name) || process.cmd?.includes(name)
);
} catch (error) {
log.error(error);
return false;
}
}
export async function getProcessPid(task: string): Promise<number> {
async function getProcessIdLinux(name: string): Promise<number | null> {
try {
const processes = await psList();
const process = processes.find(process => process.name?.includes(task) || process.cmd?.includes(task));
return process?.pid;
}
catch(error){
const { stdout } = await bsmExec(`ps awwxo pid,args | grep "${name}"`, {
log: true,
flatpak: { host: IS_FLATPAK },
});
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;
} catch(error) {
log.error(error);
return null;
}
};
}
export const getProcessId = process.platform === "win32"
? getProcessIdWindows
: getProcessIdLinux;
@@ -69,8 +69,8 @@ export class LocalPlaylistsManagerService {
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => {
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link);
const url = new URL(link);
const bplistUrl = url.host === "playlist" ? url.pathname.replace("/", "") : "";
this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${bplistUrl}`);
const bplistUrl = url.host === "playlist" ? url.pathname.replace("/", "") + url.search : "";
this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${encodeURIComponent(bplistUrl)}`);
});
this.fileAssociation.registerFileAssociation(".bplist", filePath => {
@@ -1,15 +1,20 @@
import { LaunchOption } from "shared/models/bs-launch";
import { BSLocalVersionService } from "../bs-local-version.service";
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio, spawn } from "child_process";
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from "child_process";
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 { IS_FLATPAK } from "main/constants";
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 +49,25 @@ export abstract class AbstractLauncherService {
spawnOptions.windowsVerbatimArguments = true;
}
log.info(`Launch BS exe at ${bsExePath} with args ${args?.join(" ")}`);
return spawn(bsExePath, args, spawnOptions);
spawnOptions.shell = true; // For windows to spawn properly
return bsmSpawn(`"${bsExePath}"`, {
args, options: spawnOptions, log: true,
linux: { prefix: this.linux.getProtonCommand() },
flatpak: {
host: IS_FLATPAK,
env: [
"SteamAppId",
"SteamOverlayGameId",
"SteamGameId",
"WINEDLLOVERRIDES",
"STEAM_COMPAT_DATA_PATH",
"STEAM_COMPAT_INSTALL_PATH",
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
"STEAM_COMPAT_APP_ID",
"SteamEnv",
],
},
});
}
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
@@ -8,7 +8,7 @@ import log from "electron-log";
import { sToMs } from "../../../shared/helpers/time.helpers";
import { lstat, pathExists, readdir, readlink, rename, symlink, unlink } from "fs-extra";
import { AbstractLauncherService } from "./abstract-launcher.service";
import { taskRunning } from "../../helpers/os.helpers";
import { isProcessRunning } from "../../helpers/os.helpers";
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
import { InstallationLocationService } from "../installation-location.service";
import { ensurePathNotAlreadyExist } from "../../helpers/fs.helpers";
@@ -154,7 +154,7 @@ export class OculusLauncherService extends AbstractLauncherService implements St
(async () => {
// Cannot start multiple instances of Beat Saber with Oculus
const bsRunning = await taskRunning(BS_EXECUTABLE).catch(() => false);
const bsRunning = await isProcessRunning(BS_EXECUTABLE).catch(() => false);
if(bsRunning){
throw CustomError.fromError(new Error("Cannot start two instance of Beat Saber for Oculus"), BSLaunchError.BS_ALREADY_RUNNING);
}
@@ -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,14 +67,14 @@ 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
if(!(await this.steam.steamRunning())){
if(!(await this.steam.isSteamRunning())){
obs.next({type: BSLaunchEvent.STEAM_LAUNCHING});
await this.steam.openSteam().then(() => {
@@ -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)
+18 -21
View File
@@ -3,9 +3,10 @@ import path from "path";
import { writeFileSync } from "fs";
import { BSVersion } from "shared/bs-version.interface";
import { RequestService } from "./request.service";
import { readJSON } from "fs-extra";
import { pathExistsSync, readJSON } from "fs-extra";
import { allSettled } from "../../shared/helpers/promise.helpers";
import { StaticConfigurationService } from "./static-configuration.service";
import { LinuxService } from "./linux.service";
import { IS_FLATPAK } from "main/constants";
export class BSVersionLibService {
private readonly REMOTE_BS_VERSIONS_URL: string = "https://raw.githubusercontent.com/Zagrios/bs-manager/master/assets/jsons/bs-versions.json";
@@ -13,16 +14,16 @@ export class BSVersionLibService {
private static instance: BSVersionLibService;
private linuxService: LinuxService;
private utilsService: UtilsService;
private requestService: RequestService;
private staticConfigurationService: StaticConfigurationService;
private bsVersions: BSVersion[];
private constructor() {
this.linuxService = LinuxService.getInstance();
this.utilsService = UtilsService.getInstance();
this.requestService = RequestService.getInstance();
this.staticConfigurationService = StaticConfigurationService.getInstance();
}
public static getInstance(): BSVersionLibService {
@@ -32,34 +33,30 @@ export class BSVersionLibService {
return BSVersionLibService.instance;
}
private getRemoteVersions(): Promise<BSVersion[]> {
private async getRemoteVersions(): Promise<BSVersion[]> {
return this.requestService.getJSON<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL).then(res => res.data);
}
private async getLocalVersions(): Promise<BSVersion[]> {
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
if (process.platform === "linux") {
let versions = this.staticConfigurationService.get("versions");
if (!versions) {
versions = (await readJSON(localVersionsPath)) as BSVersion[];
this.staticConfigurationService.set("versions", versions);
if (IS_FLATPAK) {
const flatpakVersionsPath = path.join(this.linuxService.getFlatpakLocalVersionFolder(), this.VERSIONS_FILE);
if (pathExistsSync(flatpakVersionsPath)) {
return readJSON(flatpakVersionsPath);
}
return versions;
}
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
return readJSON(localVersionsPath);
}
private async updateLocalVersions(versions: BSVersion[]): Promise<void> {
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
// Do not write on readonly memory in linux when running on AppImage
if (process.platform === "linux") {
this.staticConfigurationService.set("versions", versions);
} else {
writeFileSync(localVersionsPath, JSON.stringify(versions, null, "\t"), { encoding: "utf-8", flag: "w" });
}
const localVersionsPath = path.join(
IS_FLATPAK
? this.linuxService.getFlatpakLocalVersionFolder()
: this.utilsService.getAssestsJsonsPath(),
this.VERSIONS_FILE
);
writeFileSync(localVersionsPath, JSON.stringify(versions, null, "\t"), { encoding: "utf-8", flag: "w" });
}
private async loadBsVersions(): Promise<BSVersion[]> {
+82 -4
View File
@@ -1,7 +1,12 @@
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 { 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";
import { app } from "electron";
import config from "../../../electron-builder.config";
export class LinuxService {
private static instance: LinuxService;
@@ -14,11 +19,68 @@ export class LinuxService {
}
private readonly staticConfig: StaticConfigurationService;
private protonCommand = "";
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
);
}
this.protonCommand = `"${protonPath}" run`;
// 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 verifyProtonPath(protonFolder: string = ""): boolean {
if (protonFolder === "") {
if (!this.staticConfig.has("proton-folder")) {
@@ -30,7 +92,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,10 +104,26 @@ 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`);
}
return winePath;
}
public getProtonCommand(): string {
// Set in setupLaunch
return this.protonCommand;
}
// === Flatpak Specific === //
public getFlatpakLocalVersionFolder(): string {
return path.join(
app.getPath("home"),
".var", "app", config.appId,
"resources", "assets", "jsons"
);
}
}
@@ -5,7 +5,6 @@ import { BSLocalVersionService } from "../bs-local-version.service";
import path from "path";
import md5File from "md5-file";
import { RequestService } from "../request.service";
import { spawn } from "child_process";
import { BS_EXECUTABLE } from "../../constants";
import log from "electron-log";
import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers";
@@ -17,9 +16,9 @@ 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";
import { UtilsService } from "../utils.service";
import crypto from "crypto";
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
import { bsmSpawn } from "main/helpers/os.helpers";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -28,7 +27,6 @@ export class BsModsManagerService {
private readonly bsLocalService: BSLocalVersionService;
private readonly linuxService: LinuxService;
private readonly requestService: RequestService;
private readonly utilsService: UtilsService;
private manifestMatches: Mod[];
@@ -44,7 +42,6 @@ export class BsModsManagerService {
this.bsLocalService = BSLocalVersionService.getInstance();
this.linuxService = LinuxService.getInstance();
this.requestService = RequestService.getInstance();
this.utilsService = UtilsService.getInstance();
}
private async getModFromHash(hash: string): Promise<Mod> {
@@ -144,19 +141,28 @@ export class BsModsManagerService {
return false;
}
let cmd = `"${ipaPath}" "${bsExePath}" ${args.join(" ")}`;
const cmd = `"${ipaPath}" "${bsExePath}" ${args.join(" ")}`;
let winePath: string = "";
if (process.platform === "linux") {
const { error, result: winePath } = tryit(() => this.linuxService.getWinePath());
const { error, result } = tryit(() => this.linuxService.getWinePath());
if (error) {
log.error(error);
return false;
}
cmd = `"${winePath}" ${cmd}`;
winePath = `"${result}"`;
}
return new Promise<boolean>(resolve => {
log.info("START IPA PROCESS", cmd);
const processIPA = spawn(cmd, { cwd: versionPath, detached: true, shell: true });
const processIPA = bsmSpawn(cmd, {
log: true,
options: {
cwd: versionPath,
detached: true,
shell: true
},
linux: { prefix: winePath },
});
const timeout = setTimeout(() => {
log.info("Ipa process timeout");
+2 -2
View File
@@ -4,7 +4,7 @@ import log from "electron-log";
import { lstat } from "fs-extra";
import { tryit } from "../../shared/helpers/error.helpers";
import { shell } from "electron";
import { taskRunning } from "../helpers/os.helpers";
import { isProcessRunning } from "../helpers/os.helpers";
import { sToMs } from "../../shared/helpers/time.helpers";
import { execOnOs } from "../helpers/env.helpers";
@@ -99,7 +99,7 @@ export class OculusService {
}
public oculusRunning(): Promise<boolean> {
return taskRunning("OculusClient");
return isProcessRunning("OculusClient");
}
public async startOculus(): Promise<void>{
@@ -3,7 +3,6 @@ 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 {
@@ -90,7 +89,6 @@ export interface StaticConfigKeyValues {
"use-symlinks": boolean;
// Linux Specific static configs
"versions": BSVersion[];
"proton-folder": string;
};
+6 -6
View File
@@ -5,7 +5,7 @@ import { readFile } from "fs/promises";
import { pathExist } from "../helpers/fs.helpers";
import log from "electron-log";
import { app, shell } from "electron";
import { getProcessPid, taskRunning } from "../helpers/os.helpers";
import { getProcessId, isProcessRunning } from "main/helpers/os.helpers";
import { isElevated } from "query-process";
import { execOnOs } from "../helpers/env.helpers";
@@ -15,7 +15,7 @@ export class SteamService {
private static readonly PROCESS_NAME: string = process.platform === "linux"
? "steam-runtime-launcher-service"
: "steam";
: "steam.exe";
private static instance: SteamService;
@@ -37,15 +37,15 @@ export class SteamService {
return registryValue.value;
}
public async steamRunning(): Promise<boolean>{
const steamProcessRunning = await taskRunning(SteamService.PROCESS_NAME);
public async isSteamRunning(): Promise<boolean>{
const steamProcessRunning = await isProcessRunning(SteamService.PROCESS_NAME);
if(process.platform === "linux") { return steamProcessRunning; }
const activeUser = await this.getActiveUser().catch(err => log.error(err));
return steamProcessRunning && !!activeUser;
}
public async getSteamPid(): Promise<number>{
return getProcessPid(SteamService.PROCESS_NAME);
return getProcessId(SteamService.PROCESS_NAME);
}
/**
@@ -123,7 +123,7 @@ export class SteamService {
return new Promise((resolve, reject) => {
// Every 3 seconds check if steam is running
const interval = setInterval(() => {
const steamRunning = this.steamRunning().catch(() => false);
const steamRunning = this.isSteamRunning().catch(() => false);
steamRunning.then(running => {
if(!running){ return; }
clearInterval(interval);
@@ -22,7 +22,7 @@ export const ChooseProtonFolderModal: ModalComponent<{}, {}> = ({ resolver }) =>
const selectProtonPath = async () => {
const response = await lastValueFrom(ipcService.sendV2("choose-folder", {
parent: "home",
defaultPath: ".local/share/Steam/steamapps/common",
defaultPath: ".steam/steam/steamapps/common",
showHidden: true,
}));
@@ -82,22 +82,77 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
}
linkOpener.open(moreInfoMod.link);
};
// private isDependency(mod: Mod, selectedMods: Mod[], availableMods: Mod[]) {
// return selectedMods.some(m => {
// const deps = m.dependencies.map(dep => Array.from(availableMods.values()).find(m => dep.name === m.name));
// if (deps.some(depMod => depMod.name === mod.name)) {
// return true;
// }
// return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name));
// });
// }
// private async resolveDependencies(mods: Mod[], version: BSVersion): Promise<Mod[]> {
// const availableMods = await this.beatModsApi.getVersionMods(version);
// return Array.from(
// new Map<string, Mod>(
// availableMods.reduce((res, mod) => {
// if (mod.required || this.isDependency(mod, mods, availableMods)) {
// res.push([mod.name, mod]);
// }
// return res;
// }, [])
// ).values()
// );
// }
const getAllDependencies = (mods: Mod[], availableMods: Mod[]): Mod[] => {
const collectedDependencies = new Set<Mod>();
const collectDependencies = (mod: Mod) => {
if (!mod.dependencies) { return; }
for (const dependency of mod.dependencies) {
const dependencyMod = availableMods.find(avMod => avMod.name === dependency.name);
if (dependencyMod && !collectedDependencies.has(dependencyMod)) {
collectedDependencies.add(dependencyMod);
collectDependencies(dependencyMod);
}
}
};
mods.forEach(collectDependencies);
availableMods.forEach(mod => {
if(mod.required){
collectedDependencies.add(mod);
}
});
return Array.from(collectedDependencies);
};
const installMods = (reinstallAll: boolean): void => {
setReinstallAllMods(() => false);
setReinstallAllMods(false);
if (installing) {
return;
}
const modsToInstall = modsSelected.filter(mod => {
const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name);
let modsToInstall = [
...modsSelected,
...getAllDependencies(modsSelected, Array.from(modsAvailable.values()).flat())
]
if(reinstallAll || !installedMod){ return true; }
modsToInstall = reinstallAll ? (
modsToInstall // If reinstalling all, we install all selected mods
) : (
modsToInstall.filter(mod => { // Else we only install the mods that are not installed or have a newer version
const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name);
return !installedMod || lt(installedMod.version, mod.version);
})
);
return lt(installedMod.version, mod.version);
});
modsToInstall = Array.from(new Set(modsToInstall)); // Remove duplicates
if (!modsToInstall.length) {
notification.notifyInfo({ title: "pages.version-viewer.mods.notifications.all-mods-already-installed.title", desc: "pages.version-viewer.mods.notifications.all-mods-already-installed.description" });
@@ -164,7 +164,7 @@ export function SettingsPage() {
try {
const pathResponse = await lastValueFrom(ipcService.sendV2("choose-folder", {
parent: "home",
defaultPath: ".local/share/Steam/steamapps/common",
defaultPath: ".steam/steam/steamapps/common",
showHidden: true,
}));
if (