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

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

button_down(which = :left, options = {}) click to toggle source
# File lib/net/vnc.rb, line 247
def button_down(which = :left, options = {})
  button = BUTTON_MAP[which] || which
  raise ArgumentError, 'Invalid button - %p' % which unless (0..2).include?(button)

  pointer.button |= 1 << button
  wait options
end
button_press(button = :left, options = {}) { || ... } click to toggle source
# File lib/net/vnc.rb, line 240
def button_press(button = :left, options = {})
  button_down button, options
  yield if block_given?
ensure
  button_up button, options
end
button_up(which = :left, options = {}) click to toggle source
# File lib/net/vnc.rb, line 255
def button_up(which = :left, options = {})
  button = BUTTON_MAP[which] || which
  raise ArgumentError, 'Invalid button - %p' % which unless (0..2).include?(button)

  pointer.button &= ~(1 << button)
  wait options
end
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