65.9K
CodeProject 正在变化。 阅读更多。
Home

CommScript

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.42/5 (7投票s)

2010 年 3 月 23 日

CPOL

12分钟阅读

viewsIcon

25827

downloadIcon

225

一个简单的脚本引擎,用于自动化通信(例如 Telnet)

引言

本文介绍我的 CommScript 类,这是一个用于自动化通信(例如 Telnet)的简单脚本引擎。

这里展示的是多年演进的结果;最初,套接字和脚本引擎组合在一个类中,但我后来决定该引擎可以用于在其他类型的通信链接上执行脚本,因此我将它们分开。

背景

在我上一份工作中,我必须完成的一项最令人烦恼,但又最具挑战性和最终最有益的事情是自动化 Telnet 连接到 Unix 系统、运行第三方应用程序、导航基于字符的菜单/表单系统、输入数据以及响应反馈——并且还能干净地退出。很快就显而易见,我需要让我的代码编写一个脚本,然后让某个东西执行该脚本。出于各种原因,我决定编写自己的脚本语言和解释器。

在我设计和实现这个类的首要关注点是能够自动化与 Unix 系统上的第三方应用程序的交互。脚本是在 Windows 服务中生成和执行的——不需要用户界面,因此也没有考虑被交互式进程使用。同样,尽管脚本可以存储在文件中然后被读取和传递,但主要目的是在运行时直接在字符串中生成脚本。

IScriptableCommunicator

CommScript 类需要一个实现了 IScriptableCommunicator 接口的类的实例

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 ;
}

有了实现此接口的类的实例,您的代码可以

  • 设置超时(如果超时,连接将中止)
  • 指定用于在 byte[]string 之间转换的编码
  • 连接到 Telnet 服务器
  • 向服务器发送数据
  • 接收返回的数据
  • 接收发送数据时发生的异常

脚本语言

我在此处呈现的(未命名)脚本语言是从多年前我遇到的一种非常简单的脚本语言演变而来的。当时,我的一位同事正在使用一个名为“fta”的实用程序配置终端服务器(据我回忆);我不知道它的起源,也找不到相关信息,我假设它来自 Unix,尽管我们使用的是 OpenVMS。该实用程序执行脚本,脚本有两种类型的行,可以认为是“发送”和“等待”;“等待”命令以句点(.)开头,“发送”行是其他所有内容。疯狂的是,“等待”必须在“发送”命令之前,但会一直生效,直到遇到另一个“等待”。

当我决定需要创建和实现我自己的脚本语言时,我将那些fta脚本视为一个合理的起点。我认识到,面向行并且“等待”先于“发送”的做法,尽管笨拙,但允许非常简单的解析和执行。但我知道我需要的不只是两种命令类型,这意味着我需要一个实际的“发送”命令。在浏览了我旧的《编程语言原理》[^](第二版,1987 年)副本后,我确定fta所缺乏的是“规则性”(我们不都这样吗?)和“一致性”。然而,我决定继续采用面向行的方式,使用单字符符号命令(嘿!这是一种非英语的计算机语言!)。好了,如果这让你不舒服,你可以停止阅读(“这让我毛骨悚然。”——Slartibartfast)。

因此,我需要为“发送”和“等待”找到单字符符号;它们需要彼此相似,暗示其用途,并且令人难忘。我选择了“>”(大于号)作为发送,而“<”(小于号)作为等待(替换了句点)。对于连接命令,“@”(at)似乎很合适。注释可能很重要;过去,我曾用“;”(分号)表示注释,这次我再次这样做了。我需要一个退出命令,所以我选择了“$”(美元符号)来表示脚本成功完成(“钞票”)。

这些足以开始,但很快我就了解到我需要一定的灵活性来处理异常。因此,我添加了子程序,以及一种处理程序或“出错时转到”或“看到这个,调用那个”的功能。分支命令——“?”(问号)——为一种情况映射了一个处理程序。您也可以使用“^”(插入符)命令直接调用子程序。有两种类型的子程序:[] 类型子程序和 {} 类型子程序,我总是记不清哪个是哪个。区别在于,对于 [] 类型子程序,任何“等待”和“出错时转到”的设置在子程序返回后仍然有效;对于 {} 类型子程序,调用子程序之前生效的“等待”和“出错时转到”的设置将在子程序返回时恢复。(允许在子程序内终止脚本。)

