Skip to content

Commit abc6ee9

Browse files
Merge pull request #6 from parentelement/next
Next to Main
2 parents 5420c29 + 88485ae commit abc6ee9

File tree

4 files changed

+199
-53
lines changed

4 files changed

+199
-53
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
namespace ParentElement.ReProcess.Tests
2+
{
3+
public class CommandTests
4+
{
5+
[Fact]
6+
public void Start_ShouldReturnTrueIfProcessWasStarted()
7+
{
8+
var cmd = CommandBuilder.Create("dotnet")
9+
.Build();
10+
11+
var result = cmd.Start(CancellationToken.None);
12+
13+
Assert.True(result);
14+
}
15+
16+
[Fact]
17+
public async Task StartAsync_ShouldReturnTrueIfProcessWasStarted()
18+
{
19+
var cmd = CommandBuilder.Create("dotnet")
20+
.Build();
21+
22+
var result = await cmd.StartAsync(CancellationToken.None);
23+
24+
Assert.True(result);
25+
}
26+
27+
[Fact]
28+
public async Task ReadOutputAsync_ShouldOutputCorrectResult()
29+
{
30+
var output = "Usage: dotnet [options]";
31+
32+
var cmd = CommandBuilder.Create("dotnet")
33+
.WithOutput()
34+
.Build();
35+
36+
await cmd.StartAsync(CancellationToken.None);
37+
38+
await foreach (var message in cmd.ReadOutputAsync(CancellationToken.None))
39+
{
40+
Assert.Equal(output, message.Data);
41+
break;
42+
}
43+
}
44+
45+
[Fact]
46+
public async Task ReadOutputAsync_ShouldNotErrorIfStartAsyncIsNotAwaited()
47+
{
48+
var cmd = CommandBuilder.Create("dotnet")
49+
.WithOutput()
50+
.Build();
51+
52+
cmd.StartAsync(CancellationToken.None);
53+
54+
await foreach (var message in cmd.ReadOutputAsync(CancellationToken.None))
55+
{
56+
break;
57+
}
58+
59+
Assert.True(true);
60+
}
61+
}
62+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="coverlet.collector" Version="6.0.0" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
15+
<PackageReference Include="xunit" Version="2.5.3" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\src\ParentElement.ReProcess.csproj" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<Using Include="Xunit" />
25+
</ItemGroup>
26+
27+
</Project>

src/Command.cs

Lines changed: 104 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@ public sealed class Command
1212
private Process _process;
1313
private bool _isRunning = false;
1414
private CommandDefinition _definition;
15+
private CancellationToken _token;
1516

1617
private Channel<ConsoleMessage>? _buffer;
1718

19+
/// <summary>
20+
/// Exit Code of the process. Will be <see langword="null"/> if the process is still running.
21+
/// </summary>
22+
public int? ExitCode => _isRunning ? null : _process.ExitCode;
23+
1824
internal Command(CommandDefinition definition)
1925
{
2026
_definition = definition;
@@ -24,76 +30,76 @@ internal Command(CommandDefinition definition)
2430
ConfigureProcess();
2531
}
2632

27-
private void ConfigureProcess()
33+
/// <summary>
34+
/// Attempts to run the configured process.
35+
/// </summary>
36+
/// <returns><see langword="true"/> if the process starts successfully. <see langword="false"/> if the process failed or was already running</returns>
37+
public bool Start(CancellationToken cancellationToken)
2838
{
29-
_process.EnableRaisingEvents = true;
39+
if (_isRunning) return false;
3040

31-
_process.Exited += (sender, args) =>
32-
{
33-
_isRunning = false;
41+
_isRunning = true;
3442

35-
if (_buffer == null)
36-
return;
43+
CreateOutputBuffer();
3744

38-
_buffer?.Writer.TryComplete();
39-
};
45+
_isRunning = _process.Start();
4046

41-
if (_definition.RelayOutput)
47+
if(_isRunning)
4248
{
43-
_process.OutputDataReceived += (sender, args) =>
44-
{
45-
if (!string.IsNullOrWhiteSpace(args.Data))
46-
_buffer!.Writer.TryWrite(new ConsoleMessage(this, args.Data, MessageType.Output));
47-
};
49+
var s = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
50+
_token = s.Token;
51+
_token.Register(Kill);
52+
}
4853

49-
_process.ErrorDataReceived += (sender, args) =>
50-
{
51-
if (!string.IsNullOrWhiteSpace(args.Data))
52-
_buffer!.Writer.TryWrite(new ConsoleMessage(this, args.Data, MessageType.Error));
53-
};
54+
if (_definition.RelayOutput)
55+
{
56+
_process.BeginOutputReadLine();
57+
_process.BeginErrorReadLine();
5458
}
55-
}
5659

57-
private void CreateOutputBuffer()
58-
{
59-
_buffer = _definition.MaxBufferSize.HasValue
60-
? Channel.CreateBounded<ConsoleMessage>(
61-
new BoundedChannelOptions(_definition.MaxBufferSize.Value)
62-
{
63-
SingleReader = true,
64-
FullMode = BoundedChannelFullMode.DropOldest
65-
})
66-
: Channel.CreateUnbounded<ConsoleMessage>(
67-
new UnboundedChannelOptions()
68-
{
69-
SingleReader = true,
70-
}
71-
);
60+
return _isRunning;
7261
}
7362

7463
/// <summary>
75-
/// Attempts to run the configured process.
64+
/// Attempts to run the configured process asynchronously.
7665
/// </summary>
77-
/// <returns><see langword="true"/> if the process starts successfully. <see langword="false"/> if the process failed or was already running</returns>
78-
public bool Start()
66+
/// <param name="cancellationToken">Token used to cancel the process</param>
67+
/// <returns></returns>
68+
public Task<bool> StartAsync(CancellationToken cancellationToken)
7969
{
80-
if (_isRunning) return false;
70+
if(_isRunning)
71+
return Task.FromResult(false);
8172

8273
_isRunning = true;
8374

8475
CreateOutputBuffer();
8576

86-
_isRunning = _process.Start();
87-
88-
if (_definition.RelayOutput)
77+
return Task.Run(() =>
8978
{
90-
_process.BeginOutputReadLine();
91-
_process.BeginErrorReadLine();
92-
}
79+
_isRunning = _process.Start();
9380

94-
return _isRunning;
81+
if (_isRunning)
82+
{
83+
var s = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
84+
_token = s.Token;
85+
_token.Register(Kill);
86+
}
87+
88+
if (_definition.RelayOutput)
89+
{
90+
_process.BeginOutputReadLine();
91+
_process.BeginErrorReadLine();
92+
}
93+
94+
return _isRunning;
95+
});
9596
}
9697

