+++ /dev/null
-package Catalyst::Plugin::Scheduler::Base;
-
-use Data::Dumper;
-use DateTime;
-use DateTime::Event::Cron;
-use DateTime::TimeZone;
-use File::stat;
-use Set::Scalar;
-use base qw/Catalyst::Plugin::Scheduler/;
-use Catalyst::Plugin::Scheduler::Event;
-
-__PACKAGE__->mk_classdata(_events => []);
-__PACKAGE__->mk_classdata(_event_class => 'Catalyst::Plugin::Scheduler::Event');
-__PACKAGE__->mk_classdata('_app' );
-
-=head1 NAME
-
-Catalyst::Plugin::Scheduler::Base - Base class for the Catalyst Scheduler
-
-=head1 SYNOPSIS
-
- MyApp->scheduler->schedule( at => '0 0 * * *', event => '/cron/ping' );
-
- ### return all scheduled events as ::Event objects
- @events = MyApp->scheduler->list_events;
-
- ### return all pending scheduled events as ::Event objects
- @pending = MyApp->scheduler->list_pending_events;
-
- ### a dump of the current scheduler state
- $aref = MyApp->scheduler->state;
-
-=head1 METHODS
-
-=head2 $bool = MyApp->scheduler->schedule
-
-Allows you to schedule events. For full usage and documentation, consult
-the C<Catalyst::Plugin::Scheduler> documentation on method C<schedule>.
-
-=cut
-
-sub schedule {
- my $self = shift;
- my $c = $self->_app;
- my %args = @_;
-
- ### XXX more input checks?
-
- unless ( $args{event} ) {
- Catalyst::Exception->throw(
- message => 'The schedule method requires an event parameter' );
- }
-
- ### default to '1'
- $args{'auto_run'} = 1 unless defined $args{'auto_run'};
-
- if ( $args{at} ) {
-
- # replace keywords that Set::Crontab doesn't support
- $args{at} = $self->_prepare_cron( $args{at} );
-
- # parse the cron entry into a DateTime::Set
- $args{set} = eval { DateTime::Event::Cron->from_cron( $args{at} ) };
-
- Catalyst::Exception->throw(
- "Scheduler: Unable to parse 'at' value $args{at}: $@"
- ) if $@;
-
- }
-
- my $who = $self->_caller_string;
- push @{ $self->_events },
- Catalyst::Plugin::Scheduler::Event->new( scheduled_by => $who, %args );
-
- return 1;
-}
-
-### create a caller string like: "package (file.pm:#line)"
-sub _caller_string { return sprintf "%s (%s:%s)", @{[caller(1)]}[0,1,2]; }
-
-=head2 @events = $c->scheduler->list_events;
-
-Returns an array of C<Catalyst::Plugin::Scheduler::Event> objects,
-representing all the scheduled events in this application.
-
-See the C<Catalyst::Plugin::Scheduler::Event> documentation on how to use
-these objects.
-
-=cut
-
-sub list_events {
- my $self = shift;
- return @{ $self->_events || [] };
-};
-
-=head2 @events = $c->scheduler->list_pending_events;
-
-Returns an array of C<Catalyst::Plugin::Scheduler::Event> objects,
-representing all the pending events in this application. They are the
-events that are due according to your cron specification, and will be run
-at the next dispatch, or can be run by you explicitly.
-
-See the C<Catalyst::Plugin::Scheduler::Event> documentation on how to use
-these objects.
-
-=cut
-
-sub list_pending_events {
- my $self = shift;
- my $c = $self->_app;
- my $tz = $self->_config('time_zone');
-
- ### there are no events scheduled?
- my @events = $self->list_events or return;
- my $now = DateTime->now( time_zone => $tz )->epoch;
-
- ### list of pending events
- my @pending;
-
- ### XXX need NEXT RUN TIME??
- EVENT:
- for my $event (@events) {
-
- ### this event is not active, so skip it
- next EVENT unless $event->active;
-
- ### the proper trigger is being called
- if( $event->trigger && $c->req->params->{schedule_trigger} &&
- $event->trigger eq $c->req->params->{schedule_trigger}
- ) {
-
- ### if you're not authorized to call the trigger, skip it
- next EVENT unless $self->_event_authorized;
-
- push @pending, $event;
- next EVENT;
- }
-
- ### we're due according to our cron-entry...
- if( $event->set ) {
- ### is the next run time now, or even before now?
- push @pending, $event if $event->next_run <= $now;
- }
- }
-
- ### sort them by priority
- return sort { $a->priority <=> $b->priority } @pending;
-}
-
-
-
-sub _run_events {
- my $self = shift;
- my $c = $self->_app;
- my %args = @_;
-
- $self->_check_yaml();
-
- # check if a minute has passed since our last check
- # This check is not run if the user is manually triggering an event
- if ( time - $self->_last_check_time < $self->_config('check_every') ) {
- return unless $c->req->params->{schedule_trigger};
- }
-
- my @events = $self->list_pending_events;
-
- ### update the 'checked' time and save the state, so no more
- ### processes are going to be running these events
- ### the small race condition between the 'list_pending_events' call
- ### and the updating of the check time is resolved by checking if a
- ### job is running before executing it, so at worst, we have several
- ### processes sharing the load of this cron run. --kane
- $self->_last_check_time( time );
-
- EVENT:
- for my $event ( @events ) {
-
- # do some security checking for non-auto-run events
- ### XXX move this to $event->run? --kane
- if ( !$event->auto_run ) {
- next EVENT unless $self->_event_authorized;
- }
-
- $event->run;
- }
-}
-
-=head2 $aref = MyApp->scheduler->state
-
-A dump of the current state of the scheudler. For full usage and
-documentation, consult the C<Catalyst::Plugin::Scheduler> documentation on
-method C<scheduler+state>.
-
-=cut
-
-sub state {
- my $self = shift;
- my $c = $self->_app;
-
- my $event_dump = [];
- for my $event ( $self->list_events ) {
- my $dump = {};
- for my $key ( qw/at trigger event auto_run/ ) {
- $dump->{$key} = $event->$key if $event->$key;
- }
-
- # display the next run time
- $dump->{next_run} = $event->next_run_as_string;
-
- # display the last run time
- $dump->{last_run} = $event->last_run_as_string;
-
- # display the result of the last run
- my $output = $event->output;
- if ( $output ) {
- $dump->{last_output} = $output;
- }
-
- push @{$event_dump}, $dump;
- }
-
- return $event_dump;
-}
-
-sub _config {
- my $self = shift;
- my $key = shift;
- my $c = $self->_app;
- my $conf = $c->config->{scheduler};
- my $rv = $key ? $conf->{$key} : $conf;
-
- return $rv;
-}
-
-### shorthand form
-sub _last_check_time {
- my $self = shift;
- return $self->_event_class->_last_check_time( @_ );
-}
-
-# check and reload the YAML file with schedule data
-sub _check_yaml {
- my $self = shift;
- my $c = $self->_app;
-
- $self->_event_class->_get_event_state();
-
- # each process needs to load the YAML file independently
- if ( $self->_event_class->_event_state->{yaml_mtime}->{$$} ||= 0 ) {
- return if ( time - $self->_last_check_time < 60 );
- }
-
- my $file = $self->_config('yaml_file');
- return unless -e $file;
-
- eval {
- my $mtime = ( stat $file )->mtime;
- if ( $mtime > $self->_event_class->_event_state->{yaml_mtime}->{$$} ) {
- $self->_event_class->_event_state->{yaml_mtime}->{$$} = $mtime;
-
- # clean up old PIDs listed in yaml_mtime
- for my $pid (
- keys %{ $self->_event_class->_event_state->{yaml_mtime} }
- ) {
- delete $self->_event_class->_event_state->{yaml_mtime}->{$pid}
- if $self->_event_class->_event_state->{yaml_mtime}->{$pid}
- < $mtime
- }
- $self->_event_class->_save_event_state();
-
- # wipe out all current events and reload from YAML
- $self->_events( [] );
-
- my $yaml;
-
- eval { require YAML::Syck; };
- if( $@ ) {
- require YAML;
- $yaml = YAML::LoadFile( "$file" );
- }
- else {
- open( my $fh, $file ) or die $!;
- my $content = do { local $/; <$fh> };
- close $fh;
- $yaml = YAML::Syck::Load( $content );
- }
-
- foreach my $event ( @{$yaml} ) {
- $self->schedule( %{$event} );
- }
-
- $c->log->info( "Scheduler: PID $$ loaded "
- . scalar @{$yaml}
- . ' events from YAML file' )
- if $self->_config('logging');
- }
- };
-
- $c->log->error("Scheduler: Error reading YAML file: $@") if $@;
-}
-
-# Detect the current time zone
-sub _detect_timezone {
- my $self = shift;
- my $c = $self->_app;
-
- my $tz;
- eval { $tz = DateTime::TimeZone->new( name => 'local' ) };
- if ($@) {
- $c->log->warn(
- 'Scheduler: Unable to autodetect local time zone, using UTC')
- if $self->_config('logging');
- return 'UTC';
- }
- else {
- $c->log->debug(
- 'Scheduler: Using autodetected time zone: ' . $tz->name )
- if $self->_config('logging');
- return $tz->name;
- }
-}
-
-# Check for authorized users on non-auto events
-sub _event_authorized {
- my $self = shift;
- my $c = $self->_app;
-
- # this should never happen, but just in case...
- return unless $c->req->address;
-
- my $hosts_allow = $self->_config('hosts_allow');
- $hosts_allow = [$hosts_allow] unless ref($hosts_allow) eq 'ARRAY';
- my $allowed = Set::Scalar->new( @{$hosts_allow} );
-
- return $allowed->contains( $c->req->address );
-}
-
-# Set::Crontab does not support day names, or '@' shortcuts
-{ my %replace = (
- jan => 1,
- feb => 2,
- mar => 3,
- apr => 4,
- may => 5,
- jun => 6,
- jul => 7,
- aug => 8,
- sep => 9,
- 'oct' => 10,
- nov => 11,
- dec => 12,
-
- sun => 0,
- mon => 1,
- tue => 2,
- wed => 3,
- thu => 4,
- fri => 5,
- sat => 6,
- );
-
- my %replace_at = (
- 'yearly' => '0 0 1 1 *',
- 'annually' => '0 0 1 1 *',
- 'monthly' => '0 0 1 * *',
- 'weekly' => '0 0 * * 0',
- 'daily' => '0 0 * * *',
- 'midnight' => '0 0 * * *',
- 'hourly' => '0 * * * *',
- 'always' => '* * * * *',
- );
-
- sub _prepare_cron {
- my $self = shift;
- my $c = $self->_app;
- my $cron = shift;
-
- return $cron unless $cron =~ /\w/;
-
- if ( $cron =~ /^\@/ ) {
- $cron =~ s/^\@//;
- return $replace_at{ $cron };
- }
-
- for my $name ( keys %replace ) {
- my $value = $replace{$name};
- $cron =~ s/$name/$value/i;
- last unless $cron =~ /\w/;
- }
-
- return $cron;
- }
-}
-
-1;
-
-__END__
-
-=head1 SEE ALSO
-
-C<Catalyst::Plugin::Scheduler>, C<Catalyst::Plugin::Scheduler::Event>,
-
-=cut
-