update to xmlrpc tracker, work in progress
[sface.git] / sface / xmlrpcwindow.py
1 import re
2 from lxml import etree
3 from PyQt4.QtXml import QDomDocument
4 from sface.xmlwidget import XmlWindow, DomModel, XmlView, XmlDelegate, ElemNodeDelegate, TextNodeDelegate, CommentNodeDelegate
5
6 from PyQt4.QtCore import *
7 from PyQt4.QtGui import *
8
9 class XmlrpcReader():
10     def __init__(self):
11         self.rawOutput = None
12         self.responses = []
13         self.calls = []
14
15     def store(self, rawOutput):
16         self.rawOutput = rawOutput
17
18     def paramString(self, item):
19         """
20            Convert an xmlrpc <PARAM> into a human-readable string
21            Is there an easier way?
22         """
23         if item.tag == "string":
24             return item.text
25         elif item.tag == "array":
26             arrayValues = item.xpath("data/value")
27             if arrayValues:
28                 children = list(arrayValues[0])
29                 if children:
30                     return self.paramString(children[0]) + ", ..."
31                 else:
32                     return "[]"
33             else:
34                 return "[]"
35         elif item.tag == "struct":
36             dict = {}
37             members = item.xpath("member")
38             for member in members:
39                 names = member.xpath("name")
40                 values = member.xpath("value")
41                 if (names) and (values):
42                     name = names[0]
43                     value = values[0]
44                     if len(list(value))>0:
45                         data = list(value)[0]
46                         dict[name.text] = self.paramString(list(value)[0])
47             return str(dict)
48         else:
49             return "unknown"
50
51     def parseMethodCall(self, mc):
52         mc = str(mc)
53
54         request = {}
55
56         try:
57             tree = etree.fromstring(mc)
58         except etree.XMLSyntaxError, e:
59             return request
60
61         if tree.tag != "methodCall" or (len(list(tree))==0):
62             return request
63
64         methodNames = tree.xpath("//methodCall/methodName")
65
66         if len(methodNames) == 0:
67             return request
68
69         request["args"] = []
70
71         params = tree.xpath("//methodCall/params/param/value")
72         for param in params:
73             children = list(param)
74             if children:
75                 request["args"].append(self.paramString(children[0]))
76
77         request["methodName"] = methodNames[0].text
78
79         return request
80
81     def parseMethodResponse(self, mr):
82         mr = str(mr) # PyQT supplies a QByteArray; make it a string
83
84         response = {"kind": "unknown"}
85
86         try:
87             tree = etree.fromstring(mr)
88         except etree.XMLSyntaxError, e:
89             print "failed to parse XML response", str(e)
90             #file("badparse.xml","w").write(mr)
91             return response
92
93         if tree.tag != "methodResponse" or (len(list(tree))==0):
94             return response
95
96         # a fault should look like:
97         #     kind: "fault"
98         #     faultCode: "102"
99         #     faultString: "Register: Missing authority..."
100
101         faults = tree.xpath("//methodResponse/fault")
102         for fault in faults:
103             response["kind"] = "fault"
104             structs = fault.xpath("value/struct")
105             for struct in structs:
106                 members = struct.xpath("member")
107                 for member in members:
108                     names = member.xpath("name")
109                     values = member.xpath("value")
110                     if (names) and (values):
111                         name = names[0]
112                         value = values[0]
113                         if len(list(value))>0:
114                             data = list(value)[0]
115                             response[name.text] = data.text
116             # once we have the first fault, return
117             return response
118
119         # whatever didn't fault must have succeeded?
120         # "success" really isn't quite the right word. The best we can say is
121         # that the call was executed. The actual op may or may not have
122         # succeeded.
123         response["kind"] = "executed"
124
125         return response
126
127     def extractXml(self):
128         pttrnAsk = '<methodCall>.*?</methodCall>'
129         pttrnAns = '<methodResponse>.*?</methodResponse>'
130         requests = re.compile(pttrnAsk, re.DOTALL).findall(self.rawOutput)
131         replies = re.compile(pttrnAns, re.DOTALL).findall(self.rawOutput)
132         # cleaning
133         requests = [ x.replace('\\n','\n') for x in requests ]
134         replies = [ x.replace('\\n','\n').replace("'\nbody: '", '').replace("\"\nbody: '", '') for x in replies ]
135         # A well-formed XML document must have one, and only one, top-level element
136
137         self.responses = []
138
139         self.xml = ""
140         for requestXml in requests:
141             self.xml += requestXml
142             callDict = {"request": requestXml}
143             # we could have less responses than calls, so guard the pop
144             if replies:
145                 replyXml = replies.pop(0)
146                 self.xml += replyXml
147
148                 # parse the response to extract fault information
149                 responseDict = self.parseMethodResponse(replyXml)
150                 self.responses.append(responseDict)
151
152                 requestDict = self.parseMethodCall(requestXml)
153
154                 callDict["reply"] = replyXml
155                 callDict.update(responseDict)
156                 callDict.update(requestDict)
157
158             self.calls.append(callDict)
159
160         # just in case somehow we ended up with more responses than calls
161         while replies:
162             replyXml = replies.pop()
163             self.xml += replyXml
164             self.responses.append(self.parseMethodResponse(replyXml))
165
166         return self.xml
167
168     def stats(self):
169         # statistics: round-trip time, size of the com
170         pass
171
172 class XmlrpcTracker(QWidget):
173     def __init__(self, parent=None):
174         QWidget.__init__(self, parent=parent)
175
176         self.reader = XmlrpcReader()
177
178         labelCalls = QLabel("Calls: (from most recent to least recent)")
179
180         self.callTable = QTableWidget(self)
181         self.callTable.setSelectionBehavior(QAbstractItemView.SelectRows)
182         self.callTable.setSelectionMode(QAbstractItemView.SingleSelection)
183         self.callTable.setMaximumHeight(140)
184
185         labelArgs = QLabel("Params:")
186
187         self.argsTable = QTableWidget(self)
188         self.argsTable.setMaximumHeight(140)
189
190         labelXml = QLabel("XMLRPC Contents")
191         self.xmlView = XmlView(self)
192
193         self.delegate = XmlDelegate(self)
194         self.delegate.insertNodeDelegate('element', ElemNodeDelegate())
195         self.delegate.insertNodeDelegate('text', TextNodeDelegate())
196         self.delegate.insertNodeDelegate('comment', CommentNodeDelegate())
197         self.xmlView.setItemDelegate(self.delegate)
198
199         self.layout = QVBoxLayout()
200         self.layout.addWidget(labelCalls)
201         self.layout.addWidget(self.callTable)
202         self.layout.addWidget(labelArgs)
203         self.layout.addWidget(self.argsTable)
204         self.layout.addWidget(labelXml)
205         self.layout.addWidget(self.xmlView)
206
207         self.setLayout(self.layout)
208
209         self.connect(self.callTable, SIGNAL("itemSelectionChanged ()"), self.onCallSelect)
210
211     def getAndPrint(self, rawOutput):
212         self.reader.store(rawOutput)
213         self.reader.extractXml()
214         self.updateCallTable()
215
216     def updateCallTable(self):
217         self.callTable.clear()
218         self.callTable.setColumnCount(2)
219         self.callTable.setHorizontalHeaderLabels(["name", "status"])
220
221         calls = self.reader.calls
222         self.callTable.setRowCount(len(calls))
223
224         row = 0
225         reverse_calls = list(enumerate(calls))
226         reverse_calls.reverse()
227         for (index,call) in reverse_calls:
228             item = QTableWidgetItem(call.get("methodName", "unknown"))
229             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
230             item.setData(Qt.UserRole, index)
231             self.callTable.setItem(row, 0, item)
232
233             item = QTableWidgetItem(call.get("kind", "unknown"))
234             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
235             item.setData(Qt.UserRole, index)
236             self.callTable.setItem(row, 1, item)
237
238             row = row + 1
239
240         self.callTable.resizeColumnsToContents()
241
242     def updateArgsTable(self, args):
243         self.argsTable.clear()
244         self.argsTable.setColumnCount(1)
245         self.argsTable.setHorizontalHeaderLabels(["param"])
246         self.argsTable.horizontalHeader().hide()
247         self.argsTable.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
248
249         self.argsTable.setRowCount(len(args))
250
251         row = 0
252         for arg in args:
253             if len(arg)>255:
254                 arg = arg[:255] + "..."
255
256             item = QTableWidgetItem(arg)
257             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
258             self.argsTable.setItem(row, 0, item)
259
260             row = row + 1
261
262         self.argsTable.resizeColumnsToContents()
263
264     def onCallSelect(self):
265         selItems = self.callTable.selectedItems()
266         if (len(selItems) <= 0):
267             return
268
269         row = selItems[0].data(Qt.UserRole).toInt()[0]
270
271         calls = self.reader.calls
272         if len(calls)<=row:
273             return
274
275         call = calls[row]
276
277         xml = "<debug>" + call.get("request","") + call.get("reply", "") + "</debug>"
278         #xml = call.get("request","") + call.get("reply", "")
279
280         self.displayXml(xml)
281
282         self.updateArgsTable(call.get("args",[]))
283
284     def displayXml(self, xml):
285         self.document = QDomDocument("XMLRPC Tracker")
286         self.document.setContent(xml)
287         self.model = DomModel(self.document, self)
288         self.xmlView.setModel(self.model)
289
290
291 # There's one global tracker for XMLRPCs. Use init_tracker() to create it, and
292 # get_tracker() to get it.
293
294 glo_tracker = None
295
296 def init_tracker(parent):
297     global glo_tracker
298
299     glo_tracker = XmlrpcTracker(parent)
300
301     return glo_tracker
302
303 def get_tracker():
304     global glo_tracker
305
306     return glo_tracker
307
308