Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/OneWare.Essentials/Debugger/Entities/DebugLaunchRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace OneWare.Essentials.Debugger.Entities;

/// <summary>
/// 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.
/// <paramref name="RemoteEndpoint"/> 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.
/// </summary>
/// <param name="AdapterId">Identifies the backend, e.g. <c>GDB</c>.</param>
/// <param name="ExecutablePath">
/// Path to the executable, e.g. an ELF file. Carries the program and its debug symbols.
/// </param>
/// <param name="RemoteEndpoint">Remote stub address, e.g. <c>localhost:1234</c>.</param>
/// <param name="WorkingDirectory">Working directory for the debug session.</param>
public sealed record DebugLaunchRequest(
string AdapterId,
string? ExecutablePath = null,
string? RemoteEndpoint = null,
string? WorkingDirectory = null);
40 changes: 40 additions & 0 deletions src/OneWare.Essentials/Debugger/Entities/DebugSessionState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace OneWare.Essentials.Debugger.Entities;

/// <summary>
/// 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.
/// </summary>
public sealed record DebugSessionState
{
/// <summary>
/// No session, or a session that has ended. Also the state a session starts out in.
/// </summary>
public static DebugSessionState Empty { get; } = new();

/// <summary>
/// <see langword="true"/> while the target is executing — nothing can be inspected and only
/// pausing is meaningful.
/// </summary>
public bool IsRunning { get; init; }

/// <summary>
/// Where the target is halted, or <see langword="null"/> while it runs.
/// </summary>
public DebugStackFrame? CurrentFrame { get; init; }

/// <summary>
/// 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.
/// </summary>
public IReadOnlyList<RegisterValue> Registers { get; init; } = [];

/// <summary>
/// Locals of <see cref="CurrentFrame"/> 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.
/// </summary>
public IReadOnlyList<DebugVariable> Locals { get; init; } = [];
}
20 changes: 20 additions & 0 deletions src/OneWare.Essentials/Debugger/Entities/DebugStackFrame.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace OneWare.Essentials.Debugger.Entities;

/// <summary>
/// Where the target is halted.
/// </summary>
/// <param name="Function">Name of the function, if the backend reported one.</param>
/// <param name="File">
/// Absolute source path, or <see langword="null"/> if the address could not be mapped.
/// The editor only jumps to the source location when this is set.
/// </param>
/// <param name="Line">One-based line number, or <c>0</c> if unknown.</param>
/// <param name="Address">
/// Program counter as formatted by the backend, e.g. <c>0x00000108</c>. The only location
/// available when no debug symbols are present.
/// </param>
public sealed record DebugStackFrame(
string? Function,
string? File,
int Line,
string? Address);
12 changes: 12 additions & 0 deletions src/OneWare.Essentials/Debugger/Entities/DebugVariable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace OneWare.Essentials.Debugger.Entities;

/// <summary>
/// A local variable of the frame the target is halted in.
/// </summary>
/// <param name="Name">As it appears in the source.</param>
/// <param name="Value">Formatted by the backend; the UI displays the string unchanged.</param>
/// <param name="TypeName">Declared type, or <see langword="null"/> if the backend did not report one.</param>
public sealed record DebugVariable(
string Name,
string Value,
string? TypeName);
10 changes: 10 additions & 0 deletions src/OneWare.Essentials/Debugger/Entities/RegisterValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace OneWare.Essentials.Debugger.Entities;

/// <summary>
/// A single register as read from the target.
/// </summary>
/// <param name="Name">As reported by the target, e.g. <c>sp</c> or <c>pc</c>.</param>
/// <param name="Value">Formatted by the backend; the UI displays the string unchanged.</param>
public sealed record RegisterValue(
string Name,
string Value);
34 changes: 34 additions & 0 deletions src/OneWare.Essentials/Debugger/Interfaces/IDebugAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using OneWare.Essentials.Debugger.Entities;

namespace OneWare.Essentials.Debugger.Interfaces;

/// <summary>
/// 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.
/// <see cref="CreateSession"/> is synchronous by intent, so that everything which can block or
/// fail happens in <see cref="IDebugSession.StartAsync"/> — one failure path instead of two.
/// </summary>
public interface IDebugAdapter
{
/// <summary>
/// Stable identifier, referenced by <see cref="DebugLaunchRequest.AdapterId"/>.
/// </summary>
public string Id { get; }

/// <summary>
/// Shown when the user picks a backend.
/// </summary>
public string DisplayName { get; }

/// <summary>
/// Returns <see langword="true"/> 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.
/// </summary>
public bool CanLaunch(DebugLaunchRequest launchRequest);

/// <summary>
/// Only constructs the session object; launching happens inside
/// <see cref="IDebugSession.StartAsync"/>.
/// </summary>
public IDebugSession CreateSession(DebugLaunchRequest launchRequest);
}
36 changes: 36 additions & 0 deletions src/OneWare.Essentials/Debugger/Interfaces/IDebugLaunchProvider.cs
Comment thread
hendrikmennen marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using OneWare.Essentials.Debugger.Entities;

