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...
}
+
+
+ @for (version of byondService.versions; track version[0]) {
+ {{ version[0] }} ({{ statusToMessage[version[1]] }})
+
+
+ }
+
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,