VirtualTerminal, Expect and timing

+1 vote
asked Mar 26, 2010 by Stephan (190 points)
edited Jul 25, 2011

Hi

I seem to have run into a timing snag. When I start the session, the server sends me back 4 lines, the last of which being "login:". Then I need to enter the login with a newline, then wait for "Password", send the password, then wait for the prompt (identified by a ">" at the end). Here's my code

Telnet client = new Telnet("myserver");
VirtualTerminal terminal = new VirtualTerminal(120, 50);
terminal.Bind(client);
if (terminal.Expect("login:", 1000))
{
   terminal.SendToServer("mylogin\r");
   if (terminal.Expect("Password:", 1000))
   {
      terminal.SendToServer("mypass\r");
      if (terminal.Expect(">:", 1000))
        Console.WriteLine("logged in");
   }
}

I never get into the first if, but if I dump the contents of the virtual terminal after the first if

(string[] content = terminal.Screen.GetRegionText(0, 0, terminal.Screen.Columns, terminal.Screen.Rows);)

then I see the "login:" I'm looking for. When I immediately send the login, then wait for "Password" - same story. So it seems that terminal.Expect only works from the point in time where it has been called... and if the server is fast enough and has already sent the expected string, Expect will fail.

As I will be running commands that take longer (and interactive operations where responses don't always come below my input but may update the existing lines - so using the Shell object is out of the question), I need to find a way to accommodate both. Is there a way to make Expect also consider what has already been received so I won't have to resort to the clumsy way of dumping the contents of the virtual terminal each time expect returns false?

Regards Stephan

3 Answers

+1 vote
answered Mar 26, 2010 by Lukas Matyska (59,530 points)
edited Mar 26, 2010

Hello Stephan,

VirtualTerminal runs in selfprocessing mode. It means that until you call the Process method or the Expect method no data are received. Therefore wrong timing hypothesis should not be the cause.

The Expect method only checks the end of the response (not the whole response content). Please see the method documentation:

Processes any available incoming data until a response is received that ends with the specified string (and no more data is available to be read), or until the specified maximum wait time period elapses.

So I have a suspicion that the server sends "login: " (please note the last space) while you are expecting "login:" (without the last space). From the definition of the Expect method it should return false in this case.

Please update your code to include the last space:

Telnet client = new Telnet("myserver");
VirtualTerminal terminal = new VirtualTerminal(120, 50);
terminal.Bind(client);
if (terminal.Expect("login: ", 1000))
{
    terminal.SendToServer("mylogin\r");
    if (terminal.Expect("Password: ", 1000))
    {
        terminal.SendToServer("mypass\r");
        if (terminal.Expect(">: ", 1000)) 
            Console.WriteLine("logged in");
    }
}

If this is not the cause, please let us know here.

+1 vote
answered Mar 26, 2010 by Stephan (190 points)
edited Mar 26, 2010

In the meantime, I realized that I misunderstood the meaning of Expect. It only works if the END! response precisely matches the string I'm looking for. What I need is a bit more generic - I don't always know the precise end of the response (it can be ambiguous), so I wrote the following wrapper method which mimics the behavior I have in other libs that I've used previously:

public bool WaitForString(string response, int maximumWaitingTime)
    {
        if (string.IsNullOrEmpty(response))
            return false;
        if (maximumWaitingTime == 0)
            return false;
        TerminalState state;
        int waitingTime = maximumWaitingTime;
        DateTime startTime = DateTime.Now;
        DateTime endTime = DateTime.Now.AddMilliseconds(maximumWaitingTime);
        do
        {
            state = Term.Process(waitingTime);
            if (state == TerminalState.DataReceived)
            {
                if (findOnScreen(response, this.Term.Screen.GetRegionText(0, 0, Term.Screen.Columns, Term.Screen.Rows)))
                    return true;
            }
            if (maximumWaitingTime > DateTime.Now.Subtract(startTime).Milliseconds )
                waitingTime = maximumWaitingTime - DateTime.Now.Subtract(startTime).Milliseconds;
            if (waitingTime <= 0)
                break;
        }
        while (DateTime.Now <= endTime);
        return false;
    }

private bool findOnScreen(string value, string[] content)
    {
        if (content != null)
        {
            foreach (string line in content)
            {
                if (line.Contains(value))
                    return true;
            }
        }
        return false;
    }

using VirtualTerminal.Process with a loop that checks the contents of the virtual terminal does the trick for me.

0 votes
answered Apr 6, 2010 by Lukas Matyska (59,530 points)
edited Apr 6, 2010

Sorry for the delayd answer, I missed your post.

This is a dark side of the programming automatic shell scripts.

Please note that if the expected response is split into two lines, your findOnScreen method will not work. Here is a simple code that solves this (although it is not an optimal solution):

 return string.Join("", content).Contains(value);

In my opinion, you should “almost always” know how the response exactly ends. In case there are more possible endings we can add the Expect(string[] responses, int maximumWaitTime) method.

In case you really don’t know the ending of the response you have to keep in mind these scenarios:

  • expected response is scrolled out of the screen because a lot of text was received (you should avoid using the GetRegionText method)
  • expected response occurred as a part of a response which was not actually expected (waiting for "the" (like "… the file?" or "… the directory?") but received "… theater …", for example)
  • you are waiting for the response which is identical to a text that is already on the screen as a result of a previous command (you should avoid using the GetRegionText method)

Therefore you should always try to receive the whole server response to be able to determine where the response of the next command starts.

Here is some code snipped of a more robust WaitForString method (at least from my point of view):

    VirtualTerminal _terminal;
    StringBuilder _dataHolder;

    /// <summary>
    /// Receives the data from the server and searches for the occurance of the specified searchString.
    /// </summary>
    private bool WaitForString(string searchString, int maximumWaitingTime)
    {
        if (string.IsNullOrEmpty(searchString))
            return false;

        // initialize holder of the response data
        if (_dataHolder == null)
            _dataHolder = new StringBuilder();
        else
            _dataHolder.Length = 0;

        // register DataReceivedEventHandler to grab the response data
        DataReceivedEventHandler handler = new DataReceivedEventHandler(WaitForString_DataReceived);
        _terminal.DataReceived += handler;

        bool contains = false;
        int searchFrom = 0; // starting index (for time optimal search in multiblock responses)
        int start = Environment.TickCount; // start time
        do
        {
            // read all available data
            while (_terminal.Process() == TerminalState.DataReceived) ;

            // check whether response data contains searchString
            if (_dataHolder.ToString(searchFrom, _dataHolder.Length - searchFrom).Contains(searchString))
            {
                contains = true;
                break;
            }

            // adjust search start index to speed up searching in next step
            searchFrom = Math.Max(0, _dataHolder.Length - searchString.Length);
        }
        while (Environment.TickCount - start < maximumWaitingTime);

        // unregister handler to increase performance when we don't need to grab the data
        _terminal.DataReceived -= handler;
        return contains;
    }

    void WaitForString_DataReceived(object sender, DataReceivedEventArgs e)
    {
        _dataHolder.Append(e.StrippedData);
    }
...