vusb-analyzer-1.1/0000755001313400003110000000000011306026071012606 5ustar micahmtsvusb-analyzer-1.1/VUsbTools/0000755001313400003110000000000011306026071014506 5ustar micahmtsvusb-analyzer-1.1/VUsbTools/Decoders/0000755001313400003110000000000011306026071016236 5ustar micahmtsvusb-analyzer-1.1/VUsbTools/Decoders/Cypress.py0000644001313400003110000003442511132505241020246 0ustar micahmts# # VUsbTools.Decoders.Cypress # Micah Dowty # # Decodes the bootloader interface used by Cypress EZ-USB microcontrollers # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # from VUsbTools import Decode, Struct class FX2Decoder(Decode.ControlDecoder): """Decodes the control endpoint on Cypress FX2 chips. This endpoint has vendor-specific commands, implemented in hardware, to read and write RAM. """ vendorRequests = Struct.EnumDict({ 0xA0: 'FirmwareCommand', }) registerInfo = { 0xE600: ('CPUCS', 'Control & Status'), 0xE601: ('IFCONFIG', 'Interface Configuration'), 0xE602: ('PINFLAGSAB', 'FIFO FLAGA and FLAGB Assignments'), 0xE603: ('PINFLAGSCD', 'FIFO FLAGC and FLAGD Assignments'), 0xE604: ('FIFORESET', 'Restore FIFOS to default state'), 0xE605: ('BREAKPT', 'Breakpoint'), 0xE606: ('BPADDRH', 'Breakpoint Address H'), 0xE607: ('BPADDRL', 'Breakpoint Address L'), 0xE608: ('UART230', '230 Kbaud clock for T0,T1,T2'), 0xE609: ('FIFOPINPOLAR', 'FIFO polarities'), 0xE60A: ('REVID', 'Chip Revision'), 0xE60B: ('REVCTL', 'Chip Revision Control'), 0xE610: ('EP1OUTCFG', 'Endpoint 1-OUT Configuration'), 0xE611: ('EP1INCFG', 'Endpoint 1-IN Configuration'), 0xE612: ('EP2CFG', 'Endpoint 2 Configuration'), 0xE613: ('EP4CFG', 'Endpoint 4 Configuration'), 0xE614: ('EP6CFG', 'Endpoint 6 Configuration'), 0xE615: ('EP8CFG', 'Endpoint 8 Configuration'), 0xE618: ('EP2FIFOCFG', 'Endpoint 2 FIFO configuration'), 0xE619: ('EP4FIFOCFG', 'Endpoint 4 FIFO configuration'), 0xE61A: ('EP6FIFOCFG', 'Endpoint 6 FIFO configuration'), 0xE61B: ('EP8FIFOCFG', 'Endpoint 8 FIFO configuration'), 0xE620: ('EP2AUTOINLENH', 'Endpoint 2 Packet Length H (IN only)'), 0xE621: ('EP2AUTOINLENL', 'Endpoint 2 Packet Length L (IN only)'), 0xE622: ('EP4AUTOINLENH', 'Endpoint 4 Packet Length H (IN only)'), 0xE623: ('EP4AUTOINLENL', 'Endpoint 4 Packet Length L (IN only)'), 0xE624: ('EP6AUTOINLENH', 'Endpoint 6 Packet Length H (IN only)'), 0xE625: ('EP6AUTOINLENL', 'Endpoint 6 Packet Length L (IN only)'), 0xE626: ('EP8AUTOINLENH', 'Endpoint 8 Packet Length H (IN only)'), 0xE627: ('EP8AUTOINLENL', 'Endpoint 8 Packet Length L (IN only)'), 0xE630: ('EP2FIFOPFH', 'EP2 Programmable Flag trigger H'), 0xE631: ('EP2FIFOPFL', 'EP2 Programmable Flag trigger L'), 0xE632: ('EP4FIFOPFH', 'EP4 Programmable Flag trigger H'), 0xE633: ('EP4FIFOPFL', 'EP4 Programmable Flag trigger L'), 0xE634: ('EP6FIFOPFH', 'EP6 Programmable Flag trigger H'), 0xE635: ('EP6FIFOPFL', 'EP6 Programmable Flag trigger L'), 0xE636: ('EP8FIFOPFH', 'EP8 Programmable Flag trigger H'), 0xE637: ('EP8FIFOPFL', 'EP8 Programmable Flag trigger L'), 0xE640: ('EP2ISOINPKTS', 'EP2 (if ISO) IN Packets per frame (1-3)'), 0xE641: ('EP4ISOINPKTS', 'EP4 (if ISO) IN Packets per frame (1-3)'), 0xE642: ('EP6ISOINPKTS', 'EP6 (if ISO) IN Packets per frame (1-3)'), 0xE643: ('EP8ISOINPKTS', 'EP8 (if ISO) IN Packets per frame (1-3)'), 0xE648: ('INPKTEND', 'Force IN Packet End'), 0xE649: ('OUTPKTEND', 'Force OUT Packet End'), 0xE650: ('EP2FIFOIE', 'Endpoint 2 Flag Interrupt Enable'), 0xE651: ('EP2FIFOIRQ', 'Endpoint 2 Flag Interrupt Request'), 0xE652: ('EP4FIFOIE', 'Endpoint 4 Flag Interrupt Enable'), 0xE653: ('EP4FIFOIRQ', 'Endpoint 4 Flag Interrupt Request'), 0xE654: ('EP6FIFOIE', 'Endpoint 6 Flag Interrupt Enable'), 0xE655: ('EP6FIFOIRQ', 'Endpoint 6 Flag Interrupt Request'), 0xE656: ('EP8FIFOIE', 'Endpoint 8 Flag Interrupt Enable'), 0xE657: ('EP8FIFOIRQ', 'Endpoint 8 Flag Interrupt Request'), 0xE658: ('IBNIE', 'IN-BULK-NAK Interrupt Enable'), 0xE659: ('IBNIRQ', 'IN-BULK-NAK interrupt Request'), 0xE65A: ('NAKIE', 'Endpoint Ping NAK interrupt Enable'), 0xE65B: ('NAKIRQ', 'Endpoint Ping NAK interrupt Request'), 0xE65C: ('USBIE', 'USB Int Enables'), 0xE65D: ('USBIRQ', 'USB Interrupt Requests'), 0xE65E: ('EPIE', 'Endpoint Interrupt Enables'), 0xE65F: ('EPIRQ', 'Endpoint Interrupt Requests'), 0xE660: ('GPIFIE', 'GPIF Interrupt Enable'), 0xE661: ('GPIFIRQ', 'GPIF Interrupt Request'), 0xE662: ('USBERRIE', 'USB Error Interrupt Enables'), 0xE663: ('USBERRIRQ', 'USB Error Interrupt Requests'), 0xE664: ('ERRCNTLIM', 'USB Error counter and limit'), 0xE665: ('CLRERRCNT', 'Clear Error Counter EC[3..0]'), 0xE666: ('INT2IVEC', 'Interupt 2 (USB) Autovector'), 0xE667: ('INT4IVEC', 'Interupt 4 (FIFOS & GPIF) Autovector'), 0xE668: ('INTSETUP', 'Interrupt 2&4 Setup'), 0xE670: ('PORTACFG', 'I/O PORTA Alternate Configuration'), 0xE671: ('PORTCCFG', 'I/O PORTC Alternate Configuration'), 0xE672: ('PORTECFG', 'I/O PORTE Alternate Configuration'), 0xE678: ('I2CS', 'Control & Status'), 0xE679: ('I2DAT', 'Data'), 0xE67A: ('I2CTL', 'I2C Control'), 0xE67B: ('EXTAUTODAT1', 'Autoptr1 MOVX access'), 0xE67C: ('EXTAUTODAT2', 'Autoptr2 MOVX access'), 0xE680: ('USBCS', 'USB Control & Status'), 0xE681: ('SUSPEND', 'Put chip into suspend'), 0xE682: ('WAKEUPCS', 'Wakeup source and polarity'), 0xE683: ('TOGCTL', 'Toggle Control'), 0xE684: ('USBFRAMEH', 'USB Frame count H'), 0xE685: ('USBFRAMEL', 'USB Frame count L'), 0xE686: ('MICROFRAME', 'Microframe count, 0-7'), 0xE687: ('FNADDR', 'USB Function address'), 0xE68A: ('EP0BCH', 'Endpoint 0 Byte Count H'), 0xE68B: ('EP0BCL', 'Endpoint 0 Byte Count L'), 0xE68D: ('EP1OUTBC', 'Endpoint 1 OUT Byte Count'), 0xE68F: ('EP1INBC', 'Endpoint 1 IN Byte Count'), 0xE690: ('EP2BCH', 'Endpoint 2 Byte Count H'), 0xE691: ('EP2BCL', 'Endpoint 2 Byte Count L'), 0xE694: ('EP4BCH', 'Endpoint 4 Byte Count H'), 0xE695: ('EP4BCL', 'Endpoint 4 Byte Count L'), 0xE698: ('EP6BCH', 'Endpoint 6 Byte Count H'), 0xE699: ('EP6BCL', 'Endpoint 6 Byte Count L'), 0xE69C: ('EP8BCH', 'Endpoint 8 Byte Count H'), 0xE69D: ('EP8BCL', 'Endpoint 8 Byte Count L'), 0xE6A0: ('EP0CS', 'Endpoint Control and Status'), 0xE6A1: ('EP1OUTCS', 'Endpoint 1 OUT Control and Status'), 0xE6A2: ('EP1INCS', 'Endpoint 1 IN Control and Status'), 0xE6A3: ('EP2CS', 'Endpoint 2 Control and Status'), 0xE6A4: ('EP4CS', 'Endpoint 4 Control and Status'), 0xE6A5: ('EP6CS', 'Endpoint 6 Control and Status'), 0xE6A6: ('EP8CS', 'Endpoint 8 Control and Status'), 0xE6A7: ('EP2FIFOFLGS', 'Endpoint 2 Flags'), 0xE6A8: ('EP4FIFOFLGS', 'Endpoint 4 Flags'), 0xE6A9: ('EP6FIFOFLGS', 'Endpoint 6 Flags'), 0xE6AA: ('EP8FIFOFLGS', 'Endpoint 8 Flags'), 0xE6AB: ('EP2FIFOBCH', 'EP2 FIFO total byte count H'), 0xE6AC: ('EP2FIFOBCL', 'EP2 FIFO total byte count L'), 0xE6AD: ('EP4FIFOBCH', 'EP4 FIFO total byte count H'), 0xE6AE: ('EP4FIFOBCL', 'EP4 FIFO total byte count L'), 0xE6AF: ('EP6FIFOBCH', 'EP6 FIFO total byte count H'), 0xE6B0: ('EP6FIFOBCL', 'EP6 FIFO total byte count L'), 0xE6B1: ('EP8FIFOBCH', 'EP8 FIFO total byte count H'), 0xE6B2: ('EP8FIFOBCL', 'EP8 FIFO total byte count L'), 0xE6B3: ('SUDPTRH', 'Setup Data Pointer high address byte'), 0xE6B4: ('SUDPTRL', 'Setup Data Pointer low address byte'), 0xE6B5: ('SUDPTRCTL', 'Setup Data Pointer Auto Mode'), 0xE6B8: ('SETUPDAT[8]', '8 bytes of SETUP data'), 0xE6C0: ('GPIFWFSELECT', 'Waveform Selector'), 0xE6C1: ('GPIFIDLECS', 'GPIF Done, GPIF IDLE drive mode'), 0xE6C2: ('GPIFIDLECTL', 'Inactive Bus, CTL states'), 0xE6C3: ('GPIFCTLCFG', 'CTL OUT pin drive'), 0xE6C4: ('GPIFADRH', 'GPIF Address H'), 0xE6C5: ('GPIFADRL', 'GPIF Address L'), 0xE6CE: ('GPIFTCB3', 'GPIF Transaction Count Byte 3'), 0xE6CF: ('GPIFTCB2', 'GPIF Transaction Count Byte 2'), 0xE6D0: ('GPIFTCB1', 'GPIF Transaction Count Byte 1'), 0xE6D1: ('GPIFTCB0', 'GPIF Transaction Count Byte 0'), 0xE6D0: ('EP2GPIFTCH', 'EP2 GPIF Transaction Count High'), 0xE6D1: ('EP2GPIFTCL', 'EP2 GPIF Transaction Count Low'), 0xE6D2: ('EP2GPIFFLGSEL', 'EP2 GPIF Flag select'), 0xE6D3: ('EP2GPIFPFSTOP', 'Stop GPIF EP2 transaction on prog. flag'), 0xE6D4: ('EP2GPIFTRIG', 'EP2 FIFO Trigger'), 0xE6D8: ('EP4GPIFTCH', 'EP4 GPIF Transaction Count High'), 0xE6D9: ('EP4GPIFTCL', 'EP4 GPIF Transactionr Count Low'), 0xE6DA: ('EP4GPIFFLGSEL', 'EP4 GPIF Flag select'), 0xE6DB: ('EP4GPIFPFSTOP', 'Stop GPIF EP4 transaction on prog. flag'), 0xE6DC: ('EP4GPIFTRIG', 'EP4 FIFO Trigger'), 0xE6E0: ('EP6GPIFTCH', 'EP6 GPIF Transaction Count High'), 0xE6E1: ('EP6GPIFTCL', 'EP6 GPIF Transaction Count Low'), 0xE6E2: ('EP6GPIFFLGSEL', 'EP6 GPIF Flag select'), 0xE6E3: ('EP6GPIFPFSTOP', 'Stop GPIF EP6 transaction on prog. flag'), 0xE6E4: ('EP6GPIFTRIG', 'EP6 FIFO Trigger'), 0xE6E8: ('EP8GPIFTCH', 'EP8 GPIF Transaction Count High'), 0xE6E9: ('EP8GPIFTCL', 'EP8GPIF Transaction Count Low'), 0xE6EA: ('EP8GPIFFLGSEL', 'EP8 GPIF Flag select'), 0xE6EB: ('EP8GPIFPFSTOP', 'Stop GPIF EP8 transaction on prog. flag'), 0xE6EC: ('EP8GPIFTRIG', 'EP8 FIFO Trigger'), 0xE6F0: ('XGPIFSGLDATH', 'GPIF Data H (16-bit mode only)'), 0xE6F1: ('XGPIFSGLDATLX', 'Read/Write GPIF Data L & trigger transac'), 0xE6F2: ('XGPIFSGLDATLNOX', 'Read GPIF Data L, no transac trigger'), 0xE6F3: ('GPIFREADYCFG', 'Internal RDY,Sync/Async, RDY5CFG'), 0xE6F4: ('GPIFREADYSTAT', 'RDY pin states'), 0xE6F5: ('GPIFABORT', 'Abort GPIF cycles'), 0xE6C6: ('FLOWSTATE', 'Defines GPIF flow state'), 0xE6C7: ('FLOWLOGIC', 'Defines flow/hold decision criteria'), 0xE6C8: ('FLOWEQ0CTL', 'CTL states during active flow state'), 0xE6C9: ('FLOWEQ1CTL', 'CTL states during hold flow state'), 0xE6CA: ('FLOWHOLDOFF', ''), 0xE6CB: ('FLOWSTB', 'CTL/RDY Signal to use as master data strobe'), 0xE6CC: ('FLOWSTBEDGE', 'Defines active master strobe edge'), 0xE6CD: ('FLOWSTBHPERIOD', 'Half Period of output master strobe'), 0xE60C: ('GPIFHOLDAMOUNT', 'Data delay shift'), 0xE67D: ('UDMACRCH', 'CRC Upper byte'), 0xE67E: ('UDMACRCL', 'CRC Lower byte'), 0xE67F: ('UDMACRCQUAL', 'UDMA In only, host terminated use only'), 0xE6F8: ('DBUG', 'Debug'), 0xE6F9: ('TESTCFG', 'Test configuration'), 0xE6FA: ('USBTEST', 'USB Test Modes'), 0xE6FB: ('CT1', 'Chirp Test--Override'), 0xE6FC: ('CT2', 'Chirp Test--FSM'), 0xE6FD: ('CT3', 'Chirp Test--Control Signals'), 0xE6FE: ('CT4', 'Chirp Test--Inputs'), } def decode_FirmwareCommand(self, setup): direction = setup.bitmap & 0x80 != 0 address = setup.wValue length = setup.wLength setup.event.pushDecoded("FX2 %s at 0x%04x (%s)" % ( ('Write', 'Read')[direction], address, self.getAddressDescription(address))) def getAddressDescription(self, address): if address < 0x2000: return "Program Memory" if address < 0xE000: return "INVALID" if address < 0xE200: return "Scratch RAM" if address < 0xE400: return "RESERVED" if address < 0xE480: return "GPIF Waveforms" if address < 0xE600: return "RESERVED" if address < 0xE700: try: return "Register [%s] %s" % self.registerInfo[address] except KeyError: return "Unknown Register" if address < 0xE740: return "UNAVAILABLE" if address < 0xE780: return "EP0 Buffer" if address < 0xE800: return "EP1 Buffer" if address < 0xF000: return "RESERVED" return "EP2/4/6/8 Buffer" def detector(context): # # There's no way to 100% reliably detect an EZ-USB device, # but we can look for its default collection of 9 endpoints. # if context.device and context.descriptors and not context.endpoint: # Do our detection on the device descriptor, but peek around # the list of other descriptors that were returned at the # same time. endpoints = [] for desc in context.descriptors: if desc.type == 'endpoint': endpoints.append(desc.bEndpointAddress) if endpoints == [0x81, 0x82, 0x02, 0x84, 0x04, 0x86, 0x06, 0x88, 0x08]: return FX2Decoder(context.devInstance) vusb-analyzer-1.1/VUsbTools/Decoders/__init__.py0000644001313400003110000000025311132505241020345 0ustar micahmts# # VUsbTools.Decoders # Micah Dowty # # Files here will automatically be loaded as class decoders. # See Decode.DecoderFactory for more information. # vusb-analyzer-1.1/VUsbTools/Decoders/Storage.py0000644001313400003110000001732211132505241020217 0ustar micahmts# # VUsbTools.Decoders.Storage # Micah Dowty # # Decodes the USB bulk-only storage protocol, and portions of SCSI # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # from VUsbTools import Decode, Struct class SCSICommand: """Decodes a SCSI command block""" _opcodes = Struct.EnumDict({ 0x00: 'TEST_UNIT_READY', 0x01: 'REZERO_UNIT', 0x03: 'REQUEST_SENSE', 0x04: 'FORMAT_UNIT', 0x05: 'READ_BLOCKLIMITS', 0x07: 'REASSIGN_BLOCKS', 0x07: 'INIT_ELEMENT_STATUS', 0x08: 'READ(6)', 0x0a: 'WRITE(6)', 0x0a: 'PRINT', 0x0b: 'SEEK(6)', 0x0b: 'SLEW_AND_PRINT', 0x0f: 'READ_REVERSE', 0x10: 'WRITE_FILEMARKS', 0x10: 'SYNC_BUFFER', 0x11: 'SPACE', 0x12: 'INQUIRY', 0x14: 'RECOVER_BUFFERED', 0x15: 'MODE_SELECT', 0x16: 'RESERVE_UNIT', 0x17: 'RELEASE_UNIT', 0x18: 'COPY', 0x19: 'ERASE', 0x1a: 'MODE_SENSE', 0x1b: 'START_UNIT', 0x1b: 'SCAN', 0x1b: 'STOP_PRINT', 0x1c: 'RECV_DIAGNOSTIC', 0x1d: 'SEND_DIAGNOSTIC', 0x1e: 'MEDIUM_REMOVAL', 0x24: 'SET_WINDOW', 0x25: 'GET_WINDOW', 0x25: 'READ_CAPACITY', 0x28: 'READ(10)', 0x29: 'READ_GENERATION', 0x2a: 'WRITE(10)', 0x2b: 'SEEK(10)', 0x2b: 'POSITION_TO_ELEMENT', 0x2d: 'READ_UPDATED_BLOCK', 0x2e: 'WRITE_VERIFY', 0x2f: 'VERIFY', 0x30: 'SEARCH_DATA_HIGH', 0x31: 'SEARCH_DATA_EQUAL', 0x32: 'SEARCH_DATA_LOW', 0x33: 'SET_LIMITS', 0x34: 'PREFETCH', 0x34: 'READ_POSITION', 0x35: 'SYNC_CACHE', 0x36: 'LOCKUNLOCK_CACHE', 0x37: 'READ_DEFECT_DATA', 0x38: 'MEDIUM_SCAN', 0x39: 'COMPARE', 0x3a: 'COPY_VERIFY', 0x3b: 'WRITE_BUFFER', 0x3c: 'READ_BUFFER', 0x3d: 'UPDATE_BLOCK', 0x3e: 'READ_LONG', 0x3f: 'WRITE_LONG', 0x40: 'CHANGE_DEF', 0x41: 'WRITE_SAME', 0x42: 'READ_SUBCHANNEL', 0x43: 'READ_TOC', 0x44: 'READ_HEADER', 0x45: 'PLAY_AUDIO(10)', 0x46: 'GET_CONFIGURATION', 0x47: 'PLAY_AUDIO_MSF', 0x48: 'PLAY_AUDIO_TRACK', 0x49: 'PLAY_AUDIO_RELATIVE', 0x4a: 'GET_EVENT_STATUS_NOTIFICATION', 0x4b: 'PAUSE', 0x4c: 'LOG_SELECT', 0x4d: 'LOG_SENSE', 0x4e: 'STOP_PLAY', 0x51: 'READ_DISC_INFO', 0x52: 'READ_TRACK_INFO', 0x53: 'RESERVE_TRACK', 0x54: 'SEND_OPC_INFORMATION', 0x55: 'MODE_SELECT(10)', 0x56: 'RESERVE_UNIT(10)', 0x57: 'RELEASE_UNIT(10)', 0x5a: 'MODE_SENSE(10)', 0x5b: 'CLOSE_SESSION', 0x5c: 'READ_BUFFER_CAPACITY', 0x5d: 'SEND_CUE_SHEET', 0x5e: 'PERSISTENT_RESERVE_IN', 0x5f: 'PERSISTENT_RESERVE_OUT', 0x88: 'READ(16)', 0x8a: 'WRITE(16)', 0x9e: 'READ_CAPACITY(16)', 0xa0: 'REPORT_LUNS', 0xa1: 'BLANK', 0xa3: 'MAINTENANCE_IN', 0xa4: 'MAINTENANCE_OUT', 0xa3: 'SEND_KEY', 0xa4: 'REPORT_KEY', 0xa5: 'MOVE_MEDIUM', 0xa5: 'PLAY_AUDIO(12)', 0xa6: 'EXCHANGE_MEDIUM', 0xa6: 'LOADCD', 0xa8: 'READ(12)', 0xa9: 'PLAY_TRACK_RELATIVE', 0xaa: 'WRITE(12)', 0xac: 'ERASE(12)', 0xac: 'GET_PERFORMANCE', 0xad: 'READ_DVD_STRUCTURE', 0xae: 'WRITE_VERIFY(12)', 0xaf: 'VERIFY(12)', 0xb0: 'SEARCH_DATA_HIGH(12)', 0xb1: 'SEARCH_DATA_EQUAL(12)', 0xb2: 'SEARCH_DATA_LOW(12)', 0xb3: 'SET_LIMITS(12)', 0xb5: 'REQUEST_VOLUME_ELEMENT_ADDR', 0xb6: 'SEND_VOLUME_TAG', 0xb6: 'SET_STREAMING', 0xb7: 'READ_DEFECT_DATA(12)', 0xb8: 'READ_ELEMENT_STATUS', 0xb8: 'SELECT_CDROM_SPEED', 0xb9: 'READ_CD_MSF', 0xba: 'AUDIO_SCAN', 0xbb: 'SET_CDROM_SPEED', 0xbc: 'SEND_CDROM_XA_DATA', 0xbc: 'PLAY_CD', 0xbd: 'MECH_STATUS', 0xbe: 'READ_CD', 0xbf: 'SEND_DVD_STRUCTURE', }) # For every command we want to decode parameters for, this includes # a struct definition, and a format string that defines a useful # summary of the parameters. The entire struct is included after # the summary line. _structs = { # 'INQUIRY': None, # FIXME: We should decode this. SCSI-2, page 104 (8.2.5) 'READ(6)': ("0x%(length)02x blocks at 0x%(lba)04x", lambda: ( Struct.UInt8('lun'), # FIXME: lun is actually a bitfield Struct.UInt16BE('lba'), Struct.UInt8('length'), Struct.UInt8('control'))), 'READ(10)': ("0x%(length)04x blocks at 0x%(lba)08x", lambda: ( Struct.UInt8('lun'), # FIXME: lun is actually a bitfield Struct.UInt32BE('lba'), Struct.UInt8('reserved_1'), Struct.UInt16BE('length'), Struct.UInt8('control'))), } def __init__(self, cdb): self.header = Struct.Group(None, Struct.UInt8("opcode")) params = self.header.decode(cdb) self.name = self._opcodes[self.header.opcode] if self.name in self._structs: fmt, children = self._structs[self.name] self.params = Struct.Group(None, *children()) self.params.decode(params) self.summary = fmt % self.params.__dict__ else: self.params = None self.summary = '' def __str__(self): return "%s %s" % (self.name, self.summary) class CommandDecoder: """Decodes USBC command blocks""" def handleEvent(self, event): if not event.isDataTransaction(): return if not event.data.startswith("USBC"): return header = Struct.Group(None, Struct.UInt32("sig"), Struct.UInt32("tag"), Struct.UInt32("datalen"), Struct.UInt8("flag"), Struct.UInt8("lun"), Struct.UInt8("cdblen")) cdb = header.decode(event.data) command = SCSICommand(cdb) event.pushDecoded("Storage Command: %s" % command) event.appendDecoded("\n%s" % command.params) class StatusDecoder: """Decodes USBS status blocks""" _statusCodes = Struct.EnumDict({ 0x00: 'ok', 0x01: 'FAILED', 0x02: 'PHASE ERROR', }) def handleEvent(self, event): if not event.isDataTransaction(): return if not event.data.startswith("USBS"): return header = Struct.Group(None, Struct.UInt32("sig"), Struct.UInt32("tag"), Struct.UInt32("residue"), Struct.UInt8("status")) header.decode(event.data) if header.residue: residue = ', residue=%d' % header.residue else: residue = '' event.pushDecoded("Storage Status (%s%s)" % ( self._statusCodes[header.status], residue)) event.appendDecoded("\n%s" % header) def detector(context): if (context.interface and context.endpoint and context.interface.bInterfaceClass == 0x08 and context.interface.bInterfaceSubClass == 0x06 and (context.endpoint.bmAttributes & 3) == 2 ): if context.endpoint.bEndpointAddress & 0x80: return StatusDecoder() else: return CommandDecoder() vusb-analyzer-1.1/VUsbTools/Decoders/Bluetooth.py0000644001313400003110000002733011132505241020560 0ustar micahmts# # VUsbTools.Decoders.Bluetooth # Micah Dowty # # A decoder module for portions of the Bluetooth HCI protocol # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # from VUsbTools import Decode, Struct class ControlDecoder(Decode.ControlDecoder): """This extends the default ControlDecoder with support for HCI commands encapsulated within class-specific control requests. """ classRequests = Struct.EnumDict({ 0x00: 'HCICommand', }) hciOpcodes = { 0x01: ("LC", { 0x0001: "HCI_Inquiry", 0x0002: "HCI_Inquiry_Cancel", 0x0003: "HCI_Periodic_Inquiry_Mode", 0x0004: "HCI_Exit_Periodic_Inquiry_Mode", 0x0005: "HCI_Create_Connection", 0x0006: "HCI_Disconnect", 0x0008: "HCI_Create_Connection_Cancel", 0x0009: "HCI_Accept_Connection_Request", 0x000A: "HCI_Reject_Connection_Request", 0x000B: "HCI_Link_Key_Request_Reply", 0x000C: "HCI_Link_Key_Request_Negative_Reply", 0x000D: "HCI_PIN_Code_Request_Reply", 0x000E: "HCI_PIN_Code_Request_Negative_Reply", 0x000F: "HCI_Change_Connection_Packet_Type", 0x0011: "HCI_Authentication_Requested", 0x0013: "HCI_Set_Connection_Encryption", 0x0015: "HCI_Change_Connection_Link_Key", 0x0017: "HCI_Master_Link_Key", 0x0019: "HCI_Remote_Name_Request", 0x001A: "HCI_Remote_Name_Request_Cancel", 0x001B: "HCI_Read_Remote_Supported_Features", 0x001C: "HCI_Read_Remote_Extended_Features", 0x001D: "HCI_Read_Remote_Version_Information", 0x001F: "HCI_Read_Clock_Offset", 0x0020: "HCI_Read_LMP_Handle", 0x0028: "HCI_Setup_Synchronous_Connection", 0x0029: "HCI_Accept_Synchronous_Connection_Request", 0x002A: "HCI_Reject_Synchronous_Connection_Request", }), 0x02: ("LP", { 0x0001: "HCI_Hold_Mode", 0x0003: "HCI_Sniff_Mode", 0x0004: "HCI_Exit_Sniff_Mode", 0x0005: "HCI_Park_State", 0x0006: "HCI_Exit_Park_State", 0x0007: "HCI_QoS_Setup", 0x0009: "HCI_Role_Discovery", 0x000B: "HCI_Switch_Role", 0x000C: "HCI_Read_Link_Policy_Settings", 0x000D: "HCI_Write_Link_Policy_Settings", 0x000E: "HCI_Read_Default_Link_Policy_Settings", 0x000F: "HCI_Write_Default_Link_Policy_Settings", 0x0010: "HCI_Flow_Specification", }), 0x03: ("C&B", {0x0001: "HCI_Set_Event_Mask", 0x0003: "HCI_Reset", 0x0005: "HCI_Set_Event_Filter", 0x0008: "HCI_Flush", 0x0009: "HCI_Read_PIN_Type", 0x000A: "HCI_Write_PIN_Type", 0x000B: "HCI_Create_New_Unit_Key", 0x000D: "HCI_Read_Stored_Link_Key", 0x0011: "HCI_Write_Stored_Link_Key", 0x0012: "HCI_Delete_Stored_Link_Key", 0x0013: "HCI_Write_Local_Name", 0x0014: "HCI_Read_Local_Name", 0x0015: "HCI_Read_Connection_Accept_Timeout", 0x0016: "HCI_Write_Connection_Accept_Timeout", 0x0017: "HCI_Read_Page_Timeout", 0x0018: "HCI_Write_Page_Timeout", 0x0019: "HCI_Read_Scan_Enable", 0x001A: "HCI_Write_Scan_Enable", 0x001B: "HCI_Read_Page_Scan_Activity", 0x001C: "HCI_Write_Page_Scan_Activity", 0x001D: "HCI_Read_Inquiry_Scan_Activity", 0x001E: "HCI_Write_Inquiry_Scan_Activity", 0x001F: "HCI_Read_Authentication_Enable", 0x0020: "HCI_Write_Authentication_Enable", 0x0021: "HCI_Read_Encryption_Mode", 0x0022: "HCI_Write_Encryption_Mode", 0x0023: "HCI_Read_Class_of_Device", 0x0024: "HCI_Write_Class_of_Device", 0x0025: "HCI_Read_Voice_Setting", 0x0026: "HCI_Write_Voice_Setting", 0x0027: "HCI_Read_Automatic_Flush_Timeout", 0x0028: "HCI_Write_Automatic_Flush_Timeout", 0x0029: "HCI_Read_Num_Broadcast_Transmissions", 0x002A: "HCI_Write_Num_Broadcast_Transmissions", 0x002B: "HCI_Read_Hold_Mode_Activity", 0x002C: "HCI_Write_Hold_Mode_Activity", 0x002D: "HCI_Read_Transmit_Power_Level", 0x002E: "HCI_Read_Synchronous_Flow_Control_Enable", 0x002F: "HCI_Write_Synchronous_Flow_Control_Enable", 0x0031: "HCI_Set_Controller_To_Host_Flow_Control", 0x0033: "HCI_Host_Buffer_Size", 0x0035: "HCI_Host_Number_Of_Completed_Packets", 0x0036: "HCI_Read_Link_Supervision_Timeout", 0x0037: "HCI_Write_Link_Supervision_Timeout", 0x0038: "HCI_Read_Number_Of_Supported_IAC", 0x0039: "HCI_Read_Current_IAC_LAP", 0x003A: "HCI_Write_Current_IAC_LAP", 0x003B: "HCI_Read_Page_Scan_Period_Mode", 0x003C: "HCI_Write_Page_Scan_Period_Mode", 0x003F: "Set_AFH_Host_Channel_Classification", 0x0042: "HCI_Read_Inquiry_Scan_Type", 0x0043: "HCI_Write_Inquiry_Scan_Type", 0x0044: "HCI_Read_Inquiry_Mode", 0x0045: "HCI_Write_Inquiry_Mode", 0x0046: "HCI_Read_Page_Scan_Type", 0x0047: "HCI_Write_Page_Scan_Type", 0x0048: "Read_AFH_Channel_Assignment_Mode", 0x0049: "Write_AFH_Channel_Assignment_Mode", }), 0x04: ("IP", { 0x0001: "HCI_Read_Local_Version_Information", 0x0002: "HCI_Read_Local_Supported_Commands", 0x0003: "HCI_Read_Local_Supported_Features", 0x0004: "HCI_Read_Local_Extended_Features", 0x0005: "HCI_Read_Buffer_Size", 0x0009: "HCI_Read_BD_Addr", }), 0x05: ("SP", { 0x0001: "HCI_Read_Failed_Contact_Counter", 0x0002: "HCI_Reset_Failed_Contact_Counter", 0x0003: "HCI_Read_Link_Quality", 0x0005: "HCI_Read_RSSI", 0x0006: "HCI_Read_AFH_Channel_Map", 0x0007: "HCI_Read_Clock", }), 0x06: ("Test",{0x0001: "HCI_Read_Loopback_Mode", 0x0002: "HCI_Write_Loopback_Mode", 0x0003: "HCI_Enable_Device_Under_Test_Mode", }), } def decode_HCICommand(self, setup): header = Struct.Group(None, Struct.UInt16("opcode"), Struct.UInt8("numParameters"), ) params = header.decode(setup.event.data[8:]) if header.opcode is None: ocf = ogf = None else: ocf = header.opcode & 0x03FF ogf = header.opcode >> 10 ogfName = self.hciOpcodes.get(ogf, (ogf, {}))[0] ocfName = self.hciOpcodes.get(ogf, (ogf, {}))[1].get(ocf, ocf) setup.event.pushDecoded("BT Command: %s [%s]" % (ocfName, ogfName)) class EventDecoder: """This decodes incoming Interrupt traffic from a bluetooth HCI device, representing HCI events. """ eventNames = Struct.EnumDict({ 0x01: 'Inquiry Complete', 0x02: 'Inquiry Result', 0x03: 'Connection Complete', 0x04: 'Connection Request', 0x05: 'Disconnection Complete', 0x06: 'Authentication Complete', 0x07: 'Remote Name Request Complete', 0x08: 'Encryption Change', 0x09: 'Change Connection Link Key Complete', 0x0A: 'Master Link Key Complete', 0x0B: 'Read Remote Supported Features Complete', 0x0C: 'Read Remote Version Information Complete', 0x0D: 'QoS Setup Complete', 0x0E: 'Command Complete', 0x0F: 'Command Status', 0x10: 'Hardware Error', 0x11: 'Flush Occurred', 0x12: 'Role Change', 0x13: 'Number of Completed Packets', 0x14: 'Mode Change', 0x15: 'Return Link Keys', 0x16: 'PIN Code Request', 0x17: 'Link Key Request', 0x18: 'Link Key Notification', 0x19: 'Loopback Command', 0x1A: 'Data Buffer Overflow', 0x1B: 'Max Slots Change', 0x1C: 'Read Clock Offset Complete', 0x1D: 'Connection Packet Type Changed', 0x1E: 'QoS Violation', 0x20: 'Page Scan Repetition Mode Change', 0x21: 'HCI Flow Specification Complete', 0x22: 'Inquiry Result With RSSI', 0x23: 'Read Remote Extended Features Complete', 0x2C: 'Synchronous Connection Complete', 0x2D: 'Synchronous Connection Changed', }) def handleEvent(self, event): if not event.isDataTransaction(): return header = Struct.Group(None, Struct.UInt8("eventCode"), Struct.UInt8("numParameters"), ) params = header.decode(event.data) event.pushDecoded("BT Event: %s" % self.eventNames[header.eventCode]) class ACLDecoder: """This decodes incoming or outgoing Bulk data, which represents ACL transfers via the Bluetooth HCI protocol. """ packetBoundaryNames = [ "reserved_boundary_flag_00", "continuation", "first_packet", "reserved_boundary_flag_11", ] broadcastFlagNames = [ "", " active_slave_broadcast", " parked_slave_broadcast", " reserved_broadcast_flag_11", ] def handleEvent(self, event): if not event.isDataTransaction(): return header = Struct.Group(None, Struct.UInt16("handle"), Struct.UInt16("dataTotalLength"), ) params = header.decode(event.data) if header.handle is None: return boundaryFlag = (header.handle >> 12) & 3 broadcastFlag = (header.handle >> 14) & 3 handle = header.handle & 0x0FFF event.pushDecoded("BT ACL: handle=0x%04x len=0x%04x (%s%s)" % (handle, header.dataTotalLength, self.packetBoundaryNames[boundaryFlag], self.broadcastFlagNames[broadcastFlag])) def detector(context): if (context.device and context.device.bDeviceClass == 0xE0 and context.device.bDeviceSubClass == 0x01 ): if not context.endpoint: return ControlDecoder(context.devInstance) if context.endpoint.bmAttributes is not None: if context.endpoint.bmAttributes & 3 == 3: return EventDecoder() if context.endpoint.bmAttributes & 3 == 2: return ACLDecoder() vusb-analyzer-1.1/VUsbTools/Diff.py0000644001313400003110000002132311132505241015727 0ustar micahmts# # VUsbTools.Diff # Micah Dowty # # Implements UI support for a side-by-side diff mode. # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # from __future__ import division import threading, difflib, gtk, gobject from VUsbTools import Views, Types class BackgroundDiff(threading.Thread): """Runs SeuqenceMatcher.get_matching_blocks in the background, reporting progress periodically as the operation progresses. A callback is run with each match as we find it, in the main thread. """ def __init__(self, a, b, progressQueue, callback): self.a = a self.b = b self.matcher = None self.progressQueue = progressQueue self.callback = callback self.finished = False self.checkpoint = 0 threading.Thread.__init__(self) self.poll() def run(self): try: self.matcher = difflib.SequenceMatcher(None, self.a, self.b) self.matcher.get_matching_blocks() self.finished = True except KeyboardInterrupt: gtk.main_quit() def poll(self): """This runs periodically in the main thread to check on the progress of our diff thread. """ try: # Report all new matches to the callback if self.matcher and self.matcher.matching_blocks: blocks = self.matcher.matching_blocks newCheckpoint = len(blocks) for i in xrange(self.checkpoint, newCheckpoint): self.callback(blocks[i]) self.checkpoint = newCheckpoint # Update progress self.progressQueue.put(("Performing diff...", self.getProgress())) # Schedule the next poll if we're not done if not self.finished: gobject.timeout_add(250, self.poll) except KeyboardInterrupt: gtk.main_quit() def getProgress(self): if self.finished: return 1.0 if not self.matcher: return 0 if not self.matcher.matching_blocks: return 0 i, j, n = self.matcher.matching_blocks[-1] return (i + j) / (len(self.a) + len(self.b)) class DiffStatusColumn: """Uses a TreeView to display '><|' markers indicating the status of lines in a side-by-side diff. This expects to receive raw matches from difflib, rather than our DiffMarker events. """ def __init__(self): self.view = gtk.TreeView() self.view.set_sensitive(False) self.createModel() self.createColumns() self.i = 0 self.j = 0 def createModel(self): self.model = gtk.ListStore(gobject.TYPE_STRING, # 0. Marker text ) self.view.set_model(self.model) def createColumns(self): renderer = gtk.CellRendererText() renderer.set_property("xalign", 0.5) self.view.append_column(gtk.TreeViewColumn( "", renderer, text=0)) def match(self, i, j, n): """Process one match as returned by difflib""" si = self.i sj = self.j # Advance the two pointers in parallel as far as we can # without hitting this match. This indicates lines that # are different on both sides. count = min(i - si, j - sj) if count > 0: self.mark("|", count) si += count sj += count # Now see if we can advance the individual sides separately, # indicating lines on one side that match gaps on the other. count = i - si if count > 0: self.mark("<", count) si += count count = j - sj if count > 0: self.mark(">", count) sj += count # Now the pointers are sync'ed, and we can mark the match itself if n > 0: self.mark("", n) si += n sj += n self.i = si self.j = sj def mark(self, label, count): """Add 'count' copies of the indicated mark to our list""" while count: self.model.set(self.model.append(), 0, label) count -= 1 class DiffWindow: """This is an alternate main window used to compare two log files""" def __init__(self): self.views = (Views.ViewContainer(), Views.ViewContainer()) self.status = Views.StatusMonitor() self.needDiff = True self.diffMatches = {} self.status.completionCallbacks.append(self.loadingFinished) # Two scrolling columns for our transaction lists, # with one non-scrolled column in the middle for our diff markers. # Everything scrolls vertically in unison. listScroll = Views.ScrollContainer(hAxes = (True, False, True)) self.diffStatus = DiffStatusColumn() self.diffStatus.view.set_size_request(20, 1) self.diffStatus.view.set_vadjustment(listScroll.vAdjust[0]) frame = gtk.Frame() frame.add(self.diffStatus.view) listScroll.add(frame, 1, 0, 0) timingBox = gtk.VBox(True) paned = gtk.VPaned() if Views.gnomecanvas: paned.add1(timingBox) paned.add2(listScroll) paned.set_position(250) self.events = {} self.summaries = {} transactionLists = [] for i, view in ( (0, self.views[0]), (2, self.views[1]), ): self.events[view] = [] self.summaries[view] = [] tlist = Views.TransactionList(view) transactionLists.append(tlist) tlist.view.set_hadjustment(listScroll.hAdjust[i]) tlist.view.set_vadjustment(listScroll.vAdjust[0]) tlist.root.set_size_request(1, 1) listScroll.attachEvent(tlist.root) frame = gtk.Frame() frame.add(tlist.root) listScroll.add(frame, i) Views.TransactionDetailWindow(view).connectToList(tlist.view) timing = Views.TimingDiagram(view) self.status.watchCursor(timing.cursor) timingBox.pack_start(timing.root) # When a transaction is hilighted in one view, # try to find a matching transaction in the other view view.hilight.observers.append(self.matchHilights) transactionLists[0].diffPartner = transactionLists[1] transactionLists[1].diffPartner = transactionLists[0] mainvbox = gtk.VBox(False) mainvbox.pack_start(paned, True) mainvbox.pack_start(self.status.statusbar, False) self.window = gtk.Window() self.window.set_default_size(1200,900) self.window.add(mainvbox) self.window.show_all() def handleEvent(self, event, view): if isinstance(event, Types.Transaction): self.events[view].append(event) self.summaries[view].append(event.getDiffSummary()) view.handleEvent(event) def loadingFinished(self): """We're idle. If we still need a diff, start one""" if self.needDiff: self.needDiff = False self.bgDiff = BackgroundDiff(self.summaries[self.views[0]], self.summaries[self.views[1]], self.status.queue, self.diffCallback) self.bgDiff.start() def diffCallback(self, (i, j, n)): """This is called by BackgroundDiff when a new matching block is discovered. The block starts at [i] and [j] in our events/summaries lists, and extends for 'n' elements. """ self.diffStatus.match(i, j, n) if n < 1: return for view, offset, otherView, otherOffset in ( (self.views[0], i, self.views[1], j), (self.views[1], j, self.views[0], i), ): # Generate DiffMarkers to show this match visually view.handleEvent(Types.DiffMarker( (self.events[view][offset], self.events[view][offset + n - 1]), (self.events[otherView][otherOffset], self.events[otherView][otherOffset + n - 1]))) # Correlate each transaction with its match for index in xrange(n): self.diffMatches[self.events[view][offset + index]] = ( otherView, self.events[otherView][otherOffset + index]) def matchHilights(self, transaction): try: otherView, match = self.diffMatches[transaction] except KeyError: pass else: otherView.hilight.setValue(match, (self.matchHilights,)) vusb-analyzer-1.1/VUsbTools/Views.py0000644001313400003110000013666111132505241016170 0ustar micahmts# # VUsbTools.Views # Micah Dowty # # This implements the GTK+ user interface # for vusb-analyzer, with optional support # for gnomecanvas. # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # from __future__ import division import math, Queue, gtk, gobject from VUsbTools import Types, Style try: import gnomecanvas except gnomecanvas: print "Warning: You don't have gnome-canvas (or its python bindings) installed." print " The happy timing diagram will be disabled." gnomecanvas = None class ViewContainer: """A parent for several views. This holds the common hilight object, and broadcasts events to all views. """ def __init__(self): self.hilight = Types.Observable() self.children = [] def handleEvent(self, event): """Add an event to all views""" for child in self.children: child.handleEvent(event) class View: """Abstract base class for views which listen to the incoming event stream. Each view, besides getting a copy of the events, gets to listen to and control the common hilight. """ def __init__(self, container): self.hilight = container.hilight self.hilight.observers.append(self.onHilightChanged) self.root = self.createWidgets() container.children.append(self) def createWidgets(self): """Subclasses must implement this to create their GTK widgets. This returns the root of this view's widget tree. """ raise NotImplementedError def handleEvent(self, event): """Subclasses may implement this to receive event data""" pass def onHilightChanged(self, newHilight): """Called when the hilight has been changed by another view""" pass def notifyHilightChanged(self, newHilight): """Subclasses call this to notify other views that the hilight has changed""" self.hilight.setValue(newHilight, (self.onHilightChanged,)) class CanvasOwner(Types.psyobj): """Helper class for objects that own a canvas""" width = 1 height = 1 expand = (0, 1) def __init__(self): self.canvas = gnomecanvas.Canvas(aa=True) def resize(self, width, height): width = int(width + 0.5) height = int(height + 0.5) self.width = width self.height = height self.canvas.set_size_request(self.expand[0] * width, self.expand[1] * height) self.canvas.set_scroll_region(0, 0, width, height) class Ruler(CanvasOwner): """A ruler marking time divisions horizontally, using a GnomeCanvas""" timeExtent = 0 width = 0 absoluteFrame = None lastFrame = None nextMarkedFrame = None # Constants describing the scale's shape tickScale = (1.0, 20.0) divisions = ((1, 1), (10, 2), (20, 4), (100, 6)) textOffset = (5.5, 5.5) height = 30 def __init__(self, scale=512, initialExtent=10.0): CanvasOwner.__init__(self) self.scale = scale self.resizer = Resizer(self) self.resizeCallbacks = [] self.frameGroup = self.canvas.root().add(gnomecanvas.CanvasGroup) self.extend(initialExtent) def drawSecond(self, s): """Draw one second worth of tick marks and such on the scale""" group = self.canvas.root().add(gnomecanvas.CanvasGroup) w = group.add(gnomecanvas.CanvasText, anchor = gtk.ANCHOR_NW, text = "%.01fs" % s, fill_color = 'black', font = 'sans') self.resizer.track(w, x=(s, 5.5), y=(0, 5.5)) for count, scale in self.divisions: h = self.tickScale[1] / scale w = self.tickScale[0] / scale for i in xrange(0, count): fraction = i / count self.resizer.track(group.add(gnomecanvas.CanvasRect, y1 = 0, y2 = h, fill_color = 'black'), x1=(s+fraction, -w), x2=(s+fraction, w)) def extend(self, s): """Extend the scale to hold at least 's' seconds""" newTimeExtent = int(s + 1 - 1e-10) if newTimeExtent > self.timeExtent: for s in xrange(self.timeExtent , newTimeExtent): self.drawSecond(s) self.timeExtent = newTimeExtent self.resize(newTimeExtent * self.scale, self.height) for f in self.resizeCallbacks: f() def zoom(self, factor): """Zoom in/out by the provided factor, keeping the view centered""" adj = self.canvas.get_hadjustment() oldCenter = adj.value + adj.page_size // 2 self.scale *= factor self.resizer.rescale() self.resize(self.timeExtent * self.scale, self.height) for f in self.resizeCallbacks: f() adj.value = oldCenter * factor - adj.page_size // 2 def markFrame(self, event): """Plot SOF markers on our ruler. We get different sizes of markers for one frame, 100 frames, and 1000 frames. The users can compare these marks with the real-time ruler to assess how fast and how smoothly the virtual frame clock is progressing. """ color = Style.frameMarkerColor.gdkString if self.absoluteFrame is None: # Initialize the system. This is now absolute frame zero, # and we're declaring it to be the first marked frame. self.absoluteFrame = 0 self.nextMarkedFrame = [0, 0] else: # Use the delta in real frame numbers (accounting for rollover) # to update our 'absolute' frame number, which never rolls over. d = event.frame - self.lastFrame if d < 0: d += 1024 elif d == 0: # Duplicate frame, mark it in a different color color = Style.duplicateFrameColor.gdkString self.absoluteFrame += d self.lastFrame = event.frame # Small marks- default h = 3 w = 0.4 # Bigger marks every 100 frames if self.absoluteFrame > self.nextMarkedFrame[1]: self.nextMarkedFrame[1] += 100 h = 10 w = 0.5 # Huge marks every 1000 frames if self.absoluteFrame > self.nextMarkedFrame[0]: self.nextMarkedFrame[0] += 1000 h = 25 w = 0.75 self.resizer.track(self.frameGroup.add(gnomecanvas.CanvasRect, y1 = self.height - h, y2 = self.height, fill_color = color), x1=(event.timestamp, -w), x2=(event.timestamp, w)) class TimingRow(Types.psyobj): """One row of boxes in the timing diagram. This stores canvas coordinates for the row, and keeps a simple collision detection hash. The collision detection hash describes blocks of 1-D spacs using booleans. The hash keys are (origin, size) tuples, where the size must be a power of two. The values are a non-None tag object to indicate a completely occupied block, or None to indicate a block that may be partially occupied. """ def __init__(self, top, bottom): self.hash = {} self.top = top self.bottom = bottom # Determines the depth of our 'tree', and the number of discrete collision # detection steps we see in each second. self.quanta = 0x10000 # This is the largest size we ever record partially-occupied blocks at. # Values that are too large just waste space in the hash, values that # are too small cause us to spend lots of time scanning the hash for # blocks. Must be a power of two. # # Default scan is 1 second at a time self.scanDepth = self.quanta def markInterval(self, a, b, tag=True): """Mark the entire timespan between A and B as occupied""" # Quantize the interval. The quantized numbers can be thought of # as a path to a leaf on our BSP tree, each bit representing a # decision in the tree. i = int(self.quanta * a) j = int(self.quanta * b) # Mark parents of the endpoints themselves as partially full. # We don't need to worry about marking partial blocks below, # because all partial blocks we could possibly get occur # above the endpoints. (I don't have a good way to explain # this without a diagram yet ;) for ep in (i, j): pl = 1 while pl < self.scanDepth: pl <<= 1 self.hash[ep & ~(pl - 1), pl] = None # If our interval is actually less than one quanta, fudge things # a bit and expand it to exactly one quanta. This also gives # us a fast path for these common tiny intervals if i == j: self.hash[i, 1] = tag return l = 1 while 1: # Pop up one level if we've cleared all the nonzero # bits from this level, and it wouldn't cause us to step # past the end of our interval. while (i & l) == 0 and (i + l) < j and l < self.scanDepth: l = l << 1 # Venture deeper in the tree as we approach the end # of the interval, so we don't step past it. while (i + l) > j and l: l = l >> 1 # Our stopping condition is when we can't move any # further without going past the end of the interval. if not l: break # This block we just found is completely full. self.hash[i, l] = tag i += l def intervalOccupied(self, a, b, scale=1): """Return the first tag object found if anyone else has marked intervals that overlap the supplied interval. Otherwise, returns None. """ i = int(self.quanta * a) j = int(self.quanta * b) l = self.scanDepth while i < j: y = self.hash.get((i & ~(l-1), l), False) if y is False: # Nothing here, align to the beginning of the next block. # If we've cleared another zero, move up a level so we # can skip this empty space faster. i = (i & ~(l-1)) + l l2 = l << 1 if (i & (l2-1)) == 0: l = l2 elif y is None: # This may or may not be a collision, scan deeper if l > 1: l = l >> 1 else: # We found a collision, bail out now return y class TimeCursor: """Observes a cursor value, updating the position of a marker widget attached to an arbitrary CanvasOwner. """ previous = 0 def __init__(self, canvasOwner, ruler, cursor): self.ruler = ruler self.cursor = cursor self.widget = canvasOwner.canvas.root().add( gnomecanvas.CanvasRect, x1 = 0, x2 = 1, y1 = 0, y2 = 10000, fill_color = 'red') cursor.observers.append(self.move) self.move(cursor.value) def move(self, t): x = self.ruler.scale * t self.widget.move(x - self.previous, 0) self.previous = x class TimingRowStack(CanvasOwner): """A stack of TimingRows, rendered in a GnomeCanvas""" width = 0 height = 0 rowHeight = 10 rowGap = 5 allocHeight = None def __init__(self, ruler): CanvasOwner.__init__(self) self.ruler = ruler self.rows = [] self.ruler.resizeCallbacks.append(self.onRulerResized) self.onRulerResized() self.newRow() def extend(self, s): """Ensure that our widget can hold at least 's' seconds""" self.ruler.extend(s) def onRulerResized(self): """Called by the ruler when any object resizes it. Change our width to match.""" self.resize(self.ruler.width, self.height) def newRow(self): """Allocate a new row unconditionally""" row = self.newPrivateRow(self.rowHeight) self.rows.append(row) return row def newPrivateRow(self, height): """Allocate a row of specified height, don't store it in self.rows. This is used internally, plus it can be used to allocate 'special' rows that shouldn't show up in pickRow(). """ if self.allocHeight is None: top = self.rowGap // 2 else: top = self.allocHeight + self.rowGap bottom = top + height row = TimingRow(top, bottom) self.allocHeight = bottom h = bottom + self.rowGap if h > self.height: self.resize(self.width, h) return row def pickRow(self, a, b, tag=True): """Pick a row that has the specified interval free, then allocate that interval and return the row. """ for row in self.rows: if not row.intervalOccupied(a, b): row.markInterval(a, b, tag) return row row = self.newRow() row.markInterval(a, b, tag) return row class ScrollContainer(gtk.Table): """This is similar to gdk.ScrollWindow, but allows any number of scroll axes in the horizontal and vertical directions. With the default parameters it can be used to provide an alternative to gtk.ScrollWindow that doesn't require a viewport. It can also be used for more advanced collections of widgets that scroll together. Each entry in hAxes/vAxes defines a position in a grid. The boolean within indicates whether a scroll bar will be present at that position. If not, the adjustment and scroll bar for that position will be None. """ def __init__(self, hAxes=(True,), vAxes=(True,)): gtk.Table.__init__(self, len(hAxes)+1, len(vAxes)+1, False) self.attach(gtk.Label(), len(hAxes)+1, len(hAxes)+2, len(vAxes)+1, len(vAxes)+2, 0, 0) self.hAdjust = [] self.vAdjust = [] self.hScroll = [] self.vScroll = [] for i, enable in enumerate(hAxes): if enable: adj = gtk.Adjustment() self.hAdjust.append(adj) scroll = gtk.HScrollbar() scroll.set_adjustment(adj) self.hScroll.append(scroll) self.attach(scroll, i+1, i+2, len(vAxes)+1, len(vAxes)+2, gtk.FILL | gtk.SHRINK, 0) else: self.hAdjust.append(None) self.hScroll.append(None) for i, enable in enumerate(vAxes): if enable: adj = gtk.Adjustment() self.vAdjust.append(adj) scroll = gtk.VScrollbar() scroll.set_adjustment(adj) self.vScroll.append(scroll) self.attach(scroll, len(hAxes)+1, len(hAxes)+2, i+1, i+2, 0, gtk.FILL | gtk.SHRINK) else: self.vAdjust.append(None) self.vScroll.append(None) def add(self, child, x=0, y=0, xflags=gtk.FILL | gtk.EXPAND | gtk.SHRINK, yflags=gtk.FILL | gtk.EXPAND | gtk.SHRINK): self.attach(child, x+1, x+2, y+1, y+2, xflags, yflags) def attachEvent(self, widget, axis=0, horizontal=False): if horizontal: adj = self.hAdjust[axis] else: adj = self.vAdjust[axis] widget.add_events(gtk.gdk.SCROLL_MASK) widget.connect('scroll-event', self._scrollEvent, adj) def _scrollEvent(self, widget, event, adj): """Process a scroll wheel event against the supplied GtkAdjustment""" if event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_LEFT): inc = -adj.step_increment elif event.direction in (gtk.gdk.SCROLL_DOWN, gtk.gdk.SCROLL_RIGHT): inc = adj.step_increment else: inc = 0 adj.set_value(min(adj.upper - adj.page_size, adj.value + inc)) return False class Resizer: """Since we can't seem to trust gnome-canvas to do anything right, this object tracks gnome-canvas widgets that depend on the ruler scale. When the scale changes, this recalculates their position appropriately. """ def __init__(self, ruler): self.map = {} self.ruler = ruler def track(self, widget, **kw): self.map[widget] = kw s = self.ruler.scale for prop, (a, b) in kw.iteritems(): widget.set_property(prop, a*s + b) def rescale(self): s = self.ruler.scale for widget, kw in self.map.iteritems(): for prop, (a, b) in kw.iteritems(): widget.set_property(prop, a*s + b) class TimingDiagramPipe: """Represents one endpoint on one device in the timing diagram. Graphically, this is managed by a TimingRowStack. This class is responsible for the transaction decoding necessary to convert a stream of transactions into canvas widgets within a TimingRowStack. Subclasses may perform additional class decoding and display that data graphically. """ def __init__(self, view): self.view = view self.stack = TimingRowStack(self.view.ruler) TimeCursor(self.stack, self.view.ruler, self.view.cursor) # Each endpoint should always queue transactions in FIFO order. # We use this fact to pair up and down transactions- each endpoint # gets a queue of 'Down' transactions which we pair up when we get # the corresponding 'Up'. self.queue = Queue.Queue() def handleEvent(self, transaction): """Add a single transaction to this pipe. We queue up and pair transactions, delivering matched down/up pairs to addPair. """ if transaction.dir == 'Down': self.queue.put(transaction) elif transaction.dir == 'Up': try: self.addPair(self.queue.get(False), transaction) except Queue.Empty: print "*** Warning, found an 'Up' transaction with no matching 'Down'." print " This should only occur when reading partial logfiles." def getDataTransaction(self, down, up): """The 'data transaction' is the one we expect to see a useful data stage on. Normally it's Down for OUT endpoints and Up for IN, but on the control pipe this depends on the request type. The exception is that on the contro """ if down.isDataTransaction(): return down else: return up def addPair(self, down, up): """Once we have completed a full transaction, with 'down' and 'up' events, it ends up here. """ dataTransaction = self.getDataTransaction(down, up) self.stack.extend(up.timestamp) # Tag the interval we've chosen. The tag is a list that will # be updated with the canvas item reference once we have it. tag = [None] row = self.stack.pickRow(down.timestamp, up.timestamp, tag) w = self.stack.canvas.root().add( gnomecanvas.CanvasRect, y1 = row.top, y2 = row.bottom, fill_color = Style.getBarColor(dataTransaction).gdkString, outline_color = 'black', width_units = 1.0) self.view.ruler.resizer.track(w, x1=(down.timestamp, 0), x2=(up.timestamp, 0)) tag[0] = w if up.status: # An error occurred, mark it in red x = 2 err = self.stack.canvas.root().add( gnomecanvas.CanvasRect, y1 = row.top - x, y2 = row.bottom + x, fill_color_rgba = Style.errorMarkerColor.rgba) self.view.ruler.resizer.track(err, x1=(up.timestamp, -x), x2=(up.timestamp, x)) # Set up links between the up/down transactions and the widget w.downTransaction = down w.upTransaction = up w.dataTransaction = dataTransaction self.view.transactionWidgets[down] = w self.view.transactionWidgets[up] = w def detectPipeClass(dev, endpoint): """Autodetect a TimingDiagramPipe for a particular device and endpoint. This is a very early and crude start at doing graphical class-specific decoding in here. """ # XXX: Very Experimental, I'm using this to track feedback values on # USB audio devices. #if endpoint == 0x81: # return IntegerDecoderPipe return TimingDiagramPipe class IntegerDecoderPipe(TimingDiagramPipe): """An experimental sort of graphical class decoder. This one creates a line graph of an integer value decoded from each packet. """ def __init__(self, view, range=(0x150000, 0x200000)): TimingDiagramPipe.__init__(self, view) self.range = range self.graphRow = self.stack.newPrivateRow(128) def addPair(self, down, up): TimingDiagramPipe.addPair(self, down, up) # Little endian bytes = up.hexData.split() bytes.reverse() value = int(''.join(bytes), 16) f = (value - self.range[0]) / (self.range[1] - self.range[0]) # White background above the transaction itself self.stack.canvas.root().add( gnomecanvas.CanvasRect, x1 = self.view.ruler.scale * down.timestamp, x2 = self.view.ruler.scale * up.timestamp, y1 = self.graphRow.top, y2 = self.graphRow.bottom, fill_color = 'white') # Dot marking this vertex's position vertex = (self.view.ruler.scale * up.timestamp, self.graphRow.bottom + f * (self.graphRow.top - self.graphRow.bottom)) self.stack.canvas.root().add( gnomecanvas.CanvasRect, x1 = vertex[0] - 2, x2 = vertex[0] + 2, y1 = vertex[1] - 2, y2 = vertex[1] + 2, fill_color = 'black') class TimingDiagram(View): """A view that draws a graphical representation of the time interval occupied by each USB transaction. This object is responsible for the overall layout of the diagram, and for tracking hilights, cursors, and scale. The actual graphical rendering is done for each endpoint by a TimingDiagramPipe. """ hilightWidget = None vPadding = 3 labelPadding = 3 def createWidgets(self): self.transactionWidgets = {} self.pipes = {} self.scroll = ScrollContainer() self.scroll.hAdjust[0].set_property("step-increment", 20) self.cursor = Types.Observable(-1) # The ruler at the top is in charge of our time scale self.ruler = Ruler() TimeCursor(self.ruler, self.ruler, self.cursor) self.ruler.canvas.set_hadjustment(self.scroll.hAdjust[0]) # The top heading holds the ruler, and a spacer aligned with the left heading leftHeadingWidth = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) spacer = gtk.Label() leftHeadingWidth.add_widget(spacer) topHeading = gtk.HBox(False) topHeading.pack_start(spacer, False, padding=self.labelPadding) topHeading.pack_start(gtk.VSeparator(), False) topHeading.pack_start(self.ruler.canvas, True) # Everything except the top heading scrolls vertically using a viewport viewport = gtk.Viewport(None, self.scroll.vAdjust[0]) viewport.set_shadow_type(gtk.SHADOW_NONE) viewport.set_size_request(1, 1) viewportBox = gtk.VBox(False) viewportBox.pack_start(topHeading, False) viewportBox.pack_start(gtk.HSeparator(), False) viewportBox.pack_start(viewport, True) self.scroll.add(viewportBox) # Gnome Canvas has really silly event grabbing semantics. To work around # all this, we'll just grab all events before gnome-canvas sees them. for widget in (viewport, self.ruler.canvas): widget.add_events(gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK) for event in ('motion-notify-event', 'button-press-event', 'scroll-event'): widget.connect_after(event, self.mouseEvent) self.scroll.attachEvent(widget, horizontal=True) # The left heading holds labels for each canvas in the stack self.leftHeading = gtk.VBox(False) leftHeadingWidth.add_widget(self.leftHeading) # The viewport holds the left heading, then the main canvas stack scrolledBox = gtk.HBox(False) self.canvasList = [] self.canvasStack = gtk.VBox(False) scrolledBox.pack_start(self.leftHeading, False, padding=self.labelPadding) scrolledBox.pack_start(gtk.VSeparator(), False) scrolledBox.pack_start(self.canvasStack, True) viewport.add(scrolledBox) return self.scroll def mouseEvent(self, widget, event): """Mouse motion events all get processed here. We move the cursor, then if the mouse button is down we hilight the canvas item under it. This provides two useful user interaction scenarios: either the user can click on a single item they want more information on, or they can drag the mouse through the diagram and see each transaction hilighted in real-time. """ # Zoom in and out with the middle and right mouse buttons if event.type == gtk.gdk.BUTTON_PRESS: if event.button == 2: self.ruler.zoom(2) elif event.button == 3: self.ruler.zoom(0.5) # Use the ruler widget's coordinate system to update the time cursor x, y, mask = self.ruler.canvas.window.get_pointer() scroll = self.ruler.canvas.get_scroll_offsets()[0] t = (x + scroll) / self.ruler.scale self.cursor.value = t # If the mouse button is down, try to find the canvas item under the cursor. # We use the row's collision detection tree for this, for the same reason # we use it for everything else: gnome-canvas' built-in collision detection # works poorly on very small items. if mask & gtk.gdk.BUTTON1_MASK: # Search every row in every canvas for obj in self.canvasList: y = obj.canvas.get_pointer()[1] for row in obj.rows: if y >= row.top and y <= row.bottom: # Give a few pixels of slack on either side slack = 2.0 / self.ruler.scale cursorInterval = (t - slack, t + slack) # The mouse is in this row. Use the row's collision detection # to find a nearby item. tag = row.intervalOccupied(*cursorInterval) if tag and tag[0] != self.hilightWidget: self.notifyHilightChanged(tag[0].dataTransaction) self.setHilightWidget(tag[0]) return False return False def appendCanvas(self, label, obj): """Add a new canvas-bearing object to our stack of scrolling widgets""" obj.canvas.set_hadjustment(self.scroll.hAdjust[0]) labelWidget = gtk.Label(label) labelWidget.set_alignment(0, 0) group = gtk.SizeGroup(gtk.SIZE_GROUP_VERTICAL) group.add_widget(labelWidget) group.add_widget(obj.canvas) self.leftHeading.pack_start(labelWidget, False, padding=self.vPadding) self.canvasStack.pack_start(obj.canvas, False, padding=self.vPadding) self.leftHeading.pack_start(gtk.HSeparator(), False) self.canvasStack.pack_start(gtk.HSeparator(), False) self.leftHeading.show_all() self.canvasStack.show_all() self.canvasList.append(obj) def handleEvent(self, event): # Find a pipe to send transactions to if isinstance(event, Types.Transaction): key = event.dev, event.endpt try: pipe = self.pipes[key] except KeyError: pipe = self.createPipe(event) self.pipes[key] = pipe pipe.handleEvent(event) # Let the ruler display SOF markers elif isinstance(event, Types.SOFMarker): self.ruler.markFrame(event) # Mark matching regions in diffs. We have a translucent fill over # the entire matching area, and hard edges at the matching area's # boundaries. This makes it easy to see subtle boundaries in adjacent # diff matches. elif isinstance(event, Types.DiffMarker): for obj in self.canvasList: self.ruler.resizer.track(obj.canvas.root().add( gnomecanvas.CanvasRect, y1 = -50, y2 = 10000, fill_color_rgba = Style.diffMarkerColor.rgba, outline_color_rgba = Style.diffBorderColor.rgba, width_units = 1.0, ), x1 = (event.timestamp, -1.5), x2 = (event.matches[-1].timestamp, 1.5)) def createPipe(self, transaction): """Return a new pipe created for the given transaction""" pipe = detectPipeClass(transaction.dev, transaction.endpt)(self) name = "Dev %s, %s" % (transaction.dev, transaction.getTransferString()) self.appendCanvas(name, pipe.stack) return pipe def onHilightChanged(self, hilight): if hilight: # Move the cursor, and scroll to the hilighted # location. We try to keep at least 'margin' pixels # between the cursor and the edge of the viewport. self.cursor.value = hilight.timestamp x = int(self.ruler.scale * hilight.timestamp) margin = 50 self.scroll.hAdjust[0].clamp_page(x-margin, x+margin) # If this transaction corresponds to a widget, hilight that try: w = self.transactionWidgets[hilight] except KeyError: pass else: self.setHilightWidget(w) def setHilightWidget(self, widget): if self.hilightWidget: self.hilightWidget.set(width_units=1) self.hilightWidget = widget self.hilightWidget.set(width_units=3) class TransactionList(View): """A view that displays a table, each line showing a summary of a single transaction. """ settingCursor = False diffPartner = None followLog = False def createWidgets(self): self.view = gtk.TreeView() self.view.set_rules_hint(True) self.view.connect("cursor-changed", self.onCursorChanged) self.selectionInfo = Types.Observable() self.onSelectionChanged(self.view.get_selection()) # Allow multiple select, and hook up a context menu self.view.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.view.get_selection().connect("changed", self.onSelectionChanged) self.view.connect("button-press-event", self.onButtonPressed) self.createModel() self.createColumns() self.eventToIter = {} self.pipes = {} return self.view def onCursorChanged(self, view): """This is called when the cursor (the single row the user is focused on) changes. Propagate that change to other views. """ if not self.settingCursor: row = view.get_cursor()[0] i = self.model.get_iter(row) event = self.model.get(i, 9)[0] self.notifyHilightChanged(event) def onSelectionChanged(self, selection): """When the set of selected rows change, update the summary data in the progress bar. """ rows = selection.get_selected_rows()[1] if not rows: self.selectionInfo.value = "No Selection" return firstEvent = self.model.get(self.model.get_iter(rows[0]), 9)[0] lastEvent = self.model.get(self.model.get_iter(rows[-1]), 9)[0] timespan = lastEvent.timestamp - firstEvent.timestamp # Calculate the total amount of data. For consistency, this # ignores SETUP packets just like saveSelectedData() does. dataSize = 0 for row in self.view.get_selection().get_selected_rows()[1]: iter = self.model.get_iter(row) event = self.model.get(iter, 9)[0] if event.isDataTransaction(): dataSize += event.datalen or 0 kb = dataSize / 1024.0 if timespan > 0: bandwidth = "%.03f" % (kb / timespan) else: bandwidth = 'inf' self.selectionInfo.value = "%.03f kB, %.06f s, %s kB/s" % ( kb, timespan, bandwidth) def onHilightChanged(self, hilight): self.settingCursor = True iter = self.eventToIter[hilight] self.view.set_cursor(self.model.get_path(iter)) self.settingCursor = False def onButtonPressed(self, view, event): if event.button == 3: self.createMenu().popup(None, None, None, event.button, event.time) return True return False def createMenu(self): menu = gtk.Menu() item = gtk.MenuItem("Select _All") menu.append(item) item.connect("activate", self.selectAll) item.show() item = gtk.MenuItem("Save Selected Data...") menu.append(item) item.connect("activate", self.saveSelectedData) item.show() item = gtk.CheckMenuItem("Follow Log") item.set_active(self.followLog) menu.append(item) item.connect("activate", self.followLogToggled) item.show() item = gtk.MenuItem("Filter") item.set_submenu(self.createFilterMenu()) menu.append(item) item.show() return menu def selectAll(self, widget): self.view.get_selection().select_all() def followLogToggled(self, widget): self.followLog = widget.get_active() def saveSelectedData(self, widget): dialog = gtk.FileChooserDialog( title = "Save Raw Transaction Data", action = gtk.FILE_CHOOSER_ACTION_SAVE, buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT)) dialog.set_default_response(gtk.RESPONSE_ACCEPT) if dialog.run() == gtk.RESPONSE_ACCEPT: f = open(dialog.get_filename(), "wb") for row in self.view.get_selection().get_selected_rows()[1]: iter = self.model.get_iter(row) event = self.model.get(iter, 9)[0] if event.hasSetupData(): f.write(event.data[8:]) else: f.write(event.data) dialog.destroy() def createFilterMenu(self): menu = gtk.Menu() for dir in ('Down', 'Up'): item = gtk.MenuItem(dir) menu.append(item) item.connect("activate", self.filterSelection, lambda event, dir=dir: event.dir == dir) item.show() pipes = self.pipes.keys() pipes.sort() for dev, transfer, endpt in pipes: item = gtk.MenuItem("Dev %s, %s" % (dev, transfer)) menu.append(item) item.connect("activate", self.filterSelection, lambda event, dev=dev, endpt=endpt: event.dev == dev and event.endpt == endpt) item.show() return menu def filterSelection(self, widget, callback): """Filter our current selection through a callback function that accepts a Transaction and returns a boolean indicating whether it should still be selected. """ for row in self.view.get_selection().get_selected_rows()[1]: iter = self.model.get_iter(row) event = self.model.get(iter, 9)[0] if not callback(event): self.view.get_selection().unselect_iter(iter) def createModel(self): self.model = gtk.ListStore(gobject.TYPE_STRING, # 0. Time gobject.TYPE_STRING, # 1. Device gobject.TYPE_STRING, # 2. Transfer gobject.TYPE_STRING, # 3. Transfer icon gobject.TYPE_STRING, # 4. Setup gobject.TYPE_STRING, # 5. Data (summary) gobject.TYPE_STRING, # 6. Decoded (summary) gobject.TYPE_STRING, # 7. Color name gobject.TYPE_STRING, # 8. Data length gobject.TYPE_PYOBJECT, # 9. Event object ) self.view.set_model(self.model) def createColumns(self): column = gtk.TreeViewColumn("Transfer") renderer = gtk.CellRendererPixbuf() column.pack_start(renderer, False) column.set_attributes(renderer, stock_id=3) renderer = gtk.CellRendererText() column.pack_start(renderer, True) column.set_attributes(renderer, text=2) self.view.append_column(column) self.view.append_column(gtk.TreeViewColumn( "Time", gtk.CellRendererText(), text=0)) self.view.append_column(gtk.TreeViewColumn( "Device", gtk.CellRendererText(), text=1)) self.view.append_column(gtk.TreeViewColumn( "Length", gtk.CellRendererText(), text=8)) renderer = gtk.CellRendererText() renderer.set_property("font", Style.monospaceFont) self.view.append_column(gtk.TreeViewColumn( "Setup", renderer, text=4)) renderer = gtk.CellRendererText() renderer.set_property("font", Style.monospaceFont) self.view.append_column(gtk.TreeViewColumn( "Data", renderer, text=5, foreground=7)) renderer = gtk.CellRendererText() renderer.set_property("font", Style.monospaceFont) self.view.append_column(gtk.TreeViewColumn( "Decoded", renderer, text=6)) def handleEvent(self, event): if isinstance(event, Types.Transaction): transfer = event.getTransferString() self.pipes[event.dev, transfer, event.endpt] = True timeString = " %.06fs " % event.timestamp if event.frame is not None: timeString += "fr.%d " % event.frame if event.lineNumber is not None: timeString += ":%d " % event.lineNumber if event.status and event.dir == 'Up': # Error! Right now we don't have anywhere better to # display this than the data field. dataSummary = 'Status: %s' % event.status color = Style.errorMarkerColor.gdkString else: dataSummary = event.getHexDump(summarize=True) color = Style.directionColors[event.dir].gdkString rowIter = self.model.append() self.model.set(rowIter, 0, timeString, 1, str(event.dev), 2, transfer, 3, Style.directionIcons[event.dir], 4, (event.getHexSetup() or '') + " ", 5, dataSummary, 6, event.decodedSummary, 7, color, 8, "0x%04X" % (event.datalen or 0), 9, event, ) # Save a mapping from event to row iter, which we # expect to remain valid in a GtkListStore. self.eventToIter[event] = rowIter if self.followLog: self.hilight.setValue(event) # Any time we get a diff marker, check whether we need # to add padding to catch up to our partner's position. elif isinstance(event, Types.DiffMarker) and self.diffPartner: self.alignEvents(event.matches[0], event.matchedWith[0], self.diffPartner) def alignEvents(self, ourEvent, otherEvent, otherView): """Align ourEvent in this view with otherEvent in the otherView by inserting blank lines. If ourEvent is already ahead of otherEvent, this does nothing- it's the otherView's responsibility to catch up. """ ourIter = self.eventToIter[ourEvent] ourRow = self.model.get_path(ourIter)[0] otherRow = otherView.model.get_path(otherView.eventToIter[otherEvent])[0] padding = otherRow - ourRow while padding > 0: self.model.insert_before(ourIter) padding -= 1 class ScrolledTransactionList(TransactionList): """Adds a ScrolledWindow to TransactionList""" def createWidgets(self): view = TransactionList.createWidgets(self) root = gtk.ScrolledWindow() root.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) root.add(view) # Give ourselves focus by default- currently there's nothing else # usefully focusable, and this lets the user navigate between # transactions (even when they're only paying attention to other views) # using the keyboard. view.grab_focus() return root class TransactionDetail(View): """A view that displays only the hilighted transaction, but in full detail. """ def createWidgets(self): self.dataDetail = gtk.Label() self.dataDetail.set_alignment(0, 0) self.dataDetail.set_selectable(True) self.decodedDetail = gtk.Label() self.decodedDetail.set_alignment(0, 0) self.decodedDetail.set_selectable(True) scroll1 = gtk.ScrolledWindow() scroll1.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scroll1.add_with_viewport(self.dataDetail) scroll2 = gtk.ScrolledWindow() scroll2.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scroll2.add_with_viewport(self.decodedDetail) paned = gtk.HPaned() paned.add1(scroll1) paned.add2(scroll2) paned.set_position(600) return paned def onHilightChanged(self, transaction): self.dataDetail.set_markup(Style.toMonospaceMarkup(transaction.getHexDump())) self.decodedDetail.set_markup(Style.toMonospaceMarkup(transaction.decoded)) class TransactionDetailWindow(TransactionDetail): """A TransactionDetail view that opens in a separate window""" def createWidgets(self): detail = TransactionDetail.createWidgets(self) detail.show_all() window = gtk.Window() window.set_default_size(900, 200) window.add(detail) window.connect("delete-event", self.onDelete) window.set_title("Transaction Detail - VUsb Analyzer") self.visible = False return window def onDelete(self, widget, event): self.hide() return True def show(self): self.visible = True self.onHilightChanged(self.hilight.value) self.root.present() def hide(self): self.visible = False self.root.hide() def onHilightChanged(self, transaction): if self.visible: TransactionDetail.onHilightChanged(self, transaction) def connectToList(self, view): """When a row in the provided treeview is activated, show the detail window""" view.connect("row-activated", lambda widget, row, column: self.show()) class StatusMonitor: """Manages a status bar and progress bar. Progress events from any number of sources, from any thread, are processed by a queue here. Events are of the form (source, progress) where source is a descriptive name identifying the progress reporter, and 'progress' is a number in the range [0, 1]. """ interval = 500 def __init__(self): self.queue = Queue.Queue() self.sources = {} self.completionCallbacks = [] self.statusbar = gtk.Statusbar() self.progress = gtk.ProgressBar() self.progressContext = self.statusbar.get_context_id("Progress") self.statusbar.pack_end(self.progress, False) self.cursorLabel = gtk.Label() frame = gtk.Frame() frame.add(self.cursorLabel) self.statusbar.pack_end(frame, False) self.selectionLabel = gtk.Label() frame = gtk.Frame() frame.add(self.selectionLabel) self.statusbar.pack_end(frame, False) self.poll() def poll(self): try: needUpdate = False try: while 1: source, progress = self.queue.get(False) self.sources[source] = progress needUpdate = True except Queue.Empty: pass if needUpdate: self.update() gobject.timeout_add(self.interval, self.poll) except KeyboardInterrupt: gtk.main_quit() def update(self): overall = sum(self.sources.itervalues()) / len(self.sources) busySources = [source for source, progress in self.sources.iteritems() if progress < 1.0] self.statusbar.pop(self.progressContext) if busySources: self.progress.show() self.progress.set_fraction(overall) self.statusbar.push(self.progressContext, ", ".join(busySources)) else: # Finished with everything we were doing. Hide the progress bar, # clear out old progress information, and call completion handlers. self.progress.hide() self.sources = {} for f in self.completionCallbacks: f() def watchCursor(self, cursor): """Show the position of this cursor on the status bar""" cursor.observers.append(self._cursorCallback) def _cursorCallback(self, t): self.cursorLabel.set_markup(Style.toMonospaceMarkup( " %.04f s " % t)) def watchSelection(self, sel): """Watch an Observable value indicating the current selection status""" sel.observers.append(self.selectionLabel.set_text) class MainWindow(ViewContainer): """The Main Window is a container for all our included Views""" def __init__(self): ViewContainer.__init__(self) tlist = ScrolledTransactionList(self) TransactionDetailWindow(self).connectToList(tlist.view) self.status = StatusMonitor() self.status.watchSelection(tlist.selectionInfo) paned = gtk.VPaned() if gnomecanvas: timing = TimingDiagram(self) self.status.watchCursor(timing.cursor) paned.add1(timing.root) paned.add2(tlist.root) paned.set_position(250) mainvbox = gtk.VBox(False) mainvbox.pack_start(paned, True) mainvbox.pack_start(self.status.statusbar, False) self.window = gtk.Window() self.window.set_default_size(1200, 900) self.window.add(mainvbox) self.window.show_all() vusb-analyzer-1.1/VUsbTools/__init__.py0000644001313400003110000000024311132505241016614 0ustar micahmts# # VUsbTools package # Micah Dowty # # This is a Python package with USB log parsing, # transaction decoding, and user interface components. # vusb-analyzer-1.1/VUsbTools/Style.py0000644001313400003110000000325511132506533016170 0ustar micahmts# # VUsbTools.Views # Micah Dowty # # A container for color and font preferences # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # import math from VUsbTools.Types import Color # # Use the default monospace font. If this is too large/small for your # tastes, you can try a specific font name and size like "courier 9". # monospaceFont = "monospace" def toMonospaceMarkup(text): """Convert arbitrary text to pango markup in our monospace font""" return '%s' % ( monospaceFont, text.replace("&", "&").replace("<", "<")) directionColors = { "Up": Color(0x00, 0x00, 0xFF), "Down": Color(0x00, 0x80, 0x00), } directionIcons = { "Down": "gtk-go-forward", "Up": "gtk-go-back", } errorMarkerColor = Color(0xFF, 0x00, 0x00, 0x80) diffMarkerColor = Color(0x00, 0xA0, 0x00, 0x30) diffBorderColor = Color(0x00, 0xA0, 0x00, 0xA0) emptyTransactionColor = Color(0x80, 0x80, 0x80) smallTransactionColor = Color(0x80, 0x80, 0xFF) largeTransactionColor = Color(0xFF, 0xFF, 0x80) frameMarkerColor = Color(0x00, 0x00, 0xFF) duplicateFrameColor = Color(0x80, 0x00, 0x00) def getBarColor(transaction): """Get the color to use for a transaction's bar on the timing diagram. This implementation bases the color on a transaction's size. """ s = transaction.datalen or 0 if not s: return emptyTransactionColor # For non-empty transactions, the color is actually proportional # to size on a logarithmic scale. return smallTransactionColor.lerp( math.log(s) / math.log(4096), largeTransactionColor) vusb-analyzer-1.1/VUsbTools/Log.py0000644001313400003110000006710311306023535015612 0ustar micahmts# # VUsbTools.Log # Micah Dowty # # Implements parsers for USB log files. Currently # this includes slurping usbAnalyzer data out of the # VMX log, and parsing the XML logs exported by # Ellisys Visual USB. # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # from __future__ import division import sys, time, re, math, os, string, atexit import xml.sax, Queue, threading, difflib import gtk, gobject import traceback, gzip, struct from VUsbTools import Types class UsbIOParser(Types.psyobj): """Parses USBIO log lines and generates Transaction objects appropriately. Finished transactions are pushed into the supplied queue. """ lineOriented = True def __init__(self, completed): self.current = Types.Transaction() self.completed = completed def parse(self, line, timestamp=None, frame=None, lineNumber=None): tokens = line.split() finished = None if tokens[0] in ('Up', 'Down'): self.flush() self.current.dir = tokens[0] self.current.timestamp = timestamp self.current.frame = frame self.current.lineNumber = lineNumber self.parseKeyValuePairs(tokens[1:]) # new Log_HexDump() format: # USBIO: 000: 80 06 ...... elif len(tokens) >= 2 and len(tokens[0]) == 4 and len(tokens[1]) == 2: data = line.split(':') data = data[1].lstrip() self.current.appendHexData(data[:48]) # old Log_HexDump() format: # USBIO: 80 06 ...... elif len(tokens[0]) == 2: self.current.appendHexData(line[:48]) else: self.flush() self.current.appendDecoded(line.strip()) def parseKeyValuePairs(self, tokens): for token in tokens: kv = token.split('=', 1) if len(kv) > 1: if kv[0] in ('endpt'): base = 16 else: base = 10 setattr(self.current, kv[0], int(kv[1], base)) def flush(self): """Force any in-progress transactions to be completed. This should be called when you know the USB analyzer is finished outputting data, such as when a non-USBIO line appears in the log. """ if self.current.dir: self.completed.put(self.current) self.current = Types.Transaction() class TimestampLogParser: """Parse a simple format which logs timestamps in nanosecond resolution. Lines are of the form: args... The event name may be 'begin-foo' or 'end-foo' to indicate an event which executes over a span of time, or simply 'foo' to mark a single point. """ lineOriented = True def __init__(self, completed): self.epoch = None self.nameEndpoints = {} self.nextEp = 1 self.lineNumber = 0 self.completed = completed def flush(self): pass def parse(self, line): self.lineNumber += 1 tokens = line.split() try: # Extract the time, convert to seconds nanotime = int(tokens[0]) if not self.epoch: self.epoch = nanotime timestamp = (nanotime - self.epoch) / 1000000000.0 # Detect the start- or end- prefix name = tokens[1] if name.startswith("begin-"): name = name.split('-', 1)[1] dirs = ('Down',) elif name.startswith("end-"): name = name.split('-', 1)[1] dirs = ('Up',) else: dirs = ('Down', 'Up') # Generate an 'endpoint' for the event name try: endpoint = self.nameEndpoints[name] except KeyError: endpoint = self.nextEp self.nameEndpoints[name] = endpoint self.nextEp = endpoint + 1 for dir in dirs: trans = Types.Transaction() trans.dir = dir trans.timestamp = timestamp trans.lineNumber = self.lineNumber trans.endpt = endpoint trans.dev = 0 trans.status = 0 trans.datalen = 0x1000 trans.appendDecoded(" ".join(tokens[1:])) self.completed.put(trans) except: print "Error on line %d:" % self.lineNumber traceback.print_exc() class VmxLogParser(UsbIOParser): """Read the VMX log, looking for new USBIO lines and parsing them. """ frame = None epoch = None lineNumber = 0 def parse(self, line): self.lineNumber += 1 # Local to the UHCI core l = line.split("UHCI:") if len(l) == 2: m = re.search("- frame ([0-9]+) -", l[1]) if m: self.frame = int(m.group(1)) # Don't let SOF markers start the clock if self.epoch is not None: self.completed.put(Types.SOFMarker(self.parseRelativeTime(line), self.frame, self.lineNumber)) return # Local to the EHCI core l = line.split("EHCI:") if len(l) == 2: m = re.search("Execute frame ([0-9]+)[\. ]", l[1]) if m: self.frame = int(m.group(1)) # Don't let SOF markers start the clock if self.epoch is not None: self.completed.put(Types.SOFMarker(self.parseRelativeTime(line), self.frame, self.lineNumber)) return # Generic analyzer URBs l = line.split("USBIO:") if len(l) == 2: UsbIOParser.parse(self, l[1][:-1], self.parseRelativeTime(line), self.frame, self.lineNumber) else: self.flush() def parseRelativeTime(self, line): """Start the clock when we see our first USB log line""" t = self.parseTime(line) if self.epoch is None: self.epoch = t return t - self.epoch _timeCache = (None, None) def parseTime(self, line): """Return a unix-style timestamp for the given line. XXX: This assumes the current year, so logs that straddle years will have a giant discontinuity in timestamps. """ # Cache the results of strptime. It only changes every # second, and this was taking more than 50% of our parsing time! stamp = line[:15] savedStamp, parsed = self._timeCache if savedStamp != stamp: parsed = time.strptime(stamp, "%b %d %H:%M:%S") self._timeCache = stamp, parsed now = time.localtime() try: usec = int(line[16:19]) except ValueError: usec = 0 return usec / 1000.0 + time.mktime(( now.tm_year, parsed.tm_mon, parsed.tm_mday, parsed.tm_hour, parsed.tm_min, parsed.tm_sec, parsed.tm_wday, parsed.tm_yday, parsed.tm_isdst)) def parseInt(attrs, name, default=None): """The Ellisys logs include commas in their integers""" try: return int(attrs[name].replace(",", "")) except (KeyError, ValueError): return default def parseFloat(attrs, name, default=None): """The Ellisys logs include commas and spaces in their floating point numbers""" try: return float(attrs[name].replace(",", "").replace(" ", "")) except (KeyError, ValueError): return default class EllisysXmlHandler(xml.sax.handler.ContentHandler): """Handles SAX events from an XML log exported by Ellisys Visual USB. The completed USB transactions are pushed into the provided completion queue. """ frameNumber = None device = None endpoint = None current = None characterHandler = None def __init__(self, completed): self.pipes = {} self.pending = {} self.completed = completed self._frameAttrs = {} def startElement(self, name, attrs): # This will always call self.startElement_%s where %s is the # element name, but the profiler showed us spending quite a lot # of time just figuring out who to call, even if this was cached # in a dictionary. The tests below are ordered to keep very # frequent elements running fast. if name == "StartOfFrame": # Just stow the SOF attributes, decode them if we end up # actually needing them later. self._frameAttrs = attrs elif name == "data": self.characterHandler = self.current.appendHexData elif name == "Packet": self.startElement_Packet(attrs) elif name == "Transaction": self.startElement_Transaction(attrs) elif name == "Reset": self.startElement_Reset(attrs) def endElement(self, name): self.characterHandler = None if name == 'Document': for pipe in self.pipes.keys(): self.completeUrb(pipe, 'End of Log') def startElement_Transaction(self, attrs): self.device = parseInt(attrs, 'device', 0) self.endpoint = parseInt(attrs, 'endpoint') def startElement_Reset(self, attrs): # Error out any transactions that are active during a reset for pipe in self.pipes.keys(): self.completeUrb(pipe, 'Bus Reset') def beginUrb(self, pipe): """Simulate a new URB being created on the supplied pipe. This begins a Down transaction and makes it pending and current. """ t = Types.Transaction() t.dir = 'Down' t.dev, t.endpt = pipe t.timestamp = self.timestamp t.frame = parseInt(self._frameAttrs, 'frameNumber') t.status = 0 self.pipes[pipe] = t self.pending[pipe] = t self.current = t def flipUrb(self, pipe): """Begin the Up phase on a particular pipe. This completes the Down transaction, and makes an Up current (but not pending) """ del self.pending[pipe] down = self.pipes[pipe] self.completed.put(down) up = Types.Transaction() up.dir = 'Up' up.dev, up.endpt = pipe # Up and Down transactions share setup data, if applicable if down.hasSetupData(): up.data = down.data[:8] self.pipes[pipe] = up self.current = up def completeUrb(self, pipe, id): """Complete the Up phase on a pipe""" if pipe in self.pending: self.flipUrb(pipe) assert pipe in self.pipes t = self.pipes[pipe] del self.pipes[pipe] self.current = None t.timestamp = self.timestamp t.frame = parseInt(self._frameAttrs, 'frameNumber') if id in ('ACK', 'NYET'): t.status = 0 else: t.status = id self.completed.put(t) def startElement_Packet(self, attrs): id = attrs['id'] # Fast exit for common packets we don't care about if id in ('SOF', 'DATA0', 'DATA1'): return self.timestamp = parseFloat(attrs, 'time') if self.endpoint is None: return if self.endpoint == 0: # EP0 is a special case for us, since its transactions # consiste of several phases. We always begin with SETUP. # If the request has an input stage, we'll see an OUT after # that as a handshake. If not, the handshake is an empty # IN stage. pipe = self.device, 0 if id == 'SETUP': self.beginUrb(pipe) self.ep0FinalStage = False elif id == 'IN': if pipe in self.pending: self.flipUrb(pipe) else: self.current = self.pipes[pipe] if self.current.data and (ord(self.current.data[0]) & 0x80) == 0: # This is an output request, IN is our last stage self.ep0FinalStage = True elif id == 'OUT': self.current = self.pipes[pipe] if self.current.data and (ord(self.current.data[0]) & 0x80): # This is an input request, OUT is our last stage self.ep0FinalStage = True elif id == 'PING': # An acknowledged PING packet should never end a control transfer self.ep0FinalStage = False elif pipe in self.pipes and ( id == 'STALL' or (id == 'ACK' and self.ep0FinalStage)): self.completeUrb(pipe, id) else: # It's really annoying that the Ellisys logs strip the # direction bit from the endpoint number. We have to recover # this ourselves. if id == 'IN': self.endpoint = self.endpoint | 0x80 pipe = self.device, self.endpoint if id in ('OUT', 'IN', 'PING'): # These packets indicate that we'd like to be transmitting # data to a particular endpoint- so the operating system must # now have an active URB. if pipe in self.pipes: # Finish a previous packet that wasn't acknowledged. # This will be frequent if isochronous transfers are involved! self.completeUrb(pipe, 'No Handshake') self.beginUrb(pipe) if pipe in self.pending and id in ('NAK', 'NYET', 'STALL', 'IN'): self.flipUrb(pipe) if pipe in self.pipes: if id == 'ACK': # This accounts for combining individual low-level USB packets # into the larger packets that should be associated with a URB. # We only end a URB when a short packet is transferred. # # FIXME: Determine the real max packet size, rather than # using this hardcoded nonsense. if len(self.current.data) & 0x3F: self.completeUrb(pipe, id) elif id in ('NYET', 'STALL'): # Always complete on an error condition self.completeUrb(pipe, id) def characters(self, content): # This extra level of indirection seems to be necessary, I guess Expat is # binding our functions once at initialization. if self.characterHandler: self.characterHandler(content) Types.psycoBind(EllisysXmlHandler) class EllisysXmlParser: """Parses XML files exported from Ellisys Visual USB. This is just a glue object that sets up an XML parser and sends SAX events to the EllisysXmlHandler. """ lineOriented = False def __init__(self, completed): self.completed = completed self.xmlParser = xml.sax.make_parser() self.xmlParser.setContentHandler(EllisysXmlHandler(completed)) def parse(self, line): self.xmlParser.feed(line) class UsbmonLogParser: """Parses usbmon log lines and generates Transaction objects appropriately. Finished transactions are pushed into the supplied queue. This parser was originally contributed by Christoph Zimmermann. """ lineOriented = True lineNumber = 0 def __init__(self, completed): self.epoch = None self.trans = Types.Transaction() self.trans.frame = 0 self.setupData = None self.completed = completed def parse(self, line, timestamp=None, frame=None): self.lineNumber += 1 tokens = line.split() try: # Do a small stupid sanity check if this is a correct usbmon log line try: if len(tokens) < 4: return if not(int(tokens[0],16) and int(tokens[1]) and (tokens[2] in ('S', 'C', 'E'))): return except: print "Error on line %d:" % self.lineNumber return # Copied log file format description of the usbmon kernel # facility You can find the original manual including how # to use usbmon in your kernel sources: /Documentation/usb/usbmon.txt # # Copied text starts here: # Any text format data consists of a stream of events, # such as URB submission, URB callback, submission # error. Every event is a text line, which consists of # whitespace separated words. The number or position of # words may depend on the event type, but there is a set # of words, common for all types. # Here is the list of words, from left to right: # - URB Tag. This is used to identify URBs, and is # normally an in-kernel address of the URB structure in # hexadecimal, but can be a sequence number or any other # unique string, within reason. self.trans.lineNumber = self.lineNumber # - Timestamp in microseconds, a decimal number. The # timestamp's resolution depends on available clock, and # so it can be much worse than a microsecond (if the # implementation uses jiffies, for example). # Extract the time, convert to seconds microtime = int(tokens[1]) if not self.epoch: self.epoch = microtime timestamp = (microtime - self.epoch) / 1000000.0 self.trans.timestamp = timestamp # - Event Type. This type refers to the format of the # event, not URB type. Available types are: S - # submission, C - callback, E - submission error. if tokens[2] == 'S': self.trans.dir = 'Down' else: self.trans.dir = 'Up' # - "Address" word (formerly a "pipe"). It consists of # four fields, separated by colons: URB type and # direction, Bus number, Device address, Endpoint # number. Type and direction are encoded with two bytes # in the following manner: # # Ci Co Control input and output # Zi Zo Isochronous input and output # Ii Io Interrupt input and output # Bi Bo Bulk input and output # # Bus number, Device address, and Endpoint are decimal # numbers, but they may have leading zeros, for the sake # of human readers. # # Note that older kernels seem to omit the bus number field. # We can parse either format. pipe = tokens[3].split(':') self.trans.dev = int(pipe[-2]) self.trans.endpt = int(pipe[-1]) if pipe[0][1] == 'i' and self.trans.endpt != 0: # Input endpoint self.trans.endpt |= 0x80 if len(pipe) >= 4: self.trans.dev += int(pipe[-3]) * 1000 # - URB Status word. This is either a letter, or several # numbers separated by colons: URB status, interval, # start frame, and error count. Unlike the "address" # word, all fields save the status are # optional. Interval is printed only for interrupt and # isochronous URBs. Start frame is printed only for # isochronous URBs. Error count is printed only for # isochronous callback events. # # The status field is a decimal number, sometimes # negative, which represents a "status" field of the # URB. This field makes no sense for submissions, but is # present anyway to help scripts with parsing. When an # error occurs, the field contains the error code. # # In case of a submission of a Control packet, this # field contains a Setup Tag instead of an group of # numbers. It is easy to tell whether the Setup Tag is # present because it is never a number. Thus if scripts # find a set of numbers in this word, they proceed to # read Data Length (except for isochronous URBs). If # they find something else, like a letter, they read the # setup packet before reading the Data Length or # isochronous descriptors. # # - Setup packet, if present, consists of 5 words: one of # each for bmRequestType, bRequest, wValue, wIndex, # wLength, as specified by the USB Specification 2.0. # These words are safe to decode if Setup Tag was # 's'. Otherwise, the setup packet was present, but not # captured, and the fields contain filler. # # - Number of isochronous frame descriptors and # descriptors themselves. If an Isochronous transfer # event has a set of descriptors, a total number of them # in an URB is printed first, then a word per descriptor, # up to a total of 5. The word consists of 3 # colon-separated decimal numbers for status, offset, and # length respectively. For submissions, initial length is # reported. For callbacks, actual length is reported. if tokens[4] in ('s'): # This is a setup packet # Example data stage: 23 01 0010 0002 0040 self.trans.status = 0 data = ''.join(tokens[5:10]) self.trans.appendHexData(data) # save the setup data to prepend it to the setup packet data stage self.setupData = data else: status_word = tokens[4].split(':') self.trans.status = int(status_word[0]) # - Data Length. For submissions, this is the requested # length. For callbacks, this is the actual length. if len(tokens) >= 7 : self.trans.datalen = int(tokens[5]) # - Data tag. The usbmon may not always capture data, even # if length is nonzero. The data words are present only # if this tag is '='. # - Data words follow, in big endian hexadecimal # format. Notice that they are not machine words, but # really just a byte stream split into words to make it # easier to read. Thus, the last word may contain from # one to four bytes. The length of collected data is # limited and can be less than the data length report in # Data Length word. if tokens[6] in ('='): if self.setupData: # check if this is a setup package data stage # prepend setup data for the decoders self.trans.appendHexData(self.setupData) self.setupData = None self.trans.appendHexData(''.join(tokens[7:])) self.completed.put(self.trans) self.trans = Types.Transaction() # End of copied usbmon description text # End of log file parsing except: print "Error on line %d:" % self.lineNumber traceback.print_exc() class Follower(threading.Thread): """A thread that continuously scans a file, parsing each line""" pollInterval = 0.1 running = True progress = 0.0 progressInterval = 0.2 progressExpiration = 0 def __init__(self, filename, parser, progressQueue=None, tailMode=False): self.filename = filename self.parser = parser self.progressQueue = progressQueue if os.path.splitext(filename)[1] == ".gz": # On a gzip file, we need to read the uncompressed filesize from the footer f = open(filename, "rb") f.seek(-4, 2) self.fileSize = struct.unpack("= self.progressExpiration: self.setProgress(min(1.0, self.file.tell() / self.fileSize)) self.progressExpiration = now + self.progressInterval else: self.setProgress(1.0) time.sleep(self.pollInterval) except KeyboardInterrupt: gtk.main_quit() def setProgress(self, progress): self.progress = progress if self.progressQueue: self.progressQueue.put(("Loading %s" % os.path.basename(self.filename), self.progress)) def stop(self): # Keep the queue empty so it doesn't deadlock on put() if not self.running: return self.running = False try: while 1: self.parser.completed.get(False) except Queue.Empty: pass self.join() class QueueSink: """Polls a Queue for new items, via the Glib main loop. When they're available, calls a callback with them. """ interval = 200 timeSlice = 0.25 maxsize = 512 batch = range(10) def __init__(self, callback): self.queue = Queue.Queue(self.maxsize) self.callback = callback self.poll() def poll(self): try: deadline = time.clock() + self.timeSlice while time.clock() < deadline: # This avoids calling time.clock() once per queue item. for _ in self.batch: try: i = self.queue.get(False) except Queue.Empty: # We have nothing to do, set a longer interval gobject.timeout_add(self.interval, self.poll) return False else: self.callback(i) except KeyboardInterrupt: gtk.main_quit() # Come back after GTK's event queue is idle gobject.idle_add(self.poll) return False def chooseParser(filename): """Return an appropriate log parser class for the provided filename. This implementation does not try to inspect the file's content, it just looks at the filename's extension. """ base, ext = os.path.splitext(filename) if ext == ".gz": return chooseParser(base) if ext == ".xml": return EllisysXmlParser if ext == ".tslog": return TimestampLogParser if ext == ".mon": return UsbmonLogParser return VmxLogParser vusb-analyzer-1.1/VUsbTools/Decode.py0000644001313400003110000003344711132505241016254 0ustar micahmts# # VUsbTools.Decode # Micah Dowty # # Implements decoders and decoder support for vusb-analyzer. # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # import struct, os from VUsbTools import Types, Struct class DecoderContext: """This object represents the information we have about a device or interface when looking for a particular decoder. The 'device' descriptor will always be valid. When searching for an EP0 decoder, interface and endpoint will be None. When searching for an endpoint decoder, they will be valid. 'descriptors' is a list of other descriptors being processed at the same time. This can be used, for example, for a control decoder to peek into a device's endpoint list to apply heuristics. 'devInstance' will always be our Device object. This object exists so that new search parameters can be added without modifying the decoders themselves. """ def __init__(self, devInstance=None, device=None, interface=None, endpoint=None, descriptors=None): self.devInstance = devInstance self.device = device self.interface = interface self.endpoint = endpoint self.descriptors = descriptors class DecoderFactory: """Devices use this object to attach new decoders to their endpoints. Decoder modules register detector functions here. """ def __init__(self): self._detectors = [] def register(self, detector): self._detectors.append(detector) def registerModules(self, parentModule): """Automatically register all modules found within the supplied parent module""" path = os.path.split(parentModule.__file__)[0] for name in os.listdir(path): if name.lower().endswith(".py") and name[0] not in ('_', '.'): name = name[:-3] __import__("%s.%s" % (parentModule.__name__, name)) print "Loaded decoder module %r" % name self.register(getattr(parentModule, name).detector) def getDecoder(self, context): """Return an appropriate decoder object, given the DecoderContext. May return None to indicate that no decoder should be used. """ for detector in self._detectors: decoder = detector(context) if decoder: print "Installing decoder %s.%s" % ( decoder.__module__, decoder.__class__.__name__) return decoder if not context.endpoint: return ControlDecoder(context.devInstance) class Bus: """Represents the set of all devices. This receives events, and dispatches them to individual devices, creating them as necessary. """ def __init__(self): self.devices = {} self.decoders = DecoderFactory() def handleEvent(self, event): if not isinstance(event, Types.Transaction): return if event.dev not in self.devices: self.devices[event.dev] = Device(self.decoders) self.devices[event.dev].handleEvent(event) class Device: """Represents one device on the bus. A separate decoder is registered for each endpoint on each interface. This queries the provided DecoderFactory when new descriptors are seen, in order to associate new decoders with those descriptors. """ def __init__(self, decoderFactory): self.decoderFactory = decoderFactory self.currentConfig = 1 # Current altsetting numbers for each interface self.altSettings = {} # { configValue: (configDescriptor, # { (interfaceNumber, altSetting): (interfaceDescriptor, # { endpointAddress: (endpointDescriptor, decoder) }) # }) # } self.configs = {} self.deviceDescriptor = None self.controlDecoder = self.decoderFactory.getDecoder(DecoderContext(self)) self._cacheEndpointDecoders() def setInterface(self, iface, alt): self.altSettings[iface] = alt self._cacheEndpointDecoders() def setConfig(self, config): self.currentConfig = config self._cacheEndpointDecoders() def storeDescriptors(self, descriptors): iface = alt = config = None for desc in descriptors: if desc.type == 'device': self.deviceDescriptor = desc elif desc.type == 'config': config = desc.bConfigurationValue self.configs[config] = (desc, {}) self.controlDecoder = self.decoderFactory.getDecoder(DecoderContext( self, device = self.deviceDescriptor, descriptors = descriptors, )) elif desc.type == 'interface' and config is not None: iface = desc.bInterfaceNumber alt = desc.bAlternateSetting self.configs[config][1][iface, alt] = (desc, {}) elif desc.type == 'endpoint' and config is not None and iface is not None: self.configs[config][1][iface, alt][1][desc.bEndpointAddress] = ( desc, self.decoderFactory.getDecoder(DecoderContext( self, device = self.deviceDescriptor, interface = self.configs[config][1][iface, alt][0], endpoint = desc, descriptors = descriptors, ))) self._cacheEndpointDecoders() def _cacheEndpointDecoders(self): """Update the endpointDecoders dictionary using the current configuration and interface settings. """ self.endpointDecoders = {0: self.controlDecoder} try: interfaces = self.configs[self.currentConfig][1] except KeyError: return for (iface, alt), (descriptor, endpoints) in interfaces.iteritems(): if self.altSettings.get(iface, 0) != alt: continue # We found an active altsetting for this interface. # Activate all its endpoint decoders. for address, (descriptor, decoder) in endpoints.iteritems(): if decoder: self.endpointDecoders[address] = decoder def handleEvent(self, event): if event.endpt in self.endpointDecoders: self.endpointDecoders[event.endpt].handleEvent(event) class SetupPacket: """Represents the SETUP stage of a control transfer. This is responsible for decoding the bitfields and structure of the packet, but not for defining the meaning of a particular request. """ _recipNames = Struct.EnumDict({ 0x00: 'device', 0x01: 'interface', 0x02: 'endpoint', 0x03: 'other', }) _typeNames = Struct.EnumDict({ 0x00: 'standard', 0x20: 'class', 0x40: 'vendor', 0x60: 'reserved', }) def __init__(self, event): self.event = event (self.bitmap, self.request, self.wValue, self.wIndex, self.wLength) = struct.unpack("> 8 self.wValueLow = self.wValue & 0xFF self.wIndexHigh = self.wIndex >> 8 self.wIndexLow = self.wIndex & 0xFF class DescriptorGroup(Struct.Group): """Parses out USB descriptors into a Struct.Group tree. This class handles any standard descriptor, but subclasses can add class-specific descriptors as needed. """ descriptorTypes = Struct.EnumDict({ 0x01: "device", 0x02: "config", 0x03: "string", 0x04: "interface", 0x05: "endpoint", }) headerStruct = lambda self: ( Struct.UInt8("bLength"), Struct.UInt8("bDescriptorType"), ) struct_device = lambda self: ( Struct.UInt16Hex("bcdUSB"), Struct.UInt8Hex("bDeviceClass"), Struct.UInt8Hex("bDeviceSubClass"), Struct.UInt8Hex("bDeviceProtocol"), Struct.UInt8("bMaxPacketSize0"), Struct.UInt16Hex("idVendor"), Struct.UInt16Hex("idProduct"), Struct.UInt16Hex("bcdDevice"), Struct.UInt8("iManufacturer"), Struct.UInt8("iProduct"), Struct.UInt8("iSerialNumber"), Struct.UInt8("bNumConfigurations"), ) struct_config = lambda self: ( Struct.UInt16("wTotalLength"), Struct.UInt8("bNumInterfaces"), Struct.UInt8("bConfigurationValue"), Struct.UInt8("iConfiguration"), Struct.UInt8Hex("bmAttributes"), Struct.UInt8("MaxPower"), ) struct_string = lambda self: ( Struct.Utf16String("string"), ) struct_interface = lambda self: ( Struct.UInt8("bInterfaceNumber"), Struct.UInt8("bAlternateSetting"), Struct.UInt8("bNumEndpoints"), Struct.UInt8Hex("bInterfaceClass"), Struct.UInt8Hex("bInterfaceSubClass"), Struct.UInt8Hex("bInterfaceProtocol"), Struct.UInt8("iInterface"), ) struct_endpoint = lambda self: ( Struct.UInt8Hex("bEndpointAddress"), Struct.UInt8Hex("bmAttributes"), Struct.UInt16("wMaxPacketSize"), Struct.UInt8("bInterval"), ) def __init__(self): Struct.Group.__init__(self, "descriptors") def decode(self, buffer): # Common descriptor header buffer = Struct.Group.decode(self, buffer, self.headerStruct()) # Decode descriptor type self.type = self.descriptorTypes[self.bDescriptorType] # Make sure that we eat exactly the right number of bytes, # according to the descriptor header descriptor = buffer[:self.bLength - 2] buffer = buffer[self.bLength - 2:] # The rest of the decoding is done by a hander, in the form of a child item list. Struct.Group.decode(self, descriptor, getattr(self, "struct_%s" % self.type, lambda: None)()) return buffer class ControlDecoder: """Decodes standard control requests, and tries to detect additional class decoders based on descriptors we see. Control requests first have their names looked up in a table of the form Requests. They may then go to a default decoder, or we may look up one of the form decode_ from this class. This class defines all standard requests. Subclasses may define extra class- or vendor-specific requests. """ standardRequests = Struct.EnumDict({ 0x00: 'GetStatus', 0x01: 'ClearFeature', 0x03: 'SetFeature', 0x05: 'SetAddress', 0x06: 'GetDescriptor', 0x07: 'SetDescriptor', 0x08: 'GetConfiguration', 0x09: 'SetConfiguration', 0x0A: 'GetInterface', 0x0B: 'SetInterface', 0x0C: 'SynchFrame', }) standardFeatures = Struct.EnumDict({ 0x00: 'ENDPOINT_HALT', 0x01: 'DEVICE_REMOTE_WAKEUP', }) descriptorClass = DescriptorGroup def __init__(self, device): self.device = device def handleEvent(self, event): if not event.isDataTransaction(): return setup = SetupPacket(event) # Look up the request name setup.requestName = getattr(self, "%sRequests" % setup.type, Struct.EnumDict())[setup.request] # Look up a corresponding decoder getattr(self, "decode_%s" % setup.requestName, self.decodeGeneric)(setup) def decodeGeneric(self, setup): """Generic decoder for control requests""" setup.event.pushDecoded("%s %s %s(wValue=0x%04x, wIndex=0x%04x)" % ( setup.type, setup.recip, setup.requestName, setup.wValue, setup.wIndex)) def decode_SetAddress(self, setup): setup.event.pushDecoded("SetAddress(%d)" % setup.wValue) def decode_SetConfiguration(self, setup): setup.event.pushDecoded("SetConfiguration(%d)" % setup.wValue) self.device.setConfig(setup.wValue) def decode_SetDescriptor(self, setup): # Display the get/set descriptor request itself setup.event.pushDecoded("%s(%s, %s%s)" % ( setup.requestName, self.descriptorClass.descriptorTypes[setup.wValueHigh], setup.wValueLow, ("", ", lang=%04X" % setup.wIndex)[setup.wIndex != 0], )) # Parse out all descriptors descriptors = [] buffer = setup.event.data[8:] while buffer: desc = self.descriptorClass() buffer = desc.decode(buffer) setup.event.appendDecoded("\n%s descriptor:\n%s" % (desc.type, desc)) descriptors.append(desc) self.device.storeDescriptors(descriptors) decode_GetDescriptor = decode_SetDescriptor def decode_SetFeature(self, setup): feature = getattr(self, "%sFeatures" % setup.type, Struct.EnumDict())[setup.wValue] setup.event.pushDecoded("%s %s(%s, %s=0x%02x)" % ( setup.type, setup.requestName, feature, setup.recip, setup.wIndex)) decode_ClearFeature = decode_SetFeature def decode_SetInterface(self, setup): setup.event.pushDecoded("SetInterface(alt=%d, iface=%d)" % (setup.wValue, setup.wIndex)) self.device.setInterface(setup.wIndex, setup.wValue) def attachView(viewContainer): """Add decoding capabilities to a ViewContainer. """ from VUsbTools import Decoders bus = Bus() viewContainer.children.insert(0, bus) bus.decoders.registerModules(Decoders) vusb-analyzer-1.1/VUsbTools/Struct.py0000644001313400003110000000745511132505241016355 0ustar micahmts# # VUsbTools.Struct # Micah Dowty # # Utilities for decoding structures and trees of structures # into something human-readable. # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # import struct from VUsbTools import Types class EnumDict(dict): """A dictionary mapping assigned numbers to names. Currently the only difference between this and a normal dictionary is that unknown keys 'pass through'. """ format = "0x%02x" def __getitem__(self, key): try: return dict.__getitem__(self, key) except: try: return self.format % key except TypeError: return str(key) class Item: """This is an abstract base class for items that can be decoded from an arbitrary string of bytes. After decoding, the 'value' attribute should be valid. str() should return the most meaningful human-readable representation. """ _strFormat = "%s" _format = None def __init__(self, name): self._name = name def decode(self, buffer): """The default decoder makes use of struct""" size = struct.calcsize(self._format) item = buffer[:size] buffer = buffer[size:] if len(item) != size: self._value = None else: self._value = struct.unpack(self._format, item)[0] return buffer def __str__(self): if self._value is None: return "None" else: return self._strFormat % self._value class UInt8(Item): _format = " # # This package holds Event and its subclasses, # the primitives used to exchange USB log information # between the log parsers, user interface, and decoders. # It's also a bit of a dumping ground for other tiny # objects that don't seem to deserve a separate module # yet: Color, Observable and psyobj. # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # import binascii try: import gnomecanvas except gnomecanvas: print "Warning: You don't have gnome-canvas (or its python bindings) installed." print " The happy timing diagram will be disabled." gnomecanvas = None try: from psyco.classes import psyobj from psyco import bind as psycoBind except ImportError: print "Warning: psyco not found, install it for a nice speed boost." psyobj = object psycoBind = lambda _: None class Color(psyobj): """A simple color abstraction, supports linear interpolation. We store individual rgba values, as well as a 32-bit packed RGBA representation and an html/gdk-style string. """ def __init__(self, r, g, b, a=0xFF): self.r = r self.g = g self.b = b self.a = a self.gdkString = "#%02X%02X%02X" % (int(self.r + 0.5), int(self.g + 0.5), int(self.b + 0.5)) self.rgba = ((int(self.r + 0.5) << 24) | (int(self.g + 0.5) << 16) | (int(self.b + 0.5) << 8) | int(self.a + 0.5)) def lerp(self, a, other): """For a=0, returns a copy of 'self'. For a=1, returns a copy of 'other'. Other values return a new interpolated color. Values are clamped to [0,1], so this will not extrapolate new colors. """ a = min(1, max(0, a)) b = 1.0 - a return self.__class__(self.r * b + other.r * a, self.g * b + other.g * a, self.b * b + other.b * a) class Observable(psyobj): """A value with an associated set of listener functions that are notified on change. """ def __init__(self, default=None): self._value = default self.observers = [] def setValue(self, value, quiet=()): self._value = value for f in self.observers: if f not in quiet: f(value) def getValue(self): return self._value value = property(getValue, setValue) class Event(psyobj): """A generic USB log event, containing only timestamp information""" def __init__(self, timestamp=None, frame=None, lineNumber=None): self.timestamp = timestamp self.frame = frame self.lineNumber = lineNumber def hexDump(data, width=16, ascii=True, addrs=True, lineLimit=-1, asciiTable = '.' * 32 + ''.join(map(chr, range(32, 127))) + '.' * 129 ): """Create a hex dump of the provided string. Optionally prefixes each line with an address, and appends to each line an ASCII dump. If 'lines' is specified, limits the number of lines this will generate before returning. """ results = [] addr = 0 while data and lineLimit != 0: if results: results.append("\n") l, data = data[:width], data[width:] if addrs: results.append("%04X: " % addr) results.append(' '.join(["%02X" % ord(c) for c in l])) if ascii: results.append(' '.join([" " for i in xrange(len(l), width + 1)])) results.append(l.translate(asciiTable)) lineLimit -= 1 addr += len(l) return ''.join(results) class Transaction(Event): """A container for USB transaction data""" dir = None endpt = None dev = None data = '' datalen = 0 decoded = '' decodedSummary = '' def appendHexData(self, data): """Append data to this packet, given as a whitespace-separated string of hexadecimal bytes. """ self.data += binascii.a2b_hex(data.replace(' ', '')) # Increase datalen if we need to. Since the log might not # include complete data captures while it does include the # correct length, this will never decrease datalen. datalen = len(self.data) if self.hasSetupData(): datalen -= 8 self.datalen = max(self.datalen or 0, datalen) def appendDecoded(self, line): """Append one line of decoder information""" if self.decoded: self.decoded += "\n" + line else: self.decodedSummary = line self.decoded = line def pushDecoded(self, line): """Prepend a line of decoder info to the buffer. The new line will always become this transaction's decode summary. """ if self.decoded: self.decoded = line + "\n" + self.decoded else: self.decoded = line self.decodedSummary = line def getTransferString(self): """Return a string naming the endpoint and direction of the transfer""" if self.endpt is None: return "None" elif self.endpt == 0: # We don't get separate IN/OUT endpoints for EP0, # make this clear to the user. return "EP0" elif self.endpt & 0x80: return "EP%d IN" % (self.endpt & 0x7F) else: return "EP%d OUT" % self.endpt def hasSetupData(self): return self.endpt == 0 def getHexSetup(self): """Return a hex dump of this transaction's SETUP packet""" if self.hasSetupData(): return hexDump(self.data[:8], addrs=False, ascii=False) def getHexDump(self, summarize=False): """Return a hex dump of this transaction's data, not including SETUP. If 'summarize' is true, this returns only the first line, without addresses. """ if self.hasSetupData(): data = self.data[8:] else: data = self.data if summarize: return hexDump(data, addrs=False, lineLimit=1) else: return hexDump(data) def isDataTransaction(self): """Returns True if this transaction should have a useful data stage. Normally it's true for Down transactions on OUT endpoints, and Up transactions on IN endpoints. On the control pipe, we have to check the setup packet. """ if self.data and self.endpt == 0: dir = ord(self.data[0]) else: dir = self.endpt if dir & 0x80: return self.dir == 'Up' else: return self.dir == 'Down' def getDiffSummary(self): """Returns a tuple which is used to compare Events during a diff operation. This should include details which are easy to compare, and identify a transaction uniquely among multiple data capture sessions. """ if self.isDataTransaction(): datalen = self.datalen else: datalen = None return (self.dir, self.endpt, datalen, self.data[:32]) class SOFMarker(Event): """An event marking the beginning of a frame""" pass class DiffMarker(Event): """An event containing a contiguous list of other events that are part of a diff match. The event list is guaranteed to be sorted in chronological order, and it must be non-empty. 'matchedWith' is a parallel list of events that were matched in the view we're diffing against. """ def __init__(self, matches, matchedWith): self.matches = matches self.matchedWith = matchedWith Event.__init__(self, timestamp=matches[0].timestamp) vusb-analyzer-1.1/vusb-analyzer0000755001313400003110000000602211306026126015337 0ustar micahmts#!/usr/bin/env python # # Virtual USB Analyzer GUI # Micah Dowty # # Copyright (C) 2005-2009 VMware, Inc. Licensed under the MIT # License, please see the README.txt. All rights reserved. # VERSION = "1.1" import sys, os, gtk from VUsbTools import Views, Log, Diff, Decode def main(filename, tailMode=False): ui = Views.MainWindow() ui.window.connect("destroy", gtk.main_quit) ui.window.set_title("%s - VUsb Analyzer" % os.path.basename(filename)) parser = Log.chooseParser(filename) sink = Log.QueueSink(ui.handleEvent) follower = Log.Follower(filename, parser(sink.queue), ui.status.queue, tailMode) Decode.attachView(ui) gtk.gdk.threads_init() follower.start() try: gtk.main() finally: follower.stop() def diffMain(files, tailMode=False): ui = Diff.DiffWindow() ui.window.connect("destroy", gtk.main_quit) ui.window.set_title("%s - VUsb Analyzer" % " / ".join([ os.path.basename(f) for f in files])) gtk.gdk.threads_init() followers = [] for f, view in zip(files, ui.views): parser = Log.chooseParser(f) sink = Log.QueueSink(lambda event, view=view: ui.handleEvent(event, view)) Decode.attachView(view) follower = Log.Follower(f, parser(sink.queue), ui.status.queue, tailMode) follower.start() followers.append(follower) try: gtk.main() finally: for follower in followers: follower.stop() def profileMain(filename): import Queue parser = Log.chooseParser(filename)(Queue.Queue()) for line in open(filename): parser.parse(line) def usage(): print ("usage: %s [-t] vmx.log [vmx.log]\n" "\n" "PyGTK frontend for the virtual USB analyzer\n" "Micah Dowty \n" "Version %s\n" "\n" " -t Tail mode, start from the end of a growing log file.\n" "\n" "Supported log formats:\n" " VMware VMX log file (*.log)\n" " Exported XML from Ellisys Visual USB (*.xml)\n" " Linux usbmon log, raw ASCII format (*.mon)\n" "\n" "Also supports transparent decompression of gzipped\n" "(*.gz) files. Logs may be appended to while this \n" "program is running.\n" "\n" "For best results with Ellisys logs, enable 'Expand\n" "transactions packets' but not 'Expand consecutive\n" "elements' while exporting.\n" "\n" "Two log files can be specified, in order to invoke\n" "diff mode.\n" % (sys.argv[0], VERSION)) sys.exit(1) if __name__ == "__main__": tailMode = False args = sys.argv[1:] while args and args[0][0] == '-': opt = args[0] if opt == '-t': tailMode = True else: usage() del args[0] if len(args) == 1: main(args[0], tailMode) elif len(args) == 2: diffMain(args, tailMode) else: usage() vusb-analyzer-1.1/CHANGELOG.txt0000644001313400003110000000060711306026126014642 0ustar micahmtsvusb-analyzer 1.1 ----------------- - Includes support for Linux usbmon logs. (beta) This code was contribued by Christoph Zimmermann. The usbmon support hasn't been tested as extensively as the other formats, and it doesn't support all vusb-analyzer features, but other than that it should be quite usable and stable. vusb-analyzer 1.0 ----------------- Intial public release. vusb-analyzer-1.1/README.txt0000644001313400003110000000321711306025416014311 0ustar micahmts------------------------ the Virtual USB Analyzer ------------------------ Version 1.1 This is a GUI tool for analyzing logs of traced USB communications. For more information, see the project's web page: http://vusb-analyzer.sourceforge.net The Virtual USB Analyzer is distributed under the MIT License. The following license terms apply to all files in the project: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Contact ------- This project is provided as-is, with no official support from VMware. However, I will try to answer questions as time permits. If you have questions or you'd like to submit a patch, feel free to email me at: micah at vmware.com --