File Coverage

blib/lib/Test/Httpd/Apache2.pm
Criterion Covered Total %
statement 39 173 22.5
branch 1 68 1.4
condition 0 15 0.0
subroutine 13 26 50.0
pod 3 10 30.0
total 56 292 19.1


line stmt bran cond sub pod time code
1             package Test::Httpd::Apache2;
2              
3 1     1   1034 use strict;
  1         3  
  1         48  
4 1     1   6 use warnings;
  1         1  
  1         36  
5              
6 1     1   43 use 5.008;
  1         5  
  1         36  
7 1     1   1005 use Class::Accessor::Lite;
  1         1341  
  1         9  
8 1     1   53 use Cwd qw(getcwd);
  1         2  
  1         71  
9 1     1   6 use File::Spec;
  1         2  
  1         32  
10 1     1   1479 use File::Temp qw(tempdir);
  1         48366  
  1         101  
11 1     1   1105 use IO::Socket::INET;
  1         22047  
  1         10  
12 1     1   2278 use IPC::Open2 qw(open2);
  1         4828  
  1         80  
13 1     1   1369 use POSIX qw(WNOHANG);
  1         7847  
  1         8  
14 1     1   2373 use Test::TCP qw(empty_port);
  1         25970  
  1         66  
15 1     1   11 use Time::HiRes qw(sleep);
  1         3  
  1         8  
16              
17 1 50   1   170 use constant PATH_SEP => $^O eq 'MSWin32' ? ';' : ':';
  1         2  
  1         2736  
