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

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 }
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
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')

View File

@ -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' \

View File

@ -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

View File

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

View File

@ -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, ))