mirror of
https://github.com/holub/mame
synced 2025-06-04 03:46:29 +03:00
minimaws: add machine feature status flags and slot card selection with live update
This commit is contained in:
parent
224cfaeb6a
commit
8eb07ffe97
@ -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));
|
||||
}
|
||||
|
336
scripts/minimaws/lib/assets/machine.js
Normal file
336
scripts/minimaws/lib/assets/machine.js
Normal 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);
|
||||
}
|
@ -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 }
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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…<p>\n' \
|
||||
'<script>fetch_slots("${machine}");</script>\n')
|
||||
|
||||
MACHINE_ROW = string.Template(
|
||||
' <tr>\n' \
|
||||
' <td><a href="${machinehref}">${shortname}</a></td>\n' \
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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, ))
|
||||
|
Loading…
Reference in New Issue
Block a user