18              
19             our $VERSION = '0.09';
20              
21             our %Defaults = (
22             auto_start => 1,
23             pid => undef,
24             listen => undef,
25             required_modules => [],
26             server_root => undef,
27             tmpdir => undef,
28             custom_conf => '',
29             search_paths => [
30             qw(/usr/sbin /usr/local/sbin /usr/local/apache/bin)
31             ],
32             httpd => 'httpd',
33             apxs => 'apxs',
34             );
35              
36             if ($^O eq 'MSWin32') {
37             require Win32::Process;
38             Win32::Process->import;
39             my @cand_paths = map { $_ =~ s!/httpd\.exe$!!; $_ }
40             glob('C:/progra~1/apach*/apach*/bin/httpd.exe');
41             if (@cand_paths) {
42             # use the latest version, if any
43             my $path = $cand_paths[-1];
44             unshift @{$Defaults{search_paths}}, $path;
45             my $dso_path = $path;
46             $dso_path =~ s!/bin$!/modules!;
47             if (-d $dso_path) {
48             # convert to shortname since SPs in path will let the glob fail
49             $Defaults{_dso_path} = Win32::GetShortPathName($dso_path);
50             }
51             }
52             } else {
53             # search for alternative names if necessary
54             my @paths = (
55             split(PATH_SEP, $ENV{PATH}),
56             @{$Defaults{search_paths}},
57             );
58             if (grep { -x "$_/$Defaults{httpd}" } @paths) {
59             # found
60             } elsif (grep { -x "$_/apache2" } @paths) {
61             # debian / ubuntu have these alternative names
62             $Defaults{httpd} = "apache2";
63             $Defaults{apxs} = "apxs2";
64             }
65             }
66              
67             Class::Accessor::Lite->mk_accessors(keys %Defaults);
68              
69             sub new {
70 0     0 1   my $klass = shift;
71 0           my $self = bless {
72             %Defaults,
73 0 0         @_ == 1 ? %{$_[0]} : @_,
74             }, $klass;
75 0 0         if (! $self->server_root) {
76 0           $self->server_root(getcwd);
77             }
78 0 0         $self->listen("127.0.0.1:@{[empty_port()]}")
  0            
79             unless $self->listen();
80 0           $self->tmpdir(tempdir(CLEANUP => 1));
81 0 0         $self->start()
82             if $self->auto_start();
83 0           return $self;
84             }
85              
86             sub DESTROY {
87 0     0     my $self = shift;
88 0 0         $self->stop()
89             if $self->pid();
90             }
91              
92             sub start {
93 0     0 1   my $self = shift;
94 0 0         die "httpd is already running (pid:@{[$self->pid]})"
  0            
95             if $self->pid;
96             # write configuration
97 0           $self->write_conf();
98             # spawn httpd
99 0           my $pid = fork;
100 0 0         if (! defined $pid) {
    0          
101 0           die "fork failed:$!";
102             } elsif ($pid == 0) {
103             # child process
104 0           $ENV{PATH} = join(PATH_SEP, $ENV{PATH}, @{$self->search_paths});
  0            
105 0           exec $self->httpd, '-X', '-D', 'FOREGROUND', '-f', $self->conf_file;
106 0           die "failed to exec httpd:$!";
107             }
108             # wait until the port becomes available
109 0           while (1) {
110             my $sock = IO::Socket::INET->new(
111 0 0         PeerAddr => do {
112 0 0         $self->listen =~ /:/
113 0           ? $self->listen : "127.0.0.1:@{[$self->listen]}",
114             },
115             Proto => 'tcp',
116             ) and last;
117 0 0         if (waitpid($pid, WNOHANG) == $pid) {
118 0 0         if (open my $fh, '<', "@{[$self->tmpdir]}/error_log") {
  0            
119 0           print STDERR do { local $/; join '', <$fh> };
  0            
  0            
120             }
121 0           die "httpd failed to start, exitted with rc=$?";
122             }
123 0           sleep 0.1;
124             }
125             # need to override pid on mswin32
126 0 0         if ($^O eq 'MSWin32') {
127 0           my $pidfile = "@{[$self->tmpdir]}/httpd.pid";
  0            
128 0 0         open my $fh, '<', $pidfile
129             or die "failed to open $pidfile:$!";
130 0           $pid = <$fh>;
131 0           chomp $pid;
132             };
133 0           $self->pid($pid);
134             }
135              
136             sub stop {
137 0     0 1   my $self = shift;
138 0 0         die "httpd is not running"
139             unless $self->pid;
140 0 0         if ($^O eq 'MSWin32') {
141 0           Win32::Process::KillProcess($self->pid, 0);
142 0           sleep 1;
143             } else {
144 0           kill 'TERM', $self->pid;
145 0           while (waitpid($self->pid, 0) != $self->pid) {
146             }
147             }
148 0           $self->pid(undef);
149             }
150              
151             sub build_conf {
152 0     0 0   my $self = shift;
153 0           my $load_modules = do {
154 0           my %static_mods = map { $_ => 1 } @{$self->get_static_modules};
  0            
  0            
155 0           my %dynamic_mods = %{$self->get_dynamic_modules};
  0            
156 0           my @mods_to_load;
157 0           my $httpd_ver = $self->get_httpd_version;
158 0           for my $mod (@{$self->required_modules}) {
  0            
159             # rewrite authz_host => access for apache/2.0.x
160 0 0 0       if ($mod eq 'authz_host' && $self->get_httpd_version =~ m{2\.0\.}) {
161 0           $mod = 'access';
162             }
163 0 0         if ($static_mods{$mod}) {
    0          
164             # no need to do anything
165             } elsif ($dynamic_mods{$mod}) {
166 0           push @mods_to_load, $mod;
167             } else {
168 0           die "required module:$mod is not available";
169             }
170             }
171 0           my $dso_path = $self->get_dso_path;
172 0           $dso_path ? join('', map {
173 0 0         "LoadModule ${_}_module $dso_path/$dynamic_mods{$_}\n"
174             } @mods_to_load) : '';
175             };
176 0           my $conf = << "EOT";
177 0           ServerRoot @{[$self->server_root]}
  0            
178 0           PidFile @{[$self->tmpdir]}/httpd.pid
179            
180 0 0 0       LockFile @{[$self->tmpdir]}/httpd.lock
181            
182 0           @{[ $^O ne 'MSWin32' && $< == 0 ? "User nobody" : ()]}
183 0           ErrorLog @{[$self->tmpdir]}/error_log
184 0           Listen @{[$self->listen]}
185             $load_modules
186              
187             @{[$self->custom_conf]}
188             EOT
189 0           return $conf;
190             }
191              
192             sub write_conf {
193 0     0 0   my $self = shift;
194 0 0         open my $fh, '>', $self->conf_file
195 0           or die "failed to open file:@{[$self->conf_file]}:$!";
196 0           print $fh $self->build_conf;
197 0           close $fh;
198             }
199              
200             sub conf_file {
201 0     0 0   my $self = shift;
202 0           return "@{[$self->tmpdir]}/httpd.conf";
  0            
203             }
204              
205             sub get_httpd_version {
206 0     0 0   my $self = shift;
207 0   0       return $self->{_httpd_version} ||= do {
208 0 0         my $lines = $self->_read_cmd($self->httpd, '-v')
209             or die 'dying due to previous error';
210 0 0         $lines =~ m{Apache\/([0-9]+\.[0-9\.]+)}
211             or die q{failed to parse out version number from the output of "httpd -v"};
212 0           $1;
213             };
214             }
215              
216             sub get_static_modules {
217 0     0 0   my $self = shift;
218 0   0       return $self->{_static_modules} ||= do {
219 0 0         my $lines = $self->_read_cmd($self->httpd, '-l')
220             or die 'dying due to previous error';
221 0           my @mods;
222 0           for my $line (split /\n/, $lines) {
223 0 0         if ($line =~ /^\s+mod_(.*)\.c/) {
224 0           push @mods, $1;
225             }
226             }
227 0           \@mods;
228             };
229             }
230              
231             sub get_dso_path {
232 0     0 0   my $self = shift;
233 0 0         if (! exists $self->{_dso_path}) {
234             $self->{_dso_path} = sub {
235             return undef
236 0 0   0     unless grep { $_ eq 'so' } @{$self->get_static_modules};
  0            
  0            
237             # first obtain the path
238 0           my $path;
239 0 0         if (my $lines = $self->_read_cmd($self->apxs, '-q', 'LIBEXECDIR')) {
240 0           $path = (split /\n/, $lines)[0];
241             } else {
242 0           die "failed to determine the apache modules directory";
243             }
244 0           return $path;
245 0           }->();
246             }
247 0           return $self->{_dso_path};
248             }
249              
250             sub get_dynamic_modules {
251 0     0 0   my $self = shift;
252 0   0       return $self->{_dynamic_modules} ||= do {
253 0           my %mods;
254 0 0         if (my $dir = $self->get_dso_path()) {
255 0           for my $n (glob "$dir/*.so") {
256 0 0         $n =~ m{/((?:mod_|lib)([^/]+?)\.so)$}
257             and $mods{$2} = $1;
258             }
259             }
260 0           \%mods;
261             };
262             }
263              
264             sub _read_cmd {
265 0     0     my ($self, @cmd) = @_;
266 0           my ($rfh, $wfh);
267 0           local $ENV{PATH} = join PATH_SEP, $ENV{PATH}, @{$self->search_paths};
  0            
268 0 0         my $pid = open2($rfh, $wfh, @cmd)
269 0           or die "failed to run @{[join ' ', @cmd]}:$!";
270 0           close $wfh;
271 0           my $lines = do { local $/; join '', <$rfh> };
  0            
  0            
272 0           close $rfh;
273 0           while (waitpid($pid, 0) != $pid) {
274             }
275 0 0         if ($? != 0) {
276 0           warn "$cmd[0] exitted with a non-zero value:$?";
277 0           return;
278             }
279 0           return $lines;
280             }
281              
282             1;
283              
284             __END__