#! /usr/bin/env ruby

# Copyright (C) 2001 FUJITSU LABORATRIES LTD.
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. Neither the name of the project nor the names of its contributors
#    may be used to endorse or promote products derived from this software
#    without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

# Usage: xcgroup <-b baseURL> <-g group> [-A local-IPv6-address]

HTTPUserAgent = 'XCGroup/Ruby 0.0'
HTTPInterval = 30

require 'thread'
require 'net/http'
require 'mbus'

if RUBY_PLATFORM =~ /mswin32/
  pid = 0
  getlocal = 'getlocal_win32'
elsif RUBY_PLATFORM =~ /linux$/
  # XXX SSD/Linux couldn't be supported in this way...
  pid = Process.pid
  getlocal = 'getlocal_linux'
else
  pid = Process.pid
  getlocal = 'getlocal_unix'
end

MbusAddress = sprintf("(media:control module:control app:xcgroup id:%u)",
		      pid)

def usage
  $stderr.print("Usage: xcgroup <-b baseURL> <-g group>" +
		" [-A local-IPv6-address]\n")
end

def getlocal_unix
  a = nil
  `/sbin/ifconfig -au inet6`.each do |l|
    m = l.chop.split
    if m[0] != 'inet6'
      next
    elsif m[1] =~ /^fe80/ 
      next
    elsif m[1] =~ /^::1/
      next
    elsif m[1] =~ /^fe[c-f]/
      # site-local, not the best match
      a = m[1]
    else
      a = m[1]
      break	# seems the best match, break
    end
  end
  a
end

def getlocal_linux
  a = nil
  `/sbin/ifconfig`.each do |l|
    m = l.chop.split
    if m[0] != 'inet6'
      next
    elsif m[3] = 'Scope:Global'
      a = m[2].split('/')[0]
      break	# seems the best match, break
    elsif m[3] = 'Scope:Site'
      # site-local, not the best match
      a = m[2].split('/')[0]
    end
  end
  a
end

def getlocal_win32
  `ipv6 if`.each do |l|
    m = l.chop.split
    if m[0] == 'preferred' and m[1] != 'link-local'
      return m[2].chop			# remove a trailing comma
    end
  end
  nil
end

class XCGroupClient
  def initialize(url, group, laddr)
    queryurl = sprintf("%s?group=%s", url, group)

    @timestamp = Time::at(0)
    @local = laddr
    if @local.nil?
      @local = eval(getlocal)
    end
    queryurl += sprintf("&addr=%s", @local)

    # We desparately need the URI class.
    @httphost = nil
    @httpport = 80
    @httppath = nil
    if m = %r<http://([^/]+)>.match(queryurl)
      @httphost = m[1]
      @httppath = m.post_match
      if m = %r<:([^:]+)>.match(@httphost)
	@httphost = m.pre_match
	@httpport = m[1]
      end
    else
      $stderr.printf("Can't parse URL string: %s\n", queryurl)
    end
  end

  def members
    @timestamp = Time::now
    mbr = []
    Net::HTTP.start(@httphost, @httpport) {|http|
      resp , = http.get(@httppath + "&cmd=join",
			'User-Agent' => HTTPUserAgent)
      resp.body.each do |l|
	l.chop!
	if l =~ /^!ERROR/
	  $stderr.print("HTTP Access Error\n")
	  return []
	else
	  mbr.push(l) if l != @local
	end
      end
    }
    if mbr.empty?
      mbr.push(@local)
    end
    mbr
  end

  def leave
    Net::HTTP.start(@httphost, @httpport) {|http|
      resp , = http.get(@httppath + "&cmd=leave",
			'User-Agent' => HTTPUserAgent)
    }
  end

  def timeout?
     Time::now > @timestamp + HTTPInterval
  end
end

class MbusSender
  def initialize
    @mbus = Mbus.new(nil, nil, MbusAddress)
    @mbus.heartbeat(1)
  end

  def keepalive
    @mbus.heartbeat(1)
    @mbus.recv(@mbus, 50)
    @mbus.retransmit
  end

  def announce(member)
    @mbus.qmsg("()", "xcgroup.member", sprintf("\"%s\"", member.join(',')), 
	       false);
    @mbus.send
  end
end

#######################################################################

baseurl = nil
group = nil
laddr = nil
members = nil

while arg = ARGV.shift
  case arg
  when '-b'
    baseurl = ARGV.shift
  when '-g'
    group = ARGV.shift
  when '-A'
    laddr = ARGV.shift
  when '-h'
    usage
    exit(1)
  end
end

if baseurl.nil?
  $stderr.print("You should specify the URL of group management server.\n")
  usage
  exit(1)
end

if group.nil?
  $stderr.print("You should specify the group.\n")
  usage
  exit(1)
end

queryurl = sprintf("%s?group=%s&cmd=join", baseurl, group)

if laddr.nil?
  laddr = eval(getlocal)
end

queryurl += sprintf("&addr=%s", laddr)

mb = MbusSender.new
mb.keepalive

xc = XCGroupClient.new(baseurl, group, laddr)

begin
  while true
    if xc.timeout?
      mb.announce(xc.members)
    end
    mb.keepalive
  end
ensure
  xc.leave
end
