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