diff --git a/page/package-lock.json b/page/package-lock.json index 91f0a21e..3a9163c8 100644 --- a/page/package-lock.json +++ b/page/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@fontsource/inter": "^5.2.5", + "@irori/idbfs": "^0.5.0", "@radix-ui/react-checkbox": "^1.2.3", "@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-dropdown-menu": "^2.1.12", @@ -1137,6 +1138,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@irori/idbfs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@irori/idbfs/-/idbfs-0.5.0.tgz", + "integrity": "sha512-IHZU8mA9I89YfR4canqTYO51mv7+326fhWKFF5eUJo9tmECoPh0dBLrjpOzh7yhjYO8rr9EGzFI9moJsYtJ/GA==", + "license": "Unlicense" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", diff --git a/page/package.json b/page/package.json index a3262aba..c7fbb61d 100644 --- a/page/package.json +++ b/page/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@fontsource/inter": "^5.2.5", + "@irori/idbfs": "^0.5.0", "@radix-ui/react-checkbox": "^1.2.3", "@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-dropdown-menu": "^2.1.12", diff --git a/page/public/emulator-worker.js b/page/public/emulator-worker.js index c3804f53..c6d705f9 100644 --- a/page/public/emulator-worker.js +++ b/page/public/emulator-worker.js @@ -7,7 +7,7 @@ onmessage = async (event) => { const data = event.data; if (data.message == "run") { const payload = data.data; - runEmulation(payload.filesystem, payload.file, payload.options); + runEmulation(payload.file, payload.options); } else if (data.message == "event") { const payload = data.data; msgQueue.push(payload); @@ -49,22 +49,17 @@ function getMessageFromQueue() { return msgQueue.shift(); } -function runEmulation(filesystem, file, options) { +function runEmulation(file, options) { + const mainArguments = [...options, "-e", "./root", file]; + globalThis.Module = { - arguments: [...options, "-e", "./root", file], + arguments: mainArguments, + noInitialRun: true, onRuntimeInitialized: function () { - filesystem.forEach((e) => { - if (e.name.endsWith("/")) { - FS.mkdir(e.name.slice(0, -1)); - } else { - const dirs = e.name.split("/"); - const file = dirs.pop(); - const buffer = new Uint8Array(e.data); - if (FS.analyzePath(e.name).exists) { - FS.unlink(e.name); - } - FS.createDataFile("/" + dirs.join("/"), file, buffer, true, true); - } + FS.mkdir("/root"); + FS.mount(IDBFS, {}, "/root"); + FS.syncfs(true, function (err) { + Module.callMain(mainArguments); }); }, print: logLine, diff --git a/page/src/Playground.tsx b/page/src/Playground.tsx index 54955c3d..f9b98885 100644 --- a/page/src/Playground.tsx +++ b/page/src/Playground.tsx @@ -4,7 +4,7 @@ import { Output } from "@/components/output"; import { Separator } from "@/components/ui/separator"; import { Emulator, UserFile, EmulationState } from "./emulator"; -import { getFilesystem } from "./filesystem"; +import { setupFilesystem } from "./filesystem"; import "./App.css"; import { @@ -103,11 +103,11 @@ export function Playground() { logLine("Starting emulation..."); - const fs = await getFilesystem((current, total, file) => { + await setupFilesystem((current, total, file) => { logLine(`Processing filesystem (${current}/${total}): ${file}`); }); - const new_emulator = new Emulator(fs, logLines, (_) => forceUpdate()); + const new_emulator = new Emulator(logLines, (_) => forceUpdate()); new_emulator.onTerminate().then(() => setEmulator(null)); setEmulator(new_emulator); diff --git a/page/src/emulator.ts b/page/src/emulator.ts index 123dc823..3bc7f45a 100644 --- a/page/src/emulator.ts +++ b/page/src/emulator.ts @@ -5,6 +5,8 @@ import * as flatbuffers from "flatbuffers"; import * as fbDebugger from "@/fb/debugger"; import * as fbDebuggerEvent from "@/fb/debugger/event"; +import { storeFile } from "./filesystem"; + type LogHandler = (lines: string[]) => void; export interface UserFile { @@ -50,7 +52,6 @@ function decodeEvent(data: string) { type StateChangeHandler = (state: EmulationState) => void; export class Emulator { - filesystem: FileEntry[]; logHandler: LogHandler; stateChangeHandler: StateChangeHandler; terminatePromise: Promise; @@ -59,12 +60,7 @@ export class Emulator { worker: Worker; state: EmulationState = EmulationState.Stopped; - constructor( - filesystem: FileEntry[], - logHandler: LogHandler, - stateChangeHandler: StateChangeHandler, - ) { - this.filesystem = filesystem; + constructor(logHandler: LogHandler, stateChangeHandler: StateChangeHandler) { this.logHandler = logHandler; this.stateChangeHandler = stateChangeHandler; this.terminateResolve = () => {}; @@ -81,7 +77,7 @@ export class Emulator { this.worker.onmessage = this._onMessage.bind(this); } - start( + async start( settings: Settings = createDefaultSettings(), userFile: UserFile | null = null, ) { @@ -90,17 +86,14 @@ export class Emulator { const filename = userFile.name.split("/").pop()?.split("\\").pop(); const canonicalName = filename?.toLowerCase(); file = "c:/" + canonicalName; - this.filesystem.push({ - name: "root/filesys/c/" + canonicalName, - data: userFile.data, - }); + + await storeFile("root/filesys/c/" + canonicalName, userFile.data); } this._setState(EmulationState.Running); this.worker.postMessage({ message: "run", data: { - filesystem: this.filesystem, file, options: translateSettings(settings), }, diff --git a/page/src/filesystem.ts b/page/src/filesystem.ts index 4fddecb2..0b264a27 100644 --- a/page/src/filesystem.ts +++ b/page/src/filesystem.ts @@ -1,4 +1,5 @@ import { parseZipFile, FileEntry, ProgressHandler } from "./zip-file"; +import idbfsModule, { MainModule } from "@irori/idbfs"; function openDatabase(): Promise { return new Promise((resolve, reject) => { @@ -59,23 +60,6 @@ async function getData(id: string) { }); } -async function cacheAndUseData( - id: string, - asyncFunction: () => Promise, -) { - try { - let data = (await getData(id)) as FileEntry[]; - if (!data) { - data = await asyncFunction(); - await saveData(id, data); - } - return data; - } catch (error) { - console.error("Error:", error); - throw error; - } -} - function fetchFilesystemZip() { return fetch("./root.zip?1", { method: "GET", @@ -90,8 +74,57 @@ async function fetchFilesystem(progressHandler: ProgressHandler) { return await parseZipFile(filesys, progressHandler); } -export function getFilesystem(progressHandler: ProgressHandler) { - return cacheAndUseData("emulator-filesystem-2", () => - fetchFilesystem(progressHandler), - ); +function synchronizeIDBFS(idbfs: MainModule, populate: boolean) { + return new Promise((resolve, reject) => { + idbfs.FS.syncfs(populate, function (err: any) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +async function initializeIDBFS() { + const idbfs = await idbfsModule(); + + idbfs.FS.mkdir("/root"); + idbfs.FS.mount(idbfs.IDBFS, {}, "/root"); + + await synchronizeIDBFS(idbfs, true); + + return idbfs; +} + +export async function setupFilesystem(progressHandler: ProgressHandler) { + const idbfs = await initializeIDBFS(); + + if (idbfs.FS.analyzePath("/root/api-set.bin", false).exists) { + return; + } + + const filesystem = await fetchFilesystem(progressHandler); + + filesystem.forEach((e) => { + if (idbfs.FS.analyzePath("/" + e.name, false).exists) { + return; + } + + if (e.name.endsWith("/")) { + idbfs.FS.mkdir("/" + e.name.slice(0, -1)); + } else { + const buffer = new Uint8Array(e.data); + idbfs.FS.writeFile("/" + e.name, buffer); + } + }); + + await synchronizeIDBFS(idbfs, false); +} + +export async function storeFile(file: string, data: ArrayBuffer) { + const idbfs = await initializeIDBFS(); + const buffer = new Uint8Array(data); + idbfs.FS.writeFile(file, buffer); + await synchronizeIDBFS(idbfs, false); }