与第三方基于字符的菜单/表单应用程序交互时,有时“发送”(会添加回车符)并不是正确的工具。因此,我添加了按下命令——“#”(井号,磅)——该命令旨在发送按键(不带回车符)。最常见的用法是定义一个按键,然后使用按下 (#) 命令发送它。定义命令——“%”(百分号)——用于定义一个“已知字符串”(通常是一个按键或其他简短序列)。也可以将已知字符串提供给 CommScript 构造函数,这允许在多个 CommScript 实例之间共享字符串。

随着我的脚本变得更加复杂,我意识到成功 ($) 命令不够灵活;它只返回零 (0) 表示成功;我需要返回其他可能表示部分成功,或者至少不是完全失败的值。结果是错误命令——“!”(感叹号)——这几乎让成功 ($) 命令过时了。

杂项命令

  • 包含存储在其他文件中的代码似乎也是个好主意,所以我添加了 Include 命令——“&”(和号)。
  • 而且,在某些情况下,能够将输出存储(记录)到文件中会很有用,所以我添加了 Log 命令——“|”(竖线,管道)——也是如此。

许多命令还使用“=”(等号)来分隔命令的操作数。zip 文件中包含的 telnets.html 文件提供了有关脚本语言的更多信息。

还在看吗?

我敢肯定,很多读者早就跳到底部给语言投 1 票了;感谢您坚持下来。然而,正如我所说,语言一直在发展。因为我现在使用正则表达式和 EnumTransmogrifier[^] 来分割和解析命令,您现在可以使用命令名称(不区分大小写)而不是符号,或者将它们混合使用。如果命令有操作数,命令名称后面必须有一个空格字符。

操作 (Operation)

Operation 枚举包含命令的名称和符号。

[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 类包含一个操作及其操作数。有些人可能会建议为每种命令类型创建一个 Statement 的子类,但我认为这样做不会带来任何好处。

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 方法分割和解析一行,然后实例化一个 Statement。空行会产生 Noop。如果一行无法成功解析,将抛出异常。

正则表达式和枚举必须保持同步。

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 )
    {
        /* This will throw System.ArgumentException if a valid command is not found */
        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 类保存子程序的名称、类型、起始行号和结束行号。返回子程序的唯一方法是到达结束行号。

private class Subroutine
{
    public enum SubroutineType
    {
        Bracket
    ,
        Brace
    }
 
    public readonly string         Name  ;
    public readonly SubroutineType Type  ;
    public readonly int            Start ;
 
    /* End can't be readonly, it must be set (once) after construction */
    private int                    end   ;
 
    public Subroutine
    (
        string         Name
    ,
        SubroutineType Type
    ,
        int            Start
    )
    {
        this.Name  = Name  ;
        this.Type  = Type  ;
        this.Start = Start ;
        this.end   = 0     ;
 
        return ;
    }
 
    /* End can be set to a non-zero value only once */
    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

SubroutineCall 类保存对 Subroutine 的引用以及调用时的引擎状态。当子程序返回时,必须恢复程序计数器。如果子程序是 Bracket 类型子程序,则查找和分支的状态也会被恢复。

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     ;
                
        /* Clone the branches collection */
        this.Branches   = new System.Collections.Generic.Dictionary<string,string> 
            ( 
                Branches 
            , 
                Branches.Comparer 
            ) ;
 
        return ;
    }
 
    public override string
    ToString
    (
    )
    {
        return ( this.Subroutine.ToString() ) ;
    }
}

CommScript

这就引出了 CommScript 类。必须将实现 IScriptableCommunicator 的类传递给构造函数。也可以传递已知字符串的集合,这允许应用程序提前定义已知字符串;我在生产环境中使用此功能作为性能提升。

其他字段用于保存脚本、SubroutineDictionary 以及引擎的状态。

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

