1539a48af7101b46589cf04f98930407d989d90c
[freeside.git] / FS / bin / freeside-queued
1 #!/usr/bin/perl -w
2
3 use strict;
4 use vars qw( $log_file $sigterm $sigint $kids $max_kids );
5 use subs qw( _die _logmsg );
6 use Fcntl qw(:flock);
7 use POSIX qw(setsid);
8 use Date::Format;
9 use IO::File;
10 use FS::UID qw(adminsuidsetup forksuidsetup driver_name);
11 use FS::Record qw(qsearchs);
12 use FS::queue;
13
14 # no autoloading just yet
15 use FS::cust_main;
16 use FS::svc_acct;
17 use Net::SSH 0.05;
18 use FS::part_export;
19
20 my $pid_file = '/var/run/freeside-queued.pid';
21
22 $max_kids = '10'; #guess it should be a config file...
23 $kids = 0;
24
25 my $user = shift or die &usage;
26
27 &daemonize1;
28
29 sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; }
30 $SIG{CHLD} =  \&REAPER;
31
32 $sigterm = 0;
33 $sigint = 0;
34 $SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $sigint++; };
35 $SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; };
36
37 $> = $FS::UID::freeside_uid unless $>;
38 $< = $>;
39 $ENV{HOME} = (getpwuid($>))[7]; #for ssh
40 adminsuidsetup $user;
41
42 $log_file = "/usr/local/etc/freeside/queuelog.". $FS::UID::datasrc;
43
44 &daemonize2;
45
46 $SIG{__DIE__} = \&_die;
47 $SIG{__WARN__} = \&_logmsg;
48
49 warn "freeside-queued starting\n";
50
51 my $warnkids=0;
52 while (1) {
53
54   #prevent runaway forking
55   if ( $kids >= $max_kids ) {
56     warn "WARNING: maximum $kids children reached\n" unless $warnkids++;
57     sleep 1; #waiting for signals is cheap
58     next;
59   }
60   $warnkids=0;
61
62   my $nodepend = 'AND 0 = ( SELECT COUNT(*) FROM queue_depend'.
63                           ' WHERE queue_depend.jobnum = queue.jobnum ) ';
64
65   my $job = qsearchs(
66     'queue',
67     { 'status' => 'new' },
68     '',
69     driver_name =~ /^mysql$/i
70       ? "$nodepend ORDER BY jobnum LIMIT 1 FOR UPDATE"
71       : "$nodepend ORDER BY jobnum FOR UPDATE LIMIT 1"
72   ) or do {
73     sleep 5; #connecting to db is expensive
74     next;
75   };
76
77   my %hash = $job->hash;
78   $hash{'status'} = 'locked';
79   my $ljob = new FS::queue ( \%hash );
80   my $error = $ljob->replace($job);
81   die $error if $error;
82
83   my @args = $ljob->args;
84
85   defined( my $pid = fork ) or do {
86     warn "WARNING: can't fork: $!\n";
87     my %hash = $job->hash;
88     $hash{'status'} = 'failed';
89     $hash{'statustext'} = "[freeside-queued] can't fork: $!";
90     my $ljob = new FS::queue ( \%hash );
91     my $error = $ljob->replace($job);
92     die $error if $error;
93     next; #don't increment the kid counter
94   };
95
96   if ( $pid ) {
97     $kids++;
98   } else { #kid time
99
100     #get new db handle
101     $FS::UID::dbh->{InactiveDestroy} = 1;
102
103     forksuidsetup($user);
104
105     #auto-use export classes...
106     if ( $ljob->job =~ /(FS::part_export::\w+)::/ ) {
107       my $class = $1;
108       eval "use $class;";
109       if ( $@ ) {
110         warn "job use $class failed";
111         my %hash = $ljob->hash;
112         $hash{'status'} = 'failed';
113         $hash{'statustext'} = $@;
114         my $fjob = new FS::queue( \%hash );
115         my $error = $fjob->replace($ljob);
116         die $error if $error;
117         exit; #end-of-kid
118       };
119     }
120
121     my $eval = "&". $ljob->job. '(@args);';
122     warn "running $eval";
123     eval $eval; #throw away return value?  suppose so
124     if ( $@ ) {
125       warn "job $eval failed";
126       my %hash = $ljob->hash;
127       $hash{'status'} = 'failed';
128       $hash{'statustext'} = $@;
129       my $fjob = new FS::queue( \%hash );
130       my $error = $fjob->replace($ljob);
131       die $error if $error;
132     } else {
133       $ljob->delete;
134     }
135
136     exit;
137     #end-of-kid
138   }
139
140 } continue {
141   if ( $sigterm ) {
142     warn "received TERM signal; exiting\n";
143     exit;
144   }
145   if ( $sigint ) {
146     warn "received INT signal; exiting\n";
147     exit;
148   }
149 }
150
151 sub usage {
152   die "Usage:\n\n  freeside-queued user\n";
153 }
154
155 sub _die {
156   my $msg = shift;
157   unlink $pid_file if -e $pid_file;
158   _logmsg($msg);
159 }
160
161 sub _logmsg {
162   chomp( my $msg = shift );
163   my $log = new IO::File ">>$log_file";
164   flock($log, LOCK_EX);
165   seek($log, 0, 2);
166   print $log "[". time2str("%a %b %e %T %Y",time). "] [$$] $msg\n";
167   flock($log, LOCK_UN);
168   close $log;
169 }
170
171 sub daemonize1 {
172
173   chdir "/" or die "Can't chdir to /: $!";
174   open STDIN, '/dev/null'   or die "Can't read /dev/null: $!";
175   defined(my $pid = fork) or die "Can't fork: $!";
176   if ( $pid ) {
177     print "freeside-queued started with pid $pid\n"; #logging to $log_file\n";
178     exit unless $pid_file;
179     my $pidfh = new IO::File ">$pid_file" or exit;
180     print $pidfh "$pid\n";
181     exit;
182   }
183   #open STDOUT, '>/dev/null'
184   #                          or die "Can't write to /dev/null: $!";
185   #setsid                  or die "Can't start a new session: $!";
186   #open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
187
188 }
189
190 sub daemonize2 {
191   open STDOUT, '>/dev/null'
192                             or die "Can't write to /dev/null: $!";
193   setsid                  or die "Can't start a new session: $!";
194   open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
195 }
196
197 =head1 NAME
198
199 freeside-queued - Job queue daemon
200
201 =head1 SYNOPSIS
202
203   freeside-queued user
204
205 =head1 DESCRIPTION
206
207 Job queue daemon.  Should be running at all times.
208
209 user: from the mapsecrets file - see config.html from the base documentation
210
211 =head1 VERSION
212
213 =head1 BUGS
214
215 =head1 SEE ALSO
216
217 =cut
218