From mboxrd@z Thu Jan 1 00:00:00 1970 From: compudj@krystal.dyndns.org (Mathieu Desnoyers) Date: Mon, 6 Apr 2009 13:52:35 -0400 Subject: [ltt-dev] [PATCH 3/3] In-kernel filter module In-Reply-To: <49CF3500.7050109@cn.fujitsu.com> References: <20090203051144.GA23739@Krystal> <49927147.9010909@cn.fujitsu.com> <20090211065021.GA10376@Krystal> <49927D46.6010508@cn.fujitsu.com> <20090211073719.GC10376@Krystal> <499D15A9.10509@cn.fujitsu.com> <49AB8445.7050202@cn.fujitsu.com> <20090303192532.GA4334@Krystal> <49CF3437.50407@cn.fujitsu.com> <49CF3500.7050109@cn.fujitsu.com> Message-ID: <20090406175235.GC31867@Krystal> * Zhaolei (zhaolei at cn.fujitsu.com) wrote: > LTTng's in-kernel filter is used to filter event by its arguments. > User can config every event's filter option by echo to filts in > debugfs/ltt/filter > Hi Zhaolei, That's a good start, but locking will have to be rethought. Please see comments below, > Signed-off-by: Zhao Lei > --- > ltt/Kconfig | 9 + > ltt/Makefile | 1 + > ltt/ltt-filter.c | 1831 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ > 3 files changed, 1841 insertions(+), 0 deletions(-) > create mode 100644 ltt/ltt-filter.c > > diff --git a/ltt/Kconfig b/ltt/Kconfig > index 3c520f5..2cfb479 100644 > --- a/ltt/Kconfig > +++ b/ltt/Kconfig > @@ -214,4 +214,13 @@ config LTT_ASCII > Output trace data in a text-formatted ascii file, presented in > /mnt/debugfs/ltt/ascii/. > > +config LTT_FILTER > + tristate "Linux Trace Toolkit Filter" > + depends on LTT_TRACER > + depends on LTT_SERIALIZE > + default n > + help > + Filter for LTTng to control which type of event to be traced. > + Config filter rule by debugfs/ltt/filter > + > endif # LTT > diff --git a/ltt/Makefile b/ltt/Makefile > index 5ea5cca..29ab6e4 100644 > --- a/ltt/Makefile > +++ b/ltt/Makefile > @@ -19,3 +19,4 @@ obj-$(CONFIG_LTT_KPROBES) += ltt-kprobes.o > obj-$(CONFIG_LTT_TRACEPROBES) += probes/ > obj-$(CONFIG_LTT_FTRACE) += ltt-ftrace.o > obj-$(CONFIG_LTT_ASCII) += ltt-ascii.o > +obj-$(CONFIG_LTT_FILTER) += ltt-filter.o > diff --git a/ltt/ltt-filter.c b/ltt/ltt-filter.c > new file mode 100644 > index 0000000..bf2760a > --- /dev/null > +++ b/ltt/ltt-filter.c > @@ -0,0 +1,1831 @@ > +/* > + * LTT filter module. > + * > + * Copyright 2009 - > + * Zhaolei > + */ > + > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > + > +#define LTT_FILTER_DIR "filter" > + > +struct dentry *ltt_filter_dir; > + > +/* > + * Marker format operations > + * > + * Maintenance a marker_format list sync with module add and release > + * Each data-type in format are paresed and stored in marker_format struct paresed -> parsed > + * Impl a notifier for respond add/remove marker format event Notifier which responds to add/remove marker format event > + */ > + > +enum format_item_type { > + SIGNED, > + UNSIGNED, > + STRING, > + CHAR, > +}; > +const char *format_item_type_string[] = { > + "signed int", > + "unsigned int", > + "string", > + "char", > +}; > + > +struct format_item { > + struct list_head list; > + char name[PATH_MAX]; this rather large "name" field should go at the end of the struct. > + enum format_item_type type; > + size_t size; > +}; > + > +struct marker_format { > + atomic_t refcnt; > + struct list_head list; > + const char *name; > + const char *format; > + struct format_item **items; > + size_t format_item_cnt; > +}; > + > +static LIST_HEAD(marker_format_head); > +static DEFINE_MUTEX(format_lock); > + > +BLOCKING_NOTIFIER_HEAD(format_notifier_list); > +enum FORMAT_EVENT { > + ADD_FORMAT, > + DEL_FORMAT, > +}; > + > +static struct format_item *new_format_item(const char *name_head, > + const char *name_tail, int type_type, int type_size) > +{ > + struct format_item *format_item; > + const char *p; > + int i, name_len; > + > + format_item = kmalloc(sizeof(struct format_item), GFP_KERNEL); > + if (!format_item) { > + printk(KERN_ERR "%s: alloc format_item failed\n", __func__); > + goto err_alloc_format_item; > + } > + > + INIT_LIST_HEAD(&format_item->list); > + > + /* trim itemname */ > + while (name_head < name_tail > + && *name_head == ' ') > + name_head++; > + while (name_tail > name_head > + && *(name_tail - 1) == ' ') > + name_tail--; > + > + name_len = name_tail - name_head; > + if (name_len) { > + for (i = 0, p = name_head; i < PATH_MAX - 1 && p < name_tail; > + i++, p++) { > + if ((*p == '%' && p + 1 < name_tail && *(p + 1) == '%') > + || (*p == '#' && > + p + 1 < name_tail && *(p + 1) == '#')) { > + p++; > + } > + format_item->name[i] = isalnum(*p) ? *p : '_'; > + } > + format_item->name[i] = 0; > + } else { > + format_item->name[0] = 0; > + } > + > + format_item->type = type_type; > + format_item->size = type_size; > + > + return format_item; > + > +err_alloc_format_item: > + return NULL; > +} > + > +static size_t parse_format(const char *format, struct format_item ***items) > +{ > + int ret = 0; /* number of types in format string, or -ERRNO on fail */ > + int state; /* 0: in name, 1: in type, 2: in # */ > + const char *p; /* format string's iter */ > + /* name for current type, init with 0 to avoid compile warning */ > + const char *name_head, *name_tail = 0; > + enum format_item_type type_type; > + /* > + * size(in bytes) of current type > + * init with 0 to avoid compile warning > + */ > + int type_size = 0; > + > + LIST_HEAD(format_item_list); > + struct format_item *format_item, *format_item_next; > + > + for (state = 0, name_head = p = format; *p; p++) { > + switch (state) { > + case 0: > + switch (*p) { > + case '%': > + if (*(p+1) != '%') { > + name_tail = p; > + type_size = sizeof(int); > + state = 1; > + } else { > + p++; > + } > + break; > + case '#': > + if (*(p+1) != '#') { > + name_tail = p; > + state = 2; > + } else { > + p++; > + } > + break; > + default: > + break; > + } > + break; > + case 1: > + switch (*p) { > + case 'l': > + if (*(p+1) == 'l') { > + type_size = sizeof(long long); > + p++; > + } else { > + type_size = sizeof(long); > + } > + break; > + case 'L': > + type_size = sizeof(long long); > + break; > + case 'z': > + case 'Z': > + type_size = sizeof(size_t); > + break; > + case 't': > + type_size = sizeof(ptrdiff_t); > + break; > + case 'h': > + type_size = sizeof(short); > + break; > + case 'd': > + case 'i': > + type_type = SIGNED; > + goto type_finish; > + break; > + case 'u': > + case 'o': > + case 'x': > + case 'X': > + type_type = UNSIGNED; > + goto type_finish; > + break; > + case 'p': > + type_type = UNSIGNED; > + type_size = sizeof(void *); > + goto type_finish; > + break; > + case 's': > + type_type = STRING; > + type_size = sizeof(void *); > + goto type_finish; > + break; > + case 'c': > + type_type = CHAR; > + type_size = sizeof(char); > + goto type_finish; > + break; > + case '0': > + case '1': > + case '2': > + case '3': > + case '4': > + case '5': > + case '6': > + case '7': > + case '8': > + case '9': > + break; > + default: > + printk(KERN_ERR > + "%s: unknown char after %%: %c\n", > + __func__, *p); > + break; > + } > + break; > + case 2: > + switch (*p) { > + case 'l': > + case 'L': > + case 'Z': > + case 'z': > + case 't': > + case 'h': > + case 'd': > + case 'i': > + case 'u': > + case 'o': > + case 'x': > + case 'X': > + case 'p': > + case 's': > + case 'c': > + case '1': > + case '2': > + case '4': > + case '8': > + break; > + case '%': > + type_size = sizeof(int); > + state = 1; > + break; > + default: > + printk(KERN_ERR "%s: unknown char after #: %c\n", > + __func__, *p); > + break; > + } > + break; > + default: > + /* should not run to here */ > + ret = -EINVAL; > + goto err_unknown_state; > + } > + continue; > +type_finish: > + format_item = new_format_item(name_head, name_tail, type_type, > + type_size); > + if (!format_item) { > + printk(KERN_ERR "%s: add_format_item failed\n", > + __func__); > + ret = -ENOMEM; > + goto err_new_format_item; > + } > + > + list_add_tail(&format_item->list, &format_item_list); > + ret++; > + name_head = p + 1; > + state = 0; > + continue; > + } > + > + /* > + * We have parsed types in format, and build linked_list to save > + * there information. > + * Now we will build a point array to save each format_item for > + * easy access. > + */ > + > + *items = kmalloc(ret * sizeof(struct format_item *), GFP_KERNEL); > + if (!*items) { > + printk(KERN_ERR "%s: alloc format_item index failed\n", > + __func__); > + ret = -ENOMEM; > + goto err_alloc_format_item; > + } > + > + /* we reuse state as a iter for (*items)[] */ > + state = 0; > + list_for_each_entry(format_item, &format_item_list, list) > + (*items)[state++] = format_item; > + > + return ret; > + > +err_alloc_format_item: > +err_unknown_state: > +err_new_format_item: > + list_for_each_entry_safe(format_item, format_item_next, > + &format_item_list, list) > + kfree(format_item); > + return ret; > +} > + > +/* need have format_lock */ > +static struct marker_format *_find_marker_format(const char *name) > +{ > + struct marker_format *format; > + list_for_each_entry(format, &marker_format_head, list) { > + if (!strcmp(name, format->name)) > + return format; > + } > + return NULL; > +} > + > +/* > + * For add_marker_format internal use > + * need have format_lock > + */ > +static struct marker_format *_new_marker_format(struct marker *marker) > +{ > + struct marker_format *format; > + char *tmp; > + > + format = kmalloc(sizeof(struct marker_format), GFP_KERNEL); > + if (!format) > + goto err_alloc_format; > + > + atomic_set(&format->refcnt, 1); > + > + INIT_LIST_HEAD(&format->list); > + > + tmp = kmalloc(strlen(marker->name) + 1, GFP_KERNEL); > + if (!tmp) > + goto err_alloc_name; > + strcpy(tmp, marker->name); > + format->name = tmp; > + > + tmp = kmalloc(strlen(marker->format) + 1, GFP_KERNEL); > + if (!tmp) > + goto err_alloc_formatstr; > + strcpy(tmp, marker->format); > + format->format = tmp; > + > + format->format_item_cnt = parse_format(marker->format, > + &format->items); > + if (IS_ERR_VALUE(format->format_item_cnt)) { > + printk(KERN_ERR "%s: parse format failed: %s\n", __func__, > + marker->format); > + goto err_parse_file; > + } > + > + list_add(&format->list, &marker_format_head); > + > + blocking_notifier_call_chain(&format_notifier_list, ADD_FORMAT, format); > + > + return format; > + > +err_parse_file: > + kfree(format->format); > +err_alloc_formatstr: > + kfree(format->name); > +err_alloc_name: > + kfree(format); > +err_alloc_format: > + return NULL; > +} > + > +/* need have format_lock */ > +static void _destroy_marker_format(struct marker_format *format) > +{ > + int i; > + > + blocking_notifier_call_chain(&format_notifier_list, DEL_FORMAT, format); > + > + list_del(&format->list); > + > + for (i = 0; i < format->format_item_cnt; i++) > + kfree(format->items[i]); > + kfree(format->items); > + kfree(format->format); > + kfree(format->name); > + kfree(format); > + > + return; > +} > + > +static struct marker_format *add_marker_format(struct marker *marker) > +{ > + struct marker_format *format; > + > + mutex_lock(&format_lock); > + > + format = _find_marker_format(marker->name); > + if (!format) { > + format = _new_marker_format(marker); > + if (!format) { > + printk(KERN_ERR > + "%s: new format struct for %s failed\n", > + __func__, marker->name); > + goto err_alloc_format; > + } > + } else { > + atomic_inc(&format->refcnt); > + } > + > + mutex_unlock(&format_lock); > + return format; > + > +err_alloc_format: > + mutex_unlock(&format_lock); > + return NULL; > +} > + > +static int put_marker_format(const char *name) > +{ > + struct marker_format *format; > + int err = 0; > + > + mutex_lock(&format_lock); > + > + format = _find_marker_format(name); > + if (!format) { > + printk(KERN_ERR "%s: can't find format item for %s\n", > + __func__, name); > + err = -ENOENT; > + goto err_find_marker_format; > + } > + > + if (atomic_dec_and_test(&format->refcnt)) > + _destroy_marker_format(format); > + > + mutex_unlock(&format_lock); > + return 0; > + > +err_find_marker_format: > + mutex_unlock(&format_lock); > + return err; > +} > + > +static void destroy_all_format(void) > +{ > + struct marker_format *format, *next; > + > + mutex_lock(&format_lock); > + > + list_for_each_entry_safe(format, next, &marker_format_head, list) { > + _destroy_marker_format(format); > + } > + > + mutex_unlock(&format_lock); > + > + return; > +} > + > +static int bulid_exist_format(void) > +{ > + struct marker_iter iter; > + struct marker_format *format; > + int err = 0; > + > + if (!list_empty(&marker_format_head)) { > + err = -EEXIST; > + goto err_not_empty; > + } > + > + marker_iter_reset(&iter); > + marker_iter_start(&iter); > + for (; iter.marker != NULL; marker_iter_next(&iter)) { > + format = add_marker_format(iter.marker); > + if (!format) { > + err = -EIO; > + goto err_build_fail; > + } > + } > + marker_iter_stop(&iter); > + return 0; > + > +err_build_fail: > + destroy_all_format(); > +err_not_empty: > + return err; > +} > + > +static int module_notify(struct notifier_block *self, > + unsigned long val, void *data) > +{ > + struct module *mod = data; > + struct marker *iter; > + > + switch (val) { > + case MODULE_STATE_COMING: > + for (iter = mod->markers; > + iter < mod->markers + mod->num_markers; iter++) > + add_marker_format(iter); > + break; > + case MODULE_STATE_GOING: > + for (iter = mod->markers; > + iter < mod->markers + mod->num_markers; iter++) > + put_marker_format(iter->name); > + break; > + } > + return NOTIFY_OK; > +} > + > +static struct notifier_block module_nb = { > + .notifier_call = module_notify, > +}; > + > +static int marker_format_init(void) > +{ > + int err = 0; > + > + err = bulid_exist_format(); > + if (IS_ERR_VALUE(err)) { > + printk(KERN_ERR "%s: bulid_exist_format failed\n", __func__); > + goto err_add_exist_filter; > + } > + > + /* Todo: > + * If a module is insert/delete here(between > + * bulid_exist_format() and register_module_notifier()), > + * marker_format struct is not uptodate. > + */ > + > + err = register_module_notifier(&module_nb); > + if (IS_ERR_VALUE(err)) { > + printk(KERN_ERR "%s register module notifier failed\n", > + __func__); > + goto err_register_module_notifier; > + } > + > + return 0; > + > +err_register_module_notifier: > + destroy_all_format(); > +err_add_exist_filter: > + return err; > +} > + > +static void marker_format_destroy(void) > +{ > + unregister_module_notifier(&module_nb); > + destroy_all_format(); > +} > + > +static int register_format_notifier(struct notifier_block *nb) > +{ > + return blocking_notifier_chain_register(&format_notifier_list, nb); > +} > + > +static int unregister_format_notifier(struct notifier_block *nb) > +{ > + return blocking_notifier_chain_unregister(&format_notifier_list, nb); > +} > + > + > +/* > + * Control operations > + * > + * Maintenance a dir struct and datastruct for control filter > + * dir struct will dynamical updated when marker_format got changed > + * > + * Todo: > + * Control per-trace filter > + * > + */ > + > +enum compare_op { > + gt, > + ge, > + eq, > + le, > + lt, > +}; > +const char *compare_op_string[] = { > + ">", > + ">=", > + "=", > + "<=", > + "<", > +}; > + > +struct marker_filter; > +struct filter_item { > + int enable; > + union { > + struct { > + enum compare_op op; > + long long value; > + } signed_type; > + struct { > + enum compare_op op; > + unsigned long long value; > + } unsigned_type; > + struct { > + enum compare_op op; > + char value[PATH_MAX]; /* Todo: Make it dynamic */ > + } string_type; > + struct { > + enum compare_op op; > + unsigned char value; > + } char_type; > + } setting; > + struct marker_filter *parent; > + struct dentry *den[2]; /* [0]: __argX, [1]: symbol link to __argX */ > +}; > + > +struct marker_filter { > + struct list_head list; > + struct marker_format *format; > + struct filter_item *items; > + struct dentry *rootden; > + struct dentry *formatden; > + /* > + * is any of items[] are enabled > + * for bypass marker which haven't any filter fast > + */ > + int enable; > +}; > +static inline unsigned int filter_item_no(struct filter_item *filter_item) > +{ > + return filter_item - filter_item->parent->items; > +} > +static inline struct format_item *filter_item_format( > + struct filter_item *filter_item) > +{ > + return filter_item->parent->format->items[filter_item_no(filter_item)]; > +} > + > +static LIST_HEAD(marker_filter_head); > + > +/* > + * Use spinlock to lock filter struct because we can't use mutex in filter > + * callback > + */ > +static DEFINE_SPINLOCK(filter_lock); > + > +/* > + * We need call some kernel function in ltt-filter function, but many kernel > + * function have tp/markers which cause cpu jump to ltt, then jump to > + * ltt-filter from ltt code. > + * Even if we don't call any kernel function in filter code, interrupt will > + * do similar thing. > + * To make it easy, we avoid this type of nesting by lock/unlock_filter. > + */ > +DEFINE_PER_CPU(atomic_t, filter_nesting) = ATOMIC_INIT(0); > +static inline int lock_filter(void) > +{ > + preempt_disable(); > + if (atomic_xchg(&__get_cpu_var(filter_nesting), 1) > 0) { > + /* Already in lock */ > + preempt_enable(); > + return 1; > + } > + return 0; > +} > +static inline void unlock_filter(void) > +{ > + atomic_set(&__get_cpu_var(filter_nesting), 0); > + preempt_enable(); > +} Hrm, this lock_filter seems broken. If you have an interrupt nesting over lock_filter, and takes lock_filter again, the interrupt will set the filter_nesting to 0, which creates a race. Basically, what you want is to disallow filter modification used by an active tracing session _or_, if you want to allow filter modification while a tracing session is active. But to do it properly, you want to use RCU data structures synchronized with call_rcu_sched and synchronize_sched and use the RCU read lock (which is _already_ taken by LTTng around probe execution anyway) to synchronize. The filter_lock spinlock should probably become a simple mutex too. Mathieu > + > +struct marker_filter *_find_marker_filter(const char *name) > +{ > + struct marker_filter *filter; > + list_for_each_entry(filter, &marker_filter_head, list) { > + if (!strcmp(name, filter->format->name)) > + return filter; > + } > + return NULL; > +} > + > +static ssize_t format_read(struct file *filp, char __user *ubuf, size_t cnt, > + loff_t *ppos) > +{ > + size_t ret; > + struct marker_filter *filter; > + > + lock_filter(); > + spin_lock(&filter_lock); > + filter = filp->f_dentry->d_inode->i_private; > + if (filter) { > + ret = simple_read_from_buffer(ubuf, cnt, ppos, > + filter->format->format, strlen(filter->format->format)); > + } else { > + ret = -EIO; > + } > + spin_unlock(&filter_lock); > + unlock_filter(); > + > + return ret; > +} > + > +static const struct file_operations format_fops = { > + .read = format_read, > +}; > + > +static int signed_type_read(char *buf, size_t bufsize, > + struct filter_item *filter_item) > +{ > + int ret; > + int pos = 0; > + > + ret = scnprintf(buf + pos, bufsize - pos, "%s", > + compare_op_string[filter_item->setting.signed_type.op]); > + if (!ret) > + return 0; > + pos += ret; > + > + ret = scnprintf(buf + pos, bufsize - pos, " %lld", > + filter_item->setting.signed_type.value); > + if (!ret) > + return 0; > + pos += ret; > + > + return pos; > +} > + > +static int unsigned_type_read(char *buf, size_t bufsize, > + struct filter_item *filter_item) > +{ > + int ret; > + int pos = 0; > + > + ret = scnprintf(buf + pos, bufsize - pos, "%s", > + compare_op_string[filter_item->setting.unsigned_type.op]); > + if (!ret) > + return 0; > + pos += ret; > + > + ret = scnprintf(buf + pos, bufsize - pos, " %llu", > + filter_item->setting.unsigned_type.value); > + if (!ret) > + return 0; > + pos += ret; > + > + return pos; > +} > + > +static int string_type_read(char *buf, size_t bufsize, > + struct filter_item *filter_item) > +{ > + int ret; > + int pos = 0; > + > + ret = scnprintf(buf + pos, bufsize - pos, "%s", > + compare_op_string[filter_item->setting.string_type.op]); > + if (!ret) > + return 0; > + pos += ret; > + > + ret = scnprintf(buf + pos, bufsize - pos, " %s", > + filter_item->setting.string_type.value); > + if (!ret) > + return 0; > + pos += ret; > + > + return pos; > +} > + > +static int char_type_read(char *buf, size_t bufsize, > + struct filter_item *filter_item) > +{ > + int ret; > + int pos = 0; > + > + ret = scnprintf(buf + pos, bufsize - pos, "%s", > + compare_op_string[filter_item->setting.char_type.op]); > + if (!ret) > + return 0; > + pos += ret; > + > + switch (filter_item->setting.char_type.value) { > + case '\t': > + ret = scnprintf(buf + pos, bufsize - pos, " \\t"); > + break; > + case ' ': > + ret = scnprintf(buf + pos, bufsize - pos, " blank"); > + break; > + case '\n': > + ret = scnprintf(buf + pos, bufsize - pos, " \\n"); > + break; > + default: > + if (isgraph(filter_item->setting.char_type.value)) { > + ret = scnprintf(buf + pos, bufsize - pos, " %c", > + filter_item->setting.char_type.value); > + } else { > + ret = scnprintf(buf + pos, bufsize - pos, > + " \\%u", (unsigned int) > + filter_item->setting.char_type.value); > + } > + break; > + } > + if (!ret) > + return 0; > + pos += ret; > + > + return pos; > +} > + > +static ssize_t type_read(struct file *filp, char __user *ubuf, size_t cnt, > + loff_t *ppos) > +{ > + size_t ret; > + struct filter_item *filter_item; > + > + lock_filter(); > + spin_lock(&filter_lock); > + filter_item = filp->f_dentry->d_inode->i_private; > + if (filter_item) { > + struct format_item *format_item; > + char buf[128]; > + int pos = 0; > + > + format_item = filter_item_format(filter_item); > + > + ret = scnprintf(buf + pos, sizeof(buf) - pos, > + "type: %u byte(s) %s\n", > + format_item->size, > + format_item_type_string[format_item->type]); > + if (!ret) { > + ret = -EINVAL; > + goto err_format_string; > + } > + pos += ret; > + > + ret = scnprintf(buf + pos, sizeof(buf) - pos, "setting: "); > + if (!ret) { > + ret = -EINVAL; > + goto err_format_string; > + } > + pos += ret; > + > + if (!filter_item->enable) { > + ret = scnprintf(buf + pos, sizeof(buf) - pos, "*"); > + if (!ret) { > + ret = -EINVAL; > + goto err_format_string; > + } > + pos += ret; > + goto format_finish; > + } > + > + switch (format_item->type) { > + case SIGNED: > + ret = signed_type_read(buf + pos, sizeof(buf) - pos, > + filter_item); > + break; > + case UNSIGNED: > + ret = unsigned_type_read(buf + pos, sizeof(buf) - pos, > + filter_item); > + break; > + case STRING: > + ret = string_type_read(buf + pos, sizeof(buf) - pos, > + filter_item); > + break; > + case CHAR: > + ret = char_type_read(buf + pos, sizeof(buf) - pos, > + filter_item); > + break; > + default: > + /* should not run to here */ > + printk(KERN_ERR "%s: unknown type %d\n", > + __func__, format_item->type); > + ret = -EIO; > + goto err_unknown_type; > + } > + if (!ret) { > + ret = -EINVAL; > + goto err_format_string; > + } > + pos += ret; > + > +format_finish: > + ret = scnprintf(buf + pos, sizeof(buf) - pos, "\n"); > + if (!ret) { > + ret = -EINVAL; > + goto err_format_string; > + } > + pos += ret; > + > + ret = simple_read_from_buffer(ubuf, cnt, ppos, buf, > + strlen(buf)); > + } else { > + ret = -EIO; > + } > + > +err_format_string: > +err_unknown_type: > + spin_unlock(&filter_lock); > + unlock_filter(); > + > + return ret; > +} > + > +/* > + * Check op info in a input string > + * return length of op part on success, *op is set. > + * return 0 on fail > + */ > +static size_t scan_op(const char *str, enum compare_op *op) > +{ > + /* Must reverse sort with strlen */ > + static struct { > + const char *str; > + enum compare_op op; > + } str_op[] = { > + {"gt", gt}, > + {"ge", ge}, > + {"eq", eq}, > + {"le", le}, > + {"lt", lt}, > + {">=", ge}, > + {"<=", le}, > + {">", gt}, > + {"=", eq}, > + {"<", lt}, > + }; > + int i; > + > + for (i = 0; i < ARRAY_SIZE(str_op); i++) { > + if (!strncmp(str, str_op[i].str, strlen(str_op[i].str))) { > + *op = str_op[i].op; > + return strlen(str_op[i].str); > + } > + } > + > + return 0; > +} > + > +static int signed_type_write(const char *buf, struct format_item *format_item, > + struct filter_item *filter_item) > +{ > + int oplen; > + enum compare_op op; > + long long value = 0; > + > + oplen = scan_op(buf, &op); > + if (!oplen) > + return -EINVAL; > + > + switch (format_item->size) { > + case 1: > + { > + char tmp; > + if (sscanf(buf + oplen, "%hhd", &tmp) != 1) > + return -EINVAL; > + value = tmp; > + } > + break; > + case 2: > + { > + short int tmp; > + if (sscanf(buf + oplen, "%hd", &tmp) != 1) > + return -EINVAL; > + value = tmp; > + } > + break; > + case 4: > + { > + int tmp; > + if (sscanf(buf + oplen, "%d", &tmp) != 1) > + return -EINVAL; > + value = tmp; > + } > + break; > + case 8: > + if (sscanf(buf + oplen, "%lld", &value) != 1) > + return -EINVAL; > + break; > + default: > + /* should not run to here */ > + printk(KERN_ERR "%s: unknown size %zu\n", __func__, > + format_item->size); > + return -EIO; > + } > + > + filter_item->setting.signed_type.op = op; > + filter_item->setting.signed_type.value = value; > + > + return 0; > +} > + > +static int unsigned_type_write(const char *buf, struct format_item *format_item, > + struct filter_item *filter_item) > +{ > + int oplen; > + enum compare_op op; > + long long value = 0; > + > + oplen = scan_op(buf, &op); > + if (!oplen) > + return -EINVAL; > + > + switch (format_item->size) { > + case 1: > + { > + unsigned char tmp; > + if (sscanf(buf + oplen, "%hhu", &tmp) != 1) > + return -EINVAL; > + value = tmp; > + } > + break; > + case 2: > + { > + unsigned short int tmp; > + if (sscanf(buf + oplen, "%hu", &tmp) != 1) > + return -EINVAL; > + value = tmp; > + } > + break; > + case 4: > + { > + unsigned int tmp; > + if (sscanf(buf + oplen, "%u", &tmp) != 1) > + return -EINVAL; > + value = tmp; > + } > + break; > + case 8: > + if (sscanf(buf + oplen, "%llu", &value) != 1) > + return -EINVAL; > + break; > + default: > + /* should not run to here */ > + printk(KERN_ERR "%s: unknown size %zu\n", __func__, > + format_item->size); > + return -EIO; > + } > + > + filter_item->setting.unsigned_type.op = op; > + filter_item->setting.unsigned_type.value = value; > + > + return 0; > +} > + > +static int string_type_write(const char *buf, struct format_item *format_item, > + struct filter_item *filter_item) > +{ > + int oplen; > + enum compare_op op; > + int val_len; > + > + oplen = scan_op(buf, &op); > + if (!oplen) > + return -EINVAL; > + > + /* End string on \n */ > + for (val_len = 0; *(buf + oplen + val_len); val_len++) { > + if (*(buf + oplen + val_len) == '\n') > + break; > + } > + > + if (val_len > ARRAY_SIZE(filter_item->setting.string_type.value) - 1) > + return -EINVAL; > + > + filter_item->setting.string_type.op = op; > + memcpy(filter_item->setting.string_type.value, buf + oplen, val_len); > + filter_item->setting.string_type.value[val_len] = 0; > + > + return 0; > +} > + > +static int char_type_write(const char *buf, struct format_item *format_item, > + struct filter_item *filter_item) > +{ > + int oplen; > + enum compare_op op; > + unsigned char value; > + int val_start; > + > + oplen = scan_op(buf, &op); > + if (!oplen) > + return -EINVAL; > + > + /* bypass blanks */ > + for (val_start = 0; *(buf + oplen + val_start) == ' ' || > + *(buf + oplen + val_start) == '\t'; val_start++) > + ; > + > + /* > + * parse escape string, following escape string are supported: > + * "\n" > + * "\[num]" > + * "\[char expect \n and \[num]]", as "\\", "\ ", ... > + */ > + switch (*(buf + oplen + val_start)) { > + case '\0': > + case '\n': > + return -EINVAL; > + case '\\': > + switch (*(buf + oplen + val_start + 1)) { > + case '\0': > + case '\n': > + return -EINVAL; > + case 'n': > + value = '\n'; > + break; > + default: > + if (isdigit(*(buf + oplen + val_start + 1))) { > + if (sscanf(buf + oplen + val_start + 1, "%hhu", > + &value) != 1) > + return -EINVAL; > + } else { > + value = > + (*(buf + oplen + val_start + 1)); > + } > + break; > + } > + break; > + default: > + value = *(buf + oplen + val_start); > + break; > + } > + > + filter_item->setting.char_type.op = op; > + filter_item->setting.char_type.value = value; > + > + return 0; > +} > + > +static ssize_t type_write(struct file *filp, const char __user *ubuf, > + size_t cnt, loff_t *ppos) > +{ > + size_t ret; > + struct filter_item *filter_item; > + > + lock_filter(); > + spin_lock(&filter_lock); > + filter_item = filp->f_dentry->d_inode->i_private; > + if (filter_item) { > + struct format_item *format_item; > + char buf[NAME_MAX]; > + size_t buf_size; > + int val_start; > + > + format_item = filter_item_format(filter_item); > + > + buf_size = min(cnt, sizeof(buf) - 1); > + if (copy_from_user(buf, ubuf, buf_size)) { > + ret = -EFAULT; > + goto err_copy_from_user; > + } > + buf[buf_size] = 0; > + > + /* Check is to disable filter */ > + for (val_start = 0; *(buf + val_start) == ' ' || > + *(buf + val_start) == '\t'; val_start++) > + ; > + if (!strncmp(buf + val_start, "*", strlen("*")) > + || !strncmp(buf + val_start, "off", strlen("off"))) { > + struct marker_filter *filter; > + int i; > + > + filter_item->enable = 0; > + > + /* > + * rescan all items in this marker to > + * update marker_filter->enable > + */ > + filter = filter_item->parent; > + filter->enable = 0; > + for (i = 0; i < filter->format->format_item_cnt; i++) { > + if (filter->items[i].enable) { > + filter->enable = 1; > + break; > + } > + } > + > + goto scan_finish; > + } > + > + switch (format_item->type) { > + case SIGNED: > + ret = signed_type_write(buf, format_item, filter_item); > + break; > + case UNSIGNED: > + ret = unsigned_type_write(buf, format_item, > + filter_item); > + break; > + case STRING: > + ret = string_type_write(buf, format_item, filter_item); > + break; > + case CHAR: > + ret = char_type_write(buf, format_item, filter_item); > + break; > + default: > + /* should not run to here */ > + printk(KERN_ERR "%s: unknown type %d\n", > + __func__, format_item->type); > + ret = -EIO; > + goto err_unknown_type; > + } > + if (IS_ERR_VALUE(ret)) > + goto err_scan_string; > + > + filter_item->enable = 1; > + filter_item->parent->enable = 1; > + > +scan_finish: > + ret = cnt; > + } else { > + ret = -EIO; > + } > + > +err_scan_string: > +err_unknown_type: > +err_copy_from_user: > + spin_unlock(&filter_lock); > + unlock_filter(); > + > + return ret; > +} > + > +static const struct file_operations type_fops = { > + .read = type_read, > + .write = type_write, > +}; > + > +static int init_filter_items(struct marker_filter *filter, > + struct marker_format *format) > +{ > + int err = 0; > + int i; > + > + for (i = 0; i < format->format_item_cnt; i++) { > + char fname[16]; > + > + filter->items[i].parent = filter; > + filter->items[i].enable = 0; > + > + /* Create type file as __arg1, __arg2 */ > + sprintf(fname, "__arg%d", i + 1); > + filter->items[i].den[0] = debugfs_create_file(fname, S_IRUGO, > + filter->rootden, &filter->items[i], &type_fops); > + if (IS_ERR(filter->items[i].den[0]) > + || !filter->items[i].den[0]) { > + printk(KERN_ERR > + "%s: create type file of %s for %s failed\n", > + __func__, fname, filter->format->name); > + err = -EIO; > + goto err_build_typefile; > + } > + > + /* Create symbol link to type file */ > + if (filter->format->items[i]->name[0]) { > + > + filter->items[i].den[1] = debugfs_create_symlink( > + filter->format->items[i]->name, > + filter->rootden, fname); > + if (IS_ERR(filter->items[i].den[1]) > + || !filter->items[i].den[1]) { > + /* > + * same-name type is allowed, but it will cause > + * create symlink fail. > + * we ignore this type of error. > + */ > + filter->items[i].den[1] = NULL; > + } > + } > + } > + > + return 0; > + > +err_build_typefile: > + for (i--; i >= 0; i--) { > + /* see comments in _destroy_marker_filter() for why set NULL */ > + filter->items[i].den[0]->d_inode->i_private = NULL; > + debugfs_remove(filter->items[i].den[0]); > + debugfs_remove(filter->items[i].den[1]); > + } > + return err; > +} > + > +static int add_marker_filter(struct marker_format *format) > +{ > + int err = 0; > + struct marker_filter *filter; > + > + lock_filter(); > + spin_lock(&filter_lock); > + > +#ifdef _DEBUG > + if (_find_marker_filter(format->name)) { > + printk(KERN_ERR "%s: filter %s already exist\n", __func__, > + format->name); > + err = -EIO; > + goto err_already_exist; > + } > +#endif /* _DEBUG */ > + > + filter = kmalloc(sizeof(struct marker_filter), GFP_KERNEL); > + if (!filter) { > + printk(KERN_ERR "%s: alloc struct marker_filter failed\n", > + __func__); > + err = -ENOMEM; > + goto err_alloc_marker_filter; > + } > + > + INIT_LIST_HEAD(&filter->list); > + > + filter->format = format; > + > + filter->rootden = debugfs_create_dir(format->name, ltt_filter_dir); > + if (IS_ERR(filter->rootden) || !filter->rootden) { > + printk(KERN_ERR "%s: create dir of %s failed\n", __func__, > + format->name); > + err = -EIO; > + goto err_create_dir; > + } > + > + filter->formatden = debugfs_create_file("__arg0", S_IRUGO, > + filter->rootden, filter, &format_fops); > + if (IS_ERR(filter->formatden) || !filter->formatden) { > + printk(KERN_ERR > + "%s: create typefile failed\n", __func__); > + err = -EIO; > + goto err_build_typefile; > + } > + > + if (format->format_item_cnt) { > + filter->items = kmalloc(sizeof(struct filter_item) > + * format->format_item_cnt, GFP_KERNEL); > + if (!filter->items) { > + printk(KERN_ERR > + "%s: alloc filter's items failed\n", __func__); > + err = -ENOMEM; > + goto err_alloc_types; > + } > + err = init_filter_items(filter, format); > + if (IS_ERR_VALUE(err)) { > + printk(KERN_ERR > + "%s: init filter's items failed\n", __func__); > + goto err_init_types; > + } > + } else { > + filter->items = NULL; > + } > + > + filter->enable = 0; > + > + list_add(&filter->list, &marker_filter_head); > + > + spin_unlock(&filter_lock); > + unlock_filter(); > + > + return 0; > + > +err_init_types: > + kfree(filter->items); > +err_alloc_types: > + /* see comments in _destroy_marker_filter() for why set NULL */ > + filter->formatden->d_inode->i_private = NULL; > + debugfs_remove(filter->formatden); > +err_build_typefile: > + debugfs_remove(filter->rootden); > +err_create_dir: > + kfree(filter); > +err_alloc_marker_filter: > +#ifdef _DEBUG > +err_already_exist: > +#endif /* _DEBUG */ > + spin_unlock(&filter_lock); > + unlock_filter(); > + return err; > +} > + > +static void _destroy_marker_filter(struct marker_filter *filter) > +{ > + int i; > + > + list_del(&filter->list); > + > + for (i = 0; i < filter->format->format_item_cnt; i++) { > + filter->items[i].den[0]->d_inode->i_private = NULL; > + debugfs_remove(filter->items[i].den[0]); > + debugfs_remove(filter->items[i].den[1]); > + } > + kfree(filter->items); > + > + /* > + * Set i_private to NULL, so that if some fileop in process, > + * it can avoid illegal access using i_private in fileop. > + * (we must get filter_lock in fileop to ensure no other change > + * in filter struct) > + */ > + filter->formatden->d_inode->i_private = NULL; > + debugfs_remove(filter->formatden); > + > + debugfs_remove(filter->rootden); > + > + kfree(filter); > + > + return; > +} > + > +static void destroy_all_filter(void) > +{ > + struct marker_filter *filter, *next; > + > + lock_filter(); > + spin_lock(&filter_lock); > + list_for_each_entry_safe(filter, next, &marker_filter_head, list) { > + _destroy_marker_filter(filter); > + } > + spin_unlock(&filter_lock); > + unlock_filter(); > +} > + > +static int bulid_exist_filter(void) > +{ > + int err; > + struct marker_format *format; > + > + if (!list_empty(&marker_filter_head)) { > + err = -EEXIST; > + goto err_not_empty; > + } > + > + mutex_lock(&format_lock); > + list_for_each_entry(format, &marker_format_head, list) { > + err = add_marker_filter(format); > + if (IS_ERR_VALUE(err)) { > + printk("%s: add_marker_filter failed\n", __func__); > + goto err_add_filter; > + } > + } > + mutex_unlock(&format_lock); > + > + return 0; > + > +err_add_filter: > + destroy_all_filter(); > + mutex_unlock(&format_lock); > +err_not_empty: > + return err; > +} > + > +static int format_notify(struct notifier_block *self, unsigned long val, > + void *data) > +{ > + struct marker_format *format = data; > + struct marker_filter *filter, *next; > + > + switch (val) { > + case ADD_FORMAT: > + add_marker_filter(format); > + break; > + case DEL_FORMAT: > + lock_filter(); > + spin_lock(&filter_lock); > + list_for_each_entry_safe(filter, next, &marker_filter_head, > + list) { > + if (filter->format == format) > + _destroy_marker_filter(filter); > + } > + spin_unlock(&filter_lock); > + unlock_filter(); > + break; > + } > + return NOTIFY_OK; > +} > + > +static struct notifier_block format_nb = { > + .notifier_call = format_notify, > +}; > + > +static int marker_filter_init(void) > +{ > + int err = 0; > + > + err = bulid_exist_filter(); > + if (IS_ERR_VALUE(err)) { > + printk(KERN_ERR "%s: bulid_exist_filter failed\n", __func__); > + goto err_add_exist_filter; > + } > + > + err = register_format_notifier(&format_nb); > + if (IS_ERR_VALUE(err)) { > + printk(KERN_ERR "%s register format notifier failed\n", > + __func__); > + goto err_register_format_notifier; > + } > + > + return 0; > + > +err_register_format_notifier: > + destroy_all_filter(); > +err_add_exist_filter: > + return err; > +} > + > +static void marker_filter_destroy(void) > +{ > + unregister_format_notifier(&format_nb); > + destroy_all_filter(); > +} > + > + > +/* > + * Filter operations > + * > + * define filter callback for lttng to determine if to output a event > + * by user's setting. > + * > + */ > + > +/* > + * Ret: > + * 0: filted > + * 1: passed > + * <0: err > + */ > +static int signed_filter(struct format_item *format_item, > + struct filter_item *filter_item, va_list *args) > +{ > + long long int val; > + > + switch (format_item->size) { > + case 1: > + val = (char)va_arg(*args, int); > + break; > + case 2: > + val = (short int)va_arg(*args, int); > + break; > + case 4: > + val = va_arg(*args, int); > + break; > + case 8: > + val = va_arg(*args, long long int); > + break; > + default: > + /* should not run to here */ > + printk(KERN_ERR "%s: unknown size %d\n", __func__, > + format_item->size); > + return -1; > + } > + > + /* > + * We judge filter_item->enable here, because even if filter is not > + * enabled, we still need to bypass this data in va_list > + */ > + if (!filter_item->enable) > + return 1; > + > + switch (filter_item->setting.signed_type.op) { > + case gt: > + return val > filter_item->setting.signed_type.value; > + case ge: > + return val >= filter_item->setting.signed_type.value; > + case eq: > + return val == filter_item->setting.signed_type.value; > + case le: > + return val <= filter_item->setting.signed_type.value; > + case lt: > + return val < filter_item->setting.signed_type.value; > + } > + > + /* should not run to here */ > + return -1; > +} > + > +static int unsigned_filter(struct format_item *format_item, > + struct filter_item *filter_item, va_list *args) > +{ > + unsigned long long int val; > + > + switch (format_item->size) { > + case 1: > + val = (unsigned char)va_arg(*args, unsigned int); > + break; > + case 2: > + val = (short int)va_arg(*args, unsigned int); > + break; > + case 4: > + val = va_arg(*args, unsigned int); > + break; > + case 8: > + val = va_arg(*args, unsigned long long int); > + break; > + default: > + /* should not run to here */ > + printk(KERN_ERR "%s: unknown size %d\n", __func__, > + format_item->size); > + return -1; > + } > + > + /* > + * We judge filter_item->enable here, because even if filter is not > + * enabled, we still need to bypass this data in va_list > + */ > + if (!filter_item->enable) > + return 1; > + > + switch (filter_item->setting.unsigned_type.op) { > + case gt: > + return val > filter_item->setting.unsigned_type.value; > + case ge: > + return val >= filter_item->setting.unsigned_type.value; > + case eq: > + return val == filter_item->setting.unsigned_type.value; > + case le: > + return val <= filter_item->setting.unsigned_type.value; > + case lt: > + return val < filter_item->setting.unsigned_type.value; > + } > + > + /* should not run to here */ > + return -1; > +} > + > +static int string_filter(struct format_item *format_item, > + struct filter_item *filter_item, va_list *args) > +{ > + const char *val; > + > + val = va_arg(*args, const char *); > + > + /* > + * We judge filter_item->enable here, because even if filter is not > + * enabled, we still need to bypass this data in va_list > + */ > + if (!filter_item->enable) > + return 1; > + > + switch (filter_item->setting.string_type.op) { > + case gt: > + return strcmp(val, filter_item->setting.string_type.value) > 0; > + case ge: > + return strcmp(val, filter_item->setting.string_type.value) >= 0; > + case eq: > + return strcmp(val, filter_item->setting.string_type.value) == 0; > + case le: > + return strcmp(val, filter_item->setting.string_type.value) <= 0; > + case lt: > + return strcmp(val, filter_item->setting.string_type.value) < 0; > + } > + > + /* should not run to here */ > + return -1; > +} > + > +static int char_filter(struct format_item *format_item, > + struct filter_item *filter_item, va_list *args) > +{ > + unsigned char val; > + > + val = (unsigned char)va_arg(*args, unsigned int); > + > + /* > + * We judge filter_item->enable here, because even if filter is not > + * enabled, we still need to bypass this data in va_list > + */ > + if (!filter_item->enable) > + return 1; > + > + switch (filter_item->setting.char_type.op) { > + case gt: > + return val > filter_item->setting.char_type.value; > + case ge: > + return val >= filter_item->setting.char_type.value; > + case eq: > + return val == filter_item->setting.char_type.value; > + case le: > + return val <= filter_item->setting.char_type.value; > + case lt: > + return val < filter_item->setting.char_type.value; > + } > + > + /* should not run to here */ > + return -1; > +} > + > +static int ltt_filter(struct ltt_trace_struct *trace, > + const struct marker *mdata, va_list *args) > +{ > + int ret; > + struct marker_filter *filter; > + struct marker_format *format; > + int i; > + > + if (lock_filter()) { Hrm do you allow session filter modification when tracing is active for this session ? I think the lock_filter approach you are taking here is wrong. > + /* Don't output events caused by ltt-filter */ > + return 0; > + } > + > + spin_lock(&filter_lock); > + > + filter = _find_marker_filter(mdata->name); > + if (!filter) { > + printk(KERN_ERR "%s: unknown marker %s\n", __func__, > + mdata->name); > + ret = 1; > + goto done; > + } > + > + /* Bypass if no filter setting on this marker */ > + if (!filter->enable) { > + ret = 1; > + goto done; > + } > + > + format = filter->format; > + > + for (i = 0; i < format->format_item_cnt; i++) { > + int result; > + > + switch (format->items[i]->type) { > + case SIGNED: > + result = signed_filter(format->items[i], > + filter->items + i, args); > + break; > + case UNSIGNED: > + result = unsigned_filter(format->items[i], > + filter->items + i, args); > + break; > + case STRING: > + result = string_filter(format->items[i], > + filter->items + i, args); > + break; > + case CHAR: > + result = char_filter(format->items[i], > + filter->items + i, args); > + break; > + default: > + /* should not run to here */ > + printk(KERN_ERR "%s: unknown type %d\n", __func__, > + format->items[i]->type); > + ret = 1; > + goto done; > + } > + if (result < 0) { > + ret = 1; > + goto done; > + } > + if (!result) { > + ret = 0; > + goto done; > + } > + } > + > + ret = 1; > + > +done: > + spin_unlock(&filter_lock); > + unlock_filter(); > + return ret; > +} > + > + > +/* > + * Init/exit option of module > + * > + * Init marker_format and marker_filter operations > + * register filter callback in ltt > + * > + */ > + > +static int __init ltt_filter_init(void) > +{ > + int err = 0; > + struct dentry *ltt_root_dentry; > + > + ltt_root_dentry = get_ltt_root(); > + if (!ltt_root_dentry) { > + err = -ENOENT; > + goto err_no_root; > + } > + > + ltt_filter_dir = debugfs_create_dir(LTT_FILTER_DIR, ltt_root_dentry); > + if (IS_ERR(ltt_filter_dir) || !ltt_filter_dir) { > + printk(KERN_ERR "%s: create dir of %s failed\n", __func__, > + LTT_FILTER_DIR); > + err = -ENOMEM; > + goto err_create_filter_dir; > + } > + > + err = marker_format_init(); > + if (IS_ERR_VALUE(err)) { > + printk(KERN_ERR "%s: init marker format failed\n", __func__); > + goto err_marker_format_init; > + } > + > + err = marker_filter_init(); > + if (IS_ERR_VALUE(err)) { > + printk(KERN_ERR "%s: init marker filter failed\n", __func__); > + goto err_marker_filter_init; > + } > + > + err = ltt_module_register(LTT_FUNCTION_RUN_FILTER, ltt_filter, > + THIS_MODULE); > + if (IS_ERR_VALUE(err)) { > + printk(KERN_ERR "%s: register ltt filter failed\n", __func__); > + goto err_filter_register; > + } > + > + return 0; > + > +err_filter_register: > + marker_filter_destroy(); > +err_marker_filter_init: > + marker_format_destroy(); > +err_marker_format_init: > + debugfs_remove(ltt_filter_dir); > +err_create_filter_dir: > +err_no_root: > + return err; > +} > + > +static void __exit ltt_filter_exit(void) > +{ > + ltt_module_unregister(LTT_FUNCTION_RUN_FILTER); > + marker_filter_destroy(); > + marker_format_destroy(); > + > + debugfs_remove(ltt_filter_dir); > +} > + > +module_init(ltt_filter_init); > +module_exit(ltt_filter_exit); > + > +MODULE_LICENSE("GPL"); > +MODULE_AUTHOR("Zhao Lei "); > +MODULE_DESCRIPTION("Linux Trace Toolkit Filter"); > -- > 1.5.5.3 > > > > _______________________________________________ > ltt-dev mailing list > ltt-dev at lists.casi.polymtl.ca > http://lists.casi.polymtl.ca/cgi-bin/mailman/listinfo/ltt-dev > -- Mathieu Desnoyers OpenPGP key fingerprint: 8CD5 52C3 8E3C 4140 715F BA06 3F25 A8FE 3BAE 9A68