File Coverage

blib/lib/YATT/Lite/WebMVC0/SiteApp.pm
Criterion Covered Total %
statement 194 267 72.6
branch 55 114 48.2
condition 31 92 33.7
subroutine 43 53 81.1
pod 5 31 16.1
total 328 557 58.8


line stmt bran cond sub pod time code
1             package YATT::Lite::WebMVC0::SiteApp;
2 7     7   49395 use strict;
  7         17  
  7         246  
3 7     7   43 use warnings qw(FATAL all NONFATAL misc);
  7         16  
  7         310  
4 7     7   39 use Carp;
  7         14  
  7         461  
5 7     7   41 use YATT::Lite::Breakpoint;
  7         13  
  7         444  
6             sub MY () {__PACKAGE__}
7              
8 7     7   147 use 5.010; no if $] >= 5.017011, warnings => "experimental";
  7     7   25  
  7         75  
  7         995  
  7         327  
9              
10             #========================================
11             # Dispatcher Layer: load and run corresponding DirApp for incoming request.
12             #========================================
13              
14 7     7   557 use parent qw(YATT::Lite::Factory);
  7         11  
  7         58  
15 7         55 use YATT::Lite::MFields qw/cf_noheader
16             cf_is_psgi
17             cf_no_nested_query
18             allow_debug_from
19             cf_debug_cgi
20             cf_debug_psgi
21             cf_debug_connection
22             cf_debug_backend
23             cf_psgi_static
24             cf_psgi_fallback
25             cf_per_role_docroot
26             cf_per_role_docroot_key
27             cf_default_role
28             cf_backend
29             cf_site_config
30             cf_logfile
31             cf_debug_allowed_ip
32             cf_overwrite_status_code_for_errors_as
33             re_handled_ext
34 7     7   566 /;
  7         15  
35              
36 7         937 use YATT::Lite::Util qw(cached_in split_path catch
37             lookup_path nonempty try_invoke
38             mk_http_status
39             default ckrequire
40             escape
41 7     7   45 lexpand rootname extname untaint_any terse_dump);
  7         15  
42 7     7   4451 use YATT::Lite::Util::CmdLine qw(parse_params);
  7         21  
  7         469  
43 7     7   47 use YATT::Lite qw/Entity *SYS *CON/;
  7         196  
  7         130  
44 7     7   4142 use YATT::Lite::WebMVC0::DirApp ();
  7         20  
  7         451  
45             sub DirApp () {'YATT::Lite::WebMVC0::DirApp'}
46             sub default_default_app () {'YATT::Lite::WebMVC0::DirApp'}
47              
48 7     7   44 use File::Basename;
  7         20  
  7         5099  
