js-dos 7.4.9 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build.yml +51 -0
- package/.gitmodules +4 -0
- package/README.deployment.md +11 -0
- package/README.md +29 -89
- package/index.html +106 -0
- package/package.json +46 -71
- package/postcss.config.cjs +8 -0
- package/src/app.css +14 -0
- package/src/base.css +10 -0
- package/src/components/checkbox.tsx +41 -0
- package/src/components/close-button.tsx +17 -0
- package/src/components/components.css +28 -0
- package/src/components/dos-option-checkbox.tsx +94 -0
- package/src/components/dos-option-select.tsx +108 -0
- package/src/components/dos-option-slider.tsx +35 -0
- package/src/components/error.tsx +16 -0
- package/src/components/loading.tsx +26 -0
- package/src/components/lock.tsx +10 -0
- package/src/components/select.tsx +33 -0
- package/src/components/slider.tsx +69 -0
- package/src/download-file.ts +18 -0
- package/src/frame/account-frame.tsx +156 -0
- package/src/frame/editor/defaults.ts +419 -0
- package/src/frame/editor/editor-conf-frame.tsx +226 -0
- package/src/frame/editor/editor-frame.css +55 -0
- package/src/frame/editor/editor-fs-frame.tsx +314 -0
- package/src/frame/fat-drives-frame.tsx +135 -0
- package/src/frame/frame.css +119 -0
- package/src/frame/frame.tsx +34 -0
- package/src/frame/image-rendering-frame.tsx +8 -0
- package/src/frame/network-frame.tsx +157 -0
- package/src/frame/quick-save-frame.tsx +96 -0
- package/src/frame/settings-frame.tsx +14 -0
- package/src/frame/stats-frame.tsx +87 -0
- package/src/host/bundle-storage.ts +91 -0
- package/src/host/fullscreen.ts +35 -0
- package/src/host/lcache.ts +203 -0
- package/src/host/lstorage.ts +156 -0
- package/src/i18n.ts +179 -0
- package/src/index.css +7 -0
- package/src/load.ts +132 -0
- package/src/login/login.css +9 -0
- package/src/login/login.tsx +77 -0
- package/src/main.tsx +197 -0
- package/src/public/types.ts +41 -0
- package/src/sidebar/account-button.tsx +33 -0
- package/src/sidebar/cloud-save-button.tsx +89 -0
- package/src/sidebar/diskette-icon.tsx +14 -0
- package/src/sidebar/fullscreen-button.tsx +44 -0
- package/src/sidebar/network-button.tsx +39 -0
- package/src/sidebar/sidebar-button.tsx +244 -0
- package/src/sidebar/sidebar.css +47 -0
- package/src/sidebar/sidebar.tsx +30 -0
- package/src/sockdrive/LICENSE +674 -0
- package/src/sockdrive/README.md +45 -0
- package/src/sockdrive/include/sockdrive.h +23 -0
- package/src/sockdrive/js/.eslintrc.json +43 -0
- package/src/sockdrive/js/package.json +29 -0
- package/src/sockdrive/js/src/fatfs/chains.js +318 -0
- package/src/sockdrive/js/src/fatfs/dir.js +385 -0
- package/src/sockdrive/js/src/fatfs/helpers.js +210 -0
- package/src/sockdrive/js/src/fatfs/index.js +606 -0
- package/src/sockdrive/js/src/fatfs/structs.js +255 -0
- package/src/sockdrive/js/src/fatfs/vol.js +166 -0
- package/src/sockdrive/js/src/sockdrive/cache.ts +133 -0
- package/src/sockdrive/js/src/sockdrive/drive.ts +457 -0
- package/src/sockdrive/js/src/sockdrive/lru.ts +221 -0
- package/src/sockdrive/js/src/sockdrive/types.ts +14 -0
- package/src/sockdrive/js/src/sockdrive-fat.d.ts +64 -0
- package/src/sockdrive/js/src/sockdrive-fat.ts +226 -0
- package/src/sockdrive/js/src/sockdrive-native.ts +100 -0
- package/src/sockdrive/js/src/test/test.ts +288 -0
- package/src/sockdrive/js/tsconfig.json +11 -0
- package/src/sockdrive/js/webpack.config.js +68 -0
- package/src/sockdrive/js/yarn.lock +1808 -0
- package/src/store/auth.ts +115 -0
- package/src/store/dos.ts +305 -0
- package/src/store/editor.ts +61 -0
- package/src/store/init.ts +48 -0
- package/src/store/storage.ts +32 -0
- package/src/store/ui.ts +186 -0
- package/src/store.tsx +108 -0
- package/src/ui.tsx +104 -0
- package/src/v8/changes.ts +56 -0
- package/src/v8/config.ts +18 -0
- package/src/v8/subscriptions.ts +62 -0
- package/src/vite-env.d.ts +2 -0
- package/src/window/dos/controls/keyboard.ts +32 -0
- package/src/window/dos/controls/keys.ts +331 -0
- package/src/window/dos/controls/mouse/mount.ts +99 -0
- package/src/window/dos/controls/mouse/mouse-locked.ts +51 -0
- package/src/window/dos/controls/mouse/mouse-not-locked.ts +84 -0
- package/src/window/dos/controls/mouse/mouse-swipe.ts +57 -0
- package/src/window/dos/controls/mouse/pointer.ts +85 -0
- package/src/window/dos/controls/mouse.ts +21 -0
- package/src/window/dos/dos-dhry2.tsx +191 -0
- package/src/window/dos/dos-runtime.tsx +214 -0
- package/src/window/dos/dos-window.tsx +84 -0
- package/src/window/dos/render/canvas.ts +65 -0
- package/src/window/dos/render/resize.ts +31 -0
- package/src/window/dos/render/webgl.ts +173 -0
- package/src/window/dos/sound/audio-node.ts +148 -0
- package/src/window/error-window.tsx +9 -0
- package/src/window/loading-window.tsx +54 -0
- package/src/window/prerun-window.tsx +123 -0
- package/src/window/select-window.tsx +118 -0
- package/src/window/window.css +78 -0
- package/src/window/window.tsx +49 -0
- package/src/ws/ws-sockdrive.ts +101 -0
- package/src/ws/ws-transport-layer.ts +455 -0
- package/tailwind.config.cjs +48 -0
- package/tsconfig.json +32 -0
- package/tsconfig.node.json +9 -0
- package/vite.config.ts +22 -0
- package/LICENSE +0 -339
- package/dist/emulators-ui-loader.png +0 -0
- package/dist/js-dos.css +0 -1690
- package/dist/js-dos.js +0 -17
- package/dist/js-dos.js.map +0 -1
- package/dist/types/src/backend/v7/latency.d.ts +0 -8
- package/dist/types/src/backend/v7/personal.d.ts +0 -2
- package/dist/types/src/backend/v7/v7-config.d.ts +0 -16
- package/dist/types/src/components/action-bar.d.ts +0 -2
- package/dist/types/src/components/action-hide.d.ts +0 -4
- package/dist/types/src/components/action-save-or-exit.d.ts +0 -4
- package/dist/types/src/components/client.d.ts +0 -6
- package/dist/types/src/components/controls.d.ts +0 -8
- package/dist/types/src/components/cpu-control.d.ts +0 -2
- package/dist/types/src/components/horizontal-slider.d.ts +0 -13
- package/dist/types/src/components/region.d.ts +0 -6
- package/dist/types/src/components/scale-control.d.ts +0 -3
- package/dist/types/src/components/sensitivity-control.d.ts +0 -3
- package/dist/types/src/components/sidebar/latency-info.d.ts +0 -7
- package/dist/types/src/components/sidebar/main.d.ts +0 -2
- package/dist/types/src/components/sidebar/networking.d.ts +0 -2
- package/dist/types/src/components/sidebar/token/token-add-time.d.ts +0 -2
- package/dist/types/src/components/sidebar/token/token-select.d.ts +0 -2
- package/dist/types/src/components/sidebar/token/token.d.ts +0 -17
- package/dist/types/src/components/sidebar.d.ts +0 -2
- package/dist/types/src/components/sync-control.d.ts +0 -2
- package/dist/types/src/components/tip.d.ts +0 -2
- package/dist/types/src/components/vertical-slider.d.ts +0 -12
- package/dist/types/src/components/volume-control.d.ts +0 -3
- package/dist/types/src/dom.d.ts +0 -8
- package/dist/types/src/hardware-transport-layer.d.ts +0 -18
- package/dist/types/src/icons.d.ts +0 -95
- package/dist/types/src/player-app.d.ts +0 -50
- package/dist/types/src/player.d.ts +0 -29
- package/dist/types/src/request.d.ts +0 -1
- package/dist/types/src/xhr.d.ts +0 -6
- package/dist/types/v7-services/src/personal.d.ts +0 -3
- package/dist/wdosbox.js +0 -55
- package/dist/wdosbox.js.symbols +0 -3174
- package/dist/wdosbox.wasm +0 -0
- package/examples/digger/index.html +0 -71
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
.jsdos-rso {
|
|
2
|
+
.window {
|
|
3
|
+
@apply overflow-hidden;
|
|
4
|
+
|
|
5
|
+
.background-image {
|
|
6
|
+
@apply absolute left-12 right-0 h-full pointer-events-none;
|
|
7
|
+
background-position: center;
|
|
8
|
+
background-size: cover;
|
|
9
|
+
background-repeat: no-repeat;
|
|
10
|
+
&::after {
|
|
11
|
+
position: relative;
|
|
12
|
+
content: "";
|
|
13
|
+
display: block;
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 100%;
|
|
16
|
+
background-color: hsl(var(--b1)/var(--tw-bg-opacity));
|
|
17
|
+
opacity: 0.75;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.play-button {
|
|
22
|
+
&:hover {
|
|
23
|
+
color: hsl(var(--af));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.dhry2-window {
|
|
28
|
+
@apply absolute left-0 top-0 w-full h-full flex flex-col items-center justify-center;
|
|
29
|
+
@apply bg-black bg-opacity-80 text-2xl px-8 py-4 text-white;
|
|
30
|
+
|
|
31
|
+
.title {
|
|
32
|
+
@apply mb-4 text-center text-4xl;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.backend {
|
|
36
|
+
@apply mb-8 text-center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.results {
|
|
40
|
+
@apply grid grid-cols-2 gap-4;
|
|
41
|
+
|
|
42
|
+
div:nth-child(even) {
|
|
43
|
+
@apply text-green-300;
|
|
44
|
+
|
|
45
|
+
span {
|
|
46
|
+
@apply text-white;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
div:nth-child(2),
|
|
51
|
+
div:last-child {
|
|
52
|
+
@apply text-yellow-300;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.pre-run-window {
|
|
59
|
+
@apply overflow-x-hidden overflow-y-auto flex-grow flex flex-col items-center justify-center px-8 m-auto;
|
|
60
|
+
|
|
61
|
+
.slider {
|
|
62
|
+
@apply justify-center w-80 max-w-none;
|
|
63
|
+
|
|
64
|
+
.label {
|
|
65
|
+
@apply flex-grow;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.touch {
|
|
69
|
+
@apply w-40 flex-grow-0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.select-window {
|
|
75
|
+
@apply m-auto;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useSelector } from "react-redux";
|
|
2
|
+
import { State } from "../store";
|
|
3
|
+
import { DosWindow } from "./dos/dos-window";
|
|
4
|
+
import { ErrorWindow } from "./error-window";
|
|
5
|
+
import { LoadingWindow } from "./loading-window";
|
|
6
|
+
import { PreRunWindow } from "./prerun-window";
|
|
7
|
+
import { SelectWindow } from "./select-window";
|
|
8
|
+
|
|
9
|
+
export function Window(props: {}) {
|
|
10
|
+
const frameOpened = useSelector((state: State) => state.ui.frame) !== "none";
|
|
11
|
+
const frameXs = useSelector((state: State) => state.ui.frameXs);
|
|
12
|
+
const window = useSelector((state: State) => state.ui.window);
|
|
13
|
+
const background = useSelector((state: State) => state.ui.background);
|
|
14
|
+
|
|
15
|
+
let windowComponent = <Loading />;
|
|
16
|
+
switch (window) {
|
|
17
|
+
case "error": {
|
|
18
|
+
windowComponent = <ErrorWindow />;
|
|
19
|
+
} break;
|
|
20
|
+
case "loading": {
|
|
21
|
+
windowComponent = <LoadingWindow />;
|
|
22
|
+
} break;
|
|
23
|
+
case "prerun": {
|
|
24
|
+
windowComponent = <PreRunWindow />;
|
|
25
|
+
} break;
|
|
26
|
+
case "run": {
|
|
27
|
+
windowComponent = <DosWindow />;
|
|
28
|
+
} break;
|
|
29
|
+
case "select": {
|
|
30
|
+
windowComponent = <SelectWindow />;
|
|
31
|
+
} break;
|
|
32
|
+
default: ;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return <div class="window absolute w-full h-full bg-base-100">
|
|
36
|
+
<div class="background-image"
|
|
37
|
+
style={{ backgroundImage: (background ? "url(" + background + ")" : undefined ) }} />
|
|
38
|
+
<div class="relative flex flex-row h-full w-full items-start justify-start overflow-auto">
|
|
39
|
+
<div class={(frameOpened ? (frameXs ? "w-24" : "w-96") : "w-12") + " flex-shrink-0"}></div>
|
|
40
|
+
{windowComponent}
|
|
41
|
+
</div>
|
|
42
|
+
</div>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Loading() {
|
|
46
|
+
return <div class="w-full h-full flex justify-center items-center">
|
|
47
|
+
<div class="radial-progress animate-spin" style="--value:70;"></div>
|
|
48
|
+
</div>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Handle, Stats } from "../sockdrive/js/src/sockdrive/types";
|
|
2
|
+
import { Drive } from "../sockdrive/js/src/sockdrive/drive";
|
|
3
|
+
|
|
4
|
+
interface Template {
|
|
5
|
+
name: string,
|
|
6
|
+
size: number,
|
|
7
|
+
heads: number,
|
|
8
|
+
cylinders: number,
|
|
9
|
+
sectors: number,
|
|
10
|
+
sectorSize: number,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ReadResponse {
|
|
14
|
+
buffer?: Uint8Array,
|
|
15
|
+
code: number,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createSockdrive(
|
|
19
|
+
onOpen: (drive: string, read: boolean, write: boolean) => void,
|
|
20
|
+
onError: (e: Error) => void) {
|
|
21
|
+
let seq = 0;
|
|
22
|
+
const mapping: { [handle: Handle]: Drive } = {};
|
|
23
|
+
const templates: { [handle: number]: Template } = {};
|
|
24
|
+
const memory: { [handle: number]: Uint8Array } = {};
|
|
25
|
+
const stats: Stats = {
|
|
26
|
+
read: 0,
|
|
27
|
+
write: 0,
|
|
28
|
+
readTotalTime: 0,
|
|
29
|
+
cacheHit: 0,
|
|
30
|
+
cacheMiss: 0,
|
|
31
|
+
cacheUsed: 0,
|
|
32
|
+
};
|
|
33
|
+
const sockdrive = {
|
|
34
|
+
stats,
|
|
35
|
+
open: async (url: string, owner: string, name: string, token: string, aheadRange: number): Promise<Handle> => {
|
|
36
|
+
const response = await fetch(url.replace("wss://", "https://")
|
|
37
|
+
.replace("ws://", "http://") + "/template/" + owner + "/" + name);
|
|
38
|
+
const template = await response.json();
|
|
39
|
+
if (template.error) {
|
|
40
|
+
throw new Error(template.error);
|
|
41
|
+
}
|
|
42
|
+
seq++;
|
|
43
|
+
templates[seq] = {
|
|
44
|
+
name: template.name,
|
|
45
|
+
size: template.size ?? 0,
|
|
46
|
+
heads: template.heads ?? 1,
|
|
47
|
+
cylinders: template.cylinders ?? 520,
|
|
48
|
+
sectors: template.sectors ?? 63,
|
|
49
|
+
sectorSize: template.sector_size ?? 512,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const sectorSize = templates[seq].sectorSize;
|
|
53
|
+
memory[seq] = new Uint8Array(sectorSize + sectorSize * aheadRange);
|
|
54
|
+
mapping[seq] = new Drive(url, owner, name, token, stats, { HEAPU8: memory[seq] });
|
|
55
|
+
mapping[seq].onOpen((read, write) => {
|
|
56
|
+
onOpen(owner + "/" + name, read, write);
|
|
57
|
+
});
|
|
58
|
+
mapping[seq].onError(onError);
|
|
59
|
+
return seq;
|
|
60
|
+
},
|
|
61
|
+
read: (handle: Handle, sector: number): Promise<ReadResponse> => {
|
|
62
|
+
if (mapping[handle]) {
|
|
63
|
+
return (mapping[handle].read(sector, 0, false) as Promise<number>).then((code) => {
|
|
64
|
+
return {
|
|
65
|
+
code,
|
|
66
|
+
buffer: memory[handle].slice(templates[seq].sectorSize),
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.error("ERROR! sockdrive handle", handle, "not found");
|
|
72
|
+
return Promise.resolve({ code: 1 });
|
|
73
|
+
},
|
|
74
|
+
write: (handle: Handle, sector: number, buffer: Uint8Array): number => {
|
|
75
|
+
if (buffer.byteLength != templates[handle].sectorSize) {
|
|
76
|
+
onError(new Error("sockdrive write buffer size " + buffer.byteLength +
|
|
77
|
+
" != sector size " + templates[handle].sectorSize));
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
if (mapping[handle]) {
|
|
81
|
+
memory[handle].set(buffer, 0);
|
|
82
|
+
return mapping[handle].write(sector, 0);
|
|
83
|
+
}
|
|
84
|
+
console.error("ERROR! sockdrive handle", handle, "not found");
|
|
85
|
+
return 1;
|
|
86
|
+
},
|
|
87
|
+
close: (handle: Handle) => {
|
|
88
|
+
if (mapping[handle]) {
|
|
89
|
+
mapping[handle].close();
|
|
90
|
+
delete memory[handle];
|
|
91
|
+
delete templates[handle];
|
|
92
|
+
delete mapping[handle];
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
template: (handle: Handle) => {
|
|
96
|
+
return templates[handle];
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return sockdrive;
|
|
101
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TransportLayer, ClientMessage, MessageHandler,
|
|
3
|
+
ServerMessage, DataChunk, FsNode,
|
|
4
|
+
} from "emulators/dist/types/protocol/protocol";
|
|
5
|
+
|
|
6
|
+
import { createSockdrive } from "./ws-sockdrive";
|
|
7
|
+
|
|
8
|
+
const sockdriveConfirmWrite = false;
|
|
9
|
+
|
|
10
|
+
export interface Hardware {
|
|
11
|
+
readConfig(): string;
|
|
12
|
+
sendMessage(payload: string): void;
|
|
13
|
+
addKey(key: number, pressed: number, timeMs: number): void;
|
|
14
|
+
mouseMove(x: number, y: number, relative: boolean, timeMs: number): void;
|
|
15
|
+
mouseButton(button: number, pressed: number, timeMs: number): void;
|
|
16
|
+
getFramePayload(): string;
|
|
17
|
+
|
|
18
|
+
createFile(path: string): string;
|
|
19
|
+
appendFile(blob: string): string;
|
|
20
|
+
writeFile(path: string): string;
|
|
21
|
+
closeFile(): string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const textDecoder = new TextDecoder();
|
|
25
|
+
const textEncoder = new TextEncoder();
|
|
26
|
+
const fMultiplier = 200000000;
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line max-len
|
|
29
|
+
const clientMessageValues: ClientMessage[] = [
|
|
30
|
+
"wc-install", "wc-run", "wc-pack-fs-to-bundle", "wc-add-key", "wc-mouse-move", "wc-mouse-button", "wc-mouse-sync",
|
|
31
|
+
"wc-exit", "wc-sync-sleep", "wc-pause", "wc-resume", "wc-mute", "wc-unmute", "wc-connect", "wc-disconnect",
|
|
32
|
+
"wc-backend-event", "wc-asyncify-stats", "wc-fs-tree", "wc-fs-get-file", "wc-send-data-chunk",
|
|
33
|
+
];
|
|
34
|
+
const clientMessageEnum: { [msg: string]: number } = {};
|
|
35
|
+
clientMessageValues.forEach((v, i) => clientMessageEnum[v] = i);
|
|
36
|
+
|
|
37
|
+
const serverMessageValues: ServerMessage[] = [
|
|
38
|
+
"ws-extract-progress", "ws-ready", "ws-server-ready", "ws-frame-set-size",
|
|
39
|
+
"ws-update-lines", "ws-log", "ws-warn", "ws-err",
|
|
40
|
+
"ws-stdout", "ws-exit", "ws-persist", "ws-sound-init", "ws-sound-push",
|
|
41
|
+
"ws-config", "ws-sync-sleep", "ws-connected", "ws-disconnected",
|
|
42
|
+
"ws-asyncify-stats", "ws-fs-tree", "ws-send-data-chunk",
|
|
43
|
+
];
|
|
44
|
+
const serverMessageEnum: { [num: string]: ServerMessage } = {};
|
|
45
|
+
serverMessageValues.forEach((v, i) => serverMessageEnum[i] = v);
|
|
46
|
+
|
|
47
|
+
export class WsTransportLayer implements TransportLayer {
|
|
48
|
+
socket: WebSocket;
|
|
49
|
+
sessionId: string = Date.now() + "";
|
|
50
|
+
hardware: Hardware;
|
|
51
|
+
|
|
52
|
+
private cycles = 0;
|
|
53
|
+
private sockdrive = createSockdrive(this.onSockdriveOpen.bind(this), this.onSockdriveError.bind(this));
|
|
54
|
+
|
|
55
|
+
private handler: MessageHandler = () => {/**/};
|
|
56
|
+
|
|
57
|
+
private writeUint32(container: Uint8Array, value: number, offset: number) {
|
|
58
|
+
container[offset] = value & 0xFF;
|
|
59
|
+
container[offset + 1] = (value & 0x0000FF00) >> 8;
|
|
60
|
+
container[offset + 2] = (value & 0x00FF0000) >> 16;
|
|
61
|
+
container[offset + 3] = (value & 0xFF000000) >> 24;
|
|
62
|
+
return offset + 4;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private readUint32(container: Uint8Array, offset: number) {
|
|
66
|
+
return (container[offset] & 0x000000FF) |
|
|
67
|
+
((container[offset + 1] << 8) & 0x0000FF00) |
|
|
68
|
+
((container[offset + 2] << 16) & 0x00FF0000) |
|
|
69
|
+
((container[offset + 3] << 24) & 0xFF000000);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private readUint64(container: Uint8Array, offset: number) {
|
|
73
|
+
return this.readUint32(container, offset) + this.readUint32(container, offset + 4) * 2 ** 32;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private sendMessageToSocket(id: ClientMessage | number, ...payload: (Uint8Array | null)[]) {
|
|
77
|
+
let length = 1;
|
|
78
|
+
for (const next of payload) {
|
|
79
|
+
length += 4 + (next?.length ?? 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const encoded = new Uint8Array(length);
|
|
83
|
+
encoded[0] = typeof id === "string" ? clientMessageEnum[id] : id;
|
|
84
|
+
let position = 1;
|
|
85
|
+
for (const next of payload) {
|
|
86
|
+
this.writeUint32(encoded, next?.length ?? 0, position);
|
|
87
|
+
position += 4;
|
|
88
|
+
if (next) {
|
|
89
|
+
encoded.set(next, position);
|
|
90
|
+
position += next.length;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.socket.send(encoded);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private readMessage(payload: Uint8Array): { id: number, payload: (Uint8Array | null)[] } {
|
|
98
|
+
const out: Uint8Array[] = [];
|
|
99
|
+
let position = 1;
|
|
100
|
+
while (position + 4 <= payload.length) {
|
|
101
|
+
const length = this.readUint32(payload, position);
|
|
102
|
+
position += 4;
|
|
103
|
+
|
|
104
|
+
if (position + length > payload.length) {
|
|
105
|
+
throw new Error("read_message out of bounds");
|
|
106
|
+
} else {
|
|
107
|
+
out.push(payload.slice(position, position + length));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
position += length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
id: payload[0],
|
|
115
|
+
payload: out,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private onMessage(msg: Uint8Array) {
|
|
120
|
+
const { id, payload } = this.readMessage(msg);
|
|
121
|
+
const message = serverMessageEnum[id];
|
|
122
|
+
switch (message) {
|
|
123
|
+
case "ws-ready":
|
|
124
|
+
case "ws-server-ready":
|
|
125
|
+
case "ws-exit":
|
|
126
|
+
{
|
|
127
|
+
// delay ws-server-ready until ws-sound-init
|
|
128
|
+
if (message !== "ws-server-ready") {
|
|
129
|
+
this.handler(message, {});
|
|
130
|
+
}
|
|
131
|
+
} break;
|
|
132
|
+
case "ws-stdout":
|
|
133
|
+
case "ws-log":
|
|
134
|
+
case "ws-warn":
|
|
135
|
+
case "ws-err": {
|
|
136
|
+
this.handler(message, {
|
|
137
|
+
tag: textDecoder.decode(payload[0]!),
|
|
138
|
+
message: textDecoder.decode(payload[1]!),
|
|
139
|
+
});
|
|
140
|
+
} break;
|
|
141
|
+
case "ws-send-data-chunk": {
|
|
142
|
+
const chunk: DataChunk = {
|
|
143
|
+
type: textDecoder.decode(payload[0]!) as any,
|
|
144
|
+
name: textDecoder.decode(payload[1]!),
|
|
145
|
+
data: payload[2],
|
|
146
|
+
};
|
|
147
|
+
this.handler(message, { chunk });
|
|
148
|
+
} break;
|
|
149
|
+
case "ws-config": {
|
|
150
|
+
this.handler(message, {
|
|
151
|
+
dosboxConf: payload[0]!,
|
|
152
|
+
jsdosConf: textDecoder.decode(payload[1]!),
|
|
153
|
+
});
|
|
154
|
+
} break;
|
|
155
|
+
case "ws-sound-init": {
|
|
156
|
+
this.handler(message, {
|
|
157
|
+
freq: this.readUint32(payload[0]!, 0),
|
|
158
|
+
});
|
|
159
|
+
this.handler("ws-server-ready", {});
|
|
160
|
+
} break;
|
|
161
|
+
case "ws-sound-push": {
|
|
162
|
+
this.handler(message, {
|
|
163
|
+
samples: new Float32Array(payload[0]!.buffer),
|
|
164
|
+
});
|
|
165
|
+
} break;
|
|
166
|
+
case "ws-frame-set-size": {
|
|
167
|
+
this.handler(message, {
|
|
168
|
+
width: this.readUint32(payload[0]!, 0),
|
|
169
|
+
height: this.readUint32(payload[0]!, 4),
|
|
170
|
+
});
|
|
171
|
+
} break;
|
|
172
|
+
case "ws-update-lines": {
|
|
173
|
+
if (payload.length > 0) {
|
|
174
|
+
const lines: { start: number, heapu8: Uint8Array }[] = [];
|
|
175
|
+
for (const next of payload) {
|
|
176
|
+
lines.push({
|
|
177
|
+
start: this.readUint32(next!, 0),
|
|
178
|
+
heapu8: next!.slice(4),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
this.handler(message, { lines });
|
|
182
|
+
}
|
|
183
|
+
} break;
|
|
184
|
+
case "ws-asyncify-stats": {
|
|
185
|
+
this.cycles += this.readUint32(payload[0]!, 0);
|
|
186
|
+
const stats = {
|
|
187
|
+
messageSent: this.readUint32(payload[0]!, 4),
|
|
188
|
+
messageReceived: this.readUint32(payload[0]!, 8),
|
|
189
|
+
messageFrame: this.readUint32(payload[0]!, 12),
|
|
190
|
+
messageSound: this.readUint32(payload[0]!, 16),
|
|
191
|
+
nonSkippableSleepCount: 0,
|
|
192
|
+
sleepCount: 0,
|
|
193
|
+
sleepTime: 0,
|
|
194
|
+
cycles: this.cycles,
|
|
195
|
+
netSent: 0,
|
|
196
|
+
netRecv: 0,
|
|
197
|
+
driveSent: this.sockdrive.stats.write,
|
|
198
|
+
driveRecv: this.sockdrive.stats.read,
|
|
199
|
+
driveRecvTime: this.sockdrive.stats.readTotalTime,
|
|
200
|
+
driveCacheHit: this.sockdrive.stats.cacheHit,
|
|
201
|
+
driveCacheMiss: this.sockdrive.stats.cacheMiss,
|
|
202
|
+
driveCacheUsed: this.sockdrive.stats.cacheUsed,
|
|
203
|
+
};
|
|
204
|
+
this.handler(message, stats);
|
|
205
|
+
} break;
|
|
206
|
+
case "ws-connected": {
|
|
207
|
+
this.handler(message, { networkType: payload[0]![0], address: textDecoder.decode(payload[1]!) });
|
|
208
|
+
} break;
|
|
209
|
+
case "ws-disconnected": {
|
|
210
|
+
this.handler(message, { networkType: payload[0]![0] });
|
|
211
|
+
} break;
|
|
212
|
+
case "ws-fs-tree": {
|
|
213
|
+
const info: { name: string, size: number | null }[] = [];
|
|
214
|
+
for (let i = 0; i < payload.length - 1; ++i) {
|
|
215
|
+
info.push({ name: textDecoder.decode(payload[i]!), size: null });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const sizes = payload[payload.length - 1]!;
|
|
219
|
+
for (let i = 0; i < info.length; ++i) {
|
|
220
|
+
const size = this.readUint32(sizes, i * 4);
|
|
221
|
+
info[i].size = size < 0 ? null : size;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const fsTree: FsNode = {
|
|
225
|
+
name: ".",
|
|
226
|
+
nodes: [],
|
|
227
|
+
size: null,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
function lookupNode(name: string): [FsNode, string] {
|
|
231
|
+
const parts = name.split("/");
|
|
232
|
+
let node: FsNode | undefined = fsTree;
|
|
233
|
+
for (let i = 1; i < parts.length - 1; ++i) {
|
|
234
|
+
const dir = parts[i];
|
|
235
|
+
node = node!.nodes?.find((n) => n.name == dir);
|
|
236
|
+
if (!node) {
|
|
237
|
+
throw new Error("Unable to find node " + name + ", subdir " + dir);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return [node, parts[parts.length - 1]];
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
for (const { name, size } of info) {
|
|
244
|
+
const [node, basename] = lookupNode(name);
|
|
245
|
+
node.nodes?.push({
|
|
246
|
+
name: basename,
|
|
247
|
+
size,
|
|
248
|
+
nodes: size === null ? [] : null,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.handler("ws-fs-tree", {
|
|
253
|
+
fsTree,
|
|
254
|
+
});
|
|
255
|
+
} break;
|
|
256
|
+
case "ws-persist": {
|
|
257
|
+
this.handler("ws-persist", {
|
|
258
|
+
bundle: payload.length > 0 ? payload[0]! : null,
|
|
259
|
+
});
|
|
260
|
+
} break;
|
|
261
|
+
default: {
|
|
262
|
+
if (message === undefined) { // not standard messages
|
|
263
|
+
(async () => {
|
|
264
|
+
switch (id) {
|
|
265
|
+
case 100/* ws-sockdrive-open */: {
|
|
266
|
+
const url = textDecoder.decode(payload[0]!);
|
|
267
|
+
const owner = textDecoder.decode(payload[1]!);
|
|
268
|
+
const name = textDecoder.decode(payload[2]!);
|
|
269
|
+
const token = textDecoder.decode(payload[3]!);
|
|
270
|
+
const aheadRange = this.readUint32(payload[4]!, 0);
|
|
271
|
+
const handle = await this.sockdrive.open(url, owner, name, token, aheadRange);
|
|
272
|
+
const template = this.sockdrive.template(handle);
|
|
273
|
+
const packet = new Uint8Array(4 * 6);
|
|
274
|
+
let offset = 0;
|
|
275
|
+
offset = this.writeUint32(packet, handle, offset);
|
|
276
|
+
offset = this.writeUint32(packet, template.size, offset);
|
|
277
|
+
offset = this.writeUint32(packet, template.heads, offset);
|
|
278
|
+
offset = this.writeUint32(packet, template.cylinders, offset);
|
|
279
|
+
offset = this.writeUint32(packet, template.sectors, offset);
|
|
280
|
+
this.writeUint32(packet, template.sectorSize, offset);
|
|
281
|
+
this.sendMessageToSocket(100, packet);
|
|
282
|
+
} break;
|
|
283
|
+
case 101/* ws-sockdrive-read */: {
|
|
284
|
+
const handle = this.readUint32(payload[0]!, 0);
|
|
285
|
+
const sector = this.readUint32(payload[0]!, 4);
|
|
286
|
+
const response = await this.sockdrive.read(handle, sector);
|
|
287
|
+
const packet = new Uint8Array(4);
|
|
288
|
+
this.writeUint32(packet, response.code, 0);
|
|
289
|
+
this.sendMessageToSocket(101, packet, response.buffer ?? null);
|
|
290
|
+
} break;
|
|
291
|
+
case 102/* ws-sockdrive-write */: {
|
|
292
|
+
const handle = this.readUint32(payload[0]!, 0);
|
|
293
|
+
const sector = this.readUint32(payload[0]!, 4);
|
|
294
|
+
const code = this.sockdrive.write(handle, sector, payload[1]!);
|
|
295
|
+
const packet = new Uint8Array(4);
|
|
296
|
+
this.writeUint32(packet, code, 0);
|
|
297
|
+
if (sockdriveConfirmWrite) {
|
|
298
|
+
this.sendMessageToSocket(102, packet);
|
|
299
|
+
}
|
|
300
|
+
} break;
|
|
301
|
+
case 103/* ws-sockdrive-close */: {
|
|
302
|
+
this.sockdrive.close(this.readUint32(payload[0]!, 0));
|
|
303
|
+
} break;
|
|
304
|
+
default:
|
|
305
|
+
console.log("WARN! Unhandled server non standard message", id, payload);
|
|
306
|
+
}
|
|
307
|
+
})().catch((e) => {
|
|
308
|
+
this.handler("ws-err", {
|
|
309
|
+
tag: "panic",
|
|
310
|
+
message: "sockdrive error: " + (e.message ?? "???"),
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
console.warn("WARN! Unhandled server message", message);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
constructor(socket: WebSocket) {
|
|
321
|
+
this.socket = socket;
|
|
322
|
+
this.socket.addEventListener("error", (e) => {
|
|
323
|
+
this.handler("ws-err", { tag: "ws", message: (e as any).message ?? "Unknown transport layer error" });
|
|
324
|
+
this.handler("ws-exit", {});
|
|
325
|
+
this.socket.close();
|
|
326
|
+
});
|
|
327
|
+
this.socket.addEventListener("message", async (ev) => {
|
|
328
|
+
const blob: Blob = ev.data;
|
|
329
|
+
this.onMessage(new Uint8Array(await blob.arrayBuffer()));
|
|
330
|
+
});
|
|
331
|
+
this.sendMessageToSocket("wc-install");
|
|
332
|
+
this.hardware = (null) as any;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async sendMessageToServer(name: ClientMessage, props?: { [key: string]: any; }) {
|
|
336
|
+
if (props === undefined || props?.sessionId !== this.sessionId) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const messageId = clientMessageEnum[name];
|
|
341
|
+
switch (name) {
|
|
342
|
+
case "wc-run": {
|
|
343
|
+
const token = props.token ?? "";
|
|
344
|
+
this.sendMessageToSocket(messageId, textEncoder.encode(token));
|
|
345
|
+
} break;
|
|
346
|
+
case "wc-send-data-chunk": {
|
|
347
|
+
const chunk: DataChunk = props.chunk;
|
|
348
|
+
this.sendMessageToSocket(messageId,
|
|
349
|
+
textEncoder.encode(chunk.type),
|
|
350
|
+
textEncoder.encode(chunk.name),
|
|
351
|
+
chunk.data ? new Uint8Array(chunk.data) : null);
|
|
352
|
+
} break;
|
|
353
|
+
case "wc-asyncify-stats": {
|
|
354
|
+
this.sendMessageToSocket(messageId);
|
|
355
|
+
} break;
|
|
356
|
+
case "wc-add-key": {
|
|
357
|
+
const payload = new Uint8Array(3 * 4);
|
|
358
|
+
let offset = 0;
|
|
359
|
+
offset = this.writeUint32(payload, props.key, offset);
|
|
360
|
+
offset = this.writeUint32(payload, props.pressed ? 1 : 0, offset);
|
|
361
|
+
this.writeUint32(payload, props.timeMs, offset);
|
|
362
|
+
this.sendMessageToSocket(messageId, payload);
|
|
363
|
+
} break;
|
|
364
|
+
case "wc-mouse-move": {
|
|
365
|
+
const payload = new Uint8Array(3 * 4 + 3);
|
|
366
|
+
let offset = 0;
|
|
367
|
+
offset = this.writeUint32(payload, Math.abs(props.x) * fMultiplier, offset);
|
|
368
|
+
offset = this.writeUint32(payload, Math.abs(props.y) * fMultiplier, offset);
|
|
369
|
+
offset = this.writeUint32(payload, props.timeMs, offset);
|
|
370
|
+
payload[offset] = props.relative ? 1 : 0;
|
|
371
|
+
payload[offset + 1] = props.x >= 0 ? 0 : 1;
|
|
372
|
+
payload[offset + 2] = props.y >= 0 ? 0 : 1;
|
|
373
|
+
this.sendMessageToSocket(messageId, payload);
|
|
374
|
+
} break;
|
|
375
|
+
case "wc-mouse-button": {
|
|
376
|
+
const payload = new Uint8Array(4 + 2);
|
|
377
|
+
payload[0] = props.button;
|
|
378
|
+
payload[1] = props.pressed ? 1 : 0;
|
|
379
|
+
this.writeUint32(payload, props.timeMs, 2);
|
|
380
|
+
this.sendMessageToSocket(messageId, payload);
|
|
381
|
+
} break;
|
|
382
|
+
case "wc-mouse-sync": {
|
|
383
|
+
const payload = new Uint8Array(4);
|
|
384
|
+
this.writeUint32(payload, props.timeMs, 0);
|
|
385
|
+
this.sendMessageToSocket(messageId, payload);
|
|
386
|
+
} break;
|
|
387
|
+
case "wc-connect": {
|
|
388
|
+
this.sendMessageToSocket(messageId, new Uint8Array([props.networkType]),
|
|
389
|
+
textEncoder.encode(props.address));
|
|
390
|
+
} break;
|
|
391
|
+
case "wc-fs-tree": {
|
|
392
|
+
this.sendMessageToSocket(messageId);
|
|
393
|
+
} break;
|
|
394
|
+
case "wc-pack-fs-to-bundle": {
|
|
395
|
+
this.sendMessageToSocket(messageId, new Uint8Array([props.onlyChanges ? 1 : 0]));
|
|
396
|
+
} break;
|
|
397
|
+
default: {
|
|
398
|
+
console.log("Unhandled client message (wc):", name, messageId, props);
|
|
399
|
+
} break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
initMessageHandler(handler: MessageHandler) {
|
|
404
|
+
this.handler = (message, props) => {
|
|
405
|
+
props.sessionId = this.sessionId;
|
|
406
|
+
handler(message, props);
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
exit() {
|
|
411
|
+
this.sendMessageToSocket("wc-exit");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
onSockdriveOpen(drive: string, read: boolean, write: boolean) {
|
|
415
|
+
this.handler("ws-log", {
|
|
416
|
+
tag: "worker",
|
|
417
|
+
message: "sockdrive: " + drive + ", read=" + read + ", write=" + write,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
onSockdriveError(e: Error) {
|
|
422
|
+
this.handler("ws-err", {
|
|
423
|
+
tag: "panic",
|
|
424
|
+
message: e.message ?? "unable to open sockdrive",
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function createWsTransportLayer(url: string): Promise<TransportLayer> {
|
|
430
|
+
return new Promise<TransportLayer>((resolve) => {
|
|
431
|
+
let locked = false;
|
|
432
|
+
const inervalId = setInterval(() => {
|
|
433
|
+
if (locked) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
locked = true;
|
|
438
|
+
const ws = new WebSocket(url);
|
|
439
|
+
const onSuccess = () => {
|
|
440
|
+
clearInterval(inervalId);
|
|
441
|
+
console.log("Connected to", url);
|
|
442
|
+
resolve(new WsTransportLayer(ws));
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
ws.addEventListener("error", (error) => {
|
|
446
|
+
console.error("Can't conect to ", url, error);
|
|
447
|
+
ws.removeEventListener("open", onSuccess);
|
|
448
|
+
ws.close();
|
|
449
|
+
locked = false;
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
ws.addEventListener("open", onSuccess);
|
|
453
|
+
}, 1000);
|
|
454
|
+
});
|
|
455
|
+
}
|