What this means is that XDF disk images formatted as 80-track 23-sector-per-track images will work with XDF code that honors that format; however, I've not yet been able to test with "real" XDF disk images (ie, disk images created with DiskDump using the experimental --xdf flag). "Real" XDF disk images almost certainly need more work (eg, setting all the sector IDs properly), but the groundwork has been laid.
1325 lines
55 KiB
JavaScript
1325 lines
55 KiB
JavaScript
/**
|
|
* @fileoverview Implements the PCjs Computer component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* @suppress {missingProperties}
|
|
* Created 2012-Jun-15
|
|
*
|
|
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines)
|
|
* at <http://jsmachines.net/> and <http://pcjs.org/>.
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see Computer.sCopyright).
|
|
*
|
|
* 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 the
|
|
* PCjs program for purposes of the GNU General Public License, and the author does not claim
|
|
* any copyright as to their contents.
|
|
*/
|
|
|
|
/*
|
|
* BUILD INSTRUCTIONS
|
|
*
|
|
* To build PCjs (pc.js), run Google's Closure Compiler, replacing "*.js" with
|
|
* the input file sequence defined by the "pcJSFiles" property in package.json:
|
|
*
|
|
* java -jar compiler.jar
|
|
* --compilation_level ADVANCED_OPTIMIZATIONS
|
|
* --define='DEBUG=false'
|
|
* --warning_level=VERBOSE
|
|
* --js *.js
|
|
* --js_output_file pc.js
|
|
*
|
|
* Google's Closure Compiler (compiler.jar) is documented at
|
|
* https://developers.google.com/closure/compiler/ and is available
|
|
* for download here:
|
|
*
|
|
* http://closure-compiler.googlecode.com/files/compiler-latest.zip
|
|
*
|
|
* The PCjs JavaScript files do have some initialization-order dependencies.
|
|
* If you load the files individually, it's recommended that you load them in
|
|
* the same order that they're compiled.
|
|
*
|
|
* Generally speaking, component.js should be first, computer.js should be
|
|
* last (of the files based on component.js), and panel.js should be listed
|
|
* early so that the Control Panel is ready as soon as possible.
|
|
*
|
|
* Another recent ordering requirement is that rom.js must be loaded before
|
|
* ram.js; this was true before, but now it's required, because I'm starting
|
|
* to add ROM BIOS Data Area definitions to rom.js, and since the data area
|
|
* is in RAM, ram.js may want access to some of those definitions.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
if (typeof module !== 'undefined') {
|
|
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 Bus = require("./bus");
|
|
var State = require("./state");
|
|
}
|
|
|
|
/**
|
|
* Computer(parmsComputer, parmsMachine, fSuspended)
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsComputer
|
|
* @param {Object} [parmsMachine]
|
|
* @param {boolean} [fSuspended]
|
|
*
|
|
* The Computer component has no required (parmsComputer) properties, but does
|
|
* support the following:
|
|
*
|
|
* 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 Computer.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)
|
|
*
|
|
* 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 Computer(parmsComputer, parmsMachine, fSuspended) {
|
|
|
|
Component.call(this, "Computer", parmsComputer, Computer);
|
|
|
|
this.fPowered = false;
|
|
this.nBusWidth = parmsComputer['buswidth'];
|
|
this.resume = Computer.RESUME_NONE;
|
|
this.sStateData = null;
|
|
this.fServerState = this.fSave = false;
|
|
this.url = parmsMachine? parmsMachine['url'] : null;
|
|
|
|
/*
|
|
* 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)
|
|
*/
|
|
this.cpu = Component.getComponentByType("CPU", this.id);
|
|
if (!this.cpu) {
|
|
Component.error("Unable to find CPU component");
|
|
return;
|
|
}
|
|
this.dbg = Component.getComponentByType("Debugger", this.id);
|
|
|
|
/*
|
|
* Initialize the Bus component
|
|
*/
|
|
this.bus = new Bus({'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 = 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;
|
|
}
|
|
}
|
|
|
|
if (DEBUG) this.messageDebugger("PREFETCH: " + PREFETCH + ", 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) {
|
|
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 = Component.parmsURL && Component.parmsURL['state'] || (fAllowResume = true) && parmsComputer['state'];
|
|
|
|
if (sState) {
|
|
sStatePath = this.sStatePath = sState;
|
|
if (!fAllowResume) {
|
|
this.fServerState = true;
|
|
this.resume = Computer.RESUME_NONE;
|
|
}
|
|
if (this.resume) {
|
|
this.stateComputer = new State(this, Computer.sAppVer);
|
|
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 {
|
|
web.loadResource(sStatePath, true, null, this, this.onLoadSetReady);
|
|
}
|
|
|
|
if (!fSuspended) {
|
|
/*
|
|
* Power "up" the computer, giving every component the opportunity to reset or restore itself.
|
|
*/
|
|
this.wait(this.powerOn);
|
|
}
|
|
}
|
|
|
|
Component.subclass(Component, Computer);
|
|
|
|
/*
|
|
* NOTE: 1.01 is the first version to provide limited save/restore support using localStorage.
|
|
* From this point on, care must be taken to insure that any new version that's incompatible with
|
|
* previous localStorage data be released with a version number that is at least 1 greater,
|
|
* since we're tagging the localStorage data with the integer portion of the version string.
|
|
*/
|
|
Computer.sAppName = APPNAME || "PCjs";
|
|
Computer.sAppVer = APPVERSION;
|
|
Computer.sCopyright = "Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>";
|
|
|
|
/*
|
|
* Version 1.02 marks the addition of "disk history" to the FDC, so that as diskettes are mounted,
|
|
* modified, unmounted, and then later remounted, all previous modifications can be restored. This
|
|
* same disk history has also been added to the save/restore data as of v1.02, so any save/restore
|
|
* code sensitive to that change must include a reference to VERSION_102.
|
|
*/
|
|
Computer.VERSION_102 = 102;
|
|
|
|
/*
|
|
* I think it's a good idea to also display a GPL notice, putting people on notice that even
|
|
* the "compiled" source code has all the same GPL requirements as the uncompiled source code.
|
|
*/
|
|
Computer.LICENSE = "License: GPL version 3 or later <http://gnu.org/licenses/gpl.html>";
|
|
|
|
Computer.STATE_FAILSAFE = "failsafe";
|
|
Computer.STATE_VALIDATE = "validate";
|
|
Computer.STATE_TIMESTAMP = "timestamp";
|
|
Computer.STATE_VERSION = "version";
|
|
Computer.STATE_HOSTURL = "url";
|
|
Computer.STATE_BROWSER = "browser";
|
|
Computer.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.
|
|
*/
|
|
Computer.RESUME_REPOWER = -1; // resume without changing any state (for internal use only)
|
|
Computer.RESUME_NONE = 0; // default (no resume)
|
|
Computer.RESUME_AUTO = 1; // automatically save/restore state
|
|
Computer.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded)
|
|
Computer.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented)
|
|
|
|
/**
|
|
* getMachineID()
|
|
*
|
|
* @return {string}
|
|
*/
|
|
Computer.prototype.getMachineID = function()
|
|
{
|
|
return this.sMachineID;
|
|
};
|
|
|
|
/**
|
|
* getUserID()
|
|
*
|
|
* @return {string}
|
|
*/
|
|
Computer.prototype.getUserID = function()
|
|
{
|
|
return this.sUserID? this.sUserID : "";
|
|
};
|
|
|
|
/**
|
|
* onLoadSetReady(sStateFile, sStateData, nErrorCode)
|
|
*
|
|
* @this {Computer}
|
|
* @param {string} sStateFile
|
|
* @param {string} sStateData
|
|
* @param {number} nErrorCode
|
|
*/
|
|
Computer.prototype.onLoadSetReady = function(sStateFile, sStateData, nErrorCode)
|
|
{
|
|
if (!nErrorCode) {
|
|
this.sStateData = sStateData;
|
|
if (DEBUG) this.messageDebugger("loaded state file " + sStateFile.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: As with web.loadResource(), 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 {Computer}
|
|
* @param {function(...)} fn
|
|
* @param {number|Array} [parms] optional parameters
|
|
*/
|
|
Computer.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.messageDebugger("Computer.wait(ready)");
|
|
fn.call(this, parms);
|
|
};
|
|
|
|
/**
|
|
* validateState(stateComputer)
|
|
*
|
|
* NOTE: We clear() stateValidate only when there's no stateComputer.
|
|
*
|
|
* @this {Computer}
|
|
* @param {State|null} [stateComputer]
|
|
* @return {boolean} true if state passes validation, false if not
|
|
*/
|
|
Computer.prototype.validateState = function(stateComputer)
|
|
{
|
|
var fValid = true;
|
|
var stateValidate = new State(this, Computer.sAppVer, Computer.STATE_VALIDATE);
|
|
if (stateValidate.load() && stateValidate.parse()) {
|
|
var sTimestampValidate = stateValidate.get(Computer.STATE_TIMESTAMP);
|
|
var sTimestampComputer = stateComputer ? stateComputer.get(Computer.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.messageDebugger("Last state: " + sTimestampComputer + " (validate: " + sTimestampValidate + ")");
|
|
}
|
|
}
|
|
return fValid;
|
|
};
|
|
|
|
/**
|
|
* powerOn(resume)
|
|
*
|
|
* Power every component "up", applying any previously available state information.
|
|
*
|
|
* @this {Computer}
|
|
* @param {number} [resume] is a valid RESUME value; default is this.resume
|
|
*/
|
|
Computer.prototype.powerOn = function(resume)
|
|
{
|
|
if (resume === undefined) {
|
|
resume = this.resume || (this.sStateData? Computer.RESUME_AUTO : Computer.RESUME_NONE);
|
|
}
|
|
|
|
if (DEBUG) this.messageDebugger("Computer.powerOn(" + (resume == Computer.RESUME_REPOWER ? "repower" : (resume ? "resume" : "")) + ")");
|
|
|
|
var fRepower = false;
|
|
var fRestore = false;
|
|
this.fRestoreError = false;
|
|
var stateComputer = this.stateComputer || new State(this, Computer.sAppVer);
|
|
|
|
if (resume == Computer.RESUME_REPOWER) {
|
|
fRepower = true;
|
|
}
|
|
else if (resume > Computer.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, Computer.sAppVer, Computer.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 = Computer.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(Computer.STATE_TIMESTAMP, usr.getTimestamp());
|
|
this.stateFailSafe.store();
|
|
|
|
var fValidate = this.resume && !this.fServerState;
|
|
if (resume == Computer.RESUME_AUTO || web.confirmUser("Click OK to restore the previous " + Computer.sAppName + " 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 == Computer.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 != Computer.RESUME_REPOWER) {
|
|
this.wait(this.donePowerOn, aParms);
|
|
return;
|
|
}
|
|
this.donePowerOn(aParms);
|
|
};
|
|
|
|
/**
|
|
* powerRestore(component, stateComputer, fRepower, fRestore)
|
|
*
|
|
* @this {Computer}
|
|
* @param {Component} component
|
|
* @param {State} stateComputer
|
|
* @param {boolean} fRepower
|
|
* @param {boolean} fRestore
|
|
* @return {boolean} true if restore should continue, false if not
|
|
*/
|
|
Computer.prototype.powerRestore = function(component, stateComputer, fRepower, fRestore)
|
|
{
|
|
if (!component.fPowered) {
|
|
|
|
component.fPowered = 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.sStateData) {
|
|
stateComputer.clear();
|
|
this.resume = Computer.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 {Computer}
|
|
* @param {Array} aParms containing [stateComputer, resume, fRestore]
|
|
*/
|
|
Computer.prototype.donePowerOn = function(aParms)
|
|
{
|
|
var stateComputer = aParms[0];
|
|
var fRepower = (aParms[1] < 0);
|
|
var fRestore = aParms[2];
|
|
|
|
if (DEBUG && this.fPowered) this.messageDebugger("Computer.donePowerOn(): redundant");
|
|
|
|
this.fPowered = true;
|
|
|
|
if (!this.fInitialized) {
|
|
this.println(Computer.sAppName + " v" + Computer.sAppVer + "\n" + Computer.sCopyright + "\n" + Computer.LICENSE);
|
|
this.fInitialized = true;
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* powerReport(stateComputer)
|
|
*
|
|
* @this {Computer}
|
|
* @param {State} stateComputer
|
|
*/
|
|
Computer.prototype.powerReport = function(stateComputer)
|
|
{
|
|
if (web.confirmUser("There may be a problem with your " + Computer.sAppName + " machine.\n\nTo help us diagnose it, click OK to send this " + Computer.sAppName + " machine state to http://" + SITEHOST + ".")) {
|
|
web.sendReport(Computer.sAppName, Computer.sAppVer, 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 loadResource() 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 {Computer}
|
|
* @param {boolean} fSave
|
|
* @param {boolean} [fShutdown] is true if the machine is being shut down
|
|
* @return {string|null} string representing the captured state (or null if error)
|
|
*/
|
|
Computer.prototype.powerOff = function(fSave, fShutdown)
|
|
{
|
|
var data;
|
|
var sState = "none";
|
|
|
|
if (DEBUG) this.messageDebugger("Computer.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")");
|
|
|
|
var stateComputer = new State(this, Computer.sAppVer);
|
|
var stateValidate = new State(this, Computer.sAppVer, Computer.STATE_VALIDATE);
|
|
|
|
var sTimestamp = usr.getTimestamp();
|
|
stateValidate.set(Computer.STATE_TIMESTAMP, sTimestamp);
|
|
stateComputer.set(Computer.STATE_TIMESTAMP, sTimestamp);
|
|
stateComputer.set(Computer.STATE_VERSION, APPVERSION);
|
|
stateComputer.set(Computer.STATE_HOSTURL, web.getHostURL());
|
|
stateComputer.set(Computer.STATE_BROWSER, web.getUserAgent());
|
|
|
|
/*
|
|
* Always power the CPU "down" first, just to 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.haltCPU();
|
|
data = this.cpu.powerDown(fSave, fShutdown);
|
|
if (typeof data === "object") stateComputer.set(this.cpu.id, data);
|
|
if (fShutdown) {
|
|
this.cpu.fPowered = 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.fPowered) {
|
|
if (component.powerDown) {
|
|
data = component.powerDown(fSave, fShutdown);
|
|
if (typeof data === "object") stateComputer.set(component.id, data);
|
|
}
|
|
if (fShutdown) {
|
|
component.fPowered = 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 == Computer.RESUME_DELETE);
|
|
}
|
|
}
|
|
if (fClear) {
|
|
stateComputer.clear(fClearAll);
|
|
}
|
|
} else {
|
|
sState = stateComputer.toString();
|
|
}
|
|
}
|
|
|
|
if (fShutdown) this.fPowered = false;
|
|
|
|
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 {Computer}
|
|
*/
|
|
Computer.prototype.reset = function()
|
|
{
|
|
if (this.bus && this.bus.reset) {
|
|
this.messageDebugger("Resetting " + this.bus.type, true);
|
|
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.messageDebugger("Resetting " + component.type, true);
|
|
component.reset();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* start(ms, nCycles)
|
|
*
|
|
* Notify all (other) components with a start() method that the CPU has started.
|
|
*
|
|
* Note that we're called by runCPU(), which is why we exclude the CPU component,
|
|
* as well as ourselves.
|
|
*
|
|
* @this {Computer}
|
|
* @param {number} ms
|
|
* @param {number} nCycles
|
|
*/
|
|
Computer.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);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* stop(ms, nCycles)
|
|
*
|
|
* Notify all (other) components with a stop() method that the CPU has stopped.
|
|
*
|
|
* Note that we're called by runCPU(), which is why we exclude the CPU component,
|
|
* as well as ourselves.
|
|
*
|
|
* @this {Computer}
|
|
* @param {number} ms
|
|
* @param {number} nCycles
|
|
*/
|
|
Computer.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);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* setBinding(sHTMLClass, sHTMLType, sBinding, control)
|
|
*
|
|
* @this {Computer}
|
|
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
|
|
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
|
|
* @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)
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
Computer.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control)
|
|
{
|
|
var computer = this;
|
|
switch (sBinding) {
|
|
case "save":
|
|
this.bindings[sBinding] = control;
|
|
control.onclick = function onClickSave() {
|
|
var sUserID = computer.queryUserID(true);
|
|
if (sUserID) {
|
|
var fSave = !!(computer.resume && !computer.sResumePath);
|
|
var sState = computer.powerOff(fSave);
|
|
if (fSave) {
|
|
computer.saveServerState(sUserID, sState);
|
|
} else {
|
|
computer.notice("Resume disabled, machine state not saved");
|
|
}
|
|
}
|
|
};
|
|
return true;
|
|
case "reset":
|
|
this.bindings[sBinding] = control;
|
|
control.onclick = function onClickReset() {
|
|
computer.onReset();
|
|
};
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* resetUserID()
|
|
*/
|
|
Computer.prototype.resetUserID = function()
|
|
{
|
|
web.setLocalStorageItem(Computer.STATE_USERID, "");
|
|
this.sUserID = null;
|
|
};
|
|
|
|
/**
|
|
* queryUserID(fPrompt)
|
|
*
|
|
* @param {boolean} [fPrompt]
|
|
* @returns {string|null|undefined}
|
|
*/
|
|
Computer.prototype.queryUserID = function(fPrompt)
|
|
{
|
|
var sUserID = this.sUserID;
|
|
if (!sUserID) {
|
|
sUserID = web.getLocalStorageItem(Computer.STATE_USERID);
|
|
if (sUserID !== undefined) {
|
|
if (!sUserID && fPrompt) {
|
|
sUserID = web.promptUser("To save machine states on the pcjs.org server, you need a user ID (email Jeff@pcjs.org).\n\nOnce you have an ID, enter it below.");
|
|
if (sUserID) {
|
|
sUserID = this.verifyUserID(sUserID);
|
|
if (!sUserID) this.notice("Your user ID has not been approved.");
|
|
}
|
|
}
|
|
} else if (fPrompt) {
|
|
this.notice("Browser local storage is not available");
|
|
}
|
|
}
|
|
return sUserID;
|
|
};
|
|
|
|
/**
|
|
* verifyUserID(sUserID)
|
|
*
|
|
* @this {Computer}
|
|
* @param {string} sUserID
|
|
* @return {string} validated user ID, or null if error
|
|
*/
|
|
Computer.prototype.verifyUserID = function(sUserID)
|
|
{
|
|
this.sUserID = null;
|
|
if (DEBUG) this.messageDebugger("verifyUserID(" + sUserID + ")");
|
|
var sRequest = web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUserID;
|
|
var response = web.loadResource(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(Computer.STATE_USERID, response.data);
|
|
if (DEBUG) this.messageDebugger(Computer.STATE_USERID + " updated: " + response.data);
|
|
this.sUserID = response.data;
|
|
} else {
|
|
if (DEBUG) this.messageDebugger(response.code + ": " + response.data);
|
|
}
|
|
} catch (e) {
|
|
Component.error(e.message + " (" + sResponse + ")");
|
|
}
|
|
} else {
|
|
if (DEBUG) this.messageDebugger("invalid response (error " + nErrorCode + ")");
|
|
}
|
|
return this.sUserID;
|
|
};
|
|
|
|
/**
|
|
* getServerStatePath()
|
|
*
|
|
* @this {Computer}
|
|
* @return {string|null} sStatePath (null if no localStorage or no USERID stored in localStorage)
|
|
*/
|
|
Computer.prototype.getServerStatePath = function()
|
|
{
|
|
var sStatePath = null;
|
|
if (this.sUserID) {
|
|
if (DEBUG) this.messageDebugger(Computer.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, Computer.sAppVer);
|
|
} else {
|
|
if (DEBUG) this.messageDebugger(Computer.STATE_USERID + " unavailable");
|
|
}
|
|
return sStatePath;
|
|
};
|
|
|
|
/**
|
|
* saveServerState(sUserID, sState)
|
|
*
|
|
* @param {string} sUserID
|
|
* @param {string|null} sState
|
|
*/
|
|
Computer.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.messageDebugger("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.messageDebugger("no state to store");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* storeServerState(sUserID, sState, fSync)
|
|
*
|
|
* @this {Computer}
|
|
* @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
|
|
*/
|
|
Computer.prototype.storeServerState = function(sUserID, sState, fSync)
|
|
{
|
|
if (DEBUG) this.messageDebugger(Computer.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 data = {};
|
|
data[UserAPI.QUERY.REQ] = UserAPI.REQ.STORE;
|
|
data[UserAPI.QUERY.USER] = sUserID;
|
|
data[UserAPI.QUERY.STATE] = State.key(this, Computer.sAppVer);
|
|
data[UserAPI.QUERY.DATA] = sState;
|
|
var sRequest = web.getHost() + UserAPI.ENDPOINT;
|
|
if (!fSync) {
|
|
web.loadResource(sRequest, true, data);
|
|
} else {
|
|
var response = web.loadResource(sRequest, false, data);
|
|
var sResponse = response[1];
|
|
if (response[0]) {
|
|
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[0] + ',"' + UserAPI.RES.DATA + '":"' + sResponse + '"}';
|
|
}
|
|
if (DEBUG) this.messageDebugger(sResponse);
|
|
return JSON.parse(sResponse);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* onReset()
|
|
*
|
|
* @this {Computer}
|
|
*/
|
|
Computer.prototype.onReset = function()
|
|
{
|
|
/*
|
|
* 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 == Computer.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 == Computer.RESUME_AUTO || */ !web.confirmUser("Click OK to save the " + Computer.sAppName + " machine state.\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 loadResource()
|
|
* 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(Computer.RESUME_NONE);
|
|
this.fReload = false;
|
|
} else {
|
|
this.reset();
|
|
if (this.cpu) this.cpu.autoStart();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* getComponentByType(sType, componentPrev)
|
|
*
|
|
* @this {Computer}
|
|
* @param {string} sType
|
|
* @param {Component} [componentPrev] of previously returned component, if any
|
|
* @return {Component|null}
|
|
*/
|
|
Computer.prototype.getComponentByType = function(sType, 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;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* messageDebugger(sMessage, fForce)
|
|
*
|
|
* This is a combination of the Debugger's messageEnabled(MESSAGE_CMP) and message() functions, for convenience.
|
|
*
|
|
* @this {Computer}
|
|
* @param {string} sMessage is any caller-defined message string
|
|
* @param {boolean} [fForce] is true to force display of the message
|
|
*/
|
|
Computer.prototype.messageDebugger = function(sMessage, fForce)
|
|
{
|
|
if (DEBUGGER && this.dbg) {
|
|
if (fForce || this.dbg.messageEnabled(this.dbg.MESSAGE_CMP)) this.dbg.message(sMessage);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Computer.init()
|
|
*
|
|
* This function operates on every element (e) of class "computer", and initializes
|
|
* all the necessary HTML to construct the Computer(s) as spec'ed.
|
|
*
|
|
* Note that each element (e) of class "computer" is expected to have a "data-value"
|
|
* attribute containing the same JSON-encoded parameters that the Computer constructor
|
|
* expects.
|
|
*/
|
|
Computer.init = function()
|
|
{
|
|
var aeMachines = Component.getElementsByClass(window.document, PCJSCLASS + "-machine");
|
|
|
|
for (var iMachine = 0; iMachine < aeMachines.length; iMachine++) {
|
|
|
|
var eMachine = aeMachines[iMachine];
|
|
var parmsMachine = Component.getComponentParms(eMachine);
|
|
|
|
var aeComputers = Component.getElementsByClass(eMachine, PCJSCLASS, "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 Computer(parmsComputer, parmsMachine, true);
|
|
|
|
if (DEBUG) computer.messageDebugger("onInit(" + computer.fPowered + ")");
|
|
|
|
/*
|
|
* For now, all we support are "reset" and "save" buttons. We may eventually add a "power"
|
|
* button to manually suspend/resume the machine. An "erase" button was also considered, but
|
|
* "reset" now provides a way to force the machine to start from scratch again, so "erase"
|
|
* might be redundant now.
|
|
*/
|
|
Component.bindComponentControls(computer, eComputer, PCJSCLASS);
|
|
|
|
/*
|
|
* Power "up" the computer, giving every component the opportunity to reset or restore itself.
|
|
*/
|
|
computer.wait(computer.powerOn);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Computer.show()
|
|
*/
|
|
Computer.show = function()
|
|
{
|
|
var aeComputers = Component.getElementsByClass(window.document, PCJSCLASS, "computer");
|
|
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
|
|
var eComputer = aeComputers[iComputer];
|
|
var parmsComputer = Component.getComponentParms(eComputer);
|
|
var computer = Component.getComponentByType("Computer", parmsComputer['id']);
|
|
if (computer) {
|
|
|
|
if (DEBUG) computer.messageDebugger("onShow(" + computer.fInitialized + "," + computer.fPowered + ")");
|
|
|
|
if (computer.fInitialized && !computer.fPowered) {
|
|
/**
|
|
* Repower the computer, notifying every component to continue running as-is.
|
|
*/
|
|
computer.powerOn(Computer.RESUME_REPOWER);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Computer.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 Computer.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.
|
|
*/
|
|
Computer.exit = function()
|
|
{
|
|
var aeComputers = Component.getElementsByClass(window.document, PCJSCLASS, "computer");
|
|
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
|
|
var eComputer = aeComputers[iComputer];
|
|
var parmsComputer = Component.getComponentParms(eComputer);
|
|
var computer = Component.getComponentByType("Computer", parmsComputer['id']);
|
|
if (computer) {
|
|
|
|
if (DEBUG) computer.messageDebugger("onExit(" + computer.fPowered + ")");
|
|
|
|
if (computer.fPowered) {
|
|
/**
|
|
* Power "down" 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(Computer.init);
|
|
web.onShow(Computer.show);
|
|
web.onExit(Computer.exit);
|
|
|
|
if (typeof APP_PCJS !== 'undefined') APP_PCJS.Computer = Computer;
|
|
|
|
if (typeof module !== 'undefined') module.exports = Computer;
|