Introduction
This article deals with my CommScript
class, which is a simple scripting engine for automating communication (e.g., Telnet).
What is presented here is the result of several year's evolution; originally, the socket and the script engine were combined in one class, but I have since decided that the engine could be used to execute scripts on other types of communication links, so I split them apart.
Background
One of the more irksome, yet challenging and ultimately rewarding things I had to accomplish on my last job was the automation of Telnetting into a Unix system, running a third-party application, navigating a character-based menu/form system, entering data, and responding to feedback -- and still exiting neatly. It quickly became apparent that I needed to have my code write a script and then have something execute the script. For various reasons, I decided to write my own script language and interpreter.
The primary concern in my design and implementation of this class was the ability to have it automate interaction with a third-party application on a Unix system. The scripts were generated and executed within a Windows Service -- there was no need for a user interface, so no concern was given to being usable by interactive processes. Likewise, although the scripts may be stored in files then read and passed in, the primary purpose was to generate the scripts directly in strings on the fly.
IScriptableCommunicator
The CommScript
class requires an instance of a class that implements the IScriptableCommunicator
interface:
public delegate void DataReceived ( string Data ) ;
public delegate void ExceptionCaught ( System.Exception Exception ) ;
public interface IScriptableCommunicator : System.IDisposable
{
void Connect ( string Host ) ;
void WriteLine ( string Data , params object[] Parameters ) ;
void Write ( string Data , params object[] Parameters ) ;
void Close () ;
System.TimeSpan ResponseTimeout { get ; set ; }
System.Text.Encoding Encoding { get ; set ; }
event DataReceived OnDataReceived ;
event ExceptionCaught OnExceptionCaught ;
}
With an instance of a class that implements this interface, your code can:
- Set a timeout (the connection will abort if the timeout expires)
- Specify an encoding to use to convert between
byte[]
and string
- Connect to a Telnet server
- Send data to the server
- Receive data back
- Receive Exceptions that occur when sending data
The Scripting Language
The (unnamed) scripting language I present here has evolved from a very simple scripting language I encountered many years ago. At that time, one of my co-workers was configuring terminal servers with a utility called "fta" (as I recall); I have no idea where it originated, I have found no information on it, I assume it came from Unix, though we were using OpenVMS. The utility executed scripts which had two types of lines, which could be thought of as "send" and "wait"; "wait" commands began with a period (.), "send" lines were everything else. The crazy part was that the "wait" had to be before the "send" command, but would remain in effect until another "wait" was encountered.
When I determined that I needed to create and implement my own scripting language, I thought of those fta scripts as a reasonable starting point. I recognized that being line-oriented and having the "wait" precede the "send", though clumsy, allowed for very simple parsing and execution. But I knew I needed more than two types of commands, that meant that I needed an actual "send" command. After perusing my old copy of Principles of Programming Languages[^] (second edition, 1987), I determined that what fta lacked is "regularity" (don't we all?) and "consistency". However, I decided to stick with being line-oriented with single-character symbolic commands (hey! it's a non-English-based computer language!). OK, you can stop reading if that makes you queasy ("It scares the willies out of me." -- Slartibartfast)
Therefore, I needed to find single-character symbols for "send" and "wait"; they needed to be similar to each other, hint at their purpose, and be memorable. I chose ">" (greater than) for Send, and "<" (less than) for Wait (replacing period). For a Connect command, "@" (at) seems appropriate. Comments can be important; in the past, I've used ";" (semi-colon) for that, and I did so again. I needed an exit command, so I chose "$" (dollar sign) to indicate a successful completion of the script (the money shot).
Those were enough to get started, but I soon learned that I needed some amount of flexibility for dealing with exceptions. Therefore, I added subroutines, and a sort of handler or "on error gosub" or "if you see this, call that" functionality. The Branch command -- "?" (question mark) -- maps a handler for a situation. You can also Call a subroutine directly with the "^" (carat) command. There are two types of subroutines: []-type subroutines and {}-type subroutines, and I can never remember which is which. The difference is that with []-type subroutines, any "wait" and "on error gosub" settings remain in effect after the subroutine returns; with {}-type subroutines, the "wait" and "on error gosub" settings that were in effect before the subroutine was called will be restored when the subroutine returns. (Terminating a script from within a subroutine is allowed.)
When interacting with the third-party character-based menu/form application, there were times when "send" (which adds a carriage-return) wasn't the right tool. So, I added the Press command -- "#" (number , pound) -- which is designed to send keystrokes (with no carriage-return). The most common use is to define a keystroke and then send it with the Press (#) command. The Define command -- "%" (percent) -- is used to define a "known string" (typically a keystroke or other short sequence). Known strings may also be provided to the CommScript
constructor, this allows sharing the strings among several instances of CommScript
.
As my scripts became more complex, I realized that the Success ($) command was not flexible enough; it returns only zero (0) for success; I needed to return other values that might mean partial success, or at least not complete failure. The result is the Error command -- "!" (exclamation point) -- which pretty much makes the Success ($) command obsolete.
Miscellaneous commands:
- It also seemed like including code stored in other files might be a good idea, so I added the Include command -- "&" (ampersand).
- And, the ability to store (log) output in a file can be useful in some cases, so I added the Log command -- "|" (vertical bar, pipe) -- as well.
Many of the commands also make use of the "=" (equal sign) to separate operands of the command. The telnets.html file included in the zip has more information on the script language.
Still with me?
I'm sure that lots of readers would have already skipped to the bottom to 1-vote the language; thanks for staying. However, as I said, the language has evolved. Because I now use a Regular Expression and an EnumTransmogrifier[^] to split and parse the command, you can now use command names (not case-sensitive) rather than the symbols, or mix them together. If the command has operands, there must be a whitespace character after the command name.
Operation
The Operation
enumeration contains the names and symbols for the commands.
[PIEBALD.Attributes.EnumDefaultValue((int)Operation.Noop)]
public enum ScriptCommand
{
[System.ComponentModel.DescriptionAttribute("")]
Noop
,
[System.ComponentModel.DescriptionAttribute(";")]
Comment
,
[System.ComponentModel.DescriptionAttribute("%")]
Define
,
[System.ComponentModel.DescriptionAttribute("&")]
Include
,
[System.ComponentModel.DescriptionAttribute("[")]
StartBracket
,
[System.ComponentModel.DescriptionAttribute("]")]
EndBracket
,
[System.ComponentModel.DescriptionAttribute("{")]
StartBrace
,
[System.ComponentModel.DescriptionAttribute("}")]
EndBrace
,
[System.ComponentModel.DescriptionAttribute("^")]
Call
,
[System.ComponentModel.DescriptionAttribute("|")]
Log
,
[System.ComponentModel.DescriptionAttribute("@")]
Connect
,
[System.ComponentModel.DescriptionAttribute("?")]
Branch
,
[System.ComponentModel.DescriptionAttribute("<")]
Wait
,
[System.ComponentModel.DescriptionAttribute(">")]
Send
,
[System.ComponentModel.DescriptionAttribute("#")]
Press
,
[System.ComponentModel.DescriptionAttribute("!")]
Error
,
[System.ComponentModel.DescriptionAttribute("$")]
Success
}
Statement
The Statement
class holds an operation and its operands. Some of you may recommend having a sub-class of Statement
for each type of command, but I don't think anything would be gained by that.
private class Statement
{
public readonly Operation Command ;
public readonly string Operand1 ;
public readonly string Operand2 ;
private Statement
(
Operation Command
,
string Operand1
,
string Operand2
)
{
this.Command = Command ;
this.Operand1 = Operand1 ;
this.Operand2 = Operand2 ;
return ;
}
public override string
ToString
(
)
{
string result = System.String.Format
(
"{0}{1}{2}"
,
command.ToString ( this.Command )
,
this.Operand1 == null ? System.String.Empty : this.Operand1
,
this.Operand2 == null ? System.String.Empty : "=" + this.Operand2
) ;
return ( result ) ;
}
}
Parse
The Parse
method splits and parses a line, then instantiates a Statement
. Blank lines result in Noop
s. If a line can't be parsed successfully, an Exception will be thrown.
The Regular Expression and the enumeration must be kept in sync.
private static readonly System.Text.RegularExpressions.Regex reg ;
private static readonly PIEBALD.Types.EnumTransmogrifier<Operation> command ;
private static readonly System.Collections.Generic.HashSet<Operation> binaryop ;
public static readonly Statement Noop ;
static Statement
(
)
{
reg = new System.Text.RegularExpressions.Regex
(
"^\\s*(?:(?'Command'[;<>[\\]{}$^!&%|?#@])|(?:(?'Command'\\w+)\\s?))" +
"(?'Parameter'(?'Name'[^=]*)?(?:=(?'Value'.*))?)?$"
) ;
command = new EnumTransmogrifier<Operation>
(
System.StringComparer.CurrentCultureIgnoreCase
) ;
binaryop = new System.Collections.Generic.HashSet<Operation>() ;
binaryop.Add ( Operation.Branch ) ;
binaryop.Add ( Operation.Define ) ;
binaryop.Add ( Operation.Error ) ;
binaryop.Add ( Operation.StartBrace ) ;
binaryop.Add ( Operation.StartBracket ) ;
Noop = new Statement ( Operation.Noop , null , null ) ;
return ;
}
public static Statement
Parse
(
string Line
)
{
Statement result = Noop ;
System.Text.RegularExpressions.MatchCollection mat = reg.Matches ( Line ) ;
if ( mat.Count == 1 )
{
Operation cmd = command.Parse ( mat [ 0 ].Groups [ "Command" ].Value ) ;
if ( binaryop.Contains ( cmd ) )
{
result = new Statement
(
cmd
,
mat [ 0 ].Groups [ "Name" ].Value
,
mat [ 0 ].Groups [ "Value" ].Value
) ;
}
else
{
result = new Statement
(
cmd
,
mat [ 0 ].Groups [ "Parameter" ].Value
,
null
) ;
}
}
return ( result ) ;
}
Subroutine
The Subroutine
class holds the name, type, starting line number, and ending line number of a subroutine. The only way to return from a subroutine is by reaching the ending line number.
private class Subroutine
{
public enum SubroutineType
{
Bracket
,
Brace
}
public readonly string Name ;
public readonly SubroutineType Type ;
public readonly int Start ;
private int end ;
public Subroutine
(
string Name
,
SubroutineType Type
,
int Start
)
{
this.Name = Name ;
this.Type = Type ;
this.Start = Start ;
this.end = 0 ;
return ;
}
public int
End
{
get
{
return ( this.end ) ;
}
set
{
if ( this.end != 0 )
{
throw ( new System.InvalidOperationException
( "Attempt to reset the End of a Subroutine" ) ) ;
}
this.end = value ;
return ;
}
}
public override string
ToString
(
)
{
return ( this.Name ) ;
}
}
SubroutineCall
The SubroutineCall
class holds a reference to a Subroutine
and the state of the engine when it was called. When a subroutine returns, the program counter must be restored. The states of the sought and branches will also be restored if the subroutine is a Bracket-type subroutine.
private class SubroutineCall
{
public readonly Subroutine Subroutine ;
public readonly int From ;
public readonly string Sought ;
public readonly System.Collections.Generic.Dictionary<string,string> Branches ;
public SubroutineCall
(
Subroutine Subroutine
,
int From
,
string Sought
,
System.Collections.Generic.Dictionary<string,string> Branches
)
{
this.Subroutine = Subroutine ;
this.From = From ;
this.Sought = Sought ;
this.Branches = new System.Collections.Generic.Dictionary<string,string>
(
Branches
,
Branches.Comparer
) ;
return ;
}
public override string
ToString
(
)
{
return ( this.Subroutine.ToString() ) ;
}
}
CommScript
Which brings us to the CommScript
class. A class that implements IScriptableCommunicator
must be passed to the constructor. A collection of known strings may also be passed, this allows an application to define the known strings ahead of time; I used this feature as a performance boost when I used this in production.
The other fields are for holding the script, a Dictionary
of Subroutine
s, and the state of the engine.
public sealed class CommScript : System.IDisposable
{
private PIEBALD.Types.IScriptableCommunicator socket ;
private System.Collections.Generic.Dictionary<string,string> knownstrings ;
private System.Collections.Generic.List<Statement> script = null ;
private System.Collections.Generic.Dictionary<string,Subroutine> subroutines = null ;
private System.Collections.Generic.Dictionary<string,string> branches = null ;
private System.Collections.Generic.Stack<SubroutineCall> substack = null ;
private System.Collections.Generic.Stack<System.IO.TextWriter> outstream = null ;
private System.Text.StringBuilder response = null ;
private int pc = 0 ;
private string sought = "" ;
private bool found = true ;
public CommScript
(
PIEBALD.Types.IScriptableCommunicator Socket
)
: this
(
Socket
,
null
)
{
return ;
}
public CommScript
(
PIEBALD.Types.IScriptableCommunicator Socket
,
System.Collections.Generic.Dictionary<string,string> KnownStrings
)
{
if ( Socket == null )
{
throw ( new System.ArgumentNullException ( "Socket" ,
"Socket must not be null" ) ) ;
}
this.socket = Socket ;
this.socket.OnDataReceived += this.ProcessResponse ;
this.socket.OnExceptionCaught += this.RaiseExceptionCaught ;
if ( KnownStrings == null )
{
this.knownstrings =
new System.Collections.Generic.Dictionary<string,string>() ;
}
else
{
this.knownstrings = KnownStrings ;
}
return ;
}
public void
Dispose
(
)
{
if ( this.socket != null )
{
this.socket.OnDataReceived -= this.ProcessResponse ;
this.socket.OnExceptionCaught -= this.RaiseExceptionCaught ;
this.socket.Dispose() ;
}
this.knownstrings = null ;
this.response = null ;
this.branches = null ;
this.script = null ;
this.subroutines = null ;
this.substack = null ;
this.outstream = null ;
return ;
}
}
ExecuteScript
Once instantiated, the only thing you can do with a CommScript
is execute a script. The primary way I use a CommScript
is to cobble up the script in a string and pass it in. You could also execute a script that is stored in a file or piped into a utility. The data from the host will be returned in the Outstream
parameter. The return code is determined by the script engine:
- If a $ statement is executed, the returned value is zero (0).
- If a ! statement is executed, the returned value is the line number or the value provided by the ! statement.
- If an error occurs in the script, the returned value will be the negative of the line number that encountered the error.
public int
ExecuteScript
(
string Script
,
out string Outstream
)
{
if ( Script == null )
{
throw ( new System.ArgumentNullException ( "Script" ,
"Script must not be null" ) ) ;
}
System.IO.StringWriter temp = new System.IO.StringWriter() ;
try
{
temp.NewLine = "\n" ;
return ( this.DoExecuteScript (
new System.IO.StringReader ( Script ) , temp ) ) ;
}
finally
{
Outstream = temp.ToString() ;
}
}
public int
ExecuteScript
(
System.IO.TextReader Script
,
System.IO.TextWriter Outstream
)
{
if ( Script == null )
{
throw ( new System.ArgumentNullException ( "Script" ,
"Script must not be null" ) ) ;
}
if ( Outstream == null )
{
throw ( new System.ArgumentNullException ( "Outstream" ,
"Outstream must not be null" ) ) ;
}
return ( this.DoExecuteScript ( Script , Outstream ) ) ;
}
DoExecuteScript
This method sets up the fields of the CommScript
to hold the script, loads it, finds and validates any subroutines, and activates the engine.
private int
DoExecuteScript
(
System.IO.TextReader Script
,
System.IO.TextWriter Outstr
)
{
this.response = new System.Text.StringBuilder() ;
this.script = new System.Collections.Generic.List<Statement>() ;
this.substack = new System.Collections.Generic.Stack<SubroutineCall>() ;
this.branches = new System.Collections.Generic.Dictionary<string,string>() ;
this.outstream = new System.Collections.Generic.Stack<System.IO.TextWriter>() ;
this.subroutines = new System.Collections.Generic.Dictionary<string,Subroutine>() ;
this.outstream.Push ( Outstr ) ;
this.LoadScript ( Script ) ;
this.MapSubroutines() ;
this.ValidateSubroutineCalls() ;
return ( this.DoExecuteScript() ) ;
}
LoadScript
Obviously, LoadScript
loads the script into memory. Each line is parsed into a Statement
instance, then there is special handling of some statements:
- Includes will cause the specified file to be read-in in place of the statement
- Subroutines may also indicate that a file should be read-in
- Defines are checked to be sure that there's at least a name
- All other lines are added to the script as they are
private void
LoadScript
(
System.IO.TextReader Script
)
{
string line ;
Statement statement ;
while ( ( Script.Peek() != -1 ) && ( ( line = Script.ReadLine() ) != null ) )
{
statement = Statement.Parse ( line ) ;
switch ( statement.Command )
{
case Statement.Operation.Include :
{
this.IncludeFile ( statement.Operand1 ) ;
break ;
}
case Statement.Operation.StartBracket :
{
if ( statement.Operand1.Length == 0 )
{
throw ( new System.InvalidOperationException (
"No name for subroutine" ) ) ;
}
this.script.Add ( statement ) ;
if ( statement.Operand2.Length > 0 )
{
this.IncludeFile ( statement.Operand2 ) ;
this.script.Add ( Statement.Parse ( "]" ) ) ;
}
break ;
}
case Statement.Operation.StartBrace :
{
if ( statement.Operand1.Length == 0 )
{
throw ( new System.InvalidOperationException (
"No name for subroutine" ) ) ;
}
this.script.Add ( statement ) ;
if ( statement.Operand2.Length > 0 )
{
this.IncludeFile ( statement.Operand2 ) ;
this.script.Add ( Statement.Parse ( "}" ) ) ;
}
break ;
}
case Statement.Operation.Define :
{
if ( statement.Operand1.Length == 0 )
{
throw ( new System.InvalidOperationException (
"No name for known string" ) ) ;
}
this.script.Add ( statement ) ;
break ;
}
default :
{
this.script.Add ( statement ) ;
break ;
}
}
}
return ;
}
IncludeFile
Inclusion of files happens recursively -- so be careful.
private void
IncludeFile
(
string FileName
)
{
System.IO.FileInfo fi = PIEBALD.Lib.LibFil.GetExpandedFileInfo ( FileName ) ;
if ( !fi.Exists )
{
throw ( new System.ArgumentException
(
string.Format ( "File {0} does not exist" , FileName )
) ) ;
}
this.LoadScript ( fi.OpenText() ) ;
return ;
}
MapSubroutines
Any subroutine in the script must be added to the Subroutines Dictionary. This method iterates the statements of the script, seeking bracket and brace statements. When a subroutine start is detected, a Subroutine
instance is created for it and pushed onto the stack. When a subroutine end is detected, a Subroutine
instance is popped off the stack, validated, and added to the Subroutines Dictionary.
private void
MapSubroutines
(
)
{
Statement statement ;
Subroutine temp ;
int linenum = 0 ;
System.Collections.Generic.Stack<Subroutine> stack =
new System.Collections.Generic.Stack<Subroutine>() ;
while ( linenum < script.Count )
{
statement = this.script [ linenum++ ] ;
switch ( statement.Command )
{
case Statement.Operation.StartBracket :
{
stack.Push ( new Subroutine
(
statement.Operand1
,
Subroutine.SubroutineType.Bracket
,
linenum
) ) ;
break ;
}
case Statement.Operation.EndBracket :
{
if ( stack.Count == 0 )
{
throw ( new System.InvalidOperationException
(
string.Format ( "{0}:
Extraneous subroutine terminator" , linenum )
) ) ;
}
temp = stack.Pop() ;
if ( temp.Type != Subroutine.SubroutineType.Bracket )
{
throw ( new System.InvalidOperationException
(
string.Format ( "{0}:
Mismatched subroutine delimiters" , linenum )
) ) ;
}
if ( this.subroutines.ContainsKey ( temp.Name ) )
{
throw ( new System.InvalidOperationException
(
string.Format ( "{0}: Duplicate subroutine name" , linenum )
) ) ;
}
temp.End = linenum ;
this.subroutines [ temp.Name ] = temp ;
break ;
}
case Statement.Operation.StartBrace :
{
stack.Push ( new Subroutine
(
statement.Operand1
,
Subroutine.SubroutineType.Brace
,
linenum
) ) ;
break ;
}
case Statement.Operation.EndBrace :
{
if ( stack.Count == 0 )
{
throw ( new System.InvalidOperationException
(
string.Format ( "{0}:
Extraneous subroutine terminator" , linenum )
) ) ;
}
temp = stack.Pop() ;
if ( temp.Type != Subroutine.SubroutineType.Brace )
{
throw ( new System.InvalidOperationException
(
string.Format ( "{0}:
Mismatched subroutine delimiters" , linenum )
) ) ;
}
if ( this.subroutines.ContainsKey ( temp.Name ) )
{
throw ( new System.InvalidOperationException
(
string.Format ( "{0}: Duplicate subroutine name" , linenum )
) ) ;
}
temp.End = linenum ;
this.subroutines [ temp.Name ] = temp ;
break ;
}
}
}
if ( stack.Count > 0 )
{
throw ( new System.InvalidOperationException
(
"The script contains one or more unterminated subroutines"
) ) ;
}
return ;
}
ValidateSubroutineCalls
Once the subroutines are in the Dictionary
, the calls and branches to them can be validated. A subroutine call must specify a valid name. If a branch (? statement) specifies a name, it must be a valid name.
private void
ValidateSubroutineCalls
(
)
{
Statement statement ;
int linenum = 0 ;
while ( linenum < script.Count )
{
statement = this.script [ linenum++ ] ;
switch ( statement.Command )
{
case Statement.Operation.Branch :
{
if ( statement.Operand1.Length > 0 )
{
if ( !this.subroutines.ContainsKey ( statement.Operand1 ) )
{
throw ( new System.InvalidOperationException
(
string.Format
(
"{0}: No subroutine named {1} defined"
,
linenum
,
statement.Operand1
)
) ) ;
}
}
break ;
}
case Statement.Operation.Call :
{
if ( statement.Operand1.Length == 0 )
{
throw ( new System.InvalidOperationException
(
string.Format ( "{0}: No subroutine name specified" , linenum )
) ) ;
}
if ( !this.subroutines.ContainsKey ( statement.Operand1 ) )
{
throw ( new System.InvalidOperationException
(
string.Format
(
"{0}: No subroutine named {1} defined"
,
linenum
,
statement.Operand1
)
) ) ;
}
break ;
}
}
}
return ;
}
CallSubroutine
This method effects a subroutine call; it may be called by a ^ (call) command in the script or by a branch. It stores the current program counter and the states of the sought and branches so they can be restored when the subroutine returns. Then, it sets the program counter to the first line of the subroutine.
private void
CallSubroutine
(
string Name
)
{
this.substack.Push
(
new SubroutineCall
(
this.subroutines [ Name ]
,
this.pc
,
this.sought
,
this.branches
)
) ;
this.pc = this.subroutines [ Name ].Start ;
return ;
}
ProcessResponse
This is the handler for the communicator's OnDataReceived
event. Its purpose is to scan the responses from the host for the text being sought and the various texts that will trigger a branch.
I'm using a string
rather than a StringBuilder
because StringBuilder
doesn't have a built-in ability to search for a string; therefore I would have to use ToString
after each Append
, and that would probably counteract any benefit of using the StringBuilder
. If you don't like it, change it.
When data is received, it is sent to the output stream, then appended to any data still in the buffer. The buffer is then scanned for the text that is being sought and each text that will trigger a branch. By getting the offset of any such text found, and keeping only the smallest such offset, we will know which text appears first in the current response. We can then call a subroutine if a branch was triggered, remove data from the buffer, and signal the engine that it can proceed.
The algorithm could probably be more pro-active in removing data from the buffer and thereby reduce the memory load.
private void
ProcessResponse
(
string Data
)
{
try
{
string branch = System.String.Empty ;
int minoff = -1 ;
lock ( this.outstream )
{
this.outstream.Peek().Write ( Data ) ;
}
this.response += Data ;
if ( this.sought.Length > 0 )
{
minoff = this.response.IndexOf ( this.sought ) ;
}
foreach ( string temp in this.branches.Keys )
{
int offs = this.response.IndexOf ( temp ) ;
if ( offs != -1 )
{
if ( ( minoff == -1 ) || ( minoff > offs ) )
{
minoff = offs ;
branch = temp ;
}
}
}
if ( minoff != -1 )
{
if ( branch.Length > 0 )
{
this.CallSubroutine ( this.branches [ branch ] ) ;
this.response =
this.response.Substring ( minoff + branch.Length ) ;
}
else
{
this.response =
this.response.Substring ( minoff + this.sought.Length ) ;
}
this.found = true ;
}
}
catch ( System.Exception err )
{
this.RaiseExceptionCaught ( err ) ;
}
return ;
}
DoExecuteScript
Here, then, is the script engine. It is, of course, mostly a while
loop with a switch
. The while
loop terminates after a $ or ! command is executed, or if the program counter (pc
) becomes invalid (runs off the end of the script). The process will sleep and loop until ProcessResponse
signals that any of the expected strings has appeared in the output from the host. When signaled to proceed, the engine gets the next Statement
from the script and then the switch
performs the required action. If an Exception is encountered, the program counter is set to its negative, the Exception is written out to the output stream, and the method returns. When the method returns, the program counter is used as the return code.
private int
DoExecuteScript
(
)
{
Statement statement = Statement.Noop ;
this.pc = 0 ;
this.found = true ;
this.sought = System.String.Empty ;
try
{
while
(
( statement.Command != Statement.Operation.Success )
&&
( statement.Command != Statement.Operation.Error )
&&
( this.pc >= 0 )
&&
( this.pc < this.script.Count )
)
{
if ( found )
{
statement = script [ this.pc++ ] ;
lock ( this.outstream )
{
this.outstream.Peek().WriteLine ( statement.ToString() ) ;
}
switch ( statement.Command )
{
case Statement.Operation.Success :
{
this.pc = 0 ;
if ( statement.Operand1.Length > 0 )
{
this.socket.WriteLine ( statement.Operand1 ) ;
System.Threading.Thread.Sleep ( 1000 ) ;
}
break ;
}
case Statement.Operation.Error :
{
if ( statement.Operand1.Length > 0 )
{
int.TryParse ( statement.Operand1 , out this.pc ) ;
}
if ( statement.Operand2.Length > 0 )
{
this.socket.WriteLine ( statement.Operand2 ) ;
System.Threading.Thread.Sleep ( 1000 ) ;
}
break ;
}
case Statement.Operation.Log :
{
lock ( this.outstream )
{
if ( this.outstream.Count > 1 )
{
this.outstream.Pop().Close() ;
}
if ( statement.Operand1.Length > 0 )
{
this.outstream.Push
(
PIEBALD.Lib.LibFil.GetExpandedFileInfo
( statement.Operand1.Trim() ).CreateText()
) ;
}
}
break ;
}
case Statement.Operation.Define :
{
if ( statement.Operand2.Length == 0 )
{
this.knownstrings.Remove ( statement.Operand1 ) ;
}
else
{
this.knownstrings [ statement.Operand1 ] =
System.Web.HttpUtility.HtmlDecode
( statement.Operand2 ) ;
}
break ;
}
case Statement.Operation.Branch :
{
if ( statement.Operand1.Length == 0 )
{
this.branches.Remove ( statement.Operand2 ) ;
}
else
{
this.branches [ statement.Operand2 ] = statement.Operand1 ;
}
break ;
}
case Statement.Operation.Wait :
{
this.sought = statement.Operand1 ;
break ;
}
case Statement.Operation.Connect :
{
this.found =
(
( this.sought.Length == 0 )
&&
( this.branches.Count == 0 )
) ;
try
{
this.socket.Connect ( statement.Operand1 ) ;
}
catch ( System.Exception err )
{
this.ProcessResponse ( err.ToString() ) ;
}
break ;
}
case Statement.Operation.Send :
{
this.found =
(
( this.sought.Length == 0 )
&&
( this.branches.Count == 0 )
) ;
this.socket.WriteLine ( statement.Operand1 ) ;
break ;
}
case Statement.Operation.Press :
{
this.found =
(
( this.sought.Length == 0 )
&&
( this.branches.Count == 0 )
) ;
if ( this.knownstrings.ContainsKey ( statement.Operand1 ) )
{
this.socket.Write
( this.knownstrings [ statement.Operand1 ] ) ;
}
else
{
this.socket.Write ( statement.Operand1 ) ;
}
break ;
}
case Statement.Operation.Call :
{
this.CallSubroutine ( statement.Operand1 ) ;
break ;
}
case Statement.Operation.StartBracket :
case Statement.Operation.StartBrace :
{
this.pc = this.subroutines [ statement.Operand1 ].End ;
break ;
}
case Statement.Operation.EndBracket :
{
this.pc = this.substack.Pop().From ;
break ;
}
case Statement.Operation.EndBrace :
{
SubroutineCall sub = this.substack.Pop() ;
this.pc = sub.From ;
this.sought = sub.Sought ;
this.branches = sub.Branches ;
break ;
}
}
}
else
{
System.Threading.Thread.Sleep ( 100 ) ;
}
}
}
catch ( System.Exception err )
{
this.pc *= -1 ;
lock ( this.outstream )
{
while ( err != null )
{
this.outstream.Peek().WriteLine ( err ) ;
err = err.InnerException ;
}
}
}
finally
{
this.socket.Close() ;
}
return ( this.pc ) ;
}
Using the Code
The included zip file contains all the code described here and several support files, some of which have their own articles or blog entries.
The primary way these classes are intended to be used is to cobble up the script in a string and pass it to the engine.
CommScriptDemo.cs
Here's a very simple example of this; it is basically the same demo program as I used in my TelnetSocket
article, but now it creates and executes a script.
namespace CommScriptDemo
{
public static class CommScriptDemo
{
[System.STAThreadAttribute()]
public static int
Main
(
string[] args
)
{
int result = 0 ;
try
{
if ( args.Length > 3 )
{
using
(
PIEBALD.Types.CommScript engine
=
new PIEBALD.Types.CommScript
(
new PIEBALD.Types.TelnetSocket()
)
)
{
string script = System.String.Format
(
@"
<Username:
@{0}
<Password:
>{1}
<mJB>
>{2}
"
,
args [ 0 ]
,
args [ 1 ]
,
args [ 2 ]
) ;
for ( int i = 3 ; i < args.Length ; i++ )
{
script += System.String.Format
(
@"
>{0}
"
,
args [ i ]
) ;
}
script +=
@"
$logout
" ;
System.Console.WriteLine ( script ) ;
string output ;
result = engine.ExecuteScript
(
script
,
out output
) ;
System.Console.WriteLine ( output ) ;
}
}
else
{
System.Console.WriteLine (
"Syntax: CommScriptDemo address username password command..." ) ;
}
}
catch ( System.Exception err )
{
System.Console.WriteLine ( err ) ;
}
return ( result ) ;
}
}
}
telnets.cs
There is also a demonstration/test application (telnets) and a BAT file for building it so you can test your own scripts (provided you have a telnet server available). You may also want to read Telnets.html, which is about all the documentation for it as I'm likely to write.
Demo.scr
The included Demo.scr (the extension "scr" is arbitrary, you can use anything you want) file is what I have been using to test the script engine as I have been tweaking it while writing this article. I have two AlphaServers (BADGER and WEASEL), the script will connect to BADGER (or WEASEL if it can't connect to BADGER), send the output of SHOW SYSTEM to a log file, and log out.
; Demo.scr -- Demonstrates some features of CommScript
<Username:
^Badger
<Password:
>this is where the username goes
<mJB>
>this is where the password goes
|Demo.log
>show system
|
<logged out
>logout
$
; Try connecting to Badger, on failure, try Weasel
{Badger
?Weasel=A connection attempt failed
@192.168.1.2
}
; Try connecting to Weasel, on failure, abort
{Weasel
?Abort=A connection attempt failed
@192.168.1.4
}
{Abort
!
}
Here's a script that performs a similar task on an SCO Unix system:
; telnets script to log on to XXXXXX and perform ps -a
?No Connection=telnetd:
?Retry=Login incorrect
<login:
@10.1.50.50
<Password:
>this is where the username goes
<TERM
>this is where the password goes
<$
>
>ps -a
$exit
[Retry
?No Connection=Login incorrect
<login:
>
<Password:
>this is where the username goes
<TERM
>this is where the password goes
]
[No Connection
!
]
History
- 2010-03-22: First submitted
- 2010-04-02: Modified
IScriptableCommunicator
due to ProcessCommunicator
-- updated source code in the zip file