Skip to content

Commit a535948

Browse files
authored
[Exporter.Geneva] Refactor ConsoleCommand utility class (#2654)
1 parent f4dff45 commit a535948

5 files changed

Lines changed: 247 additions & 227 deletions

File tree

opentelemetry-dotnet-contrib.sln

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Share
357357
EndProject
358358
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{70CA77D4-5D7F-4D70-A6B5-8AAC07A8EA3C}"
359359
ProjectSection(SolutionItems) = preProject
360+
test\Shared\ConsoleCommand.cs = test\Shared\ConsoleCommand.cs
360361
test\Shared\CustomTextMapPropagator.cs = test\Shared\CustomTextMapPropagator.cs
361362
test\Shared\EnabledOnDockerPlatformTheoryAttribute.cs = test\Shared\EnabledOnDockerPlatformTheoryAttribute.cs
362363
test\Shared\EventSourceTestHelper.cs = test\Shared\EventSourceTestHelper.cs
363364
test\Shared\InMemoryEventListener.cs = test\Shared\InMemoryEventListener.cs
365+
test\Shared\PerfTracepointListener.cs = test\Shared\PerfTracepointListener.cs
364366
test\Shared\PlatformHelpers.cs = test\Shared\PlatformHelpers.cs
365367
test\Shared\SkipUnlessEnvVarFoundFactAttribute.cs = test\Shared\SkipUnlessEnvVarFoundFactAttribute.cs
366368
test\Shared\SkipUnlessEnvVarFoundTheoryAttribute.cs = test\Shared\SkipUnlessEnvVarFoundTheoryAttribute.cs

test/OpenTelemetry.Exporter.Geneva.Tests/OpenTelemetry.Exporter.Geneva.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
</ItemGroup>
2424

2525
<ItemGroup>
26+
<Compile Include="$(RepoRoot)\test\Shared\ConsoleCommand.cs" Link="Includes\ConsoleCommand.cs" />
27+
<Compile Include="$(RepoRoot)\test\Shared\PerfTracepointListener.cs" Link="Includes\PerfTracepointListener.cs" />
2628
<Compile Include="$(RepoRoot)\test\Shared\PlatformHelpers.cs" Link="Includes\PlatformHelpers.cs" />
2729
</ItemGroup>
2830

test/OpenTelemetry.Exporter.Geneva.Tests/UnixUserEventsDataTransportTests.cs

Lines changed: 1 addition & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33

44
#if NET6_0_OR_GREATER
55

6-
using System.Diagnostics;
76
using System.Globalization;
8-
using System.Text.RegularExpressions;
97
using Microsoft.LinuxTracepoints.Provider;
108
using OpenTelemetry.Exporter.Geneva.Transports;
9+
using OpenTelemetry.Tests;
1110
using Xunit;
1211
using Xunit.Abstractions;
1312

@@ -246,231 +245,6 @@ private static void EnsureUserEventsEnabled()
246245
throw new NotSupportedException("Kernel does not support user_events. Verify your distribution/kernel supports user_events: https://docs.kernel.org/trace/user_events.html.");
247246
}
248247
}
249-
250-
private sealed class ConsoleCommand : IDisposable
251-
{
252-
private readonly Process process;
253-
private readonly List<string> output = [];
254-
private readonly List<string> errors = [];
255-
256-
private ConsoleCommand(
257-
string command,
258-
string arguments,
259-
Action<string>? onOutputReceived,
260-
Action<string>? onErrorReceived)
261-
{
262-
Console.WriteLine($"{command} {arguments}");
263-
264-
var process = new Process
265-
{
266-
StartInfo = new()
267-
{
268-
FileName = command,
269-
Arguments = arguments,
270-
RedirectStandardOutput = true,
271-
RedirectStandardError = true,
272-
RedirectStandardInput = false,
273-
},
274-
};
275-
276-
process.OutputDataReceived += (sender, args) =>
277-
{
278-
if (!string.IsNullOrEmpty(args.Data))
279-
{
280-
this.output.Add(args.Data);
281-
Console.WriteLine($"{command} {arguments} [OUT] {args.Data}");
282-
283-
onOutputReceived?.Invoke(args.Data);
284-
}
285-
};
286-
287-
process.ErrorDataReceived += (sender, args) =>
288-
{
289-
if (!string.IsNullOrEmpty(args.Data))
290-
{
291-
this.errors.Add(args.Data);
292-
Console.WriteLine($"[ERR] {args.Data}");
293-
294-
onErrorReceived?.Invoke(args.Data);
295-
}
296-
};
297-
298-
process.Start();
299-
300-
process.BeginOutputReadLine();
301-
process.BeginErrorReadLine();
302-
303-
this.process = process;
304-
}
305-
306-
public IEnumerable<string> Output => this.output;
307-
308-
public IEnumerable<string> Errors => this.errors;
309-
310-
public static IEnumerable<string> Run(
311-
string command,
312-
string arguments)
313-
{
314-
Run(command, arguments, out _, out var errors);
315-
316-
return errors;
317-
}
318-
319-
public static void Run(
320-
string command,
321-
string arguments,
322-
out IEnumerable<string> output,
323-
out IEnumerable<string> errors)
324-
{
325-
var consoleCommand = new ConsoleCommand(command, arguments, onOutputReceived: null, onErrorReceived: null);
326-
consoleCommand.Dispose();
327-
328-
output = consoleCommand.Output;
329-
errors = consoleCommand.Errors;
330-
}
331-
332-
public static ConsoleCommand Start(
333-
string command,
334-
string arguments,
335-
Action<string>? onOutputReceived = null,
336-
Action<string>? onErrorReceived = null)
337-
=> new(command, arguments, onOutputReceived, onErrorReceived);
338-
339-
public void Kill()
340-
{
341-
this.process.Kill();
342-
}
343-
344-
public void Dispose()
345-
{
346-
this.process.WaitForExit();
347-
348-
this.process.CancelOutputRead();
349-
this.process.CancelErrorRead();
350-
351-
this.process.Dispose();
352-
}
353-
}
354-
355-
// Warning: Do NOT use this class/design to listen/read user_events in prod.
356-
// It is a hack to workaround lack of decent bits for listening. Hopefully
357-
// this can be removed if/when
358-
// https://github.com/microsoft/LinuxTracepoints-Net/ has listening bits or
359-
// dotnet/runtime supports user_events (both reading & writing) directly.
360-
private sealed class PerfTracepointListener : IDisposable
361-
{
362-
private readonly string name;
363-
private readonly PerfTracepoint tracepoint;
364-
private readonly ConsoleCommand catCommand;
365-
private readonly Regex eventRegex = new("(\\w+?)=([\\w\\(\\) .,-]*)( |$)", RegexOptions.Compiled);
366-
367-
public PerfTracepointListener(string name, string nameArgs)
368-
{
369-
this.name = name;
370-
371-
this.tracepoint = new PerfTracepoint(nameArgs);
372-
373-
// EACCES (13): Permission denied
374-
if (this.tracepoint.RegisterResult == 13)
375-
{
376-
throw new UnauthorizedAccessException($"Tracepoint could not be registered: '{this.tracepoint.RegisterResult}'. Permission denied.");
377-
}
378-
379-
if (this.tracepoint.RegisterResult != 0)
380-
{
381-
throw new NotSupportedException($"Tracepoint could not be registered: '{this.tracepoint.RegisterResult}'");
382-
}
383-
384-
this.catCommand = ConsoleCommand.Start("cat", "/sys/kernel/debug/tracing/trace_pipe", onOutputReceived: this.OnCatOutputReceived);
385-
if (this.catCommand.Errors.Any())
386-
{
387-
throw new InvalidOperationException($"Could not read '{name}' tracepoints");
388-
}
389-
}
390-
391-
public List<Dictionary<string, string>> Events { get; } = [];
392-
393-
public bool IsEnabled()
394-
{
395-
ConsoleCommand.Run(
396-
"cat",
397-
$"/sys/kernel/tracing/events/user_events/{this.name}/enable",
398-
out var output,
399-
out var errors);
400-
401-
return errors.Any() || output.Count() != 1
402-
? throw new InvalidOperationException($"Could not determine if '{this.name}' tracepoint is enabled")
403-
: output.First() != "0";
404-
}
405-
406-
public void Enable()
407-
{
408-
var errors = ConsoleCommand.Run("sh", @$"-c ""echo '1' > /sys/kernel/tracing/events/user_events/{this.name}/enable""");
409-
410-
if (errors.Any())
411-
{
412-
throw new InvalidOperationException($"Could not enable '{this.name}' tracepoint");
413-
}
414-
}
415-
416-
public void Disable()
417-
{
418-
var errors = ConsoleCommand.Run("sh", @$"-c ""echo '0' > /sys/kernel/tracing/events/user_events/{this.name}/enable""");
419-
420-
if (errors.Any())
421-
{
422-
throw new InvalidOperationException($"Could not disable '{this.name}' tracepoint");
423-
}
424-
}
425-
426-
public void Dispose()
427-
{
428-
try
429-
{
430-
if (this.catCommand != null)
431-
{
432-
if (this.catCommand.Errors.Any())
433-
{
434-
throw new InvalidOperationException($"Could not read '{this.name}' tracepoints");
435-
}
436-
437-
this.catCommand.Kill();
438-
this.catCommand.Dispose();
439-
}
440-
}
441-
finally
442-
{
443-
this.tracepoint.Dispose();
444-
}
445-
}
446-
447-
private void OnCatOutputReceived(string output)
448-
{
449-
var name = $": {this.name}:";
450-
451-
var startingPosition = output.IndexOf(name, StringComparison.Ordinal);
452-
if (startingPosition < 0)
453-
{
454-
return;
455-
}
456-
457-
startingPosition += name.Length;
458-
459-
var matches = this.eventRegex.Matches(output, startingPosition);
460-
461-
if (matches.Count > 0)
462-
{
463-
Dictionary<string, string> eventData = new(matches.Count);
464-
465-
foreach (Match match in matches)
466-
{
467-
eventData[match.Groups[1].Value] = match.Groups[2].Value;
468-
}
469-
470-
this.Events.Add(eventData);
471-
}
472-
}
473-
}
474248
}
475249

