Пример #1
0
    def __init__(self):
        Gtk.Settings.get_default().set_long_property('gtk-tooltip-timeout', 0, '')
        ##################################
        # init GUI
        self.builder = Gtk.Builder()
        self.builder.add_from_file(os.path.join(WORK_DIR, 'GameConqueror.xml'))

        self.main_window = self.builder.get_object('MainWindow')
        self.main_window.set_title('GameConqueror %s' % (VERSION,))
        self.about_dialog = self.builder.get_object('AboutDialog')
        # set version
        self.about_dialog.set_version(VERSION)

        self.process_list_dialog = self.builder.get_object('ProcessListDialog')
        self.addcheat_dialog = self.builder.get_object('AddCheatDialog')

        # init memory editor
        self.memoryeditor_window = self.builder.get_object('MemoryEditor_Window')
        self.memoryeditor_hexview = HexView()
        self.memoryeditor_window.get_child().pack_start(self.memoryeditor_hexview, True, True, 0)
        self.memoryeditor_hexview.show_all()
        self.memoryeditor_address_entry = self.builder.get_object('MemoryEditor_Address_Entry')
        self.memoryeditor_hexview.connect('char-changed', self.memoryeditor_hexview_char_changed_cb)

        self.found_count_label = self.builder.get_object('FoundCount_Label')
        self.process_label = self.builder.get_object('Process_Label')
        self.value_input = self.builder.get_object('Value_Input')
        
        self.scanoption_frame = self.builder.get_object('ScanOption_Frame')
        self.scanprogress_progressbar = self.builder.get_object('ScanProgress_ProgressBar')

        self.scan_button = self.builder.get_object('Scan_Button')
        self.reset_button = self.builder.get_object('Reset_Button')
        self.is_first_scan = True

        ###
        # Set scan data type
        self.scan_data_type_combobox = self.builder.get_object('ScanDataType_ComboBox')
        misc.build_combobox(self.scan_data_type_combobox, SCAN_VALUE_TYPES)
        # apply setting
        misc.combobox_set_active_item(self.scan_data_type_combobox, SETTINGS['scan_data_type'])

        ###
        # set search scope
        self.search_scope_scale = self.builder.get_object('SearchScope_Scale')
        self.search_scope_scale_adjustment = Gtk.Adjustment(lower=0, upper=2, step_incr=1, page_incr=1, page_size=0)
        self.search_scope_scale.set_adjustment(self.search_scope_scale_adjustment)
        # apply setting
        self.search_scope_scale.set_value(SETTINGS['search_scope'])



        # init scanresult treeview
        # we may need a cell data func here
        # create model
        self.scanresult_tv = self.builder.get_object('ScanResult_TreeView')
        self.scanresult_liststore = Gtk.ListStore(str, str, str, bool) #addr, value, type, valid
        self.scanresult_tv.set_model(self.scanresult_liststore)
        # init columns
        misc.treeview_append_column(self.scanresult_tv, 'Address', attributes=(('text',0),), properties = (('family', 'monospace'),))
        misc.treeview_append_column(self.scanresult_tv, 'Value', attributes=(('text',1),), properties = (('family', 'monospace'),))

        # init CheatList TreeView
        self.cheatlist_tv = self.builder.get_object('CheatList_TreeView')
        self.cheatlist_liststore = Gtk.ListStore(str, bool, str, str, str, str, bool) #lockflag, locked, description, addr, type, value, valid
        self.cheatlist_tv.set_model(self.cheatlist_liststore)
        self.cheatlist_tv.set_reorderable(True)
        self.cheatlist_updates = []
        self.cheatlist_editing = False
        self.cheatlist_tv.connect('key-press-event', self.cheatlist_keypressed)
        # Lock Flag
        misc.treeview_append_column(self.cheatlist_tv, '' 
                                        ,renderer_class = Gtk.CellRendererCombo
                                        ,attributes = (('text',0),)
                                        ,properties = (('editable', True)
                                                      ,('has-entry', False)
                                                      ,('model', LOCK_FLAG_TYPES)
                                                      ,('text-column', 0)
                                        )
                                        ,signals = (('edited', self.cheatlist_toggle_lock_flag_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )
        # Lock
        misc.treeview_append_column(self.cheatlist_tv, 'Lock'
                                        ,renderer_class = Gtk.CellRendererToggle
                                        ,attributes = (('active',1),)
                                        ,properties = (('activatable', True)
                                                      ,('radio', False)
                                                      ,('inconsistent', False)) 
                                        ,signals = (('toggled', self.cheatlist_toggle_lock_cb),)
                                   )
        # Description
        misc.treeview_append_column(self.cheatlist_tv, 'Description'
                                        ,attributes = (('text',2),)
                                        ,properties = (('editable', True),)
                                        ,signals = (('edited', self.cheatlist_edit_description_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )
        # Address
        misc.treeview_append_column(self.cheatlist_tv, 'Address'
                                        ,attributes = (('text',3),)
                                        ,properties = (('family', 'monospace'),)
                                   )
        # Type
        misc.treeview_append_column(self.cheatlist_tv, 'Type'
                                        ,renderer_class = Gtk.CellRendererCombo
                                        ,attributes = (('text',4),)
                                        ,properties = (('editable', True)
                                                      ,('has-entry', False)
                                                      ,('model', LOCK_VALUE_TYPES)
                                                      ,('text-column', 0))
                                        ,signals = (('edited', self.cheatlist_edit_type_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )
        # Value 
        misc.treeview_append_column(self.cheatlist_tv, 'Value'
                                        ,attributes = (('text',5),)
                                        ,properties = (('editable', True)
                                                      ,('family', 'monospace'))
                                        ,signals = (('edited', self.cheatlist_edit_value_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )

        # init ProcessList
        self.processfilter_input = self.builder.get_object('ProcessFilter_Input')
        self.userfilter_input = self.builder.get_object('UserFilter_Input')
        # init ProcessList_TreeView
        self.processlist_tv = self.builder.get_object('ProcessList_TreeView')
        self.processlist_tv.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
        self.processlist_liststore = Gtk.ListStore(str, str, str)
        self.processlist_filter = self.processlist_liststore.filter_new(root=None)
        self.processlist_filter.set_visible_func(self.processlist_filter_func, data=None)
        self.processlist_tv.set_model(self.processlist_filter)
        self.processlist_tv.set_enable_search(True)
        self.processlist_tv.set_search_column(1)
        # first col
        misc.treeview_append_column(self.processlist_tv, 'PID'
                                        ,attributes = (('text',0),)
                                   )
        # second col
        misc.treeview_append_column(self.processlist_tv, 'User'
                                        ,attributes = (('text',1),)
                                   )

        # third col
        misc.treeview_append_column(self.processlist_tv, 'Process'
                                        ,attributes = (('text',2),)
                                   )
        # init AddCheatDialog
        self.addcheat_address_input = self.builder.get_object('Address_Input')
        self.addcheat_description_input = self.builder.get_object('Description_Input')
        self.addcheat_type_combobox = self.builder.get_object('Type_ComboBox')
        misc.build_combobox(self.addcheat_type_combobox, LOCK_VALUE_TYPES)
        misc.combobox_set_active_item(self.addcheat_type_combobox, SETTINGS['lock_data_type'])
        
        
        
        # init popup menu for scanresult
        self.scanresult_popup = Gtk.Menu()
        misc.menu_append_item(self.scanresult_popup, 'Add to cheat list', self.scanresult_popup_cb, 'add_to_cheat_list')
        misc.menu_append_item(self.scanresult_popup, 'Browse this address', self.scanresult_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.scanresult_popup, 'Scan for this address', self.scanresult_popup_cb, 'scan_for_this_address')
        self.scanresult_popup.show_all()

        # init popup menu for cheatlist
        self.cheatlist_popup = Gtk.Menu()
        misc.menu_append_item(self.cheatlist_popup, 'Browse this address', self.cheatlist_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.cheatlist_popup, 'Copy address', self.cheatlist_popup_cb, 'copy_address')
        misc.menu_append_item(self.cheatlist_popup, 'Remove this entry', self.cheatlist_popup_cb, 'remove_entry')
        self.cheatlist_popup.show_all()

        self.builder.connect_signals(self)
        self.main_window.connect('destroy', self.exit)

        ###########################
        # init others (backend, flag...)
        self.pid = 0 # target pid
        self.maps = []
        self.last_hexedit_address = (0,0) # used for hexview
        self.is_scanning = False
        self.exit_flag = False # currently for data_worker only, other 'threads' may also use this flag
        self.is_data_worker_working = False

        self.backend = GameConquerorBackend()
        self.check_backend_version()
        self.search_count = 0
        GObject.timeout_add(DATA_WORKER_INTERVAL, self.data_worker)
        self.command_lock = threading.RLock()
Пример #2
0
    def __init__(self):
        ##################################
        # init GUI
        self.builder = Gtk.Builder()
        self.builder.set_translation_domain(GETTEXT_PACKAGE)
        self.builder.add_from_file(os.path.join(WORK_DIR, 'GameConqueror.xml'))

        self.main_window = self.builder.get_object('MainWindow')
        self.main_window.set_title('GameConqueror %s' % (VERSION, ))
        self.about_dialog = self.builder.get_object('AboutDialog')
        # set version
        self.about_dialog.set_version(VERSION)

        self.process_list_dialog = self.builder.get_object('ProcessListDialog')
        self.addcheat_dialog = self.builder.get_object('AddCheatDialog')

        # init memory editor
        self.memoryeditor_window = self.builder.get_object(
            'MemoryEditor_Window')
        self.memoryeditor_hexview = HexView()
        self.memoryeditor_window.get_child().pack_start(
            self.memoryeditor_hexview, True, True, 0)
        self.memoryeditor_hexview.show_all()
        self.memoryeditor_address_entry = self.builder.get_object(
            'MemoryEditor_Address_Entry')
        self.memoryeditor_hexview.connect(
            'char-changed', self.memoryeditor_hexview_char_changed_cb)

        self.found_count_label = self.builder.get_object('FoundCount_Label')
        self.process_label = self.builder.get_object('Process_Label')
        self.value_input = self.builder.get_object('Value_Input')

        self.scanoption_frame = self.builder.get_object('ScanOption_Frame')
        self.scanprogress_progressbar = self.builder.get_object(
            'ScanProgress_ProgressBar')
        self.input_box = self.builder.get_object('Value_Input')

        self.scan_button = self.builder.get_object('Scan_Button')
        self.reset_button = self.builder.get_object('Reset_Button')

        ###
        # Set scan data type
        self.scan_data_type_combobox = self.builder.get_object(
            'ScanDataType_ComboBoxText')
        for entry in SCAN_VALUE_TYPES:
            self.scan_data_type_combobox.append_text(entry)
        # apply setting
        misc.combobox_set_active_item(self.scan_data_type_combobox,
                                      SETTINGS['scan_data_type'])

        ###
        # set search scope
        self.search_scope_scale = self.builder.get_object('SearchScope_Scale')
        # apply setting
        self.search_scope_scale.set_value(SETTINGS['search_scope'])

        # init scanresult treeview
        # we may need a cell data func here
        # create model
        self.scanresult_tv = self.builder.get_object('ScanResult_TreeView')
        # liststore contents:                     addr,                value, type, valid, offset,              region type, match_id
        self.scanresult_liststore = Gtk.ListStore(GObject.TYPE_UINT64, str,
                                                  str, bool,
                                                  GObject.TYPE_UINT64, str,
                                                  int)
        self.scanresult_tv.set_model(self.scanresult_liststore)
        # init columns
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Address'),
                                    0,
                                    hex_col=0,
                                    attributes=(('text', 0), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Value'),
                                    1,
                                    attributes=(('text', 1), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Offset'),
                                    4,
                                    hex_col=4,
                                    attributes=(('text', 4), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Region Type'),
                                    5,
                                    attributes=(('text', 5), ),
                                    properties=(('family', 'monospace'), ))

        # init CheatList TreeView
        self.cheatlist_tv = self.builder.get_object('CheatList_TreeView')
        # cheatlist contents:                    lockflag, locked, description, addr,                type, value, valid
        self.cheatlist_liststore = Gtk.ListStore(str, bool, str,
                                                 GObject.TYPE_UINT64, str, str,
                                                 bool)
        self.cheatlist_tv.set_model(self.cheatlist_liststore)
        self.cheatlist_editing = False
        # Lock Flag
        misc.treeview_append_column(
            self.cheatlist_tv,
            '',
            0,
            renderer_class=Gtk.CellRendererCombo,
            attributes=(('text', 0), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model', LOCK_FLAG_TYPES), ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_toggle_lock_flag_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Lock
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Lock'),
            1,
            renderer_class=Gtk.CellRendererToggle,
            attributes=(('active', 1), ),
            properties=(('activatable', True), ('radio', False),
                        ('inconsistent', False)),
            signals=(('toggled', self.cheatlist_toggle_lock_cb), ))
        # Description
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Description'),
            2,
            attributes=(('text', 2), ),
            properties=(('editable', True), ),
            signals=(
                ('edited', self.cheatlist_edit_description_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Address
        misc.treeview_append_column(self.cheatlist_tv,
                                    _('Address'),
                                    3,
                                    hex_col=3,
                                    attributes=(('text', 3), ),
                                    properties=(('family', 'monospace'), ))
        # Type
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Type'),
            4,
            renderer_class=Gtk.CellRendererCombo,
            attributes=(('text', 4), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model',
                         misc.build_simple_str_liststore(MEMORY_TYPES)),
                        ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_edit_type_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Value
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Value'),
            5,
            attributes=(('text', 5), ),
            properties=(('editable', True), ('family', 'monospace')),
            signals=(
                ('edited', self.cheatlist_edit_value_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))

        # init ProcessList
        self.processfilter_input = self.builder.get_object(
            'ProcessFilter_Input')
        self.userfilter_input = self.builder.get_object('UserFilter_Input')
        # init ProcessList_TreeView
        self.processlist_tv = self.builder.get_object('ProcessList_TreeView')
        self.processlist_liststore = Gtk.ListStore(int, str, str)
        self.processlist_filter = self.processlist_liststore.filter_new(
            root=None)
        self.processlist_filter.set_visible_func(self.processlist_filter_func,
                                                 data=None)
        self.processlist_tv.set_model(
            Gtk.TreeModelSort(model=self.processlist_filter))
        self.processlist_tv.set_search_column(1)
        # first col
        misc.treeview_append_column(self.processlist_tv,
                                    'PID',
                                    0,
                                    attributes=(('text', 0), ))
        # second col
        misc.treeview_append_column(self.processlist_tv,
                                    _('User'),
                                    1,
                                    attributes=(('text', 1), ))

        # third col
        misc.treeview_append_column(self.processlist_tv,
                                    _('Process'),
                                    2,
                                    attributes=(('text', 2), ))

        # get list of things to be disabled during scan
        self.disablelist = []
        self.disablelist.append(self.cheatlist_tv)
        self.disablelist.append(self.scanresult_tv)
        self.disablelist.append(self.builder.get_object('processGrid'))
        self.disablelist.append(self.builder.get_object('searchGrid'))
        self.disablelist.append(self.builder.get_object('buttonGrid'))
        self.disablelist.append(self.memoryeditor_window)

        # init AddCheatDialog
        self.addcheat_address_input = self.builder.get_object('Address_Input')
        self.addcheat_address_input.override_font(
            gi.repository.Pango.FontDescription("Monospace"))

        self.addcheat_description_input = self.builder.get_object(
            'Description_Input')
        self.addcheat_length_spinbutton = self.builder.get_object(
            'Length_SpinButton')

        self.addcheat_type_combobox = self.builder.get_object(
            'Type_ComboBoxText')
        for entry in MEMORY_TYPES:
            self.addcheat_type_combobox.append_text(entry)
        misc.combobox_set_active_item(self.addcheat_type_combobox,
                                      SETTINGS['lock_data_type'])
        self.Type_ComboBoxText_changed_cb(self.addcheat_type_combobox)

        # init popup menu for scanresult
        self.scanresult_popup = Gtk.Menu()
        misc.menu_append_item(self.scanresult_popup, _('Add to cheat list'),
                              self.scanresult_popup_cb, 'add_to_cheat_list')
        misc.menu_append_item(self.scanresult_popup, _('Browse this address'),
                              self.scanresult_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.scanresult_popup,
                              _('Scan for this address'),
                              self.scanresult_popup_cb,
                              'scan_for_this_address')
        misc.menu_append_item(self.scanresult_popup, _('Remove this match'),
                              self.scanresult_delete_selected_matches)
        self.scanresult_popup.show_all()

        # init popup menu for cheatlist
        self.cheatlist_popup = Gtk.Menu()
        misc.menu_append_item(self.cheatlist_popup, _('Browse this address'),
                              self.cheatlist_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.cheatlist_popup, _('Copy address'),
                              self.cheatlist_popup_cb, 'copy_address')
        misc.menu_append_item(self.cheatlist_popup, _('Remove this entry'),
                              self.cheatlist_popup_cb, 'remove_entry')
        self.cheatlist_popup.show_all()

        self.builder.connect_signals(self)
        self.main_window.connect('destroy', self.exit)

        ###########################
        # init others (backend, flag...)
        self.pid = 0  # target pid
        self.maps = []
        self.last_hexedit_address = (0, 0)  # used for hexview
        self.is_scanning = False
        self.exit_flag = False  # currently for data_worker only, other 'threads' may also use this flag

        self.backend = GameConquerorBackend(
            os.path.join(LIBDIR, 'libscanmem.so.1'))
        self.check_backend_version()
        self.is_first_scan = True
        GLib.timeout_add(DATA_WORKER_INTERVAL, self.data_worker)
        self.command_lock = threading.RLock()
Пример #3
0
class GameConqueror():
    def __init__(self):
        Gtk.Settings.get_default().set_long_property('gtk-tooltip-timeout', 0, '')
        ##################################
        # init GUI
        self.builder = Gtk.Builder()
        self.builder.add_from_file(os.path.join(WORK_DIR, 'GameConqueror.xml'))

        self.main_window = self.builder.get_object('MainWindow')
        self.main_window.set_title('GameConqueror %s' % (VERSION,))
        self.about_dialog = self.builder.get_object('AboutDialog')
        # set version
        self.about_dialog.set_version(VERSION)

        self.process_list_dialog = self.builder.get_object('ProcessListDialog')
        self.addcheat_dialog = self.builder.get_object('AddCheatDialog')

        # init memory editor
        self.memoryeditor_window = self.builder.get_object('MemoryEditor_Window')
        self.memoryeditor_hexview = HexView()
        self.memoryeditor_window.get_child().pack_start(self.memoryeditor_hexview, True, True, 0)
        self.memoryeditor_hexview.show_all()
        self.memoryeditor_address_entry = self.builder.get_object('MemoryEditor_Address_Entry')
        self.memoryeditor_hexview.connect('char-changed', self.memoryeditor_hexview_char_changed_cb)

        self.found_count_label = self.builder.get_object('FoundCount_Label')
        self.process_label = self.builder.get_object('Process_Label')
        self.value_input = self.builder.get_object('Value_Input')
        
        self.scanoption_frame = self.builder.get_object('ScanOption_Frame')
        self.scanprogress_progressbar = self.builder.get_object('ScanProgress_ProgressBar')

        self.scan_button = self.builder.get_object('Scan_Button')
        self.reset_button = self.builder.get_object('Reset_Button')
        self.is_first_scan = True

        ###
        # Set scan data type
        self.scan_data_type_combobox = self.builder.get_object('ScanDataType_ComboBox')
        misc.build_combobox(self.scan_data_type_combobox, SCAN_VALUE_TYPES)
        # apply setting
        misc.combobox_set_active_item(self.scan_data_type_combobox, SETTINGS['scan_data_type'])

        ###
        # set search scope
        self.search_scope_scale = self.builder.get_object('SearchScope_Scale')
        self.search_scope_scale_adjustment = Gtk.Adjustment(lower=0, upper=2, step_incr=1, page_incr=1, page_size=0)
        self.search_scope_scale.set_adjustment(self.search_scope_scale_adjustment)
        # apply setting
        self.search_scope_scale.set_value(SETTINGS['search_scope'])



        # init scanresult treeview
        # we may need a cell data func here
        # create model
        self.scanresult_tv = self.builder.get_object('ScanResult_TreeView')
        self.scanresult_liststore = Gtk.ListStore(str, str, str, bool) #addr, value, type, valid
        self.scanresult_tv.set_model(self.scanresult_liststore)
        # init columns
        misc.treeview_append_column(self.scanresult_tv, 'Address', attributes=(('text',0),), properties = (('family', 'monospace'),))
        misc.treeview_append_column(self.scanresult_tv, 'Value', attributes=(('text',1),), properties = (('family', 'monospace'),))

        # init CheatList TreeView
        self.cheatlist_tv = self.builder.get_object('CheatList_TreeView')
        self.cheatlist_liststore = Gtk.ListStore(str, bool, str, str, str, str, bool) #lockflag, locked, description, addr, type, value, valid
        self.cheatlist_tv.set_model(self.cheatlist_liststore)
        self.cheatlist_tv.set_reorderable(True)
        self.cheatlist_updates = []
        self.cheatlist_editing = False
        self.cheatlist_tv.connect('key-press-event', self.cheatlist_keypressed)
        # Lock Flag
        misc.treeview_append_column(self.cheatlist_tv, '' 
                                        ,renderer_class = Gtk.CellRendererCombo
                                        ,attributes = (('text',0),)
                                        ,properties = (('editable', True)
                                                      ,('has-entry', False)
                                                      ,('model', LOCK_FLAG_TYPES)
                                                      ,('text-column', 0)
                                        )
                                        ,signals = (('edited', self.cheatlist_toggle_lock_flag_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )
        # Lock
        misc.treeview_append_column(self.cheatlist_tv, 'Lock'
                                        ,renderer_class = Gtk.CellRendererToggle
                                        ,attributes = (('active',1),)
                                        ,properties = (('activatable', True)
                                                      ,('radio', False)
                                                      ,('inconsistent', False)) 
                                        ,signals = (('toggled', self.cheatlist_toggle_lock_cb),)
                                   )
        # Description
        misc.treeview_append_column(self.cheatlist_tv, 'Description'
                                        ,attributes = (('text',2),)
                                        ,properties = (('editable', True),)
                                        ,signals = (('edited', self.cheatlist_edit_description_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )
        # Address
        misc.treeview_append_column(self.cheatlist_tv, 'Address'
                                        ,attributes = (('text',3),)
                                        ,properties = (('family', 'monospace'),)
                                   )
        # Type
        misc.treeview_append_column(self.cheatlist_tv, 'Type'
                                        ,renderer_class = Gtk.CellRendererCombo
                                        ,attributes = (('text',4),)
                                        ,properties = (('editable', True)
                                                      ,('has-entry', False)
                                                      ,('model', LOCK_VALUE_TYPES)
                                                      ,('text-column', 0))
                                        ,signals = (('edited', self.cheatlist_edit_type_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )
        # Value 
        misc.treeview_append_column(self.cheatlist_tv, 'Value'
                                        ,attributes = (('text',5),)
                                        ,properties = (('editable', True)
                                                      ,('family', 'monospace'))
                                        ,signals = (('edited', self.cheatlist_edit_value_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )

        # init ProcessList
        self.processfilter_input = self.builder.get_object('ProcessFilter_Input')
        self.userfilter_input = self.builder.get_object('UserFilter_Input')
        # init ProcessList_TreeView
        self.processlist_tv = self.builder.get_object('ProcessList_TreeView')
        self.processlist_tv.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
        self.processlist_liststore = Gtk.ListStore(str, str, str)
        self.processlist_filter = self.processlist_liststore.filter_new(root=None)
        self.processlist_filter.set_visible_func(self.processlist_filter_func, data=None)
        self.processlist_tv.set_model(self.processlist_filter)
        self.processlist_tv.set_enable_search(True)
        self.processlist_tv.set_search_column(1)
        # first col
        misc.treeview_append_column(self.processlist_tv, 'PID'
                                        ,attributes = (('text',0),)
                                   )
        # second col
        misc.treeview_append_column(self.processlist_tv, 'User'
                                        ,attributes = (('text',1),)
                                   )

        # third col
        misc.treeview_append_column(self.processlist_tv, 'Process'
                                        ,attributes = (('text',2),)
                                   )
        # init AddCheatDialog
        self.addcheat_address_input = self.builder.get_object('Address_Input')
        self.addcheat_description_input = self.builder.get_object('Description_Input')
        self.addcheat_type_combobox = self.builder.get_object('Type_ComboBox')
        misc.build_combobox(self.addcheat_type_combobox, LOCK_VALUE_TYPES)
        misc.combobox_set_active_item(self.addcheat_type_combobox, SETTINGS['lock_data_type'])
        
        
        
        # init popup menu for scanresult
        self.scanresult_popup = Gtk.Menu()
        misc.menu_append_item(self.scanresult_popup, 'Add to cheat list', self.scanresult_popup_cb, 'add_to_cheat_list')
        misc.menu_append_item(self.scanresult_popup, 'Browse this address', self.scanresult_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.scanresult_popup, 'Scan for this address', self.scanresult_popup_cb, 'scan_for_this_address')
        self.scanresult_popup.show_all()

        # init popup menu for cheatlist
        self.cheatlist_popup = Gtk.Menu()
        misc.menu_append_item(self.cheatlist_popup, 'Browse this address', self.cheatlist_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.cheatlist_popup, 'Copy address', self.cheatlist_popup_cb, 'copy_address')
        misc.menu_append_item(self.cheatlist_popup, 'Remove this entry', self.cheatlist_popup_cb, 'remove_entry')
        self.cheatlist_popup.show_all()

        self.builder.connect_signals(self)
        self.main_window.connect('destroy', self.exit)

        ###########################
        # init others (backend, flag...)
        self.pid = 0 # target pid
        self.maps = []
        self.last_hexedit_address = (0,0) # used for hexview
        self.is_scanning = False
        self.exit_flag = False # currently for data_worker only, other 'threads' may also use this flag
        self.is_data_worker_working = False

        self.backend = GameConquerorBackend()
        self.check_backend_version()
        self.search_count = 0
        GObject.timeout_add(DATA_WORKER_INTERVAL, self.data_worker)
        self.command_lock = threading.RLock()


    ###########################
    # GUI callbacks

    def MemoryEditor_Window_delete_event_cb(self, widget, event, data=None):
        self.memoryeditor_window.hide()
        return True

    def MemoryEditor_Button_clicked_cb(self, button, data=None):
        if self.pid == 0:
            self.show_error('Please select a process')
            return
        self.browse_memory()
        return True

    def MemoryEditor_Address_Entry_activate_cb(self, entry, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error('Invalid address')

    def MemoryEditor_JumpTo_Button_clicked_cb(self, button, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error('Invalid address')

    def ManuallyAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.show()
        return True

    def RemoveAllCheat_Button_clicked_cb(self, button, data=None):
        self.cheatlist_liststore.clear()
        return True

    def LoadCheat_Button_clicked_cb(self, button, data=None):
        dialog = Gtk.FileChooserDialog("Open..",
                self.main_window,
                Gtk.FileChooserAction.OPEN,
                (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                    Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
        dialog.set_default_response(Gtk.ResponseType.OK)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            try:
                with open(dialog.get_filename(), 'rb') as f:
                    obj = json.load(f)
                    for row in obj['cheat_list']:
                        self.add_to_cheat_list(row[3],row[5],row[4],row[2])
            except:
                pass
        dialog.destroy()
        return True

    def SaveCheat_Button_clicked_cb(self, button, data=None):
        dialog = Gtk.FileChooserDialog("Save..",
                self.main_window,
                Gtk.FileChooserAction.SAVE,
                (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                    Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
        dialog.set_default_response(Gtk.ResponseType.OK)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            try:
                with open(dialog.get_filename(), 'wb') as f:
                    obj = {'cheat_list' : [list(i) for i in self.cheatlist_liststore]}
                    json.dump(obj, f);
            except:
                pass
        dialog.destroy()
        return True

    def SearchScope_Scale_format_value_cb(self, scale, value, Data=None):
        return SEARCH_SCOPE_NAMES[int(value)]

    def Value_Input_activate_cb(self, entry, data=None):
        self.do_scan()
        return True

    def ScanResult_TreeView_button_release_event_cb(self, widget, event, data=None):
        if event.button == 3: # right click
            (model, iter) = self.scanresult_tv.get_selection().get_selected()
            if iter is not None:
                self.scanresult_popup.popup(None, None, None, None, event.button, event.get_time())
                return True
            return False
        return False

    def ScanResult_TreeView_popup_menu_cb(self, widget, data=None):
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        if iter is not None:
            self.scanresult_popup.popup(None, None, None, 0, 0)
            return True
        return False

    def ScanResult_TreeView_row_activated_cb(self, treeview, path, view_column, data=None):
        # add to cheat list
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        if iter is not None:
            (addr, value, typestr) = model.get(iter, 0, 1, 2)
            self.add_to_cheat_list(addr, value, typestr)
            return True
        return False

    def CheatList_TreeView_button_release_event_cb(self, widget, event, data=None):
        if event.button == 3: # right click
            (model, iter) = self.cheatlist_tv.get_selection().get_selected()
            if iter is not None:
                self.cheatlist_popup.popup(None, None, None, None, event.button, event.get_time())
                return True
            return False
        return False

    def CheatList_TreeView_popup_menu_cb(self, widget, data=None):
        (model, iter) = self.cheatlist_tv.get_selection().get_selected()
        if iter is not None:
            self.cheatlist_popup.popup(None, None, None, 0, 0)
            return True
        return False

    def ProcessFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def UserFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def ProcessList_TreeView_row_activated_cb(self, treeview, path, view_column, data=None):
        (model, iter) = self.processlist_tv.get_selection().get_selected()
        if iter is not None:
            (pid, user, process) = model.get(iter, 0, 1, 2)
            self.select_process(int(pid), process)
            self.process_list_dialog.response(Gtk.ResponseType.CANCEL)
            return True
        return False

    def SelectProcess_Button_clicked_cb(self, button, data=None):
        self.processlist_liststore.clear()
        pl = self.get_process_list()
        for p in pl:
           # self.processlist_liststore.append([p[0], (p[1][:50] if len(p) > 1 else '<unknown>')]) # limit the length here, otherwise it may crash (why?)
            self.processlist_liststore.append([p[0], (p[1] if len(p) > 1 else '<unknown>'),(p[2] if len(p) > 2 else '<unknown>')])
        self.process_list_dialog.show()
        while True:
            res = self.process_list_dialog.run()
            if res == Gtk.ResponseType.OK: # -5
                (model, iter) = self.processlist_tv.get_selection().get_selected()
                if iter is None:
                    self.show_error('Please select a process')
                    continue
                else:
                    (pid, process) = model.get(iter, 0, 1)
                    self.select_process(int(pid), process)
                    break
            else: # for None and Cancel
                break
        self.process_list_dialog.hide()
        return True

    def ConfirmAddCheat_Button_clicked_cb(self, button, data=None):
        try:
            addr = self.addcheat_address_input.get_text()
        except ValueError:
            self.show_error('Please enter a valid address.')
            return False

        description = self.addcheat_description_input.get_text()
        if not description: description = 'No Description'
        typestr = LOCK_VALUE_TYPES[self.addcheat_type_combobox.get_active()][0]
        if 'int' in typestr: value = 0
        elif 'float' in typestr: value = 0.0
        elif 'string' in typestr: value = ''
        else: value = None
        
        self.add_to_cheat_list(addr, value, typestr, description)
        self.addcheat_dialog.hide()
        return True

    def CloseAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.hide()
        return True

    def Scan_Button_clicked_cb(self, button, data=None):
        self.do_scan()
        return True

    def Reset_Button_clicked_cb(self, button, data=None):
        self.reset_scan()
        return True

    def Logo_EventBox_button_release_event_cb(self, widget, data=None):
        self.about_dialog.run()
        self.about_dialog.hide()
        return True

    #######################
    # customed callbacks
    # (i.e. not standard event names are used)
    
    def memoryeditor_hexview_char_changed_cb(self, hexview, offset, charval):
        addr = hexview.base_addr + offset
        self.write_value(addr, 'int8', charval)
        # return False such that the byte the default handler will be called, and will be displayed correctly 
        return False

    
    def cheatlist_edit_start(self, a, b, c):
        self.cheatlist_editing = True
    def cheatlist_edit_cancel(self, a):
        self.cheatlist_editing = False

    def scanresult_popup_cb(self, menuitem, data=None):
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        (addr, value, typestr) = model.get(iter, 0, 1, 2)
        if iter is None:
            return False
        if data == 'add_to_cheat_list':
            self.add_to_cheat_list(addr, value, typestr)
            return True
        elif data == 'browse_this_address':
            self.browse_memory(int(addr,16))
            return True
        elif data == 'scan_for_this_address':
            self.scan_for_addr(int(addr,16))
            return True
        return False
        
    def cheatlist_keypressed(self, cheatlist_tv, event, selection=None):
        keycode = event.keyval
        pressedkey = Gdk.keyval_name(keycode)
        if pressedkey == 'Delete':
            (model, iter) = self.cheatlist_tv.get_selection().get_selected()
            if iter is None: return
            self.cheatlist_liststore.remove(iter) 

    def cheatlist_popup_cb(self, menuitem, data=None):
        self.cheatlist_editing = False
        (model, iter) = self.cheatlist_tv.get_selection().get_selected()
        addr = model.get(iter, 3)[0]
        if iter is None:
            return False
        if data == 'remove_entry':
            self.cheatlist_liststore.remove(iter) 
            return True
        elif data == 'browse_this_address':
            self.browse_memory(int(addr,16))
            return True
        elif data == 'copy_address':
            CLIPBOARD.set_text(addr, len(addr))
            return True
        return False

    def cheatlist_toggle_lock_cb(self, cellrenderertoggle, path, data=None):
        row = int(path)
        if self.cheatlist_liststore[row][6]: # valid
            locked = self.cheatlist_liststore[row][1]
            locked = not locked
            self.cheatlist_liststore[row][1] = locked
        if locked:
            #TODO: check value(valid number & not overflow), if failed, unlock it and do nothing
            pass
        else:
            #TODO: update its value?
            pass
        return True

    def cheatlist_toggle_lock_flag_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # currently only one lock flag is supported
        return True
        row = int(path)
        self.cheatlist_liststore[row][0] = new_text
        # data_worker will handle this later
        return True

    def cheatlist_edit_description_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        row = int(path)
        self.cheatlist_liststore[row][2] = new_text
        return True

    def cheatlist_edit_value_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # ignore empty value
        if new_text == '':
            return True
        row = int(path)
        if not self.cheatlist_liststore[row][6]: #not valid
            return True
        self.cheatlist_liststore[row][5] = new_text
        if self.cheatlist_liststore[row][1]: # locked
            # data_worker will handle this
            pass
        else:
            # write it for once
            (lockflag, locked, desc, addr, typestr, value, valid) = self.cheatlist_liststore[row]
            self.cheatlist_updates.append(row)
            self.write_value(addr, typestr, value)
        return True

    def cheatlist_edit_type_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        row = int(path)
        self.cheatlist_liststore[row][4] = new_text
        if self.cheatlist_liststore[row][1]: # locked
            # false unlock it
            self.cheatlist_liststore[row][1] = False
            pass
        return True

    def processlist_filter_func(self, model, iter, data=None):
        (pid, user, process) = model.get(iter, 0, 1, 2)
        return process is not None and \
                process.find(self.processfilter_input.get_text()) != -1 and \
                user is not None and \
                user.find(self.userfilter_input.get_text()) != -1
        


    ############################
    # core functions
    def show_error(self, msg):
        dialog = Gtk.MessageDialog(None
                                 ,Gtk.DialogFlags.MODAL
                                 ,Gtk.MessageType.ERROR
                                 ,Gtk.ButtonsType.OK
                                 ,msg)
        dialog.run()
        dialog.destroy()

    # return None if unknown
    def get_pointer_width(self):
        bits = platform.architecture()[0]
        if not bits.endswith('bit'):
            return None
        try:
            bitn = int(bits[:-len('bit')])
            if bitn not in [8,16,32,64]:
                return None
            else:
                return bitn
        except:
            return None
        
    # return the size in bytes of the value in memory
    def get_type_size(self, typename, value):
        if typename in TYPESIZES: # int or float type; fixed length
            return TYPESIZES[typename]
        elif typename == 'bytearray':
            return (len(value.strip())+1)/3
        elif typename == 'string':
            return len(eval('\''+value+'\''))
        return None

    # parse bytes dumped by scanmem into number, string, etc.
    def bytes2value(self, typename, bytes):
        if bytes is None:
            return None
        if typename in TYPENAMES_G2STRUCT:
            return struct.unpack(TYPENAMES_G2STRUCT[typename], bytes)[0]
        elif typename == 'string':
            return repr('%s'%(bytes,))[1:-1]
        elif typename == 'bytearray':
            return ' '.join(['%02x'%ord(i) for i in bytes])
        else:
            return bytes
    
    def scan_for_addr(self, addr):
        bits = self.get_pointer_width()
        if bits is None:
            show_error('Unknown architecture, you may report to developers')
            return
        self.reset_scan()
        self.value_input.set_text('%#x'%(addr,))
        misc.combobox_set_active_item(self.scan_data_type_combobox, 'int%d'%(bits,))
        self.do_scan()

    def browse_memory(self, addr=None):
        # select a region contains addr
        try:
            self.read_maps()
        except:
            show_error('Cannot retieve memory maps of that process, maybe it has exited (crashed), or you don\'t have enough privilege')
        selected_region = None
        if addr:
            for m in self.maps:
                if m['start_addr'] <= addr and addr < m['end_addr']:
                    selected_region = m
                    break
            if selected_region:
                if selected_region['flags'][0] != 'r': # not readable
                    show_error('Address %x is not readable' % (addr,))
                    return
            else:
                show_error('Address %x is not valid' % (addr,))
                return
        else:
            # just select the first readable region
            for m in self.maps:
                if m['flags'][0] == 'r':
                    selected_region = m
                    break
            if selected_region is None:
                self.show_error('Cannot find a readable region')
                return
            addr = selected_region['start_addr']

        # read region if necessary
        start_addr = max(addr - 1024, selected_region['start_addr'])
        end_addr = min(addr + 1024, selected_region['end_addr'])
        if self.last_hexedit_address[0] != start_addr or \
           self.last_hexedit_address[1] != end_addr:
            data = self.read_memory(start_addr, end_addr - start_addr)
            if data is None:
                self.show_error('Cannot read memory')
                return
            self.last_hexedit_address = (start_addr, end_addr)
            self.memoryeditor_hexview.payload=data
            self.memoryeditor_hexview.base_addr = start_addr
        
        # set editable flag
        self.memoryeditor_hexview.editable = (selected_region['flags'][1] == 'w')

        if addr:
            self.memoryeditor_hexview.show_addr(addr)
        self.memoryeditor_window.show()

    # this callback will be called from other thread
    def progress_watcher(self):
        Gdk.threads_enter()
        self.scanprogress_progressbar.set_fraction(self.backend.get_scan_progress())
        Gdk.flush()
        Gdk.threads_leave()
        return True

    def add_to_cheat_list(self, addr, value, typestr, description='No Description'):
        # determine longest possible type
        types = typestr.split()
        vt = typestr
        for t in types:
            if t in TYPENAMES_S2G:
                vt = TYPENAMES_S2G[t]
                break
        self.cheatlist_liststore.prepend(['=', False, description, addr, vt, str(value), True])

    def get_process_list(self):
        return [list(map(str.strip, e.strip().split(' ',2))) for e in os.popen('ps -wweo pid=,user=,command= --sort=-pid').readlines()]

    def select_process(self, pid, process_name):
        # ask backend for attaching the target process
        # update 'current process'
        # reset flags
        # for debug/log
        self.pid = pid
        try:
            self.read_maps()
        except:
            self.pid = 0
            self.process_label.set_text('No process selected')
            self.process_label.set_property('tooltip-text', 'Select a process')
            self.show_error('Cannot retieve memory maps of that process, maybe it has exited (crashed), or you don\'t have enough privilege')
        self.process_label.set_text('%d - %s' % (pid, process_name))
        self.process_label.set_property('tooltip-text', process_name)

        self.command_lock.acquire()
        self.backend.send_command('pid %d' % (pid,))
        self.reset_scan()
        self.command_lock.release()

        # unlock all entries in cheat list
        for i in range(len(self.cheatlist_liststore)):
            self.cheatlist_liststore[i][1] = False

    def read_maps(self):
        lines = open('/proc/%d/maps' % (self.pid,)).readlines()
        self.maps = []
        for l in lines:
            item = {}
            info = l.split(' ', 5)
            addr = info[0]
            idx = addr.index('-')
            item['start_addr'] = int(addr[:idx],16)
            item['end_addr'] = int(addr[idx+1:],16)
            item['size'] = item['end_addr'] - item['start_addr']
            item['flags'] = info[1]
            item['offset'] = info[2]
            item['dev'] = info[3]
            item['inode'] = int(info[4])
            if len(info) < 6:
                item['pathname'] = ''
            else:
                item['pathname'] = info[5].lstrip() # don't use strip
            self.maps.append(item)

    def reset_scan(self):
        self.search_count = 0
        # reset search type and value type
        self.scanresult_liststore.clear()
        
        self.command_lock.acquire()
        self.backend.send_command('reset')
        self.update_scan_result()
        self.command_lock.release()

        self.scanoption_frame.set_sensitive(True)
        self.is_first_scan = True

    def apply_scan_settings (self):
        # scan data type
        active = self.scan_data_type_combobox.get_active()
        assert(active >= 0)
        dt = self.scan_data_type_combobox.get_model()[active][0]

        self.command_lock.acquire()
        self.backend.send_command('option scan_data_type %s' % (dt,))
        # search scope
        self.backend.send_command('option region_scan_level %d' %(1 + int(self.search_scope_scale.get_value()),))
        # TODO: ugly, reset to make region_scan_level taking effect
        self.backend.send_command('reset')
        self.command_lock.release()

    
    # perform scanning through backend
    # set GUI if needed
    def do_scan(self):
        if self.pid == 0:
            self.show_error('Please select a process')
            return
        active = self.scan_data_type_combobox.get_active()
        assert(active >= 0)
        data_type = self.scan_data_type_combobox.get_model()[active][0]
        cmd = self.value_input.get_text()
   
        try:
            cmd = misc.check_scan_command(data_type, cmd, self.is_first_scan)
        except Exception as e:
            # this is not quite good
            self.show_error(e.args[0])
            return

        # disable the window before perform scanning, such that if result come so fast, we won't mess it up
        self.search_count +=1 
        self.scanoption_frame.set_sensitive(False) # no need to check search_count here
        self.main_window.set_sensitive(False)
        self.memoryeditor_window.set_sensitive(False)
        self.is_scanning = True
        # set scan options only when first scan, since this will reset backend
        if self.search_count == 1:
            self.apply_scan_settings()
        self.backend.reset_scan_progress()
        self.progress_watcher_id = GObject.idle_add(self.progress_watcher)
        threading.Thread(target=self.scan_thread_func, args=(cmd,)).start()

    def scan_thread_func(self, cmd):
        self.command_lock.acquire()
        self.backend.send_command(cmd)

        GObject.source_remove(self.progress_watcher_id)
        Gdk.threads_enter()

        self.main_window.set_sensitive(True)
        self.memoryeditor_window.set_sensitive(True)
        self.is_scanning = False
        self.update_scan_result()
        self.is_first_scan = False

        Gdk.flush()
        Gdk.threads_leave()
        self.command_lock.release()

    def update_scan_result(self):
        match_count = self.backend.get_match_count()
        self.found_count_label.set_text('Found: %d' % (match_count,))
        if (match_count > SCAN_RESULT_LIST_LIMIT):
            self.scanresult_liststore.clear()
        else:
            self.command_lock.acquire()
            lines = self.backend.send_command('list', get_output = True)
            self.command_lock.release()

            self.scanresult_tv.set_model(None)
            # temporarily disable model for scanresult_liststore for the sake of performance
            self.scanresult_liststore.clear()
            for line in lines:
                line = line[line.find(']')+1:]
                (a, v, t) = list(map(str.strip, line.split(',')[:3]))
                a = '%x'%(int(a,16),)
                t = t[1:-1]
                self.scanresult_liststore.append([a, v, t, True])
            self.scanresult_tv.set_model(self.scanresult_liststore)

    # return (r1, r2) where all rows between r1 and r2 (INCLUSIVE) are visible
    # return None if no row visible
    def get_visible_rows(self, treeview):
        rect = treeview.get_visible_rect()
        (x1,y1) = treeview.convert_tree_to_widget_coords(rect.x,rect.y)
        (x2,y2) = treeview.convert_tree_to_widget_coords(rect.x+rect.width,rect.y+rect.height)
        tup = treeview.get_path_at_pos(x1, y1)
        if tup is None:
            return None
        r1 = tup[0][0]
        tup = treeview.get_path_at_pos(x2, y2)
        if tup is None:
            r2 = len(treeview.get_model()) - 1    
        else:
            r2 = tup[0][0]
        return (r1, r2)
 
    # read/write data periodically
    def data_worker(self):
        if (not self.is_scanning) and (self.pid != 0) and self.command_lock.acquire(0): # non-blocking
            Gdk.threads_enter()

            self.is_data_worker_working = True
            rows = self.get_visible_rows(self.scanresult_tv)
            if rows is not None:
                (r1, r2) = rows # [r1, r2] rows are visible
                for i in range(r1, r2+1):
                    row = self.scanresult_liststore[i]
                    addr, cur_value, scanmem_type, valid = row
                    if valid:
                        new_value = self.read_value(addr, TYPENAMES_S2G[scanmem_type.strip()], cur_value)
                        if new_value is not None:
                            row[1] = str(new_value)
                        else:
                            row[1] = '??'
                            row[3] = False
            # write locked values in cheat list and read unlocked values
            for i in range(len(self.cheatlist_liststore)):
                (lockflag, locked, desc, addr, typestr, value, valid) = self.cheatlist_liststore[i]
                if not valid:
                    continue
                if locked:
                    self.write_value(addr, typestr, value)
                elif i in self.cheatlist_updates:
                    self.write_value(addr, typestr, value)
                    self.cheatlist_updates.remove(i)
                else:
                    newvalue = self.read_value(addr, typestr, value)
                    if newvalue is None:
                        self.cheatlist_liststore[i] = (lockflag, False, desc, addr, typestr, '??', False)
                    elif newvalue != value and not locked and not self.cheatlist_editing:
                        self.cheatlist_liststore[i] = (lockflag, locked, desc, addr, typestr, str(newvalue), valid)
            self.is_data_worker_working = False 

            Gdk.flush()
            Gdk.threads_leave()
            self.command_lock.release()           
        return not self.exit_flag

    def read_value(self, addr, typestr, prev_value):
        return self.bytes2value(typestr, self.read_memory(addr, self.get_type_size(typestr, prev_value)))
    
    # addr could be int or str
    def read_memory(self, addr, length):
        if not isinstance(addr,str):
            addr = '%x'%(addr,)
        f = tempfile.NamedTemporaryFile()

        self.command_lock.acquire()
        self.backend.send_command('dump %s %d %s' % (addr, length, f.name))
        self.command_lock.release()

        data = f.read()

#        lines = self.backend.send_command('dump %s %d' % (addr, length))
#        data = ''
#        for line in lines:
#            bytes = line.strip().split()
#            for byte in bytes: data += chr(int(byte, 16))
        # TODO raise Exception here isn't good
        if len(data) != length:
            # self.show_error('Cannot access target memory')
            data = None
        return data
            
    # addr could be int or str
    def write_value(self, addr, typestr, value):
        if not isinstance(addr,str):
            addr = '%x'%(addr,)

        self.command_lock.acquire()
        self.backend.send_command('write %s %s %s'%(typestr, addr, value))
        self.command_lock.release()

    def exit(self, object, data=None):
        self.exit_flag = True
        Gtk.main_quit()

    def main(self):
        Gtk.main()

    def check_backend_version(self):
        if self.backend.get_version() != VERSION:
            self.show_error('Version of scanmem mismatched, you may encounter problems. Please make sure you are using the same version of Gamconqueror as scanmem.')
Пример #4
0
class GameConqueror():
    def __init__(self):
        ##################################
        # init GUI
        self.builder = Gtk.Builder()
        self.builder.set_translation_domain(GETTEXT_PACKAGE)
        self.builder.add_from_file(os.path.join(WORK_DIR, 'GameConqueror.xml'))

        self.main_window = self.builder.get_object('MainWindow')
        self.main_window.set_title('GameConqueror %s' % (VERSION, ))
        self.about_dialog = self.builder.get_object('AboutDialog')
        # set version
        self.about_dialog.set_version(VERSION)

        self.process_list_dialog = self.builder.get_object('ProcessListDialog')
        self.addcheat_dialog = self.builder.get_object('AddCheatDialog')

        # init memory editor
        self.memoryeditor_window = self.builder.get_object(
            'MemoryEditor_Window')
        self.memoryeditor_hexview = HexView()
        self.memoryeditor_window.get_child().pack_start(
            self.memoryeditor_hexview, True, True, 0)
        self.memoryeditor_hexview.show_all()
        self.memoryeditor_address_entry = self.builder.get_object(
            'MemoryEditor_Address_Entry')
        self.memoryeditor_hexview.connect(
            'char-changed', self.memoryeditor_hexview_char_changed_cb)

        self.found_count_label = self.builder.get_object('FoundCount_Label')
        self.process_label = self.builder.get_object('Process_Label')
        self.value_input = self.builder.get_object('Value_Input')

        self.scanoption_frame = self.builder.get_object('ScanOption_Frame')
        self.scanprogress_progressbar = self.builder.get_object(
            'ScanProgress_ProgressBar')
        self.input_box = self.builder.get_object('Value_Input')

        self.scan_button = self.builder.get_object('Scan_Button')
        self.reset_button = self.builder.get_object('Reset_Button')

        ###
        # Set scan data type
        self.scan_data_type_combobox = self.builder.get_object(
            'ScanDataType_ComboBoxText')
        for entry in SCAN_VALUE_TYPES:
            self.scan_data_type_combobox.append_text(entry)
        # apply setting
        misc.combobox_set_active_item(self.scan_data_type_combobox,
                                      SETTINGS['scan_data_type'])

        ###
        # set search scope
        self.search_scope_scale = self.builder.get_object('SearchScope_Scale')
        # apply setting
        self.search_scope_scale.set_value(SETTINGS['search_scope'])

        # init scanresult treeview
        # we may need a cell data func here
        # create model
        self.scanresult_tv = self.builder.get_object('ScanResult_TreeView')
        # liststore contents:                     addr,                value, type, valid, offset,              region type, match_id
        self.scanresult_liststore = Gtk.ListStore(GObject.TYPE_UINT64, str,
                                                  str, bool,
                                                  GObject.TYPE_UINT64, str,
                                                  int)
        self.scanresult_tv.set_model(self.scanresult_liststore)
        # init columns
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Address'),
                                    0,
                                    hex_col=0,
                                    attributes=(('text', 0), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Value'),
                                    1,
                                    attributes=(('text', 1), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Offset'),
                                    4,
                                    hex_col=4,
                                    attributes=(('text', 4), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Region Type'),
                                    5,
                                    attributes=(('text', 5), ),
                                    properties=(('family', 'monospace'), ))

        # init CheatList TreeView
        self.cheatlist_tv = self.builder.get_object('CheatList_TreeView')
        # cheatlist contents:                    lockflag, locked, description, addr,                type, value, valid
        self.cheatlist_liststore = Gtk.ListStore(str, bool, str,
                                                 GObject.TYPE_UINT64, str, str,
                                                 bool)
        self.cheatlist_tv.set_model(self.cheatlist_liststore)
        self.cheatlist_editing = False
        # Lock Flag
        misc.treeview_append_column(
            self.cheatlist_tv,
            '',
            0,
            renderer_class=Gtk.CellRendererCombo,
            attributes=(('text', 0), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model', LOCK_FLAG_TYPES), ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_toggle_lock_flag_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Lock
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Lock'),
            1,
            renderer_class=Gtk.CellRendererToggle,
            attributes=(('active', 1), ),
            properties=(('activatable', True), ('radio', False),
                        ('inconsistent', False)),
            signals=(('toggled', self.cheatlist_toggle_lock_cb), ))
        # Description
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Description'),
            2,
            attributes=(('text', 2), ),
            properties=(('editable', True), ),
            signals=(
                ('edited', self.cheatlist_edit_description_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Address
        misc.treeview_append_column(self.cheatlist_tv,
                                    _('Address'),
                                    3,
                                    hex_col=3,
                                    attributes=(('text', 3), ),
                                    properties=(('family', 'monospace'), ))
        # Type
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Type'),
            4,
            renderer_class=Gtk.CellRendererCombo,
            attributes=(('text', 4), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model',
                         misc.build_simple_str_liststore(MEMORY_TYPES)),
                        ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_edit_type_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Value
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Value'),
            5,
            attributes=(('text', 5), ),
            properties=(('editable', True), ('family', 'monospace')),
            signals=(
                ('edited', self.cheatlist_edit_value_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))

        # init ProcessList
        self.processfilter_input = self.builder.get_object(
            'ProcessFilter_Input')
        self.userfilter_input = self.builder.get_object('UserFilter_Input')
        # init ProcessList_TreeView
        self.processlist_tv = self.builder.get_object('ProcessList_TreeView')
        self.processlist_liststore = Gtk.ListStore(int, str, str)
        self.processlist_filter = self.processlist_liststore.filter_new(
            root=None)
        self.processlist_filter.set_visible_func(self.processlist_filter_func,
                                                 data=None)
        self.processlist_tv.set_model(
            Gtk.TreeModelSort(model=self.processlist_filter))
        self.processlist_tv.set_search_column(1)
        # first col
        misc.treeview_append_column(self.processlist_tv,
                                    'PID',
                                    0,
                                    attributes=(('text', 0), ))
        # second col
        misc.treeview_append_column(self.processlist_tv,
                                    _('User'),
                                    1,
                                    attributes=(('text', 1), ))

        # third col
        misc.treeview_append_column(self.processlist_tv,
                                    _('Process'),
                                    2,
                                    attributes=(('text', 2), ))

        # get list of things to be disabled during scan
        self.disablelist = []
        self.disablelist.append(self.cheatlist_tv)
        self.disablelist.append(self.scanresult_tv)
        self.disablelist.append(self.builder.get_object('processGrid'))
        self.disablelist.append(self.builder.get_object('searchGrid'))
        self.disablelist.append(self.builder.get_object('buttonGrid'))
        self.disablelist.append(self.memoryeditor_window)

        # init AddCheatDialog
        self.addcheat_address_input = self.builder.get_object('Address_Input')
        self.addcheat_address_input.override_font(
            gi.repository.Pango.FontDescription("Monospace"))

        self.addcheat_description_input = self.builder.get_object(
            'Description_Input')
        self.addcheat_length_spinbutton = self.builder.get_object(
            'Length_SpinButton')

        self.addcheat_type_combobox = self.builder.get_object(
            'Type_ComboBoxText')
        for entry in MEMORY_TYPES:
            self.addcheat_type_combobox.append_text(entry)
        misc.combobox_set_active_item(self.addcheat_type_combobox,
                                      SETTINGS['lock_data_type'])
        self.Type_ComboBoxText_changed_cb(self.addcheat_type_combobox)

        # init popup menu for scanresult
        self.scanresult_popup = Gtk.Menu()
        misc.menu_append_item(self.scanresult_popup, _('Add to cheat list'),
                              self.scanresult_popup_cb, 'add_to_cheat_list')
        misc.menu_append_item(self.scanresult_popup, _('Browse this address'),
                              self.scanresult_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.scanresult_popup,
                              _('Scan for this address'),
                              self.scanresult_popup_cb,
                              'scan_for_this_address')
        misc.menu_append_item(self.scanresult_popup, _('Remove this match'),
                              self.scanresult_delete_selected_matches)
        self.scanresult_popup.show_all()

        # init popup menu for cheatlist
        self.cheatlist_popup = Gtk.Menu()
        misc.menu_append_item(self.cheatlist_popup, _('Browse this address'),
                              self.cheatlist_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.cheatlist_popup, _('Copy address'),
                              self.cheatlist_popup_cb, 'copy_address')
        misc.menu_append_item(self.cheatlist_popup, _('Remove this entry'),
                              self.cheatlist_popup_cb, 'remove_entry')
        self.cheatlist_popup.show_all()

        self.builder.connect_signals(self)
        self.main_window.connect('destroy', self.exit)

        ###########################
        # init others (backend, flag...)
        self.pid = 0  # target pid
        self.maps = []
        self.last_hexedit_address = (0, 0)  # used for hexview
        self.is_scanning = False
        self.exit_flag = False  # currently for data_worker only, other 'threads' may also use this flag

        self.backend = GameConquerorBackend(
            os.path.join(LIBDIR, 'libscanmem.so.1'))
        self.check_backend_version()
        self.is_first_scan = True
        GLib.timeout_add(DATA_WORKER_INTERVAL, self.data_worker)
        self.command_lock = threading.RLock()

    ###########################
    # GUI callbacks

    # Memory editor

    def MemoryEditor_Button_clicked_cb(self, button, data=None):
        if self.pid == 0:
            self.show_error(_('Please select a process'))
            return
        self.browse_memory()
        return True

    def MemoryEditor_Address_Entry_activate_cb(self, entry, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error(_('Invalid address'))

    def MemoryEditor_JumpTo_Button_clicked_cb(self, button, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error(_('Invalid address'))

    # Manually add cheat

    def ConfirmAddCheat_Button_clicked_cb(self, button, data=None):
        addr = self.addcheat_address_input.get_text()
        try:
            addr = int(addr, 16)
        except ValueError:
            self.show_error(_('Please enter a valid address.'))
            return False

        description = self.addcheat_description_input.get_text()
        if not description: description = _('No Description')

        typestr = self.addcheat_type_combobox.get_active_text()
        length = self.addcheat_length_spinbutton.get_value_as_int()
        if 'int' in typestr: value = 0
        elif 'float' in typestr: value = 0.0
        elif typestr == 'string': value = ' ' * length
        elif typestr == 'bytearray': value = '00 ' * length
        else: value = None

        self.add_to_cheat_list(addr, value, typestr, description)
        self.addcheat_dialog.hide()
        return True

    def CloseAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.hide()
        return True

    # Main window

    def ManuallyAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.show()
        return True

    def RemoveAllCheat_Button_clicked_cb(self, button, data=None):
        self.cheatlist_liststore.clear()
        return True

    def LoadCheat_Button_clicked_cb(self, button, data=None):
        dialog = Gtk.FileChooserDialog(
            title=_("Open.."),
            transient_for=self.main_window,
            action=Gtk.FileChooserAction.OPEN,
            buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN,
                     Gtk.ResponseType.OK))
        dialog.set_default_response(Gtk.ResponseType.OK)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            try:
                with open(dialog.get_filename(), 'r') as f:
                    obj = json.load(f)
                    for row in obj['cheat_list']:
                        self.add_to_cheat_list(row[3], row[5], row[4], row[2],
                                               True)
            except:
                pass
        dialog.destroy()
        return True

    def SaveCheat_Button_clicked_cb(self, button, data=None):
        dialog = Gtk.FileChooserDialog(
            title=_("Save.."),
            transient_for=self.main_window,
            action=Gtk.FileChooserAction.SAVE,
            buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE,
                     Gtk.ResponseType.OK))
        dialog.set_default_response(Gtk.ResponseType.OK)
        dialog.set_do_overwrite_confirmation(True)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            try:
                with open(dialog.get_filename(), 'w') as f:
                    obj = {
                        'cheat_list':
                        [list(i) for i in self.cheatlist_liststore]
                    }
                    json.dump(obj, f)
            except:
                pass
        dialog.destroy()
        return True

    def SearchScope_Scale_format_value_cb(self, scale, value, Data=None):
        return SEARCH_SCOPE_NAMES[int(value)]

    def Value_Input_activate_cb(self, entry, data=None):
        self.do_scan()
        return True

    def ScanResult_TreeView_popup_menu_cb(self, widget, data=None):
        pathlist = self.scanresult_tv.get_selection().get_selected_rows()[1]
        if len(pathlist):
            self.scanresult_popup.popup(None, None, None, None, 0, 0)
            return True
        return False

    def ScanResult_TreeView_button_press_event_cb(self,
                                                  widget,
                                                  event,
                                                  data=None):
        # add to cheat list
        (model,
         pathlist) = self.scanresult_tv.get_selection().get_selected_rows()
        if event.button == 1 and event.get_click_count(
        )[1] > 1:  # left double click
            for path in pathlist:
                (addr, value, typestr) = model.get(model.get_iter(path), 0, 1,
                                                   2)
                self.add_to_cheat_list(addr, value, typestr)
        elif event.button == 3:  # right click
            path = self.scanresult_tv.get_path_at_pos(int(event.x),
                                                      int(event.y))
            if path is not None:
                self.scanresult_popup.popup(None, None, None, None,
                                            event.button, event.get_time())
                return path[0] in pathlist
        return False

    def CheatList_TreeView_button_press_event_cb(self,
                                                 widget,
                                                 event,
                                                 data=None):
        if event.button == 3:  # right click
            pathlist = self.cheatlist_tv.get_selection().get_selected_rows()[1]
            path = self.cheatlist_tv.get_path_at_pos(int(event.x),
                                                     int(event.y))
            if path is not None:
                self.cheatlist_popup.popup(None, None, None, None,
                                           event.button, event.get_time())
                return path[0] in pathlist
        return False

    def CheatList_TreeView_popup_menu_cb(self, widget, data=None):
        pathlist = self.cheatlist_tv.get_selection().get_selected_rows()[1]
        if len(pathlist):
            self.cheatlist_popup.popup(None, None, None, None, 0, 0)
            return True
        return False

    def Scan_Button_clicked_cb(self, button, data=None):
        self.do_scan()
        return True

    def Reset_Button_clicked_cb(self, button, data=None):
        self.reset_scan()
        return True

    def Logo_EventBox_button_release_event_cb(self, widget, data=None):
        self.about_dialog.run()
        self.about_dialog.hide()
        return True

    # Process list

    def ProcessFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def UserFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def ProcessList_TreeView_row_activated_cb(self,
                                              treeview,
                                              path,
                                              view_column,
                                              data=None):
        (model, iter) = self.processlist_tv.get_selection().get_selected()
        if iter is not None:
            (pid, process) = model.get(iter, 0, 2)
            self.select_process(pid, process)
            self.process_list_dialog.response(Gtk.ResponseType.CANCEL)
            return True
        return False

    def SelectProcess_Button_clicked_cb(self, button, data=None):
        self.processlist_liststore.clear()
        pl = self.get_process_list()
        for p in pl:
            # self.processlist_liststore.append([p[0], (p[1][:50] if len(p) > 1 else '<unknown>')]) # limit the length here, otherwise it may crash (why?)
            self.processlist_liststore.append([
                p[0], (p[1] if len(p) > 1 else '<unknown>'),
                (p[2] if len(p) > 2 else '<unknown>')
            ])
        self.process_list_dialog.show()
        while True:
            res = self.process_list_dialog.run()
            if res == Gtk.ResponseType.OK:  # -5
                (model,
                 iter) = self.processlist_tv.get_selection().get_selected()
                if iter is None:
                    self.show_error(_('Please select a process'))
                    continue
                else:
                    (pid, process) = model.get(iter, 0, 2)
                    self.select_process(pid, process)
                    break
            else:  # for None and Cancel
                break
        self.process_list_dialog.hide()
        return True

    #######################
    # customed callbacks
    # (i.e. not standard event names are used)

    # Callback to hide window when 'X' button is pressed
    def hide_window_on_delete_event_cb(self, widget, event, data=None):
        widget.hide()
        return True

    # Memory editor

    def memoryeditor_hexview_char_changed_cb(self, hexview, offset, charval):
        addr = hexview.base_addr + offset
        self.write_value(addr, 'int8', charval)
        # return False such that the byte the default handler will be called, and will be displayed correctly
        return False

    # Manually add cheat

    def focus_on_next_widget_cb(self, widget, data=None):
        widget.get_toplevel().child_focus(Gtk.DirectionType.TAB_FORWARD)
        return True

    def Type_ComboBoxText_changed_cb(self, combo_box):
        data_type = combo_box.get_active_text()
        if data_type in TYPESIZES:
            self.addcheat_length_spinbutton.set_value(TYPESIZES[data_type])
            self.addcheat_length_spinbutton.set_sensitive(False)
        else:
            self.addcheat_length_spinbutton.set_sensitive(True)

    # Main window

    def cheatlist_edit_start(self, a, b, c):
        self.cheatlist_editing = True

    def cheatlist_edit_cancel(self, a):
        self.cheatlist_editing = False

    def scanresult_delete_selected_matches(self, menuitem, data=None):
        (model,
         pathlist) = self.scanresult_tv.get_selection().get_selected_rows()
        match_id_list = [
            model.get_value(model.get_iter(path), 6) for path in pathlist
        ]
        self.command_lock.acquire()
        for mid in sorted(
                match_id_list, reverse=True
        ):  # Start from the largest, so no match id gets invalidated
            self.backend.send_command('delete %d' % (mid, ))
        self.update_scan_result()
        self.command_lock.release()

    def scanresult_popup_cb(self, menuitem, data=None):
        (model,
         pathlist) = self.scanresult_tv.get_selection().get_selected_rows()
        if data == 'add_to_cheat_list':
            for path in reversed(pathlist):
                (addr, value, typestr) = model.get(model.get_iter(path), 0, 1,
                                                   2)
                self.add_to_cheat_list(addr, value, typestr)
            return True
        addr = model.get_value(model.get_iter(pathlist[0]), 0)
        if data == 'browse_this_address':
            self.browse_memory(addr)
            return True
        elif data == 'scan_for_this_address':
            self.scan_for_addr(addr)
            return True
        return False

    def ScanResult_TreeView_key_press_event_cb(self,
                                               scanresult_tv,
                                               event,
                                               data=None):
        keycode = event.keyval
        pressedkey = Gdk.keyval_name(keycode)
        if pressedkey == 'Return':
            (model, pathlist
             ) = self.scanresult_tv.get_selection().get_selected_rows()
            for path in reversed(pathlist):
                (addr, value, typestr) = model.get(model.get_iter(path), 0, 1,
                                                   2)
                self.add_to_cheat_list(addr, value, typestr)
        elif pressedkey in {'Delete', 'BackSpace'}:
            self.scanresult_delete_selected_matches(None)

    def CheatList_TreeView_key_press_event_cb(self,
                                              cheatlist_tv,
                                              event,
                                              data=None):
        keycode = event.keyval
        pressedkey = Gdk.keyval_name(keycode)
        if pressedkey in {'Delete', 'BackSpace'}:
            (model,
             pathlist) = self.cheatlist_tv.get_selection().get_selected_rows()
            for path in reversed(pathlist):
                self.cheatlist_liststore.remove(model.get_iter(path))

    def cheatlist_popup_cb(self, menuitem, data=None):
        self.cheatlist_editing = False
        (model,
         pathlist) = self.cheatlist_tv.get_selection().get_selected_rows()
        if data == 'remove_entry':
            for path in reversed(pathlist):
                self.cheatlist_liststore.remove(model.get_iter(path))
            return True
        addr = model.get_value(model.get_iter(pathlist[0]), 3)
        if data == 'browse_this_address':
            self.browse_memory(addr)
            return True
        elif data == 'copy_address':
            addr = '%x' % (addr, )
            CLIPBOARD.set_text(addr, len(addr))
            return True
        return False

    def cheatlist_toggle_lock(self, row):
        if self.cheatlist_liststore[row][6]:  # valid
            locked = self.cheatlist_liststore[row][1]
            locked = not locked
            self.cheatlist_liststore[row][1] = locked
        if locked:
            #TODO: check value(valid number & not overflow), if failed, unlock it and do nothing
            pass
        else:
            #TODO: update its value?
            pass
        return True

    def cheatlist_toggle_lock_cb(self, cellrenderertoggle, row_str, data=None):
        pathlist = self.cheatlist_tv.get_selection().get_selected_rows()[1]
        if not row_str:
            return True
        cur_row = int(row_str)
        # check if the current row is part of the selection
        found = False
        for path in pathlist:
            row = path[0]
            if row == cur_row:
                found = True
                break
        if not found:
            self.cheatlist_toggle_lock(cur_row)
            return True
        # the current row is part of the selection
        for path in pathlist:
            row = path[0]
            self.cheatlist_toggle_lock(row)
        return True

    def cheatlist_toggle_lock_flag_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # currently only one lock flag is supported
        return True
        row = int(path)
        self.cheatlist_liststore[row][0] = new_text
        # data_worker will handle this later
        return True

    def cheatlist_edit_description_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        pathlist = self.cheatlist_tv.get_selection().get_selected_rows()[1]
        for path in pathlist:
            row = path[0]
            self.cheatlist_liststore[row][2] = new_text
        return True

    def cheatlist_edit_value_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # ignore empty value
        if new_text == '':
            return True
        pathlist = self.cheatlist_tv.get_selection().get_selected_rows()[1]
        for path in pathlist:
            row = path[0]
            if not self.cheatlist_liststore[row][6]:  #not valid
                continue
            self.cheatlist_liststore[row][5] = new_text
            if self.cheatlist_liststore[row][1]:  # locked
                # data_worker will handle this
                pass
            else:
                (addr, typestr, value) = self.cheatlist_liststore[row][3:6]
                self.write_value(addr, typestr, value)
        return True

    def cheatlist_edit_type_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        pathlist = self.cheatlist_tv.get_selection().get_selected_rows()[1]
        for path in pathlist:
            row = path[0]
            (addr, typestr, value) = self.cheatlist_liststore[row][3:6]
            if new_text == typestr:
                continue
            if new_text in {'bytearray', 'string'}:
                self.cheatlist_liststore[row][5] = self.bytes2value(
                    new_text,
                    self.read_memory(addr, self.get_type_size(typestr, value)))
            self.cheatlist_liststore[row][4] = new_text
            self.cheatlist_liststore[row][1] = False  # unlock
        return True

    # Process list

    def processlist_filter_func(self, model, iter, data=None):
        (user, process) = model.get(iter, 1, 2)
        return process is not None and \
                self.processfilter_input.get_text().lower() in process.lower() and \
                user is not None and \
                self.userfilter_input.get_text().lower() in user.lower()

    ############################
    # core functions
    def show_error(self, msg):
        dialog = Gtk.MessageDialog(transient_for=self.main_window,
                                   modal=True,
                                   message_type=Gtk.MessageType.ERROR,
                                   buttons=Gtk.ButtonsType.OK,
                                   text=msg)
        dialog.run()
        dialog.destroy()

    # return None if unknown
    def get_pointer_width(self):
        bits = platform.architecture()[0]
        if not bits.endswith('bit'):
            return None
        try:
            bitn = int(bits[:-len('bit')])
            if bitn not in {8, 16, 32, 64}:
                return None
            else:
                return bitn
        except:
            return None

    # return the size in bytes of the value in memory
    def get_type_size(self, typename, value):
        if typename in TYPESIZES:  # int or float type; fixed length
            return TYPESIZES[typename]
        elif typename == 'bytearray':
            return (len(value.strip()) + 1) / 3
        elif typename == 'string':
            return len(misc.encode(value))
        return None

    # parse bytes dumped by scanmem into number, string, etc.
    def bytes2value(self, typename, databytes):
        if databytes is None:
            return None
        if typename in TYPENAMES_G2STRUCT:
            return struct.unpack(TYPENAMES_G2STRUCT[typename], databytes)[0]
        elif typename == 'string':
            return misc.decode(databytes, 'replace')
        elif typename == 'bytearray':
            databytes = misc.str2bytes(databytes)
            return ' '.join(['%02x' % (i, ) for i in databytes])
        else:
            return databytes

    def scan_for_addr(self, addr):
        bits = self.get_pointer_width()
        if bits is None:
            self.show_error(
                _('Unknown architecture, you may report to developers'))
            return
        self.reset_scan()
        self.value_input.set_text('%#x' % (addr, ))
        misc.combobox_set_active_item(self.scan_data_type_combobox,
                                      'int%d' % (bits, ))
        self.do_scan()

    def browse_memory(self, addr=None):
        # select a region contains addr
        try:
            self.read_maps()
        except:
            self.show_error(
                _('Cannot retrieve memory maps of that process, maybe it has '
                  'exited (crashed), or you don\'t have enough privileges'))
            return
        selected_region = None
        if addr:
            for m in self.maps:
                if m['start_addr'] <= addr and addr < m['end_addr']:
                    selected_region = m
                    break
            if selected_region:
                if selected_region['flags'][0] != 'r':  # not readable
                    self.show_error(_('Address %x is not readable') % (addr, ))
                    return
            else:
                self.show_error(_('Address %x is not valid') % (addr, ))
                return
        else:
            # just select the first readable region
            for m in self.maps:
                if m['flags'][0] == 'r':
                    selected_region = m
                    break
            if selected_region is None:
                self.show_error(_('Cannot find a readable region'))
                return
            addr = selected_region['start_addr']

        # read region if necessary
        start_addr = max(addr - 1024, selected_region['start_addr'])
        end_addr = min(addr + 1024, selected_region['end_addr'])
        if self.last_hexedit_address[0] != start_addr or \
           self.last_hexedit_address[1] != end_addr:
            data = self.read_memory(start_addr, end_addr - start_addr)
            if data is None:
                self.show_error(_('Cannot read memory'))
                return
            self.last_hexedit_address = (start_addr, end_addr)
            self.memoryeditor_hexview.payload = misc.str2bytes(data)
            self.memoryeditor_hexview.base_addr = start_addr

        # set editable flag
        self.memoryeditor_hexview.editable = (
            selected_region['flags'][1] == 'w')

        if addr:
            self.memoryeditor_hexview.show_addr(addr)
        self.memoryeditor_window.show()

    # this callback will be called from other thread
    def progress_watcher(self):
        Gdk.threads_enter()
        self.scanprogress_progressbar.set_fraction(
            self.backend.get_scan_progress())
        Gdk.threads_leave()
        return True

    def add_to_cheat_list(self,
                          addr,
                          value,
                          typestr,
                          description=_('No Description'),
                          at_end=False):
        # determine longest possible type
        types = typestr.split()
        vt = typestr
        for t in types:
            if t in TYPENAMES_S2G:
                vt = TYPENAMES_S2G[t]
                break
        if at_end:
            self.cheatlist_liststore.append(
                ['=', False, description, addr, vt,
                 str(value), True])
        else:
            self.cheatlist_liststore.prepend(
                ['=', False, description, addr, vt,
                 str(value), True])

    def get_process_list(self):
        plist = []
        for proc in os.popen(
                'ps -wweo pid=,user:16=,command= --sort=-pid').readlines():
            (pid, user, pname) = [tok.strip() for tok in proc.split(None, 2)]
            plist.append((int(pid), user, pname))
        return plist

    def select_process(self, pid, process_name):
        # ask backend for attaching the target process
        # update 'current process'
        # reset flags
        # for debug/log
        self.pid = pid
        try:
            self.read_maps()
        except:
            self.pid = 0
            self.process_label.set_text(_('No process selected'))
            self.process_label.set_property('tooltip-text',
                                            _('Select a process'))
            self.show_error(
                _('Cannot retrieve memory maps of that process, maybe it has '
                  'exited (crashed), or you don\'t have enough privileges'))
        else:
            self.process_label.set_text('%d - %s' % (pid, process_name))
            self.process_label.set_property('tooltip-text', process_name)

        self.command_lock.acquire()
        self.backend.send_command('pid %d' % (pid, ))
        self.reset_scan()
        self.command_lock.release()

        # unlock all entries in cheat list
        for i in range(len(self.cheatlist_liststore)):
            self.cheatlist_liststore[i][1] = False

    def read_maps(self):
        lines = open('/proc/%d/maps' % (self.pid, )).readlines()
        self.maps = []
        for l in lines:
            item = {}
            info = l.split(' ', 5)
            addr = info[0]
            idx = addr.index('-')
            item['start_addr'] = int(addr[:idx], 16)
            item['end_addr'] = int(addr[idx + 1:], 16)
            item['size'] = item['end_addr'] - item['start_addr']
            item['flags'] = info[1]
            item['offset'] = info[2]
            item['dev'] = info[3]
            item['inode'] = int(info[4])
            if len(info) < 6:
                item['pathname'] = ''
            else:
                item['pathname'] = info[5].lstrip()  # don't use strip
            self.maps.append(item)

    def reset_scan(self):
        # reset search type and value type
        self.scanresult_liststore.clear()

        self.command_lock.acquire()
        self.backend.send_command('reset')
        self.update_scan_result()
        self.command_lock.release()

        self.scanprogress_progressbar.set_fraction(0.0)
        self.scanoption_frame.set_sensitive(True)
        self.is_first_scan = True

    def apply_scan_settings(self):
        # scan data type
        assert (self.scan_data_type_combobox.get_active() >= 0)
        datatype = self.scan_data_type_combobox.get_active_text()

        # Tell the scanresult sort function if a numeric cast is needed
        isnumeric = ('int' in datatype or 'float' in datatype
                     or 'number' in datatype)
        self.scanresult_liststore.set_sort_func(1, misc.value_compare,
                                                (1, isnumeric))

        self.command_lock.acquire()
        self.backend.send_command('option scan_data_type %s' % (datatype, ))
        # search scope
        self.backend.send_command(
            'option region_scan_level %d' %
            (1 + int(self.search_scope_scale.get_value()), ))
        # TODO: ugly, reset to make region_scan_level taking effect
        self.backend.send_command('reset')
        self.command_lock.release()

    # perform scanning through backend
    # set GUI if needed
    def do_scan(self):
        if self.pid == 0:
            self.show_error(_('Please select a process'))
            return
        assert (self.scan_data_type_combobox.get_active() >= 0)
        data_type = self.scan_data_type_combobox.get_active_text()
        cmd = self.value_input.get_text()

        try:
            cmd = misc.check_scan_command(data_type, cmd, self.is_first_scan)
        except Exception as e:
            # this is not quite good
            self.show_error(e.args[0])
            return

        # disable the window before perform scanning, such that if result come so fast, we won't mess it up
        self.scanoption_frame.set_sensitive(False)

        # disable set of widgets interfering with the scan
        for wid in self.disablelist:
            wid.set_sensitive(False)

        self.is_scanning = True
        # set scan options only when first scan, since this will reset backend
        if self.is_first_scan:
            self.apply_scan_settings()
            self.is_first_scan = False
        self.backend.reset_scan_progress()
        self.progress_watcher_id = GLib.timeout_add(
            PROGRESS_INTERVAL,
            self.progress_watcher,
            priority=GLib.PRIORITY_DEFAULT_IDLE)
        threading.Thread(target=self.scan_thread_func, args=(cmd, )).start()

    def scan_thread_func(self, cmd):
        self.command_lock.acquire()
        self.backend.send_command(cmd)

        GLib.source_remove(self.progress_watcher_id)
        Gdk.threads_enter()

        self.scanprogress_progressbar.set_fraction(1.0)

        # enable set of widgets interfering with the scan
        for wid in self.disablelist:
            wid.set_sensitive(True)

        self.is_scanning = False
        self.update_scan_result()

        Gdk.threads_leave()
        self.command_lock.release()

    def update_scan_result(self):
        match_count = self.backend.get_match_count()
        self.found_count_label.set_text(_('Found: %d') % (match_count, ))
        if (match_count > SCAN_RESULT_LIST_LIMIT):
            self.scanresult_liststore.clear()
        else:
            self.command_lock.acquire()
            lines = self.backend.send_command('list', get_output=True)
            self.command_lock.release()

            self.scanresult_tv.set_model(None)
            # temporarily disable model for scanresult_liststore for the sake of performance
            self.scanresult_liststore.clear()
            for line in lines:
                line = misc.decode(line)
                (mid, line) = line.split(']', 1)
                mid = int(mid.strip(' []'))
                (addr, off, rt, val,
                 t) = list(map(str.strip,
                               line.split(',')[:5]))
                addr = int(addr, 16)
                off = int(off.split('+')[1], 16)
                t = t.strip(' []')
                if t == 'unknown':
                    continue
                self.scanresult_liststore.append(
                    [addr, val, t, True, off, rt, mid])
            self.scanresult_tv.set_model(self.scanresult_liststore)

    # return range(r1, r2) where all rows between r1 and r2 (EXCLUSIVE) are visible
    # return range(0, 0) if no row visible
    def get_visible_rows(self, treeview):
        _range = treeview.get_visible_range()
        try:
            r1 = _range[0][0]
            r2 = _range[1][0] + 1
        except:
            r1 = r2 = 0
        return range(r1, r2)

    # read/write data periodically
    def data_worker(self):
        if (not self.is_scanning) and (
                self.pid !=
                0) and self.command_lock.acquire(0):  # non-blocking
            Gdk.threads_enter()

            # Write to memory locked values in cheat list
            for i in self.cheatlist_liststore:
                if i[1] and i[6]:  # locked and valid
                    self.write_value(i[3], i[4], i[5])  # addr, typestr, value
            # Update visible (and unlocked) cheat list rows
            rows = self.get_visible_rows(self.cheatlist_tv)
            for i in rows:
                lockflag, locked, desc, addr, typestr, value, valid = self.cheatlist_liststore[
                    i]
                if valid and not locked:
                    newvalue = self.read_value(addr, typestr, value)
                    if newvalue is None:
                        self.cheatlist_liststore[i] = (lockflag, False, desc,
                                                       addr, typestr, '??',
                                                       False)
                    elif newvalue != value and not self.cheatlist_editing:
                        self.cheatlist_liststore[i] = (lockflag, locked, desc,
                                                       addr, typestr,
                                                       str(newvalue), valid)
            # Update visible scanresult rows
            rows = self.get_visible_rows(self.scanresult_tv)
            for i in rows:
                row = self.scanresult_liststore[i]
                addr, cur_value, scanmem_type, valid = row[:4]
                if valid:
                    new_value = self.read_value(
                        addr, TYPENAMES_S2G[scanmem_type.split(' ', 1)[0]],
                        cur_value)
                    if new_value is not None:
                        row[1] = str(new_value)
                    else:
                        row[1] = '??'
                        row[3] = False

            Gdk.threads_leave()
            self.command_lock.release()
        return not self.exit_flag

    def read_value(self, addr, typestr, prev_value):
        return self.bytes2value(
            typestr,
            self.read_memory(addr, self.get_type_size(typestr, prev_value)))

    # addr could be int or str
    def read_memory(self, addr, length):
        if not isinstance(addr, str):
            addr = '%x' % (addr, )
        f = tempfile.NamedTemporaryFile()

        self.command_lock.acquire()
        self.backend.send_command('dump %s %d %s' % (addr, length, f.name))
        self.command_lock.release()

        data = f.read()

        #        lines = self.backend.send_command('dump %s %d' % (addr, length))
        #        data = ''
        #        for line in lines:
        #            bytes = line.strip().split()
        #            for byte in bytes: data += chr(int(byte, 16))
        # TODO raise Exception here isn't good
        if len(data) != length:
            # self.show_error('Cannot access target memory')
            data = None
        return data

    # addr could be int or str
    def write_value(self, addr, typestr, value):
        if not isinstance(addr, str):
            addr = '%x' % (addr, )

        self.command_lock.acquire()
        self.backend.send_command('write %s %s %s' % (typestr, addr, value))
        self.command_lock.release()

    def exit(self, object, data=None):
        self.exit_flag = True
        Gtk.main_quit()

    def check_backend_version(self):
        if self.backend.get_version() != VERSION:
            self.show_error(
                _('Version of scanmem mismatched, you may encounter problems. Please make sure you are using the same version of GameConqueror as scanmem.'
                  ))
Пример #5
0
    def __init__(self):
        ##################################
        # init GUI
        self.builder = Gtk.Builder()
        self.builder.set_translation_domain(GETTEXT_PACKAGE)
        self.builder.add_from_file(os.path.join(WORK_DIR, 'GameConqueror.xml'))

        self.main_window = self.builder.get_object('MainWindow')
        self.main_window.set_title('GameConqueror %s' % (VERSION, ))
        self.about_dialog = self.builder.get_object('AboutDialog')
        # set version
        self.about_dialog.set_version(VERSION)

        self.process_list_dialog = self.builder.get_object('ProcessListDialog')
        self.addcheat_dialog = self.builder.get_object('AddCheatDialog')

        # init memory editor
        self.memoryeditor_window = self.builder.get_object(
            'MemoryEditor_Window')
        self.memoryeditor_hexview = HexView()
        self.memoryeditor_window.get_child().pack_start(
            self.memoryeditor_hexview, True, True, 0)
        self.memoryeditor_hexview.show_all()
        self.memoryeditor_address_entry = self.builder.get_object(
            'MemoryEditor_Address_Entry')
        self.memoryeditor_hexview.connect(
            'char-changed', self.memoryeditor_hexview_char_changed_cb)

        self.found_count_label = self.builder.get_object('FoundCount_Label')
        self.process_label = self.builder.get_object('Process_Label')
        self.value_input = self.builder.get_object('Value_Input')

        self.scanoption_frame = self.builder.get_object('ScanOption_Frame')
        self.scanprogress_progressbar = self.builder.get_object(
            'ScanProgress_ProgressBar')

        self.scan_button = self.builder.get_object('Scan_Button')
        self.reset_button = self.builder.get_object('Reset_Button')
        self.is_first_scan = True

        ###
        # Set scan data type
        self.scan_data_type_combobox = self.builder.get_object(
            'ScanDataType_ComboBox')
        misc.build_combobox(self.scan_data_type_combobox, SCAN_VALUE_TYPES)
        # apply setting
        misc.combobox_set_active_item(self.scan_data_type_combobox,
                                      SETTINGS['scan_data_type'])

        ###
        # set search scope
        self.search_scope_scale = self.builder.get_object('SearchScope_Scale')
        self.search_scope_scale_adjustment = Gtk.Adjustment(lower=0,
                                                            upper=2,
                                                            step_incr=1,
                                                            page_incr=1,
                                                            page_size=0)
        self.search_scope_scale.set_adjustment(
            self.search_scope_scale_adjustment)
        # apply setting
        self.search_scope_scale.set_value(SETTINGS['search_scope'])

        # init scanresult treeview
        # we may need a cell data func here
        # create model
        self.scanresult_tv = self.builder.get_object('ScanResult_TreeView')
        # liststore contents: addr, value, type, valid, offset, region type
        self.scanresult_liststore = Gtk.ListStore(str, str, str, bool, str,
                                                  str)
        self.scanresult_tv.set_model(self.scanresult_liststore)
        self.scanresult_tv.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
        self.scanresult_last_clicked = 0
        # init columns
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Address'),
                                    attributes=(('text', 0), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Value'),
                                    attributes=(('text', 1), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Offset'),
                                    attributes=(('text', 4), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    _('Region Type'),
                                    attributes=(('text', 5), ),
                                    properties=(('family', 'monospace'), ))

        # init CheatList TreeView
        self.cheatlist_tv = self.builder.get_object('CheatList_TreeView')
        self.cheatlist_liststore = Gtk.ListStore(
            str, bool, str, str, str, str,
            bool)  #lockflag, locked, description, addr, type, value, valid
        self.cheatlist_tv.set_model(self.cheatlist_liststore)
        self.cheatlist_tv.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
        self.cheatlist_last_clicked = 0
        self.cheatlist_tv.set_reorderable(True)
        self.cheatlist_editing = False
        self.cheatlist_tv.connect('key-press-event', self.cheatlist_keypressed)
        # Lock Flag
        misc.treeview_append_column(
            self.cheatlist_tv,
            '',
            renderer_class=Gtk.CellRendererCombo,
            attributes=(('text', 0), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model', LOCK_FLAG_TYPES), ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_toggle_lock_flag_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Lock
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Lock'),
            renderer_class=Gtk.CellRendererToggle,
            attributes=(('active', 1), ),
            properties=(('activatable', True), ('radio', False),
                        ('inconsistent', False)),
            signals=(('toggled', self.cheatlist_toggle_lock_cb), ))
        # Description
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Description'),
            attributes=(('text', 2), ),
            properties=(('editable', True), ),
            signals=(
                ('edited', self.cheatlist_edit_description_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Address
        misc.treeview_append_column(self.cheatlist_tv,
                                    _('Address'),
                                    attributes=(('text', 3), ),
                                    properties=(('family', 'monospace'), ))
        # Type
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Type'),
            renderer_class=Gtk.CellRendererCombo,
            attributes=(('text', 4), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model', LOCK_VALUE_TYPES), ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_edit_type_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Value
        misc.treeview_append_column(
            self.cheatlist_tv,
            _('Value'),
            attributes=(('text', 5), ),
            properties=(('editable', True), ('family', 'monospace')),
            signals=(
                ('edited', self.cheatlist_edit_value_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))

        # init ProcessList
        self.processfilter_input = self.builder.get_object(
            'ProcessFilter_Input')
        self.userfilter_input = self.builder.get_object('UserFilter_Input')
        # init ProcessList_TreeView
        self.processlist_tv = self.builder.get_object('ProcessList_TreeView')
        self.processlist_tv.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
        self.processlist_liststore = Gtk.ListStore(str, str, str)
        self.processlist_filter = self.processlist_liststore.filter_new(
            root=None)
        self.processlist_filter.set_visible_func(self.processlist_filter_func,
                                                 data=None)
        self.processlist_tv.set_model(self.processlist_filter)
        self.processlist_tv.set_enable_search(True)
        self.processlist_tv.set_search_column(1)
        # first col
        misc.treeview_append_column(self.processlist_tv,
                                    'PID',
                                    attributes=(('text', 0), ))
        # second col
        misc.treeview_append_column(self.processlist_tv,
                                    _('User'),
                                    attributes=(('text', 1), ))

        # third col
        misc.treeview_append_column(self.processlist_tv,
                                    _('Process'),
                                    attributes=(('text', 2), ))
        # init AddCheatDialog
        self.addcheat_address_input = self.builder.get_object('Address_Input')
        self.addcheat_description_input = self.builder.get_object(
            'Description_Input')
        self.addcheat_type_combobox = self.builder.get_object('Type_ComboBox')
        misc.build_combobox(self.addcheat_type_combobox, LOCK_VALUE_TYPES)
        misc.combobox_set_active_item(self.addcheat_type_combobox,
                                      SETTINGS['lock_data_type'])

        # init popup menu for scanresult
        self.scanresult_popup = Gtk.Menu()
        misc.menu_append_item(self.scanresult_popup, _('Add to cheat list'),
                              self.scanresult_popup_cb, 'add_to_cheat_list')
        misc.menu_append_item(self.scanresult_popup, _('Browse this address'),
                              self.scanresult_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.scanresult_popup,
                              _('Scan for this address'),
                              self.scanresult_popup_cb,
                              'scan_for_this_address')
        self.scanresult_popup.connect('button-press-event',
                                      self.check_for_leftclick)
        self.scanresult_popup.show_all()

        # init popup menu for cheatlist
        self.cheatlist_popup = Gtk.Menu()
        misc.menu_append_item(self.cheatlist_popup, _('Browse this address'),
                              self.cheatlist_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.cheatlist_popup, _('Copy address'),
                              self.cheatlist_popup_cb, 'copy_address')
        misc.menu_append_item(self.cheatlist_popup, _('Remove this entry'),
                              self.cheatlist_popup_cb, 'remove_entry')
        self.cheatlist_popup.connect('button-press-event',
                                     self.check_for_leftclick)
        self.cheatlist_popup.show_all()

        self.builder.connect_signals(self)
        self.main_window.connect('destroy', self.exit)

        ###########################
        # init others (backend, flag...)
        self.pid = 0  # target pid
        self.maps = []
        self.last_hexedit_address = (0, 0)  # used for hexview
        self.is_scanning = False
        self.exit_flag = False  # currently for data_worker only, other 'threads' may also use this flag
        self.is_data_worker_working = False

        self.backend = GameConquerorBackend()
        self.check_backend_version()
        self.search_count = 0
        GObject.timeout_add(DATA_WORKER_INTERVAL, self.data_worker)
        self.command_lock = threading.RLock()
Пример #6
0
class GameConqueror():
    def __init__(self):
        gtk.settings_get_default().set_long_property('gtk-tooltip-timeout', 0, '')
        ##################################
        # init GUI
        self.builder = gtk.Builder()
        self.builder.add_from_file(os.path.join(WORK_DIR, 'GameConqueror.xml'))

        self.main_window = self.builder.get_object('MainWindow')
        self.main_window.set_title('GameConqueror %s' % (VERSION,))
        self.about_dialog = self.builder.get_object('AboutDialog')
        # set version
        self.about_dialog.set_version(VERSION)

        self.process_list_dialog = self.builder.get_object('ProcessListDialog')
        self.addcheat_dialog = self.builder.get_object('AddCheatDialog')

        # init memory editor
        self.memoryeditor_window = self.builder.get_object('MemoryEditor_Window')
        self.memoryeditor_hexview = HexView()
        self.memoryeditor_window.child.pack_start(self.memoryeditor_hexview)
        self.memoryeditor_hexview.show_all()
        self.memoryeditor_address_entry = self.builder.get_object('MemoryEditor_Address_Entry')
        self.memoryeditor_hexview.connect('char-changed', self.memoryeditor_hexview_char_changed_cb)

        self.found_count_label = self.builder.get_object('FoundCount_Label')
        self.process_label = self.builder.get_object('Process_Label')
        self.value_input = self.builder.get_object('Value_Input')
        
        self.scanoption_frame = self.builder.get_object('ScanOption_Frame')
        self.scanprogress_progressbar = self.builder.get_object('ScanProgress_ProgressBar')

        self.scan_button = self.builder.get_object('Scan_Button')
        self.reset_button = self.builder.get_object('Reset_Button')

        ###
        # Set scan data type
        self.scan_data_type_combobox = self.builder.get_object('ScanDataType_ComboBox')
        misc.build_combobox(self.scan_data_type_combobox, SCAN_VALUE_TYPES)
        # apply setting
        misc.combobox_set_active_item(self.scan_data_type_combobox, SETTINGS['scan_data_type'])

        ###
        # set search scope
        self.search_scope_scale = self.builder.get_object('SearchScope_Scale')
        self.search_scope_scale_adjustment = gtk.Adjustment(lower=0, upper=2, step_incr=1, page_incr=1, page_size=0)
        self.search_scope_scale.set_adjustment(self.search_scope_scale_adjustment)
        # apply setting
        self.search_scope_scale.set_value(SETTINGS['search_scope'])



        # init scanresult treeview
        # we may need a cell data func here
        # create model
        self.scanresult_tv = self.builder.get_object('ScanResult_TreeView')
        self.scanresult_liststore = gtk.ListStore(str, str, str, bool) #addr, value, type, valid
        self.scanresult_tv.set_model(self.scanresult_liststore)
        # init columns
        misc.treeview_append_column(self.scanresult_tv, 'Address', attributes=(('text',0),), properties = (('family', 'monospace'),))
        misc.treeview_append_column(self.scanresult_tv, 'Value', attributes=(('text',1),), properties = (('family', 'monospace'),))

        # init CheatList TreeView
        self.cheatlist_tv = self.builder.get_object('CheatList_TreeView')
        self.cheatlist_liststore = gtk.ListStore(str, bool, str, str, str, str, bool) #lockflag, locked, description, addr, type, value, valid
        self.cheatlist_tv.set_model(self.cheatlist_liststore)
        self.cheatlist_tv.set_reorderable(True)
        self.cheatlist_updates = []
        self.cheatlist_editing = False
        self.cheatlist_tv.connect('key-press-event', self.cheatlist_keypressed)
        # Lock Flag
        misc.treeview_append_column(self.cheatlist_tv, '' 
                                        ,renderer_class = gtk.CellRendererCombo
                                        ,attributes = (('text',0),)
                                        ,properties = (('editable', True)
                                                      ,('has-entry', False)
                                                      ,('model', LOCK_FLAG_TYPES)
                                                      ,('text-column', 0)
                                        )
                                        ,signals = (('edited', self.cheatlist_toggle_lock_flag_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )
        # Lock
        misc.treeview_append_column(self.cheatlist_tv, 'Lock'
                                        ,renderer_class = gtk.CellRendererToggle
                                        ,attributes = (('active',1),)
                                        ,properties = (('activatable', True)
                                                      ,('radio', False)
                                                      ,('inconsistent', False)) 
                                        ,signals = (('toggled', self.cheatlist_toggle_lock_cb),)
                                   )
        # Description
        misc.treeview_append_column(self.cheatlist_tv, 'Description'
                                        ,attributes = (('text',2),)
                                        ,properties = (('editable', True),)
                                        ,signals = (('edited', self.cheatlist_edit_description_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )
        # Address
        misc.treeview_append_column(self.cheatlist_tv, 'Address'
                                        ,attributes = (('text',3),)
                                        ,properties = (('family', 'monospace'),)
                                   )
        # Type
        misc.treeview_append_column(self.cheatlist_tv, 'Type'
                                        ,renderer_class = gtk.CellRendererCombo
                                        ,attributes = (('text',4),)
                                        ,properties = (('editable', True)
                                                      ,('has-entry', False)
                                                      ,('model', LOCK_VALUE_TYPES)
                                                      ,('text-column', 0))
                                        ,signals = (('edited', self.cheatlist_edit_type_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )
        # Value 
        misc.treeview_append_column(self.cheatlist_tv, 'Value'
                                        ,attributes = (('text',5),)
                                        ,properties = (('editable', True)
                                                      ,('family', 'monospace'))
                                        ,signals = (('edited', self.cheatlist_edit_value_cb),
                                                    ('editing-started', self.cheatlist_edit_start),
                                                    ('editing-canceled', self.cheatlist_edit_cancel),)
                                   )

        # init ProcessList
        self.processfilter_input = self.builder.get_object('ProcessFilter_Input')
        self.userfilter_input = self.builder.get_object('UserFilter_Input')
        # init ProcessList_TreeView
        self.processlist_tv = self.builder.get_object('ProcessList_TreeView')
        self.processlist_tv.get_selection().set_mode(gtk.SELECTION_SINGLE)
        self.processlist_liststore = gtk.ListStore(str, str, str)
        self.processlist_filter = self.processlist_liststore.filter_new(root=None)
        self.processlist_filter.set_visible_func(self.processlist_filter_func, data=None)
        self.processlist_tv.set_model(self.processlist_filter)
        self.processlist_tv.set_enable_search(True)
        self.processlist_tv.set_search_column(1)
        # first col
        misc.treeview_append_column(self.processlist_tv, 'PID'
                                        ,attributes = (('text',0),)
                                   )
        # second col
        misc.treeview_append_column(self.processlist_tv, 'User'
                                        ,attributes = (('text',1),)
                                   )

        # third col
        misc.treeview_append_column(self.processlist_tv, 'Process'
                                        ,attributes = (('text',2),)
                                   )
        # init AddCheatDialog
        self.addcheat_address_input = self.builder.get_object('Address_Input')
        self.addcheat_description_input = self.builder.get_object('Description_Input')
        self.addcheat_type_combobox = self.builder.get_object('Type_ComboBox')
        misc.build_combobox(self.addcheat_type_combobox, LOCK_VALUE_TYPES)
        misc.combobox_set_active_item(self.addcheat_type_combobox, SETTINGS['lock_data_type'])
        
        
        
        # init popup menu for scanresult
        self.scanresult_popup = gtk.Menu()
        misc.menu_append_item(self.scanresult_popup
                             ,'Add to cheat list'
                             ,self.scanresult_popup_cb
                             ,'add_to_cheat_list')
        misc.menu_append_item(self.scanresult_popup
                             ,'Browse this address'
                             ,self.scanresult_popup_cb
                             ,'browse_this_address')
        misc.menu_append_item(self.scanresult_popup
                             ,'Scan for this address'
                             ,self.scanresult_popup_cb
                             ,'scan_for_this_address')
        self.scanresult_popup.show_all()

        # init popup menu for cheatlist
        self.cheatlist_popup = gtk.Menu()
        misc.menu_append_item(self.cheatlist_popup
                             ,'Browse this address'
                             ,self.cheatlist_popup_cb
                             ,'browse_this_address')
        misc.menu_append_item(self.cheatlist_popup
                             ,'Copy address'
                             ,self.cheatlist_popup_cb
                             ,'copy_address')
        misc.menu_append_item(self.cheatlist_popup
                             ,'Remove this entry'
                             ,self.cheatlist_popup_cb
                             ,'remove_entry')
        self.cheatlist_popup.show_all()

        self.builder.connect_signals(self)
        self.main_window.connect('destroy', self.exit)

        ###########################
        # init others (backend, flag...)
        self.pid = 0 # target pid
        self.maps = []
        self.last_hexedit_address = (0,0) # used for hexview
        self.is_scanning = False
        self.exit_flag = False # currently for data_worker only, other 'threads' may also use this flag
        self.is_data_worker_working = False

        self.backend = GameConquerorBackend()
        self.backend.add_error_listener(self.backend_error_cb)
        self.backend.add_progress_listener(self.backend_progress_cb)
        self.check_backend_version()
        self.search_count = 0
        self.data_worker_id = gobject.timeout_add(DATA_WORKER_INTERVAL, self.data_worker)
        self.command_lock = threading.RLock()


    ###########################
    # GUI callbacks

    def MemoryEditor_Window_delete_event_cb(self, widget, event, data=None):
        self.memoryeditor_window.hide()
        return True

    def MemoryEditor_Button_clicked_cb(self, button, data=None):
        if self.pid == 0:
            self.show_error('Please select a process')
            return
        self.browse_memory()
        return True

    def MemoryEditor_Address_Entry_activate_cb(self, entry, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error('Invalid address')

    def MemoryEditor_JumpTo_Button_clicked_cb(self, button, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error('Invalid address')

    def RemoveAllCheat_Button_clicked_cb(self, button, data=None):
        self.cheatlist_liststore.clear()
        return True

    def ManuallyAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.show()
        return True

    def SearchScope_Scale_format_value_cb(self, scale, value, Data=None):
        return SEARCH_SCOPE_NAMES[int(value)]

    def Value_Input_activate_cb(self, entry, data=None):
        self.do_scan()
        return True

    def ScanResult_TreeView_button_release_event_cb(self, widget, event, data=None):
        if event.button == 3: # right click
            (model, iter) = self.scanresult_tv.get_selection().get_selected()
            if iter is not None:
                self.scanresult_popup.popup(None, None, None, event.button, event.get_time())
                return True
            return False
        return False

    def ScanResult_TreeView_popup_menu_cb(self, widget, data=None):
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        if iter is not None:
            self.scanresult_popup.popup(None, None, None, 0, 0)
            return True
        return False

    def ScanResult_TreeView_row_activated_cb(self, treeview, path, view_column, data=None):
        # add to cheat list
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        if iter is not None:
            (addr, value, typestr) = model.get(iter, 0, 1, 2)
            self.add_to_cheat_list(addr, value, typestr)
            return True
        return False

    def CheatList_TreeView_button_release_event_cb(self, widget, event, data=None):
        if event.button == 3: # right click
            (model, iter) = self.cheatlist_tv.get_selection().get_selected()
            if iter is not None:
                self.cheatlist_popup.popup(None, None, None, event.button, event.get_time())
                return True
            return False
        return False

    def CheatList_TreeView_popup_menu_cb(self, widget, data=None):
        (model, iter) = self.cheatlist_tv.get_selection().get_selected()
        if iter is not None:
            self.cheatlist_popup.popup(None, None, None, 0, 0)
            return True
        return False

    def ProcessFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def UserFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def ProcessList_TreeView_row_activated_cb(self, treeview, path, view_column, data=None):
        (model, iter) = self.processlist_tv.get_selection().get_selected()
        if iter is not None:
            (pid, user, process) = model.get(iter, 0, 1, 2)
            self.select_process(int(pid), process)
            self.process_list_dialog.response(gtk.RESPONSE_CANCEL)
            return True
        return False

    def SelectProcess_Button_clicked_cb(self, button, data=None):
        self.processlist_liststore.clear()
        pl = self.get_process_list()
        for p in pl:
           # self.processlist_liststore.append([p[0], (p[1][:50] if len(p) > 1 else '<unknown>')]) # limit the length here, otherwise it may crash (why?)
            self.processlist_liststore.append([p[0], (p[1] if len(p) > 1 else '<unknown>'),(p[2] if len(p) > 2 else '<unknown>')])
        self.process_list_dialog.show()
        while True:
            res = self.process_list_dialog.run()
            if res == gtk.RESPONSE_OK: # -5
                (model, iter) = self.processlist_tv.get_selection().get_selected()
                if iter is None:
                    self.show_error('Please select a process')
                    continue
                else:
                    (pid, process) = model.get(iter, 0, 1)
                    self.select_process(int(pid), process)
                    break
            else: # for None and Cancel
                break
        self.process_list_dialog.hide()
        return True

    def ConfirmAddCheat_Button_clicked_cb(self, button, data=None):
        try:
            addr = self.addcheat_address_input.get_text()
        except ValueError:
            self.show_error('Please enter a valid address.')
            return False

        description = self.addcheat_description_input.get_text()
        if not description: description = 'No Description'
        typestr = LOCK_VALUE_TYPES[self.addcheat_type_combobox.get_active()][0]
        if 'int' in typestr: value = 0
        elif 'float' in typestr: value = 0.0
        elif 'string' in typestr: value = ''
        else: value = None
        
        self.add_to_cheat_list(addr, value, typestr, description)
        self.addcheat_dialog.hide()
        return True

    def CloseAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.hide()
        return True

    def Scan_Button_clicked_cb(self, button, data=None):
        self.do_scan()
        return True

    def Reset_Button_clicked_cb(self, button, data=None):
        self.reset_scan()
        return True

    def Logo_EventBox_button_release_event_cb(self, widget, data=None):
        self.about_dialog.run()
        self.about_dialog.hide()
        return True

    #######################
    # customed callbacks
    # (i.e. not standard event names are used)
    
    def memoryeditor_hexview_char_changed_cb(self, hexview, offset, charval):
        addr = hexview.base_addr + offset
        self.write_value(addr, 'int8', charval)
        # return False such that the byte the default handler will be called, and will be displayed correctly 
        return False

    
    def cheatlist_edit_start(self, a, b, c):
        self.cheatlist_editing = True
    def cheatlist_edit_cancel(self, a):
        self.cheatlist_editing = False

    def scanresult_popup_cb(self, menuitem, data=None):
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        (addr, value, typestr) = model.get(iter, 0, 1, 2)
        if iter is None:
            return False
        if data == 'add_to_cheat_list':
            self.add_to_cheat_list(addr, value, typestr)
            return True
        elif data == 'browse_this_address':
            self.browse_memory(int(addr,16))
            return True
        elif data == 'scan_for_this_address':
            self.scan_for_addr(int(addr,16))
            return True
        return False
        
    def cheatlist_keypressed(self, cheatlist_tv, event, selection=None):
        keycode = event.keyval
        pressedkey = gtk.gdk.keyval_name(keycode)
        if pressedkey == 'Delete':
            (model, iter) = self.cheatlist_tv.get_selection().get_selected()
            if iter is None: return
            self.cheatlist_liststore.remove(iter) 

    def cheatlist_popup_cb(self, menuitem, data=None):
        self.cheatlist_editing = False
        (model, iter) = self.cheatlist_tv.get_selection().get_selected()
        addr = model.get(iter, 3)[0]
        if iter is None:
            return False
        if data == 'remove_entry':
            self.cheatlist_liststore.remove(iter) 
            return True
        elif data == 'browse_this_address':
            self.browse_memory(int(addr,16))
            return True
        elif data == 'copy_address':
            CLIPBOARD.set_text(addr)
            return True
        return False

    def cheatlist_toggle_lock_cb(self, cellrenderertoggle, path, data=None):
        row = int(path)
        if self.cheatlist_liststore[row][6]: # valid
            locked = self.cheatlist_liststore[row][1]
            locked = not locked
            self.cheatlist_liststore[row][1] = locked
        if locked:
            #TODO: check value(valid number & not overflow), if failed, unlock it and do nothing
            pass
        else:
            #TODO: update its value?
            pass
        return True

    def cheatlist_toggle_lock_flag_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # currently only one lock flag is supported
        return True
        row = int(path)
        self.cheatlist_liststore[row][0] = new_text
        # data_worker will handle this later
        return True

    def cheatlist_edit_description_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        row = int(path)
        self.cheatlist_liststore[row][2] = new_text
        return True

    def cheatlist_edit_value_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # ignore empty value
        if new_text == '':
            return True
        row = int(path)
        if not self.cheatlist_liststore[row][6]: #not valid
            return True
        self.cheatlist_liststore[row][5] = new_text
        if self.cheatlist_liststore[row][1]: # locked
            # data_worker will handle this
            pass
        else:
            # write it for once
            (lockflag, locked, desc, addr, typestr, value, valid) = self.cheatlist_liststore[row]
            self.cheatlist_updates.append(row)
            self.write_value(addr, typestr, value)
        return True

    def cheatlist_edit_type_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        row = int(path)
        self.cheatlist_liststore[row][4] = new_text
        if self.cheatlist_liststore[row][1]: # locked
            # false unlock it
            self.cheatlist_liststore[row][1] = False
            pass
        return True

    def processlist_filter_func(self, model, iter, data=None):
        (pid, user, process) = model.get(iter, 0, 1, 2)
        return process is not None and \
                process.find(self.processfilter_input.get_text()) != -1 and \
                user is not None and \
                user.find(self.userfilter_input.get_text()) != -1
        


    ############################
    # core functions
    def show_error(self, msg):
        dialog = gtk.MessageDialog(None
                                 ,gtk.DIALOG_MODAL
                                 ,gtk.MESSAGE_ERROR
                                 ,gtk.BUTTONS_OK
                                 ,msg)
        dialog.run()
        dialog.destroy()

    # return None if unknown
    def get_pointer_width(self):
        bits = platform.architecture()[0]
        if not bits.endswith('bit'):
            return None
        try:
            bitn = int(bits[:-len('bit')])
            if bitn not in [8,16,32,64]:
                return None
            else:
                return bitn
        except:
            return None
        
    # return the size in bytes of the value in memory
    def get_type_size(self, typename, value):
        if typename in TYPESIZES.keys(): # int or float type; fixed length
            return TYPESIZES[typename]
        elif typename == 'bytearray':
            return (len(value.strip())+1)/3
        elif typename == 'string':
            return len(eval('\''+value+'\''))
        return None

    # parse bytes dumped by scanmem into number, string, etc.
    def bytes2value(self, typename, bytes):
        if bytes is None:
            return None
        if typename in TYPENAMES_G2STRUCT.keys():
            return struct.unpack(TYPENAMES_G2STRUCT[typename], bytes)[0]
        elif typename == 'string':
            return repr('%s'%(bytes,))[1:-1]
        elif typename == 'bytearray':
            return ' '.join(['%02x'%ord(i) for i in bytes])
        else:
            return bytes
    
    def scan_for_addr(self, addr):
        bits = self.get_pointer_width()
        if bits is None:
            show_error('Unknown architecture, you may report to developers')
            return
        self.reset_scan()
        self.value_input.set_text('%#x'%(addr,))
        misc.combobox_set_active_item(self.scan_data_type_combobox, 'int%d'%(bits,))
        self.do_scan()

    def browse_memory(self, addr=None):
        # select a region contains addr
        try:
            self.read_maps()
        except:
            show_error('Cannot retieve memory maps of that process, maybe it has exited (crashed), or you don\'t have enough privilege')
        selected_region = None
        if addr:
            for m in self.maps:
                if m['start_addr'] <= addr and addr < m['end_addr']:
                    selected_region = m
                    break
            if selected_region:
                if selected_region['flags'][0] != 'r': # not readable
                    show_error('Address %x is not readable' % (addr,))
                    return
            else:
                show_error('Address %x is not valid' % (addr,))
                return
        else:
            # just select the first readable region
            for m in self.maps:
                if m['flags'][0] == 'r':
                    selected_region = m
                    break
            if selected_region is None:
                self.show_error('Cannot find a readable region')
                return
            addr = selected_region['start_addr']

        # read region if necessary
        start_addr = max(addr - 1024, selected_region['start_addr'])
        end_addr = min(addr + 1024, selected_region['end_addr'])
        if self.last_hexedit_address[0] != start_addr or \
           self.last_hexedit_address[1] != end_addr:
            data = self.read_memory(start_addr, end_addr - start_addr)
            if data is None:
                self.show_error('Cannot read memory')
                return
            self.last_hexedit_address = (start_addr, end_addr)
            self.memoryeditor_hexview.payload=data
            self.memoryeditor_hexview.base_addr = start_addr
        
        # set editable flag
        self.memoryeditor_hexview.editable = (selected_region['flags'][1] == 'w')

        if addr:
            self.memoryeditor_hexview.show_addr(addr)
        self.memoryeditor_window.show()

    # this callback will be called from other thread
    def backend_error_cb(self, msg):
        gtk.gdk.threads_enter()
        # we don't want massive error output for data worker
        if not self.is_data_worker_working:
            self.show_error('Backend error: %s'%(msg,))
        if self.is_scanning:
            self.finish_scan()
        gtk.gdk.threads_leave()
                   
    # this callback will be called from other thread
    def backend_progress_cb(self, cur, total):
        gtk.gdk.threads_enter()
        # cur and total may be 0!
        if (cur == total) and self.is_scanning:
            self.scanprogress_progressbar.set_fraction(1.0)
            self.finish_scan()
        else:
            self.scanprogress_progressbar.set_fraction(float(cur)/total)
        gtk.gdk.threads_leave()

    def add_to_cheat_list(self, addr, value, typestr, description='No Description'):
        # determine longest possible type
        types = typestr.split()
        vt = typestr
        for t in types:
            if TYPENAMES_S2G.has_key(t):
                vt = TYPENAMES_S2G[t]
                break
        self.cheatlist_liststore.prepend(['=', False, description, addr, vt, value, True])

    def get_process_list(self):
        return [map(str.strip, e.strip().split(' ',2)) for e in os.popen('ps -wweo pid=,user=,command= --sort=-pid').readlines()]

    def select_process(self, pid, process_name):
        # ask backend for attaching the target process
        # update 'current process'
        # reset flags
        # for debug/log
#        print 'Select process: %d - %s' % (pid, process_name)
        self.pid = pid
        try:
            self.read_maps()
        except:
            self.pid = 0
            self.process_label.set_text('No process selected')
            self.process_label.set_property('tooltip-text', 'Select a process')
            self.show_error('Cannot retieve memory maps of that process, maybe it has exited (crashed), or you don\'t have enough privilege')
        self.process_label.set_text('%d - %s' % (pid, process_name))
        self.process_label.set_property('tooltip-text', process_name)

        self.command_lock.acquire()
        self.backend.send_command('pid %d' % (pid,))
        self.reset_scan()
        self.command_lock.release()

        # unlock all entries in cheat list
        for i in xrange(len(self.cheatlist_liststore)):
            self.cheatlist_liststore[i][1] = False

    def read_maps(self):
        lines = open('/proc/%d/maps' % (self.pid,)).readlines()
        self.maps = []
        for l in lines:
            item = {}
            info = l.split(' ', 5)
            addr = info[0]
            idx = addr.index('-')
            item['start_addr'] = int(addr[:idx],16)
            item['end_addr'] = int(addr[idx+1:],16)
            item['size'] = item['end_addr'] - item['start_addr']
            item['flags'] = info[1]
            item['offset'] = info[2]
            item['dev'] = info[3]
            item['inode'] = int(info[4])
            if len(info) < 6:
                item['pathname'] = ''
            else:
                item['pathname'] = info[5].lstrip() # don't use strip
            self.maps.append(item)

    def reset_scan(self):
        self.command_lock.acquire()
        self.search_count = 0
        # reset search type and value type
        self.scanresult_liststore.clear()
        
        self.command_lock.acquire()
        self.backend.send_command('reset')
        self.update_scan_result()
        self.command_lock.release()

        self.scanoption_frame.set_sensitive(True)

    def apply_scan_settings (self):
        # scan data type
        active = self.scan_data_type_combobox.get_active()
        assert(active >= 0)
        dt = self.scan_data_type_combobox.get_model()[active][0]

        self.command_lock.acquire()
        self.backend.send_command('option scan_data_type %s' % (dt,))
        # search scope
        self.backend.send_command('option region_scan_level %d' %(1 + int(self.search_scope_scale.get_value()),))
        # TODO: ugly, reset to make region_scan_level taking effect
        self.backend.send_command('reset')
        self.command_lock.release()

    
    # perform scanning through backend
    # set GUI if needed
    def do_scan(self):
        if self.pid == 0:
            self.show_error('Please select a process')
            return
        active = self.scan_data_type_combobox.get_active()
        assert(active >= 0)
        data_type = self.scan_data_type_combobox.get_model()[active][0]
        cmd = self.value_input.get_text()
   
        try:
            cmd = misc.check_scan_command(data_type, cmd)
        except Exception,e:
            # this is not quite good
            self.show_error(e.args[0])
            return

        # disable the window before perform scanning, such that if result come so fast, we won't mess it up
        self.search_count +=1 
        self.scanoption_frame.set_sensitive(False) # no need to check search_count here
        self.main_window.set_sensitive(False)
        self.memoryeditor_window.set_sensitive(False)
        self.is_scanning = True
        # set scan options only when first scan, since this will reset backend

        self.command_lock.acquire()
        if self.search_count == 1:
            self.apply_scan_settings()
        self.backend.send_command(cmd, get_output = False)
Пример #7
0
class GameConqueror():
    def __init__(self):
        Gtk.Settings.get_default().set_long_property('gtk-tooltip-timeout', 0,
                                                     '')
        ##################################
        # init GUI
        self.builder = Gtk.Builder()
        self.builder.add_from_file(os.path.join(WORK_DIR, 'GameConqueror.xml'))

        self.main_window = self.builder.get_object('MainWindow')
        self.main_window.set_title('GameConqueror %s' % (VERSION, ))
        self.about_dialog = self.builder.get_object('AboutDialog')
        # set version
        self.about_dialog.set_version(VERSION)

        self.process_list_dialog = self.builder.get_object('ProcessListDialog')
        self.addcheat_dialog = self.builder.get_object('AddCheatDialog')

        # init memory editor
        self.memoryeditor_window = self.builder.get_object(
            'MemoryEditor_Window')
        self.memoryeditor_hexview = HexView()
        self.memoryeditor_window.get_child().pack_start(
            self.memoryeditor_hexview, True, True, 0)
        self.memoryeditor_hexview.show_all()
        self.memoryeditor_address_entry = self.builder.get_object(
            'MemoryEditor_Address_Entry')
        self.memoryeditor_hexview.connect(
            'char-changed', self.memoryeditor_hexview_char_changed_cb)

        self.found_count_label = self.builder.get_object('FoundCount_Label')
        self.process_label = self.builder.get_object('Process_Label')
        self.value_input = self.builder.get_object('Value_Input')

        self.scanoption_frame = self.builder.get_object('ScanOption_Frame')
        self.scanprogress_progressbar = self.builder.get_object(
            'ScanProgress_ProgressBar')

        self.scan_button = self.builder.get_object('Scan_Button')
        self.reset_button = self.builder.get_object('Reset_Button')
        self.is_first_scan = True

        ###
        # Set scan data type
        self.scan_data_type_combobox = self.builder.get_object(
            'ScanDataType_ComboBox')
        misc.build_combobox(self.scan_data_type_combobox, SCAN_VALUE_TYPES)
        # apply setting
        misc.combobox_set_active_item(self.scan_data_type_combobox,
                                      SETTINGS['scan_data_type'])

        ###
        # set search scope
        self.search_scope_scale = self.builder.get_object('SearchScope_Scale')
        self.search_scope_scale_adjustment = Gtk.Adjustment(lower=0,
                                                            upper=2,
                                                            step_incr=1,
                                                            page_incr=1,
                                                            page_size=0)
        self.search_scope_scale.set_adjustment(
            self.search_scope_scale_adjustment)
        # apply setting
        self.search_scope_scale.set_value(SETTINGS['search_scope'])

        # init scanresult treeview
        # we may need a cell data func here
        # create model
        self.scanresult_tv = self.builder.get_object('ScanResult_TreeView')
        self.scanresult_liststore = Gtk.ListStore(
            str, str, str, bool)  #addr, value, type, valid
        self.scanresult_tv.set_model(self.scanresult_liststore)
        # init columns
        misc.treeview_append_column(self.scanresult_tv,
                                    'Address',
                                    attributes=(('text', 0), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    'Value',
                                    attributes=(('text', 1), ),
                                    properties=(('family', 'monospace'), ))

        # init CheatList TreeView
        self.cheatlist_tv = self.builder.get_object('CheatList_TreeView')
        self.cheatlist_liststore = Gtk.ListStore(
            str, bool, str, str, str, str,
            bool)  #lockflag, locked, description, addr, type, value, valid
        self.cheatlist_tv.set_model(self.cheatlist_liststore)
        self.cheatlist_tv.set_reorderable(True)
        self.cheatlist_updates = []
        self.cheatlist_editing = False
        self.cheatlist_tv.connect('key-press-event', self.cheatlist_keypressed)
        # Lock Flag
        misc.treeview_append_column(
            self.cheatlist_tv,
            '',
            renderer_class=Gtk.CellRendererCombo,
            attributes=(('text', 0), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model', LOCK_FLAG_TYPES), ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_toggle_lock_flag_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Lock
        misc.treeview_append_column(
            self.cheatlist_tv,
            'Lock',
            renderer_class=Gtk.CellRendererToggle,
            attributes=(('active', 1), ),
            properties=(('activatable', True), ('radio', False),
                        ('inconsistent', False)),
            signals=(('toggled', self.cheatlist_toggle_lock_cb), ))
        # Description
        misc.treeview_append_column(
            self.cheatlist_tv,
            'Description',
            attributes=(('text', 2), ),
            properties=(('editable', True), ),
            signals=(
                ('edited', self.cheatlist_edit_description_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Address
        misc.treeview_append_column(self.cheatlist_tv,
                                    'Address',
                                    attributes=(('text', 3), ),
                                    properties=(('family', 'monospace'), ))
        # Type
        misc.treeview_append_column(
            self.cheatlist_tv,
            'Type',
            renderer_class=Gtk.CellRendererCombo,
            attributes=(('text', 4), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model', LOCK_VALUE_TYPES), ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_edit_type_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Value
        misc.treeview_append_column(
            self.cheatlist_tv,
            'Value',
            attributes=(('text', 5), ),
            properties=(('editable', True), ('family', 'monospace')),
            signals=(
                ('edited', self.cheatlist_edit_value_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))

        # init ProcessList
        self.processfilter_input = self.builder.get_object(
            'ProcessFilter_Input')
        self.userfilter_input = self.builder.get_object('UserFilter_Input')
        # init ProcessList_TreeView
        self.processlist_tv = self.builder.get_object('ProcessList_TreeView')
        self.processlist_tv.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
        self.processlist_liststore = Gtk.ListStore(str, str, str)
        self.processlist_filter = self.processlist_liststore.filter_new(
            root=None)
        self.processlist_filter.set_visible_func(self.processlist_filter_func,
                                                 data=None)
        self.processlist_tv.set_model(self.processlist_filter)
        self.processlist_tv.set_enable_search(True)
        self.processlist_tv.set_search_column(1)
        # first col
        misc.treeview_append_column(self.processlist_tv,
                                    'PID',
                                    attributes=(('text', 0), ))
        # second col
        misc.treeview_append_column(self.processlist_tv,
                                    'User',
                                    attributes=(('text', 1), ))

        # third col
        misc.treeview_append_column(self.processlist_tv,
                                    'Process',
                                    attributes=(('text', 2), ))
        # init AddCheatDialog
        self.addcheat_address_input = self.builder.get_object('Address_Input')
        self.addcheat_description_input = self.builder.get_object(
            'Description_Input')
        self.addcheat_type_combobox = self.builder.get_object('Type_ComboBox')
        misc.build_combobox(self.addcheat_type_combobox, LOCK_VALUE_TYPES)
        misc.combobox_set_active_item(self.addcheat_type_combobox,
                                      SETTINGS['lock_data_type'])

        # init popup menu for scanresult
        self.scanresult_popup = Gtk.Menu()
        misc.menu_append_item(self.scanresult_popup, 'Add to cheat list',
                              self.scanresult_popup_cb, 'add_to_cheat_list')
        misc.menu_append_item(self.scanresult_popup, 'Browse this address',
                              self.scanresult_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.scanresult_popup, 'Scan for this address',
                              self.scanresult_popup_cb,
                              'scan_for_this_address')
        self.scanresult_popup.show_all()

        # init popup menu for cheatlist
        self.cheatlist_popup = Gtk.Menu()
        misc.menu_append_item(self.cheatlist_popup, 'Browse this address',
                              self.cheatlist_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.cheatlist_popup, 'Copy address',
                              self.cheatlist_popup_cb, 'copy_address')
        misc.menu_append_item(self.cheatlist_popup, 'Remove this entry',
                              self.cheatlist_popup_cb, 'remove_entry')
        self.cheatlist_popup.show_all()

        self.builder.connect_signals(self)
        self.main_window.connect('destroy', self.exit)

        ###########################
        # init others (backend, flag...)
        self.pid = 0  # target pid
        self.maps = []
        self.last_hexedit_address = (0, 0)  # used for hexview
        self.is_scanning = False
        self.exit_flag = False  # currently for data_worker only, other 'threads' may also use this flag
        self.is_data_worker_working = False

        self.backend = GameConquerorBackend()
        self.check_backend_version()
        self.search_count = 0
        GObject.timeout_add(DATA_WORKER_INTERVAL, self.data_worker)
        self.command_lock = threading.RLock()

    ###########################
    # GUI callbacks

    def MemoryEditor_Window_delete_event_cb(self, widget, event, data=None):
        self.memoryeditor_window.hide()
        return True

    def MemoryEditor_Button_clicked_cb(self, button, data=None):
        if self.pid == 0:
            self.show_error('Please select a process')
            return
        self.browse_memory()
        return True

    def MemoryEditor_Address_Entry_activate_cb(self, entry, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error('Invalid address')

    def MemoryEditor_JumpTo_Button_clicked_cb(self, button, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error('Invalid address')

    def ManuallyAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.show()
        return True

    def RemoveAllCheat_Button_clicked_cb(self, button, data=None):
        self.cheatlist_liststore.clear()
        return True

    def LoadCheat_Button_clicked_cb(self, button, data=None):
        dialog = Gtk.FileChooserDialog(
            "Open..", self.main_window, Gtk.FileChooserAction.OPEN,
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN,
             Gtk.ResponseType.OK))
        dialog.set_default_response(Gtk.ResponseType.OK)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            try:
                with open(dialog.get_filename(), 'rb') as f:
                    obj = json.load(f)
                    for row in obj['cheat_list']:
                        self.add_to_cheat_list(row[3], row[5], row[4], row[2])
            except:
                pass
        dialog.destroy()
        return True

    def SaveCheat_Button_clicked_cb(self, button, data=None):
        dialog = Gtk.FileChooserDialog(
            "Save..", self.main_window, Gtk.FileChooserAction.SAVE,
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN,
             Gtk.ResponseType.OK))
        dialog.set_default_response(Gtk.ResponseType.OK)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            try:
                with open(dialog.get_filename(), 'wb') as f:
                    obj = {
                        'cheat_list':
                        [list(i) for i in self.cheatlist_liststore]
                    }
                    json.dump(obj, f)
            except:
                pass
        dialog.destroy()
        return True

    def SearchScope_Scale_format_value_cb(self, scale, value, Data=None):
        return SEARCH_SCOPE_NAMES[int(value)]

    def Value_Input_activate_cb(self, entry, data=None):
        self.do_scan()
        return True

    def ScanResult_TreeView_button_release_event_cb(self,
                                                    widget,
                                                    event,
                                                    data=None):
        if event.button == 3:  # right click
            (model, iter) = self.scanresult_tv.get_selection().get_selected()
            if iter is not None:
                self.scanresult_popup.popup(None, None, None, None,
                                            event.button, event.get_time())
                return True
            return False
        return False

    def ScanResult_TreeView_popup_menu_cb(self, widget, data=None):
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        if iter is not None:
            self.scanresult_popup.popup(None, None, None, 0, 0)
            return True
        return False

    def ScanResult_TreeView_row_activated_cb(self,
                                             treeview,
                                             path,
                                             view_column,
                                             data=None):
        # add to cheat list
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        if iter is not None:
            (addr, value, typestr) = model.get(iter, 0, 1, 2)
            self.add_to_cheat_list(addr, value, typestr)
            return True
        return False

    def CheatList_TreeView_button_release_event_cb(self,
                                                   widget,
                                                   event,
                                                   data=None):
        if event.button == 3:  # right click
            (model, iter) = self.cheatlist_tv.get_selection().get_selected()
            if iter is not None:
                self.cheatlist_popup.popup(None, None, None, None,
                                           event.button, event.get_time())
                return True
            return False
        return False

    def CheatList_TreeView_popup_menu_cb(self, widget, data=None):
        (model, iter) = self.cheatlist_tv.get_selection().get_selected()
        if iter is not None:
            self.cheatlist_popup.popup(None, None, None, 0, 0)
            return True
        return False

    def ProcessFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def UserFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def ProcessList_TreeView_row_activated_cb(self,
                                              treeview,
                                              path,
                                              view_column,
                                              data=None):
        (model, iter) = self.processlist_tv.get_selection().get_selected()
        if iter is not None:
            (pid, user, process) = model.get(iter, 0, 1, 2)
            self.select_process(int(pid), process)
            self.process_list_dialog.response(Gtk.ResponseType.CANCEL)
            return True
        return False

    def SelectProcess_Button_clicked_cb(self, button, data=None):
        self.processlist_liststore.clear()
        pl = self.get_process_list()
        for p in pl:
            # self.processlist_liststore.append([p[0], (p[1][:50] if len(p) > 1 else '<unknown>')]) # limit the length here, otherwise it may crash (why?)
            self.processlist_liststore.append([
                p[0], (p[1] if len(p) > 1 else '<unknown>'),
                (p[2] if len(p) > 2 else '<unknown>')
            ])
        self.process_list_dialog.show()
        while True:
            res = self.process_list_dialog.run()
            if res == Gtk.ResponseType.OK:  # -5
                (model,
                 iter) = self.processlist_tv.get_selection().get_selected()
                if iter is None:
                    self.show_error('Please select a process')
                    continue
                else:
                    (pid, process) = model.get(iter, 0, 1)
                    self.select_process(int(pid), process)
                    break
            else:  # for None and Cancel
                break
        self.process_list_dialog.hide()
        return True

    def ConfirmAddCheat_Button_clicked_cb(self, button, data=None):
        try:
            addr = self.addcheat_address_input.get_text()
        except ValueError:
            self.show_error('Please enter a valid address.')
            return False

        description = self.addcheat_description_input.get_text()
        if not description: description = 'No Description'
        typestr = LOCK_VALUE_TYPES[self.addcheat_type_combobox.get_active()][0]
        if 'int' in typestr: value = 0
        elif 'float' in typestr: value = 0.0
        elif 'string' in typestr: value = ''
        else: value = None

        self.add_to_cheat_list(addr, value, typestr, description)
        self.addcheat_dialog.hide()
        return True

    def CloseAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.hide()
        return True

    def Scan_Button_clicked_cb(self, button, data=None):
        self.do_scan()
        return True

    def Reset_Button_clicked_cb(self, button, data=None):
        self.reset_scan()
        return True

    def Logo_EventBox_button_release_event_cb(self, widget, data=None):
        self.about_dialog.run()
        self.about_dialog.hide()
        return True

    #######################
    # customed callbacks
    # (i.e. not standard event names are used)

    def memoryeditor_hexview_char_changed_cb(self, hexview, offset, charval):
        addr = hexview.base_addr + offset
        self.write_value(addr, 'int8', charval)
        # return False such that the byte the default handler will be called, and will be displayed correctly
        return False

    def cheatlist_edit_start(self, a, b, c):
        self.cheatlist_editing = True

    def cheatlist_edit_cancel(self, a):
        self.cheatlist_editing = False

    def scanresult_popup_cb(self, menuitem, data=None):
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        (addr, value, typestr) = model.get(iter, 0, 1, 2)
        if iter is None:
            return False
        if data == 'add_to_cheat_list':
            self.add_to_cheat_list(addr, value, typestr)
            return True
        elif data == 'browse_this_address':
            self.browse_memory(int(addr, 16))
            return True
        elif data == 'scan_for_this_address':
            self.scan_for_addr(int(addr, 16))
            return True
        return False

    def cheatlist_keypressed(self, cheatlist_tv, event, selection=None):
        keycode = event.keyval
        pressedkey = Gdk.keyval_name(keycode)
        if pressedkey == 'Delete':
            (model, iter) = self.cheatlist_tv.get_selection().get_selected()
            if iter is None: return
            self.cheatlist_liststore.remove(iter)

    def cheatlist_popup_cb(self, menuitem, data=None):
        self.cheatlist_editing = False
        (model, iter) = self.cheatlist_tv.get_selection().get_selected()
        addr = model.get(iter, 3)[0]
        if iter is None:
            return False
        if data == 'remove_entry':
            self.cheatlist_liststore.remove(iter)
            return True
        elif data == 'browse_this_address':
            self.browse_memory(int(addr, 16))
            return True
        elif data == 'copy_address':
            CLIPBOARD.set_text(addr, len(addr))
            return True
        return False

    def cheatlist_toggle_lock_cb(self, cellrenderertoggle, path, data=None):
        row = int(path)
        if self.cheatlist_liststore[row][6]:  # valid
            locked = self.cheatlist_liststore[row][1]
            locked = not locked
            self.cheatlist_liststore[row][1] = locked
        if locked:
            #TODO: check value(valid number & not overflow), if failed, unlock it and do nothing
            pass
        else:
            #TODO: update its value?
            pass
        return True

    def cheatlist_toggle_lock_flag_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # currently only one lock flag is supported
        return True
        row = int(path)
        self.cheatlist_liststore[row][0] = new_text
        # data_worker will handle this later
        return True

    def cheatlist_edit_description_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        row = int(path)
        self.cheatlist_liststore[row][2] = new_text
        return True

    def cheatlist_edit_value_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # ignore empty value
        if new_text == '':
            return True
        row = int(path)
        if not self.cheatlist_liststore[row][6]:  #not valid
            return True
        self.cheatlist_liststore[row][5] = new_text
        if self.cheatlist_liststore[row][1]:  # locked
            # data_worker will handle this
            pass
        else:
            # write it for once
            (lockflag, locked, desc, addr, typestr, value,
             valid) = self.cheatlist_liststore[row]
            self.cheatlist_updates.append(row)
            self.write_value(addr, typestr, value)
        return True

    def cheatlist_edit_type_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        row = int(path)
        self.cheatlist_liststore[row][4] = new_text
        if self.cheatlist_liststore[row][1]:  # locked
            # false unlock it
            self.cheatlist_liststore[row][1] = False
            pass
        return True

    def processlist_filter_func(self, model, iter, data=None):
        (pid, user, process) = model.get(iter, 0, 1, 2)
        return process is not None and \
                process.find(self.processfilter_input.get_text()) != -1 and \
                user is not None and \
                user.find(self.userfilter_input.get_text()) != -1

    ############################
    # core functions
    def show_error(self, msg):
        dialog = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL,
                                   Gtk.MessageType.ERROR, Gtk.ButtonsType.OK,
                                   msg)
        dialog.run()
        dialog.destroy()

    # return None if unknown
    def get_pointer_width(self):
        bits = platform.architecture()[0]
        if not bits.endswith('bit'):
            return None
        try:
            bitn = int(bits[:-len('bit')])
            if bitn not in [8, 16, 32, 64]:
                return None
            else:
                return bitn
        except:
            return None

    # return the size in bytes of the value in memory
    def get_type_size(self, typename, value):
        if typename in TYPESIZES:  # int or float type; fixed length
            return TYPESIZES[typename]
        elif typename == 'bytearray':
            return (len(value.strip()) + 1) / 3
        elif typename == 'string':
            return len(eval('\'' + value + '\''))
        return None

    # parse bytes dumped by scanmem into number, string, etc.
    def bytes2value(self, typename, bytes):
        if bytes is None:
            return None
        if typename in TYPENAMES_G2STRUCT:
            return struct.unpack(TYPENAMES_G2STRUCT[typename], bytes)[0]
        elif typename == 'string':
            return repr('%s' % (bytes, ))[1:-1]
        elif typename == 'bytearray':
            return ' '.join(['%02x' % ord(i) for i in bytes])
        else:
            return bytes

    def scan_for_addr(self, addr):
        bits = self.get_pointer_width()
        if bits is None:
            show_error('Unknown architecture, you may report to developers')
            return
        self.reset_scan()
        self.value_input.set_text('%#x' % (addr, ))
        misc.combobox_set_active_item(self.scan_data_type_combobox,
                                      'int%d' % (bits, ))
        self.do_scan()

    def browse_memory(self, addr=None):
        # select a region contains addr
        try:
            self.read_maps()
        except:
            show_error(
                'Cannot retieve memory maps of that process, maybe it has exited (crashed), or you don\'t have enough privilege'
            )
        selected_region = None
        if addr:
            for m in self.maps:
                if m['start_addr'] <= addr and addr < m['end_addr']:
                    selected_region = m
                    break
            if selected_region:
                if selected_region['flags'][0] != 'r':  # not readable
                    show_error('Address %x is not readable' % (addr, ))
                    return
            else:
                show_error('Address %x is not valid' % (addr, ))
                return
        else:
            # just select the first readable region
            for m in self.maps:
                if m['flags'][0] == 'r':
                    selected_region = m
                    break
            if selected_region is None:
                self.show_error('Cannot find a readable region')
                return
            addr = selected_region['start_addr']

        # read region if necessary
        start_addr = max(addr - 1024, selected_region['start_addr'])
        end_addr = min(addr + 1024, selected_region['end_addr'])
        if self.last_hexedit_address[0] != start_addr or \
           self.last_hexedit_address[1] != end_addr:
            data = self.read_memory(start_addr, end_addr - start_addr)
            if data is None:
                self.show_error('Cannot read memory')
                return
            self.last_hexedit_address = (start_addr, end_addr)
            self.memoryeditor_hexview.payload = data
            self.memoryeditor_hexview.base_addr = start_addr

        # set editable flag
        self.memoryeditor_hexview.editable = (
            selected_region['flags'][1] == 'w')

        if addr:
            self.memoryeditor_hexview.show_addr(addr)
        self.memoryeditor_window.show()

    # this callback will be called from other thread
    def progress_watcher(self):
        Gdk.threads_enter()
        self.scanprogress_progressbar.set_fraction(
            self.backend.get_scan_progress())
        Gdk.flush()
        Gdk.threads_leave()
        return True

    def add_to_cheat_list(self,
                          addr,
                          value,
                          typestr,
                          description='No Description'):
        # determine longest possible type
        types = typestr.split()
        vt = typestr
        for t in types:
            if t in TYPENAMES_S2G:
                vt = TYPENAMES_S2G[t]
                break
        self.cheatlist_liststore.prepend(
            ['=', False, description, addr, vt,
             str(value), True])

    def get_process_list(self):
        return [
            list(map(str.strip,
                     e.strip().split(' ', 2))) for e in
            os.popen('ps -wweo pid=,user=,command= --sort=-pid').readlines()
        ]

    def select_process(self, pid, process_name):
        # ask backend for attaching the target process
        # update 'current process'
        # reset flags
        # for debug/log
        self.pid = pid
        try:
            self.read_maps()
        except:
            self.pid = 0
            self.process_label.set_text('No process selected')
            self.process_label.set_property('tooltip-text', 'Select a process')
            self.show_error(
                'Cannot retieve memory maps of that process, maybe it has exited (crashed), or you don\'t have enough privilege'
            )
        self.process_label.set_text('%d - %s' % (pid, process_name))
        self.process_label.set_property('tooltip-text', process_name)

        self.command_lock.acquire()
        self.backend.send_command('pid %d' % (pid, ))
        self.reset_scan()
        self.command_lock.release()

        # unlock all entries in cheat list
        for i in range(len(self.cheatlist_liststore)):
            self.cheatlist_liststore[i][1] = False

    def read_maps(self):
        lines = open('/proc/%d/maps' % (self.pid, )).readlines()
        self.maps = []
        for l in lines:
            item = {}
            info = l.split(' ', 5)
            addr = info[0]
            idx = addr.index('-')
            item['start_addr'] = int(addr[:idx], 16)
            item['end_addr'] = int(addr[idx + 1:], 16)
            item['size'] = item['end_addr'] - item['start_addr']
            item['flags'] = info[1]
            item['offset'] = info[2]
            item['dev'] = info[3]
            item['inode'] = int(info[4])
            if len(info) < 6:
                item['pathname'] = ''
            else:
                item['pathname'] = info[5].lstrip()  # don't use strip
            self.maps.append(item)

    def reset_scan(self):
        self.search_count = 0
        # reset search type and value type
        self.scanresult_liststore.clear()

        self.command_lock.acquire()
        self.backend.send_command('reset')
        self.update_scan_result()
        self.command_lock.release()

        self.scanoption_frame.set_sensitive(True)
        self.is_first_scan = True

    def apply_scan_settings(self):
        # scan data type
        active = self.scan_data_type_combobox.get_active()
        assert (active >= 0)
        dt = self.scan_data_type_combobox.get_model()[active][0]

        self.command_lock.acquire()
        self.backend.send_command('option scan_data_type %s' % (dt, ))
        # search scope
        self.backend.send_command(
            'option region_scan_level %d' %
            (1 + int(self.search_scope_scale.get_value()), ))
        # TODO: ugly, reset to make region_scan_level taking effect
        self.backend.send_command('reset')
        self.command_lock.release()

    # perform scanning through backend
    # set GUI if needed
    def do_scan(self):
        if self.pid == 0:
            self.show_error('Please select a process')
            return
        active = self.scan_data_type_combobox.get_active()
        assert (active >= 0)
        data_type = self.scan_data_type_combobox.get_model()[active][0]
        cmd = self.value_input.get_text()

        try:
            cmd = misc.check_scan_command(data_type, cmd, self.is_first_scan)
        except Exception as e:
            # this is not quite good
            self.show_error(e.args[0])
            return

        # disable the window before perform scanning, such that if result come so fast, we won't mess it up
        self.search_count += 1
        self.scanoption_frame.set_sensitive(
            False)  # no need to check search_count here
        self.main_window.set_sensitive(False)
        self.memoryeditor_window.set_sensitive(False)
        self.is_scanning = True
        # set scan options only when first scan, since this will reset backend
        if self.search_count == 1:
            self.apply_scan_settings()
        self.backend.reset_scan_progress()
        self.progress_watcher_id = GObject.idle_add(self.progress_watcher)
        threading.Thread(target=self.scan_thread_func, args=(cmd, )).start()

    def scan_thread_func(self, cmd):
        self.command_lock.acquire()
        self.backend.send_command(cmd)

        GObject.source_remove(self.progress_watcher_id)
        Gdk.threads_enter()

        self.main_window.set_sensitive(True)
        self.memoryeditor_window.set_sensitive(True)
        self.is_scanning = False
        self.update_scan_result()
        self.is_first_scan = False

        Gdk.flush()
        Gdk.threads_leave()
        self.command_lock.release()

    def update_scan_result(self):
        match_count = self.backend.get_match_count()
        self.found_count_label.set_text('Found: %d' % (match_count, ))
        if (match_count > SCAN_RESULT_LIST_LIMIT):
            self.scanresult_liststore.clear()
        else:
            self.command_lock.acquire()
            lines = self.backend.send_command('list', get_output=True)
            self.command_lock.release()

            self.scanresult_tv.set_model(None)
            # temporarily disable model for scanresult_liststore for the sake of performance
            self.scanresult_liststore.clear()
            for line in lines:
                line = line[line.find(']') + 1:]
                (a, v, t) = list(map(str.strip, line.split(',')[:3]))
                a = '%x' % (int(a, 16), )
                t = t[1:-1]
                self.scanresult_liststore.append([a, v, t, True])
            self.scanresult_tv.set_model(self.scanresult_liststore)

    # return (r1, r2) where all rows between r1 and r2 (INCLUSIVE) are visible
    # return None if no row visible
    def get_visible_rows(self, treeview):
        rect = treeview.get_visible_rect()
        (x1, y1) = treeview.convert_tree_to_widget_coords(rect.x, rect.y)
        (x2,
         y2) = treeview.convert_tree_to_widget_coords(rect.x + rect.width,
                                                      rect.y + rect.height)
        tup = treeview.get_path_at_pos(x1, y1)
        if tup is None:
            return None
        r1 = tup[0][0]
        tup = treeview.get_path_at_pos(x2, y2)
        if tup is None:
            r2 = len(treeview.get_model()) - 1
        else:
            r2 = tup[0][0]
        return (r1, r2)

    # read/write data periodically
    def data_worker(self):
        if (not self.is_scanning) and (
                self.pid !=
                0) and self.command_lock.acquire(0):  # non-blocking
            Gdk.threads_enter()

            self.is_data_worker_working = True
            rows = self.get_visible_rows(self.scanresult_tv)
            if rows is not None:
                (r1, r2) = rows  # [r1, r2] rows are visible
                for i in range(r1, r2 + 1):
                    row = self.scanresult_liststore[i]
                    addr, cur_value, scanmem_type, valid = row
                    if valid:
                        new_value = self.read_value(
                            addr, TYPENAMES_S2G[scanmem_type.strip()],
                            cur_value)
                        if new_value is not None:
                            row[1] = str(new_value)
                        else:
                            row[1] = '??'
                            row[3] = False
            # write locked values in cheat list and read unlocked values
            for i in range(len(self.cheatlist_liststore)):
                (lockflag, locked, desc, addr, typestr, value,
                 valid) = self.cheatlist_liststore[i]
                if not valid:
                    continue
                if locked:
                    self.write_value(addr, typestr, value)
                elif i in self.cheatlist_updates:
                    self.write_value(addr, typestr, value)
                    self.cheatlist_updates.remove(i)
                else:
                    newvalue = self.read_value(addr, typestr, value)
                    if newvalue is None:
                        self.cheatlist_liststore[i] = (lockflag, False, desc,
                                                       addr, typestr, '??',
                                                       False)
                    elif newvalue != value and not locked and not self.cheatlist_editing:
                        self.cheatlist_liststore[i] = (lockflag, locked, desc,
                                                       addr, typestr,
                                                       str(newvalue), valid)
            self.is_data_worker_working = False

            Gdk.flush()
            Gdk.threads_leave()
            self.command_lock.release()
        return not self.exit_flag

    def read_value(self, addr, typestr, prev_value):
        return self.bytes2value(
            typestr,
            self.read_memory(addr, self.get_type_size(typestr, prev_value)))

    # addr could be int or str
    def read_memory(self, addr, length):
        if not isinstance(addr, str):
            addr = '%x' % (addr, )
        f = tempfile.NamedTemporaryFile()

        self.command_lock.acquire()
        self.backend.send_command('dump %s %d %s' % (addr, length, f.name))
        self.command_lock.release()

        data = f.read()

        #        lines = self.backend.send_command('dump %s %d' % (addr, length))
        #        data = ''
        #        for line in lines:
        #            bytes = line.strip().split()
        #            for byte in bytes: data += chr(int(byte, 16))
        # TODO raise Exception here isn't good
        if len(data) != length:
            # self.show_error('Cannot access target memory')
            data = None
        return data

    # addr could be int or str
    def write_value(self, addr, typestr, value):
        if not isinstance(addr, str):
            addr = '%x' % (addr, )

        self.command_lock.acquire()
        self.backend.send_command('write %s %s %s' % (typestr, addr, value))
        self.command_lock.release()

    def exit(self, object, data=None):
        self.exit_flag = True
        Gtk.main_quit()

    def main(self):
        Gtk.main()

    def check_backend_version(self):
        if self.backend.get_version() != VERSION:
            self.show_error(
                'Version of scanmem mismatched, you may encounter problems. Please make sure you are using the same version of Gamconqueror as scanmem.'
            )
class GameConqueror():
    def __init__(self):
        gtk.settings_get_default().set_long_property('gtk-tooltip-timeout', 0,
                                                     '')
        ##################################
        # init GUI
        self.builder = gtk.Builder()
        self.builder.add_from_file(os.path.join(WORK_DIR, 'GameConqueror.xml'))

        self.main_window = self.builder.get_object('MainWindow')
        self.main_window.set_title('GameConqueror %s' % (VERSION, ))
        self.about_dialog = self.builder.get_object('AboutDialog')
        # set version
        self.about_dialog.set_version(VERSION)

        self.process_list_dialog = self.builder.get_object('ProcessListDialog')
        self.addcheat_dialog = self.builder.get_object('AddCheatDialog')

        # init memory editor
        self.memoryeditor_window = self.builder.get_object(
            'MemoryEditor_Window')
        self.memoryeditor_hexview = HexView()
        self.memoryeditor_window.child.pack_start(self.memoryeditor_hexview)
        self.memoryeditor_hexview.show_all()
        self.memoryeditor_address_entry = self.builder.get_object(
            'MemoryEditor_Address_Entry')
        self.memoryeditor_hexview.connect(
            'char-changed', self.memoryeditor_hexview_char_changed_cb)

        self.found_count_label = self.builder.get_object('FoundCount_Label')
        self.process_label = self.builder.get_object('Process_Label')
        self.value_input = self.builder.get_object('Value_Input')

        self.scanoption_frame = self.builder.get_object('ScanOption_Frame')
        self.scanprogress_progressbar = self.builder.get_object(
            'ScanProgress_ProgressBar')

        self.scan_button = self.builder.get_object('Scan_Button')
        self.reset_button = self.builder.get_object('Reset_Button')

        ###
        # Set scan data type
        self.scan_data_type_combobox = self.builder.get_object(
            'ScanDataType_ComboBox')
        misc.build_combobox(self.scan_data_type_combobox, SCAN_VALUE_TYPES)
        # apply setting
        misc.combobox_set_active_item(self.scan_data_type_combobox,
                                      SETTINGS['scan_data_type'])

        ###
        # set search scope
        self.search_scope_scale = self.builder.get_object('SearchScope_Scale')
        self.search_scope_scale_adjustment = gtk.Adjustment(lower=0,
                                                            upper=2,
                                                            step_incr=1,
                                                            page_incr=1,
                                                            page_size=0)
        self.search_scope_scale.set_adjustment(
            self.search_scope_scale_adjustment)
        # apply setting
        self.search_scope_scale.set_value(SETTINGS['search_scope'])

        # init scanresult treeview
        # we may need a cell data func here
        # create model
        self.scanresult_tv = self.builder.get_object('ScanResult_TreeView')
        self.scanresult_liststore = gtk.ListStore(
            str, str, str, bool)  #addr, value, type, valid
        self.scanresult_tv.set_model(self.scanresult_liststore)
        # init columns
        misc.treeview_append_column(self.scanresult_tv,
                                    'Address',
                                    attributes=(('text', 0), ),
                                    properties=(('family', 'monospace'), ))
        misc.treeview_append_column(self.scanresult_tv,
                                    'Value',
                                    attributes=(('text', 1), ),
                                    properties=(('family', 'monospace'), ))

        # init CheatList TreeView
        self.cheatlist_tv = self.builder.get_object('CheatList_TreeView')
        self.cheatlist_liststore = gtk.ListStore(
            str, bool, str, str, str, str,
            bool)  #lockflag, locked, description, addr, type, value, valid
        self.cheatlist_tv.set_model(self.cheatlist_liststore)
        self.cheatlist_tv.set_reorderable(True)
        self.cheatlist_updates = []
        self.cheatlist_editing = False
        self.cheatlist_tv.connect('key-press-event', self.cheatlist_keypressed)
        # Lock Flag
        misc.treeview_append_column(
            self.cheatlist_tv,
            '',
            renderer_class=gtk.CellRendererCombo,
            attributes=(('text', 0), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model', LOCK_FLAG_TYPES), ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_toggle_lock_flag_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Lock
        misc.treeview_append_column(
            self.cheatlist_tv,
            'Lock',
            renderer_class=gtk.CellRendererToggle,
            attributes=(('active', 1), ),
            properties=(('activatable', True), ('radio', False),
                        ('inconsistent', False)),
            signals=(('toggled', self.cheatlist_toggle_lock_cb), ))
        # Description
        misc.treeview_append_column(
            self.cheatlist_tv,
            'Description',
            attributes=(('text', 2), ),
            properties=(('editable', True), ),
            signals=(
                ('edited', self.cheatlist_edit_description_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Address
        misc.treeview_append_column(self.cheatlist_tv,
                                    'Address',
                                    attributes=(('text', 3), ),
                                    properties=(('family', 'monospace'), ))
        # Type
        misc.treeview_append_column(
            self.cheatlist_tv,
            'Type',
            renderer_class=gtk.CellRendererCombo,
            attributes=(('text', 4), ),
            properties=(('editable', True), ('has-entry', False),
                        ('model', LOCK_VALUE_TYPES), ('text-column', 0)),
            signals=(
                ('edited', self.cheatlist_edit_type_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))
        # Value
        misc.treeview_append_column(
            self.cheatlist_tv,
            'Value',
            attributes=(('text', 5), ),
            properties=(('editable', True), ('family', 'monospace')),
            signals=(
                ('edited', self.cheatlist_edit_value_cb),
                ('editing-started', self.cheatlist_edit_start),
                ('editing-canceled', self.cheatlist_edit_cancel),
            ))

        # init ProcessList
        self.processfilter_input = self.builder.get_object(
            'ProcessFilter_Input')
        self.userfilter_input = self.builder.get_object('UserFilter_Input')
        # init ProcessList_TreeView
        self.processlist_tv = self.builder.get_object('ProcessList_TreeView')
        self.processlist_tv.get_selection().set_mode(gtk.SELECTION_SINGLE)
        self.processlist_liststore = gtk.ListStore(str, str, str)
        self.processlist_filter = self.processlist_liststore.filter_new(
            root=None)
        self.processlist_filter.set_visible_func(self.processlist_filter_func,
                                                 data=None)
        self.processlist_tv.set_model(self.processlist_filter)
        self.processlist_tv.set_enable_search(True)
        self.processlist_tv.set_search_column(1)
        # first col
        misc.treeview_append_column(self.processlist_tv,
                                    'PID',
                                    attributes=(('text', 0), ))
        # second col
        misc.treeview_append_column(self.processlist_tv,
                                    'User',
                                    attributes=(('text', 1), ))

        # third col
        misc.treeview_append_column(self.processlist_tv,
                                    'Process',
                                    attributes=(('text', 2), ))
        # init AddCheatDialog
        self.addcheat_address_input = self.builder.get_object('Address_Input')
        self.addcheat_description_input = self.builder.get_object(
            'Description_Input')
        self.addcheat_type_combobox = self.builder.get_object('Type_ComboBox')
        misc.build_combobox(self.addcheat_type_combobox, LOCK_VALUE_TYPES)
        misc.combobox_set_active_item(self.addcheat_type_combobox,
                                      SETTINGS['lock_data_type'])

        # init popup menu for scanresult
        self.scanresult_popup = gtk.Menu()
        misc.menu_append_item(self.scanresult_popup, 'Add to cheat list',
                              self.scanresult_popup_cb, 'add_to_cheat_list')
        misc.menu_append_item(self.scanresult_popup, 'Browse this address',
                              self.scanresult_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.scanresult_popup, 'Scan for this address',
                              self.scanresult_popup_cb,
                              'scan_for_this_address')
        self.scanresult_popup.show_all()

        # init popup menu for cheatlist
        self.cheatlist_popup = gtk.Menu()
        misc.menu_append_item(self.cheatlist_popup, 'Browse this address',
                              self.cheatlist_popup_cb, 'browse_this_address')
        misc.menu_append_item(self.cheatlist_popup, 'Copy address',
                              self.cheatlist_popup_cb, 'copy_address')
        misc.menu_append_item(self.cheatlist_popup, 'Remove this entry',
                              self.cheatlist_popup_cb, 'remove_entry')
        self.cheatlist_popup.show_all()

        self.builder.connect_signals(self)
        self.main_window.connect('destroy', self.exit)

        ###########################
        # init others (backend, flag...)
        self.pid = 0  # target pid
        self.maps = []
        self.last_hexedit_address = (0, 0)  # used for hexview
        self.is_scanning = False
        self.exit_flag = False  # currently for data_worker only, other 'threads' may also use this flag
        self.is_data_worker_working = False

        self.backend = GameConquerorBackend()
        self.backend.add_error_listener(self.backend_error_cb)
        self.backend.add_progress_listener(self.backend_progress_cb)
        self.check_backend_version()
        self.search_count = 0
        self.data_worker_id = gobject.timeout_add(DATA_WORKER_INTERVAL,
                                                  self.data_worker)
        self.command_lock = threading.RLock()

    ###########################
    # GUI callbacks

    def MemoryEditor_Window_delete_event_cb(self, widget, event, data=None):
        self.memoryeditor_window.hide()
        return True

    def MemoryEditor_Button_clicked_cb(self, button, data=None):
        if self.pid == 0:
            self.show_error('Please select a process')
            return
        self.browse_memory()
        return True

    def MemoryEditor_Address_Entry_activate_cb(self, entry, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error('Invalid address')

    def MemoryEditor_JumpTo_Button_clicked_cb(self, button, data=None):
        txt = self.memoryeditor_address_entry.get_text()
        if txt == '':
            return
        try:
            addr = int(txt, 16)
            self.browse_memory(addr)
        except:
            self.show_error('Invalid address')

    def RemoveAllCheat_Button_clicked_cb(self, button, data=None):
        self.cheatlist_liststore.clear()
        return True

    def ManuallyAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.show()
        return True

    def SearchScope_Scale_format_value_cb(self, scale, value, Data=None):
        return SEARCH_SCOPE_NAMES[int(value)]

    def Value_Input_activate_cb(self, entry, data=None):
        self.do_scan()
        return True

    def ScanResult_TreeView_button_release_event_cb(self,
                                                    widget,
                                                    event,
                                                    data=None):
        if event.button == 3:  # right click
            (model, iter) = self.scanresult_tv.get_selection().get_selected()
            if iter is not None:
                self.scanresult_popup.popup(None, None, None, event.button,
                                            event.get_time())
                return True
            return False
        return False

    def ScanResult_TreeView_popup_menu_cb(self, widget, data=None):
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        if iter is not None:
            self.scanresult_popup.popup(None, None, None, 0, 0)
            return True
        return False

    def ScanResult_TreeView_row_activated_cb(self,
                                             treeview,
                                             path,
                                             view_column,
                                             data=None):
        # add to cheat list
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        if iter is not None:
            (addr, value, typestr) = model.get(iter, 0, 1, 2)
            self.add_to_cheat_list(addr, value, typestr)
            return True
        return False

    def CheatList_TreeView_button_release_event_cb(self,
                                                   widget,
                                                   event,
                                                   data=None):
        if event.button == 3:  # right click
            (model, iter) = self.cheatlist_tv.get_selection().get_selected()
            if iter is not None:
                self.cheatlist_popup.popup(None, None, None, event.button,
                                           event.get_time())
                return True
            return False
        return False

    def CheatList_TreeView_popup_menu_cb(self, widget, data=None):
        (model, iter) = self.cheatlist_tv.get_selection().get_selected()
        if iter is not None:
            self.cheatlist_popup.popup(None, None, None, 0, 0)
            return True
        return False

    def ProcessFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def UserFilter_Input_changed_cb(self, widget, data=None):
        self.processlist_filter.refilter()

    def ProcessList_TreeView_row_activated_cb(self,
                                              treeview,
                                              path,
                                              view_column,
                                              data=None):
        (model, iter) = self.processlist_tv.get_selection().get_selected()
        if iter is not None:
            (pid, user, process) = model.get(iter, 0, 1, 2)
            self.select_process(int(pid), process)
            self.process_list_dialog.response(gtk.RESPONSE_CANCEL)
            return True
        return False

    def SelectProcess_Button_clicked_cb(self, button, data=None):
        self.processlist_liststore.clear()
        pl = self.get_process_list()
        for p in pl:
            # self.processlist_liststore.append([p[0], (p[1][:50] if len(p) > 1 else '<unknown>')]) # limit the length here, otherwise it may crash (why?)
            self.processlist_liststore.append([
                p[0], (p[1] if len(p) > 1 else '<unknown>'),
                (p[2] if len(p) > 2 else '<unknown>')
            ])
        self.process_list_dialog.show()
        while True:
            res = self.process_list_dialog.run()
            if res == gtk.RESPONSE_OK:  # -5
                (model,
                 iter) = self.processlist_tv.get_selection().get_selected()
                if iter is None:
                    self.show_error('Please select a process')
                    continue
                else:
                    (pid, process) = model.get(iter, 0, 1)
                    self.select_process(int(pid), process)
                    break
            else:  # for None and Cancel
                break
        self.process_list_dialog.hide()
        return True

    def ConfirmAddCheat_Button_clicked_cb(self, button, data=None):
        try:
            addr = self.addcheat_address_input.get_text()
        except ValueError:
            self.show_error('Please enter a valid address.')
            return False

        description = self.addcheat_description_input.get_text()
        if not description: description = 'No Description'
        typestr = LOCK_VALUE_TYPES[self.addcheat_type_combobox.get_active()][0]
        if 'int' in typestr: value = 0
        elif 'float' in typestr: value = 0.0
        elif 'string' in typestr: value = ''
        else: value = None

        self.add_to_cheat_list(addr, value, typestr, description)
        self.addcheat_dialog.hide()
        return True

    def CloseAddCheat_Button_clicked_cb(self, button, data=None):
        self.addcheat_dialog.hide()
        return True

    def Scan_Button_clicked_cb(self, button, data=None):
        self.do_scan()
        return True

    def Reset_Button_clicked_cb(self, button, data=None):
        self.reset_scan()
        return True

    def Logo_EventBox_button_release_event_cb(self, widget, data=None):
        self.about_dialog.run()
        self.about_dialog.hide()
        return True

    #######################
    # customed callbacks
    # (i.e. not standard event names are used)

    def memoryeditor_hexview_char_changed_cb(self, hexview, offset, charval):
        addr = hexview.base_addr + offset
        self.write_value(addr, 'int8', charval)
        # return False such that the byte the default handler will be called, and will be displayed correctly
        return False

    def cheatlist_edit_start(self, a, b, c):
        self.cheatlist_editing = True

    def cheatlist_edit_cancel(self, a):
        self.cheatlist_editing = False

    def scanresult_popup_cb(self, menuitem, data=None):
        (model, iter) = self.scanresult_tv.get_selection().get_selected()
        (addr, value, typestr) = model.get(iter, 0, 1, 2)
        if iter is None:
            return False
        if data == 'add_to_cheat_list':
            self.add_to_cheat_list(addr, value, typestr)
            return True
        elif data == 'browse_this_address':
            self.browse_memory(int(addr, 16))
            return True
        elif data == 'scan_for_this_address':
            self.scan_for_addr(int(addr, 16))
            return True
        return False

    def cheatlist_keypressed(self, cheatlist_tv, event, selection=None):
        keycode = event.keyval
        pressedkey = gtk.gdk.keyval_name(keycode)
        if pressedkey == 'Delete':
            (model, iter) = self.cheatlist_tv.get_selection().get_selected()
            if iter is None: return
            self.cheatlist_liststore.remove(iter)

    def cheatlist_popup_cb(self, menuitem, data=None):
        self.cheatlist_editing = False
        (model, iter) = self.cheatlist_tv.get_selection().get_selected()
        addr = model.get(iter, 3)[0]
        if iter is None:
            return False
        if data == 'remove_entry':
            self.cheatlist_liststore.remove(iter)
            return True
        elif data == 'browse_this_address':
            self.browse_memory(int(addr, 16))
            return True
        elif data == 'copy_address':
            CLIPBOARD.set_text(addr)
            return True
        return False

    def cheatlist_toggle_lock_cb(self, cellrenderertoggle, path, data=None):
        row = int(path)
        if self.cheatlist_liststore[row][6]:  # valid
            locked = self.cheatlist_liststore[row][1]
            locked = not locked
            self.cheatlist_liststore[row][1] = locked
        if locked:
            #TODO: check value(valid number & not overflow), if failed, unlock it and do nothing
            pass
        else:
            #TODO: update its value?
            pass
        return True

    def cheatlist_toggle_lock_flag_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # currently only one lock flag is supported
        return True
        row = int(path)
        self.cheatlist_liststore[row][0] = new_text
        # data_worker will handle this later
        return True

    def cheatlist_edit_description_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        row = int(path)
        self.cheatlist_liststore[row][2] = new_text
        return True

    def cheatlist_edit_value_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        # ignore empty value
        if new_text == '':
            return True
        row = int(path)
        if not self.cheatlist_liststore[row][6]:  #not valid
            return True
        self.cheatlist_liststore[row][5] = new_text
        if self.cheatlist_liststore[row][1]:  # locked
            # data_worker will handle this
            pass
        else:
            # write it for once
            (lockflag, locked, desc, addr, typestr, value,
             valid) = self.cheatlist_liststore[row]
            self.cheatlist_updates.append(row)
            self.write_value(addr, typestr, value)
        return True

    def cheatlist_edit_type_cb(self, cell, path, new_text, data=None):
        self.cheatlist_editing = False
        row = int(path)
        self.cheatlist_liststore[row][4] = new_text
        if self.cheatlist_liststore[row][1]:  # locked
            # false unlock it
            self.cheatlist_liststore[row][1] = False
            pass
        return True

    def processlist_filter_func(self, model, iter, data=None):
        (pid, user, process) = model.get(iter, 0, 1, 2)
        return process is not None and \
                process.find(self.processfilter_input.get_text()) != -1 and \
                user is not None and \
                user.find(self.userfilter_input.get_text()) != -1

    ############################
    # core functions
    def show_error(self, msg):
        dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
                                   gtk.BUTTONS_OK, msg)
        dialog.run()
        dialog.destroy()

    # return None if unknown
    def get_pointer_width(self):
        bits = platform.architecture()[0]
        if not bits.endswith('bit'):
            return None
        try:
            bitn = int(bits[:-len('bit')])
            if bitn not in [8, 16, 32, 64]:
                return None
            else:
                return bitn
        except:
            return None

    # return the size in bytes of the value in memory
    def get_type_size(self, typename, value):
        if typename in TYPESIZES.keys():  # int or float type; fixed length
            return TYPESIZES[typename]
        elif typename == 'bytearray':
            return (len(value.strip()) + 1) / 3
        elif typename == 'string':
            return len(eval('\'' + value + '\''))
        return None

    # parse bytes dumped by scanmem into number, string, etc.
    def bytes2value(self, typename, bytes):
        if bytes is None:
            return None
        if typename in TYPENAMES_G2STRUCT.keys():
            return struct.unpack(TYPENAMES_G2STRUCT[typename], bytes)[0]
        elif typename == 'string':
            return repr('%s' % (bytes, ))[1:-1]
        elif typename == 'bytearray':
            return ' '.join(['%02x' % ord(i) for i in bytes])
        else:
            return bytes

    def scan_for_addr(self, addr):
        bits = self.get_pointer_width()
        if bits is None:
            show_error('Unknown architecture, you may report to developers')
            return
        self.reset_scan()
        self.value_input.set_text('%#x' % (addr, ))
        misc.combobox_set_active_item(self.scan_data_type_combobox,
                                      'int%d' % (bits, ))
        self.do_scan()

    def browse_memory(self, addr=None):
        # select a region contains addr
        try:
            self.read_maps()
        except:
            show_error(
                'Cannot retieve memory maps of that process, maybe it has exited (crashed), or you don\'t have enough privilege'
            )
        selected_region = None
        if addr:
            for m in self.maps:
                if m['start_addr'] <= addr and addr < m['end_addr']:
                    selected_region = m
                    break
            if selected_region:
                if selected_region['flags'][0] != 'r':  # not readable
                    show_error('Address %x is not readable' % (addr, ))
                    return
            else:
                show_error('Address %x is not valid' % (addr, ))
                return
        else:
            # just select the first readable region
            for m in self.maps:
                if m['flags'][0] == 'r':
                    selected_region = m
                    break
            if selected_region is None:
                self.show_error('Cannot find a readable region')
                return
            addr = selected_region['start_addr']

        # read region if necessary
        start_addr = max(addr - 1024, selected_region['start_addr'])
        end_addr = min(addr + 1024, selected_region['end_addr'])
        if self.last_hexedit_address[0] != start_addr or \
           self.last_hexedit_address[1] != end_addr:
            data = self.read_memory(start_addr, end_addr - start_addr)
            if data is None:
                self.show_error('Cannot read memory')
                return
            self.last_hexedit_address = (start_addr, end_addr)
            self.memoryeditor_hexview.payload = data
            self.memoryeditor_hexview.base_addr = start_addr

        # set editable flag
        self.memoryeditor_hexview.editable = (
            selected_region['flags'][1] == 'w')

        if addr:
            self.memoryeditor_hexview.show_addr(addr)
        self.memoryeditor_window.show()

    # this callback will be called from other thread
    def backend_error_cb(self, msg):
        gtk.gdk.threads_enter()
        # we don't want massive error output for data worker
        if not self.is_data_worker_working:
            self.show_error('Backend error: %s' % (msg, ))
        if self.is_scanning:
            self.finish_scan()
        gtk.gdk.threads_leave()

    # this callback will be called from other thread
    def backend_progress_cb(self, cur, total):
        gtk.gdk.threads_enter()
        # cur and total may be 0!
        if (cur == total) and self.is_scanning:
            self.scanprogress_progressbar.set_fraction(1.0)
            self.finish_scan()
        else:
            self.scanprogress_progressbar.set_fraction(float(cur) / total)
        gtk.gdk.threads_leave()

    def add_to_cheat_list(self,
                          addr,
                          value,
                          typestr,
                          description='No Description'):
        # determine longest possible type
        types = typestr.split()
        vt = typestr
        for t in types:
            if TYPENAMES_S2G.has_key(t):
                vt = TYPENAMES_S2G[t]
                break
        self.cheatlist_liststore.prepend(
            ['=', False, description, addr, vt, value, True])

    def get_process_list(self):
        return [
            map(str.strip,
                e.strip().split(' ', 2)) for e in os.popen(
                    'ps -wweo pid=,user=,command= --sort=-pid').readlines()
        ]

    def select_process(self, pid, process_name):
        # ask backend for attaching the target process
        # update 'current process'
        # reset flags
        # for debug/log
        #        print 'Select process: %d - %s' % (pid, process_name)
        self.pid = pid
        try:
            self.read_maps()
        except:
            self.pid = 0
            self.process_label.set_text('No process selected')
            self.process_label.set_property('tooltip-text', 'Select a process')
            self.show_error(
                'Cannot retieve memory maps of that process, maybe it has exited (crashed), or you don\'t have enough privilege'
            )
        self.process_label.set_text('%d - %s' % (pid, process_name))
        self.process_label.set_property('tooltip-text', process_name)

        self.command_lock.acquire()
        self.backend.send_command('pid %d' % (pid, ))
        self.reset_scan()
        self.command_lock.release()

        # unlock all entries in cheat list
        for i in xrange(len(self.cheatlist_liststore)):
            self.cheatlist_liststore[i][1] = False

    def read_maps(self):
        lines = open('/proc/%d/maps' % (self.pid, )).readlines()
        self.maps = []
        for l in lines:
            item = {}
            info = l.split(' ', 5)
            addr = info[0]
            idx = addr.index('-')
            item['start_addr'] = int(addr[:idx], 16)
            item['end_addr'] = int(addr[idx + 1:], 16)
            item['size'] = item['end_addr'] - item['start_addr']
            item['flags'] = info[1]
            item['offset'] = info[2]
            item['dev'] = info[3]
            item['inode'] = int(info[4])
            if len(info) < 6:
                item['pathname'] = ''
            else:
                item['pathname'] = info[5].lstrip()  # don't use strip
            self.maps.append(item)

    def reset_scan(self):
        self.command_lock.acquire()
        self.search_count = 0
        # reset search type and value type
        self.scanresult_liststore.clear()

        self.command_lock.acquire()
        self.backend.send_command('reset')
        self.update_scan_result()
        self.command_lock.release()

        self.scanoption_frame.set_sensitive(True)

    def apply_scan_settings(self):
        # scan data type
        active = self.scan_data_type_combobox.get_active()
        assert (active >= 0)
        dt = self.scan_data_type_combobox.get_model()[active][0]

        self.command_lock.acquire()
        self.backend.send_command('option scan_data_type %s' % (dt, ))
        # search scope
        self.backend.send_command(
            'option region_scan_level %d' %
            (1 + int(self.search_scope_scale.get_value()), ))
        # TODO: ugly, reset to make region_scan_level taking effect
        self.backend.send_command('reset')
        self.command_lock.release()

    # perform scanning through backend
    # set GUI if needed
    def do_scan(self):
        if self.pid == 0:
            self.show_error('Please select a process')
            return
        active = self.scan_data_type_combobox.get_active()
        assert (active >= 0)
        data_type = self.scan_data_type_combobox.get_model()[active][0]
        cmd = self.value_input.get_text()

        try:
            cmd = misc.check_scan_command(data_type, cmd)
        except Exception, e:
            # this is not quite good
            self.show_error(e.args[0])
            return

        # disable the window before perform scanning, such that if result come so fast, we won't mess it up
        self.search_count += 1
        self.scanoption_frame.set_sensitive(
            False)  # no need to check search_count here
        self.main_window.set_sensitive(False)
        self.memoryeditor_window.set_sensitive(False)
        self.is_scanning = True
        # set scan options only when first scan, since this will reset backend

        self.command_lock.acquire()
        if self.search_count == 1:
            self.apply_scan_settings()
        self.backend.send_command(cmd, get_output=False)