From 136792ecdd925aa2307705816dedf090be0f23ab Mon Sep 17 00:00:00 2001 From: alexkar598 <25136265+alexkar598@users.noreply.github.com> Date: Tue, 25 Jun 2024 01:56:39 -0400 Subject: [PATCH] Basic byond fetching and extracting in the byond panel --- src/app/components/panel/panel.component.ts | 1 - src/app/panels/byond/byond.component.html | 36 +++++ src/app/panels/byond/byond.component.ts | 15 +- src/utils/sharedLock.ts | 26 ++++ src/vm/byond.service.ts | 152 +++++++++++++++----- src/vm/commandQueue.service.ts | 20 +++ src/vm/emulator.worker.ts | 1 + tsconfig.json | 6 +- 8 files changed, 217 insertions(+), 40 deletions(-) create mode 100644 src/utils/sharedLock.ts diff --git a/src/app/components/panel/panel.component.ts b/src/app/components/panel/panel.component.ts index 6055b20..3d4411b 100644 --- a/src/app/components/panel/panel.component.ts +++ b/src/app/components/panel/panel.component.ts @@ -28,7 +28,6 @@ export class PanelComponent { @Input() public set id(panel: Panel) { - console.log(panel); switch (panel) { case Panel.Controller: this.panelComponent = import( diff --git a/src/app/panels/byond/byond.component.html b/src/app/panels/byond/byond.component.html index 2005620..3593113 100644 --- a/src/app/panels/byond/byond.component.html +++ b/src/app/panels/byond/byond.component.html @@ -1,12 +1,48 @@ @if (byondService.latestVersion | async; as latestVersions) {

Latest stable: {{ latestVersions.stable }} +

@if (latestVersions.beta) {

Latest beta: {{ latestVersions.beta }} +

} } @else { Loading latest version... } + + diff --git a/src/app/panels/byond/byond.component.ts b/src/app/panels/byond/byond.component.ts index 63a630a..50d2720 100644 --- a/src/app/panels/byond/byond.component.ts +++ b/src/app/panels/byond/byond.component.ts @@ -1,12 +1,13 @@ import { Component } from '@angular/core'; -import { ByondService } from '../../../vm/byond.service'; +import { ByondService, VersionStatus } from '../../../vm/byond.service'; import { AsyncPipe } from '@angular/common'; -import { TuiLoaderModule } from '@taiga-ui/core'; +import { TuiButtonModule, TuiLoaderModule } from '@taiga-ui/core'; +import { TuiBadgeModule } from '@taiga-ui/kit'; @Component({ selector: 'app-panel-byond', standalone: true, - imports: [AsyncPipe, TuiLoaderModule], + imports: [AsyncPipe, TuiLoaderModule, TuiButtonModule, TuiBadgeModule], templateUrl: './byond.component.html', styleUrl: './byond.component.scss', }) @@ -15,4 +16,12 @@ export default class ByondPanel { static title = 'BYOND versions'; constructor(protected byondService: ByondService) {} + + protected statusToMessage: Record = { + [VersionStatus.Fetching]: 'Downloading...', + [VersionStatus.Fetched]: 'Downloaded', + [VersionStatus.Loading]: 'Loading...', + [VersionStatus.Extracting]: 'Extracting...', + [VersionStatus.Loaded]: 'Loaded', + }; } diff --git a/src/utils/sharedLock.ts b/src/utils/sharedLock.ts new file mode 100644 index 0000000..760a197 --- /dev/null +++ b/src/utils/sharedLock.ts @@ -0,0 +1,26 @@ +export class SharedLock { + private chain = Promise.resolve(); + + public wrap Promise>( + fn: T, + ): (...args: [...Parameters, skipLock?: boolean]) => ReturnType { + return (...args) => { + let skipLock = false; + let params: Parameters; + + if (args.length !== fn.length) skipLock = args.pop() ?? false; + params = args as any; + if (skipLock) return fn(...params) as ReturnType; + return this.run(fn, ...params); + }; + } + + public run Promise>( + fn: T, + ...args: Parameters + ): ReturnType { + return (this.chain = this.chain.finally(() => + fn(...args), + )) as ReturnType; + } +} diff --git a/src/vm/byond.service.ts b/src/vm/byond.service.ts index 97f61ff..a8539ea 100644 --- a/src/vm/byond.service.ts +++ b/src/vm/byond.service.ts @@ -1,51 +1,133 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { firstValueFrom, map } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; +import { SharedLock } from '../utils/sharedLock'; +import { CommandQueueService } from './commandQueue.service'; +import { EmulatorService } from './emulator.service'; + +export enum VersionStatus { + Fetching, + Fetched, + Loading, + Extracting, + Loaded, +} @Injectable({ providedIn: 'root', }) export class ByondService { - public latestVersion: Promise<{ beta?: ByondVersion; stable: ByondVersion }>; + public latestVersion: Promise<{ beta?: string; stable: string }>; + private lock = new SharedLock(); - constructor(httpClient: HttpClient) { + constructor( + private httpClient: HttpClient, + private commandQueueService: CommandQueueService, + private emulatorService: EmulatorService, + ) { this.latestVersion = firstValueFrom( - httpClient - .get('https://secure.byond.com/download/version.txt', { - responseType: 'text', - }) - .pipe( - map((x) => { - const [stable, beta] = x - .split('\n') - .filter((x) => x) - .map((x) => new ByondVersion(x)); - return { stable, beta }; - }), - ), + httpClient.get('https://secure.byond.com/download/version.txt', { + responseType: 'text', + }), + ).then((x) => { + const [stable, beta] = x.split('\n').filter((x) => x); + return { stable, beta }; + }); + void this.lock.run(() => + commandQueueService.runToSuccess( + '/bin/mkdir', + '-p\0/mnt/host/byond\0/var/lib/byond', + ), ); + void this.lock.run(async () => { + for await (const version of (await this.getByondFolder()).keys()) { + this._versions.set(version, VersionStatus.Fetched); + } + }); } -} -export class ByondVersion { - public readonly major: number; - public readonly minor: number; - - constructor(version: string); - constructor(major: number, minor: number); - constructor(versionOrMajor: string | number, minor?: number) { - if (typeof versionOrMajor === 'number') { - this.major = versionOrMajor; - this.minor = minor!; - } else { - console.log(versionOrMajor.split('.')); - const [major, minor] = versionOrMajor.split('.').map((x) => parseInt(x)); - this.major = major; - this.minor = minor; - } + private _versions = new Map(); + + public get versions(): ReadonlyMap { + return this._versions; } - toString() { - return `${this.major}.${this.minor}`; + public deleteVersion = this.lock.wrap(async (version: string) => { + const installs = await this.getByondFolder(); + await installs.removeEntry(version.toString()); + this._versions.delete(version.toString()); + await this.commandQueueService.runToCompletion( + '/bin/rm', + `-rf\0/var/lib/byond/${version}.zip\0/var/lib/byond/${version}`, + ); + }); + public getVersion = this.lock.wrap(async (version: string) => { + try { + const installs = await this.getByondFolder(); + const handle = await installs.getFileHandle(version.toString(), { + create: true, + }); + const readHandle = await handle.getFile(); + if (readHandle.size != 0) return readHandle; + + this._versions.set(version.toString(), VersionStatus.Fetching); + const major = version.split('.')[0]; + const zipFile = await firstValueFrom( + this.httpClient.get( + `https://www.byond.com/download/build/${major}/${version}_byond_linux.zip`, + { responseType: 'blob' }, + ), + ); + const writeHandle = await handle.createWritable(); + await writeHandle.write(zipFile); + this._versions.set(version.toString(), VersionStatus.Fetched); + await writeHandle.close(); + return new File([zipFile], version); + } catch (e) { + void this.deleteVersion(version); + this._versions.delete(version.toString()); + throw e; + } + }); + public setActive = this.lock.wrap(async (version: string) => { + const status = this._versions.get(version); + if (status == null || status < VersionStatus.Fetched) return; + + if (status < VersionStatus.Loaded) { + try { + this._versions.set(version, VersionStatus.Loading); + const zipFile = await this.getVersion(version, true); + await this.emulatorService.sendFile( + `byond/${version}.zip`, + new Uint8Array(await zipFile.arrayBuffer()), + ); + this._versions.set(version, VersionStatus.Extracting); + await this.commandQueueService.runToSuccess( + '/bin/mv', + `/mnt/host/byond/${version}.zip\0/var/lib/byond/`, + ); + await this.commandQueueService.runToSuccess( + '/bin/unzip', + `/var/lib/byond/${version}.zip\0byond/bin*\0-j\0-d\0/var/lib/byond/${version}`, + ); + await this.commandQueueService.runToSuccess( + '/bin/rm', + `/var/lib/byond/${version}.zip`, + ); + this._versions.set(version, VersionStatus.Loaded); + } catch (e) { + this._versions.set(version, VersionStatus.Fetched); + await this.commandQueueService.runToCompletion( + '/bin/rm', + `-rf\0/var/lib/byond/${version}.zip\0/var/lib/byond/${version}`, + ); + throw e; + } + } + }); + + private async getByondFolder() { + const opfs = await navigator.storage.getDirectory(); + return await opfs.getDirectoryHandle('byond', { create: true }); } } diff --git a/src/vm/commandQueue.service.ts b/src/vm/commandQueue.service.ts index f98cefd..8101bbc 100644 --- a/src/vm/commandQueue.service.ts +++ b/src/vm/commandQueue.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { EmulatorService } from './emulator.service'; import { Process } from './process'; import { Port } from '../utils/literalConstants'; +import { firstValueFrom } from 'rxjs'; export interface CommandResultOK { status: 'OK'; @@ -393,4 +394,23 @@ export class CommandQueueService { return trackedProcess; } + + public async runToCompletion(...args: Parameters) { + let process = await this.runProcess(...args); + let exit = await firstValueFrom(process.exit); + if (exit.cause == 'exit' && exit.code != 0) + throw new Error('Process exited abnormally: exit code ' + exit.code, { + cause: exit, + }); + return exit; + } + + public async runToSuccess(...args: Parameters) { + let exit = await this.runToCompletion(...args); + if (exit.cause == 'exit' && exit.code != 0) + throw new Error('Process exited abnormally: exit code ' + exit.code, { + cause: exit, + }); + return exit; + } } diff --git a/src/vm/emulator.worker.ts b/src/vm/emulator.worker.ts index 69d03d4..f401c47 100644 --- a/src/vm/emulator.worker.ts +++ b/src/vm/emulator.worker.ts @@ -168,6 +168,7 @@ onmessage = ({ data: e }: MessageEvent) => { } case 'sendFile': { emulator.create_file(e.name, e.data).then(() => { + //TODO: wrap promise for errors postMessage({ command: 'asyncResponse', commandID: e.commandID, diff --git a/tsconfig.json b/tsconfig.json index 5ac9778..08f78e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,11 @@ "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, - "lib": ["ES2022", "dom"] + "lib": [ + "ES2022", + "dom", + "dom.asynciterable" + ] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false,