Skip Menu |

This queue is for tickets about the Schedule-Cron CPAN distribution.

Report information
The Basics
Id: 68533
Status: resolved
Priority: 0/
Queue: Schedule-Cron

People
Owner: Nobody in particular
Requestors: tlhackque [...] yahoo.com
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: (no value)
Fixed in: (no value)



Subject: Thou shalt not REAP what thou has not forked...
I do not specify the detach option & so run tasks sequentially. However, some of these tasks do wish to fork. It turns out that Schedule::Cron installs it's SIG{CLD} handler and runs its REAPER unconditionally in its main loop (_cleanup_process_list). Of course, it's reaping forks that it did not create. This makes forking by the scheduled tasks unreliable. Although callers can overwrite the SIG{CLD} handler, we can't stop the loop level call to REAPER. Schedule::Cron should not install a CLD handler or call REAPER in _cleanup_process_list unless the 'detach' option is specified.
From: tlhackque [...] yahoo.com
Sorry, I didn't mean detach, I meant nofork. Handler and polling need to be suppressed with nofork set to 1; this has nothing to do with detach.
Subject: Patch for several RTs, including this
From: tlhackque [...] yahoo.com
I was able to create a patch to fix most of the outstanding bugs. It's received limited testing so far, but seems worth distributing. RT#58841 - typo in log message Agree (actually, noticed while doing other work), fixed T#68450 - scheduler crash when a job calls clean_timetable and does not add another job. run() will die in this case - caller can put an eval in place if it knows how to handle this. RT#68530 - Exposes too much information, performance issues. Fixed. Added nostatus item to prevent updating $0, loglevel to control logging callbacks & control whether $0 updates include arglists. RT#68533 - (this) Do not reap unless forking. Fixed. POD is updated for all changes, and all the defaults keep the current behaviors, even those that I would do differently. It should be compatible with any current application. I also conditioned some of the more expensive dbg calls on $DEBUG to save the computes used to generate argument lists to dbg. I looked at #62846, but don't see how an add can happen during a sleep. If we're not forking, it's (almost) self-evident. If we are forking, the fork can't update the parent queues. And if one tries to do an add in signal handler - well, there are other reentracy problems. If that's what was attempted, my solution is to have a job that looks for a config file change at some frequency - then updates are done at job level and should work. Not being the maintainer/owner and not having a windows environment, I didn't feel that I could include the 56926 patch. It looks reasonable, but as I don't have the problem and am unable to test, I'm not the one to handle the issue. It would be really great if you could take this patch and relase a new kit. (If I find any more issues, including with this patch, I'll add another RT.) Thanks in advance.
Subject: Schedule-Cron.patch.1

Message body is not shown because it is too large.

Thanks a lot for your investigations. As you might have noticed, the efforts on 
Schedule::Cron has been throttled for some time. However, I will look at your patch
this week (we have a public holiday on thursday, so there should be some spare
time ;-) and if it looks of for me release a new version at the end of this week (with the
patch applied).

Thanks again ....
...roland
From: tlhackque [...] yahoo.com
That would be great. I have another request - arguably it's a fix for RT#62846 that I didn't understand. In any case, I want to be able to use select where $mainloop goes to sleep. This allows me to service network IO - among other things. Thus, queue listings can be extracted and entries to the queue. (Which is what the reporter of #62846 was probably trying to do.) I updated my patch to allow this, by allowing specification of a sleep hook. Basically, the hook is responsible for sleeping for a while and returning. It's allowed to add and delete entries, which is why on return we check for entries_changed & rescan the queue. Of course it can list the queue too. I included a sample sleep routine in the pod that just wakes if you hit <CR> on STDIN. But there's a lot more one can do with this. I think it's a powerful addition. Attached is Schedule-Cron.patch.2. It combines all my patches, and is only a few more lines than the original. Many thanks. And enjoy your holiday!
Subject: Schedule-Cron.patch.2

Message body is not shown because it is too large.

Hi, 

I just uploaded 1.02 with your patch applied. Thanks again ;-)

