RT #31482 making sure the tax class is not editable after the customer has been billed.
[freeside.git] / FS / FS / Daemon / Preforking.pm
1 package FS::Daemon::Preforking;
2 use base 'Exporter';
3
4 =head1 NAME
5
6 FS::Daemon::Preforking - A preforking web server
7
8 =head1 SYNOPSIS
9
10   use FS::Daemon::Preforking qw( freeside_init1 freeside_init2 daemon_run );
11
12   my $me = 'mydaemon'; #keep unique among fs daemons, for logfiles etc.
13
14   freeside_init1($me); #daemonize, drop root and connect to freeside
15
16   #do setup tasks which should throw an error to the shell starting the daemon
17
18   freeside_init2($me); #move logging to logfile and disassociate from terminal
19
20   #do setup tasks which will warn/error to the log file, such as declining to
21   # run if our config is not in place
22
23   daemon_run(
24     'port'           => 5454, #keep unique among fs daemons
25     'handle_request' => \&handle_request,
26   );
27
28   sub handle_request {
29     my $request = shift; #HTTP::Request object
30
31     #... do your thing
32
33     return $response; #HTTP::Response object
34
35   }
36
37 =head1 AUTHOR
38
39 Based on L<http://www.perlmonks.org/?node_id=582781> by Justin Hawkins
40
41 and L<http://poe.perl.org/?POE_Cookbook/Web_Server_With_Forking>
42
43 =cut
44
45 use warnings;
46 use strict;
47
48 use constant DEBUG         => 0;       # Enable much runtime information.
49 use constant MAX_PROCESSES => 10;      # Total server process count.
50 #use constant TESTING_CHURN => 0;       # Randomly test process respawning.
51
52 use vars qw( @EXPORT_OK $FREESIDE_LOG $SERVER_PORT $user $handle_request );
53 @EXPORT_OK = qw( freeside_init1 freeside_init2 daemon_run );
54 $FREESIDE_LOG = '%%%FREESIDE_LOG%%%';
55
56 use POE 1.2;                     # Base features.
57 use POE::Filter::HTTPD;          # For serving HTTP content.
58 use POE::Wheel::ReadWrite;       # For socket I/O.
59 use POE::Wheel::SocketFactory;   # For serving socket connections.
60
61 use FS::Daemon qw( daemonize1 drop_root logfile daemonize2 );
62 use FS::UID qw( adminsuidsetup forksuidsetup dbh );
63
64 #use FS::TicketSystem;
65
66 sub freeside_init1 {
67   my $name = shift;
68
69   $user = shift @ARGV or die &usage($name);
70
71   $FS::Daemon::NOSIG = 1;
72   $FS::Daemon::PID_NEWSTYLE = 1;
73   daemonize1($name);
74
75   POE::Kernel->has_forked(); #daemonize forks...
76
77   drop_root();
78
79   adminsuidsetup($user);
80 }
81
82 sub freeside_init2 {
83   my $name = shift;
84
85   logfile("$FREESIDE_LOG/$name.log");
86
87   daemonize2();
88
89 }
90
91 sub daemon_run {
92   my %opt = @_;
93   $SERVER_PORT = $opt{port};
94   $handle_request = $opt{handle_request};
95
96   #parent doesn't need to hold a DB connection open
97   dbh->disconnect;
98   undef $FS::UID::dbh;
99
100   server_spawn(MAX_PROCESSES);
101   POE::Kernel->run();
102   #exit;
103
104 }
105
106 ### Spawn the main server.  This will run as the parent process.
107
108 sub server_spawn {
109     my ($max_processes) = @_;
110
111     POE::Session->create(
112       inline_states => {
113         _start         => \&server_start,
114         _stop          => \&server_stop,
115         do_fork        => \&server_do_fork,
116         got_error      => \&server_got_error,
117         got_sig_int    => \&server_got_sig_int,
118         got_sig_child  => \&server_got_sig_child,
119         got_connection => \&server_got_connection,
120         _child         => sub { undef },
121       },
122       heap => { max_processes => MAX_PROCESSES },
123     );
124 }
125
126 ### The main server session has started.  Set up the server socket and
127 ### bookkeeping information, then fork the initial child processes.
128
129 sub server_start {
130     my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
131
132     $heap->{server} = POE::Wheel::SocketFactory->new
133       ( BindPort     => $SERVER_PORT,
134         SuccessEvent => "got_connection",
135         FailureEvent => "got_error",
136         Reuse        => "yes",
137       );
138
139     $kernel->sig( INT  => "got_sig_int" );
140     $kernel->sig( TERM => "got_sig_int" ); #huh
141
142     $heap->{children}   = {};
143     $heap->{is_a_child} = 0;
144
145     warn "Server $$ has begun listening on port $SERVER_PORT\n";
146
147     $kernel->yield("do_fork");
148 }
149
150 ### The server session has shut down.  If this process has any
151 ### children, signal them to shutdown too.
152
153 sub server_stop {
154     my $heap = $_[HEAP];
155     DEBUG and warn "Server $$ stopped.\n";
156
157     if ( my @children = keys %{ $heap->{children} } ) {
158         DEBUG and warn "Server $$ is signaling children to stop.\n";
159         kill INT => @children;
160     }
161 }
162
163 ### The server session has encountered an error.  Shut it down.
164
165 sub server_got_error {
166     my ( $heap, $syscall, $errno, $error ) = @_[ HEAP, ARG0 .. ARG2 ];
167       warn( "Server $$ got $syscall error $errno: $error\n",
168         "Server $$ is shutting down.\n",
169       );
170     delete $heap->{server};
171 }
172
173 ### The server has a need to fork off more children.  Only honor that
174 ### request form the parent, otherwise we would surely "forkbomb".
175 ### Fork off as many child processes as we need.
176
177 sub server_do_fork {
178     my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
179
180     return if $heap->{is_a_child};
181
182     #my $current_children = keys %{ $heap->{children} };
183     #for ( $current_children + 2 .. $heap->{max_processes} ) {
184     while (scalar(keys %{$heap->{children}}) < $heap->{max_processes}) {
185
186         DEBUG and warn "Server $$ is attempting to fork.\n";
187
188         my $pid = fork();
189
190         unless ( defined($pid) ) {
191             DEBUG and
192               warn( "Server $$ fork failed: $!\n",
193                 "Server $$ will retry fork shortly.\n",
194               );
195             $kernel->delay( do_fork => 1 );
196             return;
197         }
198
199         # Parent.  Add the child process to its list.
200         if ($pid) {
201             $heap->{children}->{$pid} = 1;
202             $kernel->sig_child($pid, "got_sig_child");
203             next;
204         }
205
206         # Child.  Clear the child process list.
207         $kernel->has_forked();
208         DEBUG and warn "Server $$ forked successfully.\n";
209         $heap->{is_a_child} = 1;
210         $heap->{children}   = {};
211
212         #freeside db connection, etc.
213         forksuidsetup($user);
214
215         #why isn't this needed ala freeside-selfservice-server??
216         #FS::TicketSystem->init();
217
218         return;
219     }
220 }
221
222 ### The server session received SIGINT.  Don't handle the signal,
223 ### which in turn will trigger the process to exit gracefully.
224
225 sub server_got_sig_int {
226     my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
227     DEBUG and warn "Server $$ received SIGINT/TERM.\n";
228
229     if ( my @children = keys %{ $heap->{children} } ) {
230         DEBUG and warn "Server $$ is signaling children to stop.\n";
231         kill INT => @children;
232     }
233
234     delete $heap->{server};
235     $kernel->sig_handled();
236 }
237
238 ### The server session received a SIGCHLD, indicating that some child
239 ### server has gone away.  Remove the child's process ID from our
240 ### list, and trigger more fork() calls to spawn new children.
241
242 sub server_got_sig_child {
243     my ( $kernel, $heap, $child_pid ) = @_[ KERNEL, HEAP, ARG1 ];
244
245     return unless delete $heap->{children}->{$child_pid};
246
247    DEBUG and warn "Server $$ reaped child $child_pid.\n";
248    $kernel->yield("do_fork") if exists $_[HEAP]->{server};
249 }
250
251 ### The server session received a connection request.  Spawn off a
252 ### client handler session to parse the request and respond to it.
253
254 sub server_got_connection {
255     my ( $heap, $socket, $peer_addr, $peer_port ) = @_[ HEAP, ARG0, ARG1, ARG2 ];
256
257     DEBUG and warn "Server $$ received a connection.\n";
258
259     POE::Session->create(
260       inline_states => {
261         _start      => \&client_start,
262         _stop       => \&client_stop,
263         got_request => \&client_got_request,
264         got_flush   => \&client_flushed_request,
265         got_error   => \&client_got_error,
266         _parent     => sub { 0 },
267       },
268       heap => {
269         socket    => $socket,
270         peer_addr => $peer_addr,
271         peer_port => $peer_port,
272       },
273     );
274
275 #    # Gracefully exit if testing process churn.
276 #    delete $heap->{server}
277 #      if TESTING_CHURN and $heap->{is_a_child} and ( rand() < 0.1 );
278 }
279
280 ### The client handler has started.  Wrap its socket in a ReadWrite
281 ### wheel to begin interacting with it.
282
283 sub client_start {
284     my $heap = $_[HEAP];
285
286     $heap->{client} = POE::Wheel::ReadWrite->new
287       ( Handle => $heap->{socket},
288         Filter       => POE::Filter::HTTPD->new(),
289         InputEvent   => "got_request",
290         ErrorEvent   => "got_error",
291         FlushedEvent => "got_flush",
292       );
293
294     DEBUG and warn "Client handler $$/", $_[SESSION]->ID, " started.\n";
295 }
296
297 ### The client handler has stopped.  Log that fact.
298
299 sub client_stop {
300     DEBUG and warn "Client handler $$/", $_[SESSION]->ID, " stopped.\n";
301 }
302
303 ### The client handler has received a request.  If it's an
304 ### HTTP::Response object, it means some error has occurred while
305 ### parsing the request.  Send that back and return immediately.
306 ### Otherwise parse and process the request, generating and sending an
307 ### HTTP::Response object in response.
308
309 sub client_got_request {
310     my ( $heap, $request ) = @_[ HEAP, ARG0 ];
311
312     DEBUG and
313       warn "Client handler $$/", $_[SESSION]->ID, " is handling a request.\n";
314
315     if ( $request->isa("HTTP::Response") ) {
316         $heap->{client}->put($request);
317         return;
318    }
319
320     forksuidsetup($user) unless dbh && dbh->ping;
321
322     my $response = &{ $handle_request }( $request );
323
324     $heap->{client}->put($response);
325 }
326
327 ### The client handler received an error.  Stop the ReadWrite wheel,
328 ### which also closes the socket.
329
330 sub client_got_error {
331     my ( $heap, $operation, $errnum, $errstr ) = @_[ HEAP, ARG0, ARG1, ARG2 ];
332     DEBUG and
333       warn( "Client handler $$/", $_[SESSION]->ID,
334         " got $operation error $errnum: $errstr\n",
335         "Client handler $$/", $_[SESSION]->ID, " is shutting down.\n"
336       );
337     delete $heap->{client};
338 }
339
340 ### The client handler has flushed its response to the socket.  We're
341 ### done with the client connection, so stop the ReadWrite wheel.
342
343 sub client_flushed_request {
344     my $heap = $_[HEAP];
345     DEBUG and
346       warn( "Client handler $$/", $_[SESSION]->ID,
347         " flushed its response.\n",
348         "Client handler $$/", $_[SESSION]->ID, " is shutting down.\n"
349       );
350     delete $heap->{client};
351 }
352
353 sub usage {
354   my $name = shift;
355   die "Usage:\n\n  freeside-$name user\n";
356 }
357
358 1;