diff options
author | 2017-08-01 02:55:25 +1000 | |
---|---|---|
committer | 2017-08-01 02:55:25 +1000 | |
commit | 8981b40bd9b605014c5ad07224f9a27447d1b1f2 (patch) | |
tree | bd8c93313d3bf2e1005d5338eb75fc4eff0461e5 /scripts/minimaws | |
parent | 456fd5e4225fc58ccecf71cc5692fabf4b483071 (diff) |
Add "minimaws" sample script demonstrating how to do some tasks with
output from -listxml verb. Compatible with Python 2.7 or Python 3.
Requires at least SQLite 3.6.19 for foreign key support.
This serves a few purposes:
* Demonstrating some things that can be done with -listxml output
* Providing a reference implementation for useful queries
* Helping ensure our XML output isn't completely useless
* Providing additional queries over MAME's auxiliary verbs
* Proper glob support unlike the broken implementation in MAME right now
Right now, it's a bit ugly to use. You can only load into a completely
clean database, and you need to manually create the schema. I'll
address this later. The default database filename is minimaws.sqlite3
(you can override this with --database before the verb on the command
line). Loading isn't particularly fast, but query performance is very
good.
Create a database first:
rm -f minimaws.sqlite3
sqlite3 minimaws.sqlite3 < scripts/minimaws/schema.sql
Now you can load it using a MAME binary or XML output (use one of these
options, not both):
python scripts/minimaws/minimaws.py load --executable ./mame
python scripts/minimaws/minimaws.py load --file mame0188.xml
Once that's done you can do queries:
python scripts/minimaws/minimaws.py listfull
python scripts/minimaws/minimaws.py listclones "*cmast*"
python scripts/minimaws/minimaws.py listsource "*mous*"
python scripts/minimaws/minimaws.py listbrothers "intl*"
These work much like the equivalent MAME verbs, but without the overhead
of loading MAME's static data. But there's one already query that you
can't easily do with MAME:
python scripts/minimaws/minimaws.py listaffected "src/devices/cpu/m6805/*" src/devices/sound/qsound.cpp
This will list all runnable systems that use a device defined in any
file under devices/cpu/m6805 or in devices/sound/qsound.cpp (you can
specify and arbitrary number of files or glob patterns). This may be
useful for planning regression tests.
Another thing this does (that gives rise to the name) is serving
information over HTTP. It's implemented as a WSGI, and it mainly uses
GET requests. This means it can run hosted in Apache mod_wsgi, or
cached by Apache mod_proxy, Squid, nginx, or something else. It can
also run out-of-the-box using wsgiref.simple_server components. The
default port is 8080 but this can be changed with the --port option.
Start the web server with the serve verb (stop it with keyboard
interrupt ^C or similar):
python scripts/minimaws/minimaws.py serve
Right now it's rather crude, and doesn't list devices for you. This
means you have to know the shortname of a machine to get a useful URL.
For example, you can look at a driver and see its parent set and the
devices it references:
http://localhost:8080/machine/kof2000n
Or you can look at a device, and see the devices it refereces, as well
as the devices/systems that reference it:
http://localhost:8080/machine/zac1b11142
The links between devices/systems are clickable. They might 404 on you
if you used a single-driver build with broken parent/clone
relationships, but they should all work in a full build that passes
validation.
There's still a lot to do. In particular I want to demonstrate how to
do live DIP switch preview and dynamic slot discovery. But I've already
discovered stuff in the -listxml output that's less than ideal with
this, so it's helping.
Diffstat (limited to 'scripts/minimaws')
-rw-r--r-- | scripts/minimaws/lib/__init__.py | 4 | ||||
-rw-r--r-- | scripts/minimaws/lib/auxverbs.py | 83 | ||||
-rw-r--r-- | scripts/minimaws/lib/dbaccess.py | 292 | ||||
-rw-r--r-- | scripts/minimaws/lib/htmltmpl.py | 22 | ||||
-rw-r--r-- | scripts/minimaws/lib/lxparse.py | 227 | ||||
-rw-r--r-- | scripts/minimaws/lib/wsgiserve.py | 135 | ||||
-rwxr-xr-x | scripts/minimaws/minimaws.py | 59 | ||||
-rw-r--r-- | scripts/minimaws/schema.sql | 84 |
8 files changed, 906 insertions, 0 deletions
diff --git a/scripts/minimaws/lib/__init__.py b/scripts/minimaws/lib/__init__.py new file mode 100644 index 00000000000..d89cb7f81ba --- /dev/null +++ b/scripts/minimaws/lib/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/python +## +## license:BSD-3-Clause +## copyright-holders:Vas Crabb diff --git a/scripts/minimaws/lib/auxverbs.py b/scripts/minimaws/lib/auxverbs.py new file mode 100644 index 00000000000..c68c528af6f --- /dev/null +++ b/scripts/minimaws/lib/auxverbs.py @@ -0,0 +1,83 @@ +#!/usr/bin/python +## +## license:BSD-3-Clause +## copyright-holders:Vas Crabb + +from . import dbaccess + +import sys + + +def do_listfull(options): + dbconn = dbaccess.QueryConnection(options.database) + dbcurs = dbconn.cursor() + first = True + for shortname, description in dbcurs.listfull(options.pattern): + if first: + sys.stdout.write('Name: Description:\n') + first = False + sys.stdout.write('%-16s "%s"\n' % (shortname, description)) + if first: + sys.stderr.write('No matching systems found for \'%s\'\n' % (options.pattern, )) + dbcurs.close() + dbconn.close() + + +def do_listsource(options): + dbconn = dbaccess.QueryConnection(options.database) + dbcurs = dbconn.cursor() + shortname = None + for shortname, sourcefile in dbcurs.listsource(options.pattern): + sys.stdout.write('%-16s %s\n' % (shortname, sourcefile)) + if shortname is None: + sys.stderr.write('No matching systems found for \'%s\'\n' % (options.pattern, )) + dbcurs.close() + dbconn.close() + + +def do_listclones(options): + dbconn = dbaccess.QueryConnection(options.database) + dbcurs = dbconn.cursor() + first = True + for shortname, parent in dbcurs.listclones(options.pattern): + if first: + sys.stdout.write('Name: Clone of:\n') + first = False + sys.stdout.write('%-16s %s\n' % (shortname, parent)) + if first: + count = dbcurs.count_systems(options.pattern).fetchone()[0] + if count: + sys.stderr.write('Found %d match(es) for \'%s\' but none were clones\n' % (count, options.pattern)) + else: + sys.stderr.write('No matching systems found for \'%s\'\n' % (options.pattern, )) + dbcurs.close() + dbconn.close() + + +def do_listbrothers(options): + dbconn = dbaccess.QueryConnection(options.database) + dbcurs = dbconn.cursor() + first = True + for sourcefile, shortname, parent in dbcurs.listbrothers(options.pattern): + if first: + sys.stdout.write('%-20s %-16s %s\n' % ('Source file:', 'Name:', 'Parent:')) + first = False + sys.stdout.write('%-20s %-16s %s\n' % (sourcefile, shortname, parent or '')) + if first: + sys.stderr.write('No matching systems found for \'%s\'\n' % (options.pattern, )) + dbcurs.close() + dbconn.close() + +def do_listaffected(options): + dbconn = dbaccess.QueryConnection(options.database) + dbcurs = dbconn.cursor() + first = True + for shortname, description in dbcurs.listaffected(*options.pattern): + if first: + sys.stdout.write('Name: Description:\n') + first = False + sys.stdout.write('%-16s "%s"\n' % (shortname, description)) + if first: + sys.stderr.write('No matching systems found for \'%s\'\n' % (options.pattern, )) + dbcurs.close() + dbconn.close() diff --git a/scripts/minimaws/lib/dbaccess.py b/scripts/minimaws/lib/dbaccess.py new file mode 100644 index 00000000000..3358b538b3c --- /dev/null +++ b/scripts/minimaws/lib/dbaccess.py @@ -0,0 +1,292 @@ +#!/usr/bin/python +## +## license:BSD-3-Clause +## copyright-holders:Vas Crabb + +import sqlite3 + + +class SchemaQueries(object): + INDEX_MACHINE_ISDEVICE_SHORTNAME = 'CREATE INDEX machine_isdevice_shortname ON machine (isdevice ASC, shortname ASC)' + INDEX_MACHINE_ISDEVICE_DESCRIPTION = 'CREATE INDEX machine_isdevice_description ON machine (isdevice ASC, description ASC)' + INDEX_MACHINE_RUNNABLE_SHORTNAME = 'CREATE INDEX machine_runnable_shortname ON machine (runnable ASC, shortname ASC)' + INDEX_MACHINE_RUNNABLE_DESCRIPTION = 'CREATE INDEX machine_runnable_description ON machine (runnable ASC, description ASC)' + + INDEX_SYSTEM_YEAR = 'CREATE INDEX system_year ON system (year ASC)' + INDEX_SYSTEM_MANUFACTURER = 'CREATE INDEX system_manufacturer ON system (manufacturer ASC)' + + INDEX_ROMOF_PARENT = 'CREATE INDEX romof_parent ON romof (parent ASC)' + + INDEX_CLONEOF_PARENT = 'CREATE INDEX cloneof_parent ON cloneof (parent ASC)' + + INDEX_DEVICEREFERENCE_DEVICE = 'CREATE INDEX devicereference_device ON devicereference (device ASC)' + + INDEX_DIPSWITCH_MACHINE_ISCONFIG = 'CREATE INDEX dipswitch_machine_isconfig ON dipswitch (machine ASC, isconfig ASC)' + + DROP_MACHINE_ISDEVICE_SHORTNAME = 'DROP INDEX IF EXISTS machine_isdevice_shortname' + DROP_MACHINE_ISDEVICE_DESCRIPTION = 'DROP INDEX IF EXISTS machine_isdevice_description' + DROP_MACHINE_RUNNABLE_SHORTNAME = 'DROP INDEX IF EXISTS machine_runnable_shortname' + DROP_MACHINE_RUNNABLE_DESCRIPTION = 'DROP INDEX IF EXISTS machine_runnable_description' + + DROP_SYSTEM_YEAR = 'DROP INDEX IF EXISTS system_year' + DROP_SYSTEM_MANUFACTURER = 'DROP INDEX IF EXISTS system_manufacturer' + + DROP_ROMOF_PARENT = 'DROP INDEX IF EXISTS romof_parent' + + DROP_CLONEOF_PARENT = 'DROP INDEX IF EXISTS cloneof_parent' + + DROP_DEVICEREFERENCE_DEVICE = 'DROP INDEX IF EXISTS devicereference_device' + + DROP_DIPSWITCH_MACHINE_ISCONFIG = 'DROP INDEX IF EXISTS dipswitch_machine_isconfig' + + +class UpdateQueries(object): + ADD_FEATURETYPE = 'INSERT OR IGNORE INTO featuretype (name) VALUES (?)' + ADD_SOURCEFILE = 'INSERT OR IGNORE INTO sourcefile (filename) VALUES (?)' + ADD_MACHINE = 'INSERT INTO machine (shortname, description, sourcefile, isdevice, runnable) SELECT ?, ?, id, ?, ? FROM sourcefile WHERE filename = ?' + ADD_SYSTEM = 'INSERT INTO system (id, year, manufacturer) VALUES (?, ?, ?)' + ADD_CLONEOF = 'INSERT INTO cloneof (id, parent) VALUES (?, ?)' + ADD_ROMOF = 'INSERT INTO romof (id, parent) VALUES (?, ?)' + ADD_DEVICEREFERENCE = 'INSERT OR IGNORE INTO devicereference (machine, device) VALUES (?, ?)' + ADD_DIPSWITCH = 'INSERT INTO dipswitch (machine, isconfig, name, tag, mask) VALUES (?, ?, ?, ?, ?)' + ADD_DIPLOCATION = 'INSERT INTO diplocation (dipswitch, bit, name, num, inverted) VALUES (?, ?, ?, ?, ?)' + ADD_DIPVALUE = 'INSERT INTO dipvalue (dipswitch, name, value, isdefault) VALUES (?, ?, ?, ?)' + ADD_FEATURE = 'INSERT INTO feature (machine, featuretype, status, overall) SELECT ?, id, ?, ? FROM featuretype WHERE name = ?' + + +class QueryCursor(object): + def __init__(self, dbconn, **kwargs): + super(QueryCursor, self).__init__(**kwargs) + self.dbcurs = dbconn.cursor() + + def close(self): + self.dbcurs.close() + + def is_glob(self, *patterns): + for pattern in patterns: + if any(ch in pattern for ch in '?*['): + return True + return False + + def count_systems(self, pattern): + if pattern is not None: + return self.dbcurs.execute( + 'SELECT COUNT(*) ' \ + 'FROM machine WHERE isdevice = 0 AND shortname GLOB ? ', + (pattern, )) + else: + return self.dbcurs.execute( + 'SELECT COUNT(*) ' \ + 'FROM machine WHERE isdevice = 0 ') + + def listfull(self, pattern): + if pattern is not None: + return self.dbcurs.execute( + 'SELECT shortname, description ' \ + 'FROM machine WHERE isdevice = 0 AND shortname GLOB ? ' \ + 'ORDER BY shortname ASC', + (pattern, )) + else: + return self.dbcurs.execute( + 'SELECT shortname, description ' \ + 'FROM machine WHERE isdevice = 0 ' \ + 'ORDER BY shortname ASC') + + def listsource(self, pattern): + if pattern is not None: + return self.dbcurs.execute( + 'SELECT machine.shortname, sourcefile.filename ' \ + 'FROM machine JOIN sourcefile ON machine.sourcefile = sourcefile.id ' \ + 'WHERE machine.isdevice = 0 AND machine.shortname GLOB ? ' \ + 'ORDER BY machine.shortname ASC', + (pattern, )) + else: + return self.dbcurs.execute( + 'SELECT machine.shortname, sourcefile.filename ' \ + 'FROM machine JOIN sourcefile ON machine.sourcefile = sourcefile.id ' \ + 'WHERE machine.isdevice = 0 ORDER BY machine.shortname ASC') + + def listclones(self, pattern): + if pattern is not None: + return self.dbcurs.execute( + 'SELECT machine.shortname, cloneof.parent ' \ + 'FROM machine JOIN cloneof ON machine.id = cloneof.id ' \ + 'WHERE machine.shortname GLOB ? OR cloneof.parent GLOB ? ' \ + 'ORDER BY machine.shortname ASC', + (pattern, pattern)) + else: + return self.dbcurs.execute( + 'SELECT machine.shortname, cloneof.parent ' \ + 'FROM machine JOIN cloneof ON machine.id = cloneof.id ' \ + 'ORDER BY machine.shortname ASC') + + def listbrothers(self, pattern): + if pattern is not None: + return self.dbcurs.execute( + 'SELECT sourcefile.filename, machine.shortname, cloneof.parent ' \ + 'FROM machine JOIN sourcefile ON machine.sourcefile = sourcefile.id LEFT JOIN cloneof ON machine.id = cloneof.id ' \ + 'WHERE machine.isdevice = 0 AND sourcefile.id IN (SELECT sourcefile FROM machine WHERE shortname GLOB ?)' \ + 'ORDER BY machine.shortname ASC', + (pattern, )) + else: + return self.dbcurs.execute( + 'SELECT sourcefile.filename, machine.shortname, cloneof.parent ' \ + 'FROM machine JOIN sourcefile ON machine.sourcefile = sourcefile.id LEFT JOIN cloneof ON machine.id = cloneof.id ' \ + 'WHERE machine.isdevice = 0 ' \ + 'ORDER BY machine.shortname ASC') + + def listaffected(self, *patterns): + if 1 == len(patterns): + return self.dbcurs.execute( + 'SELECT shortname, description ' \ + 'FROM machine ' \ + 'WHERE id IN (SELECT machine FROM devicereference WHERE device IN (SELECT shortname FROM machine WHERE sourcefile IN (SELECT id FROM sourcefile WHERE filename GLOB ?))) AND runnable = 1 ' \ + 'ORDER BY shortname ASC', + patterns) + elif self.is_glob(*patterns): + return self.dbcurs.execute( + 'SELECT shortname, description ' \ + 'FROM machine ' \ + 'WHERE id IN (SELECT machine FROM devicereference WHERE device IN (SELECT shortname FROM machine WHERE sourcefile IN (SELECT id FROM sourcefile WHERE filename GLOB ?' + (' OR filename GLOB ?' * (len(patterns) - 1)) + '))) AND runnable = 1 ' \ + 'ORDER BY shortname ASC', + patterns) + else: + return self.dbcurs.execute( + 'SELECT shortname, description ' \ + 'FROM machine ' \ + 'WHERE id IN (SELECT machine FROM devicereference WHERE device IN (SELECT shortname FROM machine WHERE sourcefile IN (SELECT id FROM sourcefile WHERE filename IN (?' + (', ?' * (len(patterns) - 1)) + ')))) AND runnable = 1 ' \ + 'ORDER BY shortname ASC', + patterns) + + 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 ' \ + 'FROM machine JOIN sourcefile ON machine.sourcefile = sourcefile.id LEFT JOIN system ON machine.id = system.id LEFT JOIN cloneof ON system.id = cloneof.id LEFT JOIN romof ON system.id = romof.id ' \ + 'WHERE machine.shortname = ?', + (machine, )) + + def get_devices_referenced(self, machine): + return self.dbcurs.execute( + 'SELECT devicereference.device AS shortname, machine.description AS description ' \ + 'FROM devicereference LEFT JOIN machine ON devicereference.device = machine.shortname ' \ + 'WHERE devicereference.machine = ? ' \ + 'ORDER BY machine.description ASC, devicereference.device ASC', + (machine, )) + + def get_device_references(self, shortname): + return self.dbcurs.execute( + 'SELECT shortname, description ' \ + 'FROM machine ' \ + 'WHERE id IN (SELECT machine FROM devicereference WHERE device = ?) ' \ + 'ORDER BY description ASC', + (shortname, )) + + +class UpdateCursor(object): + def __init__(self, dbconn, **kwargs): + super(UpdateCursor, self).__init__(**kwargs) + self.dbcurs = dbconn.cursor() + + def close(self): + self.dbcurs.close() + + def add_featuretype(self, name): + self.dbcurs.execute(UpdateQueries.ADD_FEATURETYPE, (name, )) + + def add_sourcefile(self, filename): + self.dbcurs.execute(UpdateQueries.ADD_SOURCEFILE, (filename, )) + + def add_machine(self, shortname, description, sourcefile, isdevice, runnable): + self.dbcurs.execute(UpdateQueries.ADD_MACHINE, (shortname, description, isdevice, runnable, sourcefile)) + return self.dbcurs.lastrowid + + def add_system(self, machine, year, manufacturer): + self.dbcurs.execute(UpdateQueries.ADD_SYSTEM, (machine, year, manufacturer)) + return self.dbcurs.lastrowid + + def add_cloneof(self, machine, parent): + self.dbcurs.execute(UpdateQueries.ADD_CLONEOF, (machine, parent)) + return self.dbcurs.lastrowid + + def add_romof(self, machine, parent): + self.dbcurs.execute(UpdateQueries.ADD_ROMOF, (machine, parent)) + return self.dbcurs.lastrowid + + def add_devicereference(self, machine, device): + self.dbcurs.execute(UpdateQueries.ADD_DEVICEREFERENCE, (machine, device)) + + def add_dipswitch(self, machine, isconfig, name, tag, mask): + self.dbcurs.execute(UpdateQueries.ADD_DIPSWITCH, (machine, isconfig, name, tag, mask)) + return self.dbcurs.lastrowid + + def add_diplocation(self, dipswitch, bit, name, num, inverted): + self.dbcurs.execute(UpdateQueries.ADD_DIPLOCATION, (dipswitch, bit, name, num, inverted)) + return self.dbcurs.lastrowid + + def add_dipvalue(self, dipswitch, name, value, isdefault): + self.dbcurs.execute(UpdateQueries.ADD_DIPVALUE, (dipswitch, name, value, isdefault)) + return self.dbcurs.lastrowid + + def add_feature(self, machine, featuretype, status, overall): + self.dbcurs.execute(UpdateQueries.ADD_FEATURE, (machine, status, overall, featuretype)) + return self.dbcurs.lastrowid + + +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) + self.dbconn.row_factory = sqlite3.Row + self.dbconn.execute('PRAGMA foreign_keys = ON') + + def close(self): + self.dbconn.close() + + def cursor(self): + return QueryCursor(self.dbconn) + + +class UpdateConnection(object): + def __init__(self, database, **kwargs): + super(UpdateConnection, self).__init__(**kwargs) + self.dbconn = sqlite3.connect(database) + self.dbconn.execute('PRAGMA foreign_keys = ON') + + def commit(self): + self.dbconn.commit() + + def rollback(self): + self.dbconn.rollback() + + def close(self): + self.dbconn.close() + + def cursor(self): + return UpdateCursor(self.dbconn) + + def drop_indexes(self): + self.dbconn.execute(SchemaQueries.DROP_MACHINE_ISDEVICE_SHORTNAME) + self.dbconn.execute(SchemaQueries.DROP_MACHINE_ISDEVICE_DESCRIPTION) + self.dbconn.execute(SchemaQueries.DROP_MACHINE_RUNNABLE_SHORTNAME) + self.dbconn.execute(SchemaQueries.DROP_MACHINE_RUNNABLE_DESCRIPTION) + self.dbconn.execute(SchemaQueries.DROP_SYSTEM_YEAR) + self.dbconn.execute(SchemaQueries.DROP_SYSTEM_MANUFACTURER) + self.dbconn.execute(SchemaQueries.DROP_ROMOF_PARENT) + self.dbconn.execute(SchemaQueries.DROP_CLONEOF_PARENT) + self.dbconn.execute(SchemaQueries.DROP_DEVICEREFERENCE_DEVICE) + self.dbconn.execute(SchemaQueries.DROP_DIPSWITCH_MACHINE_ISCONFIG) + self.dbconn.commit() + + def create_indexes(self): + self.dbconn.execute(SchemaQueries.INDEX_MACHINE_ISDEVICE_SHORTNAME) + self.dbconn.execute(SchemaQueries.INDEX_MACHINE_ISDEVICE_DESCRIPTION) + self.dbconn.execute(SchemaQueries.INDEX_MACHINE_RUNNABLE_SHORTNAME) + self.dbconn.execute(SchemaQueries.INDEX_MACHINE_RUNNABLE_DESCRIPTION) + self.dbconn.execute(SchemaQueries.INDEX_SYSTEM_YEAR) + self.dbconn.execute(SchemaQueries.INDEX_SYSTEM_MANUFACTURER) + self.dbconn.execute(SchemaQueries.INDEX_ROMOF_PARENT) + self.dbconn.execute(SchemaQueries.INDEX_CLONEOF_PARENT) + self.dbconn.execute(SchemaQueries.INDEX_DEVICEREFERENCE_DEVICE) + self.dbconn.execute(SchemaQueries.INDEX_DIPSWITCH_MACHINE_ISCONFIG) + self.dbconn.commit() diff --git a/scripts/minimaws/lib/htmltmpl.py b/scripts/minimaws/lib/htmltmpl.py new file mode 100644 index 00000000000..2c71fea9fb9 --- /dev/null +++ b/scripts/minimaws/lib/htmltmpl.py @@ -0,0 +1,22 @@ +#!/usr/bin/python +## +## license:BSD-3-Clause +## copyright-holders:Vas Crabb + +import string + + +MACHINE_PROLOGUE = string.Template( + '<!DOCTYPE html>\n' \ + '<html>\n' \ + '<head>\n' \ + ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8">\n' \ + ' <title>${description} (${shortname})</title>\n' \ + '</head>\n' \ + '<body>\n' \ + '<h1>${description}</h1>\n' \ + '<table>\n' \ + ' <tr><th style="text-align: right">Short name:</th><td>${shortname}</td></tr>\n' \ + ' <tr><th style="text-align: right">Is device:</th><td>${isdevice}</td></tr>\n' \ + ' <tr><th style="text-align: right">Runnable:</th><td>${runnable}</td></tr>\n' \ + ' <tr><th style="text-align: right">Source file:</th><td>${sourcefile}</td></tr>\n') diff --git a/scripts/minimaws/lib/lxparse.py b/scripts/minimaws/lib/lxparse.py new file mode 100644 index 00000000000..f9061bcb798 --- /dev/null +++ b/scripts/minimaws/lib/lxparse.py @@ -0,0 +1,227 @@ +#!/usr/bin/python +## +## license:BSD-3-Clause +## copyright-holders:Vas Crabb + +from . import dbaccess + +import subprocess +import xml.sax +import xml.sax.saxutils + + +class ElementHandlerBase(object): + def __init__(self, parent, **kwargs): + super(ElementHandlerBase, self).__init__(**kwargs) + self.dbconn = parent.dbconn if parent is not None else None + self.locator = parent.locator if parent is not None else None + self.depth = 0 + self.childhandler = None + self.childdepth = 0 + + def startMainElement(self, name, attrs): + pass + + def endMainElement(self, name): + pass + + def mainCharacters(self, content): + pass + + def mainIgnorableWitespace(self, whitespace): + pass + + def startChildElement(self, name, attrs): + pass + + def endChildElement(self, name): + pass + + def childCharacters(self, content): + pass + + def childIgnorableWitespace(self, whitespace): + pass + + def endChildHandler(self, name, handler): + pass + + def setChildHandler(self, name, attrs, handler): + self.depth -= 1 + self.childhandler = handler + self.childdepth += 1 + handler.startElement(name, attrs) + + def setDocumentLocator(self, locator): + self.locator = locator + + def startElement(self, name, attrs): + if self.childhandler is not None: + self.childdepth += 1 + self.childhandler.startElement(name, attrs) + else: + self.depth += 1 + if 1 == self.depth: + self.startMainElement(name, attrs) + else: + self.startChildElement(name, attrs) + + def endElement(self, name): + if self.childhandler is not None: + self.childdepth -= 1 + self.childhandler.endElement(name) + if 0 == self.childdepth: + self.endChildHandler(name, self.childhandler) + self.childhandler = None + else: + self.depth -= 1 + if 0 == self.depth: + self.endMainElement(name) + else: + self.endChildElement(name) + + def characters(self, content): + if self.childhandler is not None: + self.childhandler.characters(content) + elif 1 < self.depth: + self.childCharacters(content) + else: + self.mainCharacters(content) + + def ignorableWhitespace(self, content): + if self.childhandler is not None: + self.childhandler.ignorableWhitespace(content) + elif 1 < self.depth: + self.childIgnorableWitespace(content) + else: + self.mainIgnorableWitespace(content) + + +class ElementHandler(ElementHandlerBase): + IGNORE = ElementHandlerBase(parent=None) + + +class TextAccumulator(ElementHandler): + def __init__(self, parent, **kwargs): + super(TextAccumulator, self).__init__(parent=parent, **kwargs) + self.text = '' + + def mainCharacters(self, content): + self.text += content + + +class DipSwitchHandler(ElementHandler): + def __init__(self, parent, **kwargs): + super(DipSwitchHandler, self).__init__(parent=parent, **kwargs) + self.dbcurs = parent.dbcurs + self.machine = parent.id + + def startMainElement(self, name, attrs): + self.mask = int(attrs['mask']) + self.bit = 0 + self.id = self.dbcurs.add_dipswitch(self.machine, name == 'configuration', attrs['name'], attrs['tag'], self.mask) + + def startChildElement(self, name, attrs): + if (name == 'diplocation') or (name == 'conflocation'): + while (0 != self.mask) and not (self.mask & 1): + self.mask >>= 1 + self.bit += 1 + self.dbcurs.add_diplocation(self.id, self.bit, attrs['name'], attrs['number'], attrs['inverted'] == 'yes' if 'inverted' in attrs else False) + self.mask >>= 1 + self.bit += 1 + elif (name == 'dipvalue') or (name == 'confsetting'): + self.dbcurs.add_dipvalue(self.id, attrs['name'], attrs['value'], attrs['default'] == 'yes' if 'default' in attrs else False) + self.setChildHandler(name, attrs, self.IGNORE) + + +class MachineHandler(ElementHandler): + def __init__(self, parent, **kwargs): + super(MachineHandler, self).__init__(parent=parent, **kwargs) + self.dbcurs = self.dbconn.cursor() + + def startMainElement(self, name, attrs): + self.shortname = attrs['name'] + self.sourcefile = attrs['sourcefile'] + self.isdevice = attrs['isdevice'] == 'yes' if 'isdevice' in attrs else False + self.runnable = attrs['runnable'] == 'yes' if 'runnable' in attrs else True + self.cloneof = attrs.get('cloneof') + self.romof = attrs.get('romof') + self.dbcurs.add_sourcefile(self.sourcefile) + + def startChildElement(self, name, attrs): + if (name == 'description') or (name == 'year') or (name == 'manufacturer'): + self.setChildHandler(name, attrs, TextAccumulator(self)) + elif (name == 'dipswitch') or (name == 'configuration'): + self.setChildHandler(name, attrs, DipSwitchHandler(self)) + else: + if name == 'device_ref': + self.dbcurs.add_devicereference(self.id, attrs['name']) + elif name == 'feaure': + 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 + self.dbcurs.add_feature(self.id, attrs['type'], status, overall) + self.setChildHandler(name, attrs, self.IGNORE) + + def endChildHandler(self, name, handler): + if name == 'description': + self.description = handler.text + self.id = self.dbcurs.add_machine(self.shortname, self.description, self.sourcefile, self.isdevice, self.runnable) + if self.cloneof is not None: + self.dbcurs.add_cloneof(self.id, self.cloneof) + if self.romof is not None: + self.dbcurs.add_romof(self.id, self.romof) + elif name == 'year': + self.year = handler.text + elif name == 'manufacturer': + self.manufacturer = handler.text + self.dbcurs.add_system(self.id, self.year, self.manufacturer) + + def endMainElement(self, name): + self.dbconn.commit() + self.dbcurs.close() + + +class ListXmlHandler(ElementHandler): + def __init__(self, dbconn, **kwargs): + super(ListXmlHandler, self).__init__(parent=None, **kwargs) + self.dbconn = dbconn + + def startDocument(self): + pass + + def endDocument(self): + pass + + def startMainElement(self, name, attrs): + if name != 'mame': + raise xml.sax.SAXParseException( + msg=('Expected "mame" element but found "%s"' % (name, )), + exception=None, + locator=self.locator) + self.dbconn.drop_indexes() + + def endMainElement(self, name): + # TODO: build index by first letter or whatever + self.dbconn.create_indexes() + + def startChildElement(self, name, attrs): + if name != 'machine': + raise xml.sax.SAXParseException( + msg=('Expected "machine" element but found "%s"' % (name, )), + exception=None, + locator=self.locator) + self.setChildHandler(name, attrs, MachineHandler(self)) + + def processingInstruction(self, target, data): + pass + + +def load_info(options): + parser = xml.sax.make_parser() + parser.setContentHandler(ListXmlHandler(dbaccess.UpdateConnection(options.database))) + if options.executable is not None: + task = subprocess.Popen([options.executable, '-listxml'], stdout=subprocess.PIPE) + parser.parse(task.stdout) + else: + parser.parse(options.file) diff --git a/scripts/minimaws/lib/wsgiserve.py b/scripts/minimaws/lib/wsgiserve.py new file mode 100644 index 00000000000..96ea43f35c5 --- /dev/null +++ b/scripts/minimaws/lib/wsgiserve.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +## +## license:BSD-3-Clause +## copyright-holders:Vas Crabb + +from . import dbaccess +from . import htmltmpl + +import cgi +import sys +import wsgiref.simple_server +import wsgiref.util + +if sys.version_info > (3, ): + import urllib.parse as urlparse +else: + import urlparse + + +class MachineHandler(object): + def __init__(self, app, application_uri, environ, start_response, **kwargs): + super(MachineHandler, self).__init__(**kwargs) + self.application_uri = application_uri + self.environ = environ + self.start_response = start_response + self.dbcurs = app.dbconn.cursor() + self.shortname = wsgiref.util.shift_path_info(environ) + + def __iter__(self): + if not self.shortname: + # could probably list machines here or something + self.start_response('403 Forbidden', [('Content-type', 'text/plain')]) + yield '403 Forbidden'.encode('utf-8') + elif self.environ['PATH_INFO']: + # subdirectory of a machine + self.start_response('404 Not Found', [('Content-type', 'text/plain')]) + yield '404 Not Found'.encode('utf-8') + else: + machine_info = self.dbcurs.get_machine_info(self.shortname).fetchone() + if not machine_info: + self.start_response('404 Not Found', [('Content-type', 'text/plain')]) + yield '404 Not Found'.encode('utf-8') + else: + self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8')]) + description = machine_info['description'] + yield htmltmpl.MACHINE_PROLOGUE.substitute( + description=cgi.escape(description), + shortname=cgi.escape(self.shortname), + isdevice=cgi.escape('Yes' if machine_info['isdevice'] else 'No'), + runnable=cgi.escape('Yes' if machine_info['runnable'] else 'No'), + sourcefile=cgi.escape(machine_info['sourcefile'])).encode('utf-8') + if machine_info['year'] is not None: + yield ( + ' <tr><th style="text-align: right">Year:</th><td>%s</td></tr>\n' \ + ' <tr><th style="text-align: right">Manufacturer:</th><td>%s</td></tr>\n' % + (cgi.escape(machine_info['year']), cgi.escape(machine_info['Manufacturer']))).encode('utf-8') + if machine_info['cloneof'] is not None: + parent = self.dbcurs.listfull(machine_info['cloneof']).fetchone() + if parent: + yield ( + ' <tr><th style="text-align: right">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') + else: + yield ( + ' <tr><th style="text-align: right">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') + 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 style="text-align: right">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') + else: + yield ( + ' <tr><th style="text-align: right">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') + yield '</table>\n'.encode('utf-8') + + devices = self.dbcurs.get_devices_referenced(machine_info['id']) + first = True + for name, desc in devices: + if first: + yield '<h2>Devices Referenced</h2>\n<ul>\n'.encode('utf-8') + first = False + if desc is not None: + yield ( + ' <li><a href="%s">%s (%s)</a></li>\n' % + (self.machine_href(name), cgi.escape(desc), cgi.escape(name))).encode('utf-8') + else: + yield ( + ' <li><a href="%s">%s</a></li>\n' % + (self.machine_href(name), cgi.escape(name))).encode('utf-8') + if not first: + yield '</ul>\n'.encode('utf-8') + + devices = self.dbcurs.get_device_references(self.shortname) + first = True + for name, desc in devices: + if first: + yield '<h2>Referenced By</h2>\n<ul>\n'.encode('utf-8') + first = False + yield ( + ' <li><a href="%s">%s (%s)</a></li>\n' % + (self.machine_href(name), cgi.escape(desc), cgi.escape(name))).encode('utf-8') + if not first: + yield '</ul>\n'.encode('utf-8') + + yield '</html>\n'.encode('utf-8') + + def machine_href(self, shortname): + return cgi.escape(urlparse.urljoin(self.application_uri, 'machine/%s' % (shortname, )), True) + + +class MiniMawsApp(object): + def __init__(self, dbfile, **kwargs): + super(MiniMawsApp, self).__init__(**kwargs) + self.dbconn = dbaccess.QueryConnection(dbfile) + + def __call__(self, environ, start_response): + application_uri = wsgiref.util.application_uri(environ) + module = wsgiref.util.shift_path_info(environ) + if module == 'machine': + return MachineHandler(self, application_uri, environ, start_response) + else: + start_response('200 OK', [('Content-type', 'text/plain')]) + return ('Module is %s\n' % (module, ), ) + + +def run_server(options): + application = MiniMawsApp(options.database) + server = wsgiref.simple_server.make_server(options.host, options.port, application) + try: + server.serve_forever() + except KeyboardInterrupt: + pass diff --git a/scripts/minimaws/minimaws.py b/scripts/minimaws/minimaws.py new file mode 100755 index 00000000000..12ddc865b56 --- /dev/null +++ b/scripts/minimaws/minimaws.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +## +## license:BSD-3-Clause +## copyright-holders:Vas Crabb + +import lib.auxverbs +import lib.lxparse +import lib.wsgiserve + +import argparse +import sys + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--database', metavar='<dbfile>', default='minimaws.sqlite3', help='SQLite 3 info database file (defaults to minimaws.sqlite3)') + subparsers = parser.add_subparsers(title='commands', dest='command', metavar='<command>') + + subparser = subparsers.add_parser('listfull', help='list short names and full names') + subparser.add_argument('pattern', nargs='?', metavar='<pat>', help='short name glob pattern') + + subparser = subparsers.add_parser('listsource', help='list short names and source files') + subparser.add_argument('pattern', nargs='?', metavar='<pat>', help='short name glob pattern') + + subparser = subparsers.add_parser('listclones', help='show clones') + subparser.add_argument('pattern', nargs='?', metavar='<pat>', help='short name/parent glob pattern') + + subparser = subparsers.add_parser('listbrothers', help='show drivers from the same source file(s)') + subparser.add_argument('pattern', nargs='?', metavar='<pat>', help='short name glob pattern') + + subparser = subparsers.add_parser('listaffected', help='show drivers affected by source change(s)') + subparser.add_argument('pattern', nargs='+', metavar='<pat>', help='source file glob pattern') + + subparser = subparsers.add_parser('serve', help='serve over HTTP') + subparser.add_argument('--port', metavar='<port>', default=8080, type=int, help='server TCP port') + subparser.add_argument('--host', metavar='<host>', default='', help='server TCP hostname') + + subparser = subparsers.add_parser('load', help='load machine information') + group = subparser.add_mutually_exclusive_group(required=True) + group.add_argument('--executable', metavar='<exe>', help='emulator executable') + group.add_argument('--file', metavar='<xmlfile>', help='XML machine information file') + + options = parser.parse_args() + if options.command == 'listfull': + lib.auxverbs.do_listfull(options) + elif options.command == 'listsource': + lib.auxverbs.do_listsource(options) + elif options.command == 'listclones': + lib.auxverbs.do_listclones(options) + elif options.command == 'listbrothers': + lib.auxverbs.do_listbrothers(options) + elif options.command == 'listaffected': + lib.auxverbs.do_listaffected(options) + elif options.command == 'serve': + lib.wsgiserve.run_server(options) + elif options.command == 'load': + lib.lxparse.load_info(options) + else: + print('%s' % (options, )) diff --git a/scripts/minimaws/schema.sql b/scripts/minimaws/schema.sql new file mode 100644 index 00000000000..7633441a123 --- /dev/null +++ b/scripts/minimaws/schema.sql @@ -0,0 +1,84 @@ +PRAGMA page_size = 4096; +PRAGMA foreign_keys = ON; + +CREATE TABLE featuretype ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + UNIQUE (name ASC)); + +CREATE TABLE sourcefile ( + id INTEGER PRIMARY KEY, + filename TEXT NOT NULL, + UNIQUE (filename ASC)); + +CREATE TABLE machine ( + id INTEGER PRIMARY KEY, + shortname TEXT NOT NULL, + description TEXT NOT NULL, + sourcefile INTEGER NOT NULL, + isdevice INTEGER NOT NULL, + runnable INTEGER NOT NULL, + UNIQUE (shortname ASC), + UNIQUE (description ASC), + FOREIGN KEY (sourcefile) REFERENCES sourcefile (id)); + +CREATE TABLE system ( + id INTEGER PRIMARY KEY, + year TEXT NOT NULL, + manufacturer TEXT NOT NULL, + FOREIGN KEY (id) REFERENCES machine (id)); + +CREATE TABLE cloneof ( + id INTEGER PRIMARY KEY, + parent TEXT NOT NULL, + FOREIGN KEY (id) REFERENCES machine (id)); + +CREATE TABLE romof ( + id INTEGER PRIMARY KEY, + parent TEXT NOT NULL, + FOREIGN KEY (id) REFERENCES machine (id)); + +CREATE TABLE devicereference ( + id INTEGER PRIMARY KEY, + machine INTEGER NOT NULL, + device TEXT NOT NULL, + UNIQUE (machine ASC, device ASC), + FOREIGN KEY (machine) REFERENCES machine (id)); + +CREATE TABLE dipswitch ( + id INTEGER PRIMARY KEY, + machine INTEGER NOT NULL, + isconfig INTEGER NOT NULL, + name TEXT NOT NULL, + tag TEXT NOT NULL, + mask INTEGER NOT NULL, + --UNIQUE (machine ASC, tag ASC, mask ASC), not necessarily true, need to expose port conditions + FOREIGN KEY (machine) REFERENCES machine (id)); + +CREATE TABLE diplocation ( + id INTEGER PRIMARY KEY, + dipswitch INTEGER NOT NULL, + bit INTEGER NOT NULL, + name TEXT NOT NULL, + num INTEGER NOT NULL, + inverted INTEGER NOT NULL, + UNIQUE (dipswitch ASC, bit ASC), + FOREIGN KEY (dipswitch) REFERENCES dipswitch (id)); + +CREATE TABLE dipvalue ( + id INTEGER PRIMARY KEY, + dipswitch INTEGER NOT NULL, + name TEXT NOT NULL, + value INTEGER NOT NULL, + isdefault INTEGER NOT NULL, + FOREIGN KEY (dipswitch) REFERENCES dipswitch (id)); + +CREATE TABLE feature ( + id INTEGER PRIMARY KEY, + machine INTEGER NOT NULL, + featuretype INTEGER NOT NULL, + status INTEGER NOT NULL, + overall INTEGER NOT NULL, + UNIQUE (machine ASC, featuretype ASC), + FOREIGN KEY (machine) REFERENCES machine (id), + FOREIGN KEY (featuretype) REFERENCES featuretype (id)); |