1# Copyright (c) 2008-2013 Zmanda, Inc.  All Rights Reserved.
2#
3# This program is free software; you can redistribute it and/or
4# modify it under the terms of the GNU General Public License
5# as published by the Free Software Foundation; either version 2
6# of the License, or (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11# for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program; if not, write to the Free Software Foundation, Inc.,
15# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16#
17# Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18# Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19
20package Amanda::Process;
21
22use strict;
23use warnings;
24use Carp;
25use POSIX ();
26use Exporter;
27use vars qw( @ISA @EXPORT_OK );
28use File::Basename;
29use Amanda::Constants;
30use Amanda::Debug qw( debug );
31@ISA = qw( Exporter );
32
33=head1 NAME
34
35Amanda::Process -- interface to process
36
37=head1 SYNOPSIS
38
39  use Amanda::Process;
40
41  Amanda::Process::load_ps_table();
42
43  Amanda::Process::scan_log($logfile);
44
45  Amanda::Process::add_child();
46
47  Amanda::Process::set_master_process(@pname);
48
49  Amanda::Process::set_master($pname, $pid);
50
51  Amanda::Process::kill_process($signal);
52
53  my $count = Amanda::Process::process_running();
54
55  my $count = Amanda::Process::count_process();
56
57  my $alive = Amanda::Process::process_alive($pid, $pname);
58
59=head1 INTERFACE
60
61This module provides an object-oriented interface to track process used by
62amanda.
63
64my $Amanda_process = Amanda::Process->new($verbose);
65
66=over
67
68=item load_ps_table
69
70  $Amanda_process->load_ps_table();
71
72Load a table of all processes in the system.
73
74=item scan_log
75
76  $Amanda_process->scan_log($logfile);
77
78Parse all 'pid' and 'pid-done' lines of the logfile.
79
80=item add_child
81
82  $Amanda_process->add_child();
83
84Add all children of already known amanda processes.
85
86=item set_master_process
87
88  $Amanda_process->set_master_process($arg, @pname);
89
90Search the process table to find a process in @pname and make it the master, $arg must be an argument of the process.
91
92=item set_master
93
94  $Amanda_process->set_master($pname, $pid);
95
96Set $Amanda_process->{master_pname} and $Amanda_process->{master_pid}.
97
98=item kill_process
99
100  $Amanda_process->kill_process($signal);
101
102Send the $signal to all amanda processes.
103
104=item process_running
105
106  my $count = $Amanda_process->process_running();
107
108Return the number of amanda process alive.
109
110=item count_process
111
112  my $count = $Amanda_process->count_process();
113
114Return the number of amanda process in the table.
115
116=item process_alive
117
118  my $alive = Amanda::Process::process_alive($pid, $pname);
119
120Return 0 if the process is not alive.
121Return 1 if the process is still alive.
122
123=back
124
125=cut
126
127sub new {
128    my $class = shift;
129    my ($verbose) = shift;
130
131    my $self = {
132	verbose => $verbose,
133	master_name => "",
134	master_pid => "",
135	pids => {},
136	pstable => {},
137	ppid => {},
138    };
139    bless ($self, $class);
140    return $self;
141}
142
143# Get information about the current set of processes, using ps -e
144# and ps -ef.
145#
146# Side effects:
147# - sets %pstable to a map (pid => process name) of all running
148#   processes
149# - sets %ppid to a map (pid -> parent pid) of all running
150#   processes' parent pids
151#
152sub load_ps_table() {
153    my $self = shift;
154    $self->{pstable} = {};
155    $self->{ppid} = ();
156    my $ps_argument = $Amanda::Constants::PS_ARGUMENT;
157    if ($ps_argument eq "CYGWIN") {
158	open(PSTABLE, "-|", "ps -f") || die("ps -f: $!");
159	my $psline = <PSTABLE>; #header line
160	while($psline = <PSTABLE>) {
161	    chomp $psline;
162	    my @psline = split " ", $psline;
163	    my $pid = $psline[1];
164	    my $ppid = $psline[2];
165	    my $stime = $psline[4];
166	    my $pname;
167	    if ($stime =~ /:/) {  # 10:32:44
168		$pname = basename($psline[5])
169	    } else {              # May 22
170		$pname = basename($psline[6])
171	    }
172	    $self->{pstable}->{$pid} = $pname;
173	    $self->{ppid}->{$pid} = $ppid;
174	}
175	close(PSTABLE);
176    } else {
177	open(PSTABLE, "-|", "$Amanda::Constants::PS $ps_argument")
178	    or die("$Amanda::Constants::PS $ps_argument: $!");
179	my $psline = <PSTABLE>; #header line
180	while($psline = <PSTABLE>) {
181	    chomp $psline;
182	    my ($pid, $ppid, $pname, $arg1, $arg2) = split " ", $psline;
183	    $pname = basename($pname);
184	    if ($pname =~ /^perl/ && defined $arg1) {
185		if ($arg1 !~ /^\-/) {
186		    $pname = $arg1;
187		} elsif (defined $arg2) {
188		    if ($arg2 !~ /^\-/) {
189			$pname = $arg2;
190		    }
191		}
192		$pname = basename($pname);
193	    }
194	    $self->{pstable}->{$pid} = $pname;
195	    $self->{ppid}->{$pid} = $ppid;
196	}
197	close(PSTABLE);
198    }
199}
200
201# Scan a logfile for processes that should still be running: processes
202# having an "INFO foo bar pid 1234" line in the log, but no corresponding
203# "INFO pid-done 1234", and only if pid 1234 has the correct process
204# name.
205#
206# Prerequisites:
207#  %pstable must be set up (use load_ps_table)
208#
209# Side effects:
210# - sets %pids to a map (pid => process name) of all still-running
211#   Amanda processes
212# - sets $master_pname to the top-level process for this run (e.g.,
213#   amdump, amflush)
214# - sets $master_pid to the pid of $master_pname
215#
216# @param $logfile: the logfile to scan
217#
218sub scan_log($) {
219    my $self = shift;
220    my $logfile = shift;
221    my $first = 1;
222    my($line);
223
224    open(LOGFILE, "<", $logfile) || die("$logfile: $!");
225    while($line = <LOGFILE>) {
226	if ($line =~ /^INFO .* (.*) pid (\d*)$/) {
227	    my ($pname, $pid) = ($1, $2);
228	    if ($first == 1) {
229		$self->{master_pname} = $pname;
230		$self->{master_pid} = $pid;
231		$first = 0;
232	    }
233	    if (defined $self->{pstable}->{$pid} && $pname eq $self->{pstable}->{$pid}) {
234		$self->{pids}->{$pid} = $pname;
235	    } elsif (defined $self->{pstable}->{$pid} && $self->{pstable}->{$pid} =~ /perl/) {
236		# We can get 'perl' for a perl script.
237		$self->{pids}->{$pid} = $pname;
238	    } elsif (defined $self->{pstable}->{$pid}) {
239		debug("pid $pid doesn't match: " . $pname . " != " . $self->{pstable}->{$pid});
240		print "pid $pid doesn't match: ", $pname, " != ", $self->{pstable}->{$pid}, "\n" if $self->{verbose};
241	    }
242	} elsif ($line =~ /^INFO .* pid-done (\d*)$/) {
243	    my $pid = $1;
244	    print "pid $pid is done\n" if $self->{verbose};
245	    delete $self->{pids}->{$pid};
246	}
247    }
248    close(LOGFILE);
249
250    # log unexpected dead process
251    if ($self->{verbose}) {
252	for my $pid (keys %{$self->{pids}}) {
253	    if (!defined $self->{pstable}->{$pid}) {
254		print "pid $pid is dead\n";
255	    }
256	}
257    }
258}
259
260# Recursive function to add all child processes of $pid to %amprocess.
261#
262# Prerequisites:
263# - %ppid must be set (load_ps_table)
264#
265# Side-effects:
266# - adds all child processes of $pid to %amprocess
267#
268# @param $pid: the process to start at
269#
270sub add_child_pid($);
271sub add_child_pid($) {
272    my $self = shift;
273    my $pid = shift;
274    foreach my $cpid (keys %{$self->{ppid}}) {
275	my $ppid = $self->{ppid}->{$cpid};
276	if ($pid == $ppid) {
277	    if (!defined $self->{amprocess}->{$cpid}) {
278		$self->{amprocess}->{$cpid} = $cpid;
279		$self->add_child_pid($cpid);
280	    }
281	}
282    }
283}
284
285# Find all children of all amanda processes, as determined by traversing
286# the process graph (via %ppid).
287#
288# Prerequisites:
289# - %ppid must be set (load_ps_table)
290# - %pids must be set (scan_log)
291#
292# Side-effects:
293# - sets %amprocess to a map (pid => pid) of all amanda processes, including
294#   children
295#
296sub add_child() {
297    my $self = shift;
298    foreach my $pid (keys %{$self->{pids}}) {
299	if (defined $pid) {
300	    $self->{amprocess}->{$pid} = $pid;
301	}
302    }
303
304    foreach my $pid (keys %{$self->{pids}}) {
305	$self->add_child_pid($pid);
306    }
307}
308
309# Set master_pname and master_pid.
310#
311# Side-effects:
312# - sets $self->{master_pname} and $self->{master_pid}.
313#
314sub set_master_process {
315    my $self = shift;
316    my $arg = shift;
317    my @pname = @_;
318
319    my $ps_argument_args = $Amanda::Constants::PS_ARGUMENT_ARGS;
320    for my $pname (@pname) {
321	my $pid;
322
323	if ($ps_argument_args eq "CYGWIN") {
324	    $pid = `ps -ef|grep -w ${pname}|grep -w ${arg}| grep -v grep | awk '{print \$2}'`;
325	} else {
326	    $pid = `$Amanda::Constants::PS $ps_argument_args|grep -w ${pname}|grep -w ${arg}| grep -v grep | awk '{print \$1}'`;
327	}
328	chomp $pid;
329	if ($pid ne "") {
330	    $self->set_master($pname, $pid);
331	}
332    }
333}
334
335# Set master_pname and master_pid.
336#
337# Side-effects:
338# - sets $self->{master_pname} and $self->{master_pid}.
339#
340sub set_master($$) {
341    my $self = shift;
342    my $pname = shift;
343    my $pid = shift;
344
345    $self->{master_pname} = $pname;
346    $self->{master_pid} = $pid;
347    $self->{pids}->{$pid} = $pname;
348}
349# Send a signal to all amanda process
350#
351# Side-effects:
352# - All amanda process receive the signal.
353#
354# Prerequisites:
355# - %amprocess must be set (add_child)
356#
357# @param $signal: the signal to send
358#
359sub kill_process($) {
360    my $self = shift;
361    my $signal = shift;
362
363    foreach my $pid (keys %{$self->{amprocess}}) {
364	print "Sendding $signal signal to pid $pid\n" if $self->{verbose};
365	kill $signal, $pid;
366    }
367}
368
369# Count the number of processes in %amprocess that are still running.  This
370# re-runs 'ps -e' every time, so calling it repeatedly may result in a
371# decreasing count.
372#
373# Prerequisites:
374# - %amprocess must be set (add_child)
375#
376# @returns: number of pids in %amprocess that are still alive
377sub process_running() {
378    my $self = shift;
379
380    $self->load_ps_table();
381    my $count = 0;
382    foreach my $pid (keys %{$self->{amprocess}}) {
383	if (defined $self->{pstable}->{$pid}) {
384	    $count++;
385	}
386    }
387
388    return $count;
389}
390
391# Count the number of processes in %amprocess.
392#
393# Prerequisites:
394# - %amprocess must be set (add_child)
395#
396# @returns: number of pids in %amprocess.
397sub count_process() {
398    my $self = shift;
399
400    return scalar keys( %{$self->{amprocess}} );
401}
402
403# return if a process is alive.  If $pname is provided,
404# only returns 1 if the name matches.
405#
406# Prerequisites:
407# - %pstable must be set (load_ps_table)
408#
409# @param $pid: the pid of the process
410# @param $pname: the name of the process (optional)
411#
412# @returns: 1 if process is alive
413#           '' if process is dead
414
415sub process_alive() {
416    my $self = shift;
417    my $pid = shift;
418    my $pname = shift;
419
420    if (defined $pname && defined $self->{pstable}->{$pid}) {
421	return $self->{pstable}->{$pid} eq $pname;
422    } else {
423	return defined $self->{pstable}->{$pid};
424    }
425}
426
4271;
428