[PATCH 5/5] core: allow building and running without udev

Aleksander Morgado aleksander at aleksander.es
Thu Sep 29 13:39:03 UTC 2016


Instead of relying on the udev daemon and GUDev to manage the devices reported
by the kernel, we can now run ModemManager relying solely on the kernel events
reported via the new ReportKernelEvent() API. Therefore, the '--no-auto-scan'
option is implicit for the ModemManager daemon when udev is disabled in the
build.

Additionally, a new custom implementation of the kernel device object is
provided, which uses sysfs to load the properties and attributes required in
each kernel device, instead of using a GUdevDevice.

The udev rule files are kept in place, and a simple custom parser is provided
which preloads all rules in memory once and then applies them to the different
kernel objects reported via ReportKernelEvent(), e.g. to set port type hints.
A simple unit test setup is prepared to validate the udev rules during the
`check' Makefile target.
---
 .gitignore                                        |   3 +
 cli/Makefile.am                                   |   7 +-
 cli/mmcli-manager.c                               |  31 +-
 configure.ac                                      |  30 +-
 libmm-glib/mm-common-helpers.c                    |  43 ++
 libmm-glib/mm-common-helpers.h                    |   2 +
 plugins/Makefile.am                               |  38 ++
 plugins/tests/test-udev-rules.c                   | 164 +++++
 src/Makefile.am                                   |  15 +
 src/kerneldevice/mm-kernel-device-generic-rules.c | 446 +++++++++++++
 src/kerneldevice/mm-kernel-device-generic-rules.h |  62 ++
 src/kerneldevice/mm-kernel-device-generic.c       | 772 +++++++++++++++++++++-
 src/kerneldevice/mm-kernel-device-generic.h       |   9 +-
 src/mm-base-manager.c                             |  50 +-
 src/mm-context.c                                  |  27 +-
 src/tests/Makefile.am                             |   5 +-
 src/tests/test-udev-rules.c                       |  79 +++
 test/Makefile.am                                  |  29 +
 test/mmrules.c                                    | 173 +++++
 19 files changed, 1942 insertions(+), 43 deletions(-)
 create mode 100644 plugins/tests/test-udev-rules.c
 create mode 100644 src/kerneldevice/mm-kernel-device-generic-rules.c
 create mode 100644 src/kerneldevice/mm-kernel-device-generic-rules.h
 create mode 100644 src/tests/test-udev-rules.c
 create mode 100644 test/mmrules.c

diff --git a/.gitignore b/.gitignore
index 36fe6c9..123ce20 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,7 @@ Makefile.in
 /src/tests/test-at-serial-port
 /src/tests/test-sms-part-3gpp
 /src/tests/test-sms-part-cdma
+/src/tests/test-udev-rules
 
 /cli/mmcli
 
@@ -158,6 +159,7 @@ Makefile.in
 /uml290/uml290mode
 
 /plugins/test-suite.log
+/plugins/test-udev-rules
 /plugins/test-modem-helpers-huawei*
 /plugins/test-modem-helpers-altair*
 /plugins/test-modem-helpers-cinterion*
@@ -169,6 +171,7 @@ Makefile.in
 
 /test/lsudev
 /test/mmtty
+/test/mmrules
 
 /ModemManager-*-coverage.info
 /ModemManager-*-coverage/
diff --git a/cli/Makefile.am b/cli/Makefile.am
index 7915f28..6c2d791 100644
--- a/cli/Makefile.am
+++ b/cli/Makefile.am
@@ -2,7 +2,6 @@ bin_PROGRAMS = mmcli
 
 mmcli_CPPFLAGS = \
 	$(MMCLI_CFLAGS) \
-        $(GUDEV_CFLAGS) \
 	-I$(top_srcdir) \
 	-I$(top_srcdir)/include \
 	-I$(top_builddir)/include \
@@ -35,11 +34,15 @@ mmcli_SOURCES = \
 	$(NULL)
 
 mmcli_LDADD = \
-        $(GUDEV_LIBS) \
 	$(MMCLI_LIBS) \
 	$(top_builddir)/libmm-glib/libmm-glib.la \
 	$(NULL)
 
+if WITH_UDEV
+mmcli_CPPFLAGS += $(GUDEV_CFLAGS)
+mmcli_LDADD    += $(GUDEV_LIBS)
+endif
+
 completiondir = $(datadir)/bash-completion/completions
 
 install-data-hook:
diff --git a/cli/mmcli-manager.c b/cli/mmcli-manager.c
index 4e078f1..24ecc4e 100644
--- a/cli/mmcli-manager.c
+++ b/cli/mmcli-manager.c
@@ -29,7 +29,9 @@
 #include <glib.h>
 #include <gio/gio.h>
 
-#include <gudev/gudev.h>
+#if WITH_UDEV
+# include <gudev/gudev.h>
+#endif
 
 #define _LIBMM_INSIDE_MMCLI
 #include "libmm-glib.h"
@@ -41,7 +43,9 @@
 typedef struct {
     MMManager *manager;
     GCancellable *cancellable;
+#if WITH_UDEV
     GUdevClient *udev;
+#endif
 } Context;
 static Context *ctx;
 
@@ -51,7 +55,10 @@ static gboolean monitor_modems_flag;
 static gboolean scan_modems_flag;
 static gchar *set_logging_str;
 static gchar *report_kernel_event_str;
+
+#if WITH_UDEV
 static gboolean report_kernel_event_auto_scan;
+#endif
 
 static GOptionEntry entries[] = {
     { "set-logging", 'G', 0, G_OPTION_ARG_STRING, &set_logging_str,
@@ -74,10 +81,12 @@ static GOptionEntry entries[] = {
       "Report kernel event",
       "[\"key=value,...\"]"
     },
+#if WITH_UDEV
     { "report-kernel-event-auto-scan", 0, 0, G_OPTION_ARG_NONE, &report_kernel_event_auto_scan,
       "Automatically report kernel events based on udev notifications",
       NULL
     },
+#endif
     { NULL }
 };
 
@@ -110,8 +119,11 @@ mmcli_manager_options_enabled (void)
                  monitor_modems_flag +
                  scan_modems_flag +
                  !!set_logging_str +
-                 !!report_kernel_event_str +
-                 report_kernel_event_auto_scan);
+                 !!report_kernel_event_str);
+
+#if WITH_UDEV
+    n_actions += report_kernel_event_auto_scan;
+#endif
 
     if (n_actions > 1) {
         g_printerr ("error: too many manager actions requested\n");
@@ -121,8 +133,10 @@ mmcli_manager_options_enabled (void)
     if (monitor_modems_flag)
         mmcli_force_async_operation ();
 
+#if WITH_UDEV
     if (report_kernel_event_auto_scan)
         mmcli_force_async_operation ();
+#endif
 
     checked = TRUE;
     return !!n_actions;
@@ -134,8 +148,11 @@ context_free (Context *ctx)
     if (!ctx)
         return;
 
+#if WITH_UDEV
     if (ctx->udev)
         g_object_unref (ctx->udev);
+#endif
+
     if (ctx->manager)
         g_object_unref (ctx->manager);
     if (ctx->cancellable)
@@ -308,6 +325,8 @@ cancelled (GCancellable *cancellable)
     mmcli_async_operation_done ();
 }
 
+#if WITH_UDEV
+
 static void
 handle_uevent (GUdevClient *client,
                const char  *action,
@@ -324,6 +343,8 @@ handle_uevent (GUdevClient *client,
     g_object_unref (properties);
 }
 
+#endif
+
 static void
 get_manager_ready (GObject      *source,
                    GAsyncResult *result,
@@ -367,6 +388,7 @@ get_manager_ready (GObject      *source,
         return;
     }
 
+#if WITH_UDEV
     if (report_kernel_event_auto_scan) {
         const gchar *subsys[] = { "tty", "usbmisc", "net", NULL };
         guint i;
@@ -400,6 +422,7 @@ get_manager_ready (GObject      *source,
                                NULL);
         return;
     }
+#endif
 
     /* Request to monitor modems? */
     if (monitor_modems_flag) {
@@ -457,10 +480,12 @@ mmcli_manager_run_synchronous (GDBusConnection *connection)
         exit (EXIT_FAILURE);
     }
 
+#if WITH_UDEV
     if (report_kernel_event_auto_scan) {
         g_printerr ("error: monitoring udev events cannot be done synchronously\n");
         exit (EXIT_FAILURE);
     }
+#endif
 
     /* Initialize context */
     ctx = g_new0 (Context, 1);
diff --git a/configure.ac b/configure.ac
index dd3111d..c74d015 100644
--- a/configure.ac
+++ b/configure.ac
@@ -95,7 +95,6 @@ dnl Build dependencies
 dnl
 
 GLIB_MIN_VERSION=2.36.0
-GUDEV_MIN_VERSION=147
 
 PKG_CHECK_MODULES(MM,
                   glib-2.0 >= $GLIB_MIN_VERSION
@@ -121,10 +120,6 @@ PKG_CHECK_MODULES(MMCLI,
 AC_SUBST(MMCLI_CFLAGS)
 AC_SUBST(MMCLI_LIBS)
 
-PKG_CHECK_MODULES(GUDEV, gudev-1.0 >= $GUDEV_MIN_VERSION)
-AC_SUBST(GUDEV_CFLAGS)
-AC_SUBST(GUDEV_LIBS)
-
 dnl Some required utilities
 GLIB_MKENUMS=`$PKG_CONFIG --variable=glib_mkenums glib-2.0`
 AC_SUBST(GLIB_MKENUMS)
@@ -183,6 +178,30 @@ fi
 AM_CONDITIONAL(HAVE_SYSTEMD, [test -n "$SYSTEMD_UNIT_DIR" -a "$SYSTEMD_UNIT_DIR" != xno ])
 
 dnl-----------------------------------------------------------------------------
+dnl udev support (enabled by default)
+dnl
+
+GUDEV_VERSION=147
+
+AC_ARG_WITH(udev, AS_HELP_STRING([--without-udev], [Build without udev support]), [], [with_udev=yes])
+AM_CONDITIONAL(WITH_UDEV, test "x$with_udev" = "xyes")
+case $with_udev in
+    yes)
+        PKG_CHECK_MODULES(GUDEV, [gudev-1.0 >= $GUDEV_VERSION], [have_gudev=yes],[have_gudev=no])
+        if test "x$have_gudev" = "xno"; then
+            AC_MSG_ERROR([Couldn't find gudev >= $GUDEV_VERSION. Install it, or otherwise configure using --without-udev to disable udev support.])
+        else
+            AC_DEFINE(WITH_UDEV, 1, [Define if you want udev support])
+            AC_SUBST(GUDEV_CFLAGS)
+            AC_SUBST(GUDEV_LIBS)
+        fi
+        ;;
+    *)
+        with_udev=no
+        ;;
+esac
+
+dnl-----------------------------------------------------------------------------
 dnl Suspend/resume support
 dnl
 
@@ -396,6 +415,7 @@ echo "
       systemd unit directory:  ${with_systemdsystemunitdir}
 
     Features:
+      udev support:            ${with_udev}
       policykit support:       ${with_polkit}
       mbim support:            ${with_mbim}
       qmi support:             ${with_qmi}
diff --git a/libmm-glib/mm-common-helpers.c b/libmm-glib/mm-common-helpers.c
index 4b784da..3a5883b 100644
--- a/libmm-glib/mm-common-helpers.c
+++ b/libmm-glib/mm-common-helpers.c
@@ -1386,6 +1386,49 @@ mm_get_uint_from_str (const gchar *str,
     return FALSE;
 }
 
+/**
+ * mm_get_uint_from_hex_str:
+ * @str: the hex string to convert to an unsigned int
+ * @out: on success, the number
+ *
+ * Converts a string to an unsigned number.  All characters in the string
+ * MUST be valid hexadecimal digits (0-9, A-F, a-f), otherwise FALSE is
+ * returned.
+ *
+ * An optional "0x" prefix may be given in @str.
+ *
+ * Returns: %TRUE if the string was converted, %FALSE if it was not or if it
+ * did not contain only digits.
+ */
+gboolean
+mm_get_uint_from_hex_str (const gchar *str,
+                          guint       *out)
+{
+    gulong num;
+
+    if (!str)
+        return FALSE;
+
+    if (g_str_has_prefix (str, "0x"))
+        str = &str[2];
+
+    if (!str[0])
+        return FALSE;
+
+    for (num = 0; str[num]; num++) {
+        if (!g_ascii_isxdigit (str[num]))
+            return FALSE;
+    }
+
+    errno = 0;
+    num = strtoul (str, NULL, 16);
+    if (!errno && num <= G_MAXUINT) {
+        *out = (guint)num;
+        return TRUE;
+    }
+    return FALSE;
+}
+
 gboolean
 mm_get_uint_from_match_info (GMatchInfo *match_info,
                              guint32 match_index,
diff --git a/libmm-glib/mm-common-helpers.h b/libmm-glib/mm-common-helpers.h
index c65d3b9..ab5777d 100644
--- a/libmm-glib/mm-common-helpers.h
+++ b/libmm-glib/mm-common-helpers.h
@@ -143,6 +143,8 @@ gboolean  mm_get_int_from_match_info             (GMatchInfo *match_info,
                                                   gint *out);
 gboolean  mm_get_uint_from_str                   (const gchar *str,
                                                   guint *out);
+gboolean  mm_get_uint_from_hex_str               (const gchar *str,
+                                                  guint       *out);
 gboolean  mm_get_uint_from_match_info            (GMatchInfo *match_info,
                                                   guint32 match_index,
                                                   guint *out);
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 435d0ed..2dcde28 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -306,6 +306,8 @@ libmm_plugin_huawei_la_LIBADD   = $(builddir)/libhelpers-huawei.la
 
 dist_udevrules_DATA += huawei/77-mm-huawei-net-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_HUAWEI=\"${srcdir}/huawei\"
+
 ################################################################################
 # plugin: ericsson mbm
 ################################################################################
@@ -321,6 +323,8 @@ libmm_plugin_ericsson_mbm_la_LIBADD   = $(MBM_COMMON_LIBADD_FLAGS)
 
 dist_udevrules_DATA += mbm/77-mm-ericsson-mbm.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_MBM=\"${srcdir}/mbm\"
+
 ################################################################################
 # plugin: option
 ################################################################################
@@ -423,6 +427,8 @@ libmm_plugin_nokia_icera_la_LIBADD   = $(ICERA_COMMON_LIBADD_FLAGS)
 
 dist_udevrules_DATA += nokia/77-mm-nokia-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_NOKIA=\"${srcdir}/nokia\"
+
 ################################################################################
 # plugin: zte
 ################################################################################
@@ -444,6 +450,8 @@ libmm_plugin_zte_la_LIBADD   = $(ICERA_COMMON_LIBADD_FLAGS)
 
 dist_udevrules_DATA += zte/77-mm-zte-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_ZTE=\"${srcdir}/zte\"
+
 ################################################################################
 # plugin: longcheer (and rebranded dongles)
 ################################################################################
@@ -460,6 +468,8 @@ libmm_plugin_longcheer_la_LDFLAGS  = $(PLUGIN_COMMON_LINKER_FLAGS)
 
 dist_udevrules_DATA += longcheer/77-mm-longcheer-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_LONGCHEER=\"${srcdir}/longcheer\"
+
 ################################################################################
 # plugin: anydata cdma
 ################################################################################
@@ -504,6 +514,8 @@ libmm_plugin_simtech_la_LDFLAGS  = $(PLUGIN_COMMON_LINKER_FLAGS)
 
 dist_udevrules_DATA += simtech/77-mm-simtech-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_SIMTECH=\"${srcdir}/simtech\"
+
 ################################################################################
 # plugin: alcatel/TCT/JRD x220D and possibly others
 ################################################################################
@@ -520,6 +532,8 @@ libmm_plugin_x22x_la_LDFLAGS  = $(PLUGIN_COMMON_LINKER_FLAGS)
 
 dist_udevrules_DATA += x22x/77-mm-x22x-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_X22X=\"${srcdir}/x22x\"
+
 ################################################################################
 # plugin: pantech
 ################################################################################
@@ -595,6 +609,8 @@ libmm_plugin_cinterion_la_LIBADD   = $(builddir)/libhelpers-cinterion.la
 
 dist_udevrules_DATA += cinterion/77-mm-cinterion-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_CINTERION=\"${srcdir}/cinterion\"
+
 ################################################################################
 # plugin: iridium
 ################################################################################
@@ -694,6 +710,8 @@ libmm_plugin_dell_la_LIBADD   = $(NOVATEL_COMMON_LIBADD_FLAGS) $(SIERRA_COMMON_L
 
 dist_udevrules_DATA += dell/77-mm-dell-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_DELL=\"${srcdir}/dell\"
+
 ################################################################################
 # plugin: altair lte
 ################################################################################
@@ -778,6 +796,8 @@ libmm_plugin_telit_la_LIBADD   = $(builddir)/libhelpers-telit.la $(TELIT_COMMON_
 
 dist_udevrules_DATA += telit/77-mm-telit-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_TELIT=\"${srcdir}/telit\"
+
 ################################################################################
 # plugin: mtk
 ################################################################################
@@ -794,6 +814,8 @@ libmm_plugin_mtk_la_LDFLAGS  = $(PLUGIN_COMMON_LINKER_FLAGS)
 
 dist_udevrules_DATA += mtk/77-mm-mtk-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_MTK=\"${srcdir}/mtk\"
+
 ################################################################################
 # plugin: haier
 ################################################################################
@@ -808,6 +830,22 @@ libmm_plugin_haier_la_LDFLAGS  = $(PLUGIN_COMMON_LINKER_FLAGS)
 
 dist_udevrules_DATA += haier/77-mm-haier-port-types.rules
 
+AM_CFLAGS += -DTESTUDEVRULESDIR_HAIER=\"${srcdir}/haier\"
+
+################################################################################
+# udev rules tester
+################################################################################
+
+noinst_PROGRAMS += test-udev-rules
+test_udev_rules_SOURCES = \
+	tests/test-udev-rules.c \
+	$(NULL)
+test_udev_rules_LDADD = \
+	$(top_builddir)/src/libkerneldevice.la \
+	$(top_builddir)/libmm-glib/libmm-glib.la \
+	$(NULL)
+
+
 ################################################################################
 
 TEST_PROGS += $(noinst_PROGRAMS)
diff --git a/plugins/tests/test-udev-rules.c b/plugins/tests/test-udev-rules.c
new file mode 100644
index 0000000..369e22b
--- /dev/null
+++ b/plugins/tests/test-udev-rules.c
@@ -0,0 +1,164 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details:
+ *
+ * Copyright (C) 2016 Aleksander Morgado <aleksander at aleksander.es>
+ */
+
+#include <glib.h>
+#include <glib-object.h>
+#include <string.h>
+#include <stdio.h>
+#include <locale.h>
+
+#define _LIBMM_INSIDE_MM
+#include <libmm-glib.h>
+
+/* Define symbol to enable test message traces */
+#undef ENABLE_TEST_MESSAGE_TRACES
+
+#include "mm-kernel-device-generic-rules.h"
+#include "mm-log.h"
+
+/************************************************************/
+
+static void
+common_test (const gchar *plugindir)
+{
+    GArray *rules;
+    GError *error = NULL;
+
+    rules = mm_kernel_device_generic_rules_load (plugindir, &error);
+    g_assert_no_error (error);
+    g_assert (rules);
+    g_assert (rules->len > 0);
+
+    g_array_unref (rules);
+}
+
+/************************************************************/
+
+static void
+test_huawei (void)
+{
+    common_test (TESTUDEVRULESDIR_HUAWEI);
+}
+
+static void
+test_mbm (void)
+{
+    common_test (TESTUDEVRULESDIR_MBM);
+}
+
+static void
+test_nokia (void)
+{
+    common_test (TESTUDEVRULESDIR_NOKIA);
+}
+
+static void
+test_zte (void)
+{
+    common_test (TESTUDEVRULESDIR_ZTE);
+}
+
+static void
+test_longcheer (void)
+{
+    common_test (TESTUDEVRULESDIR_LONGCHEER);
+}
+
+static void
+test_simtech (void)
+{
+    common_test (TESTUDEVRULESDIR_SIMTECH);
+}
+
+static void
+test_x22x (void)
+{
+    common_test (TESTUDEVRULESDIR_X22X);
+}
+
+static void
+test_cinterion (void)
+{
+    common_test (TESTUDEVRULESDIR_CINTERION);
+}
+
+static void
+test_dell (void)
+{
+    common_test (TESTUDEVRULESDIR_DELL);
+}
+
+static void
+test_telit (void)
+{
+    common_test (TESTUDEVRULESDIR_TELIT);
+}
+
+static void
+test_mtk (void)
+{
+    common_test (TESTUDEVRULESDIR_MTK);
+}
+
+static void
+test_haier (void)
+{
+    common_test (TESTUDEVRULESDIR_HAIER);
+}
+
+/************************************************************/
+
+void
+_mm_log (const char *loc,
+         const char *func,
+         guint32 level,
+         const char *fmt,
+         ...)
+{
+#if defined ENABLE_TEST_MESSAGE_TRACES
+    /* Dummy log function */
+    va_list args;
+    gchar *msg;
+
+    va_start (args, fmt);
+    msg = g_strdup_vprintf (fmt, args);
+    va_end (args);
+    g_print ("%s\n", msg);
+    g_free (msg);
+#endif
+}
+
+int main (int argc, char **argv)
+{
+    setlocale (LC_ALL, "");
+
+    g_type_init ();
+    g_test_init (&argc, &argv, NULL);
+
+    g_test_add_func ("/MM/test-udev-rules/huawei",    test_huawei);
+    g_test_add_func ("/MM/test-udev-rules/mbm",       test_mbm);
+    g_test_add_func ("/MM/test-udev-rules/nokia",     test_nokia);
+    g_test_add_func ("/MM/test-udev-rules/zte",       test_zte);
+    g_test_add_func ("/MM/test-udev-rules/longcheer", test_longcheer);
+    g_test_add_func ("/MM/test-udev-rules/simtech",   test_simtech);
+    g_test_add_func ("/MM/test-udev-rules/x22x",      test_x22x);
+    g_test_add_func ("/MM/test-udev-rules/cinterion", test_cinterion);
+    g_test_add_func ("/MM/test-udev-rules/dell",      test_dell);
+    g_test_add_func ("/MM/test-udev-rules/telit",     test_telit);
+    g_test_add_func ("/MM/test-udev-rules/mtk",       test_mtk);
+    g_test_add_func ("/MM/test-udev-rules/haier",     test_haier);
+
+    return g_test_run ();
+}
diff --git a/src/Makefile.am b/src/Makefile.am
index b1f1a40..91b4e17 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -102,14 +102,29 @@ endif
 
 noinst_LTLIBRARIES += libkerneldevice.la
 
+libkerneldevice_la_CPPFLAGS = \
+	-DUDEVRULESDIR=\"$(udevrulesdir)\" \
+	$(NULL)
+
 libkerneldevice_la_SOURCES = \
 	kerneldevice/mm-kernel-device.h \
 	kerneldevice/mm-kernel-device.c \
 	kerneldevice/mm-kernel-device-generic.h \
 	kerneldevice/mm-kernel-device-generic.c \
+	kerneldevice/mm-kernel-device-generic-rules.h \
+	kerneldevice/mm-kernel-device-generic-rules.c \
+	$(NULL)
+
+if WITH_UDEV
+libkerneldevice_la_SOURCES += \
 	kerneldevice/mm-kernel-device-udev.h \
 	kerneldevice/mm-kernel-device-udev.c \
 	$(NULL)
+endif
+
+libkerneldevice_la_LIBADD = \
+	$(top_builddir)/libmm-glib/libmm-glib.la \
+	$(NULL)
 
 ################################################################################
 # ports library
diff --git a/src/kerneldevice/mm-kernel-device-generic-rules.c b/src/kerneldevice/mm-kernel-device-generic-rules.c
new file mode 100644
index 0000000..608ca9f
--- /dev/null
+++ b/src/kerneldevice/mm-kernel-device-generic-rules.c
@@ -0,0 +1,446 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Copyright (C) 2016 Aleksander Morgado <aleksander at aleksander.es>
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <ModemManager.h>
+#define _LIBMM_INSIDE_MM
+#include <libmm-glib.h>
+
+#include "mm-log.h"
+#include "mm-kernel-device-generic-rules.h"
+
+static void
+udev_rule_match_clear (MMUdevRuleMatch *rule_match)
+{
+    g_free (rule_match->parameter);
+    g_free (rule_match->value);
+}
+
+static void
+udev_rule_clear (MMUdevRule *rule)
+{
+    switch (rule->result.type) {
+    case MM_UDEV_RULE_RESULT_TYPE_PROPERTY:
+        g_free (rule->result.content.property.name);
+        g_free (rule->result.content.property.value);
+        break;
+    case MM_UDEV_RULE_RESULT_TYPE_GOTO_TAG:
+    case MM_UDEV_RULE_RESULT_TYPE_LABEL:
+        g_free (rule->result.content.tag);
+        break;
+    case MM_UDEV_RULE_RESULT_TYPE_GOTO_INDEX:
+    case MM_UDEV_RULE_RESULT_TYPE_UNKNOWN:
+        break;
+    }
+
+    if (rule->conditions)
+        g_array_unref (rule->conditions);
+}
+
+static gboolean
+split_item (const gchar  *item,
+            gchar       **out_left,
+            gchar       **out_operator,
+            gchar       **out_right,
+            GError      **error)
+{
+    const gchar *aux;
+    gchar       *left = NULL;
+    gchar       *operator = NULL;
+    gchar       *right = NULL;
+    GError      *inner_error = NULL;
+
+    g_assert (item && out_left && out_operator && out_right);
+
+    /* Get left/operator/right */
+    if (((aux = strstr (item, "==")) != NULL) || ((aux = strstr (item, "!=")) != NULL)) {
+        operator = g_strndup (aux, 2);
+        right    = g_strdup (aux + 2);
+    } else if ((aux = strstr (item, "=")) != NULL) {
+        operator = g_strndup (aux, 1);
+        right    = g_strdup (aux + 1);
+    } else {
+        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                   "Invalid rule item, missing operator: '%s'", item);
+        goto out;
+    }
+
+    left = g_strndup (item, (aux - item));
+    g_strstrip (left);
+    if (!left[0]) {
+        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                   "Invalid rule item, missing left field: '%s'", item);
+        goto out;
+    }
+
+    g_strdelimit (right, "\"", ' ');
+    g_strstrip (right);
+    if (!right[0]) {
+        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                   "Invalid rule item, missing right field: '%s'", item);
+        goto out;
+    }
+
+out:
+    if (inner_error) {
+        g_free (left);
+        g_free (operator);
+        g_free (right);
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+
+    *out_left     = left;
+    *out_operator = operator;
+    *out_right    = right;
+    return TRUE;
+}
+
+static gboolean
+load_rule_result (MMUdevRuleResult  *rule_result,
+                  const gchar       *item,
+                  GError           **error)
+{
+    gchar  *left;
+    gchar  *operator;
+    gchar  *right;
+    GError *inner_error = NULL;
+    gsize   left_len;
+
+    if (!split_item (item, &left, &operator, &right, error))
+        return FALSE;
+
+    if (!g_str_equal (operator, "=")) {
+        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                   "Invalid rule result operator: '%s'", item);
+        goto out;
+    }
+
+    if (g_str_equal (left, "LABEL")) {
+        rule_result->type = MM_UDEV_RULE_RESULT_TYPE_LABEL;
+        rule_result->content.tag = right;
+        right = NULL;
+        goto out;
+    }
+
+    if (g_str_equal (left, "GOTO")) {
+        rule_result->type = MM_UDEV_RULE_RESULT_TYPE_GOTO_TAG;
+        rule_result->content.tag = right;
+        right = NULL;
+        goto out;
+    }
+
+    left_len = strlen (left);
+    if (g_str_has_prefix (left, "ENV{") && left[left_len - 1] == '}') {
+        rule_result->type = MM_UDEV_RULE_RESULT_TYPE_PROPERTY;
+        rule_result->content.property.name = g_strndup (left + 4, left_len - 5);
+        rule_result->content.property.value = right;
+        right = NULL;
+        goto out;
+    }
+
+    g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                 "Invalid rule result parameter: '%s'", item);
+
+out:
+    g_free (left);
+    g_free (operator);
+    g_free (right);
+
+    if (inner_error) {
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+load_rule_match (MMUdevRuleMatch  *rule_match,
+                 const gchar      *item,
+                 GError          **error)
+{
+    gchar  *left;
+    gchar  *operator;
+    gchar  *right;
+
+    if (!split_item (item, &left, &operator, &right, error))
+        return FALSE;
+
+    if (g_str_equal (operator, "=="))
+        rule_match->type = MM_UDEV_RULE_MATCH_TYPE_EQUAL;
+    else if (g_str_equal (operator, "!="))
+        rule_match->type = MM_UDEV_RULE_MATCH_TYPE_NOT_EQUAL;
+    else {
+        g_free (left);
+        g_free (operator);
+        g_free (right);
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                     "Invalid rule match, wrong match type: '%s'", item);
+
+        return FALSE;
+    }
+
+    g_free (operator);
+    rule_match->parameter = left;
+    rule_match->value     = right;
+    return TRUE;
+}
+
+static gboolean
+load_rule_from_line (MMUdevRule   *rule,
+                     const gchar  *line,
+                     GError      **error)
+{
+    gchar  **split;
+    guint    n_items;
+    GError  *inner_error = NULL;
+
+    split = g_strsplit (line, ",", -1);
+    n_items = g_strv_length (split);
+
+    /* Conditions */
+    if (n_items > 1) {
+        guint i;
+
+        rule->conditions = g_array_sized_new (FALSE, FALSE, sizeof (MMUdevRuleMatch), n_items - 1);
+        g_array_set_clear_func (rule->conditions, (GDestroyNotify) udev_rule_match_clear);
+
+        /* All items except for the last one are conditions */
+        for (i = 0; !inner_error && i < (n_items - 1); i++) {
+            MMUdevRuleMatch rule_match = { 0 };
+
+            /* If condition correctly preloaded, add it to the rule */
+            if (!load_rule_match (&rule_match, split[i], &inner_error))
+                goto out;
+            g_assert (rule_match.type != MM_UDEV_RULE_MATCH_TYPE_UNKNOWN);
+            g_assert (rule_match.parameter);
+            g_assert (rule_match.value);
+            g_array_append_val (rule->conditions, rule_match);
+        }
+    }
+
+    /* Last item, the result */
+    if (!load_rule_result (&rule->result, split[n_items - 1], &inner_error))
+        goto out;
+
+    g_assert ((rule->result.type == MM_UDEV_RULE_RESULT_TYPE_GOTO_TAG && rule->result.content.tag) ||
+              (rule->result.type == MM_UDEV_RULE_RESULT_TYPE_LABEL && rule->result.content.tag) ||
+              (rule->result.type == MM_UDEV_RULE_RESULT_TYPE_PROPERTY && rule->result.content.property.name && rule->result.content.property.value));
+
+out:
+    g_strfreev (split);
+
+    if (inner_error) {
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+    return TRUE;
+}
+
+static gboolean
+process_goto_tags (GArray  *rules,
+                   guint    first_rule_index,
+                   GError **error)
+{
+    guint i;
+
+    for (i = first_rule_index; i < rules->len; i++) {
+        MMUdevRule *rule;
+
+        rule = &g_array_index (rules, MMUdevRule, i);
+        if (rule->result.type == MM_UDEV_RULE_RESULT_TYPE_GOTO_TAG) {
+            guint j;
+            guint label_index = 0;
+
+            for (j = i + 1; j < rules->len; j++) {
+                MMUdevRule *walker;
+
+                walker = &g_array_index (rules, MMUdevRule, j);
+                if (walker->result.type == MM_UDEV_RULE_RESULT_TYPE_LABEL &&
+                    g_str_equal (rule->result.content.tag, walker->result.content.tag)) {
+
+                    if (label_index) {
+                        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                     "More than one label '%s' found", rule->result.content.tag);
+                        return FALSE;
+                    }
+
+                    label_index = j;
+                }
+            }
+
+            if (!label_index) {
+                g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                             "Couldn't find label '%s'", rule->result.content.tag);
+                return FALSE;
+            }
+
+            rule->result.type = MM_UDEV_RULE_RESULT_TYPE_GOTO_INDEX;
+            g_free (rule->result.content.tag);
+            rule->result.content.index = label_index;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+load_rules_from_file (GArray       *rules,
+                      const gchar  *path,
+                      GError      **error)
+{
+    GFile            *file;
+    GFileInputStream *fistream;
+    GDataInputStream *distream = NULL;
+    GError           *inner_error = NULL;
+    gchar            *line;
+    guint             first_rule_index;
+
+    mm_dbg ("[rules] loading rules from: %s", path);
+    first_rule_index = rules->len;
+
+    file = g_file_new_for_path (path);
+    fistream = g_file_read (file, NULL, &inner_error);
+    if (!fistream)
+        goto out;
+
+    distream = g_data_input_stream_new (G_INPUT_STREAM (fistream));
+
+    while (((line = g_data_input_stream_read_line_utf8 (distream, NULL, NULL, &inner_error)) != NULL) && !inner_error) {
+        const gchar *aux;
+
+        aux = line;
+        while (*aux == ' ')
+            aux++;
+        if (*aux != '#' && *aux != '\0') {
+            MMUdevRule rule = { 0 };
+
+            if (load_rule_from_line (&rule, aux, &inner_error))
+                g_array_append_val (rules, rule);
+            else
+                udev_rule_clear (&rule);
+        }
+        g_free (line);
+    }
+
+out:
+
+    if (distream)
+        g_object_unref (distream);
+    if (fistream)
+        g_object_unref (fistream);
+    g_object_unref (file);
+
+    if (inner_error) {
+        g_propagate_error (error, inner_error);
+        return FALSE;
+    }
+
+    if (first_rule_index < rules->len && !process_goto_tags (rules, first_rule_index, error))
+        return FALSE;
+
+    return TRUE;
+}
+
+static GList *
+list_rule_files (const gchar *rules_dir_path)
+{
+    static const gchar *expected_rules_prefix[] = { "77-mm-", "78-mm-", "79-mm-", "80-mm-" };
+    GFile              *udevrulesdir;
+    GFileEnumerator    *enumerator;
+    GList              *children = NULL;
+
+    udevrulesdir = g_file_new_for_path (rules_dir_path);
+    enumerator   = g_file_enumerate_children (udevrulesdir,
+                                              G_FILE_ATTRIBUTE_STANDARD_NAME,
+                                              G_FILE_QUERY_INFO_NONE,
+                                              NULL,
+                                              NULL);
+    if (enumerator) {
+        GFileInfo *info;
+
+        /* If we get any kind of error, assume we need to stop enumerating */
+        while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL)) != NULL) {
+            guint i;
+
+            for (i = 0; i < G_N_ELEMENTS (expected_rules_prefix); i++) {
+                if (g_str_has_prefix (g_file_info_get_name (info), expected_rules_prefix[i])) {
+                    children = g_list_prepend (children, g_build_path (G_DIR_SEPARATOR_S, rules_dir_path, g_file_info_get_name (info), NULL));
+                    break;
+                }
+            }
+            g_object_unref (info);
+        }
+        g_object_unref (enumerator);
+    }
+    g_object_unref (udevrulesdir);
+
+    return g_list_sort (children, (GCompareFunc) g_strcmp0);
+}
+
+GArray *
+mm_kernel_device_generic_rules_load (const gchar  *rules_dir,
+                                     GError      **error)
+{
+    GList  *rule_files, *l;
+    GArray *rules;
+    GError *inner_error = NULL;
+
+    mm_dbg ("[rules] rules directory set to '%s'...", rules_dir);
+
+    rules = g_array_new (FALSE, FALSE, sizeof (MMUdevRule));
+    g_array_set_clear_func (rules, (GDestroyNotify) udev_rule_clear);
+
+    /* List rule files in rules dir */
+    rule_files = list_rule_files (rules_dir);
+    if (!rule_files) {
+        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                   "No rule files found in '%s'", rules_dir);
+        goto out;
+    }
+
+    /* Iterate over rule files */
+    for (l = rule_files; l; l = g_list_next (l)) {
+        if (!load_rules_from_file (rules, (const gchar *)(l->data), &inner_error))
+            goto out;
+    }
+
+    /* Fail if no rules were loaded */
+    if (rules->len == 0) {
+        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "No rules loaded");
+        goto out;
+    }
+
+    mm_dbg ("[rules] %u loaded", rules->len);
+
+out:
+    if (rule_files)
+        g_list_free_full (rule_files, (GDestroyNotify) g_free);
+
+    if (inner_error) {
+        g_propagate_error (error, inner_error);
+        g_array_unref (rules);
+        return NULL;
+    }
+
+    return rules;
+}
diff --git a/src/kerneldevice/mm-kernel-device-generic-rules.h b/src/kerneldevice/mm-kernel-device-generic-rules.h
new file mode 100644
index 0000000..db570d1
--- /dev/null
+++ b/src/kerneldevice/mm-kernel-device-generic-rules.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details:
+ *
+ * Copyright (C) 2016 Aleksander Morgado <aleksander at aleksander.es>
+ */
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+    MM_UDEV_RULE_MATCH_TYPE_UNKNOWN,
+    MM_UDEV_RULE_MATCH_TYPE_EQUAL,
+    MM_UDEV_RULE_MATCH_TYPE_NOT_EQUAL,
+} MMUdevRuleMatchType;
+
+typedef struct {
+    MMUdevRuleMatchType  type;
+    gchar               *parameter;
+    gchar               *value;
+} MMUdevRuleMatch;
+
+typedef enum {
+    MM_UDEV_RULE_RESULT_TYPE_UNKNOWN,
+    MM_UDEV_RULE_RESULT_TYPE_PROPERTY,
+    MM_UDEV_RULE_RESULT_TYPE_LABEL,
+    MM_UDEV_RULE_RESULT_TYPE_GOTO_INDEX,
+    MM_UDEV_RULE_RESULT_TYPE_GOTO_TAG, /* internal use only */
+} MMUdevRuleResultType;
+
+typedef struct {
+    gchar *name;
+    gchar *value;
+} MMUdevRuleResultProperty;
+
+typedef struct {
+    MMUdevRuleResultType type;
+    union {
+        MMUdevRuleResultProperty  property;
+        gchar                    *tag;
+        guint                     index;
+    } content;
+} MMUdevRuleResult;
+
+typedef struct {
+    GArray           *conditions;
+    MMUdevRuleResult  result;
+} MMUdevRule;
+
+GArray *mm_kernel_device_generic_rules_load (const gchar  *rules_dir,
+                                             GError      **error);
+
+G_END_DECLS
diff --git a/src/kerneldevice/mm-kernel-device-generic.c b/src/kerneldevice/mm-kernel-device-generic.c
index a6dbc2d..32ac5ed 100644
--- a/src/kerneldevice/mm-kernel-device-generic.c
+++ b/src/kerneldevice/mm-kernel-device-generic.c
@@ -22,8 +22,13 @@
 #include <libmm-glib.h>
 
 #include "mm-kernel-device-generic.h"
+#include "mm-kernel-device-generic-rules.h"
 #include "mm-log.h"
 
+#if !defined UDEVRULESDIR
+# error UDEVRULESDIR is not defined
+#endif
+
 static void initable_iface_init (GInitableIface *iface);
 
 G_DEFINE_TYPE_EXTENDED (MMKernelDeviceGeneric, mm_kernel_device_generic,  MM_TYPE_KERNEL_DEVICE, 0,
@@ -32,6 +37,7 @@ G_DEFINE_TYPE_EXTENDED (MMKernelDeviceGeneric, mm_kernel_device_generic,  MM_TYP
 enum {
     PROP_0,
     PROP_PROPERTIES,
+    PROP_RULES,
     PROP_LAST
 };
 
@@ -40,8 +46,330 @@ static GParamSpec *properties[PROP_LAST];
 struct _MMKernelDeviceGenericPrivate {
     /* Input properties */
     MMKernelEventProperties *properties;
+    /* Rules to apply */
+    GArray *rules;
+
+    /* Contents from sysfs */
+    gchar   *driver;
+    gchar   *sysfs_path;
+    gchar   *interface_sysfs_path;
+    guint8   interface_class;
+    guint8   interface_subclass;
+    guint8   interface_protocol;
+    guint8   interface_number;
+    gchar   *physdev_sysfs_path;
+    guint16  physdev_vid;
+    guint16  physdev_pid;
+    gchar   *physdev_manufacturer;
+    gchar   *physdev_product;
 };
 
+static guint
+read_sysfs_property_as_hex (const gchar *path,
+                            const gchar *property)
+{
+    gchar *aux;
+    gchar *contents = NULL;
+    guint val = 0;
+
+    aux = g_strdup_printf ("%s/%s", path, property);
+    if (g_file_get_contents (aux, &contents, NULL, NULL)) {
+        g_strdelimit (contents, "\r\n", ' ');
+        g_strstrip (contents);
+        mm_get_uint_from_hex_str (contents, &val);
+    }
+    g_free (contents);
+    g_free (aux);
+    return val;
+}
+
+static gchar *
+read_sysfs_property_as_string (const gchar *path,
+                               const gchar *property)
+{
+    gchar *aux;
+    gchar *contents = NULL;
+
+    aux = g_strdup_printf ("%s/%s", path, property);
+    if (g_file_get_contents (aux, &contents, NULL, NULL)) {
+        g_strdelimit (contents, "\r\n", ' ');
+        g_strstrip (contents);
+    }
+    g_free (aux);
+    return contents;
+}
+
+/*****************************************************************************/
+/* Load contents */
+
+static void
+preload_sysfs_path (MMKernelDeviceGeneric *self)
+{
+    gchar *tmp;
+
+    if (self->priv->sysfs_path)
+        return;
+
+    /* sysfs can be built directly using subsystem and name; e.g. for subsystem
+     * usbmisc and name cdc-wdm0:
+     *    $ realpath /sys/class/usbmisc/cdc-wdm0
+     *    /sys/devices/pci0000:00/0000:00:1d.0/usb4/4-1/4-1.3/4-1.3:1.8/usbmisc/cdc-wdm0
+     */
+    tmp = g_strdup_printf ("/sys/class/%s/%s",
+                           mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                           mm_kernel_event_properties_get_name      (self->priv->properties));
+
+    self->priv->sysfs_path = canonicalize_file_name (tmp);
+    if (!self->priv->sysfs_path || !g_file_test (self->priv->sysfs_path, G_FILE_TEST_EXISTS)) {
+        mm_warn ("Invalid sysfs path read for %s/%s",
+                 mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                 mm_kernel_event_properties_get_name      (self->priv->properties));
+        g_clear_pointer (&self->priv->sysfs_path, g_free);
+    }
+
+    if (self->priv->sysfs_path)
+        mm_dbg ("(%s/%s) sysfs path: %s",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties),
+                self->priv->sysfs_path);
+    g_free (tmp);
+}
+
+static void
+preload_interface_sysfs_path (MMKernelDeviceGeneric *self)
+{
+    gchar *dirpath;
+    gchar *aux;
+
+    if (self->priv->interface_sysfs_path || !self->priv->sysfs_path)
+        return;
+
+    /* parent sysfs can be built directly using subsystem and name; e.g. for
+     * subsystem usbmisc and name cdc-wdm0:
+     *    $ realpath /sys/class/usbmisc/cdc-wdm0/device
+     *    /sys/devices/pci0000:00/0000:00:1d.0/usb4/4-1/4-1.3/4-1.3:1.8
+     *
+     * This sysfs path will be equal for all devices exported from within the
+     * same interface (e.g. a pair of cdc-wdm/wwan devices).
+     *
+     * The correct parent dir we want to have is the first one with "usb" subsystem.
+     */
+    aux = g_strdup_printf ("%s/device", self->priv->sysfs_path);
+    dirpath = canonicalize_file_name (aux);
+    g_free (aux);
+
+    while (dirpath) {
+        gchar *subsystem_filepath;
+
+        /* Directory must exist */
+        if (!g_file_test (dirpath, G_FILE_TEST_EXISTS))
+            break;
+
+        /* If subsystem file not found, keep looping */
+        subsystem_filepath = g_strdup_printf ("%s/subsystem", dirpath);
+        if (g_file_test (subsystem_filepath, G_FILE_TEST_EXISTS)) {
+            gchar *canonicalized_subsystem;
+            gchar *subsystem_name;
+
+            canonicalized_subsystem = canonicalize_file_name (subsystem_filepath);
+            g_free (subsystem_filepath);
+
+            subsystem_name = g_path_get_basename (canonicalized_subsystem);
+            g_free (canonicalized_subsystem);
+
+            if (subsystem_name && g_str_equal (subsystem_name, "usb")) {
+                self->priv->interface_sysfs_path = dirpath;
+                g_free (subsystem_name);
+                break;
+            }
+        } else
+            g_free (subsystem_filepath);
+
+        /* Just in case */
+        if (g_str_equal (dirpath, "/")) {
+            g_free (dirpath);
+            break;
+        }
+
+        aux = g_path_get_dirname (dirpath);
+        g_free (dirpath);
+        dirpath = aux;
+    }
+
+    if (self->priv->interface_sysfs_path)
+        mm_dbg ("(%s/%s) interface sysfs path: %s",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties),
+                self->priv->interface_sysfs_path);
+}
+
+static void
+preload_physdev_sysfs_path (MMKernelDeviceGeneric *self)
+{
+    /* physdev sysfs is the dirname of the parent sysfs path, e.g.:
+     *    /sys/devices/pci0000:00/0000:00:1d.0/usb4/4-1/4-1.3
+     *
+     * This sysfs path will be equal for all devices exported from the same
+     * physical device.
+     */
+    if (!self->priv->physdev_sysfs_path && self->priv->interface_sysfs_path)
+        self->priv->physdev_sysfs_path = g_path_get_dirname (self->priv->interface_sysfs_path);
+
+    if (self->priv->physdev_sysfs_path)
+        mm_dbg ("(%s/%s) physdev sysfs path: %s",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties),
+                self->priv->physdev_sysfs_path);
+}
+
+static void
+preload_driver (MMKernelDeviceGeneric *self)
+{
+    if (!self->priv->driver && self->priv->interface_sysfs_path) {
+        gchar *tmp;
+        gchar *tmp2;
+
+        tmp = g_strdup_printf ("%s/driver", self->priv->interface_sysfs_path);
+        tmp2 = canonicalize_file_name (tmp);
+        if (tmp2 && g_file_test (tmp2, G_FILE_TEST_EXISTS))
+            self->priv->driver = g_path_get_basename (tmp2);
+        g_free (tmp2);
+        g_free (tmp);
+    }
+
+    if (self->priv->driver)
+        mm_dbg ("(%s/%s) driver: %s",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties),
+                self->priv->driver);
+}
+
+static void
+preload_physdev_vid (MMKernelDeviceGeneric *self)
+{
+    if (!self->priv->physdev_vid && self->priv->physdev_sysfs_path) {
+        guint val;
+
+        val = read_sysfs_property_as_hex (self->priv->physdev_sysfs_path, "idVendor");
+        if (val && val <= G_MAXUINT16)
+            self->priv->physdev_vid = val;
+    }
+
+    if (self->priv->physdev_vid)
+        mm_dbg ("(%s/%s) vid: 0x%04x",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties),
+                self->priv->physdev_vid);
+    else
+        mm_dbg ("(%s/%s) vid: unknown",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties));
+
+}
+
+static void
+preload_physdev_pid (MMKernelDeviceGeneric *self)
+{
+    if (!self->priv->physdev_pid && self->priv->physdev_sysfs_path) {
+        guint val;
+
+        val = read_sysfs_property_as_hex (self->priv->physdev_sysfs_path, "idProduct");
+        if (val && val <= G_MAXUINT16)
+            self->priv->physdev_pid = val;
+    }
+
+    if (self->priv->physdev_pid)
+        mm_dbg ("(%s/%s) pid: 0x%04x",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties),
+                self->priv->physdev_pid);
+    else
+        mm_dbg ("(%s/%s) pid: unknown",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties));
+}
+
+static void
+preload_manufacturer (MMKernelDeviceGeneric *self)
+{
+    if (!self->priv->physdev_manufacturer)
+        self->priv->physdev_manufacturer = (self->priv->physdev_sysfs_path ? read_sysfs_property_as_string (self->priv->physdev_sysfs_path, "manufacturer") : NULL);
+
+    mm_dbg ("(%s/%s) manufacturer: %s",
+            mm_kernel_event_properties_get_subsystem (self->priv->properties),
+            mm_kernel_event_properties_get_name      (self->priv->properties),
+            self->priv->physdev_manufacturer ? self->priv->physdev_manufacturer : "unknown");
+}
+
+static void
+preload_product (MMKernelDeviceGeneric *self)
+{
+    if (!self->priv->physdev_product)
+        self->priv->physdev_product = (self->priv->physdev_sysfs_path ? read_sysfs_property_as_string (self->priv->physdev_sysfs_path, "product") : NULL);
+
+    mm_dbg ("(%s/%s) product: %s",
+            mm_kernel_event_properties_get_subsystem (self->priv->properties),
+            mm_kernel_event_properties_get_name      (self->priv->properties),
+            self->priv->physdev_product ? self->priv->physdev_product : "unknown");
+}
+
+static void
+preload_interface_class (MMKernelDeviceGeneric *self)
+{
+    self->priv->interface_class = (self->priv->interface_sysfs_path ? read_sysfs_property_as_hex (self->priv->interface_sysfs_path, "bInterfaceClass") : 0x00);
+    mm_dbg ("(%s/%s) interface class: 0x%02x",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties),
+                self->priv->interface_class);
+}
+
+static void
+preload_interface_subclass (MMKernelDeviceGeneric *self)
+{
+    self->priv->interface_subclass = (self->priv->interface_sysfs_path ? read_sysfs_property_as_hex (self->priv->interface_sysfs_path, "bInterfaceSubClass") : 0x00);
+    mm_dbg ("(%s/%s) interface subclass: 0x%02x",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties),
+                self->priv->interface_subclass);
+}
+
+static void
+preload_interface_protocol (MMKernelDeviceGeneric *self)
+{
+    self->priv->interface_protocol = (self->priv->interface_sysfs_path ? read_sysfs_property_as_hex (self->priv->interface_sysfs_path, "bInterfaceProtocol") : 0x00);
+    mm_dbg ("(%s/%s) interface protocol: 0x%02x",
+            mm_kernel_event_properties_get_subsystem (self->priv->properties),
+            mm_kernel_event_properties_get_name      (self->priv->properties),
+            self->priv->interface_protocol);
+}
+
+static void
+preload_interface_number (MMKernelDeviceGeneric *self)
+{
+    self->priv->interface_number = (self->priv->interface_sysfs_path ? read_sysfs_property_as_hex (self->priv->interface_sysfs_path, "bInterfaceNumber") : 0x00);
+    mm_dbg ("(%s/%s) interface number: 0x%02x",
+            mm_kernel_event_properties_get_subsystem (self->priv->properties),
+            mm_kernel_event_properties_get_name      (self->priv->properties),
+            self->priv->interface_number);
+}
+
+static void
+preload_contents (MMKernelDeviceGeneric *self)
+{
+    preload_sysfs_path           (self);
+    preload_interface_sysfs_path (self);
+    preload_interface_class      (self);
+    preload_interface_subclass   (self);
+    preload_interface_protocol   (self);
+    preload_interface_number     (self);
+    preload_physdev_sysfs_path   (self);
+    preload_manufacturer         (self);
+    preload_product              (self);
+    preload_driver               (self);
+    preload_physdev_vid          (self);
+    preload_physdev_pid          (self);
+}
+
 /*****************************************************************************/
 
 static const gchar *
@@ -65,7 +393,7 @@ kernel_device_get_sysfs_path (MMKernelDevice *self)
 {
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), NULL);
 
-    return NULL;
+    return MM_KERNEL_DEVICE_GENERIC (self)->priv->sysfs_path;
 }
 
 static const gchar *
@@ -73,16 +401,30 @@ kernel_device_get_parent_sysfs_path (MMKernelDevice *self)
 {
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), NULL);
 
-    return NULL;
+    return MM_KERNEL_DEVICE_GENERIC (self)->priv->interface_sysfs_path;
 }
 
 static const gchar *
 kernel_device_get_physdev_uid (MMKernelDevice *self)
 {
+    const gchar *uid;
+
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), NULL);
 
     /* Prefer the one coming in the properties, if any */
-    return mm_kernel_event_properties_get_uid (MM_KERNEL_DEVICE_GENERIC (self)->priv->properties);
+    if ((uid = mm_kernel_event_properties_get_uid (MM_KERNEL_DEVICE_GENERIC (self)->priv->properties)) != NULL)
+        return uid;
+
+    /* Try to load from properties set */
+    if ((uid = mm_kernel_device_get_property (self, "ID_MM_PHYSDEV_UID")) != NULL)
+        return uid;
+
+    /* Use physical device path, if any */
+    if (MM_KERNEL_DEVICE_GENERIC (self)->priv->physdev_sysfs_path)
+        return MM_KERNEL_DEVICE_GENERIC (self)->priv->physdev_sysfs_path;
+
+    /* If there is no physdev sysfs path, e.g. for platform ports, use the device sysfs itself */
+    return MM_KERNEL_DEVICE_GENERIC (self)->priv->sysfs_path;
 }
 
 static const gchar *
@@ -90,7 +432,7 @@ kernel_device_get_driver (MMKernelDevice *self)
 {
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), NULL);
 
-    return NULL;
+    return MM_KERNEL_DEVICE_GENERIC (self)->priv->driver;
 }
 
 static guint16
@@ -98,7 +440,7 @@ kernel_device_get_physdev_vid (MMKernelDevice *self)
 {
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), 0);
 
-    return 0;
+    return MM_KERNEL_DEVICE_GENERIC (self)->priv->physdev_vid;
 }
 
 static guint16
@@ -106,15 +448,59 @@ kernel_device_get_physdev_pid (MMKernelDevice *self)
 {
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), 0);
 
-    return 0;
+    return MM_KERNEL_DEVICE_GENERIC (self)->priv->physdev_pid;
 }
 
 static gboolean
 kernel_device_is_candidate (MMKernelDevice *_self,
                             gboolean        manual_scan)
 {
+    MMKernelDeviceGeneric *self;
+    const gchar           *name;
+
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (_self), FALSE);
 
+    self = MM_KERNEL_DEVICE_GENERIC (_self);
+
+    name = mm_kernel_event_properties_get_name (self->priv->properties);
+
+    /* ignore VTs */
+    if (strncmp (name, "tty", 3) == 0 && g_ascii_isdigit (name[3])) {
+        mm_dbg ("(%s/%s) VT ignored",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties));
+        return FALSE;
+    }
+
+    /* only ports tagged as candidate */
+    if (!mm_kernel_device_get_property_as_boolean (_self, "ID_MM_CANDIDATE")) {
+        mm_dbg ("(%s/%s) device not flagged with ID_MM_CANDIDATE",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties));
+        return FALSE;
+    }
+
+    /* no devices without physical device */
+    if (!self->priv->physdev_sysfs_path) {
+        mm_dbg ("(%s/%s) device without physdev sysfs path",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties));
+        return FALSE;
+    }
+
+    /* ignore ports explicitly ignored; note that in this case the property
+     * is set in this kernel device itself, unlike in the udev backend, that
+     * goes in the parent udev device */
+    if (mm_kernel_device_get_property_as_boolean (_self, "ID_MM_DEVICE_IGNORE")) {
+        mm_dbg ("(%s/%s) device flagged with ID_MM_DEVICE_IGNORE",
+                mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                mm_kernel_event_properties_get_name      (self->priv->properties));
+        return FALSE;
+    }
+
+    mm_dbg ("(%s/%s) device is candidate",
+            mm_kernel_event_properties_get_subsystem (self->priv->properties),
+            mm_kernel_event_properties_get_name      (self->priv->properties));
     return TRUE;
 }
 
@@ -129,13 +515,305 @@ kernel_device_cmp (MMKernelDevice *a,
             !g_strcmp0 (mm_kernel_device_get_name      (a), mm_kernel_device_get_name      (b)));
 }
 
+/*****************************************************************************/
+
+static gboolean
+string_match (const gchar *str,
+              const gchar *original_pattern)
+{
+    gchar    *pattern;
+    gchar    *start;
+    gboolean  open_prefix = FALSE;
+    gboolean  open_suffix = FALSE;
+    gboolean  match;
+
+    pattern = g_strdup (original_pattern);
+    start = pattern;
+
+    if (start[0] == '*') {
+        open_prefix = TRUE;
+        start++;
+    }
+
+    if (start[strlen (start) - 1] == '*') {
+        open_suffix = TRUE;
+        start[strlen (start) - 1] = '\0';
+    }
+
+    if (open_suffix && !open_prefix)
+        match = g_str_has_prefix (str, start);
+    else if (!open_suffix && open_prefix)
+        match = g_str_has_suffix (str, start);
+    else if (open_suffix && open_prefix)
+        match = !!strstr (str, start);
+    else
+        match = g_str_equal (str, start);
+
+    g_free (pattern);
+    return match;
+}
+
+static gboolean
+check_condition (MMKernelDeviceGeneric *self,
+                 MMUdevRuleMatch       *match)
+{
+    gboolean condition_equal;
+
+    condition_equal = (match->type == MM_UDEV_RULE_MATCH_TYPE_EQUAL);
+
+    /* We only apply 'add' rules */
+    if (g_str_equal (match->parameter, "ACTION"))
+        return ((!!strstr (match->value, "add")) == condition_equal);
+
+    /* We look for the subsystem string in the whole sysfs path.
+     *
+     * Note that we're not really making a difference between "SUBSYSTEMS"
+     * (where the whole device tree is checked) and "SUBSYSTEM" (where just one
+     * single device is checked), because a lot of the MM udev rules are meant
+     * to just tag the physical device (e.g. with ID_MM_DEVICE_IGNORE) instead
+     * of the single ports. In our case with the custom parsing, we do tag all
+     * independent ports.
+     */
+    if (g_str_equal (match->parameter, "SUBSYSTEMS") || g_str_equal (match->parameter, "SUBSYSTEM"))
+        return ((self->priv->sysfs_path && !!strstr (self->priv->sysfs_path, match->value)) == condition_equal);
+
+    /* Exact DRIVER match? We also include the check for DRIVERS, even if we
+     * only apply it to this port driver. */
+    if (g_str_equal (match->parameter, "DRIVER") || g_str_equal (match->parameter, "DRIVERS"))
+        return ((!g_strcmp0 (match->value, mm_kernel_device_get_driver (MM_KERNEL_DEVICE (self)))) == condition_equal);
+
+    /* Device name checks */
+    if (g_str_equal (match->parameter, "KERNEL"))
+        return (string_match (mm_kernel_device_get_name (MM_KERNEL_DEVICE (self)), match->value) == condition_equal);
+
+    /* Device sysfs path checks; we allow both a direct match and a prefix patch */
+    if (g_str_equal (match->parameter, "DEVPATH")) {
+        const gchar *sysfs_path;
+        gchar       *prefix_match = NULL;
+        gboolean     result = FALSE;
+
+        sysfs_path   = mm_kernel_device_get_sysfs_path (MM_KERNEL_DEVICE (self));
+
+        /* If not already doing a prefix match, do an implicit one. This is so that
+         * we can add properties to the usb_device owning all ports, and then apply
+         * the property to all ports individually processed here. */
+        if (match->value[0] && match->value[strlen (match->value) - 1] != '*')
+            prefix_match = g_strdup_printf ("%s/*", match->value);
+
+        if (string_match (sysfs_path, match->value) == condition_equal) {
+            result = TRUE;
+            goto out;
+        }
+
+        if (prefix_match && string_match (sysfs_path, prefix_match) == condition_equal) {
+            result = TRUE;
+            goto out;
+        }
+
+        if (g_str_has_prefix (sysfs_path, "/sys")) {
+            if (string_match (&sysfs_path[4], match->value) == condition_equal) {
+                result = TRUE;
+                goto out;
+            }
+            if (prefix_match && string_match (&sysfs_path[4], prefix_match) == condition_equal) {
+                result = TRUE;
+                goto out;
+            }
+        }
+    out:
+        g_free (prefix_match);
+        return result;
+    }
+
+    /* Attributes checks */
+    if (g_str_has_prefix (match->parameter, "ATTRS")) {
+        gchar    *attribute;
+        gchar    *contents = NULL;
+        gboolean  result = FALSE;
+        guint     val;
+
+        attribute = g_strdup (&match->parameter[5]);
+        g_strdelimit (attribute, "{}", ' ');
+        g_strstrip (attribute);
+
+        /* VID/PID directly from our API */
+        if (g_str_equal (attribute, "idVendor"))
+            result = ((mm_get_uint_from_hex_str (match->value, &val)) &&
+                      ((mm_kernel_device_get_physdev_vid (MM_KERNEL_DEVICE (self)) == val) == condition_equal));
+        else if (g_str_equal (attribute, "idProduct"))
+            result = ((mm_get_uint_from_hex_str (match->value, &val)) &&
+                      ((mm_kernel_device_get_physdev_pid (MM_KERNEL_DEVICE (self)) == val) == condition_equal));
+        /* manufacturer in the physdev */
+        else if (g_str_equal (attribute, "manufacturer"))
+            result = ((self->priv->physdev_manufacturer && g_str_equal (self->priv->physdev_manufacturer, match->value)) == condition_equal);
+        /* product in the physdev */
+        else if (g_str_equal (attribute, "product"))
+            result = ((self->priv->physdev_product && g_str_equal (self->priv->physdev_product, match->value)) == condition_equal);
+        /* interface class/subclass/protocol/number in the interface */
+        else if (g_str_equal (attribute, "bInterfaceClass"))
+            result = (g_str_equal (match->value, "?*") || ((mm_get_uint_from_hex_str (match->value, &val)) &&
+                                                           ((self->priv->interface_class == val) == condition_equal)));
+        else if (g_str_equal (attribute, "bInterfaceSubClass"))
+            result = (g_str_equal (match->value, "?*") || ((mm_get_uint_from_hex_str (match->value, &val)) &&
+                                                           ((self->priv->interface_subclass == val) == condition_equal)));
+        else if (g_str_equal (attribute, "bInterfaceProtocol"))
+            result = (g_str_equal (match->value, "?*") || ((mm_get_uint_from_hex_str (match->value, &val)) &&
+                                                           ((self->priv->interface_protocol == val) == condition_equal)));
+        else if (g_str_equal (attribute, "bInterfaceNumber"))
+            result = (g_str_equal (match->value, "?*") || ((mm_get_uint_from_hex_str (match->value, &val)) &&
+                                                           ((self->priv->interface_number == val) == condition_equal)));
+        else
+            mm_warn ("Unknown attribute: %s", attribute);
+
+        g_free (contents);
+        g_free (attribute);
+        return result;
+    }
+
+    /* Previously set property checks */
+    if (g_str_has_prefix (match->parameter, "ENV")) {
+        gchar    *property;
+        gboolean  result = FALSE;
+
+        property = g_strdup (&match->parameter[3]);
+        g_strdelimit (property, "{}", ' ');
+        g_strstrip (property);
+
+        result = ((!g_strcmp0 ((const gchar *) g_object_get_data (G_OBJECT (self), property), match->value)) == condition_equal);
+
+        g_free (property);
+        return result;
+    }
+
+    mm_warn ("Unknown match condition parameter: %s", match->parameter);
+    return FALSE;
+}
+
+static guint
+check_rule (MMKernelDeviceGeneric *self,
+            guint                  rule_i)
+{
+    MMUdevRule *rule;
+    gboolean    apply = TRUE;
+
+    g_assert (rule_i < self->priv->rules->len);
+
+    rule = &g_array_index (self->priv->rules, MMUdevRule, rule_i);
+    if (rule->conditions) {
+        guint condition_i;
+
+        for (condition_i = 0; condition_i < rule->conditions->len; condition_i++) {
+            MMUdevRuleMatch *match;
+
+            match = &g_array_index (rule->conditions, MMUdevRuleMatch, condition_i);
+            if (!check_condition (self, match)) {
+                apply = FALSE;
+                break;
+            }
+        }
+    }
+
+    if (apply) {
+        switch (rule->result.type) {
+        case MM_UDEV_RULE_RESULT_TYPE_PROPERTY: {
+            gchar *property_value_read = NULL;
+
+            if (g_str_equal (rule->result.content.property.value, "$attr{bInterfaceClass}"))
+                property_value_read = g_strdup_printf ("%02x", self->priv->interface_class);
+            else if (g_str_equal (rule->result.content.property.value, "$attr{bInterfaceSubClass}"))
+                property_value_read = g_strdup_printf ("%02x", self->priv->interface_subclass);
+            else if (g_str_equal (rule->result.content.property.value, "$attr{bInterfaceProtocol}"))
+                property_value_read = g_strdup_printf ("%02x", self->priv->interface_protocol);
+            else if (g_str_equal (rule->result.content.property.value, "$attr{bInterfaceNumber}"))
+                property_value_read = g_strdup_printf ("%02x", self->priv->interface_number);
+
+            /* add new property */
+            mm_dbg ("(%s/%s) property added: %s=%s",
+                    mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                    mm_kernel_event_properties_get_name      (self->priv->properties),
+                    rule->result.content.property.name,
+                    property_value_read ? property_value_read : rule->result.content.property.value);
+
+            if (!property_value_read)
+                /* NOTE: we keep a reference to the list of rules ourselves, so it isn't
+                 * an issue if we re-use the same string (i.e. without g_strdup-ing it)
+                 * as a property value. */
+                g_object_set_data (G_OBJECT (self),
+                                   rule->result.content.property.name,
+                                   rule->result.content.property.value);
+            else
+                g_object_set_data_full (G_OBJECT (self),
+                                        rule->result.content.property.name,
+                                        property_value_read,
+                                        g_free);
+            break;
+        }
+
+        case MM_UDEV_RULE_RESULT_TYPE_LABEL:
+            /* noop */
+            break;
+
+        case MM_UDEV_RULE_RESULT_TYPE_GOTO_INDEX:
+            /* Jump to a new index */
+            return rule->result.content.index;
+
+        case MM_UDEV_RULE_RESULT_TYPE_GOTO_TAG:
+        case MM_UDEV_RULE_RESULT_TYPE_UNKNOWN:
+            g_assert_not_reached ();
+        }
+    }
+
+    /* Go to the next rule */
+    return rule_i + 1;
+}
+
+static void
+preload_properties (MMKernelDeviceGeneric *self)
+{
+    guint i;
+
+    g_assert (self->priv->rules);
+    g_assert (self->priv->rules->len > 0);
+
+    /* Start to process rules */
+    i = 0;
+    while (i < self->priv->rules->len) {
+        guint next_rule;
+
+        next_rule = check_rule (self, i);
+        i = next_rule;
+    }
+}
+
+static void
+check_preload (MMKernelDeviceGeneric *self)
+{
+    /* Only preload when properties and rules are set */
+    if (!self->priv->properties || !self->priv->rules)
+        return;
+
+    /* Don't preload on "remove" actions, where we don't have the device any more */
+    if (g_strcmp0 (mm_kernel_event_properties_get_action (self->priv->properties), "remove") == 0)
+        return;
+
+    /* Don't preload for devices in the 'virtual' subsystem */
+    if (g_strcmp0 (mm_kernel_event_properties_get_subsystem (self->priv->properties), "virtual") == 0)
+        return;
+
+    mm_dbg ("(%s/%s) preloading contents and properties...",
+            mm_kernel_event_properties_get_subsystem (self->priv->properties),
+            mm_kernel_event_properties_get_name      (self->priv->properties));
+    preload_contents (self);
+    preload_properties (self);
+}
+
 static gboolean
 kernel_device_has_property (MMKernelDevice *self,
                             const gchar    *property)
 {
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), FALSE);
 
-    return FALSE;
+    return !!g_object_get_data (G_OBJECT (self), property);
 }
 
 static const gchar *
@@ -144,42 +822,70 @@ kernel_device_get_property (MMKernelDevice *self,
 {
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), NULL);
 
-    return NULL;
+    return g_object_get_data (G_OBJECT (self), property);
 }
 
 static gboolean
 kernel_device_get_property_as_boolean (MMKernelDevice *self,
                                        const gchar    *property)
 {
+    const gchar *value;
+
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), FALSE);
 
-    return FALSE;
+    value = g_object_get_data (G_OBJECT (self), property);
+    return (value && mm_common_get_boolean_from_string (value, NULL));
 }
 
 static gint
 kernel_device_get_property_as_int (MMKernelDevice *self,
                                    const gchar    *property)
 {
+    const gchar *value;
+    gint aux = 0;
+
     g_return_val_if_fail (MM_IS_KERNEL_DEVICE_GENERIC (self), -1);
 
-    return 0;
+    value = g_object_get_data (G_OBJECT (self), property);
+    return ((value && mm_get_int_from_str (value, &aux)) ? aux : 0);
 }
 
 /*****************************************************************************/
 
 MMKernelDevice *
-mm_kernel_device_generic_new (MMKernelEventProperties  *properties,
-                              GError                  **error)
+mm_kernel_device_generic_new_with_rules (MMKernelEventProperties  *properties,
+                                         GArray                   *rules,
+                                         GError                  **error)
 {
     g_return_val_if_fail (MM_IS_KERNEL_EVENT_PROPERTIES (properties), NULL);
+    g_return_val_if_fail (rules != NULL, NULL);
 
     return MM_KERNEL_DEVICE (g_initable_new (MM_TYPE_KERNEL_DEVICE_GENERIC,
                                              NULL,
                                              error,
                                              "properties", properties,
+                                             "rules",      rules,
                                              NULL));
 }
 
+MMKernelDevice *
+mm_kernel_device_generic_new (MMKernelEventProperties  *properties,
+                              GError                  **error)
+{
+    static GArray *rules = NULL;
+
+    g_return_val_if_fail (MM_IS_KERNEL_EVENT_PROPERTIES (properties), NULL);
+
+    /* We only try to load the default list of rules once */
+    if (G_UNLIKELY (!rules)) {
+        rules = mm_kernel_device_generic_rules_load (UDEVRULESDIR, error);
+        if (!rules)
+            return NULL;
+    }
+
+    return mm_kernel_device_generic_new_with_rules (properties, rules, error);
+}
+
 /*****************************************************************************/
 
 static void
@@ -201,6 +907,12 @@ set_property (GObject      *object,
     case PROP_PROPERTIES:
         g_assert (!self->priv->properties);
         self->priv->properties = g_value_dup_object (value);
+        check_preload (self);
+        break;
+    case PROP_RULES:
+        g_assert (!self->priv->rules);
+        self->priv->rules = g_value_dup_boxed (value);
+        check_preload (self);
         break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -220,6 +932,9 @@ get_property (GObject    *object,
     case PROP_PROPERTIES:
         g_value_set_object (value, self->priv->properties);
         break;
+    case PROP_RULES:
+        g_value_set_boxed (value, self->priv->rules);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
         break;
@@ -241,15 +956,24 @@ initable_init (GInitable     *initable,
         return FALSE;
     }
 
-    if (!g_str_equal (subsystem, "virtual")) {
+    if (!mm_kernel_device_get_name (MM_KERNEL_DEVICE (self))) {
         g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
-                     "only virtual subsystem supported");
+                     "name is mandatory in kernel device");
         return FALSE;
     }
 
-    if (!mm_kernel_device_get_name (MM_KERNEL_DEVICE (self))) {
+    /* sysfs path is mandatory as output, and will only be given if the
+     * specified device exists; but only if this wasn't a 'remove' event
+     * and not a virtual device.
+     */
+    if (self->priv->properties &&
+        g_strcmp0 (mm_kernel_event_properties_get_action (self->priv->properties), "remove") &&
+        g_strcmp0 (mm_kernel_event_properties_get_subsystem (self->priv->properties), "virtual") &&
+        !self->priv->sysfs_path) {
         g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
-                     "name is mandatory in kernel device");
+                     "device %s/%s not found",
+                     mm_kernel_event_properties_get_subsystem (self->priv->properties),
+                     mm_kernel_event_properties_get_name      (self->priv->properties));
         return FALSE;
     }
 
@@ -261,7 +985,13 @@ dispose (GObject *object)
 {
     MMKernelDeviceGeneric *self = MM_KERNEL_DEVICE_GENERIC (object);
 
-    g_clear_object (&self->priv->properties);
+    g_clear_pointer (&self->priv->physdev_product,      g_free);
+    g_clear_pointer (&self->priv->physdev_manufacturer, g_free);
+    g_clear_pointer (&self->priv->physdev_sysfs_path,   g_free);
+    g_clear_pointer (&self->priv->interface_sysfs_path, g_free);
+    g_clear_pointer (&self->priv->sysfs_path,           g_free);
+    g_clear_pointer (&self->priv->rules,                g_array_unref);
+    g_clear_object  (&self->priv->properties);
 
     G_OBJECT_CLASS (mm_kernel_device_generic_parent_class)->dispose (object);
 }
@@ -306,4 +1036,12 @@ mm_kernel_device_generic_class_init (MMKernelDeviceGenericClass *klass)
                              MM_TYPE_KERNEL_EVENT_PROPERTIES,
                              G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
     g_object_class_install_property (object_class, PROP_PROPERTIES, properties[PROP_PROPERTIES]);
+
+    properties[PROP_RULES] =
+        g_param_spec_boxed ("rules",
+                            "Rules",
+                            "List of rules to apply",
+                            G_TYPE_ARRAY,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+    g_object_class_install_property (object_class, PROP_RULES, properties[PROP_RULES]);
 }
diff --git a/src/kerneldevice/mm-kernel-device-generic.h b/src/kerneldevice/mm-kernel-device-generic.h
index 87f9994..0eb471f 100644
--- a/src/kerneldevice/mm-kernel-device-generic.h
+++ b/src/kerneldevice/mm-kernel-device-generic.h
@@ -44,8 +44,11 @@ struct _MMKernelDeviceGenericClass {
     MMKernelDeviceClass parent;
 };
 
-GType           mm_kernel_device_generic_get_type (void);
-MMKernelDevice *mm_kernel_device_generic_new      (MMKernelEventProperties  *properties,
-                                                   GError                  **error);
+GType           mm_kernel_device_generic_get_type       (void);
+MMKernelDevice *mm_kernel_device_generic_new            (MMKernelEventProperties  *properties,
+                                                         GError                  **error);
+MMKernelDevice *mm_kernel_device_generic_new_with_rules (MMKernelEventProperties  *properties,
+                                                         GArray                   *rules,
+                                                         GError                  **error);
 
 #endif /* MM_KERNEL_DEVICE_GENERIC_H */
diff --git a/src/mm-base-manager.c b/src/mm-base-manager.c
index 3e03d55..6609613 100644
--- a/src/mm-base-manager.c
+++ b/src/mm-base-manager.c
@@ -17,11 +17,17 @@
  * Copyright (C) 2011 - 2016 Aleksander Morgado <aleksander at aleksander.es>
  */
 
+#include <config.h>
+
 #include <string.h>
 #include <ctype.h>
 
 #include <gmodule.h>
-#include "mm-kernel-device-udev.h"
+
+#if WITH_UDEV
+# include "mm-kernel-device-udev.h"
+#endif
+#include "mm-kernel-device-generic.h"
 
 #include <ModemManager.h>
 #include <mm-errors-types.h>
@@ -62,8 +68,6 @@ struct _MMBaseManagerPrivate {
     gchar *plugin_dir;
     /* Path to the list of initial kernel events */
     gchar *initial_kernel_events;
-    /* The UDev client */
-    GUdevClient *udev;
     /* The authorization provider */
     MMAuthProvider *authp;
     GCancellable *authp_cancellable;
@@ -76,6 +80,11 @@ struct _MMBaseManagerPrivate {
 
     /* The Test interface support */
     MmGdbusTest *test_skeleton;
+
+#if WITH_UDEV
+    /* The UDev client */
+    GUdevClient *udev;
+#endif
 };
 
 /*****************************************************************************/
@@ -346,7 +355,11 @@ handle_kernel_event (MMBaseManager            *self,
     mm_dbg ("  name:      %s", name);
     mm_dbg ("  uid:       %s", uid ? uid : "n/a");
 
+#if WITH_UDEV
     kernel_device = mm_kernel_device_udev_new_from_properties (properties, error);
+#else
+    kernel_device = mm_kernel_device_generic_new (properties, error);
+#endif
 
     if (!kernel_device)
         return FALSE;
@@ -362,6 +375,8 @@ handle_kernel_event (MMBaseManager            *self,
     return TRUE;
 }
 
+#if WITH_UDEV
+
 static void
 handle_uevent (GUdevClient *client,
                const char *action,
@@ -474,6 +489,8 @@ process_scan (MMBaseManager *self,
     g_list_free (devices);
 }
 
+#endif
+
 static void
 process_initial_kernel_events (MMBaseManager *self)
 {
@@ -534,9 +551,13 @@ mm_base_manager_start (MMBaseManager *self,
         return;
     }
 
+#if WITH_UDEV
     mm_dbg ("Starting %s device scan...", manual_scan ? "manual" : "automatic");
     process_scan (self, manual_scan);
     mm_dbg ("Finished device scan...");
+#else
+    mm_dbg ("Unsupported %s device scan...", manual_scan ? "manual" : "automatic");
+#endif
 }
 
 /*****************************************************************************/
@@ -714,11 +735,17 @@ scan_devices_auth_ready (MMAuthProvider *authp,
     if (!mm_auth_provider_authorize_finish (authp, res, &error))
         g_dbus_method_invocation_take_error (ctx->invocation, error);
     else {
+#if WITH_UDEV
         /* Otherwise relaunch device scan */
         mm_base_manager_start (MM_BASE_MANAGER (ctx->self), TRUE);
         mm_gdbus_org_freedesktop_modem_manager1_complete_scan_devices (
             MM_GDBUS_ORG_FREEDESKTOP_MODEM_MANAGER1 (ctx->self),
             ctx->invocation);
+#else
+        g_dbus_method_invocation_return_error_literal (
+            ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
+            "Cannot request manual scan of devices: unsupported");
+#endif
     }
 
     scan_devices_context_free (ctx);
@@ -771,12 +798,14 @@ report_kernel_event_auth_ready (MMAuthProvider           *authp,
     if (!mm_auth_provider_authorize_finish (authp, res, &error))
         goto out;
 
+#if WITH_UDEV
     if (ctx->self->priv->auto_scan) {
         error = g_error_new_literal (MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                      "Cannot report kernel event: "
                                      "udev monitoring already in place");
         goto out;
     }
+#endif
 
     properties = mm_kernel_event_properties_new_from_dictionary (ctx->dictionary, &error);
     if (!properties)
@@ -990,7 +1019,6 @@ static void
 mm_base_manager_init (MMBaseManager *manager)
 {
     MMBaseManagerPrivate *priv;
-    const gchar *subsys[5] = { "tty", "net", "usb", "usbmisc", NULL };
 
     /* Setup private data */
     manager->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (manager,
@@ -1004,8 +1032,14 @@ mm_base_manager_init (MMBaseManager *manager)
     /* Setup internal lists of device objects */
     priv->devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
 
-    /* Setup UDev client */
-    priv->udev = g_udev_client_new (subsys);
+#if WITH_UDEV
+    {
+        const gchar *subsys[5] = { "tty", "net", "usb", "usbmisc", NULL };
+
+        /* Setup UDev client */
+        priv->udev = g_udev_client_new (subsys);
+    }
+#endif
 
     /* By default, enable autoscan */
     priv->auto_scan = TRUE;
@@ -1038,9 +1072,11 @@ initable_init (GInitable *initable,
 {
     MMBaseManagerPrivate *priv = MM_BASE_MANAGER (initable)->priv;
 
+#if WITH_UDEV
     /* If autoscan enabled, list for udev events */
     if (priv->auto_scan)
         g_signal_connect (priv->udev, "uevent", G_CALLBACK (handle_uevent), initable);
+#endif
 
     /* Create plugin manager */
     priv->plugin_manager = mm_plugin_manager_new (priv->plugin_dir, error);
@@ -1086,8 +1122,10 @@ finalize (GObject *object)
 
     g_hash_table_destroy (priv->devices);
 
+#if WITH_UDEV
     if (priv->udev)
         g_object_unref (priv->udev);
+#endif
 
     if (priv->plugin_manager)
         g_object_unref (priv->plugin_manager);
diff --git a/src/mm-context.c b/src/mm-context.c
index cf8025b..c00fa54 100644
--- a/src/mm-context.c
+++ b/src/mm-context.c
@@ -13,6 +13,7 @@
  * Copyright (C) 2012 Aleksander Morgado <aleksander at gnu.org>
  */
 
+#include <config.h>
 #include <stdlib.h>
 
 #include "mm-context.h"
@@ -20,14 +21,20 @@
 /*****************************************************************************/
 /* Application context */
 
-static gboolean version_flag;
-static gboolean debug;
+static gboolean     version_flag;
+static gboolean     debug;
 static const gchar *log_level;
 static const gchar *log_file;
-static gboolean show_ts;
-static gboolean rel_ts;
+static gboolean     show_ts;
+static gboolean     rel_ts;
+
+#if WITH_UDEV
+static gboolean no_auto_scan = FALSE;
+#else
+static gboolean no_auto_scan = TRUE;
+#endif
+
 static const gchar *initial_kernel_events;
-static gboolean no_auto_scan;
 
 static const GOptionEntry entries[] = {
     { "version", 'V', 0, G_OPTION_ARG_NONE, &version_flag, "Print version", NULL },
@@ -36,8 +43,14 @@ static const GOptionEntry entries[] = {
     { "log-file", 0, 0, G_OPTION_ARG_FILENAME, &log_file, "Path to log file", "[PATH]" },
     { "timestamps", 0, 0, G_OPTION_ARG_NONE, &show_ts, "Show timestamps in log output", NULL },
     { "relative-timestamps", 0, 0, G_OPTION_ARG_NONE, &rel_ts, "Use relative timestamps (from MM start)", NULL },
+#if WITH_UDEV
     { "no-auto-scan", 0, 0, G_OPTION_ARG_NONE, &no_auto_scan, "Don't auto-scan looking for devices", NULL },
-    { "initial-kernel-events", 0, 0, G_OPTION_ARG_FILENAME, &initial_kernel_events, "Path to initial kernel events file (requires --no-auto-scan)", "[PATH]" },
+#else
+    /* Keep the option when udev disabled, just so that the unit test setup can
+     * unconditionally use --no-auto-scan */
+    { "no-auto-scan", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &no_auto_scan, NULL, NULL },
+#endif
+    { "initial-kernel-events", 0, 0, G_OPTION_ARG_FILENAME, &initial_kernel_events, "Path to initial kernel events file", "[PATH]" },
     { NULL }
 };
 
@@ -176,8 +189,10 @@ mm_context_init (gint argc,
         print_version ();
 
     /* Initial kernel events processing may only be used if autoscan is disabled */
+#if WITH_UDEV
     if (!no_auto_scan && initial_kernel_events) {
         g_warning ("error: --initial-kernel-events must be used only if --no-auto-scan is also used");
         exit (1);
     }
+#endif
 }
diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am
index 97adac6..c5bb61d 100644
--- a/src/tests/Makefile.am
+++ b/src/tests/Makefile.am
@@ -17,6 +17,7 @@ AM_CFLAGS = \
 	-I${top_srcdir}/src/ \
 	-I${top_builddir}/src/ \
 	-I${top_srcdir}/src/kerneldevice \
+	-DTESTUDEVRULESDIR=\"${top_srcdir}/src/\" \
 	$(NULL)
 
 AM_LDFLAGS = \
@@ -24,6 +25,7 @@ AM_LDFLAGS = \
 	$(CODE_COVERAGE_LDFLAGS) \
 	$(top_builddir)/src/libhelpers.la \
 	$(top_builddir)/src/libport.la \
+	$(top_builddir)/src/libkerneldevice.la \
 	-lutil \
 	$(NULL)
 
@@ -39,7 +41,7 @@ endif
 
 ################################################################################
 # tests
-#  note: we abuse AM_LDFLAGS to include libhelpers and libport.
+#  note: we abuse AM_LDFLAGS to include the libraries being tested
 ################################################################################
 
 noinst_PROGRAMS = \
@@ -49,6 +51,7 @@ noinst_PROGRAMS = \
 	test-at-serial-port \
 	test-sms-part-3gpp \
 	test-sms-part-cdma \
+	test-udev-rules \
 	$(NULL)
 
 if WITH_QMI
diff --git a/src/tests/test-udev-rules.c b/src/tests/test-udev-rules.c
new file mode 100644
index 0000000..469885d
--- /dev/null
+++ b/src/tests/test-udev-rules.c
@@ -0,0 +1,79 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details:
+ *
+ * Copyright (C) 2016 Aleksander Morgado <aleksander at aleksander.es>
+ */
+
+#include <glib.h>
+#include <glib-object.h>
+#include <string.h>
+#include <stdio.h>
+#include <locale.h>
+
+#define _LIBMM_INSIDE_MM
+#include <libmm-glib.h>
+
+/* Define symbol to enable test message traces */
+#undef ENABLE_TEST_MESSAGE_TRACES
+
+#include "mm-kernel-device-generic-rules.h"
+#include "mm-log.h"
+
+/************************************************************/
+
+static void
+test_load_cleanup_core (void)
+{
+    GArray *rules;
+    GError *error = NULL;
+
+    rules = mm_kernel_device_generic_rules_load (TESTUDEVRULESDIR, &error);
+    g_assert_no_error (error);
+    g_assert (rules);
+    g_assert (rules->len > 0);
+
+    g_array_unref (rules);
+}
+
+/************************************************************/
+
+void
+_mm_log (const char *loc,
+         const char *func,
+         guint32 level,
+         const char *fmt,
+         ...)
+{
+#if defined ENABLE_TEST_MESSAGE_TRACES
+    /* Dummy log function */
+    va_list args;
+    gchar *msg;
+
+    va_start (args, fmt);
+    msg = g_strdup_vprintf (fmt, args);
+    va_end (args);
+    g_print ("%s\n", msg);
+    g_free (msg);
+#endif
+}
+
+int main (int argc, char **argv)
+{
+    setlocale (LC_ALL, "");
+
+    g_type_init ();
+    g_test_init (&argc, &argv, NULL);
+
+    g_test_add_func ("/MM/test-udev-rules/load-cleanup-core", test_load_cleanup_core);
+
+    return g_test_run ();
+}
diff --git a/test/Makefile.am b/test/Makefile.am
index ae526cb..cd2b5d5 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -6,12 +6,16 @@ EXTRA_DIST =
 # lsudev
 ################################################################################
 
+if WITH_UDEV
+
 noinst_PROGRAMS += lsudev
 
 lsudev_SOURCES  = lsudev.c
 lsudev_CPPFLAGS = $(GUDEV_CFLAGS)
 lsudev_LDADD    = $(GUDEV_LIBS)
 
+endif
+
 ################################################################################
 # mmtty
 ################################################################################
@@ -38,6 +42,31 @@ mmtty_LDADD = \
 	$(NULL)
 
 ################################################################################
+# mmrules
+################################################################################
+
+noinst_PROGRAMS += mmrules
+
+mmrules_SOURCES = mmrules.c
+
+mmrules_CPPFLAGS = \
+	$(MM_CFLAGS) \
+	-I$(top_srcdir) \
+	-I$(top_srcdir)/src \
+	-I$(top_srcdir)/src/kerneldevice \
+	-I$(top_srcdir)/include \
+	-I$(top_builddir)/include \
+	-I$(top_srcdir)/libmm-glib \
+	-I$(top_srcdir)/libmm-glib/generated \
+	-I$(top_builddir)/libmm-glib/generated
+	$(NULL)
+
+mmrules_LDADD = \
+	$(MM_LIBS) \
+	$(top_builddir)/src/libkerneldevice.la \
+	$(NULL)
+
+################################################################################
 # mmcli-test-sms
 ################################################################################
 
diff --git a/test/mmrules.c b/test/mmrules.c
new file mode 100644
index 0000000..f8c4c3e
--- /dev/null
+++ b/test/mmrules.c
@@ -0,0 +1,173 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details:
+ *
+ * Copyright (C) 2015 Aleksander Morgado <aleksander at aleksander.es>
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <locale.h>
+#include <string.h>
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#include <mm-log.h>
+#include <mm-kernel-device-generic-rules.h>
+
+#define PROGRAM_NAME    "mmrules"
+#define PROGRAM_VERSION PACKAGE_VERSION
+
+/* Context */
+static gchar    *path;
+static gboolean  verbose_flag;
+static gboolean  version_flag;
+
+static GOptionEntry main_entries[] = {
+    { "path", 'p', 0, G_OPTION_ARG_FILENAME, &path,
+      "Specify path to udev rules directory",
+      "[PATH]"
+    },
+    { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose_flag,
+      "Run action with verbose logs",
+      NULL
+    },
+    { "version", 'V', 0, G_OPTION_ARG_NONE, &version_flag,
+      "Print version",
+      NULL
+    },
+    { NULL }
+};
+
+void
+_mm_log (const char *loc,
+         const char *func,
+         guint32 level,
+         const char *fmt,
+         ...)
+{
+    va_list args;
+    gchar *msg;
+
+    if (!verbose_flag)
+        return;
+
+    va_start (args, fmt);
+    msg = g_strdup_vprintf (fmt, args);
+    va_end (args);
+    g_print ("%s\n", msg);
+    g_free (msg);
+}
+
+static void
+print_version_and_exit (void)
+{
+    g_print ("\n"
+             PROGRAM_NAME " " PROGRAM_VERSION "\n"
+             "Copyright (2015) Aleksander Morgado\n"
+             "License GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl-2.0.html>\n"
+             "This is free software: you are free to change and redistribute it.\n"
+             "There is NO WARRANTY, to the extent permitted by law.\n"
+             "\n");
+    exit (EXIT_SUCCESS);
+}
+
+static void
+print_rule (MMUdevRule *rule)
+{
+    /* Process conditions */
+    if (rule->conditions) {
+        guint i;
+
+        for (i = 0; i < rule->conditions->len; i++) {
+            MMUdevRuleMatch *rule_match;
+
+            rule_match = &g_array_index (rule->conditions, MMUdevRuleMatch, i);
+            switch (rule_match->type) {
+            case MM_UDEV_RULE_MATCH_TYPE_EQUAL:
+                g_print ("  [condition %u] %s == %s\n",
+                           i, rule_match->parameter, rule_match->value);
+                break;
+            case MM_UDEV_RULE_MATCH_TYPE_NOT_EQUAL:
+                g_print ("  [condition %u] %s != %s\n",
+                         i, rule_match->parameter, rule_match->value);
+                break;
+            case MM_UDEV_RULE_MATCH_TYPE_UNKNOWN:
+                g_assert_not_reached ();
+            }
+        }
+    }
+
+    /* Process result */
+    switch (rule->result.type) {
+    case MM_UDEV_RULE_RESULT_TYPE_LABEL:
+        g_print ("  [result] label %s\n", rule->result.content.tag);
+        break;
+    case MM_UDEV_RULE_RESULT_TYPE_GOTO_INDEX:
+        g_print ("  [result] jump to rule %u\n", rule->result.content.index);
+        break;
+    case MM_UDEV_RULE_RESULT_TYPE_PROPERTY:
+        g_print ("  [result] set property %s = %s\n",
+                 rule->result.content.property.name, rule->result.content.property.value);
+        break;
+    case MM_UDEV_RULE_RESULT_TYPE_GOTO_TAG:
+    case MM_UDEV_RULE_RESULT_TYPE_UNKNOWN:
+        g_assert_not_reached ();
+    }
+}
+
+int main (int argc, char **argv)
+{
+    GOptionContext *context;
+    GArray         *rules;
+    guint           i;
+    GError         *error = NULL;
+
+    setlocale (LC_ALL, "");
+
+    g_type_init ();
+
+    /* Setup option context, process it and destroy it */
+    context = g_option_context_new ("- ModemManager udev rules testing");
+    g_option_context_add_main_entries (context, main_entries, NULL);
+    g_option_context_parse (context, &argc, &argv, NULL);
+    g_option_context_free (context);
+
+    if (version_flag)
+        print_version_and_exit ();
+
+    /* No device path given? */
+    if (!path) {
+        g_printerr ("error: no path specified\n");
+        exit (EXIT_FAILURE);
+    }
+
+    /* Load rules from directory */
+    rules = mm_kernel_device_generic_rules_load (path, &error);
+    if (!rules) {
+        g_printerr ("error: couldn't load rules: %s", error->message);
+        exit (EXIT_FAILURE);
+    }
+
+    /* Print loaded rules */
+    for (i = 0; i < rules->len; i++) {
+        g_print ("-----------------------------------------\n");
+        g_print ("rule [%u]:\n", i);
+        print_rule (&g_array_index (rules, MMUdevRule, i));
+    }
+
+    g_array_unref (rules);
+
+    return EXIT_SUCCESS;
+}
-- 
2.9.3



More information about the ModemManager-devel mailing list