/*-
 * Copyright (c) 2000 Michael Smith
 * Copyright (c) 2000 BSDi
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
 */

#include <sys/cdefs.h>
__FBSDID("$FreeBSD: releng/9.2/lib/libdevinfo/devinfo.c 160428 2006-07-17 09:33:24Z stefanf $");

/*
 * An interface to the FreeBSD kernel's bus/device information interface.
 *
 * This interface is implemented with the
 *
 * hw.bus
 * hw.bus.devices
 * hw.bus.rman
 * 
 * sysctls.  The interface is not meant for general user application
 * consumption.
 *
 * Device information is obtained by scanning a linear list of all devices
 * maintained by the kernel.  The actual device structure pointers are
 * handed out as opaque handles in order to allow reconstruction of the
 * logical toplogy in user space.
 *
 * Resource information is obtained by scanning the kernel's resource
 * managers and fetching their contents.  Ownership of resources is
 * tracked using the device's structure pointer again as a handle.
 *
 * In order to ensure coherency of the library's picture of the kernel,
 * a generation count is maintained by the kernel.  The initial generation
 * count is obtained (along with the interface version) from the hw.bus
 * sysctl, and must be passed in with every request.  If the generation
 * number supplied by the library does not match the kernel's current
 * generation number, the request is failed and the library must discard
 * the data it has received and rescan.
 *
 * The information obtained from the kernel is exported to consumers of
 * this library through a variety of interfaces.
 */

#include <sys/param.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "devinfo.h"
#include "devinfo_var.h"

static int	devinfo_init_devices(int generation);
static int	devinfo_init_resources(int generation);

TAILQ_HEAD(,devinfo_i_dev)	devinfo_dev;
TAILQ_HEAD(,devinfo_i_rman)	devinfo_rman;
TAILQ_HEAD(,devinfo_i_res)	devinfo_res;

static int	devinfo_initted = 0;
static int	devinfo_generation = 0;

#if 0
# define debug(...)	do { \
	fprintf(stderr, "%s:", __func__); \
	fprintf(stderr, __VA_ARGS__); \
	fprintf(stderr, "\n"); \
} while (0)
#else
# define debug(...)
#endif

/*
 * Initialise our local database with the contents of the kernel's
 * tables.
 */
int
devinfo_init(void)
{
	struct u_businfo	ubus;
	size_t		ub_size;
	int			error, retries;

	if (!devinfo_initted) {
		TAILQ_INIT(&devinfo_dev);
		TAILQ_INIT(&devinfo_rman);
		TAILQ_INIT(&devinfo_res);
	}

	/*
	 * Get the generation count and interface version, verify that we 
	 * are compatible with the kernel.
	 */
	for (retries = 0; retries < 10; retries++) {
		debug("get interface version");
		ub_size = sizeof(ubus);
		if (sysctlbyname("hw.bus.info", &ubus,
		    &ub_size, NULL, 0) != 0) {
			warn("sysctlbyname(\"hw.bus.info\", ...) failed");
			return(EINVAL);
		}
		if ((ub_size != sizeof(ubus)) ||
		    (ubus.ub_version != BUS_USER_VERSION)) {
			warn("kernel bus interface version mismatch");
			return(EINVAL);
		}
		debug("generation count is %d", ubus.ub_generation);

		/*
		 * Don't rescan if the generation count hasn't changed.
		 */
		if (ubus.ub_generation == devinfo_generation)
			return(0);

		/*
		 * Generation count changed, rescan
		 */
		devinfo_free();
		devinfo_initted = 0;
		devinfo_generation = 0;

		if ((error = devinfo_init_devices(ubus.ub_generation)) != 0) {
			devinfo_free();
			if (error == EINVAL)
				continue;
			break;
		}
		if ((error = devinfo_init_resources(ubus.ub_generation)) != 0) {
			devinfo_free();
			if (error == EINVAL)
				continue;
			break;
		}
		devinfo_initted = 1;
		devinfo_generation = ubus.ub_generation;
		return(0);
	}
	debug("scan failed after %d retries", retries);
	errno = error;
	return(1);
}

