summaryrefslogtreecommitdiffstatshomepage
path: root/scripts/minimaws
diff options
context:
space:
mode:
author Vas Crabb <vas@vastheman.com>2017-08-03 23:38:22 +1000
committer Vas Crabb <vas@vastheman.com>2017-08-03 23:38:22 +1000
commit8eb07ffe97cec831711b9bbd2c5bdc97a14a5b52 (patch)
tree2b7f3e229952b2aaacaf9bdc85185604cf3584b3 /scripts/minimaws
parent224cfaeb6a3ceac946a0bf6321a822e161c494c6 (diff)
minimaws: add machine feature status flags and slot card selection with live update
Diffstat (limited to 'scripts/minimaws')
-rw-r--r--scripts/minimaws/lib/assets/common.js18
-rw-r--r--scripts/minimaws/lib/assets/machine.js336
-rw-r--r--scripts/minimaws/lib/assets/style.css3
-rw-r--r--scripts/minimaws/lib/dbaccess.py38
-rw-r--r--scripts/minimaws/lib/htmltmpl.py13
-rw-r--r--scripts/minimaws/lib/lxparse.py2
-rw-r--r--scripts/minimaws/lib/wsgiserve.py123
-rwxr-xr-xscripts/minimaws/minimaws.py74
8 files changed, 585 insertions, 22 deletions
diff --git a/scripts/minimaws/lib/assets/common.js b/scripts/minimaws/lib/assets/common.js
index 8ab57941127..2af47c9ed2c 100644
--- a/scripts/minimaws/lib/assets/common.js
+++ b/scripts/minimaws/lib/assets/common.js
@@ -25,30 +25,30 @@ function make_table_sortable(tbl)
(function (col)
{
var dir = 1;
- var sorticon = document.createElement("img");
- sorticon.setAttribute("src", assetsurl + "/sortind.png");
- sorticon.style.cssFloat = "right";
- sorticon.style.marginLeft = "0.5em";
+ var sorticon = document.createElement('img');
+ sorticon.setAttribute('src', assetsurl + '/sortind.png');
+ sorticon.style.cssFloat = 'right';
+ sorticon.style.marginLeft = '0.5em';
headers[col].appendChild(sorticon);
headers[col].addEventListener(
'click',
function ()
{
- imgsrc = sorticon.getAttribute("src");
+ imgsrc = sorticon.getAttribute('src');
imgsrc = imgsrc.substr(imgsrc.lastIndexOf('/') + 1);
if (imgsrc != 'sortind.png')
dir = -dir;
if (dir < 0)
- sorticon.setAttribute("src", assetsurl + "/sortdesc.png");
+ sorticon.setAttribute('src', assetsurl + '/sortdesc.png');
else
- sorticon.setAttribute("src", assetsurl + "/sortasc.png");
+ sorticon.setAttribute('src', assetsurl + '/sortasc.png');
var i;
for (i = 0; i < headers.length; i++)
{
if (i != col)
- headers[i].lastChild.setAttribute("src", assetsurl + "/sortind.png");
+ headers[i].lastChild.setAttribute('src', assetsurl + '/sortind.png');
}
- sort_table(tbl, col, dir, headers[col].getAttribute("class") == "numeric");
+ sort_table(tbl, col, dir, headers[col].getAttribute('class') == 'numeric');
});
}(i));
}
diff --git a/scripts/minimaws/lib/assets/machine.js b/scripts/minimaws/lib/assets/machine.js
new file mode 100644
index 00000000000..8a398d4507a
--- /dev/null
+++ b/scripts/minimaws/lib/assets/machine.js
@@ -0,0 +1,336 @@
+// license:BSD-3-Clause
+// copyright-holders:Vas Crabb
+
+var slot_info = Object.create(null);
+var machine_flags = Object.create(null);
+
+
+function update_cmd_preview()
+{
+ var result = '';
+ var first = true;
+ var slotslist = document.getElementById('list-slot-options');
+ if (slotslist)
+ {
+ for (var item = slotslist.firstChild; item; item = item.nextSibling)
+ {
+ if (item.nodeName == 'DT')
+ {
+ var selection = item.lastChild.selectedOptions[0];
+ if (selection.getAttribute('data-isdefault') != 'yes')
+ {
+ if (first)
+ first = false;
+ else
+ result += ' ';
+ var card = selection.value;
+ if (card == '')
+ card = '""';
+ result += '-' + item.getAttribute('data-slotname') + ' ' + card;
+ }
+ }
+ }
+ }
+ document.getElementById('para-cmd-preview').textContent = result;
+}
+
+
+var fetch_machine_flags = (function ()
+ {
+ var pending = Object.create(null);
+ return function (device)
+ {
+ if (!Object.prototype.hasOwnProperty.call(machine_flags, device) && !Object.prototype.hasOwnProperty.call(pending, device))
+ {
+ pending[device] = true;
+ var req = new XMLHttpRequest();
+ req.open('GET', appurl + 'rpc/flags/' + device, true);
+ req.responseType = 'json';
+ req.onload =
+ function ()
+ {
+ delete pending[device];
+ if (req.status == 200)
+ {
+ machine_flags[device] = req.response;
+ var slotslist = document.getElementById('list-slot-options');
+ if (slotslist)
+ {
+ for (var item = slotslist.firstChild; item; item = item.nextSibling)
+ {
+ if ((item.nodeName == 'DT') && (item.getAttribute('data-slotcard') == device))
+ add_flag_rows(item.nextSibling.firstChild, device);
+ }
+ }
+ }
+ };
+ req.send();
+ }
+ };
+ })();
+
+
+function add_flag_rows(table, device)
+{
+ var len, i, row, cell;
+
+ var sorted_features = Object.keys(machine_flags[device].features).sort();
+ var imperfect = [], unemulated = [];
+ len = sorted_features.length;
+ for (i = 0; i < len; i++)
+ ((machine_flags[device].features[sorted_features[i]].overall == 'unemulated') ? unemulated : imperfect).push(sorted_features[i]);
+
+ len = unemulated.length;
+ if (len > 0)
+ {
+ row = table.appendChild(document.createElement('tr'));
+ row.appendChild(document.createElement('th')).textContent = 'Unemulated features:';
+ cell = row.appendChild(document.createElement('td'));
+ cell.textContent = unemulated[0];
+ for (i = 1; i < len; i++)
+ cell.textContent += ', ' + unemulated[i];
+ }
+
+ len = imperfect.length;
+ if (len > 0)
+ {
+ row = table.appendChild(document.createElement('tr'));
+ row.appendChild(document.createElement('th')).textContent = 'Imperfect features:';
+ cell = row.appendChild(document.createElement('td'));
+ cell.textContent = imperfect[0];
+ for (i = 1; i < len; i++)
+ cell.textContent += ', ' + unemulated[i];
+ }
+}
+
+
+function make_slot_term(name, slot, defaults)
+{
+ var len, i;
+
+ var defcard = '';
+ len = defaults.length;
+ for (i = 0; (i < len) && (defcard == ''); i++)
+ {
+ if (Object.prototype.hasOwnProperty.call(defaults[i], name))
+ defcard = defaults[i][name];
+ }
+
+ var term = document.createElement('dt');
+ term.setAttribute('id', ('item-slot-choice-' + name).replace(/:/g, '-'));
+ term.setAttribute('data-slotname', name);
+ term.setAttribute('data-slotcard', '');
+ term.textContent = name + ': ';
+ var popup = document.createElement('select');
+ popup.setAttribute('id', ('select-slot-choice-' + name).replace(/:/g, '-'));
+ term.appendChild(popup);
+ var option = document.createElement('option');
+ option.setAttribute('value', '');
+ option.setAttribute('data-isdefault', ('' == defcard) ? 'yes' : 'no');
+ option.textContent = '-';
+ popup.appendChild(option);
+ var sorted_choices = Object.keys(slot).sort();
+ len = sorted_choices.length;
+ for (i = 0; i < len; i++)
+ {
+ var choice = sorted_choices[i];
+ var card = slot[choice];
+ option = document.createElement('option');
+ option.setAttribute('value', choice);
+ option.setAttribute('data-isdefault', (choice == defcard) ? 'yes' : 'no');
+ option.textContent = choice + ' - ' + card.description;
+ popup.appendChild(option);
+ }
+ popup.selectedIndex = 0;
+ popup.onchange = make_slot_change_handler(name, slot, defaults);
+ return term;
+}
+
+
+function add_slot_items(root, device, defaults, slotslist, pos)
+{
+ var defvals = Object.create(null);
+ for (var key in slot_info[device].defaults)
+ defvals[root + key] = slot_info[device].defaults[key];
+ defaults = defaults.slice();
+ defaults.push(defvals);
+ var defcnt = defaults.length;
+
+ var slots = slot_info[device].slots;
+ var sorted_slots = Object.keys(slots).sort();
+ var len = sorted_slots.length;
+ for (var i = 0; i < len; i++)
+ {
+ var slotname = sorted_slots[i];
+ var slotabs = root + slotname;
+ var slot = slots[slotname];
+ var term = make_slot_term(slotabs, slot, defaults);
+ var def = document.createElement('dd');
+ def.setAttribute('id', ('item-slot-detail-' + slotabs).replace(/:/g, '-'));
+ if (pos)
+ {
+ slotslist.insertBefore(term, pos);
+ slotslist.insertBefore(def, pos);
+ }
+ else
+ {
+ slotslist.appendChild(term);
+ slotslist.appendChild(def);
+ }
+
+ for (var j = 0; j < defcnt; j++)
+ {
+ if (Object.prototype.hasOwnProperty.call(defaults[j], slotabs))
+ {
+ var card = defaults[j][slotabs];
+ var sel = term.lastChild;
+ var found = false;
+ var choice;
+ for (choice in sel.options)
+ {
+ if (sel.options[choice].value == card)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (found)
+ {
+ sel.selectedIndex = choice;
+ sel.dispatchEvent(new Event('change'));
+ break;
+ }
+ }
+ }
+ }
+
+ update_cmd_preview();
+}
+
+
+function make_slot_change_handler(name, slot, defaults)
+{
+ var selection = null;
+ return function (event)
+ {
+ var choice = event.target.value;
+ var slotslist = event.target.parentNode.parentNode;
+ var def = event.target.parentNode.nextSibling;
+ var slotname = event.target.parentNode.getAttribute('data-slotname');
+ selection = (choice == '') ? null : slot[choice];
+
+ var prefix = slotname + ':';
+ for (var candidate = def.nextSibling; candidate && candidate.getAttribute('data-slotname').startsWith(prefix); )
+ {
+ var next = candidate.nextSibling;
+ slotslist.removeChild(candidate);
+ candidate = next.nextSibling;
+ slotslist.removeChild(next);
+ }
+
+ if (selection === null)
+ {
+ event.target.parentNode.setAttribute('data-slotcard', '');
+ if (def.firstChild)
+ def.removeChild(def.firstChild);
+ }
+ else
+ {
+ event.target.parentNode.setAttribute('data-slotcard', selection.device);
+ var tbl = document.createElement('table');
+ tbl.setAttribute('class', 'sysinfo');
+
+ var row = tbl.appendChild(document.createElement('tr'));
+ row.appendChild(document.createElement('th')).textContent = 'Short name:';
+ var link = row.appendChild(document.createElement('td')).appendChild(document.createElement('a'));
+ link.textContent = selection.device;
+ link.setAttribute('href', appurl + 'machine/' + selection.device);
+
+ if (!Object.prototype.hasOwnProperty.call(machine_flags, selection.device))
+ fetch_machine_flags(selection.device);
+ else
+ add_flag_rows(tbl, selection.device);
+
+ if (def.firstChild)
+ def.replaceChild(tbl, def.firstChild);
+ else
+ def.appendChild(tbl);
+
+ add_slot_items(slotname + ':' + choice, selection.device, defaults, slotslist, def.nextSibling);
+ }
+ update_cmd_preview();
+ };
+}
+
+
+function populate_slots(machine)
+{
+ var placeholder = document.getElementById('para-slots-placeholder');
+ var slotslist = document.createElement('dl');
+ slotslist.setAttribute('id', 'list-slot-options');
+ placeholder.parentNode.replaceChild(slotslist, placeholder);
+ add_slot_items('', machine, [], slotslist, null);
+}
+
+
+function slot_retrieve_error(device)
+{
+ var errors;
+ var placeholder = document.getElementById('para-slots-placeholder');
+ if (placeholder)
+ {
+ errors = document.createElement('div');
+ errors.setAttribute('id', 'div-slots-errors');
+ placeholder.parentNode.replaceChild(errors, placeholder);
+ }
+ else
+ {
+ errors = document.getElementById('div-slots-errors');
+ }
+ var message = document.createElement('p');
+ message.textContent = 'Error retrieving slot information for ' + device + '.';
+ errors.appendChild(message);
+}
+
+
+function fetch_slots(machine)
+{
+ function make_request(device)
+ {
+ var req = new XMLHttpRequest();
+ req.open('GET', appurl + 'rpc/slots/' + device, true);
+ req.responseType = 'json';
+ req.onload =
+ function ()
+ {
+ if (req.status == 200)
+ {
+ slot_info[device] = req.response;
+ delete pending[device];
+ for (var slotname in req.response.slots)
+ {
+ var slot = req.response.slots[slotname];
+ for (var choice in slot)
+ {
+ var card = slot[choice].device
+ if (!Object.prototype.hasOwnProperty.call(slot_info, card) && !Object.prototype.hasOwnProperty.call(pending, card))
+ {
+ pending[card] = true;
+ make_request(card);
+ }
+ }
+ }
+ if (!Object.keys(pending).length)
+ populate_slots(machine);
+ }
+ else
+ {
+ slot_retrieve_error(device);
+ }
+ };
+ req.send();
+ }
+ var pending = Object.create(null);
+ pending[machine] = true;
+ make_request(machine);
+}
diff --git a/scripts/minimaws/lib/assets/style.css b/scripts/minimaws/lib/assets/style.css
index 7e9e0125cd2..75ea0fa93de 100644
--- a/scripts/minimaws/lib/assets/style.css
+++ b/scripts/minimaws/lib/assets/style.css
@@ -6,3 +6,6 @@ th { text-align: left; background-color: #ddd; padding: 0.25em }
td { padding-left: 0.25em; padding-right: 0.25em }
table[class=sysinfo] th { text-align: right }
+
+dl[id=list-slot-options] dt { font-weight: bold; margin-top: 1em }
+dl[id=list-slot-options] dd table { margin-top: 0.5em; margin-bottom: 1em }
diff --git a/scripts/minimaws/lib/dbaccess.py b/scripts/minimaws/lib/dbaccess.py
index 9aaa57473b9..b1cc48b87b8 100644
--- a/scripts/minimaws/lib/dbaccess.py
+++ b/scripts/minimaws/lib/dbaccess.py
@@ -4,6 +4,10 @@
## copyright-holders:Vas Crabb
import sqlite3
+import sys
+
+if sys.version_info >= (3, 4):
+ import urllib.request
class SchemaQueries(object):
@@ -170,6 +174,9 @@ class QueryCursor(object):
'ORDER BY shortname ASC',
patterns)
+ def get_machine_id(self, machine):
+ return (self.dbcurs.execute('SELECT id FROM machine WHERE shortname = ?', (machine, )).fetchone() or (None, ))[0]
+
def get_machine_info(self, machine):
return self.dbcurs.execute(
'SELECT machine.id AS id, machine.description AS description, machine.isdevice AS isdevice, machine.runnable AS runnable, sourcefile.filename AS sourcefile, system.year AS year, system.manufacturer AS manufacturer, cloneof.parent AS cloneof, romof.parent AS romof ' \
@@ -221,6 +228,31 @@ class QueryCursor(object):
else:
return self.dbcurs.execute('SELECT COUNT(*) FROM sourcefile').fetchone()[0]
+ def count_slots(self, machine):
+ return self.dbcurs.execute(
+ 'SELECT COUNT(*) FROM slot WHERE machine = ?', (machine, )).fetchone()[0]
+
+ def get_feature_flags(self, machine):
+ return self.dbcurs.execute(
+ 'SELECT featuretype.name AS featuretype, feature.status AS status, feature.overall AS overall ' \
+ 'FROM feature JOIN featuretype ON feature.featuretype = featuretype.id ' \
+ 'WHERE feature.machine = ?',
+ (machine, ))
+
+ def get_slot_defaults(self, machine):
+ return self.dbcurs.execute(
+ 'SELECT slot.name AS name, slotoption.name AS option ' \
+ 'FROM slot JOIN slotdefault ON slot.id = slotdefault.id JOIN slotoption ON slotdefault.slotoption = slotoption.id ' \
+ 'WHERE slot.machine = ?',
+ (machine, ))
+
+ def get_slot_options(self, machine):
+ return self.dbcurs.execute(
+ 'SELECT slot.name AS slot, slotoption.name AS option, machine.shortname AS shortname, machine.description AS description ' \
+ 'FROM slot JOIN slotoption ON slot.id = slotoption.slot JOIN machine ON slotoption.device = machine.id ' \
+ 'WHERE slot.machine = ?',
+ (machine, ))
+
class UpdateCursor(object):
def __init__(self, dbconn, **kwargs):
@@ -286,9 +318,11 @@ class UpdateCursor(object):
class QueryConnection(object):
def __init__(self, database, **kwargs):
- # TODO: detect python versions that allow URL-based read-only connection
super(QueryConnection, self).__init__(**kwargs)
- self.dbconn = sqlite3.connect(database)
+ if sys.version_info >= (3, 4):
+ self.dbconn = sqlite3.connect('file:' + urllib.request.pathname2url(database) + '?mode=ro', uri=True)
+ else:
+ self.dbconn = sqlite3.connect(database)
self.dbconn.row_factory = sqlite3.Row
self.dbconn.execute('PRAGMA foreign_keys = ON')
diff --git a/scripts/minimaws/lib/htmltmpl.py b/scripts/minimaws/lib/htmltmpl.py
index 106f9615e9d..d465f0d993d 100644
--- a/scripts/minimaws/lib/htmltmpl.py
+++ b/scripts/minimaws/lib/htmltmpl.py
@@ -27,8 +27,12 @@ MACHINE_PROLOGUE = string.Template(
' <meta http-equiv="Content-Style-Type" content="text/css">\n' \
' <meta http-equiv="Content-Script-Type" content="text/javascript">\n' \
' <link rel="stylesheet" type="text/css" href="${assets}/style.css">\n' \
- ' <script type="text/javascript">var assetsurl="${assets}"</script>\n' \
+ ' <script type="text/javascript">\n' \
+ ' var appurl="${app}"\n' \
+ ' var assetsurl="${assets}"\n' \
+ ' </script>\n' \
' <script type="text/javascript" src="${assets}/common.js"></script>\n' \
+ ' <script type="text/javascript" src="${assets}/machine.js"></script>\n' \
' <title>Machine: ${description} (${shortname})</title>\n' \
'</head>\n' \
'<body>\n' \
@@ -39,6 +43,13 @@ MACHINE_PROLOGUE = string.Template(
' <tr><th>Runnable:</th><td>${runnable}</td></tr>\n' \
' <tr><th>Source file:</th><td><a href="${sourcehref}">${sourcefile}</a></td></tr>\n')
+MACHINE_SLOTS_PLACEHOLDER = string.Template(
+ '<h2>Options</h2>\n' \
+ '<p id="para-cmd-preview"></p>\n' \
+ '<h3>Slots</h3>\n' \
+ '<p id="para-slots-placeholder">Loading slot information&hellip;<p>\n' \
+ '<script>fetch_slots("${machine}");</script>\n')
+
MACHINE_ROW = string.Template(
' <tr>\n' \
' <td><a href="${machinehref}">${shortname}</a></td>\n' \
diff --git a/scripts/minimaws/lib/lxparse.py b/scripts/minimaws/lib/lxparse.py
index cf194128708..aeab5c1266d 100644
--- a/scripts/minimaws/lib/lxparse.py
+++ b/scripts/minimaws/lib/lxparse.py
@@ -179,7 +179,7 @@ class MachineHandler(ElementHandler):
else:
if name == 'device_ref':
self.dbcurs.add_devicereference(self.id, attrs['name'])
- elif name == 'feaure':
+ elif name == 'feature':
self.dbcurs.add_featuretype(attrs['type'])
status = 0 if 'status' not in attrs else 2 if attrs['status'] == 'unemulated' else 1
overall = status if 'overall' not in attrs else 2 if attrs['overall'] == 'unemulated' else 1
diff --git a/scripts/minimaws/lib/wsgiserve.py b/scripts/minimaws/lib/wsgiserve.py
index 96d03de4cfe..fe96a60f4f4 100644
--- a/scripts/minimaws/lib/wsgiserve.py
+++ b/scripts/minimaws/lib/wsgiserve.py
@@ -8,13 +8,15 @@ from . import htmltmpl
import cgi
import inspect
+import json
import mimetypes
import os.path
+import re
import sys
import wsgiref.simple_server
import wsgiref.util
-if sys.version_info > (3, ):
+if sys.version_info >= (3, ):
import urllib.parse as urlparse
else:
import urlparse
@@ -37,6 +39,7 @@ class HandlerBase(object):
def __init__(self, app, application_uri, environ, start_response, **kwargs):
super(HandlerBase, self).__init__(**kwargs)
self.app = app
+ self.js_escape = app.js_escape
self.application_uri = application_uri
self.environ = environ
self.start_response = start_response
@@ -99,6 +102,31 @@ class QueryPageHandler(HandlerBase):
return cgi.escape(urlparse.urljoin(self.application_uri, 'sourcefile/%s' % (sourcefile, )), True)
+class MachineRpcHandlerBase(QueryPageHandler):
+ def __init__(self, app, application_uri, environ, start_response, **kwargs):
+ super(MachineRpcHandlerBase, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
+ self.shortname = wsgiref.util.shift_path_info(environ)
+
+ def __iter__(self):
+ if not self.shortname:
+ self.start_response('403 %s' % (self.STATUS_MESSAGE[403], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
+ return self.error_page(403)
+ elif self.environ['PATH_INFO']:
+ self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
+ return self.error_page(404)
+ else:
+ machine = self.dbcurs.get_machine_id(self.shortname)
+ if machine is None:
+ self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
+ return self.error_page(404)
+ elif self.environ['REQUEST_METHOD'] != 'GET':
+ self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS'), ('Cache-Control', 'public, max-age=3600')])
+ return self.error_page(405)
+ else:
+ self.start_response('200 OK', [('Content-type', 'application/json; chearset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
+ return self.data_page(machine)
+
+
class MachineHandler(QueryPageHandler):
def __init__(self, app, application_uri, environ, start_response, **kwargs):
super(MachineHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs)
@@ -110,7 +138,6 @@ class MachineHandler(QueryPageHandler):
self.start_response('403 %s' % (self.STATUS_MESSAGE[403], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
return self.error_page(403)
elif self.environ['PATH_INFO']:
- # subdirectory of a machine
self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8'), ('Cache-Control', 'public, max-age=3600')])
return self.error_page(404)
else:
@@ -129,7 +156,8 @@ class MachineHandler(QueryPageHandler):
id = machine_info['id']
description = machine_info['description']
yield htmltmpl.MACHINE_PROLOGUE.substitute(
- assets=cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True),
+ app=self.js_escape(cgi.escape(self.application_uri, True)),
+ assets=self.js_escape(cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True)),
sourcehref=self.sourcefile_href(machine_info['sourcefile']),
description=cgi.escape(description),
shortname=cgi.escape(self.shortname),
@@ -146,23 +174,43 @@ class MachineHandler(QueryPageHandler):
if parent:
yield (
' <tr><th>Parent Machine:</th><td><a href="%s">%s (%s)</a></td></tr>\n' %
- (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['cloneof']), True), cgi.escape(parent[1]), cgi.escape(machine_info['cloneof']))).encode('utf-8')
+ (cgi.escape('%smachine/%s' % (self.application_uri, machine_info['cloneof']), True), cgi.escape(parent[1]), cgi.escape(machine_info['cloneof']))).encode('utf-8')
else:
yield (
' <tr><th>Parent Machine:</th><td><a href="%s">%s</a></td></tr>\n' %
- (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['cloneof']), True), cgi.escape(machine_info['cloneof']))).encode('utf-8')
+ (cgi.escape('%smachine/%s' % (self.application_uri, machine_info['cloneof']), True), cgi.escape(machine_info['cloneof']))).encode('utf-8')
if (machine_info['romof'] is not None) and (machine_info['romof'] != machine_info['cloneof']):
parent = self.dbcurs.listfull(machine_info['romof']).fetchone()
if parent:
yield (
' <tr><th>Parent ROM set:</th><td><a href="%s">%s (%s)</a></td></tr>\n' %
- (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['romof']), True), cgi.escape(parent[1]), cgi.escape(machine_info['romof']))).encode('utf-8')
+ (cgi.escape('%smachine/%s' % (self.application_uri, machine_info['romof']), True), cgi.escape(parent[1]), cgi.escape(machine_info['romof']))).encode('utf-8')
else:
yield (
' <tr><th>Parent Machine:</th><td><a href="%s">%s</a></td></tr>\n' %
- (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['romof']), True), cgi.escape(machine_info['romof']))).encode('utf-8')
+ (cgi.escape('%smachine/%s' % (self.application_uri, machine_info['romof']), True), cgi.escape(machine_info['romof']))).encode('utf-8')
+ unemulated = []
+ imperfect = []
+ for feature, status, overall in self.dbcurs.get_feature_flags(id):
+ if overall == 1:
+ imperfect.append(feature)
+ elif overall > 1:
+ unemulated.append(feature)
+ if (unemulated):
+ unemulated.sort()
+ yield(
+ (' <tr><th>Unemulated Features:</th><td>%s' + (', %s' * (len(unemulated) - 1)) + '</td></tr>\n') %
+ tuple(unemulated)).encode('utf-8');
+ if (imperfect):
+ yield(
+ (' <tr><th>Imperfect Features:</th><td>%s' + (', %s' * (len(imperfect) - 1)) + '</td></tr>\n') %
+ tuple(imperfect)).encode('utf-8');
yield '</table>\n'.encode('utf-8')
+ if self.dbcurs.count_slots(id):
+ yield htmltmpl.MACHINE_SLOTS_PLACEHOLDER.substitute(
+ machine=self.js_escape(self.shortname)).encode('utf=8')
+
first = True
for name, desc, src in self.dbcurs.get_devices_referenced(id):
if first:
@@ -317,7 +365,57 @@ class SourceFileHandler(QueryPageHandler):
parent=cgi.escape(machine_info['cloneof'] or '')).encode('utf-8')
+class FlagsRpcHandler(MachineRpcHandlerBase):
+ def data_page(self, machine):
+ result = { 'features': { } }
+ for feature, status, overall in self.dbcurs.get_feature_flags(machine):
+ detail = { }
+ if status == 1:
+ detail['status'] = 'imperfect'
+ elif status > 1:
+ detail['status'] = 'unemulated'
+ if overall == 1:
+ detail['overall'] = 'imperfect'
+ elif overall > 1:
+ detail['overall'] = 'unemulated'
+ result['features'][feature] = detail
+ yield json.dumps(result).encode('utf-8')
+
+
+class SlotsRpcHandler(MachineRpcHandlerBase):
+ def data_page(self, machine):
+ result = { 'defaults': { }, 'slots': { } }
+
+ # get defaults and slot options
+ for slot, default in self.dbcurs.get_slot_defaults(machine):
+ result['defaults'][slot] = default
+ prev = None
+ for slot, option, shortname, description in self.dbcurs.get_slot_options(machine):
+ if slot != prev:
+ if slot in result['slots']:
+ options = result['slots'][slot]
+ else:
+ options = { }
+ result['slots'][slot] = options
+ prev = slot
+ options[option] = { 'device': shortname, 'description': description }
+
+ # remove slots that come from default cards in other slots
+ for slot in tuple(result['slots'].keys()):
+ slot += ':'
+ for candidate in tuple(result['slots'].keys()):
+ if candidate.startswith(slot):
+ del result['slots'][candidate]
+
+ yield json.dumps(result).encode('utf-8')
+
+
class MiniMawsApp(object):
+ JS_ESCAPE = re.compile('([\"\'\\\\])')
+ RPC_SERVICES = {
+ 'flags': FlagsRpcHandler,
+ 'slots': SlotsRpcHandler }
+
def __init__(self, dbfile, **kwargs):
super(MiniMawsApp, self).__init__(**kwargs)
self.dbconn = dbaccess.QueryConnection(dbfile)
@@ -334,11 +432,22 @@ class MiniMawsApp(object):
return SourceFileHandler(self, application_uri, environ, start_response)
elif module == 'static':
return AssetHandler(self.assetsdir, self, application_uri, environ, start_response)
+ elif module == 'rpc':
+ service = wsgiref.util.shift_path_info(environ)
+ if not service:
+ return ErrorPageHandler(403, self, application_uri, environ, start_response)
+ elif service in self.RPC_SERVICES:
+ return self.RPC_SERVICES[service](self, application_uri, environ, start_response)
+ else:
+ return ErrorPageHandler(404, self, application_uri, environ, start_response)
elif not module:
return ErrorPageHandler(403, self, application_uri, environ, start_response)
else:
return ErrorPageHandler(404, self, application_uri, environ, start_response)
+ def js_escape(self, str):
+ return self.JS_ESCAPE.sub('\\\\\\1', str).replace('\0', '\\0')
+
def run_server(options):
application = MiniMawsApp(options.database)
diff --git a/scripts/minimaws/minimaws.py b/scripts/minimaws/minimaws.py
index 12ddc865b56..6f9fc938123 100755
--- a/scripts/minimaws/minimaws.py
+++ b/scripts/minimaws/minimaws.py
@@ -2,6 +2,78 @@
##
## license:BSD-3-Clause
## copyright-holders:Vas Crabb
+##
+## Demonstrates use of MAME's XML system information output
+##
+## This script requires Python 2.7 or Python 3, and ## SQLite 3.6.19 at
+## the very least. Help is provided for all command-line options (use
+## -h or --help).
+##
+## Before you can use the scripts, you need to load MAME system
+## information into a database. This currently requires a few manual
+## steps, and you need to start with a completely clean database:
+##
+## $ rm minimaws.sqlite3
+## $ sqlite3 minimaws.sqlite3 < schema.sql
+## $ python minimaws.py load --executable path/to/mame
+##
+## (The script uses the name "minimaws.sqlite3" for the database by
+## default, but you can override this with the --database option.)
+##
+## After you've loaded the database, you can use query commands. Most
+## of the query commands behave similarly to MAME's auxiliary verbs but
+## case-sensitive and with better globbing (output not shown for
+## brevity):
+##
+## $ python minimaws.py listfull "unkch*"
+## $ python minimaws.py listclones "unkch*"
+## $ python minimaws.py listbrothers superx
+##
+## One more sophisticated query command is provided that MAME has no
+## equivalent for. The listaffected command shows all runnable machines
+## that reference devices defined in specified source files:
+##
+## $ python minimaws.py listaffected "src/devices/cpu/m6805/*" src/devices/cpu/mcs40/mcs40.cpp
+##
+## This script can also run a local web server allowing you to explore
+## systems, devices and source files:
+##
+## $ python minimaws.py serve
+##
+## The default TCP port is 8080 but if desired, this can be changed with
+## the --port option. The web service is implemented using WSGI, so it
+## can be run in a web server if desired (e.g. using Apache mod_wsgi).
+## It uses get queries and provides cacheable reponses, so it should
+## work behind a caching proxy (e.g. squid or nginx). Although the
+## service is written to avoid SQL injected and directory traversal
+## attacks, and it avoids common sources of security issues, it has not
+## been audited for vulnerabilities and is not recommended for use on
+## public web sites.
+##
+## To use the web service, you need to know the short name of a device/
+## system, or the name of a source file containing a system:
+##
+## http://localhost:8080/machine/intlc440
+## http://localhost:8080/machine/a2mouse
+## http://localhost:8080/sourcefile/src/devices/cpu/m68000/m68kcpu.cpp
+##
+## You can also start with a list of all source files containing machine
+## definitions, but this is quite a large page and may perform poorly:
+##
+## http://localhost:8080/sourcefile/
+##
+## One feature that may be of iterest to front-end authors or users of
+## computer emulation is the ability to show available slot options and
+## update live as changes are made. This can be seen in action on
+## computer systems:
+##
+## http://localhost:8080/machine/ibm5150
+## http://localhost:8080/machine/apple2e
+## http://localhost:8080/machine/ti82
+##
+## On any of these, and many other systems, you can select slot options
+## and see dependent slots update. Required command-line arguments to
+## produce the selected configuration are also displayed.
import lib.auxverbs
import lib.lxparse
@@ -55,5 +127,3 @@ if __name__ == '__main__':
lib.wsgiserve.run_server(options)
elif options.command == 'load':
lib.lxparse.load_info(options)
- else:
- print('%s' % (options, ))