-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathObserve.cs
More file actions
202 lines (174 loc) · 5.96 KB
/
Copy pathObserve.cs
File metadata and controls
202 lines (174 loc) · 5.96 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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#:sdk Microsoft.NET.Sdk
#:property TargetFramework=net10.0
#:property Nullable=enable
#:property ImplicitUsings=enable
#:property AllowUnsafeBlocks=true
#:property PublishAot=true
#:project ../src/MiniPty/MiniPty.csproj
#:project ../src/MiniPty.Capture/MiniPty.Capture.csproj
using System.Runtime.InteropServices;
using System.Text;
using MiniPty;
using MiniPty.Capture;
if (!Pty.IsSupported)
{
Console.Error.WriteLine("PTY is not supported on this operating system.");
return 1;
}
try
{
await ObserveStaggeredOutputAsync();
await ObserveStdinPipelineAsync();
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"{ex.GetType().Name}: {ex.Message}");
return 1;
}
static async Task ObserveStaggeredOutputAsync()
{
Console.WriteLine("=== Observe output over time ===");
var result = await PtyCapture.RunAsync(
CreateStaggeredStartInfo(),
new PtyCaptureOptions
{
Completion = new PtyCompleteOptions
{
ExitTimeout = TimeSpan.FromSeconds(30),
},
});
var text = result.GetText();
Console.WriteLine($"exit={result.ExitCode} chunks={result.Chunks.Count} bytes={result.Output.Length} chars={text.Length}");
Console.WriteLine("timeline (elapsed since session start):");
var textChunks = result.GetTextChunks();
for (var i = 0; i < textChunks.Count; i++)
{
var chunk = textChunks[i];
var preview = EscapeForDisplay(chunk.Text.Span);
Console.WriteLine($" +{chunk.Time.TotalSeconds,7:F3}s {chunk.Text.Length,4} chars {preview}");
}
if (result.Chunks.Count > 0)
{
var last = result.Chunks[^1];
Console.WriteLine($"session span: {last.Time.TotalSeconds:F3}s");
}
var mergedOffset = 0;
foreach (var chunk in result.Chunks)
{
var slice = result.Output.Span.Slice(mergedOffset, chunk.Data.Length);
if (!chunk.Data.Span.SequenceEqual(slice))
throw new InvalidOperationException("merged byte chunks do not match result.Output");
mergedOffset += chunk.Data.Length;
}
if (mergedOffset != result.Output.Length)
throw new InvalidOperationException("merged byte chunks do not cover result.Output");
if (result.ExitCode != 0)
throw new InvalidOperationException($"child exited with {result.ExitCode}");
if (result.Chunks.Count < 2)
throw new InvalidOperationException("expected multiple chunks from staggered output");
foreach (var label in new[] { "alpha", "beta", "gamma" })
{
if (!result.ContainsUtf8(label))
throw new InvalidOperationException($"expected label '{label}' missing from output");
}
}
static async Task ObserveStdinPipelineAsync()
{
Console.WriteLine();
Console.WriteLine("=== Observe stdin + PTY pipeline ===");
var result = await PtyCapture.RunAsync(
CreateStdinPipelineStartInfo(),
new PtyCaptureOptions
{
Completion = new PtyCompleteOptions
{
Input = CreateStdinPipelineInput(),
ExitTimeout = TimeSpan.FromSeconds(15),
},
});
Console.WriteLine($"exit={result.ExitCode}");
Console.WriteLine("merged output:");
Console.WriteLine(result.GetTextString().TrimEnd());
Console.WriteLine("chunk boundaries (useful when rebuilding a consumer timeline):");
var cursor = TimeSpan.Zero;
foreach (var chunk in result.Chunks)
{
Console.WriteLine($" [{cursor.TotalSeconds:F3}s -> {chunk.Time.TotalSeconds:F3}s) {chunk.Data.Length} bytes");
cursor = chunk.Time;
}
ValidateStdinPipelineOutput(result.GetTextString(), result.ExitCode);
}
static void ValidateStdinPipelineOutput(string output, int exitCode)
{
if (exitCode != 0)
throw new InvalidOperationException($"stdin pipeline exited with {exitCode}");
if (!output.Contains("minipty-stdin-pipeline", StringComparison.Ordinal))
throw new InvalidOperationException("expected pipeline marker missing from output");
}
static string EscapeForDisplay(ReadOnlySpan<char> text)
{
var builder = new StringBuilder(text.Length);
foreach (var ch in text)
{
builder.Append(ch switch
{
'\r' => "\\r",
'\n' => "\\n\n ",
'\t' => "\\t",
< ' ' or > '~' => $"\\u{(int)ch:X4}",
_ => ch.ToString(),
});
}
return builder.ToString().TrimEnd();
}
static PtyStartInfo CreateStaggeredStartInfo()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var cmd = Environment.GetEnvironmentVariable("ComSpec") ?? @"C:\Windows\System32\cmd.exe";
return new PtyStartInfo
{
FileName = cmd,
Arguments =
[
"/c",
"echo alpha& timeout /t 1 /nobreak >nul & echo beta& timeout /t 1 /nobreak >nul & echo gamma",
],
Size = new PtySize(80, 24),
};
}
return new PtyStartInfo
{
FileName = "/bin/sh",
Arguments =
[
"-c",
"printf 'alpha\\n'; sleep 0.15; printf 'beta\\n'; sleep 0.15; printf 'gamma\\n'",
],
Size = new PtySize(80, 24),
};
}
static PtyStartInfo CreateStdinPipelineStartInfo()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var cmd = Environment.GetEnvironmentVariable("ComSpec") ?? @"C:\Windows\System32\cmd.exe";
return new PtyStartInfo
{
FileName = cmd,
Arguments = ["/c", "find /v \"\" >nul & echo minipty-stdin-pipeline"],
Size = new PtySize(80, 24),
};
}
return new PtyStartInfo
{
FileName = "/bin/sh",
Arguments = ["-c", "cat >/dev/null; printf 'minipty-stdin-pipeline\\n'"],
Size = new PtySize(80, 24),
};
}
static string CreateStdinPipelineInput() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "line 3\r\nline 1\r\nline 2\r\n"
: "line 3\nline 1\nline 2\n";