From a7e45350d25e28bb9e2e0bce1e5a139f7698b6d7 Mon Sep 17 00:00:00 2001 From: Yongjoo Ahn Date: Wed, 20 Mar 2024 16:04:02 +0900 Subject: [PATCH] [plugin-parser] Add mlops-plugin-parser - Add libmlops-plugin-parser.so to handle package install / upgrade / uninstall for Tizen Signed-off-by: Yongjoo Ahn --- meson.build | 4 + packaging/mlops-agent.spec | 3 + plugin-parser/meson.build | 35 +++ plugin-parser/mlops-plugin-parser.cc | 384 ++++++++++++++++++++++++++++++ plugin-parser/mlops-plugin-parser.info.in | 1 + 5 files changed, 427 insertions(+) create mode 100644 plugin-parser/meson.build create mode 100644 plugin-parser/mlops-plugin-parser.cc create mode 100644 plugin-parser/mlops-plugin-parser.info.in diff --git a/meson.build b/meson.build index 4094d18..13394e2 100644 --- a/meson.build +++ b/meson.build @@ -97,6 +97,9 @@ dbus_policy_dir = join_paths(get_option('sysconfdir'), 'dbus-1', 'system.d') dbus_system_service_dir = join_paths(ml_agent_install_prefix, 'share', 'dbus-1', 'system-services') systemd_service_dir = join_paths(ml_agent_install_prefix, 'lib', 'systemd', 'system') +plugin_parser_install_dir = join_paths(get_option('sysconfdir'), 'package-manager', 'parserlib', 'metadata') +plugin_parser_info_txt_install_dir = join_paths(ml_agent_install_prefix, 'share', 'parser-plugins') + # Set default configuration ml_agent_conf = configuration_data() ml_agent_conf.set('VERSION', ml_agent_version) @@ -107,4 +110,5 @@ ml_agent_conf.set('INCLUDE_INSTALL_DIR', ml_agent_install_includedir) subdir('dbus') subdir('daemon') +subdir('plugin-parser') subdir('tests') diff --git a/packaging/mlops-agent.spec b/packaging/mlops-agent.spec index a21975c..069916a 100644 --- a/packaging/mlops-agent.spec +++ b/packaging/mlops-agent.spec @@ -74,6 +74,7 @@ BuildRequires: pkgconfig(json-glib-1.0) BuildRequires: pkgconfig(dlog) BuildRequires: pkgconfig(libtzplatform-config) BuildRequires: pkgconfig(capi-appfw-app-common) +BuildRequires: pkgconfig(pkgmgr-info) %endif # tizen # For test @@ -241,6 +242,8 @@ install -m 0755 packaging/run-unittest.sh %{buildroot}%{_bindir}/tizen-unittests %attr(0644,root,root) %{_unitdir}/mlops-agent.service %attr(0644,root,root) %config %{_sysconfdir}/dbus-1/system.d/mlops-agent.conf %attr(0644,root,root) %{_datadir}/dbus-1/system-services/org.tizen.machinelearning.service.service +%{_sysconfdir}/package-manager/parserlib/metadata/libmlops-plugin-parser.so +%{_datadir}/parser-plugins/mlops-plugin-parser.info %files -n libmlops-agent %manifest mlops-agent.manifest diff --git a/plugin-parser/meson.build b/plugin-parser/meson.build new file mode 100644 index 0000000..abdff03 --- /dev/null +++ b/plugin-parser/meson.build @@ -0,0 +1,35 @@ +# plugin-parser for rpk packages + +if not get_option('enable-tizen') + message('-- enable-tizen is disabled, not building plugin-parser --') + subdir_done() +endif + +deps = [ + glib_dep, + json_glib_dep, + ml_agent_dep, + dependency('dlog'), + dependency('pkgmgr-info'), +] + +plugin_parser_src = [ + 'mlops-plugin-parser.cc', +] + +mlops_plugin_parser = shared_library ('mlops-plugin-parser', + plugin_parser_src, + dependencies: deps, + install: true, + install_dir: plugin_parser_install_dir +) + +# Configure info file +info_file_conf = configuration_data() +info_file_conf.set('metadata_name', 'mlops') +info_file_conf.set('plugin_parser_install_dir', plugin_parser_install_dir) +info_file_conf.set('plugin_parser_name', mlops_plugin_parser.full_path().split('/')[-1]) +configure_file(input: 'mlops-plugin-parser.info.in', + output: 'mlops-plugin-parser.info', + install_dir: plugin_parser_info_txt_install_dir, + configuration: info_file_conf) diff --git a/plugin-parser/mlops-plugin-parser.cc b/plugin-parser/mlops-plugin-parser.cc new file mode 100644 index 0000000..628ffbd --- /dev/null +++ b/plugin-parser/mlops-plugin-parser.cc @@ -0,0 +1,384 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/** + * Copyright (c) 2024 Samsung Electronics Co., Ltd. All Rights Reserved. + * + * @file mlops-plugin-parser.cc + * @date 21 Mar 2024 + * @brief This file contains the implementation of a plugin parser for Tizen RPK packages. + * The plugin parser is responsible for parsing JSON files in RPK and updating the database using daemon. + * @see https://github.com/nnstreamer/deviceMLOps.MLAgent + * @author Yongjoo Ahn + * @bug No known bugs except for NYI items + * @todo Add more unit tests using mocks. + * @todo Support UNINSTALL and UPGRADE. + * @todo Support allowed resource other than global resource. + */ + +#include +#include +#include +#include +#include + +#define __TAG_ "ml-agent-plugin-parser" +#define LOG_V(prio, tag, fmt, arg...) \ + ({ \ + do { \ + dlog_print (prio, tag, "%s: %s(%d) > " fmt, __MODULE__, __func__, __LINE__, ##arg); \ + } while (0); \ + }) + +#define _D(fmt, arg...) LOG_V (DLOG_DEBUG, __TAG_, fmt, ##arg) +#define _I(fmt, arg...) LOG_V (DLOG_INFO, __TAG_, fmt, ##arg) +#define _W(fmt, arg...) LOG_V (DLOG_WARN, __TAG_, fmt, ##arg) +#define _E(fmt, arg...) LOG_V (DLOG_ERROR, __TAG_, fmt, ##arg) +#define _F(fmt, arg...) LOG_V (DLOG_FATAL, __TAG_, fmt, ##arg) + +/** + * @brief Struct used to parse metadata tag from xml file. + */ +typedef struct _metadata_s { + const char *key; + const char *value; +} _metadata_s; + +/** + * @brief Make pkg-info json string. + */ +static gchar * +_make_pkg_info (const gchar *pkgid, const gchar *appid, const gchar *res_type, + const gchar *res_version) +{ + g_autoptr (JsonBuilder) builder = json_builder_new (); + json_builder_begin_object (builder); + + json_builder_set_member_name (builder, "is_rpk"); + json_builder_add_string_value (builder, "T"); + + json_builder_set_member_name (builder, "pkg_id"); + json_builder_add_string_value (builder, pkgid); + + json_builder_set_member_name (builder, "app_id"); + json_builder_add_string_value (builder, appid ? appid : ""); + + json_builder_set_member_name (builder, "res_type"); + json_builder_add_string_value (builder, res_type); + + json_builder_set_member_name (builder, "res_version"); + json_builder_add_string_value (builder, res_version); + + json_builder_end_object (builder); + + g_autoptr (JsonNode) root = json_builder_get_root (builder); + g_autoptr (JsonGenerator) gen = json_generator_new (); + json_generator_set_root (gen, root); + json_generator_set_pretty (gen, TRUE); + + return json_generator_to_data (gen, NULL); +} + +/** + * @brief Internal enumeration for data types of json. + */ +typedef enum { + MLSVC_JSON_MODEL = 0, + MLSVC_JSON_PIPELINE = 1, + MLSVC_JSON_RESOURCE = 2, + MLSVC_JSON_MAX +} mlsvc_json_type_e; + +/** + * @brief Parse json and update ml-service database via invoking daemon. + */ +static void +_parse_json (const gchar *json_path, mlsvc_json_type_e json_type, const gchar *app_info) +{ + g_autofree gchar *json_file = NULL; + gint ret; + + switch (json_type) { + case MLSVC_JSON_MODEL: + json_file = g_build_filename (json_path, "model_description.json", NULL); + break; + case MLSVC_JSON_PIPELINE: + json_file = g_build_filename (json_path, "pipeline_description.json", NULL); + break; + case MLSVC_JSON_RESOURCE: + json_file = g_build_filename (json_path, "resource_description.json", NULL); + break; + default: + _E ("Unknown data type '%d', internal error?", json_type); + return; + } + + if (!g_file_test (json_file, (GFileTest) (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))) { + _W ("Failed to find json file '%s'. RPK using ML Service API should provide this json file.", + json_file); + return; + } + + g_autoptr (JsonParser) parser = json_parser_new (); + g_autoptr (GError) err = NULL; + + json_parser_load_from_file (parser, json_file, &err); + if (err) { + _E ("Failed to parse json file '%s': %s", json_file, err->message); + return; + } + + JsonNode *root = json_parser_get_root (parser); + JsonArray *array = NULL; + JsonObject *object = NULL; + guint json_len = 1U; + + if (JSON_NODE_HOLDS_ARRAY (root)) { + array = json_node_get_array (root); + if (!array) { + _E ("Failed to get root array from json file '%s'", json_file); + return; + } + + json_len = json_array_get_length (array); + } + + /* Update ML service database. */ + for (guint i = 0; i < json_len; ++i) { + if (array) + object = json_array_get_object_element (array, i); + else + object = json_node_get_object (root); + + switch (json_type) { + case MLSVC_JSON_MODEL: + { + const gchar *name = json_object_get_string_member (object, "name"); + const gchar *model = json_object_get_string_member (object, "model"); + const gchar *desc = json_object_get_string_member (object, "description"); + const gchar *activate = json_object_get_string_member (object, "activate"); + const gchar *clear = json_object_get_string_member (object, "clear"); + + if (!name || !model) { + _E ("Failed to get name or model from json file '%s'.", json_file); + continue; + } + + guint version; + bool active = (activate && g_ascii_strcasecmp (activate, "true") == 0); + bool clear_old = (clear && g_ascii_strcasecmp (clear, "true") == 0); + + /* Remove old model from database. */ + if (clear_old) { + /* Ignore error case. */ + ml_agent_model_delete (name, 0U); + } + + ret = ml_agent_model_register (name, model, active, desc ? desc : "", + app_info ? app_info : "", &version); + + if (ret == 0) + _I ("The model with name '%s' is registered as version '%u'.", name, version); + else + _E ("Failed to register the model with name '%s'.", name); + } + break; + case MLSVC_JSON_PIPELINE: + { + const gchar *name = json_object_get_string_member (object, "name"); + const gchar *desc = json_object_get_string_member (object, "description"); + + if (!name || !desc) { + _E ("Failed to get name or description from json file '%s'.", json_file); + continue; + } + + ret = ml_agent_pipeline_set_description (name, desc); + + if (ret == 0) + _I ("The pipeline description with name '%s' is registered.", name); + else + _E ("Failed to register pipeline with name '%s'.", name); + } + break; + case MLSVC_JSON_RESOURCE: + { + const gchar *name = json_object_get_string_member (object, "name"); + const gchar *path = json_object_get_string_member (object, "path"); + const gchar *desc = json_object_get_string_member (object, "description"); + const gchar *clear = json_object_get_string_member (object, "clear"); + + if (!name || !path) { + _E ("Failed to get name or path from json file '%s'.", json_file); + continue; + } + + bool clear_old = (clear && g_ascii_strcasecmp (clear, "true") == 0); + + /* Remove old resource from database. */ + if (clear_old) { + /* Ignore error case. */ + ml_agent_resource_delete (name); + } + + ret = ml_agent_resource_add ( + name, path, desc ? desc : "", app_info ? app_info : ""); + + if (ret == 0) + _I ("The resource with name '%s' is registered.", name); + else + _E ("Failed to register the resource with name '%s'.", name); + } + break; + default: + _E ("Unknown data type '%d', internal error?", json_type); + break; + } + } +} + +/** + * @brief Handle INSTALL process. Tizen app-installer invoke this function. + */ +extern "C" int +PKGMGR_MDPARSER_PLUGIN_INSTALL (const char *pkgid, const char *appid, GList *metadata) +{ + GList *list = NULL; + _metadata_s *detail = NULL; + pkgmgrinfo_pkginfo_h handle; + int ret = 0; + + _I ("PKGMGR_MDPARSER_PLUGIN_INSTALL called"); + _I ("pkgid = %s, appid = %s\n", pkgid, appid); + + /* check metadata key/value */ + list = g_list_first (metadata); + while (list) { + detail = (_metadata_s *) list->data; + _I ("key = %s, value = %s\n", detail->key, detail->value); + list = g_list_next (list); + } + + ret = pkgmgrinfo_pkginfo_get_pkginfo (pkgid, &handle); + if (ret != PMINFO_R_OK) { + _E ("Failed to get handle."); + return -1; + } + + /* Check whether the package is rpk */ + char *pkg_type = NULL; + ret = pkgmgrinfo_pkginfo_get_type (handle, &pkg_type); + if (ret != PMINFO_R_OK) { + _E ("Failed to get package type."); + pkgmgrinfo_pkginfo_destroy_pkginfo (handle); + return -1; + } + _I ("pkg_type : %s", pkg_type); + + if (g_strcmp0 (pkg_type, "rpk") != 0) { + _I ("pkg_type is not rpk. Skip parsing."); + pkgmgrinfo_pkginfo_destroy_pkginfo (handle); + return 0; + } + + char *root_path = NULL; + ret = pkgmgrinfo_pkginfo_get_root_path (handle, &root_path); + if (ret != PMINFO_R_OK) { + _E ("Failed to get root path."); + pkgmgrinfo_pkginfo_destroy_pkginfo (handle); + return -1; + } + _I ("root path: %s", root_path); + + char *res_type = NULL; + ret = pkgmgrinfo_pkginfo_get_res_type (handle, &res_type); + _I ("res_type = %s\n", res_type); + + char *res_version = NULL; + ret = pkgmgrinfo_pkginfo_get_res_version (handle, &res_version); + _I ("res_version = %s\n", res_version); + + g_autofree gchar *app_info = _make_pkg_info (pkgid, appid, res_type, res_version); + _I ("app_info = %s\n", app_info); + + /* check description.json file */ + g_autofree gchar *json_dir_path = g_build_filename (root_path, "res", "global", res_type, NULL); + + _parse_json (json_dir_path, MLSVC_JSON_MODEL, app_info); + + _I ("PKGMGR_MDPARSER_PLUGIN_INSTALL finished"); + pkgmgrinfo_pkginfo_destroy_pkginfo (handle); + + return 0; +} + +/** + * @brief Handle UNINSTALL process. Tizen app-installer invoke this function. + */ +extern "C" int +PKGMGR_MDPARSER_PLUGIN_UNINSTALL (const char *pkgid, const char *appid, GList *metadata) +{ + _I ("PKGMGR_MDPARSER_PLUGIN_UNINSTALL called"); + return 0; +} + +/** + * @brief Handle UPGRADE process. Tizen app-installer invoke this function. + */ +extern "C" int +PKGMGR_MDPARSER_PLUGIN_UPGRADE (const char *pkgid, const char *appid, GList *metadata) +{ + _I ("PKGMGR_MDPARSER_PLUGIN_UPGRADE called"); + PKGMGR_MDPARSER_PLUGIN_UNINSTALL (pkgid, appid, metadata); + PKGMGR_MDPARSER_PLUGIN_INSTALL (pkgid, appid, metadata); + + return 0; +} + +/** + * @brief RECOVERINSTALL is invoked after the INSTALL process failed. + */ +extern "C" int +PKGMGR_MDPARSER_PLUGIN_RECOVERINSTALL (const char *pkgid, const char *appid, GList *metadata) +{ + _I ("PKGMGR_MDPARSER_PLUGIN_RECOVERINSTALL called"); + return PKGMGR_MDPARSER_PLUGIN_UNINSTALL (pkgid, appid, metadata); +} + +/** + * @brief RECOVERUPGRADE is invoked after the UPGRADE process failed. + */ +extern "C" int +PKGMGR_MDPARSER_PLUGIN_RECOVERUPGRADE (const char *pkgid, const char *appid, GList *metadata) +{ + _I ("PKGMGR_MDPARSER_PLUGIN_RECOVERUPGRADE called"); + return PKGMGR_MDPARSER_PLUGIN_UPGRADE (pkgid, appid, metadata); +} + +/** + * @brief RECOVERUNINSTALL is invoked after the UNINSTALL process failed. + */ +extern "C" int +PKGMGR_MDPARSER_PLUGIN_RECOVERUNINSTALL (const char *pkgid, const char *appid, GList *metadata) +{ + _I ("PKGMGR_MDPARSER_PLUGIN_RECOVERUNINSTALL called"); + return PKGMGR_MDPARSER_PLUGIN_INSTALL (pkgid, appid, metadata); +} + +/** + * @brief CLEAN is invoked after the installation process completed. + */ +extern "C" int +PKGMGR_MDPARSER_PLUGIN_CLEAN (const char *pkgid, const char *appid, GList *metadata) +{ + _I ("PKGMGR_MDPARSER_PLUGIN_CLEAN called"); + return 0; +} + +/** + * @brief UNDO is invoked after the installation process failed. + */ +extern "C" int +PKGMGR_MDPARSER_PLUGIN_UNDO (const char *pkgid, const char *appid, GList *metadata) +{ + _I ("PKGMGR_MDPARSER_PLUGIN_UNDO called"); + return 0; +} diff --git a/plugin-parser/mlops-plugin-parser.info.in b/plugin-parser/mlops-plugin-parser.info.in new file mode 100644 index 0000000..61281e0 --- /dev/null +++ b/plugin-parser/mlops-plugin-parser.info.in @@ -0,0 +1 @@ +type="metadata";name="http://tizen.org/metadata/@metadata_name@";path="@plugin_parser_install_dir@/@plugin_parser_name@" -- 2.7.4