-
Notifications
You must be signed in to change notification settings - Fork 474
Capture mount dumps and preserve logs when a functional test fails #2062
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tyrielv
wants to merge
3
commits into
microsoft:master
Choose a base branch
from
tyrielv:tyrielv/ft-failure-diagnostics
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |
| using GVFS.FunctionalTests.Should; | ||
| using GVFS.FunctionalTests.Tests; | ||
| using GVFS.Tests.Should; | ||
| using NUnit.Framework; | ||
| using NUnit.Framework.Interfaces; | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
|
|
@@ -169,10 +171,102 @@ public string GetPackRoot(FileSystemRunner fileSystem) | |
|
|
||
| public void DeleteEnlistment() | ||
| { | ||
| this.CaptureFailureLogs(); | ||
| TestResultsHelper.OutputGVFSLogs(this); | ||
| RepositoryHelpers.DeleteTestDirectory(this.EnlistmentRoot); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// When the current test has failed, writes a full-memory minidump of each still-running | ||
| /// GVFS.Mount process for this enlistment, so a mount *hang* can be diagnosed after the fact. | ||
| /// Must be called before the mount is unmounted or killed - once the process is gone (whether | ||
| /// cleanly unmounted or force-killed) there is nothing left to dump. Written under | ||
| /// <see cref="TestResultsHelper.DiagnosticsRoot"/> so CI can upload it. Best-effort: never | ||
| /// throws, so it cannot break teardown. | ||
| /// </summary> | ||
| public void CaptureFailureDiagnostics() | ||
| { | ||
| try | ||
| { | ||
| if (!this.TryGetFailureDiagnosticsFolder(out string destinationFolder)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| List<int> mountProcessIds = this.GetMountProcessIds(); | ||
| if (mountProcessIds.Count == 0) | ||
| { | ||
| Console.Error.WriteLine("[DIAGNOSTICS] No live GVFS.Mount process for this enlistment (already exited/crashed)"); | ||
| return; | ||
| } | ||
|
|
||
| Console.Error.WriteLine($"[DIAGNOSTICS] Test failed; capturing mount dump(s) to '{destinationFolder}'"); | ||
| Directory.CreateDirectory(destinationFolder); | ||
| foreach (int pid in mountProcessIds) | ||
| { | ||
| MiniDump.TryWrite(pid, Path.Combine(destinationFolder, $"GVFS.Mount_{pid}.dmp")); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.Error.WriteLine($"[DIAGNOSTICS] CaptureFailureDiagnostics failed: {ex.Message}"); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// When the current test has failed, preserves the enlistment's .gvfs/logs folder (robust to | ||
| /// locked / partially-flushed files) under <see cref="TestResultsHelper.DiagnosticsRoot"/> | ||
| /// before the enlistment directory is deleted. Best-effort: never throws. | ||
| /// </summary> | ||
| private void CaptureFailureLogs() | ||
| { | ||
| try | ||
| { | ||
| if (!this.TryGetFailureDiagnosticsFolder(out string destinationFolder)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| Console.Error.WriteLine($"[DIAGNOSTICS] Test failed; capturing logs to '{destinationFolder}'"); | ||
| TestResultsHelper.CopyFilesWithFallback(this.GVFSLogsRoot, Path.Combine(destinationFolder, "logs")); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.Error.WriteLine($"[DIAGNOSTICS] CaptureFailureLogs failed: {ex.Message}"); | ||
| } | ||
| } | ||
|
|
||
| private bool TryGetFailureDiagnosticsFolder(out string destinationFolder) | ||
| { | ||
| destinationFolder = null; | ||
| if (TestContext.CurrentContext.Result.Outcome.Status != TestStatus.Failed) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| destinationFolder = Path.Combine( | ||
| TestResultsHelper.DiagnosticsRoot, | ||
| SanitizeForPath(TestContext.CurrentContext.Test.Name) + "_" + Path.GetFileName(this.EnlistmentRoot)); | ||
| return true; | ||
| } | ||
|
|
||
| private static string SanitizeForPath(string name) | ||
| { | ||
| if (string.IsNullOrEmpty(name)) | ||
| { | ||
| return "test"; | ||
| } | ||
|
|
||
| foreach (char invalid in Path.GetInvalidFileNameChars()) | ||
| { | ||
| name = name.Replace(invalid, '_'); | ||
| } | ||
|
|
||
| // Flatten characters that are legal in file names but noisy in NUnit | ||
| // test names (parameterized cases, spaces). | ||
| return name.Replace('(', '_').Replace(')', '_').Replace(' ', '_').Replace(',', '_').Replace('"', '_'); | ||
| } | ||
|
|
||
| public void CloneAndMount(bool skipPrefetch) | ||
| { | ||
| Console.Error.WriteLine("[CI-DEBUG] CloneAndMount: starting clone of " + this.RepoUrl); | ||
|
|
@@ -293,6 +387,10 @@ public string SetCacheServer(string arg) | |
|
|
||
| public void UnmountAndDeleteAll() | ||
| { | ||
| // Capture the mount dump before unmounting or killing anything - once the mount process is | ||
| // gone (whether it unmounts cleanly or is force-killed below) there is nothing left to dump. | ||
| this.CaptureFailureDiagnostics(); | ||
|
|
||
| try | ||
| { | ||
| this.UnmountGVFS(); | ||
|
|
@@ -310,43 +408,96 @@ public void UnmountAndDeleteAll() | |
|
|
||
| public void KillMountProcess() | ||
| { | ||
| foreach (int pid in this.GetMountProcessIds()) | ||
| { | ||
| Console.Error.WriteLine($"[TEARDOWN] Killing GVFS.Mount (PID {pid}) for {this.EnlistmentRoot}"); | ||
| try | ||
| { | ||
| System.Diagnostics.Process.GetProcessById(pid)?.Kill(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.Error.WriteLine($"[TEARDOWN] Failed to kill PID {pid}: {ex.Message}"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns the process ids of the GVFS.Mount processes whose command line | ||
| /// references this enlistment root. Uses PowerShell's Get-CimInstance to | ||
| /// read command lines without requiring System.Management. Best-effort: | ||
| /// returns an empty list on any failure (e.g. non-Windows). | ||
| /// </summary> | ||
| private List<int> GetMountProcessIds() | ||
| { | ||
| List<int> processIds = new List<int>(); | ||
|
|
||
| try | ||
| { | ||
| // Find GVFS.Mount processes whose command line contains this | ||
| // enlistment root. Uses PowerShell's Get-CimInstance to read | ||
| // command lines without requiring System.Management. | ||
| string filter = this.EnlistmentRoot.Replace("\\", "\\\\"); | ||
| // Match on the enlistment's unique leaf folder id rather than the | ||
| // full path. PowerShell's -like treats '\' as a literal (not an | ||
| // escape), so doubling backslashes in the full path would produce a | ||
| // pattern that never matches a real (single-backslash) command line. | ||
| // The leaf id is unique and free of path separators and wildcard | ||
| // metacharacters, so it needs no escaping. | ||
| string filter = Path.GetFileName(this.EnlistmentRoot.TrimEnd('\\', '/')); | ||
| var psi = new System.Diagnostics.ProcessStartInfo("powershell.exe") | ||
| { | ||
| Arguments = $"-NoProfile -Command \"Get-CimInstance Win32_Process -Filter \\\"Name='GVFS.Mount.exe'\\\" | Where-Object {{ $_.CommandLine -like '*{filter}*' }} | ForEach-Object {{ $_.ProcessId }}\"", | ||
| RedirectStandardOutput = true, | ||
| UseShellExecute = false, | ||
| CreateNoWindow = true, | ||
| }; | ||
| var proc = System.Diagnostics.Process.Start(psi); | ||
| string output = proc.StandardOutput.ReadToEnd(); | ||
| proc.WaitForExit(10000); | ||
|
|
||
| foreach (string line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries)) | ||
| var output = new System.Text.StringBuilder(); | ||
|
|
||
| // Read output asynchronously via the event, rather than a blocking ReadToEnd() before | ||
| // WaitForExit(): ReadToEnd() blocks until the process closes its stdout handle, so if the | ||
| // helper itself hangs, the later WaitForExit(10000) timeout is never reached at all. With | ||
| // async reads, WaitForExit is the only blocking call, so it enforces a real timeout and we | ||
| // can kill the helper if it does not exit in time. | ||
| using (var proc = new System.Diagnostics.Process { StartInfo = psi }) | ||
| { | ||
| if (int.TryParse(line.Trim(), out int pid)) | ||
| proc.OutputDataReceived += (sender, args) => | ||
| { | ||
| Console.Error.WriteLine($"[TEARDOWN] Killing GVFS.Mount (PID {pid}) for {this.EnlistmentRoot}"); | ||
| if (args.Data != null) | ||
| { | ||
| output.AppendLine(args.Data); | ||
| } | ||
| }; | ||
|
|
||
| proc.Start(); | ||
| proc.BeginOutputReadLine(); | ||
|
|
||
| if (!proc.WaitForExit(10000)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The asynchronous stdout reader may still be flushing when the PID output is parsed, which can make mount process discovery intermittently miss a live process. |
||
| { | ||
| Console.Error.WriteLine("[TEARDOWN] GetMountProcessIds helper timed out; killing it"); | ||
| try | ||
| { | ||
| System.Diagnostics.Process.GetProcessById(pid)?.Kill(); | ||
| proc.Kill(); | ||
| proc.WaitForExit(2000); | ||
| } | ||
| catch (Exception ex) | ||
| catch (Exception killEx) | ||
| { | ||
| Console.Error.WriteLine($"[TEARDOWN] Failed to kill PID {pid}: {ex.Message}"); | ||
| Console.Error.WriteLine($"[TEARDOWN] Failed to kill GetMountProcessIds helper: {killEx.Message}"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| foreach (string line in output.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries)) | ||
| { | ||
| if (int.TryParse(line.Trim(), out int pid)) | ||
| { | ||
| processIds.Add(pid); | ||
| } | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.Error.WriteLine($"[TEARDOWN] KillMountProcess failed: {ex.Message}"); | ||
| Console.Error.WriteLine($"[TEARDOWN] GetMountProcessIds failed: {ex.Message}"); | ||
| } | ||
|
|
||
| return processIds; | ||
| } | ||
|
|
||
| public string GetVirtualPathTo(string path) | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.