minimaws: add machine feature status flags and slot card selection with live update

This commit is contained in:
Vas Crabb 2017-08-03 23:38:22 +10:00
parent 224cfaeb6a
commit 8eb07ffe97
8 changed files with 585 additions and 22 deletions

View File

@ -25,30 +25,30 @@ function make_table_sortable(tbl)
(function (col) (function (col)
{ {
var dir = 1; var dir = 1;
var sorticon = document.createElement("img"); var sorticon = document.createElement('img');
sorticon.setAttribute("src", assetsurl + "/sortind.png"); sorticon.setAttribute('src', assetsurl + '/sortind.png');
sorticon.style.cssFloat = "right"; sorticon.style.cssFloat = 'right';
sorticon.style.marginLeft = "0.5em"; sorticon.style.marginLeft = '0.5em';
headers[col].appendChild(sorticon); headers[col].appendChild(sorticon);
headers[col].addEventListener( headers[col].addEventListener(
'click', 'click',
function () function ()
{ {
imgsrc = sorticon.getAttribute("src"); imgsrc = sorticon.getAttribute('src');
imgsrc = imgsrc.substr(imgsrc.lastIndexOf('/') + 1); imgsrc = imgsrc.substr(imgsrc.lastIndexOf('/') + 1);
if (imgsrc != 'sortind.png') if (imgsrc != 'sortind.png')
dir = -dir; dir = -dir;
if (dir < 0) if (dir < 0)
sorticon.setAttribute("src", assetsurl + "/sortdesc.png"); sorticon.setAttribute('src', assetsurl + '/sortdesc.png');
else else
sorticon.setAttribute("src", assetsurl + "/sortasc.png"); sorticon.setAttribute('src', assetsurl + '/sortasc.png');
var i; var i;
for (i = 0; i < headers.length; i++) for (i = 0; i < headers.length; i++)
{ {
if (i != col) 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)); }(i));
} }

View File

@ -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);
}

View File

@ -6,3 +6,6 @@ th { text-align: left; background-color: #ddd; padding: 0.25em }
td { padding-left: 0.25em; padding-right: 0.25em } td { padding-left: 0.25em; padding-right: 0.25em }
table[class=sysinfo] th { text-align: right } 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 }

View File

@ -4,6 +4,10 @@
## copyright-holders:Vas Crabb ## copyright-holders:Vas Crabb
import sqlite3 import sqlite3
import sys
if sys.version_info >= (3, 4):
import urllib.request
class SchemaQueries(object): class SchemaQueries(object):
@ -170,6 +174,9 @@ class QueryCursor(object):
'ORDER BY shortname ASC', 'ORDER BY shortname ASC',
patterns) 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): def get_machine_info(self, machine):
return self.dbcurs.execute( 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 ' \ '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: else:
return self.dbcurs.execute('SELECT COUNT(*) FROM sourcefile').fetchone()[0] 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): class UpdateCursor(object):
def __init__(self, dbconn, **kwargs): def __init__(self, dbconn, **kwargs):
@ -286,9 +318,11 @@ class UpdateCursor(object):
class QueryConnection(object): class QueryConnection(object):
def __init__(self, database, **kwargs): def __init__(self, database, **kwargs):
# TODO: detect python versions that allow URL-based read-only connection
super(QueryConnection, self).__init__(**kwargs) 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.row_factory = sqlite3.Row
self.dbconn.execute('PRAGMA foreign_keys = ON') self.dbconn.execute('PRAGMA foreign_keys = ON')

View File

