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,55 @@
1
+ # Telnet negotiation example
2
+ #
3
+ # NOTE: This would cause an infinite loop if two ends of the connection ran
4
+ # this algorithm, but it appears to be safe on the client-side only with a
5
+ # smarter server.
6
+
7
+ # Options we WILL (in response to DO)
8
+ supported_will_options = [
9
+ "\000", # Binary Transmission
10
+ "\003", # Suppress Go Ahead
11
+ ]
12
+
13
+ # Options we DO (in response to WILL)
14
+ supported_do_options = [
15
+ "\000", # Binary Transmission
16
+ "\001", # Echo
17
+ "\003", # Suppress Go Ahead
18
+ ]
19
+
20
+ # Initial send
21
+ supported_do_options.each do |opt|
22
+ send "\377\375#{opt}" # IAC DO <option>
23
+ end
24
+ supported_will_options.each do |opt|
25
+ send "\377\373#{opt}" # IAC WILL <option>
26
+ end
27
+
28
+ done_telnet_nego = false
29
+ until done_telnet_nego
30
+ expect {
31
+ on(/\377([\373\374\375\376])(.)/n) { |m| # IAC WILL/WONT/DO/DONT <option>
32
+ cmd = {"\373" => :will, "\374" => :wont, "\375" => :do, "\376" => :dont}[m[1]]
33
+ opt = m[2]
34
+ puts "IAC #{cmd} #{opt.inspect}"
35
+ if cmd == :do
36
+ if supported_will_options.include?(opt)
37
+ send "\377\373#{opt}" # IAC WILL <option>
38
+ else
39
+ send "\377\374#{opt}" # IAC WONT <option>
40
+ end
41
+ elsif cmd == :will
42
+ if supported_do_options.include?(opt)
43
+ send "\377\375#{opt}" # IAC DO <option>
44
+ else
45
+ send "\377\376#{opt}" # IAC DONT <option>
46
+ end
47
+ elsif cmd == :dont
48
+ send "\377\374#{opt}" # IAC WONT <option>
49
+ elsif cmd == :wont
50
+ send "\377\376#{opt}" # IAC DONT <option>
51
+ end
52
+ }
53
+ on(/[^\377]/n) { puts "DONE"; done_telnet_nego = true } # We're done TELNET negotiation when we receive something that's not IAC
54
+ }
55
+ end
@@ -0,0 +1,104 @@
1
+ require 'scriptty/apps/capture_app'
2
+
3
+ module ScripTTY
4
+ module Apps
5
+ class CaptureApp # reopen
6
+ class Console
7
+ IAC_WILL_ECHO = "\377\373\001"
8
+ IAC_WONT_ECHO = "\377\374\001"
9
+ IAC_DO_ECHO = "\377\375\001"
10
+ IAC_DONT_ECHO = "\377\376\001"
11
+
12
+ IAC_WILL_SUPPRESS_GA = "\377\373\003"
13
+ IAC_WONT_SUPPRESS_GA = "\377\374\003"
14
+ IAC_DO_SUPPRESS_GA = "\377\375\003"
15
+ IAC_DONT_SUPPRESS_GA = "\377\376\003"
16
+
17
+ def initialize(conn, app)
18
+ @conn = conn
19
+ @app = app
20
+ conn.on_receive_bytes { |bytes| handle_receive_bytes(bytes) }
21
+ conn.on_close { |bytes| handle_close }
22
+ conn.write(IAC_WILL_ECHO + IAC_DONT_ECHO) # turn echoing off
23
+ conn.write(IAC_WILL_SUPPRESS_GA + IAC_DO_SUPPRESS_GA) # turn line-buffering off (RFC 858)
24
+ conn.write("\ec") # reset terminal (clear screen; reset attributes)
25
+ @refresh_in_progress = false
26
+ @need_another_refresh = false
27
+ @prompt_input = ""
28
+ end
29
+
30
+ def refresh!
31
+ if @refresh_in_progress
32
+ @need_another_refresh = true
33
+ return
34
+ end
35
+ screen_lines = []
36
+ screen_lines << "# #{@prompt_input}" # prompt
37
+ if @app.term
38
+ term_width = @app.term.width
39
+ screen_lines << "Cursor position: #{@app.term.cursor_pos.inspect}"
40
+ screen_lines << "+" + "-"*term_width + "+"
41
+ @app.term.text.each do |line|
42
+ screen_lines << "|#{line}|"
43
+ end
44
+ screen_lines << "+" + "-"*term_width + "+"
45
+ else
46
+ term_width = 80
47
+ screen_lines << "[ No terminal ]"
48
+ end
49
+ if @app.respond_to?(:log_messages)
50
+ screen_lines << ""
51
+ @app.log_messages.each do |line|
52
+ if line.length > term_width
53
+ line = line[0,term_width-1]
54
+ line += ">"
55
+ end
56
+ screen_lines << ":#{line}"
57
+ end
58
+ end
59
+ output = []
60
+ output << "\e[H"
61
+ output << screen_lines.map{|line| line + "\e[K" + "\r\n"}.join # erase to end of line after each line
62
+ output << "\e[;#{3+@prompt_input.length}H" # return to prompt
63
+ @refresh_in_progress = true
64
+ @conn.write(output.join) {
65
+ @refresh_in_progress = false
66
+ if @need_another_refresh
67
+ @need_another_refresh = false
68
+ refresh!
69
+ end
70
+ }
71
+ nil
72
+ end
73
+
74
+ private
75
+
76
+ def handle_receive_bytes(bytes)
77
+ bytes.split("").each do |byte|
78
+ if byte =~ /\A[\x20-\x7e]\Z/m # printable
79
+ @prompt_input << byte
80
+ elsif byte == "\b" or byte == "\x7f" # backspace or DEL
81
+ @prompt_input = @prompt_input[0..-2] || ""
82
+ elsif byte == "\r"
83
+ handle_command_entered(@prompt_input)
84
+ @prompt_input = ""
85
+ elsif byte == "\n"
86
+ # ignore
87
+ else
88
+ @conn.write("\077") # beep
89
+ end
90
+ end
91
+ refresh!
92
+ end
93
+
94
+ def handle_command_entered(cmd)
95
+ @app.handle_console_command_entered(cmd) if @app.respond_to?(:handle_console_command_entered)
96
+ end
97
+
98
+ def handle_close
99
+ @app.detach_console(self)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,65 @@
1
+ require 'scriptty/apps/capture_app'
2
+
3
+ module ScripTTY
4
+ module Apps
5
+ class CaptureApp # reopen
6
+ class PasswordPrompt
7
+ IAC_WILL_ECHO = "\377\373\001"
8
+ IAC_WONT_ECHO = "\377\374\001"
9
+ IAC_DO_ECHO = "\377\375\001"
10
+ IAC_DONT_ECHO = "\377\376\001"
11
+
12
+ def initialize(conn, prompt="Password: ")
13
+ @conn = conn
14
+ @conn.write(IAC_WILL_ECHO + IAC_DONT_ECHO) if prompt # echo off
15
+ @conn.write(prompt) if prompt
16
+ @conn.on_receive_bytes { |bytes|
17
+ bytes.split("").each { |byte|
18
+ handle_received_byte(byte) unless @done # XXX - This doesn't work well with pipelining; we throw away bytes after the prompt is finished.
19
+ }
20
+ }
21
+ @password_buffer = ""
22
+ @done = false
23
+ end
24
+
25
+ def authenticate(&block)
26
+ raise ArgumentError.new("no block given") unless block
27
+ @authenticate_proc = block
28
+ nil
29
+ end
30
+
31
+ def on_fail(&block)
32
+ raise ArgumentError.new("no block given") unless block
33
+ @fail_proc = block
34
+ nil
35
+ end
36
+
37
+ def on_success(&block)
38
+ raise ArgumentError.new("no block given") unless block
39
+ @success_proc = block
40
+ nil
41
+ end
42
+
43
+ private
44
+ def handle_received_byte(byte)
45
+ @password_buffer << byte
46
+ if byte == "\r" or byte == "\n"
47
+ @done = true
48
+ @conn.write(IAC_DO_ECHO + "\r\n") if @password_buffer =~ /#{Regexp.escape(IAC_DO_ECHO)}|#{Regexp.escape(IAC_WILL_ECHO)}/ # echo on, send newline
49
+ password = @password_buffer
50
+ password.gsub!(/\377[\373-\376]./m, "") # Strip IAC DO/DONT/WILL/WONT option from password
51
+ password.chomp! # strip trailing newline
52
+ if @authenticate_proc.call(password)
53
+ # success
54
+ @success_proc.call if @success_proc
55
+ else
56
+ # Failure
57
+ @fail_proc.call if @fail_proc
58
+ end
59
+ end
60
+ nil
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,213 @@
1
+ # = Capture app
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 'optparse'
20
+ require 'scriptty/net/event_loop'
21
+ require 'scriptty/util/transcript/writer'
22
+ require 'scriptty/term'
23
+ require 'logger'
24
+ require 'stringio'
25
+
26
+ module ScripTTY
27
+ module Apps
28
+ class CaptureApp
29
+ attr_reader :term
30
+
31
+ def initialize(argv)
32
+ @client_connection = nil
33
+ @server_connection = nil
34
+ @output_file = nil
35
+ @options = parse_options(argv)
36
+ @console_password = "" # TODO SECURITY FIXME
37
+ @attached_consoles = []
38
+ @net = ScripTTY::Net::EventLoop.new
39
+ @log_stringio = StringIO.new
40
+ @log = Logger.new(@log_stringio)
41
+ end
42
+
43
+ def detach_console(console)
44
+ @attached_consoles.delete(console)
45
+ end
46
+
47
+ def log_messages
48
+ ([""]*10 + @log_stringio.string.split("\n"))[-10..-1].map{|line| line.sub(/^.*?\]/, '')}
49
+ end
50
+
51
+ def main
52
+ @output_file = Util::Transcript::Writer.new(File.open(@options[:output], @options[:append] ? "a" : "w")) if @options[:output]
53
+ @output_file.info("--- Capture started #{Time.now} ---") if @output_file
54
+ @net.on_accept(@options[:console_addrs] || [], :multiple => true) do |conn|
55
+ p = PasswordPrompt.new(conn, "Console password: ")
56
+ p.authenticate { |password| password == @console_password }
57
+ p.on_fail { conn.write("Authentiation failed.\r\n") { conn.close } }
58
+ p.on_success {
59
+ @attached_consoles << Console.new(conn, self)
60
+ @attached_consoles.each { |c| c.refresh! }
61
+ }
62
+ end
63
+ @net.on_accept(@options[:listen_addrs], :multiple => true) do |conn|
64
+ @output_file.client_open(*conn.remote_address) if @output_file
65
+ @client_connection = conn
66
+ @client_connection.on_receive_bytes { |bytes| handle_client_receive_bytes(bytes) }
67
+ @client_connection.on_close { handle_client_close ; @client_connection = nil }
68
+ handle_client_connected
69
+ end
70
+ @net.main
71
+ ensure
72
+ if @output_file
73
+ @output_file.close
74
+ @output_file = nil
75
+ end
76
+ end
77
+
78
+ # Instruct the event loop to exit.
79
+ #
80
+ # Can be invoked by another thread.
81
+ def exit
82
+ @net.exit
83
+ end
84
+
85
+ def handle_console_command_entered(cmd)
86
+ case cmd
87
+ when /^'(.*)$/i # comment
88
+ comment = $1.strip
89
+ @output_file.info("Comment: #{comment}") if @output_file
90
+ log.info("Comment: #{comment}")
91
+ else
92
+ log.warn("Unknown console command: #{cmd}")
93
+ end
94
+ @last_command = cmd
95
+ end
96
+
97
+ private
98
+
99
+ attr_reader :log
100
+
101
+ def handle_client_connected
102
+ connect_to_server
103
+ end
104
+
105
+ def handle_server_connected
106
+ @term = ScripTTY::Term.new(@options[:term])
107
+ @term.on_unknown_sequence do |sequence|
108
+ @output_file.info("Unknown escape sequence", sequence) if @output_file
109
+ log.debug("Unknown escape sequence: #{sequence.inspect}")
110
+ end
111
+ end
112
+
113
+ def handle_server_connect_error(e)
114
+ @output_file.info("Server connection error #{e}") if @output_file # TODO - add a separate transcript record
115
+ @client_connection.close if @client_connection
116
+ end
117
+
118
+ def handle_client_receive_bytes(bytes)
119
+ return unless @server_connection # Ignore bytes received from client until server is connected.
120
+ @output_file.from_client(bytes) if @output_file
121
+ @server_connection.write(bytes)
122
+ end
123
+
124
+ def handle_server_receive_bytes(bytes)
125
+ return unless @client_connection # Ignore bytes received from client until server is connected.
126
+ @output_file.from_server(bytes) if @output_file
127
+ @client_connection.write(bytes)
128
+ @term.feed_bytes(bytes)
129
+ @attached_consoles.each { |c| c.refresh! }
130
+ end
131
+
132
+ def handle_client_close
133
+ @output_file.client_close("Client connection closed") if @output_file
134
+ @server_connection.close if @server_connection
135
+ @attached_consoles.each { |c| c.refresh! }
136
+ end
137
+
138
+ def handle_server_close
139
+ @output_file.server_close("Server connection closed") if @output_file
140
+ @client_connection.close if @client_connection
141
+ @term = nil
142
+ end
143
+
144
+ def connect_to_server
145
+ @net.connect(@options[:connect_addr]) do |server_conn|
146
+ server_conn.on_connect_error { |e| handle_server_connect_error(e) }
147
+ server_conn.on_connect {
148
+ @output_file.server_open(*server_conn.remote_address) if @output_file
149
+ @server_connection = server_conn
150
+ handle_server_connected
151
+ }
152
+ server_conn.on_receive_bytes { |bytes| handle_server_receive_bytes(bytes) }
153
+ server_conn.on_close { handle_server_close; @server_connection = nil }
154
+ end
155
+ end
156
+
157
+ def parse_options(argv)
158
+ args = argv.dup
159
+ options = {:term => 'xterm'}
160
+ opts = OptionParser.new do |opts|
161
+ opts.banner = "Usage: #{opts.program_name} [options]"
162
+ opts.separator "Stream capture application"
163
+ opts.on("-l", "--listen [HOST]:PORT", "Listen on the specified HOST:PORT") do |optarg|
164
+ addr = parse_hostport(optarg, :allow_empty_host => true, :allow_zero_port => true)
165
+ options[:listen_addrs] ||= []
166
+ options[:listen_addrs] << addr
167
+ end
168
+ opts.on("-c", "--connect HOST:PORT", "Connect to the specified HOST:PORT") do |optarg|
169
+ addr = parse_hostport(optarg)
170
+ options[:connect_addr] = addr
171
+ end
172
+ opts.on("-C", "--console [HOST]:PORT", "Debug console on the specified HOST:PORT") do |optarg|
173
+ addr = parse_hostport(optarg, :allow_empty_host => true, :allow_zero_port => true)
174
+ options[:console_addrs] ||= []
175
+ options[:console_addrs] << addr
176
+ end
177
+ opts.on("-t", "--term NAME", "Terminal to emulate") do |optarg|
178
+ raise ArgumentError.new("Unsupported terminal #{optarg.inspect}") unless ScripTTY::Term::TERMINAL_TYPES.include?(optarg)
179
+ options[:term] = optarg
180
+ end
181
+ opts.on("-o", "--output FILE", "Write transcript to FILE") do |optarg|
182
+ options[:output] = optarg
183
+ end
184
+ opts.on("-a", "--[no-]append", "Append to output file instead of overwriting it") do |optarg|
185
+ options[:append] = optarg
186
+ end
187
+ end
188
+ opts.parse!(args)
189
+ raise ArgumentError.new("No connect-to address specified") unless options[:connect_addr]
190
+ options
191
+ end
192
+
193
+ # Parse [HOST:]PORT into separate host and port. Host is optional, and
194
+ # might be surrounded by square brackets.
195
+ def parse_hostport(s, opts={})
196
+ unless s =~ /\A(\[[^\[\]]*\]|[^\[\]]*):(\d+)\Z/
197
+ raise ArgumentError.new("Unable to parse host:port")
198
+ end
199
+ host, port = [$1, $2]
200
+ host.gsub!(/\A\[(.*?)\]\Z/, '\1')
201
+ port = port.to_i
202
+ raise ArgumentError.new("Invalid port") if port < 0 or port > 0xffff or (port == 0 and !opts[:allow_zero_port])
203
+ unless opts[:allow_empty_host]
204
+ raise ArgumentError.new("Host cannot be empty") if host.empty?
205
+ end
206
+ [host, port]
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ require 'scriptty/apps/capture_app/password_prompt'
213
+ require 'scriptty/apps/capture_app/console'
@@ -0,0 +1,166 @@
1
+ # = Generates a ScripTTY screen dumps from a transcript
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 'optparse'
20
+ require 'scriptty/term'
21
+ require 'scriptty/util/transcript/reader'
22
+ require 'scriptty/screen_pattern/generator'
23
+ require 'set'
24
+ require 'digest/sha2'
25
+
26
+ module ScripTTY
27
+ module Apps
28
+ class DumpScreensApp
29
+ def initialize(argv)
30
+ @options = parse_options(argv)
31
+ end
32
+
33
+ def main
34
+ @term = ScripTTY::Term.new(@options[:term])
35
+ @term.on_unknown_sequence :ignore # DEBUG FIXME
36
+ #@term.on_unknown_sequence do |seq|
37
+ # puts "Unknown escape sequence: #{seq.inspect}" # DEBUG FIXME
38
+ #end
39
+ @screen_pattern_digests = Set.new
40
+ @prev_screen = nil
41
+ @next_num = 1
42
+ @output_file = File.open(@options[:output], "w") if @options[:output]
43
+ if @options[:output_dir]
44
+ @output_dir = @options[:output_dir]
45
+ @next_num = Dir.entries(@output_dir).map{|e| e =~ /\Ap(\d+)\.txt\Z/ && $1.to_i }.select{|e| e}.sort.last || 1
46
+ end
47
+ begin
48
+ @options[:input_files].each do |input_filename|
49
+ File.open(input_filename, "r") do |input_file|
50
+ reader = ScripTTY::Util::Transcript::Reader.new(input_file)
51
+ while (entry = reader.next_entry)
52
+ timestamp, type, args = entry
53
+ case type
54
+ when :from_server
55
+ handle_bytes_from_server(args[0])
56
+ when :server_parsed
57
+ handle_bytes_from_server(args[1])
58
+ when :from_client
59
+ handle_bytes_from_client(args[0], timestamp)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ ensure
65
+ @output_file.close if @output_file
66
+ @output_file = nil
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def handle_bytes_from_server(bytes)
73
+ @term.feed_bytes(bytes)
74
+ end
75
+
76
+ # When we receive bytes from the client, we output a new screen if the
77
+ # screen hasn't already been generated, and if the screen isn't
78
+ # different only in the way that it would be if the user type a
79
+ # single character into a prompt. (See the too_similar method.)
80
+ def handle_bytes_from_client(bytes, timestamp=nil)
81
+ ts = too_similar
82
+ @prev_screen = { :cursor_pos => @term.cursor_pos, :text => @term.text }
83
+ return if ts
84
+ pattern = generate_screen_pattern
85
+ hexdigest = Digest::SHA256.hexdigest(pattern)
86
+ return if @screen_pattern_digests.include?(hexdigest)
87
+ @screen_pattern_digests << hexdigest
88
+ pattern_name = sprintf("p%d", @next_num)
89
+ @next_num += 1
90
+ if @output_file
91
+ @output_file.puts(generate_screen_pattern(pattern_name))
92
+ @output_file.puts("")
93
+ end
94
+ if @output_dir
95
+ File.open(File.join(@output_dir, pattern_name + ".txt"), "w") do |outfile|
96
+ outfile.puts(generate_screen_pattern(pattern_name))
97
+ end
98
+ end
99
+ end
100
+
101
+ # Return the screen pattern generated from the current state of the
102
+ # terminal.
103
+ def generate_screen_pattern(name=nil)
104
+ matches = []
105
+ @term.text.each_with_index do |line, row|
106
+ matches << [[row, 0], line]
107
+ end
108
+ ScreenPattern::Generator.generate(name || "no_name", :force_cursor => / /,
109
+ :size => [@term.height, @term.width],
110
+ :cursor_pos => @term.cursor_pos,
111
+ :ignore => @options[:ignore],
112
+ :matches => matches)
113
+ end
114
+
115
+ def too_similar
116
+ return false unless @prev_screen
117
+ prev_row, prev_col = @prev_screen[:cursor_pos]
118
+ prev_text = @prev_screen[:text].map{|line| line.dup}
119
+ current_row, current_col = @term.cursor_pos
120
+ current_text = @term.text
121
+ return false if current_row != prev_row or current_col != prev_col+1
122
+ current_text[prev_row][prev_col..prev_col] = " "
123
+ return (current_text == prev_text)
124
+ end
125
+
126
+ def parse_options(argv)
127
+ args = argv.dup
128
+ options = {:term => 'xterm', :input_files => [], :rate => 2}
129
+ opts = OptionParser.new do |opts|
130
+ opts.banner = "Usage: #{opts.program_name} [options] FILE..."
131
+ opts.separator "Dump screens to a file/directory based on one or more transcript files"
132
+ opts.separator ""
133
+ opts.on("-t", "--term NAME", "Terminal to emulate") do |optarg|
134
+ raise ArgumentError.new("Unsupported terminal #{optarg.inspect}") unless ScripTTY::Term::TERMINAL_TYPES.include?(optarg)
135
+ options[:term] = optarg
136
+ end
137
+ opts.on("-O", "--output-dir DIR", "Write output to DIR") do |optarg|
138
+ options[:output_dir] = optarg
139
+ end
140
+ opts.on("-o", "--output FILE", "Write output to FILE") do |optarg|
141
+ options[:output] = optarg
142
+ end
143
+ opts.on("-I", "--ignore ROW,COL,LENGTH", "Always ignore the specified region") do |optarg|
144
+ options[:ignore] ||= []
145
+ row, col, length = optarg.split(",").map{|n|
146
+ raise ArgumentError.new("Illegal --ignore argument: #{optarg}") unless n =~ /\A\d+\Z/ and n.to_i >= 0
147
+ n.to_i
148
+ }
149
+ options[:ignore] << [row, col..col+length]
150
+ end
151
+ end
152
+ opts.parse!(args)
153
+ if args.length < 1
154
+ $stderr.puts "error: No input file(s) specified."
155
+ exit 1
156
+ end
157
+ options[:input_files] = args
158
+ if (!options[:output] and !options[:output_dir]) or !options[:term]
159
+ $stderr.puts "error: --term and --output[-dir] are mandatory"
160
+ exit 1
161
+ end
162
+ options
163
+ end
164
+ end
165
+ end
166
+ end