avoid setting logfile path too early, from #37802
[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 }
68
69 sub drop_root {
70   my $freeside_gid = scalar(getgrnam('freeside'))
71     or die "can't find freeside group\n";
72   $) = $freeside_gid;
73   $( = $freeside_gid;
74   #if freebsd can't setuid(), presumably it can't setgid() either.  grr fleabsd
75   ($(,$)) = ($),$();
76   $) = $freeside_gid;
77   
78   $> = $FS::UID::freeside_uid;
79   $< = $FS::UID::freeside_uid;
80   #freebsd is sofa king broken, won't setuid()
81   ($<,$>) = ($>,$<);
82   $> = $FS::UID::freeside_uid;
83 }
84
85 sub daemonize2 {
86   open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
87   setsid                    or die "Can't start a new session: $!";
88   open STDERR, '>&STDOUT'   or die "Can't dup stdout: $!";
89
90   $SIG{__DIE__} = \&_die;
91   $SIG{__WARN__} = \&_logmsg;
92
93   warn "$me starting\n";
94 }
95
96 sub sigint  { $sigint; }
97 sub sigterm { $sigterm; }
98
99 sub logfile { $logfile = shift; } #_logmsg('test'); }
100
101 sub myexit {
102   chomp( my $pid = slurp($pid_file) );
103   unlink $pid_file if -e $pid_file && $$ == $pid;
104   exit;  
105 }
106
107 sub _die {
108   die @_ if $^S; # $^S = 1 during an eval(), don't break exception handling
109   my $msg = shift;
110
111   chomp( my $pid = slurp($pid_file) );
112   unlink $pid_file if -e $pid_file && $$ == $pid;
113
114   _logmsg($msg);
115 }
116
117 sub _logmsg {
118   chomp( my $msg = shift );
119   # set the logfile sensibly
120   if (!$logfile) {
121     my $logname = $me;
122     $logname =~ s/^freeside-//;
123     logfile("%%%FREESIDE_LOG%%%/$logname-log.$FS::UID::datasrc");
124   }
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;