scriptty 0.5.0-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitattributes +1 -0
- data/.gitignore +3 -0
- data/COPYING +674 -0
- data/COPYING.LESSER +165 -0
- data/README.rdoc +31 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/scriptty-capture +5 -0
- data/bin/scriptty-dump-screens +4 -0
- data/bin/scriptty-replay +5 -0
- data/bin/scriptty-term-test +4 -0
- data/bin/scriptty-transcript-parse +4 -0
- data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
- data/examples/captures/xterm-vim-session.bin +262 -0
- data/examples/demo-capture.rb +19 -0
- data/examples/telnet-nego.rb +55 -0
- data/lib/scriptty/apps/capture_app/console.rb +104 -0
- data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
- data/lib/scriptty/apps/capture_app.rb +213 -0
- data/lib/scriptty/apps/dump_screens_app.rb +166 -0
- data/lib/scriptty/apps/replay_app.rb +229 -0
- data/lib/scriptty/apps/term_test_app.rb +124 -0
- data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
- data/lib/scriptty/cursor.rb +39 -0
- data/lib/scriptty/exception.rb +38 -0
- data/lib/scriptty/expect.rb +392 -0
- data/lib/scriptty/multiline_buffer.rb +192 -0
- data/lib/scriptty/net/event_loop.rb +610 -0
- data/lib/scriptty/screen_pattern/generator.rb +398 -0
- data/lib/scriptty/screen_pattern/parser.rb +558 -0
- data/lib/scriptty/screen_pattern.rb +104 -0
- data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
- data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
- data/lib/scriptty/term/dg410/parser.rb +162 -0
- data/lib/scriptty/term/dg410.rb +489 -0
- data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
- data/lib/scriptty/term/xterm.rb +661 -0
- data/lib/scriptty/term.rb +40 -0
- data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
- data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
- data/lib/scriptty/util/fsm.rb +177 -0
- data/lib/scriptty/util/transcript/reader.rb +96 -0
- data/lib/scriptty/util/transcript/writer.rb +111 -0
- data/test/apps/capture_app_test.rb +123 -0
- data/test/apps/transcript_parse_app_test.rb +118 -0
- data/test/cursor_test.rb +51 -0
- data/test/fsm_definition_parser_test.rb +220 -0
- data/test/fsm_test.rb +322 -0
- data/test/multiline_buffer_test.rb +275 -0
- data/test/net/event_loop_test.rb +402 -0
- data/test/screen_pattern/generator_test.rb +408 -0
- data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
- data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
- data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
- data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
- data/test/screen_pattern/parser_test.rb +266 -0
- data/test/term/dg410/parser_test.rb +139 -0
- data/test/term/xterm_test.rb +327 -0
- data/test/test_helper.rb +3 -0
- data/test/util/transcript/reader_test.rb +131 -0
- data/test/util/transcript/writer_test.rb +126 -0
- data/test.watchr +29 -0
- metadata +175 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
# = FSM definition parser
|
2
|
+
# Copyright (C) 2010 Infonium Inc.
|
3
|
+
#
|
4
|
+
# This file is part of ScripTTY.
|
5
|
+
#
|
6
|
+
# ScripTTY is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# ScripTTY is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
17
|
+
# along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
require 'treetop'
|
20
|
+
|
21
|
+
Treetop.load File.join(File.dirname(__FILE__), "scriptty_fsm_definition.treetop")
|
22
|
+
|
23
|
+
module ScripTTY # :nodoc:
|
24
|
+
module Util # :nodoc:
|
25
|
+
class FSM
|
26
|
+
class DefinitionParser
|
27
|
+
# Returns an array of hashes representing the state transition table
|
28
|
+
# for a given FSM definition.
|
29
|
+
#
|
30
|
+
# Each table entry has the following keys:
|
31
|
+
# [:state]
|
32
|
+
# The state to which the entry applies. State 1 is the starting state.
|
33
|
+
# [:input]
|
34
|
+
# The input used to match this entry, or the symbol :any. The :any
|
35
|
+
# input represents any input that does not match a more specific entry.
|
36
|
+
# [:event_name]
|
37
|
+
# The name to be passed to the callback when this entry is reached.
|
38
|
+
# May be nil.
|
39
|
+
# [:next_state]
|
40
|
+
# The next state to move to after the callback is invoked.
|
41
|
+
# Note that the callback might modify this variable.
|
42
|
+
def parse(definition)
|
43
|
+
parser = ScripTTYFSMDefinitionParser.new
|
44
|
+
parse_tree = parser.parse(definition + "\n") # The grammar requires a newline at the end of the file, so make sure it's there.
|
45
|
+
raise ArgumentError.new(parser.failure_reason) unless parse_tree
|
46
|
+
state_transition_table = []
|
47
|
+
load_recursive(state_transition_table, parse_tree, :start, {})
|
48
|
+
normalize_state_transition_table(state_transition_table)
|
49
|
+
state_transition_table
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def load_recursive(ttable, t, state, dupcheck_hash) # :nodoc:
|
55
|
+
t.rules.each do |rule|
|
56
|
+
# Use object_id to identify the state. This will be replaced in
|
57
|
+
# normalize_state_transition_table() by something more readable and
|
58
|
+
# consistent.
|
59
|
+
next_state = rule.sub_list ? rule.sub_list.object_id : :start
|
60
|
+
if rule.lhs.respond_to? :cc_values
|
61
|
+
# Character class (multiple equivalent inputs)
|
62
|
+
rule.lhs.cc_values.each do |value|
|
63
|
+
dup_check(dupcheck_hash, rule, state, value)
|
64
|
+
ttable << {:state => state, :input => value, :next_state => next_state, :event_name => rule.event_name}
|
65
|
+
end
|
66
|
+
else
|
67
|
+
# Single input
|
68
|
+
dup_check(dupcheck_hash, rule, state, rule.lhs.value)
|
69
|
+
ttable << {:state => state, :input => rule.lhs.value, :next_state => next_state, :event_name => rule.event_name}
|
70
|
+
end
|
71
|
+
if next_state != :start
|
72
|
+
load_recursive(ttable, rule.sub_list, next_state, dupcheck_hash)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def normalize_state_transition_table(ttable)
|
79
|
+
states = {}
|
80
|
+
n = 0
|
81
|
+
normalize_state = Proc.new {|s|
|
82
|
+
if states[s]
|
83
|
+
states[s]
|
84
|
+
else
|
85
|
+
states[s] = (n += 1)
|
86
|
+
end
|
87
|
+
}
|
88
|
+
normalize_state.call(:start) # Assign state 1 to the initial state
|
89
|
+
ttable.each do |row|
|
90
|
+
row[:state] = normalize_state.call(row[:state]) if row[:state]
|
91
|
+
row[:next_state] = normalize_state.call(row[:next_state]) if row[:next_state]
|
92
|
+
end
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def dup_check(dupcheck_hash, rule, state, input) # :nodoc:
|
97
|
+
k = [state, input]
|
98
|
+
current_linenum = rule.input[0,rule.lhs.interval.first].split("\n", -1).length
|
99
|
+
current_line = rule.input[rule.interval].chomp
|
100
|
+
prev_linenum, prev_line = dupcheck_hash[k]
|
101
|
+
if prev_linenum
|
102
|
+
# Calculate the line number
|
103
|
+
raise ArgumentError.new("rule conflict\nline #{prev_linenum}: #{prev_line}\nline #{current_linenum}: #{current_line}")
|
104
|
+
end
|
105
|
+
dupcheck_hash[k] = [current_linenum, current_line]
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
# = Treetop grammar for ScripTTY::Util::FSM::DefinitionParser
|
2
|
+
# Copyright (C) 2010 Infonium Inc.
|
3
|
+
#
|
4
|
+
# This file is part of ScripTTY.
|
5
|
+
#
|
6
|
+
# ScripTTY is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# ScripTTY is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
17
|
+
# along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
grammar ScripTTYFSMDefinition
|
20
|
+
rule list
|
21
|
+
( mapping_line / EOL )* {
|
22
|
+
def rules
|
23
|
+
elements.select { |e| e.respond_to? :lhs }
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
rule mapping_line
|
29
|
+
WS* lhs _cn:( WS* "=>" WS* event_name )? _bl:( WS* "=>" WS* braced_list )? EOL {
|
30
|
+
def event_name
|
31
|
+
if _cn.nonterminal?
|
32
|
+
_cn.elements[3].text_value
|
33
|
+
else
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
def sub_list
|
38
|
+
if _bl.nonterminal?
|
39
|
+
_bl.braced_list.list
|
40
|
+
else
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
rule lhs
|
48
|
+
string / char / char_class / other
|
49
|
+
end
|
50
|
+
|
51
|
+
rule event_name
|
52
|
+
[a-z0-9_]+
|
53
|
+
end
|
54
|
+
|
55
|
+
rule braced_list
|
56
|
+
'{' WS* list WS* '}'
|
57
|
+
end
|
58
|
+
|
59
|
+
rule string
|
60
|
+
'"' v:( str_unescaped / str_octal / str_hex / str_single )* '"' {
|
61
|
+
def value
|
62
|
+
v.elements.map{|e| e.to_s}.join
|
63
|
+
end
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
rule char
|
68
|
+
"'" v:( str_unescaped / str_octal / str_hex / str_single ) "'" {
|
69
|
+
def value
|
70
|
+
v.to_s
|
71
|
+
end
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
rule char_class
|
76
|
+
'[' carat:'^'? cc_char_or_range+ ']' {
|
77
|
+
def cc_values
|
78
|
+
retval = []
|
79
|
+
chars_or_ranges = elements[2].elements
|
80
|
+
chars_or_ranges.each do |cr|
|
81
|
+
if cr.last_char
|
82
|
+
fchar = cr.first_char.to_s.unpack("C*")[0]
|
83
|
+
lchar = cr.last_char.to_s.unpack("C*")[0]
|
84
|
+
(fchar..lchar).each do |c|
|
85
|
+
retval << c.chr
|
86
|
+
end
|
87
|
+
else
|
88
|
+
retval << cr.first_char.to_s
|
89
|
+
end
|
90
|
+
end
|
91
|
+
unless carat.empty?
|
92
|
+
# The leading carat is present, so we want all the characters *not* specified.
|
93
|
+
retval = (0..255).map{ |c| c.chr } - retval
|
94
|
+
end
|
95
|
+
retval.uniq
|
96
|
+
end
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
# A single character or range of characters
|
101
|
+
rule cc_char_or_range
|
102
|
+
first_char:cc_char r:( '-' last_char:cc_char )? {
|
103
|
+
def last_char
|
104
|
+
if r.empty?
|
105
|
+
nil
|
106
|
+
else
|
107
|
+
r.last_char
|
108
|
+
end
|
109
|
+
end
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
rule cc_char
|
114
|
+
cc_unescaped / str_octal / str_hex / str_single
|
115
|
+
end
|
116
|
+
|
117
|
+
rule other
|
118
|
+
'*' {
|
119
|
+
def value
|
120
|
+
:other
|
121
|
+
end
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
rule cc_unescaped
|
126
|
+
[^\\\-\^\[\]\t\r\n] {
|
127
|
+
def to_s
|
128
|
+
text_value
|
129
|
+
end
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
rule str_unescaped
|
134
|
+
[^"\\\t\r\n] {
|
135
|
+
def to_s
|
136
|
+
text_value
|
137
|
+
end
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
rule str_octal
|
142
|
+
"\\" [0-7] [0-7] [0-7] {
|
143
|
+
def to_s
|
144
|
+
text_value[1..-1].to_i(8).chr
|
145
|
+
end
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
rule str_hex
|
150
|
+
"\\" [Xx] [0-9A-Fa-f] [0-9A-Fa-f] {
|
151
|
+
def to_s
|
152
|
+
text_value[2..-1].to_i(16).chr
|
153
|
+
end
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
rule str_single
|
158
|
+
"\\" [enrt"'\\] {
|
159
|
+
def to_s
|
160
|
+
case text_value[1..-1]
|
161
|
+
when 'e'
|
162
|
+
"\e"
|
163
|
+
when 'n'
|
164
|
+
"\n"
|
165
|
+
when 'r'
|
166
|
+
"\r"
|
167
|
+
when 't'
|
168
|
+
"\t"
|
169
|
+
when '"'
|
170
|
+
'"'
|
171
|
+
when "'"
|
172
|
+
"'"
|
173
|
+
when "\\"
|
174
|
+
"\\"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
# whitespace
|
181
|
+
rule WS
|
182
|
+
[ \t]
|
183
|
+
end
|
184
|
+
|
185
|
+
# trailing whitespace (optional), comment (optional), newline
|
186
|
+
rule EOL
|
187
|
+
WS* ('#' [^\r\n]* )? ( "\n" / "\r\n" )
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# = Finite state machine for terminal emulation
|
2
|
+
# Copyright (C) 2010 Infonium Inc.
|
3
|
+
#
|
4
|
+
# This file is part of ScripTTY.
|
5
|
+
#
|
6
|
+
# ScripTTY is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# ScripTTY is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
17
|
+
# along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
module ScripTTY # :nodoc:
|
20
|
+
module Util # :nodoc:
|
21
|
+
class FSM
|
22
|
+
# Exception for no matching state
|
23
|
+
class NoMatch < ArgumentError
|
24
|
+
attr_reader :input_sequence, :state
|
25
|
+
def initialize(message, input_sequence, state)
|
26
|
+
@input_sequence = input_sequence
|
27
|
+
@state = state
|
28
|
+
super(message)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# The current (or previous) input.
|
33
|
+
attr_reader :input
|
34
|
+
|
35
|
+
# An array of inputs received since the initial state.
|
36
|
+
#
|
37
|
+
# This allows a callback to get the contents of a complete escape
|
38
|
+
# sequence.
|
39
|
+
attr_reader :input_sequence
|
40
|
+
|
41
|
+
# The current state
|
42
|
+
attr_reader :state
|
43
|
+
|
44
|
+
# The next state
|
45
|
+
attr_reader :next_state
|
46
|
+
|
47
|
+
# Object that will receive named events.
|
48
|
+
#
|
49
|
+
# When processing reaches a named event, the FSM will invoke the method
|
50
|
+
# specified by the callback_method attribute (by default, "call"),
|
51
|
+
# passing it the name of the event and the FSM object.
|
52
|
+
attr_accessor :callback
|
53
|
+
|
54
|
+
# The name of the method to invoke on the callback object. (default: :call)
|
55
|
+
attr_accessor :callback_method
|
56
|
+
|
57
|
+
# When not nil, all inputs are redirected, bypassing normal processing
|
58
|
+
# (but input_sequence is still updated).
|
59
|
+
#
|
60
|
+
# If redirect is a symbol, then the specified method is called on the
|
61
|
+
# callback object (this implies that the callback object can't be an
|
62
|
+
# ordinary Proc in this case). Otherwise, the "call" method on the
|
63
|
+
# redirect object is invoked.
|
64
|
+
#
|
65
|
+
# The redirect function will be passed a reference to the FSM, which it
|
66
|
+
# can use to access methods such as input, input_sequence, reset!, etc.
|
67
|
+
#
|
68
|
+
# If the redirect function returns true, the process method returns
|
69
|
+
# immediately. If the redirect function returns false, the redirection
|
70
|
+
# is removed and the current input is processed normally.
|
71
|
+
attr_accessor :redirect
|
72
|
+
|
73
|
+
# Initialize a FSM
|
74
|
+
#
|
75
|
+
# The following options are supported:
|
76
|
+
# [:definition]
|
77
|
+
# FSM definition, as a string
|
78
|
+
# [:callback]
|
79
|
+
# See the documentation for the callback attribute.
|
80
|
+
# A block may be given to the new method instead of being passed as an
|
81
|
+
# option.
|
82
|
+
# [:callback_method]
|
83
|
+
# See the documentation for the callback_method attribute.
|
84
|
+
def initialize(options={}, &block)
|
85
|
+
@redirect = nil
|
86
|
+
@input_sequence = []
|
87
|
+
@callback = options[:callback] || block
|
88
|
+
@callback_method = (options[:callback_method] || :call).to_sym
|
89
|
+
load_definition(options[:definition])
|
90
|
+
reset!
|
91
|
+
end
|
92
|
+
|
93
|
+
# Set state and next_state to 1, and clear the redirect.
|
94
|
+
def reset!
|
95
|
+
@state = 1
|
96
|
+
@next_state = 1
|
97
|
+
@redirect = nil
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
# Return true if we are at the initial state (i.e. @state == 1 and !@redirect)
|
102
|
+
def initial_state?
|
103
|
+
@state == 1 && !@redirect
|
104
|
+
end
|
105
|
+
|
106
|
+
# Invoke the callback for the specified event.
|
107
|
+
def fire_event(event)
|
108
|
+
@callback.__send__(@callback_method, event.to_sym, self) if @callback
|
109
|
+
end
|
110
|
+
|
111
|
+
# Process the specified input.
|
112
|
+
#
|
113
|
+
# If there is no matching entry in the state transition table,
|
114
|
+
# ScripTTY::Util::FSM::NoMatch is raised.
|
115
|
+
def process(input)
|
116
|
+
# Switch to @next_state
|
117
|
+
@state = @next_state
|
118
|
+
|
119
|
+
# Reset @input_sequence if we are at the initial state. Otherwise,
|
120
|
+
# append the current input to @input_sequence.
|
121
|
+
if initial_state?
|
122
|
+
@input_sequence = [input]
|
123
|
+
else
|
124
|
+
@input_sequence << input
|
125
|
+
end
|
126
|
+
|
127
|
+
# Set @input and call the redirect object (if necessary)
|
128
|
+
@input = input
|
129
|
+
if @redirect
|
130
|
+
if @redirect.is_a?(Symbol)
|
131
|
+
result = @callback.send(@redirect, self)
|
132
|
+
else
|
133
|
+
result = @redirect.call(self)
|
134
|
+
end
|
135
|
+
return true if result
|
136
|
+
@redirect = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
# The redirect function might invoke the reset! method, so fix
|
140
|
+
# @input_sequence for that case.
|
141
|
+
@input_sequence = [input] if @state == 1
|
142
|
+
|
143
|
+
# Look up for a state transition for the specified input
|
144
|
+
t = @state_transitions[@state][input]
|
145
|
+
t ||= @state_transitions[@state][:other]
|
146
|
+
raise NoMatch.new("No matching transition for input_sequence=#{input_sequence.inspect} (state=#{state.inspect})", input_sequence, state) unless t
|
147
|
+
|
148
|
+
# Set next_state and invoke the callback, if any is specified for this state transition.
|
149
|
+
@next_state = t[:next_state]
|
150
|
+
fire_event(t[:event]) if t[:event]
|
151
|
+
|
152
|
+
# Return true
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
# Load the specified FSM definition
|
159
|
+
def load_definition(definition)
|
160
|
+
# NB: We convert the specified state transition table into nested hashes (for faster lookups).
|
161
|
+
transitions = {}
|
162
|
+
DefinitionParser.new.parse(definition).each do |e|
|
163
|
+
state = e.delete(:state)
|
164
|
+
input = e.delete(:input)
|
165
|
+
raise "BUG" if !state or !input
|
166
|
+
e[:event] = e.delete(:event_name).to_sym if e[:event_name] # Replace string event_name with symbol
|
167
|
+
transitions[e[:next_state]] ||= {} if e[:next_state]
|
168
|
+
transitions[state] ||= {}
|
169
|
+
transitions[state][input] = e
|
170
|
+
end
|
171
|
+
@state_transitions = transitions
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
require 'scriptty/util/fsm/definition_parser'
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# = Transcript reader
|
2
|
+
# Copyright (C) 2010 Infonium Inc.
|
3
|
+
#
|
4
|
+
# This file is part of ScripTTY.
|
5
|
+
#
|
6
|
+
# ScripTTY is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# ScripTTY is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
17
|
+
# along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
require 'strscan'
|
20
|
+
|
21
|
+
module ScripTTY
|
22
|
+
module Util
|
23
|
+
module Transcript
|
24
|
+
# Reader for transcript files
|
25
|
+
#
|
26
|
+
# === Example
|
27
|
+
#
|
28
|
+
# File.open("transcript", "r") do |file|
|
29
|
+
# reader = ScripTTY::Util::Transcript::Reader.new
|
30
|
+
# file.each_line do |line|
|
31
|
+
# timestamp, type, args = reader.parse_line(line)
|
32
|
+
# # ... do stuff here ...
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
class Reader
|
37
|
+
|
38
|
+
OP_TYPES = {
|
39
|
+
"Copen" => :client_open, # client connection opened
|
40
|
+
"Sopen" => :server_open, # server connection opened
|
41
|
+
"C" => :from_client, # bytes from client
|
42
|
+
"S" => :from_server, # bytes from server
|
43
|
+
"*" => :info, # informational message
|
44
|
+
"Sx" => :server_close, # server closed connection
|
45
|
+
"Cx" => :client_close, # server closed connection
|
46
|
+
"Sp" => :server_parsed, # parsed escape sequence from server
|
47
|
+
"Cp" => :client_parsed, # parsed escape sequence from client
|
48
|
+
}
|
49
|
+
|
50
|
+
def initialize(io=nil)
|
51
|
+
@current_line = 0
|
52
|
+
@io = io
|
53
|
+
end
|
54
|
+
|
55
|
+
def next_entry
|
56
|
+
raise TypeError.new("no I/O object associated with this reader") unless @io
|
57
|
+
return nil if @io.eof?
|
58
|
+
line = @io.readline
|
59
|
+
return nil unless line
|
60
|
+
parse_line(line)
|
61
|
+
end
|
62
|
+
|
63
|
+
def close
|
64
|
+
@io.close if @io
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_line(line)
|
68
|
+
@current_line += 1
|
69
|
+
unless line =~ /^\[([\d]+(?:\.[\d]+)?)\] (\S+)((?: "(?:[\x20-\x21\x23-\x5b\x5d-\x7e]|\\[0-3][0-7][0-7])*")*)$/
|
70
|
+
raise ArgumentError.new("line #{@current_line}: Unable to parse basic structure")
|
71
|
+
end
|
72
|
+
timestamp, op, raw_args = [$1, $2, $3]
|
73
|
+
timestamp = timestamp.to_f
|
74
|
+
args = []
|
75
|
+
s = StringScanner.new(raw_args.strip)
|
76
|
+
until s.eos?
|
77
|
+
m = s.scan(/ +/) # skip whitespace between args
|
78
|
+
next if m
|
79
|
+
m = s.scan /"[^"]*"/
|
80
|
+
raise ArgumentError.new("line #{@current_line}: Unable to parse arguments") unless m
|
81
|
+
arg = m[1..-2].gsub(/\\[0-7][0-7][0-7]/) { |m| [m[1..-1].to_i(8)].pack("C*") } # strip quotes and unescape string
|
82
|
+
args << arg
|
83
|
+
end
|
84
|
+
type = OP_TYPES[op]
|
85
|
+
raise ArgumentError.new("line #{@current_line}: Unrecognized opcode #{op}") unless type
|
86
|
+
if [:client_open, :server_open].include?(type)
|
87
|
+
raise ArgumentError.new("line #{@current_line}: Bad port #{args[1].inspect}") unless args[1] =~ /\A(\d+)\Z/m
|
88
|
+
args[1] = args[1].to_i
|
89
|
+
end
|
90
|
+
[timestamp, type, args]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# = Transcript writer
|
2
|
+
# Copyright (C) 2010 Infonium Inc.
|
3
|
+
#
|
4
|
+
# This file is part of ScripTTY.
|
5
|
+
#
|
6
|
+
# ScripTTY is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# ScripTTY is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
17
|
+
# along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
module ScripTTY
|
20
|
+
module Util
|
21
|
+
module Transcript
|
22
|
+
class Writer
|
23
|
+
|
24
|
+
# Set this to non-nil to force the next record to have a specific timestamp
|
25
|
+
attr_accessor :override_timestamp
|
26
|
+
|
27
|
+
def initialize(io)
|
28
|
+
@io = io
|
29
|
+
@start_time = Time.now
|
30
|
+
@override_timestamp = nil
|
31
|
+
if block_given?
|
32
|
+
begin
|
33
|
+
yield self
|
34
|
+
ensure
|
35
|
+
close
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def close
|
41
|
+
@io.close
|
42
|
+
end
|
43
|
+
|
44
|
+
# Client connection opened
|
45
|
+
def client_open(host, port)
|
46
|
+
write_event("Copen", host, port.to_s)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Server connection opened
|
50
|
+
def server_open(host, port)
|
51
|
+
write_event("Sopen", host, port.to_s)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Log bytes from the client
|
55
|
+
def from_client(bytes)
|
56
|
+
write_event("C", bytes)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Log bytes from the server
|
60
|
+
def from_server(bytes)
|
61
|
+
write_event("S", bytes)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Log event from the client (i.e. bytes parsed into an escape sequence, with an event fired)
|
65
|
+
def client_parsed(event, bytes)
|
66
|
+
write_event("Cp", event.to_s, bytes)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Log event from the server (i.e. bytes parsed into an escape sequence, with an event fired)
|
70
|
+
def server_parsed(event, bytes)
|
71
|
+
write_event("Sp", event.to_s, bytes)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Log server connection close
|
75
|
+
def server_close(message)
|
76
|
+
write_event("Sx", message)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Log client connection close
|
80
|
+
def client_close(message)
|
81
|
+
write_event("Cx", message)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Log informational message
|
85
|
+
def info(*args)
|
86
|
+
write_event("*", *args)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def write_event(type, *args)
|
92
|
+
t = @override_timestamp ? @override_timestamp.to_f : (Time.now - @start_time)
|
93
|
+
encoded_args = args.map{|a| encode_string(a)}.join(" ")
|
94
|
+
@io.write sprintf("[%.03f] %s %s", t, type, encoded_args) + "\n"
|
95
|
+
@io.flush if @io.respond_to?(:flush)
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def encode_string(bytes)
|
100
|
+
escaped = bytes.gsub(/\\|"|[^\x20-\x7e]*/mn) { |m|
|
101
|
+
m.unpack("C*").map{ |c|
|
102
|
+
sprintf("\\%03o", c)
|
103
|
+
}.join
|
104
|
+
}
|
105
|
+
'"' + escaped + '"'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|