ruby-vnc 1.1.0 → 1.3.0
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.
- checksums.yaml +7 -0
- data/Changelog.rdoc +31 -0
- data/{README → README.rdoc} +12 -0
- data/Rakefile +23 -38
- data/lib/net/rfb/frame_buffer.rb +175 -0
- data/lib/net/vnc/version.rb +3 -4
- data/lib/net/vnc.rb +361 -300
- data/spec/real_net_vnc_spec.rb +76 -0
- metadata +123 -62
- data/ChangeLog +0 -13
- data/lib/cipher/des.rb +0 -439
- data/spec/cipher_des_spec.rb +0 -142
- data/spec/net_vnc_spec.rb +0 -136
data/lib/net/vnc.rb
CHANGED
@@ -1,306 +1,367 @@
|
|
1
1
|
require 'socket'
|
2
2
|
require 'yaml'
|
3
|
-
require 'thread'
|
4
|
-
require 'cipher/des'
|
5
3
|
require 'net/vnc/version'
|
6
4
|
|
7
5
|
module Net
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
key_code = get_key_code which
|
206
|
-
packet[4, 4] = [key_code].pack('N')
|
207
|
-
packet[1] = 0.chr
|
208
|
-
socket.write packet
|
209
|
-
wait options
|
210
|
-
end
|
211
|
-
|
212
|
-
def pointer_move x, y, options={}
|
213
|
-
# options[:relative]
|
214
|
-
pointer.update x, y
|
215
|
-
wait options
|
216
|
-
end
|
217
|
-
|
218
|
-
BUTTON_MAP = {
|
219
|
-
:left => 0
|
220
|
-
}
|
221
|
-
|
222
|
-
def button_press button=:left, options={}
|
223
|
-
begin
|
224
|
-
button_down button, options
|
225
|
-
yield if block_given?
|
226
|
-
ensure
|
227
|
-
button_up button, options
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
def button_down which=:left, options={}
|
232
|
-
button = BUTTON_MAP[which] || which
|
233
|
-
raise ArgumentError, 'Invalid button - %p' % which unless (0..2) === button
|
234
|
-
pointer.button |= 1 << button
|
235
|
-
wait options
|
236
|
-
end
|
237
|
-
|
238
|
-
def button_up which=:left, options={}
|
239
|
-
button = BUTTON_MAP[which] || which
|
240
|
-
raise ArgumentError, 'Invalid button - %p' % which unless (0..2) === button
|
241
|
-
pointer.button &= ~(1 << button)
|
242
|
-
wait options
|
243
|
-
end
|
244
|
-
|
245
|
-
def wait options={}
|
246
|
-
sleep options[:wait] || @options[:wait]
|
247
|
-
end
|
248
|
-
|
249
|
-
def close
|
250
|
-
# destroy packet reading thread
|
251
|
-
if @packet_reading_state == :loop
|
252
|
-
@packet_reading_state = :stop
|
253
|
-
while @packet_reading_state
|
254
|
-
# do nothing
|
255
|
-
end
|
256
|
-
end
|
257
|
-
socket.close
|
258
|
-
end
|
259
|
-
|
260
|
-
def clipboard
|
261
|
-
if block_given?
|
262
|
-
@clipboard = nil
|
263
|
-
yield
|
264
|
-
60.times do
|
265
|
-
clipboard = @mutex.synchronize { @clipboard }
|
266
|
-
return clipboard if clipboard
|
267
|
-
sleep 0.5
|
268
|
-
end
|
269
|
-
warn 'clipboard still empty after 30s'
|
270
|
-
nil
|
271
|
-
else
|
272
|
-
@mutex.synchronize { @clipboard }
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
private
|
277
|
-
|
278
|
-
def read_packet type
|
279
|
-
case type
|
280
|
-
when 3 # ServerCutText
|
281
|
-
socket.read 3 # discard padding bytes
|
282
|
-
len = socket.read(4).unpack('N')[0]
|
283
|
-
@mutex.synchronize { @clipboard = socket.read len }
|
284
|
-
else
|
285
|
-
raise NotImplementedError, 'unhandled server packet type - %d' % type
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
def packet_reading_thread
|
290
|
-
@packet_reading_state = :loop
|
291
|
-
loop do
|
292
|
-
begin
|
293
|
-
break if @packet_reading_state != :loop
|
294
|
-
next unless IO.select [socket], nil, nil, 2
|
295
|
-
type = socket.read(1)[0]
|
296
|
-
read_packet type
|
297
|
-
rescue
|
298
|
-
warn "exception in packet_reading_thread: #{$!.class}:#{$!}"
|
299
|
-
break
|
300
|
-
end
|
301
|
-
end
|
302
|
-
@packet_reading_state = nil
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
6
|
+
#
|
7
|
+
# The VNC class provides for simple rfb-protocol based control of
|
8
|
+
# a VNC server. This can be used, eg, to automate applications.
|
9
|
+
#
|
10
|
+
# Sample usage:
|
11
|
+
#
|
12
|
+
# # launch xclock on localhost. note that there is an xterm in the top-left
|
13
|
+
#
|
14
|
+
# require 'net/vnc'
|
15
|
+
# Net::VNC.open 'localhost:0', :shared => true do |vnc|
|
16
|
+
# vnc.pointer_move 10, 10
|
17
|
+
# vnc.type 'xclock'
|
18
|
+
# vnc.key_press :return
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# = TODO
|
22
|
+
#
|
23
|
+
# * The server read loop seems a bit iffy. Not sure how best to do it.
|
24
|
+
# * Should probably be changed to be more of a lower-level protocol wrapping thing, with the
|
25
|
+
# actual VNCClient sitting on top of that. all it should do is read/write the packets over
|
26
|
+
# the socket.
|
27
|
+
#
|
28
|
+
class VNC
|
29
|
+
class PointerState
|
30
|
+
attr_reader :x, :y, :button
|
31
|
+
|
32
|
+
def initialize(vnc)
|
33
|
+
@x = @y = @button = 0
|
34
|
+
@vnc = vnc
|
35
|
+
end
|
36
|
+
|
37
|
+
# could have the same for x=, and y=
|
38
|
+
def button=(button)
|
39
|
+
@button = button
|
40
|
+
refresh
|
41
|
+
end
|
42
|
+
|
43
|
+
def update(x, y, button = @button)
|
44
|
+
@x = x
|
45
|
+
@y = y
|
46
|
+
@button = button
|
47
|
+
refresh
|
48
|
+
end
|
49
|
+
|
50
|
+
def refresh
|
51
|
+
packet = 0.chr * 6
|
52
|
+
packet[0] = 5.chr
|
53
|
+
packet[1] = button.chr
|
54
|
+
packet[2, 2] = [x].pack 'n'
|
55
|
+
packet[4, 2] = [y].pack 'n'
|
56
|
+
@vnc.socket.write packet
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
BASE_PORT = 5900
|
61
|
+
CHALLENGE_SIZE = 16
|
62
|
+
DEFAULT_OPTIONS = {
|
63
|
+
shared: false,
|
64
|
+
wait: 0.1,
|
65
|
+
pix_fmt: :BGRA,
|
66
|
+
encoding: :RAW
|
67
|
+
}
|
68
|
+
|
69
|
+
keys_file = File.dirname(__FILE__) + '/../../data/keys.yaml'
|
70
|
+
KEY_MAP = YAML.load_file(keys_file).inject({}) { |h, (k, v)| h.update k.to_sym => v }
|
71
|
+
def KEY_MAP.[](key)
|
72
|
+
super or raise(ArgumentError, 'Invalid key name - %s' % key)
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader :server, :display, :options, :socket, :pointer, :desktop_name
|
76
|
+
|
77
|
+
def initialize(display = ':0', options = {})
|
78
|
+
@server = 'localhost'
|
79
|
+
if display =~ /^(.*)(:\d+)$/
|
80
|
+
@server = Regexp.last_match(1)
|
81
|
+
display = Regexp.last_match(2)
|
82
|
+
end
|
83
|
+
@display = display[1..-1].to_i
|
84
|
+
@desktop_name = nil
|
85
|
+
@options = DEFAULT_OPTIONS.merge options
|
86
|
+
@clipboard = nil
|
87
|
+
@fb = nil
|
88
|
+
@pointer = PointerState.new self
|
89
|
+
@mutex = Mutex.new
|
90
|
+
connect
|
91
|
+
@packet_reading_state = nil
|
92
|
+
@packet_reading_thread = Thread.new { packet_reading_thread }
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.open(display = ':0', options = {})
|
96
|
+
vnc = new display, options
|
97
|
+
if block_given?
|
98
|
+
begin
|
99
|
+
yield vnc
|
100
|
+
ensure
|
101
|
+
vnc.close
|
102
|
+
end
|
103
|
+
else
|
104
|
+
vnc
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def port
|
109
|
+
BASE_PORT + @display
|
110
|
+
end
|
111
|
+
|
112
|
+
def connect
|
113
|
+
@socket = TCPSocket.open(server, port)
|
114
|
+
raise 'invalid server response' unless socket.read(12) =~ /^RFB (\d{3}.\d{3})\n$/
|
115
|
+
|
116
|
+
@server_version = Regexp.last_match(1)
|
117
|
+
socket.write "RFB 003.003\n"
|
118
|
+
data = socket.read(4)
|
119
|
+
auth = data.to_s.unpack1('N')
|
120
|
+
case auth
|
121
|
+
when 0, nil
|
122
|
+
raise 'connection failed'
|
123
|
+
when 1
|
124
|
+
# ok...
|
125
|
+
when 2
|
126
|
+
raise 'Unable to authenticate - DES no longer supported'
|
127
|
+
else
|
128
|
+
raise 'Unknown authentication scheme - %d' % auth
|
129
|
+
end
|
130
|
+
|
131
|
+
# ClientInitialisation
|
132
|
+
socket.write((options[:shared] ? 1 : 0).chr)
|
133
|
+
|
134
|
+
# ServerInitialisation
|
135
|
+
@framebuffer_width = socket.read(2).to_s.unpack1('n').to_i
|
136
|
+
@framebuffer_height = socket.read(2).to_s.unpack1('n').to_i
|
137
|
+
|
138
|
+
# TODO: parse this.
|
139
|
+
_pixel_format = socket.read(16)
|
140
|
+
|
141
|
+
# read the name in byte chunks of 20
|
142
|
+
name_length = socket.read(4).to_s.unpack1('N')
|
143
|
+
@desktop_name = [].tap do |it|
|
144
|
+
while name_length > 0
|
145
|
+
len = [20, name_length].min
|
146
|
+
it << socket.read(len)
|
147
|
+
name_length -= len
|
148
|
+
end
|
149
|
+
end.join
|
150
|
+
|
151
|
+
_load_frame_buffer
|
152
|
+
end
|
153
|
+
|
154
|
+
# this types +text+ on the server
|
155
|
+
def type(text, options = {})
|
156
|
+
packet = 0.chr * 8
|
157
|
+
packet[0] = 4.chr
|
158
|
+
text.split(//).each do |char|
|
159
|
+
packet[7] = char[0]
|
160
|
+
packet[1] = 1.chr
|
161
|
+
socket.write packet
|
162
|
+
packet[1] = 0.chr
|
163
|
+
socket.write packet
|
164
|
+
end
|
165
|
+
wait options
|
166
|
+
end
|
167
|
+
|
168
|
+
# this takes an array of keys, and successively holds each down then lifts them up in
|
169
|
+
# reverse order.
|
170
|
+
# FIXME: should wait. can't recurse in that case.
|
171
|
+
def key_press(*args)
|
172
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
173
|
+
keys = args
|
174
|
+
raise ArgumentError, 'Must have at least one key argument' if keys.empty?
|
175
|
+
|
176
|
+
begin
|
177
|
+
key_down keys.first
|
178
|
+
if keys.length == 1
|
179
|
+
yield if block_given?
|
180
|
+
else
|
181
|
+
key_press(*(keys[1..-1] + [options]))
|
182
|
+
end
|
183
|
+
ensure
|
184
|
+
key_up keys.first
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def get_key_code(which)
|
189
|
+
case which
|
190
|
+
when String
|
191
|
+
raise ArgumentError, 'can only get key_code of single character strings' if which.length != 1
|
192
|
+
|
193
|
+
which[0].ord
|
194
|
+
when Symbol
|
195
|
+
KEY_MAP[which]
|
196
|
+
when Integer
|
197
|
+
which
|
198
|
+
else
|
199
|
+
raise ArgumentError, "unsupported key value: #{which.inspect}"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
private :get_key_code
|
306
203
|
|
204
|
+
def key_down(which, options = {})
|
205
|
+
packet = 0.chr * 8
|
206
|
+
packet[0] = 4.chr
|
207
|
+
key_code = get_key_code which
|
208
|
+
packet[4, 4] = [key_code].pack('N')
|
209
|
+
packet[1] = 1.chr
|
210
|
+
socket.write packet
|
211
|
+
wait options
|
212
|
+
end
|
213
|
+
|
214
|
+
def key_up(which, options = {})
|
215
|
+
packet = 0.chr * 8
|
216
|
+
packet[0] = 4.chr
|
217
|
+
key_code = get_key_code which
|
218
|
+
packet[4, 4] = [key_code].pack('N')
|
219
|
+
packet[1] = 0.chr
|
220
|
+
socket.write packet
|
221
|
+
wait options
|
222
|
+
end
|
223
|
+
|
224
|
+
def pointer_move(x, y, options = {})
|
225
|
+
# options[:relative]
|
226
|
+
pointer.update x, y
|
227
|
+
wait options
|
228
|
+
end
|
229
|
+
|
230
|
+
BUTTON_MAP = {
|
231
|
+
left: 0
|
232
|
+
}
|
233
|
+
|
234
|
+
def button_press(button = :left, options = {})
|
235
|
+
button_down button, options
|
236
|
+
yield if block_given?
|
237
|
+
ensure
|
238
|
+
button_up button, options
|
239
|
+
end
|
240
|
+
|
241
|
+
def button_down(which = :left, options = {})
|
242
|
+
button = BUTTON_MAP[which] || which
|
243
|
+
raise ArgumentError, 'Invalid button - %p' % which unless (0..2).include?(button)
|
244
|
+
|
245
|
+
pointer.button |= 1 << button
|
246
|
+
wait options
|
247
|
+
end
|
248
|
+
|
249
|
+
def button_up(which = :left, options = {})
|
250
|
+
button = BUTTON_MAP[which] || which
|
251
|
+
raise ArgumentError, 'Invalid button - %p' % which unless (0..2).include?(button)
|
252
|
+
|
253
|
+
pointer.button &= ~(1 << button)
|
254
|
+
wait options
|
255
|
+
end
|
256
|
+
|
257
|
+
# take screenshot as PNG image
|
258
|
+
# @param dest [String|IO|nil] destination file path, or IO-object, or nil
|
259
|
+
# @return [String] PNG binary data as string when dest is null
|
260
|
+
# [true] else case
|
261
|
+
def take_screenshot(dest = nil)
|
262
|
+
fb = _load_frame_buffer # on-demand loading
|
263
|
+
fb.save_pixel_data_as_png dest
|
264
|
+
end
|
265
|
+
|
266
|
+
def wait(options = {})
|
267
|
+
sleep options[:wait] || @options[:wait]
|
268
|
+
end
|
269
|
+
|
270
|
+
def close
|
271
|
+
# destroy packet reading thread
|
272
|
+
if @packet_reading_state == :loop
|
273
|
+
@packet_reading_state = :stop
|
274
|
+
while @packet_reading_state
|
275
|
+
# do nothing
|
276
|
+
end
|
277
|
+
end
|
278
|
+
socket.close
|
279
|
+
end
|
280
|
+
|
281
|
+
def reconnect
|
282
|
+
60.times do
|
283
|
+
if @packet_reading_state.nil?
|
284
|
+
connect
|
285
|
+
@packet_reading_thread = Thread.new { packet_reading_thread }
|
286
|
+
return true
|
287
|
+
end
|
288
|
+
sleep 0.5
|
289
|
+
end
|
290
|
+
warn 'reconnect failed because packet reading state had not been stopped for 30 seconds.'
|
291
|
+
false
|
292
|
+
end
|
293
|
+
|
294
|
+
def clipboard
|
295
|
+
if block_given?
|
296
|
+
@clipboard = nil
|
297
|
+
yield
|
298
|
+
60.times do
|
299
|
+
clipboard = @mutex.synchronize { @clipboard }
|
300
|
+
return clipboard if clipboard
|
301
|
+
|
302
|
+
sleep 0.5
|
303
|
+
end
|
304
|
+
warn 'clipboard still empty after 30s'
|
305
|
+
nil
|
306
|
+
else
|
307
|
+
@mutex.synchronize { @clipboard }
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def clipboard=(text)
|
312
|
+
text = text.to_s.gsub(/\R/, "\n") # eol of ClientCutText's text is LF
|
313
|
+
byte_size = text.to_s.bytes.size
|
314
|
+
packet = 0.chr * (8 + byte_size)
|
315
|
+
packet[0] = 6.chr # message-type: 6 (ClientCutText)
|
316
|
+
packet[4, 4] = [byte_size].pack('N') # length
|
317
|
+
packet[8, byte_size] = text
|
318
|
+
socket.write(packet)
|
319
|
+
@clipboard = text
|
320
|
+
end
|
321
|
+
|
322
|
+
private
|
323
|
+
|
324
|
+
def read_packet(type)
|
325
|
+
case type
|
326
|
+
when 0 # ----------------------------------------------- FramebufferUpdate
|
327
|
+
@fb.handle_response type if @fb
|
328
|
+
when 1 # --------------------------------------------- SetColourMapEntries
|
329
|
+
@fb.handle_response type if @fb
|
330
|
+
when 2 # ------------------------------------------------------------ Bell
|
331
|
+
nil # not support
|
332
|
+
when 3 # --------------------------------------------------- ServerCutText
|
333
|
+
socket.read 3 # discard padding bytes
|
334
|
+
len = socket.read(4).unpack1('N')
|
335
|
+
@mutex.synchronize { @clipboard = socket.read len }
|
336
|
+
else
|
337
|
+
warn 'unhandled server packet type - %d' % type
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def packet_reading_thread
|
342
|
+
@packet_reading_state = :loop
|
343
|
+
loop do
|
344
|
+
break if @packet_reading_state != :loop
|
345
|
+
next unless IO.select [socket], nil, nil, 2
|
346
|
+
|
347
|
+
type = socket.read(1)[0]
|
348
|
+
read_packet type.ord
|
349
|
+
rescue StandardError
|
350
|
+
warn "exception in packet_reading_thread: #{$!.class}:#{$!}\n#{$!.backtrace}"
|
351
|
+
break
|
352
|
+
end
|
353
|
+
@packet_reading_state = nil
|
354
|
+
end
|
355
|
+
|
356
|
+
def _load_frame_buffer
|
357
|
+
unless @fb
|
358
|
+
require 'net/rfb/frame_buffer'
|
359
|
+
|
360
|
+
@fb = Net::RFB::FrameBuffer.new @socket, @framebuffer_width, @framebuffer_height, @options[:pix_fmt],
|
361
|
+
@options[:encoding]
|
362
|
+
@fb.send_initial_data
|
363
|
+
end
|
364
|
+
@fb
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|