File: custom_sleep.pl

package info (click to toggle)
libschedule-cron-perl 1.05-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 256 kB
  • sloc: perl: 1,997; makefile: 6
file content (389 lines) | stat: -rw-r--r-- 11,633 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
#!/usr/bin/perl

# Copyright (c) 2011 Timothe Litt <litt at acm dot org>
#
# May be used on the same terms as Perl.

# Sleep hook demo, showing how it enables a background thread
# to provide a simple command interface to a daemon.

=head1 custom_sleep - Demo for a custom 'sleep' function

This example demonstrates the usage of the 'sleep' option
for L<Schedule::Cron> with a custom sleep method which can 
dynamically modify the crontab even inbetween to cron events.
It provides a cron daemon which listens on a TCP port for commands. 

Please note that this is an example only and should obviously not
used for production !

When started, this script will listen on port 65331 and will first
ask for a password. Use 'Purfect' here. Then the following commands
are available:

   status                  -- Print internal job queue
   add id "cron spec" name -- Add a sample jon which will bring "id: name" 
                              each time "cron spec" fires
   load /path/to/crontab   -- Load a crontab as with Schedule::Cron->load_crontab 
   delete id               -- Delete job entry
   quit                    -- Disconect

A sample session looks like:

First start the server:

  ./custom_sleep.pl
  Please wait while initialization is scheduled
  Schedule::Cron - Starting job 0
  Ready, my port is localhost::65331
  Schedule::Cron - Finished job 0
  Schedule::Cron - Starting job 5
  Now: Periodic
  Schedule::Cron - Finished job 5

And then a client:

  $ telnet localhost 65331
  Trying 127.0.0.1...
  Connected to localhost.localdomain (127.0.0.1).
  Escape character is '^]'.
  Password: Purfect
  Password accepted

  status
  Job 0 0 0 1 1 * Next: Sun Jan 1 00:00:00 2012 - NewYear( )
  End of job queue

  load cron.tab
  Loaded cron.tab

  status
  Job 1 34 2 * * Mon Next: Mon Jun 6 02:34:00 2011 - "make_stats"( )
  Job 2 43 8 * * Wed Next: Wed Jun 8 08:43:00 2011 - "Make Peace"( )
  Job 0 0 0 1 1 * Next: Sun Jan 1 00:00:00 2012 - NewYear( )
  End of job queue

  add Halloween "30 18 31 10 *" Pumpkin time
  Added 30 18 31 10 *

  add Today "11 15 * * *" Something to do
  Added 11 15 * * *
 
  add Now "*/2 * * * * 30" Periodic
  Added */2 * * * * 30

  status
  Job 5 */2 * * * * 30 Next: Thu Jun 2 13:40:30 2011 - Now( Periodic )
  Job 4 11 15 * * * Next: Thu Jun 2 15:11:00 2011 - Today( Something to do )
  Job 1 34 2 * * Mon Next: Mon Jun 6 02:34:00 2011 - "make_stats"( )
  Job 2 43 8 * * Wed Next: Wed Jun 8 08:43:00 2011 - "Make Peace"( )
  Job 3 30 18 31 10 * Next: Mon Oct 31 18:30:00 2011 - Halloween( Pumpkin time )
  Job 0 0 0 1 1 * Next: Sun Jan 1 00:00:00 2012 - NewYear( )
  End of job queue
 
  delete Today
  Deleted Today
  
  status
  Job 4 */2 * * * * 30 Next: Thu Jun 2 13:42:30 2011 - Now( Periodic )
  Job 1 34 2 * * Mon Next: Mon Jun 6 02:34:00 2011 - "make_stats"( )
  Job 2 43 8 * * Wed Next: Wed Jun 8 08:43:00 2011 - "Make Peace"( )
  Job 3 30 18 31 10 * Next: Mon Oct 31 18:30:00 2011 - Halloween( Pumpkin time )
  Job 0 0 0 1 1 * Next: Sun Jan 1 00:00:00 2012 - NewYear( )
  End of job queue

  q
  Connection closed by foreign host.

=cut

use strict;
use warnings;

use Schedule::Cron;
use Socket ':crlf';
use IO::Socket::INET;

my $port = 65331;
our $password = 'Purfect';

our( $lsock, $rin, $win, $maxfd, %servers );

