class BridgeDock(QDockWidget): def __init__(self, parent, api: PluginApi) -> None: super().__init__('', parent) self.api = api self.ui = Ui_BridgeDock() self.ui.setupUi(self) self.server_thread = None self.observer = None self.modified_timer = None self.slot_server_running(False) self.ui.pushButtonStartServer.clicked.connect(self.slot_start_server) self.ui.pushButtonStopServer.clicked.connect(self.slot_stop_server) self.ui.toolButtonLoadFolder.clicked.connect( self.slot_edit_load_folder) self.ui.toolButtonSaveFolder.clicked.connect( self.slot_edit_save_folder) self.ui.labelConnectionStatus.setText('Server not yet running.') # Initially load from repo folder self.ui.lineEditLoadFolder.setText(settings.get_repo_location()) self.visibilityChanged.connect(self.slot_visibility_changed) def slot_visibility_changed(self, visible: bool) -> None: if not visible and self.server_thread is not None: self.slot_stop_server() def slot_server_running(self, running: bool) -> None: if running: self.ui.pushButtonStartServer.setVisible(False) self.ui.pushButtonStopServer.setVisible(True) else: self.ui.pushButtonStartServer.setVisible(True) self.ui.pushButtonStopServer.setVisible(False) def slot_start_server(self) -> None: if self.ui.checkBoxCopySaves.isChecked( ) and self.ui.lineEditSaveFolder.text().strip() == '': self.api.show_error( 'Entity Explorer Bridge', 'You need to set the folder where to store the copies.') return self.server_thread = QThread() self.server_worker = ServerWorker() self.server_worker.signal_connected.connect(self.slot_connected) self.server_worker.signal_disconnected.connect(self.slot_disconnected) self.server_worker.signal_error.connect(self.slot_error) self.server_worker.signal_started.connect(self.slot_server_started) self.server_worker.signal_shutdown.connect(self.slot_server_stopped) self.server_worker.moveToThread(self.server_thread) self.server_thread.started.connect(self.server_worker.process) self.server_thread.start() self.slot_server_running(True) self.set_folders_active(False) def set_folders_active(self, active: bool) -> None: self.ui.lineEditLoadFolder.setEnabled(active) self.ui.toolButtonLoadFolder.setEnabled(active) self.ui.checkBoxCopySaves.setEnabled(active) self.ui.lineEditSaveFolder.setEnabled(active) self.ui.toolButtonSaveFolder.setEnabled(active) def slot_stop_server(self) -> None: # Shutdown needs to be triggered by the server thread, so send a request requests.get('http://localhost:10243/shutdown') self.set_folders_active(True) def slot_connected(self) -> None: self.ui.labelConnectionStatus.setText( 'Connected to Entity Explorer instance.') def slot_disconnected(self) -> None: self.ui.labelConnectionStatus.setText( 'Disconnected from Entity Explorer instance.') def slot_server_started(self) -> None: self.slot_server_running(True) self.ui.labelConnectionStatus.setText( 'Server running. Please connect Entity Explorer instance.') self.start_watchdog() def slot_server_stopped(self) -> None: self.slot_server_running(False) self.server_thread.terminate() self.ui.labelConnectionStatus.setText('Server stopped.') self.stop_watchdog() def slot_error(self, error: str) -> None: self.slot_server_running(False) self.server_thread.terminate() self.api.show_error('Entity Explorer Bridge', error) def slot_edit_load_folder(self): dir = QFileDialog.getExistingDirectory( self, 'Folder in which the save states are stored by mGBA', self.ui.lineEditLoadFolder.text()) print(dir) if dir is not None: self.ui.lineEditLoadFolder.setText(dir) def slot_edit_save_folder(self): dir = QFileDialog.getExistingDirectory( self, 'Folder in which all save states should be copied', self.ui.lineEditSaveFolder.text()) if dir is not None: self.ui.lineEditSaveFolder.setText(dir) def start_watchdog(self): if self.observer is not None: print('Already observing') return patterns = [ '*.ss0', '*.ss1', '*.ss2', '*.ss3', '*.ss4', '*.ss5', '*.ss6', '*.ss7', '*.ss8', '*.ss9', '*.State' ] ignore_patterns = None ignore_directories = False case_sensitive = True self.event_handler = PatternMatchingEventHandler( patterns, ignore_patterns, ignore_directories, case_sensitive) self.event_handler.on_modified = self.on_file_modified path = self.ui.lineEditLoadFolder.text() self.observer = Observer() self.observer.schedule(self.event_handler, path, recursive=False) self.observer.start() def stop_watchdog(self): if self.observer is not None: self.observer.stop() self.observer.join() self.observer = None # https://stackoverflow.com/a/66907107 def debounce(wait_time): """ Decorator that will debounce a function so that it is called after wait_time seconds If it is called multiple times, will wait for the last call to be debounced and run only this one. """ def decorator(function): def debounced(*args, **kwargs): def call_function(): debounced._timer = None return function(*args, **kwargs) # if we already have a call to the function currently waiting to be executed, reset the timer if debounced._timer is not None: debounced._timer.cancel() # after wait_time, call the function provided to the decorator with its arguments debounced._timer = threading.Timer(wait_time, call_function) debounced._timer.start() debounced._timer = None return debounced return decorator @debounce(0.1) def on_file_modified(self, event): with open(event.src_path, 'rb') as file: bytes = file.read() self.server_worker.slot_send_save_state(event.src_path, bytes) if self.ui.checkBoxCopySaves.isChecked( ) and self.ui.lineEditSaveFolder.text(): name = os.path.basename(event.src_path) name = datetime.now().strftime('%Y-%m-%d_%H_%M_%S_%f_') + name with open( os.path.join(self.ui.lineEditSaveFolder.text(), name), 'wb') as output: output.write(bytes)
class BridgeDock(QDockWidget): def __init__(self, parent, api: PluginApi) -> None: super().__init__('', parent) self.api = api self.ui = Ui_BridgeDock() self.ui.setupUi(self) self.server_thread = None self.observer = None self.modified_timer = None self.slot_server_running(False) self.ui.pushButtonStartServer.clicked.connect(self.slot_start_server) self.ui.pushButtonStopServer.clicked.connect(self.slot_stop_server) self.ui.labelConnectionStatus.setText('Server not yet running.') self.visibilityChanged.connect(self.slot_visibility_changed) def slot_visibility_changed(self, visible: bool) -> None: if not visible and self.server_thread is not None: self.slot_stop_server() def slot_server_running(self, running: bool) -> None: if running: self.ui.pushButtonStartServer.setVisible(False) self.ui.pushButtonStopServer.setVisible(True) else: self.ui.pushButtonStartServer.setVisible(True) self.ui.pushButtonStopServer.setVisible(False) def slot_start_server(self) -> None: self.server_thread = QThread() self.server_worker = ServerWorker() self.server_worker.signal_connected.connect(self.slot_connected) self.server_worker.signal_error.connect(self.slot_error) self.server_worker.signal_started.connect(self.slot_server_started) self.server_worker.signal_shutdown.connect(self.slot_server_stopped) self.server_worker.signal_script_addr.connect(self.slot_script_addr) self.server_worker.moveToThread(self.server_thread) self.server_thread.started.connect(self.server_worker.process) self.server_thread.start() self.slot_server_running(True) def slot_stop_server(self) -> None: # Shutdown needs to be triggered by the server thread, so send a request requests.get('http://*****:*****@') or stripped.endswith(':'): output += f'{line}\n' continue if '.ifdef' in stripped: if not ifdef_stack[-1]: ifdef_stack.append(False) output += f'{line}\n' continue # TODO check variant is_usa = stripped.split(' ')[1] == 'USA' ifdef_stack.append(is_usa) output += f'{line}\n' continue if '.ifndef' in stripped: if not ifdef_stack[-1]: ifdef_stack.append(False) output += f'{line}\n' continue is_usa = stripped.split(' ')[1] == 'USA' ifdef_stack.append(not is_usa) output += f'{line}\n' continue if '.else' in stripped: if ifdef_stack[-2]: # If the outermost ifdef is not true, this else does not change the validiness of this ifdef ifdef_stack[-1] = not ifdef_stack[-1] output += f'{line}\n' continue if '.endif' in stripped: ifdef_stack.pop() output += f'{line}\n' continue if not ifdef_stack[-1]: # Not defined for this variant output += f'{line}\n' continue if current_instruction >= len(instructions): # TODO maybe even not print additional lines? output += f'{line}\n' continue addr = instructions[current_instruction].addr prefix = '' if addr == script_offset: prefix = '>' output += f'{addr:03d}| {prefix}{line}\t\n' current_instruction += 1 if stripped.startswith('SCRIPT_END'): break self.ui.labelCode.setText(output)
class BridgeDock(QDockWidget): def __init__(self, parent, api: PluginApi) -> None: super().__init__('', parent) self.api = api self.ui = Ui_BridgeDock() self.ui.setupUi(self) self.server_thread = None self.symbols = None self.rom = None self.data_extractor_plugin = None self.slot_server_running(False) self.ui.pushButtonStartServer.clicked.connect(self.slot_start_server) self.ui.pushButtonStopServer.clicked.connect(self.slot_stop_server) self.ui.pushButtonUpload.clicked.connect(self.slot_upload_function) self.ui.pushButtonDownload.clicked.connect(self.slot_download_function) self.ui.pushButtonCopyJs.clicked.connect(self.slot_copy_js_code) self.ui.pushButtonGoTo.clicked.connect(self.slot_goto) self.ui.pushButtonDecompile.clicked.connect(self.slot_decompile) self.ui.pushButtonGlobalTypes.clicked.connect(self.slot_global_types) self.ui.pushButtonUploadAndDecompile.clicked.connect( self.slot_upload_and_decompile) self.enable_function_group(False) self.ui.labelConnectionStatus.setText('Server not yet running.') self.visibilityChanged.connect(self.slot_close) def slot_close(self, visibility: bool) -> None: # TODO temporarily disable until a good way to detect dock closing is found pass #if not visibility and self.server_thread is not None: # self.slot_stop_server() def slot_server_running(self, running: bool) -> None: if running: self.ui.pushButtonStartServer.setVisible(False) self.ui.pushButtonStopServer.setVisible(True) else: self.ui.pushButtonStartServer.setVisible(True) self.ui.pushButtonStopServer.setVisible(False) def slot_start_server(self) -> None: self.server_thread = QThread() self.server_worker = ServerWorker() self.server_worker.signal_connected.connect(self.slot_connected) self.server_worker.signal_disconnected.connect(self.slot_disconnected) self.server_worker.signal_error.connect(self.slot_error) self.server_worker.signal_c_code.connect(self.slot_received_c_code) self.server_worker.signal_started.connect(self.slot_server_started) self.server_worker.signal_shutdown.connect(self.slot_server_stopped) self.server_worker.signal_extract_data.connect(self.slot_extract_data) self.server_worker.signal_fetch_decompilation.connect( self.slot_fetch_decompilation) self.server_worker.signal_upload_function.connect( self.slot_download_requested) self.server_worker.moveToThread(self.server_thread) self.server_thread.started.connect(self.server_worker.process) self.server_thread.start() self.slot_server_running(True) def enable_function_group(self, enabled: bool) -> None: self.ui.lineEditFunctionName.setEnabled(enabled) self.ui.pushButtonUpload.setEnabled(enabled) self.ui.pushButtonDownload.setEnabled(enabled) self.ui.pushButtonGoTo.setEnabled(enabled) self.ui.pushButtonDecompile.setEnabled(enabled) self.ui.pushButtonUploadAndDecompile.setEnabled(enabled) def slot_stop_server(self) -> None: # Shutdown needs to be triggered by the server thread, so send a request requests.get('http://localhost:10241/shutdown') def slot_upload_function(self) -> None: self.upload_function(True) # Returns true if the user accepted the uploading def upload_function(self, include_function: bool) -> bool: # TODO try catch all of the slots? (err, asm, src, signature) = get_code(self.ui.lineEditFunctionName.text().strip(), include_function) if err: self.api.show_error('CExplore Bridge', asm) return if NO_CONFIRMS: # For pros also directly go to the function in Ghidra and apply the signature self.slot_goto() #self.apply_function_signature(self.ui.lineEditFunctionName.text().strip(), signature) if NO_CONFIRMS or self.api.show_question( 'CExplore Bridge', f'Replace code in CExplore with {self.ui.lineEditFunctionName.text().strip()}?' ): self.server_worker.slot_send_asm_code(extract_USA_asm(asm)) self.server_worker.slot_send_c_code(src) if not NO_CONFIRMS: self.api.show_message( 'CExplore Bridge', f'Uploaded code of {self.ui.lineEditFunctionName.text().strip()}.' ) return True return False def slot_download_requested(self, name: str) -> None: self.ui.lineEditFunctionName.setText(name) self.slot_download_function() def slot_download_function(self) -> None: self.enable_function_group(False) self.server_worker.slot_request_c_code() def slot_received_c_code(self, code: str) -> None: self.enable_function_group(True) (includes, header, src) = split_code(code) dialog = ReceivedDialog(self) dialog.signal_matching.connect(self.slot_store_matching) dialog.signal_nonmatching.connect(self.slot_store_nonmatching) dialog.show_code(includes, header, src) def slot_store_matching(self, includes: str, header: str, code: str) -> None: self.store(includes, header, code, True) def slot_store_nonmatching(self, includes: str, header: str, code: str) -> None: self.store(includes, header, code, False) def store(self, includes: str, header: str, code: str, matching: bool) -> None: (err, msg) = store_code(self.ui.lineEditFunctionName.text().strip(), includes, header, code, matching) if err: self.api.show_error('CExplore Bridge', msg) return if not NO_CONFIRMS: self.api.show_message( 'CExplore Bridge', f'Sucessfully replaced code of {self.ui.lineEditFunctionName.text().strip()}.' ) def slot_copy_js_code(self) -> None: QApplication.clipboard().setText( 'javascript:var script = document.createElement("script");script.src = "http://localhost:10241/static/bridge.js";document.body.appendChild(script);' ) self.api.show_message( 'CExplore Bridge', 'Copied JS code to clipboard.\nPaste it as the url to a bookmark.\nThen go open the CExplore instance and click on the bookmark to connect.' ) def slot_connected(self) -> None: self.ui.labelConnectionStatus.setText( 'Connected to CExplore instance.') self.enable_function_group(True) self.ui.pushButtonCopyJs.setVisible(False) def slot_disconnected(self) -> None: self.ui.labelConnectionStatus.setText( 'Disconnected from CExplore instance.') self.enable_function_group(False) def slot_server_started(self) -> None: self.slot_server_running(True) self.ui.labelConnectionStatus.setText( 'Server running. Please connect CExplore instance.') def slot_server_stopped(self) -> None: self.slot_server_running(False) self.server_thread.terminate() self.enable_function_group(False) self.ui.pushButtonCopyJs.setVisible(True) self.ui.labelConnectionStatus.setText('Server stopped.') def slot_error(self, error: str) -> None: self.slot_server_running(False) self.server_thread.terminate() self.enable_function_group(False) self.ui.pushButtonCopyJs.setVisible(True) self.api.show_error('CExplore Bridge', error) def slot_goto(self) -> None: try: r = requests.get('http://localhost:10242/goto/' + self.ui.lineEditFunctionName.text().strip()) if r.status_code != 200: self.api.show_error('CExplore Bridge', r.text) return except requests.exceptions.RequestException as e: self.api.show_error( 'CExplore Bridge', 'Could not reach Ghidra server. Did you start the script?') def slot_fetch_decompilation(self, name: str) -> None: self.ui.lineEditFunctionName.setText(name) self.slot_decompile() def slot_decompile(self) -> None: try: r = requests.get('http://localhost:10242/decompile/' + self.ui.lineEditFunctionName.text().strip()) if r.status_code != 200: self.api.show_error('CExplore Bridge', r.text) return result = r.text code = improve_decompilation(result) self.server_worker.slot_add_c_code(code) except requests.exceptions.RequestException as e: self.api.show_error( 'CExplore Bridge', 'Could not reach Ghidra server. Did you start the script?') except Exception as e: self.api.show_error('CExplore Bridge', 'An unknown error occured: ' + str(e)) def slot_upload_and_decompile(self) -> None: # Upload, but don't include the function. if self.upload_function(False): # Now add the decompiled function. self.slot_decompile() def slot_global_types(self) -> None: globals = find_globals() success = True for definition in globals: if not self.apply_global_type(definition): success = False break # Also apply function signatures from file if success: signatures = read_signatures_from_file() for signature in signatures: if not self.apply_function_signature(signature.function, signature.signature): success = False break if success: self.api.show_message('CExplore Bridge', 'Applied all global types.') def apply_global_type(self, definition: TypeDefinition) -> bool: try: url = 'http://localhost:10242/globalType/' + definition.name + '/' + definition.dataType + '/' + definition.length print(url) r = requests.get(url) if r.status_code != 200: self.api.show_error('CExplore Bridge', r.text) return False return True except requests.exceptions.RequestException as e: self.api.show_error( 'CExplore Bridge', 'Could not reach Ghidra server. Did you start the script?') return False def apply_function_signature(self, name: str, signature: str) -> bool: try: url = 'http://localhost:10242/functionType/' + name + '/' + signature print(url) r = requests.get(url) if r.status_code != 200: self.api.show_error('CExplore Bridge', r.text) return False return True except requests.exceptions.RequestException as e: self.api.show_error( 'CExplore Bridge', 'Could not reach Ghidra server. Did you start the script?') return False def slot_extract_data(self, text: str) -> None: if self.symbols is None: # First need to load symbols self.symbols = get_symbol_database().get_symbols(RomVariant.CUSTOM) if self.symbols is None: self.server_worker.slot_extracted_data({ 'status': 'error', 'text': 'No symbols for rom CUSTOM loaded' }) return if self.data_extractor_plugin is None: self.data_extractor_plugin = get_plugin('data_extractor', 'DataExtractorPlugin') if self.data_extractor_plugin is None: self.server_worker.slot_extracted_data({ 'status': 'error', 'text': 'Data Extractor plugin not loaded' }) return if self.rom is None: self.rom = get_rom(RomVariant.CUSTOM) if self.rom is None: self.server_worker.slot_extracted_data({ 'status': 'error', 'text': 'CUSTOM rom could not be loaded' }) return try: result = self.data_extractor_plugin.instance.extract_data( text, self.symbols, self.rom) if result is not None: self.server_worker.slot_extracted_data({ 'status': 'ok', 'text': result }) except Exception as e: traceback.print_exc() self.server_worker.slot_extracted_data({ 'status': 'error', 'text': str(e) })