1#	$OpenBSD: Proc.pm,v 1.10 2022/03/25 14:15:10 bluhm Exp $
2
3# Copyright (c) 2010-2020 Alexander Bluhm <bluhm@openbsd.org>
4# Copyright (c) 2014 Florian Riehm <mail@friehm.de>
5#
6# Permission to use, copy, modify, and distribute this software for any
7# purpose with or without fee is hereby granted, provided that the above
8# copyright notice and this permission notice appear in all copies.
9#
10# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
18use strict;
19use warnings;
20
21package Proc;
22use BSD::Resource qw(getrlimit setrlimit get_rlimits);
23use Carp;
24use Errno;
25use IO::File;
26use POSIX;
27use Time::HiRes qw(time alarm sleep);
28use IO::Socket::SSL;
29
30my %CHILDREN;
31
32sub kill_children {
33	my @pids = @_ ? @_ : keys %CHILDREN
34	    or return;
35	my @perms;
36	foreach my $pid (@pids) {
37		if (kill(TERM => $pid) != 1 and $!{EPERM}) {
38			push @perms, $pid;
39		}
40	}
41	if (my $sudo = $ENV{SUDO} and @perms) {
42		local $?;  # do not modify during END block
43		my @cmd = ($sudo, '/bin/kill', '-TERM', @perms);
44		system(@cmd);
45	}
46	delete @CHILDREN{@pids};
47}
48
49BEGIN {
50	$SIG{TERM} = $SIG{INT} = sub {
51		my $sig = shift;
52		kill_children();
53		$SIG{TERM} = $SIG{INT} = 'DEFAULT';
54		POSIX::raise($sig);
55	};
56}
57
58END {
59	kill_children();
60	$SIG{TERM} = $SIG{INT} = 'DEFAULT';
61}
62
63sub new {
64	my $class = shift;
65	my $self = { @_ };
66	$self->{down} ||= "Shutdown";
67	$self->{func} && ref($self->{func}) eq 'CODE'
68	    or croak "$class func not given";
69	$self->{ktracepid} && $self->{ktraceexec}
70	    and croak "$class ktrace both pid and exec given";
71	!($self->{ktracepid} || $self->{ktraceexec}) || $self->{ktracefile}
72	    or croak "$class ktrace file not given";
73	$self->{logfile}
74	    or croak "$class log file not given";
75	open(my $fh, '>', $self->{logfile})
76	    or die "$class log file $self->{logfile} create failed: $!";
77	$fh->autoflush;
78	$self->{log} = $fh;
79	$self->{ppid} = $$;
80	return bless $self, $class;
81}
82
83sub run {
84	my $self = shift;
85
86	pipe(my $reader, my $writer)
87	    or die ref($self), " pipe to child failed: $!";
88	defined(my $pid = fork())
89	    or die ref($self), " fork child failed: $!";
90	if ($pid) {
91		$CHILDREN{$pid} = 1;
92		$self->{pid} = $pid;
93		close($reader);
94		$self->{pipe} = $writer;
95		return $self;
96	}
97	%CHILDREN = ();
98	$SIG{TERM} = $SIG{INT} = 'DEFAULT';
99	$SIG{__DIE__} = sub {
100		die @_ if $^S;
101		warn @_;
102		IO::Handle::flush(\*STDERR);
103		POSIX::_exit(255);
104	};
105	open(STDERR, '>&', $self->{log})
106	    or die ref($self), " dup STDERR failed: $!";
107	open(STDOUT, '>&', $self->{log})
108	    or die ref($self), " dup STDOUT failed: $!";
109	close($writer);
110	open(STDIN, '<&', $reader)
111	    or die ref($self), " dup STDIN failed: $!";
112	close($reader);
113
114	if ($self->{rlimit}) {
115		my $rlimits = get_rlimits()
116		    or die ref($self), " get_rlimits failed: $!";
117		while (my($name, $newsoft) = each %{$self->{rlimit}}) {
118			defined(my $resource = $rlimits->{$name})
119			    or die ref($self), " rlimit $name does not exists";
120			my ($soft, $hard) = getrlimit($resource)
121			    or die ref($self), " getrlimit $name failed: $!";
122			setrlimit($resource, $newsoft, $hard) or die ref($self),
123			    " setrlimit $name to $newsoft failed: $!";
124		}
125	}
126	if ($self->{ktracepid}) {
127		my @cmd = ($self->{ktracepid}, "-i", "-f", $self->{ktracefile},
128		    "-p", $$);
129		system(@cmd)
130		    and die ref($self), " system '@cmd' failed: $?";
131	}
132	do {
133		$self->child();
134		print STDERR $self->{up}, "\n";
135		$self->{ts} = $self->{cs}
136		    if $self->{connectproto} && $self->{connectproto} eq "tls";
137		$self->{func}->($self);
138		$self->{ts}->close(SSL_fast_shutdown => 0)
139		    or die ref($self), " SSL shutdown: $!,$SSL_ERROR"
140		    if $self->{ts};
141		delete $self->{ts};
142	} while ($self->{redo});
143	print STDERR "Shutdown", "\n";
144
145	IO::Handle::flush(\*STDOUT);
146	IO::Handle::flush(\*STDERR);
147	POSIX::_exit(0);
148}
149
150sub wait {
151	my $self = shift;
152	my $flags = shift;
153
154	# if we a not the parent process, assume the child is still running
155	return 0 unless $self->{ppid} == $$;
156
157	my $pid = $self->{pid}
158	    or croak ref($self), " no child pid";
159	my $kid = waitpid($pid, $flags);
160	if ($kid > 0) {
161		my $status = $?;
162		my $code;
163		$code = "exit: ".   WEXITSTATUS($?) if WIFEXITED($?);
164		$code = "signal: ". WTERMSIG($?)    if WIFSIGNALED($?);
165		$code = "stop: ".   WSTOPSIG($?)    if WIFSTOPPED($?);
166		delete $CHILDREN{$pid} if WIFEXITED($?) || WIFSIGNALED($?);
167		return wantarray ? ($kid, $status, $code) : $kid;
168	}
169	return $kid;
170}
171
172sub loggrep {
173	my $self = shift;
174	my($regex, $timeout, $count) = @_;
175	my $exit = ($self->{exit} // 0) << 8;
176
177	my $end;
178	$end = time() + $timeout if $timeout;
179
180	do {
181		my($kid, $status, $code) = $self->wait(WNOHANG);
182		if ($kid > 0 && $status != $exit) {
183			# child terminated with failure
184			die ref($self), " child status: $status $code";
185		}
186		open(my $fh, '<', $self->{logfile})
187		    or die ref($self), " log file open failed: $!";
188		my @match = grep { /$regex/ } <$fh>;
189		return wantarray ? @match : $match[0]
190		    if !$count && @match or $count && @match >= $count;
191		close($fh);
192		# pattern not found
193		if ($kid == 0) {
194			# child still running, wait for log data
195			sleep .1;
196		} else {
197			# child terminated, no new log data possible
198			return;
199		}
200	} while ($timeout and time() < $end);
201
202	return;
203}
204
205sub up {
206	my $self = shift;
207	my $timeout = shift || 10;
208	$self->loggrep(qr/$self->{up}/, $timeout)
209	    or croak ref($self), " no '$self->{up}' in $self->{logfile} ".
210		"after $timeout seconds";
211	return $self;
212}
213
214sub down {
215	my $self = shift;
216	my $timeout = shift || 60;
217	$self->loggrep(qr/$self->{down}/, $timeout)
218	    or croak ref($self), " no '$self->{down}' in $self->{logfile} ".
219		"after $timeout seconds";
220	return $self;
221}
222
223sub kill_child {
224	my $self = shift;
225	kill_children($self->{pid});
226	return $self;
227}
228
229sub kill {
230	my $self = shift;
231	my $sig = shift // 'TERM';
232	my $pid = shift // $self->{pid};
233
234	if (kill($sig => $pid) != 1) {
235		my $sudo = $ENV{SUDO};
236		$sudo && $!{EPERM}
237		    or die ref($self), " kill $pid failed: $!";
238		my @cmd = ($sudo, '/bin/kill', "-$sig", $pid);
239		system(@cmd)
240		    and die ref($self), " sudo kill $pid failed: $?";
241	}
242	return $self;
243}
244
2451;
246