agent-virtualize credit card surcharge percentage, RT#72961
[freeside.git] / FS / FS / Daemon.pm
1 package FS::Daemon;
2
3 use vars qw( @ISA @EXPORT_OK );
4 use vars qw( $pid_dir $me $pid_file $sigint $sigterm $NOSIG $logfile );
5 use Exporter;
6 use Fcntl qw(:flock);
7 use POSIX qw(setsid);
8 use IO::File;
9 use File::Basename;
10 use File::Slurp qw(slurp);
11 use Date::Format;
12 use FS::UID qw( forksuidsetup );
13
14 #this is a simple refactoring of the stuff from freeside-queued, just to
15 #avoid duplicate code.  eventually this should use something from CPAN.
16
17 @ISA = qw(Exporter);
18 @EXPORT_OK = qw(
19   daemonize1 drop_root daemonize2 myexit logfile sigint sigterm
20   daemon_fork daemon_wait daemon_reconnect
21 );
22 %EXPORT_TAGS = ( 'all' => [ @EXPORT_OK ] );
23
24 $pid_dir = '/var/run';
25
26 $NOSIG = 0;
27 $PID_NEWSTYLE = 0;
28
29 our $MAX_KIDS = 10; # for daemon_fork
30 our $kids = 0;
31 our %kids;
32
33 sub daemonize1 {
34   $me = shift;
35
36   $pid_file = $pid_dir;
37   if ( $PID_NEWSTYLE ) {
38     $pid_file .= '/freeside';
39     mkdir $pid_file unless -d $pid_file;
40     chown $FS::UID::freeside_uid, -1, $pid_file;
41   }
42   $pid_file .= "/$me";
43   $pid_file .= '.'.shift if scalar(@_);
44   $pid_file .= '.pid';
45
46   chdir "/" or die "Can't chdir to /: $!";
47   open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
48   defined(my $pid = fork) or die "Can't fork: $!";
49   if ( $pid ) {
50     print "$me started with pid $pid\n"; #logging to $log_file\n";
51     exit unless $pid_file;
52     my $pidfh = new IO::File ">$pid_file" or exit;
53     chown $FS::UID::freeside_uid, -1, $pid_file;
54     print $pidfh "$pid\n";
55     exit;
56   }
57
58   #sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; }
59   #$SIG{CHLD} =  \&REAPER;
60   $sigterm = 0;
61   $sigint = 0;
62   unless ( $NOSIG ) {
63     $SIG{INT}  = sub { warn "SIGINT received; shutting down\n"; $sigint++;  };
64     $SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; };
65   }
66
67   # set the logfile sensibly
68   if (!$logfile) {
69     my $logname = $me;
70     $logname =~ s/^freeside-//;
71     logfile("%%%FREESIDE_LOG%%%/$logname-log.$FS::UID::datasrc");
72   }
73 }
74
75 sub drop_root {
76   my $freeside_gid = scalar(getgrnam('freeside'))
77     or die "can't find freeside group\n";
78   $) = $freeside_gid;
79   $( = $freeside_gid;
80   #if freebsd can't setuid(), presumably it can't setgid() either.  grr fleabsd
81   ($(,$)) = ($),$();
82   $) = $freeside_gid;
83   
84   $> = $FS::UID::freeside_uid;
85   $< = $FS::UID::freeside_uid;
86   #freebsd is sofa king broken, won't setuid()
87   ($<,$>) = ($>,$<);
88   $> = $FS::UID::freeside_uid;
89 }
90
91 sub daemonize2 {
92   open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
93   setsid                    or die "Can't start a new session: $!";
94   open STDERR, '>&STDOUT'   or die "Can't dup stdout: $!";
95
96   $SIG{__DIE__} = \&_die;
97   $SIG{__WARN__} = \&_logmsg;
98
99   warn "$me starting\n";
100 }
101
102 sub sigint  { $sigint; }
103 sub sigterm { $sigterm; }
104
105 sub logfile { $logfile = shift; } #_logmsg('test'); }
106
107 sub myexit {
108   chomp( my $pid = slurp($pid_file) );
109   unlink $pid_file if -e $pid_file && $$ == $pid;
110   exit;  
111 }
112
113 sub _die {
114   die @_ if $^S; # $^S = 1 during an eval(), don't break exception handling
115   my $msg = shift;
116
117   chomp( my $pid = slurp($pid_file) );
118   unlink $pid_file if -e $pid_file && $$ == $pid;
119
120   _logmsg($msg);
121 }
122
123 sub _logmsg {
124   chomp( my $msg = shift );
125   my $log = new IO::File ">>$logfile";
126   flock($log, LOCK_EX);
127   seek($log, 0, 2);
128   print $log "[". time2str("%a %b %e %T %Y",time). "] [$$] $msg\n";
129   flock($log, LOCK_UN);
130   close $log;
131 }
132
133 =item daemon_fork CODEREF[, ARGS ]
134
135 Executes CODEREF in a child process, with its own $FS::UID::dbh handle.  If
136 the number of child processes is >= $FS::Daemon::MAX_KIDS then this will
137 block until some of the child processes are finished. ARGS will be passed
138 to the coderef.
139
140 If the fork fails, this will throw an exception containing $!. Otherwise
141 it returns the PID of the child, like fork() does.
142
143 =cut
144
145 sub daemon_fork {
146   $FS::UID::dbh->{AutoInactiveDestroy} = 1;
147   # wait until there's a lane open
148   daemon_wait($MAX_KIDS - 1);
149
150   my ($code, @args) = @_;
151
152   my $user = $FS::CurrentUser::CurrentUser->username;
153
154   my $pid = fork;
155   if (!defined($pid)) {
156
157     warn "WARNING: can't fork: $!\n";
158     die "$!\n";
159
160   } elsif ( $pid > 0 ) {
161
162     $kids{ $pid } = 1;
163     $kids++;
164     return $pid;
165
166   } else { # kid
167     forksuidsetup( $user );
168     &{$code}(@args);
169     exit;
170
171   }
172 }
173
174 =item daemon_wait [ MAX ]
175
176 Waits until there are at most MAX daemon_fork() child processes running,
177 reaps the ones that are finished, and continues. MAX defaults to zero, i.e.
178 wait for everything to finish.
179
180 =cut
181
182 sub daemon_wait {
183   my $max = shift || 0;
184   while ($kids > $max) {
185     foreach my $pid (keys %kids) {
186       my $kid = waitpid($pid, WNOHANG);
187       if ( $kid > 0 ) {
188         $kids--;
189         delete $kids{$kid};
190       }
191     }
192     sleep(1);
193   }
194 }
195
196 =item daemon_reconnect
197
198 Checks whether the database connection is live, and reconnects if not.
199
200 =cut
201
202 sub daemon_reconnect {
203   my $dbh = $FS::UID::dbh;
204   unless ($dbh && $dbh->ping) {
205     warn "WARNING: connection to database lost, reconnecting...\n";
206
207     eval { $FS::UID::dbh = myconnect(); };
208
209     unless ( !$@ && $FS::UID::dbh && $FS::UID::dbh->ping ) {
210       warn "WARNING: still no connection to database, sleeping for retry...\n";
211       sleep 10;
212       next;
213     } else {
214       warn "WARNING: reconnected to database\n";
215     }
216   }
217 }
218
219 1;