[feature] can now login to Steam with QRCode + ipc management optimization

This commit is contained in:
MathieuG-P
2023-08-02 02:36:36 +02:00
parent 82bca18f3f
commit 922aad7867
35 changed files with 950 additions and 643 deletions
+85 -110
View File
@@ -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 {
+17 -10
View File
@@ -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 {