实例化后,您对 CommScript 唯一能做的就是执行脚本。我使用 CommScript 的主要方式是将其脚本组合成一个字符串并传递给它。您也可以执行存储在文件或通过管道传入实用程序中的脚本。主机的数据将返回到 Outstream 参数。返回代码由脚本引擎确定。

  • 如果执行了 $ 语句,返回值为零 (0)。
  • 如果执行了 ! 语句,返回值是行号或 ! 语句提供的值。
  • 如果在脚本中发生错误,返回值将是遇到错误的行号的负值。
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

此方法设置 CommScript 的字段以保存脚本,加载脚本,查找并验证所有子程序,然后激活引擎。

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

显然,LoadScript 将脚本加载到内存中。每一行都被解析为 Statement 实例,然后进行一些特殊语句的处理。

  • Includes 会导致指定的文件被读入并替换该语句。
  • Subroutines 也可能指示应该读入一个文件。
  • Defines 会被检查以确保至少有一个名称。
  • 所有其他行都按原样添加到脚本中。
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

文件的包含是递归发生的——所以要小心。

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

脚本中的任何子程序都必须添加到 Subroutines Dictionary 中。此方法迭代脚本的语句,查找括号和花括号语句。当检测到子程序开始时,会创建一个 Subroutine 实例并将其推送到堆栈上。当检测到子程序结束时,会从堆栈中弹出一个 Subroutine 实例,进行验证,并添加到 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

一旦子程序进入 Dictionary,就可以验证对它们的调用和分支。子程序调用必须指定一个有效的名称。如果分支(? 语句)指定了一个名称,则该名称必须是有效的。

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

此方法执行子程序调用;它可以由脚本中的 ^(调用)命令调用,也可以由分支调用。它存储当前的程序计数器以及查找和分支的状态,以便在子程序返回时恢复。然后,它将程序计数器设置为子程序的开始行。

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

这是通信器 OnDataReceived 事件的处理程序。其目的是扫描主机响应中的正在查找的文本以及将触发分支的各种文本。

我使用的是 string 而不是 StringBuilder,因为 StringBuilder 没有内置的搜索字符串功能;因此,我必须在每次 Append 后使用 ToString,这可能会抵消使用 StringBuilder 的任何好处。如果您不喜欢,请自行更改。

当数据收到时,它会被发送到输出流,然后追加到缓冲区中剩余的任何数据。然后,缓冲区会被扫描以查找正在查找的文本以及将触发分支的每种文本。通过获取任何此类文本的偏移量,并仅保留最小的偏移量,我们将知道哪种文本首先出现在当前响应中。然后,如果触发了分支,我们可以调用一个子程序,从缓冲区中移除数据,并向引擎发出信号,使其可以继续。

该算法可能可以更主动地从缓冲区中移除数据,从而减少内存负载。

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

