feat: Somewhat reliable shutdown

This commit is contained in:
Felix Rieseberg 2020-07-26 19:01:57 -07:00
parent 7133d6f7d8
commit ccedc1920d
17 changed files with 357 additions and 106 deletions

View File

@ -1,16 +1,16 @@
{
"name": "macintosh",
"productName": "macintosh",
"name": "macintosh.js",
"productName": "macintosh.js",
"version": "1.0.0",
"description": "Macintosh's System 7 in an Electron app. I'm sorry.",
"description": "Macintosh's System 8 in an Electron app. I'm sorry.",
"main": "src/main/index.js",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "npx prettier --check src/{main,renderer}/*.js",
"fix": "npx prettier --write src/{main,renderer}/*.js"
"lint": "npx prettier --check src/{main,renderer}/*.{js,css}",
"fix": "npx prettier --write src/{main,renderer}/*.{js,css}"
},
"keywords": [],
"author": {

View File

@ -1,30 +1,9 @@
const fs = require('fs');
const path = require('path');
const fs = require("fs");
const path = require("path");
const memAllocSet = new Set();
const memAllocSetPersistent = new Set();
function memAllocAdd(addr) {
if (memAllocSet.has(addr)) {
console.error(`unfreed memory alloc'd at ${addr}`);
}
memAllocSet.add(addr);
console.warn('malloc', addr);
memAllocSetPersistent.add(addr);
}
function memAllocRemove(addr) {
if (!memAllocSet.has(addr)) {
console.error(
`unalloc'd memory free'd at ${addr} (everallocd=${memAllocSetPersistent.has(
addr
)})`
);
}
console.warn('free', addr);
memAllocSet.delete(addr);
}
var pathGetFilenameRegex = /\/([^\/]+)$/;
function pathGetFilename(path) {
@ -37,10 +16,10 @@ function pathGetFilename(path) {
}
function addAutoloader(module) {
var loadDatafiles = function() {
module.autoloadFiles.forEach(function(filepath) {
var loadDatafiles = function () {
module.autoloadFiles.forEach(function (filepath) {
module.FS_createPreloadedFile(
'/',
"/",
pathGetFilename(filepath),
filepath,
true,
@ -61,10 +40,10 @@ function addCustomAsyncInit(module) {
if (module.asyncInit) {
module.preRun = module.preRun || [];
module.preRun.push(function waitForCustomAsyncInit() {
module.addRunDependency('__moduleAsyncInit');
module.addRunDependency("__moduleAsyncInit");
module.asyncInit(module, function asyncInitCallback() {
module.removeRunDependency('__moduleAsyncInit');
module.removeRunDependency("__moduleAsyncInit");
});
});
}
@ -90,22 +69,26 @@ var LockStates = {
var Module = null;
self.onmessage = function(msg) {
console.log('Worker message received', msg.data);
self.onmessage = 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}));
console.log("Start emulator worker");
startEmulator(
Object.assign({}, msg.data, { singleThreadedEmscripten: true })
);
}
if (msg && msg.data === 'save') {
const diskData = Module.FS.readFile('/disk');
const diskPath = path.join(__dirname, 'disk');
if (msg && msg.data === "disk_save") {
const diskData = Module.FS.readFile("/disk");
const diskPath = path.join(__dirname, "disk");
fs.writeFile(diskPath, diskData, (error) => {
console.log(`Finished writing disk`);
postMessage({ type: "disk_saved" });
if (error) {
console.error(error);
}
@ -140,20 +123,6 @@ function startEmulator(parentConfig) {
);
function waitForTwoStateLock(bufferView, lockIndex) {
// Atomics.wait(
// bufferView,
// lockIndex,
// LockStates.UI_THREAD_LOCK
// );
// while (!tryToAcquireCyclicalLock(bufferView, lockIndex)) {
// // spin
// }
// if (!tryToAcquireCyclicalLock(bufferView, lockIndex)) {
// throw new Error('failed to acquire lock for index', lockIndex);
// }
//
//
if (Atomics.load(bufferView, lockIndex) === LockStates.UI_THREAD_LOCK) {
while (
Atomics.compareExchange(
@ -217,9 +186,9 @@ function startEmulator(parentConfig) {
var AudioBufferQueue = [];
Module = {
autoloadFiles: ['disk', 'rom', 'prefs'],
autoloadFiles: ["disk", "rom", "prefs"],
arguments: ['--config', 'prefs'],
arguments: ["--config", "prefs"],
canvas: null,
blit: function blit(bufPtr, width, height, depth, usingPalette) {
@ -266,7 +235,7 @@ function startEmulator(parentConfig) {
if (audioDataBufferView[writingChunkAddr] === LockStates.UI_THREAD_LOCK) {
console.warn(
'worker tried to write audio data to UI-thread-locked chunk',
"worker tried to write audio data to UI-thread-locked chunk",
writingChunkIndex
);
return 0;
@ -279,7 +248,6 @@ function startEmulator(parentConfig) {
) {
nextNextChunkIndex = 0;
}
// console.assert(nextNextChunkIndex != writingChunkIndex, `writingChunkIndex=${nextNextChunkIndex} == nextChunkIndex=${nextNextChunkIndex}`)
audioDataBufferView[writingChunkAddr + 1] = nextNextChunkIndex;
audioDataBufferView.set(newAudio, writingChunkAddr + 2);
@ -290,7 +258,7 @@ function startEmulator(parentConfig) {
},
debugPointer: function debugPointer(ptr) {
console.log('debugPointer', ptr);
console.log("debugPointer", ptr);
},
acquireInputLock: acquireInputLock,
@ -302,23 +270,25 @@ function startEmulator(parentConfig) {
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
monitorRunDependencies: function (left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
if (left == 0) {
postMessage({type: 'emulator_ready'});
postMessage({ type: "emulator_ready" });
} else {
postMessage({
type: 'emulator_loading',
type: "emulator_loading",
completion: (this.totalDependencies - left) / this.totalDependencies,
});
}
},
print: (message) => {
console.log(message);
postMessage({
type: 'TTY',
data: message
type: "TTY",
data: message,
});
},
@ -332,6 +302,6 @@ function startEmulator(parentConfig) {
addCustomAsyncInit(Module);
if (parentConfig.singleThreadedEmscripten) {
importScripts('BasiliskII.js');
importScripts("BasiliskII.js");
}
}

View File

@ -5017,9 +5017,13 @@ function copyTempDouble(ptr) {
return 0;
}
function _emscripten_get_now() { abort() }function _emscripten_set_main_loop(func, fps, simulateInfiniteLoop, arg, noSetTiming) {
function _emscripten_get_now() { abort() }
function _emscripten_set_main_loop(func, fps, simulateInfiniteLoop, arg, noSetTiming) {
Module['noExitRuntime'] = true;
Module.print('Hiii')
assert(!Browser.mainLoop.func, 'emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters.');
Browser.mainLoop.func = func;
@ -5039,6 +5043,8 @@ function copyTempDouble(ptr) {
var thisMainLoopId = Browser.mainLoop.currentlyRunningMainloop;
Browser.mainLoop.runner = function Browser_mainLoop_runner() {
Module.print("runner");
if (ABORT) return;
if (Browser.mainLoop.queue.length > 0) {
var start = Date.now();

View File

@ -1,5 +1,5 @@
const isDevMode = true;
module.exports = {
isDevMode
}
isDevMode,
};

View File

@ -4,7 +4,9 @@ function registerIpcHandlers() {
ipcMain.handle("quit", () => app.quit());
ipcMain.handle("devtools", () => {
BrowserWindow.getAllWindows().forEach((w) => w.webContents.toggleDevTools());
BrowserWindow.getAllWindows().forEach((w) =>
w.webContents.toggleDevTools()
);
});
}

View File

@ -1,7 +1,7 @@
const { app, BrowserWindow, shell } = require("electron");
const path = require("path");
const { isDevMode } = require('./devmode')
const { isDevMode } = require("./devmode");
const windowList = {};
let mainWindow;
@ -26,14 +26,14 @@ function handleNewWindow(event, url, frameName, disposition, options) {
resizable: true,
webPreferences: {
nodeIntegration: false,
navigateOnDragDrop: false
navigateOnDragDrop: false,
},
});
let newWindow = new BrowserWindow(options)
let newWindow = new BrowserWindow(options);
newWindow.webContents.on('will-navigate', (event, url) => {
if (url.startsWith('http')) {
newWindow.webContents.on("will-navigate", (event, url) => {
if (url.startsWith("http")) {
event.preventDefault();
shell.openExternal(url);
}
@ -47,7 +47,7 @@ function handleNewWindow(event, url, frameName, disposition, options) {
newWindow.webContents.toggleDevTools();
}
newWindow.on('closed', () => {
newWindow.on("closed", () => {
delete windowList[url];
});
}
@ -66,7 +66,7 @@ function createWindow() {
nativeWindowOpen: true,
navigateOnDragDrop: false,
nodeIntegrationInWorker: true,
sandbox: false
sandbox: false,
},
});
@ -82,8 +82,8 @@ function createWindow() {
if (isDevMode) {
mainWindow.webContents.toggleDevTools();
}
};
}
module.exports = {
createWindow
}
createWindow,
};

View File

@ -1,8 +1,16 @@
const { quit, devtools } = require("./ipc");
const { getIsWorkerRunning, getIsWorkerSaving } = require("./worker");
const { showCloseWarning } = require("./dialogs");
function registerControls() {
document.querySelector("#close").addEventListener("click", () => {
quit();
if (!getIsWorkerRunning()) {
quit();
} else if (!getIsWorkerSaving()) {
showCloseWarning();
} else {
// We're saving, and we're doing nothing. We're making the user wait.
}
});
document.querySelector("#devtools").addEventListener("click", () => {

24
src/renderer/dialogs.js Normal file
View File

@ -0,0 +1,24 @@
const { quit } = require("./ipc");
function setupDialogs() {
// Still empty
}
function showCloseWarning() {
const warningDialog = document.querySelector("#warning");
document.querySelector("#warning-quit").onclick = () => {
quit();
};
document.querySelector("#warning-cancel").onclick = () => {
warningDialog.classList.add("hidden");
};
warningDialog.classList.remove("hidden");
}
module.exports = {
setupDialogs,
showCloseWarning,
};

View File

@ -1,7 +1,8 @@
const { drawScreen } = require("./screen");
const { openAudio } = require("./audio");
const { tryToSendInput } = require("./input");
const { registerWorker } = require("./worker");
const { registerWorker, setCanvasBlank } = require("./worker");
const { setupDialogs } = require("./dialogs");
function asyncLoop() {
drawScreen();
@ -9,8 +10,9 @@ function asyncLoop() {
requestAnimationFrame(asyncLoop);
}
function start() {
async function start() {
registerWorker();
setupDialogs();
openAudio();
asyncLoop();
}

View File

@ -2,19 +2,44 @@
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<title>macintosh.js</title>
<link rel="stylesheet" href="style/index.css">
</head>
<body>
<body class="emulator_running">
<canvas id="canvas" width="800" height="600" oncontextmenu="event.preventDefault()"></canvas>
<div class="controls">
<div id="disk_saving" class="dialog absolute_center hidden">
<p>Saving virtual machine disk.</p>
<p>We'll quit once done.</p>
<div class="lds-spinner"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
</div>
<div class="controls garamond">
<div class="clear">
<a id="close" href="#">Quit</a>
<a id="credits" href="#" onclick="window.open('credits.html')">Credits</a>
<a id="devtools" href="#">Dev</a>
</div>
</div>
<div id="warning">
<div id="warning" class="dialog hidden absolute_center">
<div>
<h1><img src="images/important_note.png" /> Don't lose your data!</h1>
<p>
Warning: <strong>All changes to your virtual Macintosh will be lost</strong>
unless you shut down the virtual machine.
</p>
<p>
Click on the <img width="17" src="images/finder_icon.png" /> icon
in the upper right and select "Finder". Then, in the menu bar at the
top of the screen, click on "Special" and select "Shut Down".
</p>
<button id="warning-quit">
Quit anyway
</button>
<button id="warning-cancel">
Cancel
</button>
</div>
</div>
<script src="./controls.js"></script>
<script src="./emulator.js"></script>

View File

@ -7,5 +7,5 @@ module.exports = {
devtools() {
ipcRenderer.invoke("devtools");
}
},
};

View File

@ -14,7 +14,10 @@ canvas.height = SCREEN_HEIGHT;
const canvasCtx = canvas.getContext("2d");
const imageData = canvasCtx.createImageData(SCREEN_WIDTH, SCREEN_HEIGHT);
let stopDrawing = false;
function drawScreen() {
if (stopDrawing) return;
const pixelsRGBA = imageData.data;
const numPixels = SCREEN_WIDTH * SCREEN_HEIGHT;
const expandedFromPalettedMode = videoModeBufferView[3];
@ -41,6 +44,26 @@ function drawScreen() {
canvasCtx.putImageData(imageData, 0, 0);
}
function setCanvasBlank() {
return new Promise((resolve) => {
stopDrawing = true;
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const bg = new Image();
// Clear
ctx.canvas.width = ctx.canvas.width;
bg.onload = () => {
const pattern = ctx.createPattern(bg, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
resolve();
};
bg.src = "images/off_bg.png";
});
}
module.exports = {
screenBuffer,
screenBufferView,
@ -48,4 +71,5 @@ module.exports = {
drawScreen,
SCREEN_WIDTH,
SCREEN_HEIGHT,
setCanvasBlank,
};

View File

@ -3,13 +3,71 @@
src: url(garamond.ttf);
}
@font-face {
font-family: Oswald;
src: url(oswald.ttf);
}
body {
font-family: Garamond, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-family: Oswald, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0 auto;
padding: 0;
font-weight: 300;
text-align: center;
}
h1, h2, h3, h4, h5, h6,
.garamond {
font-family: Garamond, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;;
}
:root {
--canvas-width: 800px;
--canvas-height: 600px;
}
button {
background-image: url(../images/button_left.png), url(../images/button_right.png), url(../images/button_middle.png);
background-position: left, right, center;
background-repeat: no-repeat, no-repeat, repeat-x;
background-size: contain;
border: none;
outline: none;
padding: 5px 9px 6px 9px;
font-family: Oswald;
margin-left: 10px;
margin-bottom: 20px;
}
button:nth-of-type(1) {
margin-left: 0;
}
button:active {
color: #fff;
background-image: url(../images/button_left_pressed.png), url(../images/button_right_pressed.png), url(../images/button_middle_pressed.png);
}
.hidden {
display: none;
}
.absolute_center {
position: absolute;
left: 50%;
top: 40%;
transform: translate(-50%,-50%);
}
.dialog {
background: #ddd;
z-index: 100;
padding: 0 20px;
border-left: 1px solid #fff;
border-top: 1px solid #fff;
border-right: 1px solid #aaa;
border-bottom: 1px solid #aaa;
outline: 1px solid #000;
text-align: left;
box-sizing: border-box;
}

View File

@ -1,6 +1,12 @@
@import "base.css";
@import "spinner.css";
canvas {
:root {
--warning-height: 250px;
--warning-width: 600px;
}
.emulator_running canvas {
cursor: none;
}
@ -17,6 +23,8 @@ canvas {
background-repeat: no-repeat, no-repeat, repeat-x;
background-size: contain;
font-size: 12px;
margin-left: auto;
margin-right: auto;
}
.controls .clear {
@ -41,12 +49,10 @@ canvas {
text-decoration: underline;
}
.emulator_running #turned_off {
display: none;
}
#warning {
height: 150px;
width: 350px;
background: #ddd;
z-index: 100;
position: absolute;
top: 200px;
left: 225px
}
width: var(--warning-width);
}

Binary file not shown.

View File

@ -0,0 +1,80 @@
.lds-spinner {
color: official;
position: relative;
width: 80px;
height: 80px;
transform: scale(0.5);
margin: auto;
}
.lds-spinner div {
transform-origin: 40px 40px;
animation: lds-spinner 1.2s linear infinite;
}
.lds-spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 18px;
border-radius: 20%;
background: #000;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@ -1,5 +1,6 @@
const { inputBuffer, INPUT_BUFFER_SIZE } = require("./input");
const { videoModeBuffer, VIDEO_MODE_BUFFER_SIZE } = require("./video");
const { setCanvasBlank } = require("./screen");
const {
screenBuffer,
SCREEN_BUFFER_SIZE,
@ -12,6 +13,43 @@ const {
audioBlockChunkSize,
AUDIO_DATA_BUFFER_SIZE,
} = require("./audio");
const { quit } = require("./ipc");
let isWorkerRunning = false;
let isWorkerSaving = false;
let worker;
function getIsWorkerRunning() {
return isWorkerRunning;
}
function getIsWorkerSaving() {
return isWorkerSaving;
}
function saveDisk() {
isWorkerSaving = true;
document.querySelector("#disk_saving").classList.remove("hidden");
worker.postMessage("disk_save");
}
function handleDiskSaved() {
isWorkerSaving = false;
// We're just gonna quit
quit();
}
async function handleWorkerShutdown() {
console.log(`Handling worker shutdown`);
document.body.classList.remove("emulator_running");
// Then, update the canvas
await setCanvasBlank();
saveDisk();
}
function registerWorker() {
var workerConfig = {
@ -29,7 +67,12 @@ function registerWorker() {
SCREEN_HEIGHT: SCREEN_HEIGHT,
};
var worker = window.emulatorWorker = new Worker("../basilisk/BasiliskII-worker-boot.js");
worker = window.emulatorWorker = new Worker(
"../basilisk/BasiliskII-worker-boot.js"
);
// We'll need this info
isWorkerRunning = true;
worker.postMessage(workerConfig);
worker.onmessage = function (e) {
@ -51,23 +94,26 @@ function registerWorker() {
// }
}
if (e.data.type === 'TTY') {
if (e.data.type === "TTY") {
// If we're shutting down, Basilisk II will send
// close_audio to TTY - our signal that we can
// save the disk image
if (e.data.message === 'close_audio') {
worker.postMessage('save');
if (e.data.data === "close_audio") {
handleWorkerShutdown();
}
}
if (e.data.type === "disk_saved") {
handleDiskSaved();
}
};
}
function saveDisk() {
worker.postMessage('save');
}
window.setCanvasBlank = setCanvasBlank;
module.exports = {
registerWorker,
getIsWorkerRunning,
getIsWorkerSaving,
setCanvasBlank,
};