pcjs/modules/pcx86/bin/pcx86
2016-12-30 15:56:01 -08:00

401 lines
14 KiB
JavaScript

#!/usr/bin/env node
/**
* @fileoverview Implements the PCx86 command-line interface
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @copyright © Jeff Parsons 2012-2017
* @suppress {missingProperties}
*
* This file is part of PCjs, a computer emulation software project at <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 modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <http://pcjs.org/modules/shared/lib/defines.js>.
*
* 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";
var path = require("path");
var fs = require("fs");
var repl = require("repl");
var defines = require("../../shared/lib/defines");
var str = require("../../shared/lib/strlib");
var proc = require("../../shared/lib/proclib");
var fConsole = false;
var fDebug = false;
var args = proc.getArgs();
var argv = args.argv;
var sCmdPrev = "";
if (argv['console'] !== undefined) fConsole = argv['console'];
if (argv['debug'] !== undefined) fDebug = argv['debug'];
var lib = path.join(path.dirname(fs.realpathSync(__filename)), "../lib/");
try {
var pkg = require(lib + "../../../package.json");
} catch(err) {
console.log(err.message);
}
/*
* We will build an array of components whose names will match the component names
* used in a JSON machine definition file; eg:
*
* [
* {name: "chipset":
* Create: ChipSet,
* objects: []
* },
* ...
* ]
*
* Every component name comes from the component filename, minus the ".js" extension;
* Create is the constructor returned by require().
*
* TODO: Update the list of ignored (ie, ignorable) components.
*/
var Component;
var dbg;
var aComponents = [];
var asComponentsIgnore = ["panel", "embed", "save"];
/*
* A few of the components are subclasses of other classes (eg, "x86cpu" is a subclass
* of "cpu"). In those situations, we "hoist" the subclass constructor into the
* corresponding superclass, because it is the name of the superclass that we rely on during
* machine initialization.
*/
var aSubClasses = {
"pcx86/lib/x86cpu": "pcx86/lib/cpu",
"pcx86/lib/debugger": "shared/lib/debugger"
};
/**
* loadComponents(asFiles)
*
* @param {Array.<string>} asFiles
*/
function loadComponents(asFiles)
{
for (var i = 0; i < asFiles.length; i++) {
var sFile = asFiles[i];
if (str.getExtension(sFile) != "js") continue;
var sName = str.getBaseName(sFile, true);
if (asComponentsIgnore.indexOf(sName) >= 0) continue;
if (fDebug) console.log(sFile);
try {
/*
* We COULD load ("require") all the files on-demand, because it's only the
* browser initialization sequence we want to mimic in loadMachine(), but this
* is simpler, and it also gives us direct references to certain components
* we'll want to access later (eg, "component" in getComponentByType()).
*/
var fn = require(lib + "../../../" + sFile);
var sSuperClass = null;
for (var s in aSubClasses) {
if (sFile.indexOf(s) >= 0) {
sSuperClass = aSubClasses[s];
break;
}
}
if (sSuperClass) {
for (var j = 0; j < aComponents.length; j++) {
if (aComponents[j].path.indexOf(sSuperClass) >= 0) {
if (fDebug) console.log("updating superclass " + aComponents[j].path + " with subclass " + sFile);
aComponents[j].Create = fn;
sName = null;
break;
}
}
}
if (sName == "component") {
fn.log = fn.println = function(s, type) {
console.log((type !== undefined? (type + ": ") : "") + (s || ""));
}; // jshint ignore:line
}
if (sName) {
aComponents.push({name: sName, path: sFile, Create: fn, objects: []});
}
if (sName == "defines") {
/*
* Enabling component console messages requires setting CONSOLE to true.
*/
if (global.DEBUG !== undefined) {
global.DEBUG = fDebug;
global.CONSOLE = fConsole;
}
}
} catch(err) {
console.log(err.message);
}
}
}
/**
* getComponentByName(sName)
*
* @param sName
* @return {*}
*/
function getComponentByName(sName)
{
for (var i = 0; i < aComponents.length; i++) {
if (aComponents[i].name == sName) {
return aComponents[i].Create;
}
}
return null;
}
/**
* getComponentByType(sType)
*
* @param sType
* @return {*}
*/
function getComponentByType(sType)
{
var component = null;
if (!Component) {
Component = getComponentByName("component");
}
if (Component) {
component = Component.getComponentByType(sType);
}
return component;
}
/**
* loadMachine(sFile)
*
* @param {string} sFile
* @return {Object} representing the machine whose component objects have been loaded into aComponents
*/
function loadMachine(sFile)
{
if (fDebug) console.log('loadMachine("' + sFile + '")');
/*
* Clear any/all saved objects from any previous machine
*/
var i, j;
Component = dbg = null;
for (i = 0; i < aComponents.length; i++) {
aComponents[i].objects = [];
}
var machine;
try {
/*
* Since our JSON files may contain comments, hex values, and/or other tokens deemed
* unacceptable by the JSON Overlords, we can't use require() to load it, as we're able to
* do with "package.json". Also note that require() assumes the same path as that of the
* requiring file, whereas fs.readFileSync() assumes the path reported by process.cwd().
*
* TODO: I've since removed the comments from my sample "ibm5150.json" file, so we could
* try to reinstate this code; however, there are still hex constants, which I find *much*
* preferable to the decimal equivalents. JSON's restrictions continue to irritate me.
*
* var machine = require(lib + "../bin/" +sFile);
*/
var sMachine = fs.readFileSync(sFile, {encoding: "utf8"});
sMachine = '(' + sMachine + ')';
if (fDebug) console.log(sMachine);
machine = eval(sMachine); // jshint ignore:line
if (machine) {
/*
* Since we have a machine object, we now mimic the initialization sequence that occurs
* in the browser, by walking the list of PCx86 components we loaded above and looking for
* matches.
*/
var idMachine = "";
/*
* 'machine' is a pseudo-component that is only used to define an ID for the entire machine;
* if it exists, then that ID is prepended to every component ID, just as our XSLT code would
* do for a machine XML file. This relieves the JSON file from having to manually prepend
* a machine ID to every component ID itself.
*
* This doesn't mean I anticipate a Node environment running multiple machines, as we do in
* a browser; it only means that I'm trying to make both environments operate similarly.
*/
if (machine['machine']) {
idMachine = machine['machine']['id'];
}
for (i = 0; i < aComponents.length; i++) {
var component = aComponents[i];
var parms = machine[component.name];
/*
* If parms is undefined, it means there is no component with that name defined in the
* machine object (NOT that the component has no parms), and therefore we should skip it.
*/
if (parms === undefined) continue;
/*
* If parms is an Array, then we must create an object for each parms element; and yes,
* I'm relying on the fact that none of my parm objects use a "length" property, as a quick
* and dirty way of differentiating objects from arrays.
*/
var aParms = parms.length !== undefined? parms : [parms];
for (j = 0; j < aParms.length; j++) {
var obj;
var parmsObj = aParms[j];
if (idMachine) parmsObj['id'] = idMachine + '.' + parmsObj['id'];
if (fDebug) {
console.log("creating " + component.name + "...");
console.log(parmsObj);
}
if (component.name == "cpu") {
parmsObj['autoStart'] = false;
}
try {
obj = new component.Create(parmsObj);
} catch(err) {
console.log("error creating " + component.name + ": " + err.message);
continue;
}
console.log(obj['id'] + " object created");
component.objects.push(obj);
if (obj.type == "Debugger") {
dbg = obj;
}
}
}
/*
* Return the original machine object only in DEBUG mode
*/
if (!fDebug) machine = true;
}
} catch(err) {
console.log(err.message);
}
return machine;
}
/**
* doCommand(sCmd)
*
* @param {string} sCmd
* @return {*}
*/
function doCommand(sCmd)
{
if (!sCmd) {
sCmd = sCmdPrev;
} else {
sCmdPrev = sCmd;
}
var result = false;
var aTokens = sCmd.split(' ');
switch(aTokens[0]) {
case "cwd":
result = process.cwd();
break;
case "load":
result = loadMachine(aTokens[1]);
break;
case "quit":
process.exit();
result = true;
break;
default:
if (sCmd) {
try {
if (dbg && !dbg.doCommands(sCmd, true)) {
sCmd = '(' + sCmd + ')';
result = eval(sCmd); // jshint ignore:line
}
} catch(err) {
console.log(err.message);
}
}
break;
}
return result;
}
/**
* onCommand(cmd, context, filename, callback)
*
* The Node docs (http://nodejs.org/api/repl.html) say that repl.start's "eval" option is:
*
* a function that will be used to eval each given line; defaults to an async wrapper for eval()
*
* and it gives this example of such a function:
*
* function eval(cmd, context, filename, callback) {
* callback(null, result);
* }
*
* but it defines NEITHER the parameters for the function NOR the parameters for the callback().
*
* It's pretty clear that "result" is expected to return whatever "eval()" would return for the expression
* in "cmd" (which is always parenthesized in preparation for a call to "eval()"), but it's not clear what
* the first callback() parameter (represented by null) is supposed to be. Should we assume it's an Error
* object, in case we want to report an error?
*
* @param {string} cmd
* @param {Object} context
* @param {string} filename
* @param {function(Object|null, Object)} callback
*/
var onCommand = function (cmd, context, filename, callback)
{
var result = false;
/*
* WARNING: After updating from Node v0.10.x to v0.11.x, the incoming expression in "cmd" is no longer
* parenthesized, so I had to tweak the RegExp below. But... WTF. Do we not care what we break, folks?
*/
var match = cmd.match(/^\(?\s*(.*?)\s*\)?$/);
if (match) result = doCommand(match[1]);
callback(null, result);
};
if (pkg) {
loadComponents(pkg.pcX86Files);
}
/*
* Before falling into the REPL, process any command-line (--cmd) commands -- which should eventually include batch files.
*/
if (argv['cmd'] !== undefined) {
var cmds = argv['cmd'];
var aCmds = (typeof cmds == "string"? [cmds] : cmds);
for (var i = 0; i < aCmds.length; i++) {
doCommand(aCmds[i]);
}
sCmdPrev = "";
}
repl.start({
prompt: "PCx86> ",
input: process.stdin,
output: process.stdout,
eval: onCommand
});