diff --git a/eng/Versions.props b/eng/Versions.props index 32eb6377308ad4..c70f7a05d68374 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -109,7 +109,7 @@ 2.0.0 17.10.0-beta1.24272.1 - 3.1.16 + 3.1.28 0.2.621003 2.1.0 2.0.3 @@ -124,6 +124,7 @@ 1.6.0 17.4.0-preview-20220707-01 + 0.1.32221 3.12.0 4.5.0 6.0.4 diff --git a/src/tests/Common/helixpublishwitharcade.proj b/src/tests/Common/helixpublishwitharcade.proj index 2d896fe0342d47..3e311881383240 100644 --- a/src/tests/Common/helixpublishwitharcade.proj +++ b/src/tests/Common/helixpublishwitharcade.proj @@ -412,12 +412,27 @@ $(XUnitLogCheckerHelixPath)XUnitLogChecker$(ExeSuffix) $(XUnitLogCheckerArgs) + + <_ExtraExecutableListFiles Include="@(_MergedPayloadFiles)" + Condition="$([System.String]::Copy('%(Identity)').ToLower().EndsWith('helix-extra-executables.list'))" /> + <_ExtraExecutables Remove="@(_ExtraExecutables)" /> + + + + + + <_ExtraExecutables Remove="@(_ExtraExecutables)" Condition="'%(Identity)' == ''" /> + + + + + @@ -458,6 +473,7 @@ + diff --git a/src/tests/build.proj b/src/tests/build.proj index ca195bf8041862..1d429a942fbf16 100644 --- a/src/tests/build.proj +++ b/src/tests/build.proj @@ -39,6 +39,7 @@ + diff --git a/src/tests/tracing/eventpipe/userevents/NuGet.config b/src/tests/tracing/eventpipe/userevents/NuGet.config new file mode 100644 index 00000000000000..583af9d11bb787 --- /dev/null +++ b/src/tests/tracing/eventpipe/userevents/NuGet.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/tests/tracing/eventpipe/userevents/UserEventsRequirements.cs b/src/tests/tracing/eventpipe/userevents/UserEventsRequirements.cs new file mode 100644 index 00000000000000..d7bc0e812294d2 --- /dev/null +++ b/src/tests/tracing/eventpipe/userevents/UserEventsRequirements.cs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; + +namespace Tracing.Tests.UserEvents +{ + internal static class UserEventsRequirements + { + private static readonly Version s_minKernelVersion = new(6, 4); + private const string TracefsPath = "/sys/kernel/tracing"; + private const string UserEventsDataPath = "/sys/kernel/tracing/user_events_data"; + private static readonly Version s_minGlibcVersion = new(2, 35); + + internal static bool IsSupported() + { + if (Environment.OSVersion.Version < s_minKernelVersion) + { + Console.WriteLine($"Kernel version '{Environment.OSVersion.Version}' is less than the minimum required '{s_minKernelVersion}'"); + return false; + } + + return IsGlibcAtLeast(s_minGlibcVersion) && + IsTracefsMounted() && + IsUserEventsDataAvailable(); + } + + private static bool IsTracefsMounted() + { + ProcessStartInfo checkTracefsStartInfo = new(); + checkTracefsStartInfo.FileName = "sudo"; + checkTracefsStartInfo.Arguments = $"-n test -d {TracefsPath}"; + checkTracefsStartInfo.UseShellExecute = false; + checkTracefsStartInfo.RedirectStandardOutput = true; + checkTracefsStartInfo.RedirectStandardError = true; + + using Process checkTracefs = new() { StartInfo = checkTracefsStartInfo }; + checkTracefs.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + Console.WriteLine($"[tracefs-check] {e.Data}"); + } + }; + checkTracefs.ErrorDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + Console.Error.WriteLine($"[tracefs-check] {e.Data}"); + } + }; + + checkTracefs.Start(); + checkTracefs.BeginOutputReadLine(); + checkTracefs.BeginErrorReadLine(); + checkTracefs.WaitForExit(); + if (checkTracefs.ExitCode == 0) + { + return true; + } + + Console.WriteLine($"Tracefs not mounted at '{TracefsPath}'."); + return false; + } + + private static bool IsUserEventsDataAvailable() + { + ProcessStartInfo checkUserEventsDataStartInfo = new(); + checkUserEventsDataStartInfo.FileName = "sudo"; + checkUserEventsDataStartInfo.Arguments = $"-n test -e {UserEventsDataPath}"; + checkUserEventsDataStartInfo.UseShellExecute = false; + checkUserEventsDataStartInfo.RedirectStandardOutput = true; + checkUserEventsDataStartInfo.RedirectStandardError = true; + + using Process checkUserEventsData = new() { StartInfo = checkUserEventsDataStartInfo }; + checkUserEventsData.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + Console.WriteLine($"[user-events-check] {e.Data}"); + } + }; + checkUserEventsData.ErrorDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + Console.Error.WriteLine($"[user-events-check] {e.Data}"); + } + }; + + checkUserEventsData.Start(); + checkUserEventsData.BeginOutputReadLine(); + checkUserEventsData.BeginErrorReadLine(); + checkUserEventsData.WaitForExit(); + if (checkUserEventsData.ExitCode == 0) + { + return true; + } + + Console.WriteLine($"User events data not available at '{UserEventsDataPath}'."); + return false; + } + + private static bool IsGlibcAtLeast(Version minVersion) + { + ProcessStartInfo lddStartInfo = new(); + lddStartInfo.FileName = "ldd"; + lddStartInfo.Arguments = "--version"; + lddStartInfo.UseShellExecute = false; + lddStartInfo.RedirectStandardOutput = true; + lddStartInfo.RedirectStandardError = true; + + using Process lddProcess = new() { StartInfo = lddStartInfo }; + string? detectedVersionLine = null; + + lddProcess.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data) && detectedVersionLine is null) + { + detectedVersionLine = e.Data; + } + }; + lddProcess.ErrorDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + Console.Error.WriteLine($"[ldd] {e.Data}"); + } + }; + + try + { + lddProcess.Start(); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to start 'ldd --version': {ex.Message}"); + return false; + } + + lddProcess.BeginOutputReadLine(); + lddProcess.BeginErrorReadLine(); + lddProcess.WaitForExit(); + + if (lddProcess.ExitCode != 0) + { + Console.WriteLine($"'ldd --version' exited with code {lddProcess.ExitCode}."); + return false; + } + + if (string.IsNullOrEmpty(detectedVersionLine)) + { + Console.WriteLine("Could not read glibc version from 'ldd --version' output."); + return false; + } + + string[] tokens = detectedVersionLine.Split(' ', StringSplitOptions.RemoveEmptyEntries); + Version? glibcVersion = null; + foreach (string token in tokens) + { + if (Version.TryParse(token, out Version? parsedVersion)) + { + glibcVersion = parsedVersion; + break; + } + } + + if (glibcVersion is null) + { + Console.WriteLine($"Failed to parse glibc version from 'ldd --version' output line: {detectedVersionLine}"); + return false; + } + + if (glibcVersion < minVersion) + { + Console.WriteLine($"glibc version '{glibcVersion}' is less than required '{minVersion}'."); + return false; + } + + return true; + } + } +} diff --git a/src/tests/tracing/eventpipe/userevents/dotnet-common.script b/src/tests/tracing/eventpipe/userevents/dotnet-common.script new file mode 100644 index 00000000000000..d6ac4f6ee89060 --- /dev/null +++ b/src/tests/tracing/eventpipe/userevents/dotnet-common.script @@ -0,0 +1,2 @@ +let Microsoft_Windows_DotNETRuntime_flags = new_dotnet_provider_flags(); +record_dotnet_provider("Microsoft-Windows-DotNETRuntime", 0x80000000000, 4, Microsoft_Windows_DotNETRuntime_flags); diff --git a/src/tests/tracing/eventpipe/userevents/userevents.cs b/src/tests/tracing/eventpipe/userevents/userevents.cs new file mode 100644 index 00000000000000..c42af630bcccda --- /dev/null +++ b/src/tests/tracing/eventpipe/userevents/userevents.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.Tracing.Etlx; + +namespace Tracing.Tests.UserEvents +{ + public class UserEventsTest + { + private const int SIGINT = 2; + + [DllImport("libc", EntryPoint = "kill", SetLastError = true)] + private static extern int Kill(int pid, int sig); + + public static int Main(string[] args) + { + if (args.Length > 0 && args[0] == "tracee") + { + UserEventsTracee.Run(); + return 0; + } + + return TestEntryPoint(); + } + + public static int TestEntryPoint() + { + string appBaseDir = AppContext.BaseDirectory; + string recordTracePath = Path.Combine(appBaseDir, "record-trace"); + string scriptFilePath = Path.Combine(appBaseDir, "dotnet-common.script"); + const string userEventsDataPath = "/sys/kernel/tracing/user_events_data"; + + if (!UserEventsRequirements.IsSupported()) + { + Console.WriteLine("Skipping test: environment does not support user events."); + return 100; + } + if (!File.Exists(recordTracePath)) + { + Console.Error.WriteLine($"record-trace not found at `{recordTracePath}`. Test cannot run."); + return -1; + } + if (!File.Exists(scriptFilePath)) + { + Console.Error.WriteLine($"dotnet-common.script not found at `{scriptFilePath}`. Test cannot run."); + return -1; + } + + string traceFilePath = Path.GetTempFileName(); + File.Delete(traceFilePath); // record-trace requires the output file to not exist + traceFilePath = Path.ChangeExtension(traceFilePath, ".nettrace"); + + ProcessStartInfo recordTraceStartInfo = new(); + recordTraceStartInfo.FileName = "sudo"; + recordTraceStartInfo.Arguments = $"-n {recordTracePath} --script-file {scriptFilePath} --out {traceFilePath}"; + recordTraceStartInfo.WorkingDirectory = appBaseDir; + recordTraceStartInfo.UseShellExecute = false; + recordTraceStartInfo.RedirectStandardOutput = true; + recordTraceStartInfo.RedirectStandardError = true; + + Console.WriteLine($"Starting record-trace: {recordTraceStartInfo.FileName} {recordTraceStartInfo.Arguments}"); + using Process recordTraceProcess = Process.Start(recordTraceStartInfo); + recordTraceProcess.OutputDataReceived += (_, args) => + { + if (!string.IsNullOrEmpty(args.Data)) + { + Console.WriteLine($"[record-trace] {args.Data}"); + } + }; + recordTraceProcess.BeginOutputReadLine(); + recordTraceProcess.ErrorDataReceived += (_, args) => + { + if (!string.IsNullOrEmpty(args.Data)) + { + Console.Error.WriteLine($"[record-trace] {args.Data}"); + } + }; + recordTraceProcess.BeginErrorReadLine(); + Console.WriteLine($"record-trace started with PID: {recordTraceProcess.Id}"); + + ProcessStartInfo traceeStartInfo = new(); + traceeStartInfo.FileName = Process.GetCurrentProcess().MainModule.FileName; + traceeStartInfo.Arguments = $"{typeof(UserEventsTest).Assembly.Location} tracee"; + traceeStartInfo.WorkingDirectory = appBaseDir; + traceeStartInfo.RedirectStandardOutput = true; + traceeStartInfo.RedirectStandardError = true; + + // record-trace currently only searches /tmp/ for diagnostic ports https://github.com/microsoft/one-collect/issues/183 + string diagnosticPortDir = "/tmp/"; + traceeStartInfo.Environment["TMPDIR"] = diagnosticPortDir; + + Console.WriteLine($"Starting tracee process: {traceeStartInfo.FileName} {traceeStartInfo.Arguments}"); + using Process traceeProcess = Process.Start(traceeStartInfo); + int traceePid = traceeProcess.Id; + Console.WriteLine($"Tracee process started with PID: {traceePid}"); + traceeProcess.OutputDataReceived += (_, args) => + { + if (!string.IsNullOrEmpty(args.Data)) + { + Console.WriteLine($"[tracee] {args.Data}"); + } + }; + traceeProcess.BeginOutputReadLine(); + traceeProcess.ErrorDataReceived += (_, args) => + { + if (!string.IsNullOrEmpty(args.Data)) + { + Console.Error.WriteLine($"[tracee] {args.Data}"); + } + }; + traceeProcess.BeginErrorReadLine(); + + Console.WriteLine($"Waiting for tracee process to exit..."); + if (!traceeProcess.HasExited && !traceeProcess.WaitForExit(5000)) + { + Console.WriteLine($"Tracee process did not exit within the 5s timeout, killing it."); + traceeProcess.Kill(); + } + traceeProcess.WaitForExit(); // flush async output + + // Since TMPDIR was configured, the diagnostic port was created outside of helix's default temp datadisk path. + // The diagnostic port should be automatically cleaned up when the tracee shutsdown, but just in case of an + // abrupt exit, ensure cleanup to avoid leaving artifacts on helix machines. + // When https://github.com/microsoft/one-collect/issues/183 is fixed, this and the above TMPDIR should be removed. + CleanupTraceeDiagnosticPorts(diagnosticPortDir, traceePid); + + if (!recordTraceProcess.HasExited) + { + // Until record-trace supports duration, the only way to stop it is to send SIGINT (ctrl+c) + Console.WriteLine($"Stopping record-trace with SIGINT."); + Kill(recordTraceProcess.Id, SIGINT); + Console.WriteLine($"Waiting for record-trace to exit..."); + if (!recordTraceProcess.WaitForExit(20000)) + { + // record-trace needs to stop gracefully to generate the trace file + Console.WriteLine($"record-trace did not exit within the 20s timeout, killing it."); + recordTraceProcess.Kill(); + } + } + else + { + Console.WriteLine($"record-trace unexpectedly exited without SIGINT with code {recordTraceProcess.ExitCode}."); + } + recordTraceProcess.WaitForExit(); // flush async output + + if (!File.Exists(traceFilePath)) + { + Console.Error.WriteLine($"Expected trace file not found at `{traceFilePath}`"); + return -1; + } + + if (!ValidateTraceeEvents(traceFilePath)) + { + Console.Error.WriteLine($"Trace file `{traceFilePath}` does not contain expected events."); + UploadTraceFile(traceFilePath); + return -1; + } + + return 100; + } + + private static void CleanupTraceeDiagnosticPorts(string diagnosticPortDir, int traceePid) + { + try + { + string[] udsFiles = Directory.GetFiles(diagnosticPortDir, $"dotnet-diagnostic-{traceePid}-*-socket"); + foreach (string udsFile in udsFiles) + { + Console.WriteLine($"Deleting tracee diagnostic port UDS file: {udsFile}"); + File.Delete(udsFile); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Failed to cleanup tracee diagnostic ports: {ex}"); + } + } + + private static bool ValidateTraceeEvents(string traceFilePath) + { + using EventPipeEventSource source = new EventPipeEventSource(traceFilePath); + bool allocationSampledEventFound = false; + + // TraceEvent's ClrTraceEventParser does not know about the AllocationSampled Event, so it shows up as "Unknown(303)" + source.Dynamic.All += (TraceEvent e) => + { + if (e.ProviderName == "Microsoft-Windows-DotNETRuntime") + { + if (e.EventName == "AllocationSampled" || (e.ID == (TraceEventID)303 && e.EventName.StartsWith("Unknown"))) + { + allocationSampledEventFound = true; + } + } + }; + + source.Process(); + return allocationSampledEventFound; + } + + private static void UploadTraceFile(string traceFilePath) + { + var helixWorkItemDirectory = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT"); + if (helixWorkItemDirectory != null && Directory.Exists(helixWorkItemDirectory)) + { + var destPath = Path.Combine(helixWorkItemDirectory, Path.GetFileName(traceFilePath)); + Console.WriteLine($"Uploading trace file to Helix work item directory: {destPath}"); + File.Copy(traceFilePath, destPath, overwrite: true); + } + else + { + Console.WriteLine($"Helix work item directory not found. Trace file remains at: {traceFilePath}"); + } + } + } +} diff --git a/src/tests/tracing/eventpipe/userevents/userevents.csproj b/src/tests/tracing/eventpipe/userevents/userevents.csproj new file mode 100644 index 00000000000000..e9f11e3444bc6d --- /dev/null +++ b/src/tests/tracing/eventpipe/userevents/userevents.csproj @@ -0,0 +1,43 @@ + + + true + true + false + true + + + + + + + + + + + + + + + Always + dotnet-common.script + + + + + + <_DestDir>$(TargetDir) + <_DestDir Condition="'$(_DestDir)' == ''">$(OutputPath) + <_RecordTraceSource>$(NuGetPackageRoot)microsoft.onecollect.recordtrace/$(MicrosoftOneCollectRecordTraceVersion)/runtimes/linux-$(TargetArchitecture)/native/record-trace + <_RecordTraceRelative Condition="$(BuildProjectRelativeDir) != ''">$(BuildProjectRelativeDir)record-trace + <_RecordTraceRelative Condition="'$(_RecordTraceRelative)' == ''">$([System.IO.Path]::GetRelativePath('$(TestBinDir)', '$(_DestDir)record-trace')) + + + + + + + + + + + diff --git a/src/tests/tracing/eventpipe/userevents/usereventstracee.cs b/src/tests/tracing/eventpipe/userevents/usereventstracee.cs new file mode 100644 index 00000000000000..766f94e2e5d30c --- /dev/null +++ b/src/tests/tracing/eventpipe/userevents/usereventstracee.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Threading; + +namespace Tracing.Tests.UserEvents +{ + public class UserEventsTracee + { + private static byte[] s_array; + + public static void Run() + { + long startTimestamp = Stopwatch.GetTimestamp(); + long targetTicks = Stopwatch.Frequency; // 1s + + while (Stopwatch.GetTimestamp() - startTimestamp < targetTicks) + { + s_array = new byte[1024 * 100]; + Thread.Sleep(100); + } + } + } +}