feat: Auto-mount disk images

This commit is contained in:
Felix Rieseberg 2020-07-27 15:07:47 -07:00
parent bf1f043bc4
commit 438dd38ab6
6 changed files with 145 additions and 39 deletions

3
.gitignore vendored
View File

@ -90,5 +90,6 @@ out/
# Custom stuff # Custom stuff
src/basilisk/user_files src/basilisk/user_files
src/basilisk/user_image_*
src/basilisk/disk src/basilisk/disk
src/basilisk/prefs

View File

@ -11,19 +11,19 @@ This is Mac OS 8, running in an [Electron](https://electronjs.org/) app. Yes, it
![Screenshot]() ![Screenshot]()
## Does it work? ## Does it work?
Yes! Quite well, actually - on macOS, Windows, and Linux. Bear in mind that this is written entirely in JavaScript, so please adjust your expectations. The virtual machine is emulating a Motorola CPU, which Apple used before switching to IBM's PowerPC architecture in the late 1990s. Yes! Quite well, actually - on macOS, Windows, and Linux. Bear in mind that this is written entirely in JavaScript, so please adjust your expectations. The virtual machine is emulating a 1991 Macintosh Quadra 900 with a Motorola CPU, which Apple used before switching to IBM's PowerPC architecture in the late 1990s.
## Should this have been a native app? ## Should this have been a native app?
Absolutely. Absolutely.
## Does it run my favorite game or app? ## Does it run my favorite game or app?
You'll likely be better off with an actual virtualization app, but the short answer is yes. In fact, you'll find various games and demos preinstalled, thanks to an old MacWorld Demo CD from 1997. Namely, Oregon Trail, Duke Nukem 3D, Civilization II, Alley 19 Bowling, Damage Incorporated, and Dungeons & Dragons. The short answer is "Yes". In fact, you'll find various games and demos preinstalled, thanks to an old MacWorld Demo CD from 1997. Namely, Oregon Trail, Duke Nukem 3D, Civilization II, Alley 19 Bowling, Damage Incorporated, and Dungeons & Dragons.
There are also various apps and trials preinstalled, including Photoshop 3, Premiere 4, Illustrator 5.5, StuffIt Expander, the Apple Web Page Construction Kit, and more. There are also various apps and trials preinstalled, including Photoshop 3, Premiere 4, Illustrator 5.5, StuffIt Expander, the Apple Web Page Construction Kit, and more.
## Can I transfer files from and to the machine? ## Can I transfer files from and to the machine?
Yes, you can. Click on the "Help" button at the bottom of the running app to see instructions. Yes, you can. Click on the "Help" button at the bottom of the running app to see instructions. You can transfer files directly - or mount disk images.
## Can I connect to the Internet? ## Can I connect to the Internet?

View File

@ -12,7 +12,7 @@ module.exports = {
appCategoryType: 'public.app-category.developer-tools', appCategoryType: 'public.app-category.developer-tools',
win32metadata: { win32metadata: {
CompanyName: 'Felix Rieseberg', CompanyName: 'Felix Rieseberg',
OriginalFilename: 'macintosh.js' OriginalFilename: 'macintoshjs'
}, },
osxSign: { osxSign: {
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)', identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
@ -41,7 +41,7 @@ module.exports = {
return { return {
name: 'macintosh.js', name: 'macintosh.js',
authors: 'Felix Rieseberg', authors: 'Felix Rieseberg',
exe: 'macintoshjs.exe', exe: 'macintosh.js.exe',
noMsi: true, noMsi: true,
remoteReleases: '', remoteReleases: '',
setupExe: `macintoshjs-${package.version}-setup-${arch}.exe`, setupExe: `macintoshjs-${package.version}-setup-${arch}.exe`,

View File

@ -22,16 +22,21 @@ function addAutoloader(module) {
try { try {
const absoluteSourcePath = path.join(macDir, sourcePath); const absoluteSourcePath = path.join(macDir, sourcePath);
const absoluteTargetPath = path.join(macintoshCopyPath, sourcePath); const absoluteTargetPath = path.join(macintoshCopyPath, sourcePath);
const targetPath = `/macintosh.js${sourcePath ? `/${sourcePath}` : '' }`; const targetPath = `/macintosh.js${sourcePath ? `/${sourcePath}` : ""}`;
const files = fs.readdirSync(absoluteSourcePath); const files = fs.readdirSync(absoluteSourcePath).filter((v) => {
// Remove iso and img files
return !v.endsWith(".iso") && !v.endsWith(".img");
});
(files || []).forEach((fileName) => { (files || []).forEach((fileName) => {
try { try {
// If not, let's move on // If not, let's move on
const fileSourcePath = path.join(absoluteSourcePath, fileName); const fileSourcePath = path.join(absoluteSourcePath, fileName);
const copyPath = path.join(absoluteTargetPath, fileName); const copyPath = path.join(absoluteTargetPath, fileName);
const relativeSourcePath = `${sourcePath ? `${sourcePath}/` : ''}${fileName}`; const relativeSourcePath = `${
const fileUrl = `user_files/${relativeSourcePath}` sourcePath ? `${sourcePath}/` : ""
}${fileName}`;
const fileUrl = `user_files/${relativeSourcePath}`;
// Check if directory // Check if directory
if (fs.statSync(fileSourcePath).isDirectory()) { if (fs.statSync(fileSourcePath).isDirectory()) {
@ -63,9 +68,9 @@ function addAutoloader(module) {
); );
} catch (error) { } catch (error) {
postMessage("showMessageBoxSync", { postMessage("showMessageBoxSync", {
type: 'error', type: "error",
title: 'Could not transfer file', title: "Could not transfer file",
message: `We tried to transfer ${fileName} to the virtual machine, but failed. The error was: ${error}` message: `We tried to transfer ${fileName} to the virtual machine, but failed. The error was: ${error}`,
}); });
console.error(`loadDatafiles: Failed to preload ${fileName}`, error); console.error(`loadDatafiles: Failed to preload ${fileName}`, error);
@ -73,20 +78,25 @@ function addAutoloader(module) {
}); });
} catch (error) { } catch (error) {
postMessage("showMessageBoxSync", { postMessage("showMessageBoxSync", {
type: 'error', type: "error",
title: 'Could not transfer files', title: "Could not transfer files",
message: `We tried to transfer files to the virtual machine, but failed. The error was: ${error}` message: `We tried to transfer files to the virtual machine, but failed. The error was: ${error}`,
}); });
console.error(`loadDatafiles: Failed to copyFilesAtPath`, error); console.error(`loadDatafiles: Failed to copyFilesAtPath`, error);
} }
} };
const loadDatafiles = function () { const loadDatafiles = function () {
module.autoloadFiles.forEach((filepath) => { module.autoloadFiles.forEach((filepath) => {
const parent = `/`;
const name = path.basename(filepath);
console.log(`Adding preload file`, { parent, name, url: filepath });
module.FS_createPreloadedFile( module.FS_createPreloadedFile(
"/", parent,
path.basename(filepath), name,
filepath, filepath,
true, true,
true true
@ -99,7 +109,7 @@ function addAutoloader(module) {
return; return;
} }
copyFilesAtPath(''); copyFilesAtPath("");
}; };
if (module.autoloadFiles) { if (module.autoloadFiles) {
@ -128,9 +138,9 @@ function writeSafely(filePath, fileData) {
fs.writeFile(filePath, fileData, (error) => { fs.writeFile(filePath, fileData, (error) => {
if (error) { if (error) {
postMessage("showMessageBoxSync", { postMessage("showMessageBoxSync", {
type: 'error', type: "error",
title: 'Could not save files', title: "Could not save files",
message: `We tried to save files from the virtual machine, but failed. The error was: ${error}` 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); console.error(`Disk save: Encountered error for ${filePath}`, error);
@ -143,6 +153,102 @@ function writeSafely(filePath, fileData) {
}); });
} }
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) { async function saveFilesInPath(folderPath) {
const entries = (Module.FS.readdir(folderPath) || []).filter( const entries = (Module.FS.readdir(folderPath) || []).filter(
(v) => !v.startsWith(".") (v) => !v.startsWith(".")
@ -177,9 +283,9 @@ async function saveFilesInPath(folderPath) {
} }
} catch (error) { } catch (error) {
postMessage("showMessageBoxSync", { postMessage("showMessageBoxSync", {
type: 'error', type: "error",
title: 'Could not safe file', title: "Could not safe file",
message: `We tried to save the file "${file}" from the virtual machine, but failed. The error was: ${error}` 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); console.error(`Disk save: Could not write ${file}`, error);
@ -328,20 +434,18 @@ function startEmulator(parentConfig) {
} }
let AudioConfig = null; let AudioConfig = null;
let AudioBufferQueue = []; let AudioBufferQueue = [];
const userImages = copyUserImages();
Module = { Module = {
autoloadFiles: ["disk", "rom", "prefs"], autoloadFiles: getAutoLoadFiles(userImages),
arguments: ["--config", "prefs"], userImages: userImages,
arguments: ["--config", getPrefs(userImages)],
canvas: null, canvas: null,
blit: function blit(bufPtr, width, height, depth, usingPalette) { blit: function blit(bufPtr, width, height, depth, usingPalette) {
// console.time('await worker video lock');
// waitForTwoStateLock(videoModeBufferView, 9);
// console.timeEnd('await worker video lock');
videoModeBufferView[0] = width; videoModeBufferView[0] = width;
videoModeBufferView[1] = height; videoModeBufferView[1] = height;
videoModeBufferView[2] = depth; videoModeBufferView[2] = depth;
@ -370,11 +474,6 @@ function startEmulator(parentConfig) {
enqueueAudio: function enqueueAudio(bufPtr, nbytes, type) { enqueueAudio: function enqueueAudio(bufPtr, nbytes, type) {
let newAudio = Module.HEAPU8.slice(bufPtr, bufPtr + nbytes); let newAudio = Module.HEAPU8.slice(bufPtr, bufPtr + nbytes);
// console.assert(
// nbytes == parentConfig.audioBlockBufferSize,
// `emulator wrote ${nbytes}, expected ${parentConfig.audioBlockBufferSize}`
// );
let writingChunkIndex = nextAudioChunkIndex; let writingChunkIndex = nextAudioChunkIndex;
let writingChunkAddr = let writingChunkAddr =
writingChunkIndex * parentConfig.audioBlockChunkSize; writingChunkIndex * parentConfig.audioBlockChunkSize;

View File

@ -30,4 +30,4 @@ mousewheellines 3
dsp /dev/dsp dsp /dev/dsp
mixer /dev/mixer mixer /dev/mixer
ignoresegv false ignoresegv false
idlewait false idlewait false

View File

@ -37,6 +37,12 @@
</ul> </ul>
</p> </p>
<h2>How can I mount disk images?</h2>
<p>
If you have files in the "iso" or "img" format, place them in the "macintosh.js" folder in your user directory. We will automatically mount them as virtual drives. There's a considerable performance hit for each mounted virtual drive and disk, so be careful.
</p>
<h2>Developer Tools</h2> <h2>Developer Tools</h2>
<p> <p>