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