49              
50             sub after_new {
51 8     8 1 21 (my MY $self) = @_;
52 8         65 $self->SUPER::after_new();
53 8         226 $self->{re_handled_ext} = qr{\.($self->{cf_ext_public}|ydo)$};
54 8   33     66 $self->{cf_per_role_docroot_key} ||= $self->default_per_role_docroot_key;
55 8   33     56 $self->{cf_default_role} ||= $self->default_default_role;
56             }
57              
58 8     8 0 29 sub default_per_role_docroot_key { 'yatt.role' }
59 8     8 0 37 sub default_default_role { 'nobody' }
60              
61             sub _cf_delegates {
62             (shift->SUPER::_cf_delegates
63 12     12   62 , qw(overwrite_status_code_for_errors_as));
64             }
65              
66             #========================================
67             # runas($type, $fh, \%ENV, \@ARGV) ... for CGI/FCGI support.
68             #========================================
69              
70             sub runas {
71 16     16 0 12293 (my $this, my $type) = splice @_, 0, 2;
72 16 50       72 my MY $self = ref $this ? $this : $this->new;
73 16 50       144 my $sub = $self->can("runas_$type")
74             or die "\n\nUnknown runas type: $type";
75 16         64 $sub->($self, @_);
76             }
77              
78             sub runas_cgi {
79 16     16 0 140 require YATT::Lite::WebMVC0::SiteApp::CGI;
80 16         94 shift->_runas_cgi(@_);
81             }
82             sub runas_fcgi {
83 0     0 0 0 require YATT::Lite::WebMVC0::SiteApp::FCGI;
84 0         0 shift->_runas_fcgi(@_);
85             }
86              
87             #========================================
88              
89             # Dispatcher::get_dirhandler
90             # -> Util::cached_in
91             # -> Factory::load
92             # -> Factory::buildspec
93              
94             sub get_dirhandler {
95 29     29 0 69 (my MY $self, my $dirPath) = @_;
96 29         273 $dirPath =~ s,/*$,,;
97 29   66     207 $self->{path2yatt}{$dirPath} ||= $self->load_yatt($dirPath);
98             }
99              
100             sub get_lochandler {
101 31     31 0 19622 (my MY $self, my ($location, $tmpldir)) = @_;
102 31 50       105 if ($self->{cf_per_role_docroot}) {
103             # When per_role_docroot is on, $tmpldir already points
104             # $per_role_docroot/$role. So just append $location.
105 0         0 $self->get_dirhandler($tmpldir.$location);
106             } else {
107 31         154 $self->SUPER::get_lochandler($location, $tmpldir);
108             }
109             }
110              
111             #----------------------------------------
112             # preload_handlers
113              
114             sub preload_apps {
115 1     1 0 3 (my MY $self, my (@dir)) = @_;
116 1 50       6 push @dir, $self->{cf_doc_root} unless @dir;
117              
118 1         2 my @apps;
119 1         5 foreach my $dir ($self->find_apps(@dir)) {
120 3         11 push @apps, my $app = $self->get_dirhandler($dir);
121             }
122 1         7 @apps;
123             }
124              
125             sub find_apps {
126 1     1 0 3 (my MY $self, my @dir) = @_;
127 1         6 require File::Find;
128 1         2 my @apps;
129             my $handler = sub {
130 10 100   10   711 push @apps, $_ if -d;
131 1         4 };
132 1         111 File::Find::find({wanted => $handler
133             , no_chdir => 1
134             , follow_skip => 2}
135             , @dir
136             );
137 1         7 @apps;
138             }
139              
140             #========================================
141             # PSGI Adaptor
142             #========================================
143 7     7   47 use YATT::Lite::PSGIEnv;
  7         15  
  7         65  
144              
145             sub to_app {
146 4     4 0 523 my MY $self = shift;
147             # XXX: Should check it.
148             # unless (defined $self->{cf_app_root}) {
149             # croak "app_root is undef!";
150             # }
151 4 50 33     23 unless (defined $self->{cf_doc_root}
152             or defined $self->{cf_per_role_docroot}) {
153 0         0 croak "document_root is undef!";
154             }
155 4         31 return $self->SUPER::to_app(@_);
156             }
157              
158             sub prepare_app {
159 4     4 1 8 (my MY $self) = @_;
160 4         12 $self->{cf_is_psgi} = 1;
161 4         175 require Plack::Request;
162 4         133596 require Plack::Response;
163 4         12 my $backend;
164 4 100 66     50 if ($backend = $self->{cf_backend}
165             and my $sub = $backend->can('startup')) {
166 1         4 $sub->($backend, $self, $self->preload_apps);
167             }
168             }
169              
170             sub call {
171 37     37 1 72 (my MY $self, my Env $env) = @_;
172              
173 37         132 YATT::Lite::Breakpoint::break_psgi_call();
174              
175 37 50       110 if ($self->has_htdebug("env")) {
176 0   0     0 return $self->psgi_dump(map {"$_\t".($env->{$_}//"(undef)")."\n"}
  0         0  
177             sort keys %$env);
178             }
179              
180 37 100 66     133 if (my $deny = $self->has_forbidden_path($env->{PATH_INFO})
181             // $self->has_forbidden_path($env->{PATH_TRANSLATED})) {
182 3         22 return $self->psgi_error(403, "Forbidden $deny");
183             }
184              
185 34 50 33     274 if (not $self->{cf_no_unicode_params}
186             and $self->{cf_output_encoding}) {
187             $env->{PATH_INFO} = Encode::decode($self->{cf_output_encoding}
188 34         162 , $env->{PATH_INFO});
189             }
190              
191 34 100 100     2245 if ($self->{loc2psgi_dict}
192             and my $psgi_app = $self->lookup_psgi_mount($env->{PATH_INFO})) {
193 3         20 require Plack::Util;
194 3         20 return Plack::Util::run_app($psgi_app, $env);
195             }
196              
197             # XXX: user_dir?
198 31         99 my ($tmpldir, $loc, $file, $trailer, $is_index)
199             = my @pi = $self->split_path_info($env);
200              
201 31         117 my ($realdir, $virtdir);
202 31 100       93 if (@pi) {
203 30         56 $realdir = "$tmpldir$loc";
204             $virtdir = defined $self->{cf_doc_root}
205 30 50       120 ? "$self->{cf_doc_root}$loc" : $realdir;
206             }
207              
208 31 50       83 if ($self->has_htdebug("path_info")) {
209 0         0 return $self->psgi_dump([tmpldir => $tmpldir]
210             , [loc => $loc]
211             , [file => $file]
212             , [trailer => $trailer]
213             , [virtdir => $virtdir, realdir => $realdir]
214             );
215             }
216              
217 31 50       91 if ($self->{cf_debug_psgi}) {
218             # XXX: should be configurable.
219 0 0       0 if (my $errfh = fileno(STDERR) ? \*STDERR : $env->{'psgi.errors'}) {
    0          
220             print $errfh join("\t"
221             , "# REQ: "
222             , terse_dump([tmpldir => $tmpldir]
223             , [loc => $loc]
224             , [file => $file]
225             , [trailer => $trailer]
226             , ['all templdirs', $self->{tmpldirs}]
227 0         0 , map {[$_ => $env->{$_}]} sort keys %$env)
  0         0  
228             ), "\n";
229             }
230             }
231              
232 31 100       78 unless (@pi) {
233 1         5 return $self->psgi_handle_fallback($env);
234             }
235              
236 30 50       658 unless (-d $realdir) {
237 0         0 return $self->psgi_error(404, "Not found: $loc");
238             }
239              
240             # Default index file.
241             # Note: Files may placed under (one of) tmpldirs instead of docroot.
242 30 50       133 if ($file eq '') {
    50          
243 0         0 $file = "$self->{cf_index_name}.$self->{cf_ext_public}";
244             } elsif ($file eq $self->{cf_index_name}) { #XXX: $is_index
245 0         0 $file .= ".$self->{cf_ext_public}";
246             }
247              
248 30 100       302 if ($file !~ $self->{re_handled_ext}) {
249 1 50 33     6 if ($self->{cf_debug_psgi} and $self->has_htdebug("static")) {
250             return $self->psgi_dump("Not handled since extension doesn't match"
251 0         0 , $file, $self->{re_handled_ext});
252             }
253 1         5 return $self->psgi_handle_static($env);
254             }
255              
256 29 50       64 my $dh = $self->get_lochandler(map {untaint_any($_)} $loc, $tmpldir) or do {
  58         175  
257 0         0 return $self->psgi_error(404, "No such directory: $loc");
258             };
259              
260             # To support $con->param and other cgi compat methods.
261 29         238 my $req = Plack::Request->new($env);
262              
263             my @params = (env => $env
264             , path_info => $env->{PATH_INFO}
265 29 100       396 , $self->connection_quad([$virtdir, $loc, $file, $trailer])
266             , $is_index ? (is_index => 1) : ()
267             , is_psgi => 1, cgi => $req);
268              
269 29         142 my $con = $self->make_connection(undef, @params, yatt => $dh, noheader => 1);
270              
271             my $error = catch {
272 29     29   120 $self->run_dirhandler($dh, $con, $file);
273 29         197 };
274              
275 29         157 try_invoke($con, 'flush_headers');
276              
277 29 100 66     111 if (not $error or is_done($error)) {
    50          
278 27         198 my $res = Plack::Response->new(200);
279             $res->content_type("text/html"
280             . ($self->{cf_header_charset}
281 27 50       608 ? qq{; charset="$self->{cf_header_charset}"}
282             : ""));
283 27 50       827 if (my @h = $con->list_header) {
284 0         0 $res->headers->header(@h);
285             }
286 27         118 $res->body($con->buffer);
287 27         219 return $res->finalize;
288             } elsif (ref $error eq 'ARRAY') {
289             # redirect
290 2 50       8 if ($self->{cf_debug_psgi}) {
291 0 0       0 if (my $errfh = fileno(STDERR) ? \*STDERR : $env->{'psgi.errors'}) {
    0          
292 0         0 print $errfh "PSGI Tuple: ", terse_dump($error), "\n";
293             }
294             }
295 2         9 return $error;
296             } else {
297             # system_error. Should be treated by PSGI Server.
298 0         0 die $error;
299             }
300             }
301              
302             sub make_debug_params {
303 0     0 0 0 (my MY $self, my ($reqrec, $args)) = @_;
304              
305 0 0       0 my ($path_info, @rest) = ref $reqrec ? @$reqrec : $reqrec;
306              
307 0         0 my Env $env = Env->psgi_simple_env;
308 0         0 $env->{PATH_INFO} = $path_info;
309 0         0 $env->{REQUEST_URI} = $path_info;
310              
311 0         0 my @params = ($self->SUPER::make_debug_params($reqrec, $args)
312             , env => $env);
313              
314             #
315             # Only for debugging aid. See YATT/samples/db_backed/1/t/t_signup.pm
316             #
317 0 0 0     0 if (@rest == 2 and defined $rest[-1] and ref $args eq 'HASH') {
      0        
318 0         0 require Hash::MultiValue;
319 0         0 push @params, hmv => Hash::MultiValue->from_mixed($args);
320             }
321              
322 0         0 @params;
323             }
324              
325             #========================================
326              
327             sub psgi_handle_static {
328 1     1 0 2 (my MY $self, my Env $env) = @_;
329             my $app = $self->{cf_psgi_static}
330 1   33     11 || $self->psgi_file_app($self->{cf_doc_root});
331              
332             # When PATH_INFO contains virtual path prefix (like /~$user/),
333             # we need to strip them (for Plack::App::File).
334 1         27 local $env->{PATH_INFO} = $self->trim_site_prefix($env->{PATH_INFO});
335              
336 1         5 $app->($env);
337             }
338              
339             sub psgi_handle_fallback {
340 1     1 0 2 (my MY $self, my Env $env) = @_;
341             (my $app = $self->{cf_psgi_fallback}
342             ||= $self->psgi_file_app($self->{cf_doc_root}))
343 1 50 33     8 or return [404, [], ["Cannot understand: ", $env->{PATH_INFO}]];
344              
345 1         5 local $env->{PATH_INFO} = $self->trim_site_prefix($env->{PATH_INFO});
346              
347 1         6 $app->($env);
348             }
349              
350             sub trim_site_prefix {
351 2     2 0 5 (my MY $self, my $path) = @_;
352 2 50       6 if (my $pfx = $self->{cf_site_prefix}) {
353 0         0 substr($path, length($pfx));
354             } else {
355 2         7 $path;
356             }
357             }
358              
359             # XXX: Do we need to care about following headers too?:
360             # * X-Content-Security-Policy
361             # * X-Request-With
362             # * X-Frame-Options
363             # * Strict-Transport-Security
364              
365             sub is_done {
366 0         0 defined $_[0] and ref $_[0] eq 'SCALAR' and not ref ${$_[0]}
367 4 50 33 4 0 55 and ${$_[0]} eq 'DONE';
  0   33     0  
368             }
369              
370             #========================================
371              
372             sub split_path_info {
373 47     47 0 83 (my MY $self, my Env $env) = @_;
374              
375 47 100 66     354 if (! $self->{cf_per_role_docroot}
    50 66        
376             && nonempty($env->{PATH_TRANSLATED})
377             && $self->is_path_translated_mode($env)) {
378             #
379             # [1] PATH_TRANSLATED mode.
380             #
381             # If REDIRECT_STATUS == 200 and PATH_TRANSLATED is not empty,
382             # use it as a template path. It must be located under app_root.
383             #
384             # In this case, PATH_TRANSLATED should be valid physical path
385             # + optionally trailing sub path_info.
386             #
387             # XXX: What should be done when app_root is empty?
388             # XXX: Is userdir ok? like /~$USER/dir?
389             # XXX: should have cut_depth option.
390             #
391             split_path($env->{PATH_TRANSLATED}, $self->{cf_app_root}
392             , $self->{cf_use_subpath}
393             , $self->{cf_ext_public}
394 8         71 );
395             # or die.
396              
397             } elsif (nonempty($env->{PATH_INFO})) {
398             #
399             # [2] Template lookup mode.
400             #
401              
402 39         67 my $tmpldirs = do {
403 39 50       116 if ($self->{cf_per_role_docroot}) {
404 0         0 my $user = $env->{$self->{cf_per_role_docroot_key}};
405 0   0     0 $user ||= $self->{cf_default_role};
406 0         0 ["$self->{cf_per_role_docroot}/$user"]
407             } else {
408             $self->{tmpldirs}
409 39         95 }
410             };
411              
412             lookup_path($env->{PATH_INFO}
413             , $tmpldirs
414             , $self->{cf_index_name}, ".$self->{cf_ext_public}"
415 39         237 , $self->{cf_use_subpath});
416             } else {
417             # or die
418 0         0 return;
419             }
420             }
421              
422             sub has_forbidden_path {
423 71     71 0 145 (my MY $self, my $path) = @_;
424 71         125 given ($path) {
425 71         123 when (undef) {
426 34         165 return undef;
427             }
428 37         81 when (m{\.lib(?:/|$)}) {
429 1         7 return ".lib: $path";
430             }
431 36         332 when (m{(?:^|/)\.ht|\.ytmpl$}) {
432             # XXX: basename() is just to ease testing.
433 2         76 return "filetype: " . basename($path);
434             }
435             }
436             }
437              
438             sub is_path_translated_mode {
439 8     8 0 18 (my MY $self, my Env $env) = @_;
440 8   50     65 ($env->{REDIRECT_STATUS} // 0) == 200
441             }
442              
443             # XXX: kludge! redundant!
444             sub split_path_url {
445 2     2 0 13 (my MY $self, my ($path_translated, $path_info, $document_root)) = @_;
446              
447 2         6 my @info = do {
448 2 100       13 if ($path_info =~ s{^(/~[^/]+)(?=/)}{}) {
449 1         4 my $user = $1;
450 1         7 my ($root, $loc, $file, $trailer)
451             = split_path($path_translated
452             , substr($path_translated, 0
453             , length($path_translated) - length($path_info))
454             , 0
455             );
456 1         10 (dir => "$root$loc", file => $file, subpath => $trailer
457             , root => $root, location => "$user$loc");
458             } else {
459 1         6 my ($root, $loc, $file, $trailer)
460             = split_path($path_translated, $document_root, 0);
461 1         8 (dir => "$root$loc", file => $file, subpath => $trailer
462             , root => $root, location => $loc);
463             }
464             };
465              
466 2 50       7 if (wantarray) {
467             @info
468 0         0 } else {
469 2         12 my %info = @info;
470 2         28 \%info;
471             }
472             }
473              
474             # どこを起点に split_path するか。UserDir の場合は '' を返す。
475             sub document_dir {
476 0     0 0 0 (my MY $self, my $cgi) = @_;
477 0         0 my $path_info = $cgi->path_info;
478 0 0       0 if (my ($user) = $path_info =~ m{^/~([^/]+)/}) {
479 0         0 '';
480             } else {
481 0   0     0 $self->{cf_doc_root} // '';
482             }
483             }
484              
485             #========================================
486             sub is_debug_allowed {
487 0     0 0 0 (my MY $self, my Env $env) = @_;
488 0 0       0 return unless $self->{allow_debug_from};
489 0 0       0 return unless defined(my $ip = $self->guess_client_ip($env));
490 0         0 $ip =~ $self->{allow_debug_from};
491             }
492              
493             sub guess_client_ip {
494 0     0 0 0 (my MY $self, my Env $env) = @_;
495 0   0     0 $env->{HTTP_X_REAL_IP} // $env->{HTTP_X_CLIENT_IP} // do {
      0        
496 0 0       0 if (defined(my $forward = $env->{HTTP_X_FORWARDED_FOR})) {
497 0         0 [split /(?:\s*,\s*|\s+)/, $forward]->[0];
498             } else {
499             $env->{REMOTE_ADDR}
500 0         0 }
501             }
502             }
503              
504             sub configure_allow_debug_from {
505 0     0 0 0 (my MY $self, my $data) = @_;
506 0         0 my $pat = join "|", map { quotemeta($_) } lexpand($data);
  0         0  
507 0         0 $self->{allow_debug_from} = qr{^(?:$pat)};
508             }
509              
510             sub has_htdebug {
511 68     68 0 117 (my MY $self, my $name) = @_;
512             defined $self->{cf_app_root}
513 68 50       1688 and -e "$self->{cf_app_root}/.htdebug_$name"
514             }
515              
516             sub psgi_dump {
517 0     0 0 0 my MY $self = shift;
518             [200
519             , [$self->secure_text_plain]
520 0         0 , [map {escape(terse_dump($_))} @_]];
  0         0  
521             }
522              
523             #========================================
524              
525             #========================================
526              
527 7     7   53 use YATT::Lite::WebMVC0::Connection;
  7         12  
  7         2585  
528             sub Connection () {'YATT::Lite::WebMVC0::Connection'}
529             sub ConnProp () {Connection}
530              
531             sub make_connection {
532 59     59 1 4200 (my MY $self, my ($fh, @args)) = @_;
533 59         607 my @opts = do {
534 59 50       160 if ($self->{cf_noheader}) {
535             # direct mode.
536 0         0 ($fh, noheader => 1);
537             } else {
538             # buffered mode.
539 59         578 (undef, parent_fh => $fh);
540             }
541             };
542              
543 59         163 push @opts, site_prefix => $self->{cf_site_prefix};
544              
545 59 50       177 if (my $fn = $self->{cf_logfile}) {
546 0         0 my $dir = $self->app_path_ensure_existing(dirname($fn));
547 0         0 my $real = "$dir/" . basename($fn);
548 0 0       0 open my $fh, '>>', $real or die "Can't open logfile: fn=$real: $!";
549 0         0 push @opts, logfh => $fh;
550             }
551              
552             push @opts, debug => $self->{cf_debug_connection}
553 59 50       144 if $self->{cf_debug_connection};
554              
555 59 50       177 if (my $back = $self->{cf_backend}) {
556 0   0     0 push @opts, (backend => try_invoke($back, 'clone') // $back);
557             }
558              
559 59 50       273 if (my $enc = $$self{cf_output_encoding}) {
560 59         123 push @opts, encoding => $enc;
561             }
562             $self->SUPER::make_connection
563             (@opts
564 59         328 , $self->cf_delegate_defined(qw(is_psgi no_nested_query))
565             , @args);
566             }
567              
568             sub finalize_connection {
569 59     59 0 100 my MY $self = shift;
570 59         173 my ConnProp $prop = (my $glob = shift)->prop;
571 59 50       232 $self->session_flush($glob) if $prop->{session};
572             }
573              
574 7     7   6260 use YATT::Lite::WebMVC0::Partial::Session;
  7         18  
  7         35  
575              
576             #========================================
577             # misc.
578             #========================================
579              
580             sub header_charset {
581 0     0 1 0 (my MY $self) = @_;
582 0 0       0 $self->{cf_header_charset} || $self->{cf_output_encoding};
583             }
584              
585             #========================================
586              
587             Entity site_config => sub {
588 0     0   0 my ($this, $name, $default) = @_;
589 0         0 my MY $self = $SYS;
590 0 0       0 return $self->{cf_site_config} unless defined $name;
591 0   0     0 $self->{cf_site_config}{$name} // $default;
592             };
593              
594             Entity is_debug_allowed_ip => sub {
595 2     2   4 my ($this, $remote_addr) = @_;
596 2         5 my MY $self = $SYS;
597              
598 2   33     9 $remote_addr //= do {
599 2         15 my Env $env = $CON->env;
600             $env->{HTTP_X_REAL_IP}
601             // $env->{HTTP_X_CLIENT_IP}
602             // $env->{HTTP_X_FORWARDED_FOR}
603 2   33     25 // $env->{REMOTE_ADDR};
      33        
      33        
604             };
605              
606 2 50 33     13 unless (defined $remote_addr and $remote_addr ne '') {
607 0         0 return 0;
608             }
609              
610 2         14 grep {$remote_addr ~~ $_} lexpand($self->{cf_debug_allowed_ip}
611 2   50     19 // ['127.0.0.1']);
612             };
613              
614             foreach my $item (map([lc($_) => uc($_)]
615             , qw/SCRIPT_NAME
616             PATH_INFO
617             REQUEST_URI
618              
619             SCRIPT_URI
620             SCRIPT_URL
621             SCRIPT_FILENAME
622             /)) {
623             my ($method, $env_name) = @$item;
624             Entity $method => sub {
625 0     0     my Env $env = $CON->env;
626 0           $env->{$env_name};
627             };
628             }
629              
630             #========================================
631              
632             YATT::Lite::Breakpoint::break_load_dispatcher();
633              
634             1;