scriptty 0.5.0-java
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitattributes +1 -0
- data/.gitignore +3 -0
- data/COPYING +674 -0
- data/COPYING.LESSER +165 -0
- data/README.rdoc +31 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/scriptty-capture +5 -0
- data/bin/scriptty-dump-screens +4 -0
- data/bin/scriptty-replay +5 -0
- data/bin/scriptty-term-test +4 -0
- data/bin/scriptty-transcript-parse +4 -0
- data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
- data/examples/captures/xterm-vim-session.bin +262 -0
- data/examples/demo-capture.rb +19 -0
- data/examples/telnet-nego.rb +55 -0
- data/lib/scriptty/apps/capture_app/console.rb +104 -0
- data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
- data/lib/scriptty/apps/capture_app.rb +213 -0
- data/lib/scriptty/apps/dump_screens_app.rb +166 -0
- data/lib/scriptty/apps/replay_app.rb +229 -0
- data/lib/scriptty/apps/term_test_app.rb +124 -0
- data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
- data/lib/scriptty/cursor.rb +39 -0
- data/lib/scriptty/exception.rb +38 -0
- data/lib/scriptty/expect.rb +392 -0
- data/lib/scriptty/multiline_buffer.rb +192 -0
- data/lib/scriptty/net/event_loop.rb +610 -0
- data/lib/scriptty/screen_pattern/generator.rb +398 -0
- data/lib/scriptty/screen_pattern/parser.rb +558 -0
- data/lib/scriptty/screen_pattern.rb +104 -0
- data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
- data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
- data/lib/scriptty/term/dg410/parser.rb +162 -0
- data/lib/scriptty/term/dg410.rb +489 -0
- data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
- data/lib/scriptty/term/xterm.rb +661 -0
- data/lib/scriptty/term.rb +40 -0
- data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
- data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
- data/lib/scriptty/util/fsm.rb +177 -0
- data/lib/scriptty/util/transcript/reader.rb +96 -0
- data/lib/scriptty/util/transcript/writer.rb +111 -0
- data/test/apps/capture_app_test.rb +123 -0
- data/test/apps/transcript_parse_app_test.rb +118 -0
- data/test/cursor_test.rb +51 -0
- data/test/fsm_definition_parser_test.rb +220 -0
- data/test/fsm_test.rb +322 -0
- data/test/multiline_buffer_test.rb +275 -0
- data/test/net/event_loop_test.rb +402 -0
- data/test/screen_pattern/generator_test.rb +408 -0
- data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
- data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
- data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
- data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
- data/test/screen_pattern/parser_test.rb +266 -0
- data/test/term/dg410/parser_test.rb +139 -0
- data/test/term/xterm_test.rb +327 -0
- data/test/test_helper.rb +3 -0
- data/test/util/transcript/reader_test.rb +131 -0
- data/test/util/transcript/writer_test.rb +126 -0
- data/test.watchr +29 -0
- metadata +175 -0
@@ -0,0 +1,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
|