* Re: [PATCH] libata: Add libata transport class.
2010-05-25 19:31 ` Gwendal Grignou
@ 2010-06-26 3:02 ` Grant Grundler
2010-08-04 4:19 ` Jeff Garzik
0 siblings, 1 reply; 4+ messages in thread
From: Grant Grundler @ 2010-06-26 3:02 UTC (permalink / raw)
To: Gwendal Grignou; +Cc: jgarzik, linux-ide
On Tue, May 25, 2010 at 12:31 PM, Gwendal Grignou <gwendal@google.com> wrote:
> This is a scheleton for libata transport class.
Ping?
Jeff,
I thought you expressed an interest in this previously?
I don't expect this to go into 2.6.34 but hoped it would make it into 2.6.35.
thanks,
grant
> All information is read only, exporting information from libata:
> - ata_port class: one per ATA port
> - ata_link class: one per ATA port or 15 for SATA Port Multiplier
> - ata_device class: up to 2 for PATA link, usually one for SATA.
>
> Signed-off-by: Gwendal Grignou <gwendal@google.com>
> Reviewed-by: Grant Grundler <grundler@google.com>
> ---
> Documentation/ABI/testing/sysfs-ata | 99 +++++
> drivers/ata/Makefile | 2 +-
> drivers/ata/libata-core.c | 43 ++-
> drivers/ata/libata-eh.c | 35 +-
> drivers/ata/libata-pmp.c | 18 +-
> drivers/ata/libata-scsi.c | 21 +-
> drivers/ata/libata-transport.c | 773 +++++++++++++++++++++++++++++++++++
> drivers/ata/libata-transport.h | 18 +
> drivers/ata/libata.h | 7 +
> include/linux/libata.h | 5 +
> 10 files changed, 982 insertions(+), 39 deletions(-)
> create mode 100644 Documentation/ABI/testing/sysfs-ata
> create mode 100644 drivers/ata/libata-transport.c
> create mode 100644 drivers/ata/libata-transport.h
>
> diff --git a/Documentation/ABI/testing/sysfs-ata b/Documentation/ABI/testing/sysfs-ata
> new file mode 100644
> index 0000000..0a93215
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-ata
> @@ -0,0 +1,99 @@
> +What: /sys/class/ata_...
> +Date: August 2008
> +Contact: Gwendal Grignou<gwendal@google.com>
> +Description:
> +
> +Provide a place in sysfs for storing the ATA topology of the system. This allows
> +retrieving various information about ATA objects.
> +
> +Files under /sys/class/ata_port
> +-------------------------------
> +
> + For each port, a directory ataX is created where X is the ata_port_id of
> + the port. The device parent is the ata host device.
> +
> +idle_irq (read)
> +
> + Number of IRQ received by the port while idle [some ata HBA only].
> +
> +nr_pmp_links (read)
> +
> + If a SATA Port Multiplier (PM) is connected, number of link behind it.
> +
> +Files under /sys/class/ata_link
> +-------------------------------
> +
> + Behind each port, there is a ata_link. If there is a SATA PM in the
> + topology, 15 ata_link objects are created.
> +
> + If a link is behind a port, the directory name is linkX, where X is
> + ata_port_id of the port.
> + If a link is behind a PM, its name is linkX.Y where X is ata_port_id
> + of the parent port and Y the PM port.
> +
> +hw_sata_spd_limit
> +
> + Maximum speed supported by the connected SATA device.
> +
> +sata_spd_limit
> +
> + Maximum speed imposed by libata.
> +
> +sata_spd
> +
> + Current speed of the link [1.5, 3Gps,...].
> +
> +Files under /sys/class/ata_device
> +---------------------------------
> +
> + Behind each link, up to two ata device are created.
> + The name of the directory is devX[.Y].Z where:
> + - X is ata_port_id of the port where the device is connected,
> + - Y the port of the PM if any, and
> + - Z the device id: for PATA, there is usually 2 devices [0,1],
> + only 1 for SATA.
> +
> +class
> + Device class. Can be "ata" for disk, "atapi" for packet device,
> + "pmp" for PM, or "none" if no device was found behind the link.
> +
> +dma_mode
> +
> + Transfer modes supported by the device when in DMA mode.
> + Mostly used by PATA device.
> +
> +pio_mode
> +
> + Transfer modes supported by the device when in PIO mode.
> + Mostly used by PATA device.
> +
> +xfer_mode
> +
> + Current transfer mode.
> +
> +id
> +
> + Cached result of IDENTIFY command, as described in ATA8 7.16 and 7.17.
> + Only valid if the device is not a PM.
> +
> +gscr
> +
> + Cached result of the dump of PM GSCR register.
> + Valid registers are:
> + 0: SATA_PMP_GSCR_PROD_ID,
> + 1: SATA_PMP_GSCR_REV,
> + 2: SATA_PMP_GSCR_PORT_INFO,
> + 32: SATA_PMP_GSCR_ERROR,
> + 33: SATA_PMP_GSCR_ERROR_EN,
> + 64: SATA_PMP_GSCR_FEAT,
> + 96: SATA_PMP_GSCR_FEAT_EN,
> + 130: SATA_PMP_GSCR_SII_GPIO
> + Only valid if the device is a PM.
> +
> +spdn_cnt
> +
> + Number of time libata decided to lower the speed of link due to errors.
> +
> +ering
> +
> + Formatted output of the error ring of the device.
> diff --git a/drivers/ata/Makefile b/drivers/ata/Makefile
> index fc936d4..cd00fc7 100644
> --- a/drivers/ata/Makefile
> +++ b/drivers/ata/Makefile
> @@ -86,7 +86,7 @@ obj-$(CONFIG_ATA_GENERIC) += ata_generic.o
> # Should be last libata driver
> obj-$(CONFIG_PATA_LEGACY) += pata_legacy.o
>
> -libata-objs := libata-core.o libata-scsi.o libata-eh.o
> +libata-objs := libata-core.o libata-scsi.o libata-eh.o libata-transport.o
> libata-$(CONFIG_ATA_SFF) += libata-sff.o
> libata-$(CONFIG_SATA_PMP) += libata-pmp.o
> libata-$(CONFIG_ATA_ACPI) += libata-acpi.o
> diff --git a/drivers/ata/libata-core.c b/drivers/ata/libata-core.c
> index 49cffb6..fd6dc46 100644
> --- a/drivers/ata/libata-core.c
> +++ b/drivers/ata/libata-core.c
> @@ -67,7 +67,7 @@
> #include <linux/cdrom.h>
>
> #include "libata.h"
> -
> +#include "libata-transport.h"
>
> /* debounce timing parameters in msecs { interval, duration, timeout } */
> const unsigned long sata_deb_timing_normal[] = { 5, 100, 2000 };
> @@ -1015,7 +1015,7 @@ const char *ata_mode_string(unsigned long xfer_mask)
> return "<n/a>";
> }
>
> -static const char *sata_spd_string(unsigned int spd)
> +const char *sata_spd_string(unsigned int spd)
> {
> static const char * const spd_str[] = {
> "1.5 Gbps",
> @@ -5632,7 +5632,8 @@ void ata_link_init(struct ata_port *ap, struct ata_link *link, int pmp)
> int i;
>
> /* clear everything except for devices */
> - memset(link, 0, offsetof(struct ata_link, device[0]));
> + memset((void *)link + ATA_LINK_CLEAR_BEGIN, 0,
> + ATA_LINK_CLEAR_END - ATA_LINK_CLEAR_BEGIN);
>
> link->ap = ap;
> link->pmp = pmp;
> @@ -5706,7 +5707,7 @@ struct ata_port *ata_port_alloc(struct ata_host *host)
> ap = kzalloc(sizeof(*ap), GFP_KERNEL);
> if (!ap)
> return NULL;
> -
> +
> ap->pflags |= ATA_PFLAG_INITIALIZING;
> ap->lock = &host->lock;
> ap->flags = ATA_FLAG_DISABLED;
> @@ -6215,9 +6216,18 @@ int ata_host_register(struct ata_host *host, struct scsi_host_template *sht)
> for (i = 0; i < host->n_ports; i++)
> host->ports[i]->print_id = ata_print_id++;
>
> +
> + /* Create associated sysfs transport objects */
> + for (i = 0; i < host->n_ports; i++) {
> + rc = ata_tport_add(host->dev,host->ports[i]);
> + if (rc) {
> + goto err_tadd;
> + }
> + }
> +
> rc = ata_scsi_add_hosts(host, sht);
> if (rc)
> - return rc;
> + goto err_tadd;
>
> /* associate with ACPI nodes */
> ata_acpi_associate(host);
> @@ -6258,6 +6268,13 @@ int ata_host_register(struct ata_host *host, struct scsi_host_template *sht)
> }
>
> return 0;
> +
> + err_tadd:
> + while (--i >= 0) {
> + ata_tport_delete(host->ports[i]);
> + }
> + return rc;
> +
> }
>
> /**
> @@ -6348,6 +6365,13 @@ static void ata_port_detach(struct ata_port *ap)
> cancel_rearming_delayed_work(&ap->hotplug_task);
>
> skip_eh:
> + if (ap->pmp_link) {
> + int i;
> + for (i = 0; i < SATA_PMP_MAX_PORTS; i++)
> + ata_tlink_delete(&ap->pmp_link[i]);
> + }
> + ata_tport_delete(ap);
> +
> /* remove the associated SCSI host */
> scsi_remove_host(ap->scsi_host);
> }
> @@ -6680,6 +6704,13 @@ static int __init ata_init(void)
> if (!ata_aux_wq)
> goto free_wq;
>
> + libata_transport_init();
> + ata_scsi_transport_template = ata_attach_transport();
> + if (ata_scsi_transport_template == NULL) {
> + destroy_workqueue(ata_wq);
> + return -ENOMEM;
> + }
> +
> printk(KERN_DEBUG "libata version " DRV_VERSION " loaded.\n");
> return 0;
>
> @@ -6692,6 +6723,8 @@ free_force_tbl:
>
> static void __exit ata_exit(void)
> {
> + ata_release_transport(ata_scsi_transport_template);
> + libata_transport_exit();
> kfree(ata_force_tbl);
> destroy_workqueue(ata_wq);
> destroy_workqueue(ata_aux_wq);
> diff --git a/drivers/ata/libata-eh.c b/drivers/ata/libata-eh.c
> index 228740f..885005f 100644
> --- a/drivers/ata/libata-eh.c
> +++ b/drivers/ata/libata-eh.c
> @@ -57,6 +57,7 @@ enum {
> /* error flags */
> ATA_EFLAG_IS_IO = (1 << 0),
> ATA_EFLAG_DUBIOUS_XFER = (1 << 1),
> + ATA_EFLAG_OLD_ER = (1 << 31),
>
> /* error categories */
> ATA_ECAT_NONE = 0,
> @@ -396,14 +397,9 @@ static struct ata_ering_entry *ata_ering_top(struct ata_ering *ering)
> return NULL;
> }
>
> -static void ata_ering_clear(struct ata_ering *ering)
> -{
> - memset(ering, 0, sizeof(*ering));
> -}
> -
> -static int ata_ering_map(struct ata_ering *ering,
> - int (*map_fn)(struct ata_ering_entry *, void *),
> - void *arg)
> +int ata_ering_map(struct ata_ering *ering,
> + int (*map_fn)(struct ata_ering_entry *, void *),
> + void *arg)
> {
> int idx, rc = 0;
> struct ata_ering_entry *ent;
> @@ -422,6 +418,17 @@ static int ata_ering_map(struct ata_ering *ering,
> return rc;
> }
>
> +int ata_ering_clear_cb(struct ata_ering_entry *ent, void *void_arg)
> +{
> + ent->eflags |= ATA_EFLAG_OLD_ER;
> + return 0;
> +}
> +
> +static void ata_ering_clear(struct ata_ering *ering)
> +{
> + ata_ering_map(ering, ata_ering_clear_cb, NULL);
> +}
> +
> static unsigned int ata_eh_dev_action(struct ata_device *dev)
> {
> struct ata_eh_context *ehc = &dev->link->eh_context;
> @@ -572,19 +579,19 @@ void ata_scsi_error(struct Scsi_Host *host)
> int nr_timedout = 0;
>
> spin_lock_irqsave(ap->lock, flags);
> -
> +
> /* This must occur under the ap->lock as we don't want
> a polled recovery to race the real interrupt handler
> -
> +
> The lost_interrupt handler checks for any completed but
> non-notified command and completes much like an IRQ handler.
> -
> +
> We then fall into the error recovery code which will treat
> this as if normal completion won the race */
>
> if (ap->ops->lost_interrupt)
> ap->ops->lost_interrupt(ap);
> -
> +
> list_for_each_entry_safe(scmd, tmp, &host->eh_cmd_q, eh_entry) {
> struct ata_queued_cmd *qc;
>
> @@ -628,7 +635,7 @@ void ata_scsi_error(struct Scsi_Host *host)
> ap->eh_tries = ATA_EH_MAX_TRIES;
> } else
> spin_unlock_wait(ap->lock);
> -
> +
> /* If we timed raced normal completion and there is nothing to
> recover nr_timedout == 0 why exactly are we doing error recovery ? */
>
> @@ -1755,7 +1762,7 @@ static int speed_down_verdict_cb(struct ata_ering_entry *ent, void *void_arg)
> struct speed_down_verdict_arg *arg = void_arg;
> int cat;
>
> - if (ent->timestamp < arg->since)
> + if ((ent->eflags & ATA_EFLAG_OLD_ER) || (ent->timestamp < arg->since))
> return -1;
>
> cat = ata_eh_categorize_error(ent->eflags, ent->err_mask,
> diff --git a/drivers/ata/libata-pmp.c b/drivers/ata/libata-pmp.c
> index 00305f4..96eba24 100644
> --- a/drivers/ata/libata-pmp.c
> +++ b/drivers/ata/libata-pmp.c
> @@ -11,6 +11,7 @@
> #include <linux/libata.h>
> #include <linux/slab.h>
> #include "libata.h"
> +#include "libata-transport.h"
>
> const struct ata_port_operations sata_pmp_port_ops = {
> .inherits = &sata_port_ops,
> @@ -286,10 +287,10 @@ static int sata_pmp_configure(struct ata_device *dev, int print_info)
> return rc;
> }
>
> -static int sata_pmp_init_links(struct ata_port *ap, int nr_ports)
> +static int sata_pmp_init_links (struct ata_port *ap, int nr_ports)
> {
> struct ata_link *pmp_link = ap->pmp_link;
> - int i;
> + int i, err;
>
> if (!pmp_link) {
> pmp_link = kzalloc(sizeof(pmp_link[0]) * SATA_PMP_MAX_PORTS,
> @@ -301,6 +302,13 @@ static int sata_pmp_init_links(struct ata_port *ap, int nr_ports)
> ata_link_init(ap, &pmp_link[i], i);
>
> ap->pmp_link = pmp_link;
> +
> + for (i = 0; i < SATA_PMP_MAX_PORTS; i++) {
> + err = ata_tlink_add(&pmp_link[i]);
> + if (err) {
> + goto err_tlink;
> + }
> + }
> }
>
> for (i = 0; i < nr_ports; i++) {
> @@ -313,6 +321,12 @@ static int sata_pmp_init_links(struct ata_port *ap, int nr_ports)
> }
>
> return 0;
> + err_tlink:
> + while (--i >= 0)
> + ata_tlink_delete(&pmp_link[i]);
> + kfree(pmp_link);
> + ap->pmp_link = NULL;
> + return err;
> }
>
> static void sata_pmp_quirks(struct ata_port *ap)
> diff --git a/drivers/ata/libata-scsi.c b/drivers/ata/libata-scsi.c
> index 0088cde..36d268b 100644
> --- a/drivers/ata/libata-scsi.c
> +++ b/drivers/ata/libata-scsi.c
> @@ -51,6 +51,7 @@
> #include <asm/unaligned.h>
>
> #include "libata.h"
> +#include "libata-transport.h"
>
> #define SECTOR_SIZE 512
> #define ATA_SCSI_RBUF_SIZE 4096
> @@ -64,9 +65,6 @@ static struct ata_device *__ata_scsi_find_dev(struct ata_port *ap,
> const struct scsi_device *scsidev);
> static struct ata_device *ata_scsi_find_dev(struct ata_port *ap,
> const struct scsi_device *scsidev);
> -static int ata_scsi_user_scan(struct Scsi_Host *shost, unsigned int channel,
> - unsigned int id, unsigned int lun);
> -
>
> #define RW_RECOVERY_MPAGE 0x1
> #define RW_RECOVERY_MPAGE_LEN 12
> @@ -106,17 +104,6 @@ static const u8 def_control_mpage[CONTROL_MPAGE_LEN] = {
> 0, 30 /* extended self test time, see 05-359r1 */
> };
>
> -/*
> - * libata transport template. libata doesn't do real transport stuff.
> - * It just needs the eh_timed_out hook.
> - */
> -static struct scsi_transport_template ata_scsi_transport_template = {
> - .eh_strategy_handler = ata_scsi_error,
> - .eh_timed_out = ata_scsi_timed_out,
> - .user_scan = ata_scsi_user_scan,
> -};
> -
> -
> static const struct {
> enum link_pm value;
> const char *name;
> @@ -3305,7 +3292,7 @@ int ata_scsi_add_hosts(struct ata_host *host, struct scsi_host_template *sht)
> *(struct ata_port **)&shost->hostdata[0] = ap;
> ap->scsi_host = shost;
>
> - shost->transportt = &ata_scsi_transport_template;
> + shost->transportt = ata_scsi_transport_template;
> shost->unique_id = ap->print_id;
> shost->max_id = 16;
> shost->max_lun = 1;
> @@ -3588,8 +3575,8 @@ void ata_scsi_hotplug(struct work_struct *work)
> * RETURNS:
> * Zero.
> */
> -static int ata_scsi_user_scan(struct Scsi_Host *shost, unsigned int channel,
> - unsigned int id, unsigned int lun)
> +int ata_scsi_user_scan(struct Scsi_Host *shost, unsigned int channel,
> + unsigned int id, unsigned int lun)
> {
> struct ata_port *ap = ata_shost_to_port(shost);
> unsigned long flags;
> diff --git a/drivers/ata/libata-transport.c b/drivers/ata/libata-transport.c
> new file mode 100644
> index 0000000..a80860f
> --- /dev/null
> +++ b/drivers/ata/libata-transport.c
> @@ -0,0 +1,773 @@
> +/*
> + * Copyright 2008 ioogle, Inc. All rights reserved.
> + * Released under GPL v2.
> + *
> + * Libata transport class.
> + *
> + * The ATA transport class contains common code to deal with ATA HBAs,
> + * an approximated representation of ATA topologies in the driver model,
> + * and various sysfs attributes to expose these topologies and management
> + * interfaces to user-space.
> + *
> + * There are 3 objects defined in in this class:
> + * - ata_port
> + * - ata_link
> + * - ata_device
> + * Each port has a link object. Each link can have up to two devices for PATA
> + * and generally one for SATA.
> + * If there is SATA port multiplier [PMP], 15 additional ata_link object are
> + * created.
> + *
> + * These objects are created when the ata host is initialized and when a PMP is
> + * found. They are removed only when the HBA is removed, cleaned before the
> + * error handler runs.
> + */
> +
> +
> +#include <linux/kernel.h>
> +#include <linux/blkdev.h>
> +#include <linux/spinlock.h>
> +#include <scsi/scsi_transport.h>
> +#include <linux/libata.h>
> +#include <linux/hdreg.h>
> +#include <linux/uaccess.h>
> +
> +#include "libata.h"
> +#include "libata-transport.h"
> +
> +#define ATA_PORT_ATTRS 2
> +#define ATA_LINK_ATTRS 3
> +#define ATA_DEV_ATTRS 9
> +
> +struct scsi_transport_template;
> +struct scsi_transport_template *ata_scsi_transport_template;
> +
> +struct ata_internal {
> + struct scsi_transport_template t;
> +
> + struct device_attribute private_port_attrs[ATA_PORT_ATTRS];
> + struct device_attribute private_link_attrs[ATA_LINK_ATTRS];
> + struct device_attribute private_dev_attrs[ATA_DEV_ATTRS];
> +
> + struct transport_container link_attr_cont;
> + struct transport_container dev_attr_cont;
> +
> + /*
> + * The array of null terminated pointers to attributes
> + * needed by scsi_sysfs.c
> + */
> + struct device_attribute *link_attrs[ATA_LINK_ATTRS + 1];
> + struct device_attribute *port_attrs[ATA_PORT_ATTRS + 1];
> + struct device_attribute *dev_attrs[ATA_DEV_ATTRS + 1];
> +};
> +#define to_ata_internal(tmpl) container_of(tmpl, struct ata_internal, t)
> +
> +
> +#define tdev_to_device(d) \
> + container_of((d), struct ata_device, tdev)
> +#define transport_class_to_dev(dev) \
> + tdev_to_device((dev)->parent)
> +
> +#define tdev_to_link(d) \
> + container_of((d), struct ata_link, tdev)
> +#define transport_class_to_link(dev) \
> + tdev_to_link((dev)->parent)
> +
> +#define tdev_to_port(d) \
> + container_of((d), struct ata_port, tdev)
> +#define transport_class_to_port(dev) \
> + tdev_to_port((dev)->parent)
> +
> +
> +/* Device objects are always created whit link objects */
> +static int ata_tdev_add(struct ata_device *dev);
> +static void ata_tdev_delete(struct ata_device *dev);
> +
> +
> +/*
> + * Hack to allow attributes of the same name in different objects.
> + */
> +#define ATA_DEVICE_ATTR(_prefix,_name,_mode,_show,_store) \
> + struct device_attribute device_attr_##_prefix##_##_name = \
> + __ATTR(_name,_mode,_show,_store)
> +
> +#define ata_bitfield_name_match(title, table) \
> +static ssize_t \
> +get_ata_##title##_names(u32 table_key, char *buf) \
> +{ \
> + char *prefix = ""; \
> + ssize_t len = 0; \
> + int i; \
> + \
> + for (i = 0; i < ARRAY_SIZE(table); i++) { \
> + if (table[i].value & table_key) { \
> + len += sprintf(buf + len, "%s%s", \
> + prefix, table[i].name); \
> + prefix = ", "; \
> + } \
> + } \
> + len += sprintf(buf + len, "\n"); \
> + return len; \
> +}
> +
> +#define ata_bitfield_name_search(title, table) \
> +static ssize_t \
> +get_ata_##title##_names(u32 table_key, char *buf) \
> +{ \
> + ssize_t len = 0; \
> + int i; \
> + \
> + for (i = 0; i < ARRAY_SIZE(table); i++) { \
> + if (table[i].value == table_key) { \
> + len += sprintf(buf + len, "%s", \
> + table[i].name); \
> + break; \
> + } \
> + } \
> + len += sprintf(buf + len, "\n"); \
> + return len; \
> +}
> +
> +static struct {
> + u32 value;
> + char *name;
> +} ata_class_names[] = {
> + { ATA_DEV_UNKNOWN, "unknown" },
> + { ATA_DEV_ATA, "ata" },
> + { ATA_DEV_ATA_UNSUP, "ata" },
> + { ATA_DEV_ATAPI, "atapi" },
> + { ATA_DEV_ATAPI_UNSUP, "atapi" },
> + { ATA_DEV_PMP, "pmp" },
> + { ATA_DEV_PMP_UNSUP, "pmp" },
> + { ATA_DEV_SEMB, "semb" },
> + { ATA_DEV_SEMB_UNSUP, "semb" },
> + { ATA_DEV_NONE, "none" }
> +};
> +ata_bitfield_name_search(class, ata_class_names)
> +
> +
> +static struct {
> + u32 value;
> + char *name;
> +} ata_err_names[] = {
> + { AC_ERR_DEV, "DeviceError" },
> + { AC_ERR_HSM, "HostStateMachineError" },
> + { AC_ERR_TIMEOUT, "Timeout" },
> + { AC_ERR_MEDIA, "MediaError" },
> + { AC_ERR_ATA_BUS, "BusError" },
> + { AC_ERR_HOST_BUS, "HostBusError" },
> + { AC_ERR_SYSTEM, "SystemError" },
> + { AC_ERR_INVALID, "InvalidArg" },
> + { AC_ERR_OTHER, "Unknown" },
> + { AC_ERR_NODEV_HINT, "NoDeviceHint" },
> + { AC_ERR_NCQ, "NCQError" }
> +};
> +ata_bitfield_name_match(err, ata_err_names)
> +
> +static struct {
> + u32 value;
> + char *name;
> +} ata_xfer_names[] = {
> + { XFER_UDMA_7, "XFER_UDMA_7" },
> + { XFER_UDMA_6, "XFER_UDMA_6" },
> + { XFER_UDMA_5, "XFER_UDMA_5" },
> + { XFER_UDMA_4, "XFER_UDMA_4" },
> + { XFER_UDMA_3, "XFER_UDMA_3" },
> + { XFER_UDMA_2, "XFER_UDMA_2" },
> + { XFER_UDMA_1, "XFER_UDMA_1" },
> + { XFER_UDMA_0, "XFER_UDMA_0" },
> + { XFER_MW_DMA_4, "XFER_MW_DMA_4" },
> + { XFER_MW_DMA_3, "XFER_MW_DMA_3" },
> + { XFER_MW_DMA_2, "XFER_MW_DMA_2" },
> + { XFER_MW_DMA_1, "XFER_MW_DMA_1" },
> + { XFER_MW_DMA_0, "XFER_MW_DMA_0" },
> + { XFER_SW_DMA_2, "XFER_SW_DMA_2" },
> + { XFER_SW_DMA_1, "XFER_SW_DMA_1" },
> + { XFER_SW_DMA_0, "XFER_SW_DMA_0" },
> + { XFER_PIO_6, "XFER_PIO_6" },
> + { XFER_PIO_5, "XFER_PIO_5" },
> + { XFER_PIO_4, "XFER_PIO_4" },
> + { XFER_PIO_3, "XFER_PIO_3" },
> + { XFER_PIO_2, "XFER_PIO_2" },
> + { XFER_PIO_1, "XFER_PIO_1" },
> + { XFER_PIO_0, "XFER_PIO_0" },
> + { XFER_PIO_SLOW, "XFER_PIO_SLOW" }
> +};
> +ata_bitfield_name_match(xfer,ata_xfer_names)
> +
> +/*
> + * ATA Port attributes
> + */
> +#define ata_port_show_simple(field, name, format_string, cast) \
> +static ssize_t \
> +show_ata_port_##name(struct device *dev, \
> + struct device_attribute *attr, char *buf) \
> +{ \
> + struct ata_port *ap = transport_class_to_port(dev); \
> + \
> + return snprintf(buf, 20, format_string, cast ap->field); \
> +}
> +
> +#define ata_port_simple_attr(field, name, format_string, type) \
> + ata_port_show_simple(field, name, format_string, (type)) \
> +static DEVICE_ATTR(name, S_IRUGO, show_ata_port_##name, NULL)
> +
> +ata_port_simple_attr(nr_pmp_links, nr_pmp_links, "%d\n", int);
> +ata_port_simple_attr(stats.idle_irq, idle_irq, "%ld\n", unsigned long);
> +
> +static DECLARE_TRANSPORT_CLASS(ata_port_class,
> + "ata_port", NULL, NULL, NULL);
> +
> +static void ata_tport_release(struct device *dev)
> +{
> + put_device(dev->parent);
> +}
> +
> +/**
> + * ata_is_port -- check if a struct device represents a ATA port
> + * @dev: device to check
> + *
> + * Returns:
> + * %1 if the device represents a ATA Port, %0 else
> + */
> +int ata_is_port(const struct device *dev)
> +{
> + return dev->release == ata_tport_release;
> +}
> +
> +static int ata_tport_match(struct attribute_container *cont,
> + struct device *dev)
> +{
> + if (!ata_is_port(dev))
> + return 0;
> + return &ata_scsi_transport_template->host_attrs.ac == cont;
> +}
> +
> +/**
> + * ata_tport_delete -- remove ATA PORT
> + * @port: ATA PORT to remove
> + *
> + * Removes the specified ATA PORT. Remove the associated link as well.
> + */
> +void ata_tport_delete(struct ata_port *ap)
> +{
> + struct device *dev = &ap->tdev;
> +
> + ata_tlink_delete(&ap->link);
> +
> + transport_remove_device(dev);
> + device_del(dev);
> + transport_destroy_device(dev);
> + put_device(dev);
> +}
> +
> +/** ata_tport_add - initialize a transport ATA port structure
> + *
> + * @parent: parent device
> + * @ap: existing ata_port structure
> + *
> + * Initialize a ATA port structure for sysfs. It will be added to the device
> + * tree below the device specified by @parent which could be a PCI device.
> + *
> + * Returns %0 on success
> + */
> +int ata_tport_add(struct device *parent,
> + struct ata_port *ap)
> +{
> + int error;
> + struct device *dev = &ap->tdev;
> +
> + device_initialize(dev);
> +
> + dev->parent = get_device(parent);
> + dev->release = ata_tport_release;
> + dev_set_name(dev, "ata%d", ap->print_id);
> + transport_setup_device(dev);
> + error = device_add(dev);
> + if (error) {
> + goto tport_err;
> + }
> +
> + transport_add_device(dev);
> + transport_configure_device(dev);
> +
> + error = ata_tlink_add(&ap->link);
> + if (error) {
> + goto tport_link_err;
> + }
> + return 0;
> +
> + tport_link_err:
> + transport_remove_device(dev);
> + device_del(dev);
> +
> + tport_err:
> + transport_destroy_device(dev);
> + put_device(dev);
> + return error;
> +}
> +
> +
> +/*
> + * ATA link attributes
> + */
> +
> +
> +#define ata_link_show_linkspeed(field) \
> +static ssize_t \
> +show_ata_link_##field(struct device *dev, \
> + struct device_attribute *attr, char *buf) \
> +{ \
> + struct ata_link *link = transport_class_to_link(dev); \
> + \
> + return sprintf(buf,"%s\n", sata_spd_string(fls(link->field))); \
> +}
> +
> +#define ata_link_linkspeed_attr(field) \
> + ata_link_show_linkspeed(field) \
> +static DEVICE_ATTR(field, S_IRUGO, show_ata_link_##field, NULL)
> +
> +ata_link_linkspeed_attr(hw_sata_spd_limit);
> +ata_link_linkspeed_attr(sata_spd_limit);
> +ata_link_linkspeed_attr(sata_spd);
> +
> +
> +static DECLARE_TRANSPORT_CLASS(ata_link_class,
> + "ata_link", NULL, NULL, NULL);
> +
> +static void ata_tlink_release(struct device *dev)
> +{
> + put_device(dev->parent);
> +}
> +
> +/**
> + * ata_is_link -- check if a struct device represents a ATA link
> + * @dev: device to check
> + *
> + * Returns:
> + * %1 if the device represents a ATA link, %0 else
> + */
> +int ata_is_link(const struct device *dev)
> +{
> + return dev->release == ata_tlink_release;
> +}
> +
> +static int ata_tlink_match(struct attribute_container *cont,
> + struct device *dev)
> +{
> + struct ata_internal* i = to_ata_internal(ata_scsi_transport_template);
> + if (!ata_is_link(dev))
> + return 0;
> + return &i->link_attr_cont.ac == cont;
> +}
> +
> +/**
> + * ata_tlink_delete -- remove ATA LINK
> + * @port: ATA LINK to remove
> + *
> + * Removes the specified ATA LINK. remove associated ATA device(s) as well.
> + */
> +void ata_tlink_delete(struct ata_link *link)
> +{
> + struct device *dev = &link->tdev;
> + struct ata_device *ata_dev;
> +
> + ata_for_each_dev(ata_dev, link, ALL) {
> + ata_tdev_delete(ata_dev);
> + }
> +
> + transport_remove_device(dev);
> + device_del(dev);
> + transport_destroy_device(dev);
> + put_device(dev);
> +}
> +
> +/**
> + * ata_tlink_add -- initialize a transport ATA link structure
> + * @link: allocated ata_link structure.
> + *
> + * Initialize an ATA LINK structure for sysfs. It will be added in the
> + * device tree below the ATA PORT it belongs to.
> + *
> + * Returns %0 on success
> + */
> +int ata_tlink_add(struct ata_link *link)
> +{
> + struct device *dev = &link->tdev;
> + struct ata_port *ap = link->ap;
> + struct ata_device *ata_dev;
> + int error;
> +
> + device_initialize(dev);
> + dev->parent = get_device(&ap->tdev);
> + dev->release = ata_tlink_release;
> + if (ata_is_host_link(link))
> + dev_set_name(dev, "link%d", ap->print_id);
> + else
> + dev_set_name(dev, "link%d.%d", ap->print_id, link->pmp);
> +
> + transport_setup_device(dev);
> +
> + error = device_add(dev);
> + if (error) {
> + goto tlink_err;
> + }
> +
> + transport_add_device(dev);
> + transport_configure_device(dev);
> +
> + ata_for_each_dev(ata_dev, link, ALL) {
> + error = ata_tdev_add(ata_dev);
> + if (error) {
> + goto tlink_dev_err;
> + }
> + }
> + return 0;
> + tlink_dev_err:
> + while (--ata_dev >= link->device) {
> + ata_tdev_delete(ata_dev);
> + }
> + transport_remove_device(dev);
> + device_del(dev);
> + tlink_err:
> + transport_destroy_device(dev);
> + put_device(dev);
> + return error;
> +}
> +
> +/*
> + * ATA device attributes
> + */
> +
> +#define ata_dev_show_class(title, field) \
> +static ssize_t \
> +show_ata_dev_##field(struct device *dev, \
> + struct device_attribute *attr, char *buf) \
> +{ \
> + struct ata_device *ata_dev = transport_class_to_dev(dev); \
> + \
> + return get_ata_##title##_names(ata_dev->field, buf); \
> +}
> +
> +#define ata_dev_attr(title, field) \
> + ata_dev_show_class(title, field) \
> +static DEVICE_ATTR(field, S_IRUGO, show_ata_dev_##field, NULL)
> +
> +ata_dev_attr(class, class);
> +ata_dev_attr(xfer, pio_mode);
> +ata_dev_attr(xfer, dma_mode);
> +ata_dev_attr(xfer, xfer_mode);
> +
> +
> +#define ata_dev_show_simple(field, format_string, cast) \
> +static ssize_t \
> +show_ata_dev_##field(struct device *dev, \
> + struct device_attribute *attr, char *buf) \
> +{ \
> + struct ata_device *ata_dev = transport_class_to_dev(dev); \
> + \
> + return snprintf(buf, 20, format_string, cast ata_dev->field); \
> +}
> +
> +#define ata_dev_simple_attr(field, format_string, type) \
> + ata_dev_show_simple(field, format_string, (type)) \
> +static DEVICE_ATTR(field, S_IRUGO, \
> + show_ata_dev_##field, NULL)
> +
> +ata_dev_simple_attr(spdn_cnt, "%d\n", int);
> +
> +struct ata_show_ering_arg {
> + char* buf;
> + int written;
> +};
> +
> +static int ata_show_ering(struct ata_ering_entry *ent, void *void_arg)
> +{
> + struct ata_show_ering_arg* arg = void_arg;
> + struct timespec time;
> +
> + jiffies_to_timespec(ent->timestamp,&time);
> + arg->written += sprintf(arg->buf + arg->written,
> + "[%5lu.%06lu]",
> + time.tv_sec, time.tv_nsec);
> + arg->written += get_ata_err_names(ent->err_mask,
> + arg->buf + arg->written);
> + return 0;
> +}
> +
> +static ssize_t
> +show_ata_dev_ering(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct ata_device *ata_dev = transport_class_to_dev(dev);
> + struct ata_show_ering_arg arg = { buf, 0 };
> +
> + ata_ering_map(&ata_dev->ering, ata_show_ering, &arg);
> + return arg.written;
> +}
> +
> +
> +static DEVICE_ATTR(ering, S_IRUGO, show_ata_dev_ering, NULL);
> +
> +static ssize_t
> +show_ata_dev_id(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct ata_device *ata_dev = transport_class_to_dev(dev);
> + int written = 0, i = 0;
> +
> + if (ata_dev->class == ATA_DEV_PMP)
> + return 0;
> + for(i=0;i<ATA_ID_WORDS;i++) {
> + written += snprintf(buf+written, 20, "%04x%c",
> + ata_dev->id[i],
> + ((i+1) & 7) ? ' ' : '\n');
> + }
> + return written;
> +}
> +
> +static DEVICE_ATTR(id, S_IRUGO, show_ata_dev_id, NULL);
> +
> +static ssize_t
> +show_ata_dev_gscr(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct ata_device *ata_dev = transport_class_to_dev(dev);
> + int written = 0, i = 0;
> +
> + if (ata_dev->class != ATA_DEV_PMP)
> + return 0;
> + for(i=0;i<SATA_PMP_GSCR_DWORDS;i++) {
> + written += snprintf(buf+written, 20, "%08x%c",
> + ata_dev->gscr[i],
> + ((i+1) & 3) ? ' ' : '\n');
> + }
> + if (SATA_PMP_GSCR_DWORDS & 3)
> + buf[written-1] = '\n';
> + return written;
> +}
> +
> +static DEVICE_ATTR(gscr, S_IRUGO, show_ata_dev_gscr, NULL);
> +
> +static DECLARE_TRANSPORT_CLASS(ata_dev_class,
> + "ata_device", NULL, NULL, NULL);
> +
> +static void ata_tdev_release(struct device *dev)
> +{
> + put_device(dev->parent);
> +}
> +
> +/**
> + * ata_is_ata_dev -- check if a struct device represents a ATA device
> + * @dev: device to check
> + *
> + * Returns:
> + * %1 if the device represents a ATA device, %0 else
> + */
> +int ata_is_ata_dev(const struct device *dev)
> +{
> + return dev->release == ata_tdev_release;
> +}
> +
> +static int ata_tdev_match(struct attribute_container *cont,
> + struct device *dev)
> +{
> + struct ata_internal* i = to_ata_internal(ata_scsi_transport_template);
> + if (!ata_is_ata_dev(dev))
> + return 0;
> + return &i->dev_attr_cont.ac == cont;
> +}
> +
> +/**
> + * ata_tdev_free -- free a ATA LINK
> + * @dev: ATA PHY to free
> + *
> + * Frees the specified ATA PHY.
> + *
> + * Note:
> + * This function must only be called on a PHY that has not
> + * successfully been added using ata_tdev_add().
> + */
> +static void ata_tdev_free(struct ata_device *dev)
> +{
> + transport_destroy_device(&dev->tdev);
> + put_device(&dev->tdev);
> +}
> +
> +/**
> + * ata_tdev_delete -- remove ATA device
> + * @port: ATA PORT to remove
> + *
> + * Removes the specified ATA device.
> + */
> +static void ata_tdev_delete(struct ata_device *ata_dev)
> +{
> + struct device *dev = &ata_dev->tdev;
> +
> + transport_remove_device(dev);
> + device_del(dev);
> + ata_tdev_free(ata_dev);
> +}
> +
> +
> +/**
> + * ata_tdev_add -- initialize a transport ATA device structure.
> + * @ata_dev: ata_dev structure.
> + *
> + * Initialize an ATA device structure for sysfs. It will be added in the
> + * device tree below the ATA LINK device it belongs to.
> + *
> + * Returns %0 on success
> + */
> +static int ata_tdev_add(struct ata_device *ata_dev)
> +{
> + struct device *dev = &ata_dev->tdev;
> + struct ata_link *link = ata_dev->link;
> + struct ata_port *ap = link->ap;
> + int error;
> +
> + device_initialize(dev);
> + dev->parent = get_device(&link->tdev);
> + dev->release = ata_tdev_release;
> + if (ata_is_host_link(link))
> + dev_set_name(dev, "dev%d.%d", ap->print_id,ata_dev->devno);
> + else
> + dev_set_name(dev, "dev%d.%d.0", ap->print_id, link->pmp);
> +
> + transport_setup_device(dev);
> + error = device_add(dev);
> + if (error) {
> + ata_tdev_free(ata_dev);
> + return error;
> + }
> +
> + transport_add_device(dev);
> + transport_configure_device(dev);
> + return 0;
> +}
> +
> +
> +/*
> + * Setup / Teardown code
> + */
> +
> +#define SETUP_TEMPLATE(attrb, field, perm, test) \
> + i->private_##attrb[count] = dev_attr_##field; \
> + i->private_##attrb[count].attr.mode = perm; \
> + i->attrb[count] = &i->private_##attrb[count]; \
> + if (test) \
> + count++
> +
> +#define SETUP_LINK_ATTRIBUTE(field) \
> + SETUP_TEMPLATE(link_attrs, field, S_IRUGO, 1)
> +
> +#define SETUP_PORT_ATTRIBUTE(field) \
> + SETUP_TEMPLATE(port_attrs, field, S_IRUGO, 1)
> +
> +#define SETUP_DEV_ATTRIBUTE(field) \
> + SETUP_TEMPLATE(dev_attrs, field, S_IRUGO, 1)
> +
> +/**
> + * ata_attach_transport -- instantiate ATA transport template
> + */
> +struct scsi_transport_template *ata_attach_transport(void)
> +{
> + struct ata_internal *i;
> + int count;
> +
> + i = kzalloc(sizeof(struct ata_internal), GFP_KERNEL);
> + if (!i)
> + return NULL;
> +
> + i->t.eh_strategy_handler = ata_scsi_error;
> + i->t.eh_timed_out = ata_scsi_timed_out;
> + i->t.user_scan = ata_scsi_user_scan;
> +
> + i->t.host_attrs.ac.attrs = &i->port_attrs[0];
> + i->t.host_attrs.ac.class = &ata_port_class.class;
> + i->t.host_attrs.ac.match = ata_tport_match;
> + transport_container_register(&i->t.host_attrs);
> +
> + i->link_attr_cont.ac.class = &ata_link_class.class;
> + i->link_attr_cont.ac.attrs = &i->link_attrs[0];
> + i->link_attr_cont.ac.match = ata_tlink_match;
> + transport_container_register(&i->link_attr_cont);
> +
> + i->dev_attr_cont.ac.class = &ata_dev_class.class;
> + i->dev_attr_cont.ac.attrs = &i->dev_attrs[0];
> + i->dev_attr_cont.ac.match = ata_tdev_match;
> + transport_container_register(&i->dev_attr_cont);
> +
> + count = 0;
> + SETUP_PORT_ATTRIBUTE(nr_pmp_links);
> + SETUP_PORT_ATTRIBUTE(idle_irq);
> + BUG_ON(count > ATA_PORT_ATTRS);
> + i->port_attrs[count] = NULL;
> +
> + count = 0;
> + SETUP_LINK_ATTRIBUTE(hw_sata_spd_limit);
> + SETUP_LINK_ATTRIBUTE(sata_spd_limit);
> + SETUP_LINK_ATTRIBUTE(sata_spd);
> + BUG_ON(count > ATA_LINK_ATTRS);
> + i->link_attrs[count] = NULL;
> +
> + count = 0;
> + SETUP_DEV_ATTRIBUTE(class);
> + SETUP_DEV_ATTRIBUTE(pio_mode);
> + SETUP_DEV_ATTRIBUTE(dma_mode);
> + SETUP_DEV_ATTRIBUTE(xfer_mode);
> + SETUP_DEV_ATTRIBUTE(spdn_cnt);
> + SETUP_DEV_ATTRIBUTE(ering);
> + SETUP_DEV_ATTRIBUTE(id);
> + SETUP_DEV_ATTRIBUTE(gscr);
> + BUG_ON(count > ATA_DEV_ATTRS);
> + i->dev_attrs[count] = NULL;
> +
> + return &i->t;
> +}
> +
> +/**
> + * ata_release_transport -- release ATA transport template instance
> + * @t: transport template instance
> + */
> +void ata_release_transport(struct scsi_transport_template *t)
> +{
> + struct ata_internal *i = to_ata_internal(t);
> +
> + transport_container_unregister(&i->t.host_attrs);
> + transport_container_unregister(&i->link_attr_cont);
> + transport_container_unregister(&i->dev_attr_cont);
> +
> + kfree(i);
> +}
> +
> +__init int libata_transport_init(void)
> +{
> + int error;
> +
> + error = transport_class_register(&ata_link_class);
> + if (error)
> + goto out_unregister_transport;
> + error = transport_class_register(&ata_port_class);
> + if (error)
> + goto out_unregister_link;
> + error = transport_class_register(&ata_dev_class);
> + if (error)
> + goto out_unregister_port;
> + return 0;
> +
> + out_unregister_port:
> + transport_class_unregister(&ata_port_class);
> + out_unregister_link:
> + transport_class_unregister(&ata_link_class);
> + out_unregister_transport:
> + return error;
> +
> +}
> +
> +void __exit libata_transport_exit(void)
> +{
> + transport_class_unregister(&ata_link_class);
> + transport_class_unregister(&ata_port_class);
> + transport_class_unregister(&ata_dev_class);
> +}
> diff --git a/drivers/ata/libata-transport.h b/drivers/ata/libata-transport.h
> new file mode 100644
> index 0000000..2820cf8
> --- /dev/null
> +++ b/drivers/ata/libata-transport.h
> @@ -0,0 +1,18 @@
> +#ifndef _LIBATA_TRANSPORT_H
> +#define _LIBATA_TRANSPORT_H
> +
> +
> +extern struct scsi_transport_template *ata_scsi_transport_template;
> +
> +int ata_tlink_add(struct ata_link *link);
> +void ata_tlink_delete(struct ata_link *link);
> +
> +int ata_tport_add(struct device *parent, struct ata_port *ap);
> +void ata_tport_delete(struct ata_port *ap);
> +
> +struct scsi_transport_template *ata_attach_transport(void);
> +void ata_release_transport(struct scsi_transport_template *t);
> +
> +__init int libata_transport_init(void);
> +void __exit libata_transport_exit(void);
> +#endif
> diff --git a/drivers/ata/libata.h b/drivers/ata/libata.h
> index 823e630..4b94908 100644
> --- a/drivers/ata/libata.h
> +++ b/drivers/ata/libata.h
> @@ -115,6 +115,7 @@ extern int ata_cmd_ioctl(struct scsi_device *scsidev, void __user *arg);
> extern struct ata_port *ata_port_alloc(struct ata_host *host);
> extern void ata_dev_enable_pm(struct ata_device *dev, enum link_pm policy);
> extern void ata_lpm_schedule(struct ata_port *ap, enum link_pm);
> +extern const char *sata_spd_string(unsigned int spd);
>
> /* libata-acpi.c */
> #ifdef CONFIG_ATA_ACPI
> @@ -150,6 +151,9 @@ extern void ata_scsi_hotplug(struct work_struct *work);
> extern void ata_schedule_scsi_eh(struct Scsi_Host *shost);
> extern void ata_scsi_dev_rescan(struct work_struct *work);
> extern int ata_bus_probe(struct ata_port *ap);
> +extern int ata_scsi_user_scan(struct Scsi_Host *shost, unsigned int channel,
> + unsigned int id, unsigned int lun);
> +
>
> /* libata-eh.c */
> extern unsigned long ata_internal_cmd_timeout(struct ata_device *dev, u8 cmd);
> @@ -177,6 +181,9 @@ extern int ata_eh_recover(struct ata_port *ap, ata_prereset_fn_t prereset,
> ata_postreset_fn_t postreset,
> struct ata_link **r_failed_disk);
> extern void ata_eh_finish(struct ata_port *ap);
> +extern int ata_ering_map(struct ata_ering *ering,
> + int (*map_fn)(struct ata_ering_entry *, void *),
> + void *arg);
>
> /* libata-pmp.c */
> #ifdef CONFIG_SATA_PMP
> diff --git a/include/linux/libata.h b/include/linux/libata.h
> index b2f2003..fe28bf0 100644
> --- a/include/linux/libata.h
> +++ b/include/linux/libata.h
> @@ -605,6 +605,7 @@ struct ata_device {
> union acpi_object *gtf_cache;
> unsigned int gtf_filter;
> #endif
> + struct device tdev;
> /* n_sector is CLEAR_BEGIN, read comment above CLEAR_BEGIN */
> u64 n_sectors; /* size of device, if ATA */
> u64 n_native_sectors; /* native size, if ATA */
> @@ -691,6 +692,7 @@ struct ata_link {
> struct ata_port *ap;
> int pmp; /* port multiplier port # */
>
> + struct device tdev;
> unsigned int active_tag; /* active tag on this link */
> u32 sactive; /* active NCQ commands */
>
> @@ -708,6 +710,8 @@ struct ata_link {
>
> struct ata_device device[ATA_MAX_DEVICES];
> };
> +#define ATA_LINK_CLEAR_BEGIN offsetof(struct ata_link, active_tag)
> +#define ATA_LINK_CLEAR_END offsetof(struct ata_link, device[0])
>
> struct ata_port {
> struct Scsi_Host *scsi_host; /* our co-allocated scsi host */
> @@ -750,6 +754,7 @@ struct ata_port {
> struct ata_port_stats stats;
> struct ata_host *host;
> struct device *dev;
> + struct device tdev;
>
> void *port_task_data;
> struct delayed_work port_task;
> --
> 1.7.0.1
>
> --
> To unsubscribe from this list: send the line "unsubscribe linux-ide" in
> the body of a message to majordomo@vger.kernel.org
> More majordomo info at http://vger.kernel.org/majordomo-info.html
>
^ permalink raw reply [flat|nested] 4+ messages in thread