| File: Linux\Network\LinuxNetworkUtilizationParser.cs | Web Access |
| Project: src\src\Libraries\Microsoft.Extensions.Diagnostics.ResourceMonitoring\Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj (Microsoft.Extensions.Diagnostics.ResourceMonitoring) |
// 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.Collections.Generic; using System.IO; using Microsoft.Extensions.ObjectPool; #if !NET8_0_OR_GREATER using Microsoft.Shared.StringSplit; #endif using Microsoft.Shared.Diagnostics; using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Network; internal sealed class LinuxNetworkUtilizationParser { private static readonly ObjectPool<BufferWriter<char>> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool<char>(); /// <remarks> /// File that provides information about currently active TCP_IPv4 connections. /// </remarks> private static readonly FileInfo _tcp = new("/proc/net/tcp"); /// <remarks> /// File that provides information about currently active TCP_IPv6 connections. /// </remarks> private static readonly FileInfo _tcp6 = new("/proc/net/tcp6"); private readonly IFileSystem _fileSystem; /// <remarks> /// Reads the contents of a file located at <see cref="_tcp"/> and parses it to extract information about the TCP/IP state info of the system. /// </remarks> public TcpStateInfo GetTcpIPv4StateInfo() => GetTcpStateInfo(_tcp); /// <remarks> /// Reads the contents of a file located at <see cref="_tcp6"/> and parses it to extract information about the TCP/IP state info of the system. /// </remarks> public TcpStateInfo GetTcpIPv6StateInfo() => GetTcpStateInfo(_tcp6); public LinuxNetworkUtilizationParser(IFileSystem fileSystem) { _fileSystem = fileSystem; } /// <remarks> /// Parses the contents of the <paramref name="buffer"/> and updates the <paramref name="tcpStateInfo"/> with the parsed information. /// For the data format expected in the <paramref name="buffer"/>, refer <see href="https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt">proc net tcp</see>. /// </remarks> private static void UpdateTcpStateInfo(ReadOnlySpan<char> buffer, TcpStateInfo tcpStateInfo) { const int Base16 = 16; // The buffer contains one line from /proc/net/tcp(6) file, e.g.: // 0: 030011AC:8AF2 C1B17822:01BB 01 00000000:00000000 02:000000D1 00000000 472 0 2481276 2 00000000c62511cb 28 4 26 10 -1 // The line may contain leading spaces, so we have to trim those. // tcpConnectionState is in the 4th column - i.e., "01". ReadOnlySpan<char> line = buffer.TrimStart(); #if NET8_0_OR_GREATER const int Target = 5; Span<Range> range = stackalloc Range[Target]; // In .NET 8+, if capacity of destination range array is less than number of ranges found by ReadOnlySpan<T>.Split(), // the last range in the array will get all the remaining elements of the ReadOnlySpan. // Therefore, we request 5 ranges instead of 4, and then range[Target - 2] will have the range we need without the remaining elements. int numRanges = line.Split(range, ' ', StringSplitOptions.RemoveEmptyEntries); #else const int Target = 4; Span<StringRange> range = stackalloc StringRange[Target]; // In our StringRange API, if capacity of destination range array is less than number of ranges found by ReadOnlySpan<T>.TrySplit(), // the last range in the array will get the last range as expected, and all remaining elements will be ignored. // Hence range[Target - 1] will have the last range as we need. _ = line.TrySplit(" ", range, out int numRanges, StringComparison.OrdinalIgnoreCase, StringSplitOptions.RemoveEmptyEntries); #endif if (numRanges < Target) { Throw.InvalidOperationException($"Could not split contents. We expected every line to contain more than {Target - 1} elements, but it has only {numRanges} elements."); } #if NET8_0_OR_GREATER ReadOnlySpan<char> tcpConnectionState = line.Slice(range[Target - 2].Start.Value, range[Target - 2].End.Value - range[Target - 2].Start.Value); #else ReadOnlySpan<char> tcpConnectionState = line.Slice(range[Target - 1].Index, range[Target - 1].Count); #endif // At this point, tcpConnectionState contains one of TCP connection states in hexadecimal format, e.g., "01", // which we now need to convert to the LinuxTcpState enum. // Note: until this API proposal is implemented https://github.com/dotnet/runtime/issues/61397 // we have to allocate and throw away memory using .ToString(). var state = (LinuxTcpState)Convert.ToInt32(tcpConnectionState.ToString(), Base16); switch (state) { case LinuxTcpState.ESTABLISHED: tcpStateInfo.EstabCount++; break; case LinuxTcpState.SYN_SENT: tcpStateInfo.SynSentCount++; break; case LinuxTcpState.SYN_RECV: tcpStateInfo.SynRcvdCount++; break; case LinuxTcpState.FIN_WAIT1: tcpStateInfo.FinWait1Count++; break; case LinuxTcpState.FIN_WAIT2: tcpStateInfo.FinWait2Count++; break; case LinuxTcpState.TIME_WAIT: tcpStateInfo.TimeWaitCount++; break; case LinuxTcpState.CLOSE: tcpStateInfo.ClosedCount++; break; case LinuxTcpState.CLOSE_WAIT: tcpStateInfo.CloseWaitCount++; break; case LinuxTcpState.LAST_ACK: tcpStateInfo.LastAckCount++; break; case LinuxTcpState.LISTEN: tcpStateInfo.ListenCount++; break; case LinuxTcpState.CLOSING: tcpStateInfo.ClosingCount++; break; default: Throw.IfOutOfRange(state); break; } } /// <remarks> /// Reads the contents of a file and parses it to extract information about the TCP/IP state info of the system. /// </remarks> private TcpStateInfo GetTcpStateInfo(FileInfo file) { // The value we are interested in starts with this "sl". const string Sl = "sl"; TcpStateInfo tcpStateInfo = new(); using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool); using IEnumerator<ReadOnlyMemory<char>> enumerableLines = _fileSystem.ReadAllByLines(file, bufferWriter.Buffer).GetEnumerator(); if (!enumerableLines.MoveNext()) { Throw.InvalidOperationException($"Could not parse '{file}'. File was empty."); } ReadOnlySpan<char> firstLine = enumerableLines.Current.TrimStart().Span; if (!firstLine.StartsWith(Sl, StringComparison.Ordinal)) { Throw.InvalidOperationException($"Could not parse '{file}'. We expected first line of the file to start with '{Sl}' but it was '{firstLine}' instead."); } while (enumerableLines.MoveNext()) { UpdateTcpStateInfo(enumerableLines.Current.Span, tcpStateInfo); } return tcpStateInfo; } }