-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProcessCache.cs
More file actions
165 lines (137 loc) · 7.04 KB
/
Copy pathProcessCache.cs
File metadata and controls
165 lines (137 loc) · 7.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
using System.Text;
using Hawkynt.ProcessManager.Query;
namespace Hawkynt.ProcessManager.Platform.Linux;
/// <summary>
/// The per-process things that do not change, kept for as long as the process lives.
/// </summary>
/// <remarks>
/// <para>
/// Two costs are avoided here. The first is the file paths: composing
/// <c>/proc/1234/status</c> four times per process per sample is four thousand buffers a second at a
/// thousand processes, so each is built once, as UTF-8 bytes ready for the syscall. The second is
/// the contents that never change — the command line, the image path, the cgroup — which are read
/// once per process rather than once per sample.
/// </para>
/// <para>
/// The name is the interesting case: <c>comm</c> <em>can</em> change (a process may rename itself),
/// so it is re-read every sample, but the bytes are compared against the cached string and a new one
/// is allocated only when they actually differ. Correct and free in the same move (PRD §4).
/// </para>
/// </remarks>
internal sealed class ProcessCache {
private static ReadOnlySpan<byte> _status => "status"u8;
private static ReadOnlySpan<byte> _io => "io"u8;
private static ReadOnlySpan<byte> _fd => "fd"u8;
private static ReadOnlySpan<byte> _smapsRollup => "smaps_rollup"u8;
private static ReadOnlySpan<byte> _attrCurrent => "attr/current"u8;
private byte[] _nameBytes = [];
private int _nameLength;
private bool _staticsLoaded;
public ProcessCache(ReadOnlySpan<byte> procRoot, int pid) {
this.StatusPath = BuildPath(procRoot, pid, _status);
this.IoPath = BuildPath(procRoot, pid, _io);
this.FdPath = BuildPath(procRoot, pid, _fd);
this.SmapsRollupPath = BuildPath(procRoot, pid, _smapsRollup);
this.SecurityContextPath = BuildPath(procRoot, pid, _attrCurrent);
this.Pid = pid;
}
public int Pid { get; }
/// <summary>NUL-terminated UTF-8, ready to hand to <c>open(2)</c>.</summary>
public byte[] StatusPath { get; }
public byte[] IoPath { get; }
public byte[] FdPath { get; }
public byte[] SmapsRollupPath { get; }
public byte[] SecurityContextPath { get; }
public string Name { get; private set; } = string.Empty;
public string? CommandLine { get; private set; }
public string? ImagePath { get; private set; }
public string? ContainerPath { get; private set; }
/// <summary>The sample number this entry was last seen in; older entries are pruned.</summary>
public int Generation { get; set; }
/// <summary>
/// What the process is running, worked out once (PRD §14, §70).
/// </summary>
/// <remarks>
/// None of it changes while the process lives: an image is not repackaged underneath a running
/// program, a file's birth time is fixed, and a runtime that is mapped stays mapped. So each of
/// these costs its read once per process rather than once per sample — which is the difference
/// between a column somebody can leave switched on and one they cannot (PRD §5.4).
/// </remarks>
public bool IdentityLoaded { get; set; }
public Model.PackageIdentity Package { get; set; } = Model.PackageIdentity.NotChecked;
public Model.SignatureStatus PackageStatus { get; set; }
public string? PackageStatusDetail { get; set; }
public Model.SignatureStatus TrustChain { get; set; }
public string? TrustChainDetail { get; set; }
public Model.UnknownReason TrustChainReason { get; set; } = Model.UnknownReason.NotSampledYet;
public string? ApplicationName { get; set; }
public bool ApplicationNameAmbiguous { get; set; }
public Model.UnknownReason ApplicationNameReason { get; set; } = Model.UnknownReason.NotSampledYet;
public Model.ProcessRuntime Runtime { get; set; }
public Model.UnknownReason RuntimeReason { get; set; } = Model.UnknownReason.NotSampledYet;
public Model.Counter ImageCreatedUtcTicks { get; set; } = Model.Counter.NotSampledYet;
private static byte[] BuildPath(ReadOnlySpan<byte> procRoot, int pid, ReadOnlySpan<byte> leaf) {
Span<byte> buffer = stackalloc byte[ProcPath.MaxLength];
return ProcPath.Build(buffer, procRoot, pid, leaf).ToArray();
}
/// <summary>
/// Extracts <c>comm</c> from a <c>stat</c> file and returns the cached string when it is unchanged.
/// </summary>
public string UpdateName(ReadOnlySpan<byte> statContent) {
var open = statContent.IndexOf((byte)'(');
var close = statContent.LastIndexOf((byte)')');
if (open < 0 || close <= open)
return this.Name;
var comm = statContent[(open + 1)..close];
if (comm.Length == this._nameLength && comm.SequenceEqual(this._nameBytes.AsSpan(0, this._nameLength)))
return this.Name;
if (this._nameBytes.Length < comm.Length)
this._nameBytes = new byte[comm.Length];
comm.CopyTo(this._nameBytes);
this._nameLength = comm.Length;
this.Name = Encoding.UTF8.GetString(comm);
return this.Name;
}
/// <summary>Reads the things that are read once: command line, image path, cgroup.</summary>
public void EnsureStatics(ProcFileReader reader, LinuxProbeOptions options, ReadOnlySpan<byte> procRoot, string procRootText) {
if (this._staticsLoaded)
return;
this._staticsLoaded = true;
Span<byte> buffer = stackalloc byte[ProcPath.MaxLength];
var cmdlinePath = ProcPath.Build(buffer, procRoot, this.Pid, "cmdline"u8);
if (reader.TryRead(cmdlinePath, out var cmdline, out _) && !cmdline.IsEmpty)
this.CommandLine = DecodeCommandLine(cmdline);
this.ImagePath = reader.TryReadLink($"{procRootText}/{this.Pid}/exe");
if (!options.ReadCgroups)
return;
Span<byte> cgroupBuffer = stackalloc byte[ProcPath.MaxLength];
var cgroupPath = ProcPath.Build(cgroupBuffer, procRoot, this.Pid, "cgroup"u8);
if (!reader.TryRead(cgroupPath, out var cgroup, out _) || cgroup.IsEmpty)
return;
// cgroup v2 writes exactly one line, "0::/path". v1 writes several, and the one worth showing is
// whichever names a container; taking the first line's path is right for v2 and a reasonable
// answer for v1, and the column is documented as the cgroup path rather than as a container name.
var scanner = new AsciiScanner(cgroup);
var line = scanner.NextLine();
var lastColon = line.LastIndexOf((byte)':');
if (lastColon >= 0 && lastColon + 1 < line.Length)
this.ContainerPath = Encoding.UTF8.GetString(line[(lastColon + 1)..]);
}
/// <summary>
/// <c>cmdline</c> is NUL-separated with a trailing NUL. Joining with spaces is what every tool
/// shows; an argument that itself contains a space is therefore indistinguishable, which is a
/// property of the display and not of the data — the argument vector is intact in the detail view.
/// </summary>
private static string DecodeCommandLine(ReadOnlySpan<byte> content) {
while (!content.IsEmpty && content[^1] == 0)
content = content[..^1];
if (content.IsEmpty)
return string.Empty;
Span<byte> copy = content.Length <= 512 ? stackalloc byte[content.Length] : new byte[content.Length];
content.CopyTo(copy);
for (var i = 0; i < copy.Length; ++i)
if (copy[i] == 0)
copy[i] = (byte)' ';
return Encoding.UTF8.GetString(copy);
}
}