Skip to content

Commit

Permalink
Basic byond fetching and extracting in the byond panel
Browse files Browse the repository at this point in the history
  • Loading branch information
alexkar598 committed Jun 25, 2024
1 parent 3e40c0e commit 136792e
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 40 deletions.
1 change: 0 additions & 1 deletion src/app/components/panel/panel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export class PanelComponent {

@Input()
public set id(panel: Panel) {
console.log(panel);
switch (panel) {
case Panel.Controller:
this.panelComponent = import(
Expand Down
36 changes: 36 additions & 0 deletions src/app/panels/byond/byond.component.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,48 @@
@if (byondService.latestVersion | async; as latestVersions) {
<p>
Latest stable: <span class="font-bold">{{ latestVersions.stable }}</span>
<button
tuiButton
size="xs"
(click)="byondService.getVersion(latestVersions.stable)"
>
Fetch
</button>
</p>
@if (latestVersions.beta) {
<p>
Latest beta: <span class="font-bold">{{ latestVersions.beta }}</span>
<button
tuiButton
size="xs"
(click)="byondService.getVersion(latestVersions.beta)"
>
Fetch
</button>
</p>
}
} @else {
Loading latest version...
}

<ul>
@for (version of byondService.versions; track version[0]) {
{{ version[0] }} ({{ statusToMessage[version[1]] }})
<button
tuiButton
appearance="primary"
size="xs"
(click)="byondService.setActive(version[0])"
>
Set active
</button>
<button
tuiButton
appearance="secondary-destructive"
size="xs"
(click)="byondService.deleteVersion(version[0])"
>
Delete
</button>
}
</ul>
15 changes: 12 additions & 3 deletions src/app/panels/byond/byond.component.ts
Original file line number Diff line number Diff line change
@@ -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',
})
Expand All @@ -15,4 +16,12 @@ export default class ByondPanel {
static title = 'BYOND versions';

constructor(protected byondService: ByondService) {}

protected statusToMessage: Record<VersionStatus, string> = {
[VersionStatus.Fetching]: 'Downloading...',
[VersionStatus.Fetched]: 'Downloaded',
[VersionStatus.Loading]: 'Loading...',
[VersionStatus.Extracting]: 'Extracting...',
[VersionStatus.Loaded]: 'Loaded',
};
}
26 changes: 26 additions & 0 deletions src/utils/sharedLock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export class SharedLock {
private chain = Promise.resolve();

public wrap<T extends (...args: any[]) => Promise<any>>(
fn: T,
): (...args: [...Parameters<T>, skipLock?: boolean]) => ReturnType<T> {
return (...args) => {
let skipLock = false;
let params: Parameters<T>;

if (args.length !== fn.length) skipLock = args.pop() ?? false;
params = args as any;
if (skipLock) return fn(...params) as ReturnType<T>;
return this.run(fn, ...params);
};
}

public run<T extends (...args: any[]) => Promise<any>>(
fn: T,
...args: Parameters<T>
): ReturnType<T> {
return (this.chain = this.chain.finally(() =>
fn(...args),
)) as ReturnType<T>;
}
}
152 changes: 117 additions & 35 deletions src/vm/byond.service.ts
Original file line number Diff line number Diff line change
@@ -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<string, VersionStatus>();

public get versions(): ReadonlyMap<string, VersionStatus> {
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 });
}
}
20 changes: 20 additions & 0 deletions src/vm/commandQueue.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<C extends Command> {
status: 'OK';
Expand Down Expand Up @@ -393,4 +394,23 @@ export class CommandQueueService {

return trackedProcess;
}

public async runToCompletion(...args: Parameters<typeof this.runProcess>) {
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<typeof this.runProcess>) {
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;
}
}
1 change: 1 addition & 0 deletions src/vm/emulator.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ onmessage = ({ data: e }: MessageEvent<WorkerMsg>) => {
}
case 'sendFile': {
emulator.create_file(e.name, e.data).then(() => {
//TODO: wrap promise for errors
postMessage({
command: 'asyncResponse',
commandID: e.commandID,
Expand Down
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": ["ES2022", "dom"]
"lib": [
"ES2022",
"dom",
"dom.asynciterable"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
Expand Down

0 comments on commit 136792e

Please sign in to comment.