1#
2# Short help/usage:
3# /jobadd hour minute day_of_month month day_of_week command
4# Possibile switches for jobadd:
5#	-disabled
6#	-server <tag>
7#	-<number>
8# /jobs [-v]
9# /jobdel [-finished] | job_number
10# /jobdisable job_number
11# /jobenable job_number
12# /jobssave
13# /jobsload
14#
15# Examples of usage:
16# /jobadd 17 45 * * * /echo This will be executed at 17:45
17# /jobadd -5 17 45 * * * /echo The same as above but only 5 times
18# /jobadd * 05 * * * /echo Execute this every hour 5 minutes after the hour
19# /jobadd */6 0 * * * /echo Execute at 0:0, 6:0, 12:0, 18:0
20# /jobadd * */30,45 * * * /echo Execute every hour at 00, 30, 45 minute
21# /jobadd * 1-15/5 * * * /echo at 1,6,11
22#
23# The servertag in -server usually is name from /ircnet, but
24# should work with servers not in any ircnet (hmm probably)
25#
26# The format was taken from crontab(5).
27# The only differences are:
28# 1) hour field is before minute field (why the hell minute is first in
29# 	crontab?). But this could be changed in final version.
30# 2) day of week is 0..6. 0 is Sunday, 1 is Monday, 6 is Saturday.
31# 	7 is illegal value while in crontab it's the same as 0 (i.e. Sunday).
32# 	I might change this, depends on demand.
33# 3) you can't use names in month and day of week. You must use numbers
34# Type 'man 5 crontab' to know more about allowed values etc.
35#
36# TODO:
37# 	- add full (or almost full) cron functionality
38# 	- probably more efficient checking for job in timeout
39# 	- imput data validation
40# 	? should we remember if the server was given with -server
41#
42# Changelog:
43#       0.12 (2014.11.12)
44#       Automatically load jobs when loaded
45#
46#	0.11 (2004.12.12)
47#	Job are executed exactly at the time (+- 1s), not up to 59s late
48#
49#	0.10 (2003.03.25):
50#	Added -<number> to execute job only <number> times. Initial patch from
51#		Marian Schubert (M dot Schubert at sh dot cvut dot cz)
52#
53#	0.9:
54#	Bugfix: according to crontab(5) when both DoM and DoW are restricted
55#		it's enough to only one of fields to match
56#
57# 	0.8:
58# 	Added -disabled to /jobadd
59# 	Added jobs loading and saving to file
60#
61#	0.7:
62#	Bugfixes. Should work now ;)
63#
64#	0.6:
65#	Added month, day of month, day of week
66#
67# 	0.5:
68# 	Initial testing release
69#
70
71use Irssi;
72use strict;
73use vars qw($VERSION %IRSSI);
74
75$VERSION = "0.12";
76%IRSSI = (
77    authors	=> 'Piotr Krukowiecki',
78    contact	=> 'piotr \at/ krukowiecki /dot\ net',
79    name	=> 'cron aka jobs',
80    description	=> 'cron implementation, allows to execute commands at given interval/time',
81    license	=> 'GNU GPLv2',
82    changed	=> '2004.12.12',
83    url	=> 'http://www.krukowiecki.net/code/irssi/'
84);
85
86my @jobs = ();
87my $seconds = (gmtime(time()))[0];
88my $timeout_tag;
89my $stop_timeout_tag;
90if ($seconds > 0) {
91	$stop_timeout_tag = Irssi::timeout_add((60-$seconds)*1000,
92		sub {
93			Irssi::timeout_remove($stop_timeout_tag);
94			$timeout_tag = Irssi::timeout_add(60000, 'sig_timeout', undef);
95		}, undef);
96} else {
97	$timeout_tag = Irssi::timeout_add(60000, 'sig_timeout', undef);
98}
99my $savefile = Irssi::get_irssi_dir() . "/cron.save";
100
101# First arg - current hour or minute.
102# Second arg - hour or minute specyfications.
103sub time_matches($$) {
104	my ($current, $spec) = @_;
105	foreach my $h (split(/,/, $spec)) {
106		if ($h =~ /(.*)\/(\d+)/) { # */number or number-number/number
107			my $step = $2;
108			if ($1 eq '*') { # */number
109				return 1 if ($current % $step == 0);
110				next;
111			}
112			if ($1 =~ /(\d+)-(\d+)/) { # number-number/number
113				my ($from, $to) = ($1, $2);
114				next if ($current < $from or $current > $to);	# not in range
115				my $current = $current;
116				if ($from > 0) { # shift time
117					$to -= $from;
118					$current -= $from;
119					$from = 0;
120				}
121				return 1 if ($current % $step == 0);
122				next;
123			}
124			next;
125		}
126		if ($h =~ /(\d+)-(\d+)/) { # number-number
127			return 1 if ($current >= $1 and $current <= $2);
128			next
129		}
130		return 1 if ($h eq '*' or $h == $current); # '*' or exact hour
131	}
132	return 0;
133}
134
135sub sig_timeout {
136	my $ctime = time();
137	my ($cminute, $chour, $cdom, $cmonth, $cdow) = (localtime($ctime))[1,2,3,4,6];
138	$cmonth += 1;
139	foreach my $job (@jobs) {
140		next if ($job->{'disabled'});
141		next if ($job->{'repeats'} == 0);
142		next if (not time_matches($chour, $job->{'hour'}));
143		next if (not time_matches($cminute, $job->{'minute'}));
144		next if (not time_matches($cmonth, $job->{'month'}));
145		if ($job->{'dom'} ne '*' and $job->{'dow'} ne '*') {
146			next if (not (time_matches($cdom,  $job->{'dom'}) or
147				time_matches($cdow, $job->{'dow'})));
148		} else {
149			next if (not time_matches($cdom, $job->{'dom'}));
150			next if (not time_matches($cdow, $job->{'dow'}));
151		}
152
153		my $server = Irssi::server_find_tag($job->{'server'});
154		if (!$server) {
155			Irssi::print("cron.pl: could not find server '$job->{server}'");
156			next;
157		}
158		$server->command($job->{'commands'});
159		if ($job->{'repeats'} > 0) {
160		    $job->{'repeats'} -= 1;
161		}
162	}
163}
164
165sub cmd_jobs {
166	my ($data, $server, $channel) = @_;
167	my $verbose = ($data eq '-v');
168	Irssi::print("Current Jobs:");
169	foreach (0 .. $#jobs) {
170		my $repeats = $jobs[$_]{'repeats'};
171		my $msg = "$_) ";
172		if (!$verbose) {
173			next if ($repeats == 0);
174			$msg .= "-$repeats " if ($repeats != -1);
175		} else {
176			$msg .= "-$repeats " if ($repeats != -1);
177		}
178
179		$msg .= ($jobs[$_]{'disabled'}?"-disabled ":"")
180			."-server $jobs[$_]{server} "
181			."$jobs[$_]{hour} $jobs[$_]{minute} $jobs[$_]{dom} "
182			."$jobs[$_]{month} $jobs[$_]{dow} "
183			."$jobs[$_]{commands}";
184		Irssi::print($msg);
185	}
186	Irssi::print("End of List");
187}
188
189# /jobdel job_number
190sub cmd_jobdel {
191	my ($data, $server, $channel) = @_;
192	if ($data eq "-finished") {
193	    foreach (reverse(0 .. $#jobs)) {
194			if ($jobs[$_]{'repeats'} == 0) {
195			    splice(@jobs, $_, 1);
196			    Irssi::print("Removed Job #$_");
197			}
198	    }
199	    return;
200    } elsif ($data !~ /\d+/ or $data < 0 or $data > $#jobs) {
201		Irssi::print("Bad Job Number");
202		return;
203	}
204	splice(@jobs, $data, 1);
205	Irssi::print("Removed Job #$data");
206}
207
208# /jobdisable job_number
209sub cmd_jobdisable {
210	my ($data, $server, $channel) = @_;
211	if ($data < 0 || $data > $#jobs) {
212		Irssi::print("Bad Job Number");
213		return;
214	}
215	$jobs[$data]{'disabled'} = 1;
216	Irssi::print("Disabled job number $data");
217}
218# /jobenable job_number
219sub cmd_jobenable {
220	my ($data, $server, $channel) = @_;
221	if ($data < 0 || $data > $#jobs) {
222		Irssi::print("Bad Job Number");
223		return;
224	}
225	$jobs[$data]{'disabled'} = 0;
226	Irssi::print("Enabled job number $data");
227}
228
229# /jobadd [-X] [-disabled] [-server servertag] hour minute day_of_month month day_of_week command
230sub cmd_jobadd {
231	my ($data, $server, $channel) = @_;
232
233	$server = $server->{tag};
234	my $disabled = 0;
235	my $repeats = -1;
236	while ($data =~ /^\s*-/) {
237		if ($data =~ s/^\s*-disabled\s+//) {
238			$disabled = 1;
239			next;
240		}
241		if ($data =~ s/^\s*-(\d+)\s+//) {
242			$repeats = $1;
243			next;
244		}
245		my $comm;
246		($comm, $server, $data) = split(' ', $data, 3);
247		if ($comm ne '-server') {
248			Irssi::print("Bad switch: '$comm'");
249			return;
250		}
251	}
252	my ($hour, $minute, $dom, $month, $dow, $commands) = split(' ', $data, 6);
253
254	push (@jobs, { 'hour' => $hour, 'minute' => $minute, 'dom' => $dom,
255		'month' => $month, 'dow' => $dow,
256		'server' => $server, 'commands' => $commands,
257		'disabled' => $disabled, 'repeats' => $repeats } );
258	Irssi::print("Job added");
259}
260
261sub cmd_jobssave {
262	if (not open (FILE, ">", $savefile)) {
263		Irssi::print("Could not open file '$savefile': $!");
264		return;
265	}
266	foreach (0 .. $#jobs) {
267		next if ($jobs[$_]->{'repeats'} == 0); # don't save finished jobs
268		print FILE
269			($jobs[$_]->{'repeats'}>0 ? "-$jobs[$_]->{'repeats'} " : "")
270			. ($jobs[$_]{'disabled'}?"-disabled ":"")
271			."-server $jobs[$_]{server} "
272			."$jobs[$_]{hour} $jobs[$_]{minute} $jobs[$_]{dom} "
273			."$jobs[$_]{month} $jobs[$_]{dow} "
274			."$jobs[$_]{commands}\n";
275	}
276	close FILE;
277	Irssi::print("Jobs saved");
278}
279
280sub cmd_jobsload {
281	if (not open (FILE, q{<}, $savefile)) {
282		Irssi::print("Could not open file '$savefile': $!");
283		return;
284	}
285	@jobs = ();
286
287	while (<FILE>) {
288		chomp;
289		cmd_jobadd($_, undef, undef);
290	}
291
292	close FILE;
293	Irssi::print("Jobs loaded");
294}
295
296cmd_jobsload();
297
298Irssi::command_bind('jobs', 'cmd_jobs', 'Cron');
299Irssi::command_bind('jobadd', 'cmd_jobadd', 'Cron');
300Irssi::command_bind('jobdel', 'cmd_jobdel', 'Cron');
301Irssi::command_bind('jobdisable', 'cmd_jobdisable', 'Cron');
302Irssi::command_bind('jobenable', 'cmd_jobenable', 'Cron');
303Irssi::command_bind('jobssave', 'cmd_jobssave', 'Cron');
304Irssi::command_bind('jobsload', 'cmd_jobsload', 'Cron');
305
306# vim:noexpandtab:ts=4
307