98+
/// <summary>
99+
/// Kills the process if it is running.
100+
/// </summary>
101+
public void Kill() => _process.Kill();
102+
97103
/// <summary>
98104
/// If "M:ReProcess.CommandBuilder.WithOutput" was called, this method will return the output of the process as it is written to StdOut and StdErr.
99105
/// </summary>
@@ -106,18 +112,16 @@ public async IAsyncEnumerable<ConsoleMessage> ReadOutputAsync([EnumeratorCancell
106112

107113
await foreach (var consoleMessage in _buffer.Reader.ReadAllAsync(cancellationToken))
108114
{
115+
if (_token.IsCancellationRequested)
116+
yield break; // Exit early if cancellation was requested from the start token
117+
109118
yield return consoleMessage;
110119

111-
if(!_definition.UseAggressiveOutputProcessing)
120+
if (!_definition.UseAggressiveOutputProcessing)
112121
await Task.Delay(10);
113122
}
114123
}
115124

116-
/// <summary>
117-
/// Kills the process if it is running.
118-
/// </summary>
119-
public void Kill() => _process.Kill();
120-
121125
/// <summary>
122126
/// Waits for the process to exit.
123127
/// </summary>
@@ -132,5 +136,52 @@ public async Task<int> WaitForExitAsync()
132136

133137
return _process.ExitCode;
134138
}
139+
140+
private void ConfigureProcess()
141+
{
142+
_process.EnableRaisingEvents = true;
143+
144+
_process.Exited += (sender, args) =>
145+
{
146+
_isRunning = false;
147+
148+
if (_buffer == null)
149+
return;
150+
151+
_buffer?.Writer.TryComplete();
152+
};
153+
154+
if (_definition.RelayOutput)
155+
{
156+
_process.OutputDataReceived += (sender, args) =>
157+
{
158+
if (!string.IsNullOrWhiteSpace(args.Data))
159+
_buffer!.Writer.TryWrite(new ConsoleMessage(this, args.Data, MessageType.Output));
160+
};
161+
162+
_process.ErrorDataReceived += (sender, args) =>
163+
{
164+
if (!string.IsNullOrWhiteSpace(args.Data))
165+
_buffer!.Writer.TryWrite(new ConsoleMessage(this, args.Data, MessageType.Error));
166+
};
167+
}
168+
}
169+
170+
private void CreateOutputBuffer()
171+
{
172+
_buffer = _definition.MaxBufferSize.HasValue
173+
? Channel.CreateBounded<ConsoleMessage>(
174+
new BoundedChannelOptions(_definition.MaxBufferSize.Value)
175+
{
176+
SingleReader = true,
177+
FullMode = BoundedChannelFullMode.DropOldest
178+
})
179+
: Channel.CreateUnbounded<ConsoleMessage>(
180+
new UnboundedChannelOptions()
181+
{
182+
SingleReader = true,
183+
}
184+
);
185+
}
135186
}
136187
}

src/ReProcess.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.9.34728.123
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParentElement.ReProcess", "ParentElement.ReProcess.csproj", "{0287F29A-426F-475F-9AAF-6F2A515CCB9D}"
77
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParentElement.ReProcess.Tests", "..\ParentElement.ReProcess.Tests\ParentElement.ReProcess.Tests.csproj", "{6CD45A2A-80FB-4B69-94D7-C3B488002895}"
9+
EndProject
810
Global
911
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1012
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
1517
{0287F29A-426F-475F-9AAF-6F2A515CCB9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
1618
{0287F29A-426F-475F-9AAF-6F2A515CCB9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
1719
{0287F29A-426F-475F-9AAF-6F2A515CCB9D}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{6CD45A2A-80FB-4B69-94D7-C3B488002895}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{6CD45A2A-80FB-4B69-94D7-C3B488002895}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{6CD45A2A-80FB-4B69-94D7-C3B488002895}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{6CD45A2A-80FB-4B69-94D7-C3B488002895}.Release|Any CPU.Build.0 = Release|Any CPU
1824
EndGlobalSection
1925
GlobalSection(SolutionProperties) = preSolution
2026
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)