Skip to content

Commit a5a1be2

Browse files
authored
Merge pull request #463 from mjcheetham/diagnose
Add diagnostic command for debugging issues
2 parents 596cd0c + 837824e commit a5a1be2

18 files changed

+898
-15
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using Microsoft.Git.CredentialManager.Diagnostics;
6+
7+
namespace GitHub.Diagnostics
8+
{
9+
public class GitHubApiDiagnostic : Diagnostic
10+
{
11+
private readonly IGitHubRestApi _api;
12+
13+
public GitHubApiDiagnostic(IGitHubRestApi api)
14+
: base("GitHub API")
15+
{
16+
_api = api;
17+
}
18+
19+
protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles)
20+
{
21+
var targetUri = new Uri("https://github.com");
22+
log.AppendLine($"Using '{targetUri}' as API target.");
23+
24+
log.Append("Querying '/meta' endpoint...");
25+
GitHubMetaInfo metaInfo = await _api.GetMetaInfoAsync(targetUri);
26+
log.AppendLine(" OK");
27+
28+
return true;
29+
}
30+
}
31+
}

src/shared/GitHub/GitHubHostProvider.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
using System.Collections.Generic;
33
using System.Net.Http;
44
using System.Threading.Tasks;
5+
using GitHub.Diagnostics;
56
using Microsoft.Git.CredentialManager;
67
using Microsoft.Git.CredentialManager.Authentication.OAuth;
8+
using Microsoft.Git.CredentialManager.Diagnostics;
79

