/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* or http://www.opensolaris.org/os/licensing.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright 2009 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <libdevinfo.h>
#include <libhotplug.h>
#include <libhotplug_impl.h>
#include <sys/sunddi.h>
#include <sys/ddi_hp.h>
#include "hotplugd_impl.h"
/*
* Define a list of hotplug nodes.
* (Only used within this module.)
*/
typedef struct {
hp_node_t head;
hp_node_t prev;
} hp_node_list_t;
/*
* Local functions.
*/
static int copy_devinfo(const char *, const char *, uint_t,
hp_node_t *);
static int copy_devices(hp_node_t, di_node_t, uint_t, hp_node_t *);
static int copy_hotplug(hp_node_t, di_node_t, const char *, uint_t,
hp_node_t *);
static char *base_path(const char *);
static int search_cb(di_node_t, void *);
static int check_search(di_node_t, uint_t);
static hp_node_t new_device_node(hp_node_t, di_node_t);
static hp_node_t new_hotplug_node(hp_node_t, di_hp_t);
static void node_list_add(hp_node_list_t *, hp_node_t);
/*
* getinfo()
*
* Build a hotplug information snapshot. The path, connection,
* and flags indicate what information should be included.
*/
int
getinfo(const char *path, const char *connection, uint_t flags, hp_node_t *retp)
{
hp_node_t root = NULL;
hp_node_t child;
char *basepath;
int rv;
if ((path == NULL) || (retp == NULL))
return (EINVAL);
dprintf("getinfo: path=%s, connection=%s, flags=0x%x\n", path,
(connection == NULL) ? "NULL" : connection, flags);
/* Allocate the base path */
if ((basepath = base_path(path)) == NULL)
return (ENOMEM);
/* Copy in device and hotplug nodes from libdevinfo */
if ((rv = copy_devinfo(basepath, connection, flags, &root)) != 0) {
hp_fini(root);
free(basepath);
return (rv);
}
/* Check if there were no connections */
if (root == NULL) {
dprintf("getinfo: no hotplug connections.\n");
free(basepath);
return (ENOENT);
}
/* Special case: exclude root nexus from snapshot */
if (strcmp(basepath, "/") == 0) {
child = root->hp_child;
if (root->hp_name != NULL)
free(root->hp_name);
free(root);
root = child;
for (child = root; child; child = child->hp_sibling)
child->hp_parent = NULL;
}
/* Store a pointer to the base path in each root node */
for (child = root; child != NULL; child = child->hp_sibling)
child->hp_basepath = basepath;
/* Copy in usage information from RCM */
if (flags & HPINFOUSAGE) {
if ((rv = copy_usage(root)) != 0) {
(void) hp_fini(root);
return (rv);
}
}
*retp = root;
return (0);
}
/*
* copy_devinfo()
*
* Copy information about device and hotplug nodes from libdevinfo.
*
* When path is set to "/", the results need to be limited only to
* branches that contain hotplug information. An initial search
* is performed to mark which branches contain hotplug nodes.
*/
static int
copy_devinfo(const char *path, const char *connection, uint_t flags,
hp_node_t *rootp)
{
hp_node_t hp_root = NULL;
di_node_t di_root;
int rv;
/* Get libdevinfo snapshot */
if ((di_root = di_init(path, DINFOSUBTREE | DINFOHP)) == DI_NODE_NIL)
return (errno);
/* Do initial search pass, if required */
if (strcmp(path, "/") == 0) {
flags |= HPINFOSEARCH;
(void) di_walk_node(di_root, DI_WALK_CLDFIRST, NULL, search_cb);
}
/*
* If a connection is specified, just copy immediate hotplug info.
* Else, copy the device tree normally.
*/
if (connection != NULL)
rv = copy_hotplug(NULL, di_root, connection, flags, &hp_root);
else
rv = copy_devices(NULL, di_root, flags, &hp_root);
/* Destroy devinfo snapshot */
di_fini(di_root);
*rootp = (rv == 0) ? hp_root : NULL;
return (rv);
}
/*
* copy_devices()
*
* Copy a full branch of device nodes. Used by copy_devinfo() and
* copy_hotplug().
*/
static int
copy_devices(hp_node_t parent, di_node_t dev, uint_t flags, hp_node_t *rootp)
{
hp_node_list_t children;
hp_node_t self, branch;
di_node_t child;
int rv = 0;
/* Initialize results */
*rootp = NULL;
/* Enforce search semantics */
if (check_search(dev, flags) == 0)
return (0);
/* Allocate new node for current device */
if ((self = new_device_node(parent, dev)) == NULL)
return (ENOMEM);
/*
* If the device has hotplug nodes, then use copy_hotplug()
* instead to build the branch associated with current device.
*/
if (di_hp_next(dev, DI_HP_NIL) != DI_HP_NIL) {
if ((rv = copy_hotplug(self, dev, NULL, flags,
&self->hp_child)) != 0) {
free(self);
return (rv);
}
*rootp = self;
return (0);
}
/*
* The device does not have hotplug nodes. Use normal
* approach of iterating through its child device nodes.
*/
(void) memset(&children, 0, sizeof (hp_node_list_t));
for (child = di_child_node(dev); child != DI_NODE_NIL;
child = di_sibling_node(child)) {
branch = NULL;
if ((rv = copy_devices(self, child, flags, &branch)) != 0) {
(void) hp_fini(children.head);
free(self);
return (rv);
}
if (branch != NULL)
node_list_add(&children, branch);
}
self->hp_child = children.head;
/* Done */
*rootp = self;
return (0);
}
/*
* copy_hotplug()
*
* Copy a full branch of hotplug nodes. Used by copy_devinfo()
* and copy_devices().
*
* If a connection is specified, the results are limited only
* to the branch associated with that specific connection.
*/
static int
copy_hotplug(hp_node_t parent, di_node_t dev, const char *connection,
uint_t flags, hp_node_t *retp)
{
hp_node_list_t connections, ports;
hp_node_t node, port_node;
di_node_t child_dev;
di_hp_t hp, port_hp;
uint_t child_flags;
int rv, physnum;
/* Stop implementing the HPINFOSEARCH flag */
child_flags = flags & ~(HPINFOSEARCH);
/* Clear lists of discovered ports and connections */
(void) memset(&ports, 0, sizeof (hp_node_list_t));
(void) memset(&connections, 0, sizeof (hp_node_list_t));
/*
* Scan virtual ports.
*
* If a connection is specified and it matches a virtual port,
* this will build the branch associated with that connection.
* Else, this will only build branches for virtual ports that
* are not associated with a physical connector.
*/
for (hp = DI_HP_NIL; (hp = di_hp_next(dev, hp)) != DI_HP_NIL; ) {
/* Ignore connectors */
if (di_hp_type(hp) != DDI_HP_CN_TYPE_VIRTUAL_PORT)
continue;
/*
* Ignore ports associated with connectors, unless
* a specific connection is being sought.
*/
if ((connection == NULL) && (di_hp_depends_on(hp) != -1))
continue;
/* If a connection is specified, ignore non-matching ports */
if ((connection != NULL) &&
(strcmp(di_hp_name(hp), connection) != 0))
continue;
/* Create a new port node */
if ((node = new_hotplug_node(parent, hp)) == NULL) {
rv = ENOMEM;
goto fail;
}
/* Add port node to connection list */
node_list_add(&connections, node);
/* Add branch of child devices to port node */
if ((child_dev = di_hp_child(hp)) != DI_NODE_NIL)
if ((rv = copy_devices(node, child_dev, child_flags,
&node->hp_child)) != 0)
goto fail;
}
/*
* Scan physical connectors.
*
* If a connection is specified, the results will be limited
* only to the branch associated with that connection.
*/
for (hp = DI_HP_NIL; (hp = di_hp_next(dev, hp)) != DI_HP_NIL; ) {
/* Ignore ports */
if (di_hp_type(hp) == DDI_HP_CN_TYPE_VIRTUAL_PORT)
continue;
/* If a connection is specified, ignore non-matching ports */
if ((connection != NULL) &&
(strcmp(di_hp_name(hp), connection) != 0))
continue;
/* Create a new connector node */
if ((node = new_hotplug_node(parent, hp)) == NULL) {
rv = ENOMEM;
goto fail;
}
/* Add connector node to connection list */
node_list_add(&connections, node);
/* Add branches of associated port nodes */
physnum = di_hp_connection(hp);
port_hp = DI_HP_NIL;
while ((port_hp = di_hp_next(dev, port_hp)) != DI_HP_NIL) {
/* Ignore irrelevant connections */
if (di_hp_depends_on(port_hp) != physnum)
continue;
/* Add new port node to port list */
if ((port_node = new_hotplug_node(node,
port_hp)) == NULL) {
rv = ENOMEM;
goto fail;
}
node_list_add(&ports, port_node);
/* Add branch of child devices */
if ((child_dev = di_hp_child(port_hp)) != DI_NODE_NIL) {
if ((rv = copy_devices(port_node, child_dev,
child_flags, &port_node->hp_child)) != 0)
goto fail;
}
}
node->hp_child = ports.head;
(void) memset(&ports, 0, sizeof (hp_node_list_t));
}
if (connections.head == NULL)
return (ENXIO);
*retp = connections.head;
return (0);
fail:
(void) hp_fini(ports.head);
(void) hp_fini(connections.head);
return (rv);
}
/*
* base_path()
*
* Normalize the base path of a hotplug information snapshot.
* The caller must free the string that is allocated.
*/
static char *
base_path(const char *path)
{
char *base_path;
size_t devices_len;
devices_len = strlen(S_DEVICES);
if (strncmp(path, S_DEVICES, devices_len) == 0)
base_path = strdup(&path[devices_len]);
else
base_path = strdup(path);
return (base_path);
}
/*
* search_cb()
*
* Callback function used by di_walk_node() to search for branches
* of the libdevinfo snapshot that contain hotplug nodes.
*/
/*ARGSUSED*/
static int
search_cb(di_node_t node, void *arg)
{
di_node_t parent;
uint_t flags;
(void) di_node_private_set(node, (void *)(uintptr_t)0);
if (di_hp_next(node, DI_HP_NIL) == DI_HP_NIL)
return (DI_WALK_CONTINUE);
for (parent = node; parent != DI_NODE_NIL;
parent = di_parent_node(parent)) {
flags = (uint_t)(uintptr_t)di_node_private_get(parent);
flags |= HPINFOSEARCH;
(void) di_node_private_set(parent, (void *)(uintptr_t)flags);
}
return (DI_WALK_CONTINUE);
}
/*
* check_search()
*
* Check if a device node was marked by an initial search pass.
*/
static int
check_search(di_node_t dev, uint_t flags)
{
uint_t dev_flags;
if (flags & HPINFOSEARCH) {
dev_flags = (uint_t)(uintptr_t)di_node_private_get(dev);
if ((dev_flags & HPINFOSEARCH) == 0)
return (0);
}
return (1);
}
/*
* node_list_add()
*
* Utility function to append one node to a list of hotplug nodes.
*/
static void
node_list_add(hp_node_list_t *listp, hp_node_t node)
{
if (listp->prev != NULL)
listp->prev->hp_sibling = node;
else
listp->head = node;
listp->prev = node;
}
/*
* new_device_node()
*
* Build a new hotplug node based on a specified devinfo node.
*/
static hp_node_t
new_device_node(hp_node_t parent, di_node_t dev)
{
hp_node_t node;
char *node_name, *bus_addr;
char name[MAXPATHLEN];
node = (hp_node_t)calloc(1, sizeof (struct hp_node));
if (node != NULL) {
node->hp_parent = parent;
node->hp_type = HP_NODE_DEVICE;
node_name = di_node_name(dev);
bus_addr = di_bus_addr(dev);
if (bus_addr && (strlen(bus_addr) > 0)) {
if (snprintf(name, sizeof (name), "%s@%s", node_name,
bus_addr) >= sizeof (name)) {
log_err("Path too long for device node.\n");
free(node);
return (NULL);
}
node->hp_name = strdup(name);
} else
node->hp_name = strdup(node_name);
}
return (node);
}
/*
* new_hotplug_node()
*
* Build a new hotplug node based on a specified devinfo hotplug node.
*/
static hp_node_t
new_hotplug_node(hp_node_t parent, di_hp_t hp)
{
hp_node_t node;
char *s;
node = (hp_node_t)calloc(1, sizeof (struct hp_node));
if (node != NULL) {
node->hp_parent = parent;
node->hp_state = di_hp_state(hp);
node->hp_last_change = di_hp_last_change(hp);
if ((s = di_hp_name(hp)) != NULL)
node->hp_name = strdup(s);
if ((s = di_hp_description(hp)) != NULL)
node->hp_description = strdup(s);
if (di_hp_type(hp) == DDI_HP_CN_TYPE_VIRTUAL_PORT)
node->hp_type = HP_NODE_PORT;
else
node->hp_type = HP_NODE_CONNECTOR;
}
return (node);
}