diff --git a/src/ChorusHub/Advertiser.cs b/src/ChorusHub/Advertiser.cs index a835364a5..9e76fc78a 100644 --- a/src/ChorusHub/Advertiser.cs +++ b/src/ChorusHub/Advertiser.cs @@ -5,6 +5,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; +using System.Threading.Tasks; using Chorus.ChorusHub; namespace ChorusHub @@ -12,6 +13,7 @@ namespace ChorusHub public class Advertiser : IDisposable { private Thread _thread; + private CancellationTokenSource _cancellationTokenSource; private UdpClient _client; private IPEndPoint _endPoint; private byte[] _sendBytes; @@ -33,6 +35,7 @@ public void Start() EnableBroadcast = true }; _endPoint = new IPEndPoint(IPAddress.Parse("255.255.255.255"), Port); + _cancellationTokenSource = new CancellationTokenSource(); _thread = new Thread(Work); _thread.Start(); } @@ -41,18 +44,17 @@ private void Work() { try { - while (true) + while (!_cancellationTokenSource.Token.IsCancellationRequested) { UpdateAdvertisementBasedOnCurrentIpAddress(); - _client.BeginSend(_sendBytes, _sendBytes.Length, _endPoint, SendCallback, _client); - Thread.Sleep(1000); + _client.BeginSend(_sendBytes, + _sendBytes.Length, + _endPoint, + SendCallback, + _client); + Task.Delay(1000).Wait(_cancellationTokenSource.Token); } } - catch(ThreadAbortException) - { - //Progress.WriteVerbose("Advertiser Thread Aborting (that's normal)"); - _client.Close(); - } catch(Exception) { //EventLog.WriteEntry("Application", string.Format("Error in Advertiser: {0}", error.Message), EventLogEntryType.Error); @@ -104,9 +106,12 @@ public void Stop() return; //EventLog.WriteEntry("Application", "Advertiser Stopping...", EventLogEntryType.Information); - _thread.Abort(); + _cancellationTokenSource.Cancel(); _thread.Join(2 * 1000); _thread = null; + _client.Close(); + _client.Dispose(); + _client = null; } public void Dispose() diff --git a/src/ChorusHub/HgServeRunner.cs b/src/ChorusHub/HgServeRunner.cs index 15757a942..4a590aa2c 100644 --- a/src/ChorusHub/HgServeRunner.cs +++ b/src/ChorusHub/HgServeRunner.cs @@ -15,6 +15,7 @@ public class HgServeRunner : IDisposable public readonly int Port; private readonly string _rootFolder; private Thread _hgServeThread; + private CancellationTokenSource _tokenSource; public HgServeRunner(string rootFolder, int port) { @@ -76,32 +77,25 @@ public bool Start() _hgServeProcess.StartInfo.RedirectStandardOutput = true; _hgServeProcess.Start(); #else + _tokenSource = new CancellationTokenSource(); _hgServeThread = new Thread(() => - { - var commandLineRunner = new CommandLineRunner(); - try - { - var progress = new ConsoleProgress(); - commandLineRunner.Start( - Chorus.MercurialLocation.PathToHgExecutable, - arguments, - Encoding.UTF8, _rootFolder, -1, - progress, s => progress.WriteMessage(s)); - } - catch (ThreadAbortException) - { - //Progress.WriteVerbose("Hg Serve command Thread Aborting (that's normal when stopping)"); - try - { - if (!commandLineRunner.Abort(1)) - { - //EventLog.WriteEntry("Application", "Hg Serve might not have closed down.", EventLogEntryType.Information); - } - } - catch { } - - } - }); + { + var commandLineRunner = new CommandLineRunner(); + var progress = new ConsoleProgress(); + _tokenSource.Token.Register(() => + { + progress.CancelRequested = true; + commandLineRunner.Abort(1); + }); + commandLineRunner.Start( + Chorus.MercurialLocation.PathToHgExecutable, + arguments, + Encoding.UTF8, + _rootFolder, + -1, + progress, + s => progress.WriteMessage(s)); + }); _hgServeThread.Start(); #endif @@ -190,16 +184,8 @@ public void Stop() if(_hgServeThread !=null && _hgServeThread.IsAlive) { //Progress.WriteMessage("Hg Server Stopping..."); - _hgServeThread.Abort(); - - if(_hgServeThread.Join(2 * 1000)) - { - //Progress.WriteMessage("Hg Server Stopped"); - } - else - { - //Progress.WriteError("***Gave up on hg server stopping"); - } + _tokenSource.Cancel(false); + _hgServeThread.Join(2 * 1000); _hgServeThread = null; } } diff --git a/src/LibChorus/ProcessStream.cs b/src/LibChorus/ProcessStream.cs deleted file mode 100644 index 888d401f9..000000000 --- a/src/LibChorus/ProcessStream.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Chorus.VCS -{ - using System; - using System.Diagnostics; - using System.Threading; - - // from SeemabK in a comment here: http://www.hanselman.com/blog/CommentView.aspx?guid=362 - public class ProcessStream - { - /* - * Class to get process stdout/stderr streams - * Author: SeemabK (seemabk@yahoo.com) - * Usage: - //create ProcessStream - ProcessStream myProcessStream = new ProcessStream(); - //create and populate Process as needed - Process myProcess = new Process(); - myProcess.StartInfo.FileName = "myexec.exe"; - myProcess.StartInfo.Arguments = "-myargs"; - - //redirect stdout and/or stderr - myProcess.StartInfo.UseShellExecute = false; - myProcess.StartInfo.RedirectStandardOutput = true; - myProcess.StartInfo.RedirectStandardError = true; - - //start Process - myProcess.Start(); - //connect to ProcessStream - myProcessStream.Read(ref myProcess); - //wait for Process to end - myProcess.WaitForExit(); - - //get the captured output :) - string output = myProcessStream.StandardOutput; - string error = myProcessStream.StandardError; - */ - - private Thread StandardOutputReader; - private Thread StandardErrorReader; - private static Process RunProcess; - - private string _StandardOutput = ""; - public string StandardOutput - { - get { return _StandardOutput; } - } - private string _StandardError = ""; - public string StandardError - { - get { return _StandardError; } - } - - public ProcessStream() - { - Init(); - } - - public int Read(ref Process process) - { - try - { - Init(); - RunProcess = process; - - if (RunProcess.StartInfo.RedirectStandardOutput) - { - StandardOutputReader = new Thread(new ThreadStart(ReadStandardOutput)); - StandardOutputReader.Start(); - } - if (RunProcess.StartInfo.RedirectStandardError) - { - StandardErrorReader = new Thread(new ThreadStart(ReadStandardError)); - StandardErrorReader.Start(); - } - - //RunProcess.WaitForExit(); - if (StandardOutputReader != null) - StandardOutputReader.Join(); - if (StandardErrorReader != null) - StandardErrorReader.Join(); - } - catch - { } - - return 1; - } - - private void ReadStandardOutput() - { - if (RunProcess != null) - _StandardOutput = RunProcess.StandardOutput.ReadToEnd(); - } - - private void ReadStandardError() - { - if (RunProcess != null) - _StandardError = RunProcess.StandardError.ReadToEnd(); - } - - private int Init() - { - _StandardError = ""; - _StandardOutput = ""; - RunProcess = null; - Stop(); - return 1; - } - - public int Stop() - { - try { StandardOutputReader.Abort(); } - catch { } - try { StandardErrorReader.Abort(); } - catch { } - StandardOutputReader = null; - StandardErrorReader = null; - return 1; - } - } - -} diff --git a/src/LibChorus/Utilities/HgProcessOutputReader.cs b/src/LibChorus/Utilities/HgProcessOutputReader.cs index 2944f925f..ebc3fca8c 100644 --- a/src/LibChorus/Utilities/HgProcessOutputReader.cs +++ b/src/LibChorus/Utilities/HgProcessOutputReader.cs @@ -47,23 +47,21 @@ public string StandardError } /// - /// Safely read the streams of the process + /// Safely read the streams of the process, must redirect stdErr, out and in before calling /// /// true if the process completed before the timeout or cancellation public bool Read(ref Process process, int secondsBeforeTimeOut, IProgress progress) { - var outputReaderArgs = new ReaderArgs() {Proc = process, Reader = process.StandardOutput}; - if (process.StartInfo.RedirectStandardOutput) - { - _outputReader = new Thread(new ParameterizedThreadStart(ReadStream)); - _outputReader.Start(outputReaderArgs); - } - var errorReaderArgs = new ReaderArgs() { Proc = process, Reader = process.StandardError }; - if (process.StartInfo.RedirectStandardError) - { - _errorReader = new Thread(new ParameterizedThreadStart(ReadStream)); - _errorReader.Start(errorReaderArgs); - } + var cts = new CancellationTokenSource(); + var outputReaderArgs = new ReaderArgs(process, process.StandardOutput, cts.Token); + _outputReader = new Thread(ReadStream); + _outputReader.Start(outputReaderArgs); + + + var errorReaderArgs = new ReaderArgs(process, process.StandardError, cts.Token); + _errorReader = new Thread(ReadStream); + _errorReader.Start(errorReaderArgs); + lock(this) { @@ -71,23 +69,24 @@ public bool Read(ref Process process, int secondsBeforeTimeOut, IProgress progre } //nb: at one point I (jh) tried adding !process.HasExited, but that made things less stable. - while (/*!process.HasExited &&*/ (_outputReader.ThreadState == ThreadState.Running || (_errorReader != null && _errorReader.ThreadState == ThreadState.Running))) + while ( _outputReader.ThreadState != ThreadState.Stopped && _errorReader.ThreadState != ThreadState.Stopped) { DateTime end; lock (this) { end = _heartbeat.AddSeconds(secondsBeforeTimeOut); } - if(progress.CancelRequested) + + if (progress.CancelRequested) + { + cts.Cancel(); return false; + } Thread.Sleep(100); if (DateTime.Now > end) { - if (_outputReader != null) - _outputReader.Abort(); - if (_errorReader != null) - _errorReader.Abort(); + cts.Cancel(); return false; } } @@ -155,34 +154,46 @@ private bool HandleChangedVsDeletedFiles(string line, StreamWriter standardInput private void ReadStream(object args) { var result = new StringBuilder(); - var readerArgs = args as ReaderArgs; - - var reader = readerArgs.Reader; - do + var readerArgs = (ReaderArgs)args; + try { - var s = reader.ReadLine(); - if (s != null) - { - // Eat up any heartbeat lines from the stream, also remove warnings about dotencode - if (s != Properties.Resources.MergeHeartbeat && s != DotEncodeWarning + var reader = readerArgs.Reader; + do + { + var s = reader.ReadLineAsync(readerArgs.Token).Result; + if (s == null) return; //cancelled + // Eat up any heartbeat lines from the stream, also remove warnings about dotencode + if (s != Properties.Resources.MergeHeartbeat + && s != DotEncodeWarning && !HandleChangedVsDeletedFiles(s, readerArgs.Proc.StandardInput)) - { - result.AppendLine(s.Trim()); - } - lock (this) - { - // set the last heartbeat if data was read from the stream - _heartbeat = DateTime.Now; - } - } - } while (!reader.EndOfStream);// && !readerArgs.Proc.HasExited); - - readerArgs.Results = result.ToString().Replace("\r\n", "\n"); + { + result.AppendLine(s.Trim()); + } + + lock (this) + { + // set the last heartbeat if data was read from the stream + _heartbeat = DateTime.Now; + } + } while (!reader.EndOfStream && !readerArgs.Token.IsCancellationRequested); // && !readerArgs.Proc.HasExited); + } + finally + { + readerArgs.Results = result.ToString().Replace("\r\n", "\n"); + } } } - class ReaderArgs + internal class ReaderArgs { + public ReaderArgs(Process proc, StreamReader reader, CancellationToken token) + { + Token = token; + Reader = reader; + Proc = proc; + } + + public CancellationToken Token; public StreamReader Reader; public Process Proc; public string Results; diff --git a/src/LibChorus/Utilities/ProcessStream.cs b/src/LibChorus/Utilities/ProcessStream.cs index 66773b2e3..d400431dc 100644 --- a/src/LibChorus/Utilities/ProcessStream.cs +++ b/src/LibChorus/Utilities/ProcessStream.cs @@ -1,6 +1,8 @@ using System; using System.Diagnostics; +using System.IO; using System.Threading; +using System.Threading.Tasks; namespace Chorus.Utilities { @@ -37,10 +39,6 @@ public class ProcessStream string error = myProcessStream.StandardError; */ - private Thread StandardOutputReader; - private Thread StandardErrorReader; - private static Process _srunningProcess; - private string _standardOutput = ""; public string StandardOutput { @@ -55,81 +53,25 @@ public string StandardError get { return _standardError; } } - public ProcessStream() - { - Init(); - } - public int Read(ref Process process, int secondsBeforeTimeOut) { -// try -// { - Init(); - _srunningProcess = process; - - if (_srunningProcess.StartInfo.RedirectStandardOutput) - { - StandardOutputReader = new Thread(new ThreadStart(ReadStandardOutput)); - StandardOutputReader.Start(); - } - if (_srunningProcess.StartInfo.RedirectStandardError) - { - StandardErrorReader = new Thread(new ThreadStart(ReadStandardError)); - StandardErrorReader.Start(); - } - - //_srunningProcess.WaitForExit(); - if (StandardOutputReader != null) - { - if (!StandardOutputReader.Join(new TimeSpan(0, 0, 0, secondsBeforeTimeOut))) - { - return kTimedOut; - } - } - if (StandardErrorReader != null) - { - if (!StandardErrorReader.Join(new TimeSpan(0, 0, 0, secondsBeforeTimeOut))) - { - return kTimedOut; - } - } -// } -// catch -// { } - - return 1; - } - - private void ReadStandardOutput() - { - if (_srunningProcess != null) - _standardOutput = _srunningProcess.StandardOutput.ReadToEnd(); - } - - private void ReadStandardError() - { - if (_srunningProcess != null) - _standardError = _srunningProcess.StandardError.ReadToEnd(); - } - - private int Init() - { - _standardError = ""; - _standardOutput = ""; - _srunningProcess = null; - Stop(); - return 1; - } + Task stdOutTask, stdErrTask; + stdOutTask = stdErrTask = Task.FromResult(string.Empty); + + if (process.StartInfo.RedirectStandardOutput) + stdOutTask = process.StandardOutput.ReadToEndAsync(secondsBeforeTimeOut); + if (process.StartInfo.RedirectStandardError) + stdErrTask = process.StandardError.ReadToEndAsync(secondsBeforeTimeOut); + var stdOut = stdOutTask.Result; + var stdErr = stdErrTask.Result; + _standardOutput = stdOut ?? string.Empty; + _standardError = stdErr ?? string.Empty; + //null indicates the read timed out + if (stdOut == null || stdErr == null) + { + return kTimedOut; + } - [System.Diagnostics.DebuggerStepThrough] - public int Stop() - { - try { StandardOutputReader.Abort(); } - catch { } - try { StandardErrorReader.Abort(); } - catch { } - StandardOutputReader = null; - StandardErrorReader = null; return 1; } } diff --git a/src/LibChorus/Utilities/StreamReaderExtensions.cs b/src/LibChorus/Utilities/StreamReaderExtensions.cs new file mode 100644 index 000000000..f484693dc --- /dev/null +++ b/src/LibChorus/Utilities/StreamReaderExtensions.cs @@ -0,0 +1,53 @@ +// // Copyright (c) 2024-2024 SIL International +// // This software is licensed under the MIT License (http://opensource.org/licenses/MIT) + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Chorus.Utilities +{ + public static class StreamReaderExtensions + { + /// + /// read the stream to the end, but return null if it takes too long + /// if the read from the stream were to return null then an empty string is returned + /// + public static async Task ReadToEndAsync(this StreamReader reader, int secondsBeforeTimeOut) + { + var readTask = reader.ReadToEndAsync(); + var timeoutTask = Task.Delay(TimeSpan.FromSeconds(secondsBeforeTimeOut)); + var result = await Task.WhenAny(readTask, timeoutTask); + if (result == timeoutTask) + { + return null; + } + + return await readTask ?? string.Empty; + } + /// + /// read a line of text, but return null if the cancellation token is cancelled + /// if the read from the stream were to return null then an empty string is returned + /// + public static async Task ReadLineAsync(this StreamReader reader, CancellationToken cancellationToken) + { + try + { + var readTask = reader.ReadLineAsync(); + var timeoutTask = Task.Delay(-1, cancellationToken); + var result = await Task.WhenAny(readTask, timeoutTask); + if (result == timeoutTask) // should never happen since the delay is infinite + { + return null; + } + + return await readTask ?? string.Empty; + } + catch (TaskCanceledException) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/src/LibChorusTests/utilities/HgProcessReaderTests.cs b/src/LibChorusTests/utilities/HgProcessReaderTests.cs new file mode 100644 index 000000000..558ce1fdb --- /dev/null +++ b/src/LibChorusTests/utilities/HgProcessReaderTests.cs @@ -0,0 +1,144 @@ +// // Copyright (c) 2024-2024 SIL International +// // This software is licensed under the MIT License (http://opensource.org/licenses/MIT) + +using System.Diagnostics; +using Chorus.Utilities; +using NUnit.Framework; +using SIL.PlatformUtilities; +using SIL.Progress; + +namespace LibChorus.Tests.utilities +{ + [TestFixture] + public class HgProcessReaderTests + { + private static readonly IProgress _progress = new NullProgress(); + + private Process Process() + { + return new Process() + { + StartInfo = + { + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardError = true, + RedirectStandardOutput = true + } + }; + } + + [Test] + public void OutputsStdOut() + { + var expectedOutput = "Hello, World!"; + var ps = new HgProcessOutputReader("/"); + var process = Process(); + if (Platform.IsUnix) { + process.StartInfo.FileName = "sh"; + process.StartInfo.Arguments = $"-c \"echo '{expectedOutput}'\""; + } else { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = "/c echo " + expectedOutput; + } + process.Start(); + var finished = ps.Read(ref process, 10, _progress); + Assert.True(finished); + Assert.AreEqual(expectedOutput, ps.StandardOutput.Trim()); + process.WaitForExit(); + } + + [Test] + public void OutputsStdErr() + { + var expectedOutput = "Hello, World!"; + var ps = new HgProcessOutputReader("/"); + var process = Process(); + if (Platform.IsUnix) { + process.StartInfo.FileName = "sh"; + process.StartInfo.Arguments = $"-c \"echo '{expectedOutput}' 1>&2\""; + } else { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = "/c echo " + expectedOutput + " 1>&2"; + } + process.Start(); + var finished = ps.Read(ref process, 10, _progress); + Assert.True(finished); + Assert.AreEqual(expectedOutput, ps.StandardError.Trim()); + process.WaitForExit(); + } + + [Test] + public void TimesOut() + { + var ps = new HgProcessOutputReader("/"); + var process = Process(); + if (Platform.IsUnix) { + process.StartInfo.FileName = "sh"; + process.StartInfo.Arguments = "-c \"sleep 10s && echo -n test\""; + } else { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = "/c waitfor /T 10 pause3 & echo test"; + } + process.Start(); + var finished = ps.Read(ref process, 1, _progress); + Assert.AreEqual(string.Empty, ps.StandardOutput); + Assert.AreEqual(string.Empty, ps.StandardError); + Assert.False(finished); + process.Kill(); + } + + [Test] + public void DoesNotTimeoutIfFinishedInTime() + { + var ps = new HgProcessOutputReader("/"); + var process = Process(); + if (Platform.IsUnix) { + process.StartInfo.FileName = "sh"; + process.StartInfo.Arguments = "-c \"sleep 1s && echo -n test\""; + } else { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = "/c waitfor /T 1 pause4 & echo test"; + } + process.Start(); + var finished = ps.Read(ref process, 10, _progress); + Assert.True(finished); + if (Platform.IsUnix) { + Assert.That(ps.StandardError?.TrimEnd(), Is.Null.Or.Empty); //should be empty because sleep does not give an error + } else { + Assert.That(ps.StandardError, Is.Not.Null.Or.Empty); //should not be empty because waitfor gives an error + } + Assert.That(ps.StandardOutput, Is.Not.Null.And.Contains("test")); + process.WaitForExit(); + } + + [Test] + public void DoesNotTimeoutWithIntermediateOutput() + { + int segmentTime = 2; + int totalSeconds = 2 * 3; + + var ps = new HgProcessOutputReader("/"); + var process = Process(); + if (Platform.IsUnix) { + process.StartInfo.FileName = "sh"; + process.StartInfo.Arguments = $"-c \"sleep {segmentTime}s && echo test && sleep {segmentTime}s && echo test2 && sleep {segmentTime} && echo test3\""; + } else { + process.StartInfo.FileName = "cmd.exe"; + //wait then output repeat, since there's output the whole time our read should not timeout + process.StartInfo.Arguments = string.Format("/c waitfor /T {0} pause5 & echo test & waitfor /T {0} pause6 & echo test2 & waitfor /T {0} pause7 & echo test3", segmentTime); + } + process.Start(); + //even though it's waiting for less than the full time there's still progress made so it should finish + var finished = ps.Read(ref process, totalSeconds - segmentTime, _progress); + Assert.True(finished); + if (Platform.IsUnix) { + Assert.That(ps.StandardError?.TrimEnd(), Is.Null.Or.Empty); //should be empty because sleep does not give an error + } else { + Assert.That(ps.StandardError, Is.Not.Null.Or.Empty); //should not be empty because waitfor gives an error + } + Assert.That(ps.StandardOutput, Is.Not.Null.And.Contains("test").And.Contains("test2").And.Contains("test3")); + process.WaitForExit(); + } + } +} \ No newline at end of file diff --git a/src/LibChorusTests/utilities/ProcessStreamTests.cs b/src/LibChorusTests/utilities/ProcessStreamTests.cs new file mode 100644 index 000000000..1e54f0dcf --- /dev/null +++ b/src/LibChorusTests/utilities/ProcessStreamTests.cs @@ -0,0 +1,108 @@ +// // Copyright (c) 2024-2024 SIL International +// // This software is licensed under the MIT License (http://opensource.org/licenses/MIT) + +using System.Diagnostics; +using Chorus.Utilities; +using NUnit.Framework; +using SIL.PlatformUtilities; + +namespace LibChorus.Tests.utilities +{ + [TestFixture] + public class ProcessStreamTests + { + [Test] + public void OutputsStdOut() + { + var expectedOutput = "Hello, World!"; + var ps = new ProcessStream(); + var process = new Process(); + if (Platform.IsUnix) { + process.StartInfo.FileName = "sh"; + process.StartInfo.Arguments = $"-c \"echo '{expectedOutput}'\""; + } else { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = "/c echo " + expectedOutput; + } + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + var readReturnCode = ps.Read(ref process, 10); + Assert.AreEqual(1, readReturnCode); + Assert.AreEqual(expectedOutput, ps.StandardOutput.Trim()); + process.WaitForExit(); + } + + [Test] + public void OutputsStdErr() + { + var expectedOutput = "Hello, World!"; + var ps = new ProcessStream(); + var process = new Process(); + if (Platform.IsUnix) { + process.StartInfo.FileName = "sh"; + process.StartInfo.Arguments = $"-c \"echo '{expectedOutput}' 1>&2\""; + } else { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = "/c echo " + expectedOutput + " 1>&2"; + } + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardError = true; + process.Start(); + var readReturnCode = ps.Read(ref process, 10); + Assert.AreEqual(1, readReturnCode); + Assert.AreEqual(expectedOutput, ps.StandardError.Trim()); + process.WaitForExit(); + } + + [Test] + public void TimesOut() + { + var ps = new ProcessStream(); + var process = new Process(); + if (Platform.IsUnix) { + process.StartInfo.FileName = "sh"; + process.StartInfo.Arguments = "-c \"sleep 10s && echo -n test\""; + } else { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = "/c waitfor /T 10 pause1 & echo test"; + } + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + var readReturnCode = ps.Read(ref process, 1); + Assert.AreEqual(string.Empty, ps.StandardOutput); + Assert.AreEqual(string.Empty, ps.StandardError); + Assert.AreEqual(ProcessStream.kTimedOut, readReturnCode); + process.Kill(); + } + + [Test] + public void DoesNotTimeoutIfFinishedInTime() + { + var ps = new ProcessStream(); + var process = new Process(); + if (Platform.IsUnix) { + process.StartInfo.FileName = "sh"; + process.StartInfo.Arguments = "-c \"sleep 1s && echo -n test\""; + } else { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = "/c waitfor /T 1 pause2 & echo test"; + } + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + var readReturnCode = ps.Read(ref process, 10); + Assert.That(ps.StandardOutput, Does.Contain("test")); + if (Platform.IsUnix) { + Assert.That(ps.StandardError?.TrimEnd(), Is.Null.Or.Empty); //should be empty because sleep does not give an error + } else { + Assert.That(ps.StandardError, Is.Not.Null.Or.Empty); //should not be empty because waitfor gives an error + } + Assert.AreEqual(1, readReturnCode); + process.WaitForExit(); + } + } +} \ No newline at end of file