minimaws updates:

* reduce network operations when initially loading a machine page
* add rudimentary software lists to machine pages
This commit is contained in:
Vas Crabb 2019-12-19 02:07:58 +11:00
parent f9d27d2104
commit c10ef269c5
3 changed files with 185 additions and 54 deletions

View file

@ -771,6 +771,14 @@ class QueryCursor(object):
'ORDER BY ramoption.size',
(machine, ))
def get_machine_softwarelists(self, machine):
return self.dbcurs.execute(
'SELECT machinesoftwarelist.tag AS tag, machinesoftwareliststatustype.value AS status, softwarelist.shortname AS shortname, softwarelist.description AS description, COUNT(software.id) AS total, COUNT(CASE software.supported WHEN 0 THEN 1 ELSE NULL END) AS supported, COUNT(CASE software.supported WHEN 1 THEN 1 ELSE NULL END) AS partiallysupported, COUNT(CASE software.supported WHEN 2 THEN 1 ELSE NULL END) AS unsupported ' \
'FROM machinesoftwarelist LEFT JOIN machinesoftwareliststatustype ON machinesoftwarelist.status = machinesoftwareliststatustype.id LEFT JOIN softwarelist ON machinesoftwarelist.softwarelist = softwarelist.id LEFT JOIN software ON softwarelist.id = software.softwarelist ' \
'WHERE machinesoftwarelist.machine = ? ' \
'GROUP BY machinesoftwarelist.id',
(machine, ))
def get_softwarelist_id(self, shortname):
return (self.dbcurs.execute('SELECT id FROM softwarelist WHERE shortname = ?', (shortname, )).fetchone() or (None, ))[0]
@ -1075,6 +1083,8 @@ class UpdateConnection(object):
def prepare_for_load(self):
# here be dragons - this is a poor man's DROP ALL TABLES etc.
self.dbconn.execute('PRAGMA foreign_keys = OFF')
for query in self.dbconn.execute('SELECT \'DROP VIEW \' || name FROM sqlite_master WHERE type = \'view\'').fetchall():
self.dbconn.execute(query[0])
for query in self.dbconn.execute('SELECT \'DROP INDEX \' || name FROM sqlite_master WHERE type = \'index\' AND NOT name GLOB \'sqlite_autoindex_*\'').fetchall():
self.dbconn.execute(query[0])
for query in self.dbconn.execute('SELECT \'DROP TABLE \' || name FROM sqlite_master WHERE type = \'table\'').fetchall():

View file

