Skip to content

Commit dc47a89

Browse files
Fix ProcessRunner issues: (Azure#16499)
- Handle buffer overload on large output - Handle process failed to start
1 parent 1dae7fe commit dc47a89

File tree

5 files changed

+145
-45
lines changed

5 files changed

+145
-45
lines changed

sdk/identity/Azure.Identity/src/IProcess.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,22 @@
33

44
using System;
55
using System.Diagnostics;
6-
using System.IO;
76

87
namespace Azure.Identity
98
{
109
internal interface IProcess : IDisposable
1110
{
1211
bool HasExited { get; }
1312
int ExitCode { get; }
14-
StreamReader StandardOutput { get; }
15-
StreamReader StandardError { get; }
1613
ProcessStartInfo StartInfo { get; set; }
1714

1815
event EventHandler Exited;
16+
event DataReceivedEventHandler OutputDataReceived;
17+
event DataReceivedEventHandler ErrorDataReceived;
1918

20-
void Start();
19+
bool Start();
2120
void Kill();
21+
void BeginOutputReadLine();
22+
void BeginErrorReadLine();
2223
}
2324
}

sdk/identity/Azure.Identity/src/ProcessRunner.cs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
57
using System.Threading;
68
using System.Threading.Tasks;
79

@@ -14,6 +16,11 @@ internal sealed class ProcessRunner
1416
private readonly IProcess _process;
1517
private readonly TimeSpan _timeout;
1618
private readonly TaskCompletionSource<string> _tcs;
19+
private readonly TaskCompletionSource<ICollection<string>> _outputTcs;
20+
private readonly TaskCompletionSource<ICollection<string>> _errorTcs;
21+
private readonly ICollection<string> _outputData;
22+
private readonly ICollection<string> _errorData;
23+
1724
private readonly CancellationToken _cancellationToken;
1825
private readonly CancellationTokenSource _timeoutCts;
1926
private CancellationTokenRegistration _ctRegistration;
@@ -22,6 +29,11 @@ public ProcessRunner(IProcess process, TimeSpan timeout, CancellationToken cance
2229
{
2330
_process = process;
2431
_timeout = timeout;
32+
33+
_outputData = new List<string>();
34+
_errorData = new List<string>();
35+
_outputTcs = new TaskCompletionSource<ICollection<string>>(TaskCreationOptions.RunContinuationsAsynchronously);
36+
_errorTcs = new TaskCompletionSource<ICollection<string>>(TaskCreationOptions.RunContinuationsAsynchronously);
2537
_tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
2638

2739
if (timeout.TotalMilliseconds >= 0)
@@ -56,27 +68,37 @@ private void StartProcess()
5668
return;
5769
}
5870

59-
_process.Exited += (o, e) => HandleExit();
60-
6171
_process.StartInfo.UseShellExecute = false;
6272
_process.StartInfo.RedirectStandardOutput = true;
6373
_process.StartInfo.RedirectStandardError = true;
6474

75+
_process.OutputDataReceived += (sender, args) => OnDataReceived(args, _outputData, _outputTcs);
76+
_process.ErrorDataReceived += (sender, args) => OnDataReceived(args, _errorData, _errorTcs);
77+
_process.Exited += (o, e) => _ = HandleExitAsync();
78+
6579
_timeoutCts?.CancelAfter(_timeout);
6680

67-
_process.Start();
81+
if (!_process.Start())
82+
{
83+
TrySetException(new InvalidOperationException($"Failed to start process '{_process.StartInfo.FileName}'"));
84+
}
85+
86+
_process.BeginOutputReadLine();
87+
_process.BeginErrorReadLine();
6888
_ctRegistration = _cancellationToken.Register(HandleCancel, false);
6989
}
7090

71-
private void HandleExit()
91+
private async ValueTask HandleExitAsync()
7292
{
7393
if (_process.ExitCode == 0)
7494
{
75-
TrySetResult(_process.StandardOutput.ReadToEnd());
95+
ICollection<string> output = await _outputTcs.Task.ConfigureAwait(false);
96+
TrySetResult(string.Join(Environment.NewLine, output));
7697
}
7798
else
7899
{
79-
TrySetException(new InvalidOperationException(_process.StandardError.ReadToEnd()));
100+
ICollection<string> error = await _errorTcs.Task.ConfigureAwait(false);
101+
TrySetException(new InvalidOperationException(string.Join(Environment.NewLine, error)));
80102
}
81103
}
82104

@@ -103,6 +125,18 @@ private void HandleCancel()
103125
TrySetCanceled();
104126
}
105127

