Skip to content

Commit 136792e

Browse files
committed
Basic byond fetching and extracting in the byond panel
1 parent 3e40c0e commit 136792e

File tree

8 files changed

+217
-40
lines changed

8 files changed

+217
-40
lines changed

src/app/components/panel/panel.component.ts

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ export class PanelComponent {
2828

2929
@Input()
3030
public set id(panel: Panel) {
31-
console.log(panel);
3231
switch (panel) {
3332
case Panel.Controller:
3433
this.panelComponent = import(
+36
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,48 @@
11
@if (byondService.latestVersion | async; as latestVersions) {
22
<p>
33
Latest stable: <span class="font-bold">{{ latestVersions.stable }}</span>
4+
<button
5+
tuiButton
6+
size="xs"
7+
(click)="byondService.getVersion(latestVersions.stable)"
8+
>
9+
Fetch
10+
</button>
411
</p>
512
@if (latestVersions.beta) {
613
<p>
714
Latest beta: <span class="font-bold">{{ latestVersions.beta }}</span>
15+
<button
16+
tuiButton
17+
size="xs"
18+
(click)="byondService.getVersion(latestVersions.beta)"
19+
>
20+
Fetch
21+
</button>
822
</p>
923
}
1024
} @else {
1125
Loading latest version...
1226
}
27+
28+
<ul>
29+
@for (version of byondService.versions; track version[0]) {
30+
{{ version[0] }} ({{ statusToMessage[version[1]] }})
31+
<button
32+
tuiButton
33+
appearance="primary"
34+
size="xs"
35+
(click)="byondService.setActive(version[0])"
36+
>
37+
Set active
38+
</button>
39+
<button
40+
tuiButton
41+
appearance="secondary-destructive"
42+
size="xs"
43+
(click)="byondService.deleteVersion(version[0])"
44+
>
45+
Delete
46+
</button>
47+
}
48+
</ul>
+12-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { Component } from '@angular/core';
2-
import { ByondService } from '../../../vm/byond.service';
2+
import { ByondService, VersionStatus } from '../../../vm/byond.service';
33
import { AsyncPipe } from '@angular/common';
4-
import { TuiLoaderModule } from '@taiga-ui/core';
4+
import { TuiButtonModule, TuiLoaderModule } from '@taiga-ui/core';
5+
import { TuiBadgeModule } from '@taiga-ui/kit';
56

67
@Component({
78
selector: 'app-panel-byond',
89
standalone: true,
9-
imports: [AsyncPipe, TuiLoaderModule],
10+
imports: [AsyncPipe, TuiLoaderModule, TuiButtonModule, TuiBadgeModule],
1011
templateUrl: './byond.component.html',
1112
styleUrl: './byond.component.scss',
1213
})
@@ -15,4 +16,12 @@ export default class ByondPanel {
1516
static title = 'BYOND versions';
1617

1718
constructor(protected byondService: ByondService) {}
19+
20+
protected statusToMessage: Record<VersionStatus, string> = {
21+
[VersionStatus.Fetching]: 'Downloading...',
22+
[VersionStatus.Fetched]: 'Downloaded',
23+
[VersionStatus.Loading]: 'Loading...',
24+
[VersionStatus.Extracting]: 'Extracting...',
25+
[VersionStatus.Loaded]: 'Loaded',
26+
};
1827
}

src/utils/sharedLock.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export class SharedLock {
2+
private chain = Promise.resolve();
3+
4+
public wrap<T extends (...args: any[]) => Promise<any>>(
5+
fn: T,
6+
): (...args: [...Parameters<T>, skipLock?: boolean]) => ReturnType<T> {
7+
return (...args) => {
8+
let skipLock = false;
9+
let params: Parameters<T>;
10+
11+
if (args.length !== fn.length) skipLock = args.pop() ?? false;
12+
params = args as any;
13+
if (skipLock) return fn(...params) as ReturnType<T>;
14+
return this.run(fn, ...params);
15+
};
16+
}
17+
18+
public run<T extends (...args: any[]) => Promise<any>>(
19+
fn: T,
20+
...args: Parameters<T>
21+
): ReturnType<T> {
22+
return (this.chain = this.chain.finally(() =>
23+
fn(...args),
24+
)) as ReturnType<T>;
25+
}
26+
}

src/vm/byond.service.ts

+117-35
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,133 @@
11
import { Injectable } from '@angular/core';
22
import { HttpClient } from '@angular/common/http';
3-
import { firstValueFrom, map } from 'rxjs';
3+
import { firstValueFrom } from 'rxjs';
4+
import { SharedLock } from '../utils/sharedLock';
5+
import { CommandQueueService } from './commandQueue.service';
6+
import { EmulatorService } from './emulator.service';
7+
8+
export enum VersionStatus {
9+
Fetching,
10+
Fetched,
11+
Loading,
12+
Extracting,
13+
Loaded,
14+
}
415

516
@Injectable({
617
providedIn: 'root',
718
})
819
export class ByondService {
9-
public latestVersion: Promise<{ beta?: ByondVersion; stable: ByondVersion }>;
20+
public latestVersion: Promise<{ beta?: string; stable: string }>;
21+
private lock = new SharedLock();
1022

11-
constructor(httpClient: HttpClient) {
23+
constructor(
24+
private httpClient: HttpClient,
25+
private commandQueueService: CommandQueueService,
26+
private emulatorService: EmulatorService,
27+
) {
1228
this.latestVersion = firstValueFrom(
13-
httpClient
14-
.get('https://secure.byond.com/download/version.txt', {
15-
responseType: 'text',
16-
})
17-
.pipe(
18-
map((x) => {
19-
const [stable, beta] = x
20-
.split('\n')
21-
.filter((x) => x)
22-
.map((x) => new ByondVersion(x));
23-
return { stable, beta };
24-
}),
25-
),
29+
httpClient.get('https://secure.byond.com/download/version.txt', {
30+
responseType: 'text',
31+
}),
32+
).then((x) => {
33+
const [stable, beta] = x.split('\n').filter((x) => x);
34+
return { stable, beta };
35+
});
36+
void this.lock.run(() =>
37+
commandQueueService.runToSuccess(
38+
'/bin/mkdir',
39+
'-p\0/mnt/host/byond\0/var/lib/byond',
40+
),
2641
);
42+
void this.lock.run(async () => {
43+
for await (const version of (await this.getByondFolder()).keys()) {
44+
this._versions.set(version, VersionStatus.Fetched);
45+
}
46+
});
2747
}
28-
}
2948

30-
export class ByondVersion {
31-
public readonly major: number;
32-
public readonly minor: number;
33-
34-
constructor(version: string);
35-
constructor(major: number, minor: number);
36-
constructor(versionOrMajor: string | number, minor?: number) {
37-
if (typeof versionOrMajor === 'number') {
38-
this.major = versionOrMajor;
39-
this.minor = minor!;
40-
} else {
41-
console.log(versionOrMajor.split('.'));
42-
const [major, minor] = versionOrMajor.split('.').map((x) => parseInt(x));
43-
this.major = major;
44-
this.minor = minor;
45-
}
49+
private _versions = new Map<string, VersionStatus>();
50+
51+
public get versions(): ReadonlyMap<string, VersionStatus> {
52+
return this._versions;
4653
}
4754

48-
toString() {
49-
return `${this.major}.${this.minor}`;
55+
public deleteVersion = this.lock.wrap(async (version: string) => {
56+
const installs = await this.getByondFolder();
57+
await installs.removeEntry(version.toString());
58+
this._versions.delete(version.toString());
59+
await this.commandQueueService.runToCompletion(
60+
'/bin/rm',
61+
`-rf\0/var/lib/byond/${version}.zip\0/var/lib/byond/${version}`,
62+
);
63+
});
64+
public getVersion = this.lock.wrap(async (version: string) => {
65+
try {
66+
const installs = await this.getByondFolder();
67+
const handle = await installs.getFileHandle(version.toString(), {
68+
create: true,
69+
});
70+
const readHandle = await handle.getFile();
71+
if (readHandle.size != 0) return readHandle;
72+
73+
this._versions.set(version.toString(), VersionStatus.Fetching);
74+
const major = version.split('.')[0];
75+
const zipFile = await firstValueFrom(
76+
this.httpClient.get(
77+
`https://www.byond.com/download/build/${major}/${version}_byond_linux.zip`,
78+
{ responseType: 'blob' },
79+
),
80+
);
81+
const writeHandle = await handle.createWritable();
82+
await writeHandle.write(zipFile);
83+
this._versions.set(version.toString(), VersionStatus.Fetched);
84+
await writeHandle.close();
85+
return new File([zipFile], version);
86+
} catch (e) {
87+
void this.deleteVersion(version);
88+
this._versions.delete(version.toString());
89+
throw e;
90+
}
91+
});
92+
public setActive = this.lock.wrap(async (version: string) => {
93+
const status = this._versions.get(version);
94+
if (status == null || status < VersionStatus.Fetched) return;
95+
96+
if (status < VersionStatus.Loaded) {
97+
try {
98+
this._versions.set(version, VersionStatus.Loading);
99+
const zipFile = await this.getVersion(version, true);
100+
await this.emulatorService.sendFile(
101+
`byond/${version}.zip`,
102+
new Uint8Array(await zipFile.arrayBuffer()),
103+
);
104+
this._versions.set(version, VersionStatus.Extracting);
105+
await this.commandQueueService.runToSuccess(
106+
'/bin/mv',
107+
`/mnt/host/byond/${version}.zip\0/var/lib/byond/`,
108+
);
109+
await this.commandQueueService.runToSuccess(
110+
'/bin/unzip',
111+
`/var/lib/byond/${version}.zip\0byond/bin*\0-j\0-d\0/var/lib/byond/${version}`,
112+
);
113+
await this.commandQueueService.runToSuccess(
114+
'/bin/rm',
115+
`/var/lib/byond/${version}.zip`,
116+
);
117+
this._versions.set(version, VersionStatus.Loaded);
118+
} catch (e) {
119+
this._versions.set(version, VersionStatus.Fetched);
120+
await this.commandQueueService.runToCompletion(
121+
'/bin/rm',
122+
`-rf\0/var/lib/byond/${version}.zip\0/var/lib/byond/${version}`,
123+
);
124+
throw e;
125+
}
126+
}
127+
});
128+
129+
private async getByondFolder() {
130+
const opfs = await navigator.storage.getDirectory();
131+
return await opfs.getDirectoryHandle('byond', { create: true });
50132
}
51133
}

src/vm/commandQueue.service.ts

+20
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
22
import { EmulatorService } from './emulator.service';
33
import { Process } from './process';
44
import { Port } from '../utils/literalConstants';
5+
import { firstValueFrom } from 'rxjs';
56

67
export interface CommandResultOK<C extends Command> {
78
status: 'OK';
@@ -393,4 +394,23 @@ export class CommandQueueService {
393394

394395
return trackedProcess;
395396
}
397+
398+
public async runToCompletion(...args: Parameters<typeof this.runProcess>) {
399+
let process = await this.runProcess(...args);
400+
let exit = await firstValueFrom(process.exit);
401+
if (exit.cause == 'exit' && exit.code != 0)
402+
throw new Error('Process exited abnormally: exit code ' + exit.code, {
403+
cause: exit,
404+
});
405+
return exit;
406+
}
407+
408+
public async runToSuccess(...args: Parameters<typeof this.runProcess>) {
409+
let exit = await this.runToCompletion(...args);
410+
if (exit.cause == 'exit' && exit.code != 0)
411+
throw new Error('Process exited abnormally: exit code ' + exit.code, {
412+
cause: exit,
413+
});
414+
return exit;
415+
}
396416
}

src/vm/emulator.worker.ts

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ onmessage = ({ data: e }: MessageEvent<WorkerMsg>) => {
168168
}
169169
case 'sendFile': {
170170
emulator.create_file(e.name, e.data).then(() => {
171+
//TODO: wrap promise for errors
171172
postMessage({
172173
command: 'asyncResponse',
173174
commandID: e.commandID,

tsconfig.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
"target": "ES2022",
2020
"module": "ES2022",
2121
"useDefineForClassFields": false,
22-
"lib": ["ES2022", "dom"]
22+
"lib": [
23+
"ES2022",
24+
"dom",
25+
"dom.asynciterable"
26+
]
2327
},
2428
"angularCompilerOptions": {
2529
"enableI18nLegacyMessageIdFormat": false,

0 commit comments

Comments
 (0)