From e6b483946dd869b86ab834f60c6a47c84cfcb16a Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:42:32 +0100 Subject: [PATCH 01/18] trace2: move tracing sources Gather tracing implementation files under a dedicated directory so the subsequent behavioral changes are easier to review. Rename writer source files at the same time and align the public region-scope API with its lifetime by naming the operation StartRegion. Folding that terminology into the source reorganization gives every later change the final scope vocabulary instead of preserving a transitional API name that would need a standalone cleanup. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core/Authentication/OAuth/OAuth2Client.cs | 6 +++--- src/Core/{ => Tracing}/Trace.cs | 0 src/Core/{ => Tracing}/Trace2.cs | 4 ++-- src/Core/{ => Tracing}/Trace2Exception.cs | 0 src/Core/{ => Tracing}/Trace2FileWriter.cs | 0 src/Core/{ => Tracing}/Trace2Message.cs | 0 .../Trace2PipeWriter.cs} | 0 .../{Trace2StreamWriter.cs => Tracing/Trace2TextWriter.cs} | 0 src/Core/{ITrace2Writer.cs => Tracing/Trace2Writer.cs} | 0 src/Core/{ => Tracing}/TraceUtils.cs | 0 src/TestInfrastructure/Objects/NullTrace.cs | 2 +- 11 files changed, 6 insertions(+), 6 deletions(-) rename src/Core/{ => Tracing}/Trace.cs (100%) rename src/Core/{ => Tracing}/Trace2.cs (99%) rename src/Core/{ => Tracing}/Trace2Exception.cs (100%) rename src/Core/{ => Tracing}/Trace2FileWriter.cs (100%) rename src/Core/{ => Tracing}/Trace2Message.cs (100%) rename src/Core/{Trace2CollectorWriter.cs => Tracing/Trace2PipeWriter.cs} (100%) rename src/Core/{Trace2StreamWriter.cs => Tracing/Trace2TextWriter.cs} (100%) rename src/Core/{ITrace2Writer.cs => Tracing/Trace2Writer.cs} (100%) rename src/Core/{ => Tracing}/TraceUtils.cs (100%) diff --git a/src/Core/Authentication/OAuth/OAuth2Client.cs b/src/Core/Authentication/OAuth/OAuth2Client.cs index b7fe452cb4..657c995b57 100644 --- a/src/Core/Authentication/OAuth/OAuth2Client.cs +++ b/src/Core/Authentication/OAuth/OAuth2Client.cs @@ -200,7 +200,7 @@ public async Task GetAuthorizationCodeAsync(IEnum public async Task GetDeviceCodeAsync(IEnumerable scopes, CancellationToken ct) { var label = "get device code"; - using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label); + using IDisposable region = _trace2.StartRegion(OAuth2Constants.Trace2Category, label); if (_endpoints.DeviceAuthorizationEndpoint is null) { @@ -238,7 +238,7 @@ public async Task GetDeviceCodeAsync(IEnumerable public async Task GetTokenByAuthorizationCodeAsync(OAuth2AuthorizationCodeResult authorizationCodeResult, CancellationToken ct) { var label = "get token by auth code"; - using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label); + using IDisposable region = _trace2.StartRegion(OAuth2Constants.Trace2Category, label); var formData = new Dictionary { @@ -277,7 +277,7 @@ public async Task GetTokenByAuthorizationCodeAsync(OAuth2Auth public async Task GetTokenByRefreshTokenAsync(string refreshToken, CancellationToken ct) { var label = "get token by refresh token"; - using IDisposable region = _trace2.CreateRegion(OAuth2Constants.Trace2Category, label); + using IDisposable region = _trace2.StartRegion(OAuth2Constants.Trace2Category, label); var formData = new Dictionary { diff --git a/src/Core/Trace.cs b/src/Core/Tracing/Trace.cs similarity index 100% rename from src/Core/Trace.cs rename to src/Core/Tracing/Trace.cs diff --git a/src/Core/Trace2.cs b/src/Core/Tracing/Trace2.cs similarity index 99% rename from src/Core/Trace2.cs rename to src/Core/Tracing/Trace2.cs index ebce213da2..cd3baba1fd 100644 --- a/src/Core/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -197,7 +197,7 @@ void WriteError( /// Message associated with entering region. /// Path of the file this method is called from. /// Line number of file this method is called from. - Region CreateRegion( + Region StartRegion( string category, string label, string message = "", @@ -409,7 +409,7 @@ public void WriteError( }); } - public Region CreateRegion( + public Region StartRegion( string category, string label, string message, diff --git a/src/Core/Trace2Exception.cs b/src/Core/Tracing/Trace2Exception.cs similarity index 100% rename from src/Core/Trace2Exception.cs rename to src/Core/Tracing/Trace2Exception.cs diff --git a/src/Core/Trace2FileWriter.cs b/src/Core/Tracing/Trace2FileWriter.cs similarity index 100% rename from src/Core/Trace2FileWriter.cs rename to src/Core/Tracing/Trace2FileWriter.cs diff --git a/src/Core/Trace2Message.cs b/src/Core/Tracing/Trace2Message.cs similarity index 100% rename from src/Core/Trace2Message.cs rename to src/Core/Tracing/Trace2Message.cs diff --git a/src/Core/Trace2CollectorWriter.cs b/src/Core/Tracing/Trace2PipeWriter.cs similarity index 100% rename from src/Core/Trace2CollectorWriter.cs rename to src/Core/Tracing/Trace2PipeWriter.cs diff --git a/src/Core/Trace2StreamWriter.cs b/src/Core/Tracing/Trace2TextWriter.cs similarity index 100% rename from src/Core/Trace2StreamWriter.cs rename to src/Core/Tracing/Trace2TextWriter.cs diff --git a/src/Core/ITrace2Writer.cs b/src/Core/Tracing/Trace2Writer.cs similarity index 100% rename from src/Core/ITrace2Writer.cs rename to src/Core/Tracing/Trace2Writer.cs diff --git a/src/Core/TraceUtils.cs b/src/Core/Tracing/TraceUtils.cs similarity index 100% rename from src/Core/TraceUtils.cs rename to src/Core/Tracing/TraceUtils.cs diff --git a/src/TestInfrastructure/Objects/NullTrace.cs b/src/TestInfrastructure/Objects/NullTrace.cs index 96fa200184..d77839d10e 100644 --- a/src/TestInfrastructure/Objects/NullTrace.cs +++ b/src/TestInfrastructure/Objects/NullTrace.cs @@ -84,7 +84,7 @@ public void WriteError( string filePath = "", int lineNumber = 0) { } - public Region CreateRegion( + public Region StartRegion( string category, string label, string message = "", From d76772688407d24388c3f6ddf60ff0814dbf2493 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:45:28 +0100 Subject: [PATCH 02/18] trace2: refactor message formatting Consolidate event serialization and normal/performance formatting so each message declares only its event-specific data. Extract the shared format and process classifications, and align writer implementations with their source names to make later lifecycle changes smaller and clearer. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core.Tests/Trace2MessageTests.cs | 14 +- src/Core/Authentication/AuthenticationBase.cs | 2 +- src/Core/Tracing/Trace2.cs | 62 +-- src/Core/Tracing/Trace2FileWriter.cs | 11 +- src/Core/Tracing/Trace2FormatTarget.cs | 11 + src/Core/Tracing/Trace2Message.cs | 447 +++++++----------- src/Core/Tracing/Trace2PipeWriter.cs | 32 +- src/Core/Tracing/Trace2ProcessClass.cs | 18 + src/Core/Tracing/Trace2TextWriter.cs | 17 +- src/Core/Tracing/Trace2Writer.cs | 16 +- 10 files changed, 239 insertions(+), 391 deletions(-) create mode 100644 src/Core/Tracing/Trace2FormatTarget.cs create mode 100644 src/Core/Tracing/Trace2ProcessClass.cs diff --git a/src/Core.Tests/Trace2MessageTests.cs b/src/Core.Tests/Trace2MessageTests.cs index 82c1249ca5..4b833d134b 100644 --- a/src/Core.Tests/Trace2MessageTests.cs +++ b/src/Core.Tests/Trace2MessageTests.cs @@ -16,7 +16,7 @@ public class Trace2MessageTests [InlineData(100000.31608, "100000.316080")] public void BuildTimeSpan_Match_Returns_Expected_String(double input, string expected) { - var actual = Trace2Message.BuildTimeSpan(input); + var actual = PerformanceFormatFields.GetTimeSpan(input); Assert.Equal(expected, actual); } @@ -25,7 +25,7 @@ public void BuildRepoSpan_Match_Returns_Expected_String() { var input = 1; var expected = " r1 "; - var actual = Trace2Message.BuildRepoSpan(input); + var actual = PerformanceFormatFields.GetRepoSpan(input); Assert.Equal(expected, actual); } @@ -36,16 +36,15 @@ public void BuildRepoSpan_Match_Returns_Expected_String() [InlineData("foobarbazfoo", " foobarbazfo ")] public void BuildCategorySpan_Match_Returns_Expected_String(string input, string expected) { - var actual = Trace2Message.BuildCategorySpan(input); + var actual = PerformanceFormatFields.GetCategorySpan(input); Assert.Equal(expected, actual); } [Fact] public void Event_Message_Without_Snake_Case_ToJson_Creates_Expected_Json() { - var errorMessage = new ErrorMessage() + var errorMessage = new ErrorMessage { - Event = Trace2Event.Error, Sid = "123", Thread = "main", Time = new DateTimeOffset(), @@ -65,9 +64,8 @@ public void Event_Message_Without_Snake_Case_ToJson_Creates_Expected_Json() [Fact] public void Event_Message_With_Snake_Case_ToJson_Creates_Expected_Json() { - var childStartMessage = new ChildStartMessage() + var childStartMessage = new ChildStartMessage { - Event = Trace2Event.ChildStart, Sid = "123", Thread = "main", Time = new DateTimeOffset(), @@ -75,7 +73,7 @@ public void Event_Message_With_Snake_Case_ToJson_Creates_Expected_Json() Line = 1, Depth = 1, Id = 1, - Classification = Trace2ProcessClass.UIHelper, + Classification = Trace2ProcessClass.UiHelper, UseShell = false, Argv = new List() { "bar", "baz" }, ElapsedTime = 0.05 diff --git a/src/Core/Authentication/AuthenticationBase.cs b/src/Core/Authentication/AuthenticationBase.cs index 15e2e11b02..71f6ae6d28 100644 --- a/src/Core/Authentication/AuthenticationBase.cs +++ b/src/Core/Authentication/AuthenticationBase.cs @@ -46,7 +46,7 @@ protected internal virtual async Task> InvokeHelperA // authentication helper's messages. Context.Trace.Flush(); - var process = ChildProcess.Start(Context.Trace2, procStartInfo, Trace2ProcessClass.UIHelper); + var process = ChildProcess.Start(Context.Trace2, procStartInfo, Trace2ProcessClass.UiHelper); if (process is null) { var format = "Failed to start helper process: {0} {1}"; diff --git a/src/Core/Tracing/Trace2.cs b/src/Core/Tracing/Trace2.cs index cd3baba1fd..33e888c904 100644 --- a/src/Core/Tracing/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -1,52 +1,11 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.Pipes; using System.Text; -using System.Text.Json.Serialization; using System.Threading; namespace GitCredentialManager; -/// -/// The different event types tracked in the TRACE2 tracing -/// system. -/// -public enum Trace2Event -{ - [JsonStringEnumMemberName("version")] - Version = 0, - [JsonStringEnumMemberName("start")] - Start = 1, - [JsonStringEnumMemberName("exit")] - Exit = 2, - [JsonStringEnumMemberName("child_start")] - ChildStart = 3, - [JsonStringEnumMemberName("child_exit")] - ChildExit = 4, - [JsonStringEnumMemberName("error")] - Error = 5, - [JsonStringEnumMemberName("region_enter")] - RegionEnter = 6, - [JsonStringEnumMemberName("region_leave")] - RegionLeave = 7, -} - -/// -/// Classifications of processes invoked by GCM. -/// -public enum Trace2ProcessClass -{ - [JsonStringEnumMemberName("none")] - None = 0, - [JsonStringEnumMemberName("ui_helper")] - UIHelper = 1, - [JsonStringEnumMemberName("git")] - Git = 2, - [JsonStringEnumMemberName("other")] - Other = 3 -} - /// /// Stores various TRACE2 format targets user has enabled. /// Check for supported formats. @@ -57,18 +16,6 @@ public class Trace2Settings new Dictionary(); } -/// -/// Specifies a "text span" (i.e. space between two pipes) for the performance format target. -/// -public class PerformanceFormatSpan -{ - public int Size { get; set; } - - public int BeginPadding { get; set; } - - public int EndPadding { get; set; } -} - /// /// Class that manages regions. /// @@ -517,16 +464,11 @@ private void InitializeWriters() { if (TryGetPipeName(formatTarget.Value, out string name)) // Write to named pipe/socket { - AddWriter(new Trace2CollectorWriter(formatTarget.Key, ( - () => new NamedPipeClientStream(".", name, - PipeDirection.Out, - PipeOptions.Asynchronous) - ) - )); + AddWriter(new Trace2PipeWriter(formatTarget.Key, name)); } else if (formatTarget.Value.IsTruthy()) // Write to stderr { - AddWriter(new Trace2StreamWriter(formatTarget.Key, _commandContext.Streams.Error)); + AddWriter(new Trace2TextWriter(formatTarget.Key, _commandContext.Streams.Error)); } else if (Path.IsPathRooted(formatTarget.Value)) // Write to file { diff --git a/src/Core/Tracing/Trace2FileWriter.cs b/src/Core/Tracing/Trace2FileWriter.cs index 16781c0053..7090d74afc 100644 --- a/src/Core/Tracing/Trace2FileWriter.cs +++ b/src/Core/Tracing/Trace2FileWriter.cs @@ -3,20 +3,13 @@ namespace GitCredentialManager; -public class Trace2FileWriter : Trace2Writer +public class Trace2FileWriter(Trace2FormatTarget formatTarget, string path) : Trace2Writer(formatTarget) { - private readonly string _path; - - public Trace2FileWriter(Trace2FormatTarget formatTarget, string path) : base(formatTarget) - { - _path = path; - } - public override void Write(Trace2Message message) { try { - File.AppendAllText(_path, Format(message)); + File.AppendAllText(path, Format(message)); } catch (DirectoryNotFoundException) { diff --git a/src/Core/Tracing/Trace2FormatTarget.cs b/src/Core/Tracing/Trace2FormatTarget.cs new file mode 100644 index 0000000000..2aad70fd86 --- /dev/null +++ b/src/Core/Tracing/Trace2FormatTarget.cs @@ -0,0 +1,11 @@ +namespace GitCredentialManager; + +/// +/// The different format targets supported in the Trace2 tracing system. +/// +public enum Trace2FormatTarget +{ + Event, + Normal, + Performance +} diff --git a/src/Core/Tracing/Trace2Message.cs b/src/Core/Tracing/Trace2Message.cs index 175fd8bf02..c70ccb594e 100644 --- a/src/Core/Tracing/Trace2Message.cs +++ b/src/Core/Tracing/Trace2Message.cs @@ -3,9 +3,33 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace GitCredentialManager; +/// +/// The different event types tracked in the Trace2 tracing system. +/// +public enum Trace2Event +{ + [JsonStringEnumMemberName("version")] + Version, + [JsonStringEnumMemberName("start")] + Start, + [JsonStringEnumMemberName("exit")] + Exit, + [JsonStringEnumMemberName("child_start")] + ChildStart, + [JsonStringEnumMemberName("child_exit")] + ChildExit, + [JsonStringEnumMemberName("error")] + Error, + [JsonStringEnumMemberName("region_enter")] + RegionEnter, + [JsonStringEnumMemberName("region_leave")] + RegionLeave, +} + [JsonSerializable(typeof(VersionMessage))] [JsonSerializable(typeof(StartMessage))] [JsonSerializable(typeof(ExitMessage))] @@ -21,16 +45,102 @@ namespace GitCredentialManager; )] public partial class Trace2JsonContext : JsonSerializerContext; -public abstract class Trace2Message +internal class PerformanceFormatFields +{ + public static readonly PerformanceFormatFields Empty = new(); + + private const string EmptySpans = "| | | | "; + private const string EmptyRepo = " "; + private const string EmptyTime = " "; + private const string EmptyCategory = " "; + + public int? Repo { get; init; } + public double? ElapsedTime { get; init; } + public double? RelativeTime { get; init; } + public string Category { get; init; } + + public override string ToString() + { + if (ReferenceEquals(this, Empty)) + { + return EmptySpans; + } + + var sb = new StringBuilder("|"); + sb.Append(Repo is not null ? GetRepoSpan(Repo.Value) : EmptyRepo); + + sb.Append('|'); + sb.Append(ElapsedTime is not null ? GetTimeSpan(ElapsedTime.Value) : EmptyTime); + + sb.Append('|'); + sb.Append(RelativeTime is not null ? GetTimeSpan(RelativeTime.Value) : EmptyTime); + + sb.Append('|'); + sb.Append(Category is not null ? GetCategorySpan(Category) : EmptyCategory); + + return sb.ToString(); + } + + internal static string GetRepoSpan(int repo) => + GetSpan($"r{repo}", 1, 2, 5); + + internal static string GetTimeSpan(double time) => + GetSpan(time.ToString("F6"), 2, 1, 11); + + internal static string GetCategorySpan(string category) => + GetSpan(category, 1, 1, 13); + + private static string GetSpan(string data, int beginPadding, int endPadding, int size) + { + data ??= string.Empty; + var paddingTotal = beginPadding + endPadding; + var dataLimit = size - paddingTotal; + var sizeDifference = dataLimit - data.Length; + + if (sizeDifference <= 0) + { + if (double.TryParse(data, out _)) + { + // Remove all padding for values that take up the entire span + if (Math.Abs(sizeDifference) >= paddingTotal) + { + beginPadding = 0; + endPadding = 0; + } + else + { + // Decrease BeginPadding for large time values that don't occupy entire span + beginPadding += sizeDifference; + } + } + else + { + // Truncate value + data = data.Substring(0, dataLimit); + } + } + + if (data.Length < dataLimit) + { + // Increase end padding for short values + endPadding += sizeDifference; + } + + var beginPaddingStr = new string(' ', beginPadding); + var endPaddingStr = new string(' ', endPadding); + + return $"{beginPaddingStr}{data}{endPaddingStr}"; + } +} + +public abstract class Trace2Message(Trace2Event @event) { private const int SourceColumnMaxWidth = 23; private const string NormalPerfTimeFormat = "HH:mm:ss.ffffff"; - protected const string EmptyPerformanceSpan = "| | | | "; - [JsonPropertyName("event")] [JsonPropertyOrder(1)] - public Trace2Event Event { get; set; } + public Trace2Event Event { get; set; } = @event; [JsonPropertyName("sid")] [JsonPropertyOrder(2)] @@ -56,34 +166,31 @@ public abstract class Trace2Message [JsonPropertyOrder(7)] public int Depth { get; set; } - public abstract string ToJson(); - - public abstract string ToNormalString(); - - public abstract string ToPerformanceString(); + public string ToJson() => JsonSerializer.Serialize(this, GetJsonTypeInfo()); - protected abstract string BuildPerformanceSpan(); - - protected string BuildNormalString() + public string ToNormalString() { string message = GetEventMessage(Trace2FormatTarget.Normal); // The normal format uses local time rather than UTC time. string time = Time.ToLocalTime().ToString(NormalPerfTimeFormat); string source = GetSource(); + string eventName = Event.ToString().ToSnakeCase(); // Git's TRACE2 normal format is: // [ - public class Trace2CollectorWriter : Trace2Writer + public class Trace2PipeWriter : Trace2Writer { private const int DefaultMaxQueueSize = 256; - private readonly Func _createPipeFunc; + private readonly string _pipeName; private readonly BlockingCollection _queue; private Thread _writerThread; private NamedPipeClientStream _pipeClient; - public Trace2CollectorWriter(Trace2FormatTarget formatTarget, - Func createPipeFunc, - int maxQueueSize = DefaultMaxQueueSize) : base(formatTarget) + public Trace2PipeWriter(Trace2FormatTarget formatTarget, string pipeName, int maxQueueSize = DefaultMaxQueueSize) + : base(formatTarget) { - EnsureArgument.NotNull(createPipeFunc, nameof(createPipeFunc)); + EnsureArgument.NotNullOrWhiteSpace(pipeName, nameof(pipeName)); EnsureArgument.Positive(maxQueueSize, nameof(maxQueueSize)); - _createPipeFunc = createPipeFunc; + _pipeName = pipeName; _queue = new BlockingCollection(new ConcurrentQueue(), boundedCapacity: maxQueueSize); Start(); @@ -58,13 +54,19 @@ private void Start() { _writerThread = new Thread(BackgroundWriterThreadProc) { - Name = nameof(Trace2CollectorWriter), + Name = nameof(Trace2PipeWriter), IsBackground = true }; _writerThread.Start(); - // Create a new pipe stream instance using the provided factory - _pipeClient = _createPipeFunc(); + + // Create a new pipe stream instance + _pipeClient = new NamedPipeClientStream( + ".", + _pipeName, + PipeDirection.Out, + PipeOptions.Asynchronous + ); // Specify an instantaneous timeout because we don't want to hold up the // background thread loop if the pipe is not available. @@ -97,7 +99,7 @@ private void BackgroundWriterThreadProc() // or the queue has been marked as completed _and_ is empty (returns false). while (_queue.TryTake(out string message, Timeout.Infinite)) { - if (message != null) + if (message is not null) { WriteMessage(message); } @@ -109,7 +111,7 @@ private void WriteMessage(string message) try { // We should signal the end of each message with a line-feed (LF) character. - if (!message.EndsWith("\n")) + if (!message.EndsWith('\n')) { message += '\n'; } diff --git a/src/Core/Tracing/Trace2ProcessClass.cs b/src/Core/Tracing/Trace2ProcessClass.cs new file mode 100644 index 0000000000..411db8f17b --- /dev/null +++ b/src/Core/Tracing/Trace2ProcessClass.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace GitCredentialManager; + +/// +/// Classifications of processes invoked by GCM. +/// +public enum Trace2ProcessClass +{ + [JsonStringEnumMemberName("none")] + None, + [JsonStringEnumMemberName("ui_helper")] + UiHelper, + [JsonStringEnumMemberName("git")] + Git, + [JsonStringEnumMemberName("other")] + Other +} diff --git a/src/Core/Tracing/Trace2TextWriter.cs b/src/Core/Tracing/Trace2TextWriter.cs index c41f45ec87..71d076074c 100644 --- a/src/Core/Tracing/Trace2TextWriter.cs +++ b/src/Core/Tracing/Trace2TextWriter.cs @@ -1,24 +1,15 @@ -using System; using System.IO; namespace GitCredentialManager; -public class Trace2StreamWriter : Trace2Writer +public class Trace2TextWriter(Trace2FormatTarget formatTarget, TextWriter writer) : Trace2Writer(formatTarget) { - private readonly TextWriter _writer; - - public Trace2StreamWriter(Trace2FormatTarget formatTarget, TextWriter writer) - : base(formatTarget) - { - _writer = writer; - } - public override void Write(Trace2Message message) { try { - _writer.Write(Format(message)); - _writer.Flush(); + writer.Write(Format(message)); + writer.Flush(); } catch { @@ -28,7 +19,7 @@ public override void Write(Trace2Message message) protected override void ReleaseManagedResources() { - _writer.Dispose(); + writer.Dispose(); base.ReleaseManagedResources(); } } diff --git a/src/Core/Tracing/Trace2Writer.cs b/src/Core/Tracing/Trace2Writer.cs index 426c69f1eb..bdfe7dc3fe 100644 --- a/src/Core/Tracing/Trace2Writer.cs +++ b/src/Core/Tracing/Trace2Writer.cs @@ -3,17 +3,6 @@ namespace GitCredentialManager; -/// -/// The different format targets supported in the TRACE2 tracing -/// system. -/// -public enum Trace2FormatTarget -{ - Event, - Normal, - Performance -} - public interface ITrace2Writer : IDisposable { bool Failed { get; } @@ -21,7 +10,7 @@ public interface ITrace2Writer : IDisposable void Write(Trace2Message message); } -public class Trace2Writer : DisposableObject, ITrace2Writer +public abstract class Trace2Writer : DisposableObject, ITrace2Writer { private readonly Trace2FormatTarget _formatTarget; @@ -58,6 +47,5 @@ protected string Format(Trace2Message message) return sb.ToString(); } - public virtual void Write(Trace2Message message) - { } + public abstract void Write(Trace2Message message); } From d935db694ae6fa2d03247e90cd3dd7844343a4fd Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 11:24:39 +0100 Subject: [PATCH 03/18] trace2: make tracing API static TRACE2 represents one process-wide event stream, so an injected instance obscures its lifetime and makes region state appear isolatable when it is not. Make event emission and writer state static first so the API shape can be reviewed apart from startup ownership and the downstream constructor migration. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core/Tracing/Trace2.cs | 307 ++++++++++++------------------------- 1 file changed, 102 insertions(+), 205 deletions(-) diff --git a/src/Core/Tracing/Trace2.cs b/src/Core/Tracing/Trace2.cs index 33e888c904..2219e42134 100644 --- a/src/Core/Tracing/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -16,203 +16,72 @@ public class Trace2Settings new Dictionary(); } -/// -/// Class that manages regions. -/// -public class Region : DisposableObject +internal class RegionScope : DisposableObject { - private readonly ITrace2 _trace2; private readonly string _category; private readonly string _label; private readonly string _filePath; private readonly int _lineNumber; private readonly string _message; + private readonly string _thread; + private readonly int _nesting; private readonly DateTimeOffset _startTime; - public Region(ITrace2 trace2, string category, string label, string filePath, int lineNumber, string message = "") + internal RegionScope( + string category, + string label, + string filePath, + int lineNumber, + string message, + string thread, + int nesting) { - _trace2 = trace2; _category = category; _label = label; _filePath = filePath; _lineNumber = lineNumber; _message = message; + _thread = thread; + _nesting = nesting; _startTime = DateTimeOffset.UtcNow; - _trace2.WriteRegionEnter(_category, _label, _message, _filePath, _lineNumber); + Trace2.WriteRegionEnter(_category, _label, _message, _thread, _nesting, _filePath, _lineNumber); } protected override void ReleaseManagedResources() { double relativeTime = (DateTimeOffset.UtcNow - _startTime).TotalSeconds; - _trace2.WriteRegionLeave(relativeTime, _category, _label, _message, _filePath, _lineNumber); + Trace2.WriteRegionLeave( + relativeTime, _category, _label, _message, _thread, _nesting, _filePath, _lineNumber); + Trace2.CompleteRegion(_nesting); } } /// -/// Represents the application's TRACE2 tracing system. +/// The application's process-wide TRACE2 tracing system. /// -public interface ITrace2 : IDisposable +public class Trace2 : DisposableObject { - /// - /// Initialize TRACE2 tracing by initializing multi-use fields and setting up any configured target formats. - /// - /// Approximate time calling application began executing. - void Initialize(DateTimeOffset startTime); - - /// - /// Write Version and Start events. - /// - /// The path to the application. - /// Args passed to the application (if applicable). - /// Path of the file this method is called from. - /// Line number of file this method is called from. - void Start(string appPath, - string[] args, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0); - - /// - /// Write Exit event and dispose of writers. - /// - /// The exit code of the GCM application. - /// Path of the file this method is called from. - /// Line number of file this method is called from. - void Stop(int exitCode, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0); - - /// - /// Writes information related to startup of child process to trace writer. - /// - /// Time at which child process began executing. - /// Process classification. - /// Specifies whether or not OS shell was used to start the process. - /// Name of application running in child process. - /// Arguments specific to the child process. - /// The child process's session id. - /// Path of the file this method is called from. - /// Line number of file this method is called from. - void WriteChildStart(DateTimeOffset startTime, - Trace2ProcessClass processClass, - bool useShell, - string appName, - string argv, - [System.Runtime.CompilerServices.CallerFilePath] - string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] - int lineNumber = 0); - - /// - /// Writes information related to exit of child process to trace writer. - /// - /// Runtime of child process. - /// Id of exiting process. - /// Process exit code. - /// Path of the file this method is called from. - /// Line number of file this method is called from. - void WriteChildExit( - double relativeTime, - int pid, - int code, - [System.Runtime.CompilerServices.CallerFilePath] - string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] - int lineNumber = 0); - - /// - /// Writes an error as a message to the trace writer. - /// - /// The error message to write. - /// The error format string. - /// Path of the file this method is called from. - /// Line number of file this method is called from. - void WriteError( - string errorMessage, - string parameterizedMessage = null, - [System.Runtime.CompilerServices.CallerFilePath] - string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] - int lineNumber = 0); - - /// - /// Creates a region and manages entry/leaving. - /// - /// Category of region. - /// Description of region. - /// Message associated with entering region. - /// Path of the file this method is called from. - /// Line number of file this method is called from. - Region StartRegion( - string category, - string label, - string message = "", - [System.Runtime.CompilerServices.CallerFilePath] - string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] - int lineNumber = 0); - - /// - /// Writes a region enter message to the trace writer. - /// - /// Category of region. - /// Description of region. - /// Message associated with entering region. - /// Path of the file this method is called from. - /// Line number of file this method is called from. - void WriteRegionEnter( - string category, - string label, - string message = "", - [System.Runtime.CompilerServices.CallerFilePath] - string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] - int lineNumber = 0); - - /// - /// Writes a region leave message to the trace writer. - /// - /// Time of region execution. - /// Category of region. - /// Description of region. - /// Message associated with entering region. - /// Path of the file this method is called from. - /// Line number of file this method is called from. - void WriteRegionLeave( - double relativeTime, - string category, - string label, - string message = "", - [System.Runtime.CompilerServices.CallerFilePath] - string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] - int lineNumber = 0); -} - -public class Trace2 : DisposableObject, ITrace2 -{ - private readonly ICommandContext _commandContext; - private readonly object _writersLock = new object(); - private readonly Encoding _utf8NoBomEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - private readonly List _writers = new List(); - - private const string GitSidVariable = "GIT_TRACE2_PARENT_SID"; + private static ICommandContext _commandContext; + private static readonly object WritersLock = new object(); + private static readonly List Writers = new List(); + private static readonly AsyncLocal RegionNesting = new AsyncLocal(); - private DateTimeOffset _applicationStartTime; - private Trace2Settings _settings; - private string _sid; + private static DateTimeOffset _applicationStartTime; + private static Trace2Settings _settings; + private static string _sid; - private bool _initialized; + private static bool _initialized; // Increment with each new child process that is tracked - private int _childProcCounter = 0; + private static int _childProcCounter; public Trace2(ICommandContext commandContext) { _commandContext = commandContext; } - public void Initialize(DateTimeOffset startTime) + public static void Initialize(DateTimeOffset startTime) { if (_initialized) { @@ -224,11 +93,10 @@ public void Initialize(DateTimeOffset startTime) _sid = ProcessManager.Sid; InitializeWriters(); - _initialized = true; } - public void Start(string appPath, + public static void Start(string appPath, string[] args, string filePath, int lineNumber) @@ -243,18 +111,22 @@ public void Start(string appPath, WriteStart(appPath, args, filePath, lineNumber); } - public void Stop(int exitCode, string filePath, int lineNumber) + public static void Stop( + int exitCode, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { WriteExit(exitCode, filePath, lineNumber); } - public void WriteChildStart(DateTimeOffset startTime, + public static void WriteChildStart( + DateTimeOffset startTime, Trace2ProcessClass processClass, bool useShell, string appName, string argv, - string filePath = "", - int lineNumber = 0) + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { // Some child processes are started before TRACE2 can be initialized. // Since certain dependencies are not available until initialization, @@ -294,12 +166,28 @@ public void WriteChildStart(DateTimeOffset startTime, }); } - public void WriteChildExit( + public static void WriteChildExit( + DateTimeOffset startTime, + int pid, + int code, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) => + WriteChildExit(DateTimeOffset.UtcNow - startTime, pid, code, filePath, lineNumber); + + public static void WriteChildExit( + TimeSpan relativeTime, + int pid, + int code, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) => + WriteChildExit(relativeTime.TotalSeconds, pid, code, filePath, lineNumber); + + public static void WriteChildExit( double relativeTime, int pid, int code, - string filePath = "", - int lineNumber = 0) + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { // Some child processes are started before TRACE2 can be initialized. // Since certain dependencies are not available until initialization, @@ -327,7 +215,7 @@ public void WriteChildExit( }); } - public void WriteError( + public static void WriteError( string errorMessage, string parameterizedMessage = null, [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", @@ -356,22 +244,26 @@ public void WriteError( }); } - public Region StartRegion( + public static IDisposable StartRegion( string category, string label, - string message, - string filePath, - int lineNumber) + string message = "", + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { - return new Region(this, category, label, filePath, lineNumber, message); + int nesting = RegionNesting.Value + 1; + RegionNesting.Value = nesting; + return new RegionScope(category, label, filePath, lineNumber, message, BuildThreadName(), nesting); } - public void WriteRegionEnter( + internal static void WriteRegionEnter( string category, string label, - string message = "", - string filePath = "", - int lineNumber = 0) + string message, + string thread, + int nesting, + string filePath, + int lineNumber) { WriteMessage(new RegionEnterMessage() { @@ -381,21 +273,24 @@ public void WriteRegionEnter( Category = category, Label = label, Message = message == "" ? label : message, - Thread = BuildThreadName(), + Thread = thread, File = Path.GetFileName(filePath), Line = lineNumber, ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds, + Nesting = nesting, Depth = ProcessManager.Depth }); } - public void WriteRegionLeave( + internal static void WriteRegionLeave( double relativeTime, string category, string label, - string message = "", - string filePath = "", - int lineNumber = 0) + string message, + string thread, + int nesting, + string filePath, + int lineNumber) { WriteMessage(new RegionLeaveMessage() { @@ -405,26 +300,32 @@ public void WriteRegionLeave( Category = category, Label = label, Message = message == "" ? label : message, - Thread = BuildThreadName(), + Thread = thread, File = Path.GetFileName(filePath), Line = lineNumber, ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds, RelativeTime = relativeTime, + Nesting = nesting, Depth = ProcessManager.Depth }); } + internal static void CompleteRegion(int nesting) + { + RegionNesting.Value = Math.Max(0, nesting - 1); + } + protected override void ReleaseManagedResources() { - lock (_writersLock) + lock (WritersLock) { try { - for (int i = _writers.Count - 1; i >= 0; i--) + for (int i = Writers.Count - 1; i >= 0; i--) { - using (_writers[i]) + using (Writers[i]) { - _writers.RemoveAt(i); + Writers.RemoveAt(i); } } } @@ -457,7 +358,7 @@ internal static bool TryGetPipeName(string eventTarget, out string name) return false; } - private void InitializeWriters() + private static void InitializeWriters() { // Set up the correct writer for every enabled format target. foreach (var formatTarget in _settings.FormatTargetsAndValues) @@ -484,7 +385,7 @@ private void InitializeWriters() } } - private void WriteVersion( + private static void WriteVersion( string gcmVersion, string filePath, int lineNumber, @@ -505,7 +406,7 @@ private void WriteVersion( }); } - private void WriteStart( + private static void WriteStart( string appPath, string[] args, string filePath, @@ -535,7 +436,7 @@ private void WriteStart( }); } - private void WriteExit(int code, string filePath = "", int lineNumber = 0) + private static void WriteExit(int code, string filePath = "", int lineNumber = 0) { EnsureArgument.NotNull(code, nameof(code)); @@ -552,37 +453,33 @@ private void WriteExit(int code, string filePath = "", int lineNumber = 0) }); } - private void AddWriter(ITrace2Writer writer) + private static void AddWriter(ITrace2Writer writer) { - ThrowIfDisposed(); - - lock (_writersLock) + lock (WritersLock) { // Try not to add the same writer more than once - if (_writers.Contains(writer)) + if (Writers.Contains(writer)) return; - _writers.Add(writer); + Writers.Add(writer); } } - private void WriteMessage(Trace2Message message) + private static void WriteMessage(Trace2Message message) { - ThrowIfDisposed(); - if (!_initialized) { return; } - lock (_writersLock) + lock (WritersLock) { - if (_writers.Count == 0) + if (Writers.Count == 0) { return; } - foreach (var writer in _writers) + foreach (var writer in Writers) { if (!writer.Failed) { From c9d160877db78989b1fed7f1d7bfeb4049107a14 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 11:25:14 +0100 Subject: [PATCH 04/18] trace2: own process initialization Tracing begins before CommandContext exists and survives until dispatcher shutdown, so injected services create a circular dependency for process identity and settings. Let Trace2 establish its SID, depth, targets, and initial events at entry, then close writers after dispatch ends. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core.Tests/ProcessManagerTests.cs | 33 ---- src/Core.Tests/Trace2Tests.cs | 61 ++++++ src/Core/ProcessManager.cs | 49 ----- src/Core/Settings.cs | 30 --- src/Core/Tracing/Trace2.cs | 175 +++++++++++++++--- .../Objects/TestSettings.cs | 11 -- src/git-credential-manager/Program.cs | 16 +- 7 files changed, 215 insertions(+), 160 deletions(-) delete mode 100644 src/Core.Tests/ProcessManagerTests.cs diff --git a/src/Core.Tests/ProcessManagerTests.cs b/src/Core.Tests/ProcessManagerTests.cs deleted file mode 100644 index 8e96c41f0f..0000000000 --- a/src/Core.Tests/ProcessManagerTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using GitCredentialManager; -using Xunit; - -namespace Core.Tests; - -public class ProcessManagerTests -{ - [Theory] - [InlineData("", 0)] - [InlineData("foo", 0)] - [InlineData("foo/bar", 1)] - [InlineData("foo/bar/baz", 2)] - public void CreateSid_Envar_Returns_Expected_Sid(string input, int expected) - { - ProcessManager.Sid = input; - var actual = ProcessManager.GetProcessDepth(); - - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("", 0)] - [InlineData("foo", 0)] - [InlineData("foo/bar", 1)] - [InlineData("foo/bar/baz", 2)] - public void TryGetProcessDepth_Returns_Expected_Depth(string input, int expected) - { - ProcessManager.Sid = input; - var actual = ProcessManager.GetProcessDepth(); - - Assert.Equal(expected, actual); - } -} \ No newline at end of file diff --git a/src/Core.Tests/Trace2Tests.cs b/src/Core.Tests/Trace2Tests.cs index 38011275db..32932ba7d0 100644 --- a/src/Core.Tests/Trace2Tests.cs +++ b/src/Core.Tests/Trace2Tests.cs @@ -1,3 +1,4 @@ +using System; using Xunit; namespace GitCredentialManager.Tests; @@ -28,4 +29,64 @@ public void TryGetPipeName_Windows_Returns_Expected_Value(string input, string e Assert.True(isSuccessful); Assert.Equal(expected, actual); } + + [Theory] + [InlineData("", 0)] + [InlineData("abc", 0)] + [InlineData("abc/def", 1)] + [InlineData("abc/def/ghi", 2)] + [InlineData("abc/", 1)] + public void GetProcessDepth_ReturnsCorrectDepthh(string sid, int expected) + { + int actual = Trace2.GetProcessDepth(sid); + Assert.Equal(expected, actual); + } + + [Fact] + public void CreateSid_ExistingParentSid_AppendsToExisting() + { + var originalSid = Environment.GetEnvironmentVariable(Trace2.SidEnvar); + + try + { + // Set parent SID + const string parentSid = "0ddfc330-30e9-49f3-86d3-6b34d99d51f4"; + Environment.SetEnvironmentVariable(Trace2.SidEnvar, parentSid); + + string actualSid = Trace2.CreateSid(); + + const string parentPrefix = $"{parentSid}/"; + Assert.StartsWith(parentPrefix, actualSid); + + string rest = actualSid.Substring(parentPrefix.Length); + Assert.False(string.IsNullOrWhiteSpace(rest)); + } + finally + { + // Restore original environment variable for this process + Environment.SetEnvironmentVariable(Trace2.SidEnvar, originalSid); + } + } + + [Fact] + public void CreateSid_NoParentSid_CreatesNew() + { + var originalSid = Environment.GetEnvironmentVariable(Trace2.SidEnvar); + + try + { + // Clear parent SID + Environment.SetEnvironmentVariable(Trace2.SidEnvar, null); + + string actualSid = Trace2.CreateSid(); + + Assert.False(string.IsNullOrWhiteSpace(actualSid)); + Assert.DoesNotContain("/", actualSid); + } + finally + { + // Restore original environment variable for this process + Environment.SetEnvironmentVariable(Trace2.SidEnvar, originalSid); + } + } } diff --git a/src/Core/ProcessManager.cs b/src/Core/ProcessManager.cs index 4c5988c4df..b9db5fa625 100644 --- a/src/Core/ProcessManager.cs +++ b/src/Core/ProcessManager.cs @@ -27,14 +27,8 @@ public interface IProcessManager public class ProcessManager : IProcessManager { - private const string SidEnvar = "GIT_TRACE2_PARENT_SID"; - protected readonly ITrace2 Trace2; - public static string Sid { get; internal set; } - - public static int Depth { get; internal set; } - public ProcessManager(ITrace2 trace2) { EnsureArgument.NotNull(trace2, nameof(trace2)); @@ -60,47 +54,4 @@ public virtual ChildProcess CreateProcess(ProcessStartInfo psi) { return new ChildProcess(Trace2, psi); } - - /// - /// Create a TRACE2 "session id" (sid) for this process. - /// - public static void CreateSid() - { - Sid = Environment.GetEnvironmentVariable(SidEnvar); - - if (!string.IsNullOrEmpty(Sid)) - { - // Use trim to ensure no accidental leading or trailing slashes - Sid = $"{Sid.Trim('/')}/{Guid.NewGuid():D}"; - // Only check for process depth if there is a parent. - // If there is not a parent, depth defaults to 0. - Depth = GetProcessDepth(); - } - else - { - // We are the root process; create our own 'root' SID - Sid = Guid.NewGuid().ToString("D"); - } - - Environment.SetEnvironmentVariable(SidEnvar, Sid); - } - - /// - /// Get "depth" of current process relative to top-level GCM process. - /// - /// Depth of current process. - internal static int GetProcessDepth() - { - char processSeparator = '/'; - - int count = 0; - // Use AsSpan() for slight performance bump over traditional foreach loop. - foreach (var c in Sid.AsSpan()) - { - if (c == processSeparator) - count++; - } - - return count; - } } diff --git a/src/Core/Settings.cs b/src/Core/Settings.cs index 15d22bb4a9..ae787ef191 100644 --- a/src/Core/Settings.cs +++ b/src/Core/Settings.cs @@ -198,11 +198,6 @@ public interface ISettings : IDisposable /// bool AllowUnsafeRemotes { get; } - /// - /// Get TRACE2 settings. - /// - /// TRACE2 settings object. - Trace2Settings GetTrace2Settings(); } public class ProxyConfiguration @@ -595,31 +590,6 @@ public bool UseSoftwareRendering KnownGitCfg.Credential.AllowUnsafeRemotes, out string str) && str.ToBooleanyOrDefault(false); - public Trace2Settings GetTrace2Settings() - { - var settings = new Trace2Settings(); - - if (TryGetSetting(Constants.EnvironmentVariables.GitTrace2Event, KnownGitCfg.Trace2.SectionName, - Constants.GitConfiguration.Trace2.EventTarget, out string value)) - { - settings.FormatTargetsAndValues.Add(Trace2FormatTarget.Event, value); - } - - if (TryGetSetting(Constants.EnvironmentVariables.GitTrace2Normal, KnownGitCfg.Trace2.SectionName, - Constants.GitConfiguration.Trace2.NormalTarget, out value)) - { - settings.FormatTargetsAndValues.Add(Trace2FormatTarget.Normal, value); - } - - if (TryGetSetting(Constants.EnvironmentVariables.GitTrace2Performance, KnownGitCfg.Trace2.SectionName, - Constants.GitConfiguration.Trace2.PerformanceTarget, out value)) - { - settings.FormatTargetsAndValues.Add(Trace2FormatTarget.Performance, value); - } - - return settings; - } - public bool IsSecretTracingEnabled => TryGetSetting(KnownEnvars.GcmTraceSecrets, KnownGitCfg.Credential.SectionName, diff --git a/src/Core/Tracing/Trace2.cs b/src/Core/Tracing/Trace2.cs index 2219e42134..f5ac318c48 100644 --- a/src/Core/Tracing/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Text; using System.Threading; @@ -10,9 +11,9 @@ namespace GitCredentialManager; /// Stores various TRACE2 format targets user has enabled. /// Check for supported formats. /// -public class Trace2Settings +internal class Trace2Settings { - public IDictionary FormatTargetsAndValues { get; set; } = + public IDictionary Targets { get; } = new Dictionary(); } @@ -61,9 +62,9 @@ protected override void ReleaseManagedResources() /// /// The application's process-wide TRACE2 tracing system. /// -public class Trace2 : DisposableObject +public static class Trace2 { - private static ICommandContext _commandContext; + internal const string SidEnvar = "GIT_TRACE2_PARENT_SID"; private static readonly object WritersLock = new object(); private static readonly List Writers = new List(); private static readonly AsyncLocal RegionNesting = new AsyncLocal(); @@ -71,32 +72,68 @@ public class Trace2 : DisposableObject private static DateTimeOffset _applicationStartTime; private static Trace2Settings _settings; private static string _sid; + private static int _depth; private static bool _initialized; // Increment with each new child process that is tracked private static int _childProcCounter; - public Trace2(ICommandContext commandContext) - { - _commandContext = commandContext; - } - - public static void Initialize(DateTimeOffset startTime) + public static void Initialize( + string[] args, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { if (_initialized) { return; } - _applicationStartTime = startTime; - _settings = _commandContext.Settings.GetTrace2Settings(); - _sid = ProcessManager.Sid; + _applicationStartTime = DateTimeOffset.UtcNow; + _sid = CreateSid(); + Environment.SetEnvironmentVariable(SidEnvar, _sid); + _depth = GetProcessDepth(_sid); + _settings = ReadSettings(); InitializeWriters(); _initialized = true; + + string appPath = Environment.ProcessPath ?? Environment.GetCommandLineArgs()[0]; + Start(appPath, args, filePath, lineNumber); + } + + internal static string CreateSid() + { + // Use trim to ensure no accidental leading or trailing slashes + var sid = Environment.GetEnvironmentVariable(SidEnvar)?.Trim('/'); + + // If we are the root process we must create our own 'root' SID, + // otherwise append a new UUID to the existing root. + sid = string.IsNullOrEmpty(sid) + ? Guid.NewGuid().ToString("D") + : $"{sid}/{Guid.NewGuid():D}"; + + return sid; } - public static void Start(string appPath, + /// + /// Get "depth" of current process relative to top-level Trace2 process. + /// + /// Depth of current process. + internal static int GetProcessDepth(string sid) + { + const char processSeparator = '/'; + + int count = 0; + for (var i = 0; i < sid.Length; i++) // use for-loop to avoid IEnumerable overhead from a foreach-loop + { + if (sid[i] == processSeparator) + count++; + } + + return count; + } + + private static void Start(string appPath, string[] args, string filePath, int lineNumber) @@ -117,6 +154,7 @@ public static void Stop( [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { WriteExit(exitCode, filePath, lineNumber); + DisposeWriters(); } public static void WriteChildStart( @@ -162,7 +200,7 @@ public static void WriteChildStart( UseShell = useShell, Argv = procArgs, ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds, - Depth = ProcessManager.Depth, + Depth = _depth, }); } @@ -211,7 +249,7 @@ public static void WriteChildExit( Code = code, ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds, RelativeTime = relativeTime, - Depth = ProcessManager.Depth + Depth = _depth }); } @@ -240,7 +278,7 @@ public static void WriteError( Line = lineNumber, Message = errorMessage, ParameterizedMessage = parameterizedMessage ?? errorMessage, - Depth = ProcessManager.Depth + Depth = _depth }); } @@ -278,7 +316,7 @@ internal static void WriteRegionEnter( Line = lineNumber, ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds, Nesting = nesting, - Depth = ProcessManager.Depth + Depth = _depth }); } @@ -306,7 +344,7 @@ internal static void WriteRegionLeave( ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds, RelativeTime = relativeTime, Nesting = nesting, - Depth = ProcessManager.Depth + Depth = _depth }); } @@ -315,7 +353,7 @@ internal static void CompleteRegion(int nesting) RegionNesting.Value = Math.Max(0, nesting - 1); } - protected override void ReleaseManagedResources() + private static void DisposeWriters() { lock (WritersLock) { @@ -334,8 +372,6 @@ protected override void ReleaseManagedResources() /* squelch */ } } - - base.ReleaseManagedResources(); } internal static bool TryGetPipeName(string eventTarget, out string name) @@ -358,10 +394,101 @@ internal static bool TryGetPipeName(string eventTarget, out string name) return false; } + private static Trace2Settings ReadSettings() + { + var settings = new Trace2Settings(); + var gitConfig = new Lazy>(ReadGitConfig); + + AddTarget(settings, gitConfig, + Trace2FormatTarget.Event, + Constants.EnvironmentVariables.GitTrace2Event, + Constants.GitConfiguration.Trace2.EventTarget); + AddTarget(settings, gitConfig, + Trace2FormatTarget.Normal, + Constants.EnvironmentVariables.GitTrace2Normal, + Constants.GitConfiguration.Trace2.NormalTarget); + AddTarget(settings, gitConfig, + Trace2FormatTarget.Performance, + Constants.EnvironmentVariables.GitTrace2Performance, + Constants.GitConfiguration.Trace2.PerformanceTarget); + + return settings; + } + + private static void AddTarget( + Trace2Settings settings, + Lazy> gitConfig, + Trace2FormatTarget format, + string environmentVariable, + string configurationProperty) + { + string value = Environment.GetEnvironmentVariable(environmentVariable); + if (value is null) + { + string key = $"{Constants.GitConfiguration.Trace2.SectionName}.{configurationProperty}"; + value = gitConfig.Value.GetValueOrDefault(key); + } + + if (value is not null) + { + settings.Targets.Add(format, value); + } + } + + private static Dictionary ReadGitConfig() + { + string programName = OperatingSystem.IsWindows() ? "git.exe" : "git"; + string gitExecPath = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.GitExecutablePath); + string candidatePath = string.IsNullOrEmpty(gitExecPath) + ? null + : Path.Combine(gitExecPath, programName); + string gitPath = candidatePath is not null && File.Exists(candidatePath) + ? candidatePath + : programName; + + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + try + { + // Read all Git's 'trace2.*' configuration in one shot to avoid repeated calls + var startInfo = new ProcessStartInfo(gitPath, "config -z --get-regexp trace2\\..*") + { + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + + using Process process = Process.Start(startInfo); + string data = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode == 0) + { + string[] kvps = data.Split('\0', StringSplitOptions.RemoveEmptyEntries); + foreach (string kvp in kvps) + { + string[] parts = kvp.Split('\n', count: 2); + if (parts.Length == 2) + { + string key = parts[0].Trim(); + string value = parts[1].Trim(); + dict[key] = value; + } + } + } + } + catch (Exception ex) when (ex is InvalidOperationException or System.ComponentModel.Win32Exception) + { + // ignore + } + + return dict; + } + private static void InitializeWriters() { // Set up the correct writer for every enabled format target. - foreach (var formatTarget in _settings.FormatTargetsAndValues) + foreach (var formatTarget in _settings.Targets) { if (TryGetPipeName(formatTarget.Value, out string name)) // Write to named pipe/socket { @@ -369,7 +496,7 @@ private static void InitializeWriters() } else if (formatTarget.Value.IsTruthy()) // Write to stderr { - AddWriter(new Trace2TextWriter(formatTarget.Key, _commandContext.Streams.Error)); + AddWriter(new Trace2TextWriter(formatTarget.Key, Console.Error)); } else if (Path.IsPathRooted(formatTarget.Value)) // Write to file { diff --git a/src/TestInfrastructure/Objects/TestSettings.cs b/src/TestInfrastructure/Objects/TestSettings.cs index ddbbd86181..45d4e8e1e7 100644 --- a/src/TestInfrastructure/Objects/TestSettings.cs +++ b/src/TestInfrastructure/Objects/TestSettings.cs @@ -55,17 +55,6 @@ public class TestSettings : ISettings public bool AllowUnsafeRemotes { get; set; } = false; - public Trace2Settings GetTrace2Settings() - { - return new Trace2Settings() - { - FormatTargetsAndValues = new Dictionary() - { - { Trace2FormatTarget.Event, "foo" } - } - }; - } - #region ISettings public bool TryGetSetting(string envarName, string section, string property, out string value) diff --git a/src/git-credential-manager/Program.cs b/src/git-credential-manager/Program.cs index b8aa0eb7c9..c0abd426dc 100644 --- a/src/git-credential-manager/Program.cs +++ b/src/git-credential-manager/Program.cs @@ -5,7 +5,6 @@ using GitHub; using GitLab; using Microsoft.AzureRepos; -using GitCredentialManager.Authentication; using GitCredentialManager.UI; namespace GitCredentialManager @@ -16,6 +15,8 @@ public static class Program public static void Main(string[] args) { + Trace2.Initialize(args); + // Create the dispatcher on the main thread. This is required // for some platform UI services such as macOS that mandates // all controls are created/accessed on the initial thread @@ -34,6 +35,7 @@ public static void Main(string[] args) Dispatcher.MainThread.Run(); // Dispatcher was shutdown + Trace2.Stop(_exitCode); Environment.Exit(_exitCode); } @@ -41,20 +43,9 @@ private static void AppMain(object o) { string[] args = (string[])o; - var startTime = DateTimeOffset.UtcNow; - // Set the session id (sid) and start time for the GCM process, to be - // used when TRACE2 tracing is enabled. - ProcessManager.CreateSid(); - using (var context = new CommandContext()) using (var app = new Application(context)) { - // Initialize TRACE2 system - context.Trace2.Initialize(startTime); - - // Write the start and version events - context.Trace2.Start(context.ApplicationPath, args); - // Register all supported host providers at the normal priority. // The generic provider should never win against a more specific one, so register it with low priority. app.RegisterProvider(new AzureReposHostProvider(context), HostProviderPriority.Normal); @@ -68,7 +59,6 @@ private static void AppMain(object o) .GetAwaiter() .GetResult(); - context.Trace2.Stop(_exitCode); Dispatcher.MainThread.Shutdown(); } } From 0395c0e4e7201b95244a89ca9a7773c4483be5f2 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 11:25:49 +0100 Subject: [PATCH 05/18] trace2: remove tracing from context CommandContext should expose replaceable execution services, not own a process-wide event stream. Removing TRACE2 from that surface makes the remaining injected dependencies explicit and avoids preserving a fake test tracer for state that can no longer vary by context. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core/CommandContext.cs | 8 --- src/TestInfrastructure/Objects/NullTrace.cs | 70 ------------------- .../Objects/TestCommandContext.cs | 2 - 3 files changed, 80 deletions(-) diff --git a/src/Core/CommandContext.cs b/src/Core/CommandContext.cs index 60f330932a..425dae025b 100644 --- a/src/Core/CommandContext.cs +++ b/src/Core/CommandContext.cs @@ -51,11 +51,6 @@ public interface ICommandContext : IDisposable /// ITrace Trace { get; } - /// - /// Application TRACE2 tracing system. - /// - ITrace2 Trace2 { get; } - /// /// File system abstraction (exists mainly for testing). /// @@ -99,7 +94,6 @@ public CommandContext() Streams = new StandardStreams(); Trace = new Trace(); - Trace2 = new Trace2(this); Console = new ConsoleService(Streams); if (PlatformUtils.IsWindows()) @@ -207,8 +201,6 @@ private static string GetGitPath(IEnvironment environment, IFileSystem fileSyste public ITrace Trace { get; } - public ITrace2 Trace2 { get; } - public IFileSystem FileSystem { get; } public ICredentialStore CredentialStore { get; } diff --git a/src/TestInfrastructure/Objects/NullTrace.cs b/src/TestInfrastructure/Objects/NullTrace.cs index d77839d10e..54230c3900 100644 --- a/src/TestInfrastructure/Objects/NullTrace.cs +++ b/src/TestInfrastructure/Objects/NullTrace.cs @@ -47,74 +47,4 @@ void IDisposable.Dispose() { } #endregion } - - public class NullTrace2 : ITrace2 - { - #region ITrace2 - - public void Initialize(DateTimeOffset startTime) { } - - public void Start(string appPath, - string[] args, - string filePath = "", - int lineNumber = 0) { } - - public void Stop(int exitCode, - string fileName, - int lineNumber) { } - - public void WriteChildStart(DateTimeOffset startTime, - Trace2ProcessClass processClass, - bool useShell, - string appName, - string argv, - string filePath = "", - int lineNumber = 0) { } - - public void WriteChildExit( - double relativeTime, - int pid, - int code, - string filePath = "", - int lineNumber = 0) { } - - public void WriteError( - string errorMessage, - string parameterizedMessage = null, - string filePath = "", - int lineNumber = 0) { } - - public Region StartRegion( - string category, - string label, - string message = "", - string filePath = "", - int lineNumber = 0) - { - return new Region(this, category, label, filePath, lineNumber, message); - } - - public void WriteRegionEnter( - string category, - string label, - string message = "", - string filePath = "", - int lineNumber = 0) { } - - public void WriteRegionLeave( - double relativeTime, - string category, - string label, - string message = "", - string filePath = "", - int lineNumber = 0) { } - - #endregion - - #region IDisposable - - void IDisposable.Dispose() { } - - #endregion - } } diff --git a/src/TestInfrastructure/Objects/TestCommandContext.cs b/src/TestInfrastructure/Objects/TestCommandContext.cs index 46f679d69e..990444fa31 100644 --- a/src/TestInfrastructure/Objects/TestCommandContext.cs +++ b/src/TestInfrastructure/Objects/TestCommandContext.cs @@ -17,7 +17,6 @@ public TestCommandContext() Console = new TestConsoleService(); SessionManager = new TestSessionManager(); Trace = new NullTrace(); - Trace2 = new NullTrace2(); FileSystem = new TestFileSystem(); CredentialStore = new TestCredentialStore(); HttpClientFactory = new TestHttpClientFactory(); @@ -34,7 +33,6 @@ public TestCommandContext() public TestConsoleService Console { get; set; } public TestSessionManager SessionManager { get; set; } public ITrace Trace { get; set; } - public ITrace2 Trace2 { get; set; } public TestFileSystem FileSystem { get; set; } public TestCredentialStore CredentialStore { get; set; } public TestHttpClientFactory HttpClientFactory { get; set; } From f79adf279bcb2687ad70f661220873c0c1abbc65 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 11:26:48 +0100 Subject: [PATCH 06/18] trace2: decouple core services Core infrastructure cannot meaningfully substitute a process-wide tracer, yet carrying it through process, Git, HTTP, and platform constructors forces every helper to mirror global lifetime. Remove that dependency from the service layer and emit its existing events through the static API, while leaving leaf exception conversion for the final pass. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core.Tests/GitConfigurationTests.cs | 78 +++++++------------ src/Core.Tests/GitTests.cs | 30 +++---- src/Core.Tests/HttpClientFactoryTests.cs | 26 +++---- src/Core.Tests/TestProcessManager.cs | 6 +- src/Core.Tests/WslUtilsTests.cs | 4 +- src/Core/ChildProcess.cs | 25 ++---- src/Core/CommandContext.cs | 11 +-- src/Core/Constants.cs | 4 +- src/Core/Diagnostics/EnvironmentDiagnostic.cs | 2 +- src/Core/Diagnostics/GitDiagnostic.cs | 3 +- src/Core/Git.cs | 30 +++---- src/Core/GitConfiguration.cs | 23 +++--- src/Core/Gpg.cs | 5 +- src/Core/HttpClientFactory.cs | 8 +- .../Interop/Windows/WindowsProcessManager.cs | 4 +- src/Core/PlatformUtils.cs | 8 +- src/Core/ProcessManager.cs | 11 +-- src/Core/WslUtils.cs | 4 +- src/TestInfrastructure/GitTestUtilities.cs | 6 +- 19 files changed, 106 insertions(+), 182 deletions(-) diff --git a/src/Core.Tests/GitConfigurationTests.cs b/src/Core.Tests/GitConfigurationTests.cs index 5005cf4319..10ad579c1b 100644 --- a/src/Core.Tests/GitConfigurationTests.cs +++ b/src/Core.Tests/GitConfigurationTests.cs @@ -47,9 +47,8 @@ public void GitProcess_GetConfiguration_ReturnsConfiguration() { string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath); + var git = new GitProcess(trace, processManager, gitPath); var config = git.GetConfiguration(); Assert.NotNull(config); } @@ -71,9 +70,8 @@ public void GitConfiguration_Enumerate_CallbackReturnsTrue_InvokesCallbackForEac string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); var actualVisitedEntries = new List<(string name, string value)>(); @@ -110,9 +108,8 @@ public void GitConfiguration_Enumerate_CallbackReturnsFalse_InvokesCallbackForEa string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); var actualVisitedEntries = new List<(string name, string value)>(); @@ -140,10 +137,9 @@ public void GitConfiguration_TryGet_Name_Exists_ReturnsTrueOutString() ExecGit(repoPath, workDirPath, "config --local user.name john.doe").AssertSuccess(); string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet("user.name", false, out string value); @@ -160,9 +156,8 @@ public void GitConfiguration_TryGet_Name_DoesNotExists_ReturnsFalse() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); string randomName = $"{Guid.NewGuid():N}.{Guid.NewGuid():N}"; @@ -180,9 +175,8 @@ public void GitConfiguration_TryGet_IsPath_True_ReturnsCanonicalPath() string homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet("example.path", true, out string value); @@ -199,9 +193,8 @@ public void GitConfiguration_TryGet_IsPath_False_ReturnsRawConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet("example.path", false, out string value); @@ -218,9 +211,8 @@ public void GitConfiguration_TryGet_BoolType_ReturnsCanonicalBool() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Bool, @@ -238,9 +230,8 @@ public void GitConfiguration_TryGet_BoolWithoutType_ReturnsRawConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); bool result = config.TryGet(GitConfigurationLevel.Local, GitConfigurationType.Raw, @@ -258,10 +249,9 @@ public void GitConfiguration_Get_Name_Exists_ReturnsString() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); string value = config.Get("user.name"); @@ -276,9 +266,8 @@ public void GitConfiguration_Get_Name_DoesNotExists_ThrowsException() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); string randomName = $"{Guid.NewGuid():N}.{Guid.NewGuid():N}"; @@ -292,9 +281,8 @@ public void GitConfiguration_Set_Local_SetsLocalConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath);; + var git = new GitProcess(trace, processManager, gitPath, repoPath);; IGitConfiguration config = git.GetConfiguration(); config.Set(GitConfigurationLevel.Local, "core.foobar", "foo123"); @@ -311,9 +299,8 @@ public void GitConfiguration_Set_All_ThrowsException() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); Assert.Throws(() => @@ -331,9 +318,8 @@ public void GitConfiguration_Unset_Global_UnsetsGlobalConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); config.Unset(GitConfigurationLevel.Global, "core.foobar"); @@ -363,9 +349,8 @@ public void GitConfiguration_Unset_Local_UnsetsLocalConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); config.Unset(GitConfigurationLevel.Local, "core.foobar"); @@ -390,9 +375,8 @@ public void GitConfiguration_Unset_All_ThrowsException() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); Assert.Throws(() => config.Unset(GitConfigurationLevel.All, "core.foobar")); @@ -408,9 +392,8 @@ public void GitConfiguration_UnsetAll_UnsetsAllConfig() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); config.UnsetAll(GitConfigurationLevel.Local, "core.foobar", "foo*"); @@ -428,9 +411,8 @@ public void GitConfiguration_UnsetAll_All_ThrowsException() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); Assert.Throws(() => @@ -446,10 +428,9 @@ public void GitConfiguration_CacheTryGet_ReturnsValueFromCache() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); // First access loads cache @@ -473,10 +454,9 @@ public void GitConfiguration_CacheGetAll_ReturnsAllValuesFromCache() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); var values = new List(config.GetAll("test.multi")); @@ -496,10 +476,9 @@ public void GitConfiguration_CacheEnumerate_EnumeratesFromCache() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); var cacheEntries = new List<(string key, string value)>(); @@ -525,10 +504,9 @@ public void GitConfiguration_CacheInvalidation_SetInvalidatesCache() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); // Load cache with initial value @@ -553,10 +531,9 @@ public void GitConfiguration_CacheInvalidation_AddInvalidatesCache() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); // Load cache @@ -582,10 +559,9 @@ public void GitConfiguration_CacheInvalidation_UnsetInvalidatesCache() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); // Load cache @@ -614,10 +590,9 @@ public void GitConfiguration_CacheLevelFilter_ReturnsOnlyLocalValues() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); // Get local value only @@ -641,10 +616,9 @@ public void GitConfiguration_TypedQuery_CanonicalizesValues() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, repoPath); + var git = new GitProcess(trace, processManager, gitPath, repoPath); IGitConfiguration config = git.GetConfiguration(); // Path type queries use a separate cache loaded with --type=path, diff --git a/src/Core.Tests/GitTests.cs b/src/Core.Tests/GitTests.cs index a6905bb8f5..c71e559bea 100644 --- a/src/Core.Tests/GitTests.cs +++ b/src/Core.Tests/GitTests.cs @@ -13,9 +13,8 @@ public void Git_GetCurrentRepository_NoLocalRepo_ReturnsNull() { string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath()); + var git = new GitProcess(trace, processManager, gitPath, Path.GetTempPath()); string actual = git.GetCurrentRepository(); @@ -29,9 +28,8 @@ public void Git_GetCurrentRepository_LocalRepo_ReturnsNotNull() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, processManager, gitPath, workDirPath); string actual = git.GetCurrentRepository(); @@ -43,9 +41,8 @@ public void Git_GetRemotes_NoLocalRepo_ReturnsEmpty() { string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath()); + var git = new GitProcess(trace, processManager, gitPath, Path.GetTempPath()); GitRemote[] remotes = git.GetRemotes().ToArray(); @@ -59,9 +56,8 @@ public void Git_GetRemotes_NoRemotes_ReturnsEmpty() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); @@ -78,10 +74,9 @@ public void Git_GetRemotes_OneRemote_ReturnsRemote() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Single(remotes); @@ -100,10 +95,9 @@ public void Git_GetRemotes_OneRemoteFetchAndPull_ReturnsRemote() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Single(remotes); @@ -124,10 +118,9 @@ public void Git_GetRemotes_NonHttpRemote_ReturnsRemote(string url) string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Single(remotes); @@ -150,10 +143,9 @@ public void Git_GetRemotes_MultipleRemotes_ReturnsAllRemotes() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Equal(3, remotes.Length); @@ -175,10 +167,9 @@ public void Git_GetRemotes_RemoteNoFetchOnlyPull_ReturnsRemote() string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, workDirPath); + var git = new GitProcess(trace, processManager, gitPath, workDirPath); GitRemote[] remotes = git.GetRemotes().ToArray(); Assert.Single(remotes); @@ -191,10 +182,9 @@ public void Git_Version_ReturnsVersion() { string gitPath = GetGitPath(); var trace = new NullTrace(); - var trace2 = new NullTrace2(); var processManager = new TestProcessManager(); - var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath()); + var git = new GitProcess(trace, processManager, gitPath, Path.GetTempPath()); GitVersion version = git.Version; Assert.NotEqual(new GitVersion(), version); diff --git a/src/Core.Tests/HttpClientFactoryTests.cs b/src/Core.Tests/HttpClientFactoryTests.cs index c499e00548..e11a5b2bb1 100644 --- a/src/Core.Tests/HttpClientFactoryTests.cs +++ b/src/Core.Tests/HttpClientFactoryTests.cs @@ -16,19 +16,19 @@ public class HttpClientFactoryTests [Fact] public void HttpClientFactory_GetClient_SetsDefaultHeaders() { - var factory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), TestConsole); + var factory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), TestConsole); HttpClient client = factory.CreateClient(); Assert.NotNull(client); - Assert.Equal(Constants.GetHttpUserAgent(Mock.Of()), client.DefaultRequestHeaders.UserAgent.ToString()); + Assert.Equal(Constants.GetHttpUserAgent(), client.DefaultRequestHeaders.UserAgent.ToString()); Assert.True(client.DefaultRequestHeaders.CacheControl.NoCache); } [Fact] public void HttpClientFactory_GetClient_MultipleCalls_ReturnsNewInstance() { - var factory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), TestConsole); + var factory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), TestConsole); HttpClient client1 = factory.CreateClient(); HttpClient client2 = factory.CreateClient(); @@ -48,7 +48,7 @@ public void HttpClientFactory_TryCreateProxy_NoProxy_ReturnsFalseOutNull() RemoteUri = repoRemoteUri, RepositoryPath = repoPath }; - var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, TestConsole); + var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, TestConsole); bool result = httpFactory.TryCreateProxy(out IWebProxy proxy); @@ -72,7 +72,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyNoCredentials_ReturnsTrueOutPr RepositoryPath = repoPath, ProxyConfiguration = proxyConfig }; - var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, TestConsole); + var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, TestConsole); bool result = httpFactory.TryCreateProxy(out IWebProxy proxy); @@ -105,7 +105,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithBypass_ReturnsTrueOutProxy RepositoryPath = repoPath, ProxyConfiguration = proxyConfig }; - var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, TestConsole); + var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, TestConsole); bool result = httpFactory.TryCreateProxy(out IWebProxy proxy); @@ -141,7 +141,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithWildcardBypass_ReturnsFals RepositoryPath = repoPath, ProxyConfiguration = proxyConfig }; - var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, TestConsole); + var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, TestConsole); bool result = httpFactory.TryCreateProxy(out IWebProxy proxy); @@ -170,7 +170,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithCredentials_ReturnsTrueOut RepositoryPath = repoPath, ProxyConfiguration = proxyConfig }; - var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, TestConsole); + var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, TestConsole); bool result = httpFactory.TryCreateProxy(out IWebProxy proxy); @@ -206,7 +206,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithNonEmptyUserAndEmptyPass_R RepositoryPath = repoPath, ProxyConfiguration = proxyConfig }; - var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, TestConsole); + var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, TestConsole); bool result = httpFactory.TryCreateProxy(out IWebProxy proxy); @@ -242,7 +242,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithEmptyUserAndNonEmptyPass_R RepositoryPath = repoPath, ProxyConfiguration = proxyConfig }; - var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, TestConsole); + var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, TestConsole); bool result = httpFactory.TryCreateProxy(out IWebProxy proxy); @@ -277,7 +277,7 @@ public void HttpClientFactory_TryCreateProxy_ProxyEmptyUserAndEmptyPass_ReturnsT RepositoryPath = repoPath, ProxyConfiguration = proxyConfig }; - var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), Mock.Of(), settings, TestConsole); + var httpFactory = new HttpClientFactory(Mock.Of(), Mock.Of(), settings, TestConsole); bool result = httpFactory.TryCreateProxy(out IWebProxy proxy); @@ -307,7 +307,7 @@ public void HttpClientFactory_GetClient_ChecksCertBundleOnlyIfEnabled(string cus UseCustomCertificateBundleWithSchannel = useCustomCertBundleWithSchannel }; - var factory = new HttpClientFactory(fileSystemMock.Object, Mock.Of(), Mock.Of(), settings, TestConsole); + var factory = new HttpClientFactory(fileSystemMock.Object, Mock.Of(), settings, TestConsole); HttpClient client = factory.CreateClient(); @@ -337,7 +337,7 @@ public void HttpClientFactory_GetClient_SetCookieOnlyIfEnabled(string cookieFile CustomCookieFilePath = cookieFilePath }; - var factory = new HttpClientFactory(fileSystemMock.Object, Mock.Of(), Mock.Of(), settings, TestConsole); + var factory = new HttpClientFactory(fileSystemMock.Object, Mock.Of(), settings, TestConsole); HttpClient client = factory.CreateClient(); diff --git a/src/Core.Tests/TestProcessManager.cs b/src/Core.Tests/TestProcessManager.cs index df54b1bb48..8f34dafd9d 100644 --- a/src/Core.Tests/TestProcessManager.cs +++ b/src/Core.Tests/TestProcessManager.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using GitCredentialManager.Tests.Objects; -using Moq; namespace GitCredentialManager.Tests; @@ -24,6 +20,6 @@ public ChildProcess CreateProcess(string path, string args, bool useShellExecute public ChildProcess CreateProcess(ProcessStartInfo psi) { - return new ChildProcess(new NullTrace2(), psi); + return new ChildProcess(psi); } } diff --git a/src/Core.Tests/WslUtilsTests.cs b/src/Core.Tests/WslUtilsTests.cs index 330e42c846..d0daa44716 100644 --- a/src/Core.Tests/WslUtilsTests.cs +++ b/src/Core.Tests/WslUtilsTests.cs @@ -101,7 +101,7 @@ public void WslUtils_CreateWslProcess() string expectedFileName = WslUtils.GetWslPath(); string expectedArgs = $"--distribution {distribution} --exec {command}"; - ChildProcess process = WslUtils.CreateWslProcess(distribution, command, Mock.Of()); + ChildProcess process = WslUtils.CreateWslProcess(distribution, command); Assert.NotNull(process); Assert.Equal(expectedArgs, process.StartInfo.Arguments); @@ -122,7 +122,7 @@ public void WslUtils_CreateWslProcess_WorkingDirectory() string expectedFileName = WslUtils.GetWslPath(); string expectedArgs = $"--distribution {distribution} --exec {command}"; - ChildProcess process = WslUtils.CreateWslProcess(distribution, command, Mock.Of(), expectedWorkingDirectory); + ChildProcess process = WslUtils.CreateWslProcess(distribution, command, expectedWorkingDirectory); Assert.NotNull(process); Assert.Equal(expectedArgs, process.StartInfo.Arguments); diff --git a/src/Core/ChildProcess.cs b/src/Core/ChildProcess.cs index 9e86cc53ff..d5468e028c 100644 --- a/src/Core/ChildProcess.cs +++ b/src/Core/ChildProcess.cs @@ -1,14 +1,11 @@ using System; using System.Diagnostics; using System.IO; -using System.Threading.Tasks; namespace GitCredentialManager; public class ChildProcess : DisposableObject { - private readonly ITrace2 _trace2; - private DateTimeOffset _startTime; private DateTimeOffset _exitTime => Process.ExitTime; private ProcessStartInfo _startInfo => Process.StartInfo; @@ -22,32 +19,26 @@ public class ChildProcess : DisposableObject public StreamReader StandardError => Process.StandardError; public int ExitCode => Process.ExitCode; - public static ChildProcess Start(ITrace2 trace2, ProcessStartInfo startInfo, Trace2ProcessClass processClass) + public static ChildProcess Start(ProcessStartInfo startInfo, Trace2ProcessClass @class = Trace2ProcessClass.None) { - var childProc = new ChildProcess(trace2, startInfo); - childProc.Start(processClass); + var childProc = new ChildProcess(startInfo); + childProc.Start(@class); return childProc; } - public ChildProcess(ITrace2 trace2, ProcessStartInfo startInfo) + public ChildProcess(ProcessStartInfo startInfo) { - _trace2 = trace2; Process = new Process() { StartInfo = startInfo }; Process.Exited += ProcessOnExited; } - public bool Start(Trace2ProcessClass processClass) + public bool Start(Trace2ProcessClass @class = Trace2ProcessClass.None) { ThrowIfDisposed(); - // Record the time just before the process starts, since: - // (1) There is no event related to Start as there is with Exit. - // (2) Using Process.StartTime causes a race condition that leads - // to an exception if the process finishes executing before the - // variable is passed to Trace2. _startTime = DateTimeOffset.UtcNow; - _trace2.WriteChildStart( + Trace2.WriteChildStart( _startTime, - processClass, + @class, _startInfo.UseShellExecute, _startInfo.FileName, _startInfo.Arguments); @@ -70,7 +61,7 @@ private void ProcessOnExited(object sender, EventArgs e) if (sender is Process) { double elapsedTime = (_exitTime - _startTime).TotalSeconds; - _trace2.WriteChildExit( + Trace2.WriteChildExit( elapsedTime, _id, Process.ExitCode); diff --git a/src/Core/CommandContext.cs b/src/Core/CommandContext.cs index 425dae025b..5ba3b156f0 100644 --- a/src/Core/CommandContext.cs +++ b/src/Core/CommandContext.cs @@ -101,11 +101,10 @@ public CommandContext() FileSystem = new WindowsFileSystem(); Environment = new WindowsEnvironment(FileSystem); SessionManager = new WindowsSessionManager(Trace, Environment, FileSystem); - ProcessManager = new WindowsProcessManager(Trace2); + ProcessManager = new WindowsProcessManager(); string gitPath = GetGitPath(Environment, FileSystem, Trace); Git = new GitProcess( Trace, - Trace2, ProcessManager, gitPath, FileSystem.GetCurrentDirectory() @@ -117,11 +116,10 @@ public CommandContext() FileSystem = new MacOSFileSystem(); Environment = new MacOSEnvironment(FileSystem); SessionManager = new MacOSSessionManager(Trace, Environment, FileSystem); - ProcessManager = new ProcessManager(Trace2); + ProcessManager = new ProcessManager(); string gitPath = GetGitPath(Environment, FileSystem, Trace); Git = new GitProcess( Trace, - Trace2, ProcessManager, gitPath, FileSystem.GetCurrentDirectory() @@ -133,11 +131,10 @@ public CommandContext() FileSystem = new LinuxFileSystem(); Environment = new PosixEnvironment(FileSystem); SessionManager = new LinuxSessionManager(Trace, Environment, FileSystem); - ProcessManager = new ProcessManager(Trace2); + ProcessManager = new ProcessManager(); string gitPath = GetGitPath(Environment, FileSystem, Trace); Git = new GitProcess( Trace, - Trace2, ProcessManager, gitPath, FileSystem.GetCurrentDirectory() @@ -149,7 +146,7 @@ public CommandContext() throw new PlatformNotSupportedException(); } - HttpClientFactory = new HttpClientFactory(FileSystem, Trace, Trace2, Settings, Console); + HttpClientFactory = new HttpClientFactory(FileSystem, Trace, Settings, Console); CredentialStore = new CredentialStore(this); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 667ff8b0ba..cff7a1a1bd 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -314,9 +314,9 @@ public static Version GcmVersion /// Get the HTTP user-agent for Git Credential Manager. /// /// User-agent string for HTTP requests. - public static string GetHttpUserAgent(ITrace2 trace2) + public static string GetHttpUserAgent() { - PlatformInformation info = PlatformUtils.GetPlatformInformation(trace2); + PlatformInformation info = PlatformUtils.GetPlatformInformation(); string osType = info.OperatingSystemType; string cpuArch = info.CpuArchitecture; string clrVersion = info.ClrVersion; diff --git a/src/Core/Diagnostics/EnvironmentDiagnostic.cs b/src/Core/Diagnostics/EnvironmentDiagnostic.cs index dbec71f02b..d0d63c386c 100644 --- a/src/Core/Diagnostics/EnvironmentDiagnostic.cs +++ b/src/Core/Diagnostics/EnvironmentDiagnostic.cs @@ -15,7 +15,7 @@ public EnvironmentDiagnostic(ICommandContext commandContext) protected override Task RunInternalAsync(StringBuilder log, IList additionalFiles) { - PlatformInformation platformInfo = PlatformUtils.GetPlatformInformation(CommandContext.Trace2); + PlatformInformation platformInfo = PlatformUtils.GetPlatformInformation(); log.AppendLine($"OSType: {platformInfo.OperatingSystemType}"); log.AppendLine($"OSVersion: {platformInfo.OperatingSystemVersion}"); diff --git a/src/Core/Diagnostics/GitDiagnostic.cs b/src/Core/Diagnostics/GitDiagnostic.cs index 74c4c76b34..f97ac60790 100644 --- a/src/Core/Diagnostics/GitDiagnostic.cs +++ b/src/Core/Diagnostics/GitDiagnostic.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics; using System.Text; using System.Threading.Tasks; @@ -32,7 +31,7 @@ protected override Task RunInternalAsync(StringBuilder log, IList log.Append("Listing all Git configuration..."); ChildProcess configProc = CommandContext.Git.CreateProcess("config --list --show-origin"); - configProc.Start(Trace2ProcessClass.Git); + configProc.Start(); // To avoid deadlocks, always read the output stream first and then wait // TODO: don't read in all the data at once; stream it string gitConfig = configProc.StandardOutput.ReadToEnd().TrimEnd(); diff --git a/src/Core/Git.cs b/src/Core/Git.cs index 82588357cd..bec8755b89 100644 --- a/src/Core/Git.cs +++ b/src/Core/Git.cs @@ -71,20 +71,17 @@ public GitRemote(string name, string fetchUrl, string pushUrl) public class GitProcess : IGit { private readonly ITrace _trace; - private readonly ITrace2 _trace2; private readonly IProcessManager _processManager; private readonly string _gitPath; private readonly string _workingDirectory; - public GitProcess(ITrace trace, ITrace2 trace2, IProcessManager processManager, string gitPath, string workingDirectory = null) + public GitProcess(ITrace trace, IProcessManager processManager, string gitPath, string workingDirectory = null) { EnsureArgument.NotNull(trace, nameof(trace)); - EnsureArgument.NotNull(trace2, nameof(trace2)); EnsureArgument.NotNull(processManager, nameof(processManager)); EnsureArgument.NotNullOrWhiteSpace(gitPath, nameof(gitPath)); _trace = trace; - _trace2 = trace2; _processManager = processManager; _gitPath = gitPath; _workingDirectory = workingDirectory; @@ -97,23 +94,18 @@ public GitVersion Version { if (_version is null) { + string data; using (var git = CreateProcess("version")) { - git.Start(Trace2ProcessClass.Git); + git.Start(); - string data = git.StandardOutput.ReadToEnd(); + data = git.StandardOutput.ReadToEnd(); git.WaitForExit(); - - Match match = Regex.Match(data, @"^git version (?'value'.*)"); - if (match.Success) - { - _version = new GitVersion(match.Groups["value"].Value); - } - else - { - _version = new GitVersion(); - } } + Match match = Regex.Match(data, @"^git version (?'value'.*)"); + _version = match.Success + ? new GitVersion(match.Groups["value"].Value) + : new GitVersion(); } return _version; @@ -145,7 +137,7 @@ private string GetCurrentRepositoryInternal(bool suppressStreams) git.StartInfo.RedirectStandardError = true; } - git.Start(Trace2ProcessClass.Git); + git.Start(); // Drain and throw away stderr asynchronously to avoid a deadlock // if the child process fills the stderr pipe buffer. @@ -178,7 +170,7 @@ public IEnumerable GetRemotes() { // Redirect stderr so we can check for 'not a git repository' errors git.StartInfo.RedirectStandardError = true; - git.Start(Trace2ProcessClass.Git); + git.Start(); // To avoid deadlocks, always read the output stream first and then wait // TODO: don't read in all the data at once; stream it string data = git.StandardOutput.ReadToEnd(); @@ -243,7 +235,7 @@ public async Task> InvokeHelperAsync(string args, ID }; var process = _processManager.CreateProcess(procStartInfo); - if (!process.Start(Trace2ProcessClass.Git)) + if (!process.Start()) { var format = "Failed to start Git helper '{0}'"; var message = string.Format(format, args); diff --git a/src/Core/GitConfiguration.cs b/src/Core/GitConfiguration.cs index 83a10d5918..d7af4a307e 100644 --- a/src/Core/GitConfiguration.cs +++ b/src/Core/GitConfiguration.cs @@ -332,7 +332,8 @@ public class GitProcessConfiguration : IGitConfiguration private readonly Dictionary _cache; private readonly bool _useCache; - internal GitProcessConfiguration(ITrace trace, GitProcess git) : this(trace, git, useCache: true) + internal GitProcessConfiguration(ITrace trace, GitProcess git) + : this(trace, git, useCache: true) { } @@ -417,7 +418,7 @@ private void EnsureCacheLoaded(GitConfigurationType type) using (ChildProcess git = _git.CreateProcess($"config list --show-scope -z {typeArg}")) { - git.Start(Trace2ProcessClass.Git); + git.Start(); // To avoid deadlocks, always read the output stream first and then wait string data = git.StandardOutput.ReadToEnd(); git.WaitForExit(); @@ -465,7 +466,7 @@ public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCa string levelArg = GetLevelFilterArg(level); using (ChildProcess git = _git.CreateProcess($"config --null {levelArg} --list")) { - git.Start(Trace2ProcessClass.Git); + git.Start(); // To avoid deadlocks, always read the output stream first and then wait // TODO: don't read in all the data at once; stream it string data = git.StandardOutput.ReadToEnd(); @@ -547,7 +548,7 @@ public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, strin string typeArg = GetCanonicalizeTypeArg(type); using (ChildProcess git = _git.CreateProcess($"config --null {levelArg} {typeArg} {QuoteCmdArg(name)}")) { - git.Start(Trace2ProcessClass.Git); + git.Start(); // To avoid deadlocks, always read the output stream first and then wait // TODO: don't read in all the data at once; stream it string data = git.StandardOutput.ReadToEnd(); @@ -585,7 +586,7 @@ public void Set(GitConfigurationLevel level, string name, string value) string levelArg = GetLevelFilterArg(level); using (ChildProcess git = _git.CreateProcess($"config {levelArg} {QuoteCmdArg(name)} {QuoteCmdArg(value)}")) { - git.Start(Trace2ProcessClass.Git); + git.Start(); git.WaitForExit(); switch (git.ExitCode) @@ -607,7 +608,7 @@ public void Add(GitConfigurationLevel level, string name, string value) string levelArg = GetLevelFilterArg(level); using (ChildProcess git = _git.CreateProcess($"config {levelArg} --add {QuoteCmdArg(name)} {QuoteCmdArg(value)}")) { - git.Start(Trace2ProcessClass.Git); + git.Start(); git.WaitForExit(); switch (git.ExitCode) @@ -629,7 +630,7 @@ public void Unset(GitConfigurationLevel level, string name) string levelArg = GetLevelFilterArg(level); using (ChildProcess git = _git.CreateProcess($"config {levelArg} --unset {QuoteCmdArg(name)}")) { - git.Start(Trace2ProcessClass.Git); + git.Start(); git.WaitForExit(); switch (git.ExitCode) @@ -671,7 +672,7 @@ public IEnumerable GetAll(GitConfigurationLevel level, GitConfigurationT using (ChildProcess git = _git.CreateProcess(gitArgs)) { - git.Start(Trace2ProcessClass.Git); + git.Start(); // To avoid deadlocks, always read the output stream first and then wait // TODO: don't read in all the data at once; stream it string data = git.StandardOutput.ReadToEnd(); @@ -713,7 +714,7 @@ public IEnumerable GetRegex(GitConfigurationLevel level, GitConfiguratio using (ChildProcess git = _git.CreateProcess(gitArgs)) { - git.Start(Trace2ProcessClass.Git); + git.Start(); // To avoid deadlocks, always read the output stream first and then wait // TODO: don't read in all the data at once; stream it string data = git.StandardOutput.ReadToEnd(); @@ -755,7 +756,7 @@ public void ReplaceAll(GitConfigurationLevel level, string name, string valueReg using (ChildProcess git = _git.CreateProcess(gitArgs)) { - git.Start(Trace2ProcessClass.Git); + git.Start(); git.WaitForExit(); switch (git.ExitCode) @@ -783,7 +784,7 @@ public void UnsetAll(GitConfigurationLevel level, string name, string valueRegex using (ChildProcess git = _git.CreateProcess(gitArgs)) { - git.Start(Trace2ProcessClass.Git); + git.Start(); git.WaitForExit(); switch (git.ExitCode) diff --git a/src/Core/Gpg.cs b/src/Core/Gpg.cs index 686cf0db98..77e1fc6610 100644 --- a/src/Core/Gpg.cs +++ b/src/Core/Gpg.cs @@ -15,18 +15,15 @@ public class Gpg : IGpg private readonly string _gpgPath; private readonly ISessionManager _sessionManager; private readonly IProcessManager _processManager; - private readonly ITrace2 _trace2; - public Gpg(string gpgPath, ISessionManager sessionManager, IProcessManager processManager, ITrace2 trace2) + public Gpg(string gpgPath, ISessionManager sessionManager, IProcessManager processManager) { EnsureArgument.NotNullOrWhiteSpace(gpgPath, nameof(gpgPath)); EnsureArgument.NotNull(sessionManager, nameof(sessionManager)); - EnsureArgument.NotNull(trace2, nameof(trace2)); _gpgPath = gpgPath; _sessionManager = sessionManager; _processManager = processManager; - _trace2 = trace2; } public string DecryptFile(string path) diff --git a/src/Core/HttpClientFactory.cs b/src/Core/HttpClientFactory.cs index e9b4cfdd09..0ec3a69f56 100644 --- a/src/Core/HttpClientFactory.cs +++ b/src/Core/HttpClientFactory.cs @@ -38,11 +38,10 @@ public class HttpClientFactory : IHttpClientFactory { private readonly IFileSystem _fileSystem; private readonly ITrace _trace; - private readonly ITrace2 _trace2; private readonly ISettings _settings; private readonly IConsoleService _console; - public HttpClientFactory(IFileSystem fileSystem, ITrace trace, ITrace2 trace2, ISettings settings, IConsoleService console) + public HttpClientFactory(IFileSystem fileSystem, ITrace trace, ISettings settings, IConsoleService console) { EnsureArgument.NotNull(fileSystem, nameof(fileSystem)); EnsureArgument.NotNull(trace, nameof(trace)); @@ -51,7 +50,6 @@ public HttpClientFactory(IFileSystem fileSystem, ITrace trace, ITrace2 trace2, I _fileSystem = fileSystem; _trace = trace; - _trace2 = trace2; _settings = settings; _console = console; } @@ -209,7 +207,7 @@ public HttpClient CreateClient() var client = new HttpClient(handler); // Add default headers - client.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.GetHttpUserAgent(_trace2)); + client.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.GetHttpUserAgent()); client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { NoCache = true @@ -295,7 +293,7 @@ public bool TryCreateProxy(out IWebProxy proxy) "Failed to convert proxy bypass hosts to regular expressions; ignoring bypass list"; _trace.WriteLine(message); _trace.WriteException(ex); - _trace2.WriteError(message); + Trace2.WriteError(message); dict["bypass"] = "<< failed to convert >>"; } } diff --git a/src/Core/Interop/Windows/WindowsProcessManager.cs b/src/Core/Interop/Windows/WindowsProcessManager.cs index 0192274110..340943d4d6 100644 --- a/src/Core/Interop/Windows/WindowsProcessManager.cs +++ b/src/Core/Interop/Windows/WindowsProcessManager.cs @@ -5,7 +5,7 @@ namespace GitCredentialManager.Interop.Windows; [SupportedOSPlatform("windows")] public class WindowsProcessManager : ProcessManager { - public WindowsProcessManager(ITrace2 trace2) : base(trace2) + public WindowsProcessManager() { PlatformUtils.EnsureWindows(); } @@ -16,7 +16,7 @@ public override ChildProcess CreateProcess(string path, string args, bool useShe if (!useShellExecute && WslUtils.IsWslPath(path)) { string wslPath = WslUtils.ConvertToDistroPath(path, out string distro); - return WslUtils.CreateWslProcess(distro, $"{wslPath} {args}", Trace2, workingDirectory); + return WslUtils.CreateWslProcess(distro, $"{wslPath} {args}", workingDirectory); } return base.CreateProcess(path, args, useShellExecute, workingDirectory); diff --git a/src/Core/PlatformUtils.cs b/src/Core/PlatformUtils.cs index 4081b15f8c..6def0c8ab3 100644 --- a/src/Core/PlatformUtils.cs +++ b/src/Core/PlatformUtils.cs @@ -14,10 +14,10 @@ public static class PlatformUtils /// Get information about the current platform (OS and CLR details). /// /// Platform information. - public static PlatformInformation GetPlatformInformation(ITrace2 trace2) + public static PlatformInformation GetPlatformInformation() { string osType = GetOSType(); - string osVersion = GetOSVersion(trace2); + string osVersion = GetOSVersion(); string cpuArch = GetCpuArchitecture(); string clrVersion = RuntimeInformation.FrameworkDescription; @@ -353,7 +353,7 @@ private static string GetOSType() private static string _linuxDistroVersion; - private static string GetOSVersion(ITrace2 trace2) + private static string GetOSVersion() { // // Since .NET 5 we can use Environment.OSVersion because it was updated to @@ -429,7 +429,7 @@ string GetLinuxDistroVersion() RedirectStandardOutput = true }; - using (var uname = new ChildProcess(trace2, psi)) + using (var uname = new ChildProcess(psi)) { uname.Start(Trace2ProcessClass.Other); uname.Process.WaitForExit(); diff --git a/src/Core/ProcessManager.cs b/src/Core/ProcessManager.cs index b9db5fa625..a645ba8960 100644 --- a/src/Core/ProcessManager.cs +++ b/src/Core/ProcessManager.cs @@ -27,15 +27,6 @@ public interface IProcessManager public class ProcessManager : IProcessManager { - protected readonly ITrace2 Trace2; - - public ProcessManager(ITrace2 trace2) - { - EnsureArgument.NotNull(trace2, nameof(trace2)); - - Trace2 = trace2; - } - public virtual ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory) { var psi = new ProcessStartInfo(path, args) @@ -52,6 +43,6 @@ public virtual ChildProcess CreateProcess(string path, string args, bool useShel public virtual ChildProcess CreateProcess(ProcessStartInfo psi) { - return new ChildProcess(Trace2, psi); + return new ChildProcess(psi); } } diff --git a/src/Core/WslUtils.cs b/src/Core/WslUtils.cs index 977677d6e5..98bb596227 100644 --- a/src/Core/WslUtils.cs +++ b/src/Core/WslUtils.cs @@ -109,12 +109,10 @@ public static bool IsWslPath(string path) /// /// WSL distribution name. /// Command to execute. - /// The applications TRACE2 tracer. /// Optional working directory. /// object ready to start. public static ChildProcess CreateWslProcess(string distribution, string command, - ITrace2 trace2, string workingDirectory = null) { var args = new StringBuilder(); @@ -132,7 +130,7 @@ public static ChildProcess CreateWslProcess(string distribution, WorkingDirectory = workingDirectory ?? string.Empty }; - return new ChildProcess(trace2, psi); + return new ChildProcess(psi); } /// diff --git a/src/TestInfrastructure/GitTestUtilities.cs b/src/TestInfrastructure/GitTestUtilities.cs index e99a00e009..011cbac26c 100644 --- a/src/TestInfrastructure/GitTestUtilities.cs +++ b/src/TestInfrastructure/GitTestUtilities.cs @@ -27,9 +27,9 @@ public static string GetGitPath() psi.RedirectStandardOutput = true; - using (var which = new ChildProcess(new NullTrace2(), psi)) + using (var which = new ChildProcess(psi)) { - which.Start(Trace2ProcessClass.None); + which.Start(); which.WaitForExit(); if (which.ExitCode != 0) @@ -80,7 +80,7 @@ public static GitResult ExecGit(string repositoryPath, string workingDirectory, procInfo.Environment["GIT_DIR"] = repositoryPath; - var proc = ChildProcess.Start(new NullTrace2(), procInfo, Trace2ProcessClass.None); + var proc = ChildProcess.Start(procInfo, Trace2ProcessClass.None); if (proc is null) { throw new Exception("Failed to start Git process"); From 87e1a58e698f94e1f0d96964853fd18c075a259b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 11:27:47 +0100 Subject: [PATCH 07/18] trace2: decouple OAuth clients OAuth clients only forward the tracer so every provider can emit into the same process stream. That parameter creates a constructor cascade without representing per-client state. Remove it at the OAuth boundary so provider wiring follows the real ownership model, while reserving leaf exception changes for the final cleanup. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- .../Cloud/BitbucketOAuth2ClientTest.cs | 9 ++--- .../DataCenter/BitbucketOAuth2ClientTest.cs | 3 +- .../BitbucketOAuth2Client.cs | 3 +- .../Cloud/BitbucketOAuth2Client.cs | 4 +- .../DataCenter/BitbucketOAuth2Client.cs | 4 +- .../OAuth2ClientRegistry.cs | 4 +- .../Authentication/OAuth2ClientTests.cs | 39 +++++++------------ src/Core/Authentication/OAuth/OAuth2Client.cs | 11 ++---- src/Core/GenericHostProvider.cs | 5 +-- src/GitHub/GitHubAuthentication.cs | 4 +- src/GitHub/GitHubOAuth2Client.cs | 4 +- src/GitLab/GitLabAuthentication.cs | 4 +- src/GitLab/GitLabOAuth2Client.cs | 4 +- 13 files changed, 38 insertions(+), 60 deletions(-) diff --git a/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs b/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs index 0e889ec302..c700c59202 100644 --- a/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs +++ b/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs @@ -68,8 +68,7 @@ public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_RespectsClient [Fact] public async Task BitbucketOAuth2Client_GetDeviceCodeAsync() { - var trace2 = new NullTrace2(); - var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2); + var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object); await Assert.ThrowsAsync(async () => await client.GetDeviceCodeAsync(scopes, ct)); } @@ -80,8 +79,7 @@ public async Task BitbucketOAuth2Client_GetDeviceCodeAsync() [InlineData("https", "example.com/", "john", "https://example.com/refresh_token")] public void BitbucketOAuth2Client_GetRefreshTokenServiceName(string protocol, string host, string username, string expectedResult) { - var trace2 = new NullTrace2(); - var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2); + var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object); var request = new GitRequest(new Dictionary { ["protocol"] = protocol, @@ -102,8 +100,7 @@ private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result) private Bitbucket.Cloud.BitbucketOAuth2Client GetBitbucketOAuth2Client() { - var trace2 = new NullTrace2(); - var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2); + var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object); client.CodeGenerator = codeGenerator.Object; return client; } diff --git a/src/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs b/src/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs index 5931a6a0c7..ec11ac0d42 100644 --- a/src/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs +++ b/src/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs @@ -77,8 +77,7 @@ private void VerifyAuthorizationCodeResult(OAuth2AuthorizationCodeResult result, private Bitbucket.DataCenter.BitbucketOAuth2Client GetBitbucketOAuth2Client() { - var trace2 = new NullTrace2(); - var client = new Bitbucket.DataCenter.BitbucketOAuth2Client(httpClient.Object, settings.Object, trace2); + var client = new Bitbucket.DataCenter.BitbucketOAuth2Client(httpClient.Object, settings.Object); client.CodeGenerator = codeGenerator.Object; return client; } diff --git a/src/Atlassian.Bitbucket/BitbucketOAuth2Client.cs b/src/Atlassian.Bitbucket/BitbucketOAuth2Client.cs index a7f50876b6..c4daec9e73 100644 --- a/src/Atlassian.Bitbucket/BitbucketOAuth2Client.cs +++ b/src/Atlassian.Bitbucket/BitbucketOAuth2Client.cs @@ -14,8 +14,7 @@ public BitbucketOAuth2Client(HttpClient httpClient, OAuth2ServerEndpoints endpoints, string clientId, Uri redirectUri, - string clientSecret, - ITrace2 trace2) : base(httpClient, endpoints, clientId, trace2, redirectUri, clientSecret, false) + string clientSecret) : base(httpClient, endpoints, clientId, redirectUri, clientSecret, false) { } diff --git a/src/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs b/src/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs index 4b5edbbf74..92592dc0ff 100644 --- a/src/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs +++ b/src/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs @@ -10,9 +10,9 @@ namespace Atlassian.Bitbucket.Cloud { public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client { - public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2 trace2) + public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings) : base(httpClient, GetEndpoints(), - GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace2) + GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings)) { } diff --git a/src/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs b/src/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs index 97abd533cb..5af985cdea 100644 --- a/src/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs +++ b/src/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs @@ -12,9 +12,9 @@ namespace Atlassian.Bitbucket.DataCenter { public class BitbucketOAuth2Client : Bitbucket.BitbucketOAuth2Client { - public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings, ITrace2 trace2) + public BitbucketOAuth2Client(HttpClient httpClient, ISettings settings) : base(httpClient, GetEndpoints(settings), - GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings), trace2) + GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings)) { } diff --git a/src/Atlassian.Bitbucket/OAuth2ClientRegistry.cs b/src/Atlassian.Bitbucket/OAuth2ClientRegistry.cs index cb7f1f9c4c..7364d1e9a4 100644 --- a/src/Atlassian.Bitbucket/OAuth2ClientRegistry.cs +++ b/src/Atlassian.Bitbucket/OAuth2ClientRegistry.cs @@ -38,9 +38,9 @@ protected override void ReleaseManagedResources() private HttpClient HttpClient => _httpClient ??= _context.HttpClientFactory.CreateClient(); private Cloud.BitbucketOAuth2Client CloudClient => - _cloudClient ??= new Cloud.BitbucketOAuth2Client(HttpClient, _context.Settings, _context.Trace2); + _cloudClient ??= new Cloud.BitbucketOAuth2Client(HttpClient, _context.Settings); private DataCenter.BitbucketOAuth2Client DataCenterClient => - _dataCenterClient ??= new DataCenter.BitbucketOAuth2Client(HttpClient, _context.Settings, _context.Trace2); + _dataCenterClient ??= new DataCenter.BitbucketOAuth2Client(HttpClient, _context.Settings); } } diff --git a/src/Core.Tests/Authentication/OAuth2ClientTests.cs b/src/Core.Tests/Authentication/OAuth2ClientTests.cs index 1ec3eae251..2f6d8a13cc 100644 --- a/src/Core.Tests/Authentication/OAuth2ClientTests.cs +++ b/src/Core.Tests/Authentication/OAuth2ClientTests.cs @@ -36,8 +36,7 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync() IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - var trace2 = new NullTrace2(); - OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); + OAuth2Client client = CreateClient(httpHandler, endpoints); OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync(expectedScopes, browser, null, CancellationToken.None); @@ -81,12 +80,10 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync_RedirectUrlOriginalStri var redirectUri = new Uri(expectedRedirectUrl); - var trace2 = new NullTrace2(); OAuth2Client client = new OAuth2Client( new HttpClient(httpHandler), endpoints, TestClientId, - trace2, redirectUri, TestClientSecret); @@ -131,8 +128,7 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync_ExtraQueryParams() IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - var trace2 = new NullTrace2(); - OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); + OAuth2Client client = CreateClient(httpHandler, endpoints); OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync(expectedScopes, browser, extraParams, CancellationToken.None); @@ -167,8 +163,7 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync_ExtraQueryParams_Overri IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - var trace2 = new NullTrace2(); - OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); + OAuth2Client client = CreateClient(httpHandler, endpoints); await Assert.ThrowsAsync(() => client.GetAuthorizationCodeAsync(expectedScopes, browser, extraParams, CancellationToken.None)); @@ -207,9 +202,8 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync_NonDefaultResponseMode_ IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - var trace2 = new NullTrace2(); OAuth2Client client = new OAuth2Client( - new HttpClient(httpHandler), endpoints, TestClientId, trace2, + new HttpClient(httpHandler), endpoints, TestClientId, TestRedirectUri, TestClientSecret, responseMode: responseMode); OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync( @@ -246,9 +240,8 @@ public async Task OAuth2Client_GetAuthorizationCodeAsync_DefaultResponseMode_Omi IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - var trace2 = new NullTrace2(); OAuth2Client client = new OAuth2Client( - new HttpClient(httpHandler), endpoints, TestClientId, trace2, + new HttpClient(httpHandler), endpoints, TestClientId, TestRedirectUri, TestClientSecret, responseMode: OAuth2ResponseMode.Default); OAuth2AuthorizationCodeResult result = await client.GetAuthorizationCodeAsync( @@ -278,8 +271,7 @@ public async Task OAuth2Client_GetDeviceCodeAsync() server.TokenGenerator.UserCodes.Add(expectedUserCode); server.TokenGenerator.DeviceCodes.Add(expectedDeviceCode); - var trace2 = new NullTrace2(); - OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); + OAuth2Client client = CreateClient(httpHandler, endpoints); OAuth2DeviceCodeResult result = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None); @@ -310,8 +302,7 @@ public async Task OAuth2Client_GetTokenByAuthorizationCodeAsync() server.TokenGenerator.AccessTokens.Add(expectedAccessToken); server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken); - var trace2 = new NullTrace2(); - OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); + OAuth2Client client = CreateClient(httpHandler, endpoints); var authCodeResult = new OAuth2AuthorizationCodeResult(authCode, TestRedirectUri); OAuth2TokenResult result = await client.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None); @@ -348,8 +339,7 @@ public async Task OAuth2Client_GetTokenByRefreshTokenAsync() server.TokenGenerator.AccessTokens.Add(expectedAccessToken); server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken); - var trace2 = new NullTrace2(); - OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); + OAuth2Client client = CreateClient(httpHandler, endpoints); OAuth2TokenResult result = await client.GetTokenByRefreshTokenAsync(oldRefreshToken, CancellationToken.None); @@ -387,8 +377,7 @@ public async Task OAuth2Client_GetTokenByDeviceCodeAsync() server.TokenGenerator.AccessTokens.Add(expectedAccessToken); server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken); - var trace2 = new NullTrace2(); - OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); + OAuth2Client client = CreateClient(httpHandler, endpoints); var deviceCodeResult = new OAuth2DeviceCodeResult(expectedDeviceCode, expectedUserCode, null, null); @@ -433,8 +422,7 @@ public async Task OAuth2Client_E2E_InteractiveWebFlowAndRefresh() IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler); - var trace2 = new NullTrace2(); - OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); + OAuth2Client client = CreateClient(httpHandler, endpoints); OAuth2AuthorizationCodeResult authCodeResult = await client.GetAuthorizationCodeAsync( expectedScopes, browser, null, CancellationToken.None); @@ -483,8 +471,7 @@ public async Task OAuth2Client_E2E_DeviceFlowAndRefresh() server.TokenGenerator.AccessTokens.Add(expectedAccessToken1); server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken1); - var trace2 = new NullTrace2(); - OAuth2Client client = CreateClient(httpHandler, endpoints, trace2); + OAuth2Client client = CreateClient(httpHandler, endpoints); OAuth2DeviceCodeResult deviceResult = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None); @@ -517,9 +504,9 @@ public async Task OAuth2Client_E2E_DeviceFlowAndRefresh() RedirectUris = new[] {TestRedirectUri} }; - private static OAuth2Client CreateClient(HttpMessageHandler httpHandler, OAuth2ServerEndpoints endpoints, ITrace2 trace2, IOAuth2CodeGenerator generator = null) + private static OAuth2Client CreateClient(HttpMessageHandler httpHandler, OAuth2ServerEndpoints endpoints, IOAuth2CodeGenerator generator = null) { - return new OAuth2Client(new HttpClient(httpHandler), endpoints, TestClientId, trace2, TestRedirectUri, TestClientSecret) + return new OAuth2Client(new HttpClient(httpHandler), endpoints, TestClientId, TestRedirectUri, TestClientSecret) { CodeGenerator = generator }; diff --git a/src/Core/Authentication/OAuth/OAuth2Client.cs b/src/Core/Authentication/OAuth/OAuth2Client.cs index 657c995b57..fda82a5f04 100644 --- a/src/Core/Authentication/OAuth/OAuth2Client.cs +++ b/src/Core/Authentication/OAuth/OAuth2Client.cs @@ -71,7 +71,6 @@ public class OAuth2Client : IOAuth2Client private readonly OAuth2ServerEndpoints _endpoints; private readonly Uri _redirectUri; private readonly string _clientId; - private readonly ITrace2 _trace2; private readonly string _clientSecret; private readonly bool _addAuthHeader; private readonly OAuth2ResponseMode _responseMode; @@ -81,7 +80,6 @@ public class OAuth2Client : IOAuth2Client public OAuth2Client(HttpClient httpClient, OAuth2ServerEndpoints endpoints, string clientId, - ITrace2 trace2, Uri redirectUri = null, string clientSecret = null, bool addAuthHeader = true, @@ -90,7 +88,6 @@ public OAuth2Client(HttpClient httpClient, _httpClient = httpClient; _endpoints = endpoints; _clientId = clientId; - _trace2 = trace2; _redirectUri = redirectUri; _clientSecret = clientSecret; _addAuthHeader = addAuthHeader; @@ -200,7 +197,7 @@ public async Task GetAuthorizationCodeAsync(IEnum public async Task GetDeviceCodeAsync(IEnumerable scopes, CancellationToken ct) { var label = "get device code"; - using IDisposable region = _trace2.StartRegion(OAuth2Constants.Trace2Category, label); + using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, label); if (_endpoints.DeviceAuthorizationEndpoint is null) { @@ -238,7 +235,7 @@ public async Task GetDeviceCodeAsync(IEnumerable public async Task GetTokenByAuthorizationCodeAsync(OAuth2AuthorizationCodeResult authorizationCodeResult, CancellationToken ct) { var label = "get token by auth code"; - using IDisposable region = _trace2.StartRegion(OAuth2Constants.Trace2Category, label); + using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, label); var formData = new Dictionary { @@ -277,7 +274,7 @@ public async Task GetTokenByAuthorizationCodeAsync(OAuth2Auth public async Task GetTokenByRefreshTokenAsync(string refreshToken, CancellationToken ct) { var label = "get token by refresh token"; - using IDisposable region = _trace2.StartRegion(OAuth2Constants.Trace2Category, label); + using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, label); var formData = new Dictionary { @@ -413,7 +410,7 @@ protected Exception CreateExceptionFromResponse(string json) { if (TryCreateExceptionFromResponse(json, out OAuth2Exception exception)) { - _trace2.WriteError(exception.Message); + Trace2.WriteError(exception.Message); return exception; } diff --git a/src/Core/GenericHostProvider.cs b/src/Core/GenericHostProvider.cs index ab17405b69..9dae9c4bb8 100644 --- a/src/Core/GenericHostProvider.cs +++ b/src/Core/GenericHostProvider.cs @@ -166,7 +166,7 @@ public async Task GenerateCredentialAsync(GitRequest request) _context.Trace.WriteLine($"\tDefaultUserName = {oauthConfig.DefaultUserName}"); return new GitResponse( - await GetOAuthAccessToken(uri, request.UserName, oauthConfig, _context.Trace2) + await GetOAuthAccessToken(uri, request.UserName, oauthConfig) ); } // Try detecting WIA for this remote, if permitted @@ -264,7 +264,7 @@ private void EnableNtlmSupport(Uri uri) } } - private async Task GetOAuthAccessToken(Uri remoteUri, string userName, GenericOAuthConfig config, ITrace2 trace2) + private async Task GetOAuthAccessToken(Uri remoteUri, string userName, GenericOAuthConfig config) { // TODO: Determined user info from a webcall? ID token? Need OIDC support string oauthUser = userName ?? config.DefaultUserName; @@ -273,7 +273,6 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa HttpClient, config.Endpoints, config.ClientId, - trace2, config.RedirectUri, config.ClientSecret, config.UseAuthHeader, diff --git a/src/GitHub/GitHubAuthentication.cs b/src/GitHub/GitHubAuthentication.cs index f2fa8c0cd5..e392a2cf07 100644 --- a/src/GitHub/GitHubAuthentication.cs +++ b/src/GitHub/GitHubAuthentication.cs @@ -410,7 +410,7 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, { ThrowIfUserInteractionDisabled(); - var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2); + var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri); // Can we launch the user's default web browser? if (!Context.SessionManager.IsWebBrowserAvailable) @@ -449,7 +449,7 @@ public async Task GetOAuthTokenViaDeviceCodeAsync(Uri targetU { ThrowIfUserInteractionDisabled(); - var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2); + var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri); OAuth2DeviceCodeResult dcr = await oauthClient.GetDeviceCodeAsync(scopes, CancellationToken.None); // If we have a desktop session show the device code in a dialog diff --git a/src/GitHub/GitHubOAuth2Client.cs b/src/GitHub/GitHubOAuth2Client.cs index 2eb4aae88d..0f67cba5fe 100644 --- a/src/GitHub/GitHubOAuth2Client.cs +++ b/src/GitHub/GitHubOAuth2Client.cs @@ -7,9 +7,9 @@ namespace GitHub { public class GitHubOAuth2Client : OAuth2Client { - public GitHubOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri, ITrace2 trace2) + public GitHubOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri) : base(httpClient, CreateEndpoints(baseUri), - GetClientId(settings), trace2, GetRedirectUri(settings, baseUri), GetClientSecret(settings)) { } + GetClientId(settings), GetRedirectUri(settings, baseUri), GetClientSecret(settings)) { } private static OAuth2ServerEndpoints CreateEndpoints(Uri uri) { diff --git a/src/GitLab/GitLabAuthentication.cs b/src/GitLab/GitLabAuthentication.cs index 60afe680f8..7cbb2503b4 100644 --- a/src/GitLab/GitLabAuthentication.cs +++ b/src/GitLab/GitLabAuthentication.cs @@ -262,7 +262,7 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, { ThrowIfUserInteractionDisabled(); - var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2); + var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri); // We require a desktop session to launch the user's default web browser if (!Context.SessionManager.IsDesktopSession) @@ -285,7 +285,7 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, public async Task GetOAuthTokenViaRefresh(Uri targetUri, string refreshToken) { - var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri, Context.Trace2); + var oauthClient = new GitLabOAuth2Client(HttpClient, Context.Settings, targetUri); return await oauthClient.GetTokenByRefreshTokenAsync(refreshToken, CancellationToken.None); } diff --git a/src/GitLab/GitLabOAuth2Client.cs b/src/GitLab/GitLabOAuth2Client.cs index ba72f5b417..410ad84416 100644 --- a/src/GitLab/GitLabOAuth2Client.cs +++ b/src/GitLab/GitLabOAuth2Client.cs @@ -7,9 +7,9 @@ namespace GitLab { public class GitLabOAuth2Client : OAuth2Client { - public GitLabOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri, ITrace2 trace2) + public GitLabOAuth2Client(HttpClient httpClient, ISettings settings, Uri baseUri) : base(httpClient, CreateEndpoints(baseUri), - GetClientId(settings), trace2, GetRedirectUri(settings), GetClientSecret(settings)) + GetClientId(settings), GetRedirectUri(settings), GetClientSecret(settings)) { } private static OAuth2ServerEndpoints CreateEndpoints(Uri baseUri) From 1fcc854083b89832196261168c9ecd5425756027 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 11:28:16 +0100 Subject: [PATCH 08/18] trace2: remove remaining tracer plumbing After constructor ownership is gone, leaf exception wrappers and command call sites still refer to a tracer that cannot vary by caller. Route those final authentication, provider, storage, UI, and Win32 events through the process API so the migration becomes buildable again without introducing an adapter that would immediately disappear. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- .../BitbucketAuthentication.cs | 4 +-- .../BitbucketHostProvider.cs | 16 ++++----- .../UI/Commands/CredentialsCommand.cs | 2 +- src/Core/Application.cs | 2 +- src/Core/Authentication/AuthenticationBase.cs | 12 +++---- .../Authentication/BasicAuthentication.cs | 4 +-- .../Entra/EntraAuthentication.Caching.cs | 4 +-- .../Entra/EntraAuthentication.PublicClient.cs | 4 +-- src/Core/Authentication/OAuth/OAuth2Client.cs | 10 +++--- .../Authentication/OAuthAuthentication.cs | 8 ++--- src/Core/Commands/GitCommandBase.cs | 8 ++--- src/Core/Commands/StoreCommand.cs | 4 +-- src/Core/CredentialStore.cs | 24 ++++++------- src/Core/GenericHostProvider.cs | 4 +-- src/Core/Git.cs | 13 +++---- src/Core/Gpg.cs | 8 ++--- src/Core/HostProviderRegistry.cs | 4 +-- src/Core/HttpClientFactory.cs | 2 +- src/Core/Interop/Windows/Native/Win32Error.cs | 19 ++-------- src/Core/Tracing/Trace2Exception.cs | 36 +++++++++---------- src/Core/UI/Commands/CredentialsCommand.cs | 2 +- src/Core/UI/Commands/DefaultAccountCommand.cs | 2 +- src/Core/UI/Commands/DeviceCodeCommand.cs | 2 +- src/Core/UI/Commands/OAuthCommand.cs | 2 +- src/GitHub/GitHubAuthentication.cs | 16 ++++----- src/GitHub/GitHubHostProvider.cs | 6 ++-- src/GitHub/UI/Commands/CredentialsCommand.cs | 2 +- src/GitHub/UI/Commands/DeviceCommand.cs | 2 +- src/GitHub/UI/Commands/TwoFactorCommand.cs | 2 +- src/GitLab/GitLabAuthentication.cs | 12 +++---- src/GitLab/GitLabHostProvider.cs | 2 +- src/GitLab/UI/Commands/CredentialsCommand.cs | 2 +- .../AzureDevOpsRestApi.cs | 6 ++-- .../AzureReposHostProvider.cs | 2 +- 34 files changed, 115 insertions(+), 133 deletions(-) diff --git a/src/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/Atlassian.Bitbucket/BitbucketAuthentication.cs index b3fe139110..be3dd0e0d0 100644 --- a/src/Atlassian.Bitbucket/BitbucketAuthentication.cs +++ b/src/Atlassian.Bitbucket/BitbucketAuthentication.cs @@ -234,12 +234,12 @@ private async Task GetCredentialsViaHelperAsync( { if (!output.TryGetValue("username", out userName)) { - throw new Trace2Exception(Context.Trace2, "Missing username in response"); + throw new Trace2Exception("Missing username in response"); } if (!output.TryGetValue("password", out string password)) { - throw new Trace2Exception(Context.Trace2, "Missing password in response"); + throw new Trace2Exception("Missing password in response"); } return new CredentialsPromptResult( diff --git a/src/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/Atlassian.Bitbucket/BitbucketHostProvider.cs index 42ed214975..1f3faabe79 100644 --- a/src/Atlassian.Bitbucket/BitbucketHostProvider.cs +++ b/src/Atlassian.Bitbucket/BitbucketHostProvider.cs @@ -86,7 +86,7 @@ public async Task GetCredentialAsync(GitRequest request) StringComparer.OrdinalIgnoreCase.Equals(request.Protocol, "http") && BitbucketHelper.IsBitbucketOrg(request)) { - throw new Trace2Exception(_context.Trace2, + throw new Trace2Exception( "Unencrypted HTTP is not recommended for Bitbucket.org. " + "Ensure the repository remote URL is using HTTPS " + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); @@ -158,7 +158,7 @@ private async Task GetRefreshedCredentials(GitRequest request, Auth { var message = "User cancelled credential prompt"; _context.Trace.WriteLine(message); - throw new Trace2Exception(_context.Trace2, message); + throw new Trace2Exception(message); } switch (result.AuthenticationMode) @@ -191,7 +191,7 @@ private async Task GetRefreshedCredentials(GitRequest request, Auth var message = "Failed to refresh existing OAuth credential using refresh token"; _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); - _context.Trace2.WriteError(message); + Trace2.WriteError(message); // We failed to refresh the AT using the RT; log the refresh failure and fall through to restart // the OAuth authentication flow @@ -317,7 +317,7 @@ public async Task GetSupportedAuthenticationModesAsync(GitR _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); - _context.Trace2.WriteError(message, format); + Trace2.WriteError(message, format); _context.Console.WriteWarning(message); @@ -374,7 +374,7 @@ private async Task ResolveOAuthUserNameAsync(GitRequest request, string return result.Response.UserName; } - throw new Trace2Exception(_context.Trace2, + throw new Trace2Exception( $"Failed to resolve username. HTTP: {result.StatusCode}"); } @@ -386,7 +386,7 @@ private async Task ResolveBasicAuthUserNameAsync(GitRequest request, str return result.Response.UserName; } - throw new Trace2Exception(_context.Trace2, + throw new Trace2Exception( $"Failed to resolve username. HTTP: {result.StatusCode}"); } @@ -427,7 +427,7 @@ private async Task ValidateCredentialsWork(GitRequest request, ICredential var message = "Failed to validate existing credentials using OAuth"; _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); - _context.Trace2.WriteError(message); + Trace2.WriteError(message); } } @@ -444,7 +444,7 @@ private async Task ValidateCredentialsWork(GitRequest request, ICredential var message = "Failed to validate existing credentials using Basic Auth"; _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); - _context.Trace2.WriteError(message); + Trace2.WriteError(message); return false; } } diff --git a/src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs b/src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs index 0e4b58ceda..7f2186ff81 100644 --- a/src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs +++ b/src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs @@ -44,7 +44,7 @@ private async Task ExecuteAsync(Uri url, string userName, bool showOAuth, b if (!viewModel.WindowResult || viewModel.SelectedMode == AuthenticationModes.None) { - throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); + throw new Trace2Exception("User cancelled dialog."); } switch (viewModel.SelectedMode) diff --git a/src/Core/Application.cs b/src/Core/Application.cs index 7099cf4b0c..e1a9e19835 100644 --- a/src/Core/Application.cs +++ b/src/Core/Application.cs @@ -105,7 +105,7 @@ void NoGuiOptionHandler(InvocationContext context) } // Trace the current version, OS, runtime, and program arguments - PlatformInformation info = PlatformUtils.GetPlatformInformation(Context.Trace2); + PlatformInformation info = PlatformUtils.GetPlatformInformation(); Context.Trace.WriteLine($"Version: {Constants.GcmVersion}"); Context.Trace.WriteLine($"Runtime: {info.ClrVersion}"); Context.Trace.WriteLine($"Platform: {info.OperatingSystemType} ({info.CpuArchitecture})"); diff --git a/src/Core/Authentication/AuthenticationBase.cs b/src/Core/Authentication/AuthenticationBase.cs index 71f6ae6d28..bf8d55177e 100644 --- a/src/Core/Authentication/AuthenticationBase.cs +++ b/src/Core/Authentication/AuthenticationBase.cs @@ -46,13 +46,13 @@ protected internal virtual async Task> InvokeHelperA // authentication helper's messages. Context.Trace.Flush(); - var process = ChildProcess.Start(Context.Trace2, procStartInfo, Trace2ProcessClass.UiHelper); + var process = ChildProcess.Start(procStartInfo, Trace2ProcessClass.UiHelper); if (process is null) { var format = "Failed to start helper process: {0} {1}"; var message = string.Format(format, path, args); - throw new Trace2Exception(Context.Trace2, message, format); + throw new Trace2Exception(message, format); } // Kill the process upon a cancellation request @@ -77,7 +77,7 @@ protected internal virtual async Task> InvokeHelperA errorMessage = "Unknown"; } - throw new Trace2Exception(Context.Trace2, $"helper error ({exitCode}): {errorMessage}"); + throw new Trace2Exception($"helper error ({exitCode}): {errorMessage}"); } return resultDict; @@ -93,7 +93,7 @@ protected void ThrowIfUserInteractionDisabled() Constants.GitConfiguration.Credential.Interactive); Context.Trace.WriteLine($"{envName} / {cfgName} is false/never; user interactivity has been disabled."); - throw new Trace2InvalidOperationException(Context.Trace2, "Cannot prompt because user interactivity has been disabled."); + throw new Trace2InvalidOperationException("Cannot prompt because user interactivity has been disabled."); } } @@ -102,7 +102,7 @@ protected void ThrowIfGuiPromptsDisabled() if (!Context.Settings.IsGuiPromptsEnabled) { Context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; GUI prompts have been disabled."); - throw new Trace2InvalidOperationException(Context.Trace2, "Cannot show prompt because GUI prompts have been disabled."); + throw new Trace2InvalidOperationException("Cannot show prompt because GUI prompts have been disabled."); } } @@ -111,7 +111,7 @@ protected void ThrowIfTerminalPromptsDisabled() if (!Context.Settings.IsTerminalPromptsEnabled) { Context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; terminal prompts have been disabled."); - throw new Trace2InvalidOperationException(Context.Trace2, "Cannot prompt because terminal prompts have been disabled."); + throw new Trace2InvalidOperationException("Cannot prompt because terminal prompts have been disabled."); } } diff --git a/src/Core/Authentication/BasicAuthentication.cs b/src/Core/Authentication/BasicAuthentication.cs index 9a8ac7be1c..1c6acd97bd 100644 --- a/src/Core/Authentication/BasicAuthentication.cs +++ b/src/Core/Authentication/BasicAuthentication.cs @@ -112,12 +112,12 @@ private async Task GetCredentialsViaHelperAsync(string command, str if (!resultDict.TryGetValue("username", out userName)) { - throw new Trace2Exception(Context.Trace2, "Missing 'username' in response"); + throw new Trace2Exception("Missing 'username' in response"); } if (!resultDict.TryGetValue("password", out string password)) { - throw new Trace2Exception(Context.Trace2, "Missing 'password' in response"); + throw new Trace2Exception("Missing 'password' in response"); } return new GitCredential(userName, password); diff --git a/src/Core/Authentication/Entra/EntraAuthentication.Caching.cs b/src/Core/Authentication/Entra/EntraAuthentication.Caching.cs index 3771ed81cd..8e17b94681 100644 --- a/src/Core/Authentication/Entra/EntraAuthentication.Caching.cs +++ b/src/Core/Authentication/Entra/EntraAuthentication.Caching.cs @@ -29,7 +29,7 @@ private async Task RegisterCacheAsync(ITokenCache cache, StoragePropertiesBuilde if (!PlatformUtils.IsWindows() && !PlatformUtils.IsPosix()) { - string osType = PlatformUtils.GetPlatformInformation(Context.Trace2).OperatingSystemType; + string osType = PlatformUtils.GetPlatformInformation().OperatingSystemType; Context.Trace.WriteLine($"Token cache integration is not supported on {osType}."); return; } @@ -51,7 +51,7 @@ private async Task RegisterCacheAsync(ITokenCache cache, StoragePropertiesBuilde Context.Console.WriteWarning("cannot persist Entra authentication token cache securely!"); Context.Trace.WriteLine(message); Context.Trace.WriteException(ex); - Context.Trace2.WriteError(message); + Trace2.WriteError(message); if (PlatformUtils.IsMacOS()) { diff --git a/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs b/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs index f0d79e40b6..94b1f8c6ac 100644 --- a/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs +++ b/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs @@ -116,7 +116,7 @@ public async Task GetInteractionModeAsync(CancellationToken ct return choice; } - throw new Trace2Exception(Context.Trace2, "Missing or invalid interaction_mode in response"); + throw new Trace2Exception("Missing or invalid interaction_mode in response"); } // TODO: show prompt in-proc @@ -343,7 +343,7 @@ private async Task UseDefaultAccountAsync(string userName, CancellationTok return str.ToBooleanyOrDefault(false); } - throw new Trace2Exception(Context.Trace2, "Missing use_default_account in response"); + throw new Trace2Exception("Missing use_default_account in response"); } var viewModel = new DefaultAccountViewModel(Context.SessionManager) diff --git a/src/Core/Authentication/OAuth/OAuth2Client.cs b/src/Core/Authentication/OAuth/OAuth2Client.cs index fda82a5f04..257e868169 100644 --- a/src/Core/Authentication/OAuth/OAuth2Client.cs +++ b/src/Core/Authentication/OAuth/OAuth2Client.cs @@ -175,19 +175,19 @@ public async Task GetAuthorizationCodeAsync(IEnum // form of failed MITM or replay attack. if (!responseParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.StateParameter, out string replyState)) { - throw new Trace2OAuth2Exception(_trace2, + throw new Trace2OAuth2Exception( $"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response."); } if (!StringComparer.Ordinal.Equals(state, replyState)) { - throw new Trace2OAuth2Exception(_trace2, + throw new Trace2OAuth2Exception( $"Invalid '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response; does not match the request."); } // We expect to have the auth code in the response otherwise terminate the flow (we failed authentication for some reason) if (!responseParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter, out string authCode)) { - throw new Trace2OAuth2Exception(_trace2, + throw new Trace2OAuth2Exception( $"Missing '{OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter}' in response."); } @@ -201,7 +201,7 @@ public async Task GetDeviceCodeAsync(IEnumerable if (_endpoints.DeviceAuthorizationEndpoint is null) { - throw new Trace2InvalidOperationException(_trace2, + throw new Trace2InvalidOperationException( "No device authorization endpoint has been configured for this client."); } @@ -416,7 +416,7 @@ protected Exception CreateExceptionFromResponse(string json) var format = "Unknown OAuth error: {0}"; var message = string.Format(format, json); - return new Trace2OAuth2Exception(_trace2, message, format); + return new Trace2OAuth2Exception(message, format); } protected static bool TryDeserializeJson(string json, JsonTypeInfo typeInfo, out T obj) diff --git a/src/Core/Authentication/OAuthAuthentication.cs b/src/Core/Authentication/OAuthAuthentication.cs index 375ee12b23..ebcbeece75 100644 --- a/src/Core/Authentication/OAuthAuthentication.cs +++ b/src/Core/Authentication/OAuthAuthentication.cs @@ -157,7 +157,7 @@ private async Task GetAuthenticationModeViaHelperAsync if (!resultDict.TryGetValue("mode", out string responseMode)) { - throw new Trace2Exception(Context.Trace2, "Missing 'mode' in response"); + throw new Trace2Exception("Missing 'mode' in response"); } switch (responseMode.ToLowerInvariant()) @@ -169,7 +169,7 @@ private async Task GetAuthenticationModeViaHelperAsync return OAuthAuthenticationModes.DeviceCode; default: - throw new Trace2Exception(Context.Trace2, + throw new Trace2Exception( $"Unknown mode value in response '{responseMode}'"); } } @@ -181,7 +181,7 @@ public async Task GetTokenByBrowserAsync(OAuth2Client client, // We require a desktop session to launch the user's default web browser if (!Context.SessionManager.IsDesktopSession) { - throw new Trace2InvalidOperationException(Context.Trace2, + throw new Trace2InvalidOperationException( "Browser authentication requires a desktop session"); } @@ -226,7 +226,7 @@ public async Task GetTokenByDeviceCodeAsync(OAuth2Client clie } catch (OperationCanceledException) { - throw new Trace2Exception(Context.Trace2, "User canceled device code authentication"); + throw new Trace2Exception("User canceled device code authentication"); } // Close the dialog diff --git a/src/Core/Commands/GitCommandBase.cs b/src/Core/Commands/GitCommandBase.cs index f9b56bb8ef..074fe4b23b 100644 --- a/src/Core/Commands/GitCommandBase.cs +++ b/src/Core/Commands/GitCommandBase.cs @@ -57,23 +57,23 @@ protected virtual void EnsureMinimumRequest(GitRequest request) { if (request.Protocol is null) { - throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'protocol' request argument"); + throw new Trace2InvalidOperationException("Missing 'protocol' request argument"); } if (string.IsNullOrWhiteSpace(request.Protocol)) { - throw new Trace2InvalidOperationException(Context.Trace2, + throw new Trace2InvalidOperationException( "Invalid 'protocol' request argument (cannot be empty)"); } if (request.Host is null) { - throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'host' request argument"); + throw new Trace2InvalidOperationException("Missing 'host' request argument"); } if (string.IsNullOrWhiteSpace(request.Host)) { - throw new Trace2InvalidOperationException(Context.Trace2, + throw new Trace2InvalidOperationException( "Invalid 'host' request argument (cannot be empty)"); } } diff --git a/src/Core/Commands/StoreCommand.cs b/src/Core/Commands/StoreCommand.cs index a4d960d9bf..b4d7c058da 100644 --- a/src/Core/Commands/StoreCommand.cs +++ b/src/Core/Commands/StoreCommand.cs @@ -26,12 +26,12 @@ protected override void EnsureMinimumRequest(GitRequest request) // An empty string username/password are valid inputs, so only check for `null` (not provided) if (request.UserName is null) { - throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'username' request argument"); + throw new Trace2InvalidOperationException("Missing 'username' request argument"); } if (request.Password is null) { - throw new Trace2InvalidOperationException(Context.Trace2, "Missing 'password' request argument"); + throw new Trace2InvalidOperationException("Missing 'password' request argument"); } } } diff --git a/src/Core/CredentialStore.cs b/src/Core/CredentialStore.cs index 95d26df320..377848ec2a 100644 --- a/src/Core/CredentialStore.cs +++ b/src/Core/CredentialStore.cs @@ -86,7 +86,7 @@ private void EnsureBackingStore() case StoreNames.Gpg: ValidateGpgPass(out string gpgStoreRoot, out string gpgExec); - IGpg gpg = new Gpg(gpgExec, _context.SessionManager, _context.ProcessManager, _context.Trace2); + IGpg gpg = new Gpg(gpgExec, _context.SessionManager, _context.ProcessManager); _backingStore = new GpgPassCredentialStore(_context.FileSystem, gpg, gpgStoreRoot, ns); break; @@ -109,7 +109,7 @@ private void EnsureBackingStore() sb.AppendLine(string.IsNullOrWhiteSpace(credStoreName) ? "No credential store has been selected." : $"Unknown credential store '{credStoreName}'."); - _context.Trace2.WriteError(sb.ToString()); + Trace2.WriteError(sb.ToString()); sb.AppendFormat( "{3}Set the {0} environment variable or the {1}.{2} Git configuration setting to one of the following options:{3}{3}", Constants.EnvironmentVariables.GcmCredentialStore, @@ -182,7 +182,7 @@ private void ValidateWindowsCredentialManager() if (!PlatformUtils.IsWindows()) { var message = $"Can only use the '{StoreNames.WindowsCredentialManager}' credential store on Windows."; - _context.Trace2.WriteError(message); + Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -191,7 +191,7 @@ private void ValidateWindowsCredentialManager() if (!WindowsCredentialManager.CanPersist()) { var message = $"Unable to persist credentials with the '{StoreNames.WindowsCredentialManager}' credential store."; - _context.Trace2.WriteError(message); + Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -203,7 +203,7 @@ private void ValidateDpapi(out string storeRoot) if (!PlatformUtils.IsWindows()) { var message = $"Can only use the '{StoreNames.Dpapi}' credential store on Windows."; - _context.Trace2.WriteError(message); + Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -226,7 +226,7 @@ private void ValidateMacOSKeychain() if (!PlatformUtils.IsMacOS()) { var message = $"Can only use the '{StoreNames.MacOSKeychain}' credential store on macOS."; - _context.Trace2.WriteError(message); + Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -238,7 +238,7 @@ private void ValidateSecretService() if (!PlatformUtils.IsLinux()) { var message = $"Can only use the '{StoreNames.SecretService}' credential store on Linux."; - _context.Trace2.WriteError(message); + Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -247,7 +247,7 @@ private void ValidateSecretService() if (!_context.SessionManager.IsDesktopSession) { var message = $"Cannot use the '{StoreNames.SecretService}' credential backing store without a graphical interface present."; - _context.Trace2.WriteError(message); + Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -259,7 +259,7 @@ private void ValidateGpgPass(out string storeRoot, out string execPath) if (!PlatformUtils.IsPosix()) { var message = $"Can only use the '{StoreNames.Gpg}' credential store on POSIX systems."; - _context.Trace2.WriteError(message); + Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -274,7 +274,7 @@ private void ValidateGpgPass(out string storeRoot, out string execPath) !_context.Environment.Variables.ContainsKey("SSH_TTY")) { var message = "GPG_TTY is not set; add `export GPG_TTY=$(tty)` to your profile."; - _context.Trace2.WriteError(message); + Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -298,7 +298,7 @@ private void ValidateCredentialCache(out string options) if (PlatformUtils.IsWindows()) { var message = $"Can not use the '{StoreNames.Cache}' credential store on Windows due to lack of UNIX socket support in Git for Windows."; - _context.Trace2.WriteError(message); + Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -345,7 +345,7 @@ private string GetGpgPath() var format = "GPG executable does not exist with path '{0}'"; var message = string.Format(format, gpgPath); - throw new Trace2Exception(_context.Trace2, message, format); + throw new Trace2Exception(message, format); } // If no explicit GPG path is specified, mimic the way `pass` diff --git a/src/Core/GenericHostProvider.cs b/src/Core/GenericHostProvider.cs index 9dae9c4bb8..9d0ecddc2f 100644 --- a/src/Core/GenericHostProvider.cs +++ b/src/Core/GenericHostProvider.cs @@ -231,7 +231,7 @@ await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, null) } else { - string osType = PlatformUtils.GetPlatformInformation(_context.Trace2).OperatingSystemType; + string osType = PlatformUtils.GetPlatformInformation().OperatingSystemType; _context.Trace.WriteLine($"Skipping check for Windows Integrated Authentication on {osType}."); } } @@ -354,7 +354,7 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa break; default: - throw new Trace2Exception(_context.Trace2, "No authentication mode selected!"); + throw new Trace2Exception("No authentication mode selected!"); } // Store the refresh token if we have one diff --git a/src/Core/Git.cs b/src/Core/Git.cs index bec8755b89..2f5371731e 100644 --- a/src/Core/Git.cs +++ b/src/Core/Git.cs @@ -159,7 +159,7 @@ private string GetCurrentRepositoryInternal(bool suppressStreams) default: var message = "Failed to get current Git repository"; _trace.WriteLine($"{message} (exit={git.ExitCode})"); - throw CreateGitException(git, message, _trace2); + throw CreateGitException(git, message); } } } @@ -186,7 +186,7 @@ public IEnumerable GetRemotes() default: var message = "Failed to enumerate Git remotes"; _trace.WriteLine($"{message} (exit={git.ExitCode})"); - throw CreateGitException(git, message, _trace2); + throw CreateGitException(git, message); } string[] lines = data.Split('\n'); @@ -239,7 +239,7 @@ public async Task> InvokeHelperAsync(string args, ID { var format = "Failed to start Git helper '{0}'"; var message = string.Format(format, args); - throw new Trace2Exception(_trace2, message, format); + throw new Trace2Exception(message, format); } if (!(standardInput is null)) @@ -268,16 +268,13 @@ public async Task> InvokeHelperAsync(string args, ID return resultDict; } - public static GitException CreateGitException(ChildProcess git, string message, ITrace2 trace2 = null) + public static GitException CreateGitException(ChildProcess git, string message) { var gitMessage = git.StartInfo.RedirectStandardError ? git.StandardError.ReadToEnd() : null; - if (trace2 != null) - throw new Trace2GitException(trace2, message, git.ExitCode, gitMessage); - - throw new GitException(message, gitMessage, git.ExitCode); + throw new Trace2GitException(message, git.ExitCode, gitMessage); } } diff --git a/src/Core/Gpg.cs b/src/Core/Gpg.cs index 77e1fc6610..eb541bbefc 100644 --- a/src/Core/Gpg.cs +++ b/src/Core/Gpg.cs @@ -43,7 +43,7 @@ public string DecryptFile(string path) { if (!gpg.Start(Trace2ProcessClass.Other)) { - throw new Trace2Exception(_trace2, "Failed to start gpg."); + throw new Trace2Exception("Failed to start gpg."); } gpg.WaitForExit(); @@ -54,7 +54,7 @@ public string DecryptFile(string path) string stderr = gpg.StandardError.ReadToEnd(); var format = "Failed to decrypt file '{0}' with gpg. exit={1}, out={2}, err={3}"; var message = string.Format(format, path, gpg.ExitCode, stdout, stderr); - throw new Trace2Exception(_trace2, message, format); + throw new Trace2Exception(message, format); } return gpg.StandardOutput.ReadToEnd(); @@ -77,7 +77,7 @@ public void EncryptFile(string path, string gpgId, string contents) { if (!gpg.Start(Trace2ProcessClass.Other)) { - throw new Trace2Exception(_trace2, "Failed to start gpg."); + throw new Trace2Exception("Failed to start gpg."); } gpg.StandardInput.Write(contents); @@ -91,7 +91,7 @@ public void EncryptFile(string path, string gpgId, string contents) string stderr = gpg.StandardError.ReadToEnd(); var format = "Failed to encrypt file '{0}' with gpg. exit={1}, out={2}, err={3}"; var message = string.Format(format, path, gpg.ExitCode, stdout, stderr); - throw new Trace2Exception(_trace2, message, format); + throw new Trace2Exception(message, format); } } } diff --git a/src/Core/HostProviderRegistry.cs b/src/Core/HostProviderRegistry.cs index e702b5e687..496d0ba519 100644 --- a/src/Core/HostProviderRegistry.cs +++ b/src/Core/HostProviderRegistry.cs @@ -152,7 +152,7 @@ public async Task GetProviderAsync(GitRequest request) var uri = request.GetRemoteUri(); if (uri is null) { - throw new Trace2Exception(_context.Trace2, "Unable to detect host provider without a remote URL"); + throw new Trace2Exception("Unable to detect host provider without a remote URL"); } // We can only probe HTTP(S) URLs - for SMTP, IMAP, etc we cannot do network probing @@ -244,7 +244,7 @@ await MatchProviderAsync(HostProviderPriority.Low, canProbeUri) ?? var message = "Failed to set host provider!"; _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); - _context.Trace2.WriteError(message); + Trace2.WriteError(message); _context.Console.WriteWarning("failed to remember result of host provider detection!"); _context.Console.WriteWarning($"try setting this manually: `git config --global {keyName} {match.Id}`"); diff --git a/src/Core/HttpClientFactory.cs b/src/Core/HttpClientFactory.cs index 0ec3a69f56..f91d3eba32 100644 --- a/src/Core/HttpClientFactory.cs +++ b/src/Core/HttpClientFactory.cs @@ -112,7 +112,7 @@ public HttpClient CreateClient() { var format = "Custom certificate bundle not found at path: {0}"; var message = string.Format(format, certBundlePath); - throw new Trace2FileNotFoundException(_trace2, message, format, certBundlePath); + throw new Trace2FileNotFoundException(message, format, certBundlePath); } Func validationCallback = (cert, chain, errors) => diff --git a/src/Core/Interop/Windows/Native/Win32Error.cs b/src/Core/Interop/Windows/Native/Win32Error.cs index f6a170bda6..e8a77480de 100644 --- a/src/Core/Interop/Windows/Native/Win32Error.cs +++ b/src/Core/Interop/Windows/Native/Win32Error.cs @@ -97,18 +97,6 @@ public static int GetLastError(bool success) return Marshal.GetLastWin32Error(); } - /// - /// Throw an if is not true. - /// - /// The application's TRACE2 tracer. - /// Windows API return code. - /// Default error message. - /// Throw if is not true. - public static void ThrowIfError(ITrace2 trace2, bool succeeded, string defaultErrorMessage = "Unknown error.") - { - ThrowIfError(GetLastError(succeeded), defaultErrorMessage, trace2); - } - /// /// Throw an if is not true. /// @@ -125,9 +113,8 @@ public static void ThrowIfError(bool succeeded, string defaultErrorMessage = "Un /// /// Windows API error code. /// Default error message. - /// The application's TRACE2 tracer. /// Throw if is not . - public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown error.", ITrace2 trace2 = null) + public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown error.") { switch (error) { @@ -136,9 +123,7 @@ public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown default: // The Win32Exception constructor will automatically get the human-readable // message for the error code. - if (trace2 != null) - throw new Trace2InteropException(trace2, defaultErrorMessage, new Win32Exception(error)); - throw new InteropException(defaultErrorMessage, new Win32Exception(error)); + throw new Trace2InteropException(defaultErrorMessage, new Win32Exception(error)); } } } diff --git a/src/Core/Tracing/Trace2Exception.cs b/src/Core/Tracing/Trace2Exception.cs index 292ec15161..0bee432293 100644 --- a/src/Core/Tracing/Trace2Exception.cs +++ b/src/Core/Tracing/Trace2Exception.cs @@ -8,68 +8,68 @@ namespace GitCredentialManager; public class Trace2Exception : Exception { - public Trace2Exception(ITrace2 trace2, string message) : base(message) + public Trace2Exception(string message) : base(message) { - trace2.WriteError(message); + Trace2.WriteError(message); } - public Trace2Exception(ITrace2 trace2, string message, string messageFormat) : base(message) + public Trace2Exception(string message, string messageFormat) : base(message) { - trace2.WriteError(message, messageFormat); + Trace2.WriteError(message, messageFormat); } } public class Trace2InvalidOperationException : InvalidOperationException { - public Trace2InvalidOperationException(ITrace2 trace2, string message) : base(message) + public Trace2InvalidOperationException(string message) : base(message) { - trace2.WriteError(message); + Trace2.WriteError(message); } } public class Trace2OAuth2Exception : OAuth2Exception { - public Trace2OAuth2Exception(ITrace2 trace2, string message) : base(message) + public Trace2OAuth2Exception(string message) : base(message) { - trace2.WriteError(message); + Trace2.WriteError(message); } - public Trace2OAuth2Exception(ITrace2 trace2, string message, string messageFormat) : base(message) + public Trace2OAuth2Exception(string message, string messageFormat) : base(message) { - trace2.WriteError(message, messageFormat); + Trace2.WriteError(message, messageFormat); } } public class Trace2InteropException : InteropException { - public Trace2InteropException(ITrace2 trace2, string message, int errorCode) : base(message, errorCode) + public Trace2InteropException(string message, int errorCode) : base(message, errorCode) { - trace2.WriteError($"message: {message} error code: {errorCode}"); + Trace2.WriteError($"message: {message} error code: {errorCode}"); } - public Trace2InteropException(ITrace2 trace2, string message, Win32Exception ex) : base(message, ex) + public Trace2InteropException(string message, Win32Exception ex) : base(message, ex) { - trace2.WriteError(message); + Trace2.WriteError(message); } } public class Trace2GitException : GitException { - public Trace2GitException(ITrace2 trace2, string message, int errorCode, string gitMessage) : + public Trace2GitException(string message, int errorCode, string gitMessage) : base(message, gitMessage, errorCode) { var format = $"message: '{message}' error code: '{errorCode}' git message: '{{0}}'"; var traceMessage = string.Format(format, gitMessage); - trace2.WriteError(traceMessage, format); + Trace2.WriteError(traceMessage, format); } } public class Trace2FileNotFoundException : FileNotFoundException { - public Trace2FileNotFoundException(ITrace2 trace2, string message, string messageFormat, string fileName) : + public Trace2FileNotFoundException(string message, string messageFormat, string fileName) : base(message, fileName) { - trace2.WriteError(message, messageFormat); + Trace2.WriteError(message, messageFormat); } } diff --git a/src/Core/UI/Commands/CredentialsCommand.cs b/src/Core/UI/Commands/CredentialsCommand.cs index c2ba791ff9..ae94708096 100644 --- a/src/Core/UI/Commands/CredentialsCommand.cs +++ b/src/Core/UI/Commands/CredentialsCommand.cs @@ -50,7 +50,7 @@ private async Task ExecuteAsync(string title, string resource, string userN if (!viewModel.WindowResult) { - throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); + throw new Trace2Exception("User cancelled dialog."); } WriteResult( diff --git a/src/Core/UI/Commands/DefaultAccountCommand.cs b/src/Core/UI/Commands/DefaultAccountCommand.cs index 4da8c0ffc8..5315336187 100644 --- a/src/Core/UI/Commands/DefaultAccountCommand.cs +++ b/src/Core/UI/Commands/DefaultAccountCommand.cs @@ -41,7 +41,7 @@ private async Task ExecuteAsync(string title, string userName, bool noLogo) if (!viewModel.WindowResult) { - throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); + throw new Trace2Exception("User cancelled dialog."); } WriteResult( diff --git a/src/Core/UI/Commands/DeviceCodeCommand.cs b/src/Core/UI/Commands/DeviceCodeCommand.cs index d260b077c5..7e05f67dbe 100644 --- a/src/Core/UI/Commands/DeviceCodeCommand.cs +++ b/src/Core/UI/Commands/DeviceCodeCommand.cs @@ -36,7 +36,7 @@ private async Task ExecuteAsync(string code, string url, bool noLogo) if (!viewModel.WindowResult) { - throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); + throw new Trace2Exception("User cancelled dialog."); } return 0; diff --git a/src/Core/UI/Commands/OAuthCommand.cs b/src/Core/UI/Commands/OAuthCommand.cs index 0042c029f3..5513971c1a 100644 --- a/src/Core/UI/Commands/OAuthCommand.cs +++ b/src/Core/UI/Commands/OAuthCommand.cs @@ -52,7 +52,7 @@ private async Task ExecuteAsync(string title, string resource, bool browser if (!viewModel.WindowResult) { - throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); + throw new Trace2Exception("User cancelled dialog."); } var result = new Dictionary(); diff --git a/src/GitHub/GitHubAuthentication.cs b/src/GitHub/GitHubAuthentication.cs index e392a2cf07..159eb6c9d6 100644 --- a/src/GitHub/GitHubAuthentication.cs +++ b/src/GitHub/GitHubAuthentication.cs @@ -306,7 +306,7 @@ private async Task GetAuthenticationViaHelperAsync( if (!resultDict.TryGetValue("mode", out string responseMode)) { - throw new Trace2Exception(Context.Trace2, "Missing 'mode' in response"); + throw new Trace2Exception("Missing 'mode' in response"); } switch (responseMode.ToLowerInvariant()) @@ -314,7 +314,7 @@ private async Task GetAuthenticationViaHelperAsync( case "pat": if (!resultDict.TryGetValue("pat", out string pat)) { - throw new Trace2Exception(Context.Trace2, "Missing 'pat' in response"); + throw new Trace2Exception("Missing 'pat' in response"); } return new AuthenticationPromptResult( @@ -329,19 +329,19 @@ private async Task GetAuthenticationViaHelperAsync( case "basic": if (!resultDict.TryGetValue("username", out userName)) { - throw new Trace2Exception(Context.Trace2, "Missing 'username' in response"); + throw new Trace2Exception("Missing 'username' in response"); } if (!resultDict.TryGetValue("password", out string password)) { - throw new Trace2Exception(Context.Trace2, "Missing 'password' in response"); + throw new Trace2Exception("Missing 'password' in response"); } return new AuthenticationPromptResult( AuthenticationModes.Basic, new GitCredential(userName, password)); default: - throw new Trace2Exception(Context.Trace2, + throw new Trace2Exception( $"Unknown mode value in response '{responseMode}'"); } } @@ -400,7 +400,7 @@ private async Task GetTwoFactorCodeViaHelperAsync(bool isSms, string arg if (!resultDict.TryGetValue("code", out string authCode)) { - throw new Trace2Exception(Context.Trace2, "Missing 'code' in response"); + throw new Trace2Exception("Missing 'code' in response"); } return authCode; @@ -415,7 +415,7 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, // Can we launch the user's default web browser? if (!Context.SessionManager.IsWebBrowserAvailable) { - throw new Trace2InvalidOperationException(Context.Trace2, + throw new Trace2InvalidOperationException( "Browser authentication requires a desktop session"); } @@ -481,7 +481,7 @@ public async Task GetOAuthTokenViaDeviceCodeAsync(Uri targetU } catch (OperationCanceledException) { - throw new Trace2InvalidOperationException(Context.Trace2, + throw new Trace2InvalidOperationException( "User canceled device code authentication"); } diff --git a/src/GitHub/GitHubHostProvider.cs b/src/GitHub/GitHubHostProvider.cs index 7c24d6d0bb..813be59daa 100644 --- a/src/GitHub/GitHubHostProvider.cs +++ b/src/GitHub/GitHubHostProvider.cs @@ -290,7 +290,7 @@ public virtual Task EraseCredentialAsync(GitRequest request) if (!_context.Settings.AllowUnsafeRemotes && StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http")) { - throw new Trace2Exception(_context.Trace2, + throw new Trace2Exception( "Unencrypted HTTP is not recommended for GitHub. " + "Ensure the repository remote URL is using HTTPS " + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); @@ -396,7 +396,7 @@ private async Task GeneratePersonalAccessTokenAsync(Uri targetUri var format = "Interactive logon for '{0}' failed."; var message = string.Format(format, targetUri); - throw new Trace2Exception(_context.Trace2, message, format); + throw new Trace2Exception(message, format); } internal async Task GetSupportedAuthenticationModesAsync(Uri targetUri) @@ -460,7 +460,7 @@ internal async Task GetSupportedAuthenticationModesAsync(Ur _context.Trace.WriteLine(message); _context.Trace.WriteException(ex); - _context.Trace2.WriteError(message, format); + Trace2.WriteError(message, format); _context.Console.WriteWarning(message); diff --git a/src/GitHub/UI/Commands/CredentialsCommand.cs b/src/GitHub/UI/Commands/CredentialsCommand.cs index 45c6cfd7fe..1b228d97bb 100644 --- a/src/GitHub/UI/Commands/CredentialsCommand.cs +++ b/src/GitHub/UI/Commands/CredentialsCommand.cs @@ -63,7 +63,7 @@ private async Task ExecuteAsync(string enterpriseUrl, string userName, if (!viewModel.WindowResult) { - throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); + throw new Trace2Exception("User cancelled dialog."); } var result = new Dictionary(); diff --git a/src/GitHub/UI/Commands/DeviceCommand.cs b/src/GitHub/UI/Commands/DeviceCommand.cs index 7ffdae7ec2..35721826cf 100644 --- a/src/GitHub/UI/Commands/DeviceCommand.cs +++ b/src/GitHub/UI/Commands/DeviceCommand.cs @@ -33,7 +33,7 @@ private async Task ExecuteAsync(string code, string url) if (!viewModel.WindowResult) { - throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); + throw new Trace2Exception("User cancelled dialog."); } return 0; diff --git a/src/GitHub/UI/Commands/TwoFactorCommand.cs b/src/GitHub/UI/Commands/TwoFactorCommand.cs index 54d4af90c0..35471ce967 100644 --- a/src/GitHub/UI/Commands/TwoFactorCommand.cs +++ b/src/GitHub/UI/Commands/TwoFactorCommand.cs @@ -30,7 +30,7 @@ private async Task ExecuteAsync(bool sms) if (!viewModel.WindowResult) { - throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); + throw new Trace2Exception("User cancelled dialog."); } WriteResult(new Dictionary diff --git a/src/GitLab/GitLabAuthentication.cs b/src/GitLab/GitLabAuthentication.cs index 7cbb2503b4..16be66986e 100644 --- a/src/GitLab/GitLabAuthentication.cs +++ b/src/GitLab/GitLabAuthentication.cs @@ -216,7 +216,7 @@ private async Task GetAuthenticationViaHelperAsync( if (!resultDict.TryGetValue("mode", out string responseMode)) { - throw new Trace2Exception(Context.Trace2, "Missing 'mode' in response"); + throw new Trace2Exception("Missing 'mode' in response"); } switch (responseMode.ToLowerInvariant()) @@ -224,7 +224,7 @@ private async Task GetAuthenticationViaHelperAsync( case "pat": if (!resultDict.TryGetValue("pat", out string pat)) { - throw new Trace2Exception(Context.Trace2, "Missing 'pat' in response"); + throw new Trace2Exception("Missing 'pat' in response"); } if (!resultDict.TryGetValue("username", out string patUserName)) @@ -241,19 +241,19 @@ private async Task GetAuthenticationViaHelperAsync( case "basic": if (!resultDict.TryGetValue("username", out userName)) { - throw new Trace2Exception(Context.Trace2, "Missing 'username' in response"); + throw new Trace2Exception("Missing 'username' in response"); } if (!resultDict.TryGetValue("password", out string password)) { - throw new Trace2Exception(Context.Trace2, "Missing 'password' in response"); + throw new Trace2Exception("Missing 'password' in response"); } return new AuthenticationPromptResult( AuthenticationModes.Basic, new GitCredential(userName, password)); default: - throw new Trace2Exception(Context.Trace2, + throw new Trace2Exception( $"Unknown mode value in response '{responseMode}'"); } } @@ -267,7 +267,7 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, // We require a desktop session to launch the user's default web browser if (!Context.SessionManager.IsDesktopSession) { - throw new Trace2InvalidOperationException(Context.Trace2, + throw new Trace2InvalidOperationException( "Browser authentication requires a desktop session"); } diff --git a/src/GitLab/GitLabHostProvider.cs b/src/GitLab/GitLabHostProvider.cs index 0e6b1001b3..8e8e8f25a4 100644 --- a/src/GitLab/GitLabHostProvider.cs +++ b/src/GitLab/GitLabHostProvider.cs @@ -99,7 +99,7 @@ public override async Task GenerateCredentialAsync(GitRequest reque if (!Context.Settings.AllowUnsafeRemotes && StringComparer.OrdinalIgnoreCase.Equals(request.Protocol, "http")) { - throw new Trace2Exception(Context.Trace2, + throw new Trace2Exception( "Unencrypted HTTP is not recommended for GitLab. " + "Ensure the repository remote URL is using HTTPS " + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); diff --git a/src/GitLab/UI/Commands/CredentialsCommand.cs b/src/GitLab/UI/Commands/CredentialsCommand.cs index 02a0f78180..dafe002d49 100644 --- a/src/GitLab/UI/Commands/CredentialsCommand.cs +++ b/src/GitLab/UI/Commands/CredentialsCommand.cs @@ -59,7 +59,7 @@ private async Task ExecuteAsync(string url, string userName, bool basic, bo if (!viewModel.WindowResult) { - throw new Trace2Exception(Context.Trace2, "User cancelled dialog."); + throw new Trace2Exception("User cancelled dialog."); } var result = new Dictionary(); diff --git a/src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs b/src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs index 0f4ab8497c..f832f2feab 100644 --- a/src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs +++ b/src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs @@ -142,13 +142,13 @@ public async Task CreatePersonalAccessTokenAsync(Uri organizationUri, st { if (TryGetFirstJsonStringField(responseText, "message", out string errorMessage)) { - throw new Trace2Exception(_context.Trace2, $"Failed to create PAT: {errorMessage}"); + throw new Trace2Exception($"Failed to create PAT: {errorMessage}"); } } } } - throw new Trace2Exception(_context.Trace2, "Failed to create PAT"); + throw new Trace2Exception("Failed to create PAT"); } #region Private Methods @@ -181,7 +181,7 @@ private async Task GetIdentityServiceUriAsync(Uri organizationUri, string a } } - throw new Trace2Exception(_context.Trace2, "Failed to find location service"); + throw new Trace2Exception("Failed to find location service"); } #endregion diff --git a/src/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/Microsoft.AzureRepos/AzureReposHostProvider.cs index ff26f772fe..03cc7db5d5 100644 --- a/src/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -248,7 +248,7 @@ private void ThrowIfUnsafeRemote(GitRequest request) if (!_context.Settings.AllowUnsafeRemotes && StringComparer.OrdinalIgnoreCase.Equals(request.Protocol, "http")) { - throw new Trace2Exception(_context.Trace2, + throw new Trace2Exception( "Unencrypted HTTP is not recommended for Azure Repos. " + "Ensure the repository remote URL is using HTTPS " + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); From 8f7bf5b9bb6d837cf686cff8111868c18c57b784 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:51:03 +0100 Subject: [PATCH 09/18] trace2: centralize exception events Exception subclasses that emit while being constructed can report failures more than once and make ordinary error handling depend on tracing. Emit once at the application boundary, preserve the richer Git and interop formats there, and return call sites to standard exception types. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- .../Cloud/BitbucketOAuth2ClientTest.cs | 2 +- .../BitbucketAuthentication.cs | 4 +- .../BitbucketHostProvider.cs | 8 +- .../UI/Commands/CredentialsCommand.cs | 2 +- .../BasicAuthenticationTests.cs | 2 +- .../Entra/EntraAuthenticationTests.cs | 2 +- src/Core.Tests/HostProviderRegistryTests.cs | 2 +- src/Core/Application.cs | 2 + src/Core/Authentication/AuthenticationBase.cs | 10 +-- .../Authentication/BasicAuthentication.cs | 4 +- .../Entra/EntraAuthentication.PublicClient.cs | 4 +- src/Core/Authentication/OAuth/OAuth2Client.cs | 11 ++- .../Authentication/OAuthAuthentication.cs | 8 +- src/Core/Commands/GitCommandBase.cs | 8 +- src/Core/Commands/StoreCommand.cs | 4 +- src/Core/CredentialStore.cs | 12 +-- src/Core/GenericHostProvider.cs | 2 +- src/Core/Git.cs | 4 +- src/Core/Gpg.cs | 8 +- src/Core/HostProviderRegistry.cs | 2 +- src/Core/HttpClientFactory.cs | 2 +- src/Core/Interop/Windows/Native/Win32Error.cs | 2 +- src/Core/Tracing/Trace2.cs | 36 +++++++++ src/Core/Tracing/Trace2Exception.cs | 75 ------------------- src/Core/UI/Commands/CredentialsCommand.cs | 3 +- src/Core/UI/Commands/DefaultAccountCommand.cs | 3 +- src/Core/UI/Commands/DeviceCodeCommand.cs | 3 +- src/Core/UI/Commands/OAuthCommand.cs | 2 +- src/Core/UI/HelperApplication.cs | 2 + src/GitHub.Tests/GitHubAuthenticationTests.cs | 4 +- src/GitHub.Tests/GitHubHostProviderTests.cs | 2 +- src/GitHub/GitHubAuthentication.cs | 16 ++-- src/GitHub/GitHubHostProvider.cs | 4 +- src/GitHub/UI/Commands/CredentialsCommand.cs | 2 +- src/GitHub/UI/Commands/DeviceCommand.cs | 3 +- src/GitHub/UI/Commands/TwoFactorCommand.cs | 3 +- src/GitLab.Tests/GitLabAuthenticationTests.cs | 4 +- src/GitLab/GitLabAuthentication.cs | 12 +-- src/GitLab/GitLabHostProvider.cs | 2 +- src/GitLab/UI/Commands/CredentialsCommand.cs | 2 +- .../AzureDevOpsApiTests.cs | 6 +- .../AzureReposHostProviderTests.cs | 2 +- .../AzureDevOpsRestApi.cs | 6 +- .../AzureReposHostProvider.cs | 2 +- 44 files changed, 129 insertions(+), 170 deletions(-) delete mode 100644 src/Core/Tracing/Trace2Exception.cs diff --git a/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs b/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs index c700c59202..ea8005a0ee 100644 --- a/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs +++ b/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs @@ -69,7 +69,7 @@ public async Task BitbucketOAuth2Client_GetAuthorizationCodeAsync_RespectsClient public async Task BitbucketOAuth2Client_GetDeviceCodeAsync() { var client = new Bitbucket.Cloud.BitbucketOAuth2Client(httpClient.Object, settings.Object); - await Assert.ThrowsAsync(async () => await client.GetDeviceCodeAsync(scopes, ct)); + await Assert.ThrowsAsync(async () => await client.GetDeviceCodeAsync(scopes, ct)); } [Theory] diff --git a/src/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/Atlassian.Bitbucket/BitbucketAuthentication.cs index be3dd0e0d0..53b86a0fa9 100644 --- a/src/Atlassian.Bitbucket/BitbucketAuthentication.cs +++ b/src/Atlassian.Bitbucket/BitbucketAuthentication.cs @@ -234,12 +234,12 @@ private async Task GetCredentialsViaHelperAsync( { if (!output.TryGetValue("username", out userName)) { - throw new Trace2Exception("Missing username in response"); + throw new Exception("Missing username in response"); } if (!output.TryGetValue("password", out string password)) { - throw new Trace2Exception("Missing password in response"); + throw new Exception("Missing password in response"); } return new CredentialsPromptResult( diff --git a/src/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/Atlassian.Bitbucket/BitbucketHostProvider.cs index 1f3faabe79..ff35884375 100644 --- a/src/Atlassian.Bitbucket/BitbucketHostProvider.cs +++ b/src/Atlassian.Bitbucket/BitbucketHostProvider.cs @@ -86,7 +86,7 @@ public async Task GetCredentialAsync(GitRequest request) StringComparer.OrdinalIgnoreCase.Equals(request.Protocol, "http") && BitbucketHelper.IsBitbucketOrg(request)) { - throw new Trace2Exception( + throw new Exception( "Unencrypted HTTP is not recommended for Bitbucket.org. " + "Ensure the repository remote URL is using HTTPS " + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); @@ -158,7 +158,7 @@ private async Task GetRefreshedCredentials(GitRequest request, Auth { var message = "User cancelled credential prompt"; _context.Trace.WriteLine(message); - throw new Trace2Exception(message); + throw new Exception(message); } switch (result.AuthenticationMode) @@ -374,7 +374,7 @@ private async Task ResolveOAuthUserNameAsync(GitRequest request, string return result.Response.UserName; } - throw new Trace2Exception( + throw new Exception( $"Failed to resolve username. HTTP: {result.StatusCode}"); } @@ -386,7 +386,7 @@ private async Task ResolveBasicAuthUserNameAsync(GitRequest request, str return result.Response.UserName; } - throw new Trace2Exception( + throw new Exception( $"Failed to resolve username. HTTP: {result.StatusCode}"); } diff --git a/src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs b/src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs index 7f2186ff81..d5131d88e1 100644 --- a/src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs +++ b/src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs @@ -44,7 +44,7 @@ private async Task ExecuteAsync(Uri url, string userName, bool showOAuth, b if (!viewModel.WindowResult || viewModel.SelectedMode == AuthenticationModes.None) { - throw new Trace2Exception("User cancelled dialog."); + throw new Exception("User cancelled dialog."); } switch (viewModel.SelectedMode) diff --git a/src/Core.Tests/Authentication/BasicAuthenticationTests.cs b/src/Core.Tests/Authentication/BasicAuthenticationTests.cs index 7e5a42488e..5c7fd04afe 100644 --- a/src/Core.Tests/Authentication/BasicAuthenticationTests.cs +++ b/src/Core.Tests/Authentication/BasicAuthenticationTests.cs @@ -70,7 +70,7 @@ public async Task BasicAuthentication_GetCredentials_NonDesktopSession_NoTermina var basicAuth = new BasicAuthentication(context); - await Assert.ThrowsAsync(() => basicAuth.GetCredentialsAsync(testResource)); + await Assert.ThrowsAsync(() => basicAuth.GetCredentialsAsync(testResource)); } [Fact] diff --git a/src/Core.Tests/Authentication/Entra/EntraAuthenticationTests.cs b/src/Core.Tests/Authentication/Entra/EntraAuthenticationTests.cs index fe2b74ee2b..4ce33ef030 100644 --- a/src/Core.Tests/Authentication/Entra/EntraAuthenticationTests.cs +++ b/src/Core.Tests/Authentication/Entra/EntraAuthenticationTests.cs @@ -25,7 +25,7 @@ public async Task GetTokenForUserAsync_NoInteraction_ThrowsException() }; var entraAuth = new EntraAuthentication(context, config); - await Assert.ThrowsAsync( + await Assert.ThrowsAsync( () => entraAuth.GetTokenForUserAsync(scopes, authority)); } diff --git a/src/Core.Tests/HostProviderRegistryTests.cs b/src/Core.Tests/HostProviderRegistryTests.cs index 0d43b1b520..638c8871ba 100644 --- a/src/Core.Tests/HostProviderRegistryTests.cs +++ b/src/Core.Tests/HostProviderRegistryTests.cs @@ -41,7 +41,7 @@ public async Task HostProviderRegistry_GetProvider_NoProviders_ThrowException() var registry = new HostProviderRegistry(context); var request = new GitRequest(new Dictionary()); - await Assert.ThrowsAsync(() => registry.GetProviderAsync(request)); + await Assert.ThrowsAsync(() => registry.GetProviderAsync(request)); } [Fact] diff --git a/src/Core/Application.cs b/src/Core/Application.cs index e1a9e19835..a6ce064414 100644 --- a/src/Core/Application.cs +++ b/src/Core/Application.cs @@ -183,6 +183,8 @@ internal static bool ContainsInterrupt(Exception ex) private bool WriteException(Exception ex) { + Trace2.WriteError(ex); + // Try and use a nicer format for some well-known exception types switch (ex) { diff --git a/src/Core/Authentication/AuthenticationBase.cs b/src/Core/Authentication/AuthenticationBase.cs index bf8d55177e..fb177d1129 100644 --- a/src/Core/Authentication/AuthenticationBase.cs +++ b/src/Core/Authentication/AuthenticationBase.cs @@ -52,7 +52,7 @@ protected internal virtual async Task> InvokeHelperA var format = "Failed to start helper process: {0} {1}"; var message = string.Format(format, path, args); - throw new Trace2Exception(message, format); + throw new Exception(message); } // Kill the process upon a cancellation request @@ -77,7 +77,7 @@ protected internal virtual async Task> InvokeHelperA errorMessage = "Unknown"; } - throw new Trace2Exception($"helper error ({exitCode}): {errorMessage}"); + throw new Exception($"helper error ({exitCode}): {errorMessage}"); } return resultDict; @@ -93,7 +93,7 @@ protected void ThrowIfUserInteractionDisabled() Constants.GitConfiguration.Credential.Interactive); Context.Trace.WriteLine($"{envName} / {cfgName} is false/never; user interactivity has been disabled."); - throw new Trace2InvalidOperationException("Cannot prompt because user interactivity has been disabled."); + throw new InvalidOperationException("Cannot prompt because user interactivity has been disabled."); } } @@ -102,7 +102,7 @@ protected void ThrowIfGuiPromptsDisabled() if (!Context.Settings.IsGuiPromptsEnabled) { Context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; GUI prompts have been disabled."); - throw new Trace2InvalidOperationException("Cannot show prompt because GUI prompts have been disabled."); + throw new InvalidOperationException("Cannot show prompt because GUI prompts have been disabled."); } } @@ -111,7 +111,7 @@ protected void ThrowIfTerminalPromptsDisabled() if (!Context.Settings.IsTerminalPromptsEnabled) { Context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; terminal prompts have been disabled."); - throw new Trace2InvalidOperationException("Cannot prompt because terminal prompts have been disabled."); + throw new InvalidOperationException("Cannot prompt because terminal prompts have been disabled."); } } diff --git a/src/Core/Authentication/BasicAuthentication.cs b/src/Core/Authentication/BasicAuthentication.cs index 1c6acd97bd..455f049efe 100644 --- a/src/Core/Authentication/BasicAuthentication.cs +++ b/src/Core/Authentication/BasicAuthentication.cs @@ -112,12 +112,12 @@ private async Task GetCredentialsViaHelperAsync(string command, str if (!resultDict.TryGetValue("username", out userName)) { - throw new Trace2Exception("Missing 'username' in response"); + throw new Exception("Missing 'username' in response"); } if (!resultDict.TryGetValue("password", out string password)) { - throw new Trace2Exception("Missing 'password' in response"); + throw new Exception("Missing 'password' in response"); } return new GitCredential(userName, password); diff --git a/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs b/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs index 94b1f8c6ac..decca2dd02 100644 --- a/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs +++ b/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs @@ -116,7 +116,7 @@ public async Task GetInteractionModeAsync(CancellationToken ct return choice; } - throw new Trace2Exception("Missing or invalid interaction_mode in response"); + throw new Exception("Missing or invalid interaction_mode in response"); } // TODO: show prompt in-proc @@ -343,7 +343,7 @@ private async Task UseDefaultAccountAsync(string userName, CancellationTok return str.ToBooleanyOrDefault(false); } - throw new Trace2Exception("Missing use_default_account in response"); + throw new Exception("Missing use_default_account in response"); } var viewModel = new DefaultAccountViewModel(Context.SessionManager) diff --git a/src/Core/Authentication/OAuth/OAuth2Client.cs b/src/Core/Authentication/OAuth/OAuth2Client.cs index 257e868169..587f6bfdf9 100644 --- a/src/Core/Authentication/OAuth/OAuth2Client.cs +++ b/src/Core/Authentication/OAuth/OAuth2Client.cs @@ -175,19 +175,19 @@ public async Task GetAuthorizationCodeAsync(IEnum // form of failed MITM or replay attack. if (!responseParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.StateParameter, out string replyState)) { - throw new Trace2OAuth2Exception( + throw new OAuth2Exception( $"Missing '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response."); } if (!StringComparer.Ordinal.Equals(state, replyState)) { - throw new Trace2OAuth2Exception( + throw new OAuth2Exception( $"Invalid '{OAuth2Constants.AuthorizationGrantResponse.StateParameter}' in response; does not match the request."); } // We expect to have the auth code in the response otherwise terminate the flow (we failed authentication for some reason) if (!responseParams.TryGetValue(OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter, out string authCode)) { - throw new Trace2OAuth2Exception( + throw new OAuth2Exception( $"Missing '{OAuth2Constants.AuthorizationGrantResponse.AuthorizationCodeParameter}' in response."); } @@ -201,7 +201,7 @@ public async Task GetDeviceCodeAsync(IEnumerable if (_endpoints.DeviceAuthorizationEndpoint is null) { - throw new Trace2InvalidOperationException( + throw new InvalidOperationException( "No device authorization endpoint has been configured for this client."); } @@ -410,13 +410,12 @@ protected Exception CreateExceptionFromResponse(string json) { if (TryCreateExceptionFromResponse(json, out OAuth2Exception exception)) { - Trace2.WriteError(exception.Message); return exception; } var format = "Unknown OAuth error: {0}"; var message = string.Format(format, json); - return new Trace2OAuth2Exception(message, format); + return new OAuth2Exception(message); } protected static bool TryDeserializeJson(string json, JsonTypeInfo typeInfo, out T obj) diff --git a/src/Core/Authentication/OAuthAuthentication.cs b/src/Core/Authentication/OAuthAuthentication.cs index ebcbeece75..e7446638ec 100644 --- a/src/Core/Authentication/OAuthAuthentication.cs +++ b/src/Core/Authentication/OAuthAuthentication.cs @@ -157,7 +157,7 @@ private async Task GetAuthenticationModeViaHelperAsync if (!resultDict.TryGetValue("mode", out string responseMode)) { - throw new Trace2Exception("Missing 'mode' in response"); + throw new Exception("Missing 'mode' in response"); } switch (responseMode.ToLowerInvariant()) @@ -169,7 +169,7 @@ private async Task GetAuthenticationModeViaHelperAsync return OAuthAuthenticationModes.DeviceCode; default: - throw new Trace2Exception( + throw new Exception( $"Unknown mode value in response '{responseMode}'"); } } @@ -181,7 +181,7 @@ public async Task GetTokenByBrowserAsync(OAuth2Client client, // We require a desktop session to launch the user's default web browser if (!Context.SessionManager.IsDesktopSession) { - throw new Trace2InvalidOperationException( + throw new InvalidOperationException( "Browser authentication requires a desktop session"); } @@ -226,7 +226,7 @@ public async Task GetTokenByDeviceCodeAsync(OAuth2Client clie } catch (OperationCanceledException) { - throw new Trace2Exception("User canceled device code authentication"); + throw new Exception("User canceled device code authentication"); } // Close the dialog diff --git a/src/Core/Commands/GitCommandBase.cs b/src/Core/Commands/GitCommandBase.cs index 074fe4b23b..76568e8942 100644 --- a/src/Core/Commands/GitCommandBase.cs +++ b/src/Core/Commands/GitCommandBase.cs @@ -57,23 +57,23 @@ protected virtual void EnsureMinimumRequest(GitRequest request) { if (request.Protocol is null) { - throw new Trace2InvalidOperationException("Missing 'protocol' request argument"); + throw new InvalidOperationException("Missing 'protocol' request argument"); } if (string.IsNullOrWhiteSpace(request.Protocol)) { - throw new Trace2InvalidOperationException( + throw new InvalidOperationException( "Invalid 'protocol' request argument (cannot be empty)"); } if (request.Host is null) { - throw new Trace2InvalidOperationException("Missing 'host' request argument"); + throw new InvalidOperationException("Missing 'host' request argument"); } if (string.IsNullOrWhiteSpace(request.Host)) { - throw new Trace2InvalidOperationException( + throw new InvalidOperationException( "Invalid 'host' request argument (cannot be empty)"); } } diff --git a/src/Core/Commands/StoreCommand.cs b/src/Core/Commands/StoreCommand.cs index b4d7c058da..16883acfe3 100644 --- a/src/Core/Commands/StoreCommand.cs +++ b/src/Core/Commands/StoreCommand.cs @@ -26,12 +26,12 @@ protected override void EnsureMinimumRequest(GitRequest request) // An empty string username/password are valid inputs, so only check for `null` (not provided) if (request.UserName is null) { - throw new Trace2InvalidOperationException("Missing 'username' request argument"); + throw new InvalidOperationException("Missing 'username' request argument"); } if (request.Password is null) { - throw new Trace2InvalidOperationException("Missing 'password' request argument"); + throw new InvalidOperationException("Missing 'password' request argument"); } } } diff --git a/src/Core/CredentialStore.cs b/src/Core/CredentialStore.cs index 377848ec2a..f919bbd28b 100644 --- a/src/Core/CredentialStore.cs +++ b/src/Core/CredentialStore.cs @@ -109,7 +109,6 @@ private void EnsureBackingStore() sb.AppendLine(string.IsNullOrWhiteSpace(credStoreName) ? "No credential store has been selected." : $"Unknown credential store '{credStoreName}'."); - Trace2.WriteError(sb.ToString()); sb.AppendFormat( "{3}Set the {0} environment variable or the {1}.{2} Git configuration setting to one of the following options:{3}{3}", Constants.EnvironmentVariables.GcmCredentialStore, @@ -182,7 +181,6 @@ private void ValidateWindowsCredentialManager() if (!PlatformUtils.IsWindows()) { var message = $"Can only use the '{StoreNames.WindowsCredentialManager}' credential store on Windows."; - Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -191,7 +189,6 @@ private void ValidateWindowsCredentialManager() if (!WindowsCredentialManager.CanPersist()) { var message = $"Unable to persist credentials with the '{StoreNames.WindowsCredentialManager}' credential store."; - Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -203,7 +200,6 @@ private void ValidateDpapi(out string storeRoot) if (!PlatformUtils.IsWindows()) { var message = $"Can only use the '{StoreNames.Dpapi}' credential store on Windows."; - Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -226,7 +222,6 @@ private void ValidateMacOSKeychain() if (!PlatformUtils.IsMacOS()) { var message = $"Can only use the '{StoreNames.MacOSKeychain}' credential store on macOS."; - Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -238,7 +233,6 @@ private void ValidateSecretService() if (!PlatformUtils.IsLinux()) { var message = $"Can only use the '{StoreNames.SecretService}' credential store on Linux."; - Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -247,7 +241,6 @@ private void ValidateSecretService() if (!_context.SessionManager.IsDesktopSession) { var message = $"Cannot use the '{StoreNames.SecretService}' credential backing store without a graphical interface present."; - Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -259,7 +252,6 @@ private void ValidateGpgPass(out string storeRoot, out string execPath) if (!PlatformUtils.IsPosix()) { var message = $"Can only use the '{StoreNames.Gpg}' credential store on POSIX systems."; - Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -274,7 +266,6 @@ private void ValidateGpgPass(out string storeRoot, out string execPath) !_context.Environment.Variables.ContainsKey("SSH_TTY")) { var message = "GPG_TTY is not set; add `export GPG_TTY=$(tty)` to your profile."; - Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -298,7 +289,6 @@ private void ValidateCredentialCache(out string options) if (PlatformUtils.IsWindows()) { var message = $"Can not use the '{StoreNames.Cache}' credential store on Windows due to lack of UNIX socket support in Git for Windows."; - Trace2.WriteError(message); throw new Exception(message + Environment.NewLine + $"See {Constants.HelpUrls.GcmCredentialStores} for more information." ); @@ -345,7 +335,7 @@ private string GetGpgPath() var format = "GPG executable does not exist with path '{0}'"; var message = string.Format(format, gpgPath); - throw new Trace2Exception(message, format); + throw new Exception(message); } // If no explicit GPG path is specified, mimic the way `pass` diff --git a/src/Core/GenericHostProvider.cs b/src/Core/GenericHostProvider.cs index 9d0ecddc2f..3480b9b439 100644 --- a/src/Core/GenericHostProvider.cs +++ b/src/Core/GenericHostProvider.cs @@ -354,7 +354,7 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa break; default: - throw new Trace2Exception("No authentication mode selected!"); + throw new Exception("No authentication mode selected!"); } // Store the refresh token if we have one diff --git a/src/Core/Git.cs b/src/Core/Git.cs index 2f5371731e..917dad73a5 100644 --- a/src/Core/Git.cs +++ b/src/Core/Git.cs @@ -239,7 +239,7 @@ public async Task> InvokeHelperAsync(string args, ID { var format = "Failed to start Git helper '{0}'"; var message = string.Format(format, args); - throw new Trace2Exception(message, format); + throw new Exception(message); } if (!(standardInput is null)) @@ -274,7 +274,7 @@ public static GitException CreateGitException(ChildProcess git, string message) ? git.StandardError.ReadToEnd() : null; - throw new Trace2GitException(message, git.ExitCode, gitMessage); + throw new GitException(message, gitMessage, git.ExitCode); } } diff --git a/src/Core/Gpg.cs b/src/Core/Gpg.cs index eb541bbefc..358ddd327f 100644 --- a/src/Core/Gpg.cs +++ b/src/Core/Gpg.cs @@ -43,7 +43,7 @@ public string DecryptFile(string path) { if (!gpg.Start(Trace2ProcessClass.Other)) { - throw new Trace2Exception("Failed to start gpg."); + throw new Exception("Failed to start gpg."); } gpg.WaitForExit(); @@ -54,7 +54,7 @@ public string DecryptFile(string path) string stderr = gpg.StandardError.ReadToEnd(); var format = "Failed to decrypt file '{0}' with gpg. exit={1}, out={2}, err={3}"; var message = string.Format(format, path, gpg.ExitCode, stdout, stderr); - throw new Trace2Exception(message, format); + throw new Exception(message); } return gpg.StandardOutput.ReadToEnd(); @@ -77,7 +77,7 @@ public void EncryptFile(string path, string gpgId, string contents) { if (!gpg.Start(Trace2ProcessClass.Other)) { - throw new Trace2Exception("Failed to start gpg."); + throw new Exception("Failed to start gpg."); } gpg.StandardInput.Write(contents); @@ -91,7 +91,7 @@ public void EncryptFile(string path, string gpgId, string contents) string stderr = gpg.StandardError.ReadToEnd(); var format = "Failed to encrypt file '{0}' with gpg. exit={1}, out={2}, err={3}"; var message = string.Format(format, path, gpg.ExitCode, stdout, stderr); - throw new Trace2Exception(message, format); + throw new Exception(message); } } } diff --git a/src/Core/HostProviderRegistry.cs b/src/Core/HostProviderRegistry.cs index 496d0ba519..7bdc4658df 100644 --- a/src/Core/HostProviderRegistry.cs +++ b/src/Core/HostProviderRegistry.cs @@ -152,7 +152,7 @@ public async Task GetProviderAsync(GitRequest request) var uri = request.GetRemoteUri(); if (uri is null) { - throw new Trace2Exception("Unable to detect host provider without a remote URL"); + throw new Exception("Unable to detect host provider without a remote URL"); } // We can only probe HTTP(S) URLs - for SMTP, IMAP, etc we cannot do network probing diff --git a/src/Core/HttpClientFactory.cs b/src/Core/HttpClientFactory.cs index f91d3eba32..81d511c873 100644 --- a/src/Core/HttpClientFactory.cs +++ b/src/Core/HttpClientFactory.cs @@ -112,7 +112,7 @@ public HttpClient CreateClient() { var format = "Custom certificate bundle not found at path: {0}"; var message = string.Format(format, certBundlePath); - throw new Trace2FileNotFoundException(message, format, certBundlePath); + throw new FileNotFoundException(message, certBundlePath); } Func validationCallback = (cert, chain, errors) => diff --git a/src/Core/Interop/Windows/Native/Win32Error.cs b/src/Core/Interop/Windows/Native/Win32Error.cs index e8a77480de..f5c4363398 100644 --- a/src/Core/Interop/Windows/Native/Win32Error.cs +++ b/src/Core/Interop/Windows/Native/Win32Error.cs @@ -123,7 +123,7 @@ public static void ThrowIfError(int error, string defaultErrorMessage = "Unknown default: // The Win32Exception constructor will automatically get the human-readable // message for the error code. - throw new Trace2InteropException(defaultErrorMessage, new Win32Exception(error)); + throw new InteropException(defaultErrorMessage, new Win32Exception(error)); } } } diff --git a/src/Core/Tracing/Trace2.cs b/src/Core/Tracing/Trace2.cs index f5ac318c48..00771c28b2 100644 --- a/src/Core/Tracing/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Text; using System.Threading; +using GitCredentialManager.Interop; namespace GitCredentialManager; @@ -282,6 +284,40 @@ public static void WriteError( }); } + internal static void WriteError( + Exception exception, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + { + EnsureArgument.NotNull(exception, nameof(exception)); + + switch (exception) + { + case GitException gitException: + { + string format = + $"message: '{gitException.Message}' error code: '{gitException.ExitCode}' git message: '{{0}}'"; + string message = string.Format(format, gitException.GitErrorMessage); + WriteError(message, format, filePath, lineNumber); + break; + } + case InteropException interopException: + { + const string format = "message: {0} error code: {1}"; + string message = string.Format( + CultureInfo.InvariantCulture, + format, + interopException.Message, + interopException.ErrorCode); + WriteError(message, format, filePath, lineNumber); + break; + } + default: + WriteError(exception.Message, filePath: filePath, lineNumber: lineNumber); + break; + } + } + public static IDisposable StartRegion( string category, string label, diff --git a/src/Core/Tracing/Trace2Exception.cs b/src/Core/Tracing/Trace2Exception.cs deleted file mode 100644 index 0bee432293..0000000000 --- a/src/Core/Tracing/Trace2Exception.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.ComponentModel; -using System.IO; -using GitCredentialManager.Authentication.OAuth; -using GitCredentialManager.Interop; - -namespace GitCredentialManager; - -public class Trace2Exception : Exception -{ - public Trace2Exception(string message) : base(message) - { - Trace2.WriteError(message); - } - - public Trace2Exception(string message, string messageFormat) : base(message) - { - Trace2.WriteError(message, messageFormat); - } -} - -public class Trace2InvalidOperationException : InvalidOperationException -{ - public Trace2InvalidOperationException(string message) : base(message) - { - Trace2.WriteError(message); - } -} - -public class Trace2OAuth2Exception : OAuth2Exception -{ - public Trace2OAuth2Exception(string message) : base(message) - { - Trace2.WriteError(message); - } - - public Trace2OAuth2Exception(string message, string messageFormat) : base(message) - { - Trace2.WriteError(message, messageFormat); - } -} - -public class Trace2InteropException : InteropException -{ - public Trace2InteropException(string message, int errorCode) : base(message, errorCode) - { - Trace2.WriteError($"message: {message} error code: {errorCode}"); - } - - public Trace2InteropException(string message, Win32Exception ex) : base(message, ex) - { - Trace2.WriteError(message); - } -} - -public class Trace2GitException : GitException -{ - public Trace2GitException(string message, int errorCode, string gitMessage) : - base(message, gitMessage, errorCode) - { - var format = $"message: '{message}' error code: '{errorCode}' git message: '{{0}}'"; - var traceMessage = string.Format(format, gitMessage); - - Trace2.WriteError(traceMessage, format); - } -} - -public class Trace2FileNotFoundException : FileNotFoundException -{ - public Trace2FileNotFoundException(string message, string messageFormat, string fileName) : - base(message, fileName) - { - Trace2.WriteError(message, messageFormat); - } -} diff --git a/src/Core/UI/Commands/CredentialsCommand.cs b/src/Core/UI/Commands/CredentialsCommand.cs index ae94708096..d8acf643e2 100644 --- a/src/Core/UI/Commands/CredentialsCommand.cs +++ b/src/Core/UI/Commands/CredentialsCommand.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.CommandLine; using System.Threading; @@ -50,7 +51,7 @@ private async Task ExecuteAsync(string title, string resource, string userN if (!viewModel.WindowResult) { - throw new Trace2Exception("User cancelled dialog."); + throw new Exception("User cancelled dialog."); } WriteResult( diff --git a/src/Core/UI/Commands/DefaultAccountCommand.cs b/src/Core/UI/Commands/DefaultAccountCommand.cs index 5315336187..c4bb776130 100644 --- a/src/Core/UI/Commands/DefaultAccountCommand.cs +++ b/src/Core/UI/Commands/DefaultAccountCommand.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.CommandLine; using System.Threading; @@ -41,7 +42,7 @@ private async Task ExecuteAsync(string title, string userName, bool noLogo) if (!viewModel.WindowResult) { - throw new Trace2Exception("User cancelled dialog."); + throw new Exception("User cancelled dialog."); } WriteResult( diff --git a/src/Core/UI/Commands/DeviceCodeCommand.cs b/src/Core/UI/Commands/DeviceCodeCommand.cs index 7e05f67dbe..e0629f8bd0 100644 --- a/src/Core/UI/Commands/DeviceCodeCommand.cs +++ b/src/Core/UI/Commands/DeviceCodeCommand.cs @@ -1,3 +1,4 @@ +using System; using System.CommandLine; using System.Threading; using System.Threading.Tasks; @@ -36,7 +37,7 @@ private async Task ExecuteAsync(string code, string url, bool noLogo) if (!viewModel.WindowResult) { - throw new Trace2Exception("User cancelled dialog."); + throw new Exception("User cancelled dialog."); } return 0; diff --git a/src/Core/UI/Commands/OAuthCommand.cs b/src/Core/UI/Commands/OAuthCommand.cs index 5513971c1a..bd8a7eb45a 100644 --- a/src/Core/UI/Commands/OAuthCommand.cs +++ b/src/Core/UI/Commands/OAuthCommand.cs @@ -52,7 +52,7 @@ private async Task ExecuteAsync(string title, string resource, bool browser if (!viewModel.WindowResult) { - throw new Trace2Exception("User cancelled dialog."); + throw new Exception("User cancelled dialog."); } var result = new Dictionary(); diff --git a/src/Core/UI/HelperApplication.cs b/src/Core/UI/HelperApplication.cs index f5504e79c5..c5d2159c13 100644 --- a/src/Core/UI/HelperApplication.cs +++ b/src/Core/UI/HelperApplication.cs @@ -54,6 +54,8 @@ private void OnException(Exception ex, InvocationContext invocationContext) private bool WriteException(Exception ex) { + Trace2.WriteError(ex); + Context.Streams.Out.WriteDictionary(new Dictionary { ["error"] = ex.Message diff --git a/src/GitHub.Tests/GitHubAuthenticationTests.cs b/src/GitHub.Tests/GitHubAuthenticationTests.cs index c441d26980..a55d206a2a 100644 --- a/src/GitHub.Tests/GitHubAuthenticationTests.cs +++ b/src/GitHub.Tests/GitHubAuthenticationTests.cs @@ -45,7 +45,7 @@ public async Task GitHubAuthentication_GetAuthenticationAsync_TerminalPromptsDis var context = new TestCommandContext(); context.Settings.IsTerminalPromptsEnabled = false; var auth = new GitHubAuthentication(context); - var exception = await Assert.ThrowsAsync( + var exception = await Assert.ThrowsAsync( () => auth.GetAuthenticationAsync(null, null, AuthenticationModes.All) ); Assert.Equal("Cannot prompt because terminal prompts have been disabled.", exception.Message); @@ -86,7 +86,7 @@ public async Task GitHubAuthentication_GetAuthenticationAsync_AuthenticationMode var context = new TestCommandContext(); context.Settings.IsInteractionAllowed = false; var auth = new GitHubAuthentication(context); - var exception = await Assert.ThrowsAsync( + var exception = await Assert.ThrowsAsync( () => auth.GetAuthenticationAsync(new Uri("https://github.com"), null, AuthenticationModes.All) ); Assert.Equal("Cannot prompt because user interactivity has been disabled.", exception.Message); diff --git a/src/GitHub.Tests/GitHubHostProviderTests.cs b/src/GitHub.Tests/GitHubHostProviderTests.cs index 2b4100e758..511e1d0adb 100644 --- a/src/GitHub.Tests/GitHubHostProviderTests.cs +++ b/src/GitHub.Tests/GitHubHostProviderTests.cs @@ -341,7 +341,7 @@ public async Task GitHubHostProvider_GenerateCredentialAsync_UnencryptedHttp_Thr var provider = new GitHubHostProvider(context, ghApi, ghAuth); - await Assert.ThrowsAsync(() => provider.GenerateCredentialAsync(remoteUri, null)); + await Assert.ThrowsAsync(() => provider.GenerateCredentialAsync(remoteUri, null)); } [Fact] diff --git a/src/GitHub/GitHubAuthentication.cs b/src/GitHub/GitHubAuthentication.cs index 159eb6c9d6..a6ad4ef826 100644 --- a/src/GitHub/GitHubAuthentication.cs +++ b/src/GitHub/GitHubAuthentication.cs @@ -306,7 +306,7 @@ private async Task GetAuthenticationViaHelperAsync( if (!resultDict.TryGetValue("mode", out string responseMode)) { - throw new Trace2Exception("Missing 'mode' in response"); + throw new Exception("Missing 'mode' in response"); } switch (responseMode.ToLowerInvariant()) @@ -314,7 +314,7 @@ private async Task GetAuthenticationViaHelperAsync( case "pat": if (!resultDict.TryGetValue("pat", out string pat)) { - throw new Trace2Exception("Missing 'pat' in response"); + throw new Exception("Missing 'pat' in response"); } return new AuthenticationPromptResult( @@ -329,19 +329,19 @@ private async Task GetAuthenticationViaHelperAsync( case "basic": if (!resultDict.TryGetValue("username", out userName)) { - throw new Trace2Exception("Missing 'username' in response"); + throw new Exception("Missing 'username' in response"); } if (!resultDict.TryGetValue("password", out string password)) { - throw new Trace2Exception("Missing 'password' in response"); + throw new Exception("Missing 'password' in response"); } return new AuthenticationPromptResult( AuthenticationModes.Basic, new GitCredential(userName, password)); default: - throw new Trace2Exception( + throw new Exception( $"Unknown mode value in response '{responseMode}'"); } } @@ -400,7 +400,7 @@ private async Task GetTwoFactorCodeViaHelperAsync(bool isSms, string arg if (!resultDict.TryGetValue("code", out string authCode)) { - throw new Trace2Exception("Missing 'code' in response"); + throw new Exception("Missing 'code' in response"); } return authCode; @@ -415,7 +415,7 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, // Can we launch the user's default web browser? if (!Context.SessionManager.IsWebBrowserAvailable) { - throw new Trace2InvalidOperationException( + throw new InvalidOperationException( "Browser authentication requires a desktop session"); } @@ -481,7 +481,7 @@ public async Task GetOAuthTokenViaDeviceCodeAsync(Uri targetU } catch (OperationCanceledException) { - throw new Trace2InvalidOperationException( + throw new InvalidOperationException( "User canceled device code authentication"); } diff --git a/src/GitHub/GitHubHostProvider.cs b/src/GitHub/GitHubHostProvider.cs index 813be59daa..f53e24706b 100644 --- a/src/GitHub/GitHubHostProvider.cs +++ b/src/GitHub/GitHubHostProvider.cs @@ -290,7 +290,7 @@ public virtual Task EraseCredentialAsync(GitRequest request) if (!_context.Settings.AllowUnsafeRemotes && StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http")) { - throw new Trace2Exception( + throw new Exception( "Unencrypted HTTP is not recommended for GitHub. " + "Ensure the repository remote URL is using HTTPS " + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); @@ -396,7 +396,7 @@ private async Task GeneratePersonalAccessTokenAsync(Uri targetUri var format = "Interactive logon for '{0}' failed."; var message = string.Format(format, targetUri); - throw new Trace2Exception(message, format); + throw new Exception(message); } internal async Task GetSupportedAuthenticationModesAsync(Uri targetUri) diff --git a/src/GitHub/UI/Commands/CredentialsCommand.cs b/src/GitHub/UI/Commands/CredentialsCommand.cs index 1b228d97bb..c5b11d5ebd 100644 --- a/src/GitHub/UI/Commands/CredentialsCommand.cs +++ b/src/GitHub/UI/Commands/CredentialsCommand.cs @@ -63,7 +63,7 @@ private async Task ExecuteAsync(string enterpriseUrl, string userName, if (!viewModel.WindowResult) { - throw new Trace2Exception("User cancelled dialog."); + throw new Exception("User cancelled dialog."); } var result = new Dictionary(); diff --git a/src/GitHub/UI/Commands/DeviceCommand.cs b/src/GitHub/UI/Commands/DeviceCommand.cs index 35721826cf..abe3788ac8 100644 --- a/src/GitHub/UI/Commands/DeviceCommand.cs +++ b/src/GitHub/UI/Commands/DeviceCommand.cs @@ -1,3 +1,4 @@ +using System; using System.CommandLine; using System.Threading; using System.Threading.Tasks; @@ -33,7 +34,7 @@ private async Task ExecuteAsync(string code, string url) if (!viewModel.WindowResult) { - throw new Trace2Exception("User cancelled dialog."); + throw new Exception("User cancelled dialog."); } return 0; diff --git a/src/GitHub/UI/Commands/TwoFactorCommand.cs b/src/GitHub/UI/Commands/TwoFactorCommand.cs index 35471ce967..129d23222d 100644 --- a/src/GitHub/UI/Commands/TwoFactorCommand.cs +++ b/src/GitHub/UI/Commands/TwoFactorCommand.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.CommandLine; using System.Threading; @@ -30,7 +31,7 @@ private async Task ExecuteAsync(bool sms) if (!viewModel.WindowResult) { - throw new Trace2Exception("User cancelled dialog."); + throw new Exception("User cancelled dialog."); } WriteResult(new Dictionary diff --git a/src/GitLab.Tests/GitLabAuthenticationTests.cs b/src/GitLab.Tests/GitLabAuthenticationTests.cs index 5ae5af74a6..44b408f855 100644 --- a/src/GitLab.Tests/GitLabAuthenticationTests.cs +++ b/src/GitLab.Tests/GitLabAuthenticationTests.cs @@ -37,7 +37,7 @@ public async Task GitLabAuthentication_GetAuthenticationAsync_TerminalPromptsDis var context = new TestCommandContext(); context.Settings.IsTerminalPromptsEnabled = false; var auth = new GitLabAuthentication(context); - var exception = await Assert.ThrowsAsync( + var exception = await Assert.ThrowsAsync( () => auth.GetAuthenticationAsync(null, null, AuthenticationModes.All) ); Assert.Equal("Cannot prompt because terminal prompts have been disabled.", exception.Message); @@ -89,7 +89,7 @@ public async Task GitLabAuthentication_GetAuthenticationAsync_AuthenticationMode var context = new TestCommandContext(); context.Settings.IsInteractionAllowed = false; var auth = new GitLabAuthentication(context); - var exception = await Assert.ThrowsAsync( + var exception = await Assert.ThrowsAsync( () => auth.GetAuthenticationAsync(new Uri("https://GitLab.com"), null, AuthenticationModes.All) ); Assert.Equal("Cannot prompt because user interactivity has been disabled.", exception.Message); diff --git a/src/GitLab/GitLabAuthentication.cs b/src/GitLab/GitLabAuthentication.cs index 16be66986e..07b69a3a5e 100644 --- a/src/GitLab/GitLabAuthentication.cs +++ b/src/GitLab/GitLabAuthentication.cs @@ -216,7 +216,7 @@ private async Task GetAuthenticationViaHelperAsync( if (!resultDict.TryGetValue("mode", out string responseMode)) { - throw new Trace2Exception("Missing 'mode' in response"); + throw new Exception("Missing 'mode' in response"); } switch (responseMode.ToLowerInvariant()) @@ -224,7 +224,7 @@ private async Task GetAuthenticationViaHelperAsync( case "pat": if (!resultDict.TryGetValue("pat", out string pat)) { - throw new Trace2Exception("Missing 'pat' in response"); + throw new Exception("Missing 'pat' in response"); } if (!resultDict.TryGetValue("username", out string patUserName)) @@ -241,19 +241,19 @@ private async Task GetAuthenticationViaHelperAsync( case "basic": if (!resultDict.TryGetValue("username", out userName)) { - throw new Trace2Exception("Missing 'username' in response"); + throw new Exception("Missing 'username' in response"); } if (!resultDict.TryGetValue("password", out string password)) { - throw new Trace2Exception("Missing 'password' in response"); + throw new Exception("Missing 'password' in response"); } return new AuthenticationPromptResult( AuthenticationModes.Basic, new GitCredential(userName, password)); default: - throw new Trace2Exception( + throw new Exception( $"Unknown mode value in response '{responseMode}'"); } } @@ -267,7 +267,7 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, // We require a desktop session to launch the user's default web browser if (!Context.SessionManager.IsDesktopSession) { - throw new Trace2InvalidOperationException( + throw new InvalidOperationException( "Browser authentication requires a desktop session"); } diff --git a/src/GitLab/GitLabHostProvider.cs b/src/GitLab/GitLabHostProvider.cs index 8e8e8f25a4..646593912c 100644 --- a/src/GitLab/GitLabHostProvider.cs +++ b/src/GitLab/GitLabHostProvider.cs @@ -99,7 +99,7 @@ public override async Task GenerateCredentialAsync(GitRequest reque if (!Context.Settings.AllowUnsafeRemotes && StringComparer.OrdinalIgnoreCase.Equals(request.Protocol, "http")) { - throw new Trace2Exception( + throw new Exception( "Unencrypted HTTP is not recommended for GitLab. " + "Ensure the repository remote URL is using HTTPS " + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); diff --git a/src/GitLab/UI/Commands/CredentialsCommand.cs b/src/GitLab/UI/Commands/CredentialsCommand.cs index dafe002d49..2ca2d8dc91 100644 --- a/src/GitLab/UI/Commands/CredentialsCommand.cs +++ b/src/GitLab/UI/Commands/CredentialsCommand.cs @@ -59,7 +59,7 @@ private async Task ExecuteAsync(string url, string userName, bool basic, bo if (!viewModel.WindowResult) { - throw new Trace2Exception("User cancelled dialog."); + throw new Exception("User cancelled dialog."); } var result = new Dictionary(); diff --git a/src/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs b/src/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs index 7ba9f45f20..085873664c 100644 --- a/src/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs +++ b/src/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs @@ -274,7 +274,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_LocSvcReturn context.HttpClientFactory.MessageHandler = httpHandler; var api = new AzureDevOpsRestApi(context); - await Assert.ThrowsAsync(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); + await Assert.ThrowsAsync(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); } [Fact] @@ -305,7 +305,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_IdentSvcRetu context.HttpClientFactory.MessageHandler = httpHandler; var api = new AzureDevOpsRestApi(context); - await Assert.ThrowsAsync(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); + await Assert.ThrowsAsync(() => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); } [Fact] @@ -339,7 +339,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_IdentSvcRetu context.HttpClientFactory.MessageHandler = httpHandler; var api = new AzureDevOpsRestApi(context); - Exception exception = await Assert.ThrowsAsync( + Exception exception = await Assert.ThrowsAsync( () => api.CreatePersonalAccessTokenAsync(orgUri, accessToken, scopes)); Assert.Contains(serverErrorMessage, exception.Message, StringComparison.Ordinal); diff --git a/src/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs b/src/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs index 786e24a655..8084d0daac 100644 --- a/src/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs +++ b/src/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs @@ -137,7 +137,7 @@ public async Task AzureReposProvider_GetCredentialAsync_UnencryptedHttp_ThrowsEx var provider = new AzureReposHostProvider(context, azDevOps, entraAuth, authorityCache, userMgr); - await Assert.ThrowsAsync(() => provider.GetCredentialAsync(request)); + await Assert.ThrowsAsync(() => provider.GetCredentialAsync(request)); } [Fact] diff --git a/src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs b/src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs index f832f2feab..d4b5743248 100644 --- a/src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs +++ b/src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs @@ -142,13 +142,13 @@ public async Task CreatePersonalAccessTokenAsync(Uri organizationUri, st { if (TryGetFirstJsonStringField(responseText, "message", out string errorMessage)) { - throw new Trace2Exception($"Failed to create PAT: {errorMessage}"); + throw new Exception($"Failed to create PAT: {errorMessage}"); } } } } - throw new Trace2Exception("Failed to create PAT"); + throw new Exception("Failed to create PAT"); } #region Private Methods @@ -181,7 +181,7 @@ private async Task GetIdentityServiceUriAsync(Uri organizationUri, string a } } - throw new Trace2Exception("Failed to find location service"); + throw new Exception("Failed to find location service"); } #endregion diff --git a/src/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/Microsoft.AzureRepos/AzureReposHostProvider.cs index 03cc7db5d5..f9ba2f7440 100644 --- a/src/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -248,7 +248,7 @@ private void ThrowIfUnsafeRemote(GitRequest request) if (!_context.Settings.AllowUnsafeRemotes && StringComparer.OrdinalIgnoreCase.Equals(request.Protocol, "http")) { - throw new Trace2Exception( + throw new Exception( "Unencrypted HTTP is not recommended for Azure Repos. " + "Ensure the repository remote URL is using HTTPS " + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); From 128c421ef1372bd7c327472f9c0c6f1ee7e584f1 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:52:15 +0100 Subject: [PATCH 10/18] trace2: own child process events Child start and exit events need a stable correlation identity and timing that survives fast process termination. Give each child wrapper its own Trace2 identifier, classify it when it is created, and report the operating system exit timestamp from the process notification. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core.Tests/TestProcessManager.cs | 9 +-- src/Core/ChildProcess.cs | 61 ++++++++++++------- src/Core/Git.cs | 5 +- src/Core/Gpg.cs | 8 +-- .../Interop/Windows/WindowsProcessManager.cs | 7 ++- src/Core/PlatformUtils.cs | 4 +- src/Core/ProcessManager.cs | 18 ++++-- src/Core/Tracing/Trace2.cs | 28 +++++---- src/Core/WslUtils.cs | 8 ++- src/TestInfrastructure/GitTestUtilities.cs | 2 +- 10 files changed, 90 insertions(+), 60 deletions(-) diff --git a/src/Core.Tests/TestProcessManager.cs b/src/Core.Tests/TestProcessManager.cs index 8f34dafd9d..d85a3e3208 100644 --- a/src/Core.Tests/TestProcessManager.cs +++ b/src/Core.Tests/TestProcessManager.cs @@ -4,7 +4,8 @@ namespace GitCredentialManager.Tests; public class TestProcessManager : IProcessManager { - public ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory) + public ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory, + Trace2ProcessClass @class) { var psi = new ProcessStartInfo(path, args) { @@ -15,11 +16,11 @@ public ChildProcess CreateProcess(string path, string args, bool useShellExecute WorkingDirectory = workingDirectory ?? string.Empty }; - return CreateProcess(psi); + return CreateProcess(psi, @class); } - public ChildProcess CreateProcess(ProcessStartInfo psi) + public ChildProcess CreateProcess(ProcessStartInfo psi, Trace2ProcessClass @class) { - return new ChildProcess(psi); + return new ChildProcess(psi, @class); } } diff --git a/src/Core/ChildProcess.cs b/src/Core/ChildProcess.cs index d5468e028c..4f38fe8c2c 100644 --- a/src/Core/ChildProcess.cs +++ b/src/Core/ChildProcess.cs @@ -1,19 +1,23 @@ using System; using System.Diagnostics; using System.IO; +using System.Threading; namespace GitCredentialManager; public class ChildProcess : DisposableObject { - private DateTimeOffset _startTime; - private DateTimeOffset _exitTime => Process.ExitTime; - private ProcessStartInfo _startInfo => Process.StartInfo; + // Increment with each new child process that is tracked + private static int _nextTrace2Id; - private int _id => Process.Id; + // The child process ID for Trace2 for this instance + private readonly int _trace2Id; + private readonly Trace2ProcessClass _processClass; + private DateTimeOffset _startTime; public ProcessStartInfo StartInfo => Process.StartInfo; public Process Process { get; } + public int Id => Process.Id; public StreamWriter StandardInput => Process.StandardInput; public StreamReader StandardOutput => Process.StandardOutput; public StreamReader StandardError => Process.StandardError; @@ -21,27 +25,32 @@ public class ChildProcess : DisposableObject public static ChildProcess Start(ProcessStartInfo startInfo, Trace2ProcessClass @class = Trace2ProcessClass.None) { - var childProc = new ChildProcess(startInfo); - childProc.Start(@class); + var childProc = new ChildProcess(startInfo, @class); + childProc.Start(); return childProc; } - public ChildProcess(ProcessStartInfo startInfo) + public ChildProcess(ProcessStartInfo startInfo, Trace2ProcessClass @class = Trace2ProcessClass.None) { - Process = new Process() { StartInfo = startInfo }; + _trace2Id = Interlocked.Increment(ref _nextTrace2Id); + _processClass = @class; + Process = new Process + { + StartInfo = startInfo, + EnableRaisingEvents = true + }; Process.Exited += ProcessOnExited; } - public bool Start(Trace2ProcessClass @class = Trace2ProcessClass.None) + public bool Start() { ThrowIfDisposed(); - _startTime = DateTimeOffset.UtcNow; - Trace2.WriteChildStart( - _startTime, - @class, - _startInfo.UseShellExecute, - _startInfo.FileName, - _startInfo.Arguments); + _startTime = Trace2.WriteChildStart( + _trace2Id, + _processClass, + Process.StartInfo.UseShellExecute, + Process.StartInfo.FileName, + Process.StartInfo.Arguments); return Process.Start(); } @@ -53,18 +62,24 @@ protected override void ReleaseManagedResources() { Process.Exited -= ProcessOnExited; Process.Dispose(); - base.ReleaseUnmanagedResources(); + base.ReleaseManagedResources(); } private void ProcessOnExited(object sender, EventArgs e) { - if (sender is Process) + if (sender is Process p) { - double elapsedTime = (_exitTime - _startTime).TotalSeconds; - Trace2.WriteChildExit( - elapsedTime, - _id, - Process.ExitCode); + // This event may have been triggered a while after the process + // actually exited, so we should read the exit time from the + // process object, and not compute the current timestamp inproc. + // Note that we continue to use the start time computed and stored + // inproc and *not* the start time recorded by the process object. + // This is because if the process has already exited and cleaned up + // by the operating system by the time we try and read the start time + // we get an error! + var exitTime = p.ExitTime.ToUniversalTime(); + var relativeTime = exitTime - _startTime; + Trace2.WriteChildExit(_trace2Id, relativeTime, p.Id, p.ExitCode); } } } diff --git a/src/Core/Git.cs b/src/Core/Git.cs index 917dad73a5..37bd25487f 100644 --- a/src/Core/Git.cs +++ b/src/Core/Git.cs @@ -214,7 +214,8 @@ public IEnumerable GetRemotes() public ChildProcess CreateProcess(string args) { - return _processManager.CreateProcess(_gitPath, args, false, _workingDirectory); + return _processManager.CreateProcess( + _gitPath, args, false, _workingDirectory, Trace2ProcessClass.Git); } // This code was originally copied from @@ -234,7 +235,7 @@ public async Task> InvokeHelperAsync(string args, ID UseShellExecute = false }; - var process = _processManager.CreateProcess(procStartInfo); + var process = _processManager.CreateProcess(procStartInfo, Trace2ProcessClass.Git); if (!process.Start()) { var format = "Failed to start Git helper '{0}'"; diff --git a/src/Core/Gpg.cs b/src/Core/Gpg.cs index 358ddd327f..d2812b1dab 100644 --- a/src/Core/Gpg.cs +++ b/src/Core/Gpg.cs @@ -39,9 +39,9 @@ public string DecryptFile(string path) PrepareEnvironment(psi); - using (var gpg = _processManager.CreateProcess(psi)) + using (var gpg = _processManager.CreateProcess(psi, Trace2ProcessClass.Other)) { - if (!gpg.Start(Trace2ProcessClass.Other)) + if (!gpg.Start()) { throw new Exception("Failed to start gpg."); } @@ -73,9 +73,9 @@ public void EncryptFile(string path, string gpgId, string contents) PrepareEnvironment(psi); - using (var gpg = _processManager.CreateProcess(psi)) + using (var gpg = _processManager.CreateProcess(psi, Trace2ProcessClass.Other)) { - if (!gpg.Start(Trace2ProcessClass.Other)) + if (!gpg.Start()) { throw new Exception("Failed to start gpg."); } diff --git a/src/Core/Interop/Windows/WindowsProcessManager.cs b/src/Core/Interop/Windows/WindowsProcessManager.cs index 340943d4d6..0b87fb2d80 100644 --- a/src/Core/Interop/Windows/WindowsProcessManager.cs +++ b/src/Core/Interop/Windows/WindowsProcessManager.cs @@ -10,15 +10,16 @@ public WindowsProcessManager() PlatformUtils.EnsureWindows(); } - public override ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory) + public override ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory, + Trace2ProcessClass @class = Trace2ProcessClass.None) { // If we're asked to start a WSL executable we must launch via the wsl.exe command tool if (!useShellExecute && WslUtils.IsWslPath(path)) { string wslPath = WslUtils.ConvertToDistroPath(path, out string distro); - return WslUtils.CreateWslProcess(distro, $"{wslPath} {args}", workingDirectory); + return WslUtils.CreateWslProcess(distro, $"{wslPath} {args}", workingDirectory, @class); } - return base.CreateProcess(path, args, useShellExecute, workingDirectory); + return base.CreateProcess(path, args, useShellExecute, workingDirectory, @class); } } diff --git a/src/Core/PlatformUtils.cs b/src/Core/PlatformUtils.cs index 6def0c8ab3..cba2c4e3c3 100644 --- a/src/Core/PlatformUtils.cs +++ b/src/Core/PlatformUtils.cs @@ -429,9 +429,9 @@ string GetLinuxDistroVersion() RedirectStandardOutput = true }; - using (var uname = new ChildProcess(psi)) + using (var uname = new ChildProcess(psi, Trace2ProcessClass.Other)) { - uname.Start(Trace2ProcessClass.Other); + uname.Start(); uname.Process.WaitForExit(); if (uname.ExitCode == 0) diff --git a/src/Core/ProcessManager.cs b/src/Core/ProcessManager.cs index a645ba8960..c78a10fdc2 100644 --- a/src/Core/ProcessManager.cs +++ b/src/Core/ProcessManager.cs @@ -14,20 +14,25 @@ public interface IProcessManager /// True to resolve using the OS shell, false to use as an absolute file path. /// /// Working directory for the new process. + /// TRACE2 process class. /// object ready to start. - ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory); + ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory, + Trace2ProcessClass @class = Trace2ProcessClass.None); /// /// Create a process ready to start. /// /// Process start info. + /// TRACE2 process class. /// object ready to start. - ChildProcess CreateProcess(ProcessStartInfo psi); + ChildProcess CreateProcess(ProcessStartInfo psi, Trace2ProcessClass @class = Trace2ProcessClass.None); } public class ProcessManager : IProcessManager { - public virtual ChildProcess CreateProcess(string path, string args, bool useShellExecute, string workingDirectory) + public virtual ChildProcess CreateProcess( + string path, string args, bool useShellExecute, string workingDirectory, + Trace2ProcessClass @class = Trace2ProcessClass.None) { var psi = new ProcessStartInfo(path, args) { @@ -38,11 +43,12 @@ public virtual ChildProcess CreateProcess(string path, string args, bool useShel WorkingDirectory = workingDirectory ?? string.Empty }; - return CreateProcess(psi); + return CreateProcess(psi, @class); } - public virtual ChildProcess CreateProcess(ProcessStartInfo psi) + public virtual ChildProcess CreateProcess( + ProcessStartInfo psi, Trace2ProcessClass @class = Trace2ProcessClass.None) { - return new ChildProcess(psi); + return new ChildProcess(psi, @class); } } diff --git a/src/Core/Tracing/Trace2.cs b/src/Core/Tracing/Trace2.cs index 00771c28b2..2404be119a 100644 --- a/src/Core/Tracing/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -77,8 +77,6 @@ public static class Trace2 private static int _depth; private static bool _initialized; - // Increment with each new child process that is tracked - private static int _childProcCounter; public static void Initialize( string[] args, @@ -159,8 +157,8 @@ public static void Stop( DisposeWriters(); } - public static void WriteChildStart( - DateTimeOffset startTime, + internal static DateTimeOffset WriteChildStart( + int childId, Trace2ProcessClass processClass, bool useShell, string appName, @@ -168,13 +166,15 @@ public static void WriteChildStart( [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { + var startTime = DateTimeOffset.UtcNow; + // Some child processes are started before TRACE2 can be initialized. // Since certain dependencies are not available until initialization, // we must immediately return if this method is invoked prior to // initialization. if (!_initialized) { - return; + return startTime; } // Always add name of the application the process is executing @@ -197,32 +197,36 @@ public static void WriteChildStart( Thread = BuildThreadName(), File = Path.GetFileName(filePath), Line = lineNumber, - Id = ++_childProcCounter, + Id = childId, Classification = processClass, UseShell = useShell, Argv = procArgs, ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds, Depth = _depth, }); + return startTime; } - public static void WriteChildExit( + internal static void WriteChildExit( + int childId, DateTimeOffset startTime, int pid, int code, [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) => - WriteChildExit(DateTimeOffset.UtcNow - startTime, pid, code, filePath, lineNumber); + WriteChildExit(childId, DateTimeOffset.UtcNow - startTime, pid, code, filePath, lineNumber); - public static void WriteChildExit( + internal static void WriteChildExit( + int childId, TimeSpan relativeTime, int pid, int code, [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) => - WriteChildExit(relativeTime.TotalSeconds, pid, code, filePath, lineNumber); + WriteChildExit(childId, relativeTime.TotalSeconds, pid, code, filePath, lineNumber); - public static void WriteChildExit( + internal static void WriteChildExit( + int childId, double relativeTime, int pid, int code, @@ -246,7 +250,7 @@ public static void WriteChildExit( Thread = BuildThreadName(), File = Path.GetFileName(filePath), Line = lineNumber, - Id = _childProcCounter, + Id = childId, Pid = pid, Code = code, ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds, diff --git a/src/Core/WslUtils.cs b/src/Core/WslUtils.cs index 98bb596227..a86b22ddbc 100644 --- a/src/Core/WslUtils.cs +++ b/src/Core/WslUtils.cs @@ -110,10 +110,12 @@ public static bool IsWslPath(string path) /// WSL distribution name. /// Command to execute. /// Optional working directory. - /// object ready to start. + /// Process class for tracing purposes. + /// object ready to start. public static ChildProcess CreateWslProcess(string distribution, string command, - string workingDirectory = null) + string workingDirectory = null, + Trace2ProcessClass @class = Trace2ProcessClass.None) { var args = new StringBuilder(); args.AppendFormat("--distribution {0} ", distribution); @@ -130,7 +132,7 @@ public static ChildProcess CreateWslProcess(string distribution, WorkingDirectory = workingDirectory ?? string.Empty }; - return new ChildProcess(psi); + return new ChildProcess(psi, @class); } /// diff --git a/src/TestInfrastructure/GitTestUtilities.cs b/src/TestInfrastructure/GitTestUtilities.cs index 011cbac26c..bd31efa840 100644 --- a/src/TestInfrastructure/GitTestUtilities.cs +++ b/src/TestInfrastructure/GitTestUtilities.cs @@ -27,7 +27,7 @@ public static string GetGitPath() psi.RedirectStandardOutput = true; - using (var which = new ChildProcess(psi)) + using (var which = new ChildProcess(psi, Trace2ProcessClass.None)) { which.Start(); which.WaitForExit(); From 09642bfd306ed4cdfaa4489b755963a4498cc30f Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:53:43 +0100 Subject: [PATCH 11/18] trace2: track logical thread context Physical managed threads do not describe asynchronous work once execution crosses awaits or dispatcher boundaries. Flow an explicit Trace2 context with the execution context so thread identities, nested regions, and relative timing remain coherent, while allowing callers to restore the main context when needed. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core.Tests/Trace2MessageTests.cs | 32 ++ src/Core/Tracing/Trace2.cs | 579 +++++++++++++++++++++------ src/Core/Tracing/Trace2Message.cs | 29 ++ 3 files changed, 516 insertions(+), 124 deletions(-) diff --git a/src/Core.Tests/Trace2MessageTests.cs b/src/Core.Tests/Trace2MessageTests.cs index 4b833d134b..05536084ee 100644 --- a/src/Core.Tests/Trace2MessageTests.cs +++ b/src/Core.Tests/Trace2MessageTests.cs @@ -84,4 +84,36 @@ public void Event_Message_With_Snake_Case_ToJson_Creates_Expected_Json() Assert.Equal(expected, actual); } + + [Fact] + public void Thread_Events_ToJson_Create_Expected_Json() + { + var startMessage = new ThreadStartMessage + { + Sid = "123", + Thread = "AppMain", + Time = new DateTimeOffset(), + File = "foo.cs", + Line = 1, + Depth = 1 + }; + var exitMessage = new ThreadExitMessage + { + Sid = "123", + Thread = "AppMain", + Time = new DateTimeOffset(), + File = "foo.cs", + Line = 2, + Depth = 1, + RelativeTime = 0.05 + }; + + Assert.Equal( + "{\"event\":\"thread_start\",\"sid\":\"123\",\"thread\":\"AppMain\",\"time\":\"0001-01-01T00:00:00+00:00\",\"file\":\"foo.cs\",\"line\":1,\"depth\":1}", + startMessage.ToJson()); + Assert.Equal( + "{\"event\":\"thread_exit\",\"sid\":\"123\",\"thread\":\"AppMain\",\"time\":\"0001-01-01T00:00:00+00:00\",\"file\":\"foo.cs\",\"line\":2,\"depth\":1,\"t_rel\":0.05}", + exitMessage.ToJson()); + } + } diff --git a/src/Core/Tracing/Trace2.cs b/src/Core/Tracing/Trace2.cs index 2404be119a..083cf77cac 100644 --- a/src/Core/Tracing/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -3,6 +3,9 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Security.Cryptography; using System.Text; using System.Threading; using GitCredentialManager.Interop; @@ -19,65 +22,38 @@ internal class Trace2Settings new Dictionary(); } -internal class RegionScope : DisposableObject -{ - private readonly string _category; - private readonly string _label; - private readonly string _filePath; - private readonly int _lineNumber; - private readonly string _message; - private readonly string _thread; - private readonly int _nesting; - private readonly DateTimeOffset _startTime; - - internal RegionScope( - string category, - string label, - string filePath, - int lineNumber, - string message, - string thread, - int nesting) - { - _category = category; - _label = label; - _filePath = filePath; - _lineNumber = lineNumber; - _message = message; - _thread = thread; - _nesting = nesting; - - _startTime = DateTimeOffset.UtcNow; - - Trace2.WriteRegionEnter(_category, _label, _message, _thread, _nesting, _filePath, _lineNumber); - } - - protected override void ReleaseManagedResources() - { - double relativeTime = (DateTimeOffset.UtcNow - _startTime).TotalSeconds; - Trace2.WriteRegionLeave( - relativeTime, _category, _label, _message, _thread, _nesting, _filePath, _lineNumber); - Trace2.CompleteRegion(_nesting); - } -} - /// -/// The application's process-wide TRACE2 tracing system. +/// The application's process-wide Trace2 tracing system. /// public static class Trace2 { internal const string SidEnvar = "GIT_TRACE2_PARENT_SID"; - private static readonly object WritersLock = new object(); - private static readonly List Writers = new List(); - private static readonly AsyncLocal RegionNesting = new AsyncLocal(); + private const string MainThreadName = "main"; + + private static readonly Lock WritersLock = new(); + private static readonly List Writers = new(); + private static readonly AsyncLocal ThreadContext = new(); + private static bool _initialized; private static DateTimeOffset _applicationStartTime; private static Trace2Settings _settings; + private static Trace2ExecutionContext _mainContext; private static string _sid; private static int _depth; - private static bool _initialized; + // Increment for each new logical thread created + private static int _nextThreadId; + /// + /// Initializes the process-wide Trace2 session and enabled output targets. + /// + /// The command-line arguments passed to the application. + /// The source file initializing Trace2. + /// The source line initializing Trace2. + /// + /// This method emits the version and start events. Subsequent + /// calls have no effect. + /// public static void Initialize( string[] args, [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", @@ -85,22 +61,32 @@ public static void Initialize( { if (_initialized) { + // Already initialised! return; } _applicationStartTime = DateTimeOffset.UtcNow; _sid = CreateSid(); Environment.SetEnvironmentVariable(SidEnvar, _sid); + _depth = GetProcessDepth(_sid); _settings = ReadSettings(); InitializeWriters(); + + // The main thread context is ambiently created with the process and Trace2 init + _mainContext = new Trace2ExecutionContext(MainThreadName, _applicationStartTime); + ThreadContext.Value = _mainContext; + _initialized = true; string appPath = Environment.ProcessPath ?? Environment.GetCommandLineArgs()[0]; Start(appPath, args, filePath, lineNumber); } + /// + /// Create the Trace2 "session id" (sid) for this process. + /// internal static string CreateSid() { // Use trim to ensure no accidental leading or trailing slashes @@ -133,6 +119,30 @@ internal static int GetProcessDepth(string sid) return count; } + private static void SetContext(Trace2ExecutionContext context) + { + ThreadContext.Value = context; + } + + private static Trace2ExecutionContext GetCurrentContext() + { + Trace2ExecutionContext context = ThreadContext.Value; + Debug.Assert(context is not null, "Trace2 event emitted without an execution context."); + // Fall back to the main thread context if we are missing one. + // This can happen when ExecutionContext flow is suppressed, an unsafe + // ThreadPool API is used, or work runs on a manually created thread without + // creating a new Trace2 thread scope. + return context ?? _mainContext; + } + + public static IDisposable UseMainContext() + { + if (!_initialized) + return NoOpDisposable.Instance; + + return new ContextScope(_mainContext); + } + private static void Start(string appPath, string[] args, string filePath, @@ -148,15 +158,45 @@ private static void Start(string appPath, WriteStart(appPath, args, filePath, lineNumber); } + /// + /// Stops the process-wide Trace2 session. + /// + /// The application exit code. + /// The source file stopping TRACE2. + /// The source line stopping TRACE2. + /// + /// This method emits the exit event and disposes all enabled output + /// targets. It has no effect if Trace2 has not been initialized. + /// public static void Stop( int exitCode, [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { + if (!_initialized) return; + WriteExit(exitCode, filePath, lineNumber); DisposeWriters(); } + /// + /// Writes an event immediately before starting a child process. + /// + /// + /// The process-local identifier used to correlate the child start and exit + /// events. + /// + /// The classification of the child process. + /// + /// Whether the child process is started through a command shell. + /// + /// The child executable name or path. + /// The child process argument string. + /// The source file starting the child process. + /// The source line starting the child process. + /// + /// The event timestamp to pass to . + /// internal static DateTimeOffset WriteChildStart( int childId, Trace2ProcessClass processClass, @@ -168,17 +208,11 @@ internal static DateTimeOffset WriteChildStart( { var startTime = DateTimeOffset.UtcNow; - // Some child processes are started before TRACE2 can be initialized. - // Since certain dependencies are not available until initialization, - // we must immediately return if this method is invoked prior to - // initialization. if (!_initialized) - { return startTime; - } // Always add name of the application the process is executing - var procArgs = new List() + var procArgs = new List { Path.GetFileName(appName) }; @@ -189,12 +223,11 @@ internal static DateTimeOffset WriteChildStart( procArgs.AddRange(argv.Split(' ')); } - WriteMessage(new ChildStartMessage() + WriteMessage(new ChildStartMessage { - Event = Trace2Event.ChildStart, Sid = _sid, Time = startTime, - Thread = BuildThreadName(), + Thread = GetCurrentContext().ThreadName, File = Path.GetFileName(filePath), Line = lineNumber, Id = childId, @@ -207,6 +240,17 @@ internal static DateTimeOffset WriteChildStart( return startTime; } + /// + /// Writes an event after a child process exits. + /// + /// + /// The process-local identifier from the corresponding child start event. + /// + /// The timestamp returned by . + /// The operating-system process identifier of the child. + /// The child process exit code. + /// The source file observing the child exit. + /// The source line observing the child exit. internal static void WriteChildExit( int childId, DateTimeOffset startTime, @@ -216,6 +260,17 @@ internal static void WriteChildExit( [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) => WriteChildExit(childId, DateTimeOffset.UtcNow - startTime, pid, code, filePath, lineNumber); + /// + /// Writes an event after a child process exits. + /// + /// + /// The process-local identifier from the corresponding child start event. + /// + /// The elapsed time between child start and exit. + /// The operating-system process identifier of the child. + /// The child process exit code. + /// The source file observing the child exit. + /// The source line observing the child exit. internal static void WriteChildExit( int childId, TimeSpan relativeTime, @@ -225,6 +280,19 @@ internal static void WriteChildExit( [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) => WriteChildExit(childId, relativeTime.TotalSeconds, pid, code, filePath, lineNumber); + /// + /// Writes an event after a child process exits. + /// + /// + /// The process-local identifier from the corresponding child start event. + /// + /// + /// The elapsed time between child start and exit, in seconds. + /// + /// The operating-system process identifier of the child. + /// The child process exit code. + /// The source file observing the child exit. + /// The source line observing the child exit. internal static void WriteChildExit( int childId, double relativeTime, @@ -233,21 +301,13 @@ internal static void WriteChildExit( [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { - // Some child processes are started before TRACE2 can be initialized. - // Since certain dependencies are not available until initialization, - // we must immediately return if this method is invoked prior to - // initialization. - if (!_initialized) - { - return; - } + if (!_initialized) return; - WriteMessage(new ChildExitMessage() + WriteMessage(new ChildExitMessage { - Event = Trace2Event.ChildExit, Sid = _sid, Time = DateTimeOffset.UtcNow, - Thread = BuildThreadName(), + Thread = GetCurrentContext().ThreadName, File = Path.GetFileName(filePath), Line = lineNumber, Id = childId, @@ -259,27 +319,29 @@ internal static void WriteChildExit( }); } + /// + /// Writes a TRACE2 error event. + /// + /// The fully formatted error message. + /// + /// The parameterized message format, or to use + /// . + /// + /// The source file reporting the error. + /// The source line reporting the error. public static void WriteError( string errorMessage, string parameterizedMessage = null, [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { - // It is possible for an error to be thrown before TRACE2 can be initialized. - // Since certain dependencies are not available until initialization, - // we must immediately return if this method is invoked prior to - // initialization. - if (!_initialized) - { - return; - } + if (!_initialized) return; - WriteMessage(new ErrorMessage() + WriteMessage(new ErrorMessage { - Event = Trace2Event.Error, Sid = _sid, Time = DateTimeOffset.UtcNow, - Thread = BuildThreadName(), + Thread = GetCurrentContext().ThreadName, File = Path.GetFileName(filePath), Line = lineNumber, Message = errorMessage, @@ -322,6 +384,115 @@ internal static void WriteError( } } + /// + /// Starts a logical Trace2 thread scope. + /// + /// The descriptive name of the logical thread. + /// The source file starting the scope. + /// The source line starting the scope. + /// + /// A scope that emits thread_exit and restores the previous logical + /// thread context when disposed. + /// + /// + /// The scope emits thread_start when started. Its context flows + /// through normal execution-context transitions, including + /// and . + /// Nested scopes must be disposed in LIFO order. Independently concurrent + /// operations should start their scopes within their respective execution + /// contexts. + /// + public static IDisposable StartThread( + string name, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + { + if (!_initialized) + return NoOpDisposable.Instance; + + // Create new logical thread execution context + int id = Interlocked.Increment(ref _nextThreadId); + string fullName = CreateThreadName(id, name); + + return new ThreadScope(fullName, filePath, lineNumber); + } + + private static DateTimeOffset WriteThreadStart( + string name, + string filePath = "", + int lineNumber = 0) + { + var startTime = DateTimeOffset.UtcNow; + + if (_initialized) + { + WriteMessage(new ThreadStartMessage + { + Sid = _sid, + Time = startTime, + Thread = name, + File = Path.GetFileName(filePath), + Line = lineNumber, + Depth = _depth + }); + } + + return startTime; + } + + private static void WriteThreadExit( + string name, + DateTimeOffset startTime, + string filePath, + int lineNumber) => + WriteThreadExit(name, DateTimeOffset.UtcNow - startTime, filePath, lineNumber); + + private static void WriteThreadExit( + string name, + TimeSpan relativeTime, + string filePath, + int lineNumber) => + WriteThreadExit(name, relativeTime.TotalSeconds, filePath, lineNumber); + + private static void WriteThreadExit( + string name, + double relativeTime, + string filePath, + int lineNumber) + { + if (!_initialized) return; + + WriteMessage(new ThreadExitMessage + { + Sid = _sid, + Time = DateTimeOffset.UtcNow, + Thread = name, + File = Path.GetFileName(filePath), + Line = lineNumber, + RelativeTime = relativeTime, + Depth = _depth + }); + } + + /// + /// Creates a nested Trace2 region on the current logical thread. + /// + /// The broad category of work represented by the region. + /// The name of the operation represented by the region. + /// + /// An optional event message. When omitted, is used. + /// + /// The source file creating the region. + /// The source line creating the region. + /// + /// A scope that emits region_leave and restores the previous nesting + /// level when disposed. + /// + /// + /// The scope emits region_enter when created. Region nesting is + /// maintained per logical thread and flows across normal asynchronous + /// continuations. Regions must be disposed in LIFO order. + /// public static IDisposable StartRegion( string category, string label, @@ -329,12 +500,14 @@ public static IDisposable StartRegion( [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) { - int nesting = RegionNesting.Value + 1; - RegionNesting.Value = nesting; - return new RegionScope(category, label, filePath, lineNumber, message, BuildThreadName(), nesting); + if (!_initialized) + return NoOpDisposable.Instance; + + Trace2ExecutionContext context = GetCurrentContext(); + return new RegionScope(context, category, label, filePath, lineNumber, message); } - internal static void WriteRegionEnter( + private static DateTimeOffset WriteRegionEnter( string category, string label, string message, @@ -343,24 +516,52 @@ internal static void WriteRegionEnter( string filePath, int lineNumber) { - WriteMessage(new RegionEnterMessage() + var start = DateTimeOffset.UtcNow; + + if (_initialized) { - Event = Trace2Event.RegionEnter, - Sid = _sid, - Time = DateTimeOffset.UtcNow, - Category = category, - Label = label, - Message = message == "" ? label : message, - Thread = thread, - File = Path.GetFileName(filePath), - Line = lineNumber, - ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds, - Nesting = nesting, - Depth = _depth - }); + WriteMessage(new RegionEnterMessage + { + Sid = _sid, + Time = start, + Category = category, + Label = label, + Message = message == "" ? label : message, + Thread = thread, + File = Path.GetFileName(filePath), + Line = lineNumber, + ElapsedTime = (start - _applicationStartTime).TotalSeconds, + Nesting = nesting, + Depth = _depth + }); + } + + return start; } - internal static void WriteRegionLeave( + private static void WriteRegionLeave( + DateTimeOffset startTime, + string category, + string label, + string message, + string thread, + int nesting, + string filePath, + int lineNumber) => + WriteRegionLeave(DateTimeOffset.UtcNow - startTime, category, label, message, thread, nesting, filePath, lineNumber); + + private static void WriteRegionLeave( + TimeSpan relativeTime, + string category, + string label, + string message, + string thread, + int nesting, + string filePath, + int lineNumber) => + WriteRegionLeave(relativeTime.TotalSeconds, category, label, message, thread, nesting, filePath, lineNumber); + + private static void WriteRegionLeave( double relativeTime, string category, string label, @@ -370,9 +571,10 @@ internal static void WriteRegionLeave( string filePath, int lineNumber) { - WriteMessage(new RegionLeaveMessage() + if (!_initialized) return; + + WriteMessage(new RegionLeaveMessage { - Event = Trace2Event.RegionLeave, Sid = _sid, Time = DateTimeOffset.UtcNow, Category = category, @@ -388,11 +590,6 @@ internal static void WriteRegionLeave( }); } - internal static void CompleteRegion(int nesting) - { - RegionNesting.Value = Math.Max(0, nesting - 1); - } - private static void DisposeWriters() { lock (WritersLock) @@ -560,12 +757,12 @@ private static void WriteVersion( { EnsureArgument.NotNull(gcmVersion, nameof(gcmVersion)); - WriteMessage(new VersionMessage() + WriteMessage(new VersionMessage { Event = Trace2Event.Version, Sid = _sid, Time = DateTimeOffset.UtcNow, - Thread = BuildThreadName(), + Thread = GetCurrentContext().ThreadName, File = Path.GetFileName(filePath), Line = lineNumber, Evt = eventFormatVersion, @@ -580,7 +777,7 @@ private static void WriteStart( int lineNumber) { // Prepend GCM exe to arguments - var argv = new List() + var argv = new List { Path.GetFileName(appPath), }; @@ -590,12 +787,12 @@ private static void WriteStart( argv.AddRange(args); } - WriteMessage(new StartMessage() + WriteMessage(new StartMessage { Event = Trace2Event.Start, Sid = _sid, Time = DateTimeOffset.UtcNow, - Thread = BuildThreadName(), + Thread = GetCurrentContext().ThreadName, File = Path.GetFileName(filePath), Line = lineNumber, Argv = argv, @@ -607,12 +804,12 @@ private static void WriteExit(int code, string filePath = "", int lineNumber = 0 { EnsureArgument.NotNull(code, nameof(code)); - WriteMessage(new ExitMessage() + WriteMessage(new ExitMessage { Event = Trace2Event.Exit, Sid = _sid, Time = DateTimeOffset.UtcNow, - Thread = BuildThreadName(), + Thread = GetCurrentContext().ThreadName, File = Path.GetFileName(filePath), Line = lineNumber, Code = code, @@ -656,27 +853,161 @@ private static void WriteMessage(Trace2Message message) } } - private static string BuildThreadName() + private static string CreateThreadName(int id, string name) { - // If this is the entry thread, call it "main", per Trace2 convention - if (Thread.CurrentThread.ManagedThreadId == 1) + // If we don't have a name for this thread then give it a generic name + if (string.IsNullOrEmpty(name)) { - return "main"; + name = "unknown"; } - // If this is a thread pool thread, name it as such - if (Thread.CurrentThread.IsThreadPoolThread) + // Threads should be named "th%d:%s" per Trace2 convention, + // where %d is the ID and %s is the thread name. + return $"th{id}:{name}"; + } + + private class NoOpDisposable : IDisposable + { + public static readonly IDisposable Instance = new NoOpDisposable(); + public void Dispose(){} + } + + private class Trace2ExecutionContext( + string threadName, + DateTimeOffset? startTime = null) + { + public AsyncLocal RegionNesting { get; } = new(); + public AsyncLocal RegionStartTime { get; } = new(); + public DateTimeOffset StartTime { get; } = startTime ?? DateTimeOffset.UtcNow; + public string ThreadName { get; } = threadName; + } + + private class ContextScope : DisposableObject + { + private readonly Trace2ExecutionContext _context; + private readonly Trace2ExecutionContext _previousContext; + + public ContextScope(Trace2ExecutionContext context) { - return $"thread_pool_{Environment.CurrentManagedThreadId}"; + _context = context; + _previousContext = Trace2.GetCurrentContext(); + Trace2.SetContext(_context); } - // Otherwise, if the thread is named, use it! - if (!string.IsNullOrEmpty(Thread.CurrentThread.Name)) + protected override void ReleaseManagedResources() { - return Thread.CurrentThread.Name; + Debug.Assert( + ReferenceEquals(Trace2.GetCurrentContext(), _context), + "Trace2 contexts must be disposed in LIFO order."); + + Trace2.SetContext(_previousContext); } + } - // We don't know what this thread is! - return string.Empty; + private class ThreadScope : DisposableObject + { + private readonly string _filePath; + private readonly int _lineNumber; + private readonly Trace2ExecutionContext _context; + private readonly Trace2ExecutionContext _prevContext; + private readonly DateTimeOffset _startTime; + + public ThreadScope(string threadName, string filePath, int lineNumber) + { + _prevContext = Trace2.GetCurrentContext(); + _filePath = filePath; + _lineNumber = lineNumber; + + _context = new Trace2ExecutionContext(threadName); + Trace2.SetContext(_context); + + _startTime = Trace2.WriteThreadStart(_context.ThreadName, _filePath, _lineNumber); + } + + protected override void ReleaseManagedResources() + { + try + { + Trace2.WriteThreadExit(_context.ThreadName, _startTime, _filePath, _lineNumber); + } + finally + { + Debug.Assert( + ReferenceEquals(Trace2.GetCurrentContext(), _context), + "Trace2 threads must be disposed in LIFO order."); + + Trace2.SetContext(_prevContext); + } + } + } + + private class RegionScope : DisposableObject + { + private readonly Trace2ExecutionContext _context; + private readonly string _category; + private readonly string _label; + private readonly string _filePath; + private readonly int _lineNumber; + private readonly string _message; + private readonly int _nesting; + private readonly DateTimeOffset? _previousRegionStartTime; + private readonly DateTimeOffset _startTime; + + internal RegionScope( + Trace2ExecutionContext context, + string category, + string label, + string filePath, + int lineNumber, + string message) + { + _context = context; + _category = category; + _label = label; + _filePath = filePath; + _lineNumber = lineNumber; + _message = message; + + // Increment nesting level as we enter the region + _nesting = _context.RegionNesting.Value + 1; + _context.RegionNesting.Value = _nesting; + _previousRegionStartTime = _context.RegionStartTime.Value; + + _startTime = Trace2.WriteRegionEnter( + _category, + _label, + _message, + _context.ThreadName, + _nesting, + _filePath, + _lineNumber); + _context.RegionStartTime.Value = _startTime; + } + + protected override void ReleaseManagedResources() + { + try + { + Trace2.WriteRegionLeave( + _startTime, + _category, + _label, + _message, + _context.ThreadName, + _nesting, + _filePath, + _lineNumber); + } + finally + { + Debug.Assert( + _context.RegionNesting.Value == _nesting, + "Trace2 regions must be disposed in LIFO order."); + + // Decrement the nesting level + _context.RegionNesting.Value = Math.Max(0, _nesting - 1); + _context.RegionStartTime.Value = _previousRegionStartTime; + } + } } } diff --git a/src/Core/Tracing/Trace2Message.cs b/src/Core/Tracing/Trace2Message.cs index c70ccb594e..67acafe0ba 100644 --- a/src/Core/Tracing/Trace2Message.cs +++ b/src/Core/Tracing/Trace2Message.cs @@ -28,6 +28,10 @@ public enum Trace2Event RegionEnter, [JsonStringEnumMemberName("region_leave")] RegionLeave, + [JsonStringEnumMemberName("thread_start")] + ThreadStart, + [JsonStringEnumMemberName("thread_exit")] + ThreadExit, } [JsonSerializable(typeof(VersionMessage))] @@ -35,6 +39,8 @@ public enum Trace2Event [JsonSerializable(typeof(ExitMessage))] [JsonSerializable(typeof(ChildStartMessage))] [JsonSerializable(typeof(ChildExitMessage))] +[JsonSerializable(typeof(ThreadStartMessage))] +[JsonSerializable(typeof(ThreadExitMessage))] [JsonSerializable(typeof(ErrorMessage))] [JsonSerializable(typeof(RegionEnterMessage))] [JsonSerializable(typeof(RegionLeaveMessage))] @@ -368,6 +374,29 @@ protected override string GetEventMessage(Trace2FormatTarget formatTarget) } } +public class ThreadStartMessage() : Trace2Message(Trace2Event.ThreadStart) +{ + protected override JsonTypeInfo GetJsonTypeInfo() => Trace2JsonContext.Default.ThreadStartMessage; + + protected override string GetEventMessage(Trace2FormatTarget formatTarget) => Thread; +} + +public class ThreadExitMessage() : Trace2Message(Trace2Event.ThreadExit) +{ + [JsonPropertyName("t_rel")] + [JsonPropertyOrder(8)] + public double RelativeTime { get; set; } + + protected override JsonTypeInfo GetJsonTypeInfo() => Trace2JsonContext.Default.ThreadExitMessage; + + private protected override PerformanceFormatFields GetPerformanceFields() => new() + { + RelativeTime = RelativeTime + }; + + protected override string GetEventMessage(Trace2FormatTarget formatTarget) => $"elapsed:{RelativeTime}"; +} + public class ErrorMessage() : Trace2Message(Trace2Event.Error) { [JsonPropertyName("msg")] From 8cb2dcc8ca08cb202cccf1010248d0eb38176513 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 12:44:34 +0100 Subject: [PATCH 12/18] trace2: import caller info attributes Repeated fully qualified caller-info attribute names obscure the Trace2 API signatures. Import the namespace to keep those signatures readable without changing behavior. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core/Tracing/Trace2.cs | 41 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Core/Tracing/Trace2.cs b/src/Core/Tracing/Trace2.cs index 083cf77cac..b38923761f 100644 --- a/src/Core/Tracing/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -5,6 +5,7 @@ using System.IO; using System.IO.Pipes; using System.Linq; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Threading; @@ -56,8 +57,8 @@ public static class Trace2 /// public static void Initialize( string[] args, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) { if (_initialized) { @@ -170,8 +171,8 @@ private static void Start(string appPath, /// public static void Stop( int exitCode, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) { if (!_initialized) return; @@ -203,8 +204,8 @@ internal static DateTimeOffset WriteChildStart( bool useShell, string appName, string argv, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) { var startTime = DateTimeOffset.UtcNow; @@ -256,8 +257,8 @@ internal static void WriteChildExit( DateTimeOffset startTime, int pid, int code, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) => + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) => WriteChildExit(childId, DateTimeOffset.UtcNow - startTime, pid, code, filePath, lineNumber); /// @@ -276,8 +277,8 @@ internal static void WriteChildExit( TimeSpan relativeTime, int pid, int code, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) => + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) => WriteChildExit(childId, relativeTime.TotalSeconds, pid, code, filePath, lineNumber); /// @@ -298,8 +299,8 @@ internal static void WriteChildExit( double relativeTime, int pid, int code, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) { if (!_initialized) return; @@ -332,8 +333,8 @@ internal static void WriteChildExit( public static void WriteError( string errorMessage, string parameterizedMessage = null, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) { if (!_initialized) return; @@ -352,8 +353,8 @@ public static void WriteError( internal static void WriteError( Exception exception, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) { EnsureArgument.NotNull(exception, nameof(exception)); @@ -404,8 +405,8 @@ internal static void WriteError( /// public static IDisposable StartThread( string name, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) { if (!_initialized) return NoOpDisposable.Instance; @@ -497,8 +498,8 @@ public static IDisposable StartRegion( string category, string label, string message = "", - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0) + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) { if (!_initialized) return NoOpDisposable.Instance; From 2ea5d32d7fa626c16824f22f903fedd20ba49314 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:54:47 +0100 Subject: [PATCH 13/18] trace2: emit command hierarchy A process start records argv but does not identify which command parser branch ultimately won. Emit the canonical command name after parsing and carry an inherited hierarchy through child processes so nested helper activity can be related to its originating command. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- docs/development.md | 1 + src/Core.Tests/Trace2MessageTests.cs | 20 ++++++++++++++ src/Core/Application.cs | 10 +++++++ src/Core/Tracing/Trace2.cs | 40 ++++++++++++++++++++++++++++ src/Core/Tracing/Trace2Message.cs | 22 +++++++++++++++ 5 files changed, 93 insertions(+) diff --git a/docs/development.md b/docs/development.md index 1c36eff4e6..eda2858dbe 100644 --- a/docs/development.md +++ b/docs/development.md @@ -261,6 +261,7 @@ method 0. `exit`: contains current executable's exit code 0. `child_start`: describes a child process that is about to be spawned 0. `child_exit`: describes a child process at exit +0. `cmd_name`: identifies the canonical command and inherited command hierarchy 0. `region_enter`: describes a region (e.g. a timer for a section of code that is interesting) on entry 0. `region_leave`: describes a region on leaving diff --git a/src/Core.Tests/Trace2MessageTests.cs b/src/Core.Tests/Trace2MessageTests.cs index 05536084ee..ccbff8300e 100644 --- a/src/Core.Tests/Trace2MessageTests.cs +++ b/src/Core.Tests/Trace2MessageTests.cs @@ -116,4 +116,24 @@ public void Thread_Events_ToJson_Create_Expected_Json() exitMessage.ToJson()); } + [Fact] + public void CommandName_Event_ToJson_Creates_Expected_Json() + { + var message = new CommandNameMessage + { + Sid = "123", + Thread = "main", + Time = new DateTimeOffset(), + File = "foo.cs", + Line = 1, + Depth = 1, + Name = "get", + Hierarchy = "git/get" + }; + + const string expected = "{\"event\":\"cmd_name\",\"sid\":\"123\",\"thread\":\"main\",\"time\":\"0001-01-01T00:00:00+00:00\",\"file\":\"foo.cs\",\"line\":1,\"depth\":1,\"name\":\"get\",\"hierarchy\":\"git/get\"}"; + + Assert.Equal(expected, message.ToJson()); + } + } diff --git a/src/Core/Application.cs b/src/Core/Application.cs index a6ce064414..977213d724 100644 --- a/src/Core/Application.cs +++ b/src/Core/Application.cs @@ -83,6 +83,15 @@ void NoGuiOptionHandler(InvocationContext context) } } + void Trace2CommandNameHandler(InvocationContext context) + { + Command command = context.ParseResult.CommandResult.Command; + if (!ReferenceEquals(command, rootCommand)) + { + Trace2.WriteCommandName(command.Name); + } + } + // Add standard commands rootCommand.AddCommand(new GetCommand(Context, _providerRegistry)); rootCommand.AddCommand(new StoreCommand(Context, _providerRegistry)); @@ -118,6 +127,7 @@ void NoGuiOptionHandler(InvocationContext context) .UseDefaults() .UseExceptionHandler(OnException) .AddMiddleware(NoGuiOptionHandler) + .AddMiddleware(Trace2CommandNameHandler) .Build(); return await parser.InvokeAsync(args); diff --git a/src/Core/Tracing/Trace2.cs b/src/Core/Tracing/Trace2.cs index b38923761f..c6a2fbce8e 100644 --- a/src/Core/Tracing/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -29,6 +29,7 @@ internal class Trace2Settings public static class Trace2 { internal const string SidEnvar = "GIT_TRACE2_PARENT_SID"; + internal const string ParentNameEnvar = "GIT_TRACE2_PARENT_NAME"; private const string MainThreadName = "main"; private static readonly Lock WritersLock = new(); @@ -180,6 +181,45 @@ public static void Stop( DisposeWriters(); } + /// + /// Writes the canonical name of the command being run. + /// + /// The canonical command name. + /// The source file writing the event. + /// The source line writing the event. + /// + /// The command hierarchy is inherited from the parent process and extended + /// for child processes through GIT_TRACE2_PARENT_NAME. + /// + public static void WriteCommandName( + string name, + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) + { + if (!_initialized) return; + + EnsureArgument.NotNullOrWhiteSpace(name, nameof(name)); + + string parentName = Environment.GetEnvironmentVariable(ParentNameEnvar); + string hierarchy = string.IsNullOrEmpty(parentName) + ? name + : $"{parentName}/{name}"; + + Environment.SetEnvironmentVariable(ParentNameEnvar, hierarchy); + + WriteMessage(new CommandNameMessage + { + Sid = _sid, + Time = DateTimeOffset.UtcNow, + Thread = GetCurrentContext().ThreadName, + File = Path.GetFileName(filePath), + Line = lineNumber, + Name = name, + Hierarchy = hierarchy, + Depth = _depth + }); + } + /// /// Writes an event immediately before starting a child process. /// diff --git a/src/Core/Tracing/Trace2Message.cs b/src/Core/Tracing/Trace2Message.cs index 67acafe0ba..4f131d9759 100644 --- a/src/Core/Tracing/Trace2Message.cs +++ b/src/Core/Tracing/Trace2Message.cs @@ -32,6 +32,8 @@ public enum Trace2Event ThreadStart, [JsonStringEnumMemberName("thread_exit")] ThreadExit, + [JsonStringEnumMemberName("cmd_name")] + CommandName, } [JsonSerializable(typeof(VersionMessage))] @@ -44,6 +46,7 @@ public enum Trace2Event [JsonSerializable(typeof(ErrorMessage))] [JsonSerializable(typeof(RegionEnterMessage))] [JsonSerializable(typeof(RegionLeaveMessage))] +[JsonSerializable(typeof(CommandNameMessage))] [JsonSourceGenerationOptions( UseStringEnumConverter = true, PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, @@ -374,6 +377,25 @@ protected override string GetEventMessage(Trace2FormatTarget formatTarget) } } +public class CommandNameMessage() : Trace2Message(Trace2Event.CommandName) +{ + [JsonPropertyName("name")] + [JsonPropertyOrder(8)] + public string Name { get; set; } + + [JsonPropertyName("hierarchy")] + [JsonPropertyOrder(9)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Hierarchy { get; set; } + + protected override JsonTypeInfo GetJsonTypeInfo() => Trace2JsonContext.Default.CommandNameMessage; + + protected override string GetEventMessage(Trace2FormatTarget formatTarget) => + string.IsNullOrEmpty(Hierarchy) + ? Name + : $"{Name} ({Hierarchy})"; +} + public class ThreadStartMessage() : Trace2Message(Trace2Event.ThreadStart) { protected override JsonTypeInfo GetJsonTypeInfo() => Trace2JsonContext.Default.ThreadStartMessage; From fe699650077d669fdd91562ac189064dc68069dc Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:55:31 +0100 Subject: [PATCH 14/18] trace2: add region-local data events Regions describe elapsed work but cannot explain the values that shaped that work. Add scalar and structured data events whose thread, nesting, and relative timing come from the active logical context, allowing later instrumentation to attach useful measurements without inventing new regions. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- docs/development.md | 2 + src/Core.Tests/Trace2MessageTests.cs | 53 +++++++++++++ src/Core/Tracing/Trace2.cs | 113 +++++++++++++++++++++++++++ src/Core/Tracing/Trace2Message.cs | 92 ++++++++++++++++++++++ 4 files changed, 260 insertions(+) diff --git a/docs/development.md b/docs/development.md index eda2858dbe..5ca0de8f2b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -265,6 +265,8 @@ method 0. `region_enter`: describes a region (e.g. a timer for a section of code that is interesting) on entry 0. `region_leave`: describes a region on leaving +0. `data`: records a thread- and region-local key/value pair +0. `data_json`: records a thread- and region-local structured JSON value You can read more about each of these format targets in the [corresponding section][trace2-events] of Git's Trace2 API documentation. diff --git a/src/Core.Tests/Trace2MessageTests.cs b/src/Core.Tests/Trace2MessageTests.cs index ccbff8300e..7bf85c44d9 100644 --- a/src/Core.Tests/Trace2MessageTests.cs +++ b/src/Core.Tests/Trace2MessageTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json; using GitCredentialManager; using Xunit; @@ -116,6 +117,31 @@ public void Thread_Events_ToJson_Create_Expected_Json() exitMessage.ToJson()); } + [Fact] + public void Data_Event_ToJson_Creates_Expected_Json() + { + var message = new DataMessage + { + Sid = "123", + Thread = "main", + Time = new DateTimeOffset(), + File = "foo.cs", + Line = 1, + Depth = 1, + ElapsedTime = 0.05, + RelativeTime = 0.01, + Repo = 1, + Nesting = 2, + Category = "index", + Key = "read/cache_nr", + Value = "3552" + }; + + const string expected = "{\"event\":\"data\",\"sid\":\"123\",\"thread\":\"main\",\"time\":\"0001-01-01T00:00:00+00:00\",\"file\":\"foo.cs\",\"line\":1,\"depth\":1,\"t_abs\":0.05,\"t_rel\":0.01,\"repo\":1,\"nesting\":2,\"category\":\"index\",\"key\":\"read/cache_nr\",\"value\":\"3552\"}"; + + Assert.Equal(expected, message.ToJson()); + } + [Fact] public void CommandName_Event_ToJson_Creates_Expected_Json() { @@ -136,4 +162,31 @@ public void CommandName_Event_ToJson_Creates_Expected_Json() Assert.Equal(expected, message.ToJson()); } + [Fact] + public void DataJson_Event_ToJson_Creates_Expected_Json() + { + using JsonDocument document = JsonDocument.Parse( + "{\"count\":2,\"items\":[\"one\",\"two\"]}"); + + var message = new DataJsonMessage + { + Sid = "123", + Thread = "main", + Time = new DateTimeOffset(), + File = "foo.cs", + Line = 1, + Depth = 1, + ElapsedTime = 0.05, + RelativeTime = 0.01, + Repo = 1, + Nesting = 2, + Category = "index", + Key = "read/statistics", + Value = document.RootElement + }; + + const string expected = "{\"event\":\"data_json\",\"sid\":\"123\",\"thread\":\"main\",\"time\":\"0001-01-01T00:00:00+00:00\",\"file\":\"foo.cs\",\"line\":1,\"depth\":1,\"t_abs\":0.05,\"t_rel\":0.01,\"repo\":1,\"nesting\":2,\"category\":\"index\",\"key\":\"read/statistics\",\"value\":{\"count\":2,\"items\":[\"one\",\"two\"]}}"; + + Assert.Equal(expected, message.ToJson()); + } } diff --git a/src/Core/Tracing/Trace2.cs b/src/Core/Tracing/Trace2.cs index c6a2fbce8e..50334bd1ab 100644 --- a/src/Core/Tracing/Trace2.cs +++ b/src/Core/Tracing/Trace2.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Threading; using GitCredentialManager.Interop; @@ -548,6 +549,118 @@ public static IDisposable StartRegion( return new RegionScope(context, category, label, filePath, lineNumber, message); } + /// + /// Writes a thread- and region-local Trace2 data event. + /// + /// The broad category of the data. + /// The name of the data value. + /// The data value. + /// The source file writing the event. + /// The source line writing the event. + public static void WriteData( + string category, + string key, + string value, + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) + { + if (!_initialized) return; + + EnsureArgument.NotNullOrWhiteSpace(category, nameof(category)); + EnsureArgument.NotNullOrWhiteSpace(key, nameof(key)); + + value ??= string.Empty; + + DateTimeOffset now = DateTimeOffset.UtcNow; + Trace2ExecutionContext context = GetCurrentContext(); + DateTimeOffset relativeStart = context.RegionStartTime.Value ?? context.StartTime; + + WriteMessage(new DataMessage + { + Sid = _sid, + Time = now, + Thread = context.ThreadName, + File = Path.GetFileName(filePath), + Line = lineNumber, + ElapsedTime = (now - _applicationStartTime).TotalSeconds, + RelativeTime = (now - relativeStart).TotalSeconds, + Nesting = context.RegionNesting.Value + 1, + Category = category, + Key = key, + Value = value, + Depth = _depth + }); + } + + /// + /// Writes a thread- and region-local integer Trace2 data event. + /// + /// The broad category of the data. + /// The name of the data value. + /// The data value. + /// The source file writing the event. + /// The source line writing the event. + public static void WriteData( + string category, + string key, + long value, + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) + { + WriteData( + category, + key, + value.ToString(CultureInfo.InvariantCulture), + filePath, + lineNumber); + } + + /// + /// Writes a thread- and region-local Trace2 structured data event. + /// + /// The broad category of the data. + /// The name of the data value. + /// The structured JSON value. + /// The source file writing the event. + /// The source line writing the event. + public static void WriteData( + string category, + string key, + JsonElement value, + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) + { + if (!_initialized) return; + + EnsureArgument.NotNullOrWhiteSpace(category, nameof(category)); + EnsureArgument.NotNullOrWhiteSpace(key, nameof(key)); + + if (value.ValueKind == JsonValueKind.Undefined) + { + throw new ArgumentException("JSON value must be defined.", nameof(value)); + } + + DateTimeOffset now = DateTimeOffset.UtcNow; + Trace2ExecutionContext context = GetCurrentContext(); + DateTimeOffset relativeStart = context.RegionStartTime.Value ?? context.StartTime; + + WriteMessage(new DataJsonMessage + { + Sid = _sid, + Time = now, + Thread = context.ThreadName, + File = Path.GetFileName(filePath), + Line = lineNumber, + ElapsedTime = (now - _applicationStartTime).TotalSeconds, + RelativeTime = (now - relativeStart).TotalSeconds, + Nesting = context.RegionNesting.Value + 1, + Category = category, + Key = key, + Value = value, + Depth = _depth + }); + } + private static DateTimeOffset WriteRegionEnter( string category, string label, diff --git a/src/Core/Tracing/Trace2Message.cs b/src/Core/Tracing/Trace2Message.cs index 4f131d9759..20aaaf03c5 100644 --- a/src/Core/Tracing/Trace2Message.cs +++ b/src/Core/Tracing/Trace2Message.cs @@ -32,6 +32,10 @@ public enum Trace2Event ThreadStart, [JsonStringEnumMemberName("thread_exit")] ThreadExit, + [JsonStringEnumMemberName("data")] + Data, + [JsonStringEnumMemberName("data_json")] + DataJson, [JsonStringEnumMemberName("cmd_name")] CommandName, } @@ -46,7 +50,9 @@ public enum Trace2Event [JsonSerializable(typeof(ErrorMessage))] [JsonSerializable(typeof(RegionEnterMessage))] [JsonSerializable(typeof(RegionLeaveMessage))] +[JsonSerializable(typeof(DataMessage))] [JsonSerializable(typeof(CommandNameMessage))] +[JsonSerializable(typeof(DataJsonMessage))] [JsonSourceGenerationOptions( UseStringEnumConverter = true, PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, @@ -493,3 +499,89 @@ public class RegionLeaveMessage() : RegionMessage(Trace2Event.RegionLeave) Category = Category }; } + +public class DataMessage() : Trace2Message(Trace2Event.Data) +{ + [JsonPropertyName("t_abs")] + [JsonPropertyOrder(8)] + public double ElapsedTime { get; set; } + + [JsonPropertyName("t_rel")] + [JsonPropertyOrder(9)] + public double RelativeTime { get; set; } + + [JsonPropertyName("repo")] + [JsonPropertyOrder(10)] + public int Repo { get; set; } = 1; + + [JsonPropertyName("nesting")] + [JsonPropertyOrder(11)] + public int Nesting { get; set; } + + [JsonPropertyName("category")] + [JsonPropertyOrder(12)] + public string Category { get; set; } + + [JsonPropertyName("key")] + [JsonPropertyOrder(13)] + public string Key { get; set; } + + [JsonPropertyName("value")] + [JsonPropertyOrder(14)] + public string Value { get; set; } + + protected override JsonTypeInfo GetJsonTypeInfo() => Trace2JsonContext.Default.DataMessage; + + private protected override PerformanceFormatFields GetPerformanceFields() => new() + { + Repo = Repo, + ElapsedTime = ElapsedTime, + RelativeTime = RelativeTime, + Category = Category + }; + + protected override string GetEventMessage(Trace2FormatTarget formatTarget) => $"{Key}:{Value}"; +} + +public class DataJsonMessage() : Trace2Message(Trace2Event.DataJson) +{ + [JsonPropertyName("t_abs")] + [JsonPropertyOrder(8)] + public double ElapsedTime { get; set; } + + [JsonPropertyName("t_rel")] + [JsonPropertyOrder(9)] + public double RelativeTime { get; set; } + + [JsonPropertyName("repo")] + [JsonPropertyOrder(10)] + public int Repo { get; set; } = 1; + + [JsonPropertyName("nesting")] + [JsonPropertyOrder(11)] + public int Nesting { get; set; } + + [JsonPropertyName("category")] + [JsonPropertyOrder(12)] + public string Category { get; set; } + + [JsonPropertyName("key")] + [JsonPropertyOrder(13)] + public string Key { get; set; } + + [JsonPropertyName("value")] + [JsonPropertyOrder(14)] + public JsonElement Value { get; set; } + + protected override JsonTypeInfo GetJsonTypeInfo() => Trace2JsonContext.Default.DataJsonMessage; + + private protected override PerformanceFormatFields GetPerformanceFields() => new() + { + Repo = Repo, + ElapsedTime = ElapsedTime, + RelativeTime = RelativeTime, + Category = Category + }; + + protected override string GetEventMessage(Trace2FormatTarget formatTarget) => $"{Key}:{Value.GetRawText()}"; +} From 14d421d68e05ff8387c5b64eb16ed746263b7738 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:56:43 +0100 Subject: [PATCH 15/18] trace2: instrument command execution Top-level traces currently stop before command parsing and provider selection, leaving the most important dispatch decisions invisible. Add focused regions and metadata around setup, input parsing, provider resolution, and each command entry point so command execution can be followed end to end. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core/Application.cs | 106 +++++++++++---------- src/Core/Commands/CapabilityCommand.cs | 3 + src/Core/Commands/ConfigurationCommands.cs | 8 +- src/Core/Commands/DiagnoseCommand.cs | 2 + src/Core/Commands/EraseCommand.cs | 5 +- src/Core/Commands/GetCommand.cs | 6 +- src/Core/Commands/GitCommandBase.cs | 20 +++- src/Core/Commands/StoreCommand.cs | 5 +- src/git-credential-manager/Program.cs | 17 ++-- 9 files changed, 103 insertions(+), 69 deletions(-) diff --git a/src/Core/Application.cs b/src/Core/Application.cs index 977213d724..3dc7d5d0d9 100644 --- a/src/Core/Application.cs +++ b/src/Core/Application.cs @@ -68,67 +68,71 @@ public void RegisterProvider(IHostProvider provider, HostProviderPriority priori protected override async Task RunInternalAsync(string[] args) { - var rootCommand = new RootCommand(); - var diagnoseCommand = new DiagnoseCommand(Context); + Parser parser; + using (Trace2.StartRegion("main", "cmd_setup")) + { + var rootCommand = new RootCommand(); + var diagnoseCommand = new DiagnoseCommand(Context); - // Add common options - var noGuiOption = new Option("--no-ui", "Do not use graphical user interface prompts"); - rootCommand.AddGlobalOption(noGuiOption); + // Add common options + var noGuiOption = new Option("--no-ui", "Do not use graphical user interface prompts"); + rootCommand.AddGlobalOption(noGuiOption); - void NoGuiOptionHandler(InvocationContext context) - { - if (context.ParseResult.HasOption(noGuiOption)) + void NoGuiOptionHandler(InvocationContext context) { - Context.Settings.IsGuiPromptsEnabled = false; + if (context.ParseResult.HasOption(noGuiOption)) + { + Context.Settings.IsGuiPromptsEnabled = false; + } } - } - void Trace2CommandNameHandler(InvocationContext context) - { - Command command = context.ParseResult.CommandResult.Command; - if (!ReferenceEquals(command, rootCommand)) + void Trace2CommandNameHandler(InvocationContext context) { - Trace2.WriteCommandName(command.Name); + Command command = context.ParseResult.CommandResult.Command; + if (!ReferenceEquals(command, rootCommand)) + { + Trace2.WriteCommandName(command.Name); + } } - } - // Add standard commands - rootCommand.AddCommand(new GetCommand(Context, _providerRegistry)); - rootCommand.AddCommand(new StoreCommand(Context, _providerRegistry)); - rootCommand.AddCommand(new EraseCommand(Context, _providerRegistry)); - rootCommand.AddCommand(new CapabilityCommand(Context)); - rootCommand.AddCommand(new ConfigureCommand(Context, _configurationService)); - rootCommand.AddCommand(new UnconfigureCommand(Context, _configurationService)); - rootCommand.AddCommand(diagnoseCommand); - - // Add any custom provider commands - foreach (ProviderCommand providerCommand in _providerCommands) - { - rootCommand.AddCommand(providerCommand); - } + // Add standard commands + rootCommand.AddCommand(new GetCommand(Context, _providerRegistry)); + rootCommand.AddCommand(new StoreCommand(Context, _providerRegistry)); + rootCommand.AddCommand(new EraseCommand(Context, _providerRegistry)); + rootCommand.AddCommand(new CapabilityCommand(Context)); + rootCommand.AddCommand(new ConfigureCommand(Context, _configurationService)); + rootCommand.AddCommand(new UnconfigureCommand(Context, _configurationService)); + rootCommand.AddCommand(diagnoseCommand); + + // Add any custom provider commands + foreach (ProviderCommand providerCommand in _providerCommands) + { + rootCommand.AddCommand(providerCommand); + } - // Add any custom provider diagnostic tests - foreach (IDiagnostic providerDiagnostic in _diagnostics) - { - diagnoseCommand.AddDiagnostic(providerDiagnostic); - } + // Add any custom provider diagnostic tests + foreach (IDiagnostic providerDiagnostic in _diagnostics) + { + diagnoseCommand.AddDiagnostic(providerDiagnostic); + } - // Trace the current version, OS, runtime, and program arguments - PlatformInformation info = PlatformUtils.GetPlatformInformation(); - Context.Trace.WriteLine($"Version: {Constants.GcmVersion}"); - Context.Trace.WriteLine($"Runtime: {info.ClrVersion}"); - Context.Trace.WriteLine($"Platform: {info.OperatingSystemType} ({info.CpuArchitecture})"); - Context.Trace.WriteLine($"OSVersion: {info.OperatingSystemVersion}"); - Context.Trace.WriteLine($"AppPath: {Context.ApplicationPath}"); - Context.Trace.WriteLine($"InstallDir: {Context.InstallationDirectory}"); - Context.Trace.WriteLine($"Arguments: {string.Join(" ", args)}"); - - var parser = new CommandLineBuilder(rootCommand) - .UseDefaults() - .UseExceptionHandler(OnException) - .AddMiddleware(NoGuiOptionHandler) - .AddMiddleware(Trace2CommandNameHandler) - .Build(); + // Trace the current version, OS, runtime, and program arguments + PlatformInformation info = PlatformUtils.GetPlatformInformation(); + Context.Trace.WriteLine($"Version: {Constants.GcmVersion}"); + Context.Trace.WriteLine($"Runtime: {info.ClrVersion}"); + Context.Trace.WriteLine($"Platform: {info.OperatingSystemType} ({info.CpuArchitecture})"); + Context.Trace.WriteLine($"OSVersion: {info.OperatingSystemVersion}"); + Context.Trace.WriteLine($"AppPath: {Context.ApplicationPath}"); + Context.Trace.WriteLine($"InstallDir: {Context.InstallationDirectory}"); + Context.Trace.WriteLine($"Arguments: {string.Join(" ", args)}"); + + parser = new CommandLineBuilder(rootCommand) + .UseDefaults() + .UseExceptionHandler(OnException) + .AddMiddleware(NoGuiOptionHandler) + .AddMiddleware(Trace2CommandNameHandler) + .Build(); + } return await parser.InvokeAsync(args); } diff --git a/src/Core/Commands/CapabilityCommand.cs b/src/Core/Commands/CapabilityCommand.cs index c80607ad3e..6ac5aca197 100644 --- a/src/Core/Commands/CapabilityCommand.cs +++ b/src/Core/Commands/CapabilityCommand.cs @@ -48,6 +48,9 @@ public CapabilityCommand(ICommandContext context) internal void Execute() { + using var _ = Trace2.StartRegion("git_cmd", "run"); + Trace2.WriteData("git_cmd", "name", "capability"); + _context.Trace.WriteLine("Start 'capability' command..."); _context.Streams.Out.WriteLine($"version {ProtocolVersion}"); diff --git a/src/Core/Commands/ConfigurationCommands.cs b/src/Core/Commands/ConfigurationCommands.cs index 1996805c9b..efca06d2c2 100644 --- a/src/Core/Commands/ConfigurationCommands.cs +++ b/src/Core/Commands/ConfigurationCommands.cs @@ -24,13 +24,17 @@ protected ConfigurationCommandBase(ICommandContext context, string name, string protected IConfigurationService ConfigurationService { get; } - internal Task ExecuteAsync(bool system) + internal async Task ExecuteAsync(bool system) { var target = system ? ConfigurationTarget.System : ConfigurationTarget.User; - return ExecuteInternalAsync(target); + using var _ = Trace2.StartRegion("cfg_cmd", "run"); + Trace2.WriteData("cfg_cmd", "name", Name); + Trace2.WriteData("cfg_cmd", "target", target.ToString()); + + await ExecuteInternalAsync(target); } protected abstract Task ExecuteInternalAsync(ConfigurationTarget target); diff --git a/src/Core/Commands/DiagnoseCommand.cs b/src/Core/Commands/DiagnoseCommand.cs index 3603d073b1..a3364f77bd 100644 --- a/src/Core/Commands/DiagnoseCommand.cs +++ b/src/Core/Commands/DiagnoseCommand.cs @@ -45,6 +45,8 @@ public void AddDiagnostic(IDiagnostic diagnostic) private async Task ExecuteAsync(string output) { + using var _ = Trace2.StartRegion("diag_cmd", "run"); + // Don't use IStandardStreams for writing output in this command as we // cannot trust any component on the ICommandContext is working correctly. Console.WriteLine($"Running diagnostics...{Environment.NewLine}"); diff --git a/src/Core/Commands/EraseCommand.cs b/src/Core/Commands/EraseCommand.cs index ab850af5ca..52d109bbd2 100644 --- a/src/Core/Commands/EraseCommand.cs +++ b/src/Core/Commands/EraseCommand.cs @@ -13,9 +13,10 @@ public EraseCommand(ICommandContext context, IHostProviderRegistry hostProviderR IsHidden = true; } - protected override Task ExecuteInternalAsync(GitRequest request, IHostProvider provider) + protected override async Task ExecuteInternalAsync(GitRequest request, IHostProvider provider) { - return provider.EraseCredentialAsync(request); + using var _ = Trace2.StartRegion("git_cmd_erase", "provider_erase"); + await provider.EraseCredentialAsync(request); } } } diff --git a/src/Core/Commands/GetCommand.cs b/src/Core/Commands/GetCommand.cs index c328924de2..551e1feac8 100644 --- a/src/Core/Commands/GetCommand.cs +++ b/src/Core/Commands/GetCommand.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Threading.Tasks; -using GitCredentialManager.Tty; namespace GitCredentialManager.Commands { @@ -21,7 +20,10 @@ protected override async Task ExecuteInternalAsync(GitRequest request, IHostProv GitResponse response; try { - response = await provider.GetCredentialAsync(request); + using (Trace2.StartRegion("git_cmd_get", "provider_get")) + { + response = await provider.GetCredentialAsync(request); + } } catch (Exception ex) when (ex is OperationCanceledException || ex is InterruptedException) { diff --git a/src/Core/Commands/GitCommandBase.cs b/src/Core/Commands/GitCommandBase.cs index 76568e8942..1dfc398a9c 100644 --- a/src/Core/Commands/GitCommandBase.cs +++ b/src/Core/Commands/GitCommandBase.cs @@ -29,11 +29,18 @@ protected GitCommandBase(ICommandContext context, string name, string descriptio internal async Task ExecuteAsync() { + using var _ = Trace2.StartRegion("git_cmd", "run"); + Trace2.WriteData("git_cmd", "name", Name); + Context.Trace.WriteLine($"Start '{Name}' command..."); // Parse standard input arguments - // git-credential treats the keys as case-sensitive; so should we. - IDictionary> inputDict = await Context.Streams.In.ReadMultiDictionaryAsync(StringComparer.Ordinal); + IDictionary> inputDict; + using (Trace2.StartRegion("git_cmd", "parse_input")) + { + // git-credential treats the keys as case-sensitive; so should we. + inputDict = await Context.Streams.In.ReadMultiDictionaryAsync(StringComparer.Ordinal); + } var request = new GitRequest(inputDict); // Validate minimum arguments are present @@ -45,7 +52,14 @@ internal async Task ExecuteAsync() // Determine the host provider Context.Trace.WriteLine("Detecting host provider for request:"); Context.Trace.WriteDictionarySecrets(inputDict, new []{ "password" }, StringComparer.OrdinalIgnoreCase); - IHostProvider provider = await _hostProviderRegistry.GetProviderAsync(request); + IHostProvider provider; + using (Trace2.StartRegion("git_cmd", "resolve_provider")) + { + provider = await _hostProviderRegistry.GetProviderAsync(request); + + Trace2.WriteData("git_cmd", "provider/id", provider.Id); + Trace2.WriteData("git_cmd", "provider/name", provider.Name); + } Context.Trace.WriteLine($"Host provider '{provider.Name}' was selected."); await ExecuteInternalAsync(request, provider); diff --git a/src/Core/Commands/StoreCommand.cs b/src/Core/Commands/StoreCommand.cs index 16883acfe3..09043ff74b 100644 --- a/src/Core/Commands/StoreCommand.cs +++ b/src/Core/Commands/StoreCommand.cs @@ -14,9 +14,10 @@ public StoreCommand(ICommandContext context, IHostProviderRegistry hostProviderR IsHidden = true; } - protected override Task ExecuteInternalAsync(GitRequest request, IHostProvider provider) + protected override async Task ExecuteInternalAsync(GitRequest request, IHostProvider provider) { - return provider.StoreCredentialAsync(request); + using var _ = Trace2.StartRegion("git_cmd_store", "provider_store"); + await provider.StoreCredentialAsync(request); } protected override void EnsureMinimumRequest(GitRequest request) diff --git a/src/git-credential-manager/Program.cs b/src/git-credential-manager/Program.cs index c0abd426dc..b162b5b424 100644 --- a/src/git-credential-manager/Program.cs +++ b/src/git-credential-manager/Program.cs @@ -46,13 +46,16 @@ private static void AppMain(object o) using (var context = new CommandContext()) using (var app = new Application(context)) { - // Register all supported host providers at the normal priority. - // The generic provider should never win against a more specific one, so register it with low priority. - app.RegisterProvider(new AzureReposHostProvider(context), HostProviderPriority.Normal); - app.RegisterProvider(new BitbucketHostProvider(context), HostProviderPriority.Normal); - app.RegisterProvider(new GitHubHostProvider(context), HostProviderPriority.Normal); - app.RegisterProvider(new GitLabHostProvider(context), HostProviderPriority.Normal); - app.RegisterProvider(new GenericHostProvider(context), HostProviderPriority.Low); + using (Trace2.StartRegion("main", "provider_reg")) + { + // Register all supported host providers at the normal priority. + // The generic provider should never win against a more specific one, so register it with low priority. + app.RegisterProvider(new AzureReposHostProvider(context), HostProviderPriority.Normal); + app.RegisterProvider(new BitbucketHostProvider(context), HostProviderPriority.Normal); + app.RegisterProvider(new GitHubHostProvider(context), HostProviderPriority.Normal); + app.RegisterProvider(new GitLabHostProvider(context), HostProviderPriority.Normal); + app.RegisterProvider(new GenericHostProvider(context), HostProviderPriority.Low); + } _exitCode = app.RunAsync(args) .ConfigureAwait(false) From 26065ed52a5b49f5a49a2036be1dabc236a3dc94 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:57:09 +0100 Subject: [PATCH 16/18] trace2: instrument core services Command dispatch alone cannot explain time spent discovering Git, reading configuration, or opening the selected credential store. Trace those service boundaries and record the configuration scope and backing-store choices that drive their behavior. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core/CommandContext.cs | 4 ++++ src/Core/CredentialStore.cs | 16 +++++++++++++--- src/Core/GitConfiguration.cs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/Core/CommandContext.cs b/src/Core/CommandContext.cs index 5ba3b156f0..9ca924d8f5 100644 --- a/src/Core/CommandContext.cs +++ b/src/Core/CommandContext.cs @@ -89,6 +89,8 @@ public class CommandContext : DisposableObject, ICommandContext { public CommandContext() { + using var _ = Trace2.StartRegion("cmd_ctx", "create"); + ApplicationPath = GetEntryApplicationPath(); InstallationDirectory = GetInstallationDirectory(); @@ -152,6 +154,8 @@ public CommandContext() private static string GetGitPath(IEnvironment environment, IFileSystem fileSystem, ITrace trace) { + using var _ = Trace2.StartRegion("cmd_ctx", "find_git"); + const string unixGitName = "git"; const string winGitName = "git.exe"; diff --git a/src/Core/CredentialStore.cs b/src/Core/CredentialStore.cs index f919bbd28b..29b9d04c5c 100644 --- a/src/Core/CredentialStore.cs +++ b/src/Core/CredentialStore.cs @@ -27,24 +27,28 @@ public CredentialStore(ICommandContext context) public IList GetAccounts(string service) { + using var _ = Trace2.StartRegion("cred_store", "get_accounts"); EnsureBackingStore(); return _backingStore.GetAccounts(service); } public ICredential Get(string service, string account) { + using var _ = Trace2.StartRegion("cred_store", "get"); EnsureBackingStore(); return _backingStore.Get(service, account); } public void AddOrUpdate(string service, string account, string secret) { + using var _ = Trace2.StartRegion("cred_store", "add"); EnsureBackingStore(); _backingStore.AddOrUpdate(service, account, secret); } public bool Remove(string service, string account) { + using var _ = Trace2.StartRegion("cred_store", "remove"); EnsureBackingStore(); return _backingStore.Remove(service, account); } @@ -58,11 +62,17 @@ private void EnsureBackingStore() return; } + using var _ = Trace2.StartRegion("cred_store", "init"); + string ns = _context.Settings.CredentialNamespace; - string credStoreName = _context.Settings.CredentialBackingStore?.ToLowerInvariant() - ?? GetDefaultStore(); + string credStoreName = _context.Settings.CredentialBackingStore?.ToLowerInvariant(); + string defaultStore = GetDefaultStore(); + + Trace2.WriteData("cred_store", "store/configured", credStoreName); + Trace2.WriteData("cred_store", "store/default", defaultStore); + Trace2.WriteData("cred_store", "store/namespace", ns); - switch (credStoreName) + switch (credStoreName ?? defaultStore) { case StoreNames.WindowsCredentialManager: ValidateWindowsCredentialManager(); diff --git a/src/Core/GitConfiguration.cs b/src/Core/GitConfiguration.cs index d7af4a307e..9da4bc0289 100644 --- a/src/Core/GitConfiguration.cs +++ b/src/Core/GitConfiguration.cs @@ -390,6 +390,9 @@ private void EnsureCacheLoaded(GitConfigurationType type) return; } + using IDisposable region = Trace2.StartRegion("git_config", "load_cache"); + Trace2.WriteData("git_config", "type", type.ToString().ToLowerInvariant()); + if (cache == null) { cache = new ConfigCache(); @@ -449,6 +452,9 @@ private void InvalidateCache() public void Enumerate(GitConfigurationLevel level, GitConfigurationEnumerationCallback cb) { + using IDisposable region = Trace2.StartRegion("git_config", "enumerate"); + Trace2.WriteData("git_config", "scope", level.ToString().ToLowerInvariant()); + if (_useCache) { EnsureCacheLoaded(GitConfigurationType.Raw); @@ -544,6 +550,9 @@ public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, strin } // Fall back to individual git config command if cache not available + using var _ = Trace2.StartRegion("git_config", "get"); + Trace2.WriteData("git_config", "scope", level.ToString().ToLowerInvariant()); + Trace2.WriteData("git_config", "type", type.ToString().ToLowerInvariant()); string levelArg = GetLevelFilterArg(level); string typeArg = GetCanonicalizeTypeArg(type); using (ChildProcess git = _git.CreateProcess($"config --null {levelArg} {typeArg} {QuoteCmdArg(name)}")) @@ -581,6 +590,9 @@ public bool TryGet(GitConfigurationLevel level, GitConfigurationType type, strin public void Set(GitConfigurationLevel level, string name, string value) { + using IDisposable region = Trace2.StartRegion("git_config", "set"); + Trace2.WriteData("git_config", "scope", level.ToString().ToLowerInvariant()); + EnsureSpecificLevel(level); string levelArg = GetLevelFilterArg(level); @@ -603,6 +615,9 @@ public void Set(GitConfigurationLevel level, string name, string value) public void Add(GitConfigurationLevel level, string name, string value) { + using IDisposable region = Trace2.StartRegion("git_config", "add"); + Trace2.WriteData("git_config", "scope", level.ToString().ToLowerInvariant()); + EnsureSpecificLevel(level); string levelArg = GetLevelFilterArg(level); @@ -625,6 +640,9 @@ public void Add(GitConfigurationLevel level, string name, string value) public void Unset(GitConfigurationLevel level, string name) { + using IDisposable region = Trace2.StartRegion("git_config", "unset"); + Trace2.WriteData("git_config", "scope", level.ToString().ToLowerInvariant()); + EnsureSpecificLevel(level); string levelArg = GetLevelFilterArg(level); @@ -648,6 +666,10 @@ public void Unset(GitConfigurationLevel level, string name) public IEnumerable GetAll(GitConfigurationLevel level, GitConfigurationType type, string name) { + using IDisposable region = Trace2.StartRegion("git_config", "get_all"); + Trace2.WriteData("git_config", "scope", level.ToString().ToLowerInvariant()); + Trace2.WriteData("git_config", "type", type.ToString().ToLowerInvariant()); + if (_useCache) { EnsureCacheLoaded(type); @@ -703,6 +725,10 @@ public IEnumerable GetAll(GitConfigurationLevel level, GitConfigurationT public IEnumerable GetRegex(GitConfigurationLevel level, GitConfigurationType type, string nameRegex, string valueRegex) { + using IDisposable region = Trace2.StartRegion("git_config", "get_regex"); + Trace2.WriteData("git_config", "scope", level.ToString().ToLowerInvariant()); + Trace2.WriteData("git_config", "type", type.ToString().ToLowerInvariant()); + string levelArg = GetLevelFilterArg(level); string typeArg = GetCanonicalizeTypeArg(type); @@ -745,6 +771,8 @@ public IEnumerable GetRegex(GitConfigurationLevel level, GitConfiguratio public void ReplaceAll(GitConfigurationLevel level, string name, string valueRegex, string value) { + using IDisposable region = Trace2.StartRegion("git_config", "replace_all"); + EnsureSpecificLevel(level); string levelArg = GetLevelFilterArg(level); @@ -773,6 +801,8 @@ public void ReplaceAll(GitConfigurationLevel level, string name, string valueReg public void UnsetAll(GitConfigurationLevel level, string name, string valueRegex) { + using IDisposable region = Trace2.StartRegion("git_config", "unset_all"); + EnsureSpecificLevel(level); string levelArg = GetLevelFilterArg(level); From 6d47a02c98058e19d61022dffebe7c418db394c3 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:57:39 +0100 Subject: [PATCH 17/18] trace2: instrument UI lifecycle Eager console creation and dispatcher hand-offs hide UI startup cost and can initialize terminal state before it is needed. Create consoles lazily and trace ANSI, Avalonia, rendering, and window-display boundaries so interactive latency is attributed to the correct lifecycle stage. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- src/Core.Tests/ConsoleServiceTests.cs | 8 ++-- src/Core.Tests/HttpClientFactoryTests.cs | 2 +- src/Core/Application.cs | 2 - src/Core/ConsoleService.cs | 27 ++++++----- src/Core/Tty/AnsiConsoleFactory.cs | 3 ++ src/Core/UI/AvaloniaUi.cs | 61 ++++++++++++++---------- src/git-credential-manager/Program.cs | 9 +++- 7 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/Core.Tests/ConsoleServiceTests.cs b/src/Core.Tests/ConsoleServiceTests.cs index 0e13cafdd4..91d67fd82c 100644 --- a/src/Core.Tests/ConsoleServiceTests.cs +++ b/src/Core.Tests/ConsoleServiceTests.cs @@ -13,8 +13,8 @@ public void ConsoleService_WriteMethods_RouteToErrorConsoleWriter() { var err = new StringWriter(); var console = new ConsoleService( - AnsiConsoleFactory.CreateHeadless(), - AnsiConsoleFactory.CreateForWriter(err, isRedirected: true)); + AnsiConsoleFactory.CreateHeadless, + () => AnsiConsoleFactory.CreateForWriter(err, isRedirected: true)); console.WriteInfo("info-[marker]"); console.WriteWarning("warn-[marker]"); @@ -38,8 +38,8 @@ public void ConsoleService_WriteFatal_RoutesToAutoFlushStreamWriter() using var sw = new StreamWriter(ms, new UTF8Encoding(false)) { AutoFlush = true, NewLine = "\n" }; var console = new ConsoleService( - AnsiConsoleFactory.CreateHeadless(), - AnsiConsoleFactory.CreateForWriter(sw, isRedirected: true)); + AnsiConsoleFactory.CreateHeadless, + () => AnsiConsoleFactory.CreateForWriter(sw, isRedirected: true)); console.WriteFatal("fatal-marker"); sw.Flush(); diff --git a/src/Core.Tests/HttpClientFactoryTests.cs b/src/Core.Tests/HttpClientFactoryTests.cs index e11a5b2bb1..c31e80e8c1 100644 --- a/src/Core.Tests/HttpClientFactoryTests.cs +++ b/src/Core.Tests/HttpClientFactoryTests.cs @@ -11,7 +11,7 @@ namespace GitCredentialManager.Tests { public class HttpClientFactoryTests { - private static readonly IConsoleService TestConsole = new ConsoleService(AnsiConsoleFactory.CreateHeadless(), AnsiConsoleFactory.CreateHeadless()); + private static readonly IConsoleService TestConsole = new ConsoleService(AnsiConsoleFactory.CreateHeadless, AnsiConsoleFactory.CreateHeadless); [Fact] public void HttpClientFactory_GetClient_SetsDefaultHeaders() diff --git a/src/Core/Application.cs b/src/Core/Application.cs index 3dc7d5d0d9..2d13f99991 100644 --- a/src/Core/Application.cs +++ b/src/Core/Application.cs @@ -10,8 +10,6 @@ using GitCredentialManager.Commands; using GitCredentialManager.Diagnostics; using GitCredentialManager.Interop; -using GitCredentialManager.Tty; -using Spectre.Console; namespace GitCredentialManager { diff --git a/src/Core/ConsoleService.cs b/src/Core/ConsoleService.cs index 5d76223efc..2569b8a1f5 100644 --- a/src/Core/ConsoleService.cs +++ b/src/Core/ConsoleService.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; using GitCredentialManager.Tty; @@ -36,32 +37,32 @@ public interface IConsoleService public class ConsoleService : IConsoleService { - private readonly IAnsiConsole _ttyConsole; - private readonly IAnsiConsole _stderrConsole; + private readonly Lazy _ttyConsole; + private readonly Lazy _stderrConsole; public ConsoleService(IStandardStreams streams) - : this(AnsiConsoleFactory.CreateForTty(), AnsiConsoleFactory.CreateForWriter(streams.Error, streams.IsErrorRedirected)) + : this(AnsiConsoleFactory.CreateForTty, () => AnsiConsoleFactory.CreateForWriter(streams.Error, streams.IsErrorRedirected)) { } - public ConsoleService(IAnsiConsole ttyConsole, IAnsiConsole stderrConsole) + public ConsoleService(Func ttyConsoleFunc, Func stderrConsoleFunc) { - _ttyConsole = ttyConsole; - _stderrConsole = stderrConsole; + _ttyConsole = new Lazy(ttyConsoleFunc); + _stderrConsole = new Lazy(stderrConsoleFunc); } - public void WriteInfo(string message) => _stderrConsole.MarkupLine($"[blue]info:[/] {Markup.Escape(message)}"); + public void WriteInfo(string message) => _stderrConsole.Value.MarkupLine($"[blue]info:[/] {Markup.Escape(message)}"); - public void WriteWarning(string message) => _stderrConsole.MarkupLine($"[yellow]warning:[/] {Markup.Escape(message)}"); + public void WriteWarning(string message) => _stderrConsole.Value.MarkupLine($"[yellow]warning:[/] {Markup.Escape(message)}"); - public void WriteError(string message) => _stderrConsole.MarkupLine($"[red]error:[/] {Markup.Escape(message)}"); + public void WriteError(string message) => _stderrConsole.Value.MarkupLine($"[red]error:[/] {Markup.Escape(message)}"); - public void WriteFatal(string message) => _stderrConsole.MarkupLine($"[red]fatal:[/] {Markup.Escape(message)}"); + public void WriteFatal(string message) => _stderrConsole.Value.MarkupLine($"[red]fatal:[/] {Markup.Escape(message)}"); - public void WriteLine(string message) => _stderrConsole.WriteLine(message); + public void WriteLine(string message) => _stderrConsole.Value.WriteLine(message); public T ShowPrompt(IPrompt prompt) => - prompt.Show(_ttyConsole); + prompt.Show(_ttyConsole.Value); public Task ShowPromptAsync(IPrompt prompt, CancellationToken ct = default) => - prompt.ShowAsync(_ttyConsole, ct); + prompt.ShowAsync(_ttyConsole.Value, ct); } diff --git a/src/Core/Tty/AnsiConsoleFactory.cs b/src/Core/Tty/AnsiConsoleFactory.cs index 4552941341..04cfc3a077 100644 --- a/src/Core/Tty/AnsiConsoleFactory.cs +++ b/src/Core/Tty/AnsiConsoleFactory.cs @@ -36,6 +36,7 @@ public static class AnsiConsoleFactory /// public static IAnsiConsole CreateForTty() { + using var _ = Trace2.StartRegion("ansi_console", "create_tty"); IAnsiConsoleOutput output = TryCreatePlatformOutput(); if (output is null) { @@ -69,6 +70,7 @@ public static IAnsiConsole CreateForTty() /// public static IAnsiConsole CreateForWriter(TextWriter writer, bool isRedirected) { + using var _ = Trace2.StartRegion("ansi_console", "create_writer"); return AnsiConsole.Create( new AnsiConsoleSettings { @@ -86,6 +88,7 @@ public static IAnsiConsole CreateForWriter(TextWriter writer, bool isRedirected) /// internal static IAnsiConsole CreateHeadless() { + using var _ = Trace2.StartRegion("ansi_console", "create_headless"); IAnsiConsole inner = AnsiConsole.Create( new AnsiConsoleSettings { diff --git a/src/Core/UI/AvaloniaUi.cs b/src/Core/UI/AvaloniaUi.cs index e813802c81..361780f051 100644 --- a/src/Core/UI/AvaloniaUi.cs +++ b/src/Core/UI/AvaloniaUi.cs @@ -59,33 +59,39 @@ public static Task ShowWindowAsync(Func windowFunc, object dataContext, var appInitialized = new ManualResetEventSlim(); - // Fire and forget the Avalonia app main loop over to our dispatcher (running on the main/entry thread). - // This action only returns on our dispatcher shutdown. - Dispatcher.MainThread.Post(appCancelToken => + // Keep the trace region to outside the dispatcher's lambda so we can attribute the + // UI init cost to the caller's thread, rather than the main thread. + using (Trace2.StartRegion("ui", "avn_init")) { - var appBuilder = AppBuilder.Configure(); - - // Set custom rendering options and modes if required - if (PlatformUtils.IsWindows() && _win32SoftwareRendering) + // Fire and forget the Avalonia app main loop over to our dispatcher (running on the main/entry thread). + // This action only returns on our dispatcher shutdown. + Dispatcher.MainThread.Post(appCancelToken => { - appBuilder.With(new Win32PlatformOptions - { RenderingMode = new[] { Win32RenderingMode.Software } }); - } - - appBuilder - .UsePlatformDetect() - .LogToTrace() - .SetupWithoutStarting(); - - appInitialized.Set(); - - // Run the application loop (only exit when the dispatcher is shutting down) - AvnDispatcher.UIThread.MainLoop(appCancelToken); - }); - - // Wait for the action posted above to be dequeued from the dispatcher's job queue - // and for the Avalonia framework (and their dispatcher) to be initialized. - appInitialized.Wait(); + var appBuilder = AppBuilder.Configure(); + + // Set custom rendering options and modes if required + if (PlatformUtils.IsWindows() && _win32SoftwareRendering) + { + Trace2.WriteData("ui", "win32/software_rendering", "true"); + appBuilder.With(new Win32PlatformOptions + { RenderingMode = new[] { Win32RenderingMode.Software } }); + } + + appBuilder + .UsePlatformDetect() + .LogToTrace() + .SetupWithoutStarting(); + + appInitialized.Set(); + + // Run the application loop (only exit when the dispatcher is shutting down) + AvnDispatcher.UIThread.MainLoop(appCancelToken); + }); + + // Wait for the action posted above to be dequeued from the dispatcher's job queue + // and for the Avalonia framework (and their dispatcher) to be initialized. + appInitialized.Wait(); + } } // Post the window action to the Avalonia dispatcher (which should be running) @@ -97,6 +103,8 @@ public static Task ShowWindowAsync(Func windowFunc, object dataContext, private static Task ShowWindowInternal(Func windowFunc, object dataContext, IntPtr parentHandle, CancellationToken ct) { + var region = Trace2.StartRegion("ui", "show_window"); + var tcs = new TaskCompletionSource(); Window window = windowFunc(); window.DataContext = dataContext; @@ -111,6 +119,7 @@ private static Task ShowWindowInternal(Func windowFunc, object dataConte // have a window handle/ID we must manually parent the window. if (parentHandle != IntPtr.Zero) { + Trace2.WriteData("ui", "parent", $"0x{parentHandle:x}"); SetParentExternal(window, parentHandle); } @@ -127,7 +136,7 @@ private static Task ShowWindowInternal(Func windowFunc, object dataConte window.Topmost = false; } - return tcs.Task; + return tcs.Task.ContinueWith(_ => region.Dispose()); } private static void SetParentExternal(Window window, IntPtr parentHandle) diff --git a/src/git-credential-manager/Program.cs b/src/git-credential-manager/Program.cs index b162b5b424..5d8a2a83b7 100644 --- a/src/git-credential-manager/Program.cs +++ b/src/git-credential-manager/Program.cs @@ -43,6 +43,11 @@ private static void AppMain(object o) { string[] args = (string[])o; + // Do NOT start a Trace2 thread scope for the 'AppMain' thread so that all traces are attributed + // to the 'main' thread. We do not gain anything accurately attributing things to this secondary + // thread that actually runs the majority of the application. + // The existence of this AppMain-thread is only to provide Avalonia UI with the actual initial + // thread #1 that some platforms require (namely macOS) for interacting with UI components. using (var context = new CommandContext()) using (var app = new Application(context)) { @@ -61,9 +66,9 @@ private static void AppMain(object o) .ConfigureAwait(false) .GetAwaiter() .GetResult(); - - Dispatcher.MainThread.Shutdown(); } + + Dispatcher.MainThread.Shutdown(); } // Required for Avalonia designer From c14c9037f349556d826045bac504345c66bfcc68 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Aug 2026 09:58:58 +0100 Subject: [PATCH 18/18] trace2: instrument authentication flows Authentication spans browser, device-code, refresh, account selection, and provider-specific decisions that are otherwise indistinguishable in a trace. Add regions and mode data at those boundaries so delays and user-flow choices can be diagnosed without recording credentials. Assisted-by: GPT-5.6 Sol Signed-off-by: Matthew John Cheetham --- .../BitbucketAuthentication.cs | 6 ++++++ src/Core/Authentication/OAuth/OAuth2Client.cs | 20 +++++++++++-------- src/GitHub/GitHubAuthentication.cs | 12 +++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/Atlassian.Bitbucket/BitbucketAuthentication.cs index 53b86a0fa9..b4e22539bb 100644 --- a/src/Atlassian.Bitbucket/BitbucketAuthentication.cs +++ b/src/Atlassian.Bitbucket/BitbucketAuthentication.cs @@ -72,6 +72,8 @@ public BitbucketAuthentication(ICommandContext context, IRegistry GetCredentialsAsync(Uri targetUri, string userName, AuthenticationModes modes) { + using var _ = Trace2.StartRegion("bitbucket", "get_creds"); + ThrowIfUserInteractionDisabled(); // If we don't have a desktop session/GUI then we cannot offer OAuth since the only @@ -250,6 +252,8 @@ private async Task GetCredentialsViaHelperAsync( public async Task CreateOAuthCredentialsAsync(GitRequest request) { + using var _ = Trace2.StartRegion("bitbucket", "oauth_browser"); + ThrowIfUserInteractionDisabled(); var browserOptions = new OAuth2WebBrowserOptions @@ -267,6 +271,8 @@ public async Task CreateOAuthCredentialsAsync(GitRequest requ public async Task RefreshOAuthCredentialsAsync(GitRequest request, string refreshToken) { + using var _ = Trace2.StartRegion("bitbucket", "oauth_refresh"); + var client = _oauth2ClientRegistry.Get(request); return await client.GetTokenByRefreshTokenAsync(refreshToken, CancellationToken.None); } diff --git a/src/Core/Authentication/OAuth/OAuth2Client.cs b/src/Core/Authentication/OAuth/OAuth2Client.cs index 587f6bfdf9..1089fae1d0 100644 --- a/src/Core/Authentication/OAuth/OAuth2Client.cs +++ b/src/Core/Authentication/OAuth/OAuth2Client.cs @@ -105,6 +105,8 @@ public IOAuth2CodeGenerator CodeGenerator public async Task GetAuthorizationCodeAsync(IEnumerable scopes, IOAuth2WebBrowser browser, IDictionary extraQueryParams, CancellationToken ct) { + using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, "get_authcode"); + string state = CodeGenerator.CreateNonce(); string codeVerifier = CodeGenerator.CreatePkceCodeVerifier(); string codeChallenge = CodeGenerator.CreatePkceCodeChallenge(OAuth2PkceChallengeMethod.Sha256, codeVerifier); @@ -167,8 +169,11 @@ public async Task GetAuthorizationCodeAsync(IEnum // Open the browser at the request URI to start the authorization code grant flow, and // intercept the response parameters delivered to the redirect URI. - IDictionary responseParams = - await browser.GetAuthenticationResponseAsync(authorizationUri, redirectUri, _responseMode, ct); + IDictionary responseParams; + using (Trace2.StartRegion(OAuth2Constants.Trace2Category, "browser")) + { + responseParams = await browser.GetAuthenticationResponseAsync(authorizationUri, redirectUri, _responseMode, ct); + } // Check for errors serious enough we should terminate the flow, such as if the state value returned does // not match the one we passed. This indicates a badly implemented Authorization Server, or worse, some @@ -196,8 +201,7 @@ public async Task GetAuthorizationCodeAsync(IEnum public async Task GetDeviceCodeAsync(IEnumerable scopes, CancellationToken ct) { - var label = "get device code"; - using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, label); + using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, "get_devicecode"); if (_endpoints.DeviceAuthorizationEndpoint is null) { @@ -234,8 +238,7 @@ public async Task GetDeviceCodeAsync(IEnumerable public async Task GetTokenByAuthorizationCodeAsync(OAuth2AuthorizationCodeResult authorizationCodeResult, CancellationToken ct) { - var label = "get token by auth code"; - using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, label); + using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, "token_by_authcode"); var formData = new Dictionary { @@ -273,8 +276,7 @@ public async Task GetTokenByAuthorizationCodeAsync(OAuth2Auth public async Task GetTokenByRefreshTokenAsync(string refreshToken, CancellationToken ct) { - var label = "get token by refresh token"; - using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, label); + using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, "token_by_refresh"); var formData = new Dictionary { @@ -306,6 +308,8 @@ public async Task GetTokenByRefreshTokenAsync(string refreshT public async Task GetTokenByDeviceCodeAsync(OAuth2DeviceCodeResult deviceCodeResult, CancellationToken ct) { + using IDisposable region = Trace2.StartRegion(OAuth2Constants.Trace2Category, "token_by_devicecode"); + var formData = new Dictionary { [OAuth2Constants.DeviceAuthorization.GrantTypeParameter] = OAuth2Constants.DeviceAuthorization.DeviceCodeGrantType, diff --git a/src/GitHub/GitHubAuthentication.cs b/src/GitHub/GitHubAuthentication.cs index a6ad4ef826..e2ad9319b1 100644 --- a/src/GitHub/GitHubAuthentication.cs +++ b/src/GitHub/GitHubAuthentication.cs @@ -72,6 +72,7 @@ public GitHubAuthentication(ICommandContext context) public async Task SelectAccountAsync(Uri targetUri, IEnumerable accounts) { + using var _ = Trace2.StartRegion("github", "select_account"); ThrowIfUserInteractionDisabled(); if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession) @@ -135,12 +136,17 @@ public async Task SelectAccountAsync(Uri targetUri, IEnumerable public async Task GetAuthenticationAsync(Uri targetUri, string userName, AuthenticationModes modes) { + using var _ = Trace2.StartRegion("github", "get_auth"); + Trace2.WriteData("github", "modes/initial", modes.ToString()); + // If we cannot start a browser then don't offer the option if (!Context.SessionManager.IsWebBrowserAvailable) { modes = modes & ~AuthenticationModes.Browser; } + Trace2.WriteData("github", "modes/available", modes.ToString()); + // We need at least one mode! if (modes == AuthenticationModes.None) { @@ -348,6 +354,8 @@ private async Task GetAuthenticationViaHelperAsync( public async Task GetTwoFactorCodeAsync(Uri targetUri, bool isSms) { + using var _ = Trace2.StartRegion("github", "get_tfa"); + ThrowIfUserInteractionDisabled(); if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession) @@ -408,6 +416,8 @@ private async Task GetTwoFactorCodeViaHelperAsync(bool isSms, string arg public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, IEnumerable scopes, string loginHint) { + using var _ = Trace2.StartRegion("github", "oauth_browser"); + ThrowIfUserInteractionDisabled(); var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri); @@ -447,6 +457,8 @@ public async Task GetOAuthTokenViaBrowserAsync(Uri targetUri, public async Task GetOAuthTokenViaDeviceCodeAsync(Uri targetUri, IEnumerable scopes) { + using var _ = Trace2.StartRegion("github", "oauth_device_code"); + ThrowIfUserInteractionDisabled(); var oauthClient = new GitHubOAuth2Client(HttpClient, Context.Settings, targetUri);