const fs = require("fs"); const path = require("path"); const homeDir = require("os").homedir(); const macDir = path.join(homeDir, "macintosh.js"); const macintoshCopyPath = path.join(__dirname, "user_files"); function cleanupCopyPath() { try { if (fs.existsSync(macintoshCopyPath)) { fs.rmdirSync(macintoshCopyPath, { recursive: true }); } fs.mkdirSync(macintoshCopyPath); } catch (error) { console.error(`cleanupCopyPath: Failed to remove`, error); } } function addAutoloader(module) { const copyFilesAtPath = function (sourcePath) { try { const absoluteSourcePath = path.join(macDir, sourcePath); const absoluteTargetPath = path.join(macintoshCopyPath, sourcePath); const targetPath = `/macintosh.js${sourcePath ? `/${sourcePath}` : ""}`; const files = fs.readdirSync(absoluteSourcePath).filter((v) => { // Remove iso and img files return !v.endsWith(".iso") && !v.endsWith(".img"); }); (files || []).forEach((fileName) => { try { // If not, let's move on const fileSourcePath = path.join(absoluteSourcePath, fileName); const copyPath = path.join(absoluteTargetPath, fileName); const relativeSourcePath = `${ sourcePath ? `${sourcePath}/` : "" }${fileName}`; const fileUrl = `user_files/${relativeSourcePath}`; // Check if directory if (fs.statSync(fileSourcePath).isDirectory()) { if (!fs.existsSync(copyPath)) { fs.mkdirSync(copyPath); } try { const virtualDirPath = `${targetPath}/${fileName}`; module.FS.mkdir(virtualDirPath); } catch (error) { console.log(error); } copyFilesAtPath(relativeSourcePath); return; } // We copy the files over and then add them as preload console.log(`loadDatafiles: Adding ${fileName}`); fs.copyFileSync(fileSourcePath, copyPath); module.FS_createPreloadedFile( targetPath, fileName, fileUrl, true, true ); } catch (error) { postMessage("showMessageBoxSync", { type: "error", title: "Could not transfer file", message: `We tried to transfer ${fileName} to the virtual machine, but failed. The error was: ${error}`, }); console.error(`loadDatafiles: Failed to preload ${fileName}`, error); } }); } catch (error) { postMessage("showMessageBoxSync", { type: "error", title: "Could not transfer files", message: `We tried to transfer files to the virtual machine, but failed. The error was: ${error}`, }); console.error(`loadDatafiles: Failed to copyFilesAtPath`, error); } }; const loadDatafiles = function () { module.autoloadFiles.forEach((filepath) => { const parent = `/`; const name = path.basename(filepath); console.log(`Adding preload file`, { parent, name, url: filepath }); module.FS_createPreloadedFile( parent, name, filepath, true, true ); }); // If the user has a macintosh.js dir, we'll copy over user // data if (!fs.existsSync(macDir)) { return; } copyFilesAtPath(""); }; if (module.autoloadFiles) { module.preRun = module.preRun || []; module.preRun.unshift(loadDatafiles); } return module; } function addCustomAsyncInit(module) { if (module.asyncInit) { module.preRun = module.preRun || []; module.preRun.push(function waitForCustomAsyncInit() { module.addRunDependency("__moduleAsyncInit"); module.asyncInit(module, function asyncInitCallback() { module.removeRunDependency("__moduleAsyncInit"); }); }); } } function writeSafely(filePath, fileData) { return new Promise((resolve) => { fs.writeFile(filePath, fileData, (error) => { if (error) { postMessage("showMessageBoxSync", { type: "error", title: "Could not save files", message: `We tried to save files from the virtual machine, but failed. The error was: ${error}`, }); console.error(`Disk save: Encountered error for ${filePath}`, error); } else { console.log(`Disk save: Finished writing ${filePath}`); } resolve(); }); }); } function getPrefs(userImages = []) { try { const prefsTemplatePath = path.join(__dirname, "prefs_template"); const prefsPath = path.join(__dirname, "prefs"); let prefs = fs.readFileSync(prefsTemplatePath, { encoding: "utf-8" }); if (userImages && userImages.length > 0) { console.log(`getPrefs: Found ${userImages.length} user images`); userImages.forEach((file) => { if (file.endsWith(".iso")) { prefs += `\ncdrom ${file}`; } else if (file.endsWith(".img")) { prefs += `\ndisk ${file}`; } }); } prefs += `\n`; fs.writeFileSync(prefsPath, prefs); } catch (error) { console.error(`getPrefs: Failed to set prefs`, error); } return "prefs"; } function isMacDirFileOfType(extension = '', v = '') { const isType = v.endsWith(`.${extension}`); const isMatch = isType && fs.statSync(path.join(macDir, v)).isFile(); console.log(`isMacDirFileOfType: ${v} is file and ${extension}: ${isMatch}`); return isMatch; } function copyUserImages() { const result = []; try { // No need if the macDir doesn't exist if (!fs.existsSync(macDir)) { console.log(`autoMountImageFiles: ${macDir} does not exist, exit`); return result; } const macDirFiles = fs.readdirSync(macDir); const imgFiles = macDirFiles.filter((v) => isMacDirFileOfType('img', v)); const isoFiles = macDirFiles.filter((v) => isMacDirFileOfType('iso', v)); const isoImgFiles = [...isoFiles, ...imgFiles]; console.log(`copyUserImages: iso and img files`, isoImgFiles); isoImgFiles.forEach((fileName, i) => { const sourcePath = path.join(macDir, fileName); const sanitizedFileName = `user_image_${i}_${fileName.replace(/[^\w\s\.]/gi, '')}`; const targetPath = path.join(__dirname, sanitizedFileName); if (fs.existsSync(targetPath)) { const sourceStat = fs.statSync(sourcePath); const targetStat = fs.statSync(targetPath); // Copy if the length is different if (sourceStat.size !== targetStat.size) { fs.copyFileSync(sourcePath, targetPath); } else { console.log( `autoMountImageFiles: ${sourcePath} already exists in ${targetPath}, not copying` ); } } else { fs.copyFileSync(sourcePath, targetPath); } console.log(`Copied over ${targetPath}`); result.push(sanitizedFileName); }); // Delete all old files const imagesCopyFiles = fs.readdirSync(__dirname); imagesCopyFiles.forEach((v) => { if (v.startsWith('user_image_') && !result.includes(v)) { fs.unlinkSync(path.join(__dirname, v)); } }); } catch (error) { console.error(`copyUserImages: Encountered error`, error); } return result; } function getAutoLoadFiles(userImages = []) { const autoLoadFiles = ["disk", "rom", "prefs", ...userImages]; return autoLoadFiles; } async function saveFilesInPath(folderPath) { const entries = (Module.FS.readdir(folderPath) || []).filter( (v) => !v.startsWith(".") ); if (!entries || entries.length === 0) return; // Ensure directory const targetDir = path.join(homeDir, folderPath); if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir); } for (const file of entries) { try { const fileSourcePath = `${folderPath}/${file}`; const stat = Module.FS.analyzePath(fileSourcePath); if (stat && stat.object && stat.object.isFolder) { // This is a folder, step into await saveFilesInPath(fileSourcePath); } else if (stat && stat.object && stat.object.contents) { const fileData = stat.object.contents; const filePath = path.join(targetDir, file); await writeSafely(filePath, fileData); } else { console.log( `Disk save: Object at ${fileSourcePath} is something, but we don't know what`, stat ); } } catch (error) { postMessage("showMessageBoxSync", { type: "error", title: "Could not safe file", message: `We tried to save the file "${file}" from the virtual machine, but failed. The error was: ${error}`, }); console.error(`Disk save: Could not write ${file}`, error); } } } let InputBufferAddresses = { globalLockAddr: 0, mouseMoveFlagAddr: 1, mouseMoveXDeltaAddr: 2, mouseMoveYDeltaAddr: 3, mouseButtonStateAddr: 4, keyEventFlagAddr: 5, keyCodeAddr: 6, keyStateAddr: 7, }; let LockStates = { READY_FOR_UI_THREAD: 0, UI_THREAD_LOCK: 1, READY_FOR_EMUL_THREAD: 2, EMUL_THREAD_LOCK: 3, }; var Module = null; self.onmessage = async function (msg) { console.log("Worker message received", msg.data); // If it's a config object, start the show if (msg && msg.data && msg.data.SCREEN_WIDTH) { console.log("Start emulator worker"); startEmulator( Object.assign({}, msg.data, { singleThreadedEmscripten: true }) ); } if (msg && msg.data === "disk_save") { const diskData = Module.FS.readFile("/disk"); const diskPath = path.join(__dirname, "disk"); // I wish we could do this with promises, but OOM crashes kill that idea try { console.log(`Trying to save disk`); fs.writeFileSync(diskPath, diskData); console.log(`Finished writing disk`); } catch (error) { console.error(`Failed to write disk`, error); } // Now, user files console.log(`Saving user files`); await saveFilesInPath("/macintosh.js"); // Clean up old copy dir cleanupCopyPath(); postMessage({ type: "disk_saved" }); } }; function startEmulator(parentConfig) { let screenBufferView = new Uint8Array( parentConfig.screenBuffer, 0, parentConfig.screenBufferSize ); let videoModeBufferView = new Int32Array( parentConfig.videoModeBuffer, 0, parentConfig.videoModeBufferSize ); let inputBufferView = new Int32Array( parentConfig.inputBuffer, 0, parentConfig.inputBufferSize ); let nextAudioChunkIndex = 0; let audioDataBufferView = new Uint8Array( parentConfig.audioDataBuffer, 0, parentConfig.audioDataBufferSize ); function waitForTwoStateLock(bufferView, lockIndex) { if (Atomics.load(bufferView, lockIndex) === LockStates.UI_THREAD_LOCK) { while ( Atomics.compareExchange( bufferView, lockIndex, LockStates.UI_THREAD_LOCK, LockStates.EMUL_THREAD_LOCK ) !== LockStates.UI_THREAD_LOCK ) { // spin // TODO use wait and wake } } else { // already unlocked } } function releaseTwoStateLock(bufferView, lockIndex) { Atomics.store(bufferView, lockIndex, LockStates.UI_THREAD_LOCK); // unlock } function tryToAcquireCyclicalLock(bufferView, lockIndex) { let res = Atomics.compareExchange( bufferView, lockIndex, LockStates.READY_FOR_EMUL_THREAD, LockStates.EMUL_THREAD_LOCK ); if (res === LockStates.READY_FOR_EMUL_THREAD) { return 1; } return 0; } function releaseCyclicalLock(bufferView, lockIndex) { Atomics.store(bufferView, lockIndex, LockStates.READY_FOR_UI_THREAD); // unlock } function acquireInputLock() { return tryToAcquireCyclicalLock( inputBufferView, InputBufferAddresses.globalLockAddr ); } function releaseInputLock() { // reset inputBufferView[InputBufferAddresses.mouseMoveFlagAddr] = 0; inputBufferView[InputBufferAddresses.mouseMoveXDeltaAddr] = 0; inputBufferView[InputBufferAddresses.mouseMoveYDeltaAddr] = 0; inputBufferView[InputBufferAddresses.mouseButtonStateAddr] = 0; inputBufferView[InputBufferAddresses.keyEventFlagAddr] = 0; inputBufferView[InputBufferAddresses.keyCodeAddr] = 0; inputBufferView[InputBufferAddresses.keyStateAddr] = 0; releaseCyclicalLock(inputBufferView, InputBufferAddresses.globalLockAddr); } let AudioConfig = null; let AudioBufferQueue = []; const userImages = copyUserImages(); Module = { autoloadFiles: getAutoLoadFiles(userImages), userImages: userImages, arguments: ["--config", getPrefs(userImages)], canvas: null, blit: function blit(bufPtr, width, height, depth, usingPalette) { videoModeBufferView[0] = width; videoModeBufferView[1] = height; videoModeBufferView[2] = depth; videoModeBufferView[3] = usingPalette; let length = width * height * (depth === 32 ? 4 : 1); // 32bpp or 8bpp for (let i = 0; i < length; i++) { screenBufferView[i] = Module.HEAPU8[bufPtr + i]; } // releaseTwoStateLock(videoModeBufferView, 9); }, openAudio: function openAudio( sampleRate, sampleSize, channels, framesPerBuffer ) { AudioConfig = { sampleRate: sampleRate, sampleSize: sampleSize, channels: channels, framesPerBuffer: framesPerBuffer, }; console.log(AudioConfig); }, enqueueAudio: function enqueueAudio(bufPtr, nbytes, type) { let newAudio = Module.HEAPU8.slice(bufPtr, bufPtr + nbytes); let writingChunkIndex = nextAudioChunkIndex; let writingChunkAddr = writingChunkIndex * parentConfig.audioBlockChunkSize; if (audioDataBufferView[writingChunkAddr] === LockStates.UI_THREAD_LOCK) { console.warn( "worker tried to write audio data to UI-thread-locked chunk", writingChunkIndex ); return 0; } let nextNextChunkIndex = writingChunkIndex + 1; if ( nextNextChunkIndex * parentConfig.audioBlockChunkSize > audioDataBufferView.length - 1 ) { nextNextChunkIndex = 0; } audioDataBufferView[writingChunkAddr + 1] = nextNextChunkIndex; audioDataBufferView.set(newAudio, writingChunkAddr + 2); audioDataBufferView[writingChunkAddr] = LockStates.UI_THREAD_LOCK; nextAudioChunkIndex = nextNextChunkIndex; return nbytes; }, debugPointer: function debugPointer(ptr) { console.log("debugPointer", ptr); }, acquireInputLock: acquireInputLock, InputBufferAddresses: InputBufferAddresses, getInputValue: function getInputValue(addr) { return inputBufferView[addr]; }, totalDependencies: 0, monitorRunDependencies: function (left) { this.totalDependencies = Math.max(this.totalDependencies, left); if (left == 0) { postMessage({ type: "emulator_ready" }); } else { postMessage({ type: "emulator_loading", completion: (this.totalDependencies - left) / this.totalDependencies, }); } }, print: (message) => { console.log(message); postMessage({ type: "TTY", data: message, }); }, printErr: console.warn.bind(console), releaseInputLock: releaseInputLock, }; addAutoloader(Module); addCustomAsyncInit(Module); if (parentConfig.singleThreadedEmscripten) { importScripts("BasiliskII.js"); } }