Developer notes¶
This page covers the internals you need to know when writing sdb commands or embedding sdb in your own tools.
Writing a new command¶
Every command is a Python class that inherits from sdb.Command (or one
of its subclasses). The minimum requirements:
Set
namesto a non-empty list of strings.Set
load_onto control when the command is registered.Implement
_call(or the appropriate method for your base class).
Command names must follow C identifier rules: start with a letter or
underscore, followed by letters, digits, or underscores. Names starting
with special characters like :, %, or / are rejected.
When a module containing your class is imported, Command.__init_subclass__
automatically adds it to the global command set. After register_commands()
runs (which sdb.start() calls for you), the command is available in the
REPL with tab completion and help.
Docstring conventions¶
The class docstring becomes the command’s help text:
First line: one-line summary shown in the
helpoutput header.Remaining lines: detailed description and examples.
Format your docstring like the built-in commands:
class MyCommand(sdb.Command):
"""
One-line summary of what the command does
DESCRIPTION
Longer description of the command's behavior.
EXAMPLES
Show some typical invocations::
sdb> my_command -v | head 5
"""
The DESCRIPTION and EXAMPLES headings are not mandatory but they
make the help text consistent with the built-in commands.
Adding arguments¶
Override _init_parser to add argparse arguments:
@classmethod
def _init_parser(cls, name: str) -> argparse.ArgumentParser:
parser = super()._init_parser(name)
parser.add_argument("-n", "--limit", type=int, default=10)
return parser
Access them as self.args.limit in _call. If the user passes
invalid arguments, argparse raises a SystemExit which sdb catches and
converts to a CommandArgumentsError – the REPL stays alive.
The @InputHandler decorator¶
InputHandler is used on methods of Locator subclasses to register
type-specific handlers. When a Locator receives input, it dispatches to
the matching handler based on the input object’s C type.
class SpaLocator(sdb.Locator, sdb.PrettyPrinter):
names = ["spa"]
output_type = "spa_t *"
load_on = [sdb.Kernel()]
def no_input(self):
# Called when the command starts a pipeline
...
@sdb.InputHandler("vdev_t *")
def from_vdev(self, vdev):
"""Given a vdev, yield its parent spa."""
yield vdev.vdev_spa
Dispatch order when a Locator receives an object:
Check
@InputHandler-decorated methods for a type match.If the object already matches
output_type, pass it through.Try the
walkdispatch to iterate the object and cast each element tooutput_type.Raise
CommandErrorif nothing matches.
Hybrid Locator / PrettyPrinter¶
A class can inherit from both Locator and PrettyPrinter. The
behavior adapts based on position in the pipeline:
First: calls
no_input()to locate objects, then pretty-prints.Last: receives input, locates matching objects, then pretty-prints.
Middle: locates objects and passes them downstream (no printing).
The pre_cmd_hook mechanism¶
sdb.start() accepts an optional pre_cmd_hook callable. It is
invoked with no arguments immediately before each REPL command is evaluated.
sdb.start(prog, pre_cmd_hook=my_refresh_fn)
The hook is not called for meta-commands (%session,
%load-commands).
Use cases:
Cache invalidation: bump a generation counter so stale pages are re-fetched (this is what GhostWire does).
Transport reconnection: re-establish a connection if it dropped.
Telemetry: count commands, measure latency.
Command registration internals¶
The registration flow:
When Python imports a module containing a
Commandsubclass,__init_subclass__callsadd_command(cls)which adds the class to theall_commandsset.register_commands()iteratesall_commandsand checks each class’sload_onlist against the current runtime (kernel or userland). If appropriate, it callsregister_command(name, cls)for each name incls.names.register_commandadds the name→class mapping toregistered_commandsand also registers Walkers and PrettyPrinters in their respective global lookup tables.
This means:
Commands are discovered at import time (step 1).
Commands are activated at registration time (step 2).
load_external_commands()handles step 1 for files outside the sdb package. You must callregister_commands()afterward to activate them.sdb.start()calls bothload_external_commandsandregister_commandsfor you.
Type canonicalization¶
sdb normalises C types to handle typedefs, qualifiers, and pointer variations. The key helpers:
type_canonicalize(type)Strip all typedefs from a
drgn.Typeand return the underlying type.type_canonical_name(type)Return the canonical name string after stripping typedefs.
type_canonicalize_name(name)Like
type_canonical_namebut accepts a name string. Looks up the type via drgn first.type_equals(a, b)Compare two types after canonicalization.
These are used internally by the pipeline’s auto-coercion logic and by
Walkers and PrettyPrinters when matching input types. If you write a
command that needs to compare types, prefer these over raw == on
drgn.Type objects.
Error handling¶
sdb defines an error hierarchy rooted at sdb.Error. Commands should
raise these rather than printing error messages and returning:
CommandError(command_name, message)General runtime error. The REPL prints it and returns to the prompt.
SymbolNotFoundError(command_name, symbol)A symbol name could not be resolved.
CommandInvalidInputError(command_name, input_type, expected_type)Type mismatch on input.
The REPL catches sdb.Error and prints err.text without a traceback.
Unexpected exceptions (anything not derived from sdb.Error) trigger a
full traceback with a link to the issue tracker.
Testing commands¶
sdb uses pytest with a two-tier test strategy:
- Unit tests (
tests/unit/) Mock the drgn program and test command logic in isolation. These run without any crash dump.
- Integration tests (
tests/integration/) Run commands against real crash dumps and compare output to reference files. These require downloading test dumps first.
When contributing a new command, add at least a unit test that exercises the core logic. If the command depends on kernel data structures, add an integration test with expected output.
Run the test suite:
pip install -e ".[dev]"
pytest -v tests/unit