Emscripten mounting support

This commit is contained in:
Dustin Brett 2023-09-18 19:59:13 -07:00
parent 64f67ca2df
commit 676d2ece21
21 changed files with 220 additions and 38 deletions

View File

@ -7,6 +7,8 @@ import useTitle from "components/system/Window/useTitle";
import { useFileSystem } from "contexts/fileSystem";
import { useProcesses } from "contexts/process";
import { getExtension, isCanvasDrawn, loadFiles } from "utils/functions";
import type { EmscriptenFS } from "contexts/fileSystem/useAsyncFs";
import useEmscriptenMount from "components/system/Files/FileManager/useEmscriptenMount";
declare global {
interface Window {
@ -37,6 +39,7 @@ const useBoxedWine = ({
const { appendFileToTitle } = useTitle(id);
const { processes: { [id]: { libs = [] } = {} } = {} } = useProcesses();
const { readFile } = useFileSystem();
const mountEmFs = useEmscriptenMount();
const loadedUrl = useRef<string>();
const blankCanvasCheckerTimer = useRef<number | undefined>();
const loadEmulator = useCallback(async (): Promise<void> => {
@ -101,15 +104,26 @@ const useBoxedWine = ({
loadFiles(libs).then(() => {
if (url) appendFileToTitle(appName || basename(url));
try {
window.BoxedWineShell(() => setLoading(false));
window.BoxedWineShell(() => {
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "BoxedWine");
});
} catch {
// Ignore BoxedWine errors
}
});
}, [appendFileToTitle, containerRef, libs, readFile, setLoading, url]);
}, [
appendFileToTitle,
containerRef,
libs,
mountEmFs,
readFile,
setLoading,
url,
]);
useEffect(() => {
if (loadedUrl.current !== url) {
if (loadedUrl.current !== url && (url || !loadedUrl.current)) {
loadedUrl.current = url;
loadEmulator();
}

View File

@ -5,6 +5,8 @@ import { useProcesses } from "contexts/process";
import { useSession } from "contexts/session";
import { TRANSITIONS_IN_MILLISECONDS } from "utils/constants";
import { loadFiles, pxToNum } from "utils/functions";
import type { EmscriptenFS } from "contexts/fileSystem/useAsyncFs";
import useEmscriptenMount from "components/system/Files/FileManager/useEmscriptenMount";
declare global {
interface Window {
@ -26,6 +28,7 @@ const useClassiCube = ({
setLoading,
}: ContainerHookProps): void => {
const { processes: { [id]: process } = {} } = useProcesses();
const mountEmFs = useEmscriptenMount();
const {
windowStates: { [id]: windowState },
} = useSession();
@ -62,7 +65,10 @@ const useClassiCube = ({
arguments: ["Singleplayer"],
canvas,
postRun: [
() => setLoading(false),
() => {
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "ClassiCube");
},
() => {
const { width, height } = canvas.getBoundingClientRect() || {};
@ -76,7 +82,7 @@ const useClassiCube = ({
loadFiles(libs);
}, TRANSITIONS_IN_MILLISECONDS.WINDOW);
}, [getCanvas, libs, setLoading]);
}, [getCanvas, libs, mountEmFs, setLoading]);
};
export default useClassiCube;

View File

@ -10,6 +10,8 @@ import { useProcesses } from "contexts/process";
import { ICON_CACHE, ICON_CACHE_EXTENSION, SAVE_PATH } from "utils/constants";
import { bufferToUrl, getExtension, loadFiles } from "utils/functions";
import { zipAsync } from "utils/zipFunctions";
import type { EmscriptenFS } from "contexts/fileSystem/useAsyncFs";
import useEmscriptenMount from "components/system/Files/FileManager/useEmscriptenMount";
const getCore = (extension: string): [string, Core] => {
return (Object.entries(emulatorCores).find(([, { ext }]) =>
@ -26,6 +28,7 @@ const useEmulator = ({
}: ContainerHookProps): void => {
const { exists, mkdirRecursive, readFile, updateFolder, writeFile } =
useFileSystem();
const mountEmFs = useEmscriptenMount();
const { linkElement, processes: { [id]: { closing = false } = {} } = {} } =
useProcesses();
const { prependFileToTitle } = useTitle(id);
@ -39,7 +42,13 @@ const useEmulator = ({
if (loadedUrlRef.current) {
if (loadedUrlRef.current !== url) {
loadedUrlRef.current = "";
window.EJS_terminate?.();
try {
window.EJS_terminate?.();
} catch {
// Ignore errors during termination
}
if (containerRef.current) {
const div = document.createElement("div");
@ -76,6 +85,7 @@ const useEmulator = ({
}
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "EmulatorJs");
emulatorRef.current = currentEmulator;
};
@ -130,6 +140,7 @@ const useEmulator = ({
mkdirRecursive,
prependFileToTitle,
readFile,
mountEmFs,
setLoading,
updateFolder,
url,
@ -140,9 +151,10 @@ const useEmulator = ({
if (url) loadRom();
else {
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "EmulatorJs");
containerRef.current?.classList.add("drop");
}
}, [containerRef, loadRom, setLoading, url]);
}, [containerRef, loadRom, mountEmFs, setLoading, url]);
useEffect(() => {
if (!loading) {

View File

@ -14,6 +14,7 @@ import {
ROOT_NAME,
} from "utils/constants";
import { haltEvent } from "utils/functions";
import { isMountedFolder } from "contexts/fileSystem/functions";
const FileExplorer: FC<ComponentProcessProps> = ({ id }) => {
const {
@ -47,7 +48,7 @@ const FileExplorer: FC<ComponentProcessProps> = ({ id }) => {
if (isMounted) {
setProcessIcon(
id,
rootFs?.mntMap[url].getName() === "FileSystemAccess"
isMountedFolder(rootFs?.mntMap[url])
? MOUNTED_FOLDER_ICON
: COMPRESSED_FOLDER_ICON
);

View File

@ -1,8 +1,10 @@
import type { DosFactoryType } from "emulators-ui/dist/types/js-dos";
import type { EmscriptenFS } from "contexts/fileSystem/useAsyncFs";
declare global {
interface Window {
Dos: DosFactoryType;
JSDOS_FS: EmscriptenFS;
SimpleKeyboardInstances?: {
emulatorKeyboard?: {
destroy: () => void;

View File

@ -12,6 +12,7 @@ import { useProcesses } from "contexts/process";
import { useSession } from "contexts/session";
import { PREVENT_SCROLL } from "utils/constants";
import { loadFiles, pxToNum } from "utils/functions";
import useEmscriptenMount from "components/system/Files/FileManager/useEmscriptenMount";
const captureKeys = (event: KeyboardEvent): void => {
if (CAPTURED_KEYS.has(event.key)) event.preventDefault();
@ -26,6 +27,7 @@ const useJSDOS = ({
}: ContainerHookProps): void => {
const { updateWindowSize } = useWindowSize(id);
const [dosInstance, setDosInstance] = useState<DosInstance>();
const mountEmFs = useEmscriptenMount();
const loadingInstanceRef = useRef(false);
const { foregroundId } = useSession();
const dosCI = useDosCI(id, url, containerRef, dosInstance);
@ -102,6 +104,7 @@ const useJSDOS = ({
);
setLoading(false);
mountEmFs(window.JSDOS_FS, "JS-DOS");
}
}, [
closeWithTransition,
@ -110,6 +113,7 @@ const useJSDOS = ({
dosInstance?.layers,
id,
loading,
mountEmFs,
setLoading,
updateWindowSize,
]);

View File

@ -4,6 +4,8 @@ import type { ContainerHookProps } from "components/system/Apps/AppContainer";
import { useProcesses } from "contexts/process";
import { useSession } from "contexts/session";
import { loadFiles, pxToNum } from "utils/functions";
import type { EmscriptenFS } from "contexts/fileSystem/useAsyncFs";
import useEmscriptenMount from "components/system/Files/FileManager/useEmscriptenMount";
declare global {
interface Window {
@ -35,6 +37,7 @@ const useQuake3 = ({
const {
sizes: { titleBar },
} = useTheme();
const mountEmFs = useEmscriptenMount();
const { size } = windowState || {};
useEffect(() => {
@ -46,8 +49,9 @@ const useQuake3 = ({
window.ioq3.callMain([]);
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "Quake3");
});
}, [containerRef, libs, setLoading]);
}, [containerRef, libs, mountEmFs, setLoading]);
useEffect(() => {
if (!window.ioq3) return;

View File

@ -3,6 +3,8 @@ import type { ContainerHookProps } from "components/system/Apps/AppContainer";
import { useProcesses } from "contexts/process";
import { TRANSITIONS_IN_MILLISECONDS } from "utils/constants";
import { loadFiles } from "utils/functions";
import type { EmscriptenFS } from "contexts/fileSystem/useAsyncFs";
import useEmscriptenMount from "components/system/Files/FileManager/useEmscriptenMount";
declare global {
interface Window {
@ -23,6 +25,7 @@ const useSpaceCadet = ({
}: ContainerHookProps): void => {
const { processes: { [id]: { libs = [] } = {} } = {} } = useProcesses();
const [canvas, setCanvas] = useState<HTMLCanvasElement>();
const mountEmFs = useEmscriptenMount();
useEffect(() => {
const containerCanvas = containerRef.current?.querySelector("canvas");
@ -30,11 +33,14 @@ const useSpaceCadet = ({
if (containerCanvas instanceof HTMLCanvasElement) {
window.Module = {
canvas: containerCanvas,
postRun: () => setLoading(false),
postRun: () => {
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "SpaceCadet");
},
};
setCanvas(containerCanvas);
}
}, [containerRef, setLoading]);
}, [containerRef, mountEmFs, setLoading]);
useEffect(() => {
if (canvas) {
@ -53,7 +59,11 @@ const useSpaceCadet = ({
return () => {
if (canvas && window.Module) {
window.Module.SDL2?.audioContext.close();
try {
window.Module.SDL2?.audioContext.close();
} catch {
// Ignore errors during closing
}
}
};
}, [canvas, containerRef, libs]);

View File

@ -9,6 +9,7 @@ import { useFileSystem } from "contexts/fileSystem";
import { useProcesses } from "contexts/process";
import { DEFAULT_TEXT_FILE_SAVE_PATH } from "utils/constants";
import { haltEvent, loadFiles } from "utils/functions";
import useEmscriptenMount from "components/system/Files/FileManager/useEmscriptenMount";
const Vim: FC<ComponentProcessProps> = ({ id }) => {
const {
@ -16,6 +17,7 @@ const Vim: FC<ComponentProcessProps> = ({ id }) => {
processes: { [id]: process },
} = useProcesses();
const { readFile, updateFolder, writeFile } = useFileSystem();
const mountEmFs = useEmscriptenMount();
const { prependFileToTitle } = useTitle(id);
const { libs = [], url = "" } = process || {};
const [updateQueue, setUpdateQueue] = useState<QueueItem[]>([]);
@ -45,6 +47,7 @@ const Vim: FC<ComponentProcessProps> = ({ id }) => {
postRun: [
() => {
loading.current = false;
mountEmFs(window.VimWrapperModule?.VimModule?.FS, "Vim");
},
],
preRun: [
@ -90,7 +93,15 @@ const Vim: FC<ComponentProcessProps> = ({ id }) => {
});
prependFileToTitle(basename(saveUrl));
}, [closeWithTransition, id, libs, prependFileToTitle, readFile, url]);
}, [
closeWithTransition,
id,
libs,
mountEmFs,
prependFileToTitle,
readFile,
url,
]);
useEffect(() => {
if (updateQueue.length > 0) {

View File

@ -1,9 +1,12 @@
import type { EmscriptenFS } from "contexts/fileSystem/useAsyncFs";
export type QueueItem = {
buffer: Buffer;
url: string;
};
type VimModule = {
FS?: EmscriptenFS;
FS_createDataFile?: (
parentPath: string,
newPath: string,

View File

@ -49,6 +49,7 @@ import {
isSafari,
isYouTubeUrl,
} from "utils/functions";
import { isMountedFolder } from "contexts/fileSystem/functions";
type InternetShortcut = {
BaseURL: string;
@ -277,7 +278,7 @@ export const getInfoWithoutExtension = (
): void =>
callback({ getIcon, icon, pid: "FileExplorer", subIcons, url: path });
const getFolderIcon = (): string => {
if (rootFs?.mntMap[path]?.getName() === "FileSystemAccess") {
if (isMountedFolder(rootFs?.mntMap[path])) {
return MOUNTED_FOLDER_ICON;
}
if (hasNewFolderIcon) return NEW_FOLDER_ICON;

View File

@ -53,6 +53,7 @@ import {
IMAGE_ENCODE_FORMATS,
} from "utils/imagemagick/formats";
import type { ImageMagickConvertFile } from "utils/imagemagick/types";
import { isMountedFolder } from "contexts/fileSystem/functions";
const { alias } = PACKAGE_DATA;
@ -116,8 +117,7 @@ const useFileContextMenu = (
const isShortcut = pathExtension === SHORTCUT_EXTENSION;
const remoteMount = rootFs?.mountList.some(
(mountPath) =>
mountPath === path &&
rootFs?.mntMap[mountPath]?.getName() === "FileSystemAccess"
mountPath === path && isMountedFolder(rootFs?.mntMap[mountPath])
);
if (!readOnly && !remoteMount) {
@ -448,7 +448,11 @@ const useFileContextMenu = (
if (remoteMount) {
menuItems.push(MENU_SEPERATOR, {
action: () => unMapFs(path),
action: () =>
unMapFs(
path,
rootFs?.mntMap[path].getName() !== "FileSystemAccess"
),
label: "Disconnect",
});
}

View File

@ -6,6 +6,7 @@ import {
import { useFileSystem } from "contexts/fileSystem";
import { MOUNTABLE_EXTENSIONS } from "utils/constants";
import { getExtension } from "utils/functions";
import { isMountedFolder } from "contexts/fileSystem/functions";
export type FileInfo = {
comment?: string;
@ -44,7 +45,7 @@ const useFileInfo = (
!extension ||
(isDirectory &&
!MOUNTABLE_EXTENSIONS.has(extension) &&
rootFs.mntMap[path]?.getName() !== "FileSystemAccess")
!isMountedFolder(rootFs.mntMap[path]))
) {
getInfoWithoutExtension(
fs,

View File

@ -0,0 +1,51 @@
import { useFileSystem } from "contexts/fileSystem";
import type { EmscriptenFS } from "contexts/fileSystem/useAsyncFs";
import { useCallback, useEffect, useRef } from "react";
type EmscriptenMounter = (FS?: EmscriptenFS, fsName?: string) => Promise<void>;
const useEmscriptenMount = (): EmscriptenMounter => {
const { mountEmscriptenFs, unMapFs, updateFolder } = useFileSystem();
const mountName = useRef<string>();
useEffect(
() => () => {
if (mountName.current) {
const unMountPath = mountName.current;
mountName.current = "";
try {
unMapFs(unMountPath, true).then(() =>
updateFolder("/", undefined, unMountPath)
);
} catch {
// Ignore error during unmounting
}
}
},
[unMapFs, updateFolder]
);
return useCallback(
async (FS?: EmscriptenFS, fsName?: string): Promise<void> => {
if (!FS) return;
let name = "";
try {
name = await mountEmscriptenFs(FS, fsName);
} catch {
// Ignore error during mounting
}
if (name) {
updateFolder("/", name);
mountName.current = name;
}
},
[mountEmscriptenFs, updateFolder]
);
};
export default useEmscriptenMount;

View File

@ -1,7 +1,11 @@
import type HTTPRequest from "browserfs/dist/node/backend/HTTPRequest";
import type IndexedDBFileSystem from "browserfs/dist/node/backend/IndexedDB";
import type OverlayFS from "browserfs/dist/node/backend/OverlayFS";
import type { RootFileSystem } from "contexts/fileSystem/useAsyncFs";
import type {
ExtendedEmscriptenFileSystem,
Mount,
RootFileSystem,
} from "contexts/fileSystem/useAsyncFs";
import { join } from "path";
import { FS_HANDLES } from "utils/constants";
import {
@ -23,6 +27,11 @@ const KNOWN_IDB_DBS = [
"keyval-store",
];
export const isMountedFolder = (mount?: Mount): boolean =>
typeof mount === "object" &&
(mount.getName() === "FileSystemAccess" ||
(mount as ExtendedEmscriptenFileSystem)._FS?.DB_STORE_NAME === "FILE_DATA");
export const addFileSystemHandle = async (
directory: string,
handle: FileSystemDirectoryHandle,

View File

@ -15,6 +15,7 @@ import {
ICON_CACHE_EXTENSION,
SESSION_FILE,
} from "utils/constants";
import type EmscriptenFileSystem from "browserfs/dist/node/backend/Emscripten";
export type AsyncFS = {
exists: (path: string) => Promise<boolean>;
@ -35,18 +36,26 @@ export type AsyncFS = {
const { BFSRequire, configure } = BrowserFS as typeof IBrowserFS;
export type EmscriptenFS = {
DB_NAME: () => string;
DB_STORE_NAME: string;
};
export type ExtendedEmscriptenFileSystem = Omit<EmscriptenFileSystem, "_FS"> & {
_FS?: EmscriptenFS;
};
export type Mount = {
_data?: Buffer;
data?: Buffer;
getName: () => string;
};
export type RootFileSystem = Omit<
MountableFileSystem,
"mntMap" | "mountList"
> & {
mntMap: Record<
string,
{
_data?: Buffer;
data?: Buffer;
getName: () => string;
}
>;
mntMap: Record<string, Mount>;
mountList: string[];
};

View File

@ -20,7 +20,12 @@ import {
} from "components/system/Files/FileManager/functions";
import type { NewPath } from "components/system/Files/FileManager/useFolder";
import { getFileSystemHandles } from "contexts/fileSystem/core";
import type { AsyncFS, RootFileSystem } from "contexts/fileSystem/useAsyncFs";
import type {
AsyncFS,
EmscriptenFS,
ExtendedEmscriptenFileSystem,
RootFileSystem,
} from "contexts/fileSystem/useAsyncFs";
import useAsyncFs from "contexts/fileSystem/useAsyncFs";
import { useProcesses } from "contexts/process";
import type { UpdateFiles } from "contexts/session/types";
@ -32,6 +37,7 @@ import {
TRANSITIONS_IN_MILLISECONDS,
} from "utils/constants";
import { bufferToBlob, getExtension } from "utils/functions";
import { isMountedFolder } from "contexts/fileSystem/functions";
type FilePasteOperations = Record<string, "copy" | "move">;
@ -69,12 +75,13 @@ type FileSystemContextState = AsyncFS & {
existingHandle?: FileSystemDirectoryHandle
) => Promise<string>;
mkdirRecursive: (path: string) => Promise<void>;
mountEmscriptenFs: (FS: EmscriptenFS, fsName?: string) => Promise<string>;
mountFs: (url: string) => Promise<void>;
moveEntries: (entries: string[]) => void;
pasteList: FilePasteOperations;
removeFsWatcher: (folder: string, updateFiles: UpdateFiles) => void;
rootFs?: RootFileSystem;
unMapFs: (directory: string) => void;
unMapFs: (directory: string, hasNoHandle?: boolean) => Promise<void>;
unMountFs: (url: string) => void;
updateFolder: (folder: string, newFile?: string, oldFile?: string) => void;
};
@ -82,7 +89,7 @@ type FileSystemContextState = AsyncFS & {
const SYSTEM_DIRECTORIES = new Set(["/OPFS"]);
const {
FileSystem: { FileSystemAccess, IsoFS, ZipFS },
FileSystem: { Emscripten, FileSystemAccess, IsoFS, ZipFS },
} = BrowserFS as IFileSystemAccess & typeof IBrowserFS;
const useFileSystemContextState = (): FileSystemContextState => {
@ -176,7 +183,7 @@ const useFileSystemContextState = (): FileSystemContextState => {
!watchedPaths.some((watchedPath) =>
watchedPath.startsWith(mountedPath)
) &&
rootFs.mntMap[mountedPath]?.getName() !== "FileSystemAccess"
!isMountedFolder(rootFs.mntMap[mountedPath])
) {
if (secondCheck) {
rootFs.umount?.(mountedPath);
@ -215,6 +222,28 @@ const useFileSystemContextState = (): FileSystemContextState => {
),
[]
);
const mountEmscriptenFs = useCallback(
async (FS: EmscriptenFS, fsName?: string) =>
new Promise<string>((resolve, reject) => {
Emscripten?.Create({ FS }, (error, newFs) => {
const emscriptenFS = newFs as unknown as ExtendedEmscriptenFileSystem;
if (error || !newFs || !emscriptenFS._FS?.DB_NAME) {
reject();
return;
}
const dbName =
fsName ||
`${emscriptenFS._FS?.DB_NAME().replace(/\/+$/, "")}${emscriptenFS
._FS?.DB_STORE_NAME}`;
rootFs?.mount?.(join("/", dbName), newFs);
resolve(dbName);
});
}),
[rootFs]
);
const mapFs = useCallback(
async (
directory: string,
@ -289,13 +318,17 @@ const useFileSystemContextState = (): FileSystemContextState => {
[rootFs]
);
const unMapFs = useCallback(
(directory: string): void => {
async (directory: string, hasNoHandle?: boolean): Promise<void> => {
unMountFs(directory);
updateFolder(dirname(directory), undefined, directory);
import("contexts/fileSystem/functions").then(
({ removeFileSystemHandle }) => removeFileSystemHandle(directory)
if (hasNoHandle) return;
const { removeFileSystemHandle } = await import(
"contexts/fileSystem/functions"
);
removeFileSystemHandle(directory);
},
[unMountFs, updateFolder]
);
@ -498,6 +531,7 @@ const useFileSystemContextState = (): FileSystemContextState => {
deletePath,
mapFs,
mkdirRecursive,
mountEmscriptenFs,
mountFs,
moveEntries,
pasteList,

View File

@ -2023,8 +2023,12 @@ var FS = {
"r+": 2,
"w": 577,
"w+": 578,
"wx": 705,
"wx+": 706,
"a": 1089,
"a+": 1090
"a+": 1090,
"ax": 1217,
"ax+": 1218
},
modeStringToFlags: function(str) {
var flags = FS.flagModes[str];

View File

@ -7650,6 +7650,7 @@ __ATEXIT__.push({
FS.quit()
})
});
VimModule["FS"] = FS;
VimModule["FS_createFolder"] = FS.createFolder;
VimModule["FS_createPath"] = FS.createPath;
VimModule["FS_createDataFile"] = FS.createDataFile;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long