Skip to content

Commit 1772a35

Browse files
committedSep 4, 2018
ExitCode can be obtained only by explicitly awaiting for it.
Eliminated the race condition when OnProcessExit and Dispose are running concurrently. Only these two methods are synced because other public methods of ProcessWrapper are not designed to be threadsafe - the same way as the Process class. Closes gitextensions#5362.
1 parent c61555f commit 1772a35

File tree

5 files changed

+47
-47
lines changed

5 files changed

+47
-47
lines changed
 

‎GitCommands/Git/Executable.cs

+24-25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99
using GitCommands.Logging;
10+
using GitUI;
1011
using GitUIPluginInterfaces;
1112
using JetBrains.Annotations;
1213

@@ -53,17 +54,15 @@ public IProcess Start(ArgumentString arguments = default, bool createWindow = fa
5354
private sealed class ProcessWrapper : IProcess
5455
{
5556
// TODO should this use TaskCreationOptions.RunContinuationsAsynchronously
56-
private readonly TaskCompletionSource<int?> _exitTaskCompletionSource = new TaskCompletionSource<int?>();
57+
private readonly TaskCompletionSource<int> _exitTaskCompletionSource = new TaskCompletionSource<int>();
5758

59+
private readonly object _syncRoot = new object();
5860
private readonly Process _process;
5961
private readonly ProcessOperation _logOperation;
6062
private readonly bool _redirectInput;
6163
private readonly bool _redirectOutput;
6264

63-
private int _disposed;
64-
65-
/// <inheritdoc />
66-
public int? ExitCode { get; private set; }
65+
private bool _disposed;
6766

6867
[SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")]
6968
public ProcessWrapper(string fileName, string arguments, string workDir, bool createWindow, bool redirectInput, bool redirectOutput, [CanBeNull] Encoding outputEncoding)
@@ -102,15 +101,17 @@ public ProcessWrapper(string fileName, string arguments, string workDir, bool cr
102101

103102
private void OnProcessExit(object sender, EventArgs eventArgs)
104103
{
105-
// The Exited event can be raised after the process is disposed, however
106-
// if the Process is disposed then reading ExitCode will throw.
107-
if (_disposed == 0)
104+
lock (_syncRoot)
108105
{
109-
ExitCode = _process.ExitCode;
106+
// The Exited event can be raised after the process is disposed, however
107+
// if the Process is disposed then reading ExitCode will throw.
108+
if (!_disposed)
109+
{
110+
var exitCode = _process.ExitCode;
111+
_logOperation.LogProcessEnd(exitCode);
112+
_exitTaskCompletionSource.TrySetResult(exitCode);
113+
}
110114
}
111-
112-
_logOperation.LogProcessEnd(ExitCode);
113-
_exitTaskCompletionSource.TrySetResult(ExitCode);
114115
}
115116

116117
/// <inheritdoc />
@@ -159,32 +160,30 @@ public StreamReader StandardError
159160
public void WaitForInputIdle() => _process.WaitForInputIdle();
160161

161162
/// <inheritdoc />
162-
public Task<int?> WaitForExitAsync() => _exitTaskCompletionSource.Task;
163+
public Task<int> WaitForExitAsync() => _exitTaskCompletionSource.Task;
163164

164165
/// <inheritdoc />
165-
public int? WaitForExit()
166+
public int WaitForExit()
166167
{
167-
if (_disposed != 0)
168-
{
169-
return ExitCode;
170-
}
171-
172-
_process.WaitForExit();
173-
174-
return GitUI.ThreadHelper.JoinableTaskFactory.Run(() => _exitTaskCompletionSource.Task);
168+
return ThreadHelper.JoinableTaskFactory.Run(() => WaitForExitAsync());
175169
}
176170

177171
/// <inheritdoc />
178172
public void Dispose()
179173
{
180-
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
174+
lock (_syncRoot)
181175
{
182-
return;
176+
if (_disposed)
177+
{
178+
return;
179+
}
180+
181+
_disposed = true;
183182
}
184183

185184
_process.Exited -= OnProcessExit;
186185

187-
_exitTaskCompletionSource.TrySetResult(null);
186+
_exitTaskCompletionSource.TrySetCanceled();
188187

189188
_process.Dispose();
190189

‎GitCommands/Git/ExecutableExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public static async Task<string> GetOutputAsync(
100100
output = outputBuffer.ToArray();
101101
error = errorBuffer.ToArray();
102102

103-
if (cache != null && process.ExitCode == 0)
103+
if (cache != null && await exitTask == 0)
104104
{
105105
cache.Add(arguments, output, error);
106106
}

‎GitCommands/Git/IProcess.cs

+2-11
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,17 @@ public interface IProcess : IDisposable
4343
/// when calling <see cref="IExecutable.Start"/>.</exception>
4444
StreamReader StandardError { get; }
4545

46-
/// <summary>
47-
/// Gets any exit code returned by this process.
48-
/// </summary>
49-
/// <remarks>
50-
/// This value will be <c>null</c> if the process is still running, or if this object was disposed
51-
/// before the process exited.
52-
/// </remarks>
53-
int? ExitCode { get; }
54-
5546
/// <summary>
5647
/// Blocks the calling thread until the process exits, or when this object is disposed.
5748
/// </summary>
5849
/// <returns>The process's exit code, or <c>null</c> if this object was disposed before the process exited.</returns>
59-
int? WaitForExit();
50+
int WaitForExit();
6051

6152
/// <summary>
6253
/// Returns a task that completes when the process exits, or when this object is disposed.
6354
/// </summary>
6455
/// <returns>A task that yields the process's exit code, or <c>null</c> if this object was disposed before the process exited.</returns>
65-
Task<int?> WaitForExitAsync();
56+
Task<int> WaitForExitAsync();
6657

6758
/// <summary>
6859
/// Waits for the process to reach an idle state.

‎GitCommands/Logging/CommandLog.cs

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public void NotifyDisposed()
4444
if (_entry.Duration == null)
4545
{
4646
_entry.Duration = _stopwatch.Elapsed;
47+
_raiseCommandsChanged();
4748
}
4849
}
4950
}

‎UnitTests/GitCommandsTests/MockExecutable.cs

+19-10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Threading;
77
using System.Threading.Tasks;
88
using GitCommands;
9+
using GitUI;
910
using GitUIPluginInterfaces;
1011
using JetBrains.Annotations;
1112
using NUnit.Framework;
@@ -94,37 +95,45 @@ public IProcess Start(ArgumentString arguments, bool createWindow, bool redirect
9495

9596
private sealed class MockProcess : IProcess
9697
{
97-
public MockProcess([CanBeNull] string output, int? exitCode)
98+
public MockProcess([CanBeNull] string output, int? exitCode = 0)
9899
{
99100
StandardOutput = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(output ?? "")));
100101
StandardError = new StreamReader(new MemoryStream());
101102
StandardInput = new StreamWriter(new MemoryStream());
102-
ExitCode = exitCode;
103+
_exitCode = exitCode;
103104
}
104105

105106
public MockProcess()
106107
{
107108
StandardOutput = new StreamReader(new MemoryStream());
108109
StandardError = new StreamReader(new MemoryStream());
109110
StandardInput = new StreamWriter(new MemoryStream());
111+
_exitCode = 0;
110112
}
111113

114+
private int? _exitCode;
112115
public StreamWriter StandardInput { get; }
113116
public StreamReader StandardOutput { get; }
114117
public StreamReader StandardError { get; }
115118

116-
public int? ExitCode { get; }
117-
118-
public int? WaitForExit()
119+
public int WaitForExit()
119120
{
120-
// TODO implement if needed
121-
return 0;
121+
return ThreadHelper.JoinableTaskFactory.Run(() => WaitForExitAsync());
122122
}
123123

124-
public Task<int?> WaitForExitAsync()
124+
public Task<int> WaitForExitAsync()
125125
{
126-
// TODO implement if needed
127-
return Task.FromResult((int?)0);
126+
if (_exitCode.HasValue)
127+
{
128+
return Task.FromResult(_exitCode.Value);
129+
}
130+
else
131+
{
132+
var cts = new CancellationTokenSource();
133+
var ct = cts.Token;
134+
cts.Cancel();
135+
return Task.FromCanceled<int>(ct);
136+
}
128137
}
129138

130139
public void WaitForInputIdle()

0 commit comments

Comments
 (0)
Please sign in to comment.