my $cron = new Schedule::Cron( sub { print 'Loaded entry: ', join('', @_ ), "\n"; }, {
                                        nofork => 1,
                                        loglevel => 0,
                                        log => sub { print $_[1], "\n"; },
                                        sleep => \&idler
                                       } );

$cron->add_entry( "* * * * * *", \&init, 'Init', $cron );
$cron->add_entry( "0 0 1 1 *", sub { print "Happy New Year\n"; }, "NewYear" );

print "Please wait while initialization is scheduled\n";
print help();

$cron->run( { detach => 0 } );

exit;


sub idler {
    my( $time ) = @_;

    my( $rout, $wout );

    my( $nfound, $ttg ) = select( $rout=$rin, $wout=$win, undef, $time );
    if( $nfound ) {
        if( $nfound == -1 ) {
            die "select() error: $!\n"; # This will be an internal error, such as a stale fd.
        }
        for( my $n = 0; $n <= $maxfd; $n++ ) {
            if( vec( $rout, $n, 1 ) ) {
                my $s = $servers{$n};
                $s->{rsub}->( );
            }
        }
        for( my $n = 0; $n <= $maxfd; $n++ ) {
            if( vec( $wout, $n, 1 ) ) {
                my $s = $servers{$n};
                $s->{wsub}->( );
            }
        }
    }
}

# First task run initializes (usually in daemon, after forking closed open files)
# I suppose this could be a postfork callback, but there isn't one...

sub init {
    my( $name, $cron ) = @_;

    $cron->delete_entry( 'Init' );

    $rin = '';
    $win = '';

    $lsock = IO::Socket::INET->new(
                                   LocalAddr => "localhost:$port",
                                   Proto => 'tcp',
                                   Type => SOCK_STREAM,
                                   Listen => 5,
                                   ReuseAddr => 1,
                                   Blocking => 0,
                                  ),
                                    or die "Unable to open status port $port $!\n";
    vec( $rin, ($maxfd = $lsock->fileno()), 1 ) = 1;
    $servers{$maxfd} = { rsub=>sub { newConn( $lsock, $cron ); } };

    print "Ready, my port is localhost:$port\nTo connect:\n    telnet localhost $port\n";

    return;
}

sub newConn {
    my( $lsock, $cron ) = @_;

    my $sock = $lsock->accept();

    $sock->blocking(0);
    my $cx = {
              rbuf => '',
              wbuf => 'Password: ',
              };
    my $fd = $sock->fileno();
    $maxfd = $fd if( $maxfd < $fd );

    vec( $rin, $fd, 1 ) = 1;
    vec( $win, $fd, 1 ) = 1;
    $servers{$fd} = { rsub=>sub { serverRd( $sock, $cx, $fd ); },
                      wsub=>sub { serverWr( $sock, $cx, $fd ); },
                      cron=>$cron,
                    };
}

