scriptty 0.5.0-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|