# # File:: monitor.rb # Description:: Utility class to monitor a perforce folder # for changes and trigger an action on that change # # Author:: Greg Smith # Author:: David Muir # 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