1# ex:ts=8 sw=4:
2# $OpenBSD: AddDelete.pm,v 1.100 2023/07/02 13:33:10 espie Exp $
3#
4# Copyright (c) 2007-2010 Marc Espie <espie@openbsd.org>
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#
18
19use v5.36;
20
21# common behavior to pkg_add / pkg_delete
22package main;
23our $not;
24
25package OpenBSD::PackingElement::FileObject;
26sub retrieve_fullname($self, $state, $pkgname)
27{
28	return $state->{destdir}.$self->fullname;
29}
30
31package OpenBSD::PackingElement::FileBase;
32sub retrieve_size($self)
33{
34	return $self->{size};
35}
36
37package OpenBSD::PackingElement::SpecialFile;
38use OpenBSD::PackageInfo;
39sub retrieve_fullname($self, $state, $pkgname)
40{
41	return installed_info($pkgname).$self->name;
42}
43
44package OpenBSD::PackingElement::FCONTENTS;
45sub retrieve_size($self)
46{
47	my $size = 0;
48	my $cname = $self->fullname;
49	if (defined $cname) {
50		$size = (stat $cname)[7];
51	}
52	return $size;
53}
54
55
56package OpenBSD::AddDelete;
57use OpenBSD::Error;
58use OpenBSD::Paths;
59use OpenBSD::PackageInfo;
60use OpenBSD::AddCreateDelete;
61our @ISA = qw(OpenBSD::AddCreateDelete);
62
63sub do_the_main_work($self, $state)
64{
65	if ($state->{bad}) {
66		return;
67	}
68
69	umask 0022;
70
71	my $handler = sub { $state->fatal("Caught SIG#1", shift); };
72	local $SIG{'INT'} = $handler;
73	local $SIG{'QUIT'} = $handler;
74	local $SIG{'HUP'} = $handler;
75	local $SIG{'KILL'} = $handler;
76	local $SIG{'TERM'} = $handler;
77
78	if ($state->defines('pkg-debug')) {
79		$self->main($state);
80	} else {
81		eval { $self->main($state); };
82	}
83	my $dielater = $@;
84	return $dielater;
85}
86
87sub handle_end_tags($self, $state)
88{
89	return if !defined $state->{tags}{atend};
90	$state->progress->for_list("Running tags",
91	    [keys %{$state->{tags}{atend}}],
92	    sub($k) {
93		return if $state->{tags}{deleted}{$k};
94		return if $state->{tags}{superseded}{$k};
95		$state->{tags}{atend}{$k}->run_tag($state);
96	    });
97}
98
99sub run_command($self, $state)
100{
101	lock_db($state->{not}, $state) unless $state->defines('nolock');
102	$state->check_root;
103	$self->process_parameters($state);
104	my $dielater = $self->do_the_main_work($state);
105	# cleanup various things
106	$self->handle_end_tags($state);
107	$state->{recorder}->cleanup($state);
108	$state->ldconfig->ensure;
109	OpenBSD::PackingElement->finish($state);
110	$state->progress->clear;
111	$state->log->dump;
112	$self->finish_display($state);
113	if ($state->verbose >= 2 || $state->{size_only} ||
114	    $state->defines('tally')) {
115		$state->vstat->tally;
116	}
117	# show any error, and show why we died...
118	rethrow $dielater;
119}
120
121sub parse_and_run($self, $cmd)
122{
123	my $state = $self->new_state($cmd);
124	$state->handle_options;
125
126	local $SIG{'INFO'} = sub { $state->status->print($state); };
127
128	my ($lflag, $termios);
129	if ($self->silence_children($state)) {
130		require POSIX;
131
132		$termios = POSIX::Termios->new;
133
134		if (defined $termios->getattr) {
135			$lflag = $termios->getlflag;
136		}
137
138		if (defined $lflag) {
139			my $NOKERNINFO = 0x02000000; # not defined in POSIX
140			$termios->setlflag($lflag | $NOKERNINFO);
141			$termios->setattr;
142		}
143	}
144
145	$self->try_and_run_command($state);
146
147	if (defined $lflag) {
148		$termios->setlflag($lflag);
149		$termios->setattr;
150	}
151
152	return $self->exit_code($state);
153}
154
155sub exit_code($self, $state)
156{
157	return $state->{bad} != 0;
158}
159
160# $self->silence_children($state)
161sub silence_children($, $)
162{
163	1;
164}
165
166# nothing to do
167sub tweak_list($, $)
168{
169}
170
171sub process_setlist($self, $state)
172{
173	$state->tracker->todo(@{$state->{setlist}});
174	# this is the actual very small loop that processes all sets
175	while (my $set = shift @{$state->{setlist}}) {
176		$state->status->what->set($set);
177		$set = $set->real_set;
178		next if $set->{finished};
179		$state->progress->set_header('Checking packages');
180		unshift(@{$state->{setlist}}, $self->process_set($set, $state));
181		$self->tweak_list($state);
182	}
183}
184
185package OpenBSD::SharedItemsRecorder;
186sub new($class)
187{
188	return bless {}, $class;
189}
190
191sub is_empty($self)
192{
193	return !(defined $self->{dirs} or defined $self->{users} or
194	    defined $self->{groups});
195}
196
197sub cleanup($self, $state)
198{
199	return if $self->is_empty or $state->{not};
200
201	require OpenBSD::SharedItems;
202	OpenBSD::SharedItems::cleanup($self, $state);
203}
204
205package OpenBSD::AddDelete::State;
206use OpenBSD::Vstat;
207use OpenBSD::Log;
208our @ISA = qw(OpenBSD::AddCreateDelete::State);
209
210sub handle_options($state, $opt_string, @usage)
211{
212	$state->{extra_stats} = 0;
213	$state->{opt}{V} = sub() {
214		$state->{extra_stats}++;
215	};
216	$state->{no_exports} = 1;
217	$state->add_interactive_options;
218	$state->SUPER::handle_options($opt_string.'aciInqsVB:', @usage);
219
220	if ($state->opt('s')) {
221		$state->{not} = 1;
222	}
223	# XXX RequiredBy
224	$main::not = $state->{not};
225	$state->{localbase} = $state->opt('L') // OpenBSD::Paths->localbase;
226	$ENV{PATH} = join(':',
227	    '/bin',
228	    '/sbin',
229	    '/usr/bin',
230	    '/usr/sbin',
231	    '/usr/X11R6/bin',
232	    "$state->{localbase}/bin",
233	    "$state->{localbase}/sbin");
234
235	$state->{size_only} = $state->opt('s');
236	$state->{quick} = $state->opt('q');
237	$state->{extra} = $state->opt('c');
238	$state->{automatic} = $state->opt('a') // 0;
239	$ENV{'PKG_DELETE_EXTRA'} = $state->{extra} ? "Yes" : "No";
240	if ($state->{not} || $state->defines('DONTLOG')) {
241		$state->{loglevel} = 0;
242	}
243	$state->{loglevel} //= 1;
244	if ($state->{loglevel}) {
245		require Sys::Syslog;
246		Sys::Syslog::openlog($state->{cmd}, "nofatal");
247	}
248	$state->{wantntogo} = $state->{extra_stats};
249	if (defined $ENV{PKG_CHECKSUM}) {
250		$state->{subst}->add('checksum', 1);
251	}
252	my $base = $state->opt('B') // '';
253	if ($base ne '') {
254		$base.='/' unless $base =~ m/\/$/o;
255	}
256	$state->{destdir} = $base;
257}
258
259sub init($self, @p)
260{
261	$self->{l} = OpenBSD::Log->new($self);
262	$self->{vstat} = OpenBSD::Vstat->new($self);
263	$self->{status} = OpenBSD::Status->new;
264	$self->{recorder} = OpenBSD::SharedItemsRecorder->new;
265	$self->{v} = 0;
266	$self->SUPER::init(@p);
267	$self->{export_level}++;
268}
269
270sub syslog($self, @p)
271{
272	return unless $self->{loglevel};
273	Sys::Syslog::syslog('info', $self->f(@p));
274}
275
276sub ntodo($state, $offset)
277{
278	return $state->tracker->sets_todo($offset);
279}
280
281# one-level dependencies tree, for nicer printouts
282sub build_deptree($state, $set, @deps)
283{
284	if (defined $state->{deptree}{$set}) {
285		$set = $state->{deptree}{$set};
286	}
287	for my $dep (@deps) {
288		$state->{deptree}{$dep} = $set
289		    unless defined $state->{deptree}{$dep};
290	}
291}
292
293sub deptree_header($state, $pkg)
294{
295	if (defined $state->{deptree}{$pkg}) {
296		my $s = $state->{deptree}{$pkg}->real_set;
297		if ($s eq $pkg) {
298			delete $state->{deptree}{$pkg};
299		} else {
300			return $s->short_print.':';
301		}
302	}
303	return '';
304}
305
306sub vstat($self)
307{
308	return $self->{vstat};
309}
310
311sub log($self, @p)
312{
313	if (@p == 0) {
314		return $self->{l};
315	} else {
316		$self->{l}->say(@p);
317	}
318}
319
320sub run_quirks($state, $sub)
321{
322	if (!exists $state->{quirks}) {
323		eval {
324			use lib ('/usr/local/libdata/perl5/site_perl');
325			require OpenBSD::Quirks;
326			# interface version number.
327			$state->{quirks} = OpenBSD::Quirks->new(1);
328		};
329		if ($@ && !$state->{not}) {
330			my $show = $state->verbose >= 2;
331			if (!$show) {
332				my $l = $state->repo->installed->match_locations(OpenBSD::Search::Stem->new('quirks'));
333				$show = @$l > 0;
334			}
335			$state->errsay("Can't load quirk: #1", $@) if $show;
336			# XXX cache that this didn't work
337			$state->{quirks} = undef;
338		}
339	}
340
341	if (defined $state->{quirks}) {
342		eval {
343			&$sub($state->{quirks});
344		};
345		if ($@) {
346			$state->errsay("Bad quirk: #1", $@);
347		}
348	}
349}
350
351sub check_root($state)
352{
353	if ($< && !$state->defines('nonroot')) {
354		if ($state->{not}) {
355			$state->errsay("#1 should be run as root",
356			    $state->{cmd}) if $state->verbose;
357		} else {
358			$state->fatal("#1 must be run as root", $state->{cmd});
359		}
360	}
361}
362
363sub choose_location($state, $name, $list, $is_quirks = 0)
364{
365	if (@$list == 0) {
366		if (!$is_quirks) {
367			$state->errsay("Can't find #1", $name);
368			$state->run_quirks(
369			    sub($quirks) {
370				$quirks->filter_obsolete([$name], $state);
371			    });
372		}
373		return undef;
374	} elsif (@$list == 1) {
375		return $list->[0];
376	}
377
378	my %h = map {($_->name, $_)} @$list;
379	if ($state->is_interactive) {
380		$h{'<None>'} = undef;
381		$state->progress->clear;
382		my $cmp = sub {		# XXX prototypable ?
383			return -1 if !defined $h{$a};
384			return 1 if !defined $h{$b};
385			my $r = $h{$a}->pkgname->to_pattern cmp
386				    $h{$b}->pkgname->to_pattern;
387			if ($r == 0) {
388				return $h{$a}->pkgname->{version}->
389				    compare($h{$b}->pkgname->{version});
390			} else {
391				return $r;
392			}
393		};
394		my $result = $state->ask_list("Ambiguous: choose package for $name", sort $cmp keys %h);
395		return $h{$result};
396	} else {
397		$state->errsay("Ambiguous: #1 could be #2",
398		    $name, join(' ', keys %h));
399		return undef;
400	}
401}
402
403sub status($self)
404{
405	return $self->{status};
406}
407
408sub replacing($self)
409{
410	return $self->{replacing};
411}
412
413OpenBSD::Auto::cache(ldconfig,
414    sub($self) {
415	return OpenBSD::LdConfig->new($self);
416    });
417
418# if we're not running as root, allow some stuff when not under /usr/local
419sub allow_nonroot($state, $path)
420{
421	return $state->defines('nonroot') &&
422	    $path !~ m,^\Q$state->{localbase}/\E,;
423}
424
425sub make_path($state, $path, $fullname)
426{
427	require File::Path;
428	if ($state->allow_nonroot($fullname)) {
429		eval {
430			File::Path::mkpath($path);
431		};
432	} else {
433		File::Path::mkpath($path);
434	}
435}
436
437# this is responsible for running ldconfig when needed
438package OpenBSD::LdConfig;
439
440sub new($class, $state)
441{
442	bless { state => $state, todo => 0 }, $class;
443}
444
445# called once to figure out which directories are actually used
446sub init($self)
447{
448	my $state = $self->{state};
449	my $destdir = $state->{destdir};
450
451	$self->{ldconfig} = [OpenBSD::Paths->ldconfig];
452
453	$self->{path} = {};
454	if ($destdir ne '') {
455		unshift @{$self->{ldconfig}}, OpenBSD::Paths->chroot, '--',
456		    $destdir;
457	}
458	open my $fh, "-|", @{$self->{ldconfig}}, "-r";
459	if (defined $fh) {
460		while (<$fh>) {
461			if (m/^\s*search directories:\s*(.*?)\s*$/o) {
462				for my $d (split(/\:/o, $1)) {
463					$self->{path}{$d} = 1;
464				}
465				last;
466			}
467		}
468		close($fh);
469	} else {
470		$state->errsay("Can't find ldconfig");
471	}
472}
473
474# called from libs to figure out whether ldconfig should be rerun
475sub mark_directory($self, $name)
476{
477	if (!defined $self->{path}) {
478		$self->init;
479	}
480	require File::Basename;
481	my $d = File::Basename::dirname($name);
482	if ($self->{path}{$d}) {
483		$self->{todo} = 1;
484	}
485}
486
487# call before running any command (or at end) to run ldconfig just in time
488sub ensure($self)
489{
490	if ($self->{todo}) {
491		my $state = $self->{state};
492		$state->vsystem(@{$self->{ldconfig}}, "-R")
493		    unless $state->{not};
494		$self->{todo} = 0;
495	}
496}
497
498# the object that gets displayed during status updates
499package OpenBSD::Status;
500
501sub print($self, $state)
502{
503	my $what = $self->{what};
504	$what //= 'Processing';
505	my $object;
506	if (defined $self->{object}) {
507		$object = $self->{object};
508	} elsif (defined $self->{set}) {
509		$object = $self->{set}->print;
510	} else {
511		$object = "Parameters";
512	}
513
514	$state->say($what." #1#2", $object, $state->ntogo_string);
515	if ($state->defines('carp')) {
516		require Carp;
517		Carp::cluck("currently here");
518	}
519}
520
521sub set($self, $set)
522{
523	delete $self->{object};
524	$self->{set} = $set;
525	return $self;
526}
527
528sub object($self, $object)
529{
530	delete $self->{set};
531	$self->{object} = $object;
532	return $self;
533}
534
535sub what($self, $what = undef)
536{
537	$self->{what} = $what;
538	return $self;
539}
540
541sub new($class)
542{
543	bless {}, $class;
544}
545
5461;
547