#region Disclaimer / License
// Copyright (C) 2011, Kenneth Skovhede
// http://www.hexad.dk, opensource@hexad.dk
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion
using System;
using System.Collections.Generic;
using System.Text;
namespace Duplicati.Library.SharpExpect
{
///
/// This class is a primitive version of the Expect program for Linux.
/// It eases the manipulation of a remote process by waiting for certain
/// patterns in the program output.
/// It does not yet support raw reading and writing of the tty/pty devices in Linux.
/// The timimings are not entirely excact, and does not account for time spent in code, but only the waited time.
///
public class SharpExpectProcess : IDisposable
{
public enum OutputSource
{
StdOut = 1,
StedErr = 2,
Both = 3
}
private System.Diagnostics.Process m_process;
private object m_lock = new object();
private List m_stdOut = new List();
private List m_stdErr = new List();
private System.Threading.AutoResetEvent m_event = new System.Threading.AutoResetEvent(false);
private int m_defaultTimeout = 30 * 1000;
private int m_logLimit = 100;
private volatile bool m_stderr_done = true;
private volatile bool m_stdout_done = true;
private List m_log = new List();
///
/// A value indicating if the log mechanism is enabled
///
public bool LogEnabled
{
get { return m_log != null; }
set
{
if (value != this.LogEnabled)
{
if (value)
m_log = new List();
else
m_log = null;
}
}
}
///
/// The number of messages to store in the process log
///
public int LogLimit
{
get { return m_logLimit; }
set { m_logLimit = Math.Max(0, value); }
}
///
/// The timeout used if no timeout is specified in the function call
///
public int DefaultTimeout
{
get { return m_defaultTimeout; }
set { m_defaultTimeout = value; }
}
///
/// Records a message in the log
///
/// The prefix to store with the message
/// The message to record
/// The recorded message (without the prefix)
private string RecordInLog(string prefix, string message)
{
if (m_log == null || message == null)
return message;
m_log.Add("*" + prefix + "*: " + message);
while (m_log.Count > m_logLimit)
m_log.RemoveAt(0);
return message;
}
///
/// Constructs a new process helper around a running process
///
/// The proccess to wrap
public SharpExpectProcess(System.Diagnostics.Process process)
{
m_process = process;
if (m_process.StartInfo.RedirectStandardError)
{
m_stderr_done = false;
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(StreamReader), m_process.StandardError);
}
if (m_process.StartInfo.RedirectStandardOutput)
{
m_stdout_done = false;
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(StreamReader), m_process.StandardOutput);
}
}
///
/// Returns the next output line (from stdout or stderr) but does not remove it
///
/// The maximum time to wait for a new line
/// The next output line (from stdout or stderr) but does not remove it
public string PeekNextOutputLine(int maxWaitTime)
{
return PeekNextOutputLine(OutputSource.Both, maxWaitTime);
}
///
/// Returns the next output line from the selected source but does not remove it
///
/// A value indicating what streams to examine for data
/// The maximum number of milliseconds to wait for a line to be received
/// The next output line from the selected source but does not remove it, returns null if no data is available
public string PeekNextOutputLine(OutputSource source, int maxWaitTime)
{
//Is there buffered output?
lock (m_lock)
{
if ((source == OutputSource.Both || source == OutputSource.StdOut) && m_stdOut.Count > 0)
return FindNextLine(m_stdOut, maxWaitTime);
if ((source == OutputSource.Both || source == OutputSource.StedErr) && m_stdErr.Count > 0)
return FindNextLine(m_stdErr, maxWaitTime);
}
return null;
}
///
/// Returns the next output line from either stdout or stderr, returns null if the operation times out.
///
/// The next output line from either stdout or stderr, or null if the operation times out.
public string GetNextOutputLine()
{
return GetNextOutputLine(m_defaultTimeout);
}
///
/// Returns the next output line from the selected sources, returns null if the operation times out.
///
/// A value indicating what streams to examine for data
/// The next output line from the selected sources, or null if the operation times out.
public string GetNextOutputLine(OutputSource source)
{
return GetNextOutputLine(source, m_defaultTimeout);
}
///
/// Returns the next output line from either stdout or stderr, returns null if the operation times out.
///
/// The number of milliseconds to wait for the line to become ready
/// The next output line from either stdout or stderr, or null if the operation times out.
public string GetNextOutputLine(int millisecondsTimeout)
{
return GetNextOutputLine(OutputSource.Both, millisecondsTimeout);
}
///
/// Returns the next output line from the selected sources, returns null if the operation times out.
///
/// A value indicating what streams to examine for data
/// The number of milliseconds to wait for the line to become ready
/// The next output line from the selected sources, or null if the operation times out.
public string GetNextOutputLine(OutputSource source, int millisecondsTimeout)
{
DateTime begin = DateTime.Now;
//Is there buffered output?
string line = GetBufferedLine(source, millisecondsTimeout);
if (line != null)
return line;
//Adjust the remaining time
if (millisecondsTimeout != System.Threading.Timeout.Infinite)
millisecondsTimeout = Math.Max(0, millisecondsTimeout - (int)((DateTime.Now - begin).TotalMilliseconds));
//If we are disposed, or done, there is no point in waiting
if (m_process == null || m_process.HasExited)
{
//Make sure the readers are done
while (m_process != null && !m_stderr_done)
System.Threading.Thread.Sleep(100);
while (m_process != null && !m_stdout_done)
System.Threading.Thread.Sleep(100);
//Make sure queue is empty
return GetBufferedLine(source, System.Threading.Timeout.Infinite);
}
begin = DateTime.Now;
//If we can wait, do so, and try again, otherwise return null
if (millisecondsTimeout == System.Threading.Timeout.Infinite || millisecondsTimeout > 0)
m_event.WaitOne(millisecondsTimeout, false);
//Adjust the remaining time
if (millisecondsTimeout != System.Threading.Timeout.Infinite)
millisecondsTimeout = Math.Max(0, millisecondsTimeout - (int)((DateTime.Now - begin).TotalMilliseconds));
return GetBufferedLine(source, millisecondsTimeout);
}
///
/// Helper method to extract a buffered line
///
/// A value indicating what streams to examine
/// The time to wait for a new line
/// The next buffered line or null
private string GetBufferedLine(OutputSource source, int maxWaitTime)
{
bool returnStdOut;
bool returnStdErr;
lock (m_lock)
{
//Check stdout
returnStdOut = ((source == OutputSource.Both || source == OutputSource.StdOut) && m_stdOut.Count > 0);
//Check stderr
returnStdErr = ((source == OutputSource.Both || source == OutputSource.StedErr) && m_stdErr.Count > 0);
//No data, so make sure the event is not set
if (!returnStdOut && !returnStdErr)
{
m_event.Reset();
return null;
}
}
if (returnStdOut)
return RecordInLog("O", ExtractNextLine(m_stdOut, maxWaitTime));
if (returnStdErr)
return RecordInLog("E", ExtractNextLine(m_stdErr, maxWaitTime));
//Notify that there was no waiting data, we should not get here
return null;
}
///
/// Internal helper to remove the next line from the queue
///
/// The queue to extract from
/// The maximum time to wait for a new line
/// The extracted line
private string ExtractNextLine(List queue, int maxWaitTime)
{
string line = FindNextLine(queue, maxWaitTime);
if (line == null)
return null;
lock (m_lock)
if (queue[0] == line)
queue.RemoveAt(0);
else
queue[0] = queue[0].Substring(line.Length);
return line;
}
///
/// This function attempts to reconstruct output into lines.
/// The reader threads put raw input into their respective queues,
/// but timing issues may mean that a line may be split over multiple
/// queue entries, and a single entry may contain more than one line.
///
/// The queue to extract from
/// The time to wait for a new line
/// The next line, or null if there is no data
private string FindNextLine(List queue, int maxWaitTime)
{
return FindNextLine(queue, maxWaitTime, true);
}
///
/// This function attempts to reconstruct output into lines.
/// The reader threads put raw input into their respective queues,
/// but timing issues may mean that a line may be split over multiple
/// queue entries, and a single entry may contain more than one line.
///
/// The queue to extract from
/// A value indicating if a wait should be performed if the extracted line does not end with a linefeed
/// The time to wait for a new line
/// The extracted line, or null if there is no data
private string FindNextLine(List queue, int maxWaitTime, bool allowWait)
{
char leadChar = '\r';
char trailChar = '\n';
string tmp;
//Make sure no-one else touches the queue
lock (m_lock)
{
if (queue.Count == 0)
return null;
//If the line is split over multiple entries, try to combine it back into a single line
while (queue[0].LastIndexOfAny(new char[] { leadChar, trailChar }) < 0 && queue.Count > 1)
{
queue[0] = queue[0] + queue[1];
queue.RemoveAt(1);
}
tmp = queue[0];
}
//If the line ends with return, then wait for a linefeed.
//The \r\n is windows style, and the remote output may be from a windows machine,
//regardless of the local OS.
if (tmp.EndsWith(leadChar.ToString()))
{
if(queue.Count == 1)
System.Threading.Thread.Sleep(500);
lock(m_lock)
if (queue.Count > 1 && queue[1].Length > 0 && queue[1][0] == trailChar)
{
tmp += trailChar;
if (queue[1].Length > 1)
queue[1] = queue[1].Substring(1);
else
queue.RemoveAt(1);
}
}
//Make sure no-one else touches the queue
lock (m_lock)
{
//If there is more than a single newline in the first entry, split it
//Find out if there are newlines in the entry
int lineBreakPos = queue[0].IndexOf(leadChar);
if (lineBreakPos >= 0)
{
lineBreakPos++;
if (queue[0].Length > lineBreakPos && queue[0][lineBreakPos] == trailChar)
lineBreakPos++;
}
else
{
lineBreakPos = queue[0].IndexOf(trailChar);
if (lineBreakPos >= 0)
lineBreakPos++;
}
//If yes, extract the first line, and insert the rest of the string in the next slot
if (lineBreakPos >= 0 && lineBreakPos != queue[0].Length)
{
tmp = queue[0].Substring(0, lineBreakPos);
queue[0] = queue[0].Substring(tmp.Length);
queue.Insert(0, tmp); //This entry may also have newlines
}
}
//If there is no more data, and the extracted line does not end in a linefeed, wait a little
if (allowWait && maxWaitTime != 0 && tmp.IndexOfAny(new char[] { leadChar, trailChar }) < 0)
{
DateTime begin = DateTime.Now;
m_event.WaitOne(maxWaitTime, false);
if (maxWaitTime != System.Threading.Timeout.Infinite)
{
maxWaitTime -= (int)((DateTime.Now - begin).TotalMilliseconds);
maxWaitTime = Math.Max(0, maxWaitTime);
}
lock (m_lock)
if (queue.Count == 1) //No new data
return tmp;
return FindNextLine(queue, maxWaitTime, maxWaitTime != 0); //Extract again, but don't wait
}
else
return tmp; //The line is good, just return
}
///
/// Waits until the output matches any of the patterns given
///
/// Any number of patterns to look for, the strings must be regular expressions
/// The index of the matched pattern, or -1 if none was matched
public int Expect(params string[] possibilities)
{
return Expect(m_defaultTimeout, possibilities);
}
///
/// Waits until the output matches any of the patterns given
///
/// The number of milliseconds to wait for the pattern to be matched
/// Any number of patterns to look for, the strings must be regular expressions
/// The index of the matched pattern, or -1 if none was matched
public int Expect(int millisecondsTimeout, params string[] possibilities)
{
List> lst = new List>();
for (int i = 0; i < possibilities.Length; i++)
if (!string.IsNullOrEmpty(possibilities[i]))
lst.Add(new KeyValuePair(new System.Text.RegularExpressions.Regex(possibilities[i]), i));
KeyValuePair match = Expect(millisecondsTimeout, lst);
if (match.Value == null)
return -1;
else
return match.Key;
}
///
/// Waits until the output matches any of the patterns given, and returns the string as well as the value for the pattern.
///
/// The number of milliseconds to wait for the pattern to be matched
/// Any number of patterns to look for, the strings must be regular expressions
/// The type of the value object to return
/// A pair with the value given, and the string matched
public KeyValuePair Expect(int millisecondsTimeout, List> possibilities)
{
List> lst = new List>();
for (int i = 0; i < possibilities.Count; i++)
if (!string.IsNullOrEmpty(possibilities[i].Key))
lst.Add(new KeyValuePair(new System.Text.RegularExpressions.Regex(possibilities[i].Key), possibilities[i].Value));
return Expect(millisecondsTimeout, possibilities);
}
///
/// Waits until the output matches any of the patterns given, and returns the string as well as the value for the pattern.
///
/// The number of milliseconds to wait for the pattern to be matched
/// Any number of patterns to look for
/// The type of the value object to return
/// A pair with the value given, and the string matched
public KeyValuePair Expect(int millisecondsTimeout, List> possibilities)
{
if (possibilities == null || possibilities.Count == 0)
return new KeyValuePair(default(T), null);
DateTime expiration = millisecondsTimeout < 0 ? DateTime.Now.AddYears(5) : DateTime.Now.AddMilliseconds(millisecondsTimeout);
string combinedLines = "";
//string line = null;
while (DateTime.Now <= expiration) //|| line != null)
{
string line = GetNextOutputLine(1000);
if (line == null && DateTime.Now > expiration)
return new KeyValuePair(default(T), null);
if (line != null)
{
combinedLines += line;
foreach (KeyValuePair expr in possibilities)
if (expr.Key.Match(combinedLines).Success)
return new KeyValuePair(expr.Value, combinedLines);
}
}
return new KeyValuePair(default(T), null);
}
///
/// Sends a line to the process
///
/// The line to send
public void Sendline(string line)
{
RecordInLog("I", line);
if (m_process.HasExited)
throw new Exception(string.Format(Backend.Strings.SharpExpectProcess.WriteAfterExitError, LogKillAndDispose()));
m_process.StandardInput.WriteLine(line);
}
///
/// Send a password to the remote process.
/// This is the same as the Sendline call, but does not log the value being sent.
///
/// The password to send
public void Sendpassword(string password)
{
RecordInLog("I", Backend.Strings.SharpExpectProcess.PasswordMarker);
if (m_process.HasExited)
throw new Exception(string.Format(Backend.Strings.SharpExpectProcess.WriteAfterExitError, LogKillAndDispose()));
m_process.StandardInput.WriteLine(password);
}
///
/// The thread method which reads data from the given StreamReader
///
/// A StreamReader object
private void StreamReader(object input)
{
//Setup the threads local data and buffer
System.IO.StreamReader sr = null;
try
{
sr = (System.IO.StreamReader)input;
char[] buf = new char[1024];
bool lastRun = false;
List queue = sr == m_process.StandardError ? m_stdErr : m_stdOut;
//Keep reading until the process exits
while (true)
{
int r = sr.Read(buf, 0, buf.Length);
if (r > 0)
{
lock (m_lock)
{
//Notify about the waiting data
queue.Add(new string(buf, 0, r));
m_event.Set();
}
}
if (m_process.HasExited)
{
//After the process exists, try one last time
//This protects against races with the reader and the process
if (!lastRun)
{
System.Threading.Thread.Sleep(100);
lastRun = true;
}
else
break; //Exit while loop
}
}
}
catch
{
}
finally
{
try
{
//Flag completion
if (sr == m_process.StandardError)
m_stderr_done = true;
else
m_stdout_done = true;
}
catch
{
//If this instance is disposed, m_process is null
}
}
}
///
/// Returns the wrapped process, do not use this to read or write data
///
public System.Diagnostics.Process Process { get { return m_process; } }
///
/// Creates a process helper object from the given command
///
/// The file to run
/// Any arguments to pass to the command
/// A process helper
public static SharpExpectProcess Spawn(string FileName, string Arguments)
{
System.Diagnostics.ProcessStartInfo pi = new System.Diagnostics.ProcessStartInfo(FileName, Arguments);
pi.RedirectStandardError = true;
pi.RedirectStandardOutput = true;
pi.RedirectStandardInput = true;
return Spawn(pi);
}
///
/// Creates a process helper object from the given command
///
/// A startinfo object describing the process to start
/// A process helper
public static SharpExpectProcess Spawn(System.Diagnostics.ProcessStartInfo startInfo)
{
return new SharpExpectProcess(System.Diagnostics.Process.Start(startInfo));
}
///
/// Destroys the helper object and returns logged input and output, usefull for returning a debug message
///
/// A string with logmessages
public string LogKillAndDispose()
{
if (this.LogEnabled)
{
this.Dispose();
StringBuilder sb = new StringBuilder();
foreach (string s in m_log)
sb.AppendLine(s);
while (PeekNextOutputLine(1000) != null)
sb.AppendLine("*U*: " + GetNextOutputLine(0));
return sb.ToString();
}
else
return Backend.Strings.SharpExpectProcess.LogDisabled;
}
#region IDisposable Members
///
/// Kills the remote process
///
public void Dispose()
{
if (m_process != null)
{
try { if (!m_process.HasExited) m_process.Kill(); }
catch { }
try { m_process.Dispose(); }
catch { }
m_process = null;
}
}
#endregion
}
}