I did the following slight modifications:

* I set the default loglevel to 0, since has you stated correctly, 
  -1 can pose a security issue (args list in 'ps'), so we should provide
  a save default.

* For the 'sleep' option, I changed the signature so that the first 
  argument will be the time in seconds to sleep (as before) and an 
  additional second argument contains the Cron::Schedule object
  itself (which then can be used to add extra entries). This also 
  slims down the example that you provided and makes sense IMO

I don't know, whether you want do this, but it would be super cool if 
you could provide a full example with the 'sleep' options showing 
how to do the dynamic reload and maybe even add a unit test.
But if not, no problem, I really appreciate the patch you delivered.

1.01 will be no release rather soon, I need still to wait on feedback of
the other two open bugs ( 56926 and  55741), which should be 
fixed as well. (Even this very bug should now work without the exrta
check on $config{nofork}, since the reaper now only waits on the PID
it has started on its own (as it was in 1.00 but was changed for  56926 and
now reverted back. But I still leave the patch here, it doesnt harm and provides
and extra check level)

bye, nice weekend ...
... roland

On Tue May 31 22:57:48 2011, tlhackque wrote:
Show quoted text
> That would be great.
>
> I have another request - arguably it's a fix for RT#62846 that I didn't
> understand.
>
> In any case, I want to be able to use select where $mainloop goes to
> sleep. This allows me to service network IO - among other things.
> Thus, queue listings can be extracted and entries to the queue. (Which
> is what the reporter of #62846 was probably trying to do.)
>
> I updated my patch to allow this, by allowing specification of a sleep
> hook. Basically, the hook is responsible for sleeping for a while and
> returning. It's allowed to add and delete entries, which is why on
> return we check for entries_changed & rescan the queue. Of course it
> can list the queue too.
>
> I included a sample sleep routine in the pod that just wakes if you hit
> <CR> on STDIN. But there's a lot more one can do with this. I think
> it's a powerful addition.
>
> Attached is Schedule-Cron.patch.2. It combines all my patches, and is
> only a few more lines than the original.
>
> Many thanks. And enjoy your holiday!


From: tlhackque [...] yahoo.com
Roland, Thanks for accepting the changes. That's a big help. I agree that 0 is the correct default for loglevel - I was just being super cautious about changing behaviour for existing users. The reason I didn't provide the $cron in the sleep call-out was that it's easy to do that in a closure - which can also provide other application-specific arguments. But what you describe should be OK. (Your upload hasn't made it to CPAN yet.) On the sleep hook - I'm not quite sure what you're looking for. My real code is rather involved & not suitable for an example. What I put in the POD works, as can be seen by putting some logging at the beginning and end. It's an advanced user option - mostly if you know how to use it, you'll recognize it instantly. If not, it takes a LOT of explaining. The simplest example I can think of is: where I put the "handle_io" stub, just call $cron->add_entry( "1/5 * * * * 30", sub { print "New entry\n"; } ); It's not useful (except for debugging), but it is an example. Now startup as usual - you do need some other entry in the queue. Nothing will happen until you type a line on STDIN. Then on the next 05:30 tick, the "New entry" message will appear. But, since you were kind enough to take my patches, I wrote the simplest useful tool for demonstrating the utility of sleep-callouts that I could think of. I think it's too much for an example (about 250 lines of perl). But you may find it useful for debugging. I'm not an "expect" guy, but you should also be able to use it for a unit test if you are. It shows how one can have a network server that runs with Schedule::Cron and provides rudimentary control. NOT production. But it shows how you can (must) thread non-blocking I/O to keep Schedule::Cron on-time, yet be responsive to requests. You'll notice that the sleep routine doesn't need $cron because all the callbacks that it generates need closures... I've attached it to this note. Here's a little user documentation: Run CronSleepExample (from an unpriv'd account, please!). Wait untl it says "my port is localhost:65331". From another window, telnet to localhost:65331. Enter the Password ('Purfect'). You have the following commands: status Shows queue add name "schedule" A string to be printed when executed Adds a new task delete name Deletes a task (by name) load file Loads a crontab file - 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. Note that all this seems to happen while your code is scheduling and executing tasks. But it's at a safe point with regard to your data structures. If your feel adventurous, open another telnet session and see that each runs independently. Here's a sample session. Uses a small crontab file (also attached). Server first: ./CronSleepExample 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 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. # I have more complex uses, but this should demonstrate the utility. Thanks again for so promptly taking the patches. Hope this helps. On Thu Jun 02 09:58:45 2011, ROLAND wrote: Show quoted text
> Hi, > > I just uploaded 1.02 with your patch applied. Thanks again ;-) > > I did the following slight modifications: > > * I set the default loglevel to 0, since has you stated correctly, > -1 can pose a security issue (args list in 'ps'), so we should provide > a save default. > > * For the 'sleep' option, I changed the signature so that the first > argument will be the time in seconds to sleep (as before) and an > additional second argument contains the Cron::Schedule object > itself (which then can be used to add extra entries). This also > slims down the example that you provided and makes sense IMO > > I don't know, whether you want do this, but it would be super cool if > you could provide a full example with the 'sleep' options showing > how to do the dynamic reload and maybe even add a unit test. > But if not, no problem, I really appreciate the patch you delivered. > > 1.01 will be no release rather soon, I need still to wait on feedback
of Show quoted text
> the other two open bugs ( 56926 and 55741), which should be > fixed as well. (Even this very bug should now work without the exrta > check on $config{nofork}, since the reaper now only waits on the PID > it has started on its own (as it was in 1.00 but was changed for
56926 and Show quoted text
> now reverted back. But I still leave the patch here, it doesnt harm
and Show quoted text
> provides > and extra check level) > > bye, nice weekend ... > ... roland
Subject: cron.tab
34 2 * * Mon "make_stats" 43 8 * * Wed "Make Peace"
Subject: CronSleepExample
#!/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. 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"; $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\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 =~ /^STATUS$/i ) { $cx->{wbuf} .= status( $cron ); } elsif( $line =~ /^ADD\s+(\w+)\s+"(.*?)"\s+(.*)$/i ) { $cron->add_entry( $2, \&announce, $1, $3 ); $cx->{wbuf} .= "Added $2$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 =~ /^LOAD\s(.*)$/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 $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 = ''; 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( 1 ) { $msg .= '( '; my @uargs = @{$qe->{args}}; $msg .= join( ', ', @uargs[1..$#uargs] ) . ' )'; } $msg .= "\n"; } $msg .= "End of job queue\n"; return $msg; }
Hi Timothe,

On Thu Jun 02 14:15:52 2011, tlhackque wrote:

Show quoted text
> Thanks for accepting the changes. That's a big help.

No problem, I have to thank you.

Show quoted text
> The reason I didn't provide the $cron in the sleep call-out was that
> it's easy to do that in a closure - which can also provide other
> application-specific arguments.

This can be still done, but I think the most common use case will
be to modify the cron entry queue like in your example, so you would
need the $cron object most of the time. And it doesn't hurt either ;-)

Show quoted text
> But what you describe should be OK. (Your upload hasn't made it to
> CPAN yet.)

Sorry, it it version 1.01_2 (not 1.02), which should be available at the mirrors
now (at least I could download it from there). I will release 1.01 final on sunday
if no big veto comes from the other two open ticket holders.

Show quoted text
> But, since you were kind enough to take my patches, I wrote the
> simplest useful tool for demonstrating the utility of sleep-callouts
> that I could think of. I think it's too much for an example (about 250
> lines of perl). But you may find it useful for debugging. I'm not
> an "expect" guy, but you should also be able to use it for a unit test
> if you are.

