Saturday, November 20, 2010

Turning off LEDs on process crashes...

Edit: As I found out today, profile_event_unregister and friends aren't supposed to be used in generic drivers, and are meant only for profiler usage. So if you use this code, there will be kittens getting hurt someplace, and people will laugh at your kernel patches. Or something like that. YMMV.

Sometimes you want to manipulate LEDs from a program. This is easy. LEDs live in /sys/class/leds, and all you need to do is set the brightness sysfs property. Unfortunately, if the task that manipulated the LED died before turning it off, you have no automatic way of cleaning up after yourself. This is why people like manipulating drivers via an file descriptor - if anything goes wrong, the close() will happen automatically.

But the LED interface happened and no one is going to change it. The solution, of course, is to implement a LED trigger. But where LED triggers usually turn LEDs on, this one will turn it off. And it will turn it off when the task that set the trigger completes its execution. I generalized this a bit so any arbitrary task can be watched, and so that any brightness can be set on exit (because I'm nice nice like, and it didn't cost me anything).

Usage is something like this (from within your program).

# echo "owner" > /sys/class/leds/XXX/trigger
# echo  "1" > /sys/class/leds/XXX/brightness
# ...do stuff...
# echo "0" > /sys/class/leds/XXX/brightness
# echo "none" >  /sys/class/leds/XXX/trigger

Implementation-wise, the driver registers a PROFILE_TASK_EXIT notifier. The notifier is global, i.e. it's not tied to any specific process, so it will be invoked for every process exiting (but only as long as the trigger is in actual use), thus the need to compare PIDs. It would be nice to get a targeted PROFILE_TASK_EXIT...



Here is the patch, against 2.6.36 -

From 46b8b42dd750e251e6b4f3ec7a8627dc1fdbb44c Mon Sep 17 00:00:00 2001
From: Andrei Warkentin <andreiw@motorola.com>
Date: Tue, 26 Oct 2010 16:17:06 -0500
Subject: [PATCH] LED: ledtrig-owner - set LED brightness on exit of a process.

By default, the set brightness is LED_OFF. By default, the process
triggering the set is the process that enabled the trigger. Both are
changable via sysfs attributes.

Change-Id: I4222bac47da1bd4b1263fe98c853d7bbe1471695
Signed-off-by: Andrei Warkentin <andreiw@motorola.com>
---
 drivers/leds/Kconfig         |    7 +
 drivers/leds/Makefile        |    1 +
 drivers/leds/ledtrig-owner.c |  362 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 370 insertions(+), 0 deletions(-)
 create mode 100644 drivers/leds/ledtrig-owner.c

diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index 859f705..9d49eb3 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -356,6 +356,13 @@ config LEDS_TRIGGER_TIMER
 
    If unsure, say Y.
 
+config LEDS_TRIGGER_OWNER
+       tristate "LED Owner Trigger"
+       help
+         Set brightness on exit of a process.
+
+  If unsure, say Y.
+
 config LEDS_TRIGGER_IDE_DISK
  bool "LED IDE Disk Trigger"
  depends on IDE_GD_ATA
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index a1462ff..4d25260 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -54,3 +54,4 @@ obj-$(CONFIG_LEDS_TRIGGER_BACKLIGHT) += ledtrig-backlight.o
 obj-$(CONFIG_LEDS_TRIGGER_GPIO)  += ledtrig-gpio.o
 obj-$(CONFIG_LEDS_TRIGGER_DEFAULT_ON) += ledtrig-default-on.o
 obj-$(CONFIG_LEDS_TRIGGER_SLEEP) += ledtrig-sleep.o
+obj-$(CONFIG_LEDS_TRIGGER_OWNER)        += ledtrig-owner.o
diff --git a/drivers/leds/ledtrig-owner.c b/drivers/leds/ledtrig-owner.c
new file mode 100644
index 0000000..e16436f
--- /dev/null
+++ b/drivers/leds/ledtrig-owner.c
@@ -0,0 +1,362 @@
+/*
+ * LED Owner trigger - set LED brightness on exit of a process.
+ *
+ * The default is to set the brightness to LED_OFF. This can be changed
+ * via the owner_brightness attribute. The default is to trigger on
+ * exit of process that enabled the trigger. That can be changed via the
+ * owner_pid attribute.
+ *
+ * Andrei Warkentin <andreiw@motorola.com>, 2010
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ */
+
+#include <linux/module.h>

+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/leds.h>
+#include <linux/slab.h>
+#include <linux/ctype.h>
+#include <linux/sched.h>

