class Net::VNC
The VNC
class provides for simple rfb-protocol based control of a VNC
server. This can be used, eg, to automate applications.
Sample usage:
# launch xclock on localhost. note that there is an xterm in the top-left require 'net/vnc' Net::VNC.open 'localhost:0', :shared => true, :password => 'mypass' do |vnc| vnc.pointer_move 10, 10 vnc.type 'xclock' vnc.key_press :return end
TODO¶ ↑
-
The server read loop seems a bit iffy. Not sure how best to do it.
-
Should probably be changed to be more of a lower-level protocol wrapping thing, with the actual VNCClient sitting on top of that. all it should do is read/write the packets over the socket.
Constants
- BASE_PORT
- BUTTON_MAP
- CHALLENGE_SIZE
- DEFAULT_OPTIONS
- KEY_MAP
- VERSION
Attributes
desktop_name[R]
display[R]
options[R]
pointer[R]
server[R]
socket[R]
Public Class Methods
new(display = ':0', options = {})
click to toggle source
# File lib/net/vnc.rb, line 78 def initialize(display = ':0', options = {}) @server = 'localhost' if display =~ /^(.*)(:\d+)$/ @server = Regexp.last_match(1) display = Regexp.last_match(2) end @display = display[1..-1].to_i @desktop_name = nil @options = DEFAULT_OPTIONS.merge options @clipboard = nil @fb = nil @pointer = PointerState.new self @mutex = Mutex.new connect @packet_reading_state = nil @packet_reading_thread = Thread.new { packet_reading_thread } end
open(display = ':0', options = {}) { |vnc| ... }
click to toggle source
# File lib/net/vnc.rb, line 96 def self.open(display = ':0', options = {}) vnc = new display, options if block_given? begin yield vnc ensure vnc.close end else vnc end end
Public Instance Methods
clipboard() { || ... }
click to toggle source
# File lib/net/vnc.rb, line 300 def clipboard if block_given? @clipboard = nil yield 60.times do clipboard = @mutex.synchronize { @clipboard } return clipboard if clipboard sleep 0.5 end warn 'clipboard still empty after 30s' nil else @mutex.synchronize { @clipboard } end end
clipboard=(text)
click to toggle source
# File lib/net/vnc.rb, line 317 def clipboard=(text) text = text.to_s.gsub(/\R/, "\n") # eol of ClientCutText's text is LF byte_size = text.to_s.bytes.size packet = 0.chr * (8 + byte_size) packet[0] = 6.chr # message-type: 6 (ClientCutText) packet[4, 4] = [byte_size].pack('N') # length packet[8, byte_size] = text socket.write(packet) @clipboard = text end
close()
click to toggle source
# File lib/net/vnc.rb, line 276 def close # destroy packet reading thread if @packet_reading_state == :loop @packet_reading_state = :stop while @packet_reading_state # do nothing end end socket.close end
connect()
click to toggle source
# File lib/net/vnc.rb, line 113 def connect @socket = TCPSocket.open(server, port) raise 'invalid server response' unless socket.read(12) =~ /^RFB (\d{3}.\d{3})\n$/ @server_version = Regexp.last_match(1) socket.write "RFB 003.003\n" data = socket.read(4) auth = data.to_s.unpack1('N') case auth when 0, nil raise 'connection failed' when 1 # ok... when 2 password = @options[:password] or raise 'Need to authenticate but no password given' challenge = socket.read CHALLENGE_SIZE response = Cipher::VNCDES.new(password).encrypt(challenge) socket.write response ok = socket.read(4).to_s.unpack1('N') raise 'Unable to authenticate - %p' % ok unless ok == 0 else raise 'Unknown authentication scheme - %d' % auth end # ClientInitialisation socket.write((options[:shared] ? 1 : 0).chr) # ServerInitialisation @framebuffer_width = socket.read(2).to_s.unpack1('n').to_i @framebuffer_height = socket.read(2).to_s.unpack1('n').to_i # TODO: parse this. _pixel_format = socket.read(16) # read the name in byte chunks of 20 name_length = socket.read(4).to_s.unpack1('N') @desktop_name = [].tap do |it| while name_length > 0 len = [20, name_length].min it << socket.read(len) name_length -= len end end.join _load_frame_buffer end
key_down(which, options = {})
click to toggle source
# File lib/net/vnc.rb, line 210 def key_down(which, options = {}) packet = 0.chr * 8 packet[0] = 4.chr key_code = get_key_code which packet[4, 4] = [key_code].pack('N') packet[1] = 1.chr socket.write packet wait options end
key_press(*args) { || ... }
click to toggle source
this takes an array of keys, and successively holds each down then lifts them up in reverse order. FIXME: should wait. can’t recurse in that case.
# File lib/net/vnc.rb, line 177 def key_press(*args) options = args.last.is_a?(Hash) ? args.pop : {} keys = args raise ArgumentError, 'Must have at least one key argument' if keys.empty? begin key_down keys.first if keys.length == 1 yield if block_given? else key_press(*(keys[1..-1] + [options])) end ensure key_up keys.first end end
key_up(which, options = {})
click to toggle source
# File lib/net/vnc.rb, line 220 def key_up(which, options = {}) packet = 0.chr * 8 packet[0] = 4.chr key_code = get_key_code which packet[4, 4] = [key_code].pack('N') packet[1] = 0.chr socket.write packet wait options end
pointer_move(x, y, options = {})
click to toggle source
# File lib/net/vnc.rb, line 230 def pointer_move(x, y, options = {}) # options[:relative] pointer.update x, y wait options end
port()
click to toggle source
# File lib/net/vnc.rb, line 109 def port BASE_PORT + @display end
reconnect()
click to toggle source
# File lib/net/vnc.rb, line 287 def reconnect 60.times do if @packet_reading_state.nil? connect @packet_reading_thread = Thread.new { packet_reading_thread } return true end sleep 0.5 end warn 'reconnect failed because packet reading state had not been stopped for 30 seconds.' false end
take_screenshot(dest = nil)
click to toggle source
take screenshot as PNG image @param dest [String|IO|nil] destination file path, or IO-object, or nil @return [String] PNG binary data as string when dest is null
[true] else case
# File lib/net/vnc.rb, line 267 def take_screenshot(dest = nil) fb = _load_frame_buffer # on-demand loading fb.save_pixel_data_as_png dest end
type(text, options = {})
click to toggle source
this types text
on the server
# File lib/net/vnc.rb, line 161 def type(text, options = {}) packet = 0.chr * 8 packet[0] = 4.chr text.split(//).each do |char| packet[7] = char[0] packet[1] = 1.chr socket.write packet packet[1] = 0.chr socket.write packet end wait options end
wait(options = {})
click to toggle source
# File lib/net/vnc.rb, line 272 def wait(options = {}) sleep options[:wait] || @options[:wait] end
Private Instance Methods
_load_frame_buffer()
click to toggle source
# File lib/net/vnc.rb, line 362 def _load_frame_buffer unless @fb require 'net/rfb/frame_buffer' @fb = Net::RFB::FrameBuffer.new @socket, @framebuffer_width, @framebuffer_height, @options[:pix_fmt], @options[:encoding] @fb.send_initial_data end @fb end
get_key_code(which)
click to toggle source
# File lib/net/vnc.rb, line 194 def get_key_code(which) case which when String raise ArgumentError, 'can only get key_code of single character strings' if which.length != 1 which[0].ord when Symbol KEY_MAP[which] when Integer which else raise ArgumentError, "unsupported key value: #{which.inspect}" end end
packet_reading_thread()
click to toggle source
# File lib/net/vnc.rb, line 347 def packet_reading_thread @packet_reading_state = :loop loop do break if @packet_reading_state != :loop next unless IO.select [socket], nil, nil, 2 type = socket.read(1)[0] read_packet type.ord rescue StandardError warn "exception in packet_reading_thread: #{$!.class}:#{$!}\n#{$!.backtrace}" break end @packet_reading_state = nil end
read_packet(type)
click to toggle source
# File lib/net/vnc.rb, line 330 def read_packet(type) case type when 0 # ----------------------------------------------- FramebufferUpdate @fb.handle_response type if @fb when 1 # --------------------------------------------- SetColourMapEntries @fb.handle_response type if @fb when 2 # ------------------------------------------------------------ Bell nil # not support when 3 # --------------------------------------------------- ServerCutText socket.read 3 # discard padding bytes len = socket.read(4).unpack1('N') @mutex.synchronize { @clipboard = socket.read len } else warn 'unhandled server packet type - %d' % type end end