From 8eb07ffe97cec831711b9bbd2c5bdc97a14a5b52 Mon Sep 17 00:00:00 2001 From: Vas Crabb Date: Thu, 3 Aug 2017 23:38:22 +1000 Subject: [PATCH] minimaws: add machine feature status flags and slot card selection with live update --- scripts/minimaws/lib/assets/common.js | 18 +- scripts/minimaws/lib/assets/machine.js | 336 +++++++++++++++++++++++++ scripts/minimaws/lib/assets/style.css | 3 + scripts/minimaws/lib/dbaccess.py | 38 ++- scripts/minimaws/lib/htmltmpl.py | 13 +- scripts/minimaws/lib/lxparse.py | 2 +- scripts/minimaws/lib/wsgiserve.py | 123 ++++++++- scripts/minimaws/minimaws.py | 74 +++++- 8 files changed, 585 insertions(+), 22 deletions(-) create mode 100644 scripts/minimaws/lib/assets/machine.js 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( ' \n' \ ' \n' \ ' \n' \ - ' \n' \ + ' \n' \ ' \n' \ + ' \n' \ ' Machine: ${description} (${shortname})\n' \ '\n' \ '\n' \ @@ -39,6 +43,13 @@ MACHINE_PROLOGUE = string.Template( ' Runnable:${runnable}\n' \ ' Source file:${sourcefile}\n') +MACHINE_SLOTS_PLACEHOLDER = string.Template( + '

Options

\n' \ + '

\n' \ + '

Slots

\n' \ + '

Loading slot information…

\n' \ + '\n') + MACHINE_ROW = string.Template( ' \n' \ ' ${shortname}\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 ( ' Parent Machine:%s (%s)\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 ( ' Parent Machine:%s\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 ( ' Parent ROM set:%s (%s)\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 ( ' Parent Machine:%s\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( + (' Unemulated Features:%s' + (', %s' * (len(unemulated) - 1)) + '\n') % + tuple(unemulated)).encode('utf-8'); + if (imperfect): + yield( + (' Imperfect Features:%s' + (', %s' * (len(imperfect) - 1)) + '\n') % + tuple(imperfect)).encode('utf-8'); yield '\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, ))