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,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