static int
devinfo_init_devices(int generation)
{
	struct u_device		udev;
	struct devinfo_i_dev	*dd;
	int			dev_idx;
	int			dev_ptr;
	int			name2oid[2];
	int			oid[CTL_MAXNAME + 12];
	size_t			oidlen, rlen;
	char			*name;
	int			error;

	/* 
	 * Find the OID for the rman interface node.
	 * This is just the usual evil, undocumented sysctl juju.
	 */
	name2oid[0] = 0;
	name2oid[1] = 3;
	oidlen = sizeof(oid);
	name = "hw.bus.devices";
	error = sysctl(name2oid, 2, oid, &oidlen, name, strlen(name));
	if (error < 0) {
		warnx("can't find hw.bus.devices sysctl node");
		return(ENOENT);
	}
	oidlen /= sizeof(int);
	if (oidlen > CTL_MAXNAME) {
		warnx("hw.bus.devices oid is too large");
		return(EINVAL);
	}
	oid[oidlen++] = generation;
	dev_ptr = oidlen++;

	/*
	 * Scan devices.
	 *
	 * Stop after a fairly insane number to avoid death in the case
	 * of kernel corruption.
	 */
	for (dev_idx = 0; dev_idx < 1000; dev_idx++) {

		/*
		 * Get the device information.
		 */
		oid[dev_ptr] = dev_idx;
		rlen = sizeof(udev);
		error = sysctl(oid, oidlen, &udev, &rlen, NULL, 0);
		if (error < 0) {
			if (errno == ENOENT)	/* end of list */
				break;
			if (errno != EINVAL)	/* gen count skip, restart */
				warn("sysctl hw.bus.devices.%d", dev_idx);
			return(errno);
		}
		if ((dd = malloc(sizeof(*dd))) == NULL)
			return(ENOMEM);
		dd->dd_dev.dd_handle = udev.dv_handle;
		dd->dd_dev.dd_parent = udev.dv_parent;
		snprintf(dd->dd_name, sizeof(dd->dd_name), "%s", udev.dv_name);
		dd->dd_dev.dd_name = &dd->dd_name[0];
		snprintf(dd->dd_desc, sizeof(dd->dd_desc), "%s", udev.dv_desc);
		dd->dd_dev.dd_desc = &dd->dd_desc[0];
		snprintf(dd->dd_drivername, sizeof(dd->dd_drivername), "%s",
		    udev.dv_drivername);
		dd->dd_dev.dd_drivername = &dd->dd_drivername[0];
		snprintf(dd->dd_pnpinfo, sizeof(dd->dd_pnpinfo), "%s",
		    udev.dv_pnpinfo);
		dd->dd_dev.dd_pnpinfo = &dd->dd_pnpinfo[0];
		snprintf(dd->dd_location, sizeof(dd->dd_location), "%s",
		    udev.dv_location);
		dd->dd_dev.dd_location = &dd->dd_location[0];
		dd->dd_dev.dd_devflags = udev.dv_devflags;
		dd->dd_dev.dd_flags = udev.dv_flags;
		dd->dd_dev.dd_state = udev.dv_state;
		TAILQ_INSERT_TAIL(&devinfo_dev, dd, dd_link);
	}
	debug("fetched %d devices", dev_idx);
	return(0);
}

