1# ex:ts=8 sw=4: 2# $OpenBSD: PackageRepository.pm,v 1.62 2009/02/02 20:41:47 espie Exp $ 3# 4# Copyright (c) 2003-2007 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 18use strict; 19use warnings; 20 21# XXX load extra class, grab match from Base class, and tweak inheritance 22# to get all methods. 23 24use OpenBSD::PackageRepository::Installed; 25$OpenBSD::PackageRepository::Installed::ISA=(qw(OpenBSD::PackageRepository)); 26 27package OpenBSD::PackageRepository; 28our @ISA=(qw(OpenBSD::PackageRepositoryBase)); 29 30use OpenBSD::PackageLocation; 31use OpenBSD::Paths; 32 33sub _new 34{ 35 my ($class, $path, $host) = @_; 36 $path .= '/' unless $path =~ m/\/$/; 37 bless { host => $host, path => $path }, $class; 38} 39 40sub baseurl 41{ 42 my $self = shift; 43 44 return $self->{path}; 45} 46 47sub new 48{ 49 my ($class, $baseurl) = @_; 50 my $o = $class->parse(\$baseurl); 51 return $o; 52} 53 54sub strip_urlscheme 55{ 56 my ($class, $r) = @_; 57 if ($$r =~ m/^(.*?)\:(.*)$/) { 58 my $scheme = lc($1); 59 if ($scheme eq $class->urlscheme) { 60 $$r = $2; 61 return 1; 62 } 63 } 64 return 0; 65} 66 67sub parse_local_url 68{ 69 my ($class, $r, @args) = @_; 70 71 my $o; 72 73 if ($$r =~ m/^(.*?)\:(.*)/) { 74 $o = $class->_new($1, @args); 75 $$r = $2; 76 } else { 77 $o = $class->_new($$r, @args); 78 $$r = ''; 79 } 80 return $o; 81} 82 83sub parse_url 84{ 85 &parse_local_url; 86} 87 88sub parse_fullurl 89{ 90 my ($class, $r) = @_; 91 92 $class->strip_urlscheme($r) or return undef; 93 return $class->parse_url($r); 94} 95 96sub parse 97{ 98 my ($class, $ref) = @_; 99 my $_ = $$ref; 100 return undef if $_ eq ''; 101 102 if (m/^ftp\:/io) { 103 return OpenBSD::PackageRepository::FTP->parse_fullurl($ref); 104 } elsif (m/^http\:/io) { 105 return OpenBSD::PackageRepository::HTTP->parse_fullurl($ref); 106 } elsif (m/^https\:/io) { 107 return OpenBSD::PackageRepository::HTTPS->parse_fullurl($ref); 108 } elsif (m/^scp\:/io) { 109 require OpenBSD::PackageRepository::SCP; 110 111 return OpenBSD::PackageRepository::SCP->parse_fullurl($ref); 112 } elsif (m/^src\:/io) { 113 require OpenBSD::PackageRepository::Source; 114 115 return OpenBSD::PackageRepository::Source->parse_fullurl($ref); 116 } elsif (m/^file\:/io) { 117 return OpenBSD::PackageRepository::Local->parse_fullurl($ref); 118 } elsif (m/^inst\:$/io) { 119 return OpenBSD::PackageRepository::Installed->parse_fullurl($ref); 120 } else { 121 return OpenBSD::PackageRepository::Local->parse_fullurl($ref); 122 } 123} 124 125sub available 126{ 127 my $self = shift; 128 129 return @{$self->list}; 130} 131 132sub stemlist 133{ 134 my $self = shift; 135 if (!defined $self->{stemlist}) { 136 require OpenBSD::PackageName; 137 138 $self->{stemlist} = OpenBSD::PackageName::avail2stems($self->available); 139 } 140 return $self->{stemlist}; 141} 142 143sub wipe_info 144{ 145 my ($self, $pkg) = @_; 146 147 require File::Path; 148 149 my $dir = $pkg->{dir}; 150 if (defined $dir) { 151 152 File::Path::rmtree($dir); 153 delete $pkg->{dir}; 154 } 155} 156 157# by default, all objects may exist 158sub may_exist 159{ 160 return 1; 161} 162 163# by default, we don't track opened files for this key 164 165sub opened 166{ 167 undef; 168} 169 170# hint: 0 premature close, 1 real error. undef, normal ! 171 172sub close 173{ 174 my ($self, $object, $hint) = @_; 175 close($object->{fh}) if defined $object->{fh}; 176 if (defined $object->{pid2}) { 177 local $SIG{ALRM} = sub { 178 kill HUP => $object->{pid2}; 179 }; 180 alarm(30); 181 waitpid($object->{pid2}, 0); 182 alarm(0); 183 } 184 $self->parse_problems($object->{errors}, $hint, $object) 185 if defined $object->{errors}; 186 undef $object->{errors}; 187 $object->deref; 188} 189 190sub make_room 191{ 192 my $self = shift; 193 194 # kill old files if too many 195 my $already = $self->opened; 196 if (defined $already) { 197 # gc old objects 198 if (@$already >= $self->maxcount) { 199 @$already = grep { defined $_->{fh} } @$already; 200 } 201 while (@$already >= $self->maxcount) { 202 my $o = shift @$already; 203 $self->close_now($o); 204 } 205 } 206 return $already; 207} 208 209# open method that tracks opened files per-host. 210sub open 211{ 212 my ($self, $object) = @_; 213 214 return unless $self->may_exist($object->{name}); 215 216 # kill old files if too many 217 my $already = $self->make_room; 218 my $fh = $self->open_pipe($object); 219 if (!defined $fh) { 220 return; 221 } 222 $object->{fh} = $fh; 223 if (defined $already) { 224 push @$already, $object; 225 } 226 return $fh; 227} 228 229sub find 230{ 231 my ($repository, $name, $arch) = @_; 232 my $self = OpenBSD::PackageLocation->new($repository, $name, $arch); 233 234 return $self->openPackage; 235} 236 237sub grabPlist 238{ 239 my ($repository, $name, $arch, $code) = @_; 240 my $self = OpenBSD::PackageLocation->new($repository, $name, $arch); 241 242 return $self->grabPlist($code); 243} 244 245sub parse_problems 246{ 247 my ($self, $filename, $hint, $object) = @_; 248 unlink $filename; 249} 250 251sub cleanup 252{ 253 # nothing to do 254} 255 256sub relative_url 257{ 258 my ($self, $name) = @_; 259 if (defined $name) { 260 return $self->baseurl.$name.".tgz"; 261 } else { 262 return $self->baseurl; 263 } 264} 265 266package OpenBSD::PackageRepository::Local; 267our @ISA=qw(OpenBSD::PackageRepository); 268use OpenBSD::Error; 269 270sub urlscheme 271{ 272 return 'file'; 273} 274 275sub parse_fullurl 276{ 277 my ($class, $r) = @_; 278 279 $class->strip_urlscheme($r); 280 return $class->parse_local_url($r); 281} 282 283# wrapper around copy, that sometimes does not copy 284sub may_copy 285{ 286 my ($self, $object, $destdir) = @_; 287 my $src = $self->relative_url($object->{name}); 288 require File::Spec; 289 my (undef, undef, $base) = File::Spec->splitpath($src); 290 my $dest = File::Spec->catfile($destdir, $base); 291 if (File::Spec->canonpath($dest) eq File::Spec->canonpath($src)) { 292 return; 293 } 294 if (-f $dest) { 295 my ($ddev, $dino) = (stat $dest)[0,1]; 296 my ($sdev, $sino) = (stat $src)[0, 1]; 297 if ($ddev == $sdev and $sino == $dino) { 298 return; 299 } 300 } 301 Copy($src, $destdir); 302} 303 304sub open_pipe 305{ 306 my ($self, $object) = @_; 307 if (defined $ENV{'PKG_CACHE'}) { 308 $self->may_copy($object, $ENV{'PKG_CACHE'}); 309 } 310 my $pid = open(my $fh, "-|"); 311 if (!defined $pid) { 312 die "Cannot fork: $!"; 313 } 314 if ($pid) { 315 return $fh; 316 } else { 317 open STDERR, ">/dev/null"; 318 exec {OpenBSD::Paths->gzip} 319 "gzip", 320 "-d", 321 "-c", 322 "-q", 323 "-f", 324 $self->relative_url($object->{name}) 325 or die "Can't run gzip"; 326 } 327} 328 329sub may_exist 330{ 331 my ($self, $name) = @_; 332 return -r $self->relative_url($name); 333} 334 335sub list 336{ 337 my $self = shift; 338 my $l = []; 339 my $dname = $self->baseurl; 340 opendir(my $dir, $dname) or return $l; 341 while (my $e = readdir $dir) { 342 next unless $e =~ m/^(.*)\.tgz$/o; 343 next unless -f "$dname/$e"; 344 push(@$l, $1); 345 } 346 close($dir); 347 return $l; 348} 349 350package OpenBSD::PackageRepository::Local::Pipe; 351our @ISA=qw(OpenBSD::PackageRepository::Local); 352 353sub urlscheme 354{ 355 return 'pipe'; 356} 357 358sub relative_url 359{ 360 return ''; 361} 362 363sub may_exist 364{ 365 return 1; 366} 367 368sub open_pipe 369{ 370 my ($self, $object) = @_; 371 my $pid = open(my $fh, "-|"); 372 if (!defined $pid) { 373 die "Cannot fork: $!"; 374 } 375 if ($pid) { 376 return $fh; 377 } else { 378 open STDERR, ">/dev/null"; 379 exec {OpenBSD::Paths->gzip} 380 "gzip", 381 "-d", 382 "-c", 383 "-q", 384 "-f", 385 "-" 386 or die "can't run gzip"; 387 } 388} 389 390package OpenBSD::PackageRepository::Distant; 391our @ISA=qw(OpenBSD::PackageRepository); 392 393sub baseurl 394{ 395 my $self = shift; 396 397 return "//$self->{host}/$self->{path}"; 398} 399 400sub parse_url 401{ 402 &parse_distant_url; 403} 404 405sub parse_distant_url 406{ 407 my ($class, $r) = @_; 408 # same heuristics as ftp(1): 409 # find host part, rest is parsed as a local url 410 if ($$r =~ m/^\/\/(.*?)(\/.*)$/) { 411 my $host = $1; 412 $$r = $2; 413 return $class->parse_local_url($r, $host); 414 } else { 415 return undef; 416 } 417} 418 419my $buffsize = 2 * 1024 * 1024; 420 421sub pkg_copy 422{ 423 my ($self, $in, $object) = @_; 424 425 require OpenBSD::Temp; 426 my $name = $object->{name}; 427 my $dir = $object->{cache_dir}; 428 429 my ($copy, $filename) = OpenBSD::Temp::permanent_file($dir, $name) or die "Can't write copy to cache"; 430 chmod 0644, $filename; 431 $object->{tempname} = $filename; 432 my $handler = sub { 433 my ($sig) = @_; 434 unlink $filename; 435 close($in); 436 $SIG{$sig} = 'DEFAULT'; 437 kill $sig, $$; 438 }; 439 440 my $nonempty = 0; 441 my $error = 0; 442 { 443 444 local $SIG{'PIPE'} = $handler; 445 local $SIG{'INT'} = $handler; 446 local $SIG{'HUP'} = $handler; 447 local $SIG{'QUIT'} = $handler; 448 local $SIG{'KILL'} = $handler; 449 local $SIG{'TERM'} = $handler; 450 451 my ($buffer, $n); 452 # copy stuff over 453 do { 454 $n = sysread($in, $buffer, $buffsize); 455 if (!defined $n) { 456 die "Error reading: $!"; 457 } 458 if ($n > 0) { 459 $nonempty = 1; 460 } 461 if (!$error) { 462 my $r = syswrite $copy, $buffer; 463 if (!defined $r || $r < $n) { 464 $error = 1; 465 } 466 } 467 syswrite STDOUT, $buffer; 468 } while ($n != 0); 469 close($copy); 470 } 471 472 if ($nonempty && !$error) { 473 rename $filename, "$dir/$name.tgz"; 474 } else { 475 unlink $filename; 476 } 477 close($in); 478} 479 480sub open_pipe 481{ 482 require OpenBSD::Temp; 483 484 my ($self, $object) = @_; 485 $object->{errors} = OpenBSD::Temp::file(); 486 $object->{cache_dir} = $ENV{'PKG_CACHE'}; 487 $object->{parent} = $$; 488 489 my ($rdfh, $wrfh); 490 pipe($rdfh, $wrfh); 491 492 my $pid = open(my $fh, "-|"); 493 if (!defined $pid) { 494 die "Cannot fork: $!"; 495 } 496 if ($pid) { 497 $object->{pid} = $pid; 498 } else { 499 open(STDIN, '<&', $rdfh) or die "Bad dup"; 500 close($rdfh); 501 close($wrfh); 502 exec {OpenBSD::Paths->gzip} 503 "gzip", 504 "-d", 505 "-c", 506 "-q", 507 "-" 508 or die "can't run gzip"; 509 } 510 my $pid2 = fork(); 511 512 if (!defined $pid2) { 513 die "Cannot fork: $!"; 514 } 515 if ($pid2) { 516 $object->{pid2} = $pid2; 517 } else { 518 open STDERR, '>', $object->{errors}; 519 open(STDOUT, '>&', $wrfh) or die "Bad dup"; 520 close($rdfh); 521 close($wrfh); 522 close($fh); 523 if (defined $object->{cache_dir}) { 524 my $pid3 = open(my $in, "-|"); 525 if (!defined $pid3) { 526 die "Cannot fork: $!"; 527 } 528 if ($pid3) { 529 $self->pkg_copy($in, $object); 530 } else { 531 $self->grab_object($object); 532 } 533 } else { 534 $self->grab_object($object); 535 } 536 exit(0); 537 } 538 close($rdfh); 539 close($wrfh); 540 return $fh; 541} 542 543sub finish_and_close 544{ 545 my ($self, $object) = @_; 546 if (defined $object->{cache_dir}) { 547 while (defined $object->intNext) { 548 } 549 } 550 $self->SUPER::finish_and_close($object); 551} 552 553package OpenBSD::PackageRepository::HTTPorFTP; 554our @ISA=qw(OpenBSD::PackageRepository::Distant); 555 556our %distant = (); 557 558 559sub grab_object 560{ 561 my ($self, $object) = @_; 562 my ($ftp, @extra) = split(/\s+/, OpenBSD::Paths->ftp); 563 if (defined $ENV{'FTP_KEEPALIVE'}) { 564 push(@extra, "-k", $ENV{'FTP_KEEPALIVE'}); 565 } 566 exec {$ftp} 567 $ftp, 568 @extra, 569 "-o", 570 "-", $self->url($object->{name}) 571 or die "can't run ".OpenBSD::Paths->ftp; 572} 573 574sub maxcount 575{ 576 return 1; 577} 578 579sub opened 580{ 581 my $self = $_[0]; 582 my $k = $self->{host}; 583 if (!defined $distant{$k}) { 584 $distant{$k} = []; 585 } 586 return $distant{$k}; 587} 588 589sub should_have 590{ 591 my ($self, $pkgname) = @_; 592 if (defined $self->{lasterror} && $self->{lasterror} == 421) { 593 return (defined $self->{list}) && 594 grep { $_ eq $pkgname } @{$self->{list}}; 595 } else { 596 return 0; 597 } 598} 599 600sub try_until_success 601{ 602 my ($self, $pkgname, $code) = @_; 603 604 for (my $retry = 5; $retry <= 160; $retry *= 2) { 605 undef $self->{lasterror}; 606 my $o = &$code; 607 if (defined $o) { 608 return $o; 609 } 610 if (defined $self->{lasterror} && $self->{lasterror} == 550) { 611 last; 612 } 613 if ($self->should_have($pkgname)) { 614 print STDERR "Temporary error, sleeping $retry seconds\n"; 615 sleep($retry); 616 } 617 } 618 return undef; 619} 620 621sub find 622{ 623 my ($self, $pkgname, @extra) = @_; 624 625 return $self->try_until_success($pkgname, 626 sub { 627 return $self->SUPER::find($pkgname, @extra); }); 628 629} 630 631sub grabPlist 632{ 633 my ($self, $pkgname, @extra) = @_; 634 635 return $self->try_until_success($pkgname, 636 sub { 637 return $self->SUPER::grabPlist($pkgname, @extra); }); 638} 639 640sub parse_problems 641{ 642 my ($self, $filename, $hint, $object) = @_; 643 CORE::open(my $fh, '<', $filename) or return; 644 645 my $baseurl = $self->url; 646 my $url = $baseurl; 647 if (defined $object) { 648 $url = $object->url; 649 } 650 my $_; 651 my $notyet = 1; 652 while(<$fh>) { 653 next if m/^(?:200|220|221|226|229|230|227|250|331|500|150)[\s\-]/o; 654 next if m/^EPSV command not understood/o; 655 next if m/^Trying [\da-f\.\:]+\.\.\./o; 656 next if m/^Requesting \Q$baseurl\E/; 657 next if m/^Remote system type is\s+/o; 658 next if m/^Connected to\s+/o; 659 next if m/^remote\:\s+/o; 660 next if m/^Using binary mode to transfer files/o; 661 next if m/^Retrieving\s+/o; 662 next if m/^Success?fully retrieved file/o; 663 next if m/^\d+\s+bytes\s+received\s+in/o; 664 next if m/^ftp: connect to address.*: No route to host/o; 665 666 if (defined $hint && $hint == 0) { 667 next if m/^ftp: -: short write/o; 668 next if m/^ftp: Writing -: Broken pipe/o; 669 next if m/^421\s+/o; 670 } 671 if ($notyet) { 672 print STDERR "Error from $url:\n" if $notyet; 673 $notyet = 0; 674 } 675 if (m/^421\s+/o || 676 m/^ftp: connect: Connection timed out/o || 677 m/^ftp: Can't connect or login to host/o) { 678 $self->{lasterror} = 421; 679 } 680 if (m/^550\s+/o) { 681 $self->{lasterror} = 550; 682 } 683 print STDERR $_; 684 } 685 CORE::close($fh); 686 $self->SUPER::parse_problems($filename, $hint, $object); 687} 688 689sub list 690{ 691 my ($self) = @_; 692 if (!defined $self->{list}) { 693 $self->make_room; 694 my $error = OpenBSD::Temp::file(); 695 $self->{list} = $self->obtain_list($error); 696 $self->parse_problems($error); 697 } 698 return $self->{list}; 699} 700 701sub get_http_list 702{ 703 my ($self, $error) = @_; 704 705 my $fullname = $self->url; 706 my $l = []; 707 my $_; 708 open(my $fh, '-|', OpenBSD::Paths->ftp." -o - $fullname 2>$error") 709 or return; 710 while(<$fh>) { 711 chomp; 712 for my $pkg (m/\<A\s+HREF=\"(.*?)\.tgz\"\>/gio) { 713 $pkg = $1 if $pkg =~ m|^.*/(.*)$|; 714 push(@$l, $pkg); 715 } 716 } 717 close($fh); 718 return $l; 719} 720 721package OpenBSD::PackageRepository::HTTP; 722our @ISA=qw(OpenBSD::PackageRepository::HTTPorFTP); 723 724sub urlscheme 725{ 726 return 'http'; 727} 728 729sub obtain_list 730{ 731 my ($self, $error) = @_; 732 return $self->get_http_list($error); 733} 734 735package OpenBSD::PackageRepository::HTTPS; 736our @ISA=qw(OpenBSD::PackageRepository::HTTP); 737 738sub urlscheme 739{ 740 return 'https'; 741} 742 743package OpenBSD::PackageRepository::FTP; 744our @ISA=qw(OpenBSD::PackageRepository::HTTPorFTP); 745 746sub urlscheme 747{ 748 return 'ftp'; 749} 750 751sub _list 752{ 753 my ($self, $cmd) = @_; 754 my $l =[]; 755 my $_; 756 open(my $fh, '-|', "$cmd") or return; 757 while(<$fh>) { 758 chomp; 759 next if m/^\d\d\d\s+\S/; 760 next unless m/^(?:\.\/)?(\S+)\.tgz\s*$/; 761 push(@$l, $1); 762 } 763 close($fh); 764 return $l; 765} 766 767sub get_ftp_list 768{ 769 my ($self, $error) = @_; 770 771 my $fullname = $self->url; 772 return $self->_list("echo 'nlist'| ".OpenBSD::Paths->ftp 773 ." $fullname 2>$error"); 774} 775 776sub obtain_list 777{ 778 my ($self, $error) = @_; 779 if (defined $ENV{'ftp_proxy'}) { 780 return $self->get_http_list($error); 781 } else { 782 return $self->get_ftp_list($error); 783 } 784} 785 7861; 787