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