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/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/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 new file mode 100644 index 000000000..da3d6d95a --- /dev/null +++ b/src/shared/Core.Tests/Trace2Tests.cs @@ -0,0 +1,58 @@ +using System; +using System.Text.RegularExpressions; +using GitCredentialManager.Tests.Objects; +using Xunit; + +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("")] + 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 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.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/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/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/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/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) 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/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/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/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/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/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/Trace2.cs b/src/shared/Core/Trace2.cs new file mode 100644 index 000000000..d929e0a29 --- /dev/null +++ b/src/shared/Core/Trace2.cs @@ -0,0 +1,422 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +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 +{ + [EnumMember(Value = "version")] + Version = 0, + [EnumMember(Value = "start")] + Start = 1, + [EnumMember(Value = "exit")] + Exit = 2 +} + +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. + /// 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); + + /// + /// Shut down TRACE2 tracing by writing Exit event and disposing 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); +} + +public class Trace2 : DisposableObject, ITrace2 +{ + private readonly object _writersLock = new object(); + 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, 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, + 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); + } + + public void Stop(int exitCode, string filePath, int lineNumber) + { + WriteExit(exitCode, filePath, lineNumber); + ReleaseManagedResources(); + } + + 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(); + } + + 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; + } + + 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 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 WriteExit(int code, string filePath = "", int lineNumber = 0) + { + EnsureArgument.NotNull(code, nameof(code)); + + WriteMessage(new ExitMessage() + { + Event = Trace2Event.Exit, + Sid = _sid, + Time = DateTimeOffset.Now, + File = Path.GetFileName(filePath).ToLower(), + Line = lineNumber, + Code = code, + ElapsedTime = (DateTimeOffset.UtcNow - _applicationStartTime).TotalSeconds + }); + } + + 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 +{ + 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; } + + [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; } + + 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: + // [