diff --git a/.gitmodules b/.gitmodules index e7973988..71df6d12 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,9 @@ [submodule "deps/iot-sdk-c"] path = v1/deps/iot-sdk-c url = https://github.com/Azure/azure-iot-sdk-c.git +[submodule "v1/deps/azure-uhttp-c"] + path = v1/deps/uhttp + url = https://github.com/Azure/azure-uhttp-c.git +[submodule "v1\\deps\\uhttp"] + path = v1\\deps\\uhttp + url = https://github.com/Azure/azure-uhttp-c.git diff --git a/v1/core/CMakeLists.txt b/v1/core/CMakeLists.txt index 0a15a0df..aa596b8d 100644 --- a/v1/core/CMakeLists.txt +++ b/v1/core/CMakeLists.txt @@ -2,6 +2,9 @@ #Licensed under the MIT license. See LICENSE file in the project root for full license information. cmake_minimum_required(VERSION 2.8.12) + +include("dependencies.cmake") + if(POLICY CMP0054) cmake_policy(SET CMP0054 OLD) endif() @@ -120,6 +123,8 @@ set(gateway_h_sources ./inc/module_loaders/dynamic_loader.h ) +include_directories(${IOTHUB_CLIENT_INC_FOLDER}) + if(${enable_dotnet_binding}) set(gateway_c_sources ${gateway_c_sources} @@ -251,8 +256,9 @@ if(${enable_core_remote_module_support}) endif() endif() -target_link_libraries(gateway parson nanomsg aziotsharedutil ${dynamic_loader_library}) -target_link_libraries(gateway_static parson nanomsg aziotsharedutil ${dynamic_loader_library}) + +target_link_libraries(gateway parson nanomsg aziotsharedutil ${dynamic_loader_library} iothub_client) +target_link_libraries(gateway_static parson nanomsg aziotsharedutil ${dynamic_loader_library} iothub_client) target_link_libraries(module_host_static parson nanomsg aziotsharedutil ${dynamic_loader_library}) if(NOT WIN32) @@ -282,6 +288,38 @@ if(NOT ${use_xplat_uuid}) endif() endif() +###################################### +# Customize for Remote Update +###################################### +if(${use_amqp}) + target_link_libraries(gateway_static iothub_client_amqp_transport) + target_link_libraries(gateway iothub_client_amqp_transport) + linkUAMQP(gateway_static) + linkUAMQP(gateway) +else() + add_definitions(-DIOTHUBMODULE_NULL_AMQP) +endif() + +if(${use_http}) + target_link_libraries(gateway_static iothub_client_http_transport) + target_link_libraries(gateway iothub_client_http_transport) + linkHttp(gateway_static) + linkHttp(gateway) +else() + add_definitions(-DIOTHUBMODULE_NULL_HTTP) +endif() + +if(${use_mqtt}) + target_link_libraries(gateway_static iothub_client_mqtt_transport) + target_link_libraries(gateway iothub_client_mqtt_transport) + linkMqttLibrary(gateway_static) + linkMqttLibrary(gateway) +else() + add_definitions(-DIOTHUBMODULE_NULL_MQTT) +endif() + + + #this adds the tests to the build process if(${run_unittests} OR ${run_e2e_tests}) add_subdirectory(tests) diff --git a/v1/core/dependencies.cmake b/v1/core/dependencies.cmake new file mode 100644 index 00000000..38ec1107 --- /dev/null +++ b/v1/core/dependencies.cmake @@ -0,0 +1,35 @@ +############################################################################### +###########################Find/Install/Build uamqp############################ +############################################################################### +findAndInstall(uamqp 1.0.25 ${PROJECT_SOURCE_DIR}/deps/uamqp ${PROJECT_SOURCE_DIR}/deps/uamqp -Duse_installed_dependencies=ON -G "${CMAKE_GENERATOR}") + +############################################################################### +###########################Find/Install/Build umqtt############################ +############################################################################### +findAndInstall(umqtt 1.0.25 ${PROJECT_SOURCE_DIR}/deps/umqtt ${PROJECT_SOURCE_DIR}/deps/umqtt -Duse_installed_dependencies=ON -G "${CMAKE_GENERATOR}") + +############################################################################### +#######################Find/Install/Build azure_iot_sdks####################### +############################################################################### +# The azure_iot_sdks repo requires special treatment. Parson submodule must be initialized. + +if(NOT EXISTS ${PROJECT_SOURCE_DIR}/deps/iot-sdk-c/deps/parson/README.md) + execute_process( + COMMAND git submodule update --init ${PROJECT_SOURCE_DIR}/deps/iot-sdk-c + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + RESULT_VARIABLE res + + ) + if(${res}) + message(FATAL_ERROR "Error pulling iot-sdk-c submodule: ${res}") + endif() + execute_process( + COMMAND git submodule update --init deps/parson + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/deps/iot-sdk-c + RESULT_VARIABLE res + ) + if(${res}) + message(FATAL_ERROR "Error pulling parson submodule: ${res}") + endif() +endif() +findAndInstall(azure_iot_sdks 1.1.5 ${PROJECT_SOURCE_DIR}/deps/iot-sdk-c ${PROJECT_SOURCE_DIR}/deps/iot-sdk-c -Duse_installed_dependencies=ON -Duse_openssl=OFF -Dbuild_as_dynamic=ON -Dskip_samples=ON -G "${CMAKE_GENERATOR}") diff --git a/v1/core/inc/gateway.h b/v1/core/inc/gateway.h index 9a24a528..144a0d82 100644 --- a/v1/core/inc/gateway.h +++ b/v1/core/inc/gateway.h @@ -20,6 +20,14 @@ #include "module_loader.h" #include "gateway_export.h" +#include "iothub_client.h" +#include "iothubtransport.h" +#include "iothubtransporthttp.h" +#include "iothubtransportamqp.h" +#include "iothubtransportmqtt.h" +#include "iothub_message.h" + + #ifdef __cplusplus extern "C" { @@ -97,6 +105,8 @@ typedef struct GATEWAY_MODULES_ENTRY_TAG /** @brief The user-defined configuration object for the module */ const void* module_configuration; + + const char* module_version; } GATEWAY_MODULES_ENTRY; /** @brief Struct representing the properties that should be used when @@ -110,6 +120,8 @@ typedef struct GATEWAY_PROPERTIES_DATA_TAG /** @brief Vector of #GATEWAY_LINK_ENTRY objects. */ VECTOR_HANDLE gateway_links; + + JSON_Object* deployConfig; } GATEWAY_PROPERTIES; /** @brief Creates a gateway using a JSON configuration file as input diff --git a/v1/core/src/gateway.c b/v1/core/src/gateway.c index 7cb44943..c8bb5b69 100644 --- a/v1/core/src/gateway.c +++ b/v1/core/src/gateway.c @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include "azure_c_shared_utility/gballoc.h" #include "azure_c_shared_utility/xlogging.h" @@ -194,6 +196,7 @@ GATEWAY_START_RESULT Gateway_Start(GATEWAY_HANDLE gw) EventSystem_ReportEvent(gw->event_system, gw, GATEWAY_STARTED); /*Codes_SRS_GATEWAY_17_013: [ This function shall return GATEWAY_START_SUCCESS upon completion. ]*/ result = GATEWAY_START_SUCCESS; + gw->runtime_status = GATEWAY_RUNTIME_STATUS_RUNNING; // 1 means runtime is running } else { @@ -206,7 +209,7 @@ GATEWAY_START_RESULT Gateway_Start(GATEWAY_HANDLE gw) void Gateway_Destroy(GATEWAY_HANDLE gw) { gateway_destroy_internal(gw); - /*Codes_SRS_GATEWAY_17_019: [ The function shall destroy the module loader list. ]*/ + /*Codes_SRS_GATEWAY_17_019: [ The function shall destroy the module loader list. ]*/ ModuleLoader_Destroy(); } diff --git a/v1/core/src/gateway_createfromjson.c b/v1/core/src/gateway_createfromjson.c index a2cba164..b0137c82 100644 --- a/v1/core/src/gateway_createfromjson.c +++ b/v1/core/src/gateway_createfromjson.c @@ -5,6 +5,8 @@ #include "azure_c_shared_utility/gballoc.h" #include "azure_c_shared_utility/xlogging.h" #include "azure_c_shared_utility/macro_utils.h" +#include "azure_c_shared_utility/httpapiex.h" +#include "azure_uamqp_c/amqp_definitions.h" #include "gateway.h" #include "parson.h" #include "experimental/event_system.h" @@ -12,6 +14,7 @@ #include "module_loaders/dynamic_loader.h" #include "gateway_internal.h" +#define GATEWAY_KEY "gateway" #define MODULES_KEY "modules" #define LOADERS_KEY "loaders" #define LOADER_KEY "loader" @@ -21,6 +24,12 @@ #define MODULE_PATH_KEY "module.path" #define ARG_KEY "args" +#define GATEWAY_KEY "gateway" +#define GATEWAY_IOTHUB_CONNECTION_STRING_KEY "connection-string" +#define GATEWAY_IOTHUB_TRANSPORT_KEY "transport" +#define GATEWAY_IOTHUB_MODULES_LOCAL_PATH "modules-local-path" +#define MODULE_REMOTE_URL "module.uri" + #define LINKS_KEY "links" #define SOURCE_KEY "source" #define SINK_KEY "sink" @@ -38,6 +47,143 @@ GATEWAY_HANDLE gateway_create_internal(const GATEWAY_PROPERTIES* properties, boo static PARSE_JSON_RESULT parse_json_internal(GATEWAY_PROPERTIES* out_properties, JSON_Value *root); static void destroy_properties_internal(GATEWAY_PROPERTIES* properties); void gateway_destroy_internal(GATEWAY_HANDLE gw); +unsigned char* gateway_get_current_module_version(JSON_Object* json, const unsigned char* moduleName); + +void gateway_deviceTwinCallback(DEVICE_TWIN_UPDATE_STATE update_state, const unsigned char* payLoad, size_t size, void* userContextCallback) +{ + GATEWAY_HANDLE gw = (GATEWAY_HANDLE)userContextCallback; + unsigned char* buf = (unsigned char*)malloc(size); + memcpy(buf, payLoad, size); + if (buf[size - 1] != '}') { + int index = size; + while (index > 0) { + if (buf[--index] == '}') { + buf[index + 1] = '\0'; + break; + } + } +// buf[size - 1] = '\0'; + } + LogInfo("Device Twin Desired Properteis Updated - '%s'", payLoad); + + JSON_Value* root = json_parse_string(buf); + JSON_Object* document = json_value_get_object(root); + JSON_Object* desiredProperties = json_object_get_object(document, "desired"); + if (desiredProperties != NULL) { + JSON_Object* gwConfig = json_object_get_object(desiredProperties, GATEWAY_KEY); + if (gwConfig != NULL) { + const char* configValue = json_object_get_string(gwConfig, "configuration"); + if (configValue != NULL) { + char* configValueHost = NULL; + const char* configValueRelPath = NULL; + int cvIndex = 0; + int cvLen = strlen(configValue); + const char cvProtocol[9] = { 'h','t','t','p','s',':','/','/','\0' }; + int cvPLen = strlen(cvProtocol); + while (cvIndex < cvPLen) { + if (cvProtocol[cvIndex] != configValue[cvIndex]) { + // error + } + cvIndex++; + } + if (cvIndex == cvPLen) { + configValueHost = &configValue[cvIndex]; + } + while (cvIndex < cvLen) { + if (configValue[cvIndex++] == '/') { + break; + } + } + if (0 < cvIndex&& cvIndex < cvLen) { + int hostLen = cvIndex - cvPLen; + configValueHost = (char*)malloc(hostLen); + memcpy(configValueHost, &configValue[cvPLen], hostLen - 1); + configValueHost[hostLen - 1] = '\0'; + configValueRelPath = &configValue[cvIndex-1]; + } + if (configValueHost == NULL || configValueRelPath == NULL) { + LogError("Bad url!"); + } + else { + HTTP_HEADERS_HANDLE httpReqHeadersHandle, httpResHeadersHandle; + httpReqHeadersHandle = HTTPHeaders_Alloc(); + httpResHeadersHandle = HTTPHeaders_Alloc(); + if (httpReqHeadersHandle == NULL || httpResHeadersHandle == NULL) { + LogError("Failed to allocate http headers"); + if (httpReqHeadersHandle != NULL) HTTPHeaders_Free(httpReqHeadersHandle); + if (httpResHeadersHandle != NULL) HTTPHeaders_Free(httpResHeadersHandle); + } + else { + BUFFER_HANDLE reqContent = BUFFER_new(); + BUFFER_HANDLE resContent = BUFFER_new(); + if (reqContent == NULL || resContent == NULL) { + LogError("Failed to allocate http contents"); + if (reqContent != NULL) BUFFER_delete(reqContent); + if (resContent != NULL) BUFFER_delete(resContent); + HTTPHeaders_Free(httpReqHeadersHandle); + HTTPHeaders_Free(httpResHeadersHandle); + } + else { + unsigned int statusCode; + HTTPAPIEX_HANDLE httpapiexHandle = HTTPAPIEX_Create(configValueHost); + if (httpapiexHandle == NULL) { + LogError("Failed to create httpapiex handle"); + HTTPHeaders_Free(httpReqHeadersHandle); + HTTPHeaders_Free(httpResHeadersHandle); + BUFFER_delete(reqContent); + BUFFER_delete(resContent); + } + else { + HTTPAPIEX_RESULT httpApiExResult = HTTPAPIEX_ExecuteRequest(httpapiexHandle, HTTPAPI_REQUEST_GET, configValueRelPath, httpReqHeadersHandle, reqContent, &statusCode, httpResHeadersHandle, resContent); + + if (httpApiExResult == HTTPAPIEX_OK) { + if (statusCode == 200) { + const char* rc = BUFFER_u_char(resContent); + int rcLen = BUFFER_length(resContent); + STRING_HANDLE sh = STRING_construct_n(rc, rcLen); + const char* configJson = STRING_c_str(sh); + LogInfo("Received configuration - \n >>>%s<<<\n", configJson); + Gateway_UpdateFromJson(gw, rc); + } + else { + LogInfo("http access result %d", statusCode); + } + } + else { + LogError("http access failed %d", httpApiExResult); + } + HTTPAPIEX_Destroy(httpapiexHandle); + HTTPHeaders_Free(httpReqHeadersHandle); + HTTPHeaders_Free(httpResHeadersHandle); + BUFFER_delete(reqContent); + BUFFER_delete(resContent); + } + } + } + } + if (configValueHost != NULL) { + free(configValueHost); + } + } + } + } + free(buf); +} + +static int strcmp_i(const char* lhs, const char* rhs) +{ + char lc, rc; + int cmp; + + do + { + lc = *lhs++; + rc = *rhs++; + cmp = tolower(lc) - tolower(rc); + } while (cmp == 0 && lc != 0 && rc != 0); + + return cmp; +} GATEWAY_HANDLE Gateway_CreateFromJson(const char* file_path) { @@ -67,7 +213,8 @@ GATEWAY_HANDLE Gateway_CreateFromJson(const char* file_path) { properties->gateway_modules = NULL; properties->gateway_links = NULL; - if ((parse_json_internal(properties, root_value) == PARSE_JSON_SUCCESS) && properties->gateway_modules != NULL && properties->gateway_links != NULL) + properties->deployConfig = NULL; + if ((parse_json_internal(properties, root_value) == PARSE_JSON_SUCCESS) && properties->gateway_modules != NULL && properties->gateway_links != NULL) { /*Codes_SRS_GATEWAY_JSON_14_007: [The function shall use the GATEWAY_PROPERTIES instance to create and return a GATEWAY_HANDLE using the lower level API.]*/ /*Codes_SRS_GATEWAY_JSON_17_004: [ The function shall set the module loader to the default dynamically linked library module loader. ]*/ @@ -79,6 +226,7 @@ GATEWAY_HANDLE Gateway_CreateFromJson(const char* file_path) } else { + /*Codes_SRS_GATEWAY_JSON_17_001: [ Upon successful creation, this function shall start the gateway. ]*/ GATEWAY_START_RESULT start_result; start_result = Gateway_Start(gw); @@ -89,6 +237,33 @@ GATEWAY_HANDLE Gateway_CreateFromJson(const char* file_path) gateway_destroy_internal(gw); gw = NULL; } + else { + JSON_Object *json_document = json_value_get_object(root_value); + JSON_Object *gwConfig = json_object_get_object(json_document, GATEWAY_KEY); + IOTHUB_CLIENT_TRANSPORT_PROVIDER transportProvider = NULL; + if (gwConfig != NULL) { + const char* connectionString = json_object_get_string(gwConfig, GATEWAY_IOTHUB_CONNECTION_STRING_KEY); + const char* iothubTransport = json_object_get_string(gwConfig, GATEWAY_IOTHUB_TRANSPORT_KEY); + const char* modulesLocalPath = json_object_get_string(gwConfig, GATEWAY_IOTHUB_MODULES_LOCAL_PATH); + if (connectionString != NULL && iothubTransport != NULL && modulesLocalPath!=NULL) { + if (strcmp_i(iothubTransport, "AMQP") == 0) + { + transportProvider = AMQP_Protocol; + } + else if (strcmp_i(iothubTransport, "MQTT") == 0) + { + transportProvider = MQTT_Protocol; + } + if (transportProvider != NULL) { + gw->iothub_client = IoTHubClient_CreateFromConnectionString(connectionString, transportProvider); + if (gw->iothub_client != NULL) { + IoTHubClient_SetDeviceTwinCallback(gw->iothub_client, gateway_deviceTwinCallback, gw); + } + } + gw->modules_local_path = STRING_construct(modulesLocalPath); // should be free + } + } + } } } /*Codes_SRS_GATEWAY_JSON_14_006: [The function shall return NULL if the JSON_Value contains incomplete information.]*/ @@ -164,6 +339,15 @@ GATEWAY_UPDATE_FROM_JSON_RESULT Gateway_UpdateFromJson(GATEWAY_HANDLE gw, const } else { + bool isUpdating = true; + while (isUpdating) { + Lock(gw->update_lock); + if (gw->runtime_status != GATEWAY_RUNTIME_STATUS_UPDATING) { + isUpdating = false; + gw->runtime_status = GATEWAY_RUNTIME_STATUS_UPDATING; + } + Unlock(gw->update_lock); + } /* Codes_SRS_GATEWAY_JSON_04_005: [ The function shall use parson to parse the JSON string to a parson JSON_Value structure. ] */ JSON_Value *root_value = json_parse_string(json_content); @@ -186,6 +370,31 @@ GATEWAY_UPDATE_FROM_JSON_RESULT Gateway_UpdateFromJson(GATEWAY_HANDLE gw, const { properties->gateway_modules = NULL; properties->gateway_links = NULL; + properties->deployConfig = NULL; + JSON_Object *json_document = json_value_get_object(root_value); + char* deployConfig = NULL; + JSON_Value* dcJsonRoot = NULL; + if (json_document != NULL) + { + JSON_Value* gateway_config = json_object_get_value(json_document, GATEWAY_KEY); + JSON_Object* gateway_object = json_value_get_object(gateway_config); + // JSON_Object* gateway_object = json_object_get_object(json_document, GATEWAY_KEY); + deployConfig = json_object_get_string(gateway_object, "deploy-path"); + // const char* deployConfig = json_value_get_string (gateway_config, "deploy-path"); + if (deployConfig != NULL) { + FILE* fpDC = fopen(deployConfig, "r"); + if (fpDC == NULL) { + unsigned char* baseDCJsonString = "{\"modules\":[]}"; + dcJsonRoot = json_parse_string(baseDCJsonString); + properties->deployConfig = json_value_get_object(dcJsonRoot); + } + else { + fclose(fpDC); + dcJsonRoot = json_parse_file(deployConfig); + properties->deployConfig = json_value_get_object(dcJsonRoot); + } + } + } /* Codes_SRS_GATEWAY_JSON_04_007: [ The function shall traverse the JSON_Value object to initialize a GATEWAY_PROPERTIES instance. ] */ /* Codes_SRS_GATEWAY_JSON_04_011: [ The function shall be able to add just `modules`, just `links` or both. ] */ if (parse_json_internal(properties, root_value) != PARSE_JSON_SUCCESS) @@ -196,7 +405,7 @@ GATEWAY_UPDATE_FROM_JSON_RESULT Gateway_UpdateFromJson(GATEWAY_HANDLE gw, const } else { - + gw->deployConfig = properties->deployConfig; VECTOR_HANDLE modules_added_successfully = VECTOR_create(sizeof(GATEWAY_MODULES_ENTRY)); if (modules_added_successfully == NULL) { @@ -366,11 +575,18 @@ GATEWAY_UPDATE_FROM_JSON_RESULT Gateway_UpdateFromJson(GATEWAY_HANDLE gw, const VECTOR_destroy(modules_added_successfully); } } + if (deployConfig!=NULL&& dcJsonRoot != NULL) { + json_serialize_to_file(dcJsonRoot, deployConfig); + json_value_free(dcJsonRoot); + } destroy_properties_internal(properties); free(properties); } json_value_free(root_value); } + Lock(gw->update_lock); + gw->runtime_status = GATEWAY_RUNTIME_STATUS_UPDATED; + Unlock(gw->update_lock); } return result; @@ -400,7 +616,7 @@ static void destroy_properties_internal(GATEWAY_PROPERTIES* properties) } } -static PARSE_JSON_RESULT parse_loader(JSON_Object* loader_json, GATEWAY_MODULE_LOADER_INFO* loader_info) +static PARSE_JSON_RESULT parse_loader(JSON_Object* loader_json, GATEWAY_MODULE_LOADER_INFO* loader_info, JSON_Object* deployConfig) { PARSE_JSON_RESULT result; @@ -428,8 +644,105 @@ static PARSE_JSON_RESULT parse_loader(JSON_Object* loader_json, GATEWAY_MODULE_L // get entrypoint JSON_Value* entrypoint_json = json_object_get_value(loader_json, LOADER_ENTRYPOINT_KEY); + if (entrypoint_json != NULL) { + // download logic module + JSON_Object* entrypoint = json_value_get_object(entrypoint_json); + const char* modulePath = json_object_get_string(entrypoint, MODULE_REMOTE_URL); + const char* deployPath = json_object_get_string(entrypoint, MODULE_PATH_KEY); + JSON_Value* entrypointParrent_json = json_value_get_parent(entrypoint_json); + const char* newModuleVersion = NULL; + const char* moduleName = NULL; + if (entrypointParrent_json != NULL) { + JSON_Value* moduleEntry_json = json_value_get_parent(entrypointParrent_json); + JSON_Object* entryPointParent = json_value_get_object(moduleEntry_json); + newModuleVersion = json_object_get_string(entryPointParent, "version"); + moduleName = json_object_get_string(entryPointParent, "name"); + } + if (modulePath != NULL && (modulePath[0] == 'h'&&modulePath[1] == 't'&&modulePath[2] == 't'&&modulePath[3] == 'p' &&modulePath[4] == 's')) { + const char* currentModuleVersion = gateway_get_current_module_version(deployConfig, moduleName); + if (currentModuleVersion == NULL || (currentModuleVersion != NULL && strcmp(newModuleVersion, currentModuleVersion) != 0)) { + unsigned char* remoteModuleHost = NULL; + unsigned char* remoteModulePath = NULL; + int modulePathIndex = strlen("https://"); + int modulePathLength = strlen(modulePath); + int lastIndex = modulePathIndex; + while (modulePathIndex < modulePathLength) { + if (modulePath[modulePathIndex] == '/') { + break; + } + modulePathIndex++; + } + remoteModuleHost = (unsigned char*)malloc(modulePathIndex - lastIndex + 1); + memcpy(remoteModuleHost, &modulePath[lastIndex], modulePathIndex - lastIndex); + remoteModuleHost[modulePathIndex - lastIndex] = '\0'; + remoteModulePath = (unsigned char*)malloc(modulePathLength - modulePathIndex + 2); + memcpy(remoteModulePath, &modulePath[modulePathIndex], modulePathLength - modulePathIndex + 1); + remoteModulePath[modulePathLength - modulePathIndex + 1] = '\0'; + + HTTP_HEADERS_HANDLE httpReqHeadersHandle, httpResHeadersHandle; + httpReqHeadersHandle = HTTPHeaders_Alloc(); + httpResHeadersHandle = HTTPHeaders_Alloc(); + if (httpReqHeadersHandle == NULL || httpResHeadersHandle == NULL) { + LogError("Failed to allocate http headers"); + if (httpReqHeadersHandle != NULL) HTTPHeaders_Free(httpReqHeadersHandle); + if (httpResHeadersHandle != NULL) HTTPHeaders_Free(httpResHeadersHandle); + } + else { + BUFFER_HANDLE reqContent = BUFFER_new(); + BUFFER_HANDLE resContent = BUFFER_new(); + if (reqContent == NULL || resContent == NULL) { + LogError("Failed to allocate http contents"); + if (reqContent != NULL) BUFFER_delete(reqContent); + if (resContent != NULL) BUFFER_delete(resContent); + HTTPHeaders_Free(httpReqHeadersHandle); + HTTPHeaders_Free(httpResHeadersHandle); + } + else { + unsigned int statusCode; + HTTPAPIEX_HANDLE httpapiexHandle = HTTPAPIEX_Create(remoteModuleHost); + if (httpapiexHandle == NULL) { + LogError("Failed to create httpapiex handle"); + HTTPHeaders_Free(httpReqHeadersHandle); + HTTPHeaders_Free(httpResHeadersHandle); + BUFFER_delete(reqContent); + BUFFER_delete(resContent); + } + else { + HTTPAPIEX_RESULT httpApiExResult = HTTPAPIEX_ExecuteRequest(httpapiexHandle, HTTPAPI_REQUEST_GET, remoteModulePath, httpReqHeadersHandle, reqContent, &statusCode, httpResHeadersHandle, resContent); + if (httpApiExResult == HTTPAPIEX_OK) { + if (statusCode == 200) { + LogInfo("Received library - %s : %d byte", deployPath, BUFFER_length(resContent)); + FILE* fp = fopen(deployPath, "wb"); + if (fp != NULL) { + fwrite(BUFFER_u_char(resContent), sizeof(unsigned char), BUFFER_length(resContent), fp); + fclose(fp); + } + else { + LogError("Loaded module file can't be stored - %s", deployPath); + } + } + else { + LogInfo("http access result %d", statusCode); + } + } + else { + LogError("http access failed %d", httpApiExResult); + } + HTTPAPIEX_Destroy(httpapiexHandle); + HTTPHeaders_Free(httpReqHeadersHandle); + HTTPHeaders_Free(httpResHeadersHandle); + BUFFER_delete(reqContent); + BUFFER_delete(resContent); + } + } + } + if (remoteModuleHost != NULL) free(remoteModuleHost); + if (remoteModulePath != NULL) free(remoteModulePath); + } + } + } loader_info->entrypoint = entrypoint_json == NULL ? NULL : - loader->api->ParseEntrypointFromJson(loader, entrypoint_json); + loader->api->ParseEntrypointFromJson(loader, entrypoint_json); // if entrypoint_json is not NULL then loader_info->entrypoint must not be NULL if (entrypoint_json != NULL && loader_info->entrypoint == NULL) @@ -480,7 +793,7 @@ static PARSE_JSON_RESULT parse_json_internal(GATEWAY_PROPERTIES* out_properties, /*Codes_SRS_GATEWAY_JSON_17_009: [ For each module, the function shall call the loader's ParseEntrypointFromJson function to parse the entrypoint JSON. ]*/ JSON_Object* loader_args = json_object_get_object(module, LOADER_KEY); GATEWAY_MODULE_LOADER_INFO loader_info; - if (parse_loader(loader_args, &loader_info) != PARSE_JSON_SUCCESS) + if (parse_loader(loader_args, &loader_info, out_properties->deployConfig) != PARSE_JSON_SUCCESS) { result = PARSE_JSON_MISSING_OR_MISCONFIGURED_CONFIG; LogError("Failed to parse loader configuration."); @@ -494,12 +807,16 @@ static PARSE_JSON_RESULT parse_json_internal(GATEWAY_PROPERTIES* out_properties, /*Codes_SRS_GATEWAY_JSON_14_005: [The function shall set the value of const void* module_properties in the GATEWAY_PROPERTIES instance to a char* representing the serialized args value for the particular module.]*/ JSON_Value *args = json_object_get_value(module, ARG_KEY); char* args_str = json_serialize_to_string(args); - + char* version_str = json_object_get_string(module, "version"); GATEWAY_MODULES_ENTRY entry = { module_name, loader_info, - args_str + args_str, + NULL }; + if (version_str != NULL) { + entry.module_version = version_str; + } /*Codes_SRS_GATEWAY_JSON_14_006: [The function shall return NULL if the JSON_Value contains incomplete information.]*/ if (VECTOR_push_back(out_properties->gateway_modules, &entry, 1) == 0) @@ -607,6 +924,7 @@ static PARSE_JSON_RESULT parse_json_internal(GATEWAY_PROPERTIES* out_properties, result = PARSE_JSON_MISCONFIGURED_OR_OTHER; LogError("JSON Configuration file is configured incorrectly or some other error occurred while parsing."); } + } /*Codes_SRS_GATEWAY_JSON_14_006: [The function shall return NULL if the JSON_Value contains incomplete information.]*/ else diff --git a/v1/core/src/gateway_internal.c b/v1/core/src/gateway_internal.c index 3369dba3..9465aacb 100644 --- a/v1/core/src/gateway_internal.c +++ b/v1/core/src/gateway_internal.c @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -149,6 +150,9 @@ GATEWAY_HANDLE gateway_create_internal(const GATEWAY_PROPERTIES* properties, boo /* For freeing up NULL ptrs in case of create failure */ memset(gateway, 0, sizeof(GATEWAY_HANDLE_DATA)); + gateway->update_lock = Lock_Init(); + gateway->runtime_status = GATEWAY_RUNTIME_STATUS_INITIALIZING; + /*Codes_SRS_GATEWAY_14_003: [This function shall create a new BROKER_HANDLE for the gateway representing this gateway's message broker. ]*/ gateway->broker = Broker_Create(); if (gateway->broker == NULL) @@ -272,6 +276,17 @@ GATEWAY_HANDLE gateway_create_internal(const GATEWAY_PROPERTIES* properties, boo return gateway; } +static void reportedStateCallback(int status_code, void* userContextCallback) + +{ + GATEWAY_HANDLE_DATA* gateway_handle = (GATEWAY_HANDLE*)userContextCallback; + + printf("Device Twin reported properties update completed with result: %d\r\n", status_code); + Lock(gateway_handle->update_lock); + gateway_handle->runtime_status = GATEWAY_RUNTIME_STATUS_TERMINATED; + Unlock(gateway_handle->update_lock); +} + void gateway_destroy_internal(GATEWAY_HANDLE gw) { /*Codes_SRS_GATEWAY_14_005: [If gw is NULL the function shall do nothing.]*/ @@ -279,6 +294,15 @@ void gateway_destroy_internal(GATEWAY_HANDLE gw) { GATEWAY_HANDLE_DATA* gateway_handle = (GATEWAY_HANDLE_DATA*)gw; + if (gateway_handle->iothub_client != NULL) { + gateway_handle->runtime_status = GATEWAY_RUNTIME_STATUS_RUNNING; + const char* reported_status = "{\"edgev1-runtime-status\":\"terminated\"}"; + IoTHubClient_SendReportedState(gateway_handle->iothub_client, reported_status, strlen(reported_status), reportedStateCallback, gateway_handle); + while (gateway_handle->runtime_status != GATEWAY_RUNTIME_STATUS_TERMINATED) { + ThreadAPI_Sleep(1000); + } + IoTHubClient_Destroy(gateway_handle->iothub_client); + } if (gateway_handle->event_system != NULL) { /* event_system might be NULL here if destroying during failed creation, event system API should cleanly handle that */ @@ -325,6 +349,9 @@ void gateway_destroy_internal(GATEWAY_HANDLE gw) Broker_Destroy(gateway_handle->broker); } + if (gateway_handle->update_lock != NULL) { + Lock_Deinit(gateway_handle->update_lock); + } free(gateway_handle); } else @@ -333,6 +360,53 @@ void gateway_destroy_internal(GATEWAY_HANDLE gw) } } +unsigned char* gateway_get_current_module_version(JSON_Object* json, const unsigned char* moduleName) +{ + if (json == NULL) { + return NULL; + } + unsigned char* version = NULL; + JSON_Array* modules = json_object_get_array(json, "modules"); + int size = json_array_get_count(modules); + for (int i = 0; i < size; i++) { + JSON_Object* moduleDC = json_array_get_object(modules, i); + unsigned char* rModuleName = json_object_get_string(moduleDC, "name"); + if (strcmp(moduleName, rModuleName) == 0) { + version = json_object_get_string(moduleDC, "version"); + break; + } + } + return version; +} + +static void set_module_version(JSON_Object* json, const unsigned char* moduleName, const unsigned char* moduleVersion) +{ + if (json == NULL) { + return NULL; + } + JSON_Object* moduleJson = NULL; + JSON_Array* modules = json_object_get_array(json, "modules"); + int size = json_array_get_count(modules); + for (int i = 0; i < size; i++) { + JSON_Object* moduleDC = json_array_get_object(modules, i); + unsigned char* rModuleName = json_object_get_string(moduleDC, "name"); + if (strcmp(moduleName, rModuleName) == 0) { + moduleJson = moduleDC; + break; + } + } + if (moduleJson == NULL) { + JSON_Value* newModuleVersion = json_value_init_object(); + JSON_Object* newModuleVersionObj = json_object(newModuleVersion); + json_object_set_string(newModuleVersionObj, "name", moduleName); + json_object_set_string(newModuleVersionObj, "version", moduleVersion); + json_array_append_value(modules, newModuleVersion); + } + else { + json_object_set_string(moduleJson,"version", moduleVersion); + } +} + bool checkIfModuleExists(GATEWAY_HANDLE_DATA* gateway_handle, const char* module_name) { MODULE_DATA** module_data = (MODULE_DATA**)VECTOR_find_if(gateway_handle->modules, module_name_find, module_name); @@ -374,8 +448,16 @@ MODULE_HANDLE gateway_addmodule_internal(GATEWAY_HANDLE_DATA* gateway_handle, co //First check if a module with a given name already exists. /*Codes_SRS_GATEWAY_04_004: [ If a module with the same module_name already exists, this function shall fail and the GATEWAY_HANDLE will be destroyed. ]*/ bool moduleExist = checkIfModuleExists(gateway_handle, module_entry->module_name); + if (moduleExist) { + const unsigned char* modulePreVersion = gateway_get_current_module_version(gateway_handle->deployConfig, module_entry->module_name); + if (modulePreVersion == NULL || (modulePreVersion != NULL & strcmp(modulePreVersion, module_entry->module_version) != 0)) { + Gateway_RemoveModuleByName(gateway_handle, module_entry->module_name); + moduleExist = false; + } + } if (!moduleExist) { + set_module_version(gateway_handle->deployConfig, module_entry->module_name, module_entry->module_version); MODULE_DATA * new_module_data = (MODULE_DATA*)malloc(sizeof(MODULE_DATA)); if (new_module_data == NULL) { diff --git a/v1/core/src/gateway_internal.h b/v1/core/src/gateway_internal.h index f817a85b..8c510a72 100644 --- a/v1/core/src/gateway_internal.h +++ b/v1/core/src/gateway_internal.h @@ -28,6 +28,16 @@ typedef struct MODULE_DATA_TAG { MODULE_HANDLE module; } MODULE_DATA; +#define GATEWAY_RUNTIME_STATUS_VALUES \ + GATEWAY_RUNTIME_STATUS_INITIALIZING, \ + GATEWAY_RUNTIME_STATUS_UPDATING, \ + GATEWAY_RUNTIME_STATUS_UPDATED, \ + GATEWAY_RUNTIME_STATUS_RUNNING, \ + GATEWAY_RUNTIME_STATUS_TERMINATED, \ + GATEWAY_RUNTIME_STATUS_REPORTED + +DEFINE_ENUM(GATEWAY_RUNTIME_STATUS, GATEWAY_RUNTIME_STATUS_VALUES); + typedef struct GATEWAY_HANDLE_DATA_TAG { /** @brief Vector of MODULE_DATA modules that the Gateway must track */ @@ -41,6 +51,16 @@ typedef struct GATEWAY_HANDLE_DATA_TAG { /** @brief Vector of LINK_DATA links that the Gateway must track */ VECTOR_HANDLE links; + + IOTHUB_CLIENT_HANDLE iothub_client; + + GATEWAY_RUNTIME_STATUS runtime_status; // 0->initializing, 1->running, 2->terminating, 3->reported + + STRING_HANDLE modules_local_path; + + JSON_Object* deployConfig; + + LOCK_HANDLE update_lock; } GATEWAY_HANDLE_DATA; typedef struct LINK_DATA_TAG { diff --git a/v1/deps/uhttp b/v1/deps/uhttp new file mode 160000 index 00000000..1673c132 --- /dev/null +++ b/v1/deps/uhttp @@ -0,0 +1 @@ +Subproject commit 1673c1321db8076729675053df58c0738ac94103 diff --git a/v1/gatewayFunctions.cmake b/v1/gatewayFunctions.cmake index fa1e0973..ae0a4300 100644 --- a/v1/gatewayFunctions.cmake +++ b/v1/gatewayFunctions.cmake @@ -179,4 +179,99 @@ function(findAndInstall libraryName version submoduleRootDirectory cmakeRootDire endif() endif() +endfunction() + +#Additional arguments to the specific projects cmake command may be specified after specifying the cmakeRootDirectory +function(notFindAndInstall libraryName submoduleRootDirectory cmakeRootDirectory) + + # If we have a build type passed in then we use it when building this + # project by passing it as the value for the --config argument to the + # "cmake --build" command. + if(CMAKE_BUILD_TYPE) + set(BUILD_CONFIGURATION --config ${CMAKE_BUILD_TYPE}) + else() + set(BUILD_CONFIGURATION "") + endif() + + if(${rebuild_deps} OR NOT ${libraryName}_FOUND) + # We are interested in doing a "find_package" only if we aren't going + # to rebuild dependencies anyway. + if(NOT ${rebuild_deps}) +# find_package(${libraryName} QUIET CONFIG HINTS ${dependency_install_prefix}) + endif() + if(${rebuild_deps} OR NOT ${libraryName}_FOUND) + if(NOT ${rebuild_deps} ) + message(STATUS "${libraryName} not found...") + endif() + + message(STATUS "Building ${libraryName}...") + + #If the library directory doesn't exist, pull submodules + if(NOT EXISTS "${cmakeRootDirectory}/CMakeLists.txt") + message("{cmakeRootDirectory}/CMakeLists.txt not found!") + execute_process( + COMMAND git submodule update --init --recursive ${submoduleRootDirectory} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + RESULT_VARIABLE res + ) + endif() + if(${res}) + message(FATAL_ERROR "Error pulling submodules: ${res}") + endif() + + #Create the build directory to run cmake, and run cmake + + #generate comand + set(CMD cmake) + foreach(arg ${ARGN}) + set(CMD ${CMD} ${arg}) + endforeach() + + # If we have a build type passed in then we propagate it down the chain + # when building this dependency. + if(CMAKE_BUILD_TYPE) + set(CMD ${CMD} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}) + endif() + + if(DEFINED ${dependency_install_prefix}) + set(CMD ${CMD} -DCMAKE_INSTALL_PREFIX=${dependency_install_prefix}) + endif() + if(CMAKE_TOOLCHAIN_FILE) + set(CMD ${CMD} -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}) + endif() + set(CMD ${CMD} ../) + + # Re-create the build folder for making a fresh build + file(REMOVE_RECURSE ${cmakeRootDirectory}/build) + file(MAKE_DIRECTORY ${cmakeRootDirectory}/build) + + execute_process( + COMMAND ${CMD} + WORKING_DIRECTORY ${cmakeRootDirectory}/build + RESULT_VARIABLE res + ) + if(${res}) + message(FATAL_ERROR "Error running cmake for ${libraryName}: ${res}") + endif() + + # Install library + message(STATUS "Installing ${libraryName}. Please wait...") + execute_process( + COMMAND cmake --build . --target install ${BUILD_CONFIGURATION} -- ${cores} + WORKING_DIRECTORY ${cmakeRootDirectory}/build + RESULT_VARIABLE res + OUTPUT_FILE output.txt + ERROR_FILE error.txt + ) + if(${res}) + message(FATAL_ERROR "**ERROR installing ${libraryName}. See " + "${cmakeRootDirectory}/build/error.txt and " + "${cmakeRootDirectory}/build/output.txt.\n") + endif() + + #Attempt to find library with the REQUIRED option +# find_package(${libraryName} REQUIRED CONFIG HINTS ${dependency_install_prefix}) + endif() + endif() + endfunction() \ No newline at end of file diff --git a/v1/samples/hello_world/src/hello_world_remote.json b/v1/samples/hello_world/src/hello_world_remote.json new file mode 100644 index 00000000..d15df571 --- /dev/null +++ b/v1/samples/hello_world/src/hello_world_remote.json @@ -0,0 +1,13 @@ +{ + "gateway": { + "connection-string": "<< IoT Hub Connection String >>", + "transport": "amqp", + "modules-local-path": "<< local path for remote updated module >>" + }, + "modules": [ + + ], + "links": [ + + ] +} diff --git a/v1/samples/hello_world/src/hello_world_win.json b/v1/samples/hello_world/src/hello_world_win.json index 4484987b..b3e6e477 100644 --- a/v1/samples/hello_world/src/hello_world_win.json +++ b/v1/samples/hello_world/src/hello_world_win.json @@ -1,32 +1,33 @@ { - "modules": [ - { - "name": "logger", - "loader": { - "name": "native", - "entrypoint": { - "module.path": "..\\..\\..\\modules\\logger\\Debug\\logger.dll" - } - }, - "args": { - "filename": "log.txt" - } + "gateway": { + "connection-string": "<< IoT Hub Connection String >>", + "transport": "amqp" }, - { - "name": "hello_world", - "loader": { - "name": "native", - "entrypoint": { - "module.path": "..\\..\\..\\modules\\hello_world\\Debug\\hello_world.dll" + "modules": [{ + "name": "logger", + "loader": { + "name": "native", + "entrypoint": { + "module.path": "..\\..\\..\\modules\\logger\\Debug\\logger.dll" + } + }, + "args": { + "filename": "log.txt" + } + }, + { + "name": "hello_world", + "loader": { + "name": "native", + "entrypoint": { + "module.path": "..\\..\\..\\modules\\hello_world\\Debug\\hello_world.dll" + } + }, + "args": null } - }, - "args": null - } - ], - "links": [ - { - "source": "hello_world", - "sink": "logger" - } - ] + ], + "links": [{ + "source": "hello_world", + "sink": "logger" + }] } \ No newline at end of file diff --git a/v1/samples/remote_update/README.md b/v1/samples/remote_update/README.md new file mode 100644 index 00000000..933f7a87 --- /dev/null +++ b/v1/samples/remote_update/README.md @@ -0,0 +1,34 @@ +# IoT Edge SDK Version 1 - Modules Remote Update Extension +This sample is to show how to use module remote update extension. +You can update modules on your edge device from cloud via device twin desired properties. + +## Step 1 +Setup Azure IoT Edge SDK ver 1 on your device by [https://github.com/Azure/iot-edge/blob/master/v1/doc/devbox_setup.md](https://github.com/Azure/iot-edge/blob/master/v1/doc/devbox_setup.md). + +## Step 2 +Regist edge device on IoT Hub and get connection string for deviceId. +Setup 'connection-string' and 'modules-local-path' in hello_world_remote_edge.json. +### connection-string +Replace '<< IoT Hub Connection String >>' part by connection string from IoT Hub. +### modules-local-path +Replace '<< local path for remote updated module >>' part by real directory on your edge device. Examples are following... +- Windows c:\\azureiot\\modules +- Linux /usr/local/azureiot/modules + +## Step 3 +Run samples/hello_world/hello_world using above hello_world_remote_edge.json as argument. + +## Step 4 +Store module library into cloud storage and prepare url to download it. +In the case of Azure blob, you create private container, store the library and get url with SAS token. +Then create edge-config.json refering edge-config-[lin|win].json. Modify '<< url for xxx >>' by above url. '<< local deploy path >>' should be same of the Step 2. +The version of each module should be changed as your valid one. +Then store edge-config.json into cloud storage and prepare url. + +## Step 5 +Update device twin desired properties refering device-twin-cloud.json with modify '<< edge-config-cloud.json url >>' using above edge-config.json on IoT Hub or Device Explorer. + +## Starting! +Device will receive device twin desired properties change then download configuration and libraries, deploy and execute them. + +Let's enjoy! diff --git a/v1/samples/remote_update/src/device-twin-cloud.json b/v1/samples/remote_update/src/device-twin-cloud.json new file mode 100644 index 00000000..d796fa8d --- /dev/null +++ b/v1/samples/remote_update/src/device-twin-cloud.json @@ -0,0 +1,4 @@ +"gateway": { + "configuration": "<< edge-config-cloud.json url >>" + +} \ No newline at end of file diff --git a/v1/samples/remote_update/src/edge-config-cloud-lin.json b/v1/samples/remote_update/src/edge-config-cloud-lin.json new file mode 100644 index 00000000..69f498a5 --- /dev/null +++ b/v1/samples/remote_update/src/edge-config-cloud-lin.json @@ -0,0 +1,40 @@ +{ + "gateway": { + "deploy-path": "<< local deploy path >>/deploy.json", + "version": "1.0.0" + }, + "modules": [ + { + "name": "logger", + "loader": { + "name": "native", + "entrypoint": { + "module.uri": "<< url for logger.so >>", + "module.path": "<< local deploy path >>/logger.so" + } + }, + "args": { + "filename": "log.txt" + }, + "version": "1.0.0" + }, + { + "name": "hello_world", + "loader": { + "name": "native", + "entrypoint": { + "module.uri": "<< url for hello_world.so >>", + "module.path": "<< local deploy path >>/hello_world.so" + } + }, + "args": null, + "version": "1.0.1" + } + ], + "links": [ + { + "source": "hello_world", + "sink": "logger" + } + ] +} diff --git a/v1/samples/remote_update/src/edge-config-cloud-win.json b/v1/samples/remote_update/src/edge-config-cloud-win.json new file mode 100644 index 00000000..5540409d --- /dev/null +++ b/v1/samples/remote_update/src/edge-config-cloud-win.json @@ -0,0 +1,40 @@ +{ + "gateway": { + "deploy-path": "<< local deploy path >>\\deploy.json", + "version": "1.0.0" + }, + "modules": [ + { + "name": "logger", + "loader": { + "name": "native", + "entrypoint": { + "module.uri": "<< url for logger.dll >>", + "module.path": "<< local deploy path >>\\logger.dll" + } + }, + "args": { + "filename": "log.txt" + }, + "version": "1.0.0" + }, + { + "name": "hello_world", + "loader": { + "name": "native", + "entrypoint": { + "module.uri": "<< url for hello_world", + "module.path": "<< local deploy path >>\\hello_world.dll" + } + }, + "args": null, + "version": "1.0.1" + } + ], + "links": [ + { + "source": "hello_world", + "sink": "logger" + } + ] +} diff --git a/v1/samples/remote_update/src/hello_world_remote_edge.json b/v1/samples/remote_update/src/hello_world_remote_edge.json new file mode 100644 index 00000000..1839b913 --- /dev/null +++ b/v1/samples/remote_update/src/hello_world_remote_edge.json @@ -0,0 +1,13 @@ +{ + "gateway": { + "connection-string": "<< IoT Hub Connection String >>", + "transport": "amqp", + "modules-local-path": "<< local path for remote updated module >>" + }, + "modules": [ + + ], + "links": [ + + ] +} \ No newline at end of file