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"); // Set by config let userDataPath; function getUserDataDiskPath() { return path.join(userDataPath, "disk"); } // File type utilities function isFile(v = "") { return fs.statSync(path.join(macDir, v)).isFile(); } function isHiddenFile(filename = '') { return filename.startsWith('.'); } function isCDImage(filename = '') { return filename.endsWith('.iso') || filename.endsWith('.toast'); } function isDiskImage(filename = '') { return filename.endsWith('.img') || filename.endsWith('.dsk'); } 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 getUserDataDiskImage() { if (!userDataPath) { console.error(`getUserDataDiskImage: userDataPath not set`); return; } const diskImageUserPath = getUserDataDiskPath(); const diskImagePath = path.join(__dirname, "disk"); // If there's a disk image, move it over if (!fs.existsSync(diskImageUserPath)) { try { fs.renameSync(diskImagePath, diskImageUserPath); } catch (error) { // This is _probably_ a permissions thing, let's copy the file fs.copyFileSync(diskImagePath, diskImageUserPath); } } else { console.log( `getUserDataDiskImage: Image in user data dir, not doing anything` ); } } // Taken a given path, it'll look at all the files in there, // copy them over to the basilisk folder, and then add them // to MEMFS function preloadFilesAtPath(module, initalSourcePath) { try { const sourcePath = path.join(macDir, initalSourcePath); const targetPath = `/macintosh.js${ initalSourcePath ? `/${initalSourcePath}` : "" }`; const files = fs.readdirSync(sourcePath).filter((v) => { // Remove hidden, iso, and img files return !isHiddenFile(v) && !isDiskImage(v) && !isCDImage(v); }); (files || []).forEach((fileName) => { try { // If not, let's move on const fileSourcePath = path.join(sourcePath, fileName); const relativeSourcePath = `${ initalSourcePath ? `${initalSourcePath}/` : "" }${fileName}`; // Check if directory if (fs.statSync(fileSourcePath).isDirectory()) { try { const virtualDirPath = `${targetPath}/${fileName}`; module.FS.mkdir(virtualDirPath); } catch (error) { console.log(error); } preloadFilesAtPath(module, relativeSourcePath); return; } createPreloadedFile(module, { parent: targetPath, name: fileName, url: fileSourcePath, }); } 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( `preloadFilesAtPath: 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(`preloadFilesAtPath: Failed to preloadFilesAtPath`, error); } } function createPreloadedFile(module, options) { const parent = options.parent || `/`; const name = options.name || path.basename(options.url); const url = options.url; console.log(`Adding preload file`, { parent, name, url }); module.FS_createPreloadedFile(parent, name, url, true, true); } function addAutoloader(module) { const loadDatafiles = function () { module.autoloadFiles.forEach(({ url, name }) => createPreloadedFile(module, { url, name }) ); // If the user has a macintosh.js dir, we'll copy over user // data if (!fs.existsSync(macDir)) { return; } // Load user files preloadFilesAtPath(module, ""); }; 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 writePrefs(userImages = []) { try { const prefsTemplatePath = path.join(__dirname, "prefs_template"); const prefsPath = path.join(userDataPath, "prefs"); let prefs = fs.readFileSync(prefsTemplatePath, { encoding: "utf-8" }); // Replace line endings, just in case prefs = prefs.replaceAll("\r\n", "\n"); if (userImages && userImages.length > 0) { console.log(`writePrefs: Found ${userImages.length} user images`); userImages.forEach(({ name }) => { if (isCDImage(name)) { prefs += `\ncdrom ${name}`; } else if (isDiskImage(name)) { prefs += `\ndisk ${name}`; } }); } prefs += `\n`; fs.writeFileSync(prefsPath, prefs); } catch (error) { console.error(`writePrefs: Failed to set prefs`, error); } } function getUserImages() { const result = []; try { // No need if the macDir doesn't exist if (!fs.existsSync(macDir)) { console.log(`getUserImages: ${macDir} does not exist, exit`); return result; } const macDirFiles = fs.readdirSync(macDir); const imgFiles = macDirFiles.filter((v) => isFile(v) && isDiskImage(v)); const isoFiles = macDirFiles.filter((v) => isFile(v) && isCDImage(v)); const isoImgFiles = [...isoFiles, ...imgFiles]; console.log(`getUserImages: iso and img files`, isoImgFiles); isoImgFiles.forEach((fileName, i) => { const url = path.join(macDir, fileName); const sanitizedFileName = `user_image_${i}_${fileName.replace( /[^\w\s\.]/gi, "" )}`; result.push({ url, name: sanitizedFileName }); }); } catch (error) { console.error(`getUserImages: Encountered error`, error); } return result; } function getAutoLoadFiles(userImages = []) { const autoLoadFiles = [ { name: "disk", url: path.join(userDataPath, "disk"), }, { name: "rom", url: path.join(__dirname, "rom"), }, { name: "prefs", url: path.join(userDataPath, "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 = getUserDataDiskPath(); // 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) { userDataPath = parentConfig.userDataPath; getUserDataDiskImage(); 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 = []; // Check for user images const userImages = getUserImages(); // Write prefs to user data dir writePrefs(userImages); // Assemble preload files const autoloadFiles = getAutoLoadFiles(userImages); // Set arguments const arguments = ["--config", "prefs"]; Module = { autoloadFiles, userImages, arguments, 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"); } }