1# ex:ts=8 sw=4: 2# $OpenBSD: Vstat.pm,v 1.72 2023/06/13 09:07:17 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 18# Provides stat and statfs-like functions for package handling. 19 20# allows user to add/remove files. 21 22# uses mount and df directly for now. 23 24use v5.36; 25 26package OpenBSD::Vstat::Object; 27my $cache = {}; 28my $dummy; 29$dummy = bless \$dummy, __PACKAGE__; 30 31sub new($class, $value = undef) 32{ 33 if (!defined $value) { 34 return $dummy; 35 } 36 if (!defined $cache->{$value}) { 37 $cache->{$value} = bless \$value, $class; 38 } 39 return $cache->{$value}; 40} 41 42sub exists($) 43{ 44 return 1; 45} 46 47sub value($self) 48{ 49 return $$self; 50} 51 52sub none($) 53{ 54 return OpenBSD::Vstat::Object::None->new; 55} 56 57package OpenBSD::Vstat::Object::None; 58our @ISA = qw(OpenBSD::Vstat::Object); 59my $none; 60$none = bless \$none, __PACKAGE__; 61 62sub exists($) 63{ 64 return 0; 65} 66 67sub new($) 68{ 69 return $none; 70} 71 72package OpenBSD::Vstat::Object::Directory; 73our @ISA = qw(OpenBSD::Vstat::Object); 74 75sub new($class, $fname, $set, $o) 76{ 77 bless { name => $fname, set => $set, o => $o }, $class; 78} 79 80# XXX directories don't do anything until you test for their presence. 81# which only happens if you want to replace a directory with a file. 82sub exists($self) 83{ 84 require OpenBSD::SharedItems; 85 86 return OpenBSD::SharedItems::check_shared($self->{set}, $self->{o}); 87} 88 89package OpenBSD::Vstat; 90use File::Basename; 91use OpenBSD::Paths; 92 93sub stat($self, $fname) 94{ 95 my $dev = (stat $fname)[0]; 96 97 if (!defined $dev && $fname ne '/') { 98 return $self->stat(dirname($fname)); 99 } 100 return OpenBSD::Mounts->find($dev, $fname, $self->{state}); 101} 102 103sub account_for($self, $name, $size) 104{ 105 my $e = $self->stat($name); 106 $e->{used} += $size; 107 return $e; 108} 109 110sub account_later($self, $name, $size) 111{ 112 my $e = $self->stat($name); 113 $e->{delayed} += $size; 114 return $e; 115} 116 117sub new($class, $state) 118{ 119 bless {v => [{}], state => $state}, $class; 120} 121 122sub exists($self, $name) 123{ 124 for my $v (@{$self->{v}}) { 125 if (defined $v->{$name}) { 126 return $v->{$name}->exists; 127 } 128 } 129 return -e $name; 130} 131 132sub value($self, $name) 133{ 134 for my $v (@{$self->{v}}) { 135 if (defined $v->{$name}) { 136 return $v->{$name}->value; 137 } 138 } 139 return undef; 140} 141 142sub synchronize($self) 143{ 144 OpenBSD::Mounts->synchronize; 145 if ($self->{state}->{not}) { 146 # this is the actual stacking case: in pretend mode, 147 # I have to put a second vfs on top 148 if (@{$self->{v}} == 2) { 149 my $top = shift @{$self->{v}}; 150 while (my ($k, $v) = each %$top) { 151 $self->{v}[0]{$k} = $v; 152 } 153 } 154 unshift(@{$self->{v}}, {}); 155 } else { 156 $self->{v} = [{}]; 157 } 158} 159 160sub drop_changes($self) 161{ 162 OpenBSD::Mounts->drop_changes; 163 # drop the top layer 164 $self->{v}[0] = {}; 165} 166 167sub add($self, $name, $size, $value) 168{ 169 $self->{v}[0]->{$name} = OpenBSD::Vstat::Object->new($value); 170 return defined($size) ? $self->account_for($name, $size) : undef; 171} 172 173sub remove($self, $name, $size) 174{ 175 $self->{v}[0]->{$name} = OpenBSD::Vstat::Object->none; 176 return defined($size) ? $self->account_later($name, -$size) : undef; 177} 178 179sub remove_first($self, $name, $size) 180{ 181 $self->{v}[0]->{$name} = OpenBSD::Vstat::Object->none; 182 return defined($size) ? $self->account_for($name, -$size) : undef; 183} 184 185# since directories may become files during updates, we may have to remove 186# them early, so we need to record them: store exactly as much info as needed 187# for SharedItems. 188sub remove_directory($self, $name, $o) 189{ 190 $self->{v}[0]->{$name} = OpenBSD::Vstat::Object::Directory->new($name, 191 $self->{state}{current_set}, $o); 192} 193 194 195sub tally($self) 196{ 197 OpenBSD::Mounts->tally($self->{state}); 198} 199 200package OpenBSD::Mounts; 201 202my $devinfo; 203my $devinfo2; 204my $giveup; 205 206sub giveup($) 207{ 208 if (!defined $giveup) { 209 $giveup = OpenBSD::MountPoint::Fail->new; 210 } 211 return $giveup; 212} 213 214sub new($class, $dev, $mp, $opts) 215{ 216 if (!defined $devinfo->{$dev}) { 217 $devinfo->{$dev} = OpenBSD::MountPoint->new($dev, $mp, $opts); 218 } 219 return $devinfo->{$dev}; 220} 221 222sub run($class, $state, @args) 223{ 224 my $code = pop @args; 225 open(my $cmd, "-|", @args) or 226 $state->errsay("Can't run #1", join(' ', @args)) 227 and return; 228 while (<$cmd>) { 229 &$code($_); 230 } 231 if (!close($cmd)) { 232 if ($!) { 233 $state->errsay("Error running #1: #2", $!, 234 join(' ', @args)); 235 } else { 236 $state->errsay("Exit status #1 from #2", $?, 237 join(' ', @args)); 238 } 239 } 240} 241 242sub ask_mount($class, $state) 243{ 244 delete $ENV{'BLOCKSIZE'}; 245 $class->run($state, OpenBSD::Paths->mount, sub($l) { 246 chomp $l; 247 if ($l =~ m/^(.*?)\s+on\s+(\/.*?)\s+type\s+.*?(?:\s+\((.*?)\))?$/o) { 248 my ($dev, $mp, $opts) = ($1, $2, $3); 249 $class->new($dev, $mp, $opts); 250 } else { 251 $state->errsay("Can't parse mount line: #1", $l); 252 } 253 }); 254} 255 256sub ask_df($class, $fname, $state) 257{ 258 my $info = $class->giveup; 259 my $blocksize = 512; 260 261 $class->ask_mount($state) if !defined $devinfo; 262 $class->run($state, OpenBSD::Paths->df, "--", $fname, 263 sub($l) { 264 chomp $l; 265 if ($l =~ m/^Filesystem\s+(\d+)\-blocks/o) { 266 $blocksize = $1; 267 } elsif ($l =~ m/^(.*?)\s+\d+\s+\d+\s+(\-?\d+)\s+\d+\%\s+\/.*?$/o) { 268 my ($dev, $avail) = ($1, $2); 269 $info = $devinfo->{$dev}; 270 if (!defined $info) { 271 $info = $class->new($dev); 272 } 273 $info->{avail} = $avail; 274 $info->{blocksize} = $blocksize; 275 } 276 }); 277 278 return $info; 279} 280 281sub find($class, $dev, $fname, $state) 282{ 283 if (!defined $dev) { 284 return $class->giveup; 285 } 286 if (!defined $devinfo2->{$dev}) { 287 $devinfo2->{$dev} = $class->ask_df($fname, $state); 288 } 289 return $devinfo2->{$dev}; 290} 291 292sub synchronize($class) 293{ 294 for my $v (values %$devinfo2) { 295 $v->synchronize; 296 } 297} 298 299sub drop_changes($class) 300{ 301 for my $v (values %$devinfo2) { 302 $v->drop_changes; 303 } 304} 305 306sub tally($self, $state) 307{ 308 for my $v ((sort {$a->name cmp $b->name } values %$devinfo2), $self->giveup) { 309 $v->tally($state); 310 } 311} 312 313package OpenBSD::MountPoint; 314 315sub parse_opts($self, $opts) 316{ 317 for my $o (split /\,\s*/o, $opts) { 318 if ($o eq 'read-only') { 319 $self->{ro} = 1; 320 } elsif ($o eq 'nodev') { 321 $self->{nodev} = 1; 322 } elsif ($o eq 'nosuid') { 323 $self->{nosuid} = 1; 324 } elsif ($o eq 'noexec') { 325 $self->{noexec} = 1; 326 } 327 } 328} 329 330sub ro($self) 331{ 332 return $self->{ro}; 333} 334 335sub nodev($self) 336{ 337 return $self->{nodev}; 338} 339 340sub nosuid($self) 341{ 342 return $self->{nosuid}; 343} 344 345sub noexec($self) 346{ 347 return $self->{noexec}; 348} 349 350sub new($class, $dev, $mp, $opts) 351{ 352 my $n = bless { commited_use => 0, used => 0, delayed => 0, 353 hw => 0, dev => $dev, mp => $mp }, $class; 354 if (defined $opts) { 355 $n->parse_opts($opts); 356 } 357 return $n; 358} 359 360 361sub avail($self, $used = 0) 362{ 363 return $self->{avail} - $self->{used}/$self->{blocksize}; 364} 365 366sub name($self) 367{ 368 return "$self->{dev} on $self->{mp}"; 369} 370 371sub report_ro($s, $state, $fname) 372{ 373 if ($state->verbose >= 3 or ++($s->{problems}) < 4) { 374 $state->errsay("Error: #1 is read-only (#2)", 375 $s->name, $fname); 376 } elsif ($s->{problems} == 4) { 377 $state->errsay("Error: ... more files for #1", $s->name); 378 } 379 $state->{problems}++; 380} 381 382sub report_overflow($s, $state, $fname) 383{ 384 if ($state->verbose >= 3 or ++($s->{problems}) < 4) { 385 $state->errsay("Error: #1 is not large enough (#2)", 386 $s->name, $fname); 387 } elsif ($s->{problems} == 4) { 388 $state->errsay("Error: ... more files do not fit on #1", 389 $s->name); 390 } 391 $state->{problems}++; 392 $state->{overflow} = 1; 393} 394 395sub report_noexec($s, $state, $fname) 396{ 397 $state->errsay("Error: #1 is noexec (#2)", $s->name, $fname); 398 $state->{problems}++; 399} 400 401sub synchronize($v) 402{ 403 if ($v->{used} > $v->{hw}) { 404 $v->{hw} = $v->{used}; 405 } 406 $v->{used} += $v->{delayed}; 407 $v->{delayed} = 0; 408 $v->{commited_use} = $v->{used}; 409} 410 411sub drop_changes($v) 412{ 413 $v->{used} = $v->{commited_use}; 414 $v->{delayed} = 0; 415} 416 417sub tally($data, $state) 418{ 419 return if $data->{used} == 0; 420 $state->print("#1: #2 bytes", $data->name, $data->{used}); 421 my $avail = $data->avail; 422 if ($avail < 0) { 423 $state->print(" (missing #1 blocks)", int(-$avail+1)); 424 } elsif ($data->{hw} >0 && $data->{hw} > $data->{used}) { 425 $state->print(" (highwater #1 bytes)", $data->{hw}); 426 } 427 $state->print("\n"); 428} 429 430package OpenBSD::MountPoint::Fail; 431our @ISA=qw(OpenBSD::MountPoint); 432 433sub avail($, $) 434{ 435 return 1; 436} 437 438sub new($class) 439{ 440 my $n = $class->SUPER::new('???', '???', ''); 441 $n->{avail} = 0; 442 return $n; 443} 444 4451; 446