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