static int
devinfo_init_resources(int generation)
{
	struct u_rman		urman;
	struct devinfo_i_rman	*dm;
	struct u_resource	ures;
	struct devinfo_i_res	*dr;
	int			rman_idx, res_idx;
	int			rman_ptr, res_ptr;
	int			name2oid[2];
	int			oid[CTL_MAXNAME + 12];
	size_t			oidlen, rlen;
	char			*name;
	int			error;

	/* 
	 * Find the OID for the rman interface node.
	 * This is just the usual evil, undocumented sysctl juju.
	 */
	name2oid[0] = 0;
	name2oid[1] = 3;
	oidlen = sizeof(oid);
	name = "hw.bus.rman";
	error = sysctl(name2oid, 2, oid, &oidlen, name, strlen(name));
	if (error < 0) {
		warnx("can't find hw.bus.rman sysctl node");
		return(ENOENT);
	}
	oidlen /= sizeof(int);
	if (oidlen > CTL_MAXNAME) {
		warnx("hw.bus.rman oid is too large");
		return(EINVAL);
	}
	oid[oidlen++] = generation;
	rman_ptr = oidlen++;
	res_ptr = oidlen++;

	/*
	 * Scan resource managers.
	 *
	 * Stop after a fairly insane number to avoid death in the case
	 * of kernel corruption.
	 */
	for (rman_idx = 0; rman_idx < 255; rman_idx++) {

		/*
		 * Get the resource manager information.
		 */
		oid[rman_ptr] = rman_idx;
		oid[res_ptr] = -1;
		rlen = sizeof(urman);
		error = sysctl(oid, oidlen, &urman, &rlen, NULL, 0);
		if (error < 0) {
			if (errno == ENOENT)	/* end of list */
				break;
			if (errno != EINVAL)	/* gen count skip, restart */
				warn("sysctl hw.bus.rman.%d", rman_idx);
			return(errno);
		}
		if ((dm = malloc(sizeof(*dm))) == NULL)
			return(ENOMEM);
		dm->dm_rman.dm_handle = urman.rm_handle;
		dm->dm_rman.dm_start = urman.rm_start;
		dm->dm_rman.dm_size = urman.rm_size;
		snprintf(dm->dm_desc, DEVINFO_STRLEN, "%s", urman.rm_descr);
		dm->dm_rman.dm_desc = &dm->dm_desc[0];
		TAILQ_INSERT_TAIL(&devinfo_rman, dm, dm_link);

		/*
		 * Scan resources on this resource manager.
		 *
		 * Stop after a fairly insane number to avoid death in the case
		 * of kernel corruption.
		 */
		for (res_idx = 0; res_idx < 1000; res_idx++) {
			/* 
			 * Get the resource information.
			 */
			oid[res_ptr] = res_idx;
			rlen = sizeof(ures);
			error = sysctl(oid, oidlen, &ures, &rlen, NULL, 0);
			if (error < 0) {
				if (errno == ENOENT)	/* end of list */
					break;
				if (errno != EINVAL)	/* gen count skip */
					warn("sysctl hw.bus.rman.%d.%d",
					    rman_idx, res_idx);
				return(errno);
			}
			if ((dr = malloc(sizeof(*dr))) == NULL)
				return(ENOMEM);
			dr->dr_res.dr_handle = ures.r_handle;
			dr->dr_res.dr_rman = ures.r_parent;
			dr->dr_res.dr_device = ures.r_device;
			dr->dr_res.dr_start = ures.r_start;
			dr->dr_res.dr_size = ures.r_size;
			TAILQ_INSERT_TAIL(&devinfo_res, dr, dr_link);
		}
		debug("fetched %d resources", res_idx);
	}
	debug("scanned %d resource managers", rman_idx);
	return(0);
}

/*
 * Free the list contents.
 */
void
devinfo_free(void)
{
	struct devinfo_i_dev	*dd;
	struct devinfo_i_rman	*dm;
	struct devinfo_i_res	*dr;

	while ((dd = TAILQ_FIRST(&devinfo_dev)) != NULL) {
		TAILQ_REMOVE(&devinfo_dev, dd, dd_link);
		free(dd);
	}
	while ((dm = TAILQ_FIRST(&devinfo_rman)) != NULL) {
		TAILQ_REMOVE(&devinfo_rman, dm, dm_link);
		free(dm);
	}
	while ((dr = TAILQ_FIRST(&devinfo_res)) != NULL) {
		TAILQ_REMOVE(&devinfo_res, dr, dr_link);
		free(dr);
	}
	devinfo_initted = 0;
	devinfo_generation = 0;
}