128+
private static void OnDataReceived(DataReceivedEventArgs args, ICollection<string> data, TaskCompletionSource<ICollection<string>> tcs)
129+
{
130+
if (args.Data != null)
131+
{
132+
data.Add(args.Data);
133+
}
134+
else
135+
{
136+
tcs.SetResult(data);
137+
}
138+
}
139+
106140
private void TrySetResult(string result)
107141
{
108142
DisposeProcess();

sdk/identity/Azure.Identity/src/ProcessService.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ public ProcessWrapper(ProcessStartInfo processStartInfo)
3030

3131
public bool HasExited => _process.HasExited;
3232
public int ExitCode => _process.ExitCode;
33-
public StreamReader StandardOutput => _process.StandardOutput;
34-
public StreamReader StandardError => _process.StandardError;
3533

3634
public ProcessStartInfo StartInfo
3735
{
@@ -45,8 +43,22 @@ public event EventHandler Exited
4543
remove => _process.Exited -= value;
4644
}
4745

48-
public void Start() => _process.Start();
46+
public event DataReceivedEventHandler OutputDataReceived
47+
{
48+
add => _process.OutputDataReceived += value;
49+
remove => _process.OutputDataReceived -= value;
50+
}
51+
52+
public event DataReceivedEventHandler ErrorDataReceived
53+
{
54+
add => _process.ErrorDataReceived += value;
55+
remove => _process.ErrorDataReceived -= value;
56+
}
57+
58+
public bool Start() => _process.Start();
4959
public void Kill() => _process.Kill();
60+
public void BeginOutputReadLine() => _process.BeginOutputReadLine();
61+
public void BeginErrorReadLine() => _process.BeginErrorReadLine();
5062
public void Dispose() => _process.Dispose();
5163
}
5264
}

sdk/identity/Azure.Identity/tests/ProcessRunnerTests.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.ComponentModel;
66
using System.Diagnostics;
7+
using System.Linq;
78
using System.Runtime.InteropServices;
89
using System.Text;
910
using System.Threading;
@@ -42,7 +43,40 @@ public async Task ProcessRunnerRealProcessSucceeded()
4243
var runner = new ProcessRunner(process, TimeSpan.FromSeconds(30), default);
4344
var result = await Run(runner);
4445

45-
Assert.AreEqual(output, result.TrimEnd());
46+
Assert.AreEqual(output, result);
47+
}
48+
49+
[Test]
50+
public async Task ProcessRunnerRealProcessLargeOutputSucceeded()
51+
{
52+
var output = string.Concat(Enumerable.Repeat("Test output", 500));
53+
var command = $"echo {output}";
54+
var process = CreateRealProcess(command, command);
55+
var runner = new ProcessRunner(process, TimeSpan.FromSeconds(30), default);
56+
var result = await Run(runner);
57+
58+
Assert.AreEqual(output, result);
59+
}
60+
61+
[Test]
62+
public async Task ProcessRunnerRealProcessMultipleLinesSucceeded()
63+
{
64+
var output = "Test output";
65+
var command = string.Join("&&", Enumerable.Repeat($"echo {output}", 100));
66+
var process = CreateRealProcess(command, command);
67+
var runner = new ProcessRunner(process, TimeSpan.FromSeconds(30), default);
68+
var result = await Run(runner);
69+
70+
Assert.AreEqual(string.Join(Environment.NewLine, Enumerable.Repeat(output, 100)), result);
71+
}
72+
73+
[Test]
74+
public void ProcessRunnerProcessFailsToStart()
75+
{
76+
var process = new TestProcess { FailedToStart = true };
77+
var runner = new ProcessRunner(process, TimeSpan.FromSeconds(30), default);
78+
79+
Assert.CatchAsync<InvalidOperationException>(async () => await Run(runner));
4680
}
4781

4882
[Test]
@@ -120,6 +154,17 @@ public void ProcessRunnerRealProcessFailedWithErrorMessage()
120154
Assert.AreEqual(error, exception.Message.Trim());
121155
}
122156

