pyGdbToolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyGdbToolkit/__init__.py +55 -0
- pyGdbToolkit/arch/__init__.py +79 -0
- pyGdbToolkit/arch/arm/__init__.py +17 -0
- pyGdbToolkit/arch/arm/coresight.py +427 -0
- pyGdbToolkit/arch/arm/cortex_m.py +504 -0
- pyGdbToolkit/arch/arm/fault.py +629 -0
- pyGdbToolkit/arch/arm/models.py +60 -0
- pyGdbToolkit/arch/arm/mpu.py +556 -0
- pyGdbToolkit/arch/arm/probe.py +10 -0
- pyGdbToolkit/arch/arm/providers/__init__.py +16 -0
- pyGdbToolkit/arch/arm/providers/base.py +77 -0
- pyGdbToolkit/arch/arm/providers/manufacturers/__init__.py +4 -0
- pyGdbToolkit/arch/arm/providers/manufacturers/stm32.py +680 -0
- pyGdbToolkit/arch/arm/sau.py +253 -0
- pyGdbToolkit/arch/arm/security.py +816 -0
- pyGdbToolkit/arch/arm/session_state.py +96 -0
- pyGdbToolkit/arch/arm/target.py +32 -0
- pyGdbToolkit/arch/base.py +143 -0
- pyGdbToolkit/arch/diagnostics.py +300 -0
- pyGdbToolkit/arch/registry.py +43 -0
- pyGdbToolkit/cmd_faultinfo.py +99 -0
- pyGdbToolkit/cmd_lscpu.py +116 -0
- pyGdbToolkit/cmd_profile.py +1402 -0
- pyGdbToolkit/cmd_rtos.py +298 -0
- pyGdbToolkit/cmd_secscan.py +466 -0
- pyGdbToolkit/cmd_svd.py +1208 -0
- pyGdbToolkit/diagnostic_runtime.py +52 -0
- pyGdbToolkit/rtos/__init__.py +8 -0
- pyGdbToolkit/rtos/camelot.py +513 -0
- pyGdbToolkit/session.py +233 -0
- pyGdbToolkit/svd.py +1194 -0
- pyGdbToolkit/target_memory.py +190 -0
- pygdbtoolkit-0.1.0.dist-info/METADATA +586 -0
- pygdbtoolkit-0.1.0.dist-info/RECORD +36 -0
- pygdbtoolkit-0.1.0.dist-info/WHEEL +5 -0
- pygdbtoolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Session-cached Arm target inspection shared by the Arm-aware GDB commands.
|
|
6
|
+
|
|
7
|
+
This module is not exported by :mod:`pyGdbToolkit.arch.arm` on purpose: it binds
|
|
8
|
+
the Arm inspection results to the unified session, whereas the ``arch`` package
|
|
9
|
+
stays independent from any session or GDB concern.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from ...session import SESSION, SessionSlice, ToolkitSession
|
|
17
|
+
from .coresight import CoreSightDiscovery, discover_rom_tables
|
|
18
|
+
from .cortex_m import CPUID_ADDRESS, CortexMTargetDescription, decode_cpuid
|
|
19
|
+
from .models import DeviceReport
|
|
20
|
+
from .providers import DEFAULT_PROVIDER_REGISTRY
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ArmInspectionState(SessionSlice):
|
|
25
|
+
"""Cache the Arm identity, the ROM-table discovery, and the vendor device report."""
|
|
26
|
+
|
|
27
|
+
target: CortexMTargetDescription | None = None
|
|
28
|
+
discovery: CoreSightDiscovery | None = None
|
|
29
|
+
report: DeviceReport | None = None
|
|
30
|
+
|
|
31
|
+
def reset(self) -> None:
|
|
32
|
+
"""Drop every cached Arm inspection result."""
|
|
33
|
+
self.target = None
|
|
34
|
+
self.discovery = None
|
|
35
|
+
self.report = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cortex_m_target(session: ToolkitSession = SESSION) -> CortexMTargetDescription:
|
|
39
|
+
"""Return the decoded Cortex-M CPUID identity of the session target.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
session : ToolkitSession
|
|
44
|
+
Session owning the target access and the cached inspection results.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
CortexMTargetDescription
|
|
49
|
+
The decoded CPUID identity.
|
|
50
|
+
"""
|
|
51
|
+
state = session.state(ArmInspectionState)
|
|
52
|
+
if state.target is None:
|
|
53
|
+
state.target = decode_cpuid(session.memory.read_uint32(CPUID_ADDRESS))
|
|
54
|
+
return state.target
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def rom_table_discovery(session: ToolkitSession = SESSION) -> CoreSightDiscovery:
|
|
58
|
+
"""Return the CoreSight ROM-table discovery of the session target.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
session : ToolkitSession
|
|
63
|
+
Session owning the target access and the cached inspection results.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
CoreSightDiscovery
|
|
68
|
+
The MCU and processor ROM-table discovery results.
|
|
69
|
+
"""
|
|
70
|
+
state = session.state(ArmInspectionState)
|
|
71
|
+
if state.discovery is None:
|
|
72
|
+
state.discovery = discover_rom_tables(session.memory)
|
|
73
|
+
return state.discovery
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def device_report(session: ToolkitSession = SESSION) -> DeviceReport:
|
|
77
|
+
"""Return the manufacturer device report of the session target.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
session : ToolkitSession
|
|
82
|
+
Session owning the target access and the cached inspection results.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
DeviceReport
|
|
87
|
+
The report of the first provider recognizing the target, or a generic one.
|
|
88
|
+
"""
|
|
89
|
+
state = session.state(ArmInspectionState)
|
|
90
|
+
if state.report is None:
|
|
91
|
+
state.report = DEFAULT_PROVIDER_REGISTRY.inspect(
|
|
92
|
+
session.memory,
|
|
93
|
+
cortex_m_target(session),
|
|
94
|
+
rom_table_discovery(session),
|
|
95
|
+
)
|
|
96
|
+
return state.report
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Shared Arm target-description types."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import StrEnum, unique
|
|
10
|
+
|
|
11
|
+
from ..base import Architecture, TargetDescription
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@unique
|
|
15
|
+
class ArmProfile(StrEnum):
|
|
16
|
+
"""Architectural execution profiles implemented by Arm cores."""
|
|
17
|
+
|
|
18
|
+
CORTEX_M = "cortex-m"
|
|
19
|
+
CORTEX_A = "cortex-a"
|
|
20
|
+
CORTEX_R = "cortex-r"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ArmTargetDescription(TargetDescription):
|
|
25
|
+
"""Target identity common to all Arm architecture profiles."""
|
|
26
|
+
|
|
27
|
+
profile: ArmProfile
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
"""Require Arm target descriptions to retain the Arm architecture tag."""
|
|
31
|
+
if self.architecture is not Architecture.ARM:
|
|
32
|
+
raise ValueError("an Arm target description must use the Arm architecture")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Portable architecture target-description and register-reading contracts."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import StrEnum, unique
|
|
10
|
+
from typing import Protocol
|
|
11
|
+
|
|
12
|
+
from ..target_memory import TargetMemory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@unique
|
|
16
|
+
class Architecture(StrEnum):
|
|
17
|
+
"""Architectures supported by the target-description registry."""
|
|
18
|
+
|
|
19
|
+
ARM = "arm"
|
|
20
|
+
RISCV = "riscv"
|
|
21
|
+
XTENSA = "xtensa"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class TargetDescription:
|
|
26
|
+
"""Architecture-neutral identity for an inspected target."""
|
|
27
|
+
|
|
28
|
+
architecture: Architecture
|
|
29
|
+
family: str
|
|
30
|
+
core_name: str
|
|
31
|
+
revision: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class RegisterValue:
|
|
36
|
+
"""A system-register value or the explicit reason it could not be read."""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
address: int
|
|
40
|
+
width_bits: int
|
|
41
|
+
value: int | None = None
|
|
42
|
+
unavailable_reason: str | None = None
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
"""Validate that a register result has exactly one value state."""
|
|
46
|
+
if (self.value is None) == (self.unavailable_reason is None):
|
|
47
|
+
raise ValueError("a register value must be available or unavailable")
|
|
48
|
+
if self.address < 0:
|
|
49
|
+
raise ValueError("a register address must not be negative")
|
|
50
|
+
if self.width_bits <= 0:
|
|
51
|
+
raise ValueError("a register width must be positive")
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def known(cls, name: str, address: int, width_bits: int, value: int) -> RegisterValue:
|
|
55
|
+
"""Create an available register value."""
|
|
56
|
+
return cls(name=name, address=address, width_bits=width_bits, value=value)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def unavailable(
|
|
60
|
+
cls,
|
|
61
|
+
name: str,
|
|
62
|
+
address: int,
|
|
63
|
+
width_bits: int,
|
|
64
|
+
reason: str,
|
|
65
|
+
) -> RegisterValue:
|
|
66
|
+
"""Create an unavailable register result."""
|
|
67
|
+
return cls(
|
|
68
|
+
name=name,
|
|
69
|
+
address=address,
|
|
70
|
+
width_bits=width_bits,
|
|
71
|
+
unavailable_reason=reason,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_available(self) -> bool:
|
|
76
|
+
"""Whether the target supplied a register value."""
|
|
77
|
+
return self.value is not None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class SystemRegisterSet:
|
|
82
|
+
"""One architecture-specific system-register block read through a common API."""
|
|
83
|
+
|
|
84
|
+
architecture: Architecture
|
|
85
|
+
block_name: str
|
|
86
|
+
registers: tuple[RegisterValue, ...]
|
|
87
|
+
|
|
88
|
+
def get(self, name: str) -> RegisterValue | None:
|
|
89
|
+
"""Find a register by case-insensitive name."""
|
|
90
|
+
normalized_name = name.upper()
|
|
91
|
+
for register in self.registers:
|
|
92
|
+
if register.name.upper() == normalized_name:
|
|
93
|
+
return register
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class ProbeResult:
|
|
99
|
+
"""A probe result containing a target description or explicit unavailability."""
|
|
100
|
+
|
|
101
|
+
target: TargetDescription | None = None
|
|
102
|
+
unavailable_reason: str | None = None
|
|
103
|
+
access_error: bool = False
|
|
104
|
+
|
|
105
|
+
def __post_init__(self) -> None:
|
|
106
|
+
"""Validate that a probe result has exactly one state."""
|
|
107
|
+
if (self.target is None) == (self.unavailable_reason is None):
|
|
108
|
+
raise ValueError("a probe result must contain a target or an unavailable reason")
|
|
109
|
+
if self.target is not None and self.access_error:
|
|
110
|
+
raise ValueError("a successful probe result cannot contain an access error")
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def detected(cls, target: TargetDescription) -> ProbeResult:
|
|
114
|
+
"""Create a successful probe result."""
|
|
115
|
+
return cls(target=target)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def unavailable(cls, reason: str, *, access_error: bool = False) -> ProbeResult:
|
|
119
|
+
"""Create an unsuccessful probe result."""
|
|
120
|
+
return cls(unavailable_reason=reason, access_error=access_error)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def is_available(self) -> bool:
|
|
124
|
+
"""Whether an architecture probe identified a target."""
|
|
125
|
+
return self.target is not None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ArchitectureProbe(Protocol):
|
|
129
|
+
"""An architecture implementation that can identify and inspect a target."""
|
|
130
|
+
|
|
131
|
+
architecture: Architecture
|
|
132
|
+
|
|
133
|
+
def probe(self, reader: TargetMemory) -> ProbeResult:
|
|
134
|
+
"""Identify a target or return an explicit unsupported result."""
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
def read_system_registers(
|
|
138
|
+
self,
|
|
139
|
+
reader: TargetMemory,
|
|
140
|
+
target: TargetDescription,
|
|
141
|
+
) -> SystemRegisterSet:
|
|
142
|
+
"""Read the architecture-specific system-register block for a target."""
|
|
143
|
+
...
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Portable diagnostic-service contracts, reports, and runtime dispatch."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import StrEnum, auto, unique
|
|
10
|
+
from typing import Protocol, TypeAlias
|
|
11
|
+
|
|
12
|
+
from ..target_memory import TargetMemory
|
|
13
|
+
from .base import Architecture, TargetDescription
|
|
14
|
+
from .registry import ArchitectureRegistry
|
|
15
|
+
|
|
16
|
+
DiagnosticValue: TypeAlias = bool | int | str | None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@unique
|
|
20
|
+
class DiagnosticServiceName(StrEnum):
|
|
21
|
+
"""Architecture-neutral diagnostics offered by command entry points."""
|
|
22
|
+
|
|
23
|
+
SECURITY_AUDIT = "security-audit"
|
|
24
|
+
FAULT_ANALYSIS = "fault-analysis"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@unique
|
|
28
|
+
class DiagnosticSeverity(StrEnum):
|
|
29
|
+
"""Common severity levels used by diagnostic findings."""
|
|
30
|
+
|
|
31
|
+
PASS = auto()
|
|
32
|
+
INFO = auto()
|
|
33
|
+
WARNING = auto()
|
|
34
|
+
ERROR = auto()
|
|
35
|
+
CRITICAL = auto()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class DiagnosticField:
|
|
40
|
+
"""One named, scalar diagnostic value suitable for a renderer."""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
value: DiagnosticValue
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class DiagnosticTableRow:
|
|
48
|
+
"""One renderer-independent row in a diagnostic table."""
|
|
49
|
+
|
|
50
|
+
values: tuple[DiagnosticValue, ...]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class DiagnosticTable:
|
|
55
|
+
"""One renderer-independent table in a diagnostic report."""
|
|
56
|
+
|
|
57
|
+
title: str
|
|
58
|
+
columns: tuple[str, ...]
|
|
59
|
+
rows: tuple[DiagnosticTableRow, ...]
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
"""Ensure every row matches the declared table column count."""
|
|
63
|
+
if not self.columns:
|
|
64
|
+
raise ValueError("a diagnostic table must have at least one column")
|
|
65
|
+
if any(len(row.values) != len(self.columns) for row in self.rows):
|
|
66
|
+
raise ValueError("diagnostic table rows must match the column count")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class DiagnosticPanel:
|
|
71
|
+
"""One renderer-independent, ordered group of diagnostic lines."""
|
|
72
|
+
|
|
73
|
+
title: str
|
|
74
|
+
lines: tuple[str, ...]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
DiagnosticContent: TypeAlias = DiagnosticTable | DiagnosticPanel
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class DiagnosticFinding:
|
|
82
|
+
"""One portable diagnostic observation."""
|
|
83
|
+
|
|
84
|
+
category: str
|
|
85
|
+
severity: DiagnosticSeverity
|
|
86
|
+
title: str
|
|
87
|
+
detail: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class DiagnosticSection:
|
|
92
|
+
"""One renderer-independent grouping of diagnostic values."""
|
|
93
|
+
|
|
94
|
+
title: str
|
|
95
|
+
fields: tuple[DiagnosticField, ...]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class DiagnosticReport:
|
|
100
|
+
"""A complete architecture-specific diagnostic report."""
|
|
101
|
+
|
|
102
|
+
service: DiagnosticServiceName
|
|
103
|
+
target: TargetDescription
|
|
104
|
+
sections: tuple[DiagnosticSection, ...] = ()
|
|
105
|
+
findings: tuple[DiagnosticFinding, ...] = ()
|
|
106
|
+
tables: tuple[DiagnosticTable, ...] = ()
|
|
107
|
+
panels: tuple[DiagnosticPanel, ...] = ()
|
|
108
|
+
blocks: tuple[DiagnosticContent, ...] = ()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class DiagnosticResult:
|
|
113
|
+
"""A completed report or a reason the requested diagnostic is unavailable."""
|
|
114
|
+
|
|
115
|
+
report: DiagnosticReport | None = None
|
|
116
|
+
target: TargetDescription | None = None
|
|
117
|
+
unavailable_reason: str | None = None
|
|
118
|
+
access_error: bool = False
|
|
119
|
+
|
|
120
|
+
def __post_init__(self) -> None:
|
|
121
|
+
"""Require exactly one completed or unavailable result state."""
|
|
122
|
+
if (self.report is None) == (self.unavailable_reason is None):
|
|
123
|
+
raise ValueError("a diagnostic result must contain a report or an unavailable reason")
|
|
124
|
+
if self.report is not None and self.target != self.report.target:
|
|
125
|
+
raise ValueError("a completed diagnostic result must retain its report target")
|
|
126
|
+
if self.report is not None and self.access_error:
|
|
127
|
+
raise ValueError("a completed diagnostic result cannot contain an access error")
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def completed(cls, report: DiagnosticReport) -> DiagnosticResult:
|
|
131
|
+
"""Create a completed diagnostic result."""
|
|
132
|
+
return cls(report=report, target=report.target)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def unavailable(
|
|
136
|
+
cls,
|
|
137
|
+
reason: str,
|
|
138
|
+
target: TargetDescription | None = None,
|
|
139
|
+
*,
|
|
140
|
+
access_error: bool = False,
|
|
141
|
+
) -> DiagnosticResult:
|
|
142
|
+
"""Create an explicitly unavailable diagnostic result."""
|
|
143
|
+
return cls(target=target, unavailable_reason=reason, access_error=access_error)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def is_available(self) -> bool:
|
|
147
|
+
"""Whether the requested diagnostic completed."""
|
|
148
|
+
return self.report is not None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class DiagnosticService(Protocol):
|
|
152
|
+
"""An architecture-specific collector behind a portable diagnostic service."""
|
|
153
|
+
|
|
154
|
+
architecture: Architecture
|
|
155
|
+
service: DiagnosticServiceName
|
|
156
|
+
|
|
157
|
+
def supports(self, target: TargetDescription) -> bool:
|
|
158
|
+
"""Return whether the service can collect this target description."""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def collect(
|
|
162
|
+
self,
|
|
163
|
+
reader: TargetMemory,
|
|
164
|
+
target: TargetDescription,
|
|
165
|
+
access: DiagnosticRuntimeAccess | None = None,
|
|
166
|
+
) -> DiagnosticReport:
|
|
167
|
+
"""Collect one report for a compatible target."""
|
|
168
|
+
...
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class DiagnosticRegisterReader(Protocol):
|
|
172
|
+
"""Read a runtime register from any of a service-provided set of names."""
|
|
173
|
+
|
|
174
|
+
def read_first(self, names: tuple[str, ...]) -> int | None:
|
|
175
|
+
"""Return the first available named register, or ``None``."""
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class DiagnosticSymbolResolver(Protocol):
|
|
180
|
+
"""Resolve a runtime address for architecture-neutral report renderers."""
|
|
181
|
+
|
|
182
|
+
def resolve(self, address: int) -> str:
|
|
183
|
+
"""Return a symbol description or ``"?"`` when no symbol is available."""
|
|
184
|
+
...
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass(frozen=True)
|
|
188
|
+
class DiagnosticRuntimeAccess:
|
|
189
|
+
"""Optional neutral runtime facilities needed by a diagnostic collector."""
|
|
190
|
+
|
|
191
|
+
registers: DiagnosticRegisterReader | None = None
|
|
192
|
+
symbols: DiagnosticSymbolResolver | None = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class DiagnosticRuntime(Protocol):
|
|
196
|
+
"""Portable runtime entry point used by architecture-neutral commands."""
|
|
197
|
+
|
|
198
|
+
def diagnose(
|
|
199
|
+
self,
|
|
200
|
+
reader: TargetMemory,
|
|
201
|
+
service: DiagnosticServiceName,
|
|
202
|
+
access: DiagnosticRuntimeAccess | None = None,
|
|
203
|
+
) -> DiagnosticResult:
|
|
204
|
+
"""Identify the target and run one requested diagnostic service."""
|
|
205
|
+
...
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class DiagnosticServiceRegistry:
|
|
209
|
+
"""Resolve diagnostic collectors by requested service and target architecture."""
|
|
210
|
+
|
|
211
|
+
def __init__(self, services: tuple[DiagnosticService, ...] = ()) -> None:
|
|
212
|
+
"""Create a registry and register its initial service implementations."""
|
|
213
|
+
self._services: dict[tuple[Architecture, DiagnosticServiceName], DiagnosticService] = {}
|
|
214
|
+
for service in services:
|
|
215
|
+
self.register(service)
|
|
216
|
+
|
|
217
|
+
def register(self, service: DiagnosticService) -> None:
|
|
218
|
+
"""Register one service implementation.
|
|
219
|
+
|
|
220
|
+
Raises
|
|
221
|
+
------
|
|
222
|
+
ValueError
|
|
223
|
+
If a service is already registered for the same architecture and name.
|
|
224
|
+
"""
|
|
225
|
+
key = (service.architecture, service.service)
|
|
226
|
+
if key in self._services:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f"diagnostic service '{service.service}' is already registered "
|
|
229
|
+
f"for architecture '{service.architecture}'"
|
|
230
|
+
)
|
|
231
|
+
self._services[key] = service
|
|
232
|
+
|
|
233
|
+
def resolve(
|
|
234
|
+
self,
|
|
235
|
+
service: DiagnosticServiceName,
|
|
236
|
+
target: TargetDescription,
|
|
237
|
+
) -> DiagnosticService | None:
|
|
238
|
+
"""Return the registered service implementation compatible with a target."""
|
|
239
|
+
implementation = self._services.get((target.architecture, service))
|
|
240
|
+
if implementation is None or not implementation.supports(target):
|
|
241
|
+
return None
|
|
242
|
+
return implementation
|
|
243
|
+
|
|
244
|
+
def dispatch(
|
|
245
|
+
self,
|
|
246
|
+
reader: TargetMemory,
|
|
247
|
+
service: DiagnosticServiceName,
|
|
248
|
+
target: TargetDescription,
|
|
249
|
+
access: DiagnosticRuntimeAccess | None = None,
|
|
250
|
+
) -> DiagnosticResult:
|
|
251
|
+
"""Run a compatible service or return an explicit unsupported result."""
|
|
252
|
+
implementation = self._services.get((target.architecture, service))
|
|
253
|
+
if implementation is None:
|
|
254
|
+
return DiagnosticResult.unavailable(
|
|
255
|
+
(
|
|
256
|
+
f"diagnostic service '{service}' is not registered "
|
|
257
|
+
f"for architecture '{target.architecture}'"
|
|
258
|
+
),
|
|
259
|
+
target,
|
|
260
|
+
)
|
|
261
|
+
if not implementation.supports(target):
|
|
262
|
+
return DiagnosticResult.unavailable(
|
|
263
|
+
f"diagnostic service '{service}' does not support target '{target.core_name}'",
|
|
264
|
+
target,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
report = implementation.collect(reader, target, access)
|
|
268
|
+
if report.service is not service:
|
|
269
|
+
raise ValueError("diagnostic service returned a report for a different service")
|
|
270
|
+
if report.target != target:
|
|
271
|
+
raise ValueError("diagnostic service returned a report for a different target")
|
|
272
|
+
return DiagnosticResult.completed(report)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class ArchitectureDiagnosticRuntime:
|
|
276
|
+
"""Adapt architecture probing and service dispatch to the diagnostic runtime protocol."""
|
|
277
|
+
|
|
278
|
+
def __init__(
|
|
279
|
+
self,
|
|
280
|
+
architecture_registry: ArchitectureRegistry,
|
|
281
|
+
service_registry: DiagnosticServiceRegistry,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Create a runtime using the supplied architecture and service registries."""
|
|
284
|
+
self._architecture_registry = architecture_registry
|
|
285
|
+
self._service_registry = service_registry
|
|
286
|
+
|
|
287
|
+
def diagnose(
|
|
288
|
+
self,
|
|
289
|
+
reader: TargetMemory,
|
|
290
|
+
service: DiagnosticServiceName,
|
|
291
|
+
access: DiagnosticRuntimeAccess | None = None,
|
|
292
|
+
) -> DiagnosticResult:
|
|
293
|
+
"""Identify the target, then dispatch its implementation of a diagnostic."""
|
|
294
|
+
probe_result = self._architecture_registry.probe(reader)
|
|
295
|
+
if probe_result.target is None:
|
|
296
|
+
return DiagnosticResult.unavailable(
|
|
297
|
+
probe_result.unavailable_reason or "target unavailable",
|
|
298
|
+
access_error=probe_result.access_error,
|
|
299
|
+
)
|
|
300
|
+
return self._service_registry.dispatch(reader, service, probe_result.target, access)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Architecture probe registry."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
from ..target_memory import TargetMemory
|
|
11
|
+
from .base import ArchitectureProbe, ProbeResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ArchitectureRegistry:
|
|
15
|
+
"""Run registered architecture probes in their declared priority order."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, probes: Sequence[ArchitectureProbe]) -> None:
|
|
18
|
+
"""Create a registry from an immutable probe sequence."""
|
|
19
|
+
self._probes = tuple(probes)
|
|
20
|
+
|
|
21
|
+
def probe(self, reader: TargetMemory) -> ProbeResult:
|
|
22
|
+
"""Return the first successful architecture probe result.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
reader
|
|
27
|
+
Target-memory reader used by each architecture probe.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
ProbeResult
|
|
32
|
+
The first detected target or an explicit unsupported result.
|
|
33
|
+
"""
|
|
34
|
+
access_error: ProbeResult | None = None
|
|
35
|
+
for probe in self._probes:
|
|
36
|
+
result = probe.probe(reader)
|
|
37
|
+
if result.is_available:
|
|
38
|
+
return result
|
|
39
|
+
if result.access_error and access_error is None:
|
|
40
|
+
access_error = result
|
|
41
|
+
if access_error is not None:
|
|
42
|
+
return access_error
|
|
43
|
+
return ProbeResult.unavailable("no registered architecture probe recognized the target")
|