/// /// 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 { /// /// This class implements iST Telnet Client functionality. /// 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" /// /// High level states of TelnetClient operation /// public enum TelnetState { Inactive, Busy, Success, Error, BusyFinal, /// Is not followed by a summary dialog FatalError, } /// /// High level state of TelnetClient operation used to update device's icon, etc. /// public TelnetState State { get { return state; } } /// readonly TelnetState state; /// /// Telnet (Network Virtual Terminal) control characters (code >= 128). /// /// 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 } /// /// Telnet (Network Virtual Terminal) options negotiated by the use of command: /// IAC + [WILL | WONT | DO | DONT] + NvtOption (sequence of 3 bytes). /// /// enum NvtOption { NA = -1, /// = NVT option not available (possibly a broken connection) SGA = 3, /// = Suppress go ahead } /// /// Network virtual terminal (state machine) states /// 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 } /// /// Network virtual terminal (state machine) state used in control at lower level /// NvtState nvtState; /// /// Network virtual terminal (state machine) events /// 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 } /// /// Values returned by communication functions /// 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 VirtualTerminal 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 cmdQueue = new Queue(); /// 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; } } } /// /// Thread safe cmdQueue.Peek() /// /// Command Command CmdQueuePeek() { lock (cmdQueue) { if (cmdQueue.Count > 0) return cmdQueue.Peek(); else return new Command(CmdAction.VOID); } } /// /// Thread safe cmdQueue.Clear(), Leaves TERMINATE in the queue, if there is any. /// /// RetVal.OK or RetVal.Teminate 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 auxBrdUpgMessages; /// /// 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. /// public TelnetClient(object objRef, string threadId, string userName, string password, string defaultPrompt, bool skipFirstLine) { 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 vt = new VirtualTerminal(); /// 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(); } /// /// Enqueue a command that terminates the worker thread, used on program exit /// public void Terminate() { lock (cmdQueue) { cmdQueue.Clear(); cmdQueue.Enqueue(new Command(CmdAction.TERMINATE)); queueEvent.Set(); } } /// /// Close the log file and finish the worker thread by enqueueing /// CmdAction.TERIMNATE into the command queue. /// 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")); } /// /// Worker thread procedure (an instance method). /// 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; } } } /// /// Disconnect the TCP client. /// /// RetVal.OK or RetVal.Error 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; } /// /// Callback method used by DisconnectTcp() /// /// IAsyncResult void DisconnectCallback(IAsyncResult ar) { Socket client = (Socket)ar.AsyncState; client.EndDisconnect(ar); /// Complete the disconnect request. disconnectDone.Set(); /// Signal that the disconnect is complete. } /// /// 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 /// /// true = poll the command queue, false = don't (e.g. when nvtState=WaitPrompt) /// true = poll TCP, false = don't (e.g. when nvtState=AwaitingConnect) /// A command from the command queue /// Received string from the TCP /// NVT event 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; } } } } /// /// Parse the host + port number string, update hostname and portNr variables. /// /// Example: "192.168.254.50" or "192.168.254.108:23" 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)); } } /// /// Login to the telnet server. /// Returns a response (a prompt) received after a login. /// /// User name /// Password /// RetVal.OK, RetVal.Timeout, RetVal.NoTcpConnection 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; } /// /// One part of login which is reused. /// /// Quit condition, login prompt should end with this pattern /// 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; } /// /// Sends a string to the telnet server. /// /// String to be sent void WriteTcp(string str) { Debug.WriteLine("WriteTcp(" + str.Replace("\n", "") + ")"); if (!tcp.Connected) return; byte[] buf = System.Text.ASCIIEncoding.ASCII.GetBytes(str.Replace("\0xFF", "\0xFF\0xFF")); tcp.GetStream().Write(buf, 0, buf.Length); } /// /// Sends a line to the telnet server - terminating '\n' is appended to the string 'str'. /// /// Line to be sent without terminaitng '\n' character(s). void WriteLineTcp(string str) { WriteTcp (str + "\n"); } /// /// Read a string from telnet until no characters available. /// string ReadTcp(TcpClient tcp) { if (!tcp.Connected) return null; StringBuilder strBldr = new StringBuilder(); ParseTelnet(strBldr); return strBldr.ToString(); } /// /// Parse telnet stream of characters. /// 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 VirtualTerminal Vt { get { return vt; } } public string Hostname { get { return hostname; } } public string NotificationText { get { return notificationText; } } public string ErrorText { get { return fatalErrorText; } } public IList AuxBrdUpgMessages { get { return auxBrdUpgMessages; } } /// /// true when there are command-s in cmdQueue /// 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; } /// /// Resets the progress to 0%. To be used before enqueing commands /// when starting upgrade (the 2nd and each next time). /// public void ResetProgress() { lock (cmdQueue) { timeDone = 0; } } /// /// Clear VT screen /// public void ClearScreen() { vt.ClearScreen(); } /// /// Insert a telnet client command to the FIFO queue 'cmdQueue' /// to be processed then by the worker thread. /// /// Command : a pair CmdAction, string public void Enqueue(Command cmd) { lock (cmdQueue) { cmdQueue.Enqueue(cmd); queueEvent.Set(); } } /// /// 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. /// /// Command : a pair CmdAction, string 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; } /// /// 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. /// /// [sec] /// [sec] 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; /// /// This method is called from within this class when a telnet connection /// was established and user was successfully logged in. /// 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; /// /// This method is called from within this class when the telnet connection was broken. /// 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; /// /// This method is called from within this class when the contents of virtual terminal has changed. /// 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 MeasurementDataReceivedHandler; /// /// This method is called from within this class when measurement data were extracted. /// 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; /// /// This method is called from within this class when /// a prompt string was received from the telnet server. /// 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; /// /// 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. /// 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; /// /// This method is called from within this class when a fatal error occurs. /// 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; /// /// 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) /// 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(); } auxBrdUpgMessages.Add(new AuxBrdUpgItemMessage(category, labelStr, messageText)); } /// /// Issued the collected errors to the UI when the queue is empty (the upgrade is complete). /// 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); } } } } }