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 77
def initialize display=':0', options={}
  @server = 'localhost'
  if display =~ /^(.*)(:\d+)$/
    @server, display = $1, $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 94
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 248
def button_down which=:left, options={}
  button = BUTTON_MAP[which] || which
  raise ArgumentError, 'Invalid button - %p' % which unless (0..2) === button
  pointer.button |= 1 << button
  wait options
end
button_press(button=:left, options={}) { || ... } click to toggle source
# File lib/net/vnc.rb, line 239
def button_press button=:left, options={}
  begin
    button_down button, options
    yield if block_given?
  ensure
    button_up button, options
  end
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) === button
  pointer.button &= ~(1 << button)
  wait options
end
clipboard() { || ... } click to toggle source
# File lib/net/vnc.rb, line 299
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 315
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 275
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 111
def connect
  @socket = TCPSocket.open(server, port)
  unless socket.read(12) =~ /^RFB (\d{3}.\d{3})\n$/
    raise 'invalid server response'
  end
  @server_version = $1
  socket.write "RFB 003.003\n"
  data = socket.read(4)
  auth = data.to_s.unpack('N')[0]
  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.unpack('N')[0]
    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.unpack('n')[0].to_i
  @framebuffer_height = socket.read(2).to_s.unpack('n')[0].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.unpack('N')[0]
  @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 209
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 176
def key_press(*args)
  options = Hash === args.last ? 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 219
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 229
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 107
def port
  BASE_PORT + @display
end
reconnect() click to toggle source
# File lib/net/vnc.rb, line 286
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 266
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 160
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 271
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 361
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 192
def get_key_code(which)
  case which
  when String
    if which.length != 1
      raise ArgumentError, 'can only get key_code of single character strings'
    end
    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 345
def packet_reading_thread
  @packet_reading_state = :loop
  loop do
    begin
      break if @packet_reading_state != :loop
      next unless IO.select [socket], nil, nil, 2
      type = socket.read(1)[0]
      read_packet type.ord
    rescue
      warn "exception in packet_reading_thread: #{$!.class}:#{$!}\n#{$!.backtrace}"
      break
    end
  end
  @packet_reading_state = nil
end
read_packet(type) click to toggle source
# File lib/net/vnc.rb, line 328
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).unpack('N')[0]
    @mutex.synchronize { @clipboard = socket.read len }
  else
    warn 'unhandled server packet type - %d' % type
  end
end