namespace OneWare.Essentials.Debugger.Interfaces;

/// <summary>
/// Analogous to <see cref="DebugLaunchRequest"/>, 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.
/// </summary>
public interface IDebugLaunchProvider
{
/// <summary>
/// Shown in the launch selection of the debug panel.
/// </summary>
public string DisplayName { get; }

/// <summary>
/// Returns <see langword="true"/> if this provider can handle the active project.
/// Must be cheap and free of side effects — the UI calls it to fill the selection.
/// </summary>
public bool CanPrepare();

/// <summary>
/// Brings the target up and returns the matching launch request.
/// Returns <see langword="null"/> if preparation failed or was cancelled; the user has
/// already been notified in that case.
/// </summary>
public Task<DebugLaunchRequest?> PrepareAsync(CancellationToken ct = default);

/// <summary>
/// Releases whatever <see cref="PrepareAsync"/> claimed. Also runs when the session ended
/// on its own.
/// </summary>
public Task CleanupAsync();
}
114 changes: 114 additions & 0 deletions src/OneWare.Essentials/Debugger/Interfaces/IDebugSession.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using OneWare.Essentials.Debugger.Entities;
using OneWare.Essentials.EditorExtensions;

namespace OneWare.Essentials.Debugger.Interfaces;

/// <summary>
/// Encapsulates one debug session and exposes <see cref="DebugSessionState"/> to the UI.
/// Backend syntax does not cross this interface (no GDB/MI).
/// <see cref="SendRawCommandAsync"/> is the one deliberate exception — it backs the console's
/// command line. Control commands return no result: what the target did afterwards arrives
/// through <see cref="StateChanged"/>, which is also how an unsolicited halt (e.g. a breakpoint
/// being hit) reaches the panels.
/// </summary>
public interface IDebugSession
{
/// <summary>
/// Identifies the backend, e.g. <c>GDB</c>.
/// </summary>
public string AdapterId { get; }

/// <summary>
/// Latest published state.
/// </summary>
public DebugSessionState State { get; }

/// <summary>
/// Fired whenever <see cref="State"/> is replaced. May arrive on any thread.
/// </summary>
public event EventHandler<DebugSessionState>? StateChanged;

/// <summary>
/// Output of the debugged program, and readable messages from the backend.
/// </summary>
public event EventHandler<string>? OutputReceived;

/// <summary>
/// Every command sent to the backend, so the console can echo it.
/// </summary>
public event EventHandler<string>? CommandSent;

/// <summary>
/// The backend process ended, whether asked to or not.
/// </summary>
public event EventHandler? Exited;

/// <summary>
/// Brings the backend up and, for a remote request, attaches to the stub.
/// Returns <see langword="false"/> if it did not come up and the session is unusable.
/// </summary>
public Task<bool> StartAsync();

/// <summary>
/// Starts the program. Separate from <see cref="ContinueAsync"/> — an attached target is
/// already loaded and only needs resuming.
/// </summary>
public Task RunAsync();

/// <summary>
/// Resumes the halted target.
/// </summary>
public Task ContinueAsync();

/// <summary>
/// Halts the running target.
/// </summary>
public Task PauseAsync();

/// <summary>
/// Steps one source line, entering called functions.
/// </summary>
public Task StepIntoAsync();

/// <summary>
/// Steps one source line, stepping over called functions.
/// </summary>
public Task StepOverAsync();

/// <summary>
/// Runs until the current function returns.
/// </summary>
public Task StepOutAsync();

/// <summary>
/// Arms a breakpoint on the target.
/// Returns <see langword="false"/> if the target refused it, e.g. because it ran out of
/// hardware breakpoints.
/// </summary>
public Task<bool> SetBreakpointAsync(BreakPoint breakpoint);

/// <summary>
/// Removes a previously armed breakpoint.
/// </summary>
public Task<bool> RemoveBreakpointAsync(BreakPoint breakpoint);

/// <summary>
/// Reads memory from the target. <paramref name="address"/> is whatever the backend accepts —
/// a literal such as <c>0x2001ff80</c>, or an expression like <c>&amp;buffer</c> when symbols
/// exist. Returns <see langword="null"/> if the memory could not be read; a running target
/// cannot be read, so call only while halted.
/// </summary>
public Task<string?> ReadMemoryAsync(string address, int byteCount);

/// <summary>
/// Sends a command verbatim to the backend. The response arrives through
/// <see cref="OutputReceived"/>, like any other backend output.
/// </summary>
public Task SendRawCommandAsync(string command);

/// <summary>
/// Tears the backend down. Synchronous and best-effort — also runs on application shutdown,
/// where there is nothing left to await on.
/// </summary>
public void Stop();
}
Loading