#
# Short help/usage:
# /jobadd hour minute day_of_month month day_of_week command
# Possibile switches for jobadd:
# -disabled
# -server
# -
# /jobs [-v]
# /jobdel [-finished] | job_number
# /jobdisable job_number
# /jobenable job_number
# /jobssave
# /jobsload
#
# Examples of usage:
# /jobadd 17 45 * * * /echo This will be executed at 17:45
# /jobadd -5 17 45 * * * /echo The same as above but only 5 times
# /jobadd * 05 * * * /echo Execute this every hour 5 minutes after the hour
# /jobadd */6 0 * * * /echo Execute at 0:0, 6:0, 12:0, 18:0
# /jobadd * */30,45 * * * /echo Execute every hour at 00, 30, 45 minute
# /jobadd * 1-15/5 * * * /echo at 1,6,11
#
# The servertag in -server usually is name from /ircnet, but
# should work with servers not in any ircnet (hmm probably)
#
# The format was taken from crontab(5).
# The only differences are:
# 1) hour field is before minute field (why the hell minute is first in
# crontab?). But this could be changed in final version.
# 2) day of week is 0..6. 0 is Sunday, 1 is Monday, 6 is Saturday.
# 7 is illegal value while in crontab it's the same as 0 (i.e. Sunday).
# I might change this, depends on demand.
# 3) you can't use names in month and day of week. You must use numbers
# Type 'man 5 crontab' to know more about allowed values etc.
#
# TODO:
# - add full (or almost full) cron functionality
# - probably more efficient checking for job in timeout
# - imput data validation
# ? should we remember if the server was given with -server
#
# Changelog:
# 0.12 (2014.11.12)
# Automatically load jobs when loaded
#
# 0.11 (2004.12.12)
# Job are executed exactly at the time (+- 1s), not up to 59s late
#
# 0.10 (2003.03.25):
# Added - to execute job only times. Initial patch from
# Marian Schubert (M dot Schubert at sh dot cvut dot cz)
#
# 0.9:
# Bugfix: according to crontab(5) when both DoM and DoW are restricted
# it's enough to only one of fields to match
#
# 0.8:
# Added -disabled to /jobadd
# Added jobs loading and saving to file
#
# 0.7:
# Bugfixes. Should work now ;)
#
# 0.6:
# Added month, day of month, day of week
#
# 0.5:
# Initial testing release
#
use Irssi;
use strict;
use vars qw($VERSION %IRSSI);
$VERSION = "0.12";
%IRSSI = (
authors => 'Piotr Krukowiecki',
contact => 'piotr \at/ krukowiecki /dot\ net',
name => 'cron aka jobs',
description => 'cron implementation, allows to execute commands at given interval/time',
license => 'GNU GPLv2',
changed => '2004.12.12',
url => 'http://www.krukowiecki.net/code/irssi/'
);
my @jobs = ();
my $seconds = (gmtime(time()))[0];
my $timeout_tag;
my $stop_timeout_tag;
if ($seconds > 0) {
$stop_timeout_tag = Irssi::timeout_add((60-$seconds)*1000,
sub {
Irssi::timeout_remove($stop_timeout_tag);
$timeout_tag = Irssi::timeout_add(60000, 'sig_timeout', undef);
}, undef);
} else {
$timeout_tag = Irssi::timeout_add(60000, 'sig_timeout', undef);
}
my $savefile = Irssi::get_irssi_dir() . "/cron.save";
# First arg - current hour or minute.
# Second arg - hour or minute specyfications.
sub time_matches($$) {
my ($current, $spec) = @_;
foreach my $h (split(/,/, $spec)) {
if ($h =~ /(.*)\/(\d+)/) { # */number or number-number/number
my $step = $2;
if ($1 eq '*') { # */number
return 1 if ($current % $step == 0);
next;
}
if ($1 =~ /(\d+)-(\d+)/) { # number-number/number
my ($from, $to) = ($1, $2);
next if ($current < $from or $current > $to); # not in range
my $current = $current;
if ($from > 0) { # shift time
$to -= $from;
$current -= $from;
$from = 0;
}
return 1 if ($current % $step == 0);
next;
}
next;
}
if ($h =~ /(\d+)-(\d+)/) { # number-number
return 1 if ($current >= $1 and $current <= $2);
next
}
return 1 if ($h eq '*' or $h == $current); # '*' or exact hour
}
return 0;
}
sub sig_timeout {
my $ctime = time();
my ($cminute, $chour, $cdom, $cmonth, $cdow) = (localtime($ctime))[1,2,3,4,6];
$cmonth += 1;
foreach my $job (@jobs) {
next if ($job->{'disabled'});
next if ($job->{'repeats'} == 0);
next if (not time_matches($chour, $job->{'hour'}));
next if (not time_matches($cminute, $job->{'minute'}));
next if (not time_matches($cmonth, $job->{'month'}));
if ($job->{'dom'} ne '*' and $job->{'dow'} ne '*') {
next if (not (time_matches($cdom, $job->{'dom'}) or
time_matches($cdow, $job->{'dow'})));
} else {
next if (not time_matches($cdom, $job->{'dom'}));
next if (not time_matches($cdow, $job->{'dow'}));
}
my $server = Irssi::server_find_tag($job->{'server'});
if (!$server) {
Irssi::print("cron.pl: could not find server '$job->{server}'");
next;
}
$server->command($job->{'commands'});
if ($job->{'repeats'} > 0) {
$job->{'repeats'} -= 1;
}
}
}
sub cmd_jobs {
my ($data, $server, $channel) = @_;
my $verbose = ($data eq '-v');
Irssi::print("Current Jobs:");
foreach (0 .. $#jobs) {
my $repeats = $jobs[$_]{'repeats'};
my $msg = "$_) ";
if (!$verbose) {
next if ($repeats == 0);
$msg .= "-$repeats " if ($repeats != -1);
} else {
$msg .= "-$repeats " if ($repeats != -1);
}
$msg .= ($jobs[$_]{'disabled'}?"-disabled ":"")
."-server $jobs[$_]{server} "
."$jobs[$_]{hour} $jobs[$_]{minute} $jobs[$_]{dom} "
."$jobs[$_]{month} $jobs[$_]{dow} "
."$jobs[$_]{commands}";
Irssi::print($msg);
}
Irssi::print("End of List");
}
# /jobdel job_number
sub cmd_jobdel {
my ($data, $server, $channel) = @_;
if ($data eq "-finished") {
foreach (reverse(0 .. $#jobs)) {
if ($jobs[$_]{'repeats'} == 0) {
splice(@jobs, $_, 1);
Irssi::print("Removed Job #$_");
}
}
return;
} elsif ($data !~ /\d+/ or $data < 0 or $data > $#jobs) {
Irssi::print("Bad Job Number");
return;
}
splice(@jobs, $data, 1);
Irssi::print("Removed Job #$data");
}
# /jobdisable job_number
sub cmd_jobdisable {
my ($data, $server, $channel) = @_;
if ($data < 0 || $data > $#jobs) {
Irssi::print("Bad Job Number");
return;
}
$jobs[$data]{'disabled'} = 1;
Irssi::print("Disabled job number $data");
}
# /jobenable job_number
sub cmd_jobenable {
my ($data, $server, $channel) = @_;
if ($data < 0 || $data > $#jobs) {
Irssi::print("Bad Job Number");
return;
}
$jobs[$data]{'disabled'} = 0;
Irssi::print("Enabled job number $data");
}
# /jobadd [-X] [-disabled] [-server servertag] hour minute day_of_month month day_of_week command
sub cmd_jobadd {
my ($data, $server, $channel) = @_;
$server = $server->{tag};
my $disabled = 0;
my $repeats = -1;
while ($data =~ /^\s*-/) {
if ($data =~ s/^\s*-disabled\s+//) {
$disabled = 1;
next;
}
if ($data =~ s/^\s*-(\d+)\s+//) {
$repeats = $1;
next;
}
my $comm;
($comm, $server, $data) = split(' ', $data, 3);
if ($comm ne '-server') {
Irssi::print("Bad switch: '$comm'");
return;
}
}
my ($hour, $minute, $dom, $month, $dow, $commands) = split(' ', $data, 6);
push (@jobs, { 'hour' => $hour, 'minute' => $minute, 'dom' => $dom,
'month' => $month, 'dow' => $dow,
'server' => $server, 'commands' => $commands,
'disabled' => $disabled, 'repeats' => $repeats } );
Irssi::print("Job added");
}
sub cmd_jobssave {
if (not open (FILE, ">", $savefile)) {
Irssi::print("Could not open file '$savefile': $!");
return;
}
foreach (0 .. $#jobs) {
next if ($jobs[$_]->{'repeats'} == 0); # don't save finished jobs
print FILE
($jobs[$_]->{'repeats'}>0 ? "-$jobs[$_]->{'repeats'} " : "")
. ($jobs[$_]{'disabled'}?"-disabled ":"")
."-server $jobs[$_]{server} "
."$jobs[$_]{hour} $jobs[$_]{minute} $jobs[$_]{dom} "
."$jobs[$_]{month} $jobs[$_]{dow} "
."$jobs[$_]{commands}\n";
}
close FILE;
Irssi::print("Jobs saved");
}
sub cmd_jobsload {
if (not open (FILE, q{<}, $savefile)) {
Irssi::print("Could not open file '$savefile': $!");
return;
}
@jobs = ();
while () {
chomp;
cmd_jobadd($_, undef, undef);
}
close FILE;
Irssi::print("Jobs loaded");
}
cmd_jobsload();
Irssi::command_bind('jobs', 'cmd_jobs', 'Cron');
Irssi::command_bind('jobadd', 'cmd_jobadd', 'Cron');
Irssi::command_bind('jobdel', 'cmd_jobdel', 'Cron');
Irssi::command_bind('jobdisable', 'cmd_jobdisable', 'Cron');
Irssi::command_bind('jobenable', 'cmd_jobenable', 'Cron');
Irssi::command_bind('jobssave', 'cmd_jobssave', 'Cron');
Irssi::command_bind('jobsload', 'cmd_jobsload', 'Cron');
# vim:noexpandtab:ts=4