2025-11-28 18:06:42 -06:00
using Microsoft.Iris.Debug ;
2025-11-29 01:32:09 -06:00
using Microsoft.Iris.Debug.Symbols ;
2025-11-28 18:06:42 -06:00
using Microsoft.Iris.Debug.SystemNet ;
2025-11-30 13:37:33 -06:00
using Microsoft.Iris.DebugAdapter.Client ;
2025-11-28 18:06:42 -06:00
using Spectre.Console ;
using Spectre.Console.Cli ;
using System.ComponentModel ;
using System.Globalization ;
namespace UIXC.Commands ;
2025-12-01 00:45:01 -06:00
public class DebugCommand : AsyncCommand < DebugCommand . Settings >
2025-11-28 18:06:42 -06:00
{
2025-12-01 00:45:01 -06:00
private readonly TaskCompletionSource _exit = new ();
2025-11-28 18:06:42 -06:00
2025-12-01 00:45:01 -06:00
public override async Task < int > ExecuteAsync ( CommandContext context , Settings settings )
2025-11-28 18:06:42 -06:00
{
2025-11-30 13:37:33 -06:00
if ( settings . ConnectionString is null )
2025-11-28 18:06:42 -06:00
{
2025-11-30 13:37:33 -06:00
AnsiConsole . MarkupLine ( "[red]Missing argument, a connection string must be specified.[/]" );
2025-11-28 18:06:42 -06:00
return - 1 ;
}
2025-11-30 13:37:33 -06:00
IDebuggerClient c ;
if ( Uri . TryCreate ( settings . ConnectionString , UriKind . Absolute , out var connectionUri ))
{
if ( connectionUri . Scheme == "tcp" )
{
c = new NetDebuggerClient ( connectionUri );
}
else
{
throw new NotSupportedException ();
}
}
else
{
c = new IrisDebugAdapterClient ( settings . ConnectionString );
}
2025-11-28 18:06:42 -06:00
DebugSymbolResolver ? symbolResolver = null ;
if ( settings . SymbolDir is not null )
{
Directory . CreateDirectory ( settings . SymbolDir );
2025-11-29 01:32:09 -06:00
symbolResolver = new ( settings . SymbolDir , settings . SourceDir );
2025-11-28 18:06:42 -06:00
}
2025-11-30 13:37:33 -06:00
AnsiConsole . MarkupLineInterpolated ( $"Connecting to '{settings.ConnectionString}'..." );
(( IRemoteDebuggerState ) c ). Connected += ( s , e ) =>
2025-11-28 18:06:42 -06:00
{
AnsiConsole . MarkupLine ( "[green]Connected[/]" );
Thread consoleThread = new (() => DebugConsole ( c , symbolResolver ));
consoleThread . Start ();
};
c . Start ();
2025-12-01 00:45:01 -06:00
await _exit . Task . ConfigureAwait ( false );
2025-11-28 18:06:42 -06:00
return 0 ;
}
private int DebugConsole ( IDebuggerClient client , DebugSymbolResolver ? symbolResolver )
{
client . InterpreterStateChanged += ( cmd ) =>
{
AnsiConsole . MarkupLineInterpolated ( $"[yellow]Application in '{cmd}' state[/]" );
};
while ( true )
{
var input = AnsiConsole . Prompt ( new TextPrompt < string >( "> " ));
var inputParts = input . Split ( ' ' , StringSplitOptions . TrimEntries | StringSplitOptions . RemoveEmptyEntries );
var command = inputParts [ 0 ]. ToUpperInvariant ();
switch ( command )
{
case "BREAK" or "B" :
string path = inputParts [ 1 ];
var fileName = Path . GetFileName ( path ). Split ( '!' )[^ 1 ];
uint breakOffset ;
if ( inputParts . Length >= 3 && inputParts [ 2 ]. StartsWith ( "0x" ))
{
2025-12-03 20:42:58 -06:00
breakOffset = Convert . ToUInt32 ( inputParts [ 2 ], 16 );
2025-11-28 18:06:42 -06:00
}
else
{
var line = int . Parse ( inputParts [ 2 ]);
var column = int . Parse ( inputParts [ 3 ]);
2025-11-29 01:32:09 -06:00
2025-12-03 20:42:58 -06:00
var fsym = symbolResolver ?. GetForFile ( fileName );
if ( fsym is null )
2025-11-29 01:32:09 -06:00
{
2025-12-03 20:42:58 -06:00
AnsiConsole . MarkupLineInterpolated ( $"[yellow]No symbols loaded for '{fileName}'.[/]" );
client . UpdateBreakpoint ( new ( path , line , column ));
AnsiConsole . MarkupLineInterpolated ( $"[green]Breakpoint set in '{path}' at line {line}, column {column}.[/]" );
2025-11-29 01:32:09 -06:00
break ;
}
2025-12-03 20:42:58 -06:00
else
2025-11-29 01:32:09 -06:00
{
2025-12-03 20:42:58 -06:00
if ( fsym . OriginalFileName is not null )
path = fsym . OriginalFileName ;
var position = new SourcePosition ( line , column );
var location = fsym !. SourceMap . GetLocationFromPosition ( position );
if ( location is null )
{
AnsiConsole . MarkupLineInterpolated ( $"[red]Line {line}, column {column} has known offset in '{fileName}'.[/]" );
break ;
}
breakOffset = location . Offset ;
if ( fsym . HasSourceCode ())
{
var sourceCode = fsym . GetSourceSubstring ( location . Span );
AnsiConsole . Write ( new Panel ( sourceCode . EscapeMarkup ()));
}
2025-11-29 01:32:09 -06:00
}
2025-11-28 18:06:42 -06:00
}
2025-11-29 01:32:09 -06:00
client . UpdateBreakpoint ( new ( path , breakOffset ));
2025-11-29 18:44:08 -06:00
AnsiConsole . MarkupLineInterpolated ( $"[green]Breakpoint set in '{path}' at offset 0x{breakOffset:X4}.[/]" );
2025-11-28 18:06:42 -06:00
break ;
case "CONTINUE" or "C" :
client . DebuggerCommand = Microsoft . Iris . Debug . Data . InterpreterCommand . Continue ;
break ;
2025-11-30 13:37:33 -06:00
case "STEP" or "S" :
client . DebuggerCommand = Microsoft . Iris . Debug . Data . InterpreterCommand . Step ;
break ;
case "ENABLE" :
break ;
2025-11-28 18:06:42 -06:00
case "CLEAR" :
// Not implemented yet, should clear all breakpoints
break ;
2025-11-30 13:37:33 -06:00
case "EXIT" or "QUIT" :
2025-12-01 00:45:01 -06:00
_exit . SetResult ();
2025-11-28 18:06:42 -06:00
return 0 ;
}
}
}
public sealed class Settings : CommandSettings
{
[Description("The directory containing pre-generated symbols.")]
[CommandOption("-s|--symbols <symbolDir>")]
public string? SymbolDir { get ; init ; }
2025-11-29 01:32:09 -06:00
[Description("The directory containing source code.")]
[CommandOption("--sources <sourceDir>")]
public string? SourceDir { get ; init ; }
2025-11-28 18:06:42 -06:00
[Description("Whether to decompile the current file when a breakpoint is hit. Generated symbols will be written to the symbol directory if specified.")]
[CommandOption("-d|--decompile")]
public bool Decompile { get ; init ; }
2025-11-30 13:37:33 -06:00
[Description("The string used to connect to the debug server.")]
2025-11-28 18:06:42 -06:00
[CommandOption("-u|--server <serverUri>")]
2025-11-30 13:37:33 -06:00
public string ConnectionString { get ; init ; } = DebugRemoting . DEFAULT_TCP_URI . ToString ();
2025-11-28 18:06:42 -06:00
}
}