File Coverage

blib/lib/Sphinx/Manager.pm
Criterion Covered Total %
statement 108 197 54.8
branch 29 108 26.8
condition 6 24 25.0
subroutine 20 26 76.9
pod 7 7 100.0
total 170 362 46.9


line stmt bran cond sub pod time code
1             package Sphinx::Manager;
2              
3 4     4   191207 use warnings;
  4         4  
  4         100  
4 4     4   15 use strict;
  4         4  
  4         64  
5 4     4   11 use base qw/Class::Accessor::Fast/;
  4         6  
  4         1714  
6              
7 4     4   7910 use Carp qw/croak/;
  4         4  
  4         145  
8 4     4   1552 use Proc::ProcessTable;
  4         14212  
  4         137  
9 4     4   385 use Path::Class;
  4         29146  
  4         142  
10 4     4   17 use File::Spec;
  4         4  
  4         56  
11 4     4   481 use Sphinx::Config;
  4         13876  
  4         74  
12 4     4   14 use Errno qw/ECHILD/;
  4         4  
  4         293  
13 4     4   1760 use POSIX;
  4         12080  
  4         18  
14              
15             our $VERSION = '0.08';
16              
17             __PACKAGE__->mk_accessors(qw/config_file
18             pid_file
19             bindir
20             searchd_args
21             searchd_sudo
22             indexer_args
23             indexer_sudo
24             process_timeout
25             pre_exec_callback
26             max_fd
27             debug/);
28              
29             my $default_config_file = 'sphinx.conf';
30             my $default_process_timeout = 10;
31             my $default_max_fd = 100;
32              
33             sub new {
34 3     3 1 2745 my $class = shift;
35 3         27 my $self = $class->SUPER::new(@_);
36              
37 3 50       33 $self->debug(0) unless $self->debug;
38 3 50       42 $self->config_file($default_config_file) unless $self->config_file;
39 3 50       24 $self->process_timeout($default_process_timeout) unless $self->process_timeout;
40 3         27 $self->max_fd($default_max_fd);
41              
42 3         12 return $self;
43             }
44              
45             # Determines pid_file from explicitly given file or by reading the config
46             sub _find_pidfile {
47 6     6   9 my $self = shift;
48              
49 6   33     9 my $config_file = $self->config_file || $default_config_file;
50              
51 6 50       57 if (my $file = $self->pid_file) {
52 0         0 return $self->{_pid_file} = Path::Class::file($file);
53             }
54 6 100 66     33 if ($self->{_config_file} && $config_file eq $self->{_config_file}) {
55             # Config file unchanged
56 3 50       156 return $self->{_pid_file} if $self->{_pid_file};
57             }
58 3         6 $self->_load_config_file;
59 3         222 return $self->{_pid_file};
60             }
61              
62             # Loads given config file and extracts the pid_file
63             sub _load_config_file {
64 3     3   3 my $self = shift;
65              
66 3   33     6 my $config_file = $self->config_file || $default_config_file;
67              
68 3         27 my $config = Sphinx::Config->new;
69 3         18 $config->parse($config_file);
70 3 50       576 if (my $pid_file = $config->get('searchd', undef, 'pid_file')) {
71 3         69 $self->{_pid_file} = Path::Class::file($pid_file);
72             }
73 3         180 $self->{_config_file} = Path::Class::file($config_file); # records which file we have loaded
74             }
75              
76             # Find executable file
77             sub _find_exe {
78 3     3   6 my $self = shift;
79 3         3 my $name = shift;
80              
81 3 50       9 return Path::Class::file($self->bindir, $name) if $self->bindir;
82              
83 0         0 my @candidates = map { Path::Class::file($_, $name) } File::Spec->path();
  0         0  
84 0         0 for my $bin (@candidates) {
85 0 0       0 return $bin if -x "$bin";
86             }
87 0         0 die "Failed to find $name binary in bindir or system path; please specify bindir correctly";
88             }
89              
90             # Find a process for given pid; return the PID if the process matches the given pattern
91             # If pid is not given, returns all process IDs matching the pattern
92             sub _findproc {
93 13     13   32 my ($self, $pid, $pat) = @_;
94              
95 13         157 my $t = Proc::ProcessTable->new;
96              
97 13 50       10652 if ($pid) {
98 0         0 my $process;
99 0         0 for (@{$t->table}) {
  0         0  
100 0 0       0 $process = $_, last if $_->pid == $pid;
101             }
102 0 0       0 return [ $pid ] if $process;
103             }
104             else {
105 13         21 my @procs;
106 13         25 for (@{$t->table}) {
  13         15055  
107 132   33     222 my $cmndline = $_->{cmndline} || $_->{fname};
108 132 50       204 warn "Checking $cmndline against $pat" if $self->debug > 2;
109 132 50       635 push(@procs, $_->pid) if $cmndline =~ /$pat/;
110             }
111 13         284 return \@procs;
112             }
113              
114 0         0 return [];
115             }
116              
117             # Waits for a PID to disappear from the process table; returns 1 if found, 0 if timeout.
118             sub _wait_for_death {
119 0     0   0 my $self = shift;
120 0         0 my $pid = shift;
121 0   0     0 my $timeout = $self->process_timeout || $default_process_timeout;
122              
123 0         0 my $ret = 0;
124 0         0 my $t = time() + $timeout;
125 0         0 while (time() < $t) {
126 0 0       0 $ret++, last unless @{$self->_findproc($pid)};
  0         0  
127 0         0 sleep(1);
128             }
129 0         0 return $ret;
130             }
131              
132             # Waits for a process matching a given pattern to appear in the process table; returns 1 if found, 0 if timeout.
133             sub _wait_for_proc {
134 1     1   4 my $self = shift;
135 1         3 my $pat = shift;
136 1   33     10 my $timeout = $self->process_timeout || $default_process_timeout;
137              
138 1         6 my $ret = 0;
139 1         6 my $t = time() + $timeout;
140 1         10 while (time() < $t) {
141 10 50       41 $ret++, last if @{$self->_findproc(undef, $pat)};
  10         68  
142 10         10001144 sleep(1);
143             }
144 1         229 return $ret;
145             }
146              
147             sub _system_with_status
148             {
149 0     0   0 my (@args) = @_;
150              
151 0 0       0 if ( ! @args ){
152 0         0 return "No parameters provided";
153             }
154              
155 0         0 local $SIG{'CHLD'} = 'DEFAULT';
156              
157 0         0 my $output = qx{@args};
158 0         0 my $status = $?;
159              
160 0 0       0 unless ($status == 0) {
161 0 0       0 if ($? == -1) {
162 0         0 return "@args failed to execute: $!";
163             }
164 0 0       0 if ($? & 127) {
165 0 0       0 return sprintf("@args died with signal %d, %s coredump\n",
166             ($? & 127), ($? & 128) ? 'with' : 'without');
167             }
168 0         0 return sprintf("@args exited with value %d. Output from execution:\n%s\n", $? >> 8, $output);
169             }
170 0         0 return '';
171             }
172              
173             # Get regexp for matching command line
174             sub _get_searchd_matchre {
175 4     4   9 my $self = shift;
176 4         31 my $c = $self->{_config_file}->stringify;
177 4         256 return qr/searchd.*(?:\s|=)$c(?:$|\s)/;
178             }
179              
180             sub get_searchd_pid {
181 3     3 1 6 my $self = shift;
182              
183 3         3 my $pids = [];
184 3         6 my $pidfile = $self->_find_pidfile;
185 3 50       6 if ( -f "$pidfile" ) {
186 0 0       0 if (my $pid = $pidfile->slurp(chomp => 1)) {
187 0 0       0 push(@$pids, $pid) if @{$self->_findproc($pid, 'searchd')};
  0         0  
188             }
189             }
190 3 50       120 if (! @$pids) {
191             # backup plan if PID file is empty or invalid
192 3         9 $pids = $self->_findproc(undef, $self->_get_searchd_matchre);
193             }
194 3 50       9 warn("Found searchd pid " . join(", ", @$pids)) if $self->debug;
195 3         15 return $pids;
196             }
197              
198             sub start_searchd {
199 3     3 1 12 my $self = shift;
200 3         3 my $ok_if_running = shift;
201              
202 3         6 my $pidfile = $self->_find_pidfile;
203 3 50       24 warn "start_searchd: Checking pidfile $pidfile" if $self->debug;
204              
205 3 50       15 if ( -f "$pidfile" ) {
206 0         0 my $pid = Path::Class::file($pidfile)->slurp(chomp => 1);
207 0 0       0 warn "start_searchd: Found PID $pid" if $self->debug;
208 0 0 0     0 if ($pid && @{$self->_findproc($pid, qr/searchd/)}) {
  0         0  
209 0 0       0 return if $ok_if_running;
210 0         0 die "searchd is already running, PID $pid";
211             }
212             }
213              
214 3         93 my @args;
215 3 50       9 if (my $sudo = $self->searchd_sudo) {
216 0 0       0 if (ref($sudo) ne 'ARRAY') {
217 0         0 $sudo = [ split(/\s+/, $sudo) ];
218             }
219 0         0 push(@args, @$sudo);
220             }
221 3         18 push(@args, $self->_find_exe('searchd'));
222 3         186 push(@args, "--config", $self->{_config_file}->stringify);
223 3 50       63 push(@args, @{$self->searchd_args}) if $self->searchd_args;
  0         0  
224 3 50       18 warn("Executing " . join(" ", @args)) if $self->debug;
225              
226 3         42 local $SIG{CHLD} = 'IGNORE';
227 3         1883 my $pid = fork();
228 3 50       96 die "Fork failed: $!" unless defined $pid;
229 3 100       34 if ($pid == 0) {
230 2 100       1547 fork() and exit; # double fork to ensure detach
231              
232 1 50       65 if ($self->pre_exec_callback) {
233             # give caller the opportunity to tidy up resources after the fork
234 0         0 $self->pre_exec_callback->($self, \@args);
235             }
236             else {
237             # close any open files. We don't know which are open so close lots.
238 1         42 POSIX::close($_) for 0 .. $self->max_fd;
239             }
240              
241 1 0       0 exec(@args)
242             or die("Failed to exec @args}: $!");
243             }
244              
245 1 50       33 die "Searchd not running after timeout"
246             unless $self->_wait_for_proc($self->_get_searchd_matchre);
247              
248             }
249              
250             sub _kill {
251 0     0   0 my ($self, $sig, @pids) = @_;
252              
253 0 0       0 if (my $sudo = $self->searchd_sudo) {
254 0         0 my @args;
255 0 0       0 if (ref($sudo) ne 'ARRAY') {
256 0         0 $sudo = [ split(/\s+/, $sudo) ];
257             }
258 0         0 push(@args, @$sudo);
259 0         0 push(@args, 'kill');
260 0         0 push(@args, "-$sig", @pids);
261 0         0 _system_with_status(@args);
262             }
263             else {
264 0         0 kill $sig, @pids;
265             }
266             }
267              
268             sub stop_searchd {
269 3     3 1 1449 my $self = shift;
270              
271 3         12 my $pids = $self->get_searchd_pid;
272 3 50       9 if (@$pids) {
273 0           $self->_kill(15, @$pids);
274 0 0         unless ($self->_wait_for_death(@$pids)) {
275 0           $self->_kill(9, @$pids);
276 0 0         unless ($self->_wait_for_death(@$pids)) {
277 0           die "Failed to stop searchd PID " . join(", ", @$pids) . ", even with sure kill";
278             }
279             }
280             }
281             # nothing found to kill so assume success
282             }
283              
284             sub restart_searchd {
285 0     0 1   my $self = shift;
286 0           $self->stop_searchd;
287 0           $self->start_searchd(1);
288             }
289              
290             sub reload_searchd {
291 0     0 1   my $self = shift;
292            
293 0           my $pids = $self->get_searchd_pid;
294 0 0         if (@$pids) {
295             # send HUP
296 0           $self->_kill(1, @$pids);
297             }
298             else {
299 0           $self->start_searchd(1);
300             }
301             }
302              
303             sub run_indexer {
304 0     0 1   my $self = shift;
305 0           my @extra_args = @_;
306              
307 0           my @args;
308 0 0         if (my $sudo = $self->indexer_sudo) {
309 0 0         if (ref($sudo) ne 'ARRAY') {
310 0           $sudo = [ split(/\s+/, $sudo) ];
311             }
312 0           push(@args, @$sudo);
313             }
314 0           push(@args, my $indexer = $self->_find_exe('indexer'));
315              
316 0 0         warn("Using indexer @args") if $self->debug;
317 0 0         die "Cannot execute Sphinx indexer binary $indexer" unless -x "$indexer";
318              
319 0   0       my $config = $self->config_file || $default_config_file;
320 0           push(@args, "--config", $config);
321 0 0         push(@args, @{$self->indexer_args}) if $self->indexer_args;
  0            
322 0 0         push(@args, @extra_args) if @extra_args;
323              
324 0 0         if (my $status = _system_with_status(@args)) {
325 0           die $status;
326             }
327             }
328              
329             1;
330              
331             __END__