sub serverRd {
    my( $sock, $cx, $fd ) = @_;

    # Read whatever is available.  1000 is arbitrary, 1 will work (with lots of overhead).
    # Huge will prevent any other thread from running.

    my $rn= $sock->sysread( $cx ->{rbuf}, 1000, length $cx->{rbuf} );
    unless( defined $rn ) {
        print "Read error: $!\n";
    }
    unless( $rn ) { # Connection closed by client
        vec( $rin, $fd, 1 ) = 0;
        vec( $win, $fd, 1 ) = 0;
        $sock->close();
        undef $cx;
        return;
    }

    # Assemble reads to form whole lines
    # Decode each line as a command.

    while( $cx->{rbuf} =~ /$LF/sm ) {
        $cx->{rbuf} =~ s/$CR//g;
        my( $line, $rest );
        ($line, $rest) = split( /$LF/, $cx->{rbuf}, 2 );
        $rest = '' unless( defined $rest );
        $cx->{rbuf} = $rest;

        # This is not secure, but one has to do something.
        # Demos always get used for more than they should..
        # Please do better...like user/account validation
        # using the system services.

        unless( $cx->{authenticated} ){
            if( $line eq $password ) {
                $cx->{authenticated} = 1;
                $cx->{wbuf} .= "Password accepted$CR$LF";
            } else {
                $cx->{wbuf} .= "Password refused.$CR${LF}Password: ";
            }
            next;
        }

        if( $line =~ /^STAT(?:US)?(?: (\w+))?$/i ) {
            $cx->{wbuf} .= status( $cron, ($1 || 'normal') );
        } elsif( $line =~ /^ADD\s+(\w+)\s+"(.*?)"\s+(.*)$/i ) {
            my( $name, $sched ) = ($1, $2);
            $cron->add_entry( $sched, \&announce, $1, $3 );
            $cx->{wbuf} .= "Added $name '$sched'$CR$LF";
        } elsif( $line =~ /^DEL(?:ETE)?\s+(["\w]+)$/i ) {
            my $name = $1;
            my $idx = $cron->check_entry( $name );
            if( defined $idx ) {
                $cron->delete_entry( $idx );
                $cx->{wbuf} .= "Deleted $name$CR$LF";
            } else {
                $cx->{wbuf} .= "$name not found$CR$LF";
            }
        } elsif( $line =~ /^HELP$/i ) {
            $cx->{wbuf} .= help();
        } elsif( $line =~ /^LOAD\s([\w\._-]+)$/i ) {
            my $cfg = $1; # Danger: File permissions of server are used here.
              eval {
                  $cron->load_crontab( $cfg );
              };
            my $emsg = $@;
            $emsg =~ s/\n/$CR$LF/gms;
            $cx->{wbuf} .= $emsg || "Loaded $cfg$CR$LF";
        } elsif( $line =~ /^Q(?:uit)?$/i ) {
            $cx->{wbuf} .= "Bye$CR$LF";
            $cx->{wend} = 1;
        } else {
            $cx->{wbuf} .= "Unrecognized command: $line$CR$LF";
        }
    }
    serverWr( $sock, $cx, $fd );
}

# Server write process
#
# Output as much as possible from our buffer.
# If more remains, keep select mask active
# If done, clear select mask.  If last write, close socket.

sub serverWr {
    my( $sock, $cx, $fd ) = @_;

    if( length $cx->{wbuf} ) {
        my $written = $sock->syswrite( $cx->{wbuf} );

        $cx->{wbuf} = substr( $cx->{wbuf}, $written );
    }
    if( length $cx->{wbuf} ) {
        vec( $win, $fd, 1 ) = 1;
        return;
    } else {
        vec( $win, $fd, 1 ) = 0;
        if( $cx->{wend} ) {
            vec( $rin, $fd, 1 ) = 0;
            $sock->close();
            return;
        }
    }
}

sub announce {
    my( $id, $msg ) = @_;

    print "$id: $msg\n";
    return;
}

sub status {
    my $cron = shift;
    my $level = shift;

    my $maxtwid = 0;
    my @entries = map { $_->[0] } sort { $a->[1] <=> $b->[1] }
                                           map { 
                                                 my $time = $_->{time};
                                                 $maxtwid = length $time if( $maxtwid < length $time );
                                                 [ $_, 
                                                   $cron->get_next_execution_time( $time ),
                                                 ]
                                               } $cron->list_entries();
    my $msg = "Job queue\n";
    foreach my $qe ( @entries ) {
        my $job = $cron->check_entry( $qe->{args}->[0] );
        next unless( defined $job ); #??
        $msg .= sprintf( "Job %-4s %-*s Next: %s - %s", 
                         $job, $maxtwid, $qe->{time},
                         (scalar localtime( $cron->get_next_execution_time( $qe->{time}, 0 ) )),
                         $qe->{args}->[0] ||  '<Unnamed>', # Task name
                       );
        if( $level =~ /^debug$/i ) {
            $msg .= '( ';
            my @uargs = @{$qe->{args}};
            $msg .= join( ', ', @uargs[1..$#uargs] ) . ' )';
        }
        $msg .= "\n";
    }
    $msg .= "End of job queue\n";
    $msg =~ s/\n/$CR$LF/mgs;

    return $msg;
}

use Cwd 'getcwd';
sub help {
    my $wd = getcwd();
   my $msg = <<"HELP";
CAUTION: Not production code.  NOT secure.
Do NOT run from privileged account.

Commands:
    status
       Shows queue

    status debug
       With argument lists

    add name "schedule" A string to be printed when executed
       Adds a new task on specified schedule

    delete name
       Deletes a task (by name)

    help
      This message.

    load file
        Loads a crontab file from $wd
        CAUTION, this is with server permissions.  If 
        the server can read /etc/passwd (or anything else), 
        it will display it in the error messages.  
        As I said, NOT production...

    quit
        Exits.

HELP

   $msg =~ s/\n/$CRLF/gms;

   return $msg;
}