476250
#endif

test/Shared/ConsoleCommand.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Diagnostics;
5+
6+
namespace OpenTelemetry.Tests;
7+
8+
internal sealed class ConsoleCommand : IDisposable
9+
{
10+
private readonly Process process;
11+
private readonly List<string> output = [];
12+
private readonly List<string> errors = [];
13+
14+
private ConsoleCommand(
15+
string command,
16+
string arguments,
17+
Action<string>? onOutputReceived,
18+
Action<string>? onErrorReceived)
19+
{
20+
Console.WriteLine($"{command} {arguments}");
21+
22+
var process = new Process
23+
{
24+
StartInfo = new()
25+
{
26+
FileName = command,
27+
Arguments = arguments,
28+
RedirectStandardOutput = true,
29+
RedirectStandardError = true,
30+
RedirectStandardInput = false,
31+
},
32+
};
33+
34+
process.OutputDataReceived += (sender, args) =>
35+
{
36+
if (!string.IsNullOrEmpty(args.Data))
37+
{
38+
this.output.Add(args.Data);
39+
Console.WriteLine($"{command} {arguments} [OUT] {args.Data}");
40+
41+
onOutputReceived?.Invoke(args.Data);
42+
}
43+
};
44+
45+
process.ErrorDataReceived += (sender, args) =>
46+
{
47+
if (!string.IsNullOrEmpty(args.Data))
48+
{
49+
this.errors.Add(args.Data);
50+
Console.WriteLine($"[ERR] {args.Data}");
51+
52+
onErrorReceived?.Invoke(args.Data);
53+
}
54+
};
55+
56+
process.Start();
57+
58+
process.BeginOutputReadLine();
59+
process.BeginErrorReadLine();
60+
61+
this.process = process;
62+
}
63+
64+
public IEnumerable<string> Output => this.output;
65+
66+
public IEnumerable<string> Errors => this.errors;
67+
68+
public static IEnumerable<string> Run(
69+
string command,
70+
string arguments)
71+
{
72+
Run(command, arguments, out _, out var errors);
73+
74+
return errors;
75+
}
76+
77+
public static void Run(
78+
string command,
79+
string arguments,
80+
out IEnumerable<string> output,
81+
out IEnumerable<string> errors)
82+
{
83+
var consoleCommand = new ConsoleCommand(command, arguments, onOutputReceived: null, onErrorReceived: null);
84+
consoleCommand.Dispose();
85+
86+
output = consoleCommand.Output;
87+
errors = consoleCommand.Errors;
88+
}
89+
90+
public static ConsoleCommand Start(
91+
string command,
92+
string arguments,
93+
Action<string>? onOutputReceived = null,
94+
Action<string>? onErrorReceived = null)
95+
=> new(command, arguments, onOutputReceived, onErrorReceived);
96+
97+
public void Kill()
98+
{
99+
this.process.Kill();
100+
}
101+
102+
public void Dispose()
103+
{
104+
this.process.WaitForExit();
105+
106+
this.process.CancelOutputRead();
107+
this.process.CancelErrorRead();
108+
109+
this.process.Dispose();
110+
}
111+
}

0 commit comments

Comments
 (0)