openmsx_control.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. # $Id$
  2. from PyQt4 import QtCore, QtXml
  3. from preferences import preferences
  4. from openmsx_utils import parseTclValue, EscapedStr
  5. from qt_utils import Signal, connect
  6. from inspect import getargspec
  7. class NotConfiguredException(Exception):
  8. pass
  9. class PrefixDemux(object):
  10. def __init__(self):
  11. self.__mapping = {}
  12. def __call__(self, name, machineId, message):
  13. handled = False
  14. for prefix, handler in self.__mapping.iteritems():
  15. if name.startswith(prefix):
  16. handler(name, machineId, message)
  17. handled = True
  18. if not handled:
  19. print 'ignore update for "%s": %s' % ( name, message )
  20. def register(self, prefixes, handler):
  21. mapping = self.__mapping
  22. for prefix in prefixes:
  23. assert prefix not in mapping
  24. mapping[prefix] = handler
  25. class ControlBridge(QtCore.QObject):
  26. logLine = Signal('QString', 'QString')
  27. def __init__(self):
  28. QtCore.QObject.__init__(self)
  29. self.__connection = None
  30. self.__initialHandlers = []
  31. self.__updateHandlers = {}
  32. # Command reply handling:
  33. self.__sendSerial = 0
  34. self.__receiveSerial = 0
  35. self.__callbacks = {}
  36. self.__machinesToIgnore = []
  37. def openConnection(self):
  38. # first check if we have an executable specified
  39. if 'system/executable' not in preferences:
  40. raise NotConfiguredException
  41. self.__connection = connection = ControlConnection(self)
  42. connection.connectionClosed.connect(self.connectionClosed)
  43. connection.start()
  44. for updateType in self.__updateHandlers.iterkeys():
  45. self.sendCommandRaw('openmsx_update enable %s' % updateType)
  46. for handler in self.__initialHandlers:
  47. handler()
  48. def closeConnection(self, callback):
  49. if self.__connection is None:
  50. # Connection is already closed.
  51. callback()
  52. else:
  53. # TODO: Have a fallback in case openMSX does not respond.
  54. # TODO: Is waiting for the quit command to be confirmed good enough,
  55. # or should we wait for the control connection end tag?
  56. self.command('exit_process')(callback)
  57. def connectionClosed(self):
  58. print 'connection with openMSX was closed'
  59. self.__connection = None
  60. # TODO: How to handle this? Attempt a reconnect?
  61. def registerInitial(self, handler):
  62. '''Registers a handler to be called after a new connection is
  63. established.
  64. You can use this mechanism to synchronize the initial state.
  65. '''
  66. assert self.__connection is None, 'register before connecting!'
  67. self.__initialHandlers.append(handler)
  68. def registerUpdate(self, updateType, handler):
  69. '''Registers a handler for a specific update type.
  70. The handler should be a callable that accepts two parameters:
  71. name (name attribute of XML tag) and message (contents of XML tag).
  72. Only one handler per type is supported.
  73. '''
  74. # TODO: Along the way, we will probably need these updates:
  75. # 'led', 'setting', 'plug', 'media', 'status'
  76. assert updateType not in self.__updateHandlers, updateType
  77. # TODO: How to deal with connected/not-connected?
  78. assert self.__connection is None, 'register before connecting!'
  79. self.__updateHandlers[updateType] = handler
  80. def registerUpdatePrefix(self, updateType, prefixes, handler):
  81. demux = self.__updateHandlers.get(updateType)
  82. if demux is None:
  83. demux = PrefixDemux()
  84. self.registerUpdate(updateType, demux)
  85. demux.register(prefixes, handler)
  86. def __formatReply(self, callbackfunc, value):
  87. '''Formats a TCL reply words to either a list of
  88. reply words, or a list with a single string in which all words
  89. are concatenated together, depending on what the callbackfunc
  90. expects.'''
  91. words = parseTclValue(value)
  92. args, varargs_, varkw_, defaults = getargspec(callbackfunc)
  93. numArgs = len(args)
  94. if numArgs != 0 and args[0] == 'self':
  95. numArgs -= 1
  96. if defaults is not None:
  97. numArgs -= len(defaults)
  98. #print 'Net num callback args: %d' % numArgs
  99. if numArgs == 1:
  100. if len(words) == 1:
  101. #print 'Returning (1 word): %s' % words
  102. return words
  103. else:
  104. #print 'Returning (multiple words): %s' % " ".join(words)
  105. return [" ".join(words)]
  106. else:
  107. #print 'Returning: %s' % words
  108. return words
  109. def command(self, *words):
  110. '''Send a Tcl command to openMSX.
  111. The words that form the command are passed as separate arguments.
  112. An object representing the command returned; when this object is called,
  113. the command will be executed. You can pass it a handler that will be
  114. called with the result of the command, or omit this if you are not
  115. interested in the result.
  116. '''
  117. if len(words) == 0:
  118. raise TypeError('command must contain at least one word')
  119. #TODO refactor this code to something more Pythonesque like
  120. # previous version (see commented out code below)
  121. line = ''
  122. for word in words:
  123. if isinstance(word, EscapedStr):
  124. line += unicode(word).replace(' ', '\\ ') + ' '
  125. else:
  126. line += unicode(word).replace('\\', '\\\\').replace(' ', '\\ ') + ' '
  127. line = line[ : -1]
  128. #line = u' '.join(
  129. # unicode(word).replace('\\', '\\\\').replace(' ', '\\ ')
  130. # for word in words
  131. # )
  132. def execute(callback = None, errback = None):
  133. if callback is None:
  134. rawCallback = None
  135. else:
  136. rawCallback = lambda result: callback(*self.__formatReply(callback, result))
  137. self.sendCommandRaw(line, rawCallback, errback)
  138. return execute
  139. def sendCommandRaw(self, command, callback = None, errback = None):
  140. if self.__connection is None:
  141. print 'IGNORE command because connection is down:', command
  142. else:
  143. print 'send %d: %s' % (self.__sendSerial, command)
  144. if callback is not None or errback is not None:
  145. assert self.__sendSerial not in self.__callbacks
  146. self.__callbacks[self.__sendSerial] = callback, errback
  147. self.__connection.sendCommand(command)
  148. self.__sendSerial += 1
  149. def _update(self, updateType, name, machine, message):
  150. print 'UPDATE: %s, %s, %s, %s' % (updateType, name, machine, message)
  151. if machine in self.__machinesToIgnore:
  152. print '(ignoring update for machine "%s")' % machine
  153. return
  154. if updateType == 'hardware' and name in self.__machinesToIgnore:
  155. if message == 'add':
  156. print '(ignoring "%s"\'s add event)' % name
  157. elif message == 'remove':
  158. print 'machine "%s" deleted, so, removing from ignore list' % name
  159. self.removeMachineToIgnore(name)
  160. return
  161. # TODO: Should updates use Tcl syntax for their message?
  162. # Right now, they don't.
  163. self.__updateHandlers[str(updateType)](str(name), str(machine), str(message))
  164. def _log(self, level, message):
  165. print 'log', str(level).upper() + ':', message
  166. self.logLine.emit(level, message)
  167. def _reply(self, ok, result):
  168. serial = self.__receiveSerial
  169. self.__receiveSerial += 1
  170. print 'command %d %s: %s' % ( serial, ('FAILED', 'OK')[ok], result )
  171. callback, errback = self.__callbacks.pop(serial, ( None, None ))
  172. if ok:
  173. if callback is None:
  174. print 'nobody cares'
  175. else:
  176. callback(unicode(result))
  177. else:
  178. result = str(result)
  179. if result.endswith('\n'):
  180. result = result[ : -1]
  181. if errback is None:
  182. self._log('warning', result)
  183. else:
  184. errback(result)
  185. def addMachineToIgnore(self, machine):
  186. '''Add a machine to the list of machines for which update
  187. events should be ignored. So far only useful when you are
  188. testing machine configurations.
  189. '''
  190. assert machine not in self.__machinesToIgnore
  191. print 'Adding machine to ignore: "%s"' % machine
  192. self.__machinesToIgnore.append(machine)
  193. def removeMachineToIgnore(self, machine):
  194. '''Remove a machine from the list of machines for which update
  195. events should be ignored. So far only useful when you are
  196. testing machine configurations.
  197. '''
  198. assert machine in self.__machinesToIgnore
  199. print 'Removing machine to ignore: "%s"' % machine
  200. self.__machinesToIgnore.remove(machine)
  201. class ControlHandler(QtXml.QXmlDefaultHandler):
  202. def __init__(self, bridge):
  203. QtXml.QXmlDefaultHandler.__init__(self)
  204. self.__bridge = bridge
  205. self.__attrs = None
  206. self.__message = None
  207. def fatalError(self, exception):
  208. print 'XML parse error: %s' % exception.message()
  209. return False # stop parsing
  210. def startElement(
  211. self, namespaceURI, localName, qName, atts
  212. # pylint: disable-msg=W0613
  213. # We don't need all the arguments, but Qt defines this interface.
  214. ):
  215. self.__attrs = atts
  216. self.__message = ''
  217. return True
  218. def endElement(
  219. self, namespaceURI, localName, qName
  220. # pylint: disable-msg=W0613
  221. # We don't need all the arguments, but Qt defines this interface.
  222. ):
  223. # pylint: disable-msg=W0212
  224. # We use methods from the ControlBridge which are not public.
  225. if qName == 'openmsx-output':
  226. pass
  227. elif qName == 'reply':
  228. self.__bridge._reply(
  229. self.__attrs.value('result') == 'ok',
  230. self.__message
  231. )
  232. elif qName == 'log':
  233. self.__bridge._log(
  234. self.__attrs.value('level'),
  235. self.__message
  236. )
  237. elif qName == 'update':
  238. self.__bridge._update(
  239. self.__attrs.value('type'),
  240. self.__attrs.value('name'),
  241. self.__attrs.value('machine'),
  242. self.__message
  243. )
  244. else:
  245. # TODO: Is it OK to ignore unknown tags?
  246. # Formulate a compatiblity strategy in the CliComm design.
  247. print 'unkown XML tag: %s' % qName
  248. return True
  249. def characters(self, content):
  250. self.__message += content
  251. return True
  252. class ControlConnection(QtCore.QObject):
  253. connectionClosed = Signal()
  254. def __init__(self, bridge):
  255. # pylint: disable-msg=W0212
  256. # We use methods from the ControlBridge which are not public.
  257. QtCore.QObject.__init__(self)
  258. self.__errBuf = ''
  259. self.__logListener = bridge._log
  260. # Create a cyclic reference to avoid being garbage collected during
  261. # signal handling. It will be collected later though.
  262. self.__cycle = self
  263. # Create process for openMSX (but don't start it yet).
  264. self.__process = process = QtCore.QProcess()
  265. # Attach output handlers.
  266. self.__handler = handler = ControlHandler(bridge)
  267. self.__parser = parser = QtXml.QXmlSimpleReader()
  268. parser.setContentHandler(handler)
  269. parser.setErrorHandler(handler)
  270. connect(process, 'error(QProcess::ProcessError)', self.processError)
  271. connect(
  272. process, 'stateChanged(QProcess::ProcessState)',
  273. self.processStateChanged
  274. )
  275. connect(process, 'readyReadStandardOutput()', self.processEvent)
  276. connect(process, 'readyReadStandardError()', self.dumpEvent)
  277. process.setReadChannel(QtCore.QProcess.StandardOutput)
  278. self.__inputSource = None
  279. def start(self):
  280. process = self.__process
  281. # Start the openMSX process.
  282. # TODO: Detect and report errors.
  283. process.start('"' +
  284. preferences['system/executable'] + '" -control stdio',
  285. # 'gdb'
  286. # ' --quiet'
  287. # ' --command=script.gdb'
  288. # ' ~/openmsx/derived/openmsx'
  289. QtCore.QIODevice.ReadWrite |
  290. QtCore.QIODevice.Text |
  291. QtCore.QIODevice.Unbuffered
  292. )
  293. status = process.write('<openmsx-control>\n')
  294. # TODO: Throw I/O exception instead.
  295. assert status != -1
  296. @QtCore.pyqtSignature('QProcess::ProcessError')
  297. def processError(self, error):
  298. print 'process error:', error
  299. if error == QtCore.QProcess.FailedToStart:
  300. self.connectionClosed.emit()
  301. @QtCore.pyqtSignature('QProcess::ProcessState')
  302. def processStateChanged(self, newState):
  303. print 'process entered state', newState, 'error', self.__process.error()
  304. if newState == QtCore.QProcess.NotRunning:
  305. self.connectionClosed.emit()
  306. @QtCore.pyqtSignature('')
  307. def dumpEvent(self):
  308. data = self.__errBuf + str(self.__process.readAllStandardError())
  309. lastNewLine = data.rfind('\n')
  310. if lastNewLine != -1:
  311. lines = data[ : lastNewLine]
  312. data = data[lastNewLine + 1 : ]
  313. print 'reported by openMSX: ', lines
  314. self.__logListener('warning', lines)
  315. self.__errBuf = data
  316. @QtCore.pyqtSignature('')
  317. def processEvent(self):
  318. inputSource = self.__inputSource
  319. first = inputSource is None
  320. if first:
  321. self.__inputSource = inputSource = QtXml.QXmlInputSource()
  322. inputSource.setData(self.__process.readAllStandardOutput())
  323. if first:
  324. ok = self.__parser.parse(self.__inputSource, True)
  325. else:
  326. ok = self.__parser.parseContinue()
  327. assert ok
  328. def sendCommand(self, command):
  329. status = self.__process.write(
  330. QtCore.QString(
  331. '<command>%s</command>\n'
  332. % command.replace('&', '&amp;')
  333. .replace('<', '&lt;').replace('>', '&gt;')
  334. ).toUtf8()
  335. )
  336. # TODO: Throw I/O exception instead.
  337. assert status != -1
  338. #self.__stream.flush()