1#! /usr/bin/perl
2
3# ex:ts=8 sw=4:
4# $OpenBSD: FwUpdate.pm,v 1.36 2023/11/25 10:17:59 espie Exp $
5#
6# Copyright (c) 2014 Marc Espie <espie@openbsd.org>
7#
8# Permission to use, copy, modify, and distribute this software for any
9# purpose with or without fee is hereby granted, provided that the above
10# copyright notice and this permission notice appear in all copies.
11#
12# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19#
20use v5.36;
21use OpenBSD::PkgAdd;
22use OpenBSD::PackageRepository;
23use OpenBSD::PackageLocator;
24
25package OpenBSD::FwUpdate::Locator;
26our @ISA = qw(OpenBSD::PackageLocator);
27
28sub add_default($self, $state, $p)
29{
30	my $path = $state->opt('p');
31	if (!$path) {
32		my $dir = OpenBSD::Paths->os_directory;
33		if (!defined $dir) {
34			$state->fatal("Couldn't find/parse OS version");
35		}
36		$path = "http://firmware.openbsd.org/firmware/$dir/";
37	}
38	$p->add(OpenBSD::PackageRepository->new($path, $state));
39}
40
41package OpenBSD::FwUpdate::State;
42our @ISA = qw(OpenBSD::PkgAdd::State);
43
44sub cache_directory($)
45{
46	return undef;
47}
48
49sub locator($)
50{
51	return "OpenBSD::FwUpdate::Locator";
52}
53
54sub handle_options($state)
55{
56	$state->OpenBSD::State::handle_options('adinp:',
57	    '[-adinv] [-D keyword] [-p path] [driver...]');
58	$state->{not} = $state->opt('n');
59	if ($state->opt('i')) {
60		$state->{not} = 1;
61	}
62	$main::not = $state->{not};
63	$state->progress->setup(0, 0, $state);
64	$state->{localbase} = OpenBSD::Paths->localbase;
65	$state->{destdir} = '';
66	$state->{wantntogo} = 0;
67	$state->{interactive} = OpenBSD::InteractiveStub->new($state);
68	$state->{subst}->add('repair', 1);
69	if ($state->opt('a') && @ARGV != 0) {
70		$state->usage;
71	}
72	$state->{fw_verbose} = $state->{v};
73	if ($state->{v}) {
74		$state->{v}--;
75	}
76	if ($state->{fw_verbose}) {
77		$state->say("Path to firmware: #1",
78		    $state->locator->printable_default_path($state));
79	}
80	$state->{subst}->add('NO_SCP', 1);
81}
82
83sub finish_init($state)
84{
85	$state->{subst}->add('FW_UPDATE', 1);
86}
87
88sub installed_drivers($self)
89{
90	return keys %{$self->{installed_drivers}};
91}
92
93sub is_installed($self, $driver)
94{
95	return $self->{installed_drivers}{$driver};
96}
97
98sub machine_drivers($self)
99{
100	return keys %{$self->{machine_drivers}};
101}
102
103sub all_drivers($self)
104{
105	return keys %{$self->{all_drivers}};
106}
107
108sub is_needed($self, $driver)
109{
110	my ($self, $driver) = @_;
111	return $self->{machine_drivers}{$driver};
112}
113
114sub display_timestamp($state, $pkgname, $timestamp)
115{
116	return unless $state->verbose;
117	$state->SUPER::display_timestamp($pkgname, $timestamp);
118}
119
120sub fw_status($state, $msg, $list)
121{
122	return if @$list == 0;
123	$state->say("#1: #2", $msg, join(' ', @$list));
124}
125
126package OpenBSD::FwUpdate::Update;
127our @ISA = qw(OpenBSD::Update);
128
129package OpenBSD::FwUpdate;
130our @ISA = qw(OpenBSD::PkgAdd);
131
132OpenBSD::Auto::cache(updater,
133    sub($) {
134	    require OpenBSD::Update;
135	    return OpenBSD::FwUpdate::Update->new;
136    });
137
138my %possible_drivers = map {($_, "$_-firmware")}
139    (qw(acx amdgpu athn bwfm bwi intel inteldrm ipw iwi
140	iwm iwn iwx malo mtw ogx otus pgt radeondrm
141	uath upgt uvideo vmm wpi));
142
143my %match = map {($_, qr{^\Q$_\E\d+\s+at\s})} (keys %possible_drivers);
144$match{'intel'} = qr{^cpu\d+: Intel};
145
146sub parse_dmesg($self, $f, $search, $found)
147{
148	while (<$f>) {
149		chomp;
150		for my $driver (keys %$search) {
151			next unless $_ =~ $match{$driver};
152			$found->{$driver} = 1;
153			delete $search->{$driver};
154		}
155	}
156}
157
158sub find_machine_drivers($self, $state)
159{
160	$state->{machine_drivers} = {};
161	$state->{all_drivers} = \%possible_drivers;
162	my %search = %possible_drivers;
163	if (open(my $f, '<', '/var/run/dmesg.boot')) {
164		$self->parse_dmesg($f, \%search, $state->{machine_drivers});
165		close($f);
166	} else {
167		$state->errsay("Can't open dmesg.boot: #1", $!);
168	}
169	if (open(my $cmd, '-|', 'dmesg')) {
170		$self->parse_dmesg($cmd, \%search, $state->{machine_drivers});
171		close($cmd);
172	} else {
173		$state->errsay("Can't run dmesg: #1", $!);
174	}
175}
176
177sub driver2firmware($k)
178{
179	return $possible_drivers{$k};
180}
181
182sub find_installed_drivers($self, $state)
183{
184	my $inst = $state->repo->installed;
185	for my $driver (keys %possible_drivers) {
186		my $search = OpenBSD::Search::Stem->new(driver2firmware($driver));
187		my $l = $inst->match_locations($search);
188		if (@$l > 0) {
189			$state->{installed_drivers}{$driver} =
190			    OpenBSD::Handle->from_location($l->[0]);
191		}
192	}
193}
194
195
196sub new_state($self, $cmd)
197{
198	return OpenBSD::FwUpdate::State->new($cmd);
199}
200
201sub find_handle($self, $state, $driver)
202{
203	my $pkgname = driver2firmware($driver);
204	my $set;
205	my $h = $state->is_installed($driver);
206	if ($h) {
207		$set = $state->updateset->add_older($h);
208	} else {
209		$set = $state->updateset->add_hints($pkgname);
210	}
211	return $set;
212}
213
214sub mark_set_for_deletion($self, $set, $state)
215{
216	# XXX to be simplified. Basically, we pre-do the work of the updater...
217	for my $h ($set->older) {
218		$h->{update_found} = 1;
219	}
220	$set->{updates}++;
221}
222
223# no quirks for firmware, bypass entirely
224sub do_quirks($self, $state)
225{
226	$state->finish_init;
227}
228
229sub to_remove($self, $state, $driver)
230{
231	$self->mark_set_for_deletion($self->to_add_or_update($state, $driver));
232}
233
234sub to_add_or_update($self, $state, $driver)
235{
236	my $set = $self->find_handle($state, $driver);
237	push(@{$state->{setlist}}, $set);
238	return $set;
239}
240
241sub show_info($self, $state)
242{
243	my (@installed, @unneeded, @needed);
244	for my $d ($state->installed_drivers) {
245		my $h = $state->is_installed($d)->pkgname;
246		if ($state->is_needed($d)) {
247			push(@installed, $h);
248		} else {
249			push(@unneeded, $h);
250		}
251	}
252	for my $d ($state->machine_drivers) {
253		if (!$state->is_installed($d)) {
254			push(@needed, driver2firmware($d));
255		}
256	}
257	$state->fw_status("Installed", \@installed);
258	$state->fw_status("Installed, extra", \@unneeded);
259	$state->fw_status("Missing", \@needed);
260}
261
262sub silence_children($, $)
263{
264	0
265}
266
267sub process_parameters($self, $state)
268{
269	$self->find_machine_drivers($state);
270	$self->find_installed_drivers($state);
271
272	if ($state->opt('i')) {
273		$self->show_info($state);
274		exit(0);
275	}
276	if (@ARGV == 0) {
277		if ($state->opt('d')) {
278			for my $driver ($state->installed_drivers) {
279				if ($state->opt('a') ||
280				    !$state->is_needed($driver)) {
281					$self->to_remove($state, $driver);
282				}
283			}
284		} else {
285			if ($state->opt('a')) {
286				for my $driver ($state->all_drivers) {
287					$self->to_add_or_update($state,
288					    $driver);
289				}
290			} else {
291				for my $driver ($state->machine_drivers) {
292					$self->to_add_or_update($state,
293					    $driver);
294				}
295				for my $driver ($state->installed_drivers) {
296					# XXX skip already done up there ^
297					next if $state->is_needed($driver);
298					$self->to_add_or_update($state,
299					    $driver);
300				}
301			}
302		}
303		if (!(defined $state->{setlist}) && $state->{fw_verbose}) {
304			$state->say($state->opt('d') ?
305			    "No firmware to delete." :
306			    "No devices found which need firmware files to be downloaded.");
307			exit(0);
308		}
309	} else {
310		for my $driver (@ARGV) {
311			$driver =~ s/\-firmware(\-\d.*)?$//;
312			if (!$possible_drivers{$driver}) {
313				$state->errsay("#1: unknown driver", $driver);
314				exit(1);
315			}
316			if ($state->opt('d') &&
317			    !$state->is_installed($driver)) {
318				$state->errsay("Can't delete uninstalled driver: #1", $driver);
319				next;
320			}
321
322			my $set = $self->to_add_or_update($state, $driver);
323			if ($state->opt('d')) {
324				$self->mark_set_for_deletion($set);
325			}
326		}
327	}
328	if ($state->{fw_verbose}) {
329		my (@deleting, @updating, @installing);
330		for my $set (@{$state->{setlist}}) {
331			for my $h ($set->older) {
332				if ($h->{update_found}) {
333					push(@deleting, $h->pkgname);
334				} else {
335					push(@updating, $h->pkgname);
336				}
337			}
338			for my $h ($set->hints) {
339				push(@installing, $h->pkgname);
340			}
341		}
342		$state->fw_status("Installing", \@installing);
343		$state->fw_status("Deleting", \@deleting);
344		$state->fw_status("Updating", \@updating);
345	}
346}
347
3481;
349