@ -27,8 +27,12 @@ MACHINE_PROLOGUE = string.Template(
' <meta http-equiv="Content-Style-Type" content="text/css">\n' \ ' <meta http-equiv="Content-Style-Type" content="text/css">\n' \
' <meta http-equiv="Content-Script-Type" content="text/javascript">\n' \ ' <meta http-equiv="Content-Script-Type" content="text/javascript">\n' \
' <link rel="stylesheet" type="text/css" href="${assets}/style.css">\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}/common.js"></script>\n' \
' <script type="text/javascript" src="${assets}/machine.js"></script>\n' \
' <title>Machine: ${description} (${shortname})</title>\n' \ ' <title>Machine: ${description} (${shortname})</title>\n' \
'</head>\n' \ '</head>\n' \
'<body>\n' \ '<body>\n' \
@ -39,6 +43,13 @@ MACHINE_PROLOGUE = string.Template(
' <tr><th>Runnable:</th><td>${runnable}</td></tr>\n' \ ' <tr><th>Runnable:</th><td>${runnable}</td></tr>\n' \
' <tr><th>Source file:</th><td><a href="${sourcehref}">${sourcefile}</a></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( MACHINE_ROW = string.Template(
' <tr>\n' \ ' <tr>\n' \
' <td><a href="${machinehref}">${shortname}</a></td>\n' \ ' <td><a href="${machinehref}">${shortname}</a></td>\n' \

View File

@ -179,7 +179,7 @@ class MachineHandler(ElementHandler):
else: else:
if name == 'device_ref': if name == 'device_ref':
self.dbcurs.add_devicereference(self.id, attrs['name']) self.dbcurs.add_devicereference(self.id, attrs['name'])
elif name == 'feaure': elif name == 'feature':
self.dbcurs.add_featuretype(attrs['type']) self.dbcurs.add_featuretype(attrs['type'])
status = 0 if 'status' not in attrs else 2 if attrs['status'] == 'unemulated' else 1 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 overall = status if 'overall' not in attrs else 2 if attrs['overall'] == 'unemulated' else 1

View File

@ -8,13 +8,15 @@ from . import htmltmpl
import cgi import cgi
import inspect import inspect
import json
import mimetypes import mimetypes
import os.path import os.path
import re
import sys import sys
import wsgiref.simple_server import wsgiref.simple_server
import wsgiref.util import wsgiref.util
if sys.version_info > (3, ): if sys.version_info >= (3, ):
import urllib.parse as urlparse import urllib.parse as urlparse
else: else:
import urlparse import urlparse
@ -37,6 +39,7 @@ class HandlerBase(object):
def __init__(self, app, application_uri, environ, start_response, **kwargs): def __init__(self, app, application_uri, environ, start_response, **kwargs):
super(HandlerBase, self).__init__(**kwargs) super(HandlerBase, self).__init__(**kwargs)
self.app = app self.app = app
self.js_escape = app.js_escape
self.application_uri = application_uri self.application_uri = application_uri
self.environ = environ self.environ = environ
self.start_response = start_response self.start_response = start_response
@ -99,6 +102,31 @@ class QueryPageHandler(HandlerBase):
return cgi.escape(urlparse.urljoin(self.application_uri, 'sourcefile/%s' % (sourcefile, )), True) 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): class MachineHandler(QueryPageHandler):
def __init__(self, app, application_uri, environ, start_response, **kwargs): 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) 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')]) 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) return self.error_page(403)
elif self.environ['PATH_INFO']: 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')]) 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) return self.error_page(404)
else: else:
@ -129,7 +156,8 @@ class MachineHandler(QueryPageHandler):
id = machine_info['id'] id = machine_info['id']
description = machine_info['description'] description = machine_info['description']
yield htmltmpl.MACHINE_PROLOGUE.substitute( 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']), sourcehref=self.sourcefile_href(machine_info['sourcefile']),
description=cgi.escape(description), description=cgi.escape(description),
shortname=cgi.escape(self.shortname), shortname=cgi.escape(self.shortname),
@ -146,23 +174,43 @@ class MachineHandler(QueryPageHandler):
if parent: if parent:
yield ( yield (
' <tr><th>Parent Machine:</th><td><a href="%s">%s (%s)</a></td></tr>\n' % ' <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: else:
yield ( yield (
' <tr><th>Parent Machine:</th><td><a href="%s">%s</a></td></tr>\n' % ' <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']): if (machine_info['romof'] is not None) and (machine_info['romof'] != machine_info['cloneof']):
parent = self.dbcurs.listfull(machine_info['romof']).fetchone() parent = self.dbcurs.listfull(machine_info['romof']).fetchone()
if parent: if parent:
yield ( yield (
' <tr><th>Parent ROM set:</th><td><a href="%s">%s (%s)</a></td></tr>\n' % ' <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: else:
yield ( yield (
' <tr><th>Parent Machine:</th><td><a href="%s">%s</a></td></tr>\n' % ' <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') 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 first = True
for name, desc, src in self.dbcurs.get_devices_referenced(id): for name, desc, src in self.dbcurs.get_devices_referenced(id):
if first: if first:
@ -317,7 +365,57 @@ class SourceFileHandler(QueryPageHandler):
parent=cgi.escape(machine_info['cloneof'] or '')).encode('utf-8') 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): class MiniMawsApp(object):
JS_ESCAPE = re.compile('([\"\'\\\\])')
RPC_SERVICES = {
'flags': FlagsRpcHandler,
'slots': SlotsRpcHandler }
def __init__(self, dbfile, **kwargs): def __init__(self, dbfile, **kwargs):
super(MiniMawsApp, self).__init__(**kwargs) super(MiniMawsApp, self).__init__(**kwargs)
self.dbconn = dbaccess.QueryConnection(dbfile) self.dbconn = dbaccess.QueryConnection(dbfile)
@ -334,11 +432,22 @@ class MiniMawsApp(object):
return SourceFileHandler(self, application_uri, environ, start_response) return SourceFileHandler(self, application_uri, environ, start_response)
elif module == 'static': elif module == 'static':
return AssetHandler(self.assetsdir, self, application_uri, environ, start_response) 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: elif not module:
return ErrorPageHandler(403, self, application_uri, environ, start_response) return ErrorPageHandler(403, self, application_uri, environ, start_response)
else: else:
return ErrorPageHandler(404, self, application_uri, environ, start_response) 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): def run_server(options):
application = MiniMawsApp(options.database) application = MiniMawsApp(options.database)

View File

@ -2,6 +2,78 @@
## ##
## license:BSD-3-Clause ## license:BSD-3-Clause
## copyright-holders:Vas Crabb ## 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.auxverbs
import lib.lxparse import lib.lxparse
@ -55,5 +127,3 @@ if __name__ == '__main__':
lib.wsgiserve.run_server(options) lib.wsgiserve.run_server(options)
elif options.command == 'load': elif options.command == 'load':
lib.lxparse.load_info(options) lib.lxparse.load_info(options)
else:
print('%s' % (options, ))