diff --git a/src/OneWare.Essentials/Debugger/Entities/DebugLaunchRequest.cs b/src/OneWare.Essentials/Debugger/Entities/DebugLaunchRequest.cs new file mode 100644 index 00000000..ff05b612 --- /dev/null +++ b/src/OneWare.Essentials/Debugger/Entities/DebugLaunchRequest.cs @@ -0,0 +1,20 @@ +namespace OneWare.Essentials.Debugger.Entities; + +/// +/// What the user asked to debug. A request with neither an executable nor a remote endpoint is +/// valid — the backend then comes up without a target, which is what makes its command line +/// usable for checking the installation or attaching by hand. +/// is the whole remote seam: a plugin that brings up a target +/// passes the address it is listening on and never learns which backend connects. +/// +/// Identifies the backend, e.g. GDB. +/// +/// Path to the executable, e.g. an ELF file. Carries the program and its debug symbols. +/// +/// Remote stub address, e.g. localhost:1234. +/// Working directory for the debug session. +public sealed record DebugLaunchRequest( + string AdapterId, + string? ExecutablePath = null, + string? RemoteEndpoint = null, + string? WorkingDirectory = null); diff --git a/src/OneWare.Essentials/Debugger/Entities/DebugSessionState.cs b/src/OneWare.Essentials/Debugger/Entities/DebugSessionState.cs new file mode 100644 index 00000000..2ceea45e --- /dev/null +++ b/src/OneWare.Essentials/Debugger/Entities/DebugSessionState.cs @@ -0,0 +1,40 @@ +namespace OneWare.Essentials.Debugger.Entities; + +/// +/// Everything the UI knows about the session and the target at one point in time. +/// A snapshot rather than a lifecycle enum plus a pile of separate events — the session +/// publishes a complete replacement on every change, so a panel binds one thing and cannot end +/// up showing registers from before the last step next to a frame from after it. +/// +public sealed record DebugSessionState +{ + /// + /// No session, or a session that has ended. Also the state a session starts out in. + /// + public static DebugSessionState Empty { get; } = new(); + + /// + /// while the target is executing — nothing can be inspected and only + /// pausing is meaningful. + /// + public bool IsRunning { get; init; } + + /// + /// Where the target is halted, or while it runs. + /// + public DebugStackFrame? CurrentFrame { get; init; } + + /// + /// Register contents as of the last halt. Empty while the target runs, and empty for a + /// backend that cannot read registers — the panel then simply shows nothing, which is what a + /// separate capability flag would have told it to do anyway. + /// + public IReadOnlyList Registers { get; init; } = []; + + /// + /// Locals of as of the last halt. Empty while the target runs, + /// and empty without debug symbols — a program linked without them has no names to report, + /// only registers. + /// + public IReadOnlyList Locals { get; init; } = []; +} diff --git a/src/OneWare.Essentials/Debugger/Entities/DebugStackFrame.cs b/src/OneWare.Essentials/Debugger/Entities/DebugStackFrame.cs new file mode 100644 index 00000000..b076e0c6 --- /dev/null +++ b/src/OneWare.Essentials/Debugger/Entities/DebugStackFrame.cs @@ -0,0 +1,20 @@ +namespace OneWare.Essentials.Debugger.Entities; + +/// +/// Where the target is halted. +/// +/// Name of the function, if the backend reported one. +/// +/// Absolute source path, or if the address could not be mapped. +/// The editor only jumps to the source location when this is set. +/// +/// One-based line number, or 0 if unknown. +/// +/// Program counter as formatted by the backend, e.g. 0x00000108. The only location +/// available when no debug symbols are present. +/// +public sealed record DebugStackFrame( + string? Function, + string? File, + int Line, + string? Address); diff --git a/src/OneWare.Essentials/Debugger/Entities/DebugVariable.cs b/src/OneWare.Essentials/Debugger/Entities/DebugVariable.cs new file mode 100644 index 00000000..bada7288 --- /dev/null +++ b/src/OneWare.Essentials/Debugger/Entities/DebugVariable.cs @@ -0,0 +1,12 @@ +namespace OneWare.Essentials.Debugger.Entities; + +/// +/// A local variable of the frame the target is halted in. +/// +/// As it appears in the source. +/// Formatted by the backend; the UI displays the string unchanged. +/// Declared type, or if the backend did not report one. +public sealed record DebugVariable( + string Name, + string Value, + string? TypeName); diff --git a/src/OneWare.Essentials/Debugger/Entities/RegisterValue.cs b/src/OneWare.Essentials/Debugger/Entities/RegisterValue.cs new file mode 100644 index 00000000..15288c7e --- /dev/null +++ b/src/OneWare.Essentials/Debugger/Entities/RegisterValue.cs @@ -0,0 +1,10 @@ +namespace OneWare.Essentials.Debugger.Entities; + +/// +/// A single register as read from the target. +/// +/// As reported by the target, e.g. sp or pc. +/// Formatted by the backend; the UI displays the string unchanged. +public sealed record RegisterValue( + string Name, + string Value); diff --git a/src/OneWare.Essentials/Debugger/Interfaces/IDebugAdapter.cs b/src/OneWare.Essentials/Debugger/Interfaces/IDebugAdapter.cs new file mode 100644 index 00000000..17892d44 --- /dev/null +++ b/src/OneWare.Essentials/Debugger/Interfaces/IDebugAdapter.cs @@ -0,0 +1,34 @@ +using OneWare.Essentials.Debugger.Entities; + +namespace OneWare.Essentials.Debugger.Interfaces; + +/// +/// More of a session factory than a real adapter. The name is borrowed from VS Code's DAP +/// (Debug Adapter Protocol), where "debug adapter" is the term for the backend itself. +/// is synchronous by intent, so that everything which can block or +/// fail happens in — one failure path instead of two. +/// +public interface IDebugAdapter +{ + /// + /// Stable identifier, referenced by . + /// + public string Id { get; } + + /// + /// Shown when the user picks a backend. + /// + public string DisplayName { get; } + + /// + /// Returns if this adapter can handle the given request. + /// Must be cheap and free of side effects — it decides whether to offer this adapter at all. + /// + public bool CanLaunch(DebugLaunchRequest launchRequest); + + /// + /// Only constructs the session object; launching happens inside + /// . + /// + public IDebugSession CreateSession(DebugLaunchRequest launchRequest); +} \ No newline at end of file diff --git a/src/OneWare.Essentials/Debugger/Interfaces/IDebugLaunchProvider.cs b/src/OneWare.Essentials/Debugger/Interfaces/IDebugLaunchProvider.cs new file mode 100644 index 00000000..7beb1871 --- /dev/null +++ b/src/OneWare.Essentials/Debugger/Interfaces/IDebugLaunchProvider.cs @@ -0,0 +1,36 @@ +using OneWare.Essentials.Debugger.Entities; + +namespace OneWare.Essentials.Debugger.Interfaces; + +/// +/// Analogous to , but as the preparation step. The core asks +/// which provider fits the current project, has it prepare, and starts with whatever request +/// comes back. That keeps the entry point in the generic UI while everything target-specific +/// stays in the plugin. +/// +public interface IDebugLaunchProvider +{ + /// + /// Shown in the launch selection of the debug panel. + /// + public string DisplayName { get; } + + /// + /// Returns if this provider can handle the active project. + /// Must be cheap and free of side effects — the UI calls it to fill the selection. + /// + public bool CanPrepare(); + + /// + /// Brings the target up and returns the matching launch request. + /// Returns if preparation failed or was cancelled; the user has + /// already been notified in that case. + /// + public Task PrepareAsync(CancellationToken ct = default); + + /// + /// Releases whatever claimed. Also runs when the session ended + /// on its own. + /// + public Task CleanupAsync(); +} diff --git a/src/OneWare.Essentials/Debugger/Interfaces/IDebugSession.cs b/src/OneWare.Essentials/Debugger/Interfaces/IDebugSession.cs new file mode 100644 index 00000000..f2efe466 --- /dev/null +++ b/src/OneWare.Essentials/Debugger/Interfaces/IDebugSession.cs @@ -0,0 +1,114 @@ +using OneWare.Essentials.Debugger.Entities; +using OneWare.Essentials.EditorExtensions; + +namespace OneWare.Essentials.Debugger.Interfaces; + +/// +/// Encapsulates one debug session and exposes to the UI. +/// Backend syntax does not cross this interface (no GDB/MI). +/// is the one deliberate exception — it backs the console's +/// command line. Control commands return no result: what the target did afterwards arrives +/// through , which is also how an unsolicited halt (e.g. a breakpoint +/// being hit) reaches the panels. +/// +public interface IDebugSession +{ + /// + /// Identifies the backend, e.g. GDB. + /// + public string AdapterId { get; } + + /// + /// Latest published state. + /// + public DebugSessionState State { get; } + + /// + /// Fired whenever is replaced. May arrive on any thread. + /// + public event EventHandler? StateChanged; + + /// + /// Output of the debugged program, and readable messages from the backend. + /// + public event EventHandler? OutputReceived; + + /// + /// Every command sent to the backend, so the console can echo it. + /// + public event EventHandler? CommandSent; + + /// + /// The backend process ended, whether asked to or not. + /// + public event EventHandler? Exited; + + /// + /// Brings the backend up and, for a remote request, attaches to the stub. + /// Returns if it did not come up and the session is unusable. + /// + public Task StartAsync(); + + /// + /// Starts the program. Separate from — an attached target is + /// already loaded and only needs resuming. + /// + public Task RunAsync(); + + /// + /// Resumes the halted target. + /// + public Task ContinueAsync(); + + /// + /// Halts the running target. + /// + public Task PauseAsync(); + + /// + /// Steps one source line, entering called functions. + /// + public Task StepIntoAsync(); + + /// + /// Steps one source line, stepping over called functions. + /// + public Task StepOverAsync(); + + /// + /// Runs until the current function returns. + /// + public Task StepOutAsync(); + + /// + /// Arms a breakpoint on the target. + /// Returns if the target refused it, e.g. because it ran out of + /// hardware breakpoints. + /// + public Task SetBreakpointAsync(BreakPoint breakpoint); + + /// + /// Removes a previously armed breakpoint. + /// + public Task RemoveBreakpointAsync(BreakPoint breakpoint); + + /// + /// Reads memory from the target. is whatever the backend accepts — + /// a literal such as 0x2001ff80, or an expression like &buffer when symbols + /// exist. Returns if the memory could not be read; a running target + /// cannot be read, so call only while halted. + /// + public Task ReadMemoryAsync(string address, int byteCount); + + /// + /// Sends a command verbatim to the backend. The response arrives through + /// , like any other backend output. + /// + public Task SendRawCommandAsync(string command); + + /// + /// Tears the backend down. Synchronous and best-effort — also runs on application shutdown, + /// where there is nothing left to await on. + /// + public void Stop(); +} \ No newline at end of file diff --git a/src/OneWare.Essentials/Debugger/Interfaces/IDebuggerService.cs b/src/OneWare.Essentials/Debugger/Interfaces/IDebuggerService.cs new file mode 100644 index 00000000..defc105b --- /dev/null +++ b/src/OneWare.Essentials/Debugger/Interfaces/IDebuggerService.cs @@ -0,0 +1,116 @@ +using OneWare.Essentials.Debugger.Entities; + +namespace OneWare.Essentials.Debugger.Interfaces; + +/// +/// The service a plugin resolves in order to take part in debugging. The dependency runs one +/// way only — plugins depend on these contracts, the core never learns that a given plugin +/// exists. +/// +public interface IDebuggerService +{ + /// + /// Registered backends, including the core's own. + /// + public IReadOnlyList Adapters { get; } + + /// + /// Registered launch providers. Whoever fits the active project shows up in the launch + /// selection of the debug panel. + /// + public IReadOnlyList LaunchProviders { get; } + + /// + /// The active session, or if none is running. + /// + public IDebugSession? CurrentSession { get; } + + /// + /// State of the active session, or if none is running. + /// + public DebugSessionState State { get; } + + /// + /// while a session is running. Gates the breakpoint margin in the + /// editor. + /// + public bool IsActive { get; } + + /// + /// Fired when , , or + /// changed. Always raised on the UI thread, so handlers can touch bound collections directly. + /// + public event EventHandler? StateChanged; + + /// + /// Registers an adapter. Resolved from the container — the implementation gets constructor + /// injection like any other service. + /// + public void RegisterAdapter() where T : IDebugAdapter; + + /// + /// Registers a launch provider. Resolved from the container like adapters. + /// + public void RegisterLaunchProvider() where T : IDebugLaunchProvider; + + /// + /// Starts a session, arms the breakpoints currently set in the editor and runs the program. + /// Returns if no adapter accepted the request or the backend did not + /// come up; nothing is left running in that case. + /// + public Task StartAsync(DebugLaunchRequest launchRequest); + + /// + /// Calls first, then starts with the + /// returned request. runs as soon as the + /// session ends, no matter how it ended. + /// + public Task StartAsync(IDebugLaunchProvider provider, CancellationToken ct = default); + + /// + /// Ends the active session. Does nothing if none is running. + /// + public Task StopAsync(); + + /// + /// Forwards to on . + /// Does nothing when no session is active. + /// + public Task ContinueAsync(); + + /// + /// Forwards to on . + /// Does nothing when no session is active. + /// + public Task PauseAsync(); + + /// + /// Forwards to on . + /// Does nothing when no session is active. + /// + public Task StepIntoAsync(); + + /// + /// Forwards to on . + /// Does nothing when no session is active. + /// + public Task StepOverAsync(); + + /// + /// Forwards to on . + /// Does nothing when no session is active. + /// + public Task StepOutAsync(); + + /// + /// Forwards to on . + /// Returns when no session is active. + /// + public Task ReadMemoryAsync(string address, int byteCount); + + /// + /// Forwards to on + /// . Does nothing when no session is active. + /// + public Task SendRawCommandAsync(string command); +}