That's exactly what I was looking for. If you don't mind, I would like to include
it into the example/ directory of Schedule::Cron (and add the user documentation
and warning you pointed out as top comments). 

Show quoted text
> I have more complex uses, but this should demonstrate the utility.

the example is perfect, thanks !

... roland

From: tlhackque [...] yahoo.com
You can ship the example - with a BIG warning about "Intended to demonstrate how to use the sleep-callout, but NOT suitable for production use." Real secuity would have made it even bigger - and is hard to make platform-independent. I think the only API that I don't exercise is "update" (and eval, which is another security trap). You might want to add an "update" command for your testing. Also, note that I only parse \w+ for task names - the cron.tab tasks are undeletable due to the double-quotes in the names. (I took them direct from your examples.) Not important - I wanted to provide the shortest useful example, not a worked-out tool. I don't want to belabor the point, but a little more explanation: I don't think it hurts to call-out with $cron, as I said, it's fine with me. It's just that most of the time you either need nothing (using a closure or perhaps a global variable), or you need your own structures in addition to $cron. My trivial example is hard-coded; in any real case the entry to be added or accessed has to be specified from somewhere. And so do the dispatch routine, its arguments, where to send errors... And that's either a closure, or another case where we'd have to pass user arguments to the call-out. The only unique data that must come from the schedular is the maximum sleep time. Everything else - it's as much style as anything else. I like simple. Especially in this case where it's a frequently-executed code path (every I/O will wake the select.) But passing one extra scalar isn't a big deal. (Dumping 200KB user args was!) I'll switch to your code as soon as I have a chance. It's been good working with you; thanks again. On Thu Jun 02 15:07:56 2011, ROLAND wrote: Show quoted text
> Hi Timothe, > > On Thu Jun 02 14:15:52 2011, tlhackque wrote: >
> > Thanks for accepting the changes. That's a big help.
> > No problem, I have to thank you. >
> > The reason I didn't provide the $cron in the sleep call-out was that > > it's easy to do that in a closure - which can also provide other > > application-specific arguments.
> > This can be still done, but I think the most common use case will > be to modify the cron entry queue like in your example, so you would > need the $cron object most of the time. And it doesn't hurt either ;-) >
> > But what you describe should be OK. (Your upload hasn't made it to > > CPAN yet.)
> > Sorry, it it version 1.01_2 (not 1.02), which should be available at
the Show quoted text
> mirrors > now (at least I could download it from there). I will release 1.01
final on Show quoted text
> sunday > if no big veto comes from the other two open ticket holders. >
> > But, since you were kind enough to take my patches, I wrote the > > simplest useful tool for demonstrating the utility of sleep-callouts > > that I could think of. I think it's too much for an example (about
250 Show quoted text
> > lines of perl). But you may find it useful for debugging. I'm not > > an "expect" guy, but you should also be able to use it for a unit
test Show quoted text
> > if you are.
> > That's exactly what I was looking for. If you don't mind, I would
like to Show quoted text
> include > it into the example/ directory of Schedule::Cron (and add the user > documentation > and warning you pointed out as top comments). >
> > I have more complex uses, but this should demonstrate the utility.
> > the example is perfect, thanks ! > > ... roland
Subject: Sleep example
From: tlhackque [...] yahoo.com
Since you plan to include CronSleepExample in the kit, I added a help display and also made it *slightly* less insecure by restricting filenames for LOAD. Attached. I intentionally didn't put the password in the help display so people will have to read the source - thereby encouraging them to read the warnings. Enjoy.
Subject: CronSleepExample
#!/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. 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; }

I put the sample from yesterday already into 1.01_3 release
as 'examples/custom_sleep.pl' and added some documentation inline
as POD into the example. I will update it with the sample you just
posted before the final release on sunday. 

I put the cron.tab also into the examples/ directory, so playing around 
should be easy. 

Although I appreciate your security awareness, I don't think people
will use this example other than, well, an example. Since it is stated
everywhere in the source and the documentation, there should be no
problem at all.

Thanks again ...
... roland

 So that 1.01 is out since a week now, its time to close this ticket.

Thank you very much again for this nice patch and the example for the custom 'sleep' method.  

... roland.