xref: /openbsd/regress/usr.sbin/syslogd/Syslogd.pm (revision 097a140d)
1#	$OpenBSD: Syslogd.pm,v 1.26 2021/03/09 15:16:28 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 Syslogd;
22use parent 'Proc';
23use Carp;
24use Cwd;
25use File::Basename;
26use File::Copy;
27use File::Temp qw(tempfile tempdir);
28use Sys::Hostname;
29use Time::HiRes qw(time alarm sleep);
30
31sub new {
32	my $class = shift;
33	my %args = @_;
34	$args{ktraceexec} = "ktrace" if $args{ktrace};
35	$args{ktraceexec} = $ENV{KTRACE} if $ENV{KTRACE};
36	$args{ktracefile} ||= "syslogd.ktrace";
37	$args{fstatfile} ||= "syslogd.fstat";
38	$args{logfile} ||= "syslogd.log";
39	$args{up} ||= "syslogd: started";
40	$args{down} ||= "syslogd: exited";
41	$args{up} = $args{down} = "execute:"
42	    if $args{foreground} || $args{daemon};
43	$args{foreground} && $args{daemon}
44	    and croak "$class cannot run in foreground and as daemon";
45	$args{func} = sub { Carp::confess "$class func may not be called" };
46	$args{execfile} ||= $ENV{SYSLOGD} ? $ENV{SYSLOGD} : "syslogd";
47	$args{conffile} ||= "syslogd.conf";
48	$args{outfile} ||= "file.log";
49	unless ($args{outpipe}) {
50		my $dir = tempdir("syslogd-regress-XXXXXXXXXX",
51		    CLEANUP => 1, TMPDIR => 1);
52		chmod(0755, $dir)
53		    or die "$class chmod directory $dir failed: $!";
54		$args{tempdir} = $dir;
55		$args{outpipe} = "$dir/pipe.log";
56	}
57	$args{outconsole} ||= "console.log";
58	$args{outuser} ||= "user.log";
59	if ($args{memory}) {
60		$args{memory} = {} unless ref $args{memory};
61		$args{memory}{name} ||= "memory";
62		$args{memory}{size} //= 1;
63	}
64	my $self = Proc::new($class, %args);
65	$self->{connectaddr}
66	    or croak "$class connect addr not given";
67
68	_make_abspath(\$self->{$_}) foreach (qw(conffile outfile outpipe));
69	_make_abspath(\$self->{ktracefile}) if $self->{chdir};
70
71	# substitute variables in config file
72	my $curdir = dirname($0) || ".";
73	my $objdir = getcwd();
74	my $hostname = hostname();
75	(my $host = $hostname) =~ s/\..*//;
76	my $connectdomain = $self->{connectdomain};
77	my $connectaddr = $self->{connectaddr};
78	my $connectproto = $self->{connectproto};
79	my $connectport = $self->{connectport};
80
81	open(my $fh, '>', $self->{conffile})
82	    or die ref($self), " create conf file $self->{conffile} failed: $!";
83	print $fh "*.*\t$self->{outfile}\n";
84	print $fh "*.*\t|dd of=$self->{outpipe}\n" unless $self->{nopipe};
85	print $fh "*.*\t/dev/console\n" unless $self->{noconsole};
86	print $fh "*.*\tsyslogd-regress\n" unless $self->{nouser};
87	my $memory = $self->{memory};
88	print $fh "*.*\t:$memory->{size}:$memory->{name}\n" if $memory;
89	my $loghost = $self->{loghost};
90	unless ($loghost) {
91		$loghost = '@$connectaddr';
92		$loghost .= ':$connectport' if $connectport;
93	}
94	my $config = "*.*\t$loghost\n";
95	$config .= $self->{conf} if $self->{conf};
96	$config =~ s/(\$[a-z]+)/$1/eeg;
97	print $fh $config;
98	close $fh;
99
100	return $self->create_out();
101}
102
103sub create_out {
104	my $self = shift;
105	my $timeout = shift || 10;
106	my @sudo = $ENV{SUDO} ? $ENV{SUDO} : ();
107
108	my $end = time() + $timeout;
109
110	open(my $fh, '>', $self->{outfile})
111	    or die ref($self), " create log file $self->{outfile} failed: $!";
112	close $fh;
113
114	open($fh, '>', $self->{outpipe})
115	    or die ref($self), " create pipe file $self->{outpipe} failed: $!";
116	chmod(0644, $self->{outpipe})
117	    or die ref($self), " chmod pipe file $self->{outpipe} failed: $!";
118	my @cmd = (@sudo, "chown", "_syslogd", $self->{outpipe});
119	system(@cmd)
120	    and die ref($self), " chown pipe file $self->{outpipe} failed: $!";
121	close $fh;
122
123	foreach my $dev (qw(console user)) {
124		my $file = $self->{"out$dev"};
125		unlink($file);
126		open($fh, '>', $file)
127		    or die ref($self), " create $dev file $file failed: $!";
128		close $fh;
129		my $user = $dev eq "console" ?
130		    "/dev/console" : "syslogd-regress";
131		my @cmd = (@sudo, "./ttylog", $user, $file);
132		$self->{"pid$dev"} = open(my $ctl, '|-', @cmd)
133		    or die ref($self), " pipe to @cmd failed: $!";
134		# remember until object is destroyed, autoclose will send EOF
135		$self->{"ctl$dev"} = $ctl;
136	}
137
138	foreach my $dev (qw(console user)) {
139		my $file = $self->{"out$dev"};
140		while ($self->{"ctl$dev"}) {
141			open(my $fh, '<', $file) or die ref($self),
142			    " open $file for reading failed: $!";
143			last if grep { /ttylog: started/ } <$fh>;
144			time() < $end
145			    or croak ref($self), " no 'started' in $file ".
146			    "after $timeout seconds";
147			sleep .1;
148		}
149	}
150
151	return $self;
152}
153
154sub ttykill {
155	my $self = shift;
156	my $dev = shift;
157	my $sig = shift;
158	my $pid = $self->{"pid$dev"}
159	    or die ref($self), " no tty log pid$dev";
160
161	if (kill($sig => $pid) != 1) {
162		my $sudo = $ENV{SUDO};
163		$sudo && $!{EPERM}
164		    or die ref($self), " kill $pid failed: $!";
165		my @cmd = ($sudo, '/bin/kill', "-$sig", $pid);
166		system(@cmd)
167		    and die ref($self), " sudo kill $pid failed: $?";
168	}
169	return $self;
170}
171
172sub child {
173	my $self = shift;
174	my @sudo = $ENV{SUDO} ? $ENV{SUDO} : "env";
175
176	my @pkill = (@sudo, "pkill", "-KILL", "-x", "syslogd");
177	my @pgrep = ("pgrep", "-x", "syslogd");
178	system(@pkill) && $? != 256
179	    and die ref($self), " system '@pkill' failed: $?";
180	while ($? == 0) {
181		print STDERR "syslogd still running\n";
182		system(@pgrep) && $? != 256
183		    and die ref($self), " system '@pgrep' failed: $?";
184	}
185	print STDERR "syslogd not running\n";
186
187	unless (${$self->{client}}->{early}) {
188		my @flush = (@sudo, "./logflush");
189		system(@flush)
190		    and die "Command '@flush' failed: $?";
191	}
192
193	chdir $self->{chdir}
194	    or die ref($self), " chdir '$self->{chdir}' failed: $!"
195	    if $self->{chdir};
196
197	my @libevent;
198	foreach (qw(EVENT_NOKQUEUE EVENT_NOPOLL EVENT_NOSELECT)) {
199		push @libevent, "$_=1" if delete $ENV{$_};
200	}
201	push @libevent, "EVENT_SHOW_METHOD=1" if @libevent;
202	my @ktrace;
203	@ktrace = ($self->{ktraceexec}, "-i", "-f", $self->{ktracefile})
204	    if $self->{ktraceexec};
205	my @cmd = (@sudo, @libevent, @ktrace, $self->{execfile},
206	    "-f", $self->{conffile});
207	push @cmd, "-d" if !$self->{foreground} && !$self->{daemon};
208	push @cmd, "-F" if $self->{foreground};
209	push @cmd, "-V" unless $self->{cacrt};
210	push @cmd, "-C", $self->{cacrt}
211	    if $self->{cacrt} && $self->{cacrt} ne "default";
212	push @cmd, "-s", $self->{ctlsock} if $self->{ctlsock};
213	push @cmd, @{$self->{options}} if $self->{options};
214	print STDERR "execute: @cmd\n";
215	exec @cmd;
216	die ref($self), " exec '@cmd' failed: $!";
217}
218
219sub up {
220	my $self = Proc::up(shift, @_);
221	my $timeout = shift || 10;
222
223	my $end = time() + $timeout;
224
225	while ($self->{fstat}) {
226		$self->fstat();
227		last unless $self->{foreground} || $self->{daemon};
228
229		# in foreground mode and as daemon we have no debug output
230		# check fstat kqueue entry to detect statup
231		open(my $fh, '<', $self->{fstatfile}) or die ref($self),
232		    " open $self->{fstatfile} for reading failed: $!";
233		last if grep { /kqueue .* state: W/ } <$fh>;
234		time() < $end
235		    or croak ref($self), " no 'kqueue' in $self->{fstatfile} ".
236		    "after $timeout seconds";
237		sleep .1;
238	}
239
240	return $self;
241}
242
243sub down {
244	my $self = shift;
245
246	if (my $dir = $self->{tempdir}) {
247		# keep all logs in single directory for easy debugging
248		copy($_, ".") foreach glob("$dir/*");
249	}
250
251	return Proc::down($self, @_) unless $self->{daemon};
252
253	my $timeout = $_[0] || 10;
254	my $end = time() + $timeout;
255
256	my @sudo = $ENV{SUDO} ? $ENV{SUDO} : "env";
257	my @pkill = (@sudo, "pkill", "-TERM", "-x", "syslogd");
258	my @pgrep = ("pgrep", "-x", "syslogd");
259	system(@pkill) && $? != 256
260	    and die ref($self), " system '@pkill' failed: $?";
261	do {
262		sleep .1;
263		system(@pgrep) && $? != 256
264		    and die ref($self), " system '@pgrep' failed: $?";
265		return Proc::down($self, @_) if $? == 256;
266		print STDERR "syslogd still running\n";
267	} while (time() < $end);
268
269	return;
270}
271
272sub fstat {
273	my $self = shift;
274
275	open(my $fh, '>', $self->{fstatfile}) or die ref($self),
276	    " open $self->{fstatfile} for writing failed: $!";
277	my @cmd = ("fstat");
278	open(my $fs, '-|', @cmd)
279	    or die ref($self), " open pipe from '@cmd' failed: $!";
280	print $fh grep { /^\w+ *syslogd *\d+/ } <$fs>;
281	close($fs) or die ref($self), $! ?
282	    " close pipe from '@cmd' failed: $!" :
283	    " command '@cmd' failed: $?";
284	close($fh)
285	    or die ref($self), " close $self->{fstatfile} failed: $!";
286}
287
288sub _make_abspath {
289	my $file = ref($_[0]) ? ${$_[0]} : $_[0];
290	if (substr($file, 0, 1) ne "/") {
291		$file = getcwd(). "/". $file;
292		${$_[0]} = $file if ref($_[0]);
293	}
294	return $file;
295}
296
297sub kill_privsep {
298	return Proc::kill(@_);
299}
300
301sub kill_syslogd {
302	my $self = shift;
303	my $sig = shift // 'TERM';
304	my $ppid = shift // $self->{pid};
305
306	# find syslogd child of privsep parent
307	my @cmd = ("ps", "-ww", "-p", $ppid, "-U", "_syslogd",
308	    "-o", "pid,ppid,comm", );
309	open(my $ps, '-|', @cmd)
310	    or die ref($self), " open pipe from '@cmd' failed: $!";
311	my @pslist;
312	my @pshead = split(' ', scalar <$ps>);
313	while (<$ps>) {
314		s/\s+$//;
315		my %h;
316		@h{@pshead} = split(' ', $_, scalar @pshead);
317		push @pslist, \%h;
318	}
319	close($ps) or die ref($self), $! ?
320	    " close pipe from '@cmd' failed: $!" :
321	    " command '@cmd' failed: $?";
322	my @pschild =
323	    grep { $_->{PPID} == $ppid && $_->{COMMAND} eq "syslogd" } @pslist;
324	@pschild == 1
325	    or die ref($self), " not one privsep child: ",
326	    join(" ", map { $_->{PID} } @pschild);
327
328	return Proc::kill($self, $sig, $pschild[0]{PID});
329}
330
331my $rotate_num = 0;
332sub rotate {
333	my $self = shift;
334
335	$self->loggrep("bytes transferred", 1) or sleep 1;
336	foreach my $name (qw(file pipe)) {
337		my $file = $self->{"out$name"};
338		for (my $i = $rotate_num; $i >= 0; $i--) {
339			my $new = $file. ".$i";
340			my $old = $file. ($i > 0 ? ".".($i-1) : "");
341
342			rename($old, $new) or die ref($self),
343			    " rename from '$old' to '$new' failed: $!";
344		}
345	}
346	$rotate_num++;
347	return $self->create_out();
348};
349
3501;
351