From 01acd5fb7ca38503bcc5e7ac1cd45e5b41ec598d Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 3 Oct 2026 16:42:55 +0200 Subject: [PATCH 1/2] Report display and input nodes, answer DMDRVI_IOCTL_DEVFS_GET_FRIEND Besides "monitor" and "block", dmdevfs now probes every node with the standard dmdrvi graphics and input ioctls and reports it to libsystemd as - "display": the driver answers DMDRVI_IOCTL_GFX_GET_INFO - e.g. for a dmview@ service per display; - "input": the driver answers DMDRVI_IOCTL_INPUT_GET_INFO - touch panels, mice, buttons. Every display or input driver implementing the standard ioctls gets its services started by device rules without reporting anything itself. report= accepts "display" and "input" too. The class names move to the public dmdevfs.h (DMDEVFS_CLASS_*). DMDRVI_IOCTL_DEVFS_GET_FRIEND (dmdrvi 2.4) on an open node is answered by dmdevfs itself, never by the driver: the index-th other member of the node's friends_group (absolute path, friend_role). This is what drivers get through dmdrvi_friend_changed(), for every module - e.g. a display service finding the touch panel of its display, without depending on the dmdevfs module. Tests: the test driver answers the new ioctls when configured with display=true / input=true; new steps check that display and input nodes start dsp@/inp@ units only under their own class, that report= limits them, that unmounting stops them, and the friends lookup. Requires dmdrvi 2.4. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 11 ++- README.md | 17 +++- include/dmdevfs.h | 29 ++++++ src/dmdevfs.c | 155 +++++++++++++++++++++++++------ tests/CMakeLists.txt | 8 +- tests/fixtures/config/mock.ini | 24 +++++ tests/fixtures/rules/devices.ini | 6 ++ tests/fixtures/units/dsp@.ini | 3 + tests/fixtures/units/inp@.ini | 3 + tests/mockdrv/CMakeLists.txt | 2 +- tests/mockdrv/mockdrv.c | 29 +++++- tests/test_dmdevfs.c | 63 ++++++++++++- 12 files changed, 306 insertions(+), 44 deletions(-) create mode 100644 tests/fixtures/units/dsp@.ini create mode 100644 tests/fixtures/units/inp@.ini diff --git a/CMakeLists.txt b/CMakeLists.txt index 675188c..9dade19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,12 +82,15 @@ dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} ) # Link to other DMOD modules (will be downloaded on build time) -# dmdrvi 2.1: DMDRVI_IOCTL_MONITOR_* (node reporting, see src/dmdevfs.c). -# libsystemd: nodes are reported as "monitor"/"block" devices so device rules -# can start services (dmdevmon, automount) for them. +# dmdrvi 2.4: DMDRVI_IOCTL_MONITOR_* / _GFX_* / _INPUT_* (node reporting) and +# DMDRVI_IOCTL_DEVFS_GET_FRIEND (answered by dmdevfs itself), see +# src/dmdevfs.c. +# libsystemd: nodes are reported as "monitor"/"block"/"display"/"input" +# devices so device rules can start services (dmdevmon, automount, dmview) +# for them. dmod_link_modules(${DMOD_MODULE_NAME} dmfsi - dmdrvi@>=2.1 + dmdrvi@>=2.4 dmini dmlist dmosi diff --git a/README.md b/README.md index cb58006..db99196 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ DMOD Driver File System - A driver-based file system module for embedded systems - **DMFSI Compatible**: Implements the standard DMOD file system interface - **DMVFS Integration**: Can be mounted as a file system in DMVFS - **Modular Design**: Built on DMOD framework for easy integration -- **libsystemd Device Reporting**: Reports monitored and block device nodes to libsystemd, so device rules can start services for them (see [libsystemd Device Reporting](#libsystemd-device-reporting)) +- **libsystemd Device Reporting**: Reports monitored, block, display and input device nodes to libsystemd, so device rules can start services for them (see [libsystemd Device Reporting](#libsystemd-device-reporting)) - **Partition Nodes**: Exposes the MBR (incl. extended/logical) and GPT partitions of block devices as their own nodes (`p`), with offsets translated by DMDEVFS (see [Partition Nodes](#partition-nodes)) - **dmdevmon Service**: Generic monitor service that drives every node implementing the dmdrvi monitor contract - presence detection, hot-plug and polling without driver threads (see [services/dmdevmon](services/dmdevmon/README.md)) @@ -151,7 +151,7 @@ parameter2 = value2 - **`driver_order`**: An integer that controls when the driver is configured relative to other drivers. All drivers with `driver_order = 0` (the default when the field is omitted) are configured before any driver with `driver_order = 1`, which in turn is configured before `driver_order = 2`, and so on. Use this to force a driver to be configured after another one when there is no dmod dependency between them to infer the order from automatically. Negative values are allowed and are configured before the default group: `driver_order = -1` runs before `0`, and `-2` before `-1`. Use a negative value to put a driver ahead of everything left at the default, without having to raise `driver_order` in every other configuration file. - **`friends_group`**: Groups configurations that form one logical peripheral. Once device paths are available, DMDEVFS reports every other member of the group through the optional driver's `dmdrvi_friend_changed()` callback. - **`friend_role`**: Optional application-defined role of this member inside its friends group, such as `chip_select`. It lets consumers distinguish several devices of the same type without relying on section names or generated paths. -- **`report`**: What this device's nodes may be reported to libsystemd as: `all` (default), `monitor`, `block` or `none`. Hot-plugged nodes inherit it. See [libsystemd Device Reporting](#libsystemd-device-reporting). +- **`report`**: What this device's nodes may be reported to libsystemd as: `all` (default), `monitor`, `block`, `display`, `input` or `none`. Hot-plugged nodes inherit it. See [libsystemd Device Reporting](#libsystemd-device-reporting). Any additional parameters in the configuration file are passed to the driver's initialization function. The interpretation of these parameters depends on the specific driver implementation. @@ -195,6 +195,13 @@ owner's group and role and generate a `ready` notification. Removing such a device with `dmdrvi_device_unavailable()` generates a `dead` notification. Both `friends_group` and the callback are optional. +Modules that are not drivers - e.g. a display service looking for the touch +panel of its display - get the same information with +`DMDRVI_IOCTL_DEVFS_GET_FRIEND` on an open node: DMDEVFS answers it itself +(the driver never sees it) with the `index`-th other member of the node's +group, its absolute path and its `friend_role`, and `-ENOENT` when there is +none. + ### libsystemd Device Reporting Once a node's absolute path is known (after `dmdrvi_path_ready()`, both for @@ -206,9 +213,11 @@ driver and asks what it is: |----------------|-------------------|--------------| | `DMDRVI_IOCTL_MONITOR_GET_POLICY` | `monitor` | `[class=monitor] start=dmdevmon@%name` - the node needs a monitor service (presence detection, hot-plug, polling); [dmdevmon](services/dmdevmon/README.md) ships with this repository | | `DMDRVI_IOCTL_BLOCK_GET_INFO` | `block` | `[class=block] start=automount@%name` - a block device | +| `DMDRVI_IOCTL_GFX_GET_INFO` | `display` | `[class=display] start=dmview@%name` - a display (any driver implementing the standard graphics ioctls) | +| `DMDRVI_IOCTL_INPUT_GET_INFO` | `input` | `[class=input] start=...@%name` - an input device (touch panel, mouse, buttons) | -A node can be reported under both classes, or under none (then nothing -happens). Each report is `libsystemd_notify_device_added(class, name, path)`: +A node can be reported under several classes, or under none (then nothing +happens). The class names are defined in `dmdevfs.h` (`DMDEVFS_CLASS_*`). Each report is `libsystemd_notify_device_added(class, name, path)`: - **name** - the node path relative to the mount with `/` replaced by `_`, because unit names cannot contain `/`: `/dmsdio0` -> `dmsdio0`, diff --git a/include/dmdevfs.h b/include/dmdevfs.h index 4e9e44d..cc044e3 100644 --- a/include/dmdevfs.h +++ b/include/dmdevfs.h @@ -15,6 +15,35 @@ extern "C" { #define DMDEVFS_VERSION_MAJOR 0 #define DMDEVFS_VERSION_MINOR 2 +/** + * @brief Device classes dmdevfs reports nodes under to libsystemd + * + * Once a node's absolute path is known, dmdevfs asks its driver what the + * node is and reports it under every class it answers to, so device rules + * (`[class=]`) can start services for it: + * + * - "monitor": DMDRVI_IOCTL_MONITOR_GET_POLICY - a monitor service + * (dmdevmon) has to drive it; + * - "block": DMDRVI_IOCTL_BLOCK_GET_INFO - a block device, e.g. for + * automount; + * - "display": DMDRVI_IOCTL_GFX_GET_INFO - a display, e.g. for dmview; + * - "input": DMDRVI_IOCTL_INPUT_GET_INFO - an input device (touch panel, + * mouse, buttons). + * + * The device name is the node path relative to the mount with '/' replaced + * by '_' (unit names cannot contain '/'): "/dmsdio0/0" -> "dmsdio0_0". The + * user value is the node's absolute path. Removal is reported when the node + * goes away, which makes libsystemd stop the matching units. These are the + * values of the `report=` configuration key, too (plus "all" and "none"). + * + * The members of a node's friends_group are available to every module + * through DMDRVI_IOCTL_DEVFS_GET_FRIEND on the node (dmdrvi_ioctl.h). + */ +#define DMDEVFS_CLASS_MONITOR "monitor" +#define DMDEVFS_CLASS_BLOCK "block" +#define DMDEVFS_CLASS_DISPLAY "display" +#define DMDEVFS_CLASS_INPUT "input" + #ifdef __cplusplus } #endif diff --git a/src/dmdevfs.c b/src/dmdevfs.c index c868ac2..b28f7bc 100644 --- a/src/dmdevfs.c +++ b/src/dmdevfs.c @@ -40,40 +40,28 @@ */ #define DMDEVFS_NULL_DEVICE_NAME "null" -/** - * @brief Device classes dmdevfs reports nodes under to libsystemd - * - * Once a node's absolute path is known, dmdevfs asks its driver what the node - * is and reports it to libsystemd, so device rules can start services for it - * (see report_node_added()): - * - * - "monitor": the driver answers DMDRVI_IOCTL_MONITOR_GET_POLICY - a - * monitor service (dmdevmon) has to drive it; - * - "block": the driver answers DMDRVI_IOCTL_BLOCK_GET_INFO - a block - * device, e.g. for automount. - * - * The device name is the node path relative to the mount with '/' replaced - * by '_' (unit names cannot contain '/'): "/dmsdio0/0" -> "dmsdio0_0". The - * user value is the node's absolute path. Removal is reported when the node - * goes away (dmdrvi_device_unavailable() or teardown), which makes libsystemd - * stop the matching units. +/* + * Device classes dmdevfs reports nodes under to libsystemd: DMDEVFS_CLASS_* + * in dmdevfs.h (see report_node_added()). */ -#define DMDEVFS_CLASS_MONITOR "monitor" -#define DMDEVFS_CLASS_BLOCK "block" /** * @brief Bits of driver_node_t::report_mask / driver_node_t::reported */ #define DMDEVFS_REPORT_MONITOR 0x01u #define DMDEVFS_REPORT_BLOCK 0x02u -#define DMDEVFS_REPORT_ALL (DMDEVFS_REPORT_MONITOR | DMDEVFS_REPORT_BLOCK) +#define DMDEVFS_REPORT_DISPLAY 0x04u +#define DMDEVFS_REPORT_INPUT 0x08u +#define DMDEVFS_REPORT_ALL (DMDEVFS_REPORT_MONITOR | DMDEVFS_REPORT_BLOCK | \ + DMDEVFS_REPORT_DISPLAY | DMDEVFS_REPORT_INPUT) /** * @brief Configuration key limiting what a device's nodes are reported as * - * "all" (default), "monitor", "block" or "none" - e.g. "none" for raw flash - * that must never be picked up by automount. Read from the same place as - * friends_group (see configure_driver()); hot-plugged nodes inherit it. + * "all" (default), "monitor", "block", "display", "input" or "none" - e.g. + * "none" for raw flash that must never be picked up by automount. Read from + * the same place as friends_group (see configure_driver()); hot-plugged nodes + * inherit it. */ #define DMDEVFS_REPORT_KEY "report" @@ -334,6 +322,7 @@ static int compare_mount_context( const void* data, const void* user_data ); static int compare_mount_owns_context( const void* data, const void* user_data ); static int compare_mount_owns_dynamic_device( const void* data, const void* user_data ); static int build_absolute_path( const char* mount_path, const char* node_path, char* out, size_t out_size ); +static int get_friend( dmfsi_context_t mount, driver_node_t* node, dmdrvi_devfs_friend_t* arg ); static void notify_driver_path_ready( const char* mount_path, driver_node_t* node ); static uint8_t read_report_mask( dmini_context_t config_ctx, const char* section_name, const char* driver_name ); static void report_node_added( const char* abs_path, driver_node_t* node ); @@ -837,6 +826,12 @@ dmod_dmfsi_dif_api_declaration( 1.0, dmdevfs, int, _ioctl, (dmfsi_context_t ctx, file_handle_t* handle = (file_handle_t*)fp; + // Device file system commands are answered here, never by the driver + if(request == DMDRVI_IOCTL_DEVFS_GET_FRIEND) + { + return get_friend(ctx, handle->driver, (dmdrvi_devfs_friend_t*)arg); + } + if(handle->driver->is_builtin) { // No driver module behind a built-in device to carry out an ioctl @@ -2932,6 +2927,14 @@ static uint8_t read_report_mask( dmini_context_t config_ctx, const char* section { return DMDEVFS_REPORT_BLOCK; } + if (strcmp(value, DMDEVFS_CLASS_DISPLAY) == 0) + { + return DMDEVFS_REPORT_DISPLAY; + } + if (strcmp(value, DMDEVFS_CLASS_INPUT) == 0) + { + return DMDEVFS_REPORT_INPUT; + } DMOD_LOG_WARN("Unknown %s=%s for driver %s, reporting all classes\n", DMDEVFS_REPORT_KEY, value, driver_name); return DMDEVFS_REPORT_ALL; @@ -2958,9 +2961,10 @@ static void build_report_name( const char* node_path, char* out, size_t out_size * @brief Ask a node's driver which classes the node belongs to * * Opens the node read-only through its driver, issues - * DMDRVI_IOCTL_MONITOR_GET_POLICY and/or DMDRVI_IOCTL_BLOCK_GET_INFO (only - * those allowed by @p wanted) and closes it again. A driver that cannot be - * opened, or answers neither, yields 0 - nothing is reported for it. + * DMDRVI_IOCTL_MONITOR_GET_POLICY, DMDRVI_IOCTL_BLOCK_GET_INFO, + * DMDRVI_IOCTL_GFX_GET_INFO and/or DMDRVI_IOCTL_INPUT_GET_INFO (only those + * allowed by @p wanted) and closes it again. A driver that cannot be opened, + * or answers none of them, yields 0 - nothing is reported for it. * * Must be called without g_devfs_mutex held (driver code). */ @@ -2993,6 +2997,18 @@ static uint8_t probe_node_classes( driver_node_t* node, uint8_t wanted ) { classes |= DMDEVFS_REPORT_BLOCK; } + dmdrvi_gfx_info_t gfx; + if ((wanted & DMDEVFS_REPORT_DISPLAY) != 0 && + dmdrvi_ioctl(node->driver_context, handle, DMDRVI_IOCTL_GFX_GET_INFO, &gfx) == 0) + { + classes |= DMDEVFS_REPORT_DISPLAY; + } + dmdrvi_input_info_t input; + if ((wanted & DMDEVFS_REPORT_INPUT) != 0 && + dmdrvi_ioctl(node->driver_context, handle, DMDRVI_IOCTL_INPUT_GET_INFO, &input) == 0) + { + classes |= DMDEVFS_REPORT_INPUT; + } dmdrvi_close(node->driver_context, handle); return classes; @@ -3061,6 +3077,14 @@ static void report_node_added( const char* abs_path, driver_node_t* node ) { report_node_class(DMDEVFS_CLASS_BLOCK, name, abs_path, true); } + if ((classes & DMDEVFS_REPORT_DISPLAY) != 0) + { + report_node_class(DMDEVFS_CLASS_DISPLAY, name, abs_path, true); + } + if ((classes & DMDEVFS_REPORT_INPUT) != 0) + { + report_node_class(DMDEVFS_CLASS_INPUT, name, abs_path, true); + } } /** @@ -3082,6 +3106,14 @@ static void report_node_removed( driver_node_t* node ) path_t name; build_report_name(node->path, name, sizeof(name)); + if ((classes & DMDEVFS_REPORT_INPUT) != 0) + { + report_node_class(DMDEVFS_CLASS_INPUT, name, NULL, false); + } + if ((classes & DMDEVFS_REPORT_DISPLAY) != 0) + { + report_node_class(DMDEVFS_CLASS_DISPLAY, name, NULL, false); + } if ((classes & DMDEVFS_REPORT_BLOCK) != 0) { report_node_class(DMDEVFS_CLASS_BLOCK, name, NULL, false); @@ -3092,6 +3124,77 @@ static void report_node_removed( driver_node_t* node ) } } +// ============================================================================ +// Friends of a node (DMDRVI_IOCTL_DEVFS_GET_FRIEND) +// ============================================================================ + +typedef struct +{ + dmfsi_context_t mount; + driver_node_t* self; + uint32_t index; // Members still to skip + dmdrvi_devfs_friend_t* out; + bool found; +} friend_lookup_t; + +/** + * @brief dmlist_foreach() callback: the index-th other member of a friends group + */ +static bool find_friend( void* data, void* user_data ) +{ + driver_node_t* node = (driver_node_t*)data; + friend_lookup_t* lookup = (friend_lookup_t*)user_data; + if (node == lookup->self || node->removed || node->is_builtin || node->friends_group == NULL || + strcmp(node->friends_group, lookup->self->friends_group) != 0) + { + return true; + } + path_t abs_path; + if (build_absolute_path(lookup->mount->mount_path, node->path, abs_path, sizeof(abs_path)) != 0) + { + return true; + } + if (lookup->index > 0) + { + lookup->index--; + return true; + } + strncpy(lookup->out->path, abs_path, sizeof(lookup->out->path) - 1); + lookup->out->path[sizeof(lookup->out->path) - 1] = '\0'; + strncpy(lookup->out->role, (node->friend_role != NULL) ? node->friend_role : "", sizeof(lookup->out->role) - 1); + lookup->out->role[sizeof(lookup->out->role) - 1] = '\0'; + lookup->found = true; + return false; +} + +/** + * @brief Answer DMDRVI_IOCTL_DEVFS_GET_FRIEND for @p node - never passed to its driver + * + * Copies everything out under g_devfs_mutex; no driver code and no logging + * is called while it is held. + * + * @return 0, -ENOENT when there is no member arg->index (or no group), + * -EINVAL for a NULL arg + */ +static int get_friend( dmfsi_context_t mount, driver_node_t* node, dmdrvi_devfs_friend_t* arg ) +{ + if (arg == NULL) + { + return -EINVAL; + } + friend_lookup_t lookup = { .mount = mount, .self = node, .index = arg->index, .out = arg, .found = false }; + arg->path[0] = '\0'; + arg->role[0] = '\0'; + + dmosi_mutex_lock(g_devfs_mutex); + if (mount->ready && node->friends_group != NULL) + { + dmlist_foreach(mount->drivers, find_friend, &lookup); + } + dmosi_mutex_unlock(g_devfs_mutex); + return lookup.found ? 0 : -ENOENT; +} + // ============================================================================ // Partitions // ============================================================================ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3e56f95..8d3c367 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -96,7 +96,9 @@ target_include_directories(${DMOD_MODULE_NAME} PRIVATE target_compile_definitions(${DMOD_MODULE_NAME} PRIVATE DMDEVFS_TEST_FIXTURES_DIR="fixtures" ) +# dmdrvi: DMDRVI_IOCTL_DEVFS_GET_FRIEND (2.4) dmod_link_modules(${DMOD_MODULE_NAME} + dmdrvi@>=2.4 dmfsi dmhaman dmosi @@ -122,7 +124,7 @@ target_compile_definitions(${DMOD_MODULE_NAME} PRIVATE ) dmod_link_modules(${DMOD_MODULE_NAME} dmfsi - dmdrvi@>=2.1 + dmdrvi@>=2.3 dmhaman dmosi ) @@ -141,7 +143,7 @@ target_compile_definitions(${DMOD_MODULE_NAME} PRIVATE ) dmod_link_modules(${DMOD_MODULE_NAME} dmfsi - dmdrvi@>=2.1 + dmdrvi@>=2.3 dmhaman dmosi libsystemd @@ -161,7 +163,7 @@ target_compile_definitions(${DMOD_MODULE_NAME} PRIVATE ) dmod_link_modules(${DMOD_MODULE_NAME} dmfsi - dmdrvi@>=2.1 + dmdrvi@>=2.3 dmhaman dmosi libsystemd diff --git a/tests/fixtures/config/mock.ini b/tests/fixtures/config/mock.ini index 0d5e527..6ea2e00 100644 --- a/tests/fixtures/config/mock.ini +++ b/tests/fixtures/config/mock.ini @@ -2,6 +2,11 @@ ; /dev/dmdevfs_mockdrv0 monitored -> reported as "monitor" ; /dev/dmdevfs_mockdrv1 monitored, opt-out -> not reported ; /dev/dmdevfs_mockdrv2 plain -> not reported +; /dev/dmdevfs_mockdrv3 display -> reported as "display" +; /dev/dmdevfs_mockdrv4 input device -> reported as "input" +; /dev/dmdevfs_mockdrv5 display, report=input -> not reported +; mock3 and mock4 are one panel (friends_group=panel): a display and its +; touch input (friend_role=touch). ; Each can hot-plug a block child /dev/dmdevfs_mockdrv/0. [mock0] driver_name=dmdevfs_mockdrv @@ -17,3 +22,22 @@ report=none [mock2] driver_name=dmdevfs_mockdrv major=2 + +[mock3] +driver_name=dmdevfs_mockdrv +major=3 +display=true +friends_group=panel + +[mock4] +driver_name=dmdevfs_mockdrv +major=4 +input=true +friends_group=panel +friend_role=touch + +[mock5] +driver_name=dmdevfs_mockdrv +major=5 +display=true +report=input diff --git a/tests/fixtures/rules/devices.ini b/tests/fixtures/rules/devices.ini index 4624534..67220c9 100644 --- a/tests/fixtures/rules/devices.ini +++ b/tests/fixtures/rules/devices.ini @@ -3,3 +3,9 @@ start=mon@%name [class=block] start=blk@%name + +[class=display] +start=dsp@%name + +[class=input] +start=inp@%name diff --git a/tests/fixtures/units/dsp@.ini b/tests/fixtures/units/dsp@.ini new file mode 100644 index 0000000..1e7c0cb --- /dev/null +++ b/tests/fixtures/units/dsp@.ini @@ -0,0 +1,3 @@ +; Started for every node dmdevfs reports as "display" (rules/devices.ini) +exec=dmdevfs_testsvc +args=%v diff --git a/tests/fixtures/units/inp@.ini b/tests/fixtures/units/inp@.ini new file mode 100644 index 0000000..c07cd4d --- /dev/null +++ b/tests/fixtures/units/inp@.ini @@ -0,0 +1,3 @@ +; Started for every node dmdevfs reports as "input" (rules/devices.ini) +exec=dmdevfs_testsvc +args=%v diff --git a/tests/mockdrv/CMakeLists.txt b/tests/mockdrv/CMakeLists.txt index c76b71e..cecd790 100644 --- a/tests/mockdrv/CMakeLists.txt +++ b/tests/mockdrv/CMakeLists.txt @@ -18,7 +18,7 @@ target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE ) dmod_link_modules(${DMOD_MODULE_NAME} - dmdrvi@>=2.1 + dmdrvi@>=2.3 dmini dmlist ) diff --git a/tests/mockdrv/mockdrv.c b/tests/mockdrv/mockdrv.c index aa8ecdc..7dba130 100644 --- a/tests/mockdrv/mockdrv.c +++ b/tests/mockdrv/mockdrv.c @@ -10,8 +10,10 @@ /* * Test driver for the libsystemd node reporting of dmdevfs. * - * Configuration keys: `major` (device number) and `monitor` ("true": the host - * node implements the monitor contract). Its policy comes from + * Configuration keys: `major` (device number), `monitor` ("true": the host + * node implements the monitor contract), `display` / `input` ("true": the + * host node answers DMDRVI_IOCTL_GFX_GET_INFO / DMDRVI_IOCTL_INPUT_GET_INFO, + * as a display or an input device would). Its policy comes from * `event_handler`, `settle_ms` and `poll_interval_ms`; MONITOR_EVENT and * MONITOR_REFRESH calls are only counted (DMDEVFS_MOCKDRV_IOCTL_GET_STATS), * as are the opens of the host node and of its child. @@ -33,6 +35,8 @@ struct dmdrvi_context uint32_t magic; uint8_t major; bool monitor; + bool display; + bool input; bool plugged; dmdrvi_monitor_policy_t policy; volatile uint32_t events; /* MONITOR_EVENT calls */ @@ -209,6 +213,8 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmdevfs_mockdrv, dmdrvi_context_t, _create, ctx->magic = MOCKDRV_CONTEXT_MAGIC; ctx->major = (uint8_t)dmini_get_int(config, NULL, "major", 0); ctx->monitor = (strcmp(monitor, "true") == 0); + ctx->display = (strcmp(dmini_get_string(config, NULL, "display", "false"), "true") == 0); + ctx->input = (strcmp(dmini_get_string(config, NULL, "input", "false"), "true") == 0); ctx->plugged = false; ctx->events = 0; ctx->refreshes = 0; @@ -345,6 +351,25 @@ static int host_ioctl(dmdrvi_context_t context, int command, void* arg) case DMDRVI_IOCTL_MONITOR_EVENT: case DMDRVI_IOCTL_MONITOR_REFRESH: return monitor_ioctl(context, command, arg); + case DMDRVI_IOCTL_GFX_GET_INFO: + if (!context->display || arg == NULL) + { + return -ENOTTY; + } + memset(arg, 0, sizeof(dmdrvi_gfx_info_t)); + ((dmdrvi_gfx_info_t*)arg)->width = 480; + ((dmdrvi_gfx_info_t*)arg)->height = 272; + return 0; + case DMDRVI_IOCTL_INPUT_GET_INFO: + if (!context->input || arg == NULL) + { + return -ENOTTY; + } + memset(arg, 0, sizeof(dmdrvi_input_info_t)); + ((dmdrvi_input_info_t*)arg)->type = DMDRVI_INPUT_TYPE_TOUCHSCREEN; + ((dmdrvi_input_info_t*)arg)->width = 480; + ((dmdrvi_input_info_t*)arg)->height = 272; + return 0; case DMDEVFS_MOCKDRV_IOCTL_SET_MEDIA: if (context->plugged || arg == NULL) { diff --git a/tests/test_dmdevfs.c b/tests/test_dmdevfs.c index 8f33771..307f94a 100644 --- a/tests/test_dmdevfs.c +++ b/tests/test_dmdevfs.c @@ -6,6 +6,8 @@ #include "dmhaman.h" #include "libsystemd.h" #include "dmdevfs_mockdrv.h" +#include "dmdrvi_ioctl.h" +#include #include /* @@ -14,8 +16,9 @@ * dmdevfs is mounted at /dev from fixtures/config with the test driver * dmdevfs_mockdrv (mockdrv/). libsystemd has the units from fixtures/units * and the rules from fixtures/rules loaded, so every report dmdevfs makes - * starts a real process (dmdevfs_testsvc) as mon@ ("monitor") or - * blk@ ("block"), and every removal stops it again - both observable + * starts a real process (dmdevfs_testsvc) as mon@ ("monitor"), + * blk@ ("block"), dsp@ ("display") or inp@ ("input"), and + * every removal stops it again - both observable * through libsystemd_status(). A unit that was never reported does not exist * at all (-ENOENT). * @@ -74,9 +77,12 @@ void dmod_test_setup(void) if (g_mount != NULL) { g_fs.mounted(g_mount, "/dev"); - /* Every mount starts mon@dmdevfs_mockdrv0 - let it come up before - * any step (or the teardown) may stop it. */ + /* Every mount starts mon@dmdevfs_mockdrv0, dsp@dmdevfs_mockdrv3 and + * inp@dmdevfs_mockdrv4 - let them come up before any step (or the + * teardown) may stop them. */ wait_ready("mon@dmdevfs_mockdrv0", "/dev/dmdevfs_mockdrv0"); + wait_ready("dsp@dmdevfs_mockdrv3", "/dev/dmdevfs_mockdrv3"); + wait_ready("inp@dmdevfs_mockdrv4", "/dev/dmdevfs_mockdrv4"); } } @@ -175,6 +181,55 @@ DMOD_TEST_STEP(dmdevfs_skips_opted_out_and_unmonitored_nodes) DMOD_TEST_EXPECT_EQ(unit_state("blk@dmdevfs_mockdrv0"), -1); /* host node is no block device */ } +DMOD_TEST_STEP(dmdevfs_reports_display_and_input_nodes) +{ + DMOD_TEST_EXPECT_NOT_NULL(g_mount); + DMOD_TEST_EXPECT_TRUE(wait_ready("dsp@dmdevfs_mockdrv3", "/dev/dmdevfs_mockdrv3")); + DMOD_TEST_EXPECT_TRUE(wait_ready("inp@dmdevfs_mockdrv4", "/dev/dmdevfs_mockdrv4")); + DMOD_TEST_EXPECT_EQ(unit_state("inp@dmdevfs_mockdrv3"), -1); /* display only */ + DMOD_TEST_EXPECT_EQ(unit_state("dsp@dmdevfs_mockdrv4"), -1); /* input only */ + DMOD_TEST_EXPECT_EQ(unit_state("mon@dmdevfs_mockdrv3"), -1); + DMOD_TEST_EXPECT_EQ(unit_state("dsp@dmdevfs_mockdrv5"), -1); /* report=input */ + DMOD_TEST_EXPECT_EQ(unit_state("inp@dmdevfs_mockdrv5"), -1); /* ... and no input device */ + DMOD_TEST_EXPECT_EQ(unit_state("dsp@dmdevfs_mockdrv2"), -1); /* plain node */ + + g_fs.deinit(g_mount); + g_mount = NULL; + DMOD_TEST_EXPECT_TRUE(wait_state("dsp@dmdevfs_mockdrv3", DMOSI_PROCESS_STATE_TERMINATED)); + DMOD_TEST_EXPECT_TRUE(wait_state("inp@dmdevfs_mockdrv4", DMOSI_PROCESS_STATE_TERMINATED)); +} + +/** DMDRVI_IOCTL_DEVFS_GET_FRIEND on @p node through dmdevfs. */ +static int get_friend(const char* node, uint32_t index, dmdrvi_devfs_friend_t* f) +{ + void* file = NULL; + if (g_fs.fopen(g_mount, &file, node, DMFSI_O_RDONLY, 0) != DMFSI_OK) + { + return -ENODEV; + } + f->index = index; + int ret = g_fs.ioctl(g_mount, file, DMDRVI_IOCTL_DEVFS_GET_FRIEND, f); + g_fs.fclose(g_mount, file); + return ret; +} + +DMOD_TEST_STEP(dmdevfs_finds_friends_of_a_node) +{ + dmdrvi_devfs_friend_t f; + + DMOD_TEST_EXPECT_NOT_NULL(g_mount); + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv3", 0, &f), 0); + DMOD_TEST_EXPECT_EQ(strcmp(f.path, "/dev/dmdevfs_mockdrv4"), 0); + DMOD_TEST_EXPECT_EQ(strcmp(f.role, "touch"), 0); + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv3", 1, &f), -ENOENT); + + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv4", 0, &f), 0); + DMOD_TEST_EXPECT_EQ(strcmp(f.path, "/dev/dmdevfs_mockdrv3"), 0); + DMOD_TEST_EXPECT_EQ(strcmp(f.role, ""), 0); + + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv0", 0, &f), -ENOENT); /* no group */ +} + DMOD_TEST_STEP(dmdevfs_reports_hot_plugged_block_node_and_its_removal) { DMOD_TEST_EXPECT_NOT_NULL(g_mount); From 800f5cc0cde73be2d9ec00e8c0987b3fc2e4fcd2 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 3 Oct 2026 17:09:50 +0200 Subject: [PATCH 2/2] DEVFS_GET_FRIEND: write into the caller's buffers, no length limit The answer is written straight into the buffers the caller passes in dmdrvi_devfs_friend_t - no path_t or other fixed-size buffer of dmdevfs in between. The lengths are always filled in; NULL or too small buffers give -ERANGE, so callers can ask for the lengths first. Nothing is logged under g_devfs_mutex. Tests cover asking for the lengths only, an exactly fitting buffer and a buffer one byte short. Co-Authored-By: Claude Opus 5.5 --- src/dmdevfs.c | 66 ++++++++++++++++++++++++++++++-------------- tests/test_dmdevfs.c | 47 ++++++++++++++++++++++++------- 2 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/dmdevfs.c b/src/dmdevfs.c index b28f7bc..a5df040 100644 --- a/src/dmdevfs.c +++ b/src/dmdevfs.c @@ -3130,11 +3130,9 @@ static void report_node_removed( driver_node_t* node ) typedef struct { - dmfsi_context_t mount; driver_node_t* self; uint32_t index; // Members still to skip - dmdrvi_devfs_friend_t* out; - bool found; + driver_node_t* found; } friend_lookup_t; /** @@ -3149,32 +3147,39 @@ static bool find_friend( void* data, void* user_data ) { return true; } - path_t abs_path; - if (build_absolute_path(lookup->mount->mount_path, node->path, abs_path, sizeof(abs_path)) != 0) - { - return true; - } if (lookup->index > 0) { lookup->index--; return true; } - strncpy(lookup->out->path, abs_path, sizeof(lookup->out->path) - 1); - lookup->out->path[sizeof(lookup->out->path) - 1] = '\0'; - strncpy(lookup->out->role, (node->friend_role != NULL) ? node->friend_role : "", sizeof(lookup->out->role) - 1); - lookup->out->role[sizeof(lookup->out->role) - 1] = '\0'; - lookup->found = true; + lookup->found = node; return false; } +/** + * @brief Copy @p len bytes of @p src and a terminator into @p dst, if it fits + */ +static bool copy_out( char* dst, size_t dst_size, const char* src, size_t len ) +{ + if (dst == NULL || dst_size <= len) + { + return false; + } + memcpy(dst, src, len); + dst[len] = '\0'; + return true; +} + /** * @brief Answer DMDRVI_IOCTL_DEVFS_GET_FRIEND for @p node - never passed to its driver * - * Copies everything out under g_devfs_mutex; no driver code and no logging - * is called while it is held. + * Writes straight into the caller's buffers - no length limit of its own. + * Everything is done under g_devfs_mutex; no driver code and no logging is + * called while it is held. * * @return 0, -ENOENT when there is no member arg->index (or no group), - * -EINVAL for a NULL arg + * -ERANGE when a buffer is too small (lengths filled in), -EINVAL + * for a NULL arg */ static int get_friend( dmfsi_context_t mount, driver_node_t* node, dmdrvi_devfs_friend_t* arg ) { @@ -3182,17 +3187,38 @@ static int get_friend( dmfsi_context_t mount, driver_node_t* node, dmdrvi_devfs_ { return -EINVAL; } - friend_lookup_t lookup = { .mount = mount, .self = node, .index = arg->index, .out = arg, .found = false }; - arg->path[0] = '\0'; - arg->role[0] = '\0'; + arg->path_length = 0; + arg->role_length = 0; + friend_lookup_t lookup = { .self = node, .index = arg->index, .found = NULL }; + int ret = -ENOENT; dmosi_mutex_lock(g_devfs_mutex); if (mount->ready && node->friends_group != NULL) { dmlist_foreach(mount->drivers, find_friend, &lookup); } + if (lookup.found != NULL) + { + /* Absolute path: the mount path (nothing for "/") followed by the node path */ + const char* prefix = (strcmp(mount->mount_path, ROOT_DIRECTORY_NAME) == 0) ? "" : mount->mount_path; + const char* role = (lookup.found->friend_role != NULL) ? lookup.found->friend_role : ""; + size_t prefix_len = strlen(prefix); + size_t node_len = strlen(lookup.found->path); + arg->path_length = prefix_len + node_len; + arg->role_length = strlen(role); + + bool fits = arg->path != NULL && arg->path_size > arg->path_length && + arg->role != NULL && arg->role_size > arg->role_length; + if (fits) + { + memcpy(arg->path, prefix, prefix_len); + (void)copy_out(arg->path + prefix_len, arg->path_size - prefix_len, lookup.found->path, node_len); + (void)copy_out(arg->role, arg->role_size, role, arg->role_length); + } + ret = fits ? 0 : -ERANGE; + } dmosi_mutex_unlock(g_devfs_mutex); - return lookup.found ? 0 : -ENOENT; + return ret; } // ============================================================================ diff --git a/tests/test_dmdevfs.c b/tests/test_dmdevfs.c index 307f94a..8b994dd 100644 --- a/tests/test_dmdevfs.c +++ b/tests/test_dmdevfs.c @@ -200,34 +200,61 @@ DMOD_TEST_STEP(dmdevfs_reports_display_and_input_nodes) } /** DMDRVI_IOCTL_DEVFS_GET_FRIEND on @p node through dmdevfs. */ -static int get_friend(const char* node, uint32_t index, dmdrvi_devfs_friend_t* f) +static int get_friend(const char* node, dmdrvi_devfs_friend_t* f) { void* file = NULL; if (g_fs.fopen(g_mount, &file, node, DMFSI_O_RDONLY, 0) != DMFSI_OK) { return -ENODEV; } - f->index = index; int ret = g_fs.ioctl(g_mount, file, DMDRVI_IOCTL_DEVFS_GET_FRIEND, f); g_fs.fclose(g_mount, file); return ret; } +static void ask_friend(dmdrvi_devfs_friend_t* f, uint32_t index, char* path, size_t path_size, char* role, size_t role_size) +{ + memset(f, 0, sizeof(*f)); + f->index = index; + f->path = path; + f->path_size = path_size; + f->role = role; + f->role_size = role_size; +} + DMOD_TEST_STEP(dmdevfs_finds_friends_of_a_node) { dmdrvi_devfs_friend_t f; + char path[64], role[16]; DMOD_TEST_EXPECT_NOT_NULL(g_mount); - DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv3", 0, &f), 0); - DMOD_TEST_EXPECT_EQ(strcmp(f.path, "/dev/dmdevfs_mockdrv4"), 0); - DMOD_TEST_EXPECT_EQ(strcmp(f.role, "touch"), 0); - DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv3", 1, &f), -ENOENT); - DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv4", 0, &f), 0); - DMOD_TEST_EXPECT_EQ(strcmp(f.path, "/dev/dmdevfs_mockdrv3"), 0); - DMOD_TEST_EXPECT_EQ(strcmp(f.role, ""), 0); + /* NULL buffers: the lengths only */ + ask_friend(&f, 0, NULL, 0, NULL, 0); + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv3", &f), -ERANGE); + DMOD_TEST_EXPECT_EQ(f.path_length, strlen("/dev/dmdevfs_mockdrv4")); + DMOD_TEST_EXPECT_EQ(f.role_length, strlen("touch")); + + /* Exactly large enough */ + ask_friend(&f, 0, path, strlen("/dev/dmdevfs_mockdrv4") + 1, role, strlen("touch") + 1); + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv3", &f), 0); + DMOD_TEST_EXPECT_EQ(strcmp(path, "/dev/dmdevfs_mockdrv4"), 0); + DMOD_TEST_EXPECT_EQ(strcmp(role, "touch"), 0); + + /* One byte short */ + ask_friend(&f, 0, path, strlen("/dev/dmdevfs_mockdrv4"), role, sizeof(role)); + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv3", &f), -ERANGE); + + ask_friend(&f, 1, path, sizeof(path), role, sizeof(role)); + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv3", &f), -ENOENT); + + ask_friend(&f, 0, path, sizeof(path), role, sizeof(role)); + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv4", &f), 0); + DMOD_TEST_EXPECT_EQ(strcmp(path, "/dev/dmdevfs_mockdrv3"), 0); + DMOD_TEST_EXPECT_EQ(strcmp(role, ""), 0); - DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv0", 0, &f), -ENOENT); /* no group */ + ask_friend(&f, 0, path, sizeof(path), role, sizeof(role)); + DMOD_TEST_EXPECT_EQ(get_friend("/dmdevfs_mockdrv0", &f), -ENOENT); /* no group */ } DMOD_TEST_STEP(dmdevfs_reports_hot_plugged_block_node_and_its_removal)