Files
2025-09-29 00:52:08 +02:00

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