Added block detection to C::E::Daemon
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
CommitLineData
fc7ec1d9 1package Catalyst::Engine;
2
3use strict;
424b2705 4use base qw/Class::Data::Inheritable Class::Accessor::Fast/;
d70195d8 5use attributes ();
fc7ec1d9 6use UNIVERSAL::require;
6dc87a0f 7use CGI::Cookie;
fc7ec1d9 8use Data::Dumper;
9use HTML::Entities;
10use HTTP::Headers;
11use Time::HiRes qw/gettimeofday tv_interval/;
0f7ecc53 12use Text::ASCIITable;
fc7ec1d9 13use Catalyst::Request;
146554c5 14use Catalyst::Request::Upload;
fc7ec1d9 15use Catalyst::Response;
a268a011 16use Catalyst::Utils;
fc7ec1d9 17
18require Module::Pluggable::Fast;
19
99fe1710 20# For pretty dumps
fc7ec1d9 21$Data::Dumper::Terse = 1;
22
1abd6db7 23__PACKAGE__->mk_classdata('components');
e88fa058 24__PACKAGE__->mk_accessors(qw/counter request response state/);
fc7ec1d9 25
fc7ec1d9 26*comp = \&component;
27*req = \&request;
28*res = \&response;
29
06e1b616 30# For backwards compatibility
31*finalize_output = \&finalize_body;
32
99fe1710 33# For statistics
e88fa058 34our $COUNT = 1;
35our $START = time;
36our $RECURSION = 1000;
fc7ec1d9 37
38=head1 NAME
39
40Catalyst::Engine - The Catalyst Engine
41
42=head1 SYNOPSIS
43
44See L<Catalyst>.
45
46=head1 DESCRIPTION
47
23f9d934 48=head1 METHODS
fc7ec1d9 49
23f9d934 50=over 4
51
23f9d934 52=item $c->benchmark($coderef)
fc7ec1d9 53
54Takes a coderef with arguments and returns elapsed time as float.
55
56 my ( $elapsed, $status ) = $c->benchmark( sub { return 1 } );
57 $c->log->info( sprintf "Processing took %f seconds", $elapsed );
58
59=cut
60
61sub benchmark {
62 my $c = shift;
63 my $code = shift;
64 my $time = [gettimeofday];
65 my @return = &$code(@_);
66 my $elapsed = tv_interval $time;
67 return wantarray ? ( $elapsed, @return ) : $elapsed;
68}
69
23f9d934 70=item $c->comp($name)
71
72=item $c->component($name)
fc7ec1d9 73
74Get a component object by name.
75
76 $c->comp('MyApp::Model::MyModel')->do_stuff;
77
78Regex search for a component.
79
80 $c->comp('mymodel')->do_stuff;
81
82=cut
83
84sub component {
3245f607 85 my $c = shift;
99fe1710 86
e88fa058 87 if (@_) {
99fe1710 88
3245f607 89 my $name = shift;
90
91 if ( my $component = $c->components->{$name} ) {
92 return $component;
93 }
94
95 else {
96 for my $component ( keys %{ $c->components } ) {
97 return $c->components->{$component} if $component =~ /$name/i;
98 }
fc7ec1d9 99 }
100 }
99fe1710 101
3245f607 102 return sort keys %{ $c->components };
fc7ec1d9 103}
104
e88fa058 105=item $c->counter
106
107Returns a hashref containing coderefs and execution counts.
108(Needed for deep recursion detection)
109
a554cc3b 110=item $c->error
23f9d934 111
a554cc3b 112=item $c->error($error, ...)
23f9d934 113
a554cc3b 114=item $c->error($arrayref)
fc7ec1d9 115
a554cc3b 116Returns an arrayref containing error messages.
fc7ec1d9 117
a554cc3b 118 my @error = @{ $c->error };
fc7ec1d9 119
120Add a new error.
121
a554cc3b 122 $c->error('Something bad happened');
fc7ec1d9 123
124=cut
125
a554cc3b 126sub error {
fc7ec1d9 127 my $c = shift;
a554cc3b 128 my $error = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
129 push @{ $c->{error} }, @$error;
130 return $c->{error};
fc7ec1d9 131}
132
6dc87a0f 133=item $c->execute($class, $coderef)
134
135Execute a coderef in given class and catch exceptions.
136Errors are available via $c->error.
137
138=cut
139
140sub execute {
141 my ( $c, $class, $code ) = @_;
91571b7b 142 $class = $c->components->{$class} || $class;
6dc87a0f 143 $c->state(0);
39de91b0 144 my $callsub = ( caller(1) )[3];
99fe1710 145
e88fa058 146 my $action = '';
147 if ( $c->debug ) {
148 $action = $c->actions->{reverse}->{"$code"};
149 $action = "/$action" unless $action =~ /\-\>/;
150 $c->counter->{"$code"}++;
151
152 if ( $c->counter->{"$code"} > $RECURSION ) {
153 my $error = qq/Deep recursion detected in "$action"/;
154 $c->log->error($error);
155 $c->error($error);
156 $c->state(0);
157 return $c->state;
158 }
159
160 $action = "-> $action" if $callsub =~ /forward$/;
161 }
162
6dc87a0f 163 eval {
164 if ( $c->debug )
165 {
a7663693 166 my ( $elapsed, @state ) = $c->benchmark( $code, $class, $c, @{ $c->req->args } );
0f7ecc53 167 push @{ $c->{stats} }, [ $action, sprintf( '%fs', $elapsed ) ];
6dc87a0f 168 $c->state(@state);
169 }
3ceed047 170 else { $c->state( &$code( $class, $c, @{ $c->req->args } ) || 0 ) }
6dc87a0f 171 };
99fe1710 172
6dc87a0f 173 if ( my $error = $@ ) {
b9ffe28b 174
175 unless ( ref $error ) {
176 chomp $error;
177 $error = qq/Caught exception "$error"/;
178 }
179
6dc87a0f 180 $c->log->error($error);
b9ffe28b 181 $c->error($error);
6dc87a0f 182 $c->state(0);
183 }
184 return $c->state;
185}
186
23f9d934 187=item $c->finalize
fc7ec1d9 188
ca39d576 189Finalize request.
fc7ec1d9 190
191=cut
192
193sub finalize {
194 my $c = shift;
23f9d934 195
6dc87a0f 196 $c->finalize_cookies;
197
49490aab 198 if ( my $location = $c->response->redirect ) {
23f9d934 199 $c->log->debug(qq/Redirecting to "$location"/) if $c->debug;
6dc87a0f 200 $c->response->header( Location => $location );
e7c0c583 201 $c->response->status(302) if $c->response->status !~ /^3\d\d$/;
6dc87a0f 202 }
203
969647fd 204 if ( $#{ $c->error } >= 0 ) {
205 $c->finalize_error;
23f9d934 206 }
207
d7945f32 208 if ( !$c->response->body && $c->response->status !~ /^(1|3)\d\d$/ ) {
969647fd 209 $c->finalize_error;
210 }
fc7ec1d9 211
d7945f32 212 if ( $c->response->body && !$c->response->content_length ) {
213 use bytes; # play safe with a utf8 aware perl
214 $c->response->content_length( length $c->response->body );
fc7ec1d9 215 }
969647fd 216
fc7ec1d9 217 my $status = $c->finalize_headers;
06e1b616 218 $c->finalize_body;
fc7ec1d9 219 return $status;
220}
221
cd3bb248 222=item $c->finalize_output
223
224<obsolete>, see finalize_body
225
06e1b616 226=item $c->finalize_body
227
228Finalize body.
229
230=cut
231
232sub finalize_body { }
233
6dc87a0f 234=item $c->finalize_cookies
235
236Finalize cookies.
237
238=cut
239
240sub finalize_cookies {
241 my $c = shift;
242
243 while ( my ( $name, $cookie ) = each %{ $c->response->cookies } ) {
244 my $cookie = CGI::Cookie->new(
245 -name => $name,
246 -value => $cookie->{value},
247 -expires => $cookie->{expires},
248 -domain => $cookie->{domain},
249 -path => $cookie->{path},
250 -secure => $cookie->{secure} || 0
251 );
252
253 $c->res->headers->push_header( 'Set-Cookie' => $cookie->as_string );
254 }
255}
256
969647fd 257=item $c->finalize_error
258
ca39d576 259Finalize error.
969647fd 260
261=cut
262
263sub finalize_error {
264 my $c = shift;
265
266 $c->res->headers->content_type('text/html');
267 my $name = $c->config->{name} || 'Catalyst Application';
268
269 my ( $title, $error, $infos );
270 if ( $c->debug ) {
271 $error = join '<br/>', @{ $c->error };
272 $error ||= 'No output';
273 $title = $name = "$name on Catalyst $Catalyst::VERSION";
274 my $req = encode_entities Dumper $c->req;
275 my $res = encode_entities Dumper $c->res;
276 my $stash = encode_entities Dumper $c->stash;
277 $infos = <<"";
278<br/>
279<b><u>Request</u></b><br/>
280<pre>$req</pre>
281<b><u>Response</u></b><br/>
282<pre>$res</pre>
283<b><u>Stash</u></b><br/>
284<pre>$stash</pre>
285
286 }
287 else {
288 $title = $name;
289 $error = '';
290 $infos = <<"";
291<pre>
292(en) Please come back later
293(de) Bitte versuchen sie es spaeter nocheinmal
294(nl) Gelieve te komen later terug
295(no) Vennligst prov igjen senere
296(fr) Veuillez revenir plus tard
297(es) Vuelto por favor mas adelante
298(pt) Voltado por favor mais tarde
299(it) Ritornato prego più successivamente
300</pre>
301
302 $name = '';
303 }
e060fe05 304 $c->res->body( <<"" );
969647fd 305<html>
306<head>
307 <title>$title</title>
308 <style type="text/css">
309 body {
310 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
311 Tahoma, Arial, helvetica, sans-serif;
312 color: #ddd;
313 background-color: #eee;
314 margin: 0px;
315 padding: 0px;
316 }
317 div.box {
318 background-color: #ccc;
319 border: 1px solid #aaa;
320 padding: 4px;
321 margin: 10px;
322 -moz-border-radius: 10px;
323 }
324 div.error {
325 background-color: #977;
326 border: 1px solid #755;
327 padding: 8px;
328 margin: 4px;
329 margin-bottom: 10px;
330 -moz-border-radius: 10px;
331 }
332 div.infos {
333 background-color: #797;
334 border: 1px solid #575;
335 padding: 8px;
336 margin: 4px;
337 margin-bottom: 10px;
338 -moz-border-radius: 10px;
339 }
340 div.name {
341 background-color: #779;
342 border: 1px solid #557;
343 padding: 8px;
344 margin: 4px;
345 -moz-border-radius: 10px;
346 }
347 </style>
348</head>
349<body>
350 <div class="box">
351 <div class="error">$error</div>
352 <div class="infos">$infos</div>
353 <div class="name">$name</div>
354 </div>
355</body>
356</html>
357
358}
359
23f9d934 360=item $c->finalize_headers
fc7ec1d9 361
ca39d576 362Finalize headers.
fc7ec1d9 363
364=cut
365
366sub finalize_headers { }
367
e2fd5b5f 368=item $c->handler( $class, @arguments )
fc7ec1d9 369
ca39d576 370Handles the request.
fc7ec1d9 371
372=cut
373
6dc87a0f 374sub handler {
e2fd5b5f 375 my ( $class, @arguments ) = @_;
fc7ec1d9 376
377 # Always expect worst case!
378 my $status = -1;
379 eval {
d41516b2 380 my @stats = ();
99fe1710 381
fc7ec1d9 382 my $handler = sub {
e2fd5b5f 383 my $c = $class->prepare(@arguments);
d41516b2 384 $c->{stats} = \@stats;
63b763c5 385 $c->dispatch;
fc7ec1d9 386 return $c->finalize;
387 };
99fe1710 388
fc7ec1d9 389 if ( $class->debug ) {
390 my $elapsed;
391 ( $elapsed, $status ) = $class->benchmark($handler);
392 $elapsed = sprintf '%f', $elapsed;
e88fa058 393 my $av = sprintf '%.3f',
394 ( $elapsed == 0 ? '??' : ( 1 / $elapsed ) );
0f7ecc53 395 my $t = Text::ASCIITable->new;
396 $t->setCols( 'Action', 'Time' );
3f36a3a3 397 $t->setColWidth( 'Action', 64, 1 );
398 $t->setColWidth( 'Time', 9, 1 );
0822f9a4 399
cd677e12 400 for my $stat (@stats) { $t->addRow( $stat->[0], $stat->[1] ) }
0f7ecc53 401 $class->log->info( "Request took $elapsed" . "s ($av/s)",
402 $t->draw );
fc7ec1d9 403 }
404 else { $status = &$handler }
99fe1710 405
fc7ec1d9 406 };
99fe1710 407
fc7ec1d9 408 if ( my $error = $@ ) {
409 chomp $error;
410 $class->log->error(qq/Caught exception in engine "$error"/);
411 }
99fe1710 412
fc7ec1d9 413 $COUNT++;
414 return $status;
415}
416
e2fd5b5f 417=item $c->prepare(@arguments)
fc7ec1d9 418
a554cc3b 419Turns the engine-specific request( Apache, CGI ... )
420into a Catalyst context .
fc7ec1d9 421
422=cut
423
424sub prepare {
e2fd5b5f 425 my ( $class, @arguments ) = @_;
99fe1710 426
fc7ec1d9 427 my $c = bless {
e88fa058 428 counter => {},
fc7ec1d9 429 request => Catalyst::Request->new(
430 {
431 arguments => [],
432 cookies => {},
433 headers => HTTP::Headers->new,
434 parameters => {},
bfde09a2 435 secure => 0,
fc7ec1d9 436 snippets => [],
437 uploads => {}
438 }
439 ),
440 response => Catalyst::Response->new(
bfde09a2 441 {
03222156 442 body => '',
bfde09a2 443 cookies => {},
444 headers => HTTP::Headers->new,
03222156 445 status => 200
bfde09a2 446 }
fc7ec1d9 447 ),
b768faa3 448 stash => {},
449 state => 0
fc7ec1d9 450 }, $class;
99fe1710 451
fc7ec1d9 452 if ( $c->debug ) {
453 my $secs = time - $START || 1;
454 my $av = sprintf '%.3f', $COUNT / $secs;
1a0250cb 455 $c->log->debug('**********************************');
fc7ec1d9 456 $c->log->debug("* Request $COUNT ($av/s) [$$]");
1a0250cb 457 $c->log->debug('**********************************');
fc7ec1d9 458 $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION );
459 }
99fe1710 460
e2fd5b5f 461 $c->prepare_request(@arguments);
bfde09a2 462 $c->prepare_connection;
ac733264 463 $c->prepare_headers;
1a80619d 464 $c->prepare_cookies;
bfde09a2 465 $c->prepare_path;
06e1b616 466 $c->prepare_action;
99fe1710 467
0556eb49 468 my $method = $c->req->method || '';
469 my $path = $c->req->path || '';
0556eb49 470 my $address = $c->req->address || '';
06e1b616 471
b4ca0ee8 472 $c->log->debug(qq/"$method" request for "$path" from $address/)
0556eb49 473 if $c->debug;
99fe1710 474
06e1b616 475 if ( $c->request->method eq 'POST' and $c->request->content_length ) {
476
477 if ( $c->req->content_type eq 'application/x-www-form-urlencoded' ) {
478 $c->prepare_parameters;
479 }
480 elsif ( $c->req->content_type eq 'multipart/form-data' ) {
481 $c->prepare_parameters;
482 $c->prepare_uploads;
483 }
484 else {
485 $c->prepare_body;
486 }
487 }
488
489 if ( $c->request->method eq 'GET' ) {
490 $c->prepare_parameters;
491 }
c85ff642 492
493 if ( $c->debug && keys %{ $c->req->params } ) {
0f7ecc53 494 my $t = Text::ASCIITable->new;
495 $t->setCols( 'Key', 'Value' );
0822f9a4 496 $t->setColWidth( 'Key', 37, 1 );
497 $t->setColWidth( 'Value', 36, 1 );
f78172f1 498 for my $key ( sort keys %{ $c->req->params } ) {
6d1ab915 499 my $param = $c->req->params->{$key};
500 my $value = defined($param) ? $param : '';
cd677e12 501 $t->addRow( $key, $value );
c85ff642 502 }
0f7ecc53 503 $c->log->debug( 'Parameters are', $t->draw );
c85ff642 504 }
99fe1710 505
fc7ec1d9 506 return $c;
507}
508
23f9d934 509=item $c->prepare_action
fc7ec1d9 510
ca39d576 511Prepare action.
fc7ec1d9 512
513=cut
514
515sub prepare_action {
516 my $c = shift;
517 my $path = $c->req->path;
518 my @path = split /\//, $c->req->path;
519 $c->req->args( \my @args );
99fe1710 520
fc7ec1d9 521 while (@path) {
7833fdfc 522 $path = join '/', @path;
0169d3a8 523 if ( my $result = ${ $c->get_action($path) }[0] ) {
fc7ec1d9 524
525 # It's a regex
526 if ($#$result) {
7e5adedd 527 my $match = $result->[1];
528 my @snippets = @{ $result->[2] };
81f6fc50 529 $c->log->debug(
530 qq/Requested action is "$path" and matched "$match"/)
fc7ec1d9 531 if $c->debug;
532 $c->log->debug(
533 'Snippets are "' . join( ' ', @snippets ) . '"' )
534 if ( $c->debug && @snippets );
535 $c->req->action($match);
536 $c->req->snippets( \@snippets );
537 }
99fe1710 538
fc7ec1d9 539 else {
540 $c->req->action($path);
81f6fc50 541 $c->log->debug(qq/Requested action is "$path"/) if $c->debug;
fc7ec1d9 542 }
99fe1710 543
fc7ec1d9 544 $c->req->match($path);
fc7ec1d9 545 last;
546 }
547 unshift @args, pop @path;
548 }
99fe1710 549
fc7ec1d9 550 unless ( $c->req->action ) {
ac733264 551 $c->req->action('default');
87e67021 552 $c->req->match('');
fc7ec1d9 553 }
99fe1710 554
5783a9a5 555 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
556 if ( $c->debug && @args );
fc7ec1d9 557}
558
06e1b616 559=item $c->prepare_body
560
561Prepare message body.
562
563=cut
564
565sub prepare_body { }
566
c9afa5fc 567=item $c->prepare_connection
0556eb49 568
ca39d576 569Prepare connection.
0556eb49 570
571=cut
572
573sub prepare_connection { }
574
c9afa5fc 575=item $c->prepare_cookies
fc7ec1d9 576
ca39d576 577Prepare cookies.
fc7ec1d9 578
579=cut
580
6dc87a0f 581sub prepare_cookies {
582 my $c = shift;
583
584 if ( my $header = $c->request->header('Cookie') ) {
585 $c->req->cookies( { CGI::Cookie->parse($header) } );
586 }
587}
fc7ec1d9 588
23f9d934 589=item $c->prepare_headers
fc7ec1d9 590
ca39d576 591Prepare headers.
fc7ec1d9 592
593=cut
594
595sub prepare_headers { }
596
23f9d934 597=item $c->prepare_parameters
fc7ec1d9 598
ca39d576 599Prepare parameters.
fc7ec1d9 600
601=cut
602
603sub prepare_parameters { }
604
23f9d934 605=item $c->prepare_path
fc7ec1d9 606
ca39d576 607Prepare path and base.
fc7ec1d9 608
609=cut
610
611sub prepare_path { }
612
23f9d934 613=item $c->prepare_request
fc7ec1d9 614
ca39d576 615Prepare the engine request.
fc7ec1d9 616
617=cut
618
619sub prepare_request { }
620
23f9d934 621=item $c->prepare_uploads
fc7ec1d9 622
ca39d576 623Prepare uploads.
fc7ec1d9 624
625=cut
626
627sub prepare_uploads { }
628
c9afa5fc 629=item $c->run
630
ca39d576 631Starts the engine.
c9afa5fc 632
633=cut
634
635sub run { }
636
61b1e958 637=item $c->request
fc7ec1d9 638
ca39d576 639=item $c->req
23f9d934 640
ca39d576 641Returns a C<Catalyst::Request> object.
fc7ec1d9 642
ca39d576 643 my $req = $c->req;
61b1e958 644
645=item $c->response
646
ca39d576 647=item $c->res
648
fc7ec1d9 649Returns a C<Catalyst::Response> object.
650
651 my $res = $c->res;
652
23f9d934 653=item $class->setup
fc7ec1d9 654
ca39d576 655Setup.
fc7ec1d9 656
657 MyApp->setup;
658
659=cut
660
661sub setup {
662 my $self = shift;
a268a011 663
664 # Initialize our data structure
665 $self->components( {} );
666
fc7ec1d9 667 $self->setup_components;
a268a011 668
669 if ( $self->debug ) {
670 my $t = Text::ASCIITable->new;
671 $t->setOptions( 'hide_HeadRow', 1 );
672 $t->setOptions( 'hide_HeadLine', 1 );
673 $t->setCols('Class');
674 $t->setColWidth( 'Class', 75, 1 );
675 $t->addRow($_) for sort keys %{ $self->components };
676 $self->log->debug( 'Loaded components', $t->draw )
677 if ( @{ $t->{tbl_rows} } );
678 }
679
680 # Add our self to components, since we are also a component
681 $self->components->{ $self } = $self;
682
683 $self->setup_actions;
684
fc7ec1d9 685 if ( $self->debug ) {
686 my $name = $self->config->{name} || 'Application';
687 $self->log->info("$name powered by Catalyst $Catalyst::VERSION");
688 }
689}
690
23f9d934 691=item $class->setup_components
fc7ec1d9 692
ca39d576 693Setup components.
fc7ec1d9 694
695=cut
696
697sub setup_components {
698 my $self = shift;
66294129 699
a268a011 700 my $callback = sub {
701 my ( $component, $context ) = @_;
702
703 unless ( $component->isa('Catalyst::Base') ) {
704 return $component;
705 }
706
707 my $suffix = Catalyst::Utils::class2classsuffix($component);
708 my $config = $self->config->{$suffix} || {};
709
710 my $instance;
711
712 eval {
713 $instance = $component->new( $context, $config );
714 };
715
846772b7 716 if ( my $error = $@ ) {
717 chomp $error;
718 die qq/Couldn't instantiate component "$component", "$error"/;
a268a011 719 }
720
721 return $instance;
722 };
723
724 eval {
725 Module::Pluggable::Fast->import(
726 name => '_components',
727 search => [
728 "$self\::Controller", "$self\::C",
729 "$self\::Model", "$self\::M",
730 "$self\::View", "$self\::V"
731 ],
732 callback => $callback
733 );
734 };
3245f607 735
b18987fe 736 if ( my $error = $@ ) {
737 chomp $error;
738 die qq/Couldn't load components "$error"/;
739 }
99fe1710 740
a268a011 741 for my $component ( $self->_components($self) ) {
742 $self->components->{ ref $component || $component } = $component;
3245f607 743 }
fc7ec1d9 744}
745
63b763c5 746=item $c->state
747
748Contains the return value of the last executed action.
749
23f9d934 750=item $c->stash
fc7ec1d9 751
ca39d576 752Returns a hashref containing all your data.
fc7ec1d9 753
754 $c->stash->{foo} ||= 'yada';
755 print $c->stash->{foo};
756
757=cut
758
759sub stash {
760 my $self = shift;
e88fa058 761 if (@_) {
c19e2f4a 762 my $stash = @_ > 1 ? {@_} : $_[0];
fc7ec1d9 763 while ( my ( $key, $val ) = each %$stash ) {
764 $self->{stash}->{$key} = $val;
765 }
766 }
767 return $self->{stash};
768}
769
23f9d934 770=back
771
fc7ec1d9 772=head1 AUTHOR
773
774Sebastian Riedel, C<sri@cpan.org>
775
776=head1 COPYRIGHT
777
778This program is free software, you can redistribute it and/or modify it under
779the same terms as Perl itself.
780
781=cut
782
7831;