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.
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
- # The VNC class provides for simple rfb-protocol based control of
10
- # a VNC server. This can be used, eg, to automate applications.
11
- #
12
- # Sample usage:
13
- #
14
- # # launch xclock on localhost. note that there is an xterm in the top-left
15
- # Net::VNC.open 'localhost:0', :shared => true, :password => 'mypass' 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, @y, @button = x, y, button
45
- refresh
46
- end
47
-
48
- def refresh
49
- packet = 0.chr * 6
50
- packet[0] = 5.chr
51
- packet[1] = button.chr
52
- packet[2, 2] = [x].pack 'n'
53
- packet[4, 2] = [y].pack 'n'
54
- @vnc.socket.write packet
55
- end
56
- end
57
-
58
- BASE_PORT = 5900
59
- CHALLENGE_SIZE = 16
60
- DEFAULT_OPTIONS = {
61
- :shared => false,
62
- :wait => 0.1
63
- }
64
-
65
- keys_file = File.dirname(__FILE__) + '/../../data/keys.yaml'
66
- KEY_MAP = YAML.load_file(keys_file).inject({}) { |h, (k, v)| h.update k.to_sym => v }
67
- def KEY_MAP.[] key
68
- super or raise ArgumentError.new('Invalid key name - %s' % key)
69
- end
70
-
71
- attr_reader :server, :display, :options, :socket, :pointer
72
-
73
- def initialize display=':0', options={}
74
- @server = 'localhost'
75
- if display =~ /^(.*)(:\d+)$/
76
- @server, display = $1, $2
77
- end
78
- @display = display[1..-1].to_i
79
- @options = DEFAULT_OPTIONS.merge options
80
- @clipboard = nil
81
- @pointer = PointerState.new self
82
- @mutex = Mutex.new
83
- connect
84
- @packet_reading_state = nil
85
- @packet_reading_thread = Thread.new { packet_reading_thread }
86
- end
87
-
88
- def self.open display=':0', options={}
89
- vnc = new display, options
90
- if block_given?
91
- begin
92
- yield vnc
93
- ensure
94
- vnc.close
95
- end
96
- else
97
- vnc
98
- end
99
- end
100
-
101
- def port
102
- BASE_PORT + @display
103
- end
104
-
105
- def connect
106
- @socket = TCPSocket.open server, port
107
- unless socket.read(12) =~ /^RFB (\d{3}.\d{3})\n$/
108
- raise 'invalid server response'
109
- end
110
- @server_version = $1
111
- socket.write "RFB 003.003\n"
112
- data = socket.read(4)
113
- auth = data.to_s.unpack('N')[0]
114
- case auth
115
- when 0, nil
116
- raise 'connection failed'
117
- when 1
118
- # ok...
119
- when 2
120
- password = @options[:password] or raise 'Need to authenticate but no password given'
121
- challenge = socket.read CHALLENGE_SIZE
122
- response = Cipher::DES.encrypt password, challenge
123
- socket.write response
124
- ok = socket.read(4).to_s.unpack('N')[0]
125
- raise 'Unable to authenticate - %p' % ok unless ok == 0
126
- else
127
- raise 'Unknown authentication scheme - %d' % auth
128
- end
129
-
130
- # ClientInitialisation
131
- socket.write((options[:shared] ? 1 : 0).chr)
132
-
133
- # ServerInitialisation
134
- # TODO: parse this.
135
- socket.read(20)
136
- data = socket.read(4)
137
- # read this many bytes in chunks of 20
138
- size = data.to_s.unpack('N')[0]
139
- while size > 0
140
- len = [20, size].min
141
- # this is the hostname, and other stuff i think...
142
- socket.read(len)
143
- size -= len
144
- end
145
- end
146
-
147
- # this types +text+ on the server
148
- def type text, options={}
149
- packet = 0.chr * 8
150
- packet[0] = 4.chr
151
- text.split(//).each do |char|
152
- packet[7] = char[0]
153
- packet[1] = 1.chr
154
- socket.write packet
155
- packet[1] = 0.chr
156
- socket.write packet
157
- end
158
- wait options
159
- end
160
-
161
- # this takes an array of keys, and successively holds each down then lifts them up in
162
- # reverse order.
163
- # FIXME: should wait. can't recurse in that case.
164
- def key_press(*args)
165
- options = Hash === args.last ? args.pop : {}
166
- keys = args
167
- raise ArgumentError, 'Must have at least one key argument' if keys.empty?
168
- begin
169
- key_down keys.first
170
- if keys.length == 1
171
- yield if block_given?
172
- else
173
- key_press(*(keys[1..-1] + [options]))
174
- end
175
- ensure
176
- key_up keys.first
177
- end
178
- end
179
-
180
- def get_key_code which
181
- if String === which
182
- if which.length != 1
183
- raise ArgumentError, 'can only get key_code of single character strings'
184
- end
185
- which[0]
186
- else
187
- KEY_MAP[which]
188
- end
189
- end
190
- private :get_key_code
191
-
192
- def key_down which, options={}
193
- packet = 0.chr * 8
194
- packet[0] = 4.chr
195
- key_code = get_key_code which
196
- packet[4, 4] = [key_code].pack('N')
197
- packet[1] = 1.chr
198
- socket.write packet
199
- wait options
200
- end
201
-
202
- def key_up which, options={}
203
- packet = 0.chr * 8
204
- packet[0] = 4.chr
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