diff options
| author | Mason Reed <mason@vector35.com> | 2026-02-22 22:42:35 -0800 |
|---|---|---|
| committer | Mason Reed <35282038+emesare@users.noreply.github.com> | 2026-02-23 00:09:44 -0800 |
| commit | d0eec62d6df0edbbb76dfeac58879797e90a8594 (patch) | |
| tree | 03cf8d04eb7d33ac0785f2eede92ecbd552a48b0 /python/plugin.py | |
| parent | f3e0bf4ab35d50b11b2dbde41a699ab7497208d1 (diff) | |
Add global plugin command type
Register plugins which are available outside the context of a binary view
Diffstat (limited to 'python/plugin.py')
| -rw-r--r-- | python/plugin.py | 74 |
1 files changed, 66 insertions, 8 deletions
diff --git a/python/plugin.py b/python/plugin.py index 0284d882..988f7c41 100644 --- a/python/plugin.py +++ b/python/plugin.py @@ -55,7 +55,8 @@ class PluginCommandContext: self._length = 0 self._function = None self._instruction = None - self._project = view.project + if view is not None: + self._project = view.project def __len__(self): return self._length @@ -140,6 +141,14 @@ class PluginCommand(metaclass=_PluginCommandMetaClass): self._type = PluginCommandType(cmd.type) @staticmethod + def _global_action(action): + try: + action() + except: + log_error_for_exception("Unhandled Python exception in PluginCommand._global_action") + + + @staticmethod def _default_action(view, action): try: file_metadata = filemetadata.FileMetadata(handle=core.BNGetFileForView(view)) @@ -255,6 +264,16 @@ class PluginCommand(metaclass=_PluginCommandMetaClass): log_error_for_exception("Unhandled Python exception in PluginCommand._project_action") @staticmethod + def _global_is_valid(is_valid): + try: + if is_valid is None: + return True + return is_valid() + except: + log_error_for_exception("Unhandled Python exception in PluginCommand._global_is_valid") + return False + + @staticmethod def _default_is_valid(view, is_valid): try: if is_valid is None: @@ -409,6 +428,38 @@ class PluginCommand(metaclass=_PluginCommandMetaClass): return False @classmethod + def register_global( + cls, name: str, description: str, action: Callable[[], None], + is_valid: Optional[Callable[[], bool]] = None + ): + r""" + ``register_global`` Register a command globally + + :param str name: name of the command (use 'Folder\\Name' to have the menu item nested in a folder) + :param str description: description of the command + :param callback action: function to call + :param callback is_valid: optional argument of a function to determine whether the command should be enabled + :rtype: None + :Example: + + >>> def my_command(): + >>> log_info(f"My command was called on bv") + >>> PluginCommand.register_global("My Command", "My command description (not used)", my_command) + True + >>> def is_valid() -> bool: + >>> return False + >>> PluginCommand.register_global("My Command (With Valid Function)", "My command description (not used)", my_plugin, is_valid) + True + + .. warning:: Calling ``register_global`` with the same function name will replace the existing function but will leak the memory of the original plugin. + """ + binaryninja._init_plugins() + action_obj = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(lambda ctxt: cls._global_action(action)) + is_valid_obj = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p)(lambda ctxt: cls._global_is_valid(is_valid)) + cls._registered_commands.append((action_obj, is_valid_obj)) + core.BNRegisterPluginCommandGlobal(name, description, action_obj, is_valid_obj, None) + + @classmethod def register( cls, name: str, description: str, action: Callable[['binaryview.BinaryView'], None], is_valid: Optional[Callable[['binaryview.BinaryView'], bool]] = None @@ -831,6 +882,17 @@ class PluginCommand(metaclass=_PluginCommandMetaClass): return result def is_valid(self, context: PluginCommandContext): + if self._command.type == PluginCommandType.ProjectPluginCommand: + if context.project is None: + return False + if not self._command.projectIsValid: + return True + return self._command.projectIsValid(self._command.context, context.project.handle) + elif self._command.type == PluginCommandType.GlobalPluginCommand: + if not self._command.globalIsValid: + return True + return self._command.globalIsValid(self._command.context) + if context.view is None: return False if self._command.type == PluginCommandType.DefaultPluginCommand: @@ -912,12 +974,6 @@ class PluginCommand(metaclass=_PluginCommandMetaClass): self._command.context, context.view.handle, context.instruction.function.handle, context.instruction.instr_index ) - elif self._command.type == PluginCommandType.ProjectPluginCommand: - if context.project is None: - return False - if not self._command.projectIsValid: - return True - return self._command.projectIsValid(self._command.context, context.project.handle) return False def execute(self, context: PluginCommandContext): @@ -933,7 +989,9 @@ class PluginCommand(metaclass=_PluginCommandMetaClass): """ if not self.is_valid(context): return - if self._command.type == PluginCommandType.DefaultPluginCommand: + if self._command.type == PluginCommandType.GlobalPluginCommand: + self._command.globalCommand(self._command.context) + elif self._command.type == PluginCommandType.DefaultPluginCommand: self._command.defaultCommand(self._command.context, context.view.handle) elif self._command.type == PluginCommandType.AddressPluginCommand: self._command.addressCommand(self._command.context, context.view.handle, context.address) |
