securityos/components/apps/V86/useV86.ts

260 lines
7.6 KiB
TypeScript

import {
BOOT_CD_FD_HD,
BOOT_FD_CD_HD,
config,
saveExtension,
} from "components/apps/V86/config";
import type { V86ImageConfig } from "components/apps/V86/image";
import { getImageType } from "components/apps/V86/image";
import type {
NavigatorWithMemory,
V86Config,
V86Starter,
} from "components/apps/V86/types";
import useV86ScreenSize from "components/apps/V86/useV86ScreenSize";
import useTitle from "components/system/Window/useTitle";
import { useFileSystem } from "contexts/fileSystem";
import { fs9pV4ToV3 } from "contexts/fileSystem/functions";
import { useProcesses } from "contexts/process";
import { useSession } from "contexts/session";
import { basename, dirname, extname, join } from "path";
import { useCallback, useEffect, useRef, useState } from "react";
import {
ICON_CACHE,
ICON_CACHE_EXTENSION,
SAVE_PATH,
TRANSITIONS_IN_MILLISECONDS,
} from "utils/constants";
import {
bufferToUrl,
cleanUpBufferUrl,
getHtmlToImage,
loadFiles,
} from "utils/functions";
const useV86 = (
id: string,
url: string,
containerRef: React.MutableRefObject<HTMLDivElement | null>,
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
loading: boolean
): void => {
const {
processes: { [id]: process },
} = useProcesses();
const { foregroundId } = useSession();
const { closing, libs = [] } = process || {};
const { appendFileToTitle } = useTitle(id);
const shutdown = useRef(false);
const [emulator, setEmulator] = useState<
Record<string, V86Starter | undefined>
>({});
const { exists, mkdirRecursive, readFile, updateFolder, writeFile } =
useFileSystem();
const saveStateAsync = useCallback(
(diskImageUrl: string): Promise<ArrayBuffer> =>
new Promise((resolve, reject) => {
emulator[diskImageUrl]?.save_state().then(resolve).catch(reject);
}),
[emulator]
);
const closeDiskImage = useCallback(
async (diskImageUrl: string, screenshot?: Buffer): Promise<void> => {
const saveName = `${basename(diskImageUrl)}${saveExtension}`;
if (!(await exists(SAVE_PATH))) {
await mkdirRecursive(SAVE_PATH);
updateFolder(dirname(SAVE_PATH));
}
const savePath = join(SAVE_PATH, saveName);
if (
await writeFile(
savePath,
Buffer.from(await saveStateAsync(diskImageUrl)),
true
)
) {
if (screenshot) {
const iconCacheRootPath = join(ICON_CACHE, SAVE_PATH);
const iconCachePath = join(
ICON_CACHE,
`${savePath}${ICON_CACHE_EXTENSION}`
);
if (!(await exists(iconCacheRootPath))) {
await mkdirRecursive(iconCacheRootPath);
updateFolder(dirname(SAVE_PATH));
}
await writeFile(iconCachePath, screenshot, true);
}
try {
emulator[diskImageUrl]?.destroy();
} catch {
// Ignore failures on destroy
} finally {
updateFolder(SAVE_PATH, saveName);
}
}
},
[emulator, exists, mkdirRecursive, saveStateAsync, updateFolder, writeFile]
);
const loadDiskImage = useCallback(async () => {
const [currentUrl] = Object.keys(emulator);
if (currentUrl) await closeDiskImage(currentUrl);
const imageContents = url ? await readFile(url) : Buffer.from("");
const ext = extname(url).toLowerCase();
const isISO = ext === ".iso";
const bufferUrl = bufferToUrl(imageContents);
const v86ImageConfig: V86ImageConfig = {
[isISO ? "cdrom" : getImageType(ext, imageContents.length)]: {
async: false,
size: imageContents.length,
url: bufferUrl,
use_parts: false,
},
};
const { deviceMemory = 0.25 } = navigator as NavigatorWithMemory;
const v86StarterConfig: V86Config = {
boot_order: isISO ? BOOT_CD_FD_HD : BOOT_FD_CD_HD,
memory_size: deviceMemory * 128 * 1024 * 1024,
screen_container: containerRef.current,
vga_memory_size: deviceMemory * 8 * 1024 * 1024,
...v86ImageConfig,
...config,
};
const savePath = join(SAVE_PATH, `${basename(url)}${saveExtension}`);
const saveContents = (await exists(savePath))
? bufferToUrl(await readFile(savePath))
: undefined;
if (saveContents) v86StarterConfig.initial_state = { url: saveContents };
v86StarterConfig.filesystem = {
basefs: URL.createObjectURL(
new Blob([JSON.stringify(fs9pV4ToV3())], { type: "application/json" })
),
baseurl: window.location?.origin ?? "/",
};
const v86 = new window.V86Starter(v86StarterConfig);
v86.add_listener("emulator-loaded", () => {
if (shutdown.current) {
v86.destroy();
return;
}
appendFileToTitle(basename(url));
cleanUpBufferUrl(bufferUrl);
if (v86StarterConfig.initial_state) {
cleanUpBufferUrl(v86StarterConfig.initial_state.url);
}
if (v86StarterConfig.filesystem) {
cleanUpBufferUrl(v86StarterConfig.filesystem.basefs);
}
containerRef.current?.addEventListener("click", v86.lock_mouse);
setEmulator({ [url]: v86 });
});
}, [
appendFileToTitle,
closeDiskImage,
containerRef,
emulator,
exists,
readFile,
url,
]);
useV86ScreenSize(id, containerRef, emulator[url]);
useEffect(() => {
if (loading) {
loadFiles(libs).then(() => {
if (window.V86Starter) setLoading(false);
});
}
}, [libs, loading, setLoading]);
useEffect(() => {
const isActiveInstance = foregroundId === id;
Object.values(emulator).forEach((emulatorInstance) =>
emulatorInstance?.keyboard_set_status(isActiveInstance)
);
}, [emulator, foregroundId, id]);
useEffect(() => {
if (process && !closing && !loading && !(url in emulator)) {
setEmulator({ [url]: undefined });
loadDiskImage();
}
const currentContainerRef = containerRef.current;
return () => {
if (url && closing && !shutdown.current) {
shutdown.current = true;
if (emulator[url]) {
const takeScreenshot = async (): Promise<Buffer | undefined> => {
let screenshot: string | undefined;
if (emulator[url]?.v86.cpu.devices.vga.graphical_mode) {
screenshot = (
currentContainerRef?.querySelector(
"canvas"
) as HTMLCanvasElement
)?.toDataURL("image/png");
} else if (currentContainerRef instanceof HTMLElement) {
const htmlToImage = await getHtmlToImage();
try {
screenshot = await htmlToImage?.toPng(currentContainerRef, {
skipAutoScale: true,
});
} catch {
// Ignore failure to capture
}
}
return screenshot
? Buffer.from(
screenshot.replace("data:image/png;base64,", ""),
"base64"
)
: undefined;
};
const scheduleSaveState = (screenshot?: Buffer): void => {
window.setTimeout(
() => closeDiskImage(url, screenshot),
TRANSITIONS_IN_MILLISECONDS.WINDOW
);
};
takeScreenshot().then(scheduleSaveState).catch(scheduleSaveState);
}
}
};
}, [
closeDiskImage,
closing,
containerRef,
emulator,
loadDiskImage,
loading,
process,
url,
]);
};
export default useV86;