File Coverage

blib/lib/App/DuckPAN/Restart.pm
Criterion Covered Total %
statement 18 71 25.3
branch 0 24 0.0
condition 0 6 0.0
subroutine 6 12 50.0
pod 0 1 0.0
total 24 114 21.0


line stmt bran cond sub pod time code
1             package App::DuckPAN::Restart;
2             our $AUTHORITY = 'cpan:DDG';
3             # ABSTRACT: Automatic restarting of application on file change
4             $App::DuckPAN::Restart::VERSION = '1017';
5 1     1   858 use File::Find::Rule;
  1         5430  
  1         7  
6 1     1   524 use Filesys::Notify::Simple;
  1         1574  
  1         24  
7              
8 1     1   5 use App::DuckPAN::TemplateDefinitions;
  1         1  
  1         18  
9 1     1   3 use Try::Tiny;
  1         1  
  1         46  
10              
11 1     1   4 use strict;
  1         1  
  1         14  
12              
13 1     1   3 use Moo::Role;
  1         1  
  1         10  
14              
15             requires '_run_app';
16              
17             sub run_restarter {
18 0     0 0   my ($self, $args) = @_;
19              
20             # exit immediately if not in an IA directory
21 0           $self->app->get_ia_type;
22              
23             # will keep (re)starting the server until the app exits
24 0           while(1){
25 0 0         defined(my $app = fork) or die 'Failed to fork application';
26 0 0         unless($app){ # app kid
27 0           $self->_run_app($args);
28 0           exit 0;
29             }
30              
31             # Slightly different format here since we need to take care of
32             # the newly spawned app on failure.
33 0           my $fmon = fork;
34 0 0         unless($fmon){ # file monitor kid
35 0 0         unless(defined $fmon){
36 0           kill SIGTERM => $app;
37 0           die 'Failed to fork file monitor';
38             }
39 0           $self->_monitor_directories;
40 0           exit 0;
41             }
42              
43             # wait for one them to exit. -1 waits for all children
44 0           my $pid = waitpid -1, 0;
45              
46             # reload the application
47 0 0         if($pid == $fmon){
    0          
48             # if we can't kill the app, let's not start another
49 0 0         unless(kill SIGTERM => $app){
50 0           die "Failed to kill the application (pid: $app). Check manually";
51             }
52             # wait for it, otherwise the next whie loop will get it
53 0           waitpid($app, 0);
54             }
55             elsif($pid == $app){ # or exit
56 0           kill SIGTERM => $fmon;
57 0           exit;
58             }
59 0           else{ die "Unknown kid $pid reaped!\n"; } # shouldn't happen
60             }
61             }
62              
63             sub _get_directories_to_monitor {
64 0     0     my $self = shift;
65 0           my @output_dirs;
66              
67             try {
68 0     0     my $template_defs = App::DuckPAN::TemplateDefinitions->new;
69 0           my @templates = $template_defs->get_templates;
70              
71 0           for my $template (@templates) {
72 0 0         push @output_dirs, $template->output_directory
73             unless $template->name =~ /test/;
74             }
75             }
76             catch {
77 0 0   0     if (/template definitions/i) {
78             # There was a problem loading the template definitions file. This
79             # can happen if the instant answer repository is of an older
80             # version and does not have the templates.yml file.
81             #
82             # In this case we use the older method of determining directories
83             # to monitor. This support can be removed sometime in the future.
84             # Note: Cheatsheet directories will not be monitored with this
85             # method.
86              
87 0           $self->app->emit_debug("Instant Answers repository does not have a " .
88             "template definitions file. Some directories may not be monitored " .
89             "for changes.");
90              
91 0           while(my ($type, $io) = each %{$self->app->get_ia_type()->{templates}}){
  0            
92 0 0         next if $type eq 'test'; # skip the test dir?
93 0           push @output_dirs, $io->{out};
94             }
95             }
96             else {
97 0           die $_;
98             }
99 0           };
100              
101 0           my %distinct_dirs;
102              
103             # add all subdirectories
104 0           for my $dir (@output_dirs) {
105 0           ++$distinct_dirs{$_} for File::Find::Rule->directory()->in($dir);
106             }
107              
108 0           ++$distinct_dirs{$self->app->get_ia_type()->{dir}};
109              
110 0           return [ keys %distinct_dirs ];
111             }
112              
113             # Monitors development directories for file changes. Tries to get the
114             # list of directories in a general way. This subroutine
115             # blocks, so when it returns we know there's been a change
116             sub _monitor_directories {
117 0     0     my $self = shift;
118              
119             # Find all of the directories that neeed to monitored
120             # Note: Could potentially be functionality added to App::DuckPAN
121             # which would return the directories involved in an IA
122             # (see https://github.com/duckduckgo/p5-app-duckpan/issues/200)
123 0           my $dirs = $self->_get_directories_to_monitor;
124              
125 0           FSMON: while(1){
126             # Find all subdirectories
127             # Create our watcher with each directory
128 0           my $watcher = Filesys::Notify::Simple->new($dirs);
129             # Wait for something to happen. This blocks, which is why
130             # it's in a wheel. On detection of update it will fall
131             # through; thus the while(1)
132 0           my $reload;
133             $watcher->wait(sub {
134 0     0     for my $event (@_) {
135 0           my $file = $event->{path};
136             # if it's a newly created directory, dot file, or pulled
137             # in dynamically there shouldn't be a need to reload.
138             # This will catch directories with files in them properly,
139             # as each file will be its own event
140 0 0 0       if( (-d $file) || ($file =~ m{^(?:.+/)?\.[^/]+$}o) || ($file =~ /\.(?:handlebars|css|js)$/oi) ){
      0        
141 0           next;
142             }
143             # All other changes trigger a reload
144 0           ++$reload;
145             }
146 0           });
147             # time to reload or keep waiting
148 0 0         last FSMON if $reload;
149             }
150             }
151              
152             1;
153              
154             __END__
155              
156             =pod
157              
158             =head1 NAME
159              
160             App::DuckPAN::Restart - Automatic restarting of application on file change
161              
162             =head1 VERSION
163              
164             version 1017
165              
166             =head1 AUTHOR
167              
168             DuckDuckGo <open@duckduckgo.com>, Zach Thompson <zach@duckduckgo.com>, Zaahir Moolla <moollaza@duckduckgo.com>, Torsten Raudssus <torsten@raudss.us> L<https://raudss.us/>
169              
170             =head1 COPYRIGHT AND LICENSE
171              
172             This software is Copyright (c) 2013 by DuckDuckGo, Inc. L<https://duckduckgo.com/>.
173              
174             This is free software, licensed under:
175              
176             The Apache License, Version 2.0, January 2004
177              
178             =cut