From 172db50c33beeb53f8e75c115699179be880b960 Mon Sep 17 00:00:00 2001 From: Farshid Monhaseri Date: Tue, 10 Nov 2020 13:05:37 +0330 Subject: Add device monitoring feature (hotplug device detection) Changes: - Implement libudev 'monitor' interface to detect hotplug devices. - Forward udev rules as AFB 'subscribe' args to udev_monitoring objects. - Filter retrieved property fields through 'mask' argument from 'subscribe' args. - Generate detached threads (event loop) and data context for each client, so every user can subscribe for device events with their own udev rules and receive their desired device parameters. Bug-AGL: SPEC-3512 Signed-off-by: Farshid Monhaseri Change-Id: I0fc007d9707deaf39d21147aa8240fde302f5f9e --- conf.d/cmake/config.cmake | 3 +- src/CMakeLists.txt | 5 +- src/platform-info-binding.c | 133 +++++++++++---- src/platform-info-binding.h | 4 +- src/platform-info-devices.c | 397 ++++++++++++++++++++++++++++++++++++++++++++ src/platform-info-devices.h | 50 ++++++ 6 files changed, 554 insertions(+), 38 deletions(-) create mode 100644 src/platform-info-devices.c create mode 100644 src/platform-info-devices.h diff --git a/conf.d/cmake/config.cmake b/conf.d/cmake/config.cmake index 8b44fd3..c410ff5 100644 --- a/conf.d/cmake/config.cmake +++ b/conf.d/cmake/config.cmake @@ -63,9 +63,10 @@ set (gcc_minimal_version 4.9) # PKG_CONFIG required packages # ----------------------------- set (PKG_REQUIRED_LIST - json-c>=0.12 + json-c>=0.13 afb-daemon afb-helpers + libudev ) # You can also consider to include libsystemd diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 45bc242..ae1da6a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,7 +18,10 @@ PROJECT_TARGET_ADD(platform-info) # Define project Targets - ADD_LIBRARY(${TARGET_NAME} MODULE platform-info-binding.c) + ADD_LIBRARY(${TARGET_NAME} MODULE + platform-info-binding.c + platform-info-devices.c + ) # Binder exposes a unique public entry point SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES diff --git a/src/platform-info-binding.c b/src/platform-info-binding.c index 5e78fd9..4ae3c70 100644 --- a/src/platform-info-binding.c +++ b/src/platform-info-binding.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2019 "IoT.bzh" + * Copyright (C) 2016-2020 "IoT.bzh" * * Author "Romain Forlot" * @@ -23,6 +23,7 @@ #include #include "platform-info-binding.h" +#include "platform-info-devices.h" #ifndef PLATFORM_INFO_DIR #define PLATFORM_INFO_DIR "/etc/platform-info" @@ -33,30 +34,42 @@ #endif void afv_get(afb_req_t req) { - char *json_path = NULL, *full_json_path = NULL; - json_object *platform_info = (json_object*) afb_api_get_userdata(req->api); - json_object *args = afb_req_json(req), *result = NULL; - - switch (json_object_get_type(args)) { - case json_type_null: - result = platform_info; - break; - case json_type_string: - full_json_path = strdupa(json_object_get_string(args)); - result = platform_info; - for(json_path = strtok(full_json_path, "."); json_path && *json_path; json_path = strtok(NULL, ".")) { - if(! json_object_object_get_ex(result, json_path, &result)) { - afb_req_fail(req, "A key hasn't been found in JSON path.", json_path); - return; + pinfo_api_ctx_t *api_ctx = (pinfo_api_ctx_t*)afb_api_get_userdata(req->api); + + if(api_ctx) { + json_object *result = NULL; + json_object *args = afb_req_json(req); + + switch (json_object_get_type(args)) { + case json_type_null: + result = api_ctx->info; + break; + case json_type_string: { + char *json_path = NULL; + char *full_json_path = NULL; + + full_json_path = strdupa(json_object_get_string(args)); + result = api_ctx->info; + + for(json_path = strtok(full_json_path, "."); + json_path && *json_path; + json_path = strtok(NULL, ".")) { + if(! json_object_object_get_ex(result, json_path, &result)) { + afb_req_fail(req, "A key hasn't been found in JSON path.", json_path); + return; + } } + break; } - break; - default: - afb_req_fail(req, "Type error", "Argument type is unknown, you must provide a string only"); - return; - } + default: + afb_req_fail(req, "Type error", "Argument type is unknown, you must provide a string only"); + return; + } - afb_req_success(req, json_object_get(result), NULL); + afb_req_success(req, json_object_get(result), NULL); + } else { + afb_req_fail(req,"failed","The API contains no context!"); + } } void afv_set(afb_req_t req) { @@ -75,27 +88,60 @@ void afv_set(afb_req_t req) { afb_req_success(req, NULL, NULL); } -// TODO RFOR: interface with inotify and udev +// TODO RFOR: interface with inotify void afv_unsubscribe(afb_req_t req) { - afb_req_success(req, NULL, NULL); + pinfo_client_ctx_t* client_ctx = NULL; + client_ctx = (pinfo_client_ctx_t*)afb_req_context_get(req); + + if(client_ctx) { + const char* event_str = afb_req_value(req,"event"); + + if(event_str && strcmp("monitor-devices",event_str) == 0) { + afb_req_unsubscribe(req,client_ctx->ev_devs_changed); + afb_req_context_clear(req); + afb_req_success(req,NULL, NULL); + } else { + afb_req_fail(req,"failed","No 'event' value provided."); + } + } else { + afb_req_fail(req,"failed","No context available for the client."); + } } void afv_subscribe(afb_req_t req) { - afb_req_success(req, NULL, NULL); + const char* event_str = NULL; + event_str = afb_req_value(req,"event"); + + if(event_str && strcmp("monitor-devices",event_str) == 0) { + if(!afb_req_context_get(req)) { + if(pinfo_device_monitor(req) == PINFO_OK) { + afb_req_success(req,NULL,NULL); + } else { + afb_req_fail(req,"failed","Unable to create new context"); + } + } else { + afb_req_fail(req,"failed","The client already subscribed."); + } + } else { + afb_req_fail(req,"failed","Invalid event subscription"); + } } -int init(afb_api_t api) { +static json_object* +afv_static_info(const char* dir) { struct dirent* dir_ent = NULL; + DIR* dir_handle = opendir(dir); + json_object* static_info = NULL; - DIR* dir_handle = opendir(PLATFORM_INFO_DIR); if (! dir_handle) { AFB_ERROR("The directory %s does not exist.", PLATFORM_INFO_DIR); - return -1; + return NULL; } - json_object *json_file = json_object_new_object(), *current_file = NULL; + static_info = json_object_new_object(); while( (dir_ent = readdir(dir_handle)) != NULL) { if(dir_ent->d_type == DT_REG && dir_ent->d_name[0] != '.') { + json_object* current_file = NULL; size_t filepath_len = strlen(PLATFORM_INFO_DIR) + strlen(dir_ent->d_name) + 2; char *filepath = alloca(filepath_len); @@ -117,14 +163,35 @@ int init(afb_api_t api) { continue; } #endif - wrap_json_object_add(json_file, current_file); - AFB_DEBUG("JSON loaded: %s", json_object_get_string(json_file)); + wrap_json_object_add(static_info, current_file); + AFB_DEBUG("JSON loaded: %s", + json_object_to_json_string_ext(current_file,JSON_C_TO_STRING_PRETTY)); } } + return static_info; +} + +int init(afb_api_t api) { // Initializing the platform_info binding object and associated it to // the api - afb_api_set_userdata(api, (void*)json_file); + AFB_DEBUG("init() ..."); + pinfo_api_ctx_t *api_ctx = NULL; + int ret = PINFO_OK; + + api_ctx = malloc(sizeof(*api_ctx)); + + if(api_ctx) { + AFB_DEBUG("init() ... OK"); + api_ctx->info = afv_static_info(PLATFORM_INFO_DIR); + AFB_API_DEBUG(api,"The API static data: %s", + json_object_to_json_string_ext(api_ctx->info, JSON_C_TO_STRING_PRETTY)); + api_ctx->client_count = 0; + afb_api_set_userdata(api, (void*)api_ctx); + } else { + AFB_API_WARNING(api,"Failed to load the static data"); + ret = PINFO_ERR; + } - return 0; + return ret; } diff --git a/src/platform-info-binding.h b/src/platform-info-binding.h index 183fa86..8943080 100644 --- a/src/platform-info-binding.h +++ b/src/platform-info-binding.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016, 2018 "IoT.bzh" + * Copyright (C) 2016, 2018, 2020 "IoT.bzh" * * Author "Romain Forlot" * @@ -23,5 +23,3 @@ struct key_search_t { afb_api_t api; json_object **result; }; - -json_object *platform_info; diff --git a/src/platform-info-devices.c b/src/platform-info-devices.c new file mode 100644 index 0000000..89796f1 --- /dev/null +++ b/src/platform-info-devices.c @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2020 "IoT.bzh" + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include +#include +#include +#include +#include +#include "platform-info-devices.h" + +#define JSON_NEW_CONST_KEY \ + (JSON_C_OBJECT_KEY_IS_CONSTANT | JSON_C_OBJECT_ADD_KEY_IS_NEW) +#define UNUSED(x) (void)(x); + +static void* pinfo_device_client_new(void* req_ctx); +static void pinfo_device_client_free(void* client_ctx); +static void* pinfo_device_monitor_loop(pinfo_client_ctx_t* ctx); +static void pinfo_device_monitor_detect(pinfo_client_ctx_t* ctx, struct json_object* jdevice); +static json_object* pinfo_device_udevice_to_jdevice(struct udev_device* udevice, struct json_object* jmask); +static void pinfo_device_jdev_destructor(json_object* jdevice, void* udevice); +static int pinfo_device_filter_monitoring(pinfo_client_ctx_t* ctx); +static json_object* pinfo_device_udevice_to_jlist( + struct udev_device* udevice, + struct udev_list_entry*(*udevice_elist)(struct udev_device*), + const char*(*elist_val_getter)(struct udev_device*,const char*), + const unsigned int jflags); +static json_object* pinfo_device_udevice_to_jlist_mask( + struct udev_device* udevice, + const char*(*udevice_val_getter)(struct udev_device*,const char*), + json_object* jkeys, + unsigned jcpy_flags); + +static void +pinfo_device_monitor_detect(pinfo_client_ctx_t* ctx, struct json_object* jdevice) +{ + afb_event_push((afb_event_t)ctx->ev_devs_changed,jdevice); +} + +int +pinfo_device_monitor(afb_req_t req) { + int res = PINFO_ERR; + + if(afb_req_context(req,0,pinfo_device_client_new,pinfo_device_client_free,req)) { + res = PINFO_OK; + } + + return res; +} + +static void +pinfo_device_client_free(void* client_ctx) { + if(client_ctx) { + pinfo_client_ctx_t* ctx = (pinfo_client_ctx_t*)client_ctx; + if(ctx) { + pthread_cancel(ctx->th); + udev_monitor_unref(ctx->umon_hndl); + udev_unref(ctx->udev_ctx); + afb_event_unref((afb_event_t)ctx->ev_devs_changed); + json_object_put(ctx->filter); + json_object_put(ctx->mask); + ctx->api_ctx->client_count--; + AFB_DEBUG("Client context released, client count: %d",ctx->api_ctx->client_count); + } + } +} + +static void* +pinfo_device_client_new(void* req_ctx) { + pinfo_client_ctx_t* ctx = NULL; + afb_req_t req = (afb_req_t)(req_ctx); + + if(req) { + ctx = malloc(sizeof(*ctx)); + if(ctx) { + ctx->udev_ctx = udev_new(); + if(ctx->udev_ctx) { + ctx->umon_hndl = udev_monitor_new_from_netlink(ctx->udev_ctx,"udev"); + ctx->filter = NULL; + ctx->mask = NULL; + ctx->umon_cb = NULL; + + if(ctx->umon_hndl) { + json_object* jval = NULL; + json_object* jargs = afb_req_json(req); + pthread_attr_t attr; + + ctx->ev_devs_changed = afb_api_make_event(req->api,"device_changed"); + if(afb_event_is_valid(ctx->ev_devs_changed)) { + if(afb_req_subscribe(req,ctx->ev_devs_changed) == PINFO_OK) { + ctx->umon_cb = (void(*)(void*,struct json_object*))pinfo_device_monitor_detect; + if(json_object_object_get_ex(jargs,"filter",&jval) && + json_object_is_type(jval,json_type_object)) { + json_object_deep_copy(jval,&ctx->filter,NULL); + } + + if(json_object_object_get_ex(jargs,"mask",&jval) && + json_object_is_type(jval,json_type_object)) { + json_object_deep_copy(jval,&ctx->mask,NULL); + } + + if(pinfo_device_filter_monitoring(ctx) == PINFO_OK) { + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); + if(pthread_create(&ctx->th,&attr, + (void*(*)(void*))&pinfo_device_monitor_loop,(void*)ctx) == PINFO_OK) { + pinfo_api_ctx_t* api_ctx = NULL; + api_ctx = (pinfo_api_ctx_t*)afb_api_get_userdata(req->api); + AFB_REQ_DEBUG(req,"New client event-loop have been started ..."); + //New client (new session) get a pointer of API context + //to access global(API) context + if(api_ctx) { + ctx->api_ctx = api_ctx; + api_ctx->client_count++; + AFB_REQ_INFO(req,"New client context allocated, client count: %d", + api_ctx->client_count); + } + } else { + AFB_REQ_ERROR(req,"Failed to run the new session monitoring thread."); + pinfo_device_client_free(ctx); + ctx = NULL; + } + pthread_attr_destroy(&attr); + } else { + AFB_REQ_ERROR(req,"Failed to apply the device monitoring filters."); + } + } else { + AFB_REQ_ERROR(req,"New client event subscribing failed."); + } + } else { + AFB_REQ_ERROR(req,"Invalid AFB event generated for new client."); + } + } else { + AFB_REQ_ERROR(req, + "Failed to generate new udev monitoring object for the new client."); + } + } else { + AFB_REQ_ERROR(req, + "Failed to generate new udev object for the new client."); + } + } else { + AFB_REQ_ERROR(req,"Failed to allocate memory for new client context."); + } + } + + return (void*)ctx; +} + +static void +pinfo_device_jdev_destructor(struct json_object* jdevice, void* udevice) +{ + UNUSED(jdevice) + udev_device_unref((struct udev_device*)udevice); +} + +static int +pinfo_device_filter_monitoring(pinfo_client_ctx_t* ctx) { + int res = PINFO_OK; + + if(ctx && ctx->umon_hndl && + json_object_is_type(ctx->filter,json_type_object)) { + json_object* jprop_filter = NULL; + json_object* jval = NULL; + + if(json_object_object_get_ex(ctx->filter,"properties",&jprop_filter)) { + const char* subsys_str = NULL; + const char* devtype_str = NULL; + + if(json_object_object_get_ex(jprop_filter,"SUBSYSTEM",&jval) && + json_object_is_type(jval,json_type_string)) { + subsys_str = json_object_get_string(jval); + } + jval = NULL; + if(json_object_object_get_ex(jprop_filter,"DEVTYPE",&jval) && + json_object_is_type(jval,json_type_string)) { + devtype_str = json_object_get_string(jval); + } + if(devtype_str || subsys_str) { + res |= udev_monitor_filter_add_match_subsystem_devtype( + ctx->umon_hndl,subsys_str,devtype_str); + } + } + + if(json_object_object_get_ex(ctx->filter,"tags",&jval)) { + if(json_object_is_type(jval,json_type_array)) { + const size_t tags_count = json_object_array_length(jval); + + if(tags_count > 0) { + int tag_idx = 0; + json_object *tag_item = NULL; + + for(tag_item = json_object_array_get_idx(jval,0); + tag_item && tag_idx < tags_count; + tag_item = json_object_array_get_idx(jval,++tag_idx)) { + + if(json_object_is_type(tag_item,json_type_string)) { + res |= udev_monitor_filter_add_match_tag( + ctx->umon_hndl, + json_object_get_string(tag_item) + ); + } else { + AFB_WARNING("Client passed invalid tag type,\ + avoid the tag expression"); + } + } + } else { + //Empty tags array + } + } else { + AFB_WARNING("Client passed invalid value for 'tags' field,\ + the value type should be a json array with json string items,\ + avoid the tags filtering"); + } + } + } + + return res; +} + + +static json_object* +pinfo_device_udevice_to_jlist( struct udev_device* udevice, + struct udev_list_entry*(*udevice_elist)(struct udev_device*), + const char*(*elist_val_get)(struct udev_device*,const char*), + const unsigned int jflags) { + json_object* jobject = NULL; + + if(udevice && udevice_elist) { + struct udev_list_entry* elist = udevice_elist(udevice); + struct udev_list_entry* elist_head = NULL; + + jobject = json_object_new_object(); + udev_list_entry_foreach(elist_head,elist) { + const char* skey = udev_list_entry_get_name(elist_head); + const char* svalue = elist_val_get(udevice,skey); + + if(skey && svalue) { + json_object_object_add_ex(jobject,skey,json_object_new_string(svalue),jflags); + } + } + } + + return jobject; +} + +static json_object* +pinfo_device_udevice_to_jlist_mask( struct udev_device* udevice, + const char*(*udevice_val_getter)(struct udev_device*,const char*), + json_object* jkeys, + unsigned jcpy_flags) { + json_object* jdev = NULL; + + if(json_object_is_type(jkeys,json_type_array)) { + const int keys_count = json_object_array_length(jkeys); + if(keys_count > 0) { + int key_idx = 0; + jdev = json_object_new_object(); + + for(;key_idx < keys_count; ++key_idx) { + const char* skey = json_object_get_string( + json_object_array_get_idx(jkeys,key_idx)); + const char* svalue = udevice_val_getter(udevice,skey); + + if(svalue) { + json_object_object_add_ex(jdev,skey,json_object_new_string(svalue),jcpy_flags); + } + } + } + } + + return jdev; +} + +static json_object* +pinfo_device_udevice_to_jdevice(struct udev_device* udevice, json_object* jmask) { + json_object *jdev = NULL; + + if(udevice) { + json_object* jprops = NULL; + json_object* jattrs = NULL; + + jdev = json_object_new_object(); + if(json_object_is_type(jmask,json_type_object)) { + json_object* jprops_mask = NULL; + json_object* jattrs_mask = NULL; + + if(json_object_object_get_ex(jmask,"properties",&jprops_mask)) { + jprops = pinfo_device_udevice_to_jlist_mask( + udevice, + udev_device_get_property_value, + jprops_mask, + JSON_NEW_CONST_KEY + ); + } + if(json_object_object_get_ex(jmask,"attributes",&jattrs_mask)) { + jattrs = pinfo_device_udevice_to_jlist_mask( + udevice, + udev_device_get_sysattr_value, + jattrs_mask, + JSON_NEW_CONST_KEY + ); + } + } + + if(!jprops) { + jprops = pinfo_device_udevice_to_jlist( + udevice, + udev_device_get_properties_list_entry, + udev_device_get_property_value, + JSON_NEW_CONST_KEY + ); + } + + if(!jattrs) { + jattrs = pinfo_device_udevice_to_jlist( + udevice, + udev_device_get_sysattr_list_entry, + udev_device_get_sysattr_value, + JSON_NEW_CONST_KEY + ); + } + + if(json_object_is_type(jprops,json_type_object) && + json_object_object_length(jprops) > 0) { + json_object_object_add_ex( + jdev, + "properties", + jprops, + JSON_NEW_CONST_KEY + ); + } + + if(json_object_is_type(jattrs,json_type_object) && + json_object_object_length(jattrs) > 0) { + json_object_object_add_ex( + jdev, + "attributes", + jattrs, + JSON_NEW_CONST_KEY + ); + } + + json_object_set_userdata(jdev,udevice,(json_object_delete_fn*)pinfo_device_jdev_destructor); + } + + return jdev; +} + +static void* +pinfo_device_monitor_loop(pinfo_client_ctx_t* ctx) { + if(ctx && ctx->umon_hndl) { + int fd; + int res = 0; + + res = udev_monitor_enable_receiving(ctx->umon_hndl); + + if(res >= 0) { + fd = udev_monitor_get_fd(ctx->umon_hndl); + while (1) { + int ret; + fd_set fds; + + FD_ZERO(&fds); + FD_SET(fd,&fds); + + ret = select(fd+1, &fds, NULL, NULL, NULL); + if(ret > 0 && FD_ISSET(fd,&fds)) { + struct udev_device* detected_dev = NULL; + + detected_dev = udev_monitor_receive_device(ctx->umon_hndl); + if(detected_dev) { + json_object* jdetected_dev = NULL; + jdetected_dev = pinfo_device_udevice_to_jdevice(detected_dev,ctx->mask); + if(jdetected_dev) { + ctx->umon_cb(ctx,jdetected_dev); + } + } + } + } + } + } + return NULL; +} diff --git a/src/platform-info-devices.h b/src/platform-info-devices.h new file mode 100644 index 0000000..ad658f9 --- /dev/null +++ b/src/platform-info-devices.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 "IoT.bzh" + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PLATFORM_INFO_DEVICE_H +#define PLATFORM_INFO_DEVICE_H + +#include +#include "json-c/json.h" + +#define PINFO_OK (0) +#define PINFO_ERR (-1) + +struct udev; +struct udev_monitor; +struct pthread_t; + +typedef struct { + struct json_object* info; + int client_count; +}pinfo_api_ctx_t; + +typedef struct { + struct udev *udev_ctx; + struct udev_monitor *umon_hndl; + struct json_object *filter; + struct json_object *mask; + pinfo_api_ctx_t *api_ctx; + void(*umon_cb)(void* client_ctx, struct json_object* jdevice); + pthread_t th; + afb_event_t ev_devs_changed; +}pinfo_client_ctx_t; + + +int pinfo_device_monitor(afb_req_t req); + +#endif -- cgit 1.2.3-korg