因此,这里是脚本引擎。它当然主要是由一个 while 循环和一个 switch 组成。在执行 $ 或 ! 命令后,或者如果程序计数器 (pc) 无效(超出脚本范围)后,while 循环终止。该进程将休眠并循环,直到 ProcessResponse 发出信号表明主机输出中出现了任何预期的字符串。当收到继续的信号时,引擎从脚本中获取下一个 Statement,然后 switch 执行所需的动作。如果遇到异常,程序计数器将设置为其负值,异常将写入输出流,方法返回。当方法返回时,程序计数器被用作返回代码。

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++ ] ; /* Note the post-increment */
 
                lock ( this.outstream )
                {
                    this.outstream.Peek().WriteLine ( statement.ToString() ) ;
                }
 
                switch ( statement.Command )
                {
                    /* On success; set the PC to zero                        */
                    /* If there is text to send, send it and wait one second */
                    case Statement.Operation.Success :
                    {
                        this.pc = 0 ;
 
                        if ( statement.Operand1.Length > 0 )
                        {
                            this.socket.WriteLine ( statement.Operand1 ) ;
 
                            System.Threading.Thread.Sleep ( 1000 ) ;
                        }
 
                        break ;
                    }
 
                    /* On error; if a value was specified, set the PC to it  */
                    /* If there is text to send, send it and wait one second */
                    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 ;
                    }
 
                    /* The Log command will close the current output stream  */
                    /* (except the first). And, if a path is specified,      */
                    /* will open a new output stream                         */
                    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 ;
                    }
 
                    /* The Define command will associate a name with a string */
                    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 ;
                    }
 
                    /* The Branch command adds, redefines, or 
			removes a branch definition */
                    case Statement.Operation.Branch :
                    {
                        if ( statement.Operand1.Length == 0 )
                        {
                            this.branches.Remove ( statement.Operand2 ) ;
                        }
                        else
                        {
                            this.branches [ statement.Operand2 ] = statement.Operand1 ;
                        }
 
                        break ;
                    }
 
                    /* The Wait command sets the text that ProcessResponse will seek */
                    /* in the data from the host                                     */
                    case Statement.Operation.Wait :
                    {
                        this.sought = statement.Operand1 ;
 
                        break ;
                    }
 
                    /* The Connect command attempts to connect to the specified host */
                    /* any Exceptions thrown by the connect will be treated as data  */
                    /* so that retries may be performed                              */
                    /* The process should then await the signal from ProcessResponse */
                    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 ;
                    }
 
                    /* The Send command sends the specified text                      */
                    /* (and a carriage-return) to the host                            */
                    /* The process should then await the signal from ProcessResponse  */
                    case Statement.Operation.Send :
                    {
                        this.found =
                        (
                            ( this.sought.Length == 0 )
                        &&
                            ( this.branches.Count == 0 )
                        ) ;
 
                        this.socket.WriteLine ( statement.Operand1 ) ;
 
                        break ;
                    }
 
                    /* The Press command sends the named or specified text to the host */
                    /* (with no carriage-return)                                       */
                    /* The process should then await the signal from ProcessResponse   */
                    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 ;
                    }
 
                    /* The Call command calls a subroutine */
                    case Statement.Operation.Call :
                    {
                        this.CallSubroutine ( statement.Operand1 ) ;
 
                        break ;
                    }
 
                    /* If the execution of the script reaches a subroutine       */
                    /* start command, it will not be executed,                   */
                    /* processing will continue after the end of the subroutine  */
                    case Statement.Operation.StartBracket :
                    case Statement.Operation.StartBrace :
                    {
                        this.pc = this.subroutines [ statement.Operand1 ].End ;
 
                        break ;
                    }
 
                    /* Upon return; bracket-type subroutines restore              */
                    /*  the program counter but not the sought and branch values  */
                    case Statement.Operation.EndBracket :
                    {
                        this.pc = this.substack.Pop().From ;
 
                        break ;
                    }
 
                    /* Upon return; brace-type subroutines restore the program counter */
                    /* as well as the sought and branch values                         */
                    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

随附的 zip 文件包含此处描述的所有代码和几个支持文件,其中一些文件有自己的文章或博客条目。

这些类的主要使用方式是将脚本组合成一个字符串并将其传递给引擎。

CommScriptDemo.cs

这是一个非常简单的例子;它基本上是我在 TelnetSocket 文章中使用的演示程序,但现在它创建并执行了一个脚本。

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

还有一个演示/测试应用程序 (telnets) 和一个用于构建它的 BAT 文件,以便您可以测试自己的脚本(前提是您有 telnet 服务器可用)。您可能还想阅读 Telnets.html,这是我可能为它编写的所有文档。

Demo.scr

随附的 Demo.scr(扩展名“scr”是任意的,您可以使用任何您想要的扩展名)文件是我在编写本文时测试脚本引擎时使用的。我有两台 AlphaServers(BADGER 和 WEASEL),脚本将连接到 BADGER(如果无法连接到 BADGER,则连接到 WEASEL),将 SHOW SYSTEM 的输出发送到日志文件,然后注销。

; 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
!
}

这是一个在 SCO Unix 系统上执行类似任务的脚本

; 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
!
]

历史

  • 2010-03-22:首次提交
  • 2010-04-02:由于 ProcessCommunicator 修改了 IScriptableCommunicator——更新了 zip 文件中的源代码
© . All rights reserved.