mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature] can now login to Steam with QRCode + ipc management optimization
This commit is contained in:
@@ -2,14 +2,16 @@ import { BS_APP_ID, BS_DEPOT } from "../constants";
|
||||
import path from "path";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { UtilsService } from "./utils.service";
|
||||
import { ChildProcessWithoutNullStreams, spawn, spawnSync } from "child_process";
|
||||
import { spawnSync } from "child_process";
|
||||
import log from "electron-log";
|
||||
import { InstallationLocationService } from "./installation-location.service";
|
||||
import { BSLocalVersionService } from "./bs-local-version.service";
|
||||
import { WindowManagerService } from "./window-manager.service";
|
||||
import { copy } from "fs-extra";
|
||||
import { ensureFolderExist, pathExist } from "../helpers/fs.helpers";
|
||||
import { net } from "electron";
|
||||
import { pathExist } from "../helpers/fs.helpers";
|
||||
import { Observable, map } from "rxjs";
|
||||
import { DepotDownloaderArgsOptions, DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderInfoEvent } from "../../shared/models/depot-downloader.model";
|
||||
import { DepotDownloader } from "../models/depot-downloader.class";
|
||||
|
||||
export class BSInstallerService {
|
||||
private static instance: BSInstallerService;
|
||||
@@ -19,7 +21,7 @@ export class BSInstallerService {
|
||||
private readonly localVersionService: BSLocalVersionService;
|
||||
private readonly windows: WindowManagerService;
|
||||
|
||||
private downloadProcess: ChildProcessWithoutNullStreams;
|
||||
private depotDownloader: DepotDownloader;
|
||||
|
||||
private constructor() {
|
||||
this.utils = UtilsService.getInstance();
|
||||
@@ -28,7 +30,7 @@ export class BSInstallerService {
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
|
||||
this.windows.getWindow("index.html")?.on("close", () => {
|
||||
this.killDownloadProcess();
|
||||
this.depotDownloader?.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,27 +45,6 @@ export class BSInstallerService {
|
||||
return path.join(this.utils.getAssetsScriptsPath(), "depot-downloader", "DepotDownloader.exe");
|
||||
}
|
||||
|
||||
private removeSpecialSchar(txt: string): string {
|
||||
return txt.replaceAll(/[\[\]]/g, "");
|
||||
}
|
||||
|
||||
private sendDownloadEvent(event: DownloadEventType, data?: string | number, success = true): void {
|
||||
if (typeof data === "string") {
|
||||
data = this.removeSpecialSchar(data);
|
||||
}
|
||||
this.utils.ipcSend(`bs-download.${event}`, { success, data });
|
||||
}
|
||||
|
||||
public sendInputProcess(input: string) {
|
||||
if (this.downloadProcess.stdin.writable) {
|
||||
this.downloadProcess.stdin.write(`${input}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
public killDownloadProcess(): boolean {
|
||||
return this.downloadProcess.kill();
|
||||
}
|
||||
|
||||
public async isDotNet6Installed(): Promise<boolean> {
|
||||
try {
|
||||
const process = spawnSync(this.getDepotDownloaderExePath());
|
||||
@@ -78,95 +59,89 @@ export class BSInstallerService {
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadBsVersion(downloadInfos: DownloadInfo): Promise<DownloadEvent> {
|
||||
// TODO : Can be a lot improved by using ipcV2 and Observable -- This will be reworked for qrCode login
|
||||
|
||||
if (this.downloadProcess?.connected) {
|
||||
throw "AlreadyDownloading";
|
||||
}
|
||||
const { bsVersion } = downloadInfos;
|
||||
if (!bsVersion) {
|
||||
return { type: "[Error]" };
|
||||
}
|
||||
if (!net.isOnline()) {
|
||||
throw "no-internet";
|
||||
}
|
||||
|
||||
await ensureFolderExist(this.installLocationService.versionsDirectory);
|
||||
|
||||
const versionPath = await this.localVersionService.getVersionPath(bsVersion);
|
||||
|
||||
const dest = !downloadInfos.isVerification ? await this.getPathNotAleardyExist(versionPath) : versionPath;
|
||||
private async buildDepotDownloaderInstance(downloadInfos: DownloadInfo, qr?: boolean): Promise<DepotDownloader> {
|
||||
|
||||
const versionPath = await this.localVersionService.getVersionPath(downloadInfos.bsVersion);
|
||||
const dest = downloadInfos.isVerification ? versionPath : await this.getPathNotAleardyExist(versionPath);
|
||||
const downloadVersion: BSVersion = { ...downloadInfos.bsVersion, ...(path.basename(dest) !== downloadInfos.bsVersion.BSVersion && { name: path.basename(dest) }) };
|
||||
|
||||
this.downloadProcess = spawn(this.getDepotDownloaderExePath(), ["-app", BS_APP_ID, "-depot", BS_DEPOT, "-manifest", bsVersion.BSManifest, "-username", downloadInfos.username, "-dir", this.localVersionService.getVersionFolder(downloadVersion)], { cwd: this.installLocationService.versionsDirectory });
|
||||
const depotDownloaderOptions: DepotDownloaderArgsOptions = {
|
||||
app: BS_APP_ID,
|
||||
depot: BS_DEPOT,
|
||||
dir: this.localVersionService.getVersionFolder(downloadVersion),
|
||||
manifest: downloadInfos.bsVersion.BSManifest,
|
||||
username: downloadInfos.username,
|
||||
password: downloadInfos.password,
|
||||
"remember-password": downloadInfos.stay,
|
||||
validate: downloadInfos.isVerification,
|
||||
qr
|
||||
}
|
||||
|
||||
this.utils.ipcSend("start-download-version", { success: true, data: downloadVersion });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.downloadProcess.stdout.on("data", data => {
|
||||
const matched = (data.toString() as string).match(/(?:\[(.*?)\])\|(?:\[(.*?)\]\|)?(.*?)(?=$|\[)/gm) ?? [];
|
||||
matched.forEach(match => {
|
||||
const out = match.split("|");
|
||||
|
||||
if (out[0] !== "[Progress]" && out[0] !== "[Validated]") {
|
||||
log.info(data.toString());
|
||||
}
|
||||
|
||||
if (out[0] === ("[Password]" as DownloadEventType) && downloadInfos.password) {
|
||||
this.sendInputProcess(downloadInfos.password);
|
||||
} else if (out[0] === ("[Password]" as DownloadEventType)) {
|
||||
this.killDownloadProcess();
|
||||
resolve({ type: "[Password]", data: bsVersion });
|
||||
} else if (out[0] === ("[2FA]" as DownloadEventType) || out[0] === ("[Guard]" as DownloadEventType)) {
|
||||
this.sendDownloadEvent("[2FA]");
|
||||
} else if (out[0] === ("[Progress]" as DownloadEventType) || (out[0] === ("[Validated]" as DownloadEventType) && parseFloat(out[1]) < 100)) {
|
||||
this.sendDownloadEvent("[Progress]", parseFloat(out[1].replaceAll(",", ".")));
|
||||
} else if (out[0] === ("[Finished]" as DownloadEventType) || (out[0] === ("[Validated]" as DownloadEventType) && parseFloat(out[1]) === 100)) {
|
||||
resolve({ type: "[Finished]" });
|
||||
this.killDownloadProcess();
|
||||
} else if (out[0] === ("[SteamID]" as DownloadEventType)) {
|
||||
this.sendDownloadEvent("[SteamID]", out[1]);
|
||||
} else if (out[0] === ("[Warning]" as DownloadEventType)) {
|
||||
this.sendDownloadEvent("[Warning]", out[1]);
|
||||
} else if (out[0] === ("[Error]" as DownloadEventType)) {
|
||||
reject(this.removeSpecialSchar(out[1]));
|
||||
this.killDownloadProcess();
|
||||
log.error("Download Event, Error", data.toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.downloadProcess.stdout.on("error", err => {
|
||||
log.error("BS-DOWNLOAD ERROR", err.toString());
|
||||
this.killDownloadProcess();
|
||||
if (err.toString().includes(".NET") || err.toString().includes("dotnet") || err.toString().includes(".dll")) {
|
||||
reject("dotnet");
|
||||
}
|
||||
reject();
|
||||
});
|
||||
this.downloadProcess.stderr.on("data", err => {
|
||||
log.error("BS-DOWNLOAD ERROR", err.toString());
|
||||
this.killDownloadProcess();
|
||||
if (err.toString().includes(".NET") || err.toString().includes("dotnet") || err.toString().includes(".dll")) {
|
||||
reject("dotnet");
|
||||
}
|
||||
reject();
|
||||
});
|
||||
this.downloadProcess.stderr.on("error", err => {
|
||||
log.error("BS-DOWNLOAD ERROR", err.toString());
|
||||
this.killDownloadProcess();
|
||||
if (err.toString().includes(".NET") || err.toString().includes("dotnet") || err.toString().includes(".dll")) {
|
||||
reject("dotnet");
|
||||
}
|
||||
reject();
|
||||
});
|
||||
|
||||
this.downloadProcess.on("close", () => reject());
|
||||
return new DepotDownloader({
|
||||
command: this.getDepotDownloaderExePath(),
|
||||
args: DepotDownloader.buildArgs(depotDownloaderOptions),
|
||||
options: { cwd: this.installLocationService.versionsDirectory },
|
||||
echoStartData: downloadVersion
|
||||
});
|
||||
}
|
||||
|
||||
private buildDepotDownloaderObservable(downloadInfos: DownloadInfo, qr?: boolean): Observable<DepotDownloaderEvent> {
|
||||
return new Observable(sub => {
|
||||
|
||||
const depotDownloaderBuildPromise = this.buildDepotDownloaderInstance(downloadInfos, qr);
|
||||
|
||||
depotDownloaderBuildPromise.then(depotDownloader => {
|
||||
|
||||
if(this.depotDownloader?.running){
|
||||
this.depotDownloader.stop();
|
||||
}
|
||||
|
||||
this.depotDownloader = depotDownloader;
|
||||
|
||||
depotDownloader.$events().pipe(map(event => {
|
||||
if(event.type === DepotDownloaderEventType.Error){
|
||||
throw event;
|
||||
}
|
||||
return event;
|
||||
})).subscribe(sub);
|
||||
|
||||
}).catch(err => sub.error({
|
||||
type: DepotDownloaderEventType.Error,
|
||||
subType: DepotDownloaderErrorEvent.Unknown,
|
||||
data: err
|
||||
} as DepotDownloaderEvent));
|
||||
|
||||
return () => {
|
||||
depotDownloaderBuildPromise.then(depotDownloader => depotDownloader.stop());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public downloadBsVersion(downloadInfos: DownloadInfo): Observable<DepotDownloaderEvent> {
|
||||
return this.buildDepotDownloaderObservable(downloadInfos);
|
||||
}
|
||||
|
||||
public autoDownloadBsVersion(downloadInfos: DownloadInfo): Observable<DepotDownloaderEvent> {
|
||||
return this.buildDepotDownloaderObservable({...downloadInfos, password: null, stay: true}).pipe(map(event => {
|
||||
if(event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.Password){
|
||||
throw new Error("Ask for password while auto download");
|
||||
}
|
||||
return event;
|
||||
}));
|
||||
}
|
||||
|
||||
public downloadBsVersionWithQRCode(downloadInfos: DownloadInfo): Observable<DepotDownloaderEvent> {
|
||||
return this.buildDepotDownloaderObservable(downloadInfos, true);
|
||||
}
|
||||
|
||||
public sendInput(input: string): boolean {
|
||||
return this.depotDownloader?.sendInput(input);
|
||||
}
|
||||
|
||||
public stopDownload(): void {
|
||||
this.depotDownloader?.stop();
|
||||
}
|
||||
|
||||
private async getPathNotAleardyExist(path: string): Promise<string> {
|
||||
let destPath = path;
|
||||
let folderExist = await pathExist(destPath);
|
||||
@@ -198,10 +173,10 @@ export class BSInstallerService {
|
||||
|
||||
export interface DownloadInfo {
|
||||
bsVersion: BSVersion;
|
||||
username: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
stay?: boolean;
|
||||
isVerification: boolean;
|
||||
isVerification?: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadEvent {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { ipcMain } from "electron";
|
||||
import { Observable } from "rxjs";
|
||||
import { IpcCompleteChannel, IpcErrorChannel } from "shared/models/ipc/ipc-response.interface";
|
||||
import { IpcCompleteChannel, IpcErrorChannel, IpcTearDownChannel } from "shared/models/ipc/ipc-response.interface";
|
||||
import { AppWindow } from "shared/models/window-manager/app-window.model";
|
||||
import { WindowManagerService } from "./window-manager.service";
|
||||
import { IpcReplier } from "shared/models/ipc/ipc-request.interface";
|
||||
@@ -31,6 +31,10 @@ export class IpcService {
|
||||
return `${channel}_complete`;
|
||||
}
|
||||
|
||||
private getTearDownChannel(channel: string): IpcTearDownChannel {
|
||||
return `${channel}_teardown`;
|
||||
}
|
||||
|
||||
private buildProxyListener<T>(listener: IpcListener<T>) {
|
||||
return (event: Electron.IpcMainEvent, req: IpcRequest<T>) => {
|
||||
const window = this.windows.getAppWindowFromWebContents(event.sender);
|
||||
@@ -44,18 +48,21 @@ export class IpcService {
|
||||
}
|
||||
|
||||
private connectStream(channel: string, window: AppWindow, observable: Observable<unknown>): void {
|
||||
observable.subscribe(
|
||||
data => {
|
||||
this.send(channel, window, data);
|
||||
},
|
||||
error => {
|
||||
|
||||
const sub = observable.subscribe({
|
||||
next: data => this.send(channel, window, data),
|
||||
error: error => {
|
||||
log.error(error);
|
||||
this.send(this.getErrorChannel(channel), window, error);
|
||||
},
|
||||
() => {
|
||||
this.send(this.getCompleteChannel(channel), window);
|
||||
}
|
||||
);
|
||||
complete: () => this.send(this.getCompleteChannel(channel), window)
|
||||
})
|
||||
|
||||
ipcMain.once(this.getTearDownChannel(channel), () => sub.unsubscribe());
|
||||
|
||||
sub.add(() => {
|
||||
ipcMain.removeAllListeners(this.getTearDownChannel(channel));
|
||||
});
|
||||
}
|
||||
|
||||
public on<T>(channel: string, listener: IpcListener<T>): void {
|
||||
|
||||
Reference in New Issue
Block a user