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