@ -70,6 +70,45 @@ MACHINE_CLONES_ROW = string.Template(
' <td>${manufacturer}</td>\n' \
' </tr>\n')
MACHINE_SOFTWARELISTS_TABLE_PROLOGUE = string.Template(
'<h2 id="heading-softwarelists">Software Lists</h2>\n' \
'<table id="tbl-softwarelists">\n' \
' <thead>\n' \
' <tr>\n' \
' <th>Short name</th>\n' \
' <th>Description</th>\n' \
' <th>Status</th>\n' \
' <th class="numeric">Total</th>\n' \
' <th class="numeric">Supported</th>\n' \
' <th class="numeric">Partially supported</th>\n' \
' <th class="numeric">Unsupported</th>\n' \
' </tr>\n' \
' </thead>\n' \
' <tbody>\n')
MACHINE_SOFTWARELISTS_TABLE_ROW = string.Template(
' <tr>\n' \
' <td><a href="${href}">${shortname}</a></td>\n' \
' <td><a href="${href}">${description}</a></td>\n' \
' <td>${status}</td>\n' \
' <td style="text-align: right">${total}</td>\n' \
' <td style="text-align: right">${supported}</td>\n' \
' <td style="text-align: right">${partiallysupported}</td>\n' \
' <td style="text-align: right">${unsupported}</td>\n' \
' </tr>\n')
MACHINE_SOFTWARELISTS_TABLE_EPILOGUE = string.Template(
' </tbody>\n' \
'</table>\n' \
'<script>\n' \
' make_table_sortable(document.getElementById("tbl-softwarelists"));\n' \
' if (!document.getElementById("tbl-softwarelists").tBodies[0].rows.length)\n' \
' {\n' \
' document.getElementById("heading-softwarelists").style.display = "none";\n' \
' document.getElementById("tbl-softwarelists").style.display = "none";\n' \
' }\n' \
'</script>\n')
MACHINE_OPTIONS_HEADING = string.Template(
'<h2>Options</h2>\n' \
'<p>\n' \
@ -92,10 +131,14 @@ MACHINE_RAM_PROLOGUE = string.Template(
MACHINE_RAM_OPTION = string.Template(
' <option value="${name}" data-isdefault="${isdefault}">${name} (${size})</option>\n')
MACHINE_SLOTS_PLACEHOLDER = string.Template(
MACHINE_SLOTS_PLACEHOLDER_PROLOGUE = string.Template(
'<h3>Slots</h3>\n' \
'<p id="para-slots-placeholder">Loading slot information&hellip;<p>\n' \
'<script>fetch_slots("${machine}");</script>\n')
'<script>\n')
MACHINE_SLOTS_PLACEHOLDER_EPILOGUE = string.Template(
' populate_slots(${machine});\n'
'</script>\n')
MACHINE_ROW = string.Template(
' <tr>\n' \

View file

@ -109,6 +109,76 @@ class QueryPageHandler(HandlerBase):
def software_href(self, softwarelist, software):
return cgi.escape(urlparse.urljoin(self.application_uri, 'softwarelist/%s/%s' % (urlquote(softwarelist), urlquote(software))), True)
def bios_data(self, machine):
result = { }
for name, description, isdefault in self.dbcurs.get_biossets(machine):
result[name] = { 'description': description, 'isdefault': True if isdefault else False }
return result
def flags_data(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
return result
def slot_data(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]
return result
def softwarelist_data(self, machine):
result = { }
# get software lists referenced by machine
for softwarelist in self.dbcurs.get_machine_softwarelists(machine):
result[softwarelist['tag']] = {
'status': softwarelist['status'],
'shortname': softwarelist['shortname'],
'description': softwarelist['description'],
'total': softwarelist['total'],
'supported': softwarelist['supported'],
'partiallysupported': softwarelist['partiallysupported'],
'unsupported': softwarelist['unsupported'] }
# remove software lists that come from default cards in slots
for slot, default in self.dbcurs.get_slot_defaults(machine):
slot += ':'
for candidate in tuple(result.keys()):
if candidate.startswith(slot):
del result[candidate]
return result
class MachineRpcHandlerBase(QueryPageHandler):
def __init__(self, app, application_uri, environ, start_response, **kwargs):
@ -215,6 +285,7 @@ class MachineHandler(QueryPageHandler):
tuple(imperfect)).encode('utf-8');
yield '</table>\n'.encode('utf-8')
# make a table of clones
first = True
for clone, clonedescription, cloneyear, clonemanufacturer in self.dbcurs.get_clones(self.shortname):
if first:
@ -229,6 +300,21 @@ class MachineHandler(QueryPageHandler):
if not first:
yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-clones').encode('utf-8')
# make a table of software lists
yield htmltmpl.MACHINE_SOFTWARELISTS_TABLE_PROLOGUE.substitute().encode('utf-8')
for softwarelist in self.dbcurs.get_machine_softwarelists(id):
total = softwarelist['total']
yield htmltmpl.MACHINE_SOFTWARELISTS_TABLE_ROW.substitute(
href=self.softwarelist_href(softwarelist['shortname']),
shortname=cgi.escape(softwarelist['shortname']),
description=cgi.escape(softwarelist['description']),
status=cgi.escape(softwarelist['status']),
total=cgi.escape('%d' % (total, )),
supported=cgi.escape('%.1f%%' % (softwarelist['supported'] * 100.0 / (total or 1), )),
partiallysupported=cgi.escape('%.1f%%' % (softwarelist['partiallysupported'] * 100.0 / (total or 1), )),
unsupported=cgi.escape('%.1f%%' % (softwarelist['unsupported'] * 100.0 / (total or 1), ))).encode('utf-8')
yield htmltmpl.MACHINE_SOFTWARELISTS_TABLE_EPILOGUE.substitute().encode('utf-8')
# allow system BIOS selection
haveoptions = False
for name, desc, isdef in self.dbcurs.get_biossets(id):
@ -264,8 +350,28 @@ class MachineHandler(QueryPageHandler):
if not haveoptions:
haveoptions = True
yield htmltmpl.MACHINE_OPTIONS_HEADING.substitute().encode('utf-8')
yield htmltmpl.MACHINE_SLOTS_PLACEHOLDER.substitute(
machine=self.js_escape(self.shortname)).encode('utf=8')
yield htmltmpl.MACHINE_SLOTS_PLACEHOLDER_PROLOGUE.substitute().encode('utf=8')
pending = set((self.shortname, ))
added = set((self.shortname, ))
haveextra = set()
while pending:
requested = pending.pop()
slots = self.slot_data(self.dbcurs.get_machine_id(requested))
yield (' slot_info[%s] = %s;\n' % (self.sanitised_json(requested), self.sanitised_json(slots))).encode('utf-8')
for slotname, slot in slots['slots'].items():
for choice, card in slot.items():
carddev = card['device']
if carddev not in added:
pending.add(carddev)
added.add(carddev)
if (carddev not in haveextra) and (slots['defaults'].get(slotname) == choice):
haveextra.add(carddev)
cardid = self.dbcurs.get_machine_id(carddev)
carddev = self.sanitised_json(carddev)
yield (' bios_sets[%s] = %s;\n' % (carddev, self.sanitised_json(self.bios_data(cardid)))).encode('utf-8')
yield (' machine_flags[%s] = %s;\n' % (carddev, self.sanitised_json(self.flags_data(cardid)))).encode('utf-8')
yield htmltmpl.MACHINE_SLOTS_PLACEHOLDER_EPILOGUE.substitute(
machine=self.sanitised_json(self.shortname)).encode('utf=8')
# list devices referenced by this system/device
first = True
@ -332,6 +438,10 @@ class MachineHandler(QueryPageHandler):
description=cgi.escape(description or ''),
sourcefile=cgi.escape(sourcefile or '')).encode('utf-8')
@staticmethod
def sanitised_json(data):
return json.dumps(data).replace('<', '\\u003c').replace('>', '\\u003e')
class SourceFileHandler(QueryPageHandler):
def __init__(self, app, application_uri, environ, start_response, **kwargs):
@ -531,7 +641,13 @@ class SoftwareListHandler(QueryPageHandler):
if first:
yield htmltmpl.SOFTWARELIST_MACHINE_TABLE_HEADER.substitute().encode('utf-8')
first = False
yield self.machine_row(machine_info)
yield htmltmpl.SOFTWARELIST_MACHINE_TABLE_ROW.substitute(
machinehref=self.machine_href(machine_info['shortname']),
shortname=cgi.escape(machine_info['shortname']),
description=cgi.escape(machine_info['description']),
year=cgi.escape(machine_info['year'] or ''),
manufacturer=cgi.escape(machine_info['manufacturer'] or ''),
status=cgi.escape(machine_info['status'])).encode('utf-8')
if not first:
yield htmltmpl.SORTABLE_TABLE_EPILOGUE.substitute(id='tbl-machines').encode('utf-8')
@ -613,15 +729,6 @@ class SoftwareListHandler(QueryPageHandler):
publisher=cgi.escape(clone_info['publisher']),
supported=self.format_supported(clone_info['supported'])).encode('utf-8')
def machine_row(self, machine_info):
return htmltmpl.SOFTWARELIST_MACHINE_TABLE_ROW.substitute(
machinehref=self.machine_href(machine_info['shortname']),
shortname=cgi.escape(machine_info['shortname']),
description=cgi.escape(machine_info['description']),
year=cgi.escape(machine_info['year'] or ''),
manufacturer=cgi.escape(machine_info['manufacturer'] or ''),
status=cgi.escape(machine_info['status'])).encode('utf-8')
@staticmethod
def format_supported(supported):
return 'Yes' if supported == 0 else 'Partial' if supported == 1 else 'No'
@ -659,47 +766,17 @@ class BiosRpcHandler(MachineRpcHandlerBase):
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')
yield json.dumps(self.flags_data(machine)).encode('utf-8')
class SlotsRpcHandler(MachineRpcHandlerBase):
def data_page(self, machine):
result = { 'defaults': { }, 'slots': { } }
yield json.dumps(self.slot_data(machine)).encode('utf-8')
# 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 SoftwareListsRpcHandler(MachineRpcHandlerBase):
def data_page(self, machine):
yield json.dumps(self.softwarelist_data(machine)).encode('utf-8')
class RomDumpsRpcHandler(QueryPageHandler):
@ -817,11 +894,12 @@ class DiskDumpsRpcHandler(QueryPageHandler):
class MiniMawsApp(object):
JS_ESCAPE = re.compile('([\"\'\\\\])')
RPC_SERVICES = {
'bios': BiosRpcHandler,
'flags': FlagsRpcHandler,
'slots': SlotsRpcHandler,
'romdumps': RomDumpsRpcHandler,
'diskdumps': DiskDumpsRpcHandler }
'bios': BiosRpcHandler,
'flags': FlagsRpcHandler,
'slots': SlotsRpcHandler,
'softwarelists': SoftwareListsRpcHandler,
'romdumps': RomDumpsRpcHandler,
'diskdumps': DiskDumpsRpcHandler }
def __init__(self, dbfile, **kwargs):
super(MiniMawsApp, self).__init__(**kwargs)