/** * @fileoverview Implements the PDP11 Computer component. * @author Jeff Parsons * @copyright © Jeff Parsons 2012-2016 * * This file is part of PCjs, a computer emulation software project at . * * It has been adapted from the JavaScript PDP 11/70 Emulator v1.4 written by Paul Nankervis * (paulnank@hotmail.com) as of September 2016 at . This code * may be used freely provided the original authors are acknowledged in any modified source code. * * PCjs is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCjs. If not, * see . * * You are required to include the above copyright notice in every modified copy of this work * and to display that copyright notice when the software starts running; see COPYRIGHT in * . * * Some PCjs files also attempt to load external resource files, such as character-image files, * ROM files, and disk image files. Those external resource files are not considered part of PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ "use strict"; if (NODE) { var str = require("../../shared/lib/strlib"); var usr = require("../../shared/lib/usrlib"); var web = require("../../shared/lib/weblib"); var UserAPI = require("../../shared/lib/userapi"); var ReportAPI = require("../../shared/lib/reportapi"); var Component = require("../../shared/lib/component"); var State = require("../../shared/lib/state"); var PDP11 = require("./defines"); var BusPDP11 = require("./bus"); var MessagesPDP11 = require("./messages"); } /** * ComputerPDP11(parmsComputer, parmsMachine, fSuspended) * * @constructor * @extends Component * @param {Object} parmsComputer * @param {Object} [parmsMachine] * @param {boolean} [fSuspended] * * The ComputerPDP11 component has no required (parmsComputer) properties, but it does * support the following: * * autoPower: true to automatically power the computer (default), false to wait; * false is honored only if a "power" button binding exists. * * busWidth: number of memory address lines (address bits) on the computer's "bus"; * 20 is the minimum (and the default), which implies 8086/8088 real-mode addressing, * while 24 is required for 80286 protected-mode addressing. This value is passed * directly through to the Bus component; see that component for more details. * * resume: one of the ComputerPDP11.RESUME constants, which are as follows: * '0' if resume disabled (default) * '1' if enabled without prompting * '2' if enabled with prompting * '3' if enabled with prompting and auto-delete * or a string containing the path of a predefined JSON-encoded state * * state: the path to JSON-encoded state file (see details regarding 'state' below) * * The parmsMachine object, if provided, may contain any of: * * autoMount: if set, this should override any 'autoMount' property in the FDC's * parmsFDC object. * * autoPower: if set, this should override any 'autoPower' property in the ComputerPDP11's * parmsComputer object. * * messages: if set, this should override any 'messages' property in the Debugger's * parmsDbg object. * * state: if set, this should override any 'state' property in the ComputerPDP11's * parmsComputer object. * * url: the location of the machine XML file * * If a predefined state is supplied AND it's successfully loaded, then resume behavior * defaults to '1' (ie, resume enabled without prompting). * * This component insures that all components are ready before "powering" them. * * Different components become ready at different times, and initialization order (ie, * the order the scripts are combined on the page) only partially determines readiness. * This is because components like ROM and Video must finish loading their resource files * before they are ready. Other components become ready after we call their initBus() * function, because they have a Bus or CPU dependency, such as access to memory management * functions. And other components, like CPU and Panel, are ready as soon as their * constructor finishes. * * Once a component has indicated it's ready, we call its powerUp() notification * function (if it has one--it's optional). We call the CPU's powerUp() function last, * so that the CPU is assured that all other components are ready and "powered". */ function ComputerPDP11(parmsComputer, parmsMachine, fSuspended) { Component.call(this, "Computer", parmsComputer, ComputerPDP11, MessagesPDP11.COMPUTER); this.flags.powered = false; this.setMachineParms(parmsMachine); this.fAutoPower = this.getMachineParm('autoPower', parmsComputer); /* * nPowerChange is 0 while the power state is stable, 1 while power is transitioning * to "on", and -1 while power is transitioning to "off". */ this.nPowerChange = 0; /* * TODO: Deprecate 'buswidth' (it should have always used camelCase) */ this.nBusWidth = parmsComputer['busWidth'] || parmsComputer['buswidth']; this.resume = ComputerPDP11.RESUME_NONE; this.sStateData = null; this.fStateData = false; // remembers if sStateData was loaded this.fServerState = false; this.url = this.getMachineParm('url') || ""; /* * Generate a random number x (where 0 <= x < 1), add 0.1 so that it's guaranteed to be * non-zero, convert to base 36, and chop off the leading digit and "decimal" point. */ this.sMachineID = (Math.random() + 0.1).toString(36).substr(2,12); this.sUserID = this.queryUserID(); /* * Find the appropriate CPU (and Debugger and Control Panel, if any) * * CLOSURE COMPILER TIP: To override the type of a right-hand expression (as we need to do here, * where we know getComponentByType() will only return an CPUState object or null), wrap the expression * in parentheses. I never knew this until I stumbled across it in "Closure: The Definitive Guide". */ this.cpu = /** @type {CPUStatePDP11} */ (Component.getComponentByType("CPU", this.id)); if (!this.cpu) { Component.error("Unable to find CPU component"); return; } this.dbg = /** @type {DebuggerPDP11} */ (Component.getComponentByType("Debugger", this.id)); /* * Initialize the Bus component */ this.bus = new BusPDP11({'id': this.idMachine + '.bus', 'busWidth': this.nBusWidth}, this.cpu, this.dbg); /* * Iterate through all the components and connect them to the Control Panel, if any */ var iComponent, component; var aComponents = Component.getComponents(this.id); this.panel = /** @type {PanelPDP11} */ (Component.getComponentByType("Panel", this.id)); if (this.panel && this.panel.controlPrint) { for (iComponent = 0; iComponent < aComponents.length; iComponent++) { component = aComponents[iComponent]; /* * I can think of many "cleaner" ways for the Control Panel component to pass its * notice(), println(), etc, overrides on to all the other components, but it's just * too darn convenient to slam those overrides into the components directly. */ component.notice = this.panel.notice; component.println = this.panel.println; component.controlPrint = this.panel.controlPrint; } } this.println(PDP11.APPNAME + " v" + PDP11.APPVERSION + "\n" + COPYRIGHT + "\n" + LICENSE); this.println("Portions adapted from the PDP-11/70 Emulator v1.4 by Paul Nankervis "); if (DEBUG && this.messageEnabled()) this.printMessage("TYPEDARRAYS: " + TYPEDARRAYS); /* * Iterate through all the components again and call their initBus() handler, if any */ for (iComponent = 0; iComponent < aComponents.length; iComponent++) { component = aComponents[iComponent]; if (component.initBus) component.initBus(this, this.bus, this.cpu, this.dbg); } var sStatePath = null; var sResume = parmsComputer['resume']; if (sResume !== undefined) { /* * DEPRECATE: This goofiness is a holdover from when the 'resume' property was a string (either a * single-digit string or a path); now it's always a number, so it never has a 'length' property and * the call to parseInt() is unnecessary. */ if (sResume.length > 1) { sStatePath = this.sResumePath = sResume; } else { this.resume = parseInt(sResume, 10); } } /* * The Computer 'state' property allows a state file to be specified independent of the 'resume' feature; * previously, you could only use 'resume' to load a state file -- which we still support, but loading a state * file that way prevents the machine's state from being saved, since we always resume from the 'resume' file. * * The other wrinkle is on the restore side: we need to IGNORE the 'state' property if a saved state now exists. * So we have to peek at localStorage, and unfortunately, the only way to "peek" is to actually load the data, * but we're not ready to use it yet, so powerUp() has been changed to use any existing stateComputer that we've * already loaded. * * However, there's now a wrinkle to the wrinkle: if a 'state' parameter has been passed via the URL, then that * OVERRIDES everything; it overrides any 'state' Computer parameter AND it disables resume of any saved state in * localStorage (in other words, it prevents fAllowResume from being true, and forcing resume off). */ var fAllowResume; var sState = this.getMachineParm('state') || (fAllowResume = true) && parmsComputer['state']; if (sState) { sStatePath = this.sStatePath = sState; if (!fAllowResume) { this.fServerState = true; this.resume = ComputerPDP11.RESUME_NONE; } if (this.resume) { this.stateComputer = new State(this, PDP11.APPVERSION); if (this.stateComputer.load()) { sStatePath = null; } else { delete this.stateComputer; } } } /* * If sStatePath is set, we must use it. But if there's no sStatePath AND resume is set, * then we have the option of resuming from a server-side state, assuming a valid USERID. */ if (!sStatePath && this.resume) { sStatePath = this.getServerStatePath(); if (sStatePath) this.fServerState = true; } if (!sStatePath) { this.setReady(); } else { var cmp = this; web.getResource(sStatePath, null, true, function(sURL, sResource, nErrorCode) { cmp.doneLoad(sURL, sResource, nErrorCode); }); } if (!this.bindings["power"]) this.fAutoPower = true; /* * Power on the computer, giving every component the opportunity to reset or restore itself. */ if (!fSuspended && this.fAutoPower) this.wait(this.powerOn); } Component.subclass(ComputerPDP11); ComputerPDP11.STATE_FAILSAFE = "failsafe"; ComputerPDP11.STATE_VALIDATE = "validate"; ComputerPDP11.STATE_TIMESTAMP = "timestamp"; ComputerPDP11.STATE_VERSION = "version"; ComputerPDP11.STATE_HOSTURL = "url"; ComputerPDP11.STATE_BROWSER = "browser"; ComputerPDP11.STATE_USERID = "user"; /* * The following constants define all the resume options. Negative values (eg, RESUME_REPOWER) are for * internal use only, and RESUME_DELETE is not documented (it provides a way of deleting ALL saved states * whenever a resume is declined). As a result, the only "end-user" values are 0, 1 and 2. */ ComputerPDP11.RESUME_REPOWER = -1; // resume without changing any state (for internal use only) ComputerPDP11.RESUME_NONE = 0; // default (no resume) ComputerPDP11.RESUME_AUTO = 1; // automatically save/restore state ComputerPDP11.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded) ComputerPDP11.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented) /** * getMachineID() * * @return {string} */ ComputerPDP11.prototype.getMachineID = function() { return this.sMachineID; }; /** * setMachineParms(parmsMachine) * * If no explicit machine parms were provided, then we check for 'parms' in the bundled resources (if any). * * @param {Object} [parmsMachine] */ ComputerPDP11.prototype.setMachineParms = function(parmsMachine) { if (!parmsMachine) { var sParms; if (typeof resources == 'object' && (sParms = resources['parms'])) { try { parmsMachine = /** @type {Object} */ (eval("(" + sParms + ")")); } catch(e) { Component.error(e.message + " (" + sParms + ")"); } } } this.parmsMachine = parmsMachine; }; /** * getMachineParm(sParm, parmsComponent) * * If the machine parameter doesn't exist, we check for a matching component parameter (if parmsComponent is provided), * and failing that, we check the bundled resources (if any). * * At the moment, the only bundled resource request we expect to encounter is 'state'; if it exists, then we return * 'state' back to the caller (ie, the name of the resource), so that the caller will then attempt to load the 'state' * resource to obtain the actual state. * * @param {string} sParm * @param {Object} [parmsComponent] * @return {string|undefined} */ ComputerPDP11.prototype.getMachineParm = function(sParm, parmsComponent) { /* * When checking parmsURL, the check is allowed be a bit looser, because URL parameters are * user-supplied, whereas most other parameters are developer-supplied. Granted, a developer * may also be sloppy and neglect to use correct case (eg, 'automount' instead of 'autoMount'), * but there are limits to my paranoia. */ var sParmLC = sParm.toLowerCase(); var value = Component.parmsURL[sParm] || Component.parmsURL[sParmLC]; if (value === undefined && this.parmsMachine) { value = this.parmsMachine[sParm]; } if (value === undefined && parmsComponent) { value = parmsComponent[sParm]; } if (value === undefined && typeof resources == 'object' && resources[sParm]) { value = sParm; } return value; }; /** * saveMachineParms() * * @return {string|null} */ ComputerPDP11.prototype.saveMachineParms = function() { return this.parmsMachine? JSON.stringify(this.parmsMachine) : null; }; /** * getUserID() * * @return {string} */ ComputerPDP11.prototype.getUserID = function() { return this.sUserID || ""; }; /** * doneLoad(sURL, sStateData, nErrorCode) * * @this {ComputerPDP11} * @param {string} sURL * @param {string} sStateData * @param {number} nErrorCode */ ComputerPDP11.prototype.doneLoad = function(sURL, sStateData, nErrorCode) { if (!nErrorCode) { this.sStateData = sStateData; this.fStateData = true; if (DEBUG && this.messageEnabled()) { this.printMessage("loaded state file " + sURL.replace(this.sUserID || "xxx", "xxx")); } } else { this.sResumePath = null; this.fServerState = false; this.notice('Unable to load machine state from server (error ' + nErrorCode + (sStateData? ': ' + str.trim(sStateData) : '') + ')'); } this.setReady(); }; /** * wait(fn, parms) * * wait() waits until every component is ready (including ourselves, the last component we check), then calls the * specified Computer method. * * TODO: The Closure Compiler makes it difficult for us to define a function type for "fn" that works in all cases; * sometimes we want to pass a function that takes only a "number", and other times we want to pass a function that * takes only an "Array" (the type will mirror that of the "parms" parameter). However, the Closure Compiler insists * that both functions must be declared as accepting both types of parameters. So once again, we must use an untyped * function declaration, instead of something stricter like: * * param {function(this:Computer, (number|Array|undefined)): undefined} fn * * @this {ComputerPDP11} * @param {function(...)} fn * @param {number|Array} [parms] optional parameters */ ComputerPDP11.prototype.wait = function(fn, parms) { var computer = this; var aComponents = Component.getComponents(this.id); for (var iComponent = 0; iComponent <= aComponents.length; iComponent++) { var component = (iComponent < aComponents.length ? aComponents[iComponent] : this); if (!component.isReady()) { component.isReady(function onComponentReady() { computer.wait(fn, parms); }); return; } } if (DEBUG && this.messageEnabled()) this.printMessage("ComputerPDP11.wait(ready)"); fn.call(this, parms); }; /** * validateState(stateComputer) * * NOTE: We clear() stateValidate only when there's no stateComputer. * * @this {ComputerPDP11} * @param {State|null} [stateComputer] * @return {boolean} true if state passes validation, false if not */ ComputerPDP11.prototype.validateState = function(stateComputer) { var fValid = true; var stateValidate = new State(this, PDP11.APPVERSION, ComputerPDP11.STATE_VALIDATE); if (stateValidate.load() && stateValidate.parse()) { var sTimestampValidate = stateValidate.get(ComputerPDP11.STATE_TIMESTAMP); var sTimestampComputer = stateComputer? stateComputer.get(ComputerPDP11.STATE_TIMESTAMP) : "unknown"; if (sTimestampValidate != sTimestampComputer) { this.notice("Machine state may be out-of-date\n(" + sTimestampValidate + " vs. " + sTimestampComputer + ")\nCheck your browser's local storage limits"); fValid = false; if (!stateComputer) stateValidate.clear(); } else { if (DEBUG && this.messageEnabled()) { this.printMessage("Last state: " + sTimestampComputer + " (validate: " + sTimestampValidate + ")"); } } } return fValid; }; /** * powerOn(resume) * * Power every component "up", applying any previously available state information. * * @this {ComputerPDP11} * @param {number} [resume] is a valid RESUME value; default is this.resume */ ComputerPDP11.prototype.powerOn = function(resume) { if (resume === undefined) { resume = this.resume || (this.sStateData? ComputerPDP11.RESUME_AUTO : ComputerPDP11.RESUME_NONE); } if (DEBUG && this.messageEnabled()) { this.printMessage("ComputerPDP11.powerOn(" + (resume == ComputerPDP11.RESUME_REPOWER ? "repower" : (resume ? "resume" : "")) + ")"); } if (this.nPowerChange) { return; } this.nPowerChange++; var fRepower = false; var fRestore = false; this.fRestoreError = false; var stateComputer = this.stateComputer || new State(this, PDP11.APPVERSION); if (resume == ComputerPDP11.RESUME_REPOWER) { fRepower = true; } else if (resume > ComputerPDP11.RESUME_NONE) { if (stateComputer.load(this.sStateData)) { /* * Since we're resuming something (either a predefined state or a state from localStorage), let's * create a "failsafe" checkpoint in localStorage, and destroy it at the end of a successful powerOn(). * Which means, of course, that if a previous "failsafe" checkpoint already exists, something bad * may have happened the last time around. */ this.stateFailSafe = new State(this, PDP11.APPVERSION, ComputerPDP11.STATE_FAILSAFE); if (this.stateFailSafe.load()) { this.powerReport(stateComputer); /* * We already know resume is something other than RESUME_NONE, so we'll go ahead and bump it * all the way to RESUME_PROMPT, so that the user will be prompted, and if the user declines to * restore, the state will be removed. */ resume = ComputerPDP11.RESUME_PROMPT; /* * To ensure that the set() below succeeds, we need to call unload(), otherwise it may fail * with a "read only" error (eg, "TypeError: Cannot assign to read only property 'timestamp'"). */ this.stateFailSafe.unload(); } this.stateFailSafe.set(ComputerPDP11.STATE_TIMESTAMP, usr.getTimestamp()); this.stateFailSafe.store(); var fValidate = this.resume && !this.fServerState; if (resume == ComputerPDP11.RESUME_AUTO || web.confirmUser("Click OK to restore the previous " + PDP11.APPNAME + " machine state, or CANCEL to reset the machine.")) { fRestore = stateComputer.parse(); if (fRestore) { var sCode = stateComputer.get(UserAPI.RES.CODE); var sData = stateComputer.get(UserAPI.RES.DATA); if (sCode) { if (sCode == UserAPI.CODE.OK) { stateComputer.load(sData); } else { /* * A missing (or not yet created) state file is no cause for alarm, but other errors might be */ if (sCode == UserAPI.CODE.FAIL && sData != UserAPI.FAIL.NOSTATE) { this.notice("Error: " + sData); if (sData == UserAPI.FAIL.VERIFY) this.resetUserID(); } else { this.println(sCode + ": " + sData); } /* * Try falling back to the state that we should have saved in localStorage, as a backup to the * server-side state. */ stateComputer.unload(); // discard the invalid server-side state first if (stateComputer.load()) { fRestore = stateComputer.parse(); fValidate = true; } else { fRestore = false; // hmmm, there was nothing in localStorage either } } } } /* * If the load/parse was successful, and it was from localStorage (not sStateData), * then we should to try verify that localStorage snapshot is current. One reason it may * NOT be current is if localStorage was full and we got a quota error during the last * powerOff(). */ if (fValidate) this.validateState(fRestore? stateComputer : null); } else { /* * RESUME_PROMPT indicates we should delete the state if they clicked Cancel to confirm() above. */ if (resume == ComputerPDP11.RESUME_PROMPT) stateComputer.clear(); } } else { /* * If there's no state, then there should also be no validation timestamp; if there is, then once again, * we're probably dealing with a quota error. */ this.validateState(); } delete this.sStateData; delete this.stateComputer; } /* * Start powering all components, including any data they may need to restore their state; * we restore power to the CPU last. */ var aComponents = Component.getComponents(this.id); for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { var component = aComponents[iComponent]; if (component !== this && component != this.cpu) { fRestore = this.powerRestore(component, stateComputer, fRepower, fRestore); } } /* * Assuming this is not a repower, we must perform another wait, because some components may * have marked themselves as "not ready" again (eg, the FDC component, if the restore forced it * to mount one or more additional disk images). */ var aParms = [stateComputer, resume, fRestore]; if (resume != ComputerPDP11.RESUME_REPOWER) { this.wait(this.donePowerOn, aParms); return; } this.donePowerOn(aParms); }; /** * powerRestore(component, stateComputer, fRepower, fRestore) * * @this {ComputerPDP11} * @param {Component} component * @param {State} stateComputer * @param {boolean} fRepower * @param {boolean} fRestore * @return {boolean} true if restore should continue, false if not */ ComputerPDP11.prototype.powerRestore = function(component, stateComputer, fRepower, fRestore) { if (!component.flags.powered) { component.flags.powered = true; if (component.powerUp) { var data = null; if (fRestore) { data = stateComputer.get(component.id); if (!data) { /* * This is a hack that makes it possible for a machine whose ID has been * supplemented with a suffix (a single letter or digit) to find object IDs * in states created from a machine without the suffix. * * For example, if a state file was created from a machine with ID "ibm5160" * but the current machine is "ibm5160a", this attempts a second lookup with * "ibm5160", enabling us to find objects that match the original machine ID * (eg, "ibm5160.romEGA"). */ data = stateComputer.get(component.id.replace(/[a-z0-9]\./i, '.')); } } /* * State.get() will return whatever was originally passed to State.set() (eg, an * Object or a string), but components are supposed to store only Objects, so if a * string comes back, something went wrong. By explicitly eliminating "string" data, * the Closure Compiler stops complaining that we might be passing strings to our * powerUp() functions (even though we know we're not). * * TODO: Determine if there's some way to coerce the Closure Compiler into treating * data as Object or null, without having to include this runtime check. An assert * would be a good idea, but this is overkill. */ if (typeof data === "string") data = null; /* * If computer is null, this is simply a repower notification, which most components * don't do anything with. Exceptions include: CPU (since it may be halted) and Video * (since its screen may be "turned off"). */ if (!component.powerUp(data, fRepower) && data) { Component.error("Unable to restore state for " + component.type); /* * If this is a resume error for a machine that also has a predefined state * AND we're not restoring from that state, then throw away the current state, * prevent any new state from being created, and then force a reload, which will * hopefully restore us to the functioning predefined state. * * TODO: Considering doing this in ALL cases, not just in situations where a * 'state' exists but we're not actually resuming from it. */ if (this.sStatePath && !this.fStateData) { stateComputer.clear(); this.resume = ComputerPDP11.RESUME_NONE; web.reloadPage(); } else { /* * In all other cases, we set fRestoreError, which should trigger a call to * powerReport() and then delete the offending state. */ this.fRestoreError = true; } /* * Any failure triggers an automatic to call powerUp() again, without any state, * in the hopes that the component can recover by performing a reset. */ component.powerUp(null); /* * We also disable the rest of the restore operation, because it's not clear * the remaining state information can be trusted; the machine is already in an * inconsistent state, so we're not likely to make things worse, and the only * alternative (starting over and performing a state-less reset) isn't likely to make * the user any happier. But, we'll see... we need some experience with the code. */ fRestore = false; } } if (!fRepower && component.comment) { var asComments = component.comment.split("|"); for (var i = 0; i < asComments.length; i++) { component.status(asComments[i]); } } } return fRestore; }; /** * donePowerOn(aParms) * * This is nothing more than a continuation of powerOn(), giving us the option of calling wait() one more time. * * @this {ComputerPDP11} * @param {Array} aParms containing [stateComputer, resume, fRestore] */ ComputerPDP11.prototype.donePowerOn = function(aParms) { var stateComputer = aParms[0]; var fRepower = (aParms[1] < 0); var fRestore = aParms[2]; if (DEBUG && this.flags.powered && this.messageEnabled()) { this.printMessage("ComputerPDP11.donePowerOn(): redundant"); } this.fInitialized = true; this.flags.powered = true; var controlPower = this.bindings["power"]; if (controlPower) controlPower.textContent = "Shutdown"; /* * Once we get to this point, we're guaranteed that all components are ready, so it's safe to power the CPU; * the CPU should begin executing immediately, unless a debugger is attached. */ if (this.cpu) { /* * TODO: Do we not care about the return value here? (ie, is checking fRestoreError sufficient)? */ this.powerRestore(this.cpu, stateComputer, fRepower, fRestore); this.cpu.autoStart(); } /* * If the state was bad, offer to report it and then delete it. Deleting may be moot, since invariably a new * state will be created on powerOff() before the next powerOn(), but it seems like good paranoia all the same. */ if (this.fRestoreError) { this.powerReport(stateComputer); stateComputer.clear(); } if (!fRepower && this.stateFailSafe) { this.stateFailSafe.clear(); delete this.stateFailSafe; } this.nPowerChange = 0; }; /** * checkPower() * * @this {ComputerPDP11} * @return {boolean} true if the computer is fully powered, false otherwise */ ComputerPDP11.prototype.checkPower = function() { if (this.flags.powered) return true; var component = null, iComponent; var aComponents = Component.getComponents(this.id); for (iComponent = 0; iComponent < aComponents.length; iComponent++) { component = aComponents[iComponent]; if (component !== this && !component.flags.ready) break; } if (iComponent == aComponents.length) { for (iComponent = 0; iComponent < aComponents.length; iComponent++) { component = aComponents[iComponent]; if (component !== this && !component.flags.powered) break; } } if (iComponent == aComponents.length) component = this; var s = "The " + component.type + " component (" + component.id + ") is not " + (!component.flags.ready? "ready yet" + (component.fnReady? " (waiting for notification)" : "") : "powered yet") + "."; web.alertUser(s); return false; }; /** * powerReport(stateComputer) * * @this {ComputerPDP11} * @param {State} stateComputer */ ComputerPDP11.prototype.powerReport = function(stateComputer) { if (web.confirmUser("There may be a problem with your " + PDP11.APPNAME + " machine.\n\nTo help us diagnose it, click OK to send this " + PDP11.APPNAME + " machine state to http://" + SITEHOST + ".")) { web.sendReport(PDP11.APPNAME, PDP11.APPVERSION, this.url, this.getUserID(), ReportAPI.TYPE.BUG, stateComputer.toString()); } }; /** * powerOff(fSave, fShutdown) * * Power every component "down" and optionally save the machine state. * * There's one scenario that powerOff() isn't currently able to deal with very effectively: what to do when * the user switches away while it's still being restored, causing Disk getResource() calls to fail. The * Disk component calls notify() when that happens -- see Disk.mount() -- but the FDC and HDC controllers don't * notify *us* of those problems, so Computer assumes that the restore was completely successful, when in fact * it was only partially successful. * * Then we immediately arrive here to perform a save, following that incomplete restore. It would be wrong to * deal with that incomplete restore by setting fRestoreError, because we don't want to trigger a powerReport() * and the deletion of the previous state, because the state itself was presumably OK. Unfortunately, the new * state we now save will no longer include manually mounted disk images whose remounts were interrupted, so future * restores won't remount them either. * * We could perhaps solve this by having the Disk component notify us in those situations, set a new flag * (fRestoreIncomplete?), and set fSave to false if that's ever set. Be careful though: when fSave is false, * that means MORE than not saving; it also means deleting any previous state, which is NOT what you'd want to * do in a "fRestoreIncomplete" situation. Also, we have to worry about Disk operations that fail for other reasons, * making sure those failures don't interfere with the save process in the same way. * * As it stands, the worst that happens is any manually mounted disk images might have to be manually remounted, * which doesn't seem like a huge problem. * * @this {ComputerPDP11} * @param {boolean} [fSave] is true to request a saved state * @param {boolean} [fShutdown] is true if the machine is being shut down * @return {string|null} string representing the saved state (or null if error) */ ComputerPDP11.prototype.powerOff = function(fSave, fShutdown) { var data; var sState = "none"; if (DEBUG && this.messageEnabled()) { this.printMessage("ComputerPDP11.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")"); } if (this.nPowerChange) { return null; } this.nPowerChange--; var stateComputer = new State(this, PDP11.APPVERSION); var stateValidate = new State(this, PDP11.APPVERSION, ComputerPDP11.STATE_VALIDATE); var sTimestamp = usr.getTimestamp(); stateValidate.set(ComputerPDP11.STATE_TIMESTAMP, sTimestamp); stateComputer.set(ComputerPDP11.STATE_TIMESTAMP, sTimestamp); stateComputer.set(ComputerPDP11.STATE_VERSION, APPVERSION); stateComputer.set(ComputerPDP11.STATE_HOSTURL, web.getHostURL()); stateComputer.set(ComputerPDP11.STATE_BROWSER, web.getUserAgent()); /* * Always power the CPU "down" first, just to help insure it doesn't ask other components to do anything * after they're no longer ready. */ if (this.cpu && this.cpu.powerDown) { if (fShutdown) this.cpu.stopCPU(); data = this.cpu.powerDown(fSave, fShutdown); if (typeof data === "object") stateComputer.set(this.cpu.id, data); if (fShutdown) { this.cpu.flags.powered = false; if (data === false) sState = null; } } var aComponents = Component.getComponents(this.id); for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { var component = aComponents[iComponent]; if (component.flags.powered) { if (component.powerDown) { data = component.powerDown(fSave, fShutdown); if (typeof data === "object") stateComputer.set(component.id, data); } if (fShutdown) { component.flags.powered = false; if (data === false) sState = null; } } } if (sState) { if (fShutdown) { var fClear = false; var fClearAll = false; if (fSave) { if (this.sUserID) { this.saveServerState(this.sUserID, stateComputer.toString()); } if (!stateValidate.store() || !stateComputer.store()) { sState = null; /* * New behavior as of v1.13.2: if it appears that localStorage is full, we blow it ALL away. * Dedicated server-side storage is the only way we'll ever be able to reliably preserve a * particular machine's state. Historically, attempting to limp along with whatever localStorage * is left just generates the same useless and annoying warnings over and over. */ fClear = fClearAll = true; } } else { /* * I used to ALWAYS clear (ie, delete) any associated computer state, but now I do this only if the * current machine is "resumable", because there are situations where I have two configurations * for the same machine -- one resumable and one not -- and I don't want the latter throwing away the * state of the former. * * So this code is here now strictly for callers to delete the state of a "resumable" machine, not as * some paranoid clean-up operation. * * An undocumented feature of this operation is that if your configuration uses the special 'resume="3"' * value, and you click the "Reset" button, and then you click OK to reset the everything, this will * actually reset EVERYTHING (ie, all localStorage for ALL configs will be reclaimed). */ if (this.resume) { fClear = true; fClearAll = (this.resume == ComputerPDP11.RESUME_DELETE); } } if (fClear) { stateComputer.clear(fClearAll); } } else { sState = stateComputer.toString(); } } if (fShutdown) { this.flags.powered = false; var controlPower = this.bindings["power"]; if (controlPower) controlPower.textContent = "Power"; } this.nPowerChange = 0; return sState; }; /** * reset() * * Notify all (other) components with a reset() method that the Computer is being reset. * * NOTE: We'd like to reset the Bus first (due to the importance of the A20 line), but since we * allocated the Bus object ourselves, after all the other components were allocated, it ends * up near the end of Component's list of components. Hence the special case for this.bus below. * * @this {ComputerPDP11} */ ComputerPDP11.prototype.reset = function() { if (this.bus && this.bus.reset) { /* * TODO: Why does WebStorm think that this.bus.type is undefined? The base class (Component) * constructor defines it. */ this.printMessage("Resetting " + this.bus.type); this.bus.reset(); } var aComponents = Component.getComponents(this.id); for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { var component = aComponents[iComponent]; if (component !== this && component !== this.bus && component.reset) { this.printMessage("Resetting " + component.type); component.reset(); } } }; /** * start(ms, nCycles) * * Notify all (other) components with a start() method that the CPU has started. * * Note that we're called by startCPU(), which is why we exclude the CPU component, * as well as ourselves. * * @this {ComputerPDP11} * @param {number} ms * @param {number} nCycles */ ComputerPDP11.prototype.start = function(ms, nCycles) { var aComponents = Component.getComponents(this.id); for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { var component = aComponents[iComponent]; if (component.type == "CPU" || component === this) continue; if (component.start) { component.start(ms, nCycles); } } this.updateStatus(true); }; /** * stop(ms, nCycles) * * Notify all (other) components with a stop() method that the CPU has stopped. * * Note that we're called by stopCPU(), which is why we exclude the CPU component, * as well as ourselves. * * @this {ComputerPDP11} * @param {number} ms * @param {number} nCycles */ ComputerPDP11.prototype.stop = function(ms, nCycles) { var aComponents = Component.getComponents(this.id); for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { var component = aComponents[iComponent]; if (component.type == "CPU" || component === this) continue; if (component.stop) { component.stop(ms, nCycles); } } this.updateStatus(true); }; /** * setBinding(sType, sBinding, control, sValue) * * @this {ComputerPDP11} * @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc) * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) * @param {string} [sValue] optional data value * @return {boolean} true if binding was successful, false if unrecognized binding request */ ComputerPDP11.prototype.setBinding = function(sType, sBinding, control, sValue) { var computer = this; switch (sBinding) { case "power": this.bindings[sBinding] = control; control.onclick = function onClickPower() { computer.onPower(); }; return true; case "reset": this.bindings[sBinding] = control; control.onclick = function onClickReset() { computer.onReset(); }; return true; /* * Technically, this binding should now be called "saveState", to clearly distinguish it from * the "Save Machine" control that's normally bound to the savePC() function in save.js. Saving * an entire machine includes everything needed to start/restore the machine; eg, the machine * XML configuration file(s) *and* the JSON-encoded machine state. */ case "save": /* * Since this feature depends on the server supporting the PCjs User API (see userapi.js), * and since pcjs.org is no longer running a Node web server, we disable the feature for that * particular host. */ if (str.endsWith(web.getHost(), "pcjs.org")) { if (DEBUG) this.log("Remote user API not available"); /* * We could also simply hide the control; eg: * * control.style.display = "none"; * * but removing the control altogether seems better. */ control.parentNode.removeChild(/** @type {Node} */ (control)); return false; } this.bindings[sBinding] = control; control.onclick = function onClickSave() { var sUserID = computer.queryUserID(true); if (sUserID) { /* * I modified the test to include a check for sStatePath so that I could save new states * for machines with existing states; otherwise, I'd have no (easy) way of capturing and * updating their state. Making the machine (even temporarily) resumable would have been * one work-around, but it's not appropriate for some machines, as their state is simply * too large (for localStorage anyway, which is the default storage solution). */ var fSave = !!(computer.resume && !computer.sResumePath || computer.sStatePath); var sState = computer.powerOff(fSave); if (fSave) { computer.saveServerState(sUserID, sState); } else { computer.notice("Resume disabled, machine state not saved"); } } /* * This seemed like a handy alternative, but it turned out to be a no-go, at least for large states: * * var sState = computer.powerOff(true); * if (sState) { * sState = "data:text/json;charset=utf-8," + encodeURIComponent(sState); * window.open(sState); * } * * Perhaps if I embedded the data in a link on the current page instead; eg: * * $('Download').appendTo('#container'); */ }; return true; default: break; } return false; }; /** * resetUserID() */ ComputerPDP11.prototype.resetUserID = function() { web.setLocalStorageItem(ComputerPDP11.STATE_USERID, ""); this.sUserID = null; }; /** * queryUserID(fPrompt) * * @param {boolean} [fPrompt] * @returns {string|null|undefined} */ ComputerPDP11.prototype.queryUserID = function(fPrompt) { var sUserID = this.sUserID; if (!sUserID) { sUserID = web.getLocalStorageItem(ComputerPDP11.STATE_USERID); if (sUserID !== undefined) { if (!sUserID && fPrompt) { /* * NOTE: Warning the user here that "Save" operations are not currently supported by pcjs.org is * merely a precaution, because ordinarily, setBinding() should have already determined if we are * running from pcjs.org and disabled any "Save" button. */ sUserID = web.promptUser("Saving machine states on the pcjs.org server is currently unsupported.\n\nIf you're running your own server, enter your user ID below."); if (sUserID) { sUserID = this.verifyUserID(sUserID); if (!sUserID) this.notice("The user ID is invalid."); } } } else if (fPrompt) { this.notice("Browser local storage is not available"); } } return sUserID; }; /** * verifyUserID(sUserID) * * @this {ComputerPDP11} * @param {string} sUserID * @return {string} validated user ID, or null if error */ ComputerPDP11.prototype.verifyUserID = function(sUserID) { this.sUserID = null; var fMessages = DEBUG && this.messageEnabled(); if (fMessages) this.printMessage("verifyUserID(" + sUserID + ")"); var sRequest = web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUserID; var response = web.getResource(sRequest); var nErrorCode = response[0]; var sResponse = response[1]; if (!nErrorCode && sResponse) { try { response = eval("(" + sResponse + ")"); if (response.code && response.code == UserAPI.CODE.OK) { web.setLocalStorageItem(ComputerPDP11.STATE_USERID, response.data); if (fMessages) this.printMessage(ComputerPDP11.STATE_USERID + " updated: " + response.data); this.sUserID = response.data; } else { if (fMessages) this.printMessage(response.code + ": " + response.data); } } catch (e) { Component.error(e.message + " (" + sResponse + ")"); } } else { if (fMessages) this.printMessage("invalid response (error " + nErrorCode + ")"); } return this.sUserID; }; /** * getServerStatePath() * * @this {ComputerPDP11} * @return {string|null} sStatePath (null if no localStorage or no USERID stored in localStorage) */ ComputerPDP11.prototype.getServerStatePath = function() { var sStatePath = null; if (this.sUserID) { if (DEBUG && this.messageEnabled()) { this.printMessage(ComputerPDP11.STATE_USERID + " for load: " + this.sUserID); } sStatePath = web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.LOAD + '&' + UserAPI.QUERY.USER + '=' + this.sUserID + '&' + UserAPI.QUERY.STATE + '=' + State.key(this, PDP11.APPVERSION); } else { if (DEBUG && this.messageEnabled()) { this.printMessage(ComputerPDP11.STATE_USERID + " unavailable"); } } return sStatePath; }; /** * saveServerState(sUserID, sState) * * @param {string} sUserID * @param {string|null} sState */ ComputerPDP11.prototype.saveServerState = function(sUserID, sState) { /* * We must pass fSync == true, because (as I understand it) browsers will blow off any async * requests when a page is being closed. Since our request is synchronous, storeServerState() * should also return a result, but there's not much we can do with it, since browsers ALSO * tend to blow off alerts() and the like when closing down. */ if (sState) { if (DEBUG && this.messageEnabled()) { this.printMessage("size of server state: " + sState.length + " bytes"); } var response = this.storeServerState(sUserID, sState, true); if (response && response[UserAPI.RES.CODE] == UserAPI.CODE.OK) { this.notice("Machine state saved to server"); } else if (sState) { var sError = (response && response[UserAPI.RES.DATA]) || UserAPI.FAIL.BADSTORE; if (response[UserAPI.RES.CODE] == UserAPI.CODE.FAIL) { sError = "Error: " + sError; } else { sError = "Error " + response[UserAPI.RES.CODE] + ": " + sError; } this.notice(sError); this.resetUserID(); } } else { if (DEBUG && this.messageEnabled()) { this.printMessage("no state to store"); } } }; /** * storeServerState(sUserID, sState, fSync) * * @this {ComputerPDP11} * @param {string} sUserID * @param {string} sState * @param {boolean} [fSync] is true if we're powering down and should perform a synchronous request (default is async) * @return {*} server response if fSync is true and a response was received; otherwise null */ ComputerPDP11.prototype.storeServerState = function(sUserID, sState, fSync) { if (DEBUG && this.messageEnabled()) { this.printMessage(ComputerPDP11.STATE_USERID + " for store: " + sUserID); } /* * TODO: Determine whether or not any browsers cancel our request if we're called during a browser "shutdown" event, * and whether or not it matters if we do an async request (currently, we're not, to try to ensure the request goes through). */ var dataPost = {}; dataPost[UserAPI.QUERY.REQ] = UserAPI.REQ.STORE; dataPost[UserAPI.QUERY.USER] = sUserID; dataPost[UserAPI.QUERY.STATE] = State.key(this, PDP11.APPVERSION); dataPost[UserAPI.QUERY.DATA] = sState; var sRequest = web.getHost() + UserAPI.ENDPOINT; if (!fSync) { web.getResource(sRequest, dataPost, true); } else { var response = web.getResource(sRequest, dataPost); var sResponse = response[0]; if (response[1]) { if (sResponse) { var i = sResponse.indexOf('\n'); if (i > 0) sResponse = sResponse.substr(0, i); if (!sResponse.indexOf("Error: ")) sResponse = sResponse.substr(7); } sResponse = '{"' + UserAPI.RES.CODE + '":' + response[1] + ',"' + UserAPI.RES.DATA + '":"' + sResponse + '"}'; } if (DEBUG && this.messageEnabled()) this.printMessage(sResponse); return JSON.parse(sResponse); } return null; }; /** * onPower() * * This handles UI requests to toggle the computer's power (eg, see the "power" button binding). * * @this {ComputerPDP11} */ ComputerPDP11.prototype.onPower = function() { if (!this.nPowerChange) { if (!this.flags.powered) { this.wait(this.powerOn); } else { this.powerOff(false, true); } } }; /** * onReset() * * This handles UI requests to reset the computer's state (eg, see the "reset" button binding). * * @this {ComputerPDP11} */ ComputerPDP11.prototype.onReset = function() { /* * I'm going to start with the presumption that it makes little sense for an "unpowered" computer to be "reset"; * ditto if the power state is currently being changed. */ if (!this.flags.powered || this.nPowerChange) return; /* * If this is a "resumable" machine (and it's not using a predefined state), then we overload the reset * operation to offer an explicit "save or discard" option first. This is currently the only UI we offer to * discard a machine's state, including any disk changes. The traditional "reset" operation is still available * for non-resumable machines. * * TODO: Break this behavior out into a separate "discard" operation, in case the designer of the machine really * wants to clutter the UI with confusing options. ;-) */ if (this.resume && !this.sResumePath) { /* * I used to bypass the prompt if this.resume == ComputerPDP11.RESUME_AUTO, setting fSave to true automatically, * but that gives the user no means of resetting a resumable machine that contains errors in its resume state. */ var fSave = (/* this.resume == ComputerPDP11.RESUME_AUTO || */ web.confirmUser("Click OK to save changes to this " + PDP11.APPNAME + " machine.\n\nWARNING: If you CANCEL, all disk changes will be discarded.")); this.powerOff(fSave, true); /* * Forcing the page to reload is an expedient option, but ugly. It's preferable to call powerOn() * and rely on all the components to reset themselves to their default state. The components with * the greatest burden here are FDC and HDC, which must rely on the fReload flag to determine whether * or not to unload/reload all their original auto-mounted disk images. * * However, if we started with a predefined state (ie, sStatePath is set), we take this shortcut, because * we don't (yet) have code in place to gracefully reload the initial state (requires calling getResource() * again); alternatively, we could avoid throwing that state away, but it seems better to save the memory. * * TODO: Make this more graceful, so that we can stop using the reloadPage() sledgehammer. */ if (!fSave && this.sStatePath) { web.reloadPage(); return; } if (!fSave) this.fReload = true; this.powerOn(ComputerPDP11.RESUME_NONE); this.fReload = false; } else { this.reset(); if (this.cpu) this.cpu.autoStart(); } }; /** * getMachineComponent(sType, componentPrev) * * @this {ComputerPDP11} * @param {string} sType * @param {Component|null} [componentPrev] of previously returned component, if any * @return {Component|null} */ ComputerPDP11.prototype.getMachineComponent = function(sType, componentPrev) { var componentLast = componentPrev; var aComponents = Component.getComponents(this.id); for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { var component = aComponents[iComponent]; if (componentPrev) { if (componentPrev == component) componentPrev = null; continue; } if (component.type == sType) return component; } if (!componentLast) Component.log("Machine component type '" + sType + "' not found", "warning"); return null; }; /** * setFocus(fScroll) * * NOTE: When soft keyboard buttons call us to return focus to the machine (and away from the button), * the browser's default behavior is to scroll the element into view, which can be annoying, especially on iOS, * where the display is more constrained, so we no longer do it by default (fScroll must be true). * * @this {ComputerPDP11} * @param {boolean} [fScroll] (true if you really want the control scrolled into view) */ ComputerPDP11.prototype.setFocus = function(fScroll) { if (this.controlPrint) { /* * This seems to be recommended work-around to prevent the browser from scrolling the focused element * into view. The CPU is not a visual component, so when the CPU wants to set focus, the primary intent * is to ensure that keyboard input is fielded properly. */ var x = 0, y = 0; if (!fScroll && window) { x = window.scrollX; y = window.scrollY; } this.controlPrint.focus(); if (!fScroll && window) { window.scrollTo(x, y); } } }; /** * updateStatus(fForce) * * If any DOM controls were bound to the CPU, then we need to call its updateStatus() handler; if there are no * such bindings, then cpu.updateStatus() does nothing. * * Similarly, if there's a Panel, then we need to call its updateStatus() handler, in case it created its own canvas * and implemented its own register display (eg, dumpRegisters()); if not, then panel.updateStatus() also does nothing. * * In practice, there will *either* be a Panel with a custom canvas *or* a set of DOM controls bound to the CPU *or* * neither. In theory, there could be BOTH, but that would be unusual. * * TODO: Consider alternate approaches to these largely register-oriented display updates. Ordinarily, we like to * separate logic from presentation, and currently the CPUState contains both, since it's the component that intimately * knows the names, number, sizes, etc, of all the active registers. The Panel component is the logical candidate, * but Panel is an optional component; generally, only machines that include Debugger also include Panel. * * @this {ComputerPDP11} * @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled) */ ComputerPDP11.prototype.updateStatus = function(fForce) { /* * fForce is generally set to true whenever the CPU is transitioning to/from a running state, in which case * cpu.updateStatus() will definitely want to hide/show register contents; however, at other times, when the * CPU is running, constantly updating the DOM controls too frequently can adversely impact overall performance. * * So fForce serves as a hint to help cpu.updateStatus() make a more informed decision. panel.updateStatus() * currently doesn't care, on the theory that canvas updates should be significantly faster than DOM updates, * but we still pass fForce on. */ if (this.cpu) this.cpu.updateStatus(fForce); if (this.panel) this.panel.updateStatus(fForce); }; /** * ComputerPDP11.init() * * For every machine represented by an HTML element of class "pdp11-machine", this function * locates the HTML element of class "computer", extracting the JSON-encoded parameters for the * Computer constructor from the element's "data-value" attribute, invoking the constructor to * create a Computer component, and then binding any associated HTML controls to the new component. */ ComputerPDP11.init = function() { /* * In non-COMPILED builds, embedMachine() may have set XMLVERSION. */ if (!COMPILED && XMLVERSION) PDP11.APPVERSION = XMLVERSION; var aeMachines = Component.getElementsByClass(document, PDP11.APPCLASS + "-machine"); for (var iMachine = 0; iMachine < aeMachines.length; iMachine++) { var eMachine = aeMachines[iMachine]; var parmsMachine = Component.getComponentParms(eMachine); var aeComputers = Component.getElementsByClass(eMachine, PDP11.APPCLASS, "computer"); for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { var eComputer = aeComputers[iComputer]; var parmsComputer = Component.getComponentParms(eComputer); /* * We set fSuspended in the Computer constructor because we want to "power up" the * computer ourselves, after any/all bindings are in place. */ var computer = new ComputerPDP11(parmsComputer, parmsMachine, true); if (DEBUG && computer.messageEnabled()) { computer.printMessage("onInit(" + computer.flags.powered + ")"); } /* * Bind any "power", "reset" and "save" buttons. An "erase" button was also considered, * but "reset" now provides a way to force the machine to start from scratch again, so "erase" * may be redundant now. */ Component.bindComponentControls(computer, eComputer, PDP11.APPCLASS); /* * Power on the computer, giving every component the opportunity to reset or restore itself. */ if (computer.fAutoPower) computer.wait(computer.powerOn); } } }; /** * ComputerPDP11.show() * * When exit() is using an "onbeforeunload" handler, this "onpageshow" handler allows us to repower everything, * without either resetting or restoring. We call powerOn() with a special resume value (RESUME_REPOWER) if the * computer is already marked as "ready", meaning the browser didn't change anything. This "repower" process * should be very quick, essentially just marking all components as powered again (so that, for example, the Video * component will start drawing again) and firing the CPU up again. */ ComputerPDP11.show = function() { var aeComputers = Component.getElementsByClass(document, PDP11.APPCLASS, "computer"); for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { var eComputer = aeComputers[iComputer]; var parmsComputer = Component.getComponentParms(eComputer); var computer = /** @type {ComputerPDP11} */ (Component.getComponentByType("Computer", parmsComputer['id'])); if (computer) { if (DEBUG && computer.messageEnabled()) { computer.printMessage("onShow(" + computer.fInitialized + "," + computer.flags.powered + ")"); } /* * Note that the FIRST 'onpageshow' event, and therefore the first show() callback, occurs * AFTER the the initial 'onload' event, and at that point in time, fInitialized will not be set yet. * So, practically speaking, the first show() callback isn't all that useful. */ if (computer.fInitialized && !computer.flags.powered) { /** * Repower the computer, notifying every component to continue running as-is. */ computer.powerOn(ComputerPDP11.RESUME_REPOWER); } } } }; /** * ComputerPDP11.exit() * * The Computer is currently the only component that uses an "exit" handler, which web.onExit() defines as * either an "unload" or "onbeforeunload" handler. This gives us the opportunity to save the machine state, * using our powerOff() function, before the page goes away. * * It's worth noting that "onbeforeunload" offers one nice feature when used instead of "onload": the entire * page (and therefore this entire application) is retained in its current state by the browser (well, some * browsers), so that if you go to a new URL, either by entering a new URL in the same window/tab, or by pressing * the FORWARD button, and then you press the BACK button, the page is immediately restored to its previous state. * * In fact, that's how some browsers operate whether you have an "onbeforeunload" handler or not; in other words, * an "onbeforeunload" handler doesn't change the page retention behavior of the browser. By contrast, the mere * presence of an "onunload" handler generally causes a browser to throw the page away once the handler returns. * * However, in order to safely use "onbeforeunload", we must add yet another handler ("onpageshow") to repower * everything, without either resetting or restoring. Hence, the ComputerPDP11.show() function, which calls powerOn() * with a special resume value (RESUME_REPOWER) if the computer is already marked as "ready", meaning the browser * didn't change anything. This "repower" process should be very quick, essentially just marking all components as * powered again (so that, for example, the Video component will start drawing again) and firing the CPU up again. * * Reportedly, some browsers (eg, Opera) don't support "onbeforeunload", in which case Component will have to use * "unload" instead. But even when the page must be rebuilt from scratch, the combination of browser cache and * localStorage means the simulation should be restored and become operational almost immediately. */ ComputerPDP11.exit = function() { var aeComputers = Component.getElementsByClass(document, PDP11.APPCLASS, "computer"); for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { var eComputer = aeComputers[iComputer]; var parmsComputer = Component.getComponentParms(eComputer); var computer = /** @type {ComputerPDP11} */ (Component.getComponentByType("Computer", parmsComputer['id'])); if (computer) { if (DEBUG && computer.messageEnabled()) { computer.printMessage("onExit(" + computer.flags.powered + ")"); } if (computer.flags.powered) { /** * Power off the computer, giving every component an opportunity to save its state, * but only if 'resume' has been set AND there is no valid resume path (because if a valid resume * path exists, we'll always load our state from there, and not from whatever we save here). */ computer.powerOff(!!(computer.resume && !computer.sResumePath), true); } } } }; /* * Initialize every Computer on the page. */ web.onInit(ComputerPDP11.init); web.onShow(ComputerPDP11.show); web.onExit(ComputerPDP11.exit); if (NODE) module.exports = ComputerPDP11;