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