3 # based on http://www.perlmonks.org/?node_id=582781 by Justin Hawkins
4 # and http://poe.perl.org/?POE_Cookbook/Web_Server_With_Forking
7 # modules and constants and variables, oh my
13 use constant DEBUG => 1; # Enable much runtime information.
14 use constant MAX_PROCESSES => 10; # Total server process count.
15 use constant SERVER_PORT => 8080; # Server port.
16 use constant TESTING_CHURN => 0; # Randomly test process respawning.
18 use POE; # Base features.
19 use POE::Filter::HTTPD; # For serving HTTP content.
20 use POE::Wheel::ReadWrite; # For socket I/O.
21 use POE::Wheel::SocketFactory; # For serving socket connections.
23 #use SOAP::Transport::HTTP;
24 use XMLRPC::Transport::HTTP;
25 use XMLRPC::Lite; # for XMLRPC::Serializer
27 use FS::UID qw(adminsuidsetup);
28 #use FS::SelfService::XMLRPC;
29 use FS::ClientAPI qw( load_clientapi_modules );
30 use FS::ClientAPI_XMLRPC;
34 base64 => [10, sub {$_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/}, 'as_base64'],
35 dateTime => [35, sub {$_[0] =~ /^\d{8}T\d\d:\d\d:\d\d$/}, 'as_dateTime'],
36 string => [40, sub {1}, 'as_string'],
39 # These are HTTP::Request headers that have methods.
41 qw( authorization authorization_basic
42 content content_encoding content_language content_length content_type
43 date expires from if_modified_since if_unmodified_since last_modified
44 method protocol proxy_authorization proxy_authorization_basic referer
45 server title url user_agent www_authenticate
48 # These are HTTP::Request headers that do not have methods.
50 qw( username opaque stale algorithm realm uri qop auth nonce cnonce
58 my $user = shift or die &usage;
61 load_clientapi_modules;
67 # Spawn up to MAX_PROCESSES server processes, and then run them. Exit
70 server_spawn(MAX_PROCESSES);
73 #XXX we probably want to sleep a bit and then try all over again...
80 ### Spawn the main server. This will run as the parent process.
83 my ($max_processes) = @_;
87 _start => \&server_start,
88 _stop => \&server_stop,
89 do_fork => \&server_do_fork,
90 got_error => \&server_got_error,
91 got_sig_int => \&server_got_sig_int,
92 got_sig_child => \&server_got_sig_child,
93 got_connection => \&server_got_connection,
94 _child => sub { undef },
96 heap => { max_processes => MAX_PROCESSES },
100 ### The main server session has started. Set up the server socket and
101 ### bookkeeping information, then fork the initial child processes.
104 my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
106 $heap->{server} = POE::Wheel::SocketFactory->new
107 ( BindPort => SERVER_PORT,
108 SuccessEvent => "got_connection",
109 FailureEvent => "got_error",
113 #XXX?#$kernel->sig( CHLD => "got_sig_child" );
114 $kernel->sig( INT => "got_sig_int" );
116 $heap->{children} = {};
117 $heap->{is_a_child} = 0;
119 warn "Server $$ has begun listening on port ", SERVER_PORT, "\n";
121 $kernel->yield("do_fork");
124 ### The server session has shut down. If this process has any
125 ### children, signal them to shutdown too.
129 DEBUG and warn "Server $$ stopped.\n";
132 #if ( my @children = keys %{ $heap->{children} } ) {
133 # DEBUG and warn "Server $$ is signaling children to stop.\n";
134 # kill INT => @children;
138 ### The server session has encountered an error. Shut it down.
140 sub server_got_error {
141 my ( $heap, $syscall, $errno, $error ) = @_[ HEAP, ARG0 .. ARG2 ];
142 warn( "Server $$ got $syscall error $errno: $error\n",
143 "Server $$ is shutting down.\n",
145 delete $heap->{server};
148 ### The server has a need to fork off more children. Only honor that
149 ### request form the parent, otherwise we would surely "forkbomb".
150 ### Fork off as many child processes as we need.
153 my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
155 return if $heap->{is_a_child};
157 #my $current_children = keys %{ $heap->{children} };
158 #for ( $current_children + 2 .. $heap->{max_processes} ) {
159 while (scalar(keys %{$heap->{children}}) < $heap->{max_processes}) {
161 DEBUG and warn "Server $$ is attempting to fork.\n";
165 unless ( defined($pid) ) {
167 warn( "Server $$ fork failed: $!\n",
168 "Server $$ will retry fork shortly.\n",
170 $kernel->delay( do_fork => 1 );
174 # Parent. Add the child process to its list.
176 $heap->{children}->{$pid} = 1;
177 $kernel->sig_child($pid, "got_sig_child");
181 # Child. Clear the child process list.
182 $kernel->has_forked();
183 DEBUG and warn "Server $$ forked successfully.\n";
184 $heap->{is_a_child} = 1;
185 $heap->{children} = {};
191 ### The server session received SIGINT. Don't handle the signal,
192 ### which in turn will trigger the process to exit gracefully.
194 sub server_got_sig_int {
195 DEBUG and warn "Server $$ received SIGINT.\n";
196 delete $_[HEAP]->{server};
197 $_[KERNEL]->sig_handled();
200 ### The server session received a SIGCHLD, indicating that some child
201 ### server has gone away. Remove the child's process ID from our
202 ### list, and trigger more fork() calls to spawn new children.
204 sub server_got_sig_child {
205 my ( $kernel, $heap, $child_pid ) = @_[ KERNEL, HEAP, ARG1 ];
207 return unless delete $heap->{children}->{$child_pid};
209 DEBUG and warn "Server $$ reaped child $child_pid.\n";
210 $kernel->yield("do_fork") if exists $_[HEAP]->{server};
213 ### The server session received a connection request. Spawn off a
214 ### client handler session to parse the request and respond to it.
216 sub server_got_connection {
217 my ( $heap, $socket, $peer_addr, $peer_port ) = @_[ HEAP, ARG0, ARG1, ARG2 ];
219 DEBUG and warn "Server $$ received a connection.\n";
221 POE::Session->create(
223 _start => \&client_start,
224 _stop => \&client_stop,
225 got_request => \&client_got_request,
226 got_flush => \&client_flushed_request,
227 got_error => \&client_got_error,
228 _parent => sub { 0 },
232 peer_addr => $peer_addr,
233 peer_port => $peer_port,
237 # Gracefully exit if testing process churn.
238 delete $heap->{server}
239 if TESTING_CHURN and $heap->{is_a_child} and ( rand() < 0.1 );
242 ### The client handler has started. Wrap its socket in a ReadWrite
243 ### wheel to begin interacting with it.
248 $heap->{client} = POE::Wheel::ReadWrite->new
249 ( Handle => $heap->{socket},
250 Filter => POE::Filter::HTTPD->new(),
251 InputEvent => "got_request",
252 ErrorEvent => "got_error",
253 FlushedEvent => "got_flush",
256 DEBUG and warn "Client handler $$/", $_[SESSION]->ID, " started.\n";
259 ### The client handler has stopped. Log that fact.
262 DEBUG and warn "Client handler $$/", $_[SESSION]->ID, " stopped.\n";
265 ### The client handler has received a request. If it's an
266 ### HTTP::Response object, it means some error has occurred while
267 ### parsing the request. Send that back and return immediately.
268 ### Otherwise parse and process the request, generating and sending an
269 ### HTTP::Response object in response.
271 sub client_got_request {
272 my ( $heap, $request ) = @_[ HEAP, ARG0 ];
276 my $serializer = new XMLRPC::Serializer(typelookup => \%typelookup);
278 #my $soap = SOAP::Transport::HTTP::Server
279 my $soap = XMLRPC::Transport::HTTP::Server
281 -> dispatch_to('FS::ClientAPI_XMLRPC')
282 -> serializer($serializer);
285 warn "Client handler $$/", $_[SESSION]->ID, " is handling a request.\n";
287 if ( $request->isa("HTTP::Response") ) {
288 $heap->{client}->put($request);
292 $soap->request($request);
294 my $response = $soap->response;
296 $heap->{client}->put($response);
299 #setup the database connection and other things FS::SelfService::XMLRPC
300 #expects to be in place. aka "kid time" in freeside-selfservice-server
301 sub freeside_kid_time {
303 #if we need a db connection in the parent
305 #$FS::UID::dbh->{InactiveDestroy} = 1;
306 #forksuidsetup($user);
308 adminsuidsetup($user);
313 ### The client handler received an error. Stop the ReadWrite wheel,
314 ### which also closes the socket.
316 sub client_got_error {
317 my ( $heap, $operation, $errnum, $errstr ) = @_[ HEAP, ARG0, ARG1, ARG2 ];
319 warn( "Client handler $$/", $_[SESSION]->ID,
320 " got $operation error $errnum: $errstr\n",
321 "Client handler $$/", $_[SESSION]->ID, " is shutting down.\n"
323 delete $heap->{client};
326 ### The client handler has flushed its response to the socket. We're
327 ### done with the client connection, so stop the ReadWrite wheel.
329 sub client_flushed_request {
332 warn( "Client handler $$/", $_[SESSION]->ID,
333 " flushed its response.\n",
334 "Client handler $$/", $_[SESSION]->ID, " is shutting down.\n"
336 delete $heap->{client};
340 die "Usage:\n\n freeside-selfservice-xmlrpcd user\n";