diff --git a/src/Agent.Sdk/ProcessInvoker.cs b/src/Agent.Sdk/ProcessInvoker.cs index 3a9d32e6eb..f4861cde1d 100644 --- a/src/Agent.Sdk/ProcessInvoker.cs +++ b/src/Agent.Sdk/ProcessInvoker.cs @@ -404,6 +404,10 @@ protected virtual void Dispose(bool disposing) { if (_proc != null) { + //Dispose the standard output/error stream. Refer: https://github.com/dotnet/runtime/issues/58872 + _proc.StandardOutput?.Dispose(); + _proc.StandardError?.Dispose(); + _proc.Dispose(); _proc = null; } diff --git a/src/Agent.Worker/ResourceMetricsManager.cs b/src/Agent.Worker/ResourceMetricsManager.cs index e56f9cb35e..632e53b523 100644 --- a/src/Agent.Worker/ResourceMetricsManager.cs +++ b/src/Agent.Worker/ResourceMetricsManager.cs @@ -50,12 +50,14 @@ public sealed class ResourceMetricsManager : AgentService, IResourceMetricsManag #region MetricStructs private struct CpuInfo { + public bool IsProcRunning; public DateTime Updated; public double Usage; } private struct DiskInfo { + public bool IsProcRunning; public DateTime Updated; public double TotalDiskSpaceMB; public double FreeDiskSpaceMB; @@ -64,6 +66,7 @@ private struct DiskInfo public struct MemoryInfo { + public bool IsProcRunning; public DateTime Updated; public long TotalMemoryMB; public long UsedMemoryMB; @@ -118,101 +121,121 @@ private async Task GetCpuInfoAsync(CancellationToken cancellationToken) return; } - if (PlatformUtil.RunningOnWindows) + lock (_cpuInfoLock) { - await Task.Run(() => + if (_cpuInfo.IsProcRunning) { - using var query = new ManagementObjectSearcher("SELECT PercentIdleTime FROM Win32_PerfFormattedData_PerfOS_Processor WHERE Name=\"_Total\""); - - ManagementObject cpuInfo = query.Get().OfType().FirstOrDefault() ?? throw new Exception("Failed to execute WMI query"); - var cpuInfoIdle = Convert.ToDouble(cpuInfo["PercentIdleTime"]); - - lock (_cpuInfoLock) - { - _cpuInfo.Updated = DateTime.Now; - _cpuInfo.Usage = 100 - cpuInfoIdle; - } - }, cancellationToken); + return; + } + _cpuInfo.IsProcRunning = true; } - if (PlatformUtil.RunningOnLinux) + try { - List samples = new(); - int samplesCount = 10; - - // /proc/stat updates linearly in real time and shows CPU time counters during the whole system uptime - // so we need to collect multiple samples to calculate CPU usage - for (int i = 0; i < samplesCount + 1; i++) + if (PlatformUtil.RunningOnWindows) { - string[] strings = await File.ReadAllLinesAsync("/proc/stat", cancellationToken); - if (cancellationToken.IsCancellationRequested) + await Task.Run(() => { - return; - } + using var query = new ManagementObjectSearcher("SELECT PercentIdleTime FROM Win32_PerfFormattedData_PerfOS_Processor WHERE Name=\"_Total\""); - samples.Add(strings[0] - .Split(' ', StringSplitOptions.RemoveEmptyEntries) - .Skip(1) - .Select(float.Parse) - .ToArray()); + ManagementObject cpuInfo = query.Get().OfType().FirstOrDefault() ?? throw new Exception("Failed to execute WMI query"); + var cpuInfoIdle = Convert.ToDouble(cpuInfo["PercentIdleTime"]); - await Task.Delay(100, cancellationToken); + lock (_cpuInfoLock) + { + _cpuInfo.Updated = DateTime.Now; + _cpuInfo.Usage = 100 - cpuInfoIdle; + } + }, cancellationToken); } - // The CPU time counters in the /proc/stat are: - // user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice - // - // We need to get deltas for idle and total CPU time using the gathered samples - // and calculate the average to provide the CPU utilization in the moment - double cpuUsage = 0.0; - for (int i = 1; i < samplesCount + 1; i++) + if (PlatformUtil.RunningOnLinux) { - double idle = samples[i][3] - samples[i - 1][3]; - double total = samples[i].Sum() - samples[i - 1].Sum(); + List samples = new(); + int samplesCount = 10; - cpuUsage += 1.0 - (idle / total); - } + // /proc/stat updates linearly in real time and shows CPU time counters during the whole system uptime + // so we need to collect multiple samples to calculate CPU usage + for (int i = 0; i < samplesCount + 1; i++) + { + string[] strings = await File.ReadAllLinesAsync("/proc/stat", cancellationToken); + if (cancellationToken.IsCancellationRequested) + { + return; + } - lock (_cpuInfoLock) - { - _cpuInfo.Updated = DateTime.Now; - _cpuInfo.Usage = (cpuUsage / samplesCount) * 100; + samples.Add(strings[0] + .Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Skip(1) + .Select(float.Parse) + .ToArray()); + + await Task.Delay(100, cancellationToken); + } + + // The CPU time counters in the /proc/stat are: + // user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice + // + // We need to get deltas for idle and total CPU time using the gathered samples + // and calculate the average to provide the CPU utilization in the moment + double cpuUsage = 0.0; + for (int i = 1; i < samplesCount + 1; i++) + { + double idle = samples[i][3] - samples[i - 1][3]; + double total = samples[i].Sum() - samples[i - 1].Sum(); + + cpuUsage += 1.0 - (idle / total); + } + + lock (_cpuInfoLock) + { + _cpuInfo.Updated = DateTime.Now; + _cpuInfo.Usage = (cpuUsage / samplesCount) * 100; + } } - } - if (PlatformUtil.RunningOnMacOS) - { - using var processInvoker = HostContext.CreateService(); - List outputs = new List(); - processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) + if (PlatformUtil.RunningOnMacOS) { - outputs.Add(message.Data); - }; + using var processInvoker = HostContext.CreateService(); - processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) - { - Trace.Error($"Error on receiving CPU info: {message.Data}"); - }; + List outputs = new List(); + processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) + { + outputs.Add(message.Data); + }; - var filePath = "/bin/bash"; - var arguments = "-c \"top -l 2 -o cpu | grep ^CPU\""; - await processInvoker.ExecuteAsync( - workingDirectory: string.Empty, - fileName: filePath, - arguments: arguments, - environment: null, - requireExitCodeZero: true, - outputEncoding: null, - killProcessOnCancel: true, - cancellationToken: cancellationToken); - - // Use second sample for more accurate calculation - var cpuInfoIdle = double.Parse(outputs[1].Split(' ', (char)StringSplitOptions.RemoveEmptyEntries)[6].Trim('%')); + processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) + { + Trace.Error($"Error on receiving CPU info: {message.Data}"); + }; + + var filePath = "/bin/bash"; + var arguments = "-c \"top -l 2 -o cpu | grep ^CPU\""; + + await processInvoker.ExecuteAsync( + workingDirectory: string.Empty, + fileName: filePath, + arguments: arguments, + environment: null, + requireExitCodeZero: true, + outputEncoding: null, + killProcessOnCancel: true, + cancellationToken: cancellationToken); + // Use second sample for more accurate calculation + var cpuInfoIdle = double.Parse(outputs[1].Split(' ', (char)StringSplitOptions.RemoveEmptyEntries)[6].Trim('%')); + lock (_cpuInfoLock) + { + _cpuInfo.Updated = DateTime.Now; + _cpuInfo.Usage = 100 - cpuInfoIdle; + } + } + } + finally + { lock (_cpuInfoLock) { - _cpuInfo.Updated = DateTime.Now; - _cpuInfo.Usage = 100 - cpuInfoIdle; + _cpuInfo.IsProcRunning = false; } } } @@ -224,15 +247,34 @@ private void GetDiskInfo() return; } - string root = Path.GetPathRoot(_context.GetVariableValueOrDefault(Constants.Variables.Agent.WorkFolder)); - var driveInfo = new DriveInfo(root); - lock (_diskInfoLock) { - _diskInfo.Updated = DateTime.Now; - _diskInfo.TotalDiskSpaceMB = (double)driveInfo.TotalSize / 1048576; - _diskInfo.FreeDiskSpaceMB = (double)driveInfo.AvailableFreeSpace / 1048576; - _diskInfo.VolumeRoot = root; + if (_diskInfo.IsProcRunning) + { + return; + } + _diskInfo.IsProcRunning = true; + } + + try + { + string root = Path.GetPathRoot(_context.GetVariableValueOrDefault(Constants.Variables.Agent.WorkFolder)); + var driveInfo = new DriveInfo(root); + + lock (_diskInfoLock) + { + _diskInfo.Updated = DateTime.Now; + _diskInfo.TotalDiskSpaceMB = (double)driveInfo.TotalSize / 1048576; + _diskInfo.FreeDiskSpaceMB = (double)driveInfo.AvailableFreeSpace / 1048576; + _diskInfo.VolumeRoot = root; + } + } + finally + { + lock (_diskInfoLock) + { + _diskInfo.IsProcRunning = false; + } } } @@ -243,95 +285,115 @@ private async Task GetMemoryInfoAsync(CancellationToken cancellationToken) return; } - if (PlatformUtil.RunningOnWindows) + lock (_memoryInfoLock) + { + if (_memoryInfo.IsProcRunning) + { + return; + } + _memoryInfo.IsProcRunning = true; + } + + try { - await Task.Run(() => + if (PlatformUtil.RunningOnWindows) { - using var query = new ManagementObjectSearcher("SELECT FreePhysicalMemory, TotalVisibleMemorySize FROM CIM_OperatingSystem"); + await Task.Run(() => + { + using var query = new ManagementObjectSearcher("SELECT FreePhysicalMemory, TotalVisibleMemorySize FROM CIM_OperatingSystem"); - ManagementObject memoryInfo = query.Get().OfType().FirstOrDefault() ?? throw new Exception("Failed to execute WMI query"); - var freeMemory = Convert.ToInt64(memoryInfo["FreePhysicalMemory"]); - var totalMemory = Convert.ToInt64(memoryInfo["TotalVisibleMemorySize"]); + ManagementObject memoryInfo = query.Get().OfType().FirstOrDefault() ?? throw new Exception("Failed to execute WMI query"); + var freeMemory = Convert.ToInt64(memoryInfo["FreePhysicalMemory"]); + var totalMemory = Convert.ToInt64(memoryInfo["TotalVisibleMemorySize"]); + + lock (_memoryInfoLock) + { + _memoryInfo.Updated = DateTime.Now; + _memoryInfo.TotalMemoryMB = totalMemory / 1024; + _memoryInfo.UsedMemoryMB = (totalMemory - freeMemory) / 1024; + } + }, cancellationToken); + } + + if (PlatformUtil.RunningOnLinux) + { + string[] memoryInfo = await File.ReadAllLinesAsync("/proc/meminfo", cancellationToken); + if (cancellationToken.IsCancellationRequested) + { + return; + } + + // The /proc/meminfo file contains several memory counters. To calculate the available memory + // we need to get the total memory and the available memory counters + // The available memory contains the sum of free, cached, and buffer memory + // it shows more accurate information about the memory usage than the free memory counter + int totalMemory = int.Parse(memoryInfo[0].Split(" ", StringSplitOptions.RemoveEmptyEntries)[1]); + int availableMemory = int.Parse(memoryInfo[2].Split(" ", StringSplitOptions.RemoveEmptyEntries)[1]); lock (_memoryInfoLock) { _memoryInfo.Updated = DateTime.Now; _memoryInfo.TotalMemoryMB = totalMemory / 1024; - _memoryInfo.UsedMemoryMB = (totalMemory - freeMemory) / 1024; + _memoryInfo.UsedMemoryMB = (totalMemory - availableMemory) / 1024; } - }, cancellationToken); - } + } - if (PlatformUtil.RunningOnLinux) - { - string[] memoryInfo = await File.ReadAllLinesAsync("/proc/meminfo", cancellationToken); - if (cancellationToken.IsCancellationRequested) + if (PlatformUtil.RunningOnMacOS) { - return; - } + // vm_stat allows to get the most detailed information about memory usage on MacOS + // but unfortunately it returns values in pages and has no built-in arguments for custom output + // so we need to parse and cast the output manually - // The /proc/meminfo file contains several memory counters. To calculate the available memory - // we need to get the total memory and the available memory counters - // The available memory contains the sum of free, cached, and buffer memory - // it shows more accurate information about the memory usage than the free memory counter - int totalMemory = int.Parse(memoryInfo[0].Split(" ", StringSplitOptions.RemoveEmptyEntries)[1]); - int availableMemory = int.Parse(memoryInfo[2].Split(" ", StringSplitOptions.RemoveEmptyEntries)[1]); + using var processInvoker = HostContext.CreateService(); - lock (_memoryInfoLock) - { - _memoryInfo.Updated = DateTime.Now; - _memoryInfo.TotalMemoryMB = totalMemory / 1024; - _memoryInfo.UsedMemoryMB = (totalMemory - availableMemory) / 1024; - } - } + List outputs = new List(); + processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) + { + outputs.Add(message.Data); + }; - if (PlatformUtil.RunningOnMacOS) - { - // vm_stat allows to get the most detailed information about memory usage on MacOS - // but unfortunately it returns values in pages and has no built-in arguments for custom output - // so we need to parse and cast the output manually + processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) + { + Trace.Error($"Error on receiving memory info: {message.Data}"); + }; - using var processInvoker = HostContext.CreateService(); + var filePath = "vm_stat"; - List outputs = new List(); - processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) - { - outputs.Add(message.Data); - }; + await processInvoker.ExecuteAsync( + workingDirectory: string.Empty, + fileName: filePath, + arguments: string.Empty, + environment: null, + requireExitCodeZero: true, + outputEncoding: null, + killProcessOnCancel: true, + cancellationToken: cancellationToken); - processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) - { - Trace.Error($"Error on receiving memory info: {message.Data}"); - }; + var pageSize = int.Parse(outputs[0].Split(" ", StringSplitOptions.RemoveEmptyEntries)[7]); + + var pagesFree = long.Parse(outputs[1].Split(" ", StringSplitOptions.RemoveEmptyEntries)[2].Trim('.')); + var pagesActive = long.Parse(outputs[2].Split(" ", StringSplitOptions.RemoveEmptyEntries)[2].Trim('.')); + var pagesInactive = long.Parse(outputs[3].Split(" ", StringSplitOptions.RemoveEmptyEntries)[2].Trim('.')); + var pagesSpeculative = long.Parse(outputs[4].Split(" ", StringSplitOptions.RemoveEmptyEntries)[2].Trim('.')); + var pagesWiredDown = long.Parse(outputs[6].Split(" ", StringSplitOptions.RemoveEmptyEntries)[3].Trim('.')); + var pagesOccupied = long.Parse(outputs[16].Split(" ", StringSplitOptions.RemoveEmptyEntries)[4].Trim('.')); - var filePath = "vm_stat"; - await processInvoker.ExecuteAsync( - workingDirectory: string.Empty, - fileName: filePath, - arguments: string.Empty, - environment: null, - requireExitCodeZero: true, - outputEncoding: null, - killProcessOnCancel: true, - cancellationToken: cancellationToken); - - var pageSize = int.Parse(outputs[0].Split(" ", StringSplitOptions.RemoveEmptyEntries)[7]); - - var pagesFree = long.Parse(outputs[1].Split(" ", StringSplitOptions.RemoveEmptyEntries)[2].Trim('.')); - var pagesActive = long.Parse(outputs[2].Split(" ", StringSplitOptions.RemoveEmptyEntries)[2].Trim('.')); - var pagesInactive = long.Parse(outputs[3].Split(" ", StringSplitOptions.RemoveEmptyEntries)[2].Trim('.')); - var pagesSpeculative = long.Parse(outputs[4].Split(" ", StringSplitOptions.RemoveEmptyEntries)[2].Trim('.')); - var pagesWiredDown = long.Parse(outputs[6].Split(" ", StringSplitOptions.RemoveEmptyEntries)[3].Trim('.')); - var pagesOccupied = long.Parse(outputs[16].Split(" ", StringSplitOptions.RemoveEmptyEntries)[4].Trim('.')); - - var freeMemory = (pagesFree + pagesInactive) * pageSize; - var usedMemory = (pagesActive + pagesSpeculative + pagesWiredDown + pagesOccupied) * pageSize; + var freeMemory = (pagesFree + pagesInactive) * pageSize; + var usedMemory = (pagesActive + pagesSpeculative + pagesWiredDown + pagesOccupied) * pageSize; + lock (_memoryInfoLock) + { + _memoryInfo.Updated = DateTime.Now; + _memoryInfo.TotalMemoryMB = (freeMemory + usedMemory) / 1048576; + _memoryInfo.UsedMemoryMB = usedMemory / 1048576; + } + } + } + finally + { lock (_memoryInfoLock) { - _memoryInfo.Updated = DateTime.Now; - _memoryInfo.TotalMemoryMB = (freeMemory + usedMemory) / 1048576; - _memoryInfo.UsedMemoryMB = usedMemory / 1048576; + _memoryInfo.IsProcRunning = false; } } }