Files
windows-user-space-emulator/page/src/playground.tsx
2025-08-23 19:04:55 +02:00

420 lines
11 KiB
TypeScript

import React from "react";
import { Output } from "@/components/output";
import { Emulator, EmulationState, isFinalState } from "./emulator";
import {
Filesystem,
setupFilesystem,
windowsToInternalPath,
} from "./filesystem";
import { memory64 } from "wasm-feature-detect";
import "./App.css";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Settings, loadSettings, saveSettings } from "./settings";
import { SettingsMenu } from "@/components/settings-menu";
import {
PlayFill,
StopFill,
GearFill,
PauseFill,
HouseFill,
} from "react-bootstrap-icons";
import { StatusIndicator } from "@/components/status-indicator";
import { Header } from "./Header";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer";
import { FilesystemExplorer } from "./filesystem-explorer";
import { EmulationStatus } from "./emulator";
import { EmulationSummary } from "./components/emulation-summary";
import { downloadBinaryFilePercent } from "./download";
export interface PlaygroundFile {
file: string;
storage: string;
}
export interface PlaygroundProps {}
export interface PlaygroundState {
settings: Settings;
filesystemPromise?: Promise<Filesystem>;
filesystem?: Filesystem;
emulator?: Emulator;
emulationStatus?: EmulationStatus;
application?: string;
drawerOpen: boolean;
allowWasm64: boolean;
file?: PlaygroundFile;
}
function decodeFileData(data: string | null): PlaygroundFile | undefined {
if (!data) {
return undefined;
}
try {
const jsonData = JSON.parse(atob(data));
return {
file: jsonData.file,
storage: jsonData.storage,
};
} catch (e) {
console.log(e);
}
return undefined;
}
interface GlobalThisExt {
emulateCache?: string | null;
}
function getGlobalThis() {
return globalThis as GlobalThisExt;
}
export function storeEmulateData(data?: string) {
getGlobalThis().emulateCache = undefined;
if (data) {
localStorage.setItem("emulate", data);
} else {
localStorage.removeItem("emulate");
}
}
function getEmulateData() {
const gt = getGlobalThis();
if (gt.emulateCache) {
return gt.emulateCache;
}
const emulateData = localStorage.getItem("emulate");
localStorage.removeItem("emulate");
gt.emulateCache = emulateData;
return emulateData;
}
export class Playground extends React.Component<
PlaygroundProps,
PlaygroundState
> {
private output: React.RefObject<Output | null>;
private iconCache: Map<string, string | null> = new Map();
constructor(props: PlaygroundProps) {
super(props);
this.output = React.createRef();
this.start = this.start.bind(this);
this.resetFilesys = this.resetFilesys.bind(this);
this.startEmulator = this.startEmulator.bind(this);
this.toggleEmulatorState = this.toggleEmulatorState.bind(this);
this.state = {
settings: loadSettings(),
drawerOpen: false,
allowWasm64: false,
file: decodeFileData(getEmulateData()),
};
}
componentDidMount(): void {
memory64().then((allowWasm64) => {
this.setState({ allowWasm64 });
});
if (this.state.file) {
this.emulateRemoteFile(this.state.file);
}
}
async resetFilesys() {
if (!this.state.filesystem) {
return;
}
await this.state.filesystem.delete();
this.setState({
filesystemPromise: undefined,
filesystem: undefined,
drawerOpen: false,
});
this.output.current?.clear();
location.reload();
}
_onEmulatorStatusChanged(s: EmulationStatus) {
this.setState({ emulationStatus: s });
}
_onEmulatorStateChanged(s: EmulationState, persistFs: boolean) {
if (isFinalState(s) && persistFs) {
this.setState({ filesystemPromise: undefined, filesystem: undefined });
this.initFilesys(true);
} else {
this.forceUpdate();
}
}
initFilesys(force: boolean = false) {
if (!force && this.state.filesystemPromise) {
return this.state.filesystemPromise;
}
const promise = new Promise<Filesystem>((resolve) => {
if (!force) {
this.logLine("Loading filesystem...");
}
setupFilesystem(
(current, total, file) => {
this.logLine(`Processing filesystem (${current}/${total}): ${file}`);
},
(percent) => {
this.logLine(`Downloading filesystem: ${percent}%`);
},
).then(resolve);
});
promise.then((filesystem) => this.setState({ filesystem }));
this.setState({ filesystemPromise: promise });
return promise;
}
setDrawerOpen(drawerOpen: boolean) {
this.setState({ drawerOpen });
}
async downloadFileToFilesystem(file: PlaygroundFile) {
const fs = await this.initFilesys();
const fileData = await downloadBinaryFilePercent(
file.storage,
(percent) => {
this.logLine(`Downloading binary: ${percent}%`);
},
);
await fs.storeFiles([
{
name: windowsToInternalPath(file.file),
data: fileData,
},
]);
}
async emulateRemoteFile(file: PlaygroundFile) {
await this.downloadFileToFilesystem(file);
await this.startEmulator(file.file);
}
async start() {
await this.initFilesys();
this.setDrawerOpen(true);
}
logLine(line: string) {
this.output.current?.logLine(line);
}
logLines(lines: string[]) {
this.output.current?.logLines(lines);
}
isEmulatorPaused() {
return (
this.state.emulator &&
this.state.emulator.getState() == EmulationState.Paused
);
}
toggleEmulatorState() {
if (this.isEmulatorPaused()) {
this.state.emulator?.resume();
} else {
this.state.emulator?.pause();
}
}
async startEmulator(userFile: string) {
this.state.emulator?.stop();
this.output.current?.clear();
this.setDrawerOpen(false);
this.logLine("Starting emulation...");
if (this.state.filesystemPromise) {
await this.state.filesystemPromise;
}
const persistFs = this.state.settings.persist;
const new_emulator = new Emulator(
(l) => this.logLines(l),
(s) => this._onEmulatorStateChanged(s, persistFs),
(s) => this._onEmulatorStatusChanged(s),
);
//new_emulator.onTerminate().then(() => this.setState({ emulator: null }));
this.setState({ emulator: new_emulator, application: userFile });
new_emulator.start(this.state.settings, userFile);
}
render() {
return (
<>
<Header
title="Sogen - Playground"
description="Playground to test and run Sogen, a Windows user space emulator, right in your browser."
preload={
[
/*"./emulator-worker.js", "./analyzer.js", "./analyzer.wasm"*/
]
}
/>
<div className="h-[100dvh] flex flex-col">
<header className="flex shrink-0 items-center gap-2 border-b p-2 overflow-y-auto">
<a href="#/">
<Button size="sm" variant="secondary" className="fancy">
<HouseFill />
</Button>
</a>
<Button size="sm" className="fancy" onClick={this.start}>
<PlayFill /> <span>Start</span>
</Button>
<Button
disabled={
!this.state.emulator ||
isFinalState(this.state.emulator.getState())
}
size="sm"
variant="secondary"
className="fancy"
onClick={() => this.state.emulator?.stop()}
>
<StopFill /> <span className="hidden sm:inline">Stop</span>
</Button>
<Button
size="sm"
disabled={
!this.state.emulator ||
isFinalState(this.state.emulator.getState())
}
variant="secondary"
className="fancy"
onClick={this.toggleEmulatorState}
>
{this.isEmulatorPaused() ? (
<>
<PlayFill /> <span className="hidden sm:inline">Resume</span>
</>
) : (
<>
<PauseFill /> <span className="hidden sm:inline">Pause</span>
</>
)}
</Button>
<Popover>
<PopoverTrigger asChild>
<Button size="sm" variant="secondary" className="fancy">
<GearFill />{" "}
<span className="hidden sm:inline">Settings</span>
</Button>
</PopoverTrigger>
<PopoverContent>
<SettingsMenu
settings={this.state.settings}
allowWasm64={this.state.allowWasm64}
onChange={(s) => {
saveSettings(s);
this.setState({ settings: s });
}}
/>
</PopoverContent>
</Popover>
{!this.state.filesystem ? (
<></>
) : (
<Drawer
open={this.state.drawerOpen}
onOpenChange={(o) => this.setState({ drawerOpen: o })}
>
<DrawerContent className="!will-change-auto">
<DrawerHeader>
<DrawerTitle className="hidden">
Filesystem Explorer
</DrawerTitle>
<DrawerDescription className="hidden">
Filesystem Explorer
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<FilesystemExplorer
filesystem={this.state.filesystem}
iconCache={this.iconCache}
runFile={this.startEmulator}
resetFilesys={this.resetFilesys}
path={["c"]}
/>
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
{/* Separator */}
<div className="flex-1"></div>
<div className="text-right items-center">
<StatusIndicator
application={this.state.application}
state={
this.state.emulator
? this.state.emulator.getState()
: EmulationState.Stopped
}
/>
</div>
</header>
<div className="flex flex-1">
<EmulationSummary status={this.state.emulationStatus} />
<div className="flex flex-1 flex-col pl-1 overflow-auto">
<Output ref={this.output} />
</div>
</div>
</div>
</>
);
}
}