openbsd-ports/infrastructure/lib/DPB/Locks.pm
espie 587eb9c193 a bit more love for the control socket
- try to create it as %L/dpb-$$  by default.
If we can't, no biggie. If the user explicitly asked for a control socket,
we DO abort otherwise.
- if CONTROL is explicitly empty, do not load the extra code.
- try to remove the socket at exit

- add a new wipehost command, that will try to kill any job on that host
(if some remain), then zap all locks and affinity info related to that host
"it's dead jim"
2019-07-01 08:59:41 +00:00

405 lines
8.0 KiB
Perl

# ex:ts=8 sw=4:
# $OpenBSD: Locks.pm,v 1.52 2019/07/01 08:59:41 espie Exp $
#
# Copyright (c) 2010-2013 Marc Espie <espie@openbsd.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
use strict;
use warnings;
use DPB::User;
package DPB::Lock;
sub new
{
my ($class, $fh) = @_;
bless {fh => $fh}, $class;
}
sub write
{
my ($self, $key, $value) = @_;
if (defined $value) {
print {$self->{fh}} "$key=$value\n";
} else {
print {$self->{fh}} "$key\n";
}
}
sub close
{
my $self = shift;
close($self->{fh});
undef $self->{fh};
}
package DPB::LockInfo;
sub new
{
my ($class, $filename, $logger) = @_;
bless { filename => $filename, logger => $logger}, $class;
}
sub fullpkgpath
{
my $self = shift;
return $self->{locked};
}
sub is_host
{
my $self = shift;
if ($self->{filename} =~ m,/host:[^/]+$,) {
return 1;
} else {
return 0;
}
}
sub is_dist
{
my $self = shift;
if ($self->{filename} =~ m,\.dist$,) {
return 1;
} else {
return 0;
}
}
sub is_pkgpath
{
my $self = shift;
return !($self->is_dist || $self->is_host);
}
sub is_bad { 0 }
sub set_field
{
my ($i, $k, $v) = @_;
if (exists $i->{$k}) {
$i->set_bad("duplicate $k field");
} else {
$i->{$k} = $v;
}
}
sub set_bad
{
my ($i, $error) = @_;
bless $i, 'DPB::LockInfo::Bad';
$i->{parseerror} = $error;
print {$i->{logger}->append($i->{logger}->logfile("debug"))}
"Problem in lock $i->{filename}: $i->{parseerror}\n";
}
sub parse_file
{
my ($i, $locker, $fh) = @_;
while(<$fh>) {
chomp;
if (m/^dpb\=(\d+)\s+on\s+(\S+)$/) {
if (defined $i->{dpb_pib}) {
$i->set_bad("duplicate dpb field");
next;
}
($i->{dpb_pid}, $i->{dpb_host}) = ($1, $2);
if ($i->{dpb_host} eq $locker->{dpb_host}) {
$i->{same_host} = 1;
if ($i->{dpb_pid} == $locker->{dpb_pid}) {
$i->{same_pid} = 1;
}
}
} elsif (m/^(pid|mem)\=(\d+)$/) {
$i->set_field($1, $2);
} elsif (m/^(start|end)\=(\d+)\s/) {
$i->set_field($1, $2);
} elsif (m/^(error|status|todo)\=(.*)$/) {
$i->set_field($1, $2);
$i->{errored} = 1;
} elsif (m/^(host|tag|parent|locked|path)\=(.+)$/) {
$i->set_field($1, $2);
} elsif (m/^(wanted|needed)\=(.*)$/) {
$i->set_field($1, [split(/\s+/, $2)]);
} elsif (m/^(nojunk|cleaned)$/) {
$i->set_field($1, 1);
} else {
$i->set_bad("Parse error on $_");
}
}
$i->{host} //= DPB::Core::Local->hostname;
}
package DPB::LockInfo::Bad;
our @ISA = qw(DPB::LockInfo);
sub is_bad { 1 }
package DPB::Locks;
our @ISA = (qw(DPB::UserProxy));
use File::Path;
use Fcntl;
# Fcntl doesn't export this
use constant O_CLOEXEC => 0x10000;
sub new
{
my ($class, $state) = @_;
my $lockdir = $state->{lockdir};
my $o = bless {lockdir => $lockdir,
dpb_pid => $$,
logger => $state->logger,
user => $state->{log_user},
dpb_host => DPB::Core::Local->hostname}, $class;
$o->make_path($lockdir);
$o->run_as(
sub {
if (!$state->defines("DONT_CLEAN_LOCKS")) {
$o->{stalelocks} = $o->clean_old_locks($state);
}
});
return $o;
}
sub get_info_from_fh
{
my ($self, $fh, $filename) = @_;
my $i = DPB::LockInfo->new($filename, $self->{logger});
$i->parse_file($self, $fh);
return $i;
}
sub get_info_from_file
{
my ($self, $f) = @_;
my $fh = $self->open('<', $f);
if (defined $fh) {
return $self->get_info_from_fh($fh, $f);
} else {
return DPB::LockInfo::Bad->new($f, $self->{logger});
}
}
sub get_info
{
my ($self, $v) = @_;
return $self->get_info_from_file($self->lockname($v));
}
sub scan_lockdir
{
my ($self, $code) = @_;
my $dir = $self->opendir($self->{lockdir});
while (my $e = readdir($dir)) {
next if $e eq '..' or $e eq '.';
# and zap vim temp files as well!
next if $e =~ m/\.swp$/;
&$code($self->get_info_from_file("$self->{lockdir}/$e"));
}
}
sub wipehost
{
my ($self, $h) = @_;
my @wipe;
$self->scan_lockdir(
sub {
my $i = shift;
push(@wipe, $i->{filename}) if $i->{host} eq $h;
});
for my $f (@wipe) {
$self->unlink($f);
}
}
sub clean_old_locks
{
my ($self, $state) = @_;
my $hostpaths = {};
START:
my @problems = ();
my $locks = {};
# first we get all live locks that pertain to us a a dpb host
$self->scan_lockdir(
sub {
my $i = shift;
if ($i->is_bad) {
push @problems, $i->{filename};
return;
}
if (!$i->{same_host} || defined $i->{errored}) {
return;
}
# on the way, let's retaint cores
if (defined $i->{tag}) {
DPB::Core::Init->taint($i->{host}, $i->{tag},
$i->fullpkgpath);
}
push(@{$locks->{$i->{dpb_pid}}}, $i);
});
if (keys %$locks != 0) {
# use ps to check for live dpb (and kill their lists)
open(my $ps, "-|", "ps", "-axww", "-o", "pid args");
my $junk = <$ps>;
while (<$ps>) {
if (m/^(\d+)\s+(.*)$/) {
my ($pid, $cmd) = ($1, $2);
if ($locks->{$pid} && $cmd =~ m/\bdpb\b/) {
delete $locks->{$pid};
}
}
}
# so what's left are stalelocks: remove them, and get a list
# to unlock them manually
for my $list (values %$locks) {
for my $i (@$list) {
# there might be stale host locks in there
# make sure to clean them as well
push(@{$hostpaths->{$i->{host}}},
$i->fullpkgpath) if defined $i->fullpkgpath;
$self->unlink($i->{filename});
}
}
}
# just in case there are weird locks in there
if (@problems) {
$state->say("Problematic lockfiles I can't parse:\n\t#1\n".
"Waiting for ten seconds",
join(' ', @problems));
sleep 10;
goto START;
}
return $hostpaths;
}
sub build_lockname
{
my ($self, $f) = @_;
$f =~ tr|/|.|;
return "$self->{lockdir}/$f";
}
sub lockname
{
my ($self, $v) = @_;
return $self->build_lockname($v->lockname);
}
sub dolock
{
my ($self, $name, $v) = @_;
$self->run_as(
sub {
if (sysopen my $fh, $name,
O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC, 0666) {
DPB::Util->make_hot($fh);
my $lock = DPB::Lock->new($fh);
$lock->write("locked", $v->logname);
$lock->write("dpb",
$self->{dpb_pid}." on ".$self->{dpb_host});
$v->write_parent($lock);
return $lock;
} else {
return 0;
}
});
}
sub lock_has_other_owner
{
my ($self, $v) = @_;
my $info = $self->get_info($v);
if (!$info->is_bad && !$info->{same_pid}) {
return "$info->{dpb_pid} on $info->{dpb_host}";
}
return undef;
}
sub lock
{
my ($self, $v) = @_;
my $lock = $self->lockname($v);
my $fh = $self->dolock($lock, $v);
if ($fh) {
return $fh;
}
return undef;
}
sub unlock
{
my ($self, $v) = @_;
$self->unlink($self->lockname($v));
}
sub locked
{
my ($self, $v) = @_;
return $self->run_as(
sub {
return -e $self->lockname($v);
});
}
sub find_dependencies
{
my ($self, $hostname) = @_;
my $h = {};
my $nojunk;
$self->scan_lockdir(
sub {
my $i = shift;
return if $i->is_bad;
return if defined $i->{cleaned};
return unless defined $i->{host} && $i->{host} eq $hostname;
return unless $i->is_pkgpath;
for my $k (qw(wanted needed)) {
if (defined $i->{$k}) {
for my $v (@{$i->{$k}}) {
$h->{$v} = 1;
}
}
}
# XXX we don't need to do anything more
$nojunk = $i->fullpkgpath if $i->{nojunk};
});
return ($nojunk, $h);
}
sub find_tag
{
my ($self, $hostname) = @_;
my ($tag, $tagowner);
$self->scan_lockdir(
sub {
my $i = shift;
return if $i->is_bad;
return if $i->{cleaned};
if (defined $i->{host} && $i->{host} eq $hostname) {
$tag //= $i->{tag};
$tagowner //= $i->fullpkgpath;
}
});
if (wantarray) {
return ($tag, $tagowner);
} else {
return $tag;
}
}
1;