scriptty 0.5.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/.gitattributes +1 -0
  2. data/.gitignore +3 -0
  3. data/COPYING +674 -0
  4. data/COPYING.LESSER +165 -0
  5. data/README.rdoc +31 -0
  6. data/Rakefile +49 -0
  7. data/VERSION +1 -0
  8. data/bin/scriptty-capture +5 -0
  9. data/bin/scriptty-dump-screens +4 -0
  10. data/bin/scriptty-replay +5 -0
  11. data/bin/scriptty-term-test +4 -0
  12. data/bin/scriptty-transcript-parse +4 -0
  13. data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
  14. data/examples/captures/xterm-vim-session.bin +262 -0
  15. data/examples/demo-capture.rb +19 -0
  16. data/examples/telnet-nego.rb +55 -0
  17. data/lib/scriptty/apps/capture_app/console.rb +104 -0
  18. data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
  19. data/lib/scriptty/apps/capture_app.rb +213 -0
  20. data/lib/scriptty/apps/dump_screens_app.rb +166 -0
  21. data/lib/scriptty/apps/replay_app.rb +229 -0
  22. data/lib/scriptty/apps/term_test_app.rb +124 -0
  23. data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
  24. data/lib/scriptty/cursor.rb +39 -0
  25. data/lib/scriptty/exception.rb +38 -0
  26. data/lib/scriptty/expect.rb +392 -0
  27. data/lib/scriptty/multiline_buffer.rb +192 -0
  28. data/lib/scriptty/net/event_loop.rb +610 -0
  29. data/lib/scriptty/screen_pattern/generator.rb +398 -0
  30. data/lib/scriptty/screen_pattern/parser.rb +558 -0
  31. data/lib/scriptty/screen_pattern.rb +104 -0
  32. data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
  33. data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
  34. data/lib/scriptty/term/dg410/parser.rb +162 -0
  35. data/lib/scriptty/term/dg410.rb +489 -0
  36. data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
  37. data/lib/scriptty/term/xterm.rb +661 -0
  38. data/lib/scriptty/term.rb +40 -0
  39. data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
  40. data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
  41. data/lib/scriptty/util/fsm.rb +177 -0
  42. data/lib/scriptty/util/transcript/reader.rb +96 -0
  43. data/lib/scriptty/util/transcript/writer.rb +111 -0
  44. data/test/apps/capture_app_test.rb +123 -0
  45. data/test/apps/transcript_parse_app_test.rb +118 -0
  46. data/test/cursor_test.rb +51 -0
  47. data/test/fsm_definition_parser_test.rb +220 -0
  48. data/test/fsm_test.rb +322 -0
  49. data/test/multiline_buffer_test.rb +275 -0
  50. data/test/net/event_loop_test.rb +402 -0
  51. data/test/screen_pattern/generator_test.rb +408 -0
  52. data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
  53. data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
  54. data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
  55. data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
  56. data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
  57. data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
  58. data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
  59. data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
  60. data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
  61. data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
  62. data/test/screen_pattern/parser_test.rb +266 -0
  63. data/test/term/dg410/parser_test.rb +139 -0
  64. data/test/term/xterm_test.rb +327 -0
  65. data/test/test_helper.rb +3 -0
  66. data/test/util/transcript/reader_test.rb +131 -0
  67. data/test/util/transcript/writer_test.rb +126 -0
  68. data/test.watchr +29 -0
  69. 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
+