#!/usr/bin/perl -w
#
# dmassage, version 0.6
#
# Copyright (c) 2002 Camiel Dobbelaar <cd@sentia.nl>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

use strict;
use Getopt::Std;

# Globals.
my $GETCONF = "/bin/echo 'lines 1000\nlist' | /usr/sbin/config -e";
my $DMESG = "/var/run/dmesg.boot";
my %dmesgdev;

# Main.
my %opts;
getopts('d:f:s:t', \%opts) || usage();

# Use a different dmesg.
if (exists $opts{'d'}) {
    $DMESG = $opts{'d'};
}

# Switch.
if (exists $opts{'f'}) {
    # "Fastboot" option.
    parse_dmesg();
    parse_kernel($opts{'f'});
} elsif (exists $opts{'s'}) {
    # "Small kernel" option.
    parse_dmesg();
    parse_kernelconfig($opts{'s'});
} elsif (exists $opts{'t'}) {
    # "Tree" option.
    parse_dmesg();
    printdev("root");
} else {
    usage();
}

# End of main.

sub matchdev
{
    my $child = shift;
    my $parent = shift;

    # Literal '?' and '*' should match a digit.
    $child =~ s/[?*]$/\\d/;
    if ($parent eq "mii?") {
	# phy's can have any parent.
	$parent = ".+";
    } else {
	$parent =~ s/[?*]$/\\d/;
    }

    my $par;
    foreach $par (keys %dmesgdev) {
	next unless ($par =~ m/^$parent$/);
	# Parent matched if we get here.
	foreach (keys %{$dmesgdev{$par}}) {
	    next unless m/^$child$/;
	    # Child matched if we get here.
	    return 1;
	}
    }
    # Failed to find a match.
    return 0;
}

sub parse_dmesg
{
    open(DMESG, $DMESG) || die "cannot read dmesg: $!\n";

    while (<DMESG>) {
	chomp;
	s/\s+/ /g;
	s/^ //;
	if (m/^([a-z]{2,}[0-9]+) at ([a-z]{2,}[0-9]+|root)/) {
	    $dmesgdev{$2}{$1}++;
	}
    }
    close(DMESG) || die "cannot close dmesg: $!\n";
}

sub parse_kernel
{
    my $kernel = shift;

    -r $kernel ||
	die "cannot read kernel $kernel: $!\n";
    open(CONFIG, "$GETCONF $kernel |") ||
	die "cannot get kernel config: $!\n";

    LINE:
    while (<CONFIG>) {
	chomp;
	s/\s+/ /g;
	s/^ //;
	if (m/^(\d+) ([a-z0-9*]+) at ([a-z0-9*|]+)/) {
	    my ($num, $dev, $par) = ($1, $2, $3);
	    my @pars = split /\|/, $par;
	    foreach (@pars) {
	        next LINE if matchdev($dev, $_);
	    }
	    # Did not find a matching device.
	    printf("disable %-3d  %s at %.40s\n", $num, $dev, $par);
	}
    }
    close(CONFIG);
    print "quit\n";
}

sub parse_kernelconfig
{
    my $config = shift;

    open(CONFIG, $config) || die "cannot read config $config: $!\n";
	
    LINE:
    while (<CONFIG>) {
	chomp;
	my $orig = $_;
	s/\s+/ /g;
	s/^ //;

	# The following line is funky in i386/conf/GENERIC at the moment
	# (the space between pci and ?).
	s/^pciide\* at pci \?/pciide* at pci?/;

	if (m/^([a-z0-9?*]+) at ([a-z0-9?*]+)/) {
	    unless (matchdev($1, $2)) {
		# This device is not in the dmesg.
		print "#T $orig\n";
		next LINE;
	    }
	}
	# It's not a device, or the device is in the dmesg.
	print "$orig\n";
    }
}

sub printdev
{
    my $dev = shift;
    my $prefix = shift || "";

    print "$dev\n";

    # Handle any siblings of this device.
    if (exists $dmesgdev{$dev}) { 
	my @leafs = sort keys %{$dmesgdev{$dev}};
	my $last = pop @leafs;
	foreach (@leafs) {
	    print "$prefix |-";
	    printdev($_, "$prefix | ");
	}
	print "$prefix \\-";
	printdev($last, "$prefix   ");
    }
}

sub usage
{
    die <<USAGE
Usage: dmassage [-d dmesg] <-f kernel | -s kernelconfig | -t>
USAGE
}

__END__

=head1 NAME

B<dmassage> - dmesg parser

=head1 SYNOPSIS

=over

=item B<dmassage> [B<-d> I<dmesg>] B<-f> I<kernel>

=item B<dmassage> [B<-d> I<dmesg>] B<-s> I<kernelconfig>

=item B<dmassage> [B<-d> I<dmesg>] B<-t>

=cut

=back

=head1 DESCRIPTION

B<dmassage> parses your system's dmesg to learn which devices are
succesfully detected by the kernel.  This information can be used
for three purposes: to make the kernel boot faster, to help build a
smaller kernel or to show all the devices in a tree-like hierarchy.

=head1 OPTIONS

=over 4

=item B<-d> I<dmesg>

Read dmesg from this file instead of the default /var/run/dmesg.boot.
Use '-' to read the dmesg from standard input.

=item B<-f> I<kernel>

"Fastboot".  Disable all devices in the kernel that do not appear
in the dmesg.  This is done using the config(8) utility, so the
kernel does not have to be rebuild.  When the tweaked kernel boots,
the disabled devices do not have to be probed, resulting in a faster
boot.

=item B<-s> I<kernelconfig>

"Small kernel".  Comment out all devices in a kernel configuration
file (e.g. GENERIC) that do not appear in the dmesg.  The resulting
configuration file can be used to build a minimal kernel with full
functionality.

=item B<-t>

"Tree".  Show a tree-like hierarchy of all devices in the dmesg.
Devices on the same level are shown in alphabetic order.

=back

=head1 EXAMPLES

B<< dmassage -t >>

B<< dmassage -f /bsd | config -e -o /nbsd /bsd >>

B<< dmassage -s GENERIC >SMALLKERNEL >>

=head1 AUTHOR

Camiel Dobbelaar <cd@sentia.nl>

http://www.sentia.org/projects/dmassage

=head1 CAVEATS

dmassage does not fully support recent OpenBSD releases and
will often no longer directly create a working kernel configuration
file; in most cases you will need to make additional changes yourself.

Additionally, note that using a custom kernel is unsupported.
If reporting any OS bugs, be sure to verify that they still occur
with GENERIC or GENERIC.MP.

=cut