+#include <linux/semaphore.h>
+#include <linux/workqueue.h>
+#include <linux/profile.h>
+#include "leds.h"
+
+/*
+   The lock protects both the list of LEDs on which the trigger
+   is set set (and which haven"t triggered yet), and the count.
+*/
+static DECLARE_RWSEM(monitored_leds_lock);
+static LIST_HEAD(monitored_leds_list);
+static unsigned monitored_leds = 0;
+
+struct owner_trig_data {
+ struct list_head head;
+
+ /* Process on whose exit we"ll set led"s
+    brightness to target_brightness. */
+ __kernel_pid_t tgid;
+ bool registered;
+ struct semaphore sem;
+ struct led_classdev *led;
+ struct work_struct work;
+ enum led_brightness target_brightness;
+};
+
+static int
+owner_exit_notifier(struct notifier_block *self, unsigned long val, void *data);
+
+static struct notifier_block exit_nb = {
+ .notifier_call = owner_exit_notifier,
+};
+
+static int
+owner_register(struct owner_trig_data *owner_data, __kernel_pid_t tgid)
+{
+ int err = 0;
+ down(&owner_data->sem);
+ down_write(&monitored_leds_lock);
+ owner_data->tgid = tgid;
+
+ if (owner_data->registered)
+  goto done;
+
+ /* Registers a callback if this is the
+    first monitored LED. */
+ if (!monitored_leds) {
+  err = profile_event_register(PROFILE_TASK_EXIT, &exit_nb);
+  if (err) {
+   pr_err("%s: failed to register task exit notification\n",
+          __func__);
+   goto done;
+  }
+  monitored_leds++;
+ }
+ list_add_tail(&owner_data->head, &monitored_leds_list);
+ owner_data->registered = true;
+done:
+ up_write(&monitored_leds_lock);
+ up(&owner_data->sem);
+ return err;
+}
+
+static ssize_t owner_pid_get(struct device *dev,
+        struct device_attribute *attr,
+        char *buf)
+{
+ ssize_t retlen;
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct owner_trig_data *owner_data = led_cdev->trigger_data;
+
+ if (!owner_data) {
+  pr_err("%s: no trigger_data, ignoring\n", __func__);
+  return sprintf(buf, "none, not registered\n");
+ }
+
+ down(&owner_data->sem);
+ if (owner_data->registered)
+  retlen = sprintf(buf, "%d\n", owner_data->tgid);
+ else
+  retlen = sprintf(buf, "none\n");
+ up(&owner_data->sem);
+ return retlen;
+}
+
+static ssize_t owner_pid_set(struct device *dev,
+        struct device_attribute *attr,
+        const char *buf,
+        size_t size)
+{
+ char *after;
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct owner_trig_data *owner_data = led_cdev->trigger_data;
+ __kernel_pid_t tgid = (__kernel_pid_t) simple_strtoul(buf, &after, 10);
+ size_t count = after - buf;
+ int ret = -EINVAL;
+
+ if (isspace(*after))
+  count++;
+
+ if (count != size) {
+  pr_warn("%s: unknown data passed\n", __func__);
+  goto done;
+ }
+ ret = count;
+
+ if (!owner_data) {
+  pr_err("%s: no trigger_data, ignoring\n", __func__);
+  goto done;
+ }
+
+ owner_register(owner_data, tgid);
+done:
+ return ret;
+}
+
+static ssize_t owner_brightness_get(struct device *dev,
+        struct device_attribute *attr,
+        char *buf)
+{
+ ssize_t retlen;
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct owner_trig_data *owner_data = led_cdev->trigger_data;
+
+ if (!owner_data) {
+  pr_err("%s: no trigger_data, ignoring\n", __func__);
+  return sprintf(buf, "unknown, not registered\n");
+ }
+
+ down(&owner_data->sem);
+ retlen = sprintf(buf, "%u\n", owner_data->target_brightness);
+ up(&owner_data->sem);
+ return retlen;
+}
+
+static ssize_t owner_brightness_set(struct device *dev,
+        struct device_attribute *attr,
+        const char *buf,
+        size_t size)
+{
+ char *after;
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct owner_trig_data *owner_data = led_cdev->trigger_data;
+ enum led_brightness bright = (enum led_brightness)
+  simple_strtoul(buf, &after, 10);
+
+ size_t count = after - buf;
+ int ret = -EINVAL;
+
+ if (isspace(*after))
+  count++;
+
+ if (count != size) {
+  pr_warn("%s: unknown data passed\n", __func__);
+  goto done;
+ }
+ ret = count;
+
+ if (!owner_data) {
+  pr_err("%s: no trigger_data, ignoring\n", __func__);
+  goto done;
+ }
+
+ down(&owner_data->sem);
+ owner_data->target_brightness = bright;
+ up(&owner_data->sem);
+done:
+ return ret;
+}
+
+static DEVICE_ATTR(owner_brightness, 0644, owner_brightness_get, owner_brightness_set);
+static DEVICE_ATTR(owner_pid, 0644, owner_pid_get, owner_pid_set);
+
+static void
+_owner_deregister(struct owner_trig_data *owner_data)
+{
+ down_write(&monitored_leds_lock);
+
+ /* Only if it wasn"t already deregistered. */
+ if (owner_data->registered) {
+  list_del(&owner_data->head);
+  owner_data->registered = false;
+
+  if (--monitored_leds == 0) {
+   profile_event_unregister(PROFILE_TASK_EXIT, &exit_nb);
+  }
+ }
+
+ up_write(&monitored_leds_lock);
+}
+
+static void
+owner_deregister(struct owner_trig_data *owner_data)
+{
+ down(&owner_data->sem);
+ _owner_deregister(owner_data);
+ up(&owner_data->sem);
+}
+
+static void owner_deregister_work(struct work_struct *work)
+{
+        struct owner_trig_data *owner_data =
+  container_of(work, struct owner_trig_data, work);
+
+ _owner_deregister(owner_data);
+ up(&owner_data->sem);
+ up_write(&owner_data->led->trigger_lock);
+}
+
+static int
+owner_exit_notifier(struct notifier_block *self, unsigned long val, void *data)
+{
+ struct owner_trig_data *mon_led;
+ struct owner_trig_data *owner_data = NULL;
+ struct task_struct *task = (struct task_struct *) data;
+
+ down_read(&monitored_leds_lock);
+ list_for_each_entry(mon_led, &monitored_leds_list, head) {
+  down(&mon_led->sem);
+  if (mon_led->tgid == task->tgid) {
+   owner_data = mon_led;
+   break;
+  }
+  up(&mon_led->sem);
+ }
+ up_read(&monitored_leds_lock);
+
+ if (owner_data) {
+  led_set_brightness(owner_data->led, owner_data->target_brightness);
+
+  /*
+     We can"t call owner_deregister from here, because
+     we would deadlock on profile_event_unregister, so
+     we schedule a work item. We need to guarantee that
+     the trigger data isn"t cleaned up before the work
+     item runs, so we"ll take the LED trigger lock - the
+     same lock taken on activate/deactivate by LED class.
+  */
+
+  down_write(&owner_data->led->trigger_lock);
+  schedule_work(&owner_data->work);
+ }
+ return 0;
+}
+
+static void
+owner_activate(struct led_classdev *led_cdev)
+{
+ int err;
+ struct owner_trig_data *owner_data;
+
+ owner_data = kzalloc(sizeof(struct owner_trig_data), GFP_KERNEL);
+ if (!owner_data) {
+  pr_err("%s: trigger_data allocation failed, ignoring\n", __func__);
+  return;
+ }
+
+ INIT_LIST_HEAD(&owner_data->head);
+ init_MUTEX(&owner_data->sem);
+ owner_data->led = led_cdev;
+ INIT_WORK(&owner_data->work, owner_deregister_work);
+
+ /* By default, turn off the LED. */
+ owner_data->target_brightness = LED_OFF;
+ led_cdev->trigger_data = owner_data;
+
+ err = owner_register(owner_data, current->tgid);
+ if (err) {
+  pr_err("%s: owner_register failed with %d\n",
+         __func__, err);
+  goto err_register;
+ }
+
+ err = device_create_file(led_cdev->dev, &dev_attr_owner_brightness);
+ if (err) {
+  pr_err("%s: dev_attr_owner_brightness register failed with %d\n",
+         __func__, err);
+  goto err_brightness;
+ }
+
+ err = device_create_file(led_cdev->dev, &dev_attr_owner_pid);
+ if (err) {
+  pr_err("%s: dev_attr_owner_pid register failed with %d\n",
+         __func__, err);
+  goto err_pid;
+ }
+
+ return;
+err_pid:
+ device_remove_file(led_cdev->dev, &dev_attr_owner_brightness);
+err_brightness:
+ owner_deregister(owner_data);
+err_register:
+ kfree(led_cdev->trigger_data);
+ led_cdev->trigger_data = NULL;
+}
+
+static void
+owner_deactivate(struct led_classdev *led_cdev)
+{
+ struct owner_trig_data *owner_data = led_cdev->trigger_data;
+ if (!owner_data) {
+  pr_err("%s: no trigger_data, ignoring\n", __func__);
+  return;
+ }
+
+ device_remove_file(led_cdev->dev, &dev_attr_owner_pid);
+ device_remove_file(led_cdev->dev, &dev_attr_owner_brightness);
+ owner_deregister(owner_data);
+
+ kfree(led_cdev->trigger_data);
+ led_cdev->trigger_data = NULL;
+ return;
+}
+
+static struct led_trigger owner_led_trigger = {
+ .name     = "owner",
+ .activate = owner_activate,
+ .deactivate = owner_deactivate,
+};
+
+static int __init
+owner_trig_init(void)
+{
+ return led_trigger_register(&owner_led_trigger);
+}
+
+static void __exit
+owner_trig_exit(void)
+{
+ led_trigger_unregister(&owner_led_trigger);
+}
+
+module_init(owner_trig_init);
+module_exit(owner_trig_exit);
+
+MODULE_AUTHOR("Andrei Warkentin <andreiw@motorola.com>");
+MODULE_DESCRIPTION("Owner LED trigger");
+MODULE_LICENSE("GPL");
-- 
1.7.0.4

No comments:

Post a Comment