/*
 * Find a device by its handle.
 */
struct devinfo_dev *
devinfo_handle_to_device(devinfo_handle_t handle)
{
	struct devinfo_i_dev	*dd;

	/*
	 * Find the root device, whose parent is NULL
	 */
	if (handle == DEVINFO_ROOT_DEVICE) {
		TAILQ_FOREACH(dd, &devinfo_dev, dd_link)
		    if (dd->dd_dev.dd_parent == DEVINFO_ROOT_DEVICE)
			    return(&dd->dd_dev);
		return(NULL);
	}

	/*
	 * Scan for the device
	 */
	TAILQ_FOREACH(dd, &devinfo_dev, dd_link)
	    if (dd->dd_dev.dd_handle == handle)
		    return(&dd->dd_dev);
	return(NULL);
}

/*
 * Find a resource by its handle.
 */
struct devinfo_res *
devinfo_handle_to_resource(devinfo_handle_t handle)
{
	struct devinfo_i_res	*dr;

	TAILQ_FOREACH(dr, &devinfo_res, dr_link)
	    if (dr->dr_res.dr_handle == handle)
		    return(&dr->dr_res);
	return(NULL);
}

/*
 * Find a resource manager by its handle.
 */
struct devinfo_rman *
devinfo_handle_to_rman(devinfo_handle_t handle)
{
	struct devinfo_i_rman	*dm;

	TAILQ_FOREACH(dm, &devinfo_rman, dm_link)
	    if (dm->dm_rman.dm_handle == handle)
		    return(&dm->dm_rman);
	return(NULL);
}

/*
 * Iterate over the children of a device, calling (fn) on each.  If
 * (fn) returns nonzero, abort the scan and return.
 */
int
devinfo_foreach_device_child(struct devinfo_dev *parent, 
    int (* fn)(struct devinfo_dev *child, void *arg), 
    void *arg)
{
	struct devinfo_i_dev	*dd;
	int				error;

	TAILQ_FOREACH(dd, &devinfo_dev, dd_link)
	    if (dd->dd_dev.dd_parent == parent->dd_handle)
		    if ((error = fn(&dd->dd_dev, arg)) != 0)
			    return(error);
	return(0);
}

/*
 * Iterate over all the resources owned by a device, calling (fn) on each.
 * If (fn) returns nonzero, abort the scan and return.
 */
int
devinfo_foreach_device_resource(struct devinfo_dev *dev,
    int (* fn)(struct devinfo_dev *dev, struct devinfo_res *res, void *arg),
    void *arg)
{
	struct devinfo_i_res	*dr;
	int				error;

	TAILQ_FOREACH(dr, &devinfo_res, dr_link)
	    if (dr->dr_res.dr_device == dev->dd_handle)
		    if ((error = fn(dev, &dr->dr_res, arg)) != 0)
			    return(error);
	return(0);
}

/*
 * Iterate over all the resources owned by a resource manager, calling (fn)
 * on each.  If (fn) returns nonzero, abort the scan and return.
 */
extern int
devinfo_foreach_rman_resource(struct devinfo_rman *rman,
    int (* fn)(struct devinfo_res *res, void *arg),
    void *arg)
{
	struct devinfo_i_res	*dr;
	int				error;

	TAILQ_FOREACH(dr, &devinfo_res, dr_link)
	    if (dr->dr_res.dr_rman == rman->dm_handle)
		    if ((error = fn(&dr->dr_res, arg)) != 0)
			    return(error);
	return(0);
}

/*
 * Iterate over all the resource managers, calling (fn) on each.  If (fn)
 * returns nonzero, abort the scan and return.
 */
extern int
devinfo_foreach_rman(int (* fn)(struct devinfo_rman *rman, void *arg),
    void *arg)
{
    struct devinfo_i_rman	*dm;
    int				error;

    TAILQ_FOREACH(dm, &devinfo_rman, dm_link)
	if ((error = fn(&dm->dm_rman, arg)) != 0)
	    return(error);
    return(0);
}