157+
[Test]
158+
public void ProcessRunnerRealProcessFailedWithLargeErrorMessage()
159+
{
160+
var error = string.Concat(Enumerable.Repeat("Test error", 500));;
161+
var process = CreateRealProcess($"echo {error} 1>&2 & exit 1", $">&2 echo {error} & exit 1");
162+
var runner = new ProcessRunner(process, TimeSpan.FromSeconds(30), default);
163+
164+
var exception = Assert.CatchAsync<InvalidOperationException>(async () => await Run(runner));
165+
Assert.AreEqual(error, exception.Message.Trim());
166+
}
167+
123168
[Test]
124169
public void ProcessRunnerFailedOnKillProcess()
125170
{

sdk/identity/Azure.Identity/tests/TestProcess.cs

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@
33

44
using System;
55
using System.Diagnostics;
6-
using System.IO;
6+
using System.Linq.Expressions;
7+
using System.Reflection;
78
using System.Threading;
89
using System.Threading.Tasks;
910

1011
namespace Azure.Identity.Tests
1112
{
1213
internal sealed class TestProcess : IProcess
1314
{
15+
private static readonly Lazy<Func<string, DataReceivedEventArgs>> s_createDataReceivedEventArgs = new Lazy<Func<string, DataReceivedEventArgs>>(() =>
16+
{
17+
ConstructorInfo constructor = typeof(DataReceivedEventArgs).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
18+
ParameterExpression dataParameter = Expression.Parameter(typeof(string), "data");
19+
NewExpression callConstructor = Expression.New(constructor, dataParameter);
20+
return Expression.Lambda<Func<string, DataReceivedEventArgs>>(callConstructor, dataParameter).Compile();
21+
});
22+
1423
private bool _hasStarted;
1524
private bool _hasExited;
1625
private int _exitCode;
1726
private CancellationTokenSource _cts;
18-
private StreamReader _outputStreamReader;
19-
private StreamReader _errorStreamReader;
20-
private StreamWriter _outputStreamWriter;
21-
private StreamWriter _errorStreamWriter;
2227
private ProcessStartInfo _startInfo;
2328

2429
public ProcessStartInfo StartInfo
@@ -27,17 +32,14 @@ public ProcessStartInfo StartInfo
2732
set => _startInfo = value;
2833
}
2934

35+
public bool FailedToStart { get; set; }
3036
public string Output { get; set; }
3137
public string Error { get; set; }
3238
public int? CodeOnExit { get; set; }
3339
public int Timeout { get; set; }
3440
public Exception ExceptionOnProcessKill { get; set; }
3541

36-
public void Dispose()
37-
{
38-
_outputStreamReader?.Dispose();
39-
_errorStreamReader?.Dispose();
40-
}
42+
public void Dispose() { }
4143

4244
public bool HasExited
4345
{
@@ -65,20 +67,21 @@ public int ExitCode
6567
}
6668
}
6769

68-
public StreamReader StandardOutput => _outputStreamReader;
69-
public StreamReader StandardError => _errorStreamReader;
70-
7170
public event EventHandler Exited;
7271
public event EventHandler Started;
72+
public event DataReceivedEventHandler OutputDataReceived;
73+
public event DataReceivedEventHandler ErrorDataReceived;
7374

74-
public void Start()
75+
public bool Start()
7576
{
77+
if (FailedToStart)
78+
{
79+
return false;
80+
}
81+
7682
_hasStarted = true;
7783
Started?.Invoke(this, EventArgs.Empty);
7884

79-
Create(out _outputStreamReader, out _outputStreamWriter);
80-
Create(out _errorStreamReader, out _errorStreamWriter);
81-
8285
if (Timeout > 0)
8386
{
8487
_cts = new CancellationTokenSource();
@@ -88,6 +91,8 @@ public void Start()
8891
{
8992
Task.Run(FinishProcessRun);
9093
}
94+
95+
return true;
9196
}
9297

9398
private void FinishProcessRun(Task delayTask)
@@ -100,31 +105,29 @@ private void FinishProcessRun(Task delayTask)
100105

101106
private void FinishProcessRun()
102107
{
103-
WriteToStream(Output, _outputStreamWriter);
104-
WriteToStream(Error, _errorStreamWriter);
108+
Notify(Output, OutputDataReceived);
109+
Notify(Error, ErrorDataReceived);
105110

106111
_hasExited = true;
107112
_exitCode = CodeOnExit ?? (Error != default ? 1 : 0);
108113
Exited?.Invoke(this, EventArgs.Empty);
109114
}
110115

111-
private static void Create(out StreamReader reader, out StreamWriter writer)
116+
private void Notify(string data, DataReceivedEventHandler handler)
112117
{
113-
var stream = new MemoryStream();
114-
reader = new StreamReader(stream);
115-
writer = new StreamWriter(stream);
116-
}
117-
118-
private static void WriteToStream(string str, StreamWriter writer)
119-
{
120-
if (str == default)
118+
if (handler == default)
121119
{
122120
return;
123121
}
124122

125-
writer.Write(str);
126-
writer.Flush();
127-
writer.BaseStream.Seek(0, SeekOrigin.Begin);
123+
Task.Run(() =>
124+
{
125+
if (data != default)
126+
{
127+
handler(this, CreateDataReceivedEventArgs(data));
128+
}
129+
handler(this, CreateDataReceivedEventArgs(null));
130+
});
128131
}
129132

130133
public void Kill()
@@ -137,5 +140,10 @@ public void Kill()
137140
throw ExceptionOnProcessKill;
138141
}
139142
}
143+
144+
public void BeginOutputReadLine() {}
145+
public void BeginErrorReadLine() {}
146+
147+
private static DataReceivedEventArgs CreateDataReceivedEventArgs(string data) => s_createDataReceivedEventArgs.Value(data);
140148
}
141149
}

0 commit comments

Comments
 (0)