From 7cee518ca65a7d455d5c6fcbbad0402222d311b7 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Tue, 15 Nov 2022 09:46:52 -0800 Subject: [PATCH 1/9] assembly: add assemblyutils The implementation of TRACE2 tracing will require use of the TryGetAssemblyVersion method. To prepare for this, move this method out of the DiagnoseCommand class and into its own static class. --- src/shared/Core/AssemblyUtils.cs | 24 +++++++++++++++++++++ src/shared/Core/Commands/DiagnoseCommand.cs | 20 +---------------- 2 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 src/shared/Core/AssemblyUtils.cs diff --git a/src/shared/Core/AssemblyUtils.cs b/src/shared/Core/AssemblyUtils.cs new file mode 100644 index 000000000..f2d66a753 --- /dev/null +++ b/src/shared/Core/AssemblyUtils.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace GitCredentialManager; + +public static class AssemblyUtils +{ + public static bool TryGetAssemblyVersion(out string version) + { + try + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + var assemblyVersionAttribute = assembly.GetCustomAttribute(); + version = assemblyVersionAttribute is null + ? assembly.GetName().Version.ToString() + : assemblyVersionAttribute.InformationalVersion; + return true; + } + catch + { + version = null; + return false; + } + } +} diff --git a/src/shared/Core/Commands/DiagnoseCommand.cs b/src/shared/Core/Commands/DiagnoseCommand.cs index 20a646dd8..b8b4aaa56 100644 --- a/src/shared/Core/Commands/DiagnoseCommand.cs +++ b/src/shared/Core/Commands/DiagnoseCommand.cs @@ -86,7 +86,7 @@ private async Task ExecuteAsync(string output) fullLog.WriteLine($"AppPath: {_context.ApplicationPath}"); fullLog.WriteLine($"InstallDir: {_context.InstallationDirectory}"); fullLog.WriteLine( - TryGetAssemblyVersion(out string version) + AssemblyUtils.TryGetAssemblyVersion(out string version) ? $"Version: {version}" : "Version: [!] Failed to get version information [!]" ); @@ -198,24 +198,6 @@ private async Task ExecuteAsync(string output) return numFailed; } - private bool TryGetAssemblyVersion(out string version) - { - try - { - var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); - var assemblyVersionAttribute = assembly.GetCustomAttribute(); - version = assemblyVersionAttribute is null - ? assembly.GetName().Version.ToString() - : assemblyVersionAttribute.InformationalVersion; - return true; - } - catch - { - version = null; - return false; - } - } - private static class ConsoleEx { public static void WriteLineIndent(string str) From c2366f7444e93600f6421da61e3773701f663e80 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Tue, 7 Feb 2023 10:59:44 -0700 Subject: [PATCH 2/9] traceutils: add traceutils The implementation of TRACE2 tracing will require truncation of long file names just as TRACE does. To prepare for this, move this logic out of the TRACE class and into its own method in a new static TraceUtils class. Additionally, add a unit test to validate this logic. --- src/shared/Core.Tests/TraceUtilsTests.cs | 18 ++++++++++++++++++ src/shared/Core/Trace.cs | 17 +---------------- src/shared/Core/TraceUtils.cs | 24 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 src/shared/Core.Tests/TraceUtilsTests.cs create mode 100644 src/shared/Core/TraceUtils.cs diff --git a/src/shared/Core.Tests/TraceUtilsTests.cs b/src/shared/Core.Tests/TraceUtilsTests.cs new file mode 100644 index 000000000..9ab18c215 --- /dev/null +++ b/src/shared/Core.Tests/TraceUtilsTests.cs @@ -0,0 +1,18 @@ +using System; +using System.IO; +using System.Text; +using Xunit; + +namespace GitCredentialManager.Tests; + +public class TraceUtilsTests +{ + [Theory] + [InlineData("/foo/bar/baz/boo", 10, "...baz/boo")] + [InlineData("thisfileshouldbetruncated", 12, "...truncated")] + public void FormatSource_ReturnsExpectedSourceValues(string path, int sourceColumnMaxWidth, string expectedSource) + { + string actualSource = TraceUtils.FormatSource(path, sourceColumnMaxWidth); + Assert.Equal(actualSource, expectedSource); + } +} \ No newline at end of file diff --git a/src/shared/Core/Trace.cs b/src/shared/Core/Trace.cs index a6a9fc5e8..34055d16f 100644 --- a/src/shared/Core/Trace.cs +++ b/src/shared/Core/Trace.cs @@ -307,22 +307,7 @@ private static string FormatText(string message, string filePath, int lineNumber if (source.Length > sourceColumnMaxWidth) { - int idx = 0; - int maxlen = sourceColumnMaxWidth - 3; - int srclen = source.Length; - - while (idx >= 0 && (srclen - idx) > maxlen) - { - idx = source.IndexOf('\\', idx + 1); - } - - // If we cannot find a path separator which allows the path to be long enough, just truncate the file name - if (idx < 0) - { - idx = srclen - maxlen; - } - - source = "..." + source.Substring(idx); + source = TraceUtils.FormatSource(source, sourceColumnMaxWidth); } // Git's trace format is "{timestamp,-15} {source,-23} trace: {details}" diff --git a/src/shared/Core/TraceUtils.cs b/src/shared/Core/TraceUtils.cs new file mode 100644 index 000000000..a0cf9d5b5 --- /dev/null +++ b/src/shared/Core/TraceUtils.cs @@ -0,0 +1,24 @@ +namespace GitCredentialManager; + +public static class TraceUtils +{ + public static string FormatSource(string source, int sourceColumnMaxWidth) + { + int idx = 0; + int maxlen = sourceColumnMaxWidth - 3; + int srclen = source.Length; + + while (idx >= 0 && (srclen - idx) > maxlen) + { + idx = source.IndexOf('\\', idx + 1); + } + + // If we cannot find a path separator which allows the path to be long enough, just truncate the file name + if (idx < 0) + { + idx = srclen - maxlen; + } + + return "..." + source.Substring(idx); + } +} From 4adb60e670733d92d13f6b8d9744163bb4f05a81 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Tue, 31 Jan 2023 18:25:24 -0700 Subject: [PATCH 3/9] trace2: add initial functionality Add initial TRACE2 functionality, including: 1. The ability to add writers (which will eventually write to Normal, Perf, and Event format targets). 2. An abstract Trace2Message class. 3. Logic to send Trace2Messages to writers. 4. Ability to release writers prior to application exit. --- src/shared/Core/ITrace2Writer.cs | 10 +++ src/shared/Core/Trace2.cs | 109 +++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/shared/Core/ITrace2Writer.cs create mode 100644 src/shared/Core/Trace2.cs diff --git a/src/shared/Core/ITrace2Writer.cs b/src/shared/Core/ITrace2Writer.cs new file mode 100644 index 000000000..4474555cd --- /dev/null +++ b/src/shared/Core/ITrace2Writer.cs @@ -0,0 +1,10 @@ +using System; + +namespace GitCredentialManager; + +public interface ITrace2Writer : IDisposable +{ + bool Failed { get; } + + void Write(Trace2Message message); +} diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs new file mode 100644 index 000000000..f9dfc2cc8 --- /dev/null +++ b/src/shared/Core/Trace2.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace GitCredentialManager; + +/// +/// The different event types tracked in the TRACE2 tracing +/// system. +/// +public enum Trace2Event +{ } + +/// +/// Represents the application's TRACE2 tracing system. +/// +public interface ITrace2 : IDisposable +{ } + +public class Trace2 : DisposableObject, ITrace2 +{ + private readonly object _writersLock = new object(); + private List _writers = new List(); + + protected override void ReleaseManagedResources() + { + lock (_writersLock) + { + try + { + for (int i = 0; i < _writers.Count; i += 1) + { + using (var writer = _writers[i]) + { + _writers.Remove(writer); + } + } + } + catch + { + /* squelch */ + } + } + + base.ReleaseManagedResources(); + } + + private void AddWriter(ITrace2Writer writer) + { + ThrowIfDisposed(); + + lock (_writersLock) + { + // Try not to add the same writer more than once + if (_writers.Contains(writer)) + return; + + _writers.Add(writer); + } + } + + private void WriteMessage(Trace2Message message) + { + ThrowIfDisposed(); + + lock (_writersLock) + { + if (_writers.Count == 0) + { + return; + } + + foreach (var writer in _writers) + { + if (!writer.Failed) + { + writer.Write(message); + } + } + } + } +} + +public abstract class Trace2Message +{ + private const int SourceColumnMaxWidth = 23; + protected const string TimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffff'Z'"; + + [JsonProperty("event", Order = 1)] + public Trace2Event Event { get; set; } + + [JsonProperty("sid", Order = 2)] + public string Sid { get; set; } + + [JsonProperty("thread", Order = 3)] + public string Thread { get; set; } + + [JsonProperty("time", Order = 4)] + public DateTimeOffset Time { get; set; } + + [JsonProperty("file", Order = 5)] + public string File { get; set; } + + [JsonProperty("line", Order = 6)] + public int Line { get; set; } +} From 9642434335f2bbf918e18852857c7dbb65e70286 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Thu, 2 Feb 2023 14:08:28 -0700 Subject: [PATCH 4/9] trace2: add collectorwriter Add the Trace2CollectorWriter class to accept Trace2 messages in the event target format and write them to the OTel Collector/Telemetry Service. --- src/shared/Core/Trace2.cs | 2 + src/shared/Core/Trace2CollectorWriter.cs | 130 +++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/shared/Core/Trace2CollectorWriter.cs diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs index f9dfc2cc8..95d5c6bc4 100644 --- a/src/shared/Core/Trace2.cs +++ b/src/shared/Core/Trace2.cs @@ -106,4 +106,6 @@ public abstract class Trace2Message [JsonProperty("line", Order = 6)] public int Line { get; set; } + + public abstract string ToJson(); } diff --git a/src/shared/Core/Trace2CollectorWriter.cs b/src/shared/Core/Trace2CollectorWriter.cs new file mode 100644 index 000000000..500245e03 --- /dev/null +++ b/src/shared/Core/Trace2CollectorWriter.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO.Pipes; +using System.Text; +using System.Threading; +using KnownGitCfg = GitCredentialManager.Constants.GitConfiguration; + +namespace GitCredentialManager +{ + /// + /// Accepts string messages from multiple threads and dispatches them over a named pipe from a + /// background thread. + /// + public class Trace2CollectorWriter : DisposableObject, ITrace2Writer + { + private const int DefaultMaxQueueSize = 256; + + private readonly Func _createPipeFunc; + private readonly BlockingCollection _queue; + + private Thread _writerThread; + private NamedPipeClientStream _pipeClient; + + public bool Failed { get; private set; } + + public Trace2CollectorWriter(Func createPipeFunc, + int maxQueueSize = DefaultMaxQueueSize) + { + EnsureArgument.NotNull(createPipeFunc, nameof(createPipeFunc)); + EnsureArgument.Positive(maxQueueSize, nameof(maxQueueSize)); + + _createPipeFunc = createPipeFunc; + _queue = new BlockingCollection(new ConcurrentQueue(), boundedCapacity: maxQueueSize); + + Start(); + } + + public void Write(Trace2Message message) + { + _queue.TryAdd(message.ToJson()); + } + + protected override void ReleaseManagedResources() + { + Stop(); + + _pipeClient.Dispose(); + _queue.Dispose(); + base.ReleaseManagedResources(); + + _pipeClient = null; + _writerThread = null; + } + + private void Start() + { + try + { + _writerThread = new Thread(BackgroundWriterThreadProc) + { + Name = nameof(Trace2CollectorWriter), + IsBackground = true + }; + + _writerThread.Start(); + // Create a new pipe stream instance using the provided factory + _pipeClient = _createPipeFunc(); + + // Specify an instantaneous timeout because we don't want to hold up the + // background thread loop if the pipe is not available. + _pipeClient.Connect(timeout: 0); + } + catch + { + // Start failed. Disable this writer for this run. + Failed = true; + } + } + + private void Stop() + { + if (_queue.IsAddingCompleted) + { + return; + } + + // Signal to the queue draining thread that it should drain once more and then terminate. + _queue.CompleteAdding(); + _writerThread.Join(); + ReleaseManagedResources(); + } + + private void BackgroundWriterThreadProc() + { + // Drain the queue of all messages currently in the queue. + // TryTake() using an infinite timeout will block until either a message is available (returns true) + // or the queue has been marked as completed _and_ is empty (returns false). + while (_queue.TryTake(out string message, Timeout.Infinite)) + { + if (message != null) + { + WriteMessage(message); + } + } + } + + private void WriteMessage(string message) + { + try + { + // We should signal the end of each message with a line-feed (LF) character. + if (!message.EndsWith("\n")) + { + message += '\n'; + } + + byte[] data = Encoding.UTF8.GetBytes(message); + _pipeClient.Write(data, 0, data.Length); + _pipeClient.Flush(); + } + catch + { + // We can't send this message for some reason (e.g., broken pipe); we attempt no recovery or retry + // mechanism but rather disable the writer for the rest of this run. + Failed = true; + } + } + } +} From f6736ac8d2187916d52ea680aef42ac36aaf71c8 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Tue, 7 Feb 2023 11:01:39 -0700 Subject: [PATCH 5/9] trace2: add streamwriter Add Trace2StreamWriter class which will be used to write to stderr or a file, based on the TextWriter that is passed to it. It will also adapt the output format based on the format target that is passed to it. --- src/shared/Core/Trace2.cs | 2 + src/shared/Core/Trace2StreamWriter.cs | 65 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/shared/Core/Trace2StreamWriter.cs diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs index 95d5c6bc4..f666e00cd 100644 --- a/src/shared/Core/Trace2.cs +++ b/src/shared/Core/Trace2.cs @@ -108,4 +108,6 @@ public abstract class Trace2Message public int Line { get; set; } public abstract string ToJson(); + + public abstract string ToNormalString(); } diff --git a/src/shared/Core/Trace2StreamWriter.cs b/src/shared/Core/Trace2StreamWriter.cs new file mode 100644 index 000000000..2cb6ca60b --- /dev/null +++ b/src/shared/Core/Trace2StreamWriter.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; + +namespace GitCredentialManager; + +/// +/// The different format targets supported in the TRACE2 tracing +/// system. +/// +public enum Trace2FormatTarget +{ + Event, + Normal +} + +public class Trace2StreamWriter : DisposableObject, ITrace2Writer +{ + private readonly TextWriter _writer; + private readonly Trace2FormatTarget _formatTarget; + + public bool Failed { get; private set; } + + public Trace2StreamWriter(TextWriter writer, Trace2FormatTarget formatTarget) + { + _writer = writer; + _formatTarget = formatTarget; + } + + public void Write(Trace2Message message) + { + try + { + _writer.Write(Format(message)); + _writer.Write('\n'); + _writer.Flush(); + } + catch + { + Failed = true; + } + } + + protected override void ReleaseManagedResources() + { + _writer.Dispose(); + base.ReleaseManagedResources(); + } + + private string Format(Trace2Message message) + { + EnsureArgument.NotNull(message, nameof(message)); + + switch (_formatTarget) + { + case Trace2FormatTarget.Event: + return message.ToJson(); + case Trace2FormatTarget.Normal: + return message.ToNormalString(); + default: + Console.WriteLine($"warning: unrecognized format target '{_formatTarget}', disabling TRACE2 tracing."); + Failed = true; + return ""; + } + } +} From 1790779667c8c4f679e00ffb7aa0e836848f79b2 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Thu, 2 Feb 2023 14:15:00 -0700 Subject: [PATCH 6/9] trace2: add sid A key component of Git's TRACE2 tracing system is the session id (sid). This identifies the process instance to allow all events emitted by it to be identified. We check to see if a parent sid (i.e. from a Git process) exists. If so, we separate this sid from the GCM sid (a GUID) using a "/". If there is no parent sid, we simply generate a GUID for the GCM sid and use that. The above also requires addition of a new SetEnvironmentVariable() method in EnvironmentBase.cs to set the GCM-specific SID variable. --- src/shared/Core.Tests/EnvironmentTests.cs | 32 +++++++++++++++++++ src/shared/Core.Tests/Trace2Tests.cs | 24 ++++++++++++++ src/shared/Core/EnvironmentBase.cs | 19 +++++++++++ .../Core/Interop/Posix/PosixEnvironment.cs | 8 ++--- .../Interop/Windows/WindowsEnvironment.cs | 8 ++--- src/shared/Core/Trace2.cs | 27 ++++++++++++++++ .../Objects/TestEnvironment.cs | 8 +++++ 7 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 src/shared/Core.Tests/Trace2Tests.cs diff --git a/src/shared/Core.Tests/EnvironmentTests.cs b/src/shared/Core.Tests/EnvironmentTests.cs index 9c8eae028..bd7a8c99b 100644 --- a/src/shared/Core.Tests/EnvironmentTests.cs +++ b/src/shared/Core.Tests/EnvironmentTests.cs @@ -150,5 +150,37 @@ public void MacOSEnvironment_TryLocateExecutable_Paths_Are_Ignored() Assert.True(actualResult); Assert.Equal(expectedPath, actualPath); } + + [PlatformFact(Platforms.Posix)] + public void PosixEnvironment_SetEnvironmentVariable_Sets_Expected_Value() + { + var variable = "FOO_BAR"; + var value = "baz"; + + var fs = new TestFileSystem(); + var envars = new Dictionary(); + var env = new PosixEnvironment(fs, envars); + + env.SetEnvironmentVariable(variable, value); + + Assert.Contains(env.Variables, item + => item.Key.Equals(variable) && item.Value.Equals(value)); + } + + [PlatformFact(Platforms.Windows)] + public void WindowsEnvironment_SetEnvironmentVariable_Sets_Expected_Value() + { + var variable = "FOO_BAR"; + var value = "baz"; + + var fs = new TestFileSystem(); + var envars = new Dictionary(); + var env = new WindowsEnvironment(fs, envars); + + env.SetEnvironmentVariable(variable, value); + + Assert.Contains(env.Variables, item + => item.Key.Equals(variable) && item.Value.Equals(value)); + } } } diff --git a/src/shared/Core.Tests/Trace2Tests.cs b/src/shared/Core.Tests/Trace2Tests.cs new file mode 100644 index 000000000..bcda67867 --- /dev/null +++ b/src/shared/Core.Tests/Trace2Tests.cs @@ -0,0 +1,24 @@ +using System.Text.RegularExpressions; +using GitCredentialManager.Tests.Objects; +using Xunit; + +namespace GitCredentialManager.Tests; + +public class Trace2Tests +{ + [Theory] + [InlineData("20190408T191610.507018Z-H9b68c35f-P000059a8")] + [InlineData("")] + public void SetSid_Envar_Returns_Expected_Value(string parentSid) + { + Regex rx = new Regex(@$"{parentSid}\/[\d\w-]*"); + + var environment = new TestEnvironment(); + environment.Variables.Add("GIT_TRACE2_PARENT_SID", parentSid); + + var trace2 = new Trace2(environment); + var sid = trace2.SetSid(); + + Assert.Matches(rx, sid); + } +} diff --git a/src/shared/Core/EnvironmentBase.cs b/src/shared/Core/EnvironmentBase.cs index a2aa36cf2..63790589a 100644 --- a/src/shared/Core/EnvironmentBase.cs +++ b/src/shared/Core/EnvironmentBase.cs @@ -56,6 +56,15 @@ public interface IEnvironment /// Working directory for the new process. /// object ready to start. Process CreateProcess(string path, string args, bool useShellExecute, string workingDirectory); + + /// + /// Set an environment variable at the specified target level. + /// + /// Name of the environment variable to set. + /// Value of the environment variable to set. + /// Target level of environment variable to set (Machine, Process, or User). + void SetEnvironmentVariable(string variable, string value, + EnvironmentVariableTarget target = EnvironmentVariableTarget.Process); } public abstract class EnvironmentBase : IEnvironment @@ -141,6 +150,16 @@ internal virtual bool TryLocateExecutable(string program, ICollection pa path = null; return false; } + + public void SetEnvironmentVariable(string variable, string value, + EnvironmentVariableTarget target = EnvironmentVariableTarget.Process) + { + if (Variables.Keys.Contains(variable)) return; + Environment.SetEnvironmentVariable(variable, value, target); + Variables = GetCurrentVariables(); + } + + protected abstract IReadOnlyDictionary GetCurrentVariables(); } public static class EnvironmentExtensions diff --git a/src/shared/Core/Interop/Posix/PosixEnvironment.cs b/src/shared/Core/Interop/Posix/PosixEnvironment.cs index da2e76c72..c725c18e1 100644 --- a/src/shared/Core/Interop/Posix/PosixEnvironment.cs +++ b/src/shared/Core/Interop/Posix/PosixEnvironment.cs @@ -1,18 +1,18 @@ using System; using System.Collections.Generic; +using System.Linq; namespace GitCredentialManager.Interop.Posix { public class PosixEnvironment : EnvironmentBase { public PosixEnvironment(IFileSystem fileSystem) - : this(fileSystem, GetCurrentVariables()) { } + : this(fileSystem, null) { } internal PosixEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables) : base(fileSystem) { - EnsureArgument.NotNull(variables, nameof(variables)); - Variables = variables; + Variables = variables ?? GetCurrentVariables(); } #region EnvironmentBase @@ -34,7 +34,7 @@ protected override string[] SplitPathVariable(string value) #endregion - private static IReadOnlyDictionary GetCurrentVariables() + protected override IReadOnlyDictionary GetCurrentVariables() { var dict = new Dictionary(); var variables = Environment.GetEnvironmentVariables(); diff --git a/src/shared/Core/Interop/Windows/WindowsEnvironment.cs b/src/shared/Core/Interop/Windows/WindowsEnvironment.cs index b85979d66..67aea7d64 100644 --- a/src/shared/Core/Interop/Windows/WindowsEnvironment.cs +++ b/src/shared/Core/Interop/Windows/WindowsEnvironment.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; namespace GitCredentialManager.Interop.Windows @@ -9,13 +10,12 @@ namespace GitCredentialManager.Interop.Windows public class WindowsEnvironment : EnvironmentBase { public WindowsEnvironment(IFileSystem fileSystem) - : this(fileSystem, GetCurrentVariables()) { } + : this(fileSystem, null) { } internal WindowsEnvironment(IFileSystem fileSystem, IReadOnlyDictionary variables) : base(fileSystem) { - EnsureArgument.NotNull(variables, nameof(variables)); - Variables = variables; + Variables = variables ?? GetCurrentVariables(); } #region EnvironmentBase @@ -84,7 +84,7 @@ public override Process CreateProcess(string path, string args, bool useShellExe #endregion - private static IReadOnlyDictionary GetCurrentVariables() + protected override IReadOnlyDictionary GetCurrentVariables() { // On Windows it is technically possible to get env vars which differ only by case // even though the general assumption is that they are case insensitive on Windows. diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs index f666e00cd..bbcfca346 100644 --- a/src/shared/Core/Trace2.cs +++ b/src/shared/Core/Trace2.cs @@ -25,6 +25,17 @@ public class Trace2 : DisposableObject, ITrace2 private readonly object _writersLock = new object(); private List _writers = new List(); + private const string GitSidVariable = "GIT_TRACE2_PARENT_SID"; + + private IEnvironment _environment; + private string _sid; + + public Trace2(IEnvironment environment) + { + _environment = environment; + _sid = SetSid(); + } + protected override void ReleaseManagedResources() { lock (_writersLock) @@ -62,6 +73,22 @@ private void AddWriter(ITrace2Writer writer) } } + internal string SetSid() + { + var sids = new List(); + if (_environment.Variables.TryGetValue(GitSidVariable, out string parentSid)) + { + sids.Add(parentSid); + } + + // Add GCM "child" sid + sids.Add(Guid.NewGuid().ToString("D")); + var combinedSid = string.Join("/", sids); + + _environment.SetEnvironmentVariable(GitSidVariable, combinedSid); + return combinedSid; + } + private void WriteMessage(Trace2Message message) { ThrowIfDisposed(); diff --git a/src/shared/TestInfrastructure/Objects/TestEnvironment.cs b/src/shared/TestInfrastructure/Objects/TestEnvironment.cs index 645c97a10..037745832 100644 --- a/src/shared/TestInfrastructure/Objects/TestEnvironment.cs +++ b/src/shared/TestInfrastructure/Objects/TestEnvironment.cs @@ -116,6 +116,14 @@ public Process CreateProcess(string path, string args, bool useShellExecute, str return new Process { StartInfo = psi }; } + + public void SetEnvironmentVariable(string variable, string value, + EnvironmentVariableTarget target = EnvironmentVariableTarget.Process) + { + if (Variables.Keys.Contains(variable)) return; + Environment.SetEnvironmentVariable(variable, value, target); + Variables.Add(variable, value); + } #endregion } From 8dca18bd08cddf981587720e61d13f3bc9731335 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Wed, 8 Feb 2023 13:17:46 -0700 Subject: [PATCH 7/9] trace2: detect event/normal formats Add the infrastructure to detect whether a user has enabled the TRACE2 event format or normal format targets. This implementation has been designed to be extended to include the perf format target in a future series. Additionally it involved some refactoring/cleanup to set the Application Path and InstallationDirectory in the CommandContext, rather than in GCM/UI helper Program.cs files. --- .../Program.cs | 6 +- .../Core.Tests/StringExtensionsTests.cs | 1 + src/shared/Core.Tests/Trace2Tests.cs | 36 ++++- src/shared/Core/ApplicationBase.cs | 17 +-- src/shared/Core/CommandContext.cs | 35 ++++- src/shared/Core/Constants.cs | 9 ++ src/shared/Core/Settings.cs | 25 ++++ src/shared/Core/StringExtensions.cs | 12 ++ src/shared/Core/Trace2.cs | 125 +++++++++++++++--- .../Program.cs | 5 +- src/shared/Git-Credential-Manager/Program.cs | 19 ++- src/shared/GitHub.UI.Avalonia/Program.cs | 5 +- src/shared/GitLab.UI.Avalonia/Program.cs | 6 +- .../TestInfrastructure/Objects/NullTrace.cs | 16 +++ .../Objects/TestCommandContext.cs | 7 +- .../Objects/TestSettings.cs | 10 ++ .../Atlassian.Bitbucket.UI.Windows/Program.cs | 5 +- .../Program.cs | 5 +- src/windows/GitHub.UI.Windows/Program.cs | 5 +- src/windows/GitLab.UI.Windows/Program.cs | 5 +- 20 files changed, 279 insertions(+), 75 deletions(-) diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs index bfec51c08..ef4a5473f 100644 --- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs +++ b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs @@ -1,5 +1,5 @@ using System; -using System.CommandLine; +using System.Diagnostics; using System.Threading; using Atlassian.Bitbucket.UI.Commands; using Atlassian.Bitbucket.UI.Controls; @@ -45,9 +45,7 @@ private static void AppMain(object o) { string[] args = (string[]) o; - string appPath = ApplicationBase.GetEntryApplicationPath(); - string installDir = ApplicationBase.GetInstallationDirectory(); - using (var context = new CommandContext(appPath, installDir)) + using (var context = new CommandContext(args)) using (var app = new HelperApplication(context)) { app.RegisterCommand(new CredentialsCommandImpl(context)); diff --git a/src/shared/Core.Tests/StringExtensionsTests.cs b/src/shared/Core.Tests/StringExtensionsTests.cs index ba9c32ccc..24fb99b21 100644 --- a/src/shared/Core.Tests/StringExtensionsTests.cs +++ b/src/shared/Core.Tests/StringExtensionsTests.cs @@ -245,6 +245,7 @@ public void StringExtensions_TrimUntilLastIndexOf_Character_Null_ThrowsArgumentN [InlineData("foo://", "://", "")] [InlineData("foo://bar", "://", "bar")] [InlineData("foo://bar/", "://", "bar/")] + [InlineData("foo:/bar/baz", ":", "/bar/baz")] public void StringExtensions_TrimUntilLastIndexOf_String(string input, string trim, string expected) { string actual = StringExtensions.TrimUntilLastIndexOf(input, trim); diff --git a/src/shared/Core.Tests/Trace2Tests.cs b/src/shared/Core.Tests/Trace2Tests.cs index bcda67867..da3d6d95a 100644 --- a/src/shared/Core.Tests/Trace2Tests.cs +++ b/src/shared/Core.Tests/Trace2Tests.cs @@ -1,3 +1,4 @@ +using System; using System.Text.RegularExpressions; using GitCredentialManager.Tests.Objects; using Xunit; @@ -6,6 +7,38 @@ namespace GitCredentialManager.Tests; public class Trace2Tests { + [PlatformTheory(Platforms.Posix)] + [InlineData("af_unix:foo", "foo")] + [InlineData("af_unix:stream:foo-bar", "foo-bar")] + [InlineData("af_unix:dgram:foo-bar-baz", "foo-bar-baz")] + public void TryParseEventTarget_Posix_Returns_Expected_Value(string input, string expected) + { + var environment = new TestEnvironment(); + var settings = new TestSettings(); + + var trace2 = new Trace2(environment, settings.GetTrace2Settings(), new []{""}, DateTimeOffset.UtcNow); + var isSuccessful = trace2.TryGetPipeName(input, out var actual); + + Assert.True(isSuccessful); + Assert.Matches(actual, expected); + } + + [PlatformTheory(Platforms.Windows)] + [InlineData("\\\\.\\pipe\\git-foo", "git-foo")] + [InlineData("\\\\.\\pipe\\git-foo-bar", "git-foo-bar")] + [InlineData("\\\\.\\pipe\\foo\\git-bar", "git-bar")] + public void TryParseEventTarget_Windows_Returns_Expected_Value(string input, string expected) + { + var environment = new TestEnvironment(); + var settings = new TestSettings(); + + var trace2 = new Trace2(environment, settings.GetTrace2Settings(), new []{""}, DateTimeOffset.UtcNow); + var isSuccessful = trace2.TryGetPipeName(input, out var actual); + + Assert.True(isSuccessful); + Assert.Matches(actual, expected); + } + [Theory] [InlineData("20190408T191610.507018Z-H9b68c35f-P000059a8")] [InlineData("")] @@ -16,7 +49,8 @@ public void SetSid_Envar_Returns_Expected_Value(string parentSid) var environment = new TestEnvironment(); environment.Variables.Add("GIT_TRACE2_PARENT_SID", parentSid); - var trace2 = new Trace2(environment); + var settings = new TestSettings(); + var trace2 = new Trace2(environment, settings.GetTrace2Settings(), new []{""}, DateTimeOffset.UtcNow); var sid = trace2.SetSid(); Assert.Matches(rx, sid); diff --git a/src/shared/Core/ApplicationBase.cs b/src/shared/Core/ApplicationBase.cs index c2e1c05b6..f5e2e25db 100644 --- a/src/shared/Core/ApplicationBase.cs +++ b/src/shared/Core/ApplicationBase.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; +using System.IO.Pipes; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -75,6 +77,9 @@ public Task RunAsync(string[] args) Context.Trace.WriteLine("Tracing of secrets is enabled. Trace output may contain sensitive information."); } + // Enable TRACE2 tracing + Context.Trace2.Start(Context.Streams.Error, Context.FileSystem, Context.ApplicationPath); + return RunInternalAsync(args); } @@ -82,18 +87,6 @@ public Task RunAsync(string[] args) #region Helpers - public static string GetEntryApplicationPath() - { - return PlatformUtils.GetNativeEntryPath() ?? - Process.GetCurrentProcess().MainModule?.FileName ?? - Environment.GetCommandLineArgs()[0]; - } - - public static string GetInstallationDirectory() - { - return AppContext.BaseDirectory; - } - /// /// Wait until a debugger has attached to the currently executing process. /// diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index 0ccd7bc33..d8a45fcb6 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using GitCredentialManager.Interop.Linux; using GitCredentialManager.Interop.MacOS; @@ -15,7 +17,7 @@ public interface ICommandContext : IDisposable /// /// Absolute path the application entry executable. /// - string ApplicationPath { get; } + string ApplicationPath { get; set; } /// /// Absolute path to the Git Credential Manager installation directory. @@ -47,6 +49,11 @@ public interface ICommandContext : IDisposable /// ITrace Trace { get; } + /// + /// Application TRACE2 tracing system. + /// + ITrace2 Trace2 { get; } + /// /// File system abstraction (exists mainly for testing). /// @@ -78,12 +85,11 @@ public interface ICommandContext : IDisposable /// public class CommandContext : DisposableObject, ICommandContext { - public CommandContext(string appPath, string installDir) + public CommandContext(string[] argv) { - EnsureArgument.NotNullOrWhiteSpace(appPath, nameof (appPath)); - - ApplicationPath = appPath; - InstallationDirectory = installDir; + var applicationStartTime = DateTimeOffset.UtcNow; + ApplicationPath = GetEntryApplicationPath(); + InstallationDirectory = GetInstallationDirectory(); Streams = new StandardStreams(); Trace = new Trace(); @@ -139,6 +145,7 @@ public CommandContext(string appPath, string installDir) throw new PlatformNotSupportedException(); } + Trace2 = new Trace2(Environment, Settings.GetTrace2Settings(), argv, applicationStartTime); HttpClientFactory = new HttpClientFactory(FileSystem, Trace, Settings, Streams); CredentialStore = new CredentialStore(this); } @@ -177,7 +184,7 @@ private static string GetGitPath(IEnvironment environment, IFileSystem fileSyste #region ICommandContext - public string ApplicationPath { get; } + public string ApplicationPath { get; set; } public string InstallationDirectory { get; } @@ -191,6 +198,8 @@ private static string GetGitPath(IEnvironment environment, IFileSystem fileSyste public ITrace Trace { get; } + public ITrace2 Trace2 { get; } + public IFileSystem FileSystem { get; } public ICredentialStore CredentialStore { get; } @@ -214,5 +223,17 @@ protected override void ReleaseManagedResources() } #endregion + + public static string GetEntryApplicationPath() + { + return PlatformUtils.GetNativeEntryPath() ?? + Process.GetCurrentProcess().MainModule?.FileName ?? + System.Environment.GetCommandLineArgs()[0]; + } + + public static string GetInstallationDirectory() + { + return AppContext.BaseDirectory; + } } } diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index b8c9fa750..6fe006716 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -54,6 +54,8 @@ public static class EnvironmentVariables public const string GcmAuthority = "GCM_AUTHORITY"; public const string GitTerminalPrompts = "GIT_TERMINAL_PROMPT"; public const string GcmAllowWia = "GCM_ALLOW_WINDOWSAUTH"; + public const string GitTrace2Event = "GIT_TRACE2_EVENT"; + public const string GitTrace2Normal = "GIT_TRACE2"; /* * Unlike other environment variables, these proxy variables are normally lowercase only. @@ -164,6 +166,13 @@ public static class Remote public const string FetchUrl = "url"; public const string PushUrl = "pushUrl"; } + + public static class Trace2 + { + public const string SectionName = "trace2"; + public const string EventTarget = "eventtarget"; + public const string NormalTarget = "normaltarget"; + } } public static class WindowsRegistry diff --git a/src/shared/Core/Settings.cs b/src/shared/Core/Settings.cs index 12b104515..3eafa0baa 100644 --- a/src/shared/Core/Settings.cs +++ b/src/shared/Core/Settings.cs @@ -166,6 +166,12 @@ public interface ISettings : IDisposable /// of host provider auto-detection. Use a zero or negative value to disable probing. /// int AutoDetectProviderTimeout { get; } + + /// + /// Get TRACE2 settings. + /// + /// TRACE2 settings object. + Trace2Settings GetTrace2Settings(); } public class ProxyConfiguration @@ -504,6 +510,25 @@ public bool IsInteractionAllowed public bool GetTracingEnabled(out string value) => _environment.Variables.TryGetValue(KnownEnvars.GcmTrace, out value) && !value.IsFalsey(); + 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); + } + + return settings; + } + public bool IsSecretTracingEnabled => _environment.Variables.GetBooleanyOrDefault(KnownEnvars.GcmTraceSecrets, false); public bool IsMsalTracingEnabled => _environment.Variables.GetBooleanyOrDefault(Constants.EnvironmentVariables.GcmTraceMsAuth, false); diff --git a/src/shared/Core/StringExtensions.cs b/src/shared/Core/StringExtensions.cs index caea3e9b3..5c9a37455 100644 --- a/src/shared/Core/StringExtensions.cs +++ b/src/shared/Core/StringExtensions.cs @@ -228,5 +228,17 @@ public static string TrimMiddle(this string str, string value, StringComparison return str; } + + /// + /// Check whether string contains a specified substring. + /// + /// String to check. + /// String to locate. + /// Comparison rule for comparing the strings. + /// True if the string contains the substring, false if not. + public static bool Contains(this string str, string value, StringComparison comparisonType) + { + return str?.IndexOf(value, comparisonType) >= 0; + } } } diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs index bbcfca346..8302b1350 100644 --- a/src/shared/Core/Trace2.cs +++ b/src/shared/Core/Trace2.cs @@ -1,6 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; using System.Linq; +using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; @@ -14,28 +18,56 @@ namespace GitCredentialManager; public enum Trace2Event { } +public class Trace2Settings +{ + public IDictionary FormatTargetsAndValues { get; set; } = + new Dictionary(); +} + /// /// Represents the application's TRACE2 tracing system. /// public interface ITrace2 : IDisposable -{ } +{ + /// + /// Initialize TRACE2 tracing by setting up any configured target formats and + /// writing Version and Start events. + /// + /// The standard error text stream connected back to the calling process. + /// File system abstraction. + /// The path to the GCM application. + void Start(TextWriter error, IFileSystem fileSystem, string appPath); +} public class Trace2 : DisposableObject, ITrace2 { private readonly object _writersLock = new object(); - private List _writers = new List(); + private readonly Encoding _utf8NoBomEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); private const string GitSidVariable = "GIT_TRACE2_PARENT_SID"; + private List _writers = new List(); private IEnvironment _environment; + private Trace2Settings _settings; + private string[] _argv; + private DateTimeOffset _applicationStartTime; private string _sid; - public Trace2(IEnvironment environment) + public Trace2(IEnvironment environment, Trace2Settings settings, string[] argv, DateTimeOffset applicationStartTime) { _environment = environment; + _settings = settings; + _argv = argv; + _applicationStartTime = applicationStartTime; + _sid = SetSid(); } + public void Start(TextWriter error, IFileSystem fileSystem, string appPath) + { + TryParseSettings(error, fileSystem); + } + protected override void ReleaseManagedResources() { lock (_writersLock) @@ -59,20 +91,6 @@ protected override void ReleaseManagedResources() base.ReleaseManagedResources(); } - private void AddWriter(ITrace2Writer writer) - { - ThrowIfDisposed(); - - lock (_writersLock) - { - // Try not to add the same writer more than once - if (_writers.Contains(writer)) - return; - - _writers.Add(writer); - } - } - internal string SetSid() { var sids = new List(); @@ -89,6 +107,77 @@ internal string SetSid() return combinedSid; } + internal bool TryGetPipeName(string eventTarget, out string name) + { + // Use prefixes to determine whether target is a named pipe/socket + if (eventTarget.Contains("af_unix:", StringComparison.OrdinalIgnoreCase) || + eventTarget.Contains("\\\\.\\pipe\\", StringComparison.OrdinalIgnoreCase) || + eventTarget.Contains("/./pipe/", StringComparison.OrdinalIgnoreCase)) + { + name = PlatformUtils.IsWindows() + ? eventTarget.TrimUntilLastIndexOf("\\") + : eventTarget.TrimUntilLastIndexOf(":"); + return true; + } + + name = ""; + return false; + } + + private void TryParseSettings(TextWriter error, IFileSystem fileSystem) + { + // Set up the correct writer for every enabled format target. + foreach (var formatTarget in _settings.FormatTargetsAndValues) + { + if (TryGetPipeName(formatTarget.Value, out string name)) // Write to named pipe/socket + { + AddWriter(new Trace2CollectorWriter(( + () => new NamedPipeClientStream(".", name, + PipeDirection.Out, + PipeOptions.Asynchronous) + ) + )); + } + else if (formatTarget.Value.IsTruthy()) // Write to stderr + { + AddWriter(new Trace2StreamWriter(error, formatTarget.Key)); + } + else if (Path.IsPathRooted(formatTarget.Value)) // Write to file + { + try + { + Stream stream = fileSystem.OpenFileStream(formatTarget.Value, FileMode.Append, + FileAccess.Write, FileShare.ReadWrite); + AddWriter(new Trace2StreamWriter(new StreamWriter(stream, _utf8NoBomEncoding, + 4096, leaveOpen: false), formatTarget.Key)); + } + catch (Exception ex) + { + error.WriteLine($"warning: unable to trace to file '{formatTarget.Value}': {ex.Message}"); + } + } + } + + if (_writers.Count == 0) + { + error.WriteLine("warning: unable to set up TRACE2 tracing. No traces will be written."); + } + } + + private void AddWriter(ITrace2Writer writer) + { + ThrowIfDisposed(); + + lock (_writersLock) + { + // Try not to add the same writer more than once + if (_writers.Contains(writer)) + return; + + _writers.Add(writer); + } + } + private void WriteMessage(Trace2Message message) { ThrowIfDisposed(); @@ -113,7 +202,6 @@ private void WriteMessage(Trace2Message message) public abstract class Trace2Message { - private const int SourceColumnMaxWidth = 23; protected const string TimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffff'Z'"; [JsonProperty("event", Order = 1)] @@ -129,6 +217,7 @@ public abstract class Trace2Message public DateTimeOffset Time { get; set; } [JsonProperty("file", Order = 5)] + public string File { get; set; } [JsonProperty("line", Order = 6)] diff --git a/src/shared/Git-Credential-Manager.UI.Avalonia/Program.cs b/src/shared/Git-Credential-Manager.UI.Avalonia/Program.cs index 9328e0847..4fa10db4a 100644 --- a/src/shared/Git-Credential-Manager.UI.Avalonia/Program.cs +++ b/src/shared/Git-Credential-Manager.UI.Avalonia/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading; using Avalonia; using GitCredentialManager.UI.Commands; @@ -42,9 +43,7 @@ private static void AppMain(object o) { string[] args = (string[]) o; - string appPath = ApplicationBase.GetEntryApplicationPath(); - string installDir = ApplicationBase.GetInstallationDirectory(); - using (var context = new CommandContext(appPath, installDir)) + using (var context = new CommandContext(args)) using (var app = new HelperApplication(context)) { app.RegisterCommand(new CredentialsCommandImpl(context)); diff --git a/src/shared/Git-Credential-Manager/Program.cs b/src/shared/Git-Credential-Manager/Program.cs index 14f20d13a..973726df5 100644 --- a/src/shared/Git-Credential-Manager/Program.cs +++ b/src/shared/Git-Credential-Manager/Program.cs @@ -11,9 +11,7 @@ public static class Program { public static void Main(string[] args) { - string appPath = ApplicationBase.GetEntryApplicationPath(); - string installDir = ApplicationBase.GetInstallationDirectory(); - using (var context = new CommandContext(appPath, installDir)) + using (var context = new CommandContext(args)) using (var app = new Application(context)) { // Workaround for https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/2560 @@ -31,7 +29,7 @@ public static void Main(string[] args) ); } } - + // // Git Credential Manager's executable used to be named "git-credential-manager-core" before // dropping the "-core" suffix. In order to prevent "helper not found" errors for users who @@ -43,18 +41,19 @@ public static void Main(string[] args) // // On UNIX systems we do the same check, except instead of a copy we use a symlink. // - - if (!string.IsNullOrWhiteSpace(appPath)) + if (!string.IsNullOrWhiteSpace(context.ApplicationPath)) { // Trim any (.exe) file extension if we're on Windows // Note that in some circumstances (like being called by Git when config is set // to just `helper = manager-core`) we don't always have ".exe" at the end. - if (PlatformUtils.IsWindows() && appPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + if (PlatformUtils.IsWindows() && context.ApplicationPath.EndsWith(".exe", + StringComparison.OrdinalIgnoreCase)) { - appPath = appPath.Substring(0, appPath.Length - 4); + context.ApplicationPath = context.ApplicationPath + .Substring(0, context.ApplicationPath.Length - 4); } - - if (appPath.EndsWith("git-credential-manager-core", StringComparison.OrdinalIgnoreCase)) + if (context.ApplicationPath.EndsWith("git-credential-manager-core", + StringComparison.OrdinalIgnoreCase)) { context.Streams.Error.WriteLine( "warning: git-credential-manager-core was renamed to git-credential-manager"); diff --git a/src/shared/GitHub.UI.Avalonia/Program.cs b/src/shared/GitHub.UI.Avalonia/Program.cs index 50e48dc46..8c429415d 100644 --- a/src/shared/GitHub.UI.Avalonia/Program.cs +++ b/src/shared/GitHub.UI.Avalonia/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading; using Avalonia; using GitHub.UI.Controls; @@ -44,9 +45,7 @@ private static void AppMain(object o) { string[] args = (string[]) o; - string appPath = ApplicationBase.GetEntryApplicationPath(); - string installDir = ApplicationBase.GetInstallationDirectory(); - using (var context = new CommandContext(appPath, installDir)) + using (var context = new CommandContext(args)) using (var app = new HelperApplication(context)) { app.RegisterCommand(new CredentialsCommandImpl(context)); diff --git a/src/shared/GitLab.UI.Avalonia/Program.cs b/src/shared/GitLab.UI.Avalonia/Program.cs index e88a67f13..91d98a1c2 100644 --- a/src/shared/GitLab.UI.Avalonia/Program.cs +++ b/src/shared/GitLab.UI.Avalonia/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading; using Avalonia; using GitLab.UI.Controls; @@ -43,10 +44,7 @@ public static void Main(string[] args) private static void AppMain(object o) { string[] args = (string[]) o; - - string appPath = ApplicationBase.GetEntryApplicationPath(); - string installDir = ApplicationBase.GetInstallationDirectory(); - using (var context = new CommandContext(appPath, installDir)) + using (var context = new CommandContext(args)) using (var app = new HelperApplication(context)) { app.RegisterCommand(new CredentialsCommandImpl(context)); diff --git a/src/shared/TestInfrastructure/Objects/NullTrace.cs b/src/shared/TestInfrastructure/Objects/NullTrace.cs index 54230c390..0cb3c299c 100644 --- a/src/shared/TestInfrastructure/Objects/NullTrace.cs +++ b/src/shared/TestInfrastructure/Objects/NullTrace.cs @@ -47,4 +47,20 @@ void IDisposable.Dispose() { } #endregion } + + public class NullTrace2 : ITrace2 + { + #region ITrace2 + public void AddWriter(ITrace2Writer writer) { } + + public void Start(TextWriter error, IFileSystem fileSystem, string appPath) { } + + #endregion + + #region IDisposable + + void IDisposable.Dispose() { } + + #endregion + } } diff --git a/src/shared/TestInfrastructure/Objects/TestCommandContext.cs b/src/shared/TestInfrastructure/Objects/TestCommandContext.cs index 3aaaef255..1b61b43c2 100644 --- a/src/shared/TestInfrastructure/Objects/TestCommandContext.cs +++ b/src/shared/TestInfrastructure/Objects/TestCommandContext.cs @@ -33,6 +33,7 @@ public TestCommandContext() public TestTerminal Terminal { 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; } @@ -41,7 +42,11 @@ public TestCommandContext() #region ICommandContext - string ICommandContext.ApplicationPath => AppPath; + string ICommandContext.ApplicationPath + { + get => AppPath; + set => AppPath = value; + } string ICommandContext.InstallationDirectory => InstallDir; diff --git a/src/shared/TestInfrastructure/Objects/TestSettings.cs b/src/shared/TestInfrastructure/Objects/TestSettings.cs index a7ea19abb..e0d03d8fb 100644 --- a/src/shared/TestInfrastructure/Objects/TestSettings.cs +++ b/src/shared/TestInfrastructure/Objects/TestSettings.cs @@ -46,6 +46,16 @@ public class TestSettings : ISettings public bool UseCustomCertificateBundleWithSchannel { get; set; } public int AutoDetectProviderTimeout { get; set; } = Constants.DefaultAutoDetectProviderTimeoutMs; + public Trace2Settings GetTrace2Settings() + { + return new Trace2Settings() + { + FormatTargetsAndValues = new Dictionary() + { + { Trace2FormatTarget.Event, "foo" } + } + }; + } #region ISettings diff --git a/src/windows/Atlassian.Bitbucket.UI.Windows/Program.cs b/src/windows/Atlassian.Bitbucket.UI.Windows/Program.cs index 1444c0cd5..f348a20cc 100644 --- a/src/windows/Atlassian.Bitbucket.UI.Windows/Program.cs +++ b/src/windows/Atlassian.Bitbucket.UI.Windows/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using Atlassian.Bitbucket.UI.Commands; using Atlassian.Bitbucket.UI.Controls; @@ -11,9 +12,7 @@ public static class Program { public static async Task Main(string[] args) { - string appPath = ApplicationBase.GetEntryApplicationPath(); - string installDir = ApplicationBase.GetInstallationDirectory(); - using (var context = new CommandContext(appPath, installDir)) + using (var context = new CommandContext(args)) using (var app = new HelperApplication(context)) { if (args.Length == 0) diff --git a/src/windows/Git-Credential-Manager.UI.Windows/Program.cs b/src/windows/Git-Credential-Manager.UI.Windows/Program.cs index 10e59312b..ad675049d 100644 --- a/src/windows/Git-Credential-Manager.UI.Windows/Program.cs +++ b/src/windows/Git-Credential-Manager.UI.Windows/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using GitCredentialManager.UI.Commands; using GitCredentialManager.UI.Controls; @@ -9,9 +10,7 @@ public static class Program { public static async Task Main(string[] args) { - string appPath = ApplicationBase.GetEntryApplicationPath(); - string installDir = ApplicationBase.GetInstallationDirectory(); - using (var context = new CommandContext(appPath, installDir)) + using (var context = new CommandContext(args)) using (var app = new HelperApplication(context)) { if (args.Length == 0) diff --git a/src/windows/GitHub.UI.Windows/Program.cs b/src/windows/GitHub.UI.Windows/Program.cs index ce880348f..7924b8f4b 100644 --- a/src/windows/GitHub.UI.Windows/Program.cs +++ b/src/windows/GitHub.UI.Windows/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using GitHub.UI.Commands; using GitHub.UI.Controls; @@ -11,9 +12,7 @@ public static class Program { public static async Task Main(string[] args) { - string appPath = ApplicationBase.GetEntryApplicationPath(); - string installDir = ApplicationBase.GetInstallationDirectory(); - using (var context = new CommandContext(appPath, installDir)) + using (var context = new CommandContext(args)) using (var app = new HelperApplication(context)) { if (args.Length == 0) diff --git a/src/windows/GitLab.UI.Windows/Program.cs b/src/windows/GitLab.UI.Windows/Program.cs index cead333de..6c58d8279 100644 --- a/src/windows/GitLab.UI.Windows/Program.cs +++ b/src/windows/GitLab.UI.Windows/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using GitLab.UI.Commands; using GitLab.UI.Controls; @@ -11,9 +12,7 @@ public static class Program { public static async Task Main(string[] args) { - string appPath = ApplicationBase.GetEntryApplicationPath(); - string installDir = ApplicationBase.GetInstallationDirectory(); - using (var context = new CommandContext(appPath, installDir)) + using (var context = new CommandContext(args)) using (var app = new HelperApplication(context)) { if (args.Length == 0) From ddb7591e43b5ad4a2c0bd3b9d42499cde948aa71 Mon Sep 17 00:00:00 2001 From: Lessley Dennington Date: Tue, 7 Feb 2023 11:25:27 -0700 Subject: [PATCH 8/9] trace2: write version and start events Write the TRACE2 version event, which identifies the event format version (currently hardcoded as 3 as it is in Git) and the version of GCM. Additionally, write the TRACE2 start event, which reflects the elapsed time and application arguments (including the application name). These are paired because the telemetry tool/OTel collector require both these events to be sent for a given session. --- src/shared/Core/Trace2.cs | 141 +++++++++++++++++- .../TestInfrastructure/Objects/NullTrace.cs | 10 +- 2 files changed, 145 insertions(+), 6 deletions(-) diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs index 8302b1350..bd511d12d 100644 --- a/src/shared/Core/Trace2.cs +++ b/src/shared/Core/Trace2.cs @@ -4,6 +4,7 @@ using System.IO; using System.IO.Pipes; using System.Linq; +using System.Runtime.Serialization; using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -16,7 +17,12 @@ namespace GitCredentialManager; /// system. /// public enum Trace2Event -{ } +{ + [EnumMember(Value = "version")] + Version = 0, + [EnumMember(Value = "start")] + Start = 1, +} public class Trace2Settings { @@ -36,7 +42,13 @@ public interface ITrace2 : IDisposable /// The standard error text stream connected back to the calling process. /// File system abstraction. /// The path to the GCM application. - void Start(TextWriter error, IFileSystem fileSystem, string appPath); + /// Path of the file this method is called from. + /// Line number of file this method is called from. + void Start(TextWriter error, + IFileSystem fileSystem, + string appPath, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0); } public class Trace2 : DisposableObject, ITrace2 @@ -63,9 +75,22 @@ public Trace2(IEnvironment environment, Trace2Settings settings, string[] argv, _sid = SetSid(); } - public void Start(TextWriter error, IFileSystem fileSystem, string appPath) + public void Start(TextWriter error, + IFileSystem fileSystem, + string appPath, + string filePath, + int lineNumber) { TryParseSettings(error, fileSystem); + + if (!AssemblyUtils.TryGetAssemblyVersion(out string version)) + { + // A version is required for TRACE2, so if this call fails + // manually set the version. + version = "0.0.0"; + } + WriteVersion(version, filePath, lineNumber); + WriteStart(appPath, filePath, lineNumber); } protected override void ReleaseManagedResources() @@ -164,6 +189,50 @@ private void TryParseSettings(TextWriter error, IFileSystem fileSystem) } } + private void WriteVersion( + string gcmVersion, + string filePath, + int lineNumber, + string eventFormatVersion = "3") + { + EnsureArgument.NotNull(gcmVersion, nameof(gcmVersion)); + + WriteMessage(new VersionMessage() + { + Event = Trace2Event.Version, + Sid = _sid, + Time = DateTimeOffset.UtcNow, + File = Path.GetFileName(filePath).ToLower(), + Line = lineNumber, + Evt = eventFormatVersion, + Exe = gcmVersion + }); + } + + private void WriteStart( + string appPath, + string filePath, + int lineNumber) + { + // Prepend GCM exe to arguments + var argv = new List() + { + Path.GetFileName(appPath), + }; + argv.AddRange(_argv); + + WriteMessage(new StartMessage() + { + Event = Trace2Event.Start, + Sid = _sid, + Time = DateTimeOffset.UtcNow, + File = Path.GetFileName(filePath).ToLower(), + Line = lineNumber, + Argv = argv, + ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds + }); + } + private void AddWriter(ITrace2Writer writer) { ThrowIfDisposed(); @@ -203,6 +272,7 @@ private void WriteMessage(Trace2Message message) public abstract class Trace2Message { protected const string TimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffff'Z'"; + private const int SourceColumnMaxWidth = 23; [JsonProperty("event", Order = 1)] public Trace2Event Event { get; set; } @@ -226,4 +296,69 @@ public abstract class Trace2Message public abstract string ToJson(); public abstract string ToNormalString(); + + protected string BuildNormalString(string message) + { + // The normal format uses local time rather than UTC time. + string time = Time.ToLocalTime().ToString("HH:mm:ss.ffffff"); + + // Source column format is file:line + string source = $"{File.ToLower()}:{Line}"; + if (source.Length > SourceColumnMaxWidth) + { + source = TraceUtils.FormatSource(source, SourceColumnMaxWidth); + } + + // Git's TRACE2 normal format is: + // [