diff --git a/.github/workflows/Build-And-Test.yml b/.github/workflows/Build-And-Test.yml index aec71a4df..db5f95201 100644 --- a/.github/workflows/Build-And-Test.yml +++ b/.github/workflows/Build-And-Test.yml @@ -130,11 +130,10 @@ jobs: which g++ which gdb echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope - # TODO: Try to make core dumps work - # Related CoreDump Tests: https://github.com/microsoft/MIEngine/issues/1170 - # echo 1 | sudo tee /proc/sys/kernel/core_uses_pid - # ulimit -S -c unlimited - # sudo sysctl -w kernel.core_pattern=${{ github.workspace }}/core.%e + # Enable core dumps: set pattern to "core" so dumps land in CWD (the debuggee output dir) + echo core | sudo tee /proc/sys/kernel/core_pattern + echo 0 | sudo tee /proc/sys/kernel/core_uses_pid + sudo systemctl disable apport.service 2>/dev/null || true - run: | ${{ github.workspace }}/eng/Scripts/CI-Build.sh diff --git a/IL/Microsoft.Internal.VisualStudio.Shell.Embeddable.il b/IL/Microsoft.Internal.VisualStudio.Shell.Embeddable.il new file mode 100644 index 000000000..3dc5c75a9 --- /dev/null +++ b/IL/Microsoft.Internal.VisualStudio.Shell.Embeddable.il @@ -0,0 +1,25 @@ +.assembly extern mscorlib +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89) + .ver 4:0:0:0 +} +.assembly Microsoft.Internal.VisualStudio.Shell.Embeddable +{ + .custom instance void [mscorlib]System.Runtime.InteropServices.ImportedFromTypeLibAttribute::.ctor(string) = { string('Microsoft.Internal.VisualStudio.Shell.Embeddable') } + .custom instance void [mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string) = { string('190F8999-290E-4111-AE15-1509B895B58C') } + .publickey = (00 24 00 00 04 80 00 00 94 00 00 00 06 02 00 00 00 24 00 00 52 53 41 31 00 04 00 00 01 00 01 00 07 D1 FA 57 C4 AE D9 F0 A3 2E 84 AA 0F AE FD 0D E9 E8 FD 6A EC 8F 87 FB 03 76 6C 83 4C 99 92 1E B2 3B E7 9A D9 D5 DC C1 DD 9A D2 36 13 21 02 90 0B 72 3C F9 80 95 7F C4 E1 77 10 8F C6 07 77 4F 29 E8 32 0E 92 EA 05 EC E4 E8 21 C0 A5 EF E8 F1 64 5C 4C 0C 93 C1 AB 99 28 5D 62 2C AA 65 2C 1D FA D6 3D 74 5D 6F 2D E5 F1 7E 5E AF 0F C4 96 3D 26 1C 8A 12 43 65 18 20 6D C0 93 34 4D 5A D2 93) + .hash algorithm 0x00008004 + .ver 16:0:0:0 +} +.namespace Microsoft.Internal.VisualStudio.Shell.Embeddable.Feedback +{ + .class interface public abstract auto ansi import IFeedbackDiagnosticFileProvider + { + .custom instance void [mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string) = { string('5F67C426-4C63-4CDB-917B-45B400B96C31') } + .method public hidebysig newslot abstract virtual + instance class [mscorlib]System.Collections.Generic.IReadOnlyCollection`1 + GetFiles() cil managed + { + } + } +} diff --git a/build/version.settings.targets b/build/version.settings.targets index b4b4aca63..53ed9072f 100644 --- a/build/version.settings.targets +++ b/build/version.settings.targets @@ -2,9 +2,9 @@ - 17 - 12 - 2022 + 18 + 8 + 2025 14.0.0.0 @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/eng/Scripts/CI-Test.sh b/eng/Scripts/CI-Test.sh index fd9c4c60a..24f9a9bf1 100755 --- a/eng/Scripts/CI-Test.sh +++ b/eng/Scripts/CI-Test.sh @@ -19,4 +19,8 @@ if [ ! -f "$RootDir/bin/DebugAdapterProtocolTests/Debug/CppTests/config.xml" ]; fi fi -dotnet test "$RootDir"/bin/DebugAdapterProtocolTests/Debug/CppTests/CppTests.dll --logger "trx;LogFileName=$RootDir/bin/DebugAdapterProtocolTests/Debug/CppTests/results.trx" \ No newline at end of file +# Run tests in a subshell with core dumps enabled (ulimit resets when subshell exits) +( + ulimit -c unlimited + dotnet test "$RootDir"/bin/DebugAdapterProtocolTests/Debug/CppTests/CppTests.dll --logger "trx;LogFileName=$RootDir/bin/DebugAdapterProtocolTests/Debug/CppTests/results.trx" +) diff --git a/loc/lci/OpenFolderSchema.json.lci b/loc/lci/OpenFolderSchema.json.lci index e3d3ac8b4..10049e79b 100644 --- a/loc/lci/OpenFolderSchema.json.lci +++ b/loc/lci/OpenFolderSchema.json.lci @@ -388,6 +388,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs b/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs index 9b42b62e0..3d4462044 100644 --- a/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs +++ b/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs @@ -308,6 +308,23 @@ public static void Reset() { throw new NotImplementedException(); } + + /// + /// Returns true if the feedback log is currently active + /// + public static bool IsFeedbackLogEnabled + { + get { throw new NotImplementedException(); } + } + + /// + /// Writes a message to the feedback circular buffer and, if active, to the feedback log file. + /// + /// The message to write to the feedback log. + public static void WriteFeedbackLog(string message) + { + throw new NotImplementedException(); + } } /// diff --git a/src/DebugEngineHost.VSCode/HostLogger.cs b/src/DebugEngineHost.VSCode/HostLogger.cs index 6723a4aee..1bd6805ab 100644 --- a/src/DebugEngineHost.VSCode/HostLogger.cs +++ b/src/DebugEngineHost.VSCode/HostLogger.cs @@ -51,5 +51,14 @@ public static void Reset() s_engineLogChannel?.Close(); s_engineLogChannel = null; } + + public static bool IsFeedbackLogEnabled + { + get { return false; } + } + + public static void WriteFeedbackLog(string message) + { + } } } diff --git a/src/DebugEngineHost/DebugEngineHost.csproj b/src/DebugEngineHost/DebugEngineHost.csproj index bed223019..a0f375ed7 100755 --- a/src/DebugEngineHost/DebugEngineHost.csproj +++ b/src/DebugEngineHost/DebugEngineHost.csproj @@ -99,6 +99,7 @@ + @@ -106,6 +107,11 @@ $(GeneratedAssembliesDir)Microsoft.Internal.VisualStudio.Interop.dll + + $(GeneratedAssembliesDir)Microsoft.Internal.VisualStudio.Shell.Embeddable.dll + true + false + diff --git a/src/DebugEngineHost/FeedbackDiagnosticFileProvider.cs b/src/DebugEngineHost/FeedbackDiagnosticFileProvider.cs new file mode 100644 index 000000000..db4a5f58b --- /dev/null +++ b/src/DebugEngineHost/FeedbackDiagnosticFileProvider.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Internal.VisualStudio.Shell.Embeddable.Feedback; + +namespace Microsoft.DebugEngineHost +{ + [Export(typeof(IFeedbackDiagnosticFileProvider))] + public class FeedbackDiagnosticFileProvider : IFeedbackDiagnosticFileProvider + { + public IReadOnlyCollection GetFiles() + { + if (!HostLogger.HasFeedbackEntries) + { + return Array.Empty(); + } + + string logFileName = HostLogger.GetFeedbackLogFilePath(Process.GetCurrentProcess().Id); + + IReadOnlyCollection entries = HostLogger.GetNewFeedbackEntries(); + if (entries.Count > 0) + { + _ = Task.Run(() => WriteFeedbackEntries(logFileName, entries)); + } + + return new[] { logFileName }; + } + + private static void WriteFeedbackEntries(string logFileName, IReadOnlyCollection entries) + { + try + { + using (StreamWriter logWriter = FeedbackLogBuffer.OpenLogFile(logFileName)) + { + FeedbackLogBuffer.WriteEntries(logWriter, entries); + } + } + catch + { + } + } + } +} diff --git a/src/DebugEngineHost/FeedbackLogBuffer.cs b/src/DebugEngineHost/FeedbackLogBuffer.cs new file mode 100644 index 000000000..c7f522ae0 --- /dev/null +++ b/src/DebugEngineHost/FeedbackLogBuffer.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Microsoft.DebugEngineHost +{ + /// + /// In-memory circular buffer that caches recent log messages for feedback reports. + /// + internal class FeedbackLogBuffer + { + private const int s_maxLogBufferSize = 256 * 1024; + + private readonly LinkedList _logBuffer = new LinkedList(); + private int _logLength; + private long _writeSequence; + private long _lastFlushSequence; + private readonly object _syncObj = new object(); + + /// + /// Returns true if any entries have ever been written to the buffer. + /// + internal bool HasEntries + { + get + { + lock (_syncObj) + { + return _writeSequence > 0; + } + } + } + + internal void Write(string logLine) + { + lock (_syncObj) + { + if (logLine.Length > s_maxLogBufferSize) + { + logLine = logLine.Substring(0, s_maxLogBufferSize); + } + + _logBuffer.AddLast(logLine); + _logLength += logLine.Length; + _writeSequence++; + + while (_logLength > s_maxLogBufferSize) + { + string entry = _logBuffer.First(); + _logLength -= entry.Length; + _logBuffer.RemoveFirst(); + } + } + } + + internal IReadOnlyCollection FlushNewEntries() + { + lock (_syncObj) + { + long newEntryCount = System.Math.Min(_writeSequence - _lastFlushSequence, _logBuffer.Count); + _lastFlushSequence = _writeSequence; + + if (newEntryCount <= 0) + { + return Array.Empty(); + } + + int skipCount = _logBuffer.Count - (int)newEntryCount; + return _logBuffer.Skip(skipCount).ToList().AsReadOnly(); + } + } + + /// + /// Opens the feedback log file for appending with shared read/write access. + /// + internal static StreamWriter OpenLogFile(string logFileName) + { + var fs = new FileStream(logFileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + try + { + return new StreamWriter(fs, Encoding.UTF8); + } + catch + { + fs.Dispose(); + throw; + } + } + + /// + /// Writes a collection of log entries to the given writer. + /// + internal static void WriteEntries(StreamWriter writer, IEnumerable entries) + { + foreach (string logLine in entries) + { + writer.WriteLine(logLine); + } + + writer.Flush(); + } + } +} diff --git a/src/DebugEngineHost/HostLogger.cs b/src/DebugEngineHost/HostLogger.cs index 22a1934e3..fbeca7eb4 100644 --- a/src/DebugEngineHost/HostLogger.cs +++ b/src/DebugEngineHost/HostLogger.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Globalization; +using System.IO; +using System.Threading; namespace Microsoft.DebugEngineHost { @@ -16,12 +16,20 @@ public static class HostLogger private static string s_engineLogFile; + private static FeedbackLogBuffer s_circularBuffer; + private static VSFeedbackLogger s_feedbackLogger; + public static void EnableHostLogging(Action callback, LogLevel level = LogLevel.Verbose) { if (s_engineLogChannel == null) { s_engineLogChannel = new HostLogChannel(callback, s_engineLogFile, level); } + + if (s_feedbackLogger == null) + { + s_feedbackLogger = new VSFeedbackLogger(EnsureFeedbackBuffer()); + } } public static void EnableNatvisDiagnostics(Action callback, LogLevel level = LogLevel.Verbose) @@ -52,6 +60,69 @@ public static ILogChannel GetNatvisLogChannel() return s_natvisLogChannel; } + /// + /// Returns true if the feedback log is currently active (buffer has been created). + /// + public static bool IsFeedbackLogEnabled + { + get { return s_circularBuffer != null; } + } + + /// + /// Writes a message to the feedback circular buffer and, if active, to the feedback log file. + /// + public static void WriteFeedbackLog(string message) + { + if (string.IsNullOrEmpty(message)) + { + return; + } + + string timestamp = DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss.fff", CultureInfo.InvariantCulture); + string logLine = string.Format(CultureInfo.InvariantCulture, "{0}: {1}", timestamp, message); + + EnsureFeedbackBuffer().Write(logLine); + s_feedbackLogger?.Write(logLine); + } + + private static FeedbackLogBuffer EnsureFeedbackBuffer() + { + if (s_circularBuffer == null) + { + Interlocked.CompareExchange(ref s_circularBuffer, new FeedbackLogBuffer(), null); + } + + return s_circularBuffer; + } + + /// + /// Returns true if feedback entries have ever been written during this session. + /// + internal static bool HasFeedbackEntries + { + get + { + FeedbackLogBuffer buffer = s_circularBuffer; + return buffer != null && buffer.HasEntries; + } + } + + /// + /// Returns log entries added since the last flush, then advances the flush marker. + /// + internal static IReadOnlyCollection GetNewFeedbackEntries() + { + return s_circularBuffer?.FlushNewEntries() ?? Array.Empty(); + } + + /// + /// Gets the path for the feedback log file for a given VS process ID. + /// + internal static string GetFeedbackLogFilePath(int vsPid) + { + return Path.Combine(Path.GetTempPath(), string.Format(CultureInfo.InvariantCulture, "Microsoft.VisualStudio.MIDebugEngine-{0}.log", vsPid)); + } + public static void Reset() { s_natvisLogChannel?.Close(); diff --git a/src/DebugEngineHost/VSFeedbackLogger.cs b/src/DebugEngineHost/VSFeedbackLogger.cs new file mode 100644 index 000000000..30dd240cb --- /dev/null +++ b/src/DebugEngineHost/VSFeedbackLogger.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace Microsoft.DebugEngineHost +{ + /// + /// Watches for the VS feedback tool's semaphore file and writes to a log file when recording is active. + /// + internal class VSFeedbackLogger + { + private const string s_vsFeedbackSemaphoreDir = @"Microsoft\VSFeedbackCollector"; + private const string s_vsFeedbackSemaphoreFile = "feedback.recording.json"; + + private readonly int _vsPid; + private readonly System.DateTime _vsStartTime; + private bool _enabled; + + private readonly FileSystemWatcher _vsFeedbackFileWatcher; + private readonly FeedbackLogBuffer _circularBuffer; + + private StreamWriter _logWriter; + private readonly object _syncObj = new object(); + + internal VSFeedbackLogger(FeedbackLogBuffer circularBuffer) + { + _circularBuffer = circularBuffer; + + try + { + Process vsProcess = Process.GetCurrentProcess(); + _vsPid = vsProcess.Id; + _vsStartTime = vsProcess.StartTime; + _enabled = false; + + string vsFeedbackTempDir = Path.Combine(Path.GetTempPath(), s_vsFeedbackSemaphoreDir); + + Directory.CreateDirectory(vsFeedbackTempDir); + + _vsFeedbackFileWatcher = new FileSystemWatcher(vsFeedbackTempDir, s_vsFeedbackSemaphoreFile); + _vsFeedbackFileWatcher.Created += OnFeedbackSemaphoreCreated; + _vsFeedbackFileWatcher.Deleted += OnFeedbackSemaphoreDeleted; + _vsFeedbackFileWatcher.Changed += OnFeedbackSemaphoreChanged; + + if (File.Exists(Path.Combine(vsFeedbackTempDir, s_vsFeedbackSemaphoreFile))) + { + OnFeedbackSemaphoreCreated(_vsFeedbackFileWatcher, new FileSystemEventArgs(WatcherChangeTypes.Created, vsFeedbackTempDir, s_vsFeedbackSemaphoreFile)); + } + + _vsFeedbackFileWatcher.EnableRaisingEvents = true; + } + catch + { + _vsFeedbackFileWatcher?.Dispose(); + _vsFeedbackFileWatcher = null; + } + } + + private void OnFeedbackSemaphoreCreated(object sender, FileSystemEventArgs e) + { + try + { + if (!_enabled && IsLoggingEnabledForThisVSInstance(e.FullPath)) + { + lock (_syncObj) + { + if (!_enabled) + { + string logFileName = HostLogger.GetFeedbackLogFilePath(_vsPid); + StreamWriter writer = FeedbackLogBuffer.OpenLogFile(logFileName); + try + { + FeedbackLogBuffer.WriteEntries(writer, _circularBuffer.FlushNewEntries()); + } + catch + { + writer.Dispose(); + throw; + } + + _logWriter = writer; + _enabled = true; + } + } + } + } + catch + { + } + } + + private void OnFeedbackSemaphoreDeleted(object sender, FileSystemEventArgs e) + { + lock (_syncObj) + { + if (_enabled) + { + _enabled = false; + _circularBuffer.FlushNewEntries(); + + if (_logWriter != null) + { + _logWriter.Dispose(); + _logWriter = null; + } + } + } + } + + private void OnFeedbackSemaphoreChanged(object sender, FileSystemEventArgs e) + { + OnFeedbackSemaphoreCreated(sender, e); + } + + private bool IsLoggingEnabledForThisVSInstance(string semaphoreFilePath) + { + try + { + if (_vsStartTime > File.GetCreationTime(semaphoreFilePath)) + { + return false; + } + + string content = File.ReadAllText(semaphoreFilePath); + JObject root = JObject.Parse(content); + JContainer pidCollection = root["processIds"] as JContainer; + if (pidCollection != null) + { + return pidCollection.Values().Contains(_vsPid); + } + } + catch + { + } + + return false; + } + + internal void Write(string logLine) + { + if (_enabled) + { + lock (_syncObj) + { + if (_logWriter != null) + { + _logWriter.WriteLine(logLine); + _logWriter.Flush(); + } + } + } + } + } +} diff --git a/src/MICore/CommandFactories/MICommandFactory.cs b/src/MICore/CommandFactories/MICommandFactory.cs index 7b8454857..07de24092 100644 --- a/src/MICore/CommandFactories/MICommandFactory.cs +++ b/src/MICore/CommandFactories/MICommandFactory.cs @@ -567,6 +567,15 @@ public virtual async Task BreakCondition(string bkptno, string expr) await _debugger.CmdAsync(command, ResultClass.done); } + /// + /// Sends -break-after to set an ignore count on a breakpoint. + /// + public virtual async Task BreakAfter(string bkptno, uint count) + { + string command = string.Format(CultureInfo.InvariantCulture, "-break-after {0} {1}", bkptno, count); + return await _debugger.CmdAsync(command, ResultClass.done); + } + public virtual IEnumerable GetSupportedExceptionCategories() { return new Guid[0]; diff --git a/src/MICore/JsonLaunchOptions.cs b/src/MICore/JsonLaunchOptions.cs index 6277a5633..6f872157e 100644 --- a/src/MICore/JsonLaunchOptions.cs +++ b/src/MICore/JsonLaunchOptions.cs @@ -122,6 +122,12 @@ public abstract partial class BaseOptions /// [JsonProperty("unknownBreakpointHandling", DefaultValueHandling = DefaultValueHandling.Ignore)] public UnknownBreakpointHandling? UnknownBreakpointHandling { get; set; } + + /// + /// Controls GDB's debuginfod behavior. + /// + [JsonProperty("debuginfod", DefaultValueHandling = DefaultValueHandling.Ignore)] + public DebuginfodSettings Debuginfod { get; set; } } internal class VisualizerFileConverter : JsonConverter @@ -315,6 +321,21 @@ public enum UnknownBreakpointHandling Stop } + public partial class DebuginfodSettings + { + /// + /// If true (default), GDB's debuginfod support is enabled. + /// + [JsonProperty("enabled")] + public bool? Enabled { get; set; } + + /// + /// The timeout in seconds for debuginfod server requests. Default is 30. Set to 0 for no override. + /// + [JsonProperty("timeout", DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? Timeout { get; set; } + } + public partial class LaunchOptions : BaseOptions { #region Public Properties for Serialization diff --git a/src/MICore/LaunchOptions.cs b/src/MICore/LaunchOptions.cs index dbe934817..c53bc89d0 100644 --- a/src/MICore/LaunchOptions.cs +++ b/src/MICore/LaunchOptions.cs @@ -137,6 +137,8 @@ static internal PipeLaunchOptions CreateFromJson(JObject parsedOptions) quoteArgs = platformSpecificTransportOptions.QuoteArgs ?? quoteArgs; } + Json.LaunchOptions.BaseOptions baseOptions = Json.LaunchOptions.LaunchOptionHelpers.GetLaunchOrAttachOptions(parsedOptions); + PipeLaunchOptions pipeOptions = new PipeLaunchOptions( pipePath: pipeProgram, pipeArguments: EnsurePipeArguments(pipeArgs, debuggerPath, gdbPathDefault, quoteArgs), @@ -145,7 +147,6 @@ static internal PipeLaunchOptions CreateFromJson(JObject parsedOptions) pipeEnvironment: GetEnvironmentEntries(pipeEnv) ); - Json.LaunchOptions.BaseOptions baseOptions = Json.LaunchOptions.LaunchOptionHelpers.GetLaunchOrAttachOptions(parsedOptions); pipeOptions.InitializeCommonOptions(baseOptions); if (baseOptions is Json.LaunchOptions.LaunchOptions) { @@ -300,6 +301,12 @@ public EnvironmentEntry(Json.LaunchOptions.Environment jsonEntry) this.Value = jsonEntry.Value; } + public EnvironmentEntry(string name, string value) + { + this.Name = name; + this.Value = value; + } + /// /// [Required] Name of the environment variable /// @@ -805,6 +812,12 @@ public UnixShellPortLaunchOptions(string startRemoteDebuggerCommand, this.InitializeCommonOptions(baseLaunchOptions); this.BaseOptions = baseLaunchOptions; } + + string prefix = GetDebuginfodEnvironmentPrefix(); + if (!string.IsNullOrEmpty(prefix)) + { + this.StartRemoteDebuggerCommand = prefix + this.StartRemoteDebuggerCommand; + } } } @@ -1211,6 +1224,81 @@ public UnknownBreakpointHandling UnknownBreakpointHandling } } + private bool _enableDebuginfod = true; + + /// + /// If true (default), GDB's debuginfod support is enabled. + /// + public bool EnableDebuginfod + { + get { return _enableDebuginfod; } + set + { + VerifyCanModifyProperty(nameof(EnableDebuginfod)); + _enableDebuginfod = value; + } + } + + private int _debuginfodTimeout = 30; + + /// + /// The timeout in seconds for debuginfod requests. Default is 30. Set to 0 for no override. + /// + public int DebuginfodTimeout + { + get { return _debuginfodTimeout; } + set + { + VerifyCanModifyProperty(nameof(DebuginfodTimeout)); + _debuginfodTimeout = value; + } + } + + /// + /// Returns environment entries to configure debuginfod on the GDB process. + /// + public List GetDebuginfodEnvironmentEntries() + { + var entries = new List(); + if (DebuggerMIMode != MIMode.Gdb) + return entries; + + if (EnableDebuginfod) + { + if (DebuginfodTimeout > 0) + { + string timeoutStr = DebuginfodTimeout.ToString(System.Globalization.CultureInfo.InvariantCulture); + entries.Add(new EnvironmentEntry("DEBUGINFOD_TIMEOUT", timeoutStr)); + entries.Add(new EnvironmentEntry("DEBUGINFOD_MAXTIME", timeoutStr)); + } + } + else + { + entries.Add(new EnvironmentEntry("DEBUGINFOD_URLS", "")); + } + + return entries; + } + + /// + /// Returns an 'env' command prefix for debuginfod settings, for use in shell-based remote commands. + /// Returns empty string if no env vars are needed. + /// + public string GetDebuginfodEnvironmentPrefix() + { + var entries = GetDebuginfodEnvironmentEntries(); + if (entries.Count == 0) + return string.Empty; + + var sb = new StringBuilder(); + sb.Append("env "); + foreach (var entry in entries) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "{0}={1} ", entry.Name, entry.Value); + } + return sb.ToString(); + } + public string GetOptionsString() { try @@ -1824,6 +1912,9 @@ protected void InitializeCommonOptions(Json.LaunchOptions.BaseOptions options) } this.UnknownBreakpointHandling = options.UnknownBreakpointHandling ?? UnknownBreakpointHandling.Throw; + this.EnableDebuginfod = options.Debuginfod?.Enabled ?? true; + int debuginfodTimeout = options.Debuginfod?.Timeout ?? 30; + this.DebuginfodTimeout = debuginfodTimeout >= 0 ? debuginfodTimeout : 30; } protected void InitializeCommonOptions(Xml.LaunchOptions.BaseLaunchOptions source) @@ -1855,6 +1946,8 @@ protected void InitializeCommonOptions(Xml.LaunchOptions.BaseLaunchOptions sourc this.ShowDisplayString = source.ShowDisplayString; this.WaitDynamicLibLoad = source.WaitDynamicLibLoad; + this.EnableDebuginfod = source.EnableDebuginfod; + this.DebuginfodTimeout = source.DebuginfodTimeout >= 0 ? source.DebuginfodTimeout : 30; this.SetupCommands = LaunchCommand.CreateCollection(source.SetupCommands); this.PostRemoteConnectCommands = LaunchCommand.CreateCollection(source.PostRemoteConnectCommands); diff --git a/src/MICore/LaunchOptions.xsd b/src/MICore/LaunchOptions.xsd index db1f833a9..5449b012e 100644 --- a/src/MICore/LaunchOptions.xsd +++ b/src/MICore/LaunchOptions.xsd @@ -207,6 +207,29 @@ + + + + If true (default), GDB's debuginfod support is enabled, allowing automatic downloading of debug symbols. + Set to false to disable debuginfod, which can prevent GDB from hanging when debuginfod servers are unavailable. + + + + + + + The timeout in seconds for debuginfod server requests. Default is 30. Only applies when EnableDebuginfod is true. + A value of 0 means no timeout override is applied (GDB/libdebuginfod defaults are used). + This sets the DEBUGINFOD_TIMEOUT and DEBUGINFOD_MAXTIME environment variables on the GDB process. + Applies to local, runInTerminal, and SSH attach transports. This setting is not applied for pipeTransport; set DEBUGINFOD_TIMEOUT and DEBUGINFOD_MAXTIME in the debugger's environment manually. + + + + + + + + diff --git a/src/MICore/LaunchOptions.xsd.types.designer.cs b/src/MICore/LaunchOptions.xsd.types.designer.cs index 0c0c83941..ab1b8c954 100644 --- a/src/MICore/LaunchOptions.xsd.types.designer.cs +++ b/src/MICore/LaunchOptions.xsd.types.designer.cs @@ -99,12 +99,24 @@ public partial class AndroidLaunchOptions { [System.ComponentModel.DefaultValueAttribute(true)] public bool WaitDynamicLibLoad; + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(true)] + public bool EnableDebuginfod; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(30)] + public int DebuginfodTimeout; + public AndroidLaunchOptions() { this.Attach = false; this.SourceRoots = ""; this.JVMPort = 65534; this.JVMHost = "localhost"; this.WaitDynamicLibLoad = true; + this.EnableDebuginfod = true; + this.DebuginfodTimeout = 30; } } @@ -454,8 +466,20 @@ public partial class BaseLaunchOptions { [System.ComponentModel.DefaultValueAttribute(true)] public bool WaitDynamicLibLoad; + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(true)] + public bool EnableDebuginfod; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(30)] + public int DebuginfodTimeout; + public BaseLaunchOptions() { this.WaitDynamicLibLoad = true; + this.EnableDebuginfod = true; + this.DebuginfodTimeout = 30; } } @@ -536,8 +560,20 @@ public partial class IOSLaunchOptions { [System.ComponentModel.DefaultValueAttribute(true)] public bool WaitDynamicLibLoad; + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(true)] + public bool EnableDebuginfod; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(30)] + public int DebuginfodTimeout; + public IOSLaunchOptions() { this.WaitDynamicLibLoad = true; + this.EnableDebuginfod = true; + this.DebuginfodTimeout = 30; } } diff --git a/src/MICore/Logger.cs b/src/MICore/Logger.cs index 671366294..361d34722 100644 --- a/src/MICore/Logger.cs +++ b/src/MICore/Logger.cs @@ -112,6 +112,11 @@ public void WriteLine(LogLevel level, string line) { WriteLineImpl(level, line); } + + if (HostLogger.IsFeedbackLogEnabled) + { + HostLogger.WriteFeedbackLog(line); + } } /// @@ -122,9 +127,10 @@ public void WriteLine(LogLevel level, string line) /// arguments to use in the format string public void WriteLine(LogLevel level, string format, params object[] args) { - if (s_isEnabled) + if (s_isEnabled || HostLogger.IsFeedbackLogEnabled) { - WriteLineImpl(level, format, args); + string message = string.Format(CultureInfo.CurrentCulture, format, args); + WriteLine(level, message); } } @@ -140,6 +146,11 @@ public void WriteTextBlock(LogLevel level, string prefix, string textBlock) { WriteTextBlockImpl(level, prefix, textBlock); } + + if (HostLogger.IsFeedbackLogEnabled) + { + HostLogger.WriteFeedbackLog((!string.IsNullOrEmpty(prefix) ? prefix : string.Empty) + textBlock); + } } /// diff --git a/src/MICore/Transports/LocalTransport.cs b/src/MICore/Transports/LocalTransport.cs index 71e6f0aa3..85c3e56bd 100755 --- a/src/MICore/Transports/LocalTransport.cs +++ b/src/MICore/Transports/LocalTransport.cs @@ -40,6 +40,11 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, proc.StartInfo.SetEnvironmentVariable("PATH", path); } + foreach (var entry in options.GetDebuginfodEnvironmentEntries()) + { + proc.StartInfo.SetEnvironmentVariable(entry.Name, entry.Value); + } + // Allow to execute custom commands before launching debugger. // For ex., instructing GDB not to break for certain signals if (options.DebuggerMIMode == MIMode.Gdb && !string.IsNullOrWhiteSpace(options.WorkingDirectory)) diff --git a/src/MICore/Transports/RunInTerminalTransport.cs b/src/MICore/Transports/RunInTerminalTransport.cs index 4418f105d..a9fbb8fc5 100644 --- a/src/MICore/Transports/RunInTerminalTransport.cs +++ b/src/MICore/Transports/RunInTerminalTransport.cs @@ -163,7 +163,8 @@ public override async void Init(ITransportCallback transportCallback, LaunchOpti } // Do not pass the launchOptions Environment entries as those are used for the debuggee only. - RunInTerminalLauncher launcher = new RunInTerminalLauncher(windowtitle, new List(0).AsReadOnly()); + var debuggerEnv = options.GetDebuginfodEnvironmentEntries().AsReadOnly(); + RunInTerminalLauncher launcher = new RunInTerminalLauncher(windowtitle, debuggerEnv); launcher.Launch( cmdArgs, diff --git a/src/MIDebugEngine/AD7.Impl/AD7BoundBreakpoint.cs b/src/MIDebugEngine/AD7.Impl/AD7BoundBreakpoint.cs index 39609f20e..ce45bc98e 100644 --- a/src/MIDebugEngine/AD7.Impl/AD7BoundBreakpoint.cs +++ b/src/MIDebugEngine/AD7.Impl/AD7BoundBreakpoint.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.Debugger.Interop; using System; using System.Diagnostics; +using System.Threading.Tasks; namespace Microsoft.MIDebugEngine { @@ -19,6 +20,8 @@ internal class AD7BoundBreakpoint : IDebugBoundBreakpoint2 private BoundBreakpoint _bp; private bool _deleted; + private enum_BP_PASSCOUNT_STYLE _passCountStyle; + private uint _passCountValue; internal bool Enabled { @@ -37,6 +40,7 @@ internal bool Enabled internal string Number { get { return _bp.Number; } } internal AD7PendingBreakpoint PendingBreakpoint { get { return _pendingBreakpoint; } } internal bool IsDataBreakpoint { get { return PendingBreakpoint.IsDataBreakpoint; } } + internal bool HasPassCount { get { return _passCountStyle != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE; } } public AD7BoundBreakpoint(AD7Engine engine, AD7PendingBreakpoint pendingBreakpoint, AD7BreakpointResolution breakpointResolution, BoundBreakpoint bp) { @@ -143,8 +147,7 @@ int IDebugBoundBreakpoint2.GetState(enum_BP_STATE[] pState) return Constants.S_OK; } - // The sample engine does not support hit counts on breakpoints. A real-world debugger will want to keep track - // of how many times a particular bound breakpoint has been hit and return it here. + // Returns the number of times this breakpoint has been hit. int IDebugBoundBreakpoint2.GetHitCount(out uint pdwHitCount) { pdwHitCount = _bp.HitCount; @@ -156,29 +159,91 @@ int IDebugBoundBreakpoint2.SetCondition(BP_CONDITION bpCondition) return ((IDebugPendingBreakpoint2)_pendingBreakpoint).SetCondition(bpCondition); // setting on the pending break will set the condition } - // The sample engine does not support hit counts on breakpoints. A real-world debugger will want to keep track - // of how many times a particular bound breakpoint has been hit. The debugger calls SetHitCount when the user - // resets a breakpoint's hit count. + // Called by the debugger when the user resets a breakpoint's hit count. int IDebugBoundBreakpoint2.SetHitCount(uint dwHitCount) { - throw new NotImplementedException(); + _bp.SetHitCount(dwHitCount); + _pendingBreakpoint?.RecomputeBreakAfter(dwHitCount); + + return Constants.S_OK; + } + + /// + /// Syncs the hit count from GDB's "times" field using a delta + /// to preserve any user-initiated hit count reset. + /// + internal void SetHitCount(uint hitCount) + { + _bp.SetGdbHitCount(hitCount); } - // The sample engine does not support pass counts on breakpoints. // This is used to specify the breakpoint hit count condition. int IDebugBoundBreakpoint2.SetPassCount(BP_PASSCOUNT bpPassCount) { - if (bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE) - { - Delete(); - _engine.Callback.OnBreakpointUnbound(this, enum_BP_UNBOUND_REASON.BPUR_BREAKPOINT_ERROR); - return Constants.E_FAIL; - } + _passCountStyle = bpPassCount.stylePassCount; + _passCountValue = bpPassCount.dwPassCount; return Constants.S_OK; } #endregion + internal uint HitCount => _bp.HitCount; + + internal void IncrementHitCount() + { + _bp.IncrementHitCount(); + } + + /// + /// Evaluates whether the debugger should break at this breakpoint based on the + /// current hit count and the configured pass count condition. + /// Must be called after IncrementHitCount. + /// + internal bool ShouldBreak() + { + uint hitCount = _bp.HitCount; + switch (_passCountStyle) + { + case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE: + return true; + case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL: + return hitCount == _passCountValue; + case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL_OR_GREATER: + return hitCount >= _passCountValue; + case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD: + return _passCountValue != 0 && (hitCount % _passCountValue) == 0; + default: + return true; + } + } + + /// + /// Re-sends -break-after to GDB after a pass count breakpoint fires. + /// MOD: skips passCount-1 hits. EQUAL: clears the ignore count. + /// + internal async Task RearmBreakAfterAsync() + { + uint ignoreCount; + switch (_passCountStyle) + { + case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD: + if (_passCountValue == 0) return; + ignoreCount = _passCountValue - 1; + break; + case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL: + ignoreCount = 0; + break; + default: + return; + } + + PendingBreakpoint bp = _pendingBreakpoint?.PendingBreakpoint; + if (bp != null && _engine?.DebuggedProcess != null) + { + await bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess); + } + } + internal void UpdateAddr(ulong addr) { _bp.Addr = addr; diff --git a/src/MIDebugEngine/AD7.Impl/AD7PendingBreakpoint.cs b/src/MIDebugEngine/AD7.Impl/AD7PendingBreakpoint.cs index f929c73f2..59025d4f3 100644 --- a/src/MIDebugEngine/AD7.Impl/AD7PendingBreakpoint.cs +++ b/src/MIDebugEngine/AD7.Impl/AD7PendingBreakpoint.cs @@ -115,11 +115,6 @@ private bool CanBind() return false; } } - if ((_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) != 0) - { - this.SetError(new AD7ErrorBreakpoint(this, ResourceStrings.UnsupportedPassCountBreakpoint, enum_BP_ERROR_TYPE.BPET_GENERAL_ERROR)); - return false; - } return true; } @@ -393,6 +388,40 @@ internal async Task BindAsync() } } } + + // Set ignore count via -break-after if a pass count is configured + if (_bp != null && (_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) != 0 + && _bpRequestInfo.bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE) + { + uint ignoreCount = ComputeIgnoreCount(_bpRequestInfo.bpPassCount.stylePassCount, _bpRequestInfo.bpPassCount.dwPassCount, 0); + await _bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess); + } + } + } + + /// + /// Computes the ignore count for -break-after, accounting for hits already + /// counted from a prior breakpoint (). + /// + private static uint ComputeIgnoreCount(enum_BP_PASSCOUNT_STYLE style, uint passCount, uint currentHits) + { + if (passCount == 0) + { + return 0; + } + + switch (style) + { + case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL: + case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL_OR_GREATER: + // Need to stop at hit N. Already counted currentHits, so skip (N - 1 - currentHits) more. + return passCount - 1 > currentHits ? passCount - 1 - currentHits : 0; + case enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD: + // Next stop is at the next multiple of passCount after currentHits. + uint remainder = currentHits % passCount; + return remainder == 0 ? passCount - 1 : passCount - 1 - remainder; + default: + return 0; } } @@ -406,6 +435,11 @@ internal AD7BoundBreakpoint AddBoundBreakpoint(BoundBreakpoint bp) } AD7BreakpointResolution breakpointResolution = new AD7BreakpointResolution(_engine, IsDataBreakpoint, bp.Addr, bp.FunctionName, bp.DocumentContext(_engine)); AD7BoundBreakpoint boundBreakpoint = new AD7BoundBreakpoint(_engine, this, breakpointResolution, bp); + // Apply pass count (hit count condition) from the original request to the bound breakpoint + if ((_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) != 0) + { + ((IDebugBoundBreakpoint2)boundBreakpoint).SetPassCount(_bpRequestInfo.bpPassCount); + } //check can bind one last time. If the pending breakpoint was deleted before now, we need to clean up gdb side if (CanBind()) { @@ -645,17 +679,77 @@ int IDebugPendingBreakpoint2.SetCondition(BP_CONDITION bpCondition) return Constants.S_OK; } - // The sample engine does not support pass counts on breakpoints. int IDebugPendingBreakpoint2.SetPassCount(BP_PASSCOUNT bpPassCount) { - if (bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE) + _bpRequestInfo.bpPassCount = bpPassCount; + _bpRequestInfo.dwFields |= enum_BPREQI_FIELDS.BPREQI_PASSCOUNT; + + PendingBreakpoint bp = null; + lock (_boundBreakpoints) { - this.SetError(new AD7ErrorBreakpoint(this, ResourceStrings.UnsupportedPassCountBreakpoint, enum_BP_ERROR_TYPE.BPET_GENERAL_ERROR), true); - return Constants.E_FAIL; + foreach (AD7BoundBreakpoint boundBp in _boundBreakpoints) + { + ((IDebugBoundBreakpoint2)boundBp).SetPassCount(bpPassCount); + } + if (_bp != null) + { + bp = _bp; + } + } + + // When the pass count is cleared (NONE), send ignore count 0 to clear + // any stale GDB ignore count from the previous condition. + if (bp != null) + { + uint ignoreCount = 0; + if (bpPassCount.stylePassCount != enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE) + { + uint currentHits = 0; + lock (_boundBreakpoints) + { + foreach (AD7BoundBreakpoint boundBp in _boundBreakpoints) + { + uint hc; + if (((IDebugBoundBreakpoint2)boundBp).GetHitCount(out hc) == Constants.S_OK && hc > currentHits) + { + currentHits = hc; + } + } + } + ignoreCount = ComputeIgnoreCount(bpPassCount.stylePassCount, bpPassCount.dwPassCount, currentHits); + } + _engine.DebuggedProcess.WorkerThread.RunOperation(() => + { + _engine.DebuggedProcess.AddInternalBreakAction( + () => bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess) + ); + }); } return Constants.S_OK; } + /// + /// Re-sends -break-after to GDB after a hit count reset. + /// + internal void RecomputeBreakAfter(uint currentHits) + { + if (_bp == null + || (_bpRequestInfo.dwFields & enum_BPREQI_FIELDS.BPREQI_PASSCOUNT) == 0 + || _bpRequestInfo.bpPassCount.stylePassCount == enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE) + { + return; + } + + PendingBreakpoint bp = _bp; + uint ignoreCount = ComputeIgnoreCount(_bpRequestInfo.bpPassCount.stylePassCount, _bpRequestInfo.bpPassCount.dwPassCount, currentHits); + _engine.DebuggedProcess.WorkerThread.RunOperation(() => + { + _engine.DebuggedProcess.AddInternalBreakAction( + () => bp.SetBreakAfterAsync(ignoreCount, _engine.DebuggedProcess) + ); + }); + } + // Toggles the virtualized state of this pending breakpoint. When a pending breakpoint is virtualized, // the debug engine will attempt to bind it every time new code loads into the program. // The sample engine will does not support this. diff --git a/src/MIDebugEngine/Engine.Impl/BreakpointManager.cs b/src/MIDebugEngine/Engine.Impl/BreakpointManager.cs index 39090e1f0..47d587317 100644 --- a/src/MIDebugEngine/Engine.Impl/BreakpointManager.cs +++ b/src/MIDebugEngine/Engine.Impl/BreakpointManager.cs @@ -71,6 +71,30 @@ public async Task BreakpointModified(object sender, EventArgs args) return; } + // Sync GDB's hit count ("times") for pass count breakpoints. + // e.g. =breakpoint-modified,bkpt={number="1",...,times="5",ignore="2",...} + string timesStr = bkpt.TryFindString("times"); + if (!string.IsNullOrEmpty(timesStr) && uint.TryParse(timesStr, out uint times)) + { + foreach (AD7BoundBreakpoint boundBp in pending.EnumBoundBreakpoints()) + { + if (boundBp.HasPassCount) + { + uint previousHitCount = boundBp.HitCount; + boundBp.SetHitCount(times); + + // Re-arm GDB's ignore count so it skips to the next target hit. + // HitCount guard: -break-after itself triggers =breakpoint-modified. + // ShouldBreak guard: GDB also emits =breakpoint-modified on ignored + // hits, and re-arming then would shift the next stop. + if (boundBp.HitCount != previousHitCount && boundBp.ShouldBreak()) + { + await boundBp.RearmBreakAfterAsync(); + } + } + } + } + string warning = bkpt.TryFindString("warning"); if (!string.IsNullOrEmpty(warning)) { @@ -212,7 +236,15 @@ public AD7BoundBreakpoint[] FindHitBreakpoints(string bkptno, ulong addr, /*OPTI continue; } - hitBoundBreakpoints.Add(currBoundBp); + // Pass count breakpoints get their hit count from =breakpoint-modified. + if (!currBoundBp.HasPassCount) + { + currBoundBp.IncrementHitCount(); + } + if (currBoundBp.ShouldBreak()) + { + hitBoundBreakpoints.Add(currBoundBp); + } } fContinue = (hitBoundBreakpoints.Count == 0 && hitBps.Length != 0); diff --git a/src/MIDebugEngine/Engine.Impl/Breakpoints.cs b/src/MIDebugEngine/Engine.Impl/Breakpoints.cs index 1f22ae702..7d370cdf0 100644 --- a/src/MIDebugEngine/Engine.Impl/Breakpoints.cs +++ b/src/MIDebugEngine/Engine.Impl/Breakpoints.cs @@ -353,6 +353,18 @@ internal async Task SetConditionAsync(string expr, DebuggedProcess process) await process.MICommandFactory.BreakCondition(Number, expr); } } + + /// + /// Sends -break-after to set an ignore count on this breakpoint. + /// + internal async Task SetBreakAfterAsync(uint count, DebuggedProcess process) + { + if (process.ProcessState != MICore.ProcessState.Exited) + { + return await process.MICommandFactory.BreakAfter(Number, count); + } + return null; + } } internal class BoundBreakpoint @@ -363,6 +375,8 @@ internal class BoundBreakpoint /*OPTIONAL*/ public string FunctionName { get; private set; } internal uint HitCount { get; private set; } + private uint _rawGdbHitCount; + private readonly object _hitCountLock = new object(); internal bool Enabled { get; set; } internal bool IsDataBreakpoint { get { return _parent.AD7breakpoint.IsDataBreakpoint; } } private MITextPosition _textPosition; @@ -398,6 +412,7 @@ internal BoundBreakpoint(PendingBreakpoint parent, ulong addr, /*optional*/ Tupl internal BoundBreakpoint(PendingBreakpoint parent, ulong addr, uint size, string bkptno) { Addr = addr; + HitCount = 0; Enabled = true; this.Number = bkptno; _parent = parent; @@ -434,5 +449,37 @@ internal uint Line _textPosition = new MITextPosition(_textPosition.FileName, value); } } + + internal void IncrementHitCount() + { + lock (_hitCountLock) + { + HitCount++; + } + } + + internal void SetHitCount(uint count) + { + lock (_hitCountLock) + { + HitCount = count; + } + } + + /// + /// Applies the delta from GDB's "times" field, preserving user-initiated resets. + /// + internal void SetGdbHitCount(uint gdbTimes) + { + lock (_hitCountLock) + { + if (gdbTimes >= _rawGdbHitCount) + { + uint delta = gdbTimes - _rawGdbHitCount; + HitCount += delta; + } + _rawGdbHitCount = gdbTimes; + } + } } } diff --git a/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs b/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs index aaabb974a..b5e8d1bd3 100755 --- a/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs +++ b/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs @@ -624,7 +624,10 @@ private async Task> GetInitializeCommands() if (_launchOptions.DebuggerMIMode == MIMode.Gdb) { commands.Add(new LaunchCommand("-interpreter-exec console \"set pagination off\"")); - commands.Add(new LaunchCommand("set debuginfod enabled on", ignoreFailures:true)); + if (_launchOptions.EnableDebuginfod) + { + commands.Add(new LaunchCommand("set debuginfod enabled on", ignoreFailures: true)); + } } // When user specifies loading directives then the debugger cannot auto load symbols, the MIEngine must intervene at each solib-load event and make a determination @@ -944,52 +947,67 @@ private void DetermineAndAddExecutablePathCommand(IList commands, // Runs a shell command to get the full path of the exe. // /proc file system does not exist on OSX. And querying lsof on privilaged process fails with no output on Mac, while on Linux the command succeedes with // embedded error text in lsof output like "(readlink error)". - string absoluteExePath; // Must have a processId Debug.Assert(_launchOptions.ProcessId.HasValue, "ProcessId should have a value."); + string shellCommand; if (launchOptions.UnixPort.IsOSX()) { // Usually the first FD=txt in the output of lsof points to the executable. - absoluteExePath = string.Format(CultureInfo.InvariantCulture, "shell lsof -p {0} | awk '$4 == \"txt\" {{ print $9 }}'|awk 'NR==1 {{print $1}}'", _launchOptions.ProcessId.Value); + shellCommand = string.Format(CultureInfo.InvariantCulture, "lsof -p {0} | awk '$4 == \"txt\" {{ print $9 }}'|awk 'NR==1 {{print $1}}'", _launchOptions.ProcessId.Value); } else if (launchOptions.UnixPort.IsLinux()) { - absoluteExePath = string.Format(CultureInfo.InvariantCulture, @"shell readlink -f /proc/{0}/exe", _launchOptions.ProcessId.Value); + shellCommand = string.Format(CultureInfo.InvariantCulture, "readlink -f /proc/{0}/exe", _launchOptions.ProcessId.Value); } else { throw new LaunchErrorException(ResourceStrings.Error_UnsupportedPlatform); } - Action failureHandler = (string miError) => + string commandOutput; + int exitCode; + try { - string message = string.Format(CultureInfo.CurrentCulture, ResourceStrings.Error_FailedToGetExePath, miError); + launchOptions.UnixPort.ExecuteSyncCommand( + string.Format(CultureInfo.CurrentCulture, ResourceStrings.DeterminingExecutablePath, _launchOptions.ProcessId.Value), + shellCommand, + out commandOutput, + timeout: 30000, + out exitCode); + } + catch (TimeoutException) + { + string message = string.Format(CultureInfo.CurrentCulture, ResourceStrings.Error_FailedToGetExePath, + string.Format(CultureInfo.CurrentCulture, ResourceStrings.Error_GetExePathTimedOut, shellCommand)); throw new LaunchErrorException(message); - }; + } - Func successHandler = async (string exePath) => + if (exitCode != 0 || string.IsNullOrWhiteSpace(commandOutput)) { - string trimmedExePath = exePath.Trim(); - try - { - // If the folder contains a space, we need to quote the path. - if (trimmedExePath.Contains(' ')) - { - trimmedExePath = "\"" + trimmedExePath + "\""; - } + string errorDetail = !string.IsNullOrWhiteSpace(commandOutput) + ? commandOutput.Trim() + : string.Format(CultureInfo.CurrentCulture, ResourceStrings.Error_GetExePathFailed, shellCommand, exitCode); + string message = string.Format(CultureInfo.CurrentCulture, ResourceStrings.Error_FailedToGetExePath, errorDetail); + throw new LaunchErrorException(message); + } - await CmdAsync("-file-exec-and-symbols " + trimmedExePath, ResultClass.done); - } - catch (UnexpectedMIResultException miException) - { - string message = string.Format(CultureInfo.CurrentCulture, ResourceStrings.Error_ExePathInvalid, trimmedExePath, MICommandFactory.Name, miException.MIError); - throw new LaunchErrorException(message); - } + string trimmedExePath = commandOutput.Trim(); + + // If the folder contains a space, we need to quote the path. + if (trimmedExePath.Contains(' ')) + { + trimmedExePath = "\"" + trimmedExePath + "\""; + } + + Action failureHandler = (string miError) => + { + string message = string.Format(CultureInfo.CurrentCulture, ResourceStrings.Error_ExePathInvalid, trimmedExePath, MICommandFactory.Name, miError); + throw new LaunchErrorException(message); }; - commands.Add(new LaunchCommand(absoluteExePath, ignoreFailures: false, failureHandler: failureHandler, successHandler: successHandler)); + commands.Add(new LaunchCommand("-file-exec-and-symbols " + trimmedExePath, ignoreFailures: false, failureHandler: failureHandler)); } private TargetArchitecture DefaultArch() @@ -2121,6 +2139,18 @@ internal async Task ReadProcessMemory(ulong address, uint count, byte[] by internal async Task> FindValidMemoryRange(ulong address, uint count, int offset) { + // Debugging coredump with LLDB doesn't work well with '-data-read-memory-bytes', the function + // returns an error. Namely 'LLDB unable to read entire memory block of n bytes at address 0x0....'. + // As a result we have to skip calling '-data-read-memory-bytes' and just use the address + offet and count + // in order to get the memory range. + if (IsCoreDump && _launchOptions.DebuggerMIMode == MIMode.Lldb) + { + ulong startAddress = (ulong)((long)address + offset); + ulong endAddress = startAddress + count; + + return new Tuple(startAddress, endAddress); + } + var ret = new Tuple(0, 0); // init to an empty range string cmd = String.Format(CultureInfo.InvariantCulture, "-data-read-memory-bytes -o {0} {1} {2}", offset.ToString(CultureInfo.InvariantCulture), EngineUtils.AsAddr(address, Is64BitArch), count.ToString(CultureInfo.InvariantCulture)); Results results = await CmdAsync(cmd, ResultClass.None); diff --git a/src/MIDebugEngine/Natvis.Impl/Natvis.cs b/src/MIDebugEngine/Natvis.Impl/Natvis.cs index 41711e6fc..edf7f702a 100755 --- a/src/MIDebugEngine/Natvis.Impl/Natvis.cs +++ b/src/MIDebugEngine/Natvis.Impl/Natvis.cs @@ -202,6 +202,13 @@ internal class VisualizerInfo public VisualizerType Visualizer { get; private set; } public Dictionary ScopedNames { get; private set; } + /// + /// Intrinsics defined in this type block, keyed by name. + /// Stored as IntrinsicType so that Parameter[] is available at call time + /// for argument substitution in parametrized intrinsics. + /// + public Dictionary Intrinsics { get; } + public VisualizerId[] GetUIVisualizers() { return this.Visualizer.Items.Where((i) => i is UIVisualizerItemType).Select(i => @@ -220,12 +227,26 @@ public VisualizerInfo(VisualizerType viz, TypeName name) { ScopedNames["$T" + (i + 1).ToString(CultureInfo.InvariantCulture)] = name.Args[i].FullyQualifiedName; } + // collect intrinsics defined in this type block + Intrinsics = new Dictionary(); + if (viz.Items != null) + { + foreach (var item in viz.Items) + { + if (item is IntrinsicType intrinsic && !string.IsNullOrEmpty(intrinsic.Name)) + { + Intrinsics[intrinsic.Name] = intrinsic; + } + } + } } } private static Regex s_variableName = new Regex("[a-zA-Z$_][a-zA-Z$_0-9]*"); private static Regex s_subfieldNameHere = new Regex(@"\G((\.|->)[a-zA-Z$_][a-zA-Z$_0-9]*)+"); private static Regex s_expression = new Regex(@"^\{[^\}]*\}"); + private static readonly Regex s_moduleQualifiedPrefix = new Regex(@"\w+(?:\.\w+)*\.(?:dll|exe)!", RegexOptions.IgnoreCase); + private static readonly Regex s_intrinsicCallPattern = new Regex(@"\b(\w+)\s*\("); private List _typeVisualizers; private DebuggedProcess _process; private HostConfigurationStore _configStore; @@ -456,11 +477,11 @@ private bool LoadFile(string path) { DisplayStringType display = item as DisplayStringType; // e.g. {{ size={_Mypair._Myval2._Mylast - _Mypair._Myval2._Myfirst} }} - if (!EvalCondition(display.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(display.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } - return (FormatValue(display.Value, variable, visualizer.ScopedNames), visualizer.GetUIVisualizers()); + return (FormatValue(display.Value, variable, visualizer.ScopedNames, visualizer.Intrinsics), visualizer.GetUIVisualizers()); } } } @@ -582,17 +603,17 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) if (i is ItemType && !(variable is PaginatedVisualizerWrapper)) // we do not want to repeatedly display other ItemTypes when expanding the "[More...]" node { ItemType item = (ItemType)i; - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } - IVariableInformation expr = GetExpression(item.Value, variable, visualizer.ScopedNames, item.Name); + IVariableInformation expr = GetExpression(item.Value, variable, visualizer.ScopedNames, item.Name, visualizer.Intrinsics); children.Add(expr); } else if (i is ArrayItemsType) { ArrayItemsType item = (ArrayItemsType)i; - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } @@ -606,7 +627,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) totalSize = 1; if (!int.TryParse(item.Rank, NumberStyles.None, CultureInfo.InvariantCulture, out rank)) { - string expressionValue = GetExpressionValue(item.Rank, variable, visualizer.ScopedNames); + string expressionValue = GetExpressionValue(item.Rank, variable, visualizer.ScopedNames, visualizer.Intrinsics); rank = Int32.Parse(expressionValue, CultureInfo.InvariantCulture); } if (rank <= 0) @@ -618,7 +639,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { // replace $i with Item.Rank here before passing it into GetExpressionValue string substitute = item.Size.Replace("$i", idx.ToString(CultureInfo.InvariantCulture)); - string val = GetExpressionValue(substitute, variable, visualizer.ScopedNames); + string val = GetExpressionValue(substitute, variable, visualizer.ScopedNames, visualizer.Intrinsics); uint tmp = MICore.Debugger.ParseUint(val, throwOnError: true); dimensions[idx] = tmp; totalSize *= tmp; @@ -626,7 +647,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) } else { - string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames); + string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames, visualizer.Intrinsics); totalSize = MICore.Debugger.ParseUint(val, throwOnError: true); } @@ -639,9 +660,9 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) ValuePointerType[] vptrs = item.ValuePointer; foreach (var vp in vptrs) { - if (EvalCondition(vp.Condition, variable, visualizer.ScopedNames)) + if (EvalCondition(vp.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { - IVariableInformation ptrExpr = GetExpression("*(" + vp.Value + ")", variable, visualizer.ScopedNames); + IVariableInformation ptrExpr = GetExpression("*(" + vp.Value + ")", variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); string typename = ptrExpr.TypeName; if (String.IsNullOrWhiteSpace(typename)) { @@ -669,7 +690,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) arrayBuilder.Append("))"); string arrayStr = arrayBuilder.ToString(); - IVariableInformation arrayExpr = GetExpression(arrayStr, variable, visualizer.ScopedNames); + IVariableInformation arrayExpr = GetExpression(arrayStr, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); arrayExpr.EnsureChildren(); if (arrayExpr.CountChildren != 0) { @@ -696,7 +717,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) else if (i is TreeItemsType) { TreeItemsType item = (TreeItemsType)i; - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } @@ -709,7 +730,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { continue; } - string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames); + string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames, visualizer.Intrinsics); uint size = MICore.Debugger.ParseUint(val, throwOnError: true); IVariableInformation headVal; if (variable is TreeContinueWrapper tcw) @@ -718,7 +739,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) } else { - headVal = GetExpression(item.HeadPointer, variable, visualizer.ScopedNames); + headVal = GetExpression(item.HeadPointer, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); } ulong head = MICore.Debugger.ParseAddr(headVal.Value); var content = new List(); @@ -736,9 +757,9 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { getValue = (v) => v.FindChildByName(item.ValueNode.Value); } - else if (GetExpression(item.ValueNode.Value, headVal, visualizer.ScopedNames) != null) + else if (GetExpression(item.ValueNode.Value, headVal, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics) != null) { - getValue = (v) => GetExpression(item.ValueNode.Value, v, visualizer.ScopedNames); + getValue = (v) => GetExpression(item.ValueNode.Value, v, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); } if (goLeft == null || goRight == null || getValue == null) { @@ -768,9 +789,9 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) // m_element // LinkedListItemsType item = (LinkedListItemsType)i; - if (String.IsNullOrWhiteSpace(item.Condition)) + if (!String.IsNullOrWhiteSpace(item.Condition)) { - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) continue; } if (String.IsNullOrWhiteSpace(item.HeadPointer) || String.IsNullOrWhiteSpace(item.NextPointer)) @@ -784,7 +805,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) uint size = MAX_EXPAND; if (!String.IsNullOrWhiteSpace(item.Size)) { - string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames); + string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames, visualizer.Intrinsics); size = MICore.Debugger.ParseUint(val); } IVariableInformation headVal; @@ -794,7 +815,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) } else { - headVal = GetExpression(item.HeadPointer, variable, visualizer.ScopedNames); + headVal = GetExpression(item.HeadPointer, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); } ulong head = MICore.Debugger.ParseAddr(headVal.Value); var content = new List(); @@ -813,10 +834,10 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) } else { - var value = GetExpression(item.ValueNode, headVal, visualizer.ScopedNames); + var value = GetExpression(item.ValueNode, headVal, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); if (value != null && !value.Error) { - getValue = (v) => GetExpression(item.ValueNode, v, visualizer.ScopedNames); + getValue = (v) => GetExpression(item.ValueNode, v, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); } } if (goNext == null || getValue == null) @@ -845,7 +866,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) // *(_M_vector._M_array[$i]) // IndexListItemsType item = (IndexListItemsType)i; - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } @@ -859,9 +880,9 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { if (string.IsNullOrWhiteSpace(s.Value)) continue; - if (EvalCondition(s.Condition, variable, visualizer.ScopedNames)) + if (EvalCondition(s.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { - string val = GetExpressionValue(s.Value, variable, visualizer.ScopedNames); + string val = GetExpressionValue(s.Value, variable, visualizer.ScopedNames, visualizer.Intrinsics); size = MICore.Debugger.ParseUint(val); break; } @@ -875,9 +896,9 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { if (string.IsNullOrWhiteSpace(v.Value)) continue; - if (EvalCondition(v.Condition, variable, visualizer.ScopedNames)) + if (EvalCondition(v.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { - string processedExpr = ReplaceNamesInExpression(v.Value, variable, visualizer.ScopedNames); + string processedExpr = ReplaceNamesInExpression(v.Value, variable, visualizer.ScopedNames, visualizer.Intrinsics); Dictionary indexDic = new Dictionary(); uint currentIndex = 0; if (variable is PaginatedVisualizerWrapper pvwVariable) @@ -917,7 +938,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) // if (item.Condition != null) { - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } @@ -926,7 +947,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { continue; } - var expand = GetExpression(item.Value, variable, visualizer.ScopedNames); + var expand = GetExpression(item.Value, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); var eChildren = Expand(expand); if (eChildren != null) { @@ -1094,12 +1115,12 @@ private static string BaseName(string type) return type; } - private bool EvalCondition(string condition, IVariableInformation variable, IDictionary scopedNames) + private bool EvalCondition(string condition, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null) { bool res = true; if (!String.IsNullOrWhiteSpace(condition)) { - string exprValue = GetExpressionValue(condition, variable, scopedNames); + string exprValue = GetExpressionValue(condition, variable, scopedNames, intrinsics); bool exprBool = false; int exprInt = 0; @@ -1237,7 +1258,7 @@ private VisualizerInfo FindType(IVariableInformation variable) return null; } - private string FormatValue(string format, IVariableInformation variable, IDictionary scopedNames) + private string FormatValue(string format, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null) { if (String.IsNullOrWhiteSpace(format)) { @@ -1258,7 +1279,7 @@ private string FormatValue(string format, IVariableInformation variable, IDictio Match m = s_expression.Match(format.Substring(i)); if (m.Success) { - string exprValue = GetExpressionValue(format.Substring(i + 1, m.Length - 2), variable, scopedNames); + string exprValue = GetExpressionValue(format.Substring(i + 1, m.Length - 2), variable, scopedNames, intrinsics); value.Append(exprValue); i += m.Length - 1; } @@ -1390,8 +1411,176 @@ internal static bool IsPrecededByMemberAccessOperator(string expression, int ind return false; } - private string ReplaceNamesInExpression(string expression, IVariableInformation variable, IDictionary scopedNames) + /// + /// Find the index of the closing parenthesis that matches the opening paren at . + /// Returns -1 if not found. + /// + internal static int FindMatchingParen(string s, int openPos) + { + int depth = 0; + for (int i = openPos; i < s.Length; i++) + { + if (s[i] == '(') depth++; + else if (s[i] == ')') + { + depth--; + if (depth == 0) return i; + } + } + return -1; + } + + /// + /// Split a comma-separated argument list (the text inside the parentheses) + /// at depth-zero commas only, so nested calls like f(a,b) are kept intact. + /// Only parentheses and square brackets are treated as nesting — angle brackets + /// are intentionally excluded because '>' is also a comparison operator and + /// NatVis intrinsic arguments are never C++ template types. + /// + internal static List SplitArguments(string argsText) + { + var result = new List(); + int depth = 0; + int start = 0; + for (int i = 0; i < argsText.Length; i++) + { + char c = argsText[i]; + if (c == '(' || c == '[') depth++; + else if (c == ')' || c == ']') depth--; + else if (c == ',' && depth == 0) + { + result.Add(argsText.Substring(start, i - start).Trim()); + start = i + 1; + } + } + string last = argsText.Substring(start).Trim(); + if (last.Length > 0 || result.Count > 0) + result.Add(last); + return result; + } + + /// + /// Substitute named parameters in an intrinsic expression with the supplied argument + /// values. Each parameter name is replaced as a whole word so that e.g. "val" inside + /// "interval" is not touched. + /// + internal static string SubstituteIntrinsicParameters(string body, IntrinsicParameterType[] parameters, List args) { + if (parameters == null || parameters.Length == 0) + return body; + + string result = body; + for (int i = 0; i < parameters.Length && i < args.Count; i++) + { + string paramName = parameters[i].Name; + if (string.IsNullOrEmpty(paramName)) continue; + // whole-word replacement + result = Regex.Replace(result, @"\b" + Regex.Escape(paramName) + @"\b", args[i]); + } + return result; + } + + /// + /// Expand intrinsic calls in into their C++ equivalents. + /// For example, given an intrinsic day() = "jd - 2440588" the call day() + 1 + /// becomes (jd - 2440588) + 1. + /// Recurses up to times to handle chained calls. + /// + internal static string ResolveIntrinsicCalls(string expression, IDictionary intrinsics, int maxDepth = 20) + { + if (string.IsNullOrEmpty(expression) || intrinsics == null || intrinsics.Count == 0 || maxDepth <= 0) + return expression; + + bool anyReplaced = false; + string result = expression; + + // We scan left-to-right and build the output string incrementally. + // Using a loop rather than Regex.Replace because we need to consume the + // matched argument list (which the regex does not capture fully). + // s_intrinsicCallPattern matches a word immediately followed by '('; + // \b on the right side is intentionally absent — the '(' is the boundary. + int pos = 0; + var sb = new StringBuilder(); + + while (pos < result.Length) + { + Match m = s_intrinsicCallPattern.Match(result, pos); + if (!m.Success) break; + + string name = m.Groups[1].Value; + + // Skip if the identifier is a member or scope access (.name, ->name, ::name). + // \b matches after '.' / '>' / ':' because those are non-word characters, so + // we must guard here to avoid re-expanding e.g. _q_value.value() when "value" + // is also an intrinsic name. + if (IsPrecededByMemberAccessOperator(result, m.Index)) + { + sb.Append(result, pos, m.Index - pos + name.Length); + pos = m.Index + name.Length; + continue; + } + + if (!intrinsics.TryGetValue(name, out IntrinsicType intrinsic)) + { + // Not one of our intrinsics — skip past the identifier and keep going + sb.Append(result, pos, m.Index - pos + name.Length); + pos = m.Index + name.Length; + continue; + } + + // Found an intrinsic call. Locate the matching close paren. + int openParen = m.Index + m.Length - 1; // position of '(' + int closeParen = FindMatchingParen(result, openParen); + if (closeParen < 0) + { + // Malformed — leave as-is + sb.Append(result, pos, m.Index - pos + name.Length); + pos = m.Index + name.Length; + continue; + } + + // Append everything before the call + sb.Append(result, pos, m.Index - pos); + + // Extract and split arguments + string argsText = result.Substring(openParen + 1, closeParen - openParen - 1); + List args = string.IsNullOrWhiteSpace(argsText) + ? new List() + : SplitArguments(argsText); + + // Expand: substitute parameters into the intrinsic expression body + string body = intrinsic.Expression ?? string.Empty; + body = SubstituteIntrinsicParameters(body, intrinsic.Parameter, args); + + // Wrap in parens to preserve operator precedence + sb.Append('('); + sb.Append(body); + sb.Append(')'); + + pos = closeParen + 1; + anyReplaced = true; + } + + // Append any trailing text after the last match + sb.Append(result, pos, result.Length - pos); + result = sb.ToString(); + + // Recurse if we expanded anything (handles chained intrinsics) + if (anyReplaced) + result = ResolveIntrinsicCalls(result, intrinsics, maxDepth - 1); + + return result; + } + + private string ReplaceNamesInExpression(string expression, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null) + { + // Strip Windows dll!-qualified type prefixes (e.g. Qt6Cored.dll!) + // for GDB/LLDB compatibility — meaningless outside Windows + expression = s_moduleQualifiedPrefix.Replace(expression, ""); + + // Expand intrinsic calls (e.g. day(), memberOffset(3)) into plain C++ expressions + expression = ResolveIntrinsicCalls(expression, intrinsics); + return ProcessNamesInString(expression, new Substitute[] { (m)=> { @@ -1427,17 +1616,17 @@ private string ReplaceNamesInExpression(string expression, IVariableInformation /// /// /// - private IVariableInformation GetExpression(string expression, IVariableInformation variable, IDictionary scopedNames, string displayName = null) + private IVariableInformation GetExpression(string expression, IVariableInformation variable, IDictionary scopedNames, string displayName = null, IDictionary intrinsics = null) { - string processedExpr = ReplaceNamesInExpression(expression, variable, scopedNames); + string processedExpr = ReplaceNamesInExpression(expression, variable, scopedNames, intrinsics); IVariableInformation expressionVariable = new VariableInformation(processedExpr, variable, _process.Engine, displayName); expressionVariable.SyncEval(); return expressionVariable; } - private string GetExpressionValue(string expression, IVariableInformation variable, IDictionary scopedNames) + private string GetExpressionValue(string expression, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null) { - string processedExpr = ReplaceNamesInExpression(expression, variable, scopedNames); + string processedExpr = ReplaceNamesInExpression(expression, variable, scopedNames, intrinsics); IVariableInformation expressionVariable = new VariableInformation(processedExpr, variable, _process.Engine, null); expressionVariable.SyncEval(); diff --git a/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs b/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs index e7b922409..c3fdb38e0 100644 --- a/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs +++ b/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs @@ -1636,6 +1636,7 @@ public CustomVisualizerType[] CustomVisualizer { /// [System.Xml.Serialization.XmlElementAttribute("DisplayString", typeof(DisplayStringType))] [System.Xml.Serialization.XmlElementAttribute("Expand", typeof(ExpandType1))] + [System.Xml.Serialization.XmlElementAttribute("Intrinsic", typeof(IntrinsicType))] [System.Xml.Serialization.XmlElementAttribute("StringView", typeof(StringViewType))] [System.Xml.Serialization.XmlElementAttribute("UIVisualizer", typeof(UIVisualizerItemType))] public object[] Items { @@ -1822,4 +1823,96 @@ public string Value { } } } + + /// + /// Represents a <Parameter> child element of <Intrinsic>. + /// + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")] + public partial class IntrinsicParameterType { + + private string nameField; + private string typeField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Name { + get { return this.nameField; } + set { this.nameField = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Type { + get { return this.typeField; } + set { this.typeField = value; } + } + } + + /// + /// Represents an <Intrinsic> element — an inline named expression that can be + /// called by name (with optional arguments) in any subsequent NatVis expression + /// within the same <Type> block. + /// + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")] + public partial class IntrinsicType { + + private string nameField; + private string expressionField; + private bool optionalField; + private bool optionalFieldSpecified; + private string categoryField; + private string moduleNameField; + private IntrinsicParameterType[] parameterField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Name { + get { return this.nameField; } + set { this.nameField = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Expression { + get { return this.expressionField; } + set { this.expressionField = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public bool Optional { + get { return this.optionalField; } + set { this.optionalField = value; } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool OptionalSpecified { + get { return this.optionalFieldSpecified; } + set { this.optionalFieldSpecified = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Category { + get { return this.categoryField; } + set { this.categoryField = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string ModuleName { + get { return this.moduleNameField; } + set { this.moduleNameField = value; } + } + + /// + [System.Xml.Serialization.XmlElementAttribute("Parameter")] + public IntrinsicParameterType[] Parameter { + get { return this.parameterField; } + set { this.parameterField = value; } + } + } } diff --git a/src/MIDebugEngine/Natvis.Impl/natvis.xsd b/src/MIDebugEngine/Natvis.Impl/natvis.xsd index 7b1bc61b1..65f71c429 100644 --- a/src/MIDebugEngine/Natvis.Impl/natvis.xsd +++ b/src/MIDebugEngine/Natvis.Impl/natvis.xsd @@ -577,6 +577,34 @@ + + + Defines a single parameter of an Intrinsic function, giving it a name and an optional C++ type hint. + + + + + + + + + Defines an inline named expression (an "intrinsic") that can be called by name within any subsequent + NatVis expression in the same <Type> block. For example: + <Intrinsic Name="day" Expression="jd - 2440588"/> + allows writing {day()} in a DisplayString. Parameters are substituted positionally: + <Intrinsic Name="offset" Expression="sizeof(int) * n"><Parameter Name="n" Type="int"/></Intrinsic> + + + + + + + + + + + + Specifies a visualizer entry which customizes the debugger view of a type. @@ -589,6 +617,7 @@ + diff --git a/src/MIDebugEngine/ResourceStrings.Designer.cs b/src/MIDebugEngine/ResourceStrings.Designer.cs index 12d25ce8d..fd9cda12e 100755 --- a/src/MIDebugEngine/ResourceStrings.Designer.cs +++ b/src/MIDebugEngine/ResourceStrings.Designer.cs @@ -118,6 +118,33 @@ internal static string Error_FailedToGetExePath { } } + /// + /// Looks up a localized string similar to Determining executable path for process {0}. + /// + internal static string DeterminingExecutablePath { + get { + return ResourceManager.GetString("DeterminingExecutablePath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' timed out. The remote system may be unresponsive.. + /// + internal static string Error_GetExePathTimedOut { + get { + return ResourceManager.GetString("Error_GetExePathTimedOut", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' failed with exit code {1}. Verify the process is still running and accessible.. + /// + internal static string Error_GetExePathFailed { + get { + return ResourceManager.GetString("Error_GetExePathFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to obtain target architecture.. /// diff --git a/src/MIDebugEngine/ResourceStrings.resx b/src/MIDebugEngine/ResourceStrings.resx index ea389d970..e6b9e7cc2 100755 --- a/src/MIDebugEngine/ResourceStrings.resx +++ b/src/MIDebugEngine/ResourceStrings.resx @@ -241,6 +241,18 @@ This may occur if the process's executable was changed after the process was sta Failed to get executable path with error '{0}'. 0 = failure message + + Determining executable path for process {0} + {0} = process ID + + + '{0}' timed out. The remote system may be unresponsive. + {0} = shell command that was executed + + + '{0}' failed with exit code {1}. Verify the process is still running and accessible. + {0} = shell command that was executed, {1} = exit code + Attaching to process {0} with {1} failed because of insufficient privileges with error message '{2}'. diff --git a/src/MIDebugEngineUnitTests/NatvisIntrinsicTest.cs b/src/MIDebugEngineUnitTests/NatvisIntrinsicTest.cs new file mode 100644 index 000000000..2d90690c9 --- /dev/null +++ b/src/MIDebugEngineUnitTests/NatvisIntrinsicTest.cs @@ -0,0 +1,259 @@ +using System.Collections.Generic; +using Xunit; +using Microsoft.MIDebugEngine.Natvis; + +namespace MIDebugEngineUnitTests +{ + /// + /// Unit tests for the NatVis <Intrinsic> expansion logic: + /// ResolveIntrinsicCalls and its helpers FindMatchingParen, + /// SplitArguments, and SubstituteIntrinsicParameters. + /// + public class NatvisIntrinsicTest + { + // ── helpers ────────────────────────────────────────────────────────── + + private static IntrinsicType MakeIntrinsic(string name, string expression, params (string name, string type)[] parameters) + { + var intrinsic = new IntrinsicType(); + intrinsic.Name = name; + intrinsic.Expression = expression; + if (parameters.Length > 0) + { + var ps = new IntrinsicParameterType[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + ps[i] = new IntrinsicParameterType(); + ps[i].Name = parameters[i].name; + ps[i].Type = parameters[i].type; + } + intrinsic.Parameter = ps; + } + return intrinsic; + } + + private static Dictionary Dict(params IntrinsicType[] intrinsics) + { + var d = new Dictionary(); + foreach (var i in intrinsics) + d[i.Name] = i; + return d; + } + + // ── FindMatchingParen ───────────────────────────────────────────────── + + [Fact] + public void FindMatchingParen_SimpleCall() + { + // "foo(bar)" — open paren at 3, close at 7 + Assert.Equal(7, Natvis.FindMatchingParen("foo(bar)", 3)); + } + + [Fact] + public void FindMatchingParen_NestedParens() + { + // "f(g(x))" — outer open at 1, outer close at 6 + Assert.Equal(6, Natvis.FindMatchingParen("f(g(x))", 1)); + } + + [Fact] + public void FindMatchingParen_EmptyArgs() + { + // "f()" — open at 1, close at 2 + Assert.Equal(2, Natvis.FindMatchingParen("f()", 1)); + } + + [Fact] + public void FindMatchingParen_Unmatched_ReturnsMinusOne() + { + Assert.Equal(-1, Natvis.FindMatchingParen("f(abc", 1)); + } + + // ── SplitArguments ──────────────────────────────────────────────────── + + [Fact] + public void SplitArguments_NoArgs_EmptyString() + { + // Empty string → no arguments. In practice ResolveIntrinsicCalls + // guards with IsNullOrWhiteSpace before calling SplitArguments, so + // zero-arg calls never reach it; but the helper itself should be consistent. + var result = Natvis.SplitArguments(""); + Assert.Empty(result); + } + + [Fact] + public void SplitArguments_SingleArg() + { + var result = Natvis.SplitArguments("42"); + Assert.Equal(new[] { "42" }, result); + } + + [Fact] + public void SplitArguments_MultipleArgs() + { + var result = Natvis.SplitArguments("a, b, c"); + Assert.Equal(new[] { "a", "b", "c" }, result); + } + + [Fact] + public void SplitArguments_NestedParens_NotSplit() + { + // "f(a, b), c" — the comma inside f(...) is not a split point + var result = Natvis.SplitArguments("f(a, b), c"); + Assert.Equal(new[] { "f(a, b)", "c" }, result); + } + + [Fact] + public void SplitArguments_ComparisonOperator_SplitsCorrectly() + { + // "a > 0, b" — '>' is a comparison operator, not a bracket; the comma is + // at depth 0 and must be treated as a split point. + // (Angle brackets are intentionally not tracked to avoid this ambiguity.) + var result = Natvis.SplitArguments("a > 0, b"); + Assert.Equal(new[] { "a > 0", "b" }, result); + } + + // ── SubstituteIntrinsicParameters ───────────────────────────────────── + + [Fact] + public void SubstituteIntrinsicParameters_NoParameters_BodyUnchanged() + { + string result = Natvis.SubstituteIntrinsicParameters("jd + 1", null, new List()); + Assert.Equal("jd + 1", result); + } + + [Fact] + public void SubstituteIntrinsicParameters_SingleParam() + { + var ps = new[] { new IntrinsicParameterType { Name = "count", Type = "int" } }; + string result = Natvis.SubstituteIntrinsicParameters("sizeof(int) * count", ps, new List { "3" }); + Assert.Equal("sizeof(int) * 3", result); + } + + [Fact] + public void SubstituteIntrinsicParameters_WholeWordOnly() + { + // "val" should not be replaced inside "interval" + var ps = new[] { new IntrinsicParameterType { Name = "val", Type = "int" } }; + string result = Natvis.SubstituteIntrinsicParameters("interval + val", ps, new List { "99" }); + Assert.Equal("interval + 99", result); + } + + [Fact] + public void SubstituteIntrinsicParameters_MultipleParams() + { + var ps = new[] + { + new IntrinsicParameterType { Name = "a", Type = "int" }, + new IntrinsicParameterType { Name = "b", Type = "int" } + }; + string result = Natvis.SubstituteIntrinsicParameters("a + b", ps, new List { "1", "2" }); + Assert.Equal("1 + 2", result); + } + + // ── ResolveIntrinsicCalls ───────────────────────────────────────────── + + [Fact] + public void ResolveIntrinsicCalls_NullDict_ReturnsUnchanged() + { + string result = Natvis.ResolveIntrinsicCalls("day() + 1", null); + Assert.Equal("day() + 1", result); + } + + [Fact] + public void ResolveIntrinsicCalls_EmptyDict_ReturnsUnchanged() + { + string result = Natvis.ResolveIntrinsicCalls("day() + 1", new Dictionary()); + Assert.Equal("day() + 1", result); + } + + [Fact] + public void ResolveIntrinsicCalls_UnknownName_ReturnsUnchanged() + { + var dict = Dict(MakeIntrinsic("month", "jd / 30")); + string result = Natvis.ResolveIntrinsicCalls("day() + 1", dict); + Assert.Equal("day() + 1", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ZeroArgIntrinsic() + { + // day() with no parameters + var dict = Dict(MakeIntrinsic("day", "jd - 5")); + string result = Natvis.ResolveIntrinsicCalls("day() + 1", dict); + Assert.Equal("(jd - 5) + 1", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ParametrizedIntrinsic() + { + // memberOffset(3) where Expression = "sizeof(int) * count", param count + var dict = Dict(MakeIntrinsic("memberOffset", "sizeof(int) * count", ("count", "int"))); + string result = Natvis.ResolveIntrinsicCalls("memberOffset(3)", dict); + Assert.Equal("(sizeof(int) * 3)", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ChainedIntrinsics() + { + // year() = N() + 1, N() = jd / 2 + var dict = Dict( + MakeIntrinsic("N", "jd / 2"), + MakeIntrinsic("year", "N() + 1") + ); + string result = Natvis.ResolveIntrinsicCalls("year()", dict); + Assert.Equal("((jd / 2) + 1)", result); + } + + [Fact] + public void ResolveIntrinsicCalls_MemberAccessNotReExpanded() + { + // value() = _q_value.value() — after expansion the ".value()" must NOT + // be re-expanded even though "value" is in the intrinsics dictionary. + var dict = Dict(MakeIntrinsic("value", "_q_value.value()")); + string result = Natvis.ResolveIntrinsicCalls("value()", dict); + Assert.Equal("(_q_value.value())", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ArrowAccessNotReExpanded() + { + // ptr->get() — "get" is an intrinsic but must not expand when after "->" + var dict = Dict(MakeIntrinsic("get", "inner")); + string result = Natvis.ResolveIntrinsicCalls("ptr->get()", dict); + Assert.Equal("ptr->get()", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ParametrizedChained() + { + // isEmpty(size) = size==0; hasScheme() = !isEmpty(scheme_size) + var dict = Dict( + MakeIntrinsic("isEmpty", "size==0", ("size", "int")), + MakeIntrinsic("hasScheme", "!isEmpty(scheme_size)") + ); + string result = Natvis.ResolveIntrinsicCalls("hasScheme()", dict); + Assert.Equal("(!(scheme_size==0))", result); + } + + [Fact] + public void ResolveIntrinsicCalls_MultipleCallsInExpression() + { + var dict = Dict( + MakeIntrinsic("x", "a + 1"), + MakeIntrinsic("y", "b + 2") + ); + string result = Natvis.ResolveIntrinsicCalls("x() * y()", dict); + Assert.Equal("(a + 1) * (b + 2)", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ExpressionWithNoCall_Unchanged() + { + var dict = Dict(MakeIntrinsic("day", "jd - 5")); + // Expression references "day" but without (), so no expansion + string result = Natvis.ResolveIntrinsicCalls("day + 1", dict); + Assert.Equal("day + 1", result); + } + } +} diff --git a/src/MIDebugPackage/OpenFolderSchema.json b/src/MIDebugPackage/OpenFolderSchema.json index e8431f00f..0039be008 100644 --- a/src/MIDebugPackage/OpenFolderSchema.json +++ b/src/MIDebugPackage/OpenFolderSchema.json @@ -193,6 +193,24 @@ "stop" ], "description": "Controls how breakpoints set externally (usually via raw GDB commands) are handled when hit.\nAllowed values are \"throw\", which acts as if an exception was thrown by the application, and \"stop\", which only pauses the debug session. The default value is \"throw\"." + }, + "debuginfod": { + "type": "object", + "description": "Controls GDB's debuginfod behavior for automatic downloading of debug symbols.", + "default": { "enabled": true, "timeout": 30 }, + "properties": { + "enabled": { + "type": "boolean", + "description": "If true (default), GDB's debuginfod support is enabled. Set to false to disable debuginfod, which can prevent GDB from hanging when debuginfod servers are unavailable.", + "default": true + }, + "timeout": { + "type": "integer", + "description": "The timeout in seconds for debuginfod server requests. Default is 30. Set to 0 for no timeout override (GDB defaults apply). Applies to local, runInTerminal, and SSH attach transports. This setting is not applied for pipeTransport; set DEBUGINFOD_TIMEOUT and DEBUGINFOD_MAXTIME in the debugger's environment manually.", + "default": 30, + "minimum": 0 + } + } } }, "definitions": { diff --git a/src/MIDebugPackage/source.extension.vsixmanifest b/src/MIDebugPackage/source.extension.vsixmanifest index 8d87e5a86..fa0dab33d 100644 --- a/src/MIDebugPackage/source.extension.vsixmanifest +++ b/src/MIDebugPackage/source.extension.vsixmanifest @@ -21,5 +21,6 @@ + diff --git a/src/OpenDebugAD7/AD7DebugSession.cs b/src/OpenDebugAD7/AD7DebugSession.cs index bd22d1d05..19b03af8d 100644 --- a/src/OpenDebugAD7/AD7DebugSession.cs +++ b/src/OpenDebugAD7/AD7DebugSession.cs @@ -1095,7 +1095,8 @@ protected override void HandleInitializeRequestAsync(IRequestResponder errorMessages = new List(); if (!string.IsNullOrEmpty(bp.LogMessage)) { // Make sure tracepoint is valid. - verified = pBPRequest.SetLogMessage(bp.LogMessage); + if (!pBPRequest.SetLogMessage(bp.LogMessage)) + { + errorMessages.Add(AD7Resources.Error_UnableToParseLogMessage); + } + } + + if (!pBPRequest.IsHitConditionValid) + { + errorMessages.Add(AD7Resources.Error_UnableToParseHitCondition); } + bool verified = errorMessages.Count == 0; + string errorMessage = verified ? null : string.Join(" ", errorMessages); + if (verified) { eb.CheckHR(m_engine.CreatePendingBreakpoint(pBPRequest, out pendingBp)); @@ -2510,7 +2556,7 @@ protected override void HandleSetBreakpointsRequestAsync(IRequestResponder m_Tracepoint; #endregion + + #region Hit Conditions + + /// + /// Updates the hit condition string so that subsequent + /// calls return the new pass count, and future comparisons see the updated value. + /// + internal void UpdateHitCondition(string hitCondition) + { + HitCondition = hitCondition; + } + + /// + /// Attempts to parse the hit condition string into a pass count style and value. + /// Returns true if HitCondition is null/empty (no condition) or if it is a valid hit condition. + /// + internal bool TryParseHitCondition(out enum_BP_PASSCOUNT_STYLE style, out uint passCount) + { + style = enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_NONE; + passCount = 0; + + if (string.IsNullOrWhiteSpace(HitCondition)) + { + return true; + } + + string hc = HitCondition.Trim(); + string numberPart = hc; + + if (hc.StartsWith(">=")) + { + style = enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL_OR_GREATER; + numberPart = hc.Substring(2).Trim(); + } + else if (hc.StartsWith("%")) + { + style = enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_MOD; + numberPart = hc.Substring(1).Trim(); + } + else + { + style = enum_BP_PASSCOUNT_STYLE.BP_PASSCOUNT_EQUAL; + } + + return uint.TryParse(numberPart, out passCount); + } + + /// + /// Validates that the hit condition string can be parsed into a pass count. + /// Returns true if HitCondition is null/empty (no condition) or if it is a valid hit condition. + /// + internal bool IsHitConditionValid => TryParseHitCondition(out _, out _); + + #endregion } } diff --git a/src/OpenDebugAD7/AD7Resources.Designer.cs b/src/OpenDebugAD7/AD7Resources.Designer.cs index abb7d62bf..0af693149 100644 --- a/src/OpenDebugAD7/AD7Resources.Designer.cs +++ b/src/OpenDebugAD7/AD7Resources.Designer.cs @@ -478,6 +478,15 @@ internal static string Error_UnableToParseLogMessage { } } + /// + /// Looks up a localized string similar to Unable to parse 'hitCondition'. Expected a number, optionally prefixed with >= or %.. + /// + internal static string Error_UnableToParseHitCondition { + get { + return ResourceManager.GetString("Error_UnableToParseHitCondition", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error setting breakpoint. {0}. /// diff --git a/src/OpenDebugAD7/AD7Resources.resx b/src/OpenDebugAD7/AD7Resources.resx index e50d73343..b3dd45908 100644 --- a/src/OpenDebugAD7/AD7Resources.resx +++ b/src/OpenDebugAD7/AD7Resources.resx @@ -198,6 +198,9 @@ Unable to parse 'logMessage'. + + Unable to parse 'hitCondition'. Expected a number, optionally prefixed with >= or %. + This operation is not supported when debugging dump files. diff --git a/src/SSHDebugPS/AD7/AD7Port.cs b/src/SSHDebugPS/AD7/AD7Port.cs index 951887bbd..8f8ce6d28 100644 --- a/src/SSHDebugPS/AD7/AD7Port.cs +++ b/src/SSHDebugPS/AD7/AD7Port.cs @@ -11,6 +11,7 @@ using Microsoft.VisualStudio.Debugger.Interop; using Microsoft.VisualStudio.Debugger.Interop.UnixPortSupplier; using Microsoft.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.Shell; namespace Microsoft.SSHDebugPS { @@ -153,11 +154,18 @@ void IDebugUnixShellPort.ExecuteSyncCommand(string commandDescription, string co string output = null; string errorMessage; - string waitPrompt = StringResources.WaitingOp_ExecutingCommand.FormatCurrentCultureWithArgs(commandDescription); - VS.VSOperationWaiter.Wait(waitPrompt, throwOnCancel: true, action: (cancellationToken) => + if (ThreadHelper.CheckAccess()) + { + string waitPrompt = StringResources.WaitingOp_ExecutingCommand.FormatCurrentCultureWithArgs(commandDescription); + VS.VSOperationWaiter.Wait(waitPrompt, throwOnCancel: true, action: (cancellationToken) => + { + code = GetConnection().ExecuteCommand(commandText, timeout, out output, out errorMessage); + }); + } + else { code = GetConnection().ExecuteCommand(commandText, timeout, out output, out errorMessage); - }); + } exitCode = code; commandOutput = output; diff --git a/test/CppTests/OpenDebug/CrossPlatCpp/LaunchCommand.cs b/test/CppTests/OpenDebug/CrossPlatCpp/LaunchCommand.cs index 06e37fb21..5da394a34 100644 --- a/test/CppTests/OpenDebug/CrossPlatCpp/LaunchCommand.cs +++ b/test/CppTests/OpenDebug/CrossPlatCpp/LaunchCommand.cs @@ -40,6 +40,33 @@ public sealed class CppLaunchCommandArgs : LaunchCommandArgs [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public bool ShowDisplayString; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public DebuginfodArgs debuginfod; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public SetupCommandArg[] setupCommands; + } + + public sealed class DebuginfodArgs + { + [JsonProperty("enabled", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Enabled { get; set; } + + [JsonProperty("timeout", DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? Timeout { get; set; } + } + + public sealed class SetupCommandArg + { + [JsonProperty("text")] + public string Text { get; set; } + + [JsonProperty("description", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Description { get; set; } + + [JsonProperty("ignoreFailures", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? IgnoreFailures { get; set; } } #endregion diff --git a/test/CppTests/Tests/BreakpointTests.cs b/test/CppTests/Tests/BreakpointTests.cs index 4ea4279b9..358d70a8f 100644 --- a/test/CppTests/Tests/BreakpointTests.cs +++ b/test/CppTests/Tests/BreakpointTests.cs @@ -202,9 +202,8 @@ public void LineLogBreakpointsBasic(ITestSettings settings) [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] [RequiresTestSettings] // TODO: https://github.com/microsoft/MIEngine/issues/1170 - // - gdb_gnu // - lldb - [UnsupportedDebugger(SupportedDebugger.Lldb | SupportedDebugger.Gdb_Gnu | SupportedDebugger.Gdb_Cygwin | SupportedDebugger.Gdb_MinGW, SupportedArchitecture.x64 | SupportedArchitecture.x86)] + [UnsupportedDebugger(SupportedDebugger.Lldb | SupportedDebugger.Gdb_Cygwin | SupportedDebugger.Gdb_MinGW, SupportedArchitecture.x64 | SupportedArchitecture.x86)] public void RunModeBreakpoints(ITestSettings settings) { this.TestPurpose("Tests setting breakpoints while in run mode"); @@ -516,11 +515,1115 @@ public void BreakpointSettingsVerification(ITestSettings settings) Assert.True(runner.InitializeResponse.body.supportsFunctionBreakpoints.HasValue && runner.InitializeResponse.body.supportsFunctionBreakpoints.Value == true, "Function breakpoints should be supported"); + Assert.True(runner.InitializeResponse.body.supportsHitConditionalBreakpoints.HasValue + && runner.InitializeResponse.body.supportsHitConditionalBreakpoints.Value == true, "Hit conditional breakpoints should be supported"); + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterConfigurationDone(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointEqual(ITestSettings settings) + { + this.TestPurpose("Tests that a breakpoint with a hit count condition (equal) only breaks on the Nth hit"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition = 5 inside a loop that iterates 10 times"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "5"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should only stop on 5th hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify that the loop variable equals 4 (0-indexed, 5th iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "4"); + } + + this.Comment("Run to completion - hit count 5 already reached, should not stop again"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointGreaterOrEqual(ITestSettings settings) + { + this.TestPurpose("Tests that a breakpoint with a hit count condition (>=) breaks on the Nth hit and every subsequent hit"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition >= 8 inside a loop that iterates 10 times"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: ">=8"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on 8th hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify that the loop variable equals 7 (0-indexed, 8th iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "7"); + } + + this.Comment("Continue - should stop on 9th hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + this.Comment("Verify that the loop variable equals 8 (0-indexed, 9th iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "8"); + } + + this.Comment("Continue - should stop on 10th hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + this.Comment("Verify that the loop variable equals 9 (0-indexed, 10th iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "9"); + } + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointModulo(ITestSettings settings) + { + this.TestPurpose("Tests that a breakpoint with a hit count condition (modulo) breaks on every Nth hit"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition %3 inside a loop that iterates 10 times"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "%3"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on 3rd hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify that the loop variable equals 2 (0-indexed, 3rd iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "2"); + } + + this.Comment("Continue - should stop on 6th hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + this.Comment("Verify that the loop variable equals 5 (0-indexed, 6th iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "5"); + } + + this.Comment("Continue - should stop on 9th hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + this.Comment("Verify that the loop variable equals 8 (0-indexed, 9th iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "8"); + } + + this.Comment("Run to completion - no more multiples of 3 within 10 iterations"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointEqualFirst(ITestSettings settings) + { + this.TestPurpose("Tests that hitCondition '1' breaks on the very first hit"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition = 1 (first hit)"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "1"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on the very first hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify i == 0 (first iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "0"); + } + + this.Comment("Run to completion - equal condition already satisfied, should not stop again"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointEqualLast(ITestSettings settings) + { + this.TestPurpose("Tests that hitCondition equal to the loop bound stops on the last iteration"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition = 10 (last hit in a 10-iteration loop)"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "10"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on the 10th and final hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify i == 9 (last iteration, 0-indexed)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "9"); + } + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointEqualExceedsIterations(ITestSettings settings) + { + this.TestPurpose("Tests that hitCondition exceeding total iterations never stops"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition = 99 inside a loop that only iterates 10 times"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "99"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to completion - breakpoint should never fire"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterConfigurationDone(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointGreaterOrEqualOne(ITestSettings settings) + { + this.TestPurpose("Tests that hitCondition '>=1' stops on every single hit (same as no condition)"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition >= 1 (should stop on every hit)"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: ">=1"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on first hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "0"); + } + + this.Comment("Continue - should stop on second hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "1"); + } + + this.Comment("Continue - should stop on third hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "2"); + } + + // Remove the breakpoint so we can run to completion without stopping 7 more times + this.Comment("Remove the breakpoint and run to completion"); + callingBreakpoints.Remove(17); + runner.SetBreakpoints(callingBreakpoints); + + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointModuloOne(ITestSettings settings) + { + this.TestPurpose("Tests that hitCondition '%%1' stops on every hit (degenerate modulo)"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition %1 (every hit is a multiple of 1)"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "%1"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on first hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "0"); + } + + this.Comment("Continue - should stop on second hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "1"); + } + + // Remove the breakpoint so we can run to completion + this.Comment("Remove the breakpoint and run to completion"); + callingBreakpoints.Remove(17); + runner.SetBreakpoints(callingBreakpoints); + + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointModuloNoMultiple(ITestSettings settings) + { + this.TestPurpose("Tests that a modulo hit condition whose value exceeds total iterations only fires once at the Nth hit"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition %7 inside a loop that iterates 10 times"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "%7"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on 7th hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify i == 6 (0-indexed, 7th iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "6"); + } + + this.Comment("Run to completion - 14th hit would be next multiple but loop only has 10 iterations"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointModuloRearms(ITestSettings settings) + { + this.TestPurpose("Tests that a modulo breakpoint fires on every Nth hit across many cycles, verifying that GDB's ignore count is re-armed after each stop"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition %2 inside a loop that iterates 10 times"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "%2"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on 2nd hit (i == 1)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "1"); + } + + this.Comment("Continue - should stop on 4th hit (i == 3)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "3"); + } + + this.Comment("Continue - should stop on 6th hit (i == 5)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "5"); + } + + this.Comment("Continue - should stop on 8th hit (i == 7)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "7"); + } + + this.Comment("Continue - should stop on 10th hit (i == 9)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "9"); + } + + this.Comment("Run to completion - loop is exhausted"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointModifyMidRun(ITestSettings settings) + { + this.TestPurpose("Tests changing a hitCondition while stopped at the breakpoint"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition = 3"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "3"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on 3rd hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify i == 2 (3rd iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "2"); + } + + // Change from EQUAL to GREATER_OR_EQUAL. The engine carries over the + // 3 hits already counted, so >=8 should fire on the 8th overall hit. + this.Comment("Change hit condition to >=8 while stopped"); + callingBreakpoints.Remove(17); + callingBreakpoints.Add(17, hitCondition: ">=8"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Continue - should stop on 8th overall hit (i == 7)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + this.Comment("Verify i == 7 (8th iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "7"); + } + + this.Comment("Continue - should stop on 9th hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "8"); + } + + this.Comment("Continue - should stop on 10th hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "9"); + } + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointClearMidRun(ITestSettings settings) + { + this.TestPurpose("Tests that clearing a hit condition mid-run resets GDB's ignore count so the breakpoint fires on the next hit"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition %5 inside a loop that iterates 10 times"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "%5"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on 5th hit (i == 4)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "4"); + } + + this.Comment("Clear the hit condition while stopped — remove and re-add without hitCondition"); + callingBreakpoints.Remove(17); + callingBreakpoints.Add(17); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Continue - with no hit condition, should stop on the very next hit (i == 5)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + this.Comment("Verify the breakpoint fired immediately, not at the next modulo multiple"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "5"); + } + + this.Comment("Remove the breakpoint and run to completion"); + callingBreakpoints.Remove(17); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointModifyGteToEqual(ITestSettings settings) + { + this.TestPurpose("Tests changing hitCondition from >=N to exact N (GTE -> EQUAL) mid-run"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition >=2"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: ">=2"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on 2nd hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify i == 1 (2nd iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "1"); + } + + // Change from GTE to EQUAL. Hit count is 2. We want to stop at exactly hit 5. + this.Comment("Change hit condition to 5 (exact) while stopped at hit 2"); + callingBreakpoints.Remove(17); + callingBreakpoints.Add(17, hitCondition: "5"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Continue - should stop on 5th overall hit (i == 4)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + this.Comment("Verify i == 4 (5th iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "4"); + } + + // Now change from EQUAL to a target that the hit count has already passed. + // Changing to "3" while at hit 5 means the target is already passed — program should run to completion. + this.Comment("Change hit condition to 3 (already passed) while stopped at hit 5"); + callingBreakpoints.Remove(17); + callingBreakpoints.Add(17, hitCondition: "3"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to completion - hit 3 already passed, EQUAL never fires again"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointRemoveAndReAdd(ITestSettings settings) + { + this.TestPurpose("Tests removing a hit condition breakpoint and re-adding it with a different condition"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with hit condition = 2"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "2"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on 2nd hit"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify i == 1 (2nd iteration)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "1"); + } + + this.Comment("Remove the breakpoint entirely"); + callingBreakpoints.Remove(17); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Re-add with modulo condition %4 - hit count resets for new breakpoint"); + callingBreakpoints.Add(17, hitCondition: "%4"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Continue - breakpoint was removed and re-added, hit count restarted from 0"); + // After removal and re-add, the next hits are 3rd through 10th loop iterations. + // With a fresh %4 condition, it should stop when the new hit count reaches 4. + // The 3rd loop iteration is the 1st new hit, so 4th new hit = 6th loop iteration (i==5). + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + this.Comment("Verify the breakpoint stopped at the expected iteration"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "5"); + } + + this.Comment("Continue - next %4 would be the 8th new hit = 10th loop iteration (i==9)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "9"); + } + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointWithCondition(ITestSettings settings) + { + this.TestPurpose("Tests combining a boolean condition with a hitCondition on the same breakpoint"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with condition 'i >= 4' and hitCondition '3'"); + // The loop runs i = 0..9. The condition 'i >= 4' is true for i = 4,5,6,7,8,9. + // Among those qualifying hits, the hitCondition '3' means break on the 3rd qualifying hit. + // 1st qualifying hit: i=4, 2nd: i=5, 3rd: i=6 -> should stop at i=6. + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, condition: "i >= 4", hitCondition: "3"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + this.Comment("Verify the breakpoint stopped at the 3rd hit where i >= 4 (i == 6)"); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "6"); + } + + this.Comment("Run to completion - equal condition already satisfied"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointGreaterOrEqualWithCondition(ITestSettings settings) + { + this.TestPurpose("Tests combining a boolean condition with a >= hitCondition"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with condition 'i % 2 == 0' and hitCondition '>=3'"); + // The loop runs i = 0..9. The condition 'i % 2 == 0' is true for i = 0,2,4,6,8. + // Among those qualifying hits: 1st: i=0, 2nd: i=2, 3rd: i=4, 4th: i=6, 5th: i=8 + // The hitCondition '>=3' means break from the 3rd qualifying hit onward. + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, condition: "i % 2 == 0", hitCondition: ">=3"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to breakpoint - should stop on 3rd qualifying hit (i == 4)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "4"); + } + + this.Comment("Continue - should stop on 4th qualifying hit (i == 6)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "6"); + } + + this.Comment("Continue - should stop on 5th qualifying hit (i == 8)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "8"); + } + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + [UnsupportedDebugger(SupportedDebugger.VsDbg, SupportedArchitecture.x86 | SupportedArchitecture.x64)] + public void InvalidHitConditionBreakpoint(ITestSettings settings) + { + this.TestPurpose("Tests that an invalid hit condition returns a non-verified breakpoint with an error message"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with an invalid hit condition"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "invalid"); + var response = runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Verify breakpoint is not verified and has an error message about hit condition"); + Assert.NotNull(response.body.breakpoints); + Assert.Single(response.body.breakpoints); + Assert.False(response.body.breakpoints[0].verified, "Breakpoint with invalid hit condition should not be verified"); + Assert.Contains("hitCondition", response.body.breakpoints[0].message); + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterConfigurationDone(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + [UnsupportedDebugger(SupportedDebugger.VsDbg, SupportedArchitecture.x86 | SupportedArchitecture.x64)] + public void InvalidLogMessageBreakpoint(ITestSettings settings) + { + this.TestPurpose("Tests that an invalid log message returns a non-verified breakpoint with an error message"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with an invalid log message (unmatched brace)"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, logMessage: "{unmatched"); + var response = runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Verify breakpoint is not verified and has an error message about log message"); + Assert.NotNull(response.body.breakpoints); + Assert.Single(response.body.breakpoints); + Assert.False(response.body.breakpoints[0].verified, "Breakpoint with invalid log message should not be verified"); + Assert.Contains("logMessage", response.body.breakpoints[0].message); + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterConfigurationDone(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + [UnsupportedDebugger(SupportedDebugger.VsDbg, SupportedArchitecture.x86 | SupportedArchitecture.x64)] + public void InvalidLogMessageAndHitConditionBreakpoint(ITestSettings settings) + { + this.TestPurpose("Tests that both invalid log message and invalid hit condition errors are returned together"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set a breakpoint with both an invalid log message and an invalid hit condition"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, logMessage: "{unmatched", hitCondition: "invalid"); + var response = runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Verify breakpoint is not verified and error message mentions both logMessage and hitCondition"); + Assert.NotNull(response.body.breakpoints); + Assert.Single(response.body.breakpoints); + Assert.False(response.body.breakpoints[0].verified, "Breakpoint with invalid log message and hit condition should not be verified"); + Assert.Contains("logMessage", response.body.breakpoints[0].message); + Assert.Contains("hitCondition", response.body.breakpoints[0].message); + + this.Comment("Run to completion"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterConfigurationDone(); + + runner.DisconnectAndVerify(); + } + } + + #endregion + + #region HitCondition Edge Case Tests + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointEqualFiresOnce(ITestSettings settings) + { + this.TestPurpose("Tests that an EQUAL hitCondition fires exactly once and never again, even with re-arm"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set EQUAL breakpoint at hit 3 on loop body, plus unconditional breakpoint after loop"); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: "3"); + callingBreakpoints.Add(21); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run - should stop at hit 3 (i == 2)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterConfigurationDone(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "2"); + } + + this.Comment("Continue - EQUAL should not fire on hits 4-10; next stop is the post-loop breakpoint"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 21) + .AfterContinue(); + this.Comment("Run to completion"); runner.Expects.ExitedEvent() .TerminatedEvent() + .AfterContinue(); + + runner.DisconnectAndVerify(); + } + } + + [Theory] + [DependsOnTest(nameof(CompileKitchenSinkForBreakpointTests))] + [RequiresTestSettings] + public void HitConditionBreakpointEqualAfterGte(ITestSettings settings) + { + this.TestPurpose("Tests changing from >=N to EQUAL mid-run, verifying re-arm clears stale ignore count"); + this.WriteSettings(settings); + + IDebuggee debuggee = SinkHelper.Open(this, settings.CompilerSettings, DebuggeeMonikers.KitchenSink.Breakpoint); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + runner.Launch(settings.DebuggerSettings, debuggee, "-fCalling"); + + this.Comment("Set breakpoint with >=3 — fires on hits 3, 4, 5, ..."); + SourceBreakpoints callingBreakpoints = new SourceBreakpoints(debuggee, SinkHelper.Calling); + callingBreakpoints.Add(17, hitCondition: ">=3"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Run to first fire at hit 3 (i == 2)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) .AfterConfigurationDone(); + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "2"); + } + + this.Comment("Continue - >=3 fires on hit 4 (i == 3)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + this.Comment("Switch to EQUAL 7 while stopped at hit 4"); + callingBreakpoints.Remove(17); + callingBreakpoints.Add(17, hitCondition: "7"); + runner.SetBreakpoints(callingBreakpoints); + + this.Comment("Continue - should skip hits 5 and 6, fire at hit 7 (i == 6)"); + runner.Expects.HitBreakpointEvent(SinkHelper.Calling, 17) + .AfterContinue(); + + using (IThreadInspector inspector = runner.GetThreadInspector()) + { + IFrameInspector mainFrame = inspector.Stack.First(); + mainFrame.AssertVariables("i", "6"); + } + + this.Comment("Run to completion - EQUAL 7 already satisfied, should not fire again"); + runner.Expects.ExitedEvent() + .TerminatedEvent() + .AfterContinue(); + runner.DisconnectAndVerify(); } } diff --git a/test/CppTests/Tests/CoreDumpTests.cs b/test/CppTests/Tests/CoreDumpTests.cs index f1a6e00e7..ef85aa366 100644 --- a/test/CppTests/Tests/CoreDumpTests.cs +++ b/test/CppTests/Tests/CoreDumpTests.cs @@ -51,9 +51,6 @@ public CoreDumpTests(ITestOutputHelper outputHelper) : base(outputHelper) [Theory] [RequiresTestSettings] [SupportedPlatform(SupportedPlatform.Linux, SupportedArchitecture.x64 | SupportedArchitecture.x86)] - // TODO: https://github.com/microsoft/MIEngine/issues/1170 - // - gdb_gnu - [UnsupportedDebugger(SupportedDebugger.Gdb_Gnu, SupportedArchitecture.x64 | SupportedArchitecture.x86)] public void CoreDumpBasic(ITestSettings settings) { this.TestPurpose("This test checks to see if core dump can be launched successfully"); @@ -69,9 +66,6 @@ public void CoreDumpBasic(ITestSettings settings) [Theory] [RequiresTestSettings] [SupportedPlatform(SupportedPlatform.Linux, SupportedArchitecture.x64 | SupportedArchitecture.x86)] - // TODO: https://github.com/microsoft/MIEngine/issues/1170 - // - gdb_gnu - [UnsupportedDebugger(SupportedDebugger.Gdb_Gnu, SupportedArchitecture.x64 | SupportedArchitecture.x86)] public void CoreDumpBasicMismatchedSourceAndSymbols(ITestSettings settings) { this.TestPurpose("This test checks to see if core dump can be launched successfully with mismathed source code."); @@ -90,9 +84,6 @@ public void CoreDumpBasicMismatchedSourceAndSymbols(ITestSettings settings) [Theory] [RequiresTestSettings] [SupportedPlatform(SupportedPlatform.Linux, SupportedArchitecture.x64 | SupportedArchitecture.x86)] - // TODO: https://github.com/microsoft/MIEngine/issues/1170 - // - gdb_gnu - [UnsupportedDebugger(SupportedDebugger.Gdb_Gnu, SupportedArchitecture.x64 | SupportedArchitecture.x86)] public void CoreDumpVerifyActions(ITestSettings settings) { this.TestPurpose("This test checks to see the behavior when do actions during core dump debugging."); @@ -144,32 +135,32 @@ public void CoreDumpVerifyActions(ITestSettings settings) StepInCommand stepInCommand = new StepInCommand(runner.DarRunner.CurrentThreadId); runner.RunCommandExpectFailure(stepInCommand); this.WriteLine(string.Format(CultureInfo.InvariantCulture, "Actual respone message: {0}", stepInCommand.Message)); - Assert.Contains(stepInCommand.Message, string.Format(CultureInfo.InvariantCulture, stepError, "step in")); + Assert.Contains(string.Format(CultureInfo.InvariantCulture, stepError, "step in"), stepInCommand.Message); this.Comment("Try to step over and verify the error message"); StepOverCommand stepOverCommand = new StepOverCommand(runner.DarRunner.CurrentThreadId); runner.RunCommandExpectFailure(stepOverCommand); this.WriteLine(string.Format(CultureInfo.InvariantCulture, "Actual respone message: {0}", stepOverCommand.Message)); - Assert.Contains(stepOverCommand.Message, string.Format(CultureInfo.InvariantCulture, stepError, "step next")); + Assert.Contains(string.Format(CultureInfo.InvariantCulture, stepError, "step next"), stepOverCommand.Message); this.Comment("Try to step out and verify the error message"); StepOutCommand stepOutCommand = new StepOutCommand(runner.DarRunner.CurrentThreadId); runner.RunCommandExpectFailure(stepOutCommand); this.WriteLine(string.Format(CultureInfo.InvariantCulture, "Actual respone message: {0}", stepOutCommand.Message)); - Assert.Contains(stepOutCommand.Message, string.Format(CultureInfo.InvariantCulture, stepError, "step out")); + Assert.Contains(string.Format(CultureInfo.InvariantCulture, stepError, "step out"), stepOutCommand.Message); this.Comment("Try to continue and verify the error message"); ContinueCommand continueCommand = new ContinueCommand(runner.DarRunner.CurrentThreadId); runner.RunCommandExpectFailure(continueCommand); this.WriteLine(string.Format(CultureInfo.InvariantCulture, "Actual respone message: {0}", continueCommand.Message)); - Assert.Contains(continueCommand.Message, string.Format(CultureInfo.InvariantCulture, stepError, "continue")); + Assert.Contains(string.Format(CultureInfo.InvariantCulture, stepError, "continue"), continueCommand.Message); this.Comment("Try to set a breakpoint and verify the error message"); SourceBreakpoints bp = debuggee.Breakpoints(srcAppName, 16); SetBreakpointsResponseValue setBpResponse = runner.SetBreakpoints(bp); Assert.False(setBpResponse.body.breakpoints[0].verified); this.WriteLine(string.Format(CultureInfo.InvariantCulture, "Actual respone message: {0}", setBpResponse.body.breakpoints[0].message)); - Assert.Contains(setBpResponse.body.breakpoints[0].message, bpError); + Assert.Contains(bpError, setBpResponse.body.breakpoints[0].message); this.Comment("Stop core dump debugging"); runner.DisconnectAndVerify(); diff --git a/test/CppTests/Tests/DebuggeeMonikers.cs b/test/CppTests/Tests/DebuggeeMonikers.cs index 6738f4c99..c49a53408 100644 --- a/test/CppTests/Tests/DebuggeeMonikers.cs +++ b/test/CppTests/Tests/DebuggeeMonikers.cs @@ -60,5 +60,10 @@ internal static class Natvis { public const int Default = 1; } + + internal static class Debuginfod + { + public const int Default = 1; + } } } diff --git a/test/CppTests/Tests/DebuginfodTests.cs b/test/CppTests/Tests/DebuginfodTests.cs new file mode 100644 index 000000000..b14dae27b --- /dev/null +++ b/test/CppTests/Tests/DebuginfodTests.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using DebuggerTesting; +using DebuggerTesting.Compilation; +using DebuggerTesting.OpenDebug; +using DebuggerTesting.OpenDebug.Commands; +using DebuggerTesting.OpenDebug.CrossPlatCpp; +using DebuggerTesting.OpenDebug.Events; +using DebuggerTesting.OpenDebug.Extensions; +using DebuggerTesting.Ordering; +using DebuggerTesting.Settings; +using Xunit; +using Xunit.Abstractions; + +namespace CppTests.Tests +{ + /// + /// Tests that validate debuginfod settings don't cause GDB to hang when + /// debuginfod servers are unreachable. + /// + [TestCaseOrderer(DependencyTestOrderer.TypeName, DependencyTestOrderer.AssemblyName)] + public class DebuginfodTests : TestBase + { + #region Constructor + + public DebuginfodTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + #endregion + + private const string DebuggeeName = "debuginfod"; + private const string SourceFileName = "debuginfod_test.cpp"; + + // RFC 5737 TEST-NET-1: guaranteed non-routable, connections will be dropped + private const string UnreachableDebuginfodUrl = "http://192.0.2.1:8002"; + + #region Tests + + [Theory] + [RequiresTestSettings] + public void CompileDebuginfodDebuggee(ITestSettings settings) + { + this.TestPurpose("Create and compile the debuginfod test debuggee"); + this.WriteSettings(settings); + + IDebuggee debuggee = Debuggee.Create(this, settings.CompilerSettings, DebuggeeName, DebuggeeMonikers.Debuginfod.Default); + debuggee.AddSourceFiles(SourceFileName); + debuggee.Compile(); + } + + /// + /// Tests that disabling debuginfod prevents hangs when stepping into library code + /// with DEBUGINFOD_URLS pointing to an unreachable server. + /// + [Theory] + [DependsOnTest(nameof(CompileDebuginfodDebuggee))] + [RequiresTestSettings] + [UnsupportedDebugger(SupportedDebugger.VsDbg | SupportedDebugger.Lldb, SupportedArchitecture.x64 | SupportedArchitecture.x86)] + public void DebuginfodDisabledDoesNotHang(ITestSettings settings) + { + this.TestPurpose("Verify that disabling debuginfod prevents GDB from hanging when stepping into library code."); + this.WriteSettings(settings); + + IDebuggee debuggee = Debuggee.Open(this, settings.CompilerSettings, DebuggeeName, DebuggeeMonikers.Debuginfod.Default); + + string originalUrl = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS"); + string originalVerbose = Environment.GetEnvironmentVariable("DEBUGINFOD_VERBOSE"); + try + { + Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", UnreachableDebuginfodUrl); + Environment.SetEnvironmentVariable("DEBUGINFOD_VERBOSE", "1"); + + Stopwatch sw = Stopwatch.StartNew(); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Launch with debuginfod disabled"); + LaunchCommand launch = new LaunchCommand(settings.DebuggerSettings, debuggee.OutputPath); + launch.Args.debuginfod = new DebuginfodArgs { Enabled = false }; + runner.RunCommand(launch); + + this.Comment("Set breakpoint at regex_search call and run to it"); + runner.SetBreakpoints(debuggee.Breakpoints(SourceFileName, 13)); + runner.Expects.StoppedEvent(StoppedReason.Breakpoint).AfterConfigurationDone(); + + this.Comment("Step into std::regex_search to trigger debuginfod lookup"); + runner.Expects.StoppedEvent(StoppedReason.Step).AfterStepIn(); + + sw.Stop(); + this.Comment($"Step into library completed in {sw.ElapsedMilliseconds}ms"); + + Assert.True(sw.ElapsedMilliseconds < 10000, + $"Step into library took {sw.ElapsedMilliseconds}ms with debuginfod disabled. " + + "Expected < 10s. Debuginfod may not be properly disabled."); + + runner.Expects.ExitedEvent().TerminatedEvent().AfterContinue(); + runner.DisconnectAndVerify(); + } + + string engineLogPath = Path.Combine(PathSettings.TempPath, + $"EngineLog-{nameof(DebuginfodDisabledDoesNotHang)}-{settings.DebuggerSettings.DebuggeeArchitecture}-{settings.DebuggerSettings.DebuggerType}.log"); + if (File.Exists(engineLogPath)) + { + string logContent = File.ReadAllText(engineLogPath); + this.Comment("Verifying debuginfod was NOT enabled in GDB"); + Assert.DoesNotContain("debuginfod enabled", logContent); + } + } + finally + { + Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalUrl); + Environment.SetEnvironmentVariable("DEBUGINFOD_VERBOSE", originalVerbose); + } + } + + /// + /// Tests that a short debuginfod timeout prevents hangs when stepping into library code + /// with DEBUGINFOD_URLS pointing to an unreachable server. + /// + [Theory] + [DependsOnTest(nameof(CompileDebuginfodDebuggee))] + [RequiresTestSettings] + [UnsupportedDebugger(SupportedDebugger.VsDbg | SupportedDebugger.Lldb | SupportedDebugger.Gdb_MinGW | SupportedDebugger.Gdb_Cygwin, SupportedArchitecture.x64 | SupportedArchitecture.x86)] + public void DebuginfodTimeoutPreventsHang(ITestSettings settings) + { + this.TestPurpose("Verify that debuginfod timeout prevents GDB from hanging when stepping into library code."); + this.WriteSettings(settings); + + IDebuggee debuggee = Debuggee.Open(this, settings.CompilerSettings, DebuggeeName, DebuggeeMonikers.Debuginfod.Default); + + string originalUrl = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS"); + string originalVerbose = Environment.GetEnvironmentVariable("DEBUGINFOD_VERBOSE"); + try + { + Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", UnreachableDebuginfodUrl); + Environment.SetEnvironmentVariable("DEBUGINFOD_VERBOSE", "1"); + + Stopwatch sw = Stopwatch.StartNew(); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Launch with debuginfod enabled but short timeout (5s)"); + LaunchCommand launch = new LaunchCommand(settings.DebuggerSettings, debuggee.OutputPath); + launch.Args.debuginfod = new DebuginfodArgs { Enabled = true, Timeout = 5 }; + launch.Args.setupCommands = new SetupCommandArg[] + { + new SetupCommandArg { Text = "set debuginfod verbose 1", IgnoreFailures = true } + }; + runner.RunCommand(launch); + + this.Comment("Set breakpoint at regex_search call and run to it"); + runner.SetBreakpoints(debuggee.Breakpoints(SourceFileName, 13)); + runner.Expects.StoppedEvent(StoppedReason.Breakpoint).AfterConfigurationDone(); + + this.Comment("Step into std::regex_search to trigger debuginfod lookup"); + runner.Expects.StoppedEvent(StoppedReason.Step).AfterStepIn(); + + sw.Stop(); + this.Comment($"Step into library completed in {sw.ElapsedMilliseconds}ms"); + + Assert.True(sw.ElapsedMilliseconds < 30000, + $"Step into library took {sw.ElapsedMilliseconds}ms with debuginfod timeout=5s. " + + "Expected < 30s. The timeout may not be applied correctly."); + + runner.Expects.ExitedEvent().TerminatedEvent().AfterContinue(); + runner.DisconnectAndVerify(); + } + + string engineLogPath = Path.Combine(PathSettings.TempPath, + $"EngineLog-{nameof(DebuginfodTimeoutPreventsHang)}-{settings.DebuggerSettings.DebuggeeArchitecture}-{settings.DebuggerSettings.DebuggerType}.log"); + if (File.Exists(engineLogPath)) + { + string logContent = File.ReadAllText(engineLogPath); + this.Comment("Verifying debuginfod was enabled in GDB"); + Assert.Contains("debuginfod enabled", logContent); + } + } + finally + { + Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalUrl); + Environment.SetEnvironmentVariable("DEBUGINFOD_VERBOSE", originalVerbose); + } + } + + #endregion + } +} diff --git a/test/CppTests/Tests/ExecutionTests.cs b/test/CppTests/Tests/ExecutionTests.cs index 916285b9b..0439c2658 100644 --- a/test/CppTests/Tests/ExecutionTests.cs +++ b/test/CppTests/Tests/ExecutionTests.cs @@ -177,8 +177,8 @@ public void ExecutionStepRecursiveCall(ITestSettings settings) [Theory] [DependsOnTest(nameof(CompileKitchenSinkForExecution))] [RequiresTestSettings] - // TODO: Re-enable for VsDbg and Gdb_Gnu - [UnsupportedDebugger(SupportedDebugger.Gdb_Cygwin | SupportedDebugger.Gdb_MinGW | SupportedDebugger.Gdb_Gnu | SupportedDebugger.Lldb | SupportedDebugger.VsDbg, SupportedArchitecture.x86 | SupportedArchitecture.x64)] + // TODO: Re-enable for VsDbg + [UnsupportedDebugger(SupportedDebugger.Gdb_Cygwin | SupportedDebugger.Gdb_MinGW | SupportedDebugger.Lldb | SupportedDebugger.VsDbg, SupportedArchitecture.x86 | SupportedArchitecture.x64)] public void ExecutionAsyncBreak(ITestSettings settings) { this.TestPurpose("Verify break all should work run function"); @@ -192,6 +192,22 @@ public void ExecutionAsyncBreak(ITestSettings settings) this.Comment("Configure launch"); runner.Launch(settings.DebuggerSettings, debuggee, "-fNonTerminating", "-fCalling"); + // Use a function breakpoint to deterministically confirm the debuggee is running + // before sending async break. Without this, the pause can arrive before GDB has + // fully started the inferior, resulting in no stopped event. + this.Comment("Set a function breakpoint to verify the debuggee enters the loop"); + FunctionBreakpoints funcBp = new FunctionBreakpoints("NonTerminating::DoSleep"); + runner.SetFunctionBreakpoints(funcBp); + + this.Comment("Start running and hit the function breakpoint"); + runner.ExpectBreakpointAndStepToTarget(SinkHelper.NonTerminating, startLine: 37, targetLine: 38) + .AfterConfigurationDone(); + + this.Comment("Remove the function breakpoint and continue so the debuggee is in run mode"); + funcBp.Remove("NonTerminating::DoSleep"); + runner.SetFunctionBreakpoints(funcBp); + runner.Continue(); + this.Comment("Try to break all"); StoppedEvent breakAllEvent = new StoppedEvent(StoppedReason.Pause); runner.Expects.Event(breakAllEvent).AfterAsyncBreak(); @@ -210,7 +226,7 @@ public void ExecutionAsyncBreak(ITestSettings settings) { IFrameInspector firstFrame = threadInspector.Stack.First(); this.WriteLine(firstFrame.ToString()); - firstFrame.GetVariable("shouldExit").Value = "true"; + firstFrame.GetVariable("this", "shouldExit").Value = "1"; } this.Comment("Continue running at the end of application"); diff --git a/test/CppTests/Tests/NatvisTests.cs b/test/CppTests/Tests/NatvisTests.cs index 58b2a2223..46f31299c 100644 --- a/test/CppTests/Tests/NatvisTests.cs +++ b/test/CppTests/Tests/NatvisTests.cs @@ -38,8 +38,8 @@ public NatvisTests(ITestOutputHelper outputHelper) : base(outputHelper) private const string NatvisSourceName = "main.cpp"; // These line numbers will need to change if src/natvis/main.cpp changes - private const int SimpleClassAssignmentLine = 65; - private const int ReturnSourceLine = 80; + private const int SimpleClassAssignmentLine = 66; + private const int ReturnSourceLine = 90; [Theory] [RequiresTestSettings] @@ -674,6 +674,52 @@ public void TestHideRawView(ITestSettings settings) } } + [Theory] + [DependsOnTest(nameof(CompileNatvisDebuggee))] + [RequiresTestSettings] + public void TestLinkedListItemsCondition(ITestSettings settings) + { + this.TestPurpose("This test checks that a Condition attribute on LinkedListItems is evaluated correctly."); + this.WriteSettings(settings); + + IDebuggee debuggee = Debuggee.Open(this, settings.CompilerSettings, NatvisName, DebuggeeMonikers.Natvis.Default); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + string visFile = Path.Join(debuggee.SourceRoot, "visualizer_files", "Simple.natvis"); + + LaunchCommand launch = new LaunchCommand(settings.DebuggerSettings, debuggee.OutputPath, visFile, false); + runner.RunCommand(launch); + + this.Comment("Set Breakpoint"); + SourceBreakpoints writerBreakpoints = debuggee.Breakpoints(NatvisSourceName, ReturnSourceLine); + runner.SetBreakpoints(writerBreakpoints); + + runner.Expects.StoppedEvent(StoppedReason.Breakpoint).AfterConfigurationDone(); + + using (IThreadInspector threadInspector = runner.GetThreadInspector()) + { + IFrameInspector currentFrame = threadInspector.Stack.First(); + + this.Comment("Verifying LinkedListItems with Condition='isActive' when isActive is true"); + var activeList = currentFrame.GetVariable("activeList"); + Assert.Contains("active=true", activeList.Value); + Assert.Equal("3", activeList.GetVariable("Count").Value); + Assert.True(activeList.Variables.ContainsKey("[0]"), "activeList should show indexed children when condition is true"); + + this.Comment("Verifying LinkedListItems with Condition='isActive' when isActive is false"); + var inactiveList = currentFrame.GetVariable("inactiveList"); + Assert.Contains("active=false", inactiveList.Value); + Assert.Equal("2", inactiveList.GetVariable("Count").Value); + Assert.False(inactiveList.Variables.ContainsKey("[0]"), "inactiveList should not show indexed children when condition is false"); + } + + runner.Expects.ExitedEvent(exitCode: 0).TerminatedEvent().AfterContinue(); + runner.DisconnectAndVerify(); + } + } + #endregion } } diff --git a/test/CppTests/debuggees/debuginfod/src/debuginfod_test.cpp b/test/CppTests/debuggees/debuginfod/src/debuginfod_test.cpp new file mode 100644 index 000000000..396682c92 --- /dev/null +++ b/test/CppTests/debuggees/debuginfod/src/debuginfod_test.cpp @@ -0,0 +1,29 @@ +// Test program that calls into library functions where debuginfod would +// attempt to download debug symbols/source. When stepping through regex +// or libc internals, GDB triggers debuginfod lookups if enabled. + +#include +#include +#include + +int do_regex_match(const std::string& input, const std::string& pattern) +{ + std::regex re(pattern); + std::smatch match; + if (std::regex_search(input, match, re)) + { + std::cout << "Match: " << match[0] << std::endl; + return 1; // breakpoint line + } + return 0; +} + +int main() +{ + std::string text = "Hello debuginfod test 12345"; + std::string pattern = R"(\d+)"; + + int result = do_regex_match(text, pattern); // step into here + std::cout << "Result: " << result << std::endl; + return result; +} diff --git a/test/CppTests/debuggees/natvis/src/ConditionalLinkedList.h b/test/CppTests/debuggees/natvis/src/ConditionalLinkedList.h new file mode 100644 index 000000000..93301010b --- /dev/null +++ b/test/CppTests/debuggees/natvis/src/ConditionalLinkedList.h @@ -0,0 +1,25 @@ +#include + +class ConditionalLinkedList +{ +private: + struct Node { + int data; + Node* next; + Node(int value) : data(value), next(NULL) {} + }; + + Node* head; + int numElements; + bool isActive; + +public: + ConditionalLinkedList(bool active) : head(NULL), numElements(0), isActive(active) {} + + void Add(int val) { + Node* n = new Node(val); + n->next = head; + head = n; + numElements++; + } +}; diff --git a/test/CppTests/debuggees/natvis/src/main.cpp b/test/CppTests/debuggees/natvis/src/main.cpp index 1a578fa08..1216a3e13 100644 --- a/test/CppTests/debuggees/natvis/src/main.cpp +++ b/test/CppTests/debuggees/natvis/src/main.cpp @@ -6,6 +6,7 @@ #include "SimpleMatrix.h" #include "SimpleTemplated.h" #include "DataPoint.h" +#include "ConditionalLinkedList.h" class SimpleDisplayObject { @@ -77,5 +78,14 @@ int main(int argc, char** argv) DataPoint dp(42); DataPoint *dpPtr = &dp; + ConditionalLinkedList activeList(true); + activeList.Add(10); + activeList.Add(20); + activeList.Add(30); + + ConditionalLinkedList inactiveList(false); + inactiveList.Add(100); + inactiveList.Add(200); + return 0; } diff --git a/test/CppTests/debuggees/natvis/src/visualizer_files/Simple.natvis b/test/CppTests/debuggees/natvis/src/visualizer_files/Simple.natvis index 189136e35..0c833dde5 100644 --- a/test/CppTests/debuggees/natvis/src/visualizer_files/Simple.natvis +++ b/test/CppTests/debuggees/natvis/src/visualizer_files/Simple.natvis @@ -121,4 +121,18 @@ {{ {this}={*this} }} + + {{ active={isActive}, count={numElements} }} + + isActive + numElements + + numElements + head + next + data + + + + \ No newline at end of file diff --git a/test/DebuggerTesting/OpenDebug/Commands/InitializeCommand.cs b/test/DebuggerTesting/OpenDebug/Commands/InitializeCommand.cs index caca01140..b91fe14d0 100644 --- a/test/DebuggerTesting/OpenDebug/Commands/InitializeCommand.cs +++ b/test/DebuggerTesting/OpenDebug/Commands/InitializeCommand.cs @@ -35,6 +35,9 @@ public sealed class Body [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public bool? supportsSetVariable; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? supportsHitConditionalBreakpoints; } public Body body = new Body(); diff --git a/test/DebuggerTesting/OpenDebug/Commands/SetBreakpointsCommand.cs b/test/DebuggerTesting/OpenDebug/Commands/SetBreakpointsCommand.cs index d16f9fabd..4ccc12c72 100644 --- a/test/DebuggerTesting/OpenDebug/Commands/SetBreakpointsCommand.cs +++ b/test/DebuggerTesting/OpenDebug/Commands/SetBreakpointsCommand.cs @@ -18,12 +18,13 @@ public sealed class SetBreakpointsCommandArgs : JsonValue { public sealed class SourceBreakpoint { - public SourceBreakpoint(int line, int? column, string condition, string logMessage) + public SourceBreakpoint(int line, int? column, string condition, string logMessage, string hitCondition) { this.line = line; this.column = column; this.condition = condition; this.logMessage = logMessage; + this.hitCondition = hitCondition; } public int line; @@ -36,6 +37,9 @@ public SourceBreakpoint(int line, int? column, string condition, string logMessa [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string logMessage; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string hitCondition; } public Source source = new Source(); @@ -72,11 +76,11 @@ public SourceBreakpoints(string sourceRoot, string relativePath) #region Add/Remove - public SourceBreakpoints Add(int lineNumber, string condition = null, string logMessage = null) + public SourceBreakpoints Add(int lineNumber, string condition = null, string logMessage = null, string hitCondition = null) { if (this.Breakpoints.ContainsKey(lineNumber)) throw new RunnerException("Breakpoint line {0} already added to file {1}.", lineNumber, this.RelativePath); - this.Breakpoints.Add(lineNumber, new SetBreakpointsCommandArgs.SourceBreakpoint(lineNumber, null, condition, logMessage)); + this.Breakpoints.Add(lineNumber, new SetBreakpointsCommandArgs.SourceBreakpoint(lineNumber, null, condition, logMessage, hitCondition)); return this; }