Improved separation of FDC media parameters from drive parameters
This commit is contained in:
parent
2ce025aa78
commit
1809543577
32 changed files with 1915 additions and 1887 deletions
|
|
@ -51,19 +51,19 @@
|
|||
|
||||
/*
|
||||
* Client/Server Disk I/O
|
||||
*
|
||||
*
|
||||
* To support large disks without consuming large amounts of client-side memory, and to push
|
||||
* client-side disk changes back the server, we need a DiskIO API that can be used in place of
|
||||
* the DiskDump API.
|
||||
*
|
||||
*
|
||||
* Use of the DiskIO API and any associated disk images must be tightly coupled to per-user
|
||||
* storage and specific machine configurations, to prevent the disk images from being corrupted
|
||||
* by inconsistent I/O operations. Our basic User API (userapi.js) already provides some
|
||||
* per-user storage that we can use to get the design rolling.
|
||||
*
|
||||
*
|
||||
* The DiskIO API must also provide the ability to create new (empty) hard disk images in per-user
|
||||
* storage and automatically associate them with the machine configurations that requested them.
|
||||
*
|
||||
*
|
||||
* Principles
|
||||
* ---
|
||||
* Originally, when the Disk class was given a disk image to load and mount, it would request the
|
||||
|
|
@ -71,84 +71,84 @@
|
|||
* for larger disks -- let's just say anything stored on the server as an IMG file -- we'd prefer
|
||||
* to interact with that disk using "On-Demand I/O". Any IMG file on the same server as the PCjs
|
||||
* application should be a candidate for on-demand access.
|
||||
*
|
||||
*
|
||||
* On-Demand I/O means that nothing is initially transferred from the server. As sectors are
|
||||
* requested by the PCjs machine, PCjs requests them from the server, and maintains an MRU cache
|
||||
* of sectors, periodically discarding the least-used clean sectors above a certain memory limit.
|
||||
* Dirty sectors (ie, those that the PCjs machine has written to) must be periodically sent
|
||||
* back to the server and then marked as clean, so that they can be discarded like any other
|
||||
* sector.
|
||||
*
|
||||
*
|
||||
* We also support "local" init-only disk images, which means that dirty sectors are never sent
|
||||
* back to the server and are instead retained by the client for the lifetime of the app; such
|
||||
* images are "read-only" as far as the server is concerned, but "read-write" as far as the client
|
||||
* is concerned. Reloading/restarting an app with an "local" disk will return the disk to its
|
||||
* initial state.
|
||||
*
|
||||
* initial state.
|
||||
*
|
||||
* Practice
|
||||
* ---
|
||||
* Let's first look at what we *already* do for the HDC component:
|
||||
*
|
||||
*
|
||||
* 1) Creating new (empty) disk images
|
||||
* 2) Pre-loading pre-built JSON-encoded disk images (converting them to JSON on the fly as needed)
|
||||
*
|
||||
*
|
||||
* An example of #1 is in /configs/pc/machines/5160/cga/256kb/demo/machine.xml:
|
||||
*
|
||||
*
|
||||
* <hdc id="hdcXT" drives='[{name:"10Mb Hard Disk",type:3}]'/>
|
||||
*
|
||||
*
|
||||
* and an example of #2 is in /configs/pc/disks/fixed/win101.xml:
|
||||
*
|
||||
*
|
||||
* <hdc id="hdcXT" drives='[{name:"10Mb Hard Disk",path:"/disks/pc/fixed/win101/10mb.json",type:3}]'/>
|
||||
*
|
||||
* The HDC component expects an array of drive entries. Array position determines drive numbering
|
||||
* (the first entry is drive 0, the second is drive 1, etc), and each entry contains the following
|
||||
* properties:
|
||||
*
|
||||
*
|
||||
* 'name': user-friendly name for the disk, if any
|
||||
* 'path': URL of the disk image, if any
|
||||
* 'type': a drive type
|
||||
*
|
||||
*
|
||||
* Of those properties, only 'type' is required, which provides an index into an HDC "Drive Type"
|
||||
* table that determines disk geometry and therefore disk size. As we add support for larger disks and
|
||||
* newer disk controllers, the 'type' parameter will be superseded by either a user-defined 'geometry'
|
||||
* parameter that will define number of heads, cylinders, tracks, sectors per track, and (max) bytes per
|
||||
* sector, or perhaps a generic 'size' parameter that leaves geometry choices to the HDC component,
|
||||
* which will then pass those decisions on to the Disk component.
|
||||
*
|
||||
*
|
||||
* We will enable on-demand I/O for a disk image with a new 'mode' parameter that looks like:
|
||||
*
|
||||
*
|
||||
* 'mode': one of "local", "preload", "demandrw", "demandro"
|
||||
*
|
||||
*
|
||||
* "preload" means the disk image will be completely preloaded, exactly as before; "demandrw" enables
|
||||
* full on-demand I/O support; and "demandro" enables on-demand I/O for reads only (all writes are retained
|
||||
* and never written back to the server).
|
||||
*
|
||||
*
|
||||
* "ro" will be the fallback for "rw" unless TWO other important criteria are met: 1) the user has a
|
||||
* private user key, and therefore per-user storage; and 2) the disk image 'path' contains an asterisk (*)
|
||||
* that the server can internally remap to a directory in the user's storage; eg:
|
||||
*
|
||||
*
|
||||
* 'path': <asterisk>/10mb.img (path components following the asterisk are optional)
|
||||
*
|
||||
*
|
||||
* If the disk image does not already exist, it will be created (but not formatted).
|
||||
*
|
||||
*
|
||||
* This preserves the promise that EVERYTHING a user does within a PCjs machine is private (ie, not
|
||||
* visible to any other PCjs users). I don't want to be in the business of saving any user machine
|
||||
* states or disk changes, but at least those operations are limited to users who have asked for (and
|
||||
* received) a private user key.
|
||||
*
|
||||
*
|
||||
* Another important consideration at this stage is dealing with multiple machines writing to the same
|
||||
* disk image; even though we're limiting the "demandrw" mode to per-user images, a single user may still
|
||||
* inadvertently start up multiple machines that refer to the same disk image.
|
||||
*
|
||||
*
|
||||
* So, every PCjs machine needs to generate a unique token and include that token with every Disk I/O API
|
||||
* operation, so that the server can revoke a previous machine's "rw" access to a disk image when a new
|
||||
* machine requests "rw" access to the same disk image.
|
||||
*
|
||||
*
|
||||
* From the client's perspective, revocation can be quietly dealt with by reverting to "demandro" mode;
|
||||
* that client becomes stuck with all their dirty sectors until they can reclaim "rw" access, which should
|
||||
* only happen if no intervening writes to the disk image on the server have occurred (if I bother allowing
|
||||
* reclamation at all).
|
||||
*
|
||||
*
|
||||
* The real challenge here is avoiding revocation of a machine that still has critical changes to commit,
|
||||
* but since we can't even solve the problem of a user closing their browser at an inopportune time
|
||||
* and potentially leaving a disk image in an inconsistent state, premature revocation is the least of
|
||||
|
|
@ -159,12 +159,12 @@
|
|||
"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 DiskAPI = require("../../shared/lib/diskapi");
|
||||
var DumpAPI = require("../../shared/lib/dumpapi");
|
||||
var Component = require("../../shared/lib/component");
|
||||
var str = require("../../shared/lib/strlib");
|
||||
var usr = require("../../shared/lib/usrlib");
|
||||
var web = require("../../shared/lib/weblib");
|
||||
var DiskAPI = require("../../shared/lib/diskapi");
|
||||
var DumpAPI = require("../../shared/lib/dumpapi");
|
||||
var Component = require("../../shared/lib/component");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -177,7 +177,7 @@ if (typeof module !== 'undefined') {
|
|||
* @property {number} iHead
|
||||
* @property {number} iModify
|
||||
* @property {number} cModify
|
||||
*
|
||||
*
|
||||
* Every Sector object (once loaded and fully parsed) should have ALL of the following named properties:
|
||||
*
|
||||
* 'sector': sector number
|
||||
|
|
@ -186,7 +186,7 @@ if (typeof module !== 'undefined') {
|
|||
* 'pattern': dword pattern to use for empty or partial sectors (or null if sector still needs to be loaded)
|
||||
*
|
||||
* initSector() also sets the following properties, to help us quickly identify its location within aDiskData:
|
||||
*
|
||||
*
|
||||
* iCylinder
|
||||
* iHead
|
||||
*
|
||||
|
|
@ -195,7 +195,7 @@ if (typeof module !== 'undefined') {
|
|||
* iModify: index of first modified dword in sector
|
||||
* cModify: number of modified dwords in sector
|
||||
* fDirty: true if sector is dirty, false if clean (or cleaning in progress)
|
||||
*
|
||||
*
|
||||
* fDirty is used in conjunction with "demandrw" disks; it is set to true whenever the sector is modified, and is
|
||||
* set to false whenever the sector has been sent to the server. If the server write succeeds and fDirty is still
|
||||
* false, then the sector modifications are removed (cModify is set to zero). If the write succeeds but fDirty was
|
||||
|
|
@ -203,7 +203,7 @@ if (typeof module !== 'undefined') {
|
|||
* in place (since we don't keep track of more than one modification range within a sector). And if the write failed,
|
||||
* then fDirty is set back to true and again all modifications remain in place; the best we can do is schedule another
|
||||
* write attempt.
|
||||
*
|
||||
*
|
||||
* TODO: Perhaps we should also maintain a failure count and stop trying to write sectors that reach a certain
|
||||
* threshold. Error-handling, as usual, is the thorniest problem.
|
||||
*/
|
||||
|
|
@ -223,7 +223,7 @@ if (typeof module !== 'undefined') {
|
|||
*
|
||||
* This means, for example, that all references to "track[iSector].data" must actually appear as
|
||||
* "track[iSector]['data']".
|
||||
*
|
||||
*
|
||||
* @constructor
|
||||
* @extends Component
|
||||
* @param {HDC|FDC} controller
|
||||
|
|
@ -233,13 +233,13 @@ if (typeof module !== 'undefined') {
|
|||
function Disk(controller, drive, mode)
|
||||
{
|
||||
Component.call(this, "Disk", {'id': controller.idMachine + ".disk" + Disk.nDisks++}, Disk);
|
||||
|
||||
|
||||
this.controller = controller;
|
||||
this.cmp = controller.cmp;
|
||||
this.dbg = controller.dbg;
|
||||
this.drive = drive;
|
||||
this.mode = mode;
|
||||
|
||||
|
||||
/*
|
||||
* We pull out a number of drive properties that we may or may not need as defaults
|
||||
*/
|
||||
|
|
@ -258,11 +258,11 @@ function Disk(controller, drive, mode)
|
|||
|
||||
/*
|
||||
* The following dirty sector and timer properties are used only with fOnDemand disks,
|
||||
* assuming fRemote was successfully set.
|
||||
* assuming fRemote was successfully set.
|
||||
*/
|
||||
this.aDirtySectors = [];
|
||||
this.aDirtyTimestamps = []; // this array is parallel to aDirtySectors
|
||||
this.timerWrite = null; // REMOTE_WRITE_DELAY timer in effect, if any
|
||||
this.timerWrite = null; // REMOTE_WRITE_DELAY timer in effect, if any
|
||||
this.msTimerWrite = 0; // the time that the write timer, if any, is set to fire
|
||||
this.fWriteInProgress = false;
|
||||
|
||||
|
|
@ -276,12 +276,12 @@ function Disk(controller, drive, mode)
|
|||
* @property {number} 2 contains iSector
|
||||
* @property {number} 3 contains nSectors
|
||||
* @property {boolean} 4 contains fAsync
|
||||
* @property {function(nErrorCode:number,fAsync:boolean)} 5 contains done
|
||||
* @property {function(nErrorCode:number,fAsync:boolean)} 5 contains done
|
||||
*/
|
||||
|
||||
/**
|
||||
* The default number of milliseconds to wait before writing a dirty sector back to a remote disk image
|
||||
*
|
||||
*
|
||||
* @const {number}
|
||||
*/
|
||||
Disk.REMOTE_WRITE_DELAY = 2000; // 2-second delay
|
||||
|
|
@ -295,7 +295,7 @@ Component.subclass(Component, Disk);
|
|||
|
||||
/**
|
||||
* initBus(cmp, bus, cpu, dbg)
|
||||
*
|
||||
*
|
||||
* We have no real interest in this notification, other than to obtain a reference to the Debugger
|
||||
* for every disk loaded BEFORE the initBus() phase; any disk loaded AFTER that point will get its Debugger
|
||||
* reference, if any, from the disk controller passed to the Disk() constructor.
|
||||
|
|
@ -315,7 +315,7 @@ Disk.prototype.initBus = function(cmp, bus, cpu, dbg) {
|
|||
*
|
||||
* As with powerDown(), our sole concern here is for REMOTE disks: if a powerDown() call disconnected an
|
||||
* "on-demand" disk, we need to get reconnected. Calling our own load() function should get the job done.
|
||||
*
|
||||
*
|
||||
* The HDC component could have triggered this as well, but its powerUp() function only calls autoMount()
|
||||
* in case of page (ie, application) reload, which is fine for local disks but insufficient for remote disks,
|
||||
* which have a server connection that must re-established.
|
||||
|
|
@ -339,14 +339,14 @@ Disk.prototype.powerUp = function(data, fRepower) {
|
|||
*
|
||||
* Our sole concern here is for REMOTE disks, making sure any unwritten changes get flushed to
|
||||
* the server during a shutdown. No local state is ever returned, so fSave is ignored.
|
||||
*
|
||||
*
|
||||
* Local disks are managed by the controller (ie, FDC or HDC) that mounted them; the controller's
|
||||
* powerDown() handler will take care of calling save() as needed.
|
||||
*
|
||||
*
|
||||
* TODO: Consider taking responsibility for saving the state of local disks as well; the only reason
|
||||
* the controllers still take care of them is historical, because this component originally didn't
|
||||
* exist, and even after it was created, it didn't originally receive powerDown() notifications.
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @param {boolean} fSave
|
||||
* @param {boolean} [fShutdown]
|
||||
|
|
@ -385,7 +385,7 @@ Disk.prototype.powerDown = function(fSave, fShutdown)
|
|||
|
||||
/**
|
||||
* create()
|
||||
*
|
||||
*
|
||||
* Initializes the disk contents according to the current drive mode and parameters.
|
||||
*/
|
||||
Disk.prototype.create = function()
|
||||
|
|
@ -407,7 +407,7 @@ Disk.prototype.create = function()
|
|||
* Now that our read() and write() functions can deal with unallocated data
|
||||
* arrays, and can read/write the specified pattern on-the-fly, we no longer need
|
||||
* to pre-allocate and pre-initialize the 'data' array.
|
||||
*
|
||||
*
|
||||
* For "local" disks, we can assume a 'pattern' of 0, but for "demandrw" and "demandro"
|
||||
* disks, 'pattern' is set to null, as yet another indication that I/O is required to load
|
||||
* the sector from the server (or to write it back to the server).
|
||||
|
|
@ -429,12 +429,12 @@ Disk.prototype.create = function()
|
|||
* TODO: Figure out how we can strongly type fnNotify, because the Closure Compiler has issues with:
|
||||
*
|
||||
* param {function(Component,Object,Disk,string,string)} fnNotify
|
||||
*
|
||||
*
|
||||
* Also, while we're at it, learn if there are ways to:
|
||||
*
|
||||
*
|
||||
* 1) declare a function taking NO parameters (ie, generate a warning if any parameters are specified)
|
||||
* 2) declare a type for a function's return value
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @param {string} sDiskName
|
||||
* @param {string} sDiskPath
|
||||
|
|
@ -443,7 +443,7 @@ Disk.prototype.create = function()
|
|||
Disk.prototype.load = function(sDiskName, sDiskPath, fnNotify)
|
||||
{
|
||||
var sDiskURL = sDiskPath;
|
||||
|
||||
|
||||
/*
|
||||
* We could use this.log() as well, but it wouldn't also log which component initiated the load.
|
||||
*/
|
||||
|
|
@ -452,7 +452,7 @@ Disk.prototype.load = function(sDiskName, sDiskPath, fnNotify)
|
|||
this.controller.log(sMessage);
|
||||
this.messageDebugger(sMessage);
|
||||
}
|
||||
|
||||
|
||||
this.sDiskName = sDiskName;
|
||||
this.sDiskPath = sDiskPath;
|
||||
this.fnNotify = fnNotify;
|
||||
|
|
@ -484,16 +484,16 @@ Disk.prototype.load = function(sDiskName, sDiskPath, fnNotify)
|
|||
* disk BPBs, you'll always get a standard PC XT 10mb disk image, so if the 'file' or 'dir' contains
|
||||
* more than 10mb of data, the request will fail. Ultimately, I want to honor the controller's
|
||||
* driveConfig 'size' parm, or to match the capacity required by the driveConfig 'type' parameter.
|
||||
*
|
||||
*
|
||||
* If a 'disk' is specified, we pass mbhd=0, because the actual size will depend on the image.
|
||||
* However, I don't currently have any .DSK or .IMG files containing hard disk images; those formats
|
||||
* were really intended for floppy disk images. If I never create any hard disk image files, then
|
||||
* we can simply eliminate sSizeParm in the 'disk' case.
|
||||
*
|
||||
*
|
||||
* Added more extensions to the list of paths-treated-as-disk-images, so that URLs to files located here:
|
||||
*
|
||||
*
|
||||
* ftp://ftp.oldskool.org/pub/TOPBENCH/dskimage/
|
||||
*
|
||||
*
|
||||
* can be used as-is. TODO: There's a TODO in netlib.getFile() regarding remote support that needs
|
||||
* to be resolved first; DiskDump relies on that function for its remote requests, and it currently
|
||||
* supports only HTTP.
|
||||
|
|
@ -513,10 +513,10 @@ Disk.prototype.load = function(sDiskName, sDiskPath, fnNotify)
|
|||
|
||||
/**
|
||||
* onLoadDisk(sDiskFile, sDiskData, nErrorCode, sDiskPath)
|
||||
*
|
||||
*
|
||||
* This function was originally called mount(). If the mount is successful, we pass the Disk object to the
|
||||
* caller's fnNotify handler; otherwise, we pass null.
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @param {string} sDiskFile
|
||||
* @param {string} sDiskData
|
||||
|
|
@ -578,18 +578,18 @@ Disk.prototype.onLoadDisk = function(sDiskFile, sDiskData, nErrorCode, sDiskPath
|
|||
* eval(). In particular, the 10Mb disk image we use for the Windows 1.01 demo config fails in
|
||||
* IE9 with an "Out of memory" exception. One work-around would be to chop the data into chunks
|
||||
* (perhaps one track per chunk, using regular expressions) and then manually re-assemble it.
|
||||
*
|
||||
*
|
||||
* However, it turns out that using JSON.parse(sDiskData) instead of eval("(" + sDiskData + ")")
|
||||
* is a much easier fix. The only drawback is that we must first quote any unquoted property names
|
||||
* and remove any comments, because while eval() was cool with them, JSON.parse() is more particular;
|
||||
* the following RegExp replacements take care of those requirements.
|
||||
*
|
||||
*
|
||||
* The use of hex values is something else that eval() was OK with, but JSON.parse() is not, and
|
||||
* while I've stopped using hex values in DumpAPI responses (at least when "format=json" is specified),
|
||||
* I can't guarantee they won't show up in "legacy" images, and there's no simple RegExp replacement
|
||||
* for transforming hex values into decimal values, so I cop out and fall back to eval() if I detect
|
||||
* any hex prefixes ("0x") in the sequence. Ditto for error messages, which appear like so:
|
||||
*
|
||||
*
|
||||
* ["unrecognized disk path: test.img"]
|
||||
*/
|
||||
var aDiskData;
|
||||
|
|
@ -622,18 +622,18 @@ Disk.prototype.onLoadDisk = function(sDiskFile, sDiskData, nErrorCode, sDiskPath
|
|||
* aDiskData is an array of cylinders, each of which is an array of heads, each of which
|
||||
* is an array of sector objects. The format does not impose any limitations on number of
|
||||
* cylinders, number of heads, or number of bytes in any of the sector object byte-arrays.
|
||||
*
|
||||
*
|
||||
* WARNING: All accesses to sector object properties must be via their string names, not their
|
||||
* "dot" names, otherwise code will break after it's been processed by the Closure Compiler.
|
||||
*
|
||||
*
|
||||
* Sector object properties include:
|
||||
*
|
||||
*
|
||||
* 'sector' the sector number (1-based, not required to be sequential)
|
||||
* 'length' the byte-length (ie, formatted length) of the sector
|
||||
* 'data' the dword-array containing the sector data
|
||||
* 'pattern' if the dword-array length is less than 'length'/4, this value must be used
|
||||
* to pad out the sector; if no 'pattern' is specified, it's assumed to be zero
|
||||
*
|
||||
*
|
||||
* We still support the older JSON encoding, where sector data was encoded as an array of 'bytes'
|
||||
* rather than a dword 'data' array. However, our support is strictly limited to an on-the-fly
|
||||
* conversion to a forward-compatible 'data' array.
|
||||
|
|
@ -698,27 +698,27 @@ Disk.prototype.onLoadDisk = function(sDiskFile, sDiskData, nErrorCode, sDiskPath
|
|||
}
|
||||
/*
|
||||
* The current sector should now have ALL the properties of a proper Sector object; ie:
|
||||
*
|
||||
*
|
||||
* 'sector': sector number
|
||||
* 'length': size of the sector, in bytes
|
||||
* 'data': array of dwords
|
||||
* 'pattern': dword pattern to use for empty or partial sectors
|
||||
*
|
||||
*
|
||||
* In addition, we will maintain the following information on a per-sector basis,
|
||||
* as sectors are modified:
|
||||
*
|
||||
*
|
||||
* iModify: index of first modified dword in sector
|
||||
* cModify: number of modified dwords in sector
|
||||
* fDirty: true if sector is dirty, false if clean (or cleaning in progress)
|
||||
*
|
||||
*
|
||||
* And for the disk as a whole, we maintain a checksum of the original unmodified data:
|
||||
*
|
||||
*
|
||||
* dwChecksum: summation of all dwords in all non-empty sectors
|
||||
*/
|
||||
this.initSector(sector, iCylinder, iHead);
|
||||
/*
|
||||
* Pattern-filling of sectors is deferred until absolutely necessary (eg, when a sector is
|
||||
* being written). So all we need to do at this point is checksum all the initial sector data.
|
||||
* being written). So all we need to do at this point is checksum all the initial sector data.
|
||||
*/
|
||||
for (var idw = 0; idw < adw.length; idw++) {
|
||||
dwChecksum = (dwChecksum + adw[idw]) & 0xffffffff;
|
||||
|
|
@ -741,7 +741,7 @@ Disk.prototype.onLoadDisk = function(sDiskFile, sDiskData, nErrorCode, sDiskPath
|
|||
|
||||
/**
|
||||
* initSector(sector, iCylinder, iHead, iSector, cbSector, dwPattern)
|
||||
*
|
||||
*
|
||||
* @param {Object} sector
|
||||
* @param {number} iCylinder
|
||||
* @param {number} iHead
|
||||
|
|
@ -779,7 +779,7 @@ Disk.prototype.onLoadParseSectors = function(sURLName, sURLData, nErrorCode, sec
|
|||
var iSector = sectorInfo[2];
|
||||
var nSectors = sectorInfo[3];
|
||||
fAsync = sectorInfo[4];
|
||||
|
||||
|
||||
if (DEBUG) this.messageDebugger("Disk.onLoadParseSectors(" + iCylinder + ":" + iHead + ":" + iSector + ":" + nSectors + ")");
|
||||
|
||||
var abData = JSON.parse(sURLData);
|
||||
|
|
@ -788,10 +788,10 @@ Disk.prototype.onLoadParseSectors = function(sURLName, sURLData, nErrorCode, sec
|
|||
/*
|
||||
* We call seek with fWrite == true to prevent seek() from triggering another call
|
||||
* to readRemoteSectors() and endlessly recursing. That also forces seek() to:
|
||||
*
|
||||
*
|
||||
* 1) zero the sector's 'pattern'
|
||||
* 2) disable warning about reading an uninitialized sector
|
||||
*
|
||||
*
|
||||
* We KNOW this is an uninitialized sector, because we're about to initialize it.
|
||||
*/
|
||||
var sector = this.seek(iCylinder, iHead, iSector, true);
|
||||
|
|
@ -814,11 +814,11 @@ Disk.prototype.onLoadParseSectors = function(sURLName, sURLData, nErrorCode, sec
|
|||
|
||||
/**
|
||||
* connectRemoteDisk(sDiskPath)
|
||||
*
|
||||
*
|
||||
* Unlike disconnect(), we don't issue the connect request ourselves; instead, we piggyback on the existing
|
||||
* preload code in load() to establish the connection. That, in turn, will trigger a call to mount(), which
|
||||
* will check fOnDemand and set fRemote if the connection was successful.
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @param {string} sDiskPath
|
||||
* @return {string} is the URL connection string required to connect to sDiskPath
|
||||
|
|
@ -836,7 +836,7 @@ Disk.prototype.connectRemoteDisk = function(sDiskPath)
|
|||
|
||||
/**
|
||||
* readRemoteSectors(iCylinder, iHead, iSector, nSectors, done)
|
||||
*
|
||||
*
|
||||
* @param {number} iCylinder
|
||||
* @param {number} iHead
|
||||
* @param {number} iSector
|
||||
|
|
@ -847,7 +847,7 @@ Disk.prototype.connectRemoteDisk = function(sDiskPath)
|
|||
Disk.prototype.readRemoteSectors = function(iCylinder, iHead, iSector, cbSector, nSectors, done)
|
||||
{
|
||||
if (DEBUG) this.messageDebugger("Disk.readRemoteSectors(" + iCylinder + ":" + iHead + ":" + iSector + ":" + nSectors + "," + cbSector + ")");
|
||||
|
||||
|
||||
if (this.fRemote) {
|
||||
var sParms = DiskAPI.QUERY.ACTION + '=' + DiskAPI.ACTION.READ;
|
||||
sParms += '&' + DiskAPI.QUERY.VOLUME + '=' + this.sDiskPath;
|
||||
|
|
@ -868,12 +868,12 @@ Disk.prototype.readRemoteSectors = function(iCylinder, iHead, iSector, cbSector,
|
|||
* Writes to a remote disk are performed on a timer-driven basis. When a sector is modified for the first time,
|
||||
* a reference to that sector is "pushed" onto (ie, appended to the end of) aDirtySectors, and if aDirtySectors was
|
||||
* originally empty, then a REMOTE_WRITE_DELAY timer is set.
|
||||
*
|
||||
*
|
||||
* When the timer fires, the first batch of contiguous sectors is sent off the server, and when the server responds
|
||||
* (ie, when cleanDirtySectors() is called), if the response indicates success, every sector that was sent is marked
|
||||
* clean -- unless one or more writes to the sector occurred in the meantime, which we track through a per-sector
|
||||
* fDirty flag.
|
||||
*
|
||||
* fDirty flag.
|
||||
*
|
||||
* @param {number} iCylinder
|
||||
* @param {number} iHead
|
||||
* @param {number} iSector
|
||||
|
|
@ -885,7 +885,7 @@ Disk.prototype.readRemoteSectors = function(iCylinder, iHead, iSector, cbSector,
|
|||
Disk.prototype.writeRemoteSectors = function(iCylinder, iHead, iSector, nSectors, abSectors, fAsync)
|
||||
{
|
||||
if (DEBUG) this.messageDebugger("Disk.writeRemoteSectors(" + iCylinder + ":" + iHead + ":" + iSector + ":" + nSectors + ")");
|
||||
|
||||
|
||||
if (this.fRemote) {
|
||||
var data = {};
|
||||
this.fWriteInProgress = true;
|
||||
|
|
@ -904,7 +904,7 @@ Disk.prototype.writeRemoteSectors = function(iCylinder, iHead, iSector, nSectors
|
|||
|
||||
/**
|
||||
* disconnectRemoteDisk()
|
||||
*
|
||||
*
|
||||
* This is called by our powerDown() notification handler. If fRemote is true, we issue the disconnect request and
|
||||
* then immediately set fRemote to false; we don't wait for (or test) the response.
|
||||
*
|
||||
|
|
@ -928,7 +928,7 @@ Disk.prototype.disconnectRemoteDisk = function()
|
|||
*
|
||||
* Mark the specified sector as dirty, add it to the queue (aDirtySectors) if not already added,
|
||||
* and establish a timeout handler (findDirtySectors) if not already established.
|
||||
*
|
||||
*
|
||||
* A freshly dirtied sector should sit in the queue for a short period of time (eg, 2 seconds)
|
||||
* before we attempt to write it; that is, a REMOTE_WRITE_DELAY timer should start ticking again
|
||||
* for any sector that is rewritten. However, there will be exceptions; for example, when a sector
|
||||
|
|
@ -961,7 +961,7 @@ Disk.prototype.queueDirtySector = function(sector, fAsync)
|
|||
*
|
||||
* If a timer is already active, make sure it's still valid (ie, the time the timer is scheduled to fire is
|
||||
* >= the timestamp of the next dirty sector + REMOTE_WRITE_DELAY); if not, cancel the timer and start a new one.
|
||||
*
|
||||
*
|
||||
* @return {boolean} true if write timer set, false if not
|
||||
*/
|
||||
Disk.prototype.updateWriteTimer = function()
|
||||
|
|
@ -1050,7 +1050,7 @@ Disk.prototype.onWriteCleanSectors = function(sURLName, sURLData, nErrorCode, se
|
|||
var nSectors = sectorInfo[3];
|
||||
var fAsync = sectorInfo[4];
|
||||
this.fWriteInProgress = false;
|
||||
|
||||
|
||||
if (iCylinder >= 0 && iCylinder < this.aDiskData.length && iHead >= 0 && iHead < this.aDiskData[iCylinder].length) {
|
||||
for (var i = iSector - 1; nSectors-- > 0 && i >= 0 && i < this.aDiskData[iCylinder][iHead].length; i++) {
|
||||
var sector = this.aDiskData[iCylinder][iHead][i];
|
||||
|
|
@ -1071,7 +1071,7 @@ Disk.prototype.onWriteCleanSectors = function(sURLName, sURLData, nErrorCode, se
|
|||
|
||||
/**
|
||||
* info()
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @return {Array} containing: [nCylinders, nHeads, nSectorsPerTrack, nBytesPerSector]
|
||||
*/
|
||||
|
|
@ -1079,7 +1079,7 @@ Disk.prototype.info = function()
|
|||
{
|
||||
if (!this.aDiskData.length) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
}
|
||||
return [this.aDiskData.length, this.aDiskData[0].length, this.aDiskData[0][0].length, this.aDiskData[0][0][0]['length']];
|
||||
};
|
||||
|
||||
|
|
@ -1091,7 +1091,7 @@ Disk.prototype.info = function()
|
|||
* Sectors that were initially compressed should remain compressed unless/until they were modified.
|
||||
*
|
||||
* TODO: Check sectors (or at least modified sectors) to see if they can be recompressed.
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @return {string} containing the entire disk image as JSON-encoded data
|
||||
*/
|
||||
|
|
@ -1104,7 +1104,7 @@ Disk.prototype.dump = function()
|
|||
* back again during mount() -- or whenever JSON.parse() is used instead of eval(). But I still remove
|
||||
* them temporarily, so that any remaining property names (eg, "iModify", "cModify", "fDirty") can
|
||||
* easily be stripped out, by virtue of their being the only quoted properties left. We then "requote"
|
||||
* all the property names that remain.
|
||||
* all the property names that remain.
|
||||
*/
|
||||
s = s.replace(/"(sector|length|data|pattern)":/gm, "$1:");
|
||||
/*
|
||||
|
|
@ -1124,17 +1124,17 @@ Disk.prototype.dump = function()
|
|||
* properties. That code used to be in the FDC component, where it was perfectly reasonable
|
||||
* to access those properties. We need a cleaner interface back to the drive, similar to the
|
||||
* info() interface we provide to the controller.
|
||||
*
|
||||
*
|
||||
* Whether or not the "dynamic reconfiguration" feature itself is perfectly reasonable is,
|
||||
* of course, a separate question.
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @param {number} iCylinder
|
||||
* @param {number} iHead
|
||||
* @param {number} iSector
|
||||
* @param {boolean} [fWrite]
|
||||
* @param {function(Object,boolean)} [done]
|
||||
* @return {Object|null} is the requested sector, or null if not found (or not available yet)
|
||||
* @return {Object|null} is the requested sector, or null if not found (or not available yet)
|
||||
*/
|
||||
Disk.prototype.seek = function(iCylinder, iHead, iSector, fWrite, done)
|
||||
{
|
||||
|
|
@ -1181,7 +1181,7 @@ Disk.prototype.seek = function(iCylinder, iHead, iSector, fWrite, done)
|
|||
this.readRemoteSectors(iCylinder, iHead, iSector, drive.cbSector, nSectors, function onReadRemoteComplete(err, fAsync) {
|
||||
if (err) sector = null;
|
||||
// noinspection JSReferencingMutableVariableFromClosure
|
||||
done(sector, fAsync);
|
||||
done(sector, fAsync);
|
||||
});
|
||||
return null;
|
||||
} else {
|
||||
|
|
@ -1205,7 +1205,7 @@ Disk.prototype.seek = function(iCylinder, iHead, iSector, fWrite, done)
|
|||
|
||||
/**
|
||||
* fill(sector, ab, off)
|
||||
*
|
||||
*
|
||||
* @param {Object} sector
|
||||
* @param {*} ab (technically, this should be typed as Array.<number> but I'm having trouble coercing JSON.parse() to that)
|
||||
* @param {number} off
|
||||
|
|
@ -1251,7 +1251,7 @@ Disk.prototype.toBytes = function(sector)
|
|||
|
||||
/**
|
||||
* read(sector, ibSector, fCompare)
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @param {Object} sector (returned from a previous seek)
|
||||
* @param {number} ibSector a byte index within the given sector
|
||||
|
|
@ -1261,7 +1261,7 @@ Disk.prototype.toBytes = function(sector)
|
|||
Disk.prototype.read = function(sector, ibSector, fCompare)
|
||||
{
|
||||
var b = -1;
|
||||
|
||||
|
||||
if (DEBUG && !ibSector && !fCompare) this.messageDebugger("Disk.read(" + this.controller.id + ":" + this.drive.iDrive + "," + sector.iCylinder + ":" + sector.iHead + ":" + sector['sector'] + ")");
|
||||
|
||||
if (ibSector < sector['length']) {
|
||||
|
|
@ -1275,7 +1275,7 @@ Disk.prototype.read = function(sector, ibSector, fCompare)
|
|||
|
||||
/**
|
||||
* write(sector, ibSector, b)
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @param {Object} sector (returned from a previous seek)
|
||||
* @param {number} ibSector a byte index within the given sector
|
||||
|
|
@ -1286,7 +1286,7 @@ Disk.prototype.write = function(sector, ibSector, b)
|
|||
{
|
||||
if (this.fWriteProtected)
|
||||
return false;
|
||||
|
||||
|
||||
if (DEBUG && !ibSector) this.messageDebugger("Disk.write(" + this.controller.id + ":" + this.drive.iDrive + "," + sector.iCylinder + ":" + sector.iHead + ":" + sector['sector'] + ")");
|
||||
|
||||
if (ibSector < sector['length']) {
|
||||
|
|
@ -1299,7 +1299,7 @@ Disk.prototype.write = function(sector, ibSector, b)
|
|||
* Ensure every byte up to the specified byte is properly initialized.
|
||||
*/
|
||||
for (var i = adw.length; i <= idw; i++) adw[i] = dwPattern;
|
||||
|
||||
|
||||
if (!sector.cModify) {
|
||||
sector.iModify = idw;
|
||||
sector.cModify = 1;
|
||||
|
|
@ -1330,7 +1330,7 @@ Disk.prototype.write = function(sector, ibSector, b)
|
|||
* [iCylinder, iHead, iSector, iModify, [...]]
|
||||
*
|
||||
* where [...] is an array of modified dword(s) in the corresponding sector.
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @return {Array} of modified sectors
|
||||
*/
|
||||
|
|
@ -1373,7 +1373,7 @@ Disk.prototype.save = function()
|
|||
* [iCylinder, iHead, iSector, iModify, [...]]
|
||||
*
|
||||
* where [...] is an array of modified dword(s) in the corresponding sector.
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @param {Array} deltas
|
||||
* @return {number} 0 if no changes applied, -1 if an error occurred, otherwise the number of sectors modified
|
||||
|
|
@ -1382,7 +1382,7 @@ Disk.prototype.restore = function(deltas)
|
|||
{
|
||||
/*
|
||||
* If deltas is undefined, that's not necessarily an error; the controller may simply be (re)initializing
|
||||
* itself (although neither controller should be calling restore() under those conditions anymore).
|
||||
* itself (although neither controller should be calling restore() under those conditions anymore).
|
||||
*/
|
||||
var nChanges = 0;
|
||||
var sReason = "unsupported restore format";
|
||||
|
|
@ -1426,7 +1426,7 @@ Disk.prototype.restore = function(deltas)
|
|||
* Note the buried test for write-protection. Yes, an invariant condition should be tested
|
||||
* outside the loop, not inside, but (a) it's a trivial test, (b) the test should never fail
|
||||
* because save() should never generate any mods for a write-protected disk, and (c) it
|
||||
* centralizes all the failure conditions we're currently checking (which, admittedly, ain't much).
|
||||
* centralizes all the failure conditions we're currently checking (which, admittedly, ain't much).
|
||||
*/
|
||||
if (iCylinder >= this.aDiskData.length || iHead >= this.aDiskData[iCylinder].length || iSector >= this.aDiskData[iCylinder][iHead].length) {
|
||||
sReason = "sector " + iCylinder + ":" + iHead + ":" + iSector + " out of range (" + nChanges + " changes applied)";
|
||||
|
|
@ -1471,7 +1471,7 @@ Disk.prototype.restore = function(deltas)
|
|||
* messageDebugger(sMessage)
|
||||
*
|
||||
* This is a combination of the Debugger's messageEnabled(MESSAGE_DISK) and message() functions, for convenience.
|
||||
*
|
||||
*
|
||||
* @this {Disk}
|
||||
* @param {string} sMessage is any caller-defined message string
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue