xref: /openbsd/usr.sbin/pkg_add/OpenBSD/Vstat.pm (revision 039cbdaa)
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