349 lines
10 KiB
Ruby
Executable File
349 lines
10 KiB
Ruby
Executable File
#
|
|
# File:: monitor.rb
|
|
# Description:: Utility class to monitor a perforce folder
|
|
# for changes and trigger an action on that change
|
|
#
|
|
# Author:: Greg Smith <greg.smith@rockstarnorth.com>
|
|
# Author:: David Muir <david.muir@rockstarnorth.com>
|
|
# Date:: 17 July 2008
|
|
#
|
|
# == Example Usage
|
|
#
|
|
# path = '//depot/jimmy/build/independent/...'
|
|
# monitor = SCM::Monitor.new( path, server, client, user, '', 'monitor.xml' )
|
|
#
|
|
# # Start continual polling loop, until monitor.stop called, this loop could
|
|
# # run in its own thread.
|
|
# monitor.poll() do |changelist, files, skipped|
|
|
# end
|
|
#
|
|
# # Poll SCM repository once, if you are monitoring multiple repositories
|
|
# # or paths and act on them you likely want this version (e.g. Codebuilder)
|
|
# monitor.poll_once() do |changelist, files, skipped|
|
|
# end
|
|
#
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Uses
|
|
#-----------------------------------------------------------------------------
|
|
require 'pipeline/config/projects'
|
|
require 'pipeline/resourcing/convert'
|
|
require 'pipeline/scm/perforce'
|
|
require 'pipeline/util/thread'
|
|
require 'rexml/document'
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Entry-Point
|
|
#-----------------------------------------------------------------------------
|
|
|
|
module Pipeline
|
|
module SCM
|
|
|
|
#
|
|
# == Description
|
|
# The MonitorStatus class is an object created by the Monitor class that
|
|
# stores the current Perforce changelist.
|
|
#
|
|
class MonitorStatus
|
|
attr_reader :filename
|
|
attr_accessor :current_change_list
|
|
attr_accessor :maximum_change_list
|
|
|
|
NODE_ROOT = 'local'
|
|
ATTR_CHANGELIST = 'changelist'
|
|
|
|
def initialize( filename )
|
|
@filename = filename
|
|
reload( )
|
|
end
|
|
|
|
#
|
|
# Return the REXML::Element for the status XML file. This allows
|
|
# builder apps to add additional data into the XML file. Be very
|
|
# careful you do not overwrite the root node's changelist attribute,
|
|
# otherwise bad things will happen.
|
|
#
|
|
# Maybe in the future we will present a better interface.
|
|
#
|
|
def xmlroot( )
|
|
@xmldoc.root
|
|
end
|
|
|
|
#
|
|
# Force a reload of the XML disk file.
|
|
#
|
|
def reload( )
|
|
|
|
if ( ::File::exists?( @filename ) ) then
|
|
File.open( @filename ) do |file|
|
|
|
|
@xmldoc = REXML::Document.new( file )
|
|
@current_change_list = @xmldoc.root.attributes[ATTR_CHANGELIST]
|
|
@maximum_change_list = @current_change_list
|
|
end
|
|
else
|
|
@current_change_list = 0
|
|
@maximum_change_list = 0
|
|
|
|
@xmldoc = REXML::Document.new( )
|
|
@xmldoc << REXML::XMLDecl.new
|
|
@xmldoc.add_element( NODE_ROOT )
|
|
@xmldoc.root.add_attribute( ATTR_CHANGELIST, @current_change_list.to_s )
|
|
save( )
|
|
end
|
|
end
|
|
|
|
#
|
|
# Save the current status to XML disk file.
|
|
#
|
|
def save( )
|
|
|
|
# Update XML Document
|
|
@xmldoc.root.attributes[ATTR_CHANGELIST] = @current_change_list.to_s
|
|
|
|
# Flush to disk
|
|
File.open( @filename, "w+" ) do |file|
|
|
@xmldoc.write( file, 4 )
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# == Description
|
|
#
|
|
class Monitor
|
|
DEF_MAX_CHANGE_JUMP = 50
|
|
DEF_POLL_TIME = 60.0
|
|
DEF_MAX_CHANGE_LIST = 9999999999
|
|
|
|
attr_reader :p4 # SCM::Perforce object.
|
|
attr_reader :running
|
|
attr_reader :max_jump
|
|
attr_reader :poll_time
|
|
attr_reader :root_folder # Directory being monitored.
|
|
attr_reader :status # MonitorStatus object.
|
|
|
|
#---------------------------------------------------------------------
|
|
# Virtual Attributes
|
|
#---------------------------------------------------------------------
|
|
def current_change_list( )
|
|
return ( @status.current_change_list )
|
|
end
|
|
|
|
def maximum_change_list( )
|
|
return ( @status.maximum_change_list )
|
|
end
|
|
|
|
#
|
|
# Monitor constructor, specifying Perforce server details including
|
|
# root folder to watch (note: append '/...' for recursive watch).
|
|
#
|
|
def initialize( sc_root, # P4 path to monitor
|
|
sc_server, # P4 server:port string
|
|
sc_clientspec, # P4 clientspec name to use
|
|
sc_username, # P4 username
|
|
config_filename,
|
|
max_jump = DEF_MAX_CHANGE_JUMP,
|
|
poll_time = DEF_POLL_TIME,
|
|
connected = false )
|
|
|
|
@status = MonitorStatus.new( config_filename )
|
|
@max_jump = max_jump
|
|
@poll_time = poll_time
|
|
@c = Pipeline::Config.instance
|
|
@root_folder = sc_root
|
|
|
|
@p4 = SCM::Perforce.new unless connected
|
|
@p4 = SCM::Perforce_Connected.new if connected
|
|
|
|
@p4.exception_level = P4::RAISE_ERRORS
|
|
@p4.user = sc_username
|
|
@p4.client = sc_clientspec
|
|
@p4.port = sc_server
|
|
|
|
Monitor.log().debug( "Connecting to #{@p4.port}, #{@p4.client}..." )
|
|
@p4.connect
|
|
Monitor.log().debug( "Connected? #{@p4.connected?}" )
|
|
end
|
|
|
|
#
|
|
# Start the monitor with an optional block, continually polling the
|
|
# Perforce server for changes. The block takes four arguments:
|
|
# Perforce object, change list ID, P4 sync output hash, and skipped bool.
|
|
#
|
|
def poll( &block )
|
|
@running = true
|
|
|
|
while ( @running ) do
|
|
|
|
self.poll_until_head( &block )
|
|
Kernel.sleep( @poll_time )
|
|
end
|
|
end
|
|
|
|
#
|
|
# Start the monitor with an optional block, only polling the Perforce
|
|
# server once for changes and processing all of those changes.
|
|
#
|
|
def poll_until_head( &block )
|
|
begin
|
|
@p4.connect( )
|
|
|
|
out = @p4.run_changes( '-ssubmitted', '-l', @root_folder + "@#{@status.current_change_list},@#{DEF_MAX_CHANGE_LIST}" )
|
|
@status.maximum_change_list = out[0]['change'].to_i unless ( out.nil? or (not out) or ( 0 == out.size ) )
|
|
Monitor.log().debug( "#{out.size-1} changelists behind HEAD" ) unless ( out.nil? or (not out) or ( 0 == out.size ) )
|
|
|
|
if ( out.size > @max_jump ) then
|
|
# Skip to most recent changelist
|
|
update_to( out[0], true, &block )
|
|
elsif ( out.size > 1 ) then
|
|
# Update to head.
|
|
out.reverse.each do |change|
|
|
update_to( change, false, &block )
|
|
end
|
|
end
|
|
@p4.disconnect( )
|
|
|
|
rescue Exception => ex
|
|
puts "Exception in Monitor.poll_once(): #{ex.message}"
|
|
puts ex.backtrace.join( "\n" )
|
|
end
|
|
end
|
|
|
|
#
|
|
# Start the monitor with an optional block, only polling the Perforce
|
|
# server once for changes. The block takes four arguments:
|
|
# Perforce object, change list ID, P4 sync output hash, and skipped bool.
|
|
#
|
|
def poll_once( &block )
|
|
|
|
begin
|
|
@p4.connect( )
|
|
|
|
out = @p4.run_changes( '-ssubmitted', '-l', @root_folder + "@#{@status.current_change_list},@#{DEF_MAX_CHANGE_LIST}" )
|
|
@status.maximum_change_list = out[0]['change'].to_i unless ( out.nil? or (not out) or ( 0 == out.size ) )
|
|
Monitor.log().debug( "#{out.size-1} changelists behind HEAD" ) unless ( out.nil? or (not out) or ( 0 == out.size ) )
|
|
|
|
if ( out.size > @max_jump ) then
|
|
# Skip to most recent changelist
|
|
update_to( out[0], true, &block )
|
|
elsif ( out.size > 1 ) then
|
|
# Update to next changelist.
|
|
update_to( out[out.size-2], false, &block )
|
|
end
|
|
|
|
@p4.disconnect( )
|
|
|
|
rescue Exception => ex
|
|
puts "Exception in Monitor.poll_once(): #{ex.message}"
|
|
puts ex.backtrace.join( "\n" )
|
|
end
|
|
end
|
|
|
|
#
|
|
# Sync our root folder to its head revision. The block takes the same
|
|
# four arguments that the poll methods take.
|
|
#
|
|
def sync_to_head( &block )
|
|
|
|
begin
|
|
@p4.connect() unless ( @p4.connected?() )
|
|
|
|
out = @p4.run_changes( '-ssubmitted', '-L', @root_folder + "#head" )
|
|
@status.maximum_change_list = out[0]['change'].to_i unless ( out.nil? or ( not out ) or ( 0 == out.size ) )
|
|
Monitor.log().debug( "Syncing to HEAD." )
|
|
|
|
update_to( out[0], true, &block ) if ( out and out.size > 0 )
|
|
|
|
rescue Exception => ex
|
|
puts "Exception in Monitor.sync_to_head(): #{ex.message}"
|
|
puts ex.backtrace.join( "\n" )
|
|
end
|
|
end
|
|
|
|
#
|
|
# Stop the monitor on its next iteration. This might not stop the
|
|
# monitor immediately as it might be blocking in a Kernel.sleep().
|
|
#
|
|
def stop( )
|
|
@running = false
|
|
@p4.disconnect( ) if ( @p4.connected?() )
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
# Class Methods
|
|
#---------------------------------------------------------------------
|
|
|
|
#
|
|
# Return a Monitor instance from a P4 object.
|
|
#
|
|
def Monitor::from_p4( p4, sc_root, config_filename, max_jump = DEF_MAX_CHANGE_JUMP, poll_time = DEF_POLL_TIME )
|
|
throw ArgumentError.new( "Invalid P4 object specified (#{p4.class})." ) \
|
|
unless ( p4.is_a?( P4 ) )
|
|
|
|
Monitor.new( sc_root, p4.port, p4.client, p4.user, config_filename, max_jump, poll_time )
|
|
end
|
|
|
|
#
|
|
# Return the Monitor class logger object.
|
|
#
|
|
def Monitor::log()
|
|
|
|
@@log = Log.new( 'monitor' ) if @@log.nil?
|
|
@@log
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
# Protected
|
|
#---------------------------------------------------------------------
|
|
protected
|
|
@@log = nil
|
|
|
|
#
|
|
# Sync our root folder to a specific changelist.
|
|
#
|
|
def update_to( change, skipped )
|
|
Monitor.log().debug( "update_to : #{@root_folder} @#{change['change']}" )
|
|
Monitor.log().debug( "connected? #{@p4.connected?}" )
|
|
|
|
change_list = change["change"]
|
|
return if ( change_list == @status.current_change_list )
|
|
|
|
out = nil
|
|
begin
|
|
1.upto(2) do |try|
|
|
|
|
begin
|
|
case try
|
|
when 1
|
|
out = @p4.run_sync(@root_folder + "@#{change_list}")
|
|
break
|
|
when 2
|
|
out = @p4.run_sync("-f",@root_folder + "@#{change_list}")
|
|
break
|
|
end
|
|
rescue P4Exception => ex
|
|
puts "P4Exception: #{ex.message}"
|
|
|
|
if try == 1 then
|
|
Monitor.log().warn( "Sync failed to get change list #{change_list} A force get will now be issued. #{ex.inspect} #{ex.message}" )
|
|
elsif try == 2 then
|
|
Monitor.log().error( "Failed to get change list when force getting - it is possible the file is locked. #{change_list} #{ex.inspect} #{ex.message}" )
|
|
end
|
|
end
|
|
end
|
|
ensure
|
|
@status.current_change_list = change_list
|
|
@status.save( )
|
|
end
|
|
|
|
something_synced = ( out.is_a?( Array ) and ( out.size > 0 ) )
|
|
yield( change, out, skipped ) if ( block_given? )
|
|
end
|
|
end
|
|
|
|
end # SCM module
|
|
end # Pipeline module
|
|
|
|
# monitor.rb
|