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.
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
+