diff --git a/DebugAdapter/UIX.DebugAdapter.Client/IrisDebugAdapterClient.cs b/DebugAdapter/UIX.DebugAdapter.Client/IrisDebugAdapterClient.cs new file mode 100644 index 0000000..28d0b4b --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Client/IrisDebugAdapterClient.cs @@ -0,0 +1,147 @@ +using Microsoft.Iris.Debug; +using Microsoft.Iris.Debug.Data; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using IrisBreakpoint = Microsoft.Iris.Debug.Data.Breakpoint; + +namespace Microsoft.Iris.DebugAdapter.Client; + +public class IrisDebugAdapterClient : IDebuggerClient, IRemoteDebuggerState, IDisposable +{ + private Stream _inputStream; + private Stream _outputStream; + private DebugAdapterClient? _debugAdapter; + private InterpreterCommand _debuggerCommand; + + public InterpreterCommand DebuggerCommand + { + get => _debuggerCommand; + set + { + _debuggerCommand = value; + + _ = value switch + { + InterpreterCommand.Break => _debugAdapter.RequestPause(new()), + InterpreterCommand.Continue => _debugAdapter.RequestContinue(new()), + InterpreterCommand.Step => _debugAdapter.RequestNext(new()), + _ => Task.CompletedTask + }; + } + } + + public string ConnectionString { get; } + + public event Action InterpreterStateChanged; + public event EventHandler InterpreterDecode; + public event EventHandler InterpreterExecute; + public event Action DispatcherStep; + public event Action Connected; + + public IrisDebugAdapterClient(Stream inputStream, Stream outputStream) + { + _inputStream = inputStream; + _outputStream = outputStream; + } + + public IrisDebugAdapterClient(string connectionString) + { + ConnectionString = connectionString; + } + + public async Task StartAsync() + { + if (ConnectionString is not null) + { + ConnectionStringHelper.CreateFromString(ConnectionString, out _inputStream, out _outputStream); + } + + _debugAdapter = await DebugAdapterClient.From(options => + { + options + .WithInput(_inputStream) + .WithOutput(_outputStream) + .OnInitialize((server, _, cancellationToken) => + { + return Task.CompletedTask; + }) + .OnInitialized((_, _, response, _) => + { + return Task.CompletedTask; + }) + .OnContinued(args => + { + _debuggerCommand = InterpreterCommand.Continue; + InterpreterStateChanged?.Invoke(_debuggerCommand); + }) + .OnStopped(args => + { + _debuggerCommand = InterpreterCommand.Break; + InterpreterStateChanged?.Invoke(_debuggerCommand); + }) + ; + }).ConfigureAwait(false); + } + + public void Dispose() + { + _debugAdapter?.Dispose(); + _inputStream.Dispose(); + _outputStream.Dispose(); + } + + public void UpdateBreakpoint(IrisBreakpoint irisBreakpoint) + { + if (irisBreakpoint.Line > 0) + { + List sourceBreakpoints = [ + new() + { + Line = irisBreakpoint.Line, + Column = irisBreakpoint.Column > 0 + ? irisBreakpoint.Column + : null + } + ]; + + _ = _debugAdapter.SetBreakpoints(new() + { + Breakpoints = sourceBreakpoints + }); + + } + else if (irisBreakpoint.Offset != uint.MaxValue) + { + // Instruction breakpoint + List instructionBreakpoints = [ + new() + { + InstructionReference = $"{irisBreakpoint.Uri}@0x{irisBreakpoint.Offset:X}", + //Offset = irisBreakpoint.Offset, + } + ]; + + _ = _debugAdapter.SetInstructionBreakpoints(new() + { + Breakpoints = instructionBreakpoints + }); + } + } + + public void RequestLineNumberTable(string uri, Action callback) + { + } + + public void Start() + { + StartAsync().RunSynchronously(); + Connected?.Invoke(this, EventArgs.Empty); + } +} diff --git a/DebugAdapter/UIX.DebugAdapter.Client/UIX.DebugAdapter.Client.csproj b/DebugAdapter/UIX.DebugAdapter.Client/UIX.DebugAdapter.Client.csproj new file mode 100644 index 0000000..c0f216f --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Client/UIX.DebugAdapter.Client.csproj @@ -0,0 +1,20 @@ + + + + net461;net6.0;net6.0-windows10.0.22000 + 12 + Microsoft.Iris.DebugAdapter.Client + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + diff --git a/DebugAdapter/UIX.DebugAdapter.Server/DapDebuggerServer.cs b/DebugAdapter/UIX.DebugAdapter.Server/DapDebuggerServer.cs new file mode 100644 index 0000000..c9d72d8 --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Server/DapDebuggerServer.cs @@ -0,0 +1,84 @@ +using Microsoft.Iris.Debug; +using Microsoft.Iris.Debug.Data; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using System; + +namespace Microsoft.Iris.DebugAdapter.Server; + +public class DapDebuggerServer : IDebuggerServer, IRemoteDebuggerState +{ + private readonly IrisDebugServerOptions _debugAdapterOptions; + private IrisDebugAdapterServer? _debugAdapter; + private InterpreterCommand _debuggerCommand; + + public InterpreterCommand DebuggerCommand + { + get => _debuggerCommand; + set + { + _debuggerCommand = value; + + if (_debugAdapter?.Server is null) + return; + + if (value is InterpreterCommand.Continue) + { + _debugAdapter.Server.SendContinued(new() + { + ThreadId = Environment.CurrentManagedThreadId, + }); + } + else if (value is InterpreterCommand.Break) + { + _debugAdapter.Server.SendStopped(new() + { + Reason = new("unknown"), + ThreadId = Environment.CurrentManagedThreadId, + }); + } + } + } + + public string ConnectionString { get; } + + public event Action? Connected; + + public DapDebuggerServer(string connectionString, IrisDebugServerOptions options) + { + ConnectionString = connectionString; + + _debugAdapterOptions = options; + } + + public void LogDispatcher(string message) + { + } + + public void LogInterpreterDecode(object context, InterpreterInstruction instruction) + { + } + + public void LogInterpreterExecute(object context, InterpreterEntry entry) + { + } + + public MarkupLineNumberEntry[] OnLineNumberTableRequested(string uri) + { + return []; + } + + public void Start() + { + ConnectionStringHelper.CreateFromString(ConnectionString, out var input, out var output); + + _debugAdapter = new(input, output, _debugAdapterOptions); + _debugAdapter.StartAsync().RunSynchronously(); + + Connected?.Invoke(this, EventArgs.Empty); + } + + public void WaitForContinue() + { + while (DebuggerCommand is InterpreterCommand.Break) ; + } +} diff --git a/DebugAdapter/UIX.DebugAdapter.Server/Handlers/BreakpointHandler.cs b/DebugAdapter/UIX.DebugAdapter.Server/Handlers/BreakpointHandler.cs new file mode 100644 index 0000000..5227c44 --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Server/Handlers/BreakpointHandler.cs @@ -0,0 +1,76 @@ +using Microsoft.Iris.Debug.Symbols; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DapBreakpoint = OmniSharp.Extensions.DebugAdapter.Protocol.Models.Breakpoint; +using IrisBreakpoint = Microsoft.Iris.Debug.Data.Breakpoint; + +namespace Microsoft.Iris.DebugAdapter.Server.Handlers; + +internal class BreakpointHandler(DebugSymbolResolver symbolResolver) + : ISetInstructionBreakpointsHandler, ISetBreakpointsHandler +{ + public Task Handle(SetInstructionBreakpointsArguments request, CancellationToken cancellationToken) + { + List dapBreakpoints = []; + + foreach (var requestedBreakpoint in request.Breakpoints) + { + + } + + SetInstructionBreakpointsResponse response = new() + { + Breakpoints = dapBreakpoints + }; + return Task.FromResult(response); + } + + public Task Handle(SetBreakpointsArguments request, CancellationToken cancellationToken) + { + // Debug symbols are required for setting source (line:column) breakpoints + var fsym = symbolResolver?.GetForFile(request.Source.Path); + if (fsym is null) + return Task.FromResult(new SetBreakpointsResponse()); + + List dapBreakpoints = []; + + // TODO: Clear existing breakpoints for file + + foreach (var requestedBreakpoint in request.Breakpoints ?? []) + { + SourceMap.Entry location; + + if (requestedBreakpoint.Column is not null) + { + SourcePosition position = new(requestedBreakpoint.Line, requestedBreakpoint.Column.Value); + location = fsym.SourceMap.GetLocationFromPosition(position); + } + else + { + // Get first location that contains this line + location = fsym.SourceMap.GetLocationFromLine(requestedBreakpoint.Line); + } + + IrisBreakpoint irisBreakpoint = new(fsym.CompiledFileName, location.Offset); + Application.DebugSettings.Breakpoints.Add(irisBreakpoint); + + DapBreakpoint dapBreakpoint = new() + { + Line = location.Span.Start.Line, + Column = location.Span.Start.Column, + EndLine = location.Span.End.Line, + EndColumn = location.Span.End.Column, + }; + dapBreakpoints.Add(dapBreakpoint); + } + + SetBreakpointsResponse response = new() + { + Breakpoints = dapBreakpoints + }; + return Task.FromResult(response); + } +} diff --git a/DebugAdapter/UIX.DebugAdapter.Server/Handlers/DebuggerActionHandlers.cs b/DebugAdapter/UIX.DebugAdapter.Server/Handlers/DebuggerActionHandlers.cs new file mode 100644 index 0000000..06fd0ef --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Server/Handlers/DebuggerActionHandlers.cs @@ -0,0 +1,58 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Iris.Debug; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace Microsoft.Iris.DebugAdapter.Server.Handlers; + +internal class ContinueHandler : ContinueHandlerBase +{ + private readonly IDebuggerServer _debugService; + + public ContinueHandler(IDebuggerServer debugService) => _debugService = debugService; + + public override Task Handle(ContinueArguments request, CancellationToken cancellationToken) + { + _debugService.DebuggerCommand = Debug.Data.InterpreterCommand.Continue; + return Task.FromResult(new ContinueResponse()); + } +} + +internal class NextHandler : NextHandlerBase +{ + private readonly IDebuggerServer _debugService; + + public NextHandler(IDebuggerServer debugService) => _debugService = debugService; + + public override Task Handle(NextArguments request, CancellationToken cancellationToken) + { + _debugService.DebuggerCommand = Debug.Data.InterpreterCommand.Step; + return Task.FromResult(new NextResponse()); + } +} + +internal class PauseHandler : PauseHandlerBase +{ + private readonly IDebuggerServer _debugService; + + public PauseHandler(IDebuggerServer debugService) => _debugService = debugService; + + public override Task Handle(PauseArguments request, CancellationToken cancellationToken) + { + _debugService.DebuggerCommand = Debug.Data.InterpreterCommand.Break; + return Task.FromResult(new PauseResponse()); + } +} + +internal class StepInHandler : StepInHandlerBase +{ + private readonly IDebuggerServer _debugService; + + public StepInHandler(IDebuggerServer debugService) => _debugService = debugService; + + public override Task Handle(StepInArguments request, CancellationToken cancellationToken) + { + _debugService.DebuggerCommand = Debug.Data.InterpreterCommand.Step; + return Task.FromResult(new StepInResponse()); + } +} diff --git a/DebugAdapter/UIX.DebugAdapter.Server/IrisDebugAdapterServer.cs b/DebugAdapter/UIX.DebugAdapter.Server/IrisDebugAdapterServer.cs new file mode 100644 index 0000000..26d4875 --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Server/IrisDebugAdapterServer.cs @@ -0,0 +1,121 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Iris.Debug; +using Microsoft.Iris.DebugAdapter.Server.Handlers; +using OmniSharp.Extensions.DebugAdapter.Server; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.Iris.DebugAdapter.Server; + +public class IrisDebugAdapterServer : IDisposable +{ + private readonly Stream _inputStream; + private readonly Stream _outputStream; + private readonly TaskCompletionSource _serverStopped; + + public IrisDebugAdapterServer( + Stream inputStream, + Stream outputStream, + IrisDebugServerOptions options) + { + _inputStream = inputStream; + _outputStream = outputStream; + _serverStopped = new(); + + Options = options; + } + + internal IrisDebugServerOptions Options { get; } + + internal DebugAdapterServer? Server { get; private set; } + + /// + /// Start the debug server listening. + /// + /// A task that completes when the server is ready. + public async Task StartAsync() + { + Server = await DebugAdapterServer.From(options => + { + // We need to let the PowerShell Context Service know that we are in a debug session + // so that it doesn't send the powerShell/startDebugger message. + //_psesHost = ServiceProvider.GetService(); + //_psesHost.DebugContext.IsDebugServerActive = true; + + options + .WithInput(_inputStream) + .WithOutput(_outputStream) + .WithServices(serviceCollection => + serviceCollection + .AddOptions() + .AddIrisDebugServices(Options.SymbolDir, Options.SourceDir) + ) + // TODO: Consider replacing all WithHandler with AddSingleton + //.WithHandler() + //.WithHandler() + .WithHandler() + //.WithHandler() + //.WithHandler() + //.WithHandler() + //.WithHandler() + //.WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + .WithHandler() + //.WithHandler() + //.WithHandler() + //.WithHandler() + //.WithHandler() + // The OnInitialize delegate gets run when we first receive the _Initialize_ request: + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize + .OnInitialize(async (server, _, cancellationToken) => + { + // Start the host if not already started, and enable debug mode (required + // for remote debugging). + // + // TODO: We might need to fill in HostStartOptions here. + //_startedPses = !await _psesHost.TryStartAsync(new HostStartOptions(), cancellationToken).ConfigureAwait(false); + //_psesHost.DebugContext.EnableDebugMode(); + + // Clear any existing breakpoints before proceeding. + //BreakpointService breakpointService = server.GetService(); + //await breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false); + }) + // The OnInitialized delegate gets run right before the server responds to the _Initialize_ request: + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize + .OnInitialized((_, _, response, _) => + { + //response.SupportsConditionalBreakpoints = true; + //response.SupportsConfigurationDoneRequest = true; + //response.SupportsFunctionBreakpoints = true; + //response.SupportsHitConditionalBreakpoints = true; + //response.SupportsLogPoints = true; + //response.SupportsSetVariable = true; + //response.SupportsDelayedStackTraceLoading = true; + + return Task.CompletedTask; + }) + ; + }).ConfigureAwait(false); + } + + public void Dispose() + { + // Note that the lifetime of the DebugContext is longer than the debug server; + // It represents the debugger on the PowerShell process we're in, + // while a new debug server is spun up for every debugging session + //_psesHost.DebugContext.IsDebugServerActive = false; + Server?.Dispose(); + _inputStream.Dispose(); + _outputStream.Dispose(); + _serverStopped.SetResult(true); + } + + public async Task WaitForShutdownAsync() => await _serverStopped.Task.ConfigureAwait(false); + + public event EventHandler? SessionEnded; + + internal void OnSessionEnded() => SessionEnded?.Invoke(this, EventArgs.Empty); +} diff --git a/DebugAdapter/UIX.DebugAdapter.Server/IrisDebugServerOptions.cs b/DebugAdapter/UIX.DebugAdapter.Server/IrisDebugServerOptions.cs new file mode 100644 index 0000000..2fe2d21 --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Server/IrisDebugServerOptions.cs @@ -0,0 +1,8 @@ +namespace Microsoft.Iris.DebugAdapter; + +public class IrisDebugServerOptions +{ + public string SymbolDir { get; set; } + + public string? SourceDir { get; set; } +} diff --git a/DebugAdapter/UIX.DebugAdapter.Server/ServiceCollectionExtensions.cs b/DebugAdapter/UIX.DebugAdapter.Server/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..76193a9 --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Server/ServiceCollectionExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Iris.DebugAdapter.Server; + +internal static class ServiceCollectionExtensions +{ + public static IServiceCollection AddIrisDebugServices(this IServiceCollection services, string symbolDir, string? sourceDir) + { + DebugSymbolResolver symbolResolver = new(symbolDir, sourceDir); + + return services + .AddSingleton(symbolResolver); + } +} diff --git a/DebugAdapter/UIX.DebugAdapter.Server/UIX.DebugAdapter.Server.csproj b/DebugAdapter/UIX.DebugAdapter.Server/UIX.DebugAdapter.Server.csproj new file mode 100644 index 0000000..5a0072b --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Server/UIX.DebugAdapter.Server.csproj @@ -0,0 +1,21 @@ + + + + net461;net6.0;net6.0-windows10.0.22000 + 12 + enable + Microsoft.Iris.DebugAdapter.Server + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + diff --git a/DebugAdapter/UIX.DebugAdapter.Shared/ConnectionStringHelper.cs b/DebugAdapter/UIX.DebugAdapter.Shared/ConnectionStringHelper.cs new file mode 100644 index 0000000..c7e4514 --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Shared/ConnectionStringHelper.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.IO.Pipes; + +namespace Microsoft.Iris.DebugAdapter; + +public static class ConnectionStringHelper +{ + private const string PIPE_PREFIX = @"\\.\pipe\"; + + public static void CreateFromString(string connectionString, out Stream input, out Stream output) + { + // TODO: Support other transports + var pipeName = connectionString.StartsWith(PIPE_PREFIX) + ? connectionString + : NamedPipeUtils.GenerateValidNamedPipeName(); + + input = NamedPipeUtils.CreateNamedPipe(pipeName, PipeDirection.InOut); + output = input; + } +} diff --git a/DebugAdapter/UIX.DebugAdapter.Shared/DebugSymbolResolver.cs b/DebugAdapter/UIX.DebugAdapter.Shared/DebugSymbolResolver.cs new file mode 100644 index 0000000..9be7b8f --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Shared/DebugSymbolResolver.cs @@ -0,0 +1,76 @@ +using Microsoft.Iris.Debug.Symbols; +using System; +using System.IO; +using System.Linq; + +namespace Microsoft.Iris.DebugAdapter; + +public class DebugSymbolResolver(string symbolDir, string? sourceDir) +{ + private readonly DirectoryInfo _symbolDir = new(symbolDir); + private readonly DirectoryInfo? _sourceDir = sourceDir is not null ? new(sourceDir) : null; + + public ApplicationDebugSymbols? GetForApplication(string? application) + { + if (application is null) + return null; + + var fileName = $"{application}.asym.json"; + var asymFile = FindFile(fileName, _symbolDir); + + if (asymFile is null) + return null; + + using var stream = asymFile.OpenRead(); + using var reader = new StreamReader(stream); + return DebugSymbolsJsonParser.ParseForApplication(reader.ReadToEnd()); + } + + public FileDebugSymbols? GetForFile(string? file, string? application = null) + { + if (file is null) + return null; + + var fileName = $"{file}.fsym.json"; + var fsymFile = FindFile(fileName, _symbolDir); + + if (fsymFile is not null) + { + using var stream = fsymFile.OpenRead(); + using var reader = new StreamReader(stream); + var fsym = DebugSymbolsJsonParser.ParseForFile(reader.ReadToEnd()); + + var sourceFile = FindFile(file, _sourceDir); + if (sourceFile is not null) + { + using var sourceStream = sourceFile.OpenRead(); + using var sourceReader = new StreamReader(sourceStream); + fsym.SetSourceCode(sourceReader.ReadToEnd()); + } + + return fsym; + } + + if (application is null) + return null; + + var asym = GetForApplication(application); + if (asym is null) + return null; + + return asym.GetForFile(fileName); + } + + private static FileInfo? FindFile(string fileName, DirectoryInfo? dir) + { + return dir? + .EnumerateFiles() + .FirstOrDefault(f => AreEquivalentFileNames(f.Name, fileName)); + } + + private static bool AreEquivalentFileNames(string fileName1, string fileName2) + { + // NOTE: Assumes Windows. Should be trivial to support other filesystems later. + return fileName1.Equals(fileName2, StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/DebugAdapter/UIX.DebugAdapter.Shared/NamedPipeUtils.cs b/DebugAdapter/UIX.DebugAdapter.Shared/NamedPipeUtils.cs new file mode 100644 index 0000000..27ecb49 --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Shared/NamedPipeUtils.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; + + +#if !NET +using System.Security.Principal; +using System.Security.AccessControl; +#else +using System.Runtime.InteropServices; +#endif + +namespace Microsoft.Iris.DebugAdapter; + +/// +/// Utility class for handling named pipe creation in .NET Core and .NET Framework. +/// +public static class NamedPipeUtils +{ +#if !NET + // .NET Framework requires the buffer size to be specified + private const int PipeBufferSize = 1024; +#endif + + public static NamedPipeServerStream CreateNamedPipe( + string pipeName, + PipeDirection pipeDirection) + { +#if NET + return new NamedPipeServerStream( + pipeName: pipeName, + direction: pipeDirection, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); +#else + + // In .NET Framework, we must manually ACL the named pipes we create + + PipeSecurity pipeSecurity = new(); + + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new(identity); + + if (principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + // Allow the Administrators group full access to the pipe. + pipeSecurity.AddAccessRule( + new PipeAccessRule( + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null).Translate(typeof(NTAccount)), + PipeAccessRights.FullControl, AccessControlType.Allow)); + } + else + { + // Allow the current user read/write access to the pipe. + pipeSecurity.AddAccessRule(new PipeAccessRule( + WindowsIdentity.GetCurrent().User, + PipeAccessRights.ReadWrite, AccessControlType.Allow)); + } + + return new NamedPipeServerStream( + pipeName, + pipeDirection, + maxNumberOfServerInstances: 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + inBufferSize: PipeBufferSize, + outBufferSize: PipeBufferSize, + pipeSecurity); +#endif + } + + /// + /// Generate a named pipe name known to not already be in use. + /// + /// Prefix variants of the pipename to test, if any. + /// A named pipe name or name suffix that is safe to you. + public static string GenerateValidNamedPipeName(IReadOnlyCollection? prefixes = null) + { + for (int i = 0; i < 10; i++) + { + string pipeName = $"IrisUIX_{Path.GetRandomFileName()}"; + + // In the simple prefix-less case, just test the pipe name + if (prefixes == null) + { + if (!IsPipeNameValid(pipeName)) + { + continue; + } + + return pipeName; + } + + // If we have prefixes, test that all prefix/pipename combinations are valid + bool allPipeNamesValid = true; + foreach (string prefix in prefixes) + { + string prefixedPipeName = $"IrisUIX_{prefix}_{pipeName}"; + if (!IsPipeNameValid(prefixedPipeName)) + { + allPipeNamesValid = false; + break; + } + } + + if (allPipeNamesValid) + { + return pipeName; + } + } + + throw new IOException("Unable to create named pipe; no available names"); + } + + /// + /// Validate that a named pipe file name is a legitimate named pipe file name and is not already in use. + /// + /// The named pipe name to validate. This should be a simple name rather than a path. + /// True if the named pipe name is valid, false otherwise. + public static bool IsPipeNameValid(string pipeName) + { + if (string.IsNullOrEmpty(pipeName)) + { + return false; + } + + return !File.Exists(GetNamedPipePath(pipeName)); + } + + /// + /// Get the path of a named pipe given its name. + /// + /// The simple name of the named pipe. + /// The full path of the named pipe. +#pragma warning disable IDE0022 + public static string GetNamedPipePath(string pipeName) + { +#if NET + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{pipeName}"); + } +#endif + return $@"\\.\pipe\{pipeName}"; + } +} +#pragma warning restore IDE0022 diff --git a/DebugAdapter/UIX.DebugAdapter.Shared/UIX.DebugAdapter.Shared.csproj b/DebugAdapter/UIX.DebugAdapter.Shared/UIX.DebugAdapter.Shared.csproj new file mode 100644 index 0000000..ebd1bbb --- /dev/null +++ b/DebugAdapter/UIX.DebugAdapter.Shared/UIX.DebugAdapter.Shared.csproj @@ -0,0 +1,19 @@ + + + + net461;net6.0;net6.0-windows10.0.22000 + 12 + enable + Microsoft.Iris.DebugAdapter + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + diff --git a/UIXC/Commands/DebugCommand.cs b/UIXC/Commands/DebugCommand.cs index 7a236f4..c695674 100644 --- a/UIXC/Commands/DebugCommand.cs +++ b/UIXC/Commands/DebugCommand.cs @@ -1,6 +1,7 @@ using Microsoft.Iris.Debug; using Microsoft.Iris.Debug.Symbols; using Microsoft.Iris.Debug.SystemNet; +using Microsoft.Iris.DebugAdapter.Client; using Spectre.Console; using Spectre.Console.Cli; using System.ComponentModel; @@ -14,12 +15,30 @@ public class DebugCommand : Command public override int Execute(CommandContext context, Settings settings) { - if (settings.ServerUri is null) + if (settings.ConnectionString is null) { - AnsiConsole.MarkupLine("[red]Missing argument: must specify a debug server to connect to.[/]"); + AnsiConsole.MarkupLine("[red]Missing argument, a connection string must be specified.[/]"); return -1; } + 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); + } + DebugSymbolResolver? symbolResolver = null; if (settings.SymbolDir is not null) @@ -28,10 +47,9 @@ public class DebugCommand : Command symbolResolver = new(settings.SymbolDir, settings.SourceDir); } - AnsiConsole.MarkupLineInterpolated($"Connecting to '{settings.ServerUri}'..."); - - var c = new NetDebuggerClient(settings.ServerUri); - c.Connected += (s, e) => + AnsiConsole.MarkupLineInterpolated($"Connecting to '{settings.ConnectionString}'..."); + + ((IRemoteDebuggerState)c).Connected += (s, e) => { AnsiConsole.MarkupLine("[green]Connected[/]"); Thread consoleThread = new(() => DebugConsole(c, symbolResolver)); @@ -117,11 +135,19 @@ public class DebugCommand : Command client.DebuggerCommand = Microsoft.Iris.Debug.Data.InterpreterCommand.Continue; break; + case "STEP" or "S": + client.DebuggerCommand = Microsoft.Iris.Debug.Data.InterpreterCommand.Step; + break; + + case "ENABLE": + + break; + case "CLEAR": // Not implemented yet, should clear all breakpoints break; - case "EXIT": + case "EXIT" or "QUIT": isRunning = false; return 0; } @@ -142,8 +168,8 @@ public class DebugCommand : Command [CommandOption("-d|--decompile")] public bool Decompile { get; init; } - [Description("The URI of the debug server to connect to.")] + [Description("The string used to connect to the debug server.")] [CommandOption("-u|--server ")] - public Uri ServerUri { get; init; } = DebugRemoting.DEFAULT_TCP_URI; + public string ConnectionString { get; init; } = DebugRemoting.DEFAULT_TCP_URI.ToString(); } } diff --git a/UIXC/Properties/launchSettings.json b/UIXC/Properties/launchSettings.json index d1ba32f..9a45604 100644 --- a/UIXC/Properties/launchSettings.json +++ b/UIXC/Properties/launchSettings.json @@ -3,8 +3,8 @@ "UIXC": { "commandName": "Project", - //"commandLineArgs": "debug --symbols E:\\Repos\\ZuneDev\\ZuneUIXTools\\test\\syms --sources E:\\Repos\\ZuneDev\\ZuneUIXTools\\test", - "commandLineArgs": "decompile E:\\Documents\\REProj\\Zune\\Resources\\ZuneShellResources48\\NOWPLAYINGMUSICBACKGROUND.UIX -l xml -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o E:\\Repos\\ZuneDev\\ZuneUIXTools\\test --symbols E:\\Repos\\ZuneDev\\ZuneUIXTools\\test\\syms", + "commandLineArgs": "debug --symbols E:\\Repos\\ZuneDev\\ZuneUIXTools\\test\\syms --sources E:\\Repos\\ZuneDev\\ZuneUIXTools\\test", + //"commandLineArgs": "decompile E:\\Documents\\REProj\\Zune\\Resources\\ZuneShellResources48\\NOWPLAYINGMUSICBACKGROUND.UIX -l xml -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o E:\\Repos\\ZuneDev\\ZuneUIXTools\\test --symbols E:\\Repos\\ZuneDev\\ZuneUIXTools\\test\\syms", //"commandLineArgs": "decompile E:\\Documents\\REProj\\Zune\\Resources\\ZuneMarketplaceResources48\\MarketplaceData.schema.xml -l xml -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o E:\\Repos\\ZuneDev\\ZuneUIXTools\\test", //"commandLineArgs": "decompile E:\\Documents\\REProj\\Zune\\Resources\\ZuneShellResources48\\DeviceLandElements.uix -l xml -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o E:\\Repos\\ZuneDev\\ZuneUIXTools\\test", diff --git a/UIXC/UIXC.csproj b/UIXC/UIXC.csproj index fc19f54..f4fc7e7 100644 --- a/UIXC/UIXC.csproj +++ b/UIXC/UIXC.csproj @@ -12,6 +12,7 @@ + diff --git a/ZuneUIXTools.sln b/ZuneUIXTools.sln index b41da89..7ecbb6d 100644 --- a/ZuneUIXTools.sln +++ b/ZuneUIXTools.sln @@ -17,6 +17,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIXC", "UIXC\UIXC.csproj", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIX.DecompXml", "libs\UIX.DecompXml\UIX.DecompXml.csproj", "{3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIX.DebugAdapter.Server", "DebugAdapter\UIX.DebugAdapter.Server\UIX.DebugAdapter.Server.csproj", "{C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DebugAdapter", "DebugAdapter", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIX.DebugAdapter.Shared", "DebugAdapter\UIX.DebugAdapter.Shared\UIX.DebugAdapter.Shared.csproj", "{B93521C9-4570-41FC-BA15-09C34F2DFACE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIX.DebugAdapter.Client", "DebugAdapter\UIX.DebugAdapter.Client\UIX.DebugAdapter.Client.csproj", "{7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +107,42 @@ Global {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Release|x64.Build.0 = Release|Any CPU {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Release|x86.ActiveCfg = Release|Any CPU {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Release|x86.Build.0 = Release|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Debug|x64.ActiveCfg = Debug|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Debug|x64.Build.0 = Debug|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Debug|x86.ActiveCfg = Debug|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Debug|x86.Build.0 = Debug|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Release|Any CPU.Build.0 = Release|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Release|x64.ActiveCfg = Release|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Release|x64.Build.0 = Release|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Release|x86.ActiveCfg = Release|Any CPU + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308}.Release|x86.Build.0 = Release|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Debug|x64.ActiveCfg = Debug|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Debug|x64.Build.0 = Debug|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Debug|x86.ActiveCfg = Debug|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Debug|x86.Build.0 = Debug|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Release|Any CPU.Build.0 = Release|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Release|x64.ActiveCfg = Release|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Release|x64.Build.0 = Release|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Release|x86.ActiveCfg = Release|Any CPU + {B93521C9-4570-41FC-BA15-09C34F2DFACE}.Release|x86.Build.0 = Release|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Debug|x64.ActiveCfg = Debug|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Debug|x64.Build.0 = Debug|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Debug|x86.Build.0 = Debug|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Release|Any CPU.Build.0 = Release|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Release|x64.ActiveCfg = Release|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Release|x64.Build.0 = Release|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Release|x86.ActiveCfg = Release|Any CPU + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -109,6 +153,9 @@ Global {D311AFFC-CA27-4349-8B01-D5CF0E5FC2B6} = {470F14DD-0489-4BE5-BB4B-421EDB4021AC} {C401799F-64DA-41E2-BC37-BE3C304FAE19} = {470F14DD-0489-4BE5-BB4B-421EDB4021AC} {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2} = {470F14DD-0489-4BE5-BB4B-421EDB4021AC} + {C588C2B9-83F9-4DB8-AE82-BD9FDF2E9308} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {B93521C9-4570-41FC-BA15-09C34F2DFACE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {7AF18BEB-5E48-4D28-8BAB-1FC7B7AFC8D4} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4FFAF324-5FC4-4AAF-A0D7-BAA0D81C2B4C} diff --git a/libs/MicrosoftIris b/libs/MicrosoftIris index 453a8e8..1ab545d 160000 --- a/libs/MicrosoftIris +++ b/libs/MicrosoftIris @@ -1 +1 @@ -Subproject commit 453a8e8b2939a6e259f343b12101bf5e175aac72 +Subproject commit 1ab545d4e2c4bab38f8feb63b04ef989d8d2a561