1#!/usr/bin/python2 2# SPDX-License-Identifier: GPL-2.0 3# exported-sql-viewer.py: view data from sql database 4# Copyright (c) 2014-2018, Intel Corporation. 5 6# To use this script you will need to have exported data using either the 7# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those 8# scripts for details. 9# 10# Following on from the example in the export scripts, a 11# call-graph can be displayed for the pt_example database like this: 12# 13# python tools/perf/scripts/python/exported-sql-viewer.py pt_example 14# 15# Note that for PostgreSQL, this script supports connecting to remote databases 16# by setting hostname, port, username, password, and dbname e.g. 17# 18# python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example" 19# 20# The result is a GUI window with a tree representing a context-sensitive 21# call-graph. Expanding a couple of levels of the tree and adjusting column 22# widths to suit will display something like: 23# 24# Call Graph: pt_example 25# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) 26# v- ls 27# v- 2638:2638 28# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 29# |- unknown unknown 1 13198 0.1 1 0.0 30# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 31# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 32# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 33# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 34# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 35# >- __libc_csu_init ls 1 10354 0.1 10 0.0 36# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 37# v- main ls 1 8182043 99.6 180254 99.9 38# 39# Points to note: 40# The top level is a command name (comm) 41# The next level is a thread (pid:tid) 42# Subsequent levels are functions 43# 'Count' is the number of calls 44# 'Time' is the elapsed time until the function returns 45# Percentages are relative to the level above 46# 'Branch Count' is the total number of branches for that function and all 47# functions that it calls 48 49import sys 50import weakref 51import threading 52import string 53import cPickle 54import re 55import os 56from PySide.QtCore import * 57from PySide.QtGui import * 58from PySide.QtSql import * 59from decimal import * 60from ctypes import * 61from multiprocessing import Process, Array, Value, Event 62 63# Data formatting helpers 64 65def dsoname(name): 66 if name == "[kernel.kallsyms]": 67 return "[kernel]" 68 return name 69 70# Percent to one decimal place 71 72def PercentToOneDP(n, d): 73 if not d: 74 return "0.0" 75 x = (n * Decimal(100)) / d 76 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) 77 78# Helper for queries that must not fail 79 80def QueryExec(query, stmt): 81 ret = query.exec_(stmt) 82 if not ret: 83 raise Exception("Query failed: " + query.lastError().text()) 84 85# Background thread 86 87class Thread(QThread): 88 89 done = Signal(object) 90 91 def __init__(self, task, param=None, parent=None): 92 super(Thread, self).__init__(parent) 93 self.task = task 94 self.param = param 95 96 def run(self): 97 while True: 98 if self.param is None: 99 done, result = self.task() 100 else: 101 done, result = self.task(self.param) 102 self.done.emit(result) 103 if done: 104 break 105 106# Tree data model 107 108class TreeModel(QAbstractItemModel): 109 110 def __init__(self, root, parent=None): 111 super(TreeModel, self).__init__(parent) 112 self.root = root 113 self.last_row_read = 0 114 115 def Item(self, parent): 116 if parent.isValid(): 117 return parent.internalPointer() 118 else: 119 return self.root 120 121 def rowCount(self, parent): 122 result = self.Item(parent).childCount() 123 if result < 0: 124 result = 0 125 self.dataChanged.emit(parent, parent) 126 return result 127 128 def hasChildren(self, parent): 129 return self.Item(parent).hasChildren() 130 131 def headerData(self, section, orientation, role): 132 if role == Qt.TextAlignmentRole: 133 return self.columnAlignment(section) 134 if role != Qt.DisplayRole: 135 return None 136 if orientation != Qt.Horizontal: 137 return None 138 return self.columnHeader(section) 139 140 def parent(self, child): 141 child_item = child.internalPointer() 142 if child_item is self.root: 143 return QModelIndex() 144 parent_item = child_item.getParentItem() 145 return self.createIndex(parent_item.getRow(), 0, parent_item) 146 147 def index(self, row, column, parent): 148 child_item = self.Item(parent).getChildItem(row) 149 return self.createIndex(row, column, child_item) 150 151 def DisplayData(self, item, index): 152 return item.getData(index.column()) 153 154 def FetchIfNeeded(self, row): 155 if row > self.last_row_read: 156 self.last_row_read = row 157 if row + 10 >= self.root.child_count: 158 self.fetcher.Fetch(glb_chunk_sz) 159 160 def columnAlignment(self, column): 161 return Qt.AlignLeft 162 163 def columnFont(self, column): 164 return None 165 166 def data(self, index, role): 167 if role == Qt.TextAlignmentRole: 168 return self.columnAlignment(index.column()) 169 if role == Qt.FontRole: 170 return self.columnFont(index.column()) 171 if role != Qt.DisplayRole: 172 return None 173 item = index.internalPointer() 174 return self.DisplayData(item, index) 175 176# Table data model 177 178class TableModel(QAbstractTableModel): 179 180 def __init__(self, parent=None): 181 super(TableModel, self).__init__(parent) 182 self.child_count = 0 183 self.child_items = [] 184 self.last_row_read = 0 185 186 def Item(self, parent): 187 if parent.isValid(): 188 return parent.internalPointer() 189 else: 190 return self 191 192 def rowCount(self, parent): 193 return self.child_count 194 195 def headerData(self, section, orientation, role): 196 if role == Qt.TextAlignmentRole: 197 return self.columnAlignment(section) 198 if role != Qt.DisplayRole: 199 return None 200 if orientation != Qt.Horizontal: 201 return None 202 return self.columnHeader(section) 203 204 def index(self, row, column, parent): 205 return self.createIndex(row, column, self.child_items[row]) 206 207 def DisplayData(self, item, index): 208 return item.getData(index.column()) 209 210 def FetchIfNeeded(self, row): 211 if row > self.last_row_read: 212 self.last_row_read = row 213 if row + 10 >= self.child_count: 214 self.fetcher.Fetch(glb_chunk_sz) 215 216 def columnAlignment(self, column): 217 return Qt.AlignLeft 218 219 def columnFont(self, column): 220 return None 221 222 def data(self, index, role): 223 if role == Qt.TextAlignmentRole: 224 return self.columnAlignment(index.column()) 225 if role == Qt.FontRole: 226 return self.columnFont(index.column()) 227 if role != Qt.DisplayRole: 228 return None 229 item = index.internalPointer() 230 return self.DisplayData(item, index) 231 232# Model cache 233 234model_cache = weakref.WeakValueDictionary() 235model_cache_lock = threading.Lock() 236 237def LookupCreateModel(model_name, create_fn): 238 model_cache_lock.acquire() 239 try: 240 model = model_cache[model_name] 241 except: 242 model = None 243 if model is None: 244 model = create_fn() 245 model_cache[model_name] = model 246 model_cache_lock.release() 247 return model 248 249# Find bar 250 251class FindBar(): 252 253 def __init__(self, parent, finder, is_reg_expr=False): 254 self.finder = finder 255 self.context = [] 256 self.last_value = None 257 self.last_pattern = None 258 259 label = QLabel("Find:") 260 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 261 262 self.textbox = QComboBox() 263 self.textbox.setEditable(True) 264 self.textbox.currentIndexChanged.connect(self.ValueChanged) 265 266 self.progress = QProgressBar() 267 self.progress.setRange(0, 0) 268 self.progress.hide() 269 270 if is_reg_expr: 271 self.pattern = QCheckBox("Regular Expression") 272 else: 273 self.pattern = QCheckBox("Pattern") 274 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 275 276 self.next_button = QToolButton() 277 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown)) 278 self.next_button.released.connect(lambda: self.NextPrev(1)) 279 280 self.prev_button = QToolButton() 281 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp)) 282 self.prev_button.released.connect(lambda: self.NextPrev(-1)) 283 284 self.close_button = QToolButton() 285 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) 286 self.close_button.released.connect(self.Deactivate) 287 288 self.hbox = QHBoxLayout() 289 self.hbox.setContentsMargins(0, 0, 0, 0) 290 291 self.hbox.addWidget(label) 292 self.hbox.addWidget(self.textbox) 293 self.hbox.addWidget(self.progress) 294 self.hbox.addWidget(self.pattern) 295 self.hbox.addWidget(self.next_button) 296 self.hbox.addWidget(self.prev_button) 297 self.hbox.addWidget(self.close_button) 298 299 self.bar = QWidget() 300 self.bar.setLayout(self.hbox); 301 self.bar.hide() 302 303 def Widget(self): 304 return self.bar 305 306 def Activate(self): 307 self.bar.show() 308 self.textbox.setFocus() 309 310 def Deactivate(self): 311 self.bar.hide() 312 313 def Busy(self): 314 self.textbox.setEnabled(False) 315 self.pattern.hide() 316 self.next_button.hide() 317 self.prev_button.hide() 318 self.progress.show() 319 320 def Idle(self): 321 self.textbox.setEnabled(True) 322 self.progress.hide() 323 self.pattern.show() 324 self.next_button.show() 325 self.prev_button.show() 326 327 def Find(self, direction): 328 value = self.textbox.currentText() 329 pattern = self.pattern.isChecked() 330 self.last_value = value 331 self.last_pattern = pattern 332 self.finder.Find(value, direction, pattern, self.context) 333 334 def ValueChanged(self): 335 value = self.textbox.currentText() 336 pattern = self.pattern.isChecked() 337 index = self.textbox.currentIndex() 338 data = self.textbox.itemData(index) 339 # Store the pattern in the combo box to keep it with the text value 340 if data == None: 341 self.textbox.setItemData(index, pattern) 342 else: 343 self.pattern.setChecked(data) 344 self.Find(0) 345 346 def NextPrev(self, direction): 347 value = self.textbox.currentText() 348 pattern = self.pattern.isChecked() 349 if value != self.last_value: 350 index = self.textbox.findText(value) 351 # Allow for a button press before the value has been added to the combo box 352 if index < 0: 353 index = self.textbox.count() 354 self.textbox.addItem(value, pattern) 355 self.textbox.setCurrentIndex(index) 356 return 357 else: 358 self.textbox.setItemData(index, pattern) 359 elif pattern != self.last_pattern: 360 # Keep the pattern recorded in the combo box up to date 361 index = self.textbox.currentIndex() 362 self.textbox.setItemData(index, pattern) 363 self.Find(direction) 364 365 def NotFound(self): 366 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found") 367 368# Context-sensitive call graph data model item base 369 370class CallGraphLevelItemBase(object): 371 372 def __init__(self, glb, row, parent_item): 373 self.glb = glb 374 self.row = row 375 self.parent_item = parent_item 376 self.query_done = False; 377 self.child_count = 0 378 self.child_items = [] 379 380 def getChildItem(self, row): 381 return self.child_items[row] 382 383 def getParentItem(self): 384 return self.parent_item 385 386 def getRow(self): 387 return self.row 388 389 def childCount(self): 390 if not self.query_done: 391 self.Select() 392 if not self.child_count: 393 return -1 394 return self.child_count 395 396 def hasChildren(self): 397 if not self.query_done: 398 return True 399 return self.child_count > 0 400 401 def getData(self, column): 402 return self.data[column] 403 404# Context-sensitive call graph data model level 2+ item base 405 406class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): 407 408 def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item): 409 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item) 410 self.comm_id = comm_id 411 self.thread_id = thread_id 412 self.call_path_id = call_path_id 413 self.branch_count = branch_count 414 self.time = time 415 416 def Select(self): 417 self.query_done = True; 418 query = QSqlQuery(self.glb.db) 419 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)" 420 " FROM calls" 421 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 422 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 423 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 424 " WHERE parent_call_path_id = " + str(self.call_path_id) + 425 " AND comm_id = " + str(self.comm_id) + 426 " AND thread_id = " + str(self.thread_id) + 427 " GROUP BY call_path_id, name, short_name" 428 " ORDER BY call_path_id") 429 while query.next(): 430 child_item = CallGraphLevelThreeItem(self.glb, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), int(query.value(5)), self) 431 self.child_items.append(child_item) 432 self.child_count += 1 433 434# Context-sensitive call graph data model level three item 435 436class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): 437 438 def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item): 439 super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item) 440 dso = dsoname(dso) 441 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 442 self.dbid = call_path_id 443 444# Context-sensitive call graph data model level two item 445 446class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): 447 448 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item): 449 super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item) 450 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 451 self.dbid = thread_id 452 453 def Select(self): 454 super(CallGraphLevelTwoItem, self).Select() 455 for child_item in self.child_items: 456 self.time += child_item.time 457 self.branch_count += child_item.branch_count 458 for child_item in self.child_items: 459 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 460 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 461 462# Context-sensitive call graph data model level one item 463 464class CallGraphLevelOneItem(CallGraphLevelItemBase): 465 466 def __init__(self, glb, row, comm_id, comm, parent_item): 467 super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item) 468 self.data = [comm, "", "", "", "", "", ""] 469 self.dbid = comm_id 470 471 def Select(self): 472 self.query_done = True; 473 query = QSqlQuery(self.glb.db) 474 QueryExec(query, "SELECT thread_id, pid, tid" 475 " FROM comm_threads" 476 " INNER JOIN threads ON thread_id = threads.id" 477 " WHERE comm_id = " + str(self.dbid)) 478 while query.next(): 479 child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 480 self.child_items.append(child_item) 481 self.child_count += 1 482 483# Context-sensitive call graph data model root item 484 485class CallGraphRootItem(CallGraphLevelItemBase): 486 487 def __init__(self, glb): 488 super(CallGraphRootItem, self).__init__(glb, 0, None) 489 self.dbid = 0 490 self.query_done = True; 491 query = QSqlQuery(glb.db) 492 QueryExec(query, "SELECT id, comm FROM comms") 493 while query.next(): 494 if not query.value(0): 495 continue 496 child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self) 497 self.child_items.append(child_item) 498 self.child_count += 1 499 500# Context-sensitive call graph data model 501 502class CallGraphModel(TreeModel): 503 504 def __init__(self, glb, parent=None): 505 super(CallGraphModel, self).__init__(CallGraphRootItem(glb), parent) 506 self.glb = glb 507 508 def columnCount(self, parent=None): 509 return 7 510 511 def columnHeader(self, column): 512 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 513 return headers[column] 514 515 def columnAlignment(self, column): 516 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 517 return alignment[column] 518 519 def FindSelect(self, value, pattern, query): 520 if pattern: 521 # postgresql and sqlite pattern patching differences: 522 # postgresql LIKE is case sensitive but sqlite LIKE is not 523 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not 524 # postgresql supports ILIKE which is case insensitive 525 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive 526 if not self.glb.dbref.is_sqlite3: 527 # Escape % and _ 528 s = value.replace("%", "\%") 529 s = s.replace("_", "\_") 530 # Translate * and ? into SQL LIKE pattern characters % and _ 531 trans = string.maketrans("*?", "%_") 532 match = " LIKE '" + str(s).translate(trans) + "'" 533 else: 534 match = " GLOB '" + str(value) + "'" 535 else: 536 match = " = '" + str(value) + "'" 537 QueryExec(query, "SELECT call_path_id, comm_id, thread_id" 538 " FROM calls" 539 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 540 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 541 " WHERE symbols.name" + match + 542 " GROUP BY comm_id, thread_id, call_path_id" 543 " ORDER BY comm_id, thread_id, call_path_id") 544 545 def FindPath(self, query): 546 # Turn the query result into a list of ids that the tree view can walk 547 # to open the tree at the right place. 548 ids = [] 549 parent_id = query.value(0) 550 while parent_id: 551 ids.insert(0, parent_id) 552 q2 = QSqlQuery(self.glb.db) 553 QueryExec(q2, "SELECT parent_id" 554 " FROM call_paths" 555 " WHERE id = " + str(parent_id)) 556 if not q2.next(): 557 break 558 parent_id = q2.value(0) 559 # The call path root is not used 560 if ids[0] == 1: 561 del ids[0] 562 ids.insert(0, query.value(2)) 563 ids.insert(0, query.value(1)) 564 return ids 565 566 def Found(self, query, found): 567 if found: 568 return self.FindPath(query) 569 return [] 570 571 def FindValue(self, value, pattern, query, last_value, last_pattern): 572 if last_value == value and pattern == last_pattern: 573 found = query.first() 574 else: 575 self.FindSelect(value, pattern, query) 576 found = query.next() 577 return self.Found(query, found) 578 579 def FindNext(self, query): 580 found = query.next() 581 if not found: 582 found = query.first() 583 return self.Found(query, found) 584 585 def FindPrev(self, query): 586 found = query.previous() 587 if not found: 588 found = query.last() 589 return self.Found(query, found) 590 591 def FindThread(self, c): 592 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern: 593 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern) 594 elif c.direction > 0: 595 ids = self.FindNext(c.query) 596 else: 597 ids = self.FindPrev(c.query) 598 return (True, ids) 599 600 def Find(self, value, direction, pattern, context, callback): 601 class Context(): 602 def __init__(self, *x): 603 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x 604 def Update(self, *x): 605 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern) 606 if len(context): 607 context[0].Update(value, direction, pattern) 608 else: 609 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None)) 610 # Use a thread so the UI is not blocked during the SELECT 611 thread = Thread(self.FindThread, context[0]) 612 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection) 613 thread.start() 614 615 def FindDone(self, thread, callback, ids): 616 callback(ids) 617 618# Vertical widget layout 619 620class VBox(): 621 622 def __init__(self, w1, w2, w3=None): 623 self.vbox = QWidget() 624 self.vbox.setLayout(QVBoxLayout()); 625 626 self.vbox.layout().setContentsMargins(0, 0, 0, 0) 627 628 self.vbox.layout().addWidget(w1) 629 self.vbox.layout().addWidget(w2) 630 if w3: 631 self.vbox.layout().addWidget(w3) 632 633 def Widget(self): 634 return self.vbox 635 636# Context-sensitive call graph window 637 638class CallGraphWindow(QMdiSubWindow): 639 640 def __init__(self, glb, parent=None): 641 super(CallGraphWindow, self).__init__(parent) 642 643 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) 644 645 self.view = QTreeView() 646 self.view.setModel(self.model) 647 648 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): 649 self.view.setColumnWidth(c, w) 650 651 self.find_bar = FindBar(self, self) 652 653 self.vbox = VBox(self.view, self.find_bar.Widget()) 654 655 self.setWidget(self.vbox.Widget()) 656 657 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") 658 659 def DisplayFound(self, ids): 660 if not len(ids): 661 return False 662 parent = QModelIndex() 663 for dbid in ids: 664 found = False 665 n = self.model.rowCount(parent) 666 for row in xrange(n): 667 child = self.model.index(row, 0, parent) 668 if child.internalPointer().dbid == dbid: 669 found = True 670 self.view.setCurrentIndex(child) 671 parent = child 672 break 673 if not found: 674 break 675 return found 676 677 def Find(self, value, direction, pattern, context): 678 self.view.setFocus() 679 self.find_bar.Busy() 680 self.model.Find(value, direction, pattern, context, self.FindDone) 681 682 def FindDone(self, ids): 683 found = True 684 if not self.DisplayFound(ids): 685 found = False 686 self.find_bar.Idle() 687 if not found: 688 self.find_bar.NotFound() 689 690# Child data item finder 691 692class ChildDataItemFinder(): 693 694 def __init__(self, root): 695 self.root = root 696 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5 697 self.rows = [] 698 self.pos = 0 699 700 def FindSelect(self): 701 self.rows = [] 702 if self.pattern: 703 pattern = re.compile(self.value) 704 for child in self.root.child_items: 705 for column_data in child.data: 706 if re.search(pattern, str(column_data)) is not None: 707 self.rows.append(child.row) 708 break 709 else: 710 for child in self.root.child_items: 711 for column_data in child.data: 712 if self.value in str(column_data): 713 self.rows.append(child.row) 714 break 715 716 def FindValue(self): 717 self.pos = 0 718 if self.last_value != self.value or self.pattern != self.last_pattern: 719 self.FindSelect() 720 if not len(self.rows): 721 return -1 722 return self.rows[self.pos] 723 724 def FindThread(self): 725 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern: 726 row = self.FindValue() 727 elif len(self.rows): 728 if self.direction > 0: 729 self.pos += 1 730 if self.pos >= len(self.rows): 731 self.pos = 0 732 else: 733 self.pos -= 1 734 if self.pos < 0: 735 self.pos = len(self.rows) - 1 736 row = self.rows[self.pos] 737 else: 738 row = -1 739 return (True, row) 740 741 def Find(self, value, direction, pattern, context, callback): 742 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern) 743 # Use a thread so the UI is not blocked 744 thread = Thread(self.FindThread) 745 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection) 746 thread.start() 747 748 def FindDone(self, thread, callback, row): 749 callback(row) 750 751# Number of database records to fetch in one go 752 753glb_chunk_sz = 10000 754 755# size of pickled integer big enough for record size 756 757glb_nsz = 8 758 759# Background process for SQL data fetcher 760 761class SQLFetcherProcess(): 762 763 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep): 764 # Need a unique connection name 765 conn_name = "SQLFetcher" + str(os.getpid()) 766 self.db, dbname = dbref.Open(conn_name) 767 self.sql = sql 768 self.buffer = buffer 769 self.head = head 770 self.tail = tail 771 self.fetch_count = fetch_count 772 self.fetching_done = fetching_done 773 self.process_target = process_target 774 self.wait_event = wait_event 775 self.fetched_event = fetched_event 776 self.prep = prep 777 self.query = QSqlQuery(self.db) 778 self.query_limit = 0 if "$$last_id$$" in sql else 2 779 self.last_id = -1 780 self.fetched = 0 781 self.more = True 782 self.local_head = self.head.value 783 self.local_tail = self.tail.value 784 785 def Select(self): 786 if self.query_limit: 787 if self.query_limit == 1: 788 return 789 self.query_limit -= 1 790 stmt = self.sql.replace("$$last_id$$", str(self.last_id)) 791 QueryExec(self.query, stmt) 792 793 def Next(self): 794 if not self.query.next(): 795 self.Select() 796 if not self.query.next(): 797 return None 798 self.last_id = self.query.value(0) 799 return self.prep(self.query) 800 801 def WaitForTarget(self): 802 while True: 803 self.wait_event.clear() 804 target = self.process_target.value 805 if target > self.fetched or target < 0: 806 break 807 self.wait_event.wait() 808 return target 809 810 def HasSpace(self, sz): 811 if self.local_tail <= self.local_head: 812 space = len(self.buffer) - self.local_head 813 if space > sz: 814 return True 815 if space >= glb_nsz: 816 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer 817 nd = cPickle.dumps(0, cPickle.HIGHEST_PROTOCOL) 818 self.buffer[self.local_head : self.local_head + len(nd)] = nd 819 self.local_head = 0 820 if self.local_tail - self.local_head > sz: 821 return True 822 return False 823 824 def WaitForSpace(self, sz): 825 if self.HasSpace(sz): 826 return 827 while True: 828 self.wait_event.clear() 829 self.local_tail = self.tail.value 830 if self.HasSpace(sz): 831 return 832 self.wait_event.wait() 833 834 def AddToBuffer(self, obj): 835 d = cPickle.dumps(obj, cPickle.HIGHEST_PROTOCOL) 836 n = len(d) 837 nd = cPickle.dumps(n, cPickle.HIGHEST_PROTOCOL) 838 sz = n + glb_nsz 839 self.WaitForSpace(sz) 840 pos = self.local_head 841 self.buffer[pos : pos + len(nd)] = nd 842 self.buffer[pos + glb_nsz : pos + sz] = d 843 self.local_head += sz 844 845 def FetchBatch(self, batch_size): 846 fetched = 0 847 while batch_size > fetched: 848 obj = self.Next() 849 if obj is None: 850 self.more = False 851 break 852 self.AddToBuffer(obj) 853 fetched += 1 854 if fetched: 855 self.fetched += fetched 856 with self.fetch_count.get_lock(): 857 self.fetch_count.value += fetched 858 self.head.value = self.local_head 859 self.fetched_event.set() 860 861 def Run(self): 862 while self.more: 863 target = self.WaitForTarget() 864 if target < 0: 865 break 866 batch_size = min(glb_chunk_sz, target - self.fetched) 867 self.FetchBatch(batch_size) 868 self.fetching_done.value = True 869 self.fetched_event.set() 870 871def SQLFetcherFn(*x): 872 process = SQLFetcherProcess(*x) 873 process.Run() 874 875# SQL data fetcher 876 877class SQLFetcher(QObject): 878 879 done = Signal(object) 880 881 def __init__(self, glb, sql, prep, process_data, parent=None): 882 super(SQLFetcher, self).__init__(parent) 883 self.process_data = process_data 884 self.more = True 885 self.target = 0 886 self.last_target = 0 887 self.fetched = 0 888 self.buffer_size = 16 * 1024 * 1024 889 self.buffer = Array(c_char, self.buffer_size, lock=False) 890 self.head = Value(c_longlong) 891 self.tail = Value(c_longlong) 892 self.local_tail = 0 893 self.fetch_count = Value(c_longlong) 894 self.fetching_done = Value(c_bool) 895 self.last_count = 0 896 self.process_target = Value(c_longlong) 897 self.wait_event = Event() 898 self.fetched_event = Event() 899 glb.AddInstanceToShutdownOnExit(self) 900 self.process = Process(target=SQLFetcherFn, args=(glb.dbref, sql, self.buffer, self.head, self.tail, self.fetch_count, self.fetching_done, self.process_target, self.wait_event, self.fetched_event, prep)) 901 self.process.start() 902 self.thread = Thread(self.Thread) 903 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection) 904 self.thread.start() 905 906 def Shutdown(self): 907 # Tell the thread and process to exit 908 self.process_target.value = -1 909 self.wait_event.set() 910 self.more = False 911 self.fetching_done.value = True 912 self.fetched_event.set() 913 914 def Thread(self): 915 if not self.more: 916 return True, 0 917 while True: 918 self.fetched_event.clear() 919 fetch_count = self.fetch_count.value 920 if fetch_count != self.last_count: 921 break 922 if self.fetching_done.value: 923 self.more = False 924 return True, 0 925 self.fetched_event.wait() 926 count = fetch_count - self.last_count 927 self.last_count = fetch_count 928 self.fetched += count 929 return False, count 930 931 def Fetch(self, nr): 932 if not self.more: 933 # -1 inidcates there are no more 934 return -1 935 result = self.fetched 936 extra = result + nr - self.target 937 if extra > 0: 938 self.target += extra 939 # process_target < 0 indicates shutting down 940 if self.process_target.value >= 0: 941 self.process_target.value = self.target 942 self.wait_event.set() 943 return result 944 945 def RemoveFromBuffer(self): 946 pos = self.local_tail 947 if len(self.buffer) - pos < glb_nsz: 948 pos = 0 949 n = cPickle.loads(self.buffer[pos : pos + glb_nsz]) 950 if n == 0: 951 pos = 0 952 n = cPickle.loads(self.buffer[0 : glb_nsz]) 953 pos += glb_nsz 954 obj = cPickle.loads(self.buffer[pos : pos + n]) 955 self.local_tail = pos + n 956 return obj 957 958 def ProcessData(self, count): 959 for i in xrange(count): 960 obj = self.RemoveFromBuffer() 961 self.process_data(obj) 962 self.tail.value = self.local_tail 963 self.wait_event.set() 964 self.done.emit(count) 965 966# Fetch more records bar 967 968class FetchMoreRecordsBar(): 969 970 def __init__(self, model, parent): 971 self.model = model 972 973 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:") 974 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 975 976 self.fetch_count = QSpinBox() 977 self.fetch_count.setRange(1, 1000000) 978 self.fetch_count.setValue(10) 979 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 980 981 self.fetch = QPushButton("Go!") 982 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 983 self.fetch.released.connect(self.FetchMoreRecords) 984 985 self.progress = QProgressBar() 986 self.progress.setRange(0, 100) 987 self.progress.hide() 988 989 self.done_label = QLabel("All records fetched") 990 self.done_label.hide() 991 992 self.spacer = QLabel("") 993 994 self.close_button = QToolButton() 995 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) 996 self.close_button.released.connect(self.Deactivate) 997 998 self.hbox = QHBoxLayout() 999 self.hbox.setContentsMargins(0, 0, 0, 0) 1000 1001 self.hbox.addWidget(self.label) 1002 self.hbox.addWidget(self.fetch_count) 1003 self.hbox.addWidget(self.fetch) 1004 self.hbox.addWidget(self.spacer) 1005 self.hbox.addWidget(self.progress) 1006 self.hbox.addWidget(self.done_label) 1007 self.hbox.addWidget(self.close_button) 1008 1009 self.bar = QWidget() 1010 self.bar.setLayout(self.hbox); 1011 self.bar.show() 1012 1013 self.in_progress = False 1014 self.model.progress.connect(self.Progress) 1015 1016 self.done = False 1017 1018 if not model.HasMoreRecords(): 1019 self.Done() 1020 1021 def Widget(self): 1022 return self.bar 1023 1024 def Activate(self): 1025 self.bar.show() 1026 self.fetch.setFocus() 1027 1028 def Deactivate(self): 1029 self.bar.hide() 1030 1031 def Enable(self, enable): 1032 self.fetch.setEnabled(enable) 1033 self.fetch_count.setEnabled(enable) 1034 1035 def Busy(self): 1036 self.Enable(False) 1037 self.fetch.hide() 1038 self.spacer.hide() 1039 self.progress.show() 1040 1041 def Idle(self): 1042 self.in_progress = False 1043 self.Enable(True) 1044 self.progress.hide() 1045 self.fetch.show() 1046 self.spacer.show() 1047 1048 def Target(self): 1049 return self.fetch_count.value() * glb_chunk_sz 1050 1051 def Done(self): 1052 self.done = True 1053 self.Idle() 1054 self.label.hide() 1055 self.fetch_count.hide() 1056 self.fetch.hide() 1057 self.spacer.hide() 1058 self.done_label.show() 1059 1060 def Progress(self, count): 1061 if self.in_progress: 1062 if count: 1063 percent = ((count - self.start) * 100) / self.Target() 1064 if percent >= 100: 1065 self.Idle() 1066 else: 1067 self.progress.setValue(percent) 1068 if not count: 1069 # Count value of zero means no more records 1070 self.Done() 1071 1072 def FetchMoreRecords(self): 1073 if self.done: 1074 return 1075 self.progress.setValue(0) 1076 self.Busy() 1077 self.in_progress = True 1078 self.start = self.model.FetchMoreRecords(self.Target()) 1079 1080# SQL data preparation 1081 1082def SQLTableDataPrep(query, count): 1083 data = [] 1084 for i in xrange(count): 1085 data.append(query.value(i)) 1086 return data 1087 1088# SQL table data model item 1089 1090class SQLTableItem(): 1091 1092 def __init__(self, row, data): 1093 self.row = row 1094 self.data = data 1095 1096 def getData(self, column): 1097 return self.data[column] 1098 1099# SQL table data model 1100 1101class SQLTableModel(TableModel): 1102 1103 progress = Signal(object) 1104 1105 def __init__(self, glb, sql, column_count, parent=None): 1106 super(SQLTableModel, self).__init__(parent) 1107 self.glb = glb 1108 self.more = True 1109 self.populated = 0 1110 self.fetcher = SQLFetcher(glb, sql, lambda x, y=column_count: SQLTableDataPrep(x, y), self.AddSample) 1111 self.fetcher.done.connect(self.Update) 1112 self.fetcher.Fetch(glb_chunk_sz) 1113 1114 def DisplayData(self, item, index): 1115 self.FetchIfNeeded(item.row) 1116 return item.getData(index.column()) 1117 1118 def AddSample(self, data): 1119 child = SQLTableItem(self.populated, data) 1120 self.child_items.append(child) 1121 self.populated += 1 1122 1123 def Update(self, fetched): 1124 if not fetched: 1125 self.more = False 1126 self.progress.emit(0) 1127 child_count = self.child_count 1128 count = self.populated - child_count 1129 if count > 0: 1130 parent = QModelIndex() 1131 self.beginInsertRows(parent, child_count, child_count + count - 1) 1132 self.insertRows(child_count, count, parent) 1133 self.child_count += count 1134 self.endInsertRows() 1135 self.progress.emit(self.child_count) 1136 1137 def FetchMoreRecords(self, count): 1138 current = self.child_count 1139 if self.more: 1140 self.fetcher.Fetch(count) 1141 else: 1142 self.progress.emit(0) 1143 return current 1144 1145 def HasMoreRecords(self): 1146 return self.more 1147 1148# SQL automatic table data model 1149 1150class SQLAutoTableModel(SQLTableModel): 1151 1152 def __init__(self, glb, table_name, parent=None): 1153 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz) 1154 if table_name == "comm_threads_view": 1155 # For now, comm_threads_view has no id column 1156 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz) 1157 self.column_headers = [] 1158 query = QSqlQuery(glb.db) 1159 if glb.dbref.is_sqlite3: 1160 QueryExec(query, "PRAGMA table_info(" + table_name + ")") 1161 while query.next(): 1162 self.column_headers.append(query.value(1)) 1163 if table_name == "sqlite_master": 1164 sql = "SELECT * FROM " + table_name 1165 else: 1166 if table_name[:19] == "information_schema.": 1167 sql = "SELECT * FROM " + table_name 1168 select_table_name = table_name[19:] 1169 schema = "information_schema" 1170 else: 1171 select_table_name = table_name 1172 schema = "public" 1173 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'") 1174 while query.next(): 1175 self.column_headers.append(query.value(0)) 1176 super(SQLAutoTableModel, self).__init__(glb, sql, len(self.column_headers), parent) 1177 1178 def columnCount(self, parent=None): 1179 return len(self.column_headers) 1180 1181 def columnHeader(self, column): 1182 return self.column_headers[column] 1183 1184# Base class for custom ResizeColumnsToContents 1185 1186class ResizeColumnsToContentsBase(QObject): 1187 1188 def __init__(self, parent=None): 1189 super(ResizeColumnsToContentsBase, self).__init__(parent) 1190 1191 def ResizeColumnToContents(self, column, n): 1192 # Using the view's resizeColumnToContents() here is extrememly slow 1193 # so implement a crude alternative 1194 font = self.view.font() 1195 metrics = QFontMetrics(font) 1196 max = 0 1197 for row in xrange(n): 1198 val = self.data_model.child_items[row].data[column] 1199 len = metrics.width(str(val) + "MM") 1200 max = len if len > max else max 1201 val = self.data_model.columnHeader(column) 1202 len = metrics.width(str(val) + "MM") 1203 max = len if len > max else max 1204 self.view.setColumnWidth(column, max) 1205 1206 def ResizeColumnsToContents(self): 1207 n = min(self.data_model.child_count, 100) 1208 if n < 1: 1209 # No data yet, so connect a signal to notify when there is 1210 self.data_model.rowsInserted.connect(self.UpdateColumnWidths) 1211 return 1212 columns = self.data_model.columnCount() 1213 for i in xrange(columns): 1214 self.ResizeColumnToContents(i, n) 1215 1216 def UpdateColumnWidths(self, *x): 1217 # This only needs to be done once, so disconnect the signal now 1218 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths) 1219 self.ResizeColumnsToContents() 1220 1221# Table window 1222 1223class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase): 1224 1225 def __init__(self, glb, table_name, parent=None): 1226 super(TableWindow, self).__init__(parent) 1227 1228 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name)) 1229 1230 self.model = QSortFilterProxyModel() 1231 self.model.setSourceModel(self.data_model) 1232 1233 self.view = QTableView() 1234 self.view.setModel(self.model) 1235 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) 1236 self.view.verticalHeader().setVisible(False) 1237 self.view.sortByColumn(-1, Qt.AscendingOrder) 1238 self.view.setSortingEnabled(True) 1239 1240 self.ResizeColumnsToContents() 1241 1242 self.find_bar = FindBar(self, self, True) 1243 1244 self.finder = ChildDataItemFinder(self.data_model) 1245 1246 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) 1247 1248 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 1249 1250 self.setWidget(self.vbox.Widget()) 1251 1252 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table") 1253 1254 def Find(self, value, direction, pattern, context): 1255 self.view.setFocus() 1256 self.find_bar.Busy() 1257 self.finder.Find(value, direction, pattern, context, self.FindDone) 1258 1259 def FindDone(self, row): 1260 self.find_bar.Idle() 1261 if row >= 0: 1262 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) 1263 else: 1264 self.find_bar.NotFound() 1265 1266# Table list 1267 1268def GetTableList(glb): 1269 tables = [] 1270 query = QSqlQuery(glb.db) 1271 if glb.dbref.is_sqlite3: 1272 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name") 1273 else: 1274 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name") 1275 while query.next(): 1276 tables.append(query.value(0)) 1277 if glb.dbref.is_sqlite3: 1278 tables.append("sqlite_master") 1279 else: 1280 tables.append("information_schema.tables") 1281 tables.append("information_schema.views") 1282 tables.append("information_schema.columns") 1283 return tables 1284 1285# Action Definition 1286 1287def CreateAction(label, tip, callback, parent=None, shortcut=None): 1288 action = QAction(label, parent) 1289 if shortcut != None: 1290 action.setShortcuts(shortcut) 1291 action.setStatusTip(tip) 1292 action.triggered.connect(callback) 1293 return action 1294 1295# Typical application actions 1296 1297def CreateExitAction(app, parent=None): 1298 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) 1299 1300# Typical MDI actions 1301 1302def CreateCloseActiveWindowAction(mdi_area): 1303 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) 1304 1305def CreateCloseAllWindowsAction(mdi_area): 1306 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) 1307 1308def CreateTileWindowsAction(mdi_area): 1309 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) 1310 1311def CreateCascadeWindowsAction(mdi_area): 1312 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) 1313 1314def CreateNextWindowAction(mdi_area): 1315 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) 1316 1317def CreatePreviousWindowAction(mdi_area): 1318 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) 1319 1320# Typical MDI window menu 1321 1322class WindowMenu(): 1323 1324 def __init__(self, mdi_area, menu): 1325 self.mdi_area = mdi_area 1326 self.window_menu = menu.addMenu("&Windows") 1327 self.close_active_window = CreateCloseActiveWindowAction(mdi_area) 1328 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) 1329 self.tile_windows = CreateTileWindowsAction(mdi_area) 1330 self.cascade_windows = CreateCascadeWindowsAction(mdi_area) 1331 self.next_window = CreateNextWindowAction(mdi_area) 1332 self.previous_window = CreatePreviousWindowAction(mdi_area) 1333 self.window_menu.aboutToShow.connect(self.Update) 1334 1335 def Update(self): 1336 self.window_menu.clear() 1337 sub_window_count = len(self.mdi_area.subWindowList()) 1338 have_sub_windows = sub_window_count != 0 1339 self.close_active_window.setEnabled(have_sub_windows) 1340 self.close_all_windows.setEnabled(have_sub_windows) 1341 self.tile_windows.setEnabled(have_sub_windows) 1342 self.cascade_windows.setEnabled(have_sub_windows) 1343 self.next_window.setEnabled(have_sub_windows) 1344 self.previous_window.setEnabled(have_sub_windows) 1345 self.window_menu.addAction(self.close_active_window) 1346 self.window_menu.addAction(self.close_all_windows) 1347 self.window_menu.addSeparator() 1348 self.window_menu.addAction(self.tile_windows) 1349 self.window_menu.addAction(self.cascade_windows) 1350 self.window_menu.addSeparator() 1351 self.window_menu.addAction(self.next_window) 1352 self.window_menu.addAction(self.previous_window) 1353 if sub_window_count == 0: 1354 return 1355 self.window_menu.addSeparator() 1356 nr = 1 1357 for sub_window in self.mdi_area.subWindowList(): 1358 label = str(nr) + " " + sub_window.name 1359 if nr < 10: 1360 label = "&" + label 1361 action = self.window_menu.addAction(label) 1362 action.setCheckable(True) 1363 action.setChecked(sub_window == self.mdi_area.activeSubWindow()) 1364 action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x)) 1365 self.window_menu.addAction(action) 1366 nr += 1 1367 1368 def setActiveSubWindow(self, nr): 1369 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) 1370 1371# Font resize 1372 1373def ResizeFont(widget, diff): 1374 font = widget.font() 1375 sz = font.pointSize() 1376 font.setPointSize(sz + diff) 1377 widget.setFont(font) 1378 1379def ShrinkFont(widget): 1380 ResizeFont(widget, -1) 1381 1382def EnlargeFont(widget): 1383 ResizeFont(widget, 1) 1384 1385# Unique name for sub-windows 1386 1387def NumberedWindowName(name, nr): 1388 if nr > 1: 1389 name += " <" + str(nr) + ">" 1390 return name 1391 1392def UniqueSubWindowName(mdi_area, name): 1393 nr = 1 1394 while True: 1395 unique_name = NumberedWindowName(name, nr) 1396 ok = True 1397 for sub_window in mdi_area.subWindowList(): 1398 if sub_window.name == unique_name: 1399 ok = False 1400 break 1401 if ok: 1402 return unique_name 1403 nr += 1 1404 1405# Add a sub-window 1406 1407def AddSubWindow(mdi_area, sub_window, name): 1408 unique_name = UniqueSubWindowName(mdi_area, name) 1409 sub_window.setMinimumSize(200, 100) 1410 sub_window.resize(800, 600) 1411 sub_window.setWindowTitle(unique_name) 1412 sub_window.setAttribute(Qt.WA_DeleteOnClose) 1413 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) 1414 sub_window.name = unique_name 1415 mdi_area.addSubWindow(sub_window) 1416 sub_window.show() 1417 1418# Main window 1419 1420class MainWindow(QMainWindow): 1421 1422 def __init__(self, glb, parent=None): 1423 super(MainWindow, self).__init__(parent) 1424 1425 self.glb = glb 1426 1427 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) 1428 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) 1429 self.setMinimumSize(200, 100) 1430 1431 self.mdi_area = QMdiArea() 1432 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) 1433 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 1434 1435 self.setCentralWidget(self.mdi_area) 1436 1437 menu = self.menuBar() 1438 1439 file_menu = menu.addMenu("&File") 1440 file_menu.addAction(CreateExitAction(glb.app, self)) 1441 1442 edit_menu = menu.addMenu("&Edit") 1443 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find)) 1444 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)])) 1445 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")])) 1446 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")])) 1447 1448 reports_menu = menu.addMenu("&Reports") 1449 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) 1450 1451 self.TableMenu(GetTableList(glb), menu) 1452 1453 self.window_menu = WindowMenu(self.mdi_area, menu) 1454 1455 def Find(self): 1456 win = self.mdi_area.activeSubWindow() 1457 if win: 1458 try: 1459 win.find_bar.Activate() 1460 except: 1461 pass 1462 1463 def FetchMoreRecords(self): 1464 win = self.mdi_area.activeSubWindow() 1465 if win: 1466 try: 1467 win.fetch_bar.Activate() 1468 except: 1469 pass 1470 1471 def ShrinkFont(self): 1472 win = self.mdi_area.activeSubWindow() 1473 ShrinkFont(win.view) 1474 1475 def EnlargeFont(self): 1476 win = self.mdi_area.activeSubWindow() 1477 EnlargeFont(win.view) 1478 1479 def TableMenu(self, tables, menu): 1480 table_menu = menu.addMenu("&Tables") 1481 for table in tables: 1482 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda t=table: self.NewTableView(t), self)) 1483 1484 def NewCallGraph(self): 1485 CallGraphWindow(self.glb, self) 1486 1487 def NewTableView(self, table_name): 1488 TableWindow(self.glb, table_name, self) 1489 1490# Global data 1491 1492class Glb(): 1493 1494 def __init__(self, dbref, db, dbname): 1495 self.dbref = dbref 1496 self.db = db 1497 self.dbname = dbname 1498 self.app = None 1499 self.mainwindow = None 1500 self.instances_to_shutdown_on_exit = weakref.WeakSet() 1501 1502 def AddInstanceToShutdownOnExit(self, instance): 1503 self.instances_to_shutdown_on_exit.add(instance) 1504 1505 # Shutdown any background processes or threads 1506 def ShutdownInstances(self): 1507 for x in self.instances_to_shutdown_on_exit: 1508 try: 1509 x.Shutdown() 1510 except: 1511 pass 1512 1513# Database reference 1514 1515class DBRef(): 1516 1517 def __init__(self, is_sqlite3, dbname): 1518 self.is_sqlite3 = is_sqlite3 1519 self.dbname = dbname 1520 1521 def Open(self, connection_name): 1522 dbname = self.dbname 1523 if self.is_sqlite3: 1524 db = QSqlDatabase.addDatabase("QSQLITE", connection_name) 1525 else: 1526 db = QSqlDatabase.addDatabase("QPSQL", connection_name) 1527 opts = dbname.split() 1528 for opt in opts: 1529 if "=" in opt: 1530 opt = opt.split("=") 1531 if opt[0] == "hostname": 1532 db.setHostName(opt[1]) 1533 elif opt[0] == "port": 1534 db.setPort(int(opt[1])) 1535 elif opt[0] == "username": 1536 db.setUserName(opt[1]) 1537 elif opt[0] == "password": 1538 db.setPassword(opt[1]) 1539 elif opt[0] == "dbname": 1540 dbname = opt[1] 1541 else: 1542 dbname = opt 1543 1544 db.setDatabaseName(dbname) 1545 if not db.open(): 1546 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) 1547 return db, dbname 1548 1549# Main 1550 1551def Main(): 1552 if (len(sys.argv) < 2): 1553 print >> sys.stderr, "Usage is: exported-sql-viewer.py <database name>" 1554 raise Exception("Too few arguments") 1555 1556 dbname = sys.argv[1] 1557 1558 is_sqlite3 = False 1559 try: 1560 f = open(dbname) 1561 if f.read(15) == "SQLite format 3": 1562 is_sqlite3 = True 1563 f.close() 1564 except: 1565 pass 1566 1567 dbref = DBRef(is_sqlite3, dbname) 1568 db, dbname = dbref.Open("main") 1569 glb = Glb(dbref, db, dbname) 1570 app = QApplication(sys.argv) 1571 glb.app = app 1572 mainwindow = MainWindow(glb) 1573 glb.mainwindow = mainwindow 1574 mainwindow.show() 1575 err = app.exec_() 1576 glb.ShutdownInstances() 1577 db.close() 1578 sys.exit(err) 1579 1580if __name__ == "__main__": 1581 Main() 1582