810
namespace GitHub
911
{
10-
public class GitHubHostProvider : HostProvider
12+
public class GitHubHostProvider : HostProvider, IDiagnosticProvider
1113
{
1214
private static readonly string[] GitHubOAuthScopes =
1315
{
@@ -295,6 +297,11 @@ protected override void ReleaseManagedResources()
295297
base.ReleaseManagedResources();
296298
}
297299

300+
public IEnumerable<IDiagnostic> GetDiagnostics()
301+
{
302+
yield return new GitHubApiDiagnostic(_gitHubApi);
303+
}
304+
298305
#region Private Methods
299306

300307
public static bool IsGitHubDotCom(string targetUrl)

src/shared/Microsoft.Git.CredentialManager/Application.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Text.RegularExpressions;
1010
using System.Threading.Tasks;
1111
using Microsoft.Git.CredentialManager.Commands;
12+
using Microsoft.Git.CredentialManager.Diagnostics;
1213
using Microsoft.Git.CredentialManager.Interop;
1314

1415
namespace Microsoft.Git.CredentialManager
@@ -18,6 +19,7 @@ public class Application : ApplicationBase, IConfigurableComponent
1819
private readonly IHostProviderRegistry _providerRegistry;
1920
private readonly IConfigurationService _configurationService;
2021
private readonly IList<ProviderCommand> _providerCommands = new List<ProviderCommand>();
22+
private readonly List<IDiagnostic> _diagnostics = new List<IDiagnostic>();
2123

2224
public Application(ICommandContext context)
2325
: this(context, new HostProviderRegistry(context), new ConfigurationService(context))
@@ -54,30 +56,46 @@ public void RegisterProvider(IHostProvider provider, HostProviderPriority priori
5456
ProviderCommand providerCommand = cmdProvider.CreateCommand();
5557
_providerCommands.Add(providerCommand);
5658
}
59+
60+
// If the provider exposes custom diagnostics use them
61+
if (provider is IDiagnosticProvider diagnosticProvider)
62+
{
63+
IEnumerable<IDiagnostic> providerDiagnostics = diagnosticProvider.GetDiagnostics();
64+
_diagnostics.AddRange(providerDiagnostics);
65+
}
5766
}
5867

5968
protected override async Task<int> RunInternalAsync(string[] args)
6069
{
6170
var rootCommand = new RootCommand();
71+
var diagnoseCommand = new DiagnoseCommand(Context);
6272

6373
// Add standard commands
6474
rootCommand.AddCommand(new GetCommand(Context, _providerRegistry));
6575
rootCommand.AddCommand(new StoreCommand(Context, _providerRegistry));
6676
rootCommand.AddCommand(new EraseCommand(Context, _providerRegistry));
6777
rootCommand.AddCommand(new ConfigureCommand(Context, _configurationService));
6878
rootCommand.AddCommand(new UnconfigureCommand(Context, _configurationService));
79+
rootCommand.AddCommand(diagnoseCommand);
6980

7081
// Add any custom provider commands
7182
foreach (ProviderCommand providerCommand in _providerCommands)
7283
{
7384
rootCommand.AddCommand(providerCommand);
7485
}
7586

87+
// Add any custom provider diagnostic tests
88+
foreach (IDiagnostic providerDiagnostic in _diagnostics)
89+
{
90+
diagnoseCommand.AddDiagnostic(providerDiagnostic);
91+
}
92+
7693
// Trace the current version, OS, runtime, and program arguments
7794
PlatformInformation info = PlatformUtils.GetPlatformInformation();
7895
Context.Trace.WriteLine($"Version: {Constants.GcmVersion}");
7996
Context.Trace.WriteLine($"Runtime: {info.ClrVersion}");
8097
Context.Trace.WriteLine($"Platform: {info.OperatingSystemType} ({info.CpuArchitecture})");
98+
Context.Trace.WriteLine($"OSVersion: {info.OperatingSystemVersion}");
8199
Context.Trace.WriteLine($"AppPath: {Context.ApplicationPath}");
82100
Context.Trace.WriteLine($"Arguments: {string.Join(" ", args)}");
83101

src/shared/Microsoft.Git.CredentialManager/Authentication/MicrosoftAuthentication.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
211211
return new MsalResult(result);
212212
}
213213

214-
private MicrosoftAuthenticationFlowType GetFlowType()
214+
internal MicrosoftAuthenticationFlowType GetFlowType()
215215
{
216216
if (Context.Settings.TryGetSetting(
217217
Constants.EnvironmentVariables.MsAuthFlow,
@@ -368,7 +368,7 @@ private async Task RegisterTokenCacheAsync(IPublicClientApplication app)
368368
}
369369
}
370370

371-
private StorageCreationProperties CreateTokenCacheProps(bool useLinuxFallback)
371+
internal StorageCreationProperties CreateTokenCacheProps(bool useLinuxFallback)
372372
{
373373
const string cacheFileName = "msal.cache";
374374
string cacheDirectory;
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.CommandLine;
4+
using System.CommandLine.Invocation;
5+
using System.IO;
6+
using System.Reflection;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Microsoft.Git.CredentialManager.Diagnostics;
10+
11+
namespace Microsoft.Git.CredentialManager.Commands
12+
{
13+
public class DiagnoseCommand : Command
14+
{
15+
private const string TestOutputIndent = " ";
16+
17+
private readonly ICommandContext _context;
18+
private readonly ICollection<IDiagnostic> _diagnostics;
19+
20+
public DiagnoseCommand(ICommandContext context)
21+
: base("diagnose", "Run diagnostics and gather logs to diagnose problems with Git Credential Manager")
22+
{
23+
EnsureArgument.NotNull(context, nameof(context));
24+
25+
_context = context;
26+
_diagnostics = new List<IDiagnostic>
27+
{
28+
// Add standard diagnostics
29+
new EnvironmentDiagnostic(context.Environment),
30+
new FileSystemDiagnostic(context.FileSystem),
31+
new NetworkingDiagnostic(context.HttpClientFactory),
32+
new GitDiagnostic(context.Git),
33+
new CredentialStoreDiagnostic(context.CredentialStore),
34+
new MicrosoftAuthenticationDiagnostic(context)
35+
};
36+
37+
AddOption(
38+
new Option<string>(new []{"--output", "-o"}, "Output directory for diagnostic logs.")
39+
);
40+
41+
Handler = CommandHandler.Create<string>(ExecuteAsync);
42+
}
43+
44+
public void AddDiagnostic(IDiagnostic diagnostic)
45+
{
46+
_diagnostics.Add(diagnostic);
47+
}
48+
49+
private async Task<int> ExecuteAsync(string output)
50+
{
51+
// Don't use IStandardStreams for writing output in this command as we
52+
// cannot trust any component on the ICommandContext is working correctly.
53+
Console.WriteLine($"Running diagnostics...{Environment.NewLine}");
54+
55+
if (_diagnostics.Count == 0)
56+
{
57+
Console.WriteLine("No diagnostics to run.");
58+
return 0;
59+
}
60+
61+
int numFailed = 0;
62+
int numSkipped = 0;
63+
64+
string currentDir = Directory.GetCurrentDirectory();
65+
string outputDir;
66+
if (string.IsNullOrWhiteSpace(output))
67+
{
68+
outputDir = currentDir;
69+
}
70+
else
71+
{
72+
if (!Directory.Exists(output))
73+
{
74+
Directory.CreateDirectory(output);
75+
}
76+
77+
outputDir = Path.GetFullPath(Path.Combine(currentDir, output));
78+
}
79+
80+
string logFilePath = Path.Combine(outputDir, "gcm-diagnose.log");
81+
var extraLogs = new List<string>();
82+
83+
using var fullLog = new StreamWriter(logFilePath, append: false, Encoding.UTF8);
84+
fullLog.WriteLine("Diagnose log at {0:s}Z", DateTime.UtcNow);
85+
fullLog.WriteLine();
86+
fullLog.WriteLine($"Executable: {_context.ApplicationPath}");
87+
fullLog.WriteLine(
88+
TryGetAssemblyVersion(out string version)
89+
? $"Version: {version}"
90+
: "Version: [!] Failed to get version information [!]"
91+
);
92+
fullLog.WriteLine();
93+
94+
foreach (IDiagnostic diagnostic in _diagnostics)
95+
{
96+
fullLog.WriteLine("------------");
97+
fullLog.WriteLine($"Diagnostic: {diagnostic.Name}");
98+
99+
if (!diagnostic.CanRun())
100+
{
101+
fullLog.WriteLine("Skipped: True");
102+
fullLog.WriteLine();
103+
104+
Console.Write(" ");
105+
ConsoleEx.WriteColor("[SKIP]", ConsoleColor.Gray);
106+
Console.WriteLine(" {0}", diagnostic.Name);
107+
108+
numSkipped++;
109+
continue;
110+
}
111+
112+
string inProgressMsg = $" >>>> {diagnostic.Name}";
113+
Console.Write(inProgressMsg);
114+
115+
fullLog.WriteLine("Skipped: False");
116+
DiagnosticResult result = await diagnostic.RunAsync();
117+
fullLog.WriteLine("Success: {0}", result.IsSuccess);
118+
119+
if (result.Exception is null)
120+
{
121+
fullLog.WriteLine("Exception: None");
122+
}
123+
else
124+
{
125+
fullLog.WriteLine("Exception:");
126+
fullLog.WriteLine(result.Exception.ToString());
127+
}
128+
129+
fullLog.WriteLine("Log:");
130+
fullLog.WriteLine(result.DiagnosticLog);
131+
132+
Console.Write(new string('\b', inProgressMsg.Length - 1));
133+
ConsoleEx.WriteColor(
134+
result.IsSuccess ? "[ OK ]" : "[FAIL]",
135+
result.IsSuccess ? ConsoleColor.DarkGreen : ConsoleColor.Red
136+
);
137+
Console.WriteLine(" {0}", diagnostic.Name);
138+
139+
if (!result.IsSuccess)
140+
{
141+
numFailed++;
142+
143+
if (result.Exception is not null)
144+
{
145+
Console.WriteLine();
146+
ConsoleEx.WriteLineIndent("[!] Encountered an exception [!]");
147+
ConsoleEx.WriteLineIndent(result.Exception.ToString());
148+
}
149+
150+
Console.WriteLine();
151+
ConsoleEx.WriteLineIndent("[*] Diagnostic test log [*]");
152+
ConsoleEx.WriteLineIndent(result.DiagnosticLog);
153+
154+
Console.WriteLine();
155+
}
156+
157+
foreach (string filePath in result.AdditionalFiles)
158+
{
159+
string fileName = Path.GetFileName(filePath);
160+
string destPath = Path.Combine(outputDir, fileName);
161+
try
162+
{
163+
File.Copy(filePath, destPath, overwrite: true);
164+
}
165+
catch
166+
{
167+
ConsoleEx.WriteLineIndent($"Failed to copy additional file '{filePath}'");
168+
}
169+
170+
extraLogs.Add(destPath);
171+
}
172+
173+
fullLog.Flush();
174+
}
175+
176+
Console.WriteLine();
177+
string summary = $"Diagnostic summary: {_diagnostics.Count - numFailed} passed, {numSkipped} skipped, {numFailed} failed.";
178+
Console.WriteLine(summary);
179+
Console.WriteLine("Log files:");
180+
Console.WriteLine($" {logFilePath}");
181+
foreach (string log in extraLogs)
182+
{
183+
Console.WriteLine($" {log}");
184+
}
185+
Console.WriteLine();
186+
Console.WriteLine("Caution: Log files may include sensitive information - redact before sharing.");
187+
Console.WriteLine();
188+
189+
if (numFailed > 0)
190+
{
191+
Console.WriteLine("Diagnostics indicate a possible problem with your installation.");
192+
Console.WriteLine($"Please open an issue at {Constants.HelpUrls.GcmNewIssue} and include log files.");
193+
Console.WriteLine();
194+
}
195+
196+
fullLog.Close();
197+
return numFailed;
198+
}
199+
200+
private bool TryGetAssemblyVersion(out string version)
201+
{
202+
try
203+
{
204+
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
205+
var assemblyVersionAttribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
206+
version = assemblyVersionAttribute is null
207+
? assembly.GetName().Version.ToString()
208+
: assemblyVersionAttribute.InformationalVersion;
209+
return true;
210+
}
211+
catch
212+
{
213+
version = null;
214+
return false;
215+
}
216+
}
217+
218+
private static class ConsoleEx
219+
{
220+
public static void WriteLineIndent(string str)
221+
{
222+
string[] lines = str?.Split('\n', '\r');
223+
224+
if (lines is null) return;
225+
226+
foreach (string line in lines)
227+
{
228+
Console.Write(TestOutputIndent);
229+
Console.WriteLine(line);
230+
}
231+
}
232+
233+
public static void WriteColor(string str, ConsoleColor fgColor)
234+
{
235+
var initFgColor = Console.ForegroundColor;
236+
Console.ForegroundColor = fgColor;
237+
Console.Write(str);
238+
Console.ForegroundColor = initFgColor;
239+
}
240+
}
241+
}
242+
}

src/shared/Microsoft.Git.CredentialManager/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public static class WindowsRegistry
119119
public static class HelpUrls
120120
{
121121
public const string GcmProjectUrl = "https://aka.ms/gcmcore";
122+
public const string GcmNewIssue = "https://aka.ms/gcmcore-bug";
122123
public const string GcmAuthorityDeprecated = "https://aka.ms/gcmcore-authority";
123124
public const string GcmHttpProxyGuide = "https://aka.ms/gcmcore-httpproxy";
124125
public const string GcmTlsVerification = "https://aka.ms/gcmcore-tlsverify";

0 commit comments

Comments
 (0)