mirror of
https://github.com/gaasedelen/lighthouse
synced 2026-06-08 14:19:10 +00:00
413 lines
13 KiB
Python
413 lines
13 KiB
Python
import abc
|
|
import logging
|
|
|
|
import lighthouse
|
|
|
|
from lighthouse.util import lmsg
|
|
from lighthouse.util.qt import *
|
|
from lighthouse.util.update import check_for_update
|
|
from lighthouse.util.disassembler import disassembler
|
|
|
|
from lighthouse.ui import *
|
|
from lighthouse.metadata import metadata_progress
|
|
from lighthouse.exceptions import *
|
|
|
|
logger = logging.getLogger("Lighthouse.Core")
|
|
|
|
#------------------------------------------------------------------------------
|
|
# Lighthouse Plugin Core
|
|
#------------------------------------------------------------------------------
|
|
|
|
class LighthouseCore(object):
|
|
__metaclass__ = abc.ABCMeta
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Plugin Metadata
|
|
#--------------------------------------------------------------------------
|
|
|
|
PLUGIN_VERSION = "0.9.4-DEV"
|
|
AUTHORS = "Markus Gaasedelen"
|
|
DATE = "2024"
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Initialization
|
|
#--------------------------------------------------------------------------
|
|
|
|
def load(self):
|
|
"""
|
|
Load the plugin, and integrate its UI into the disassembler.
|
|
"""
|
|
self._update_checked = False
|
|
self.lighthouse_contexts = {}
|
|
|
|
# the plugin color palette
|
|
self.palette = LighthousePalette()
|
|
self.palette.theme_changed(self.refresh_theme)
|
|
|
|
def create_coverage_overview(name, parent, dctx):
|
|
lctx = self.get_context(dctx, startup=False)
|
|
widget = disassembler.create_dockable_widget(parent, name)
|
|
overview = CoverageOverview(lctx, widget)
|
|
return widget
|
|
|
|
# the coverage overview widget
|
|
disassembler.register_dockable("Coverage Overview", create_coverage_overview)
|
|
|
|
# install disassembler UI
|
|
self._install_ui()
|
|
|
|
# install entry point for headless / terminal access...
|
|
lighthouse.get_context = self.get_context
|
|
|
|
# plugin loaded successfully, print the plugin banner
|
|
self.print_banner()
|
|
logger.info("Successfully loaded plugin")
|
|
|
|
def unload(self):
|
|
"""
|
|
Unload the plugin, and remove any UI integrations.
|
|
"""
|
|
self._uninstall_ui()
|
|
|
|
# remove headless entry point
|
|
lighthouse.get_context = lambda x: None
|
|
|
|
# spin down any active contexts (stop threads, cleanup qt state, etc)
|
|
for lctx in self.lighthouse_contexts.values():
|
|
lctx.terminate()
|
|
|
|
logger.info("-"*75)
|
|
logger.info("Plugin terminated")
|
|
|
|
def print_banner(self):
|
|
"""
|
|
Print the plugin banner.
|
|
"""
|
|
|
|
# build the main banner title
|
|
banner_params = (self.PLUGIN_VERSION, self.AUTHORS, self.DATE)
|
|
banner_title = "v%s - (c) %s - %s" % banner_params
|
|
|
|
# print plugin banner
|
|
lmsg("Loaded %s" % banner_title)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Disassembler / Database Context Selector
|
|
#--------------------------------------------------------------------------
|
|
|
|
@abc.abstractmethod
|
|
def get_context(self, dctx, startup=True):
|
|
"""
|
|
Get the LighthouseContext object for a given database context.
|
|
"""
|
|
pass
|
|
|
|
#--------------------------------------------------------------------------
|
|
# UI Integration (Internal)
|
|
#--------------------------------------------------------------------------
|
|
|
|
def _install_ui(self):
|
|
"""
|
|
Initialize & integrate all plugin UI elements.
|
|
"""
|
|
self._install_load_file()
|
|
self._install_load_batch()
|
|
self._install_open_coverage_xref()
|
|
self._install_open_coverage_overview()
|
|
|
|
def _uninstall_ui(self):
|
|
"""
|
|
Cleanup & remove all plugin UI integrations.
|
|
"""
|
|
self._uninstall_open_coverage_overview()
|
|
self._uninstall_open_coverage_xref()
|
|
self._uninstall_load_batch()
|
|
self._uninstall_load_file()
|
|
|
|
@abc.abstractmethod
|
|
def _install_load_file(self):
|
|
"""
|
|
Install the 'File->Load->Code coverage file...' menu entry.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def _install_load_batch(self):
|
|
"""
|
|
Install the 'File->Load->Code coverage batch...' menu entry.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def _install_open_coverage_xref(self):
|
|
"""
|
|
Install the right click 'Coverage Xref' context menu entry.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def _install_open_coverage_overview(self):
|
|
"""
|
|
Install the 'View->Open subviews->Coverage Overview' menu entry.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def _uninstall_load_file(self):
|
|
"""
|
|
Remove the 'File->Load file->Code coverage file...' menu entry.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def _uninstall_load_batch(self):
|
|
"""
|
|
Remove the 'File->Load file->Code coverage batch...' menu entry.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def _uninstall_open_coverage_xref(self):
|
|
"""
|
|
Remove the right click 'Coverage Xref' context menu entry.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def _uninstall_open_coverage_overview(self):
|
|
"""
|
|
Remove the 'View->Open subviews->Coverage Overview' menu entry.
|
|
"""
|
|
pass
|
|
|
|
#--------------------------------------------------------------------------
|
|
# UI Actions (Public)
|
|
#--------------------------------------------------------------------------
|
|
|
|
def refresh_theme(self):
|
|
"""
|
|
Refresh UI facing elements to reflect the current theme.
|
|
"""
|
|
for lctx in self.lighthouse_contexts.values():
|
|
lctx.director.refresh_theme()
|
|
if lctx.coverage_overview:
|
|
lctx.coverage_overview.refresh_theme()
|
|
lctx.painter.force_repaint()
|
|
|
|
def open_coverage_overview(self, dctx=None):
|
|
"""
|
|
Open the dockable 'Coverage Overview' dialog.
|
|
"""
|
|
lctx = self.get_context(dctx)
|
|
|
|
# the coverage overview is already open & visible, nothing to do
|
|
if lctx.coverage_overview and lctx.coverage_overview.visible:
|
|
return
|
|
|
|
# show the coverage overview
|
|
disassembler.show_dockable("Coverage Overview")
|
|
|
|
# trigger an update check (this should only ever really 'check' once)
|
|
self.check_for_update()
|
|
|
|
def open_coverage_xref(self, address, dctx=None):
|
|
"""
|
|
Open the 'Coverage Xref' dialog for a given address.
|
|
"""
|
|
lctx = self.get_context(dctx)
|
|
|
|
# show the coverage xref dialog
|
|
dialog = CoverageXref(lctx.director, address)
|
|
if not dialog.exec_():
|
|
return
|
|
|
|
# activate the user selected xref (if one was double clicked)
|
|
if dialog.selected_coverage:
|
|
lctx.director.select_coverage(dialog.selected_coverage)
|
|
return
|
|
|
|
# load a coverage file from disk
|
|
disassembler.show_wait_box("Loading coverage from disk...")
|
|
created_coverage, errors = lctx.director.load_coverage_files(
|
|
[dialog.selected_filepath],
|
|
disassembler.replace_wait_box
|
|
)
|
|
|
|
if not created_coverage:
|
|
lmsg("No coverage files could be loaded...")
|
|
disassembler.hide_wait_box()
|
|
warn_errors(errors)
|
|
return
|
|
|
|
disassembler.replace_wait_box("Selecting coverage...")
|
|
lctx.director.select_coverage(created_coverage[0].name)
|
|
disassembler.hide_wait_box()
|
|
|
|
def interactive_load_batch(self, dctx=None):
|
|
"""
|
|
Perform the user-interactive loading of a coverage batch.
|
|
"""
|
|
lctx = self.get_context(dctx)
|
|
|
|
#
|
|
# kick off an asynchronous metadata refresh. this will run in the
|
|
# background while the user is selecting which coverage files to load
|
|
#
|
|
|
|
future = lctx.metadata.refresh_async(progress_callback=metadata_progress)
|
|
|
|
#
|
|
# we will now prompt the user with an interactive file dialog so they
|
|
# can select the coverage files they would like to load from disk
|
|
#
|
|
|
|
filepaths = lctx.select_coverage_files()
|
|
if not filepaths:
|
|
lctx.director.metadata.abort_refresh()
|
|
return
|
|
|
|
# prompt the user to name the new coverage aggregate
|
|
default_name = "BATCH_%s" % lctx.director.peek_shorthand()
|
|
ok, batch_name = prompt_string(
|
|
"Batch Name:",
|
|
"Please enter a name for this coverage",
|
|
default_name
|
|
)
|
|
|
|
#
|
|
# if user didn't enter a name for the batch (or hit cancel) we should
|
|
# abort the loading process...
|
|
#
|
|
|
|
if not (ok and batch_name):
|
|
lmsg("User failed to enter a name for the batch coverage...")
|
|
lctx.director.metadata.abort_refresh()
|
|
return
|
|
|
|
#
|
|
# to begin mapping the loaded coverage data, we require that the
|
|
# asynchronous database metadata refresh has completed. if it is
|
|
# not done yet, we will block here until it completes.
|
|
#
|
|
# a progress dialog depicts the work remaining in the refresh
|
|
#
|
|
|
|
disassembler.show_wait_box("Building database metadata...")
|
|
lctx.metadata.go_synchronous()
|
|
await_future(future)
|
|
|
|
#
|
|
# now that the database metadata is available, we can use the director
|
|
# to normalize and condense (aggregate) all the coverage data
|
|
#
|
|
|
|
disassembler.replace_wait_box("Loading coverage from disk...")
|
|
batch_coverage, errors = lctx.director.load_coverage_batch(
|
|
filepaths,
|
|
batch_name,
|
|
disassembler.replace_wait_box
|
|
)
|
|
|
|
# if batch creation fails...
|
|
if not batch_coverage:
|
|
lmsg("Creation of batch '%s' failed..." % batch_name)
|
|
disassembler.hide_wait_box()
|
|
warn_errors(errors)
|
|
return
|
|
|
|
# select the newly created batch coverage
|
|
disassembler.replace_wait_box("Selecting coverage...")
|
|
lctx.director.select_coverage(batch_name)
|
|
|
|
# all done! pop the coverage overview to show the user their results
|
|
disassembler.hide_wait_box()
|
|
lmsg("Successfully loaded batch %s..." % batch_name)
|
|
self.open_coverage_overview(lctx.dctx)
|
|
|
|
# finally, emit any notable issues that occurred during load
|
|
warn_errors(errors, lctx.director.suppressed_errors)
|
|
|
|
def interactive_load_file(self, dctx=None):
|
|
"""
|
|
Perform the user-interactive loading of individual coverage files.
|
|
"""
|
|
lctx = self.get_context(dctx)
|
|
|
|
#
|
|
# kick off an asynchronous metadata refresh. this will run in the
|
|
# background while the user is selecting which coverage files to load
|
|
#
|
|
|
|
future = lctx.metadata.refresh_async(progress_callback=metadata_progress)
|
|
|
|
#
|
|
# we will now prompt the user with an interactive file dialog so they
|
|
# can select the coverage files they would like to load from disk
|
|
#
|
|
|
|
filenames = lctx.select_coverage_files()
|
|
if not filenames:
|
|
lctx.metadata.abort_refresh()
|
|
return
|
|
|
|
#
|
|
# to begin mapping the loaded coverage data, we require that the
|
|
# asynchronous database metadata refresh has completed. if it is
|
|
# not done yet, we will block here until it completes.
|
|
#
|
|
# a progress dialog depicts the work remaining in the refresh
|
|
#
|
|
|
|
disassembler.show_wait_box("Building database metadata...")
|
|
lctx.metadata.go_synchronous()
|
|
await_future(future)
|
|
|
|
#
|
|
# now that the database metadata is available, we can use the director
|
|
# to load and normalize the selected coverage files
|
|
#
|
|
|
|
disassembler.replace_wait_box("Loading coverage from disk...")
|
|
created_coverage, errors = lctx.director.load_coverage_files(filenames, disassembler.replace_wait_box)
|
|
|
|
#
|
|
# if the director failed to map any coverage, the user probably
|
|
# provided bad files. emit any warnings and bail...
|
|
#
|
|
|
|
if not created_coverage:
|
|
lmsg("No coverage files could be loaded...")
|
|
disassembler.hide_wait_box()
|
|
warn_errors(errors)
|
|
return
|
|
|
|
#
|
|
# activate the first of the newly loaded coverage file(s). this is the
|
|
# one that will be visible in the coverage overview once opened
|
|
#
|
|
|
|
disassembler.replace_wait_box("Selecting coverage...")
|
|
lctx.director.select_coverage(created_coverage[0].name)
|
|
|
|
# all done! pop the coverage overview to show the user their results
|
|
disassembler.hide_wait_box()
|
|
lmsg("Successfully loaded %u coverage file(s)..." % len(created_coverage))
|
|
self.open_coverage_overview(lctx.dctx)
|
|
|
|
# finally, emit any notable issues that occurred during load
|
|
warn_errors(errors, lctx.director.suppressed_errors)
|
|
|
|
def check_for_update(self):
|
|
"""
|
|
Check if there is an update available for Lighthouse.
|
|
"""
|
|
if self._update_checked:
|
|
return
|
|
|
|
# wrap the callback (a popup) to ensure it gets called from the UI
|
|
callback = disassembler.execute_ui(disassembler.warning)
|
|
|
|
# kick off the async update check
|
|
check_for_update(self.PLUGIN_VERSION, callback)
|
|
self._update_checked = True
|