tbf/TestBenchFramework/BenchControl/Network/Telnet/TelnetClient.cs

1379 lines
43 KiB
C#

///
/// Copyright (c) 2017 Sensus Metering Systems
///
using System;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Threading;
using System.Net.Sockets;
using log4net;
namespace TBF.BenchControl.Network.Telnet
{
/// <summary>
/// This class implements iST Telnet Client functionality.
/// </summary>
public class TelnetClient
{
static ILog log = LogManager.GetLogger("TelnetClient");
///
/// Common commands
///
public const string CtrlCCommand = "\u0003"; /// CTRL-C command to close the connection after a failed login
public const string RebootCommand = "sudo shutdown -r now"; /// 'reboot' command to reboot and start a new session, CmdAction.REBOOT_RECONNECT
public const string ExitCommand = "\u0004"; /// CTRL-D command to terminate the session, used by CmdAction.EXIT_RECONNECT,
///
/// Re-connect after re-boot behavior:
///
const int DelayAfterRebootMs = 20000; /// [ms] Delay before the first attempt to re-connect: 20 sec
const int LoginTimeOutMs = 20000; /// [ms] Login time out period, applies when TCP connection was established
const int LoginRetryPeriodMs = 25000; /// [ms] Delay before every next attempt to re-connect, normally applies when TCP connection was refused
const int TimeIncrementMs = 1000; /// [ms] Can be much larger now, times out blocking read operations, does not delay normal program flow
const int DefaultTelnetPortNumber = 23; /// Can be ooverriden by a host string like "192.168.0.5:8023"
/// <summary>
/// High level states of TelnetClient operation
/// </summary>
public enum TelnetState
{
Inactive,
Busy,
Success,
Error,
BusyFinal, /// Is not followed by a summary dialog
FatalError,
}
/// <summary>
/// High level state of TelnetClient operation used to update device's icon, etc.
/// </summary>
public TelnetState State { get { return state; } } /// readonly
TelnetState state;
/// <summary>
/// Telnet (Network Virtual Terminal) control characters (code >= 128).
/// <seealso cref = http://support.microsoft.com/kb/231866 />
/// </summary>
enum NvtCommand
{
NA = -1, /// -1 = NVT command not available (possibly a broken connection)
SE = 240, /// 240 = End of sub-negotiation parameters
NOP, /// 241 = No operation
DM, /// 242 = Data mark
BRK, /// 243 = Break
IP, /// 244 = Suspend
AO, /// 245 = Abort output
AYT, /// 246 = Are you there
EC, /// 247 = Erase characters
EL, /// 248 = Erase line
GA, /// 249 = Go ahead
SB, /// 250 = Sub-negotiation
WILL, /// 251 = will
WONT, /// 252 = wont
DO, /// 253 = do
DONT, /// 254 = don't
IAC, /// 255 = Interpret as command
}
/// <summary>
/// Telnet (Network Virtual Terminal) options negotiated by the use of command:
/// IAC + [WILL | WONT | DO | DONT] + NvtOption (sequence of 3 bytes).
/// <seealso cref = http://support.microsoft.com/kb/231866 />
/// </summary>
enum NvtOption
{
NA = -1, /// = NVT option not available (possibly a broken connection)
SGA = 3, /// = Suppress go ahead
}
/// <summary>
/// Network virtual terminal (state machine) states
/// </summary>
enum NvtState
{
AwaitingConnect = 0, /// Awaits CONNECT command from the cmdQueue, no session is active, TCP is not polled
Login, /// Retries a login after REBOOT_RECONNECT or EXIT_RECONNECT commands, queue is not polled
Session, /// Telnet session: awaits commands from the cmdQueue, receives characters from TCP (full-duplex)
Measurement, /// Telnet session: awaits CTRL-C command from the cmdQueue, receives characters from TCP and extracts data
WaitResponse, /// Receives characters from TCP, builds a response str., awaiting the prompt, queue is not polled
WaitReboot, /// Wait until TCP connection is terminated and reboot starts
NvtStatesCount
}
/// <summary>
/// Network virtual terminal (state machine) state used in control at lower level
/// </summary>
NvtState nvtState;
/// <summary>
/// Network virtual terminal (state machine) events
/// </summary>
enum NvtEvent
{
None = 0,
TcpReceived, /// TCP data received
CmdReceived, /// Command from the cmdQueue received
Timeout, /// Timeout occurred
ConnectionLost, /// TCP connection lost
Terminate /// Command CmdAction.TERMINATE received
}
/// <summary>
/// Values returned by communication functions
/// </summary>
enum RetVal
{
OK = 0,
LoginRejected, /// Login rejected, invalid username or password
Timeout,
NoTcpConnection, /// No TCP connection (connection lost)
Terminate, /// Returned by CmdQueueClear() if TERINATE is in the queue
Error,
}
///
/// Private fields
///
object objRef; /// Reference to object to be included in info sent by event handlers
IVirtualTerminal vt; /// Virtual terminal
string hostname; /// Telnet server host name or IP address.
int portNr = DefaultTelnetPortNumber; /// Telnet server port number.
TcpClient tcp = null; /// Reference to TcpClient
AutoResetEvent disconnectDone;
string prompt; /// Prompt string used for command complete detection
/// ... initialized to a default iCOM board prompt.
string labelStr = String.Empty; /// Set by a command with CmdAction.LABEL.
string completeResponse; /// A complete response used by IF_RECEIVED, IF_NOT_RECEIVED.
Thread thread; /// Worker thread.
bool disposed = false;
Queue<Command> cmdQueue = new Queue<Command>(); /// Worker thread command queue.
AutoResetEvent queueEvent; /// Sync. event set when cmdQueue is filled (=Enqueue())
/// Get command queue size
int cmdQueueCount
{
get { lock (cmdQueue) { return cmdQueue.Count; } }
}
/// <summary>
/// Thread safe cmdQueue.Peek()
/// </summary>
/// <returns>Command</returns>
Command CmdQueuePeek()
{
lock (cmdQueue)
{
if (cmdQueue.Count > 0) return cmdQueue.Peek();
else return new Command(CmdAction.VOID);
}
}
/// <summary>
/// Thread safe cmdQueue.Clear(), Leaves TERMINATE in the queue, if there is any.
/// </summary>
/// <returns>RetVal.OK or RetVal.Teminate</returns>
RetVal CmdQueueClear()
{
lock (cmdQueue)
{
if (cmdQueue.Count > 0 && cmdQueue.Peek().Action == CmdAction.TERMINATE) return RetVal.Terminate;
cmdQueue.Clear();
labelStr = String.Empty;
return RetVal.OK;
}
}
///
/// Properties set by the constructor
///
readonly string telnetUserName;
readonly string telnetPassword;
readonly string defaultPrompt;
readonly bool skipFirstLine; /// Skip the first line when logging in
DateTime cmdStartTime; /// Time of the last Dequeue (= command execution start time).
TimeSpan cmdTimeOut; /// Time out period of the last dequeued command.
DateTime connectTimeout; /// Checked when handling NvtEvent.Timeout and in the state NvtState.Login
int timeDone = 0; /// Duration of commands in [s] executed so far ...
/// ... starting from Command(CmdAction.NOTIFICATION, "Begin")
Command currentCmd = new Command(CmdAction.VOID); /// The currently executed command, used to calculate timeDone
string notificationText;
string fatalErrorText;
IList<AuxBrdUpgItemMessage> auxBrdUpgMessages;
/// <summary>
/// Creates a telnet connection, uses default port number, does not log in.
/// Hostname (IP address) and the port number is passed to worker thread
/// in a Command with CmdAction.CONNECT and str=hostname.
/// </summary>
public TelnetClient(object objRef, string threadId, string userName, string password, string defaultPrompt, bool skipFirstLine, bool useVT)
{
this.objRef = objRef;
this.telnetUserName = userName;
this.telnetPassword = password;
this.defaultPrompt = defaultPrompt;
this.skipFirstLine = skipFirstLine;
state = TelnetState.Inactive; /// Device's icon is being updated by HNIP
///
if (useVT)
{
vt = new VirtualTerminal();
}
else
{
vt = new DummyVT();
}
/// TODO: pass a VirtualTerminal reference
this.portNr = DefaultTelnetPortNumber;
queueEvent = new AutoResetEvent(false);
disconnectDone = new AutoResetEvent(false);
thread = new Thread(new ThreadStart(this.WorkerThread));
thread.Name = "Telnet_" + threadId + "_"; /// Is used also as a log file root name
thread.Start();
}
~TelnetClient()
{
Terminate();
}
/// <summary>
/// Enqueue a command that terminates the worker thread, used on program exit
/// </summary>
public void Terminate()
{
lock (cmdQueue)
{
cmdQueue.Clear();
cmdQueue.Enqueue(new Command(CmdAction.TERMINATE));
queueEvent.Set();
}
}
/// <summary>
/// Close the log file and finish the worker thread by enqueueing
/// CmdAction.TERIMNATE into the command queue.
/// </summary>
public void Dispose()
{
/// Check to see if Dispose has already been called. Avoid multiple Terminate()-s, thread.Join()-s.
lock (this)
{
if (disposed) return;
disposed = true;
}
Terminate();
if (!thread.Join(5000)) throw(new Exception("Fatal error"));
}
/// <summary>
/// Worker thread procedure (an instance method).
/// </summary>
void WorkerThread()
{
log.Info("-------------------- Worker thread started --------------------");
/// Turn timeout off (turned on by commands)
cmdStartTime = DateTime.Now;
cmdTimeOut = TimeSpan.MaxValue;
/// State machine state (NVT state)
nvtState = NvtState.AwaitingConnect;
/// Builds the response from particular 'ds'-es
StringBuilder responseBuilder = new StringBuilder();
Command cmd = new Command(CmdAction.VOID); /// Command from the command queue
string ds = null; /// String received from TCP
while (true)
{
bool pollCmd = (nvtState == NvtState.AwaitingConnect || nvtState == NvtState.Session || nvtState == NvtState.Measurement);
bool pollTcp = (nvtState != NvtState.AwaitingConnect && nvtState != NvtState.Login);
///
/// NVT state machine event read. Contains (blocking) cmdQueue and TCP reads.
/// Possible events are:
/// NvtEvent.Terminate ....... cmd.Action==CmdAction.TERMINATE occurred
/// NvtEvent.CmdReceived ..... command 'cmd' received from the command queue
/// NvtEvent.TcpReceived ..... string 'ds' received from the TCP
/// NvtEvent.ConnectionLost .. TCP connection lost (when reading TCP)
/// NvtEvent.Timeout ......... timeout occurred
///
NvtEvent nvtEvent = NvtWaitLoop(pollCmd, pollTcp, ref cmd, ref ds);
Debug.WriteLine("nvtEvent = " + nvtEvent.ToString() + ", cmd = " + cmd.ToString() +
((ds!=null && ds.Length>0) ? (", ds = '" + ds + "'") : "") );
///
/// NVT state machine state change, the event processing.
/// No command queue or TCP read can be done here.
///
try
{
if (nvtEvent == NvtEvent.Terminate)
{
DisconnectTcp();
return; /// Exit the thread
}
/// -------------------------------
/// Process command available event
/// -------------------------------
else if (nvtEvent == NvtEvent.CmdReceived)
{
if (cmd.Action == CmdAction.NOTIFICATION)
{
OnNotification(cmd.Str);
continue;
}
else if (cmd.Action == CmdAction.LABEL)
{
labelStr = cmd.Str;
log.Info("Starting to process Aux.Board Upgrade Item Nr." + labelStr);
continue;
}
else if (cmd.Action == CmdAction.SET_PROMPT)
{
prompt = cmd.Str;
continue;
}
else if (cmd.Action == CmdAction.CLEAR_RESPONSE)
{
responseBuilder.Clear();
continue;
}
else if (cmd.Action == CmdAction.CONNECT && nvtState == NvtState.AwaitingConnect)
{
ParseHostString(cmd.Str);
connectTimeout = cmdStartTime + cmdTimeOut;
cmdTimeOut = TimeSpan.Zero;
nvtState = NvtState.Login;
continue;
}
else if (nvtState == NvtState.Session || nvtState == NvtState.Measurement)
{
switch (cmd.Action)
{
case CmdAction.SEND_STRING:
vt.WriteVt(cmd.Str + System.Environment.NewLine);
WriteLineTcp(cmd.Str);
OnVtTextChanged();
break;
case CmdAction.SEND_MSRMNT_CMD:
vt.WriteVt(cmd.Str + System.Environment.NewLine);
WriteLineTcp(cmd.Str);
OnVtTextChanged();
/// state change
responseBuilder.Clear();
nvtState = NvtState.Measurement;
break;
case CmdAction.SEND_COMMAND:
vt.WriteVt(cmd.Str + System.Environment.NewLine);
WriteLineTcp(cmd.Str);
OnVtTextChanged();
/// state change
responseBuilder.Clear();
nvtState = NvtState.WaitResponse;
break;
case CmdAction.DISCONNECT:
if (RetVal.OK == DisconnectTcp())
{
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.AwaitingConnect;
}
else
{
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.Session;
}
break;
case CmdAction.REBOOT_RECONNECT:
vt.WriteVt(RebootCommand + System.Environment.NewLine);
WriteLineTcp(RebootCommand);
OnVtTextChanged();
connectTimeout = cmdStartTime + cmdTimeOut; /// Both were set when REBOOT_RECONNECT was dequeued
cmdTimeOut = TimeSpan.FromMilliseconds(DelayAfterRebootMs);
responseBuilder.Clear();
nvtState = NvtState.WaitReboot;
break;
case CmdAction.EXIT_RECONNECT:
if (RetVal.OK != DisconnectTcp())
{
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.Session;
break;
}
connectTimeout = cmdStartTime + cmdTimeOut;
cmdTimeOut = TimeSpan.Zero;
nvtState = NvtState.Login;
break;
default:
break;
}
continue;
}
}
/// --------------------------------
/// Process TCP data available event
/// --------------------------------
else if (nvtEvent == NvtEvent.TcpReceived)
{
switch (nvtState)
{
case NvtState.Session:
if (ds.Length > 0)
{
vt.WriteVt(ds);
OnVtTextChanged();
}
break;
case NvtState.Measurement:
if (ds.Length > 0)
{
vt.WriteVt(ds);
responseBuilder.Append(ds);
OnVtTextChanged();
}
{
/// Extract measurements
int from = responseBuilder.ToString().IndexOf('[');
if (from >= 0)
{
int len = responseBuilder.ToString(from + 1, responseBuilder.Length - from - 1).IndexOf(']');
if (len >= 0)
{
OnMeasurementDataReceived(this, new MeasuredDataEventArgs(responseBuilder.ToString(from + 1, len)));
responseBuilder.Remove(0, from + len + 2);
}
}
}
break;
case NvtState.WaitResponse:
if (ds.Length > 0)
{
vt.WriteVt(ds);
responseBuilder.Append(ds);
OnVtTextChanged();
}
/// Raise OnPromptReceived(response) and clear responseBuilder when prompt detected.
if (responseBuilder.ToString().EndsWith(prompt))
{
completeResponse = responseBuilder.ToString(0, responseBuilder.Length - prompt.Length);
responseBuilder.Clear();
cmdStartTime = DateTime.Now; /// Reset timeout
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.Session; /// Normal program flow
OnPromptReceived(completeResponse);
if (cmd.FatalRegex != null)
{
/// Check for a fatal error
Match match = cmd.FatalRegex.Match(completeResponse);
if (match.Success)
{
cmdTimeOut = TimeSpan.MaxValue;
DisconnectTcp(); /// Disconnect first
nvtState = NvtState.AwaitingConnect;
OnFatalError(GetRegexSubstring(match));
continue;
}
}
if (cmd.ErrorRegex != null)
{
/// Check for an error
Match match = cmd.ErrorRegex.Match(completeResponse);
if (match.Success) OnAddMessage(Category.Error, GetRegexSubstring(match));
}
if (cmd.WarningRegex != null)
{
/// Check for a warning
Match match = cmd.WarningRegex.Match(completeResponse);
if (match.Success) OnAddMessage(Category.Warning, GetRegexSubstring(match));
}
if (cmd.SuccessRegex != null)
{
/// Check for a success
Match match = cmd.SuccessRegex.Match(completeResponse);
if (match.Success) OnAddMessage(Category.Success, GetRegexSubstring(match));
}
}
break;
case NvtState.WaitReboot:
if (ds.Length > 0)
{
vt.WriteVt(ds);
responseBuilder.Append(ds);
OnVtTextChanged();
}
/// Check the expected response after 'reboot' command.
if (responseBuilder.ToString().EndsWith("Sending SIGTERM to all processes.\r\n"))
{
responseBuilder.Clear();
if (RetVal.OK == DisconnectTcp())
{
cmdTimeOut = TimeSpan.FromMilliseconds(DelayAfterRebootMs);
nvtState = NvtState.Login;
}
else
{
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.Session;
}
}
break;
default:
break;
}
continue;
}
/// ---------------------------------
/// Process TCP connection lost event
/// ---------------------------------
else if (nvtEvent == NvtEvent.ConnectionLost)
{
DisconnectTcp();
OnFatalError("Lost connection to telnet server " + hostname + ":" + portNr);
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.AwaitingConnect;
continue;
}
/// ---------------------
/// Process timeout event
/// ---------------------
else if (nvtEvent == NvtEvent.Timeout)
{
cmdStartTime = DateTime.Now; /// Reset time after any timeout
if (nvtState == NvtState.Login)
{
if (cmdStartTime > connectTimeout)
{
DisconnectTcp();
OnFatalError("(Re)Connect timeout elapsed");
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.AwaitingConnect;
continue;
}
else
{
try
{
tcp = new TcpClient(hostname, portNr);
log.Info("TCP client connected to " + hostname);
OnConnected();
}
catch (SocketException)
{
/// Cannot connect if iCOM is busy rebooting.
/// No state change, retry connection after LoginRetryPeriodMs
cmdTimeOut = TimeSpan.FromMilliseconds(LoginRetryPeriodMs);
continue;
}
prompt = defaultPrompt; /// Reset prompt to default
if (RetVal.OK == Login(telnetUserName, telnetPassword))
{
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.Session;
continue;
}
else
{
log.Info("Telnet login refused " + hostname);
DisconnectTcp();
// No state change, retry connection after LoginRetryPeriodMs
cmdTimeOut = TimeSpan.FromMilliseconds(LoginRetryPeriodMs);
continue;
}
}
}
else if (nvtState == NvtState.WaitResponse)
{
DisconnectTcp(); /// Disconnect first
OnFatalError("Timeout occurred at Telnet command " + cmd.ToString());
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.AwaitingConnect;
continue;
}
}
}
catch (Exception e)
{
string msg = e.Message;
DisconnectTcp();
OnDisconnected();
OnFatalError("Lost connection to telnet server " + hostname + ":" + portNr + " Details: " + msg);
cmdTimeOut = TimeSpan.MaxValue;
nvtState = NvtState.AwaitingConnect;
}
}
}
/// <summary>
/// Disconnect the TCP client.
/// </summary>
/// <returns>RetVal.OK or RetVal.Error</returns>
RetVal DisconnectTcp()
{
if (tcp == null || !tcp.Connected)
{
return RetVal.OK; /// Already disconnected
}
tcp.Client.Shutdown(SocketShutdown.Both);
tcp.Client.BeginDisconnect(false, new AsyncCallback(DisconnectCallback), tcp.Client); /// (reuseSocket, callback, ...)
/// Wait for the disconnect to complete.
disconnectDone.WaitOne();
if (tcp.Client.Connected)
{
OnFatalError("Cannot disconnect from telnet server " + hostname + ":" + portNr);
return RetVal.Error;
}
OnDisconnected();
tcp = null;
return RetVal.OK;
}
/// <summary>
/// Callback method used by DisconnectTcp()
/// </summary>
/// <param name="ar">IAsyncResult</param>
void DisconnectCallback(IAsyncResult ar)
{
Socket client = (Socket)ar.AsyncState;
client.EndDisconnect(ar); /// Complete the disconnect request.
disconnectDone.Set(); /// Signal that the disconnect is complete.
}
/// <summary>
/// NVT state machine event read loop.
/// Possible events are:
/// NvtEvent.Terminate ....... cmd.Action==CmdAction.TERMINATE occurred
/// NvtEvent.CmdReceived ..... command 'cmd' received from the command queue
/// NvtEvent.TcpReceived ..... string 'ds' received from the TCP
/// NvtEvent.ConnectionLost .. TCP connection lost (when reading TCP)
/// NvtEvent.Timeout ......... timeout occurred
/// Notes:
/// 'pollCmd == false' means WaitResponse or Rebooting+Login is in progress, cmdQueue is not polled
/// </summary>
/// <param name="pollCmd">true = poll the command queue, false = don't (e.g. when nvtState=WaitPrompt)</param>
/// <param name="pollTcp">true = poll TCP, false = don't (e.g. when nvtState=AwaitingConnect)</param>
/// <param name="cmd">A command from the command queue</param>
/// <param name="ds">Received string from the TCP</param>
/// <returns>NVT event</returns>
NvtEvent NvtWaitLoop(bool pollCmd, bool pollTcp, ref Command cmd, ref string ds)
{
while (true)
{
DateTime dateTimeNow = DateTime.Now; /// Current time is read at this single place in the loop
if (CmdQueuePeek().Action == CmdAction.TERMINATE)
{
/// On program exit
return NvtEvent.Terminate;
}
else if ((cmdTimeOut != TimeSpan.MaxValue) && (dateTimeNow >= (cmdStartTime + cmdTimeOut)))
{
return NvtEvent.Timeout;
}
else if (pollCmd && (cmdQueueCount > 0))
{
/// In NvtState.AwaitingCmg when queue is not empty
cmd = Dequeue();
return (cmd.Action == CmdAction.TERMINATE) ? NvtEvent.Terminate : NvtEvent.CmdReceived;
}
else if (pollTcp)
{
/// In NvtState.Session, NvtState.WaitResponse and NvtState.WaitReboot
try
{
if (tcp.Client.Poll(1000 * TimeIncrementMs, SelectMode.SelectRead)) /// Blocking, time is in microseconds
{
ds = ReadTcp(tcp);
return NvtEvent.TcpReceived;
}
}
catch (Exception)
{
return NvtEvent.ConnectionLost;
}
}
else
{
if (!pollCmd)
{
/// The remaining time out period is always positive, conversion done from ticks (100ns) to ms
int remainingTimeoutMs = (int)(((cmdStartTime + cmdTimeOut).Ticks - dateTimeNow.Ticks) / 10000);
if ((cmdTimeOut != TimeSpan.MaxValue) && (0 < remainingTimeoutMs) && (remainingTimeoutMs < TimeIncrementMs))
{
Thread.Sleep(remainingTimeoutMs);
}
else
{
Thread.Sleep(TimeIncrementMs);
}
}
else if (queueEvent.WaitOne(TimeIncrementMs, false) && (cmdQueueCount > 0))
{
/// In NvtState.AwaitingConnect when queue was empty and one element was just enqueued
cmd = Dequeue();
return cmd.Action == CmdAction.TERMINATE ? NvtEvent.Terminate : NvtEvent.CmdReceived;
}
}
}
}
/// <summary>
/// Parse the host + port number string, update hostname and portNr variables.
/// </summary>
/// <param name="hostStr">Example: "192.168.254.50" or "192.168.254.108:23"</param>
void ParseHostString(string hostStr)
{
/// Extract a hostname (or IP address) and a port number from cmd.Str
int colonPos = hostStr.IndexOf(':');
if (colonPos == -1)
{
hostname = hostStr;
portNr = DefaultTelnetPortNumber;
}
else
{
hostname = hostStr.Substring(0, colonPos);
portNr = Convert.ToUInt16(hostStr.Substring(colonPos + 1));
}
}
/// <summary>
/// Login to the telnet server.
/// Returns a response (a prompt) received after a login.
/// </summary>
/// <param name="username">User name</param>
/// <param name="password">Password</param>
/// <returns>RetVal.OK, RetVal.Timeout, RetVal.NoTcpConnection</returns>
RetVal Login(string username, string password)
{
if (username == null || password == null) return RetVal.LoginRejected;
///
/// Username
///
RetVal retval = WaitForLoginPrompt(": ", skipFirstLine);
if (retval != RetVal.OK) return retval;
vt.WriteVt(username + System.Environment.NewLine);
WriteLineTcp(username);
///
/// Password (hidden on the VT)
///
retval = WaitForLoginPrompt(": ", false);
if (retval != RetVal.OK) return retval;
WriteLineTcp(password);
///
/// Reset 'prompt' after (re)login and wait for the prompt
///
retval = WaitForLoginPrompt(prompt, false);
if (retval != RetVal.OK) return retval;
log.Info("Telnet login successfully completed " + hostname);
return RetVal.OK;
}
/// <summary>
/// One part of login which is reused.
/// </summary>
/// <param name="endsWithPattern">Quit condition, login prompt should end with this pattern</param>
/// <returns></returns>
RetVal WaitForLoginPrompt(string endsWithPattern, bool skipFirstLine)
{
StringBuilder s = new StringBuilder();
do {
Command cmd = new Command(CmdAction.VOID); /// Command from the command queue
string ds = null; /// String received from TCP
cmdStartTime = DateTime.Now;
cmdTimeOut = TimeSpan.FromMilliseconds(LoginTimeOutMs);
NvtEvent retval = NvtWaitLoop(false, true, ref cmd, ref ds); /// No cmd poll, do only TCP poll
Debug.WriteLine("retval = " + retval.ToString() + ", cmd = " + cmd.ToString() +
((ds != null && ds.Length > 0) ? (", ds = '" + ds + "'") : ""));
if (retval == NvtEvent.Timeout)
{
return RetVal.Timeout;
}
else if (retval != NvtEvent.TcpReceived)
{
return RetVal.NoTcpConnection;
}
if (ds != null && ds.Length > 0)
{
if (skipFirstLine)
{
if (ds.Contains(Environment.NewLine))
{
skipFirstLine = false;
string afterNL = ds.Substring(ds.IndexOf(Environment.NewLine) + Environment.NewLine.Length);
if (afterNL.Length > 0)
{
vt.WriteVt(afterNL);
s.Append(afterNL);
OnVtTextChanged();
}
}
}
else
{
vt.WriteVt(ds);
s.Append(ds);
OnVtTextChanged();
}
}
}
while (!s.ToString().EndsWith(endsWithPattern));
return RetVal.OK;
}
string GetRegexSubstring(Match match)
{
Group group = match.Groups[0];
if (group.Success) return group.Value;
// No group defined but expression matched anyway,
// return the matched substring instead
return match.Value;
}
/// <summary>
/// Sends a string to the telnet server.
/// </summary>
/// <param name="str">String to be sent</param>
void WriteTcp(string str)
{
Debug.WriteLine("WriteTcp(" + str.Replace("\n", "<LF>") + ")");
if (!tcp.Connected) return;
byte[] buf = System.Text.ASCIIEncoding.ASCII.GetBytes(str.Replace("\0xFF", "\0xFF\0xFF"));
tcp.GetStream().Write(buf, 0, buf.Length);
}
/// <summary>
/// Sends a line to the telnet server - terminating '\n' is appended to the string 'str'.
/// </summary>
/// <param name="str">Line to be sent without terminaitng '\n' character(s).</param>
void WriteLineTcp(string str)
{
WriteTcp (str + "\n");
}
/// <summary>
/// Read a string from telnet until no characters available.
/// </summary>
string ReadTcp(TcpClient tcp)
{
if (!tcp.Connected) return null;
StringBuilder strBldr = new StringBuilder();
ParseTelnet(strBldr);
return strBldr.ToString();
}
/// <summary>
/// Parse telnet stream of characters.
/// </summary>
void ParseTelnet(StringBuilder strBldr)
{
while (tcp.Available > 0)
{
NvtCommand input = (NvtCommand)tcp.GetStream().ReadByte();
switch (input)
{
case NvtCommand.NA:
break;
case NvtCommand.IAC:
/// Interpret as command
NvtCommand inputCmd = (NvtCommand)tcp.GetStream().ReadByte();
if (inputCmd == NvtCommand.NA) break;
switch (inputCmd)
{
case NvtCommand.IAC:
/// Literal IAC = 255 escaped, so append char 255 to the string
strBldr.Append(inputCmd);
break;
case NvtCommand.DO:
case NvtCommand.DONT:
case NvtCommand.WILL:
case NvtCommand.WONT:
/// Reply to all commands with "WONT", unless it is SGA (suppres go ahead)
NvtOption inputOpt = (NvtOption)tcp.GetStream().ReadByte();
if (inputOpt == NvtOption.NA) break;
tcp.GetStream().WriteByte((byte)NvtCommand.IAC);
if (inputOpt == NvtOption.SGA)
{
if (inputCmd == NvtCommand.DO)
tcp.GetStream().WriteByte((byte)NvtCommand.WILL);
else
tcp.GetStream().WriteByte((byte)NvtCommand.DO);
}
else
{
if (inputCmd == NvtCommand.DO)
tcp.GetStream().WriteByte((byte)NvtCommand.WONT);
else
tcp.GetStream().WriteByte((byte)NvtCommand.DONT);
}
tcp.GetStream().WriteByte((byte)inputOpt);
break;
default:
break;
}
break;
default:
strBldr.Append( (char)input );
break;
}
}
}
///
/// Public properties (read only)
///
public IVirtualTerminal Vt { get { return vt; } }
public string Hostname { get { return hostname; } }
public string NotificationText { get { return notificationText; } }
public string ErrorText { get { return fatalErrorText; } }
public IList<AuxBrdUpgItemMessage> AuxBrdUpgMessages { get { return auxBrdUpgMessages; } }
/// <summary>
/// true when there are command-s in cmdQueue
/// </summary>
public bool InProgress
{
get { return (cmdQueueCount > 0); }
}
///
/// Public methods (self explanatory)
///
public void ResetNotificationText() { notificationText = null; }
public void ResetErrorText() { fatalErrorText = null; }
public void ResetCollectedMessages()
{
auxBrdUpgMessages = null;
}
/// <summary>
/// Resets the progress to 0%. To be used before enqueing commands
/// when starting upgrade (the 2nd and each next time).
/// </summary>
public void ResetProgress()
{
lock (cmdQueue)
{
timeDone = 0;
}
}
/// <summary>
/// Clear VT screen
/// </summary>
public void ClearScreen()
{
vt.ClearScreen();
}
/// <summary>
/// Insert a telnet client command to the FIFO queue 'cmdQueue'
/// to be processed then by the worker thread.
/// </summary>
/// <param name="cmd">Command : a pair CmdAction, string</param>
public void Enqueue(Command cmd)
{
lock (cmdQueue)
{
cmdQueue.Enqueue(cmd);
queueEvent.Set();
}
}
/// <summary>
/// Get a command from the FIFO queue 'cmdQueue'.
/// Called by the worker thread when commands are really processed.
/// Does also auxiliary actions, resets cmdStartTime time, etc.
/// </summary>
/// <param name="cmd">Command : a pair CmdAction, string</param>
Command Dequeue()
{
Command nextCmd;
bool queueJustEmptied = false;
try
{
lock (cmdQueue)
{
queueJustEmptied = (cmdQueue.Count == 1);
nextCmd = cmdQueue.Dequeue();
}
log.Info("Command: " + nextCmd.ToString());
}
catch
{
log.Debug("Debug: Attempt to read a command from an empty queue.");
nextCmd = new Command(CmdAction.VOID);
}
cmdStartTime = DateTime.Now;
if (nextCmd.Timeout > 0)
{
cmdTimeOut = new TimeSpan(0, 0, nextCmd.Timeout); /// in seconds
}
else
{
cmdTimeOut = TimeSpan.MaxValue; /// no timeout (timeout = weeks)
}
lock (cmdQueue)
{
if (currentCmd.Duration >= 0)
{
timeDone += currentCmd.Duration;
}
}
if (queueJustEmptied && nextCmd.Action != CmdAction.TERMINATE)
{
IssueSummary();
}
currentCmd = nextCmd;
return currentCmd;
}
/// <summary>
/// Progress info to be polled by UI routines.
/// Info is based on the commands in the queue and 'timeDone'
/// Is thread safe, merely reads some fields and does safe calculations.
/// </summary>
/// <param name="timeDone">[sec]</param>
/// <param name="timeTodo">[sec]</param>
public void ProgressInfo(ref int timeDone, ref int timeTodo)
{
Command[] cmds;
lock (cmdQueue)
{
cmds = cmdQueue.ToArray();
timeDone = this.timeDone;
}
timeTodo = currentCmd.Duration;
for (int i = 0; i < cmds.GetLength(0); i++)
{
timeTodo += cmds[i].Duration;
}
return;
}
public event EventHandler connectedHandler;
/// <summary>
/// This method is called from within this class when a telnet connection
/// was established and user was successfully logged in.
/// </summary>
protected virtual void OnConnected()
{
Debug.WriteLine("TelnetClient:OnConnected");
log.Warn("Connected");
if (null != connectedHandler)
{
try
{
connectedHandler(objRef, System.EventArgs.Empty);
}
catch (Exception e)
{
log.Info("Internal error: ConnectedHandler exception: " + e.Message);
}
}
}
public event EventHandler disconnectedHandler;
/// <summary>
/// This method is called from within this class when the telnet connection was broken.
/// </summary>
protected virtual void OnDisconnected()
{
Debug.WriteLine("TelnetClient:OnDisconnected");
log.Warn("Disconnected");
if (null != disconnectedHandler)
{
try
{
disconnectedHandler(objRef, System.EventArgs.Empty);
}
catch (Exception e)
{
log.Info("Internal error: DisconnectedHandler exception: " + e.Message);
}
}
}
///
/// VtTextChangedEventHandler delegate and handler
///
public delegate void VtTextChangedEventHandler(object sender, VtTextChangedEventArgs e);
public event VtTextChangedEventHandler vtTextChangedHandler;
/// <summary>
/// This method is called from within this class when the contents of virtual terminal has changed.
/// </summary>
protected virtual void OnVtTextChanged()
{
if (null != vtTextChangedHandler)
{
try
{
vtTextChangedHandler(objRef, new VtTextChangedEventArgs(vt.Text));
}
catch (Exception e)
{
log.Info("Internal error: VtTextChangedHandler exception: " + e.Message);
}
}
}
///
/// MeasurementDataReceivedHandler handler
///
public event EventHandler<MeasuredDataEventArgs> MeasurementDataReceivedHandler;
/// <summary>
/// This method is called from within this class when measurement data were extracted.
/// </summary>
public void OnMeasurementDataReceived(object sender, MeasuredDataEventArgs msrmtData)
{
if (MeasurementDataReceivedHandler == null) return;
try { MeasurementDataReceivedHandler(sender, msrmtData); }
catch (Exception e) { log.Error("MeasurementDataReceivedHandler(...) failed", e); }
}
///
/// PromptReceviedEventHandler delegate and handler
///
public delegate void PromptReceivedEventHandler(object sender, PromptReceivedEventArgs e);
public event PromptReceivedEventHandler promptReceivedHandler;
/// <summary>
/// This method is called from within this class when
/// a prompt string was received from the telnet server.
/// </summary>
protected virtual void OnPromptReceived(string response)
{
Debug.WriteLine("TelnetClient:OnPromptReceived");
log.Info("Prompt received, response:" + Environment.NewLine + completeResponse);
if (null != promptReceivedHandler)
{
try
{
promptReceivedHandler(objRef, new PromptReceivedEventArgs(response));
}
catch (Exception e)
{
log.Info("Internal error: PromptReceivedHandler exception: " + e.Message);
}
}
}
///
/// NotificationEventHandler delegate and handler
///
public delegate void NotificationEventHandler(object sender, NotificationEventArgs e);
public event NotificationEventHandler notificationHandler;
/// <summary>
/// This method is called from within this class when the command processing loop
/// receives a Command with Action==NOTIFICATION. This can be used for synchronization.
/// </summary>
protected virtual void OnNotification(string notificationText)
{
Debug.WriteLine("TelnetClient:OnNotification");
log.Info("Notification: " + notificationText);
this.notificationText = notificationText;
if (null != notificationHandler)
{
try
{
notificationHandler(objRef, new NotificationEventArgs(notificationText));
}
catch (Exception e)
{
log.Info("Internal error: NotificationHandler exception: " + e.Message);
}
}
}
///
/// FatalEventHandler delegate and handler
///
public delegate void FatalEventHandler(object sender, FatalEventArgs e);
public event FatalEventHandler fatalErrorHandler;
/// <summary>
/// This method is called from within this class when a fatal error occurs.
/// </summary>
protected virtual void OnFatalError(string fatalErrorText)
{
Debug.WriteLine("TelnetClient:OnFatalError(" + fatalErrorText + ")");
log.FatalFormat("Fatal: {0}", fatalErrorText);
/// Skip the remaining command queue actions (except of TERMINATE)
CmdQueueClear();
this.fatalErrorText = fatalErrorText;
state = TelnetState.FatalError;
if (null != fatalErrorHandler)
{
try
{
fatalErrorHandler(objRef, new FatalEventArgs(fatalErrorText));
}
catch (Exception e)
{
log.Info("Internal fatal: FatalHandler exception: " + e.Message);
}
}
}
///
/// SummaryEventHandler delegate and handler
///
public delegate void SummaryEventHandler(object sender, SummaryEventArgs e);
public event SummaryEventHandler showSummaryHandler;
/// <summary>
/// This method is called from within this class when a error occurs.
/// Error does not interrupt the processing. It merely buffers the error.
/// The error is issued to UI when the queue is empty (the upgrade is complete)
/// </summary>
protected virtual void OnAddMessage(Category category, string messageText)
{
Debug.WriteLine("TelnetClient:OnAddMessage");
log.WarnFormat("Aux.Board Upgrade Item Nr.{0} {1}({2})", labelStr, category, messageText);
if (category == Category.Error) state = TelnetState.Error;
if (auxBrdUpgMessages == null)
{
auxBrdUpgMessages = new List<AuxBrdUpgItemMessage>();
}
auxBrdUpgMessages.Add(new AuxBrdUpgItemMessage(category, labelStr, messageText));
}
/// <summary>
/// Issued the collected errors to the UI when the queue is empty (the upgrade is complete).
/// </summary>
protected virtual void IssueSummary()
{
if (state == TelnetState.BusyFinal || state == TelnetState.Inactive)
{
state = TelnetState.Inactive;
return;
}
if (state == TelnetState.Busy) state = TelnetState.Success; /// Success in case there wasn't an error so far
if (null != showSummaryHandler)
{
try
{
showSummaryHandler(objRef, new SummaryEventArgs(auxBrdUpgMessages));
}
catch (Exception e)
{
log.Info("Internal error: SummaryEventHandler exception: " + e.Message);
}
}
}
}
}