From a8a14fd2843ab272344e67cb6f373783e1ab0513 Mon Sep 17 00:00:00 2001 From: Piotr Roszkowski Date: Wed, 14 Jun 2023 11:39:51 +0200 Subject: [PATCH] Anjay 3.4.0 Features - New APIs for server connection lifecycle management: anjay_server_schedule_reconnect() and anjay_schedule_register() - New options in anjay_configuration_t that allow for optional more strict LwM2M TS compliance: update_immediately_on_dm_change and enable_self_notify - Added option to disable auto-closing of the socket when in queue mode. - avs_coap_observe_cancel() is now public API (for direct users of avs_coap) - Added Advanced Firmware Update as an Anjay module - Added simplified demo of Advanced Firmware Update with two firmware images - Added tutorial for Advanced Firmware Update Improvements - Observations are now automatically cancelled if the client needs to send a new Register messsage - Added explicit casts in macros that involve negating an unsigned value, to silence warnings generated by some compilers - Various improvements and refactors of integration tests, to make them run faster and more stable - (commercial feature only) Disabled servers are saved now in Core Persistence, which prevents them from registering until the timeout passes with respect to the real clock. Bugfixes - Fixed handling of SenML payload that could cause erroneous behavior and memory leaks when parsing payloads only containing the Base Name field, without Name - Fixed error handling in bootstrapper (commercial only) and factory provisioning modules so that the changes are properly rolled back in case of error - Fixed assertion error (or 4.05 Method Not Allowed when compiled without assertions) when attempting to set the Disable Timeout resource in the Server object when the ANJAY_WITHOUT_DEREGISTER configuration option is set - Fixed inequality comparisons on some time values that could cause erroneous behavior on platforms with low-resolution system clocks, and updated unit tests to not assume a high-resolution clock - Fixed a problem where anjay_ongoing_registration_exists() inconditionally returned true if any server connection in a "disabled" state existed - Timeout when sending a Confirmable Notification now cancels the observation, as required by RFC 7641 - Removed sending of Release messages when using CoAP+TCP, which fixes the issue of erroneously sending them at the beginning after reconnecting a LwM2M connection socket - Fixed the serial port handling code in the sample NIDD driver, to properly handle cases where more than one line is received in a single read() call - Fixed compatibility of integration tests with the current versions of the Python cryptography module - Fixed problems with compiling the library without WITH_AVS_COAP_BLOCK enabled (contributed by Flonidan A/S) - Fixed a bug in the pymbedtls library used by tests, that prevented it from working in DTLS client mode - Fixed bug in the "Custom (D)TLS layers" code examples --- .github/workflows/anjay-tests.yml | 10 +- CHANGELOG.md | 59 +- CMakeLists.txt | 20 +- demo/CMakeLists.txt | 12 + demo/advanced_firmware_update.c | 1250 ++++++ demo/advanced_firmware_update.h | 165 + demo/advanced_firmware_update_addimg.c | 126 + demo/advanced_firmware_update_app.c | 208 + demo/demo.c | 51 + demo/demo.h | 8 + demo/demo_args.c | 151 +- demo/demo_args.h | 56 +- demo/demo_cmds.c | 110 +- demo/demo_utils.c | 143 +- demo/demo_utils.h | 19 +- demo/firmware_update.c | 152 +- .../include_public/avsystem/coap/observe.h | 32 + .../src/async/avs_coap_async_server.c | 23 +- deps/avs_coap/src/avs_coap_ctx.c | 10 +- deps/avs_coap/src/avs_coap_observe.c | 20 +- deps/avs_coap/src/avs_coap_observe.h | 3 - .../src/streaming/avs_coap_streaming_server.c | 2 + deps/avs_coap/src/tcp/avs_coap_tcp_ctx.c | 14 +- .../src/tcp/avs_coap_tcp_pending_requests.c | 9 +- deps/avs_coap/src/udp/avs_coap_udp_ctx.c | 2 +- .../avs_coap/src/udp/avs_coap_udp_msg_cache.c | 2 +- deps/avs_coap/tests/mock_clock.c | 2 - deps/avs_coap/tests/tcp/env.h | 4 - deps/avs_commons | 2 +- devconfig | 2 + doc/sphinx/snippet_sources.md5 | 33 +- .../AdvancedTopics/AT-AttributeStorage.rst | 6 +- .../AT_CO_MultipleResourceInstances.rst | 8 +- .../AT_CO_SingleInstanceReadOnly.rst | 4 +- .../AT-NetworkErrorHandling.rst | 25 +- .../CommercialFeatures/CF-CorePersistence.rst | 15 + doc/sphinx/source/FirmwareUpdateTutorial.rst | 1 + .../FU-AdvancedFirmwareUpdate.rst | 35 + .../FU-AFU-BasicImplementation.rst | 850 ++++ .../FU-AFU-Examples.rst | 446 ++ .../FU-AFU-ResourceDefinitions.rst | 413 ++ .../FU-AFU-StateDiagram.rst | 35 + .../_files/33629.xml | 257 ++ .../_images/FU-AFU-StateDiagram.svg | 1 + .../FU-BasicImplementation.rst | 2 + .../FU-Introduction.rst | 2 +- .../FU-SecureDownloads.rst | 2 +- doc/sphinx/source/Migrating.rst | 2 + .../Migrating/MigratingFromAnjay214.rst | 43 + .../Migrating/MigratingFromAnjay215.rst | 43 + .../Migrating/MigratingFromAnjay225.rst | 43 + .../source/Migrating/MigratingFromAnjay24.rst | 43 + .../source/Migrating/MigratingFromAnjay26.rst | 43 + .../source/Migrating/MigratingFromAnjay27.rst | 43 + .../source/Migrating/MigratingFromAnjay28.rst | 43 + .../source/Migrating/MigratingFromAnjay30.rst | 54 +- .../source/Migrating/MigratingFromAnjay32.rst | 71 + .../source/Migrating/MigratingFromAnjay33.rst | 57 + .../CustomTLS/CustomTLS-CertificatesBasic.rst | 10 +- .../CustomTLS/CustomTLS-Minimal.rst | 2 +- .../TimeAPI.rst | 2 + doc/sphinx/source/conf.py.in | 3 +- .../embedded_lwm2m10/anjay/anjay_config.h | 14 +- .../embedded_lwm2m11/anjay/anjay_config.h | 14 +- .../linux_lwm2m10/anjay/anjay_config.h | 14 +- .../linux_lwm2m11/anjay/anjay_config.h | 14 +- examples/CMakeLists.txt | 1 + .../src/tls_impl.c | 2 +- .../certificates-advanced/src/tls_impl.c | 2 +- .../certificates-basic/src/tls_impl.c | 2 +- .../custom-tls/config-features/src/tls_impl.c | 2 +- examples/custom-tls/minimal/src/tls_impl.c | 2 +- .../resumption-buffer/src/tls_impl.c | 2 +- .../resumption-simple/src/tls_impl.c | 2 +- .../custom-tls/tcp-support/src/tls_impl.c | 2 +- .../tutorial/firmware-update/CMakeLists.txt | 1 + .../advanced-firmware-update/CMakeLists.txt | 15 + .../src/advanced_firmware_update.c | 643 +++ .../src/advanced_firmware_update.h | 28 + .../advanced-firmware-update/src/main.c | 141 + .../src/time_object.c | 1 + .../src/time_object.h | 1 + include_public/anjay/advanced_fw_update.h | 862 ++++ include_public/anjay/anjay_config.h.in | 14 +- include_public/anjay/core.h | 114 +- include_public/anjay/dm.h | 13 +- include_public/anjay/factory_provisioning.h | 1 + include_public/anjay/fw_update.h | 24 +- include_public/anjay/io.h | 42 +- src/anjay_config_log.h | 10 + src/anjay_modules/anjay_bootstrap.h | 4 + src/anjay_modules/anjay_dm_utils.h | 12 + src/core/anjay_bootstrap_core.c | 8 +- src/core/anjay_bootstrap_core.h | 4 - src/core/anjay_core.c | 7 +- src/core/anjay_core.h | 2 + src/core/anjay_lwm2m_send.c | 2 + src/core/anjay_lwm2m_send.h | 2 + src/core/anjay_notify.c | 28 +- src/core/anjay_servers_private.h | 29 +- src/core/anjay_servers_utils.h | 2 + src/core/anjay_utils_core.c | 3 + src/core/anjay_utils_private.h | 3 + src/core/dm/anjay_dm_execute.c | 18 +- src/core/downloader/anjay_coap.c | 2 +- src/core/io/anjay_batch_builder.c | 1 + src/core/io/anjay_senml_in.c | 4 - src/core/observe/anjay_observe_core.c | 145 +- src/core/observe/anjay_observe_core.h | 5 + src/core/observe/anjay_observe_planning.c | 2 +- src/core/servers/anjay_activate.c | 73 +- src/core/servers/anjay_connection_ip.c | 4 + src/core/servers/anjay_connections.c | 17 +- src/core/servers/anjay_connections.h | 8 +- src/core/servers/anjay_register.c | 108 +- src/core/servers/anjay_register.h | 2 - src/core/servers/anjay_server_connections.c | 7 + src/core/servers/anjay_servers_internal.c | 10 +- src/core/servers/anjay_servers_internal.h | 11 + .../anjay_advanced_fw_update.c | 2209 ++++++++++ .../factory_provisioning/anjay_provisioning.c | 23 +- src/modules/server/anjay_mod_server.c | 18 +- src/modules/server/anjay_mod_server.h | 2 + tests/core/anjay.c | 486 ++- tests/core/bootstrap.c | 18 +- tests/core/observe/observe.c | 43 - tests/core/socket_mock.c | 17 + .../downloader_mock.h => socket_mock.h} | 15 +- tests/integration/CMakeLists.txt | 49 +- tests/integration/framework/asserts.py | 42 +- .../integration/framework/firmware_package.py | 55 +- .../framework/lwm2m/coap/server.py | 104 +- tests/integration/framework/lwm2m/server.py | 10 +- tests/integration/framework/lwm2m/tlv.py | 16 +- .../nsh-lwm2m/pymbedtls/src/socket.cpp | 33 +- tests/integration/framework/test_suite.py | 47 +- tests/integration/framework/test_utils.py | 54 +- tests/integration/run_tests.sh.in | 27 +- tests/integration/runtest.py | 64 +- .../default/advanced_firmware_update.py | 3700 +++++++++++++++++ tests/integration/suites/default/async.py | 100 +- .../suites/default/bootstrap_client.py | 4 +- .../suites/default/bootstrap_transaction.py | 126 +- .../suites/default/client_block_request.py | 3 - tests/integration/suites/default/crash.py | 2 - .../suites/default/disable_server.py | 5 +- .../integration/suites/default/downloader.py | 2 +- .../suites/default/firmware_update.py | 88 +- tests/integration/suites/default/msg_cache.py | 5 - .../suites/default/notification_timestamps.py | 12 +- .../suites/default/notifications.py | 129 + tests/integration/suites/default/offline.py | 66 +- .../integration/suites/default/queue_mode.py | 143 +- tests/integration/suites/default/reboot.py | 2 - tests/integration/suites/default/register.py | 13 +- .../suites/default/retransmissions.py | 29 +- tests/integration/suites/default/security.py | 2 - tests/integration/suites/default/send.py | 17 +- .../suites/default/separate_response.py | 14 +- .../integration/suites/default/test_object.py | 2 - tests/integration/suites/default/update.py | 68 +- .../suites/default/write_composite.py | 42 +- .../sensitive/advanced_firmware_update.py | 50 + .../suites/sensitive/bootstrap_client.py | 4 +- tests/integration/suites/sensitive/update.py | 1 - .../testfest/dm/advanced_firmware_update.py | 1267 ++++++ .../suites/testfest/dm/firmware_update.py | 2 +- .../integration/suites/testfest/management.py | 2 +- .../factory_provisioning/provisioning.c | 66 + tests/modules/server/persistence.c | 8 + tests/utils/dm.c | 28 +- tests/utils/dm.h | 75 +- tests/utils/mock_clock.c | 2 - tests/utils/mock_dm.c | 34 +- tests/utils/mock_dm.h | 96 +- tools/ci/rockylinux-9/Dockerfile | 6 +- tools/ci/ubuntu-18.04/Dockerfile | 2 +- tools/ci/ubuntu-20.04/Dockerfile | 2 +- tools/ci/ubuntu-22.04/Dockerfile | 2 +- tools/symlink-check.sh | 1 + 180 files changed, 16207 insertions(+), 1157 deletions(-) create mode 100644 demo/advanced_firmware_update.c create mode 100644 demo/advanced_firmware_update.h create mode 100644 demo/advanced_firmware_update_addimg.c create mode 100644 demo/advanced_firmware_update_app.c create mode 100644 doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate.rst create mode 100644 doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-BasicImplementation.rst create mode 100644 doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-Examples.rst create mode 100644 doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-ResourceDefinitions.rst create mode 100644 doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-StateDiagram.rst create mode 100644 doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/_files/33629.xml create mode 100644 doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/_images/FU-AFU-StateDiagram.svg create mode 100644 doc/sphinx/source/Migrating/MigratingFromAnjay32.rst create mode 100644 doc/sphinx/source/Migrating/MigratingFromAnjay33.rst create mode 100644 examples/tutorial/firmware-update/advanced-firmware-update/CMakeLists.txt create mode 100644 examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c create mode 100644 examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.h create mode 100644 examples/tutorial/firmware-update/advanced-firmware-update/src/main.c create mode 120000 examples/tutorial/firmware-update/advanced-firmware-update/src/time_object.c create mode 120000 examples/tutorial/firmware-update/advanced-firmware-update/src/time_object.h create mode 100644 include_public/anjay/advanced_fw_update.h create mode 100644 src/modules/advanced_fw_update/anjay_advanced_fw_update.c create mode 100644 tests/core/socket_mock.c rename tests/core/{downloader/downloader_mock.h => socket_mock.h} (67%) create mode 100644 tests/integration/suites/default/advanced_firmware_update.py create mode 100644 tests/integration/suites/sensitive/advanced_firmware_update.py create mode 100644 tests/integration/suites/testfest/dm/advanced_firmware_update.py create mode 100644 tests/modules/factory_provisioning/provisioning.c diff --git a/.github/workflows/anjay-tests.yml b/.github/workflows/anjay-tests.yml index be2e49380..25181ceee 100644 --- a/.github/workflows/anjay-tests.yml +++ b/.github/workflows/anjay-tests.yml @@ -10,7 +10,7 @@ on: [push] jobs: ubuntu1804-compilers-test: runs-on: ubuntu-latest - container: avsystemembedded/anjay-travis:ubuntu-18.04-1.0 + container: avsystemembedded/anjay-travis:ubuntu-18.04-1.1 env: CC: ${{ matrix.CC }} CXX: ${{ matrix.CXX }} @@ -37,7 +37,7 @@ jobs: ubuntu2004-compilers-test: runs-on: ubuntu-latest - container: avsystemembedded/anjay-travis:ubuntu-20.04-1.0 + container: avsystemembedded/anjay-travis:ubuntu-20.04-1.1 env: CC: ${{ matrix.CC }} CXX: ${{ matrix.CXX }} @@ -68,7 +68,7 @@ jobs: ubuntu2204-compilers-test: runs-on: ubuntu-latest - container: avsystemembedded/anjay-travis:ubuntu-22.04-1.0 + container: avsystemembedded/anjay-travis:ubuntu-22.04-1.1 env: CC: ${{ matrix.CC }} CXX: ${{ matrix.CXX }} @@ -110,7 +110,7 @@ jobs: rockylinux9-compilers-test: runs-on: ubuntu-latest - container: avsystemembedded/anjay-travis:rockylinux-9-1.0 + container: avsystemembedded/anjay-travis:rockylinux-9-1.1 env: CC: ${{ matrix.CC }} CXX: ${{ matrix.CXX }} @@ -123,6 +123,8 @@ jobs: submodules: recursive - run: dnf update -y - run: dnf install -y $CC + # Solve issues with EPERM when running dumpcap + - run: setcap '' $(which dumpcap) - run: ./devconfig --with-valgrind --without-analysis -DWITH_VALGRIND_TRACK_ORIGINS=OFF -DWITH_URL_CHECK=OFF -DWITH_IPV6=OFF - run: env CC=gcc LC_ALL=C.UTF-8 make -j - run: env CC=gcc LC_ALL=C.UTF-8 make check diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dd3d4265..84869e09c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,68 @@ # Changelog +## 3.4.0 (June 14th, 2023) + +### Features + +- New APIs for server connection lifecycle management: + ``anjay_server_schedule_reconnect()`` and ``anjay_schedule_register()`` +- New options in ``anjay_configuration_t`` that allow for optional more strict + LwM2M TS compliance: ``update_immediately_on_dm_change`` and + ``enable_self_notify`` +- Added option to disable auto-closing of the socket when in queue mode. +- `avs_coap_observe_cancel()` is now public API (for direct users of avs_coap) +- Added Advanced Firmware Update as an Anjay module +- Added simplified demo of Advanced Firmware Update with two firmware images +- Added tutorial for Advanced Firmware Update + +### Improvements + +- Observations are now automatically cancelled if the client needs to send a new + Register messsage +- Added explicit casts in macros that involve negating an unsigned value, to + silence warnings generated by some compilers +- Various improvements and refactors of integration tests, to make them run + faster and more stable +- (commercial feature only) Disabled servers are saved now in Core Persistence, + which prevents them from registering until the timeout passes with respect to + the real clock. + +### Bugfixes + +- Fixed handling of SenML payload that could cause erroneous behavior and memory + leaks when parsing payloads only containing the Base Name field, without Name +- Fixed error handling in bootstrapper (commercial only) and factory + provisioning modules so that the changes are properly rolled back in case of + error +- Fixed assertion error (or 4.05 Method Not Allowed when compiled without + assertions) when attempting to set the Disable Timeout resource in the Server + object when the `ANJAY_WITHOUT_DEREGISTER` configuration option is set +- Fixed inequality comparisons on some time values that could cause erroneous + behavior on platforms with low-resolution system clocks, and updated unit + tests to not assume a high-resolution clock +- Fixed a problem where `anjay_ongoing_registration_exists()` inconditionally + returned `true` if any server connection in a "disabled" state existed +- Timeout when sending a Confirmable Notification now cancels the observation, + as required by RFC 7641 +- Removed sending of Release messages when using CoAP+TCP, which fixes the issue + of erroneously sending them at the beginning after reconnecting a LwM2M + connection socket +- Fixed the serial port handling code in the sample NIDD driver, to properly + handle cases where more than one line is received in a single read() call +- Fixed compatibility of integration tests with the current versions of the + Python cryptography module +- Fixed problems with compiling the library without `WITH_AVS_COAP_BLOCK` + enabled (contributed by Flonidan A/S) +- Fixed a bug in the pymbedtls library used by tests, that prevented it from + working in DTLS client mode +- Fixed bug in the "Custom (D)TLS layers" code examples + ## 3.3.1 (March 10th, 2023) ### Improvements - `anjay_disable_server()` and `anjay_disable_server_with_timeout()` can now be - be called on servers that are not enabled as well + called on servers that are not enabled as well ### Bugfixes diff --git a/CMakeLists.txt b/CMakeLists.txt index 0164265a9..56ac986ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ cmake_minimum_required(VERSION 3.6.0) project(anjay C) -set(ANJAY_VERSION "3.3.1" CACHE STRING "Anjay library version") +set(ANJAY_VERSION "3.4.0" CACHE STRING "Anjay library version") set(ANJAY_BINARY_VERSION 1.0.0) set(ANJAY_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") @@ -271,6 +271,7 @@ option(WITHOUT_IP_STICKINESS "Disable support for IP stickiness" OFF) cmake_dependent_option(WITH_SENML_JSON "Enable support for SenML JSON content format" ON WITH_LWM2M11 OFF) cmake_dependent_option(WITH_CBOR "Enable support for CBOR and SenML CBOR content formats" ON WITH_LWM2M11 OFF) cmake_dependent_option(WITH_SEND "Enable support for LwM2M 1.1 Send operation" ON "WITH_CBOR OR WITH_SENML_JSON" OFF) +option(WITHOUT_QUEUE_MODE_AUTOCLOSE "Disable automatic closing of server connection sockets after MAX_TRANSMIT_WAIT of inactivity" OFF) cmake_dependent_option(WITH_OBSERVATION_STATUS "Enable support for anjay_resource_observation_status() API" ON "WITH_OBSERVE" OFF) cmake_dependent_option(WITH_COAP_DOWNLOAD "Enable support for CoAP(S) downloads" ON WITH_DOWNLOADER OFF) @@ -330,11 +331,13 @@ cmake_dependent_option(WITHOUT_MODULE_fw_update_PUSH_MODE "Disable support for PUSH mode Firmware Update" OFF "WITH_MODULE_fw_update;WITH_DOWNLOADER" OFF) cmake_dependent_option(WITH_MODULE_factory_provisioning "Factory provisioning module" ON "WITH_BOOTSTRAP;WITH_CBOR" OFF) +option(WITH_MODULE_advanced_fw_update "Advanced Firmware Update object module" OFF) ################# CODE ######################################################### add_library(anjay include_public/anjay/access_control.h + include_public/anjay/advanced_fw_update.h include_public/anjay/anjay.h include_public/anjay/attr_storage.h include_public/anjay/core.h @@ -473,6 +476,7 @@ add_library(anjay src/modules/access_control/anjay_access_control_persistence.c src/modules/access_control/anjay_mod_access_control.c src/modules/access_control/anjay_mod_access_control.h + src/modules/advanced_fw_update/anjay_advanced_fw_update.c src/modules/factory_provisioning/anjay_provisioning.c src/modules/fw_update/anjay_fw_update.c src/modules/ipso/anjay_ipso_3d_sensor.c @@ -522,6 +526,7 @@ set(ANJAY_WITHOUT_IP_STICKINESS "${WITHOUT_IP_STICKINESS}") set(ANJAY_WITH_MODULE_ACCESS_CONTROL "${WITH_MODULE_access_control}") set(ANJAY_WITH_MODULE_IPSO_OBJECTS "${WITH_MODULE_ipso_objects}") set(ANJAY_WITH_MODULE_FW_UPDATE "${WITH_MODULE_fw_update}") +set(ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE "${WITH_MODULE_advanced_fw_update}") set(ANJAY_WITHOUT_MODULE_FW_UPDATE_PUSH_MODE "${WITHOUT_MODULE_fw_update_PUSH_MODE}") set(ANJAY_WITH_MODULE_SECURITY "${WITH_MODULE_security}") set(ANJAY_WITH_MODULE_SERVER "${WITH_MODULE_server}") @@ -539,6 +544,7 @@ set(ANJAY_WITH_LWM2M11 "${WITH_LWM2M11}") set(ANJAY_WITH_SECURITY_STRUCTURED "${WITH_SECURITY_STRUCTURED}") set(ANJAY_WITH_SEND "${WITH_SEND}") set(ANJAY_WITH_SENML_JSON "${WITH_SENML_JSON}") +set(ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE "${WITHOUT_QUEUE_MODE_AUTOCLOSE}") configure_file(include_public/anjay/anjay_config.h.in include_public/anjay/anjay_config.h) @@ -603,10 +609,11 @@ if(WITH_TEST) $ tests/core/coap/utils.c tests/core/coap/utils.h - tests/core/downloader/downloader_mock.h tests/core/bootstrap_mock.h tests/core/io/bigdata.h tests/core/observe/observe_mock.h + tests/core/socket_mock.c + tests/core/socket_mock.h tests/utils/dm.c tests/utils/dm.h tests/utils/mock_clock.c @@ -625,6 +632,9 @@ if(WITH_TEST) if(WITH_SEND) target_sources(anjay_test PRIVATE tests/core/lwm2m_send.c) endif() + if(WITH_MODULE_factory_provisioning) + target_sources(anjay_test PRIVATE tests/modules/factory_provisioning/provisioning.c) + endif() target_include_directories(anjay_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" $) @@ -758,7 +768,7 @@ if(WITH_TEST) if(WITH_INTEGRATION_TESTS) set(TEST_RERUNS 3 CACHE STRING "Maximal number of times we try to rerun each testsuite") - set(TEST_SENSITIVE_RERUNS 3 CACHE STRING "Maximal number of times we try to rerun each sensitive testsuite") + option(TEST_KEEP_SUCCESS_LOGS "Store all logs from test cases, even if no error happened" ON) add_subdirectory(tests/integration) endif() else(WITH_TEST) @@ -856,6 +866,10 @@ if(WITH_MODULE_fw_update) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/include_public/anjay/fw_update.h" DESTINATION include/anjay) endif() +if(WITH_MODULE_advanced_fw_update) + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/include_public/anjay/advanced_fw_update.h" + DESTINATION include/anjay) +endif() if(WITH_MODULE_security) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/include_public/anjay/security.h" DESTINATION include/anjay) diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 4a5232010..2265ad32c 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -33,6 +33,14 @@ if (${ANJAY_WITH_MODULE_FW_UPDATE}) set(SOURCES ${SOURCES} firmware_update.c) endif() +if (${ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE}) + set(SOURCES ${SOURCES} + advanced_firmware_update.c + advanced_firmware_update_app.c + advanced_firmware_update_addimg.c) + +endif() + if(NOT WIN32) set(SOURCES ${SOURCES} objects/ip_ping.c) endif() @@ -48,6 +56,10 @@ if (${ANJAY_WITH_MODULE_FW_UPDATE}) set(HEADERS ${HEADERS} firmware_update.h) endif() +if (${ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE}) + set(HEADERS ${HEADERS} advanced_firmware_update.h) +endif() + set(ALL_SOURCES ${SOURCES} ${HEADERS}) if(NOT TARGET anjay) diff --git a/demo/advanced_firmware_update.c b/demo/advanced_firmware_update.c new file mode 100644 index 000000000..24b2fa51a --- /dev/null +++ b/demo/advanced_firmware_update.c @@ -0,0 +1,1250 @@ +/* + * Copyright 2017-2023 AVSystem + * AVSystem Anjay LwM2M SDK + * All rights reserved. + * + * Licensed under the AVSystem-5-clause License. + * See the attached LICENSE file for details. + */ + +#include "advanced_firmware_update.h" +#include +#include +#include + +#include +#include + +typedef struct { + states_results_paths_t states_results_paths; + char *download_file; + anjay_advanced_fw_update_severity_t severity; + avs_time_real_t last_state_change_time; + avs_time_real_t update_deadline; + char current_ver[IMG_VER_STR_MAX_LEN + 1]; +} advanced_firmware_update_persistence_file_data_t; + +typedef struct { + anjay_t *anjay; + anjay_iid_t iid; + anjay_advanced_fw_update_state_t delayed_state; + anjay_advanced_fw_update_result_t delayed_result; +} set_delayed_advanced_fw_update_result_args_t; + +static void set_delayed_fw_update_result(avs_sched_t *sched, const void *arg) { + (void) sched; + const set_delayed_advanced_fw_update_result_args_t *args = + (const set_delayed_advanced_fw_update_result_args_t *) arg; + + anjay_advanced_fw_update_set_state_and_result( + args->anjay, args->iid, args->delayed_state, args->delayed_result); +} + +static void fix_fw_meta_endianness(advanced_fw_metadata_t *meta) { + meta->version = avs_convert_be16(meta->version); + meta->force_error_case = avs_convert_be16(meta->force_error_case); + meta->crc = avs_convert_be32(meta->crc); +} + +static int read_fw_meta_from_file(FILE *f, + advanced_fw_metadata_t *out_metadata, + uint32_t *out_metadata_len) { + advanced_fw_metadata_t m; + memset(&m, 0, sizeof(m)); + + if (fread(m.magic, sizeof(m.magic), 1, f) != 1 + || fread(&m.version, sizeof(m.version), 1, f) != 1 + || fread(&m.force_error_case, sizeof(m.force_error_case), 1, f) != 1 + || fread(&m.crc, sizeof(m.crc), 1, f) != 1 + || fread(m.linked, sizeof(m.linked), 1, f) != 1) { + demo_log(ERROR, "could not read firmware metadata"); + return -1; + } + + if (fread(&m.pkg_ver_len, sizeof(m.pkg_ver_len), 1, f) != 1) { + demo_log(ERROR, "could not read firmware metadata"); + return -1; + } + + if (m.pkg_ver_len > IMG_VER_STR_MAX_LEN || m.pkg_ver_len == 0) { + demo_log(ERROR, "Wrong pkg version len"); + return ANJAY_ADVANCED_FW_UPDATE_ERR_UNSUPPORTED_PACKAGE_TYPE; + } + + if (fread(m.pkg_ver, m.pkg_ver_len, 1, f) != 1) { + demo_log(ERROR, "could not read firmware metadata"); + return -1; + } + m.pkg_ver[m.pkg_ver_len] = '\0'; + + fix_fw_meta_endianness(&m); + *out_metadata = m; + *out_metadata_len = sizeof(m.magic) + sizeof(m.version) + + sizeof(m.force_error_case) + sizeof(m.crc) + + sizeof(m.linked) + sizeof(m.pkg_ver_len) + + m.pkg_ver_len; + return 0; +} + +static int +handle_multipackage(FILE *f, + advanced_fw_multipkg_metadata_t *out_multiple_metadata) { + advanced_fw_multipkg_metadata_t mm; + memset(&mm, 0, sizeof(mm)); + + if (fread(mm.magic, sizeof(mm.magic), 1, f) != 1 + || fread(&mm.version, sizeof(mm.version), 1, f) != 1) { + demo_log(ERROR, "could not read firmware metadata"); + return -1; + } + + mm.version = avs_convert_be16(mm.version); + if (mm.version == 3 && !memcmp(mm.magic, "MULTIPKG", sizeof(mm.magic))) { + demo_log(INFO, "Received multi package firmware"); + if (fread(&mm.packages_count, sizeof(mm.packages_count), 1, f) != 1) { + demo_log(ERROR, "could not read firmware metadata"); + return -1; + } + mm.packages_count = avs_convert_be16(mm.packages_count); + if (mm.packages_count > FW_UPDATE_IID_IMAGE_SLOTS) { + demo_log(ERROR, + "Received packages_count %u is more than" + " available slots", + mm.packages_count); + return -1; + } + for (uint16_t i = 0; i < mm.packages_count; ++i) { + if (fread(&mm.package_len[i], sizeof(mm.package_len[i]), 1, f) + != 1) { + demo_log(ERROR, "could not read firmware metadata"); + return -1; + } + mm.package_len[i] = avs_convert_be32(mm.package_len[i]); + if (mm.package_len == 0) { + demo_log( + ERROR, + "Zero-length packages within multipackage not allowed"); + return -1; + } + } + demo_log(INFO, "Multi meta: {version: %u, packages_count: %u}", + mm.version, mm.packages_count); + *out_multiple_metadata = mm; + } else { + /* It is not multipackage, move stream to the beginning to easily handle + * like standard package */ + fseek(f, 0L, SEEK_SET); + } + return 0; +} + +static int copy_file_contents_by_bytes(FILE *dst, FILE *src, uint32_t len) { + char buf[4096]; + uint32_t to_read = 0; + while (len) { + to_read = len > sizeof(buf) ? sizeof(buf) : len; + size_t bytes_read = fread(buf, 1, to_read, src); + if (bytes_read != to_read) { + return -1; + } + + if (fwrite(buf, 1, bytes_read, dst) != bytes_read) { + return -1; + } + len -= (uint32_t) bytes_read; + } + return 0; +} + +static int unpack_fw_to_file(FILE *fw, + uint32_t fw_len, + const char *target_path, + advanced_fw_metadata_t *out_metadata) { + int result = -1; + FILE *tmp = NULL; + uint32_t metadata_len = 0; + + tmp = fopen(target_path, "wb"); + if (!tmp) { + demo_log(ERROR, "could not open file: %s", target_path); + goto cleanup; + } + + result = read_fw_meta_from_file(fw, out_metadata, &metadata_len); + if (result) { + demo_log(ERROR, "could not read metadata"); + goto cleanup; + } + if (fw_len) { + result = copy_file_contents_by_bytes(tmp, fw, fw_len - metadata_len); + } else { + result = copy_file_contents(tmp, fw); + } + if (result) { + demo_log(ERROR, "could not copy firmware"); + goto cleanup; + } + + result = 0; + +cleanup: + if (tmp) { + fclose(tmp); + } + return result; +} + +static void maybe_delete_firmware_file(advanced_fw_update_logic_t *fw) { + if (fw->next_target_path) { + unlink(fw->next_target_path); + demo_log(INFO, "Deleted %s", fw->next_target_path); + avs_free(fw->next_target_path); + fw->next_target_path = NULL; + } +} + +const char *const MAGICS[] = { + [FW_UPDATE_IID_APP] = "AJAY_APP", + [FW_UPDATE_IID_TEE] = "AJAY_TEE", + [FW_UPDATE_IID_BOOT] = "AJAYBOOT", + [FW_UPDATE_IID_MODEM] = "AJAYMODE" +}; + +static int find_instance_magic_based(advanced_fw_metadata_t *meta, + anjay_iid_t *iid) { + for (size_t i = 0; i < AVS_ARRAY_SIZE(MAGICS); ++i) { + if (!memcmp(meta->magic, MAGICS[i], sizeof(meta->magic))) { + *iid = (anjay_iid_t) i; + return 0; + } + } + return -1; +} + +static int unpack_firmware(FILE *firmware, + uint32_t len, + unpacked_imgs_info_t *unpacked_info) { + char *tmp_path = generate_random_target_filepath(); + if (!tmp_path) { + return -1; + } + + advanced_fw_metadata_t metadata; + memset(&metadata, 0x00, sizeof(metadata)); + int result = unpack_fw_to_file(firmware, len, tmp_path, &metadata); + if (result) { + goto cleanup; + } + anjay_iid_t iid; + result = find_instance_magic_based(&metadata, &iid); + if (!result) { + unpacked_info[iid].path = tmp_path; + unpacked_info[iid].meta = metadata; + } + +cleanup: + if (result) { + unlink(tmp_path); + avs_free(tmp_path); + } + return result; +} + +static bool is_state_downloaded(advanced_fw_update_logic_t *fw) { + assert(fw->anjay); + anjay_advanced_fw_update_state_t state = + ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE; + anjay_advanced_fw_update_get_state(fw->anjay, fw->iid, &state); + return state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED; +} + +static int unpack_firmware_in_place(anjay_iid_t iid, + advanced_fw_update_logic_t *fw_table, + anjay_iid_t *downloaded_iids, + int *downloaded_iids_count) { + advanced_fw_update_logic_t *fw = &fw_table[iid]; + + *downloaded_iids_count = 0; + advanced_fw_multipkg_metadata_t multi_metadata; + memset(&multi_metadata, 0x00, sizeof(multi_metadata)); + FILE *firmware = NULL; + unpacked_imgs_info_t unpacked_info[FW_UPDATE_IID_IMAGE_SLOTS]; + memset(&unpacked_info, 0x00, sizeof(unpacked_info)); + uint16_t to_unpack = 0; + firmware = fopen(fw->next_target_path, "rb"); + if (!firmware) { + demo_log(ERROR, "could not open file: %s", fw->next_target_path); + return -1; + } + int result = handle_multipackage(firmware, &multi_metadata); + if (result) { + goto cleanup; + } + + /* packages_count == 0 means that it is not multipackage, but there is + * still one 'normal' package to unpack */ + to_unpack = AVS_MAX(1, multi_metadata.packages_count); + + for (int i = 0; i < to_unpack; ++i) { + if ((result = unpack_firmware(firmware, multi_metadata.package_len[i], + unpacked_info))) { + goto cleanup; + } + } + + for (anjay_iid_t i = 0; i < FW_UPDATE_IID_IMAGE_SLOTS; ++i) { + if (unpacked_info[i].path && is_state_downloaded(&fw_table[i])) { + demo_log(ERROR, + "Failure. Multipackage contains package for " + "instance /" AVS_QUOTE_MACRO( + ANJAY_ADVANCED_FW_UPDATE_OID) "/%d which is " + "already in " + "DOWNLOADED state.", + i); + anjay_advanced_fw_update_set_conflicting_instances(fw->anjay, + fw->iid, &i, 1); + result = ANJAY_ADVANCED_FW_UPDATE_ERR_CONFLICTING_STATE; + goto cleanup; + } + } + + for (anjay_iid_t i = 0; i < FW_UPDATE_IID_IMAGE_SLOTS; ++i) { + if (unpacked_info[i].path) { + fw_update_common_maybe_create_firmware_file(&fw_table[i]); + if ((result = rename(unpacked_info[i].path, + fw_table[i].next_target_path)) + == -1) { + demo_log(ERROR, "could not rename %s to %s: %s", + unpacked_info[i].path, fw_table[i].next_target_path, + strerror(errno)); + goto cleanup; + } + if ((result = chmod(fw_table[i].next_target_path, 0700)) == -1) { + demo_log(ERROR, "could not set permissions for %s: %s", + fw_table[i].next_target_path, strerror(errno)); + goto cleanup; + } + fw_table[i].metadata = unpacked_info[i].meta; + downloaded_iids[*downloaded_iids_count] = i; + (*downloaded_iids_count)++; + } + } + +cleanup: + for (int i = 0; i < FW_UPDATE_IID_IMAGE_SLOTS; ++i) { + if (unpacked_info[i].path) { + unlink(unpacked_info[i].path); + avs_free(unpacked_info[i].path); + } + } + + if (firmware) { + fclose(firmware); + } + if (result) { + maybe_delete_firmware_file(fw); + } + return result; +} + +static bool fw_magic_valid(const advanced_fw_metadata_t *meta, + anjay_iid_t iid) { + switch (iid) { + case FW_UPDATE_IID_APP: + case FW_UPDATE_IID_TEE: + case FW_UPDATE_IID_BOOT: + case FW_UPDATE_IID_MODEM: + if (!memcmp(meta->magic, MAGICS[iid], sizeof(meta->magic))) { + return true; + } + break; + default: + break; + } + demo_log(ERROR, "invalid firmware magic"); + return false; +} + +static bool fw_version_supported(const advanced_fw_metadata_t *meta) { + if (meta->version != 2) { + demo_log(ERROR, "unsupported firmware version: %u", meta->version); + return false; + } + + return true; +} + +static int validate_firmware(advanced_fw_update_logic_t *fw) { + if (!fw_magic_valid(&fw->metadata, fw->iid) + || !fw_version_supported(&fw->metadata)) { + return ANJAY_ADVANCED_FW_UPDATE_ERR_UNSUPPORTED_PACKAGE_TYPE; + } + + uint32_t actual_crc; + int result = calc_file_crc32(fw->next_target_path, &actual_crc); + + if (result) { + demo_log(WARNING, "unable to check firmware CRC"); + return ANJAY_ADVANCED_FW_UPDATE_ERR_INTEGRITY_FAILURE; + } + + if (fw->metadata.crc != actual_crc) { + demo_log(WARNING, "CRC mismatch: expected %08x != %08x actual", + fw->metadata.crc, actual_crc); + return ANJAY_ADVANCED_FW_UPDATE_ERR_INTEGRITY_FAILURE; + } + + switch (fw->metadata.force_error_case) { + case FORCE_ERROR_OUT_OF_MEMORY: + return ANJAY_ADVANCED_FW_UPDATE_ERR_OUT_OF_MEMORY; + default: + break; + } + + return 0; +} + +static int process_linked(advanced_fw_update_logic_t *fw) { + anjay_iid_t linked[METADATA_LINKED_SLOTS]; + size_t linked_count = 0; + memset(linked, 0xFF, sizeof(linked)); + for (int i = 0; i < METADATA_LINKED_SLOTS; ++i) { + // Below condition compares metadata.linked[] with + // METADATA_LINKED_SLOTS, because max handled iid is derived from + // available slots. + if (fw->metadata.linked[i] < METADATA_LINKED_SLOTS) { + linked[linked_count++] = fw->metadata.linked[i]; + } else if (fw->metadata.linked[i] != 0xFF) { + demo_log(WARNING, "Unexpected linked instance iid"); + } + } + return anjay_advanced_fw_update_set_linked_instances(fw->anjay, fw->iid, + linked, linked_count); +} + +static int preprocess_firmware(anjay_iid_t iid, + advanced_fw_update_logic_t *fw_table) { + anjay_iid_t downloaded_iids[FW_UPDATE_IID_IMAGE_SLOTS]; + int downloaded_iids_count = 0; + int result = unpack_firmware_in_place(iid, fw_table, downloaded_iids, + &downloaded_iids_count); + if (result) { + return result; + } + + result = -1; + for (int i = 0; i < downloaded_iids_count; ++i) { + advanced_fw_update_logic_t *fw = &fw_table[downloaded_iids[i]]; + if ((result = validate_firmware(fw))) { + break; + } + if ((result = process_linked(fw))) { + break; + } + demo_log(INFO, + "firmware for instance /" AVS_QUOTE_MACRO( + ANJAY_ADVANCED_FW_UPDATE_OID) "/%u downloaded " + "successfully", + downloaded_iids[i]); + if ((result = anjay_advanced_fw_update_set_state_and_result( + fw->anjay, fw->iid, + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL))) { + break; + } + } + return result; +} + +#if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) \ + && defined(AVS_COMMONS_STREAM_WITH_FILE) +static advanced_firmware_update_persistence_file_data_t +advanced_firmware_update_read_persistence_file(const char *path) { + advanced_firmware_update_persistence_file_data_t data; + memset(&data, 0, sizeof(data)); + avs_stream_t *stream = NULL; + int8_t results8[FW_UPDATE_IID_IMAGE_SLOTS]; + memset(results8, 0x00, sizeof(results8)); + int8_t states8[FW_UPDATE_IID_IMAGE_SLOTS]; + memset(states8, 0x00, sizeof(states8)); + for (size_t i = 0; i < AVS_ARRAY_SIZE(results8); ++i) { + results8[i] = (int8_t) ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL; + states8[i] = (int8_t) ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE; + } + if ((stream = avs_stream_file_create(path, AVS_STREAM_FILE_READ))) { + // invalid or empty but existing file still signifies success but only + // for APP instance + results8[FW_UPDATE_IID_APP] = + (int8_t) ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS; + } + avs_persistence_context_t ctx = + avs_persistence_restore_context_create(stream); + uint8_t severity8 = (uint8_t) ANJAY_ADVANCED_FW_UPDATE_SEVERITY_MANDATORY; + int64_t last_state_change_timestamp = 0; + int64_t update_timestamp = 0; + char *current_ver = NULL; + if (!stream + || avs_is_err(avs_persistence_bytes(&ctx, (uint8_t *) &results8, + sizeof(results8))) + || avs_is_err(avs_persistence_bytes(&ctx, (uint8_t *) &states8, + sizeof(states8))) + || avs_is_err(avs_persistence_bytes(&ctx, &severity8, 1)) + || avs_is_err( + avs_persistence_i64(&ctx, &last_state_change_timestamp)) + || avs_is_err(avs_persistence_i64(&ctx, &update_timestamp)) + || avs_is_err(avs_persistence_string(&ctx, ¤t_ver))) { + demo_log(WARNING, + "Invalid data in the firmware state persistence file"); + memset(&data, 0, sizeof(data)); + } else { + for (anjay_iid_t iid = FW_UPDATE_IID_APP; + iid < FW_UPDATE_IID_IMAGE_SLOTS; + ++iid) { + if (avs_is_err(avs_persistence_string( + &ctx, + &data.states_results_paths.next_target_paths[iid]))) { + for (anjay_iid_t i = FW_UPDATE_IID_APP; + i < FW_UPDATE_IID_IMAGE_SLOTS; + ++i) { + avs_free(data.states_results_paths.next_target_paths[i]); + } + demo_log(WARNING, + "Invalid data in the firmware state persistence file"); + memset(&data, 0, sizeof(data)); + break; + } + } + } + if (current_ver && strlen(current_ver) <= IMG_VER_STR_MAX_LEN) { + strcpy(data.current_ver, current_ver); + } else { + demo_log(WARNING, "Invalid version string"); + } + avs_free(current_ver); + for (size_t i = 0; i < AVS_ARRAY_SIZE(results8); ++i) { + data.states_results_paths.inst_results[i] = + (anjay_advanced_fw_update_result_t) results8[i]; + data.states_results_paths.inst_states[i] = + (anjay_advanced_fw_update_state_t) states8[i]; + } + data.severity = (anjay_advanced_fw_update_severity_t) severity8; + data.last_state_change_time = + avs_time_real_from_scalar(last_state_change_timestamp, AVS_TIME_S); + data.update_deadline = + avs_time_real_from_scalar(update_timestamp, AVS_TIME_S); + if (stream) { + avs_stream_cleanup(&stream); + } + return data; +} + +int advanced_firmware_update_write_persistence_file( + const char *path, + states_results_paths_t *states_results_paths, + anjay_advanced_fw_update_severity_t severity, + avs_time_real_t last_state_change_time, + avs_time_real_t update_deadline, + const char *current_ver) { + avs_stream_t *stream = avs_stream_file_create(path, AVS_STREAM_FILE_WRITE); + avs_persistence_context_t ctx = + avs_persistence_store_context_create(stream); + int8_t results8[FW_UPDATE_IID_IMAGE_SLOTS]; + memset(results8, 0x00, sizeof(results8)); + int8_t states8[FW_UPDATE_IID_IMAGE_SLOTS]; + memset(states8, 0x00, sizeof(states8)); + for (size_t i = 0; i < AVS_ARRAY_SIZE(results8); ++i) { + results8[i] = (int8_t) states_results_paths->inst_results[i]; + states8[i] = (int8_t) states_results_paths->inst_states[i]; + } + uint8_t severity8 = (uint8_t) severity; + int64_t last_state_change_timestamp = 0; + avs_time_real_to_scalar(&last_state_change_timestamp, AVS_TIME_S, + last_state_change_time); + int64_t update_timestamp = 0; + avs_time_real_to_scalar(&update_timestamp, AVS_TIME_S, update_deadline); + + int retval = 0; + if (!stream + || avs_is_err(avs_persistence_bytes(&ctx, (uint8_t *) &results8, + sizeof(results8))) + || avs_is_err(avs_persistence_bytes(&ctx, (uint8_t *) &states8, + sizeof(states8))) + || avs_is_err(avs_persistence_bytes(&ctx, &severity8, 1)) + || avs_is_err( + avs_persistence_i64(&ctx, &last_state_change_timestamp)) + || avs_is_err(avs_persistence_i64(&ctx, &update_timestamp)) + || avs_is_err(avs_persistence_string( + &ctx, (char **) (intptr_t) ¤t_ver))) { + demo_log(ERROR, "Could not write firmware state persistence file"); + retval = -1; + } else { + for (anjay_iid_t iid = FW_UPDATE_IID_APP; + iid < FW_UPDATE_IID_IMAGE_SLOTS; + ++iid) { + if (avs_is_err(avs_persistence_string( + &ctx, &states_results_paths->next_target_paths[iid]))) { + demo_log(ERROR, + "Could not write firmware state persistence file"); + retval = -1; + break; + } + } + } + if (stream) { + avs_stream_cleanup(&stream); + } + if (retval) { + unlink(path); + } + return retval; +} + +void advanced_firmware_update_delete_persistence_file( + const advanced_fw_update_logic_t *fw) { + if (fw->persistence_file) { + unlink(fw->persistence_file); + } +} + +#else // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && + // defined(AVS_COMMONS_STREAM_WITH_FILE) +advanced_firmware_update_persistence_file_data_t +advanced_firmware_update_read_persistence_file(const char *path) { + (void) path; + demo_log(WARNING, "Persistence not compiled in"); + persistence_file_data_t retval; + memset(&retval, 0, sizeof(retval)); + return retval; +} + +int advanced_firmware_update_write_persistence_file( + const char *path, + anjay_advanced_fw_update_state_t state, + anjay_advanced_fw_update_state_t result) { + (void) path; + (void) state; + (void) result; + demo_log(WARNING, "Persistence not compiled in"); + return 0; +} + +void advanced_firmware_update_delete_persistence_file( + const advanced_fw_update_logic_t *fw) { + (void) fw; + demo_log(WARNING, "Persistence not compiled in"); +} +#endif // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && + // defined(AVS_COMMONS_STREAM_WITH_FILE) + +static void fw_reset(advanced_fw_update_logic_t *fw) { + if (fw->stream) { + fclose(fw->stream); + fw->stream = NULL; + } + maybe_delete_firmware_file(fw); + advanced_firmware_update_delete_persistence_file(fw); +} + +int advanced_firmware_update_read_states_results_paths( + advanced_fw_update_logic_t *fw_table, + states_results_paths_t *out_states_results_paths) { + for (int i = 0; i < FW_UPDATE_IID_IMAGE_SLOTS; ++i) { + if (anjay_advanced_fw_update_get_state( + fw_table[i].anjay, + fw_table[i].iid, + &out_states_results_paths->inst_states[i])) { + return -1; + } + if (anjay_advanced_fw_update_get_result( + fw_table[i].anjay, + fw_table[i].iid, + &out_states_results_paths->inst_results[i])) { + return -1; + } + out_states_results_paths->next_target_paths[i] = + fw_table[i].next_target_path; + } + return 0; +} + +int fw_update_common_open(anjay_iid_t iid, void *fw_) { + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + advanced_fw_update_logic_t *fw = &fw_table[iid]; + assert(!fw->stream); + + if (fw_update_common_maybe_create_firmware_file(fw)) { + return -1; + } + if (!(fw->stream = fopen(fw->next_target_path, "wb"))) { + return -1; + } + + states_results_paths_t states_results_paths; + if (advanced_firmware_update_read_states_results_paths( + fw_table, &states_results_paths)) { + return -1; + } + states_results_paths.inst_states[FW_UPDATE_IID_APP] = + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING; + states_results_paths.inst_results[FW_UPDATE_IID_APP] = + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL; + if (fw->persistence_file + && advanced_firmware_update_write_persistence_file( + fw->persistence_file, &states_results_paths, + anjay_advanced_fw_update_get_severity(fw->anjay, + fw->iid), + anjay_advanced_fw_update_get_last_state_change_time( + fw->anjay, fw->iid), + anjay_advanced_fw_update_get_deadline(fw->anjay, + fw->iid), + fw->current_ver)) { + fw_reset(fw); + return -1; + } + return 0; +} + +int fw_update_common_write(anjay_iid_t iid, + void *fw_, + const void *data, + size_t length) { + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + advanced_fw_update_logic_t *fw = &fw_table[iid]; + if (!fw->stream) { + demo_log(ERROR, "stream not open"); + return -1; + } + if (length + && (fwrite(data, length, 1, fw->stream) != 1 + // Firmware update integration tests measure download + // progress by checking file size, so avoiding buffering + // is required. + || fflush(fw->stream) != 0)) { + demo_log(ERROR, "fwrite or fflush failed: %s", strerror(errno)); + return ANJAY_ADVANCED_FW_UPDATE_ERR_NOT_ENOUGH_SPACE; + } + return 0; +} + +static int stream_finish(anjay_iid_t iid, void *fw_) { + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + advanced_fw_update_logic_t *fw = &fw_table[iid]; + if (!fw->stream) { + demo_log(ERROR, "stream not open"); + return -1; + } + fclose(fw->stream); + fw->stream = NULL; + + anjay_advanced_fw_update_state_t tee_state; + anjay_advanced_fw_update_result_t tee_result; + anjay_advanced_fw_update_get_state(fw_table[FW_UPDATE_IID_TEE].anjay, + FW_UPDATE_IID_TEE, &tee_state); + anjay_advanced_fw_update_get_result(fw_table[FW_UPDATE_IID_TEE].anjay, + FW_UPDATE_IID_TEE, &tee_result); + + states_results_paths_t states_results_paths; + int result = advanced_firmware_update_read_states_results_paths( + fw_table, &states_results_paths); + if (result) { + return result; + } + states_results_paths.inst_states[FW_UPDATE_IID_APP] = + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED; + states_results_paths.inst_results[FW_UPDATE_IID_APP] = + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL; + if ((result = preprocess_firmware(iid, fw_table)) + || (fw->persistence_file + && (result = advanced_firmware_update_write_persistence_file( + fw->persistence_file, &states_results_paths, + anjay_advanced_fw_update_get_severity(fw->anjay, + fw->iid), + anjay_advanced_fw_update_get_last_state_change_time( + fw->anjay, fw->iid), + anjay_advanced_fw_update_get_deadline(fw->anjay, + fw->iid), + fw->current_ver)))) { + fw_reset(fw); + } + return result; +} + +const char *fw_update_common_get_current_version(anjay_iid_t iid, void *fw_) { + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + advanced_fw_update_logic_t *fw = &fw_table[iid]; + return (const char *) fw->current_ver; +} + +const char *fw_update_common_get_pkg_version(anjay_iid_t iid, void *fw_) { + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + advanced_fw_update_logic_t *fw = &fw_table[iid]; + return (const char *) fw->metadata.pkg_ver; +} + +static int +add_conflicting(anjay_iid_t (*inout_conflicting)[FW_UPDATE_IID_IMAGE_SLOTS], + size_t *inout_conflicting_len, + anjay_iid_t add_conf) { + if (*inout_conflicting_len + 1 > FW_UPDATE_IID_IMAGE_SLOTS + || ((*inout_conflicting) == NULL && *inout_conflicting_len != 0)) { + return -1; + } + + anjay_iid_t new_conflicting[FW_UPDATE_IID_IMAGE_SLOTS]; + memset(new_conflicting, 0x00, sizeof(new_conflicting)); + size_t position = 0; + + for (position = 0; position < *inout_conflicting_len; ++position) { + if ((*inout_conflicting)[position] == add_conf) { + return 0; + } else if ((*inout_conflicting)[position] > add_conf) { + break; + } + } + + for (size_t i = 0, j = 0; j < *inout_conflicting_len; ++i, ++j) { + if (i == position) { + i++; + } + new_conflicting[i] = (*inout_conflicting)[j]; + } + new_conflicting[position] = add_conf; + + for (size_t j = 0; j < *inout_conflicting_len + 1; ++j) { + (*inout_conflicting)[j] = new_conflicting[j]; + } + *inout_conflicting_len = *inout_conflicting_len + 1; + return 0; +} + +static void check_version_logic( + anjay_iid_t iid_in_check, + advanced_fw_update_logic_t *fw_table, + anjay_iid_t (*inout_conflicting_instances)[FW_UPDATE_IID_IMAGE_SLOTS], + size_t *inout_conflicting_instances_count) { + if (iid_in_check == FW_UPDATE_IID_APP) { + const char *app_pkg_ver = fw_update_common_get_pkg_version( + fw_table[FW_UPDATE_IID_APP].iid, fw_table); + const char *tee_cur_ver = fw_update_common_get_current_version( + fw_table[FW_UPDATE_IID_TEE].iid, fw_table); + /* Check major version, assuming that it is one digit len, first in ver + * string */ + if (app_pkg_ver[0] > tee_cur_ver[0]) { + add_conflicting(inout_conflicting_instances, + inout_conflicting_instances_count, + fw_table[FW_UPDATE_IID_TEE].iid); + } + } +} + +int fw_update_common_finish(anjay_iid_t iid, void *fw_) { + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + int result = -1; + result = stream_finish(iid, fw_); + if (!result) { + /* Below code checks two things: + * 1. Relationship between instances in DOWNLOADED state + * and their linked instances with context of common_finish + * 2. Version logic which is logic specific for targeted platform + * Then it sets conflicting instances accordingly + * */ + for (int i = 0; i < FW_UPDATE_IID_IMAGE_SLOTS; ++i) { + if (is_state_downloaded(&fw_table[i])) { + const anjay_iid_t *linked_instances; + size_t linked_instances_count = 0; + anjay_iid_t conflicting_instances[FW_UPDATE_IID_IMAGE_SLOTS]; + size_t conflicting_instances_count = 0; + anjay_advanced_fw_update_get_linked_instances( + fw_table[i].anjay, fw_table[i].iid, &linked_instances, + &linked_instances_count); + for (size_t j = 0; j < linked_instances_count; ++j) { + if (!is_state_downloaded(&fw_table[linked_instances[j]])) { + conflicting_instances[conflicting_instances_count++] = + linked_instances[j]; + } + } + + check_version_logic(fw_table[i].iid, + fw_table, + &conflicting_instances, + &conflicting_instances_count); + + anjay_advanced_fw_update_set_conflicting_instances( + fw_table[i].anjay, fw_table[i].iid, + conflicting_instances, conflicting_instances_count); + } + } + } + return result; +} + +void fw_update_common_reset(anjay_iid_t iid, void *fw_) { + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + advanced_fw_update_logic_t *fw = &fw_table[iid]; + if (fw->stream) { + fclose(fw->stream); + fw->stream = NULL; + } + maybe_delete_firmware_file(fw); + advanced_firmware_update_delete_persistence_file(fw); + anjay_advanced_fw_update_set_conflicting_instances(fw->anjay, fw->iid, NULL, + 0); + anjay_advanced_fw_update_set_linked_instances(fw->anjay, fw->iid, NULL, 0); + if (fw->update_job) { + avs_sched_del(&fw->update_job); + } + demo_log(INFO, + "Reset done for instance: /" AVS_QUOTE_MACRO( + ANJAY_ADVANCED_FW_UPDATE_OID) "/%u", + iid); + + /* Below code checks two things: + * 1. Relationship between instances in DOWNLOADED state + * and their linked instances with context of common_reset + * 2. Version logic which is logic specific for targeted platform + * Then it sets conflicting instances accordingly + * */ + for (int i = 0; i < FW_UPDATE_IID_IMAGE_SLOTS; ++i) { + /* Reset could be called by anjay befor all instances are initialized. + * Check of fw_table[i].anjay below makes sure that inst already + * initialized */ + if (fw_table[i].anjay && is_state_downloaded(&fw_table[i])) { + const anjay_iid_t *linked_instances; + size_t linked_instances_count = 0; + anjay_iid_t conflicting_instances[FW_UPDATE_IID_IMAGE_SLOTS]; + size_t conflicting_instances_count = 0; + anjay_advanced_fw_update_get_linked_instances( + fw_table[i].anjay, fw_table[i].iid, &linked_instances, + &linked_instances_count); + for (size_t j = 0; j < linked_instances_count; ++j) { + if ((!is_state_downloaded(&fw_table[linked_instances[j]])) + || linked_instances[j] == fw->iid) { + conflicting_instances[conflicting_instances_count++] = + linked_instances[j]; + } + } + + check_version_logic(fw_table[i].iid, + fw_table, + &conflicting_instances, + &conflicting_instances_count); + + anjay_advanced_fw_update_set_conflicting_instances( + fw_table[i].anjay, fw_table[i].iid, conflicting_instances, + conflicting_instances_count); + } + } +} + +int fw_update_common_perform_upgrade( + anjay_iid_t iid, + void *fw_, + const anjay_iid_t *requested_supplemental_iids, + size_t requested_supplemental_iids_count) { + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + advanced_fw_update_logic_t *fw = &fw_table[iid]; + const anjay_iid_t *conflicting_instances = NULL; + size_t conflicting_instances_count; + anjay_advanced_fw_update_get_conflicting_instances( + fw->anjay, iid, &conflicting_instances, + &conflicting_instances_count); + if (conflicting_instances) { + demo_log(ERROR, + "Trying to update /" AVS_QUOTE_MACRO( + ANJAY_ADVANCED_FW_UPDATE_OID) "/%u, but there are " + "conflicting " + "images", + fw->iid); + return ANJAY_ADVANCED_FW_UPDATE_ERR_DEPENDENCY_ERROR; + } + + const anjay_iid_t *update_with_iid = NULL; + size_t update_with_iid_count = 0; + if (requested_supplemental_iids) { + demo_log(INFO, "Received supplemental iids"); + update_with_iid = requested_supplemental_iids; + update_with_iid_count = requested_supplemental_iids_count; + } else { + const anjay_iid_t *linked_instances; + size_t linked_instances_count; + anjay_advanced_fw_update_get_linked_instances( + fw->anjay, iid, &linked_instances, &linked_instances_count); + if (linked_instances) { + update_with_iid = linked_instances; + update_with_iid_count = linked_instances_count; + } + } + int result = 0; + if (update_with_iid) { + for (size_t i = 0; i < update_with_iid_count; ++i) { + anjay_advanced_fw_update_set_state_and_result( + fw_table[update_with_iid[i]].anjay, + fw_table[update_with_iid[i]].iid, + ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + assert(fw_table[update_with_iid[i]].check_yourself); + if (fw_table[update_with_iid[i]].check_yourself( + &fw_table[update_with_iid[i]])) { + result = -1; + } + } + } + assert(fw->check_yourself); + if (fw->check_yourself(fw)) { + result = -1; + } + if (result) { + return result; + } + + if (update_with_iid) { + for (size_t i = 0; i < update_with_iid_count; ++i) { + assert(fw_table[update_with_iid[i]].update_yourself); + if ((result = fw_table[update_with_iid[i]].update_yourself( + &fw_table[update_with_iid[i]]))) { + return result; + } + } + } + assert(fw->update_yourself); + return fw->update_yourself(fw); +} + +int fw_update_common_maybe_create_firmware_file( + advanced_fw_update_logic_t *fw) { + if (!fw->next_target_path) { + if (fw->administratively_set_target_path) { + fw->next_target_path = + avs_strdup(fw->administratively_set_target_path); + } else { + fw->next_target_path = generate_random_target_filepath(); + } + if (!fw->next_target_path) { + return -1; + } + demo_log(INFO, "Created %s", fw->next_target_path); + } + return 0; +} + +static void afu_logic_destroy(advanced_fw_update_logic_t *fw) { + assert(fw); + if (fw->stream) { + fclose(fw->stream); + } + if (fw->update_job) { + avs_sched_del(&fw->update_job); + } + avs_free(fw->administratively_set_target_path); + avs_free(fw->next_target_path); +} + +const char *const ADD_IMG_NAMES[] = { + [FW_UPDATE_IID_TEE] = "TEE", + [FW_UPDATE_IID_BOOT] = "Bootloader", + [FW_UPDATE_IID_MODEM] = "Modem" +}; + +int advanced_firmware_update_install( + anjay_t *anjay, + advanced_fw_update_logic_t *fw_table, + const char *persistence_file, + const avs_net_security_info_t *security_info, + const avs_coap_udp_tx_params_t *tx_params, + anjay_advanced_fw_update_result_t delayed_result, + bool prefer_same_socket_downloads, + const char *original_img_file_path +#ifdef ANJAY_WITH_SEND + , + bool use_lwm2m_send +#endif // ANJAY_WITH_SEND +) { + advanced_fw_update_logic_t *fw_logic_app = NULL; + int result = -1; + + anjay_advanced_fw_update_global_config_t config = { +#ifdef ANJAY_WITH_SEND + .use_lwm2m_send = use_lwm2m_send, +#endif // ANJAY_WITH_SEND + .prefer_same_socket_downloads = prefer_same_socket_downloads + }; + result = anjay_advanced_fw_update_install(anjay, &config); + if (!result && !original_img_file_path) { + demo_log( + INFO, + "Advanced Firmware Update init not finished. Lack of original " + "image path, which is a path to file used to compare with file " + "obtained from server during update."); + /* Already installed object (by anjay_advanced_fw_update_install()) + * stays in demo and is not destroyed because some integration tests + * (other than AFU) needs accordance between objects in demo and objects + * defined in test_utils.py */ + return 0; + } + + advanced_firmware_update_persistence_file_data_t data = + advanced_firmware_update_read_persistence_file(persistence_file); + + if (!result) { + fw_logic_app = &fw_table[FW_UPDATE_IID_APP]; + fw_logic_app->iid = FW_UPDATE_IID_APP; + + fw_logic_app->anjay = anjay; + fw_logic_app->persistence_file = persistence_file; + memcpy(fw_logic_app->current_ver, VER_DEFAULT, sizeof(VER_DEFAULT)); + + advanced_firmware_update_delete_persistence_file(fw_logic_app); + demo_log( + INFO, + "Initial state of firmware upgrade of instance " + "/" AVS_QUOTE_MACRO( + ANJAY_ADVANCED_FW_UPDATE_OID) "/%u - " + "state: %d, result: %d, ", + (int) fw_logic_app->iid, + (int) data.states_results_paths.inst_states[FW_UPDATE_IID_APP], + (int) data.states_results_paths + .inst_results[FW_UPDATE_IID_APP]); + fw_logic_app->next_target_path = + data.states_results_paths.next_target_paths[FW_UPDATE_IID_APP]; + data.states_results_paths.next_target_paths[FW_UPDATE_IID_APP] = NULL; + anjay_advanced_fw_update_initial_state_t state = { + .state = data.states_results_paths.inst_states[FW_UPDATE_IID_APP], + .result = data.states_results_paths.inst_results[FW_UPDATE_IID_APP], + .persisted_severity = data.severity, + .persisted_last_state_change_time = data.last_state_change_time, + .persisted_update_deadline = data.update_deadline + }; + + if (delayed_result != ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL) { + demo_log(INFO, + "delayed_result == %d; initializing Advanced Firmware " + "Update in UPDATING state", + (int) delayed_result); + state.state = ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING; + state.result = ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL; + + // Simulate FOTA process that finishes after the LwM2M client starts + // by changing the Update Result later at runtime + set_delayed_advanced_fw_update_result_args_t args = { + .anjay = anjay, + .iid = FW_UPDATE_IID_APP, + .delayed_result = delayed_result, + }; + if (args.delayed_result + == ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS) { + args.delayed_state = ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE; + } else if (args.delayed_result + == ANJAY_ADVANCED_FW_UPDATE_RESULT_FAILED) { + args.delayed_state = ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE; + } else { + demo_log(WARNING, "Other configurations should not occur."); + } + if (AVS_SCHED_NOW(anjay_get_scheduler(anjay), NULL, + set_delayed_fw_update_result, &args, + sizeof(args))) { + result = -1; + goto exit; + } + } + + if (state.state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING) { + if (!fw_logic_app->next_target_path + || !(fw_logic_app->stream = + fopen(fw_logic_app->next_target_path, "ab"))) { + if (fw_logic_app->stream) { + fclose(fw_logic_app->stream); + fw_logic_app->stream = NULL; + } + state.state = ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE; + } + } else if (state.state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE) { + // we're initializing in the "Idle" state, so the firmware file is + // not supposed to exist; delete it if we have it for any weird + // reason + maybe_delete_firmware_file(fw_logic_app); + } + result = advanced_firmware_update_application_install( + anjay, fw_table, &state, security_info, tx_params); + if (result) { + demo_log(ERROR, "AFU instance %u install failed", + FW_UPDATE_IID_APP); + result = -1; + } + } + + for (anjay_iid_t i = FW_UPDATE_IID_TEE; i < FW_UPDATE_IID_IMAGE_SLOTS; + ++i) { + if (!result) { + advanced_fw_update_logic_t *fw_logic_add_inst = &fw_table[i]; + fw_logic_add_inst->iid = i; + fw_logic_add_inst->anjay = anjay; + fw_logic_add_inst->original_img_file_path = original_img_file_path; + anjay_advanced_fw_update_initial_state_t state = { + .state = data.states_results_paths.inst_states[i], + .result = data.states_results_paths.inst_results[i] + }; + fw_logic_add_inst->next_target_path = + data.states_results_paths.next_target_paths[i]; + data.states_results_paths.next_target_paths[i] = NULL; + demo_log(INFO, + "Initial state of firmware upgrade of instance " + "/" AVS_QUOTE_MACRO( + ANJAY_ADVANCED_FW_UPDATE_OID) "/%u - " + "state: %d, result: " + "%d, ", + (int) fw_logic_add_inst->iid, (int) state.state, + (int) state.result); + result = advanced_firmware_update_additional_image_install( + anjay, fw_logic_add_inst->iid, fw_table, &state, + ADD_IMG_NAMES[i]); + + if (result) { + demo_log(ERROR, "AFU instance %u install failed", + FW_UPDATE_IID_TEE); + result = -1; + break; + } + } + } + + if (!result) { + demo_log(INFO, "AFU object install success"); + } + +exit: + if (result) { + for (int i = 0; i < FW_UPDATE_IID_IMAGE_SLOTS; ++i) { + afu_logic_destroy(&fw_table[i]); + // If next_target_paths were read properly but some of + // image_install() failed, there still could be need to free + // allocated memory + avs_free(data.states_results_paths.next_target_paths[i]); + } + } + return result; +} + +void advanced_firmware_update_set_package_path( + advanced_fw_update_logic_t *fw_logic, const char *path) { + if (fw_logic->stream) { + demo_log(ERROR, + "cannot set package path while a download is in progress"); + return; + } + char *new_target_path = avs_strdup(path); + if (!new_target_path) { + demo_log(ERROR, "out of memory"); + return; + } + + avs_free(fw_logic->administratively_set_target_path); + fw_logic->administratively_set_target_path = new_target_path; + demo_log(INFO, "firmware package path set to %s", + fw_logic->administratively_set_target_path); +} + +void advanced_firmware_update_uninstall(advanced_fw_update_logic_t *fw_table) { + for (int i = 0; i < FW_UPDATE_IID_IMAGE_SLOTS; ++i) { + afu_logic_destroy(&fw_table[i]); + } +} diff --git a/demo/advanced_firmware_update.h b/demo/advanced_firmware_update.h new file mode 100644 index 000000000..872225ffc --- /dev/null +++ b/demo/advanced_firmware_update.h @@ -0,0 +1,165 @@ +/* + * Copyright 2017-2023 AVSystem + * AVSystem Anjay LwM2M SDK + * All rights reserved. + * + * Licensed under the AVSystem-5-clause License. + * See the attached LICENSE file for details. + */ + +#ifndef ADVANCED_FIRMWARE_UPDATE_H +#define ADVANCED_FIRMWARE_UPDATE_H + +#include +#include + +#include "demo_utils.h" +#include +#include + +#define IMG_VER_STR_MAX_LEN (sizeof("255.255.65535.4294967295") - 1) +#define VER_DEFAULT "1.0" + +#define FW_UPDATE_IID_APP 0 +#define FW_UPDATE_IID_TEE 1 +#define FW_UPDATE_IID_BOOT 2 +#define FW_UPDATE_IID_MODEM 3 +#define FW_UPDATE_IID_IMAGE_SLOTS 4 +#define METADATA_LINKED_SLOTS 8 + +#define FORCE_ERROR_OUT_OF_MEMORY 1 +#define FORCE_ERROR_FAILED_UPDATE 2 +#define FORCE_DELAYED_SUCCESS 3 +#define FORCE_DELAYED_ERROR_FAILED_UPDATE 4 +#define FORCE_SET_SUCCESS_FROM_PERFORM_UPGRADE 5 +#define FORCE_SET_FAILURE_FROM_PERFORM_UPGRADE 6 +#define FORCE_DO_NOTHING 7 +#define FORCE_DEFER 8 + +enum target_image_t { + TARGET_IMAGE_TYPE_APPLICATION = 0, + TARGET_IMAGE_TYPE_ADDITIONAL_IMAGE, +}; + +typedef struct advanced_firmware_metadata { + uint8_t magic[8]; + uint16_t version; + uint16_t force_error_case; + uint32_t crc; + uint8_t linked[METADATA_LINKED_SLOTS]; + uint8_t pkg_ver_len; + uint8_t pkg_ver[IMG_VER_STR_MAX_LEN + 1]; +} advanced_fw_metadata_t; + +typedef struct unpacked_imgs_info { + char *path; + advanced_fw_metadata_t meta; +} unpacked_imgs_info_t; + +typedef struct advanced_firmware_multipkg_metadata { + uint8_t magic[8]; + uint16_t version; + uint16_t packages_count; + uint32_t package_len[FW_UPDATE_IID_IMAGE_SLOTS]; +} advanced_fw_multipkg_metadata_t; + +struct advanced_fw_update_logic { + anjay_iid_t iid; + const char *original_img_file_path; + char current_ver[IMG_VER_STR_MAX_LEN + 1]; + anjay_t *anjay; + advanced_fw_metadata_t metadata; + char *administratively_set_target_path; + char *next_target_path; + const char *persistence_file; + FILE *stream; + avs_net_security_info_t security_info; + int (*check_yourself)(struct advanced_fw_update_logic *); + int (*update_yourself)(struct advanced_fw_update_logic *); + avs_sched_handle_t update_job; +}; +typedef struct advanced_fw_update_logic advanced_fw_update_logic_t; + +int advanced_firmware_update_application_install( + anjay_t *anjay, + advanced_fw_update_logic_t *fw_logic, + anjay_advanced_fw_update_initial_state_t *init_state, + const avs_net_security_info_t *security_info, + const avs_coap_udp_tx_params_t *tx_params); +int advanced_firmware_update_app_perform(advanced_fw_update_logic_t *fw); +const char *advanced_firmware_update_app_get_pkg_version(anjay_iid_t iid, + void *fw_); +const char *advanced_firmware_update_app_get_current_version(anjay_iid_t iid, + void *fw_); + +int advanced_firmware_update_additional_image_install( + anjay_t *anjay, + anjay_iid_t iid, + advanced_fw_update_logic_t *fw_table, + anjay_advanced_fw_update_initial_state_t *init_state, + const char *component_name); + +const char * +advanced_firmware_update_additional_image_get_pkg_version(anjay_iid_t iid, + void *fw_); +const char * +advanced_firmware_update_additional_image_get_current_version(anjay_iid_t iid, + void *fw_); + +int advanced_firmware_update_install( + anjay_t *anjay, + advanced_fw_update_logic_t *fw_table, + const char *persistence_file, + const avs_net_security_info_t *security_info, + const avs_coap_udp_tx_params_t *tx_params, + anjay_advanced_fw_update_result_t delayed_result, + bool prefer_same_socket_downloads, + const char *original_img_file_path +#ifdef ANJAY_WITH_SEND + , + bool use_lwm2m_send +#endif // ANJAY_WITH_SEND +); + +void advanced_firmware_update_uninstall(advanced_fw_update_logic_t *fw_table); +int fw_update_common_open(anjay_iid_t iid, void *fw_); +int fw_update_common_write(anjay_iid_t iid, + void *user_ptr, + const void *data, + size_t length); +const char *fw_update_common_get_current_version(anjay_iid_t iid, void *fw_); +const char *fw_update_common_get_pkg_version(anjay_iid_t iid, void *fw_); +int fw_update_common_finish(anjay_iid_t iid, void *fw_); +void fw_update_common_reset(anjay_iid_t iid, void *fw_); +int fw_update_common_perform_upgrade( + anjay_iid_t iid, + void *fw_, + const anjay_iid_t *requested_supplemental_iids, + size_t requested_supplemental_iids_count); +int fw_update_common_maybe_create_firmware_file(advanced_fw_update_logic_t *fw); + +typedef struct { + anjay_advanced_fw_update_state_t inst_states[FW_UPDATE_IID_IMAGE_SLOTS]; + anjay_advanced_fw_update_result_t inst_results[FW_UPDATE_IID_IMAGE_SLOTS]; + char *next_target_paths[FW_UPDATE_IID_IMAGE_SLOTS]; +} states_results_paths_t; + +int advanced_firmware_update_read_states_results_paths( + advanced_fw_update_logic_t *fw_table, + states_results_paths_t *out_states_results_paths); + +int advanced_firmware_update_write_persistence_file( + const char *path, + states_results_paths_t *states_results_paths, + anjay_advanced_fw_update_severity_t severity, + avs_time_real_t last_state_change_time, + avs_time_real_t update_deadline, + const char *current_ver); + +void advanced_firmware_update_delete_persistence_file( + const advanced_fw_update_logic_t *fw); + +void advanced_firmware_update_set_package_path( + advanced_fw_update_logic_t *fw_logic, const char *path); + +#endif // ADVANCED_FIRMWARE_UPDATE_H diff --git a/demo/advanced_firmware_update_addimg.c b/demo/advanced_firmware_update_addimg.c new file mode 100644 index 000000000..1959df7d8 --- /dev/null +++ b/demo/advanced_firmware_update_addimg.c @@ -0,0 +1,126 @@ +/* + * Copyright 2017-2023 AVSystem + * AVSystem Anjay LwM2M SDK + * All rights reserved. + * + * Licensed under the AVSystem-5-clause License. + * See the attached LICENSE file for details. + */ + +#include "advanced_firmware_update.h" +#include "demo_utils.h" + +#include + +static advanced_fw_update_logic_t *fw_global; + +static int fw_stream_open(anjay_iid_t iid, void *fw_) { + (void) iid; + + return fw_update_common_open(iid, fw_); +} + +static int compare_files(FILE *s1, FILE *s2) { + while (!feof(s1)) { + char buf_1[1024]; + char buf_2[1024]; + size_t bytes_read_1 = fread(buf_1, 1, sizeof(buf_1), s1); + size_t bytes_read_2 = fread(buf_2, 1, sizeof(buf_2), s2); + if (bytes_read_1 != bytes_read_2) { + return -1; + } + if (memcmp(buf_1, buf_2, bytes_read_1)) { + return -1; + } + } + return 0; +} + +static int compare_images(const char *file_path_1, const char *file_path_2) { + int result = -1; + FILE *stream1 = fopen(file_path_1, "r"); + FILE *stream2 = NULL; + + if (!stream1) { + demo_log(ERROR, "could not open file: %s", file_path_1); + goto cleanup; + } + + stream2 = fopen(file_path_2, "r"); + if (!stream2) { + demo_log(ERROR, "could not open file: %s", file_path_2); + goto cleanup; + } + + result = compare_files(stream1, stream2); +cleanup: + if (stream1) { + fclose(stream1); + } + if (stream2) { + fclose(stream2); + } + return result; +} + +static int prepare_and_validate_update(advanced_fw_update_logic_t *fw) { + demo_log(INFO, + "Checking image of " AVS_QUOTE_MACRO( + ANJAY_ADVANCED_FW_UPDATE_OID) "/%u instance", + fw->iid); + if (!compare_images(fw->original_img_file_path, fw->next_target_path)) { + demo_log(INFO, "Image check success"); + return 0; + } + demo_log(ERROR, "Image check failure"); + anjay_advanced_fw_update_set_state_and_result( + fw->anjay, fw->iid, ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED, + ANJAY_ADVANCED_FW_UPDATE_RESULT_FAILED); + return -1; +} + +static int update(advanced_fw_update_logic_t *fw) { + demo_log(INFO, "*** FIRMWARE UPDATE: %s ***", fw->next_target_path); + demo_log(INFO, + "Update success for " AVS_QUOTE_MACRO( + ANJAY_ADVANCED_FW_UPDATE_OID) "/%u instance", + fw->iid); + memcpy(fw->current_ver, fw->metadata.pkg_ver, fw->metadata.pkg_ver_len); + anjay_advanced_fw_update_set_state_and_result( + fw->anjay, fw->iid, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS); + return 0; +} + +static const anjay_advanced_fw_update_handlers_t handlers = { + .stream_open = fw_stream_open, + .stream_write = fw_update_common_write, + .stream_finish = fw_update_common_finish, + .reset = fw_update_common_reset, + .get_pkg_version = fw_update_common_get_pkg_version, + .get_current_version = fw_update_common_get_current_version, + .perform_upgrade = fw_update_common_perform_upgrade +}; + +int advanced_firmware_update_additional_image_install( + anjay_t *anjay, + anjay_iid_t iid, + advanced_fw_update_logic_t *fw_table, + anjay_advanced_fw_update_initial_state_t *init_state, + const char *component_name) { + advanced_fw_update_logic_t *fw_logic = &fw_table[iid]; + memcpy(fw_logic->current_ver, VER_DEFAULT, sizeof(VER_DEFAULT)); + fw_global = fw_logic; + int result = + anjay_advanced_fw_update_instance_add(anjay, fw_logic->iid, + component_name, &handlers, + fw_table, init_state); + if (!result) { + fw_logic->check_yourself = prepare_and_validate_update; + fw_logic->update_yourself = update; + } + if (result) { + memset(fw_global, 0x00, sizeof(advanced_fw_update_logic_t)); + } + return result; +} diff --git a/demo/advanced_firmware_update_app.c b/demo/advanced_firmware_update_app.c new file mode 100644 index 000000000..0ab9fc78b --- /dev/null +++ b/demo/advanced_firmware_update_app.c @@ -0,0 +1,208 @@ +/* + * Copyright 2017-2023 AVSystem + * AVSystem Anjay LwM2M SDK + * All rights reserved. + * + * Licensed under the AVSystem-5-clause License. + * See the attached LICENSE file for details. + */ + +#include "advanced_firmware_update.h" +#include "demo_utils.h" + +#include +#include + +static advanced_fw_update_logic_t *fw_global; + +static int fw_stream_open(anjay_iid_t iid, void *fw_) { + (void) iid; + return fw_update_common_open(iid, fw_); +} + +struct execute_new_app_args { + advanced_fw_update_logic_t *fw; +}; + +static int prepare_and_validate_update(advanced_fw_update_logic_t *fw) { + demo_log(INFO, + "Checking image of " AVS_QUOTE_MACRO( + ANJAY_ADVANCED_FW_UPDATE_OID) "/%u instance", + fw->iid); + if (fw->metadata.force_error_case) { + demo_log(INFO, "force_error_case present and set to: %d", + (int) fw->metadata.force_error_case); + } + if (fw->metadata.force_error_case == FORCE_ERROR_FAILED_UPDATE) { + demo_log(ERROR, "Image check failure"); + advanced_firmware_update_delete_persistence_file(fw); + anjay_advanced_fw_update_set_state_and_result( + fw->anjay, fw->iid, ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED, + ANJAY_ADVANCED_FW_UPDATE_RESULT_FAILED); + return -1; + } + demo_log(ERROR, "Image check success"); + return 0; +} + +static void execute_new_app(avs_sched_t *sched, const void *args_) { + (void) sched; + const struct execute_new_app_args *args = + (const struct execute_new_app_args *) args_; + demo_log(INFO, "App image going to execv from %s", + args->fw->next_target_path); + execv(args->fw->next_target_path, argv_get()); + demo_log(ERROR, "execv failed (%s)", strerror(errno)); +} + +static int update(advanced_fw_update_logic_t *fw) { + /* This function only works with APP so fw always points to fw_table: */ + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw; + demo_log(INFO, "*** FIRMWARE UPDATE: %s ***", fw->next_target_path); + states_results_paths_t states_results_paths; + if (advanced_firmware_update_read_states_results_paths( + fw_table, &states_results_paths)) { + return -1; + } + states_results_paths.inst_states[FW_UPDATE_IID_APP] = + ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE; + states_results_paths.inst_results[FW_UPDATE_IID_APP] = + ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS; + if (fw->persistence_file + && advanced_firmware_update_write_persistence_file( + fw->persistence_file, &states_results_paths, + anjay_advanced_fw_update_get_severity(fw->anjay, + fw->iid), + anjay_advanced_fw_update_get_last_state_change_time( + fw->anjay, fw->iid), + anjay_advanced_fw_update_get_deadline(fw->anjay, + fw->iid), + fw->current_ver)) { + advanced_firmware_update_delete_persistence_file(fw); + return -1; + } + if (fw->metadata.force_error_case) { + demo_log(INFO, "force_error_case present and set to: %d", + (int) fw->metadata.force_error_case); + } + switch (fw->metadata.force_error_case) { + case FORCE_ERROR_FAILED_UPDATE: + AVS_UNREACHABLE("Update process should fail earlier"); + case FORCE_DELAYED_SUCCESS: + if (argv_append("--delayed-afu-result") || argv_append("1")) { + demo_log(ERROR, "could not append delayed result to argv"); + return -1; + } + break; + case FORCE_DELAYED_ERROR_FAILED_UPDATE: + if (argv_append("--delayed-afu-result") || argv_append("8")) { + demo_log(ERROR, "could not append delayed result to argv"); + return -1; + } + break; + case FORCE_SET_SUCCESS_FROM_PERFORM_UPGRADE: + if (anjay_advanced_fw_update_set_state_and_result( + fw->anjay, fw->iid, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS)) { + demo_log(ERROR, + "anjay_advanced_fw_update_set_state_and_result failed"); + return -1; + } + return 0; + case FORCE_SET_FAILURE_FROM_PERFORM_UPGRADE: + if (anjay_advanced_fw_update_set_state_and_result( + fw->anjay, fw->iid, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + ANJAY_ADVANCED_FW_UPDATE_RESULT_FAILED)) { + demo_log(ERROR, "anjay_fw_update_set_result failed"); + return -1; + } + return 0; + case FORCE_DO_NOTHING: + return 0; + case FORCE_DEFER: + return ANJAY_ADVANCED_FW_UPDATE_ERR_DEFERRED; + default: + break; + } + struct execute_new_app_args args = { + .fw = fw, + }; + if (AVS_SCHED_NOW(anjay_get_scheduler(fw->anjay), &fw->update_job, + execute_new_app, &args, sizeof(args))) { + demo_log(WARNING, "Could not schedule the upgrade job"); + return ANJAY_ERR_INTERNAL; + } + return 0; +} + +static avs_coap_udp_tx_params_t advanced_fw_get_coap_tx_params_wrapper( + anjay_iid_t iid, void *user_ptr, const char *download_uri) { + (void) iid; + advanced_fw_update_logic_t *fw_table = + (advanced_fw_update_logic_t *) user_ptr; + advanced_fw_update_logic_t *fw = &fw_table[FW_UPDATE_IID_APP]; + return fw_get_coap_tx_params(fw, download_uri); +} + +static anjay_advanced_fw_update_handlers_t handlers = { + .stream_open = fw_stream_open, + .stream_write = fw_update_common_write, + .stream_finish = fw_update_common_finish, + .reset = fw_update_common_reset, + .get_pkg_version = fw_update_common_get_pkg_version, + .get_current_version = fw_update_common_get_current_version, + .perform_upgrade = fw_update_common_perform_upgrade, + .get_coap_tx_params = advanced_fw_get_coap_tx_params_wrapper +}; + +static int fw_get_security_config(anjay_iid_t iid, + void *fw_, + anjay_security_config_t *out_security_config, + const char *download_uri) { + (void) iid; + advanced_fw_update_logic_t *fw_table = (advanced_fw_update_logic_t *) fw_; + advanced_fw_update_logic_t *fw = &fw_table[FW_UPDATE_IID_APP]; + (void) download_uri; + memset(out_security_config, 0, sizeof(*out_security_config)); + out_security_config->security_info = fw->security_info; + return 0; +} + +int advanced_firmware_update_application_install( + anjay_t *anjay, + advanced_fw_update_logic_t *fw_table, + anjay_advanced_fw_update_initial_state_t *init_state, + const avs_net_security_info_t *security_info, + const avs_coap_udp_tx_params_t *tx_params) { + advanced_fw_update_logic_t *fw_logic = &fw_table[FW_UPDATE_IID_APP]; + + if (security_info) { + memcpy(&fw_logic->security_info, security_info, + sizeof(fw_logic->security_info)); + handlers.get_security_config = fw_get_security_config; + } else { + handlers.get_security_config = NULL; + } + + if (tx_params) { + fw_set_coap_tx_params(tx_params); + } else { + handlers.get_coap_tx_params = NULL; + } + + fw_global = fw_logic; + int result = anjay_advanced_fw_update_instance_add(anjay, + fw_logic->iid, + "application", + &handlers, + fw_table, + init_state); + if (!result) { + fw_logic->check_yourself = prepare_and_validate_update; + fw_logic->update_yourself = update; + } + if (result) { + memset(fw_global, 0x00, sizeof(advanced_fw_update_logic_t)); + } + return result; +} diff --git a/demo/demo.c b/demo/demo.c index 2435adda8..c4cf7b7fe 100644 --- a/demo/demo.c +++ b/demo/demo.c @@ -25,6 +25,10 @@ # include "firmware_update.h" #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE +# include "advanced_firmware_update.h" +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + #include "objects.h" #include @@ -237,6 +241,9 @@ static void demo_delete(anjay_demo_t *demo) { #ifdef ANJAY_WITH_MODULE_FW_UPDATE firmware_update_destroy(&demo->fw_update); #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + advanced_firmware_update_uninstall(demo->advanced_fw_update_logic_table); +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE AVS_LIST_CLEAR(&demo->allocated_strings); avs_free(demo); @@ -474,6 +481,9 @@ static int demo_init(anjay_demo_t *demo, cmdline_args_t *cmdline_args) { .prefer_hierarchical_formats = cmdline_args->prefer_hierarchical_formats, .use_connection_id = cmdline_args->use_connection_id, + .update_immediately_on_dm_change = + cmdline_args->update_immediately_on_dm_change, + .enable_self_notify = cmdline_args->enable_self_notify, .default_tls_ciphersuites = { .ids = cmdline_args->default_ciphersuites, .num_ids = cmdline_args->default_ciphersuites_count @@ -482,6 +492,9 @@ static int demo_init(anjay_demo_t *demo, cmdline_args_t *cmdline_args) { .lwm2m_version_config = &cmdline_args->lwm2m_version_config, .rebuild_client_cert_chain = cmdline_args->rebuild_client_cert_chain, #endif // ANJAY_WITH_LWM2M11 +#if defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) + .coap_tcp_request_timeout = cmdline_args->tcp_request_timeout, +#endif // defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) }; #ifdef ANJAY_WITH_LWM2M11 @@ -506,6 +519,15 @@ static int demo_init(anjay_demo_t *demo, cmdline_args_t *cmdline_args) { } #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + const avs_net_security_info_t *advanced_fw_security_info_ptr = NULL; + if (cmdline_args->advanced_fw_security_info.mode + != (avs_net_security_mode_t) -1) { + advanced_fw_security_info_ptr = + &cmdline_args->advanced_fw_security_info; + } +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + demo->connection_args = &cmdline_args->connection_args; #ifdef AVS_COMMONS_STREAM_WITH_FILE # ifdef ANJAY_WITH_ATTR_STORAGE @@ -653,6 +675,29 @@ static int demo_init(anjay_demo_t *demo, cmdline_args_t *cmdline_args) { } #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + // Install Advanced Firmware Update Object at the end, because installed + // Device Object and Server Object's instances may be needed. + if (advanced_firmware_update_install( + demo->anjay, + demo->advanced_fw_update_logic_table, + cmdline_args->advanced_fw_updated_marker_path, + advanced_fw_security_info_ptr, + cmdline_args->advanced_fwu_tx_params_modified + ? &cmdline_args->advanced_fwu_tx_params + : NULL, + cmdline_args->advanced_fw_update_delayed_result, + cmdline_args->prefer_same_socket_downloads, + cmdline_args->original_img_file_path +# ifdef ANJAY_WITH_SEND + , + cmdline_args->advanced_fw_update_use_send +# endif // ANJAY_WITH_SEND + )) { + return -1; + } +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + #ifdef ANJAY_WITH_MODULE_ACCESS_CONTROL if (!dm_persistence_restored && (add_default_access_entries(demo) @@ -681,6 +726,12 @@ static int demo_init(anjay_demo_t *demo, cmdline_args_t *cmdline_args) { reschedule_notify_time_dependent(demo); + if (cmdline_args->start_offline + && anjay_transport_enter_offline(demo->anjay, + ANJAY_TRANSPORT_SET_ALL)) { + return -1; + } + return 0; } diff --git a/demo/demo.h b/demo/demo.h index 41ccc5114..7d67193d3 100644 --- a/demo/demo.h +++ b/demo/demo.h @@ -23,6 +23,10 @@ #ifdef ANJAY_WITH_MODULE_FW_UPDATE # include "firmware_update.h" #endif // ANJAY_WITH_MODULE_FW_UPDATE + +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE +# include "advanced_firmware_update.h" +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE #include "objects.h" typedef struct { @@ -62,6 +66,10 @@ struct anjay_demo_struct { #ifdef ANJAY_WITH_MODULE_FW_UPDATE fw_update_logic_t fw_update; #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + advanced_fw_update_logic_t + advanced_fw_update_logic_table[FW_UPDATE_IID_IMAGE_SLOTS]; +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE AVS_LIST(anjay_demo_object_t) objects; diff --git a/demo/demo_args.c b/demo/demo_args.c index 5315a25f8..06a3fffff 100644 --- a/demo/demo_args.c +++ b/demo/demo_args.c @@ -68,6 +68,12 @@ static const cmdline_args_t DEFAULT_CMDLINE_ARGS = { }, #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + .advanced_fw_security_info = { + .mode = (avs_net_security_mode_t) -1 + }, +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + #ifdef AVS_COMMONS_STREAM_WITH_FILE # ifdef ANJAY_WITH_ATTR_STORAGE .attr_storage_file = NULL, @@ -88,6 +94,10 @@ static const cmdline_args_t DEFAULT_CMDLINE_ARGS = { .fwu_tx_params_modified = false, .fwu_tx_params = ANJAY_COAP_DEFAULT_UDP_TX_PARAMS, #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + .advanced_fwu_tx_params_modified = false, + .advanced_fwu_tx_params = ANJAY_COAP_DEFAULT_UDP_TX_PARAMS, +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE #ifdef ANJAY_WITH_LWM2M11 .lwm2m_version_config = { .minimum_version = ANJAY_LWM2M_VERSION_1_0, @@ -95,6 +105,8 @@ static const cmdline_args_t DEFAULT_CMDLINE_ARGS = { }, #endif // ANJAY_WITH_LWM2M11 .prefer_hierarchical_formats = false, + .update_immediately_on_dm_change = false, + .enable_self_notify = false, .prefer_same_socket_downloads = false, }; @@ -430,6 +442,48 @@ static void print_help(const struct option *options) { "Provide key from ASCII string (see -k parameter for more details)" }, { 317, "VERSION", "TLS library default", "Minimum (D)TLS version to use." }, +#if defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) + { 319, "TIMEOUT", "30.0", + "Time in seconds to wait for incoming response after sending a TCP " + "request" }, + { 320, NULL, NULL, + "Send the Update message immediately when Object Instances are " + "created or deleted." }, + { 321, NULL, NULL, + "Send the Notify messages as a result of a server action (e.g. " + "Write) even to the initiating server." }, +#endif // defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + { 322, "ADDITIONAL_IMG_FILE_PATH", NULL, + "Path to additional img binary file. Used to compare with obtained " + "through advanced firmware update procedure" }, +# if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) \ + && defined(AVS_COMMONS_STREAM_WITH_FILE) + { 323, "AFU_PERSISTENCE_FILE", NULL, + "Path to file used to persist advanced firmware update data, " + "if file not exists, it will be created" }, +# endif // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && + // defined(AVS_COMMONS_STREAM_WITH_FILE) + { 324, "CERT_FILE", NULL, + "Require certificate validation against specified file when " + "downloading firmware over encrypted channels. This argument is " + "used by Advanced Firmware Update." }, + { 325, "RESULT", NULL, + "If specified and nonzero, initializes the Advanced Firmware Update " + "object in " + "UPDATING state, and sets the result to given value after a short " + "while" }, +# ifdef ANJAY_WITH_SEND + { 326, NULL, NULL, + "Enables using LwM2M Send to report state and result of advanced " + "firmware update" }, +# endif // ANJAY_WITH_SEND + { 327, "ACK_TIMEOUT", "2.0", + "Configures ACK_TIMEOUT (defined in RFC7252) in seconds for advanced " + "firmware update" }, +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + { 328, NULL, NULL, + "Enter offline mode before starting the event loop." }, }; const size_t screen_width = get_screen_width(); @@ -708,9 +762,9 @@ int demo_parse_argv(cmdline_args_t *parsed_args, int argc, char *argv[]) { #endif // ANJAY_WITH_LWM2M11 #ifdef ANJAY_WITH_MODULE_FW_UPDATE { "delayed-upgrade-result", required_argument, 0, 'r' }, -#if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && defined(AVS_COMMONS_STREAM_WITH_FILE) +# if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && defined(AVS_COMMONS_STREAM_WITH_FILE) { "fw-updated-marker-path", required_argument, 0, 256 }, -#endif // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && defined(AVS_COMMONS_STREAM_WITH_FILE) +# endif // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && defined(AVS_COMMONS_STREAM_WITH_FILE) { "fw-cert-file", required_argument, 0, 257 }, { "fw-cert-path", required_argument, 0, 258 }, { "fw-psk-identity", required_argument, 0, 259 }, @@ -765,6 +819,24 @@ int demo_parse_argv(cmdline_args_t *parsed_args, int argc, char *argv[]) { { "identity-as-string", required_argument, 0, 307 }, { "key-as-string", required_argument, 0, 308 }, { "tls-version", required_argument, 0, 317 }, +#if defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) + { "tcp-request-timeout", required_argument, 0, 319 }, +#endif // defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) + { "update-immediately-on-dm-change", no_argument, 0, 320 }, + { "enable-self-notify", no_argument, 0, 321 }, +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + { "afu-original-img-file-path", required_argument, 0, 322 }, +# if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && defined(AVS_COMMONS_STREAM_WITH_FILE) + { "afu-marker-path", required_argument, 0, 323 }, +# endif // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && defined(AVS_COMMONS_STREAM_WITH_FILE) + { "afu-cert-file", required_argument, 0, 324 }, + { "delayed-afu-result", required_argument, 0, 325 }, +# if defined(ANJAY_WITH_SEND) + { "afu-use-send", no_argument, 0, 326 }, +# endif // defined(ANJAY_WITH_SEND) + { "afu-ack-timeout", required_argument, 0, 327 }, +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + { "start-offline", no_argument, 0, 328 }, { 0, 0, 0, 0 } // clang-format on }; @@ -1423,6 +1495,81 @@ int demo_parse_argv(cmdline_args_t *parsed_args, int argc, char *argv[]) { goto finish; } break; +#if defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) + case 319: { + double tcp_request_timeout; + if (parse_double(optarg, &tcp_request_timeout)) { + demo_log(ERROR, "Expected TCP request timeout to be a floating " + "point number"); + goto finish; + } + parsed_args->tcp_request_timeout = + avs_time_duration_from_fscalar(tcp_request_timeout, + AVS_TIME_S); + break; + } +#endif // defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) + case 320: + parsed_args->update_immediately_on_dm_change = true; + break; + case 321: + parsed_args->enable_self_notify = true; + break; +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + case 322: + parsed_args->original_img_file_path = optarg; + break; +# if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) \ + && defined(AVS_COMMONS_STREAM_WITH_FILE) + case 323: + parsed_args->advanced_fw_updated_marker_path = optarg; + break; +# endif // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && + // defined(AVS_COMMONS_STREAM_WITH_FILE) + case 324: { + + const avs_net_certificate_info_t cert_info = { + .server_cert_validation = true, + .trusted_certs = + avs_crypto_certificate_chain_info_from_file(optarg) + }; + parsed_args->advanced_fw_security_info = + avs_net_security_info_from_certificates(cert_info); + break; + } + case 325: { + int result; + if (parse_i32(optarg, &result) + || result < (int) ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL + || result > (int) ANJAY_ADVANCED_FW_UPDATE_RESULT_UNSUPPORTED_PROTOCOL) { + demo_log(ERROR, "invalid update result value: %s", optarg); + goto finish; + } + parsed_args->advanced_fw_update_delayed_result = + (anjay_advanced_fw_update_result_t) result; + break; + } +# ifdef ANJAY_WITH_SEND + case 326: + parsed_args->advanced_fw_update_use_send = true; + break; +# endif // ANJAY_WITH_SEND + case 327: { + double ack_timeout_s; + if (parse_double(optarg, &ack_timeout_s)) { + demo_log(ERROR, + "Expected ACK_TIMEOUT to be a floating point number"); + goto finish; + } + parsed_args->advanced_fwu_tx_params.ack_timeout = + avs_time_duration_from_fscalar(ack_timeout_s, AVS_TIME_S); + parsed_args->advanced_fwu_tx_params_modified = true; + break; + } +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + case 328: + parsed_args->start_offline = true; + break; case 0: goto process; } diff --git a/demo/demo_args.h b/demo/demo_args.h index 07edc605c..c4e6fc26c 100644 --- a/demo/demo_args.h +++ b/demo/demo_args.h @@ -13,7 +13,14 @@ #include #include #include -#include + +#ifdef ANJAY_WITH_MODULE_FW_UPDATE +# include +#endif // ANJAY_WITH_MODULE_FW_UPDATE + +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE +# include +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE #include "demo_utils.h" #include "objects.h" @@ -51,8 +58,32 @@ typedef struct cmdline_args { * which the client is restarted while upgrade is still in progress. */ anjay_fw_update_result_t fw_update_delayed_result; -#endif // ANJAY_WITH_MODULE_FW_UPDATE - +# ifdef ANJAY_WITH_SEND + bool fw_update_use_send; +# endif // ANJAY_WITH_SEND +#endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + const char *advanced_fw_updated_marker_path; + avs_net_security_info_t advanced_fw_security_info; + /** + * If nonzero (not @ref ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL), + * Advanced Firmware Update object will be initialized in UPDATING state. + * In that case, @ref anjay_advanced_fw_update_set_state_and_result will be + * used after a while to trigger a transition to this update result. + * This simulates a FOTA procedure during which the client is restarted + * while upgrade is still in progress. + */ + anjay_advanced_fw_update_result_t advanced_fw_update_delayed_result; +# ifdef ANJAY_WITH_SEND + bool advanced_fw_update_use_send; +# endif // ANJAY_WITH_SEND + /** + * This is a file path to file with original image. After additional + * image is downloaded, update can be performed. Updating additional + * image (other than main application) includes only files comparison. + */ + const char *original_img_file_path; +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE #ifdef AVS_COMMONS_STREAM_WITH_FILE # ifdef ANJAY_WITH_ATTR_STORAGE const char *attr_storage_file; @@ -81,20 +112,30 @@ typedef struct cmdline_args { bool fwu_tx_params_modified; avs_coap_udp_tx_params_t fwu_tx_params; #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + /** + * This flag allows to enable callback providing tx_params for firmware + * update only if some of parameters were changed by passing proper command + * line argument to demo. Otherwise tx_params should be inherited from + * Anjay. + */ + bool advanced_fwu_tx_params_modified; + avs_coap_udp_tx_params_t advanced_fwu_tx_params; +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE #ifdef ANJAY_WITH_LWM2M11 anjay_lwm2m_version_config_t lwm2m_version_config; #endif // ANJAY_WITH_LWM2M11 size_t stored_notification_limit; bool prefer_hierarchical_formats; + bool update_immediately_on_dm_change; + bool enable_self_notify; bool use_connection_id; + bool start_offline; uint32_t *default_ciphersuites; size_t default_ciphersuites_count; bool prefer_same_socket_downloads; -#ifdef ANJAY_WITH_SEND - bool fw_update_use_send; -#endif // ANJAY_WITH_SEND #ifdef ANJAY_WITH_LWM2M11 const char *pkix_trust_store; bool rebuild_client_cert_chain; @@ -102,6 +143,9 @@ typedef struct cmdline_args { bool alternative_logger; +#if defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) + avs_time_duration_t tcp_request_timeout; +#endif // defined(ANJAY_WITH_LWM2M11) && defined(WITH_AVS_COAP_TCP) } cmdline_args_t; int demo_parse_argv(cmdline_args_t *parsed_args, int argc, char **argv); diff --git a/demo/demo_cmds.c b/demo/demo_cmds.c index a8c670d95..626451a5b 100644 --- a/demo/demo_cmds.c +++ b/demo/demo_cmds.c @@ -14,6 +14,10 @@ # include "firmware_update.h" #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE +# include "advanced_firmware_update.h" +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + #include #include #include @@ -39,6 +43,22 @@ static int parse_ssid(const char *text, anjay_ssid_t *out_ssid) { return 0; } +static void cmd_send_register(anjay_demo_t *demo, const char *args_string) { + anjay_ssid_t ssid = ANJAY_SSID_ANY; + if (*args_string && parse_ssid(args_string, &ssid)) { + demo_log(ERROR, "invalid Short Server ID: %s", args_string); + return; + } + + if (anjay_schedule_register(demo->anjay, ssid)) { + demo_log(ERROR, "could not schedule registration"); + } else if (ssid == ANJAY_SSID_ANY) { + demo_log(INFO, "registration scheduled for all servers"); + } else { + demo_log(INFO, "registration scheduled for server %" PRIu16, ssid); + } +} + static void cmd_send_update(anjay_demo_t *demo, const char *args_string) { anjay_ssid_t ssid = ANJAY_SSID_ANY; if (*args_string && parse_ssid(args_string, &ssid)) { @@ -56,6 +76,19 @@ static void cmd_send_update(anjay_demo_t *demo, const char *args_string) { } } +static void cmd_reconnect_server(anjay_demo_t *demo, const char *args_string) { + anjay_ssid_t ssid = ANJAY_SSID_ANY; + if (*args_string && parse_ssid(args_string, &ssid)) { + demo_log(ERROR, "invalid Short Server ID: %s", args_string); + return; + } + + if (anjay_server_schedule_reconnect(demo->anjay, ssid)) { + demo_log(ERROR, "could not enable server with SSID %" PRIu16, ssid); + return; + } +} + static int parse_transports(const char *text, anjay_transport_set_t *out_transport_set) { char *text_copy = avs_strdup(text); @@ -114,6 +147,49 @@ static void cmd_set_fw_package_path(anjay_demo_t *demo, } #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE +static void cmd_set_afu_package_path(anjay_demo_t *demo, + const char *args_string) { + const char *path = args_string; + while (isspace(*path)) { + ++path; + } + + /* This allows setting package path only for first (APP) image */ + advanced_fw_update_logic_t *fw_logic_app = + demo->advanced_fw_update_logic_table; + assert(fw_logic_app->iid == FW_UPDATE_IID_APP); + advanced_firmware_update_set_package_path(fw_logic_app, path); +} + +static void cmd_get_afu_deadline(anjay_demo_t *demo, const char *args_string) { + (void) args_string; + int64_t update_deadline_timestamp = 0; + avs_time_real_to_scalar(&update_deadline_timestamp, AVS_TIME_S, + anjay_advanced_fw_update_get_deadline( + demo->anjay, FW_UPDATE_IID_APP)); + printf("AFU_APP_UPDATE_DEADLINE==%" PRId64 "\n", update_deadline_timestamp); +} + +static void cmd_set_afu_result(anjay_demo_t *demo, const char *args_string) { + int result; + if (sscanf(args_string, " %d", &result) != 1) { + demo_log(ERROR, "Advanced Firmware Update result not specified"); + return; + } + if (anjay_advanced_fw_update_set_state_and_result( + demo->anjay, + FW_UPDATE_IID_APP, + (anjay_advanced_fw_update_state_t) + ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + (anjay_advanced_fw_update_result_t) result)) { + demo_log(ERROR, + "Advanced Firmware Update result set for APP image at runtime " + "failed."); + } +} +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + static void cmd_open_location_csv(anjay_demo_t *demo, const char *args_string) { const anjay_dm_object_def_t **location_obj = demo_find_object(demo, DEMO_OID_LOCATION); @@ -1277,8 +1353,12 @@ struct cmd_handler_def { { (name), sizeof(name) - 1, (func), (args), (help) } static const struct cmd_handler_def COMMAND_HANDLERS[] = { // clang-format off + CMD_HANDLER("send-register", "[ssid=0]", + cmd_send_register, "Sends Register messages to LwM2M servers"), CMD_HANDLER("send-update", "[ssid=0]", cmd_send_update, "Sends Update messages to LwM2M servers"), + CMD_HANDLER("reconnect-server", "ssid", cmd_reconnect_server, + "Reconnects a server with given SSID"), CMD_HANDLER("reconnect", "[transports...]", cmd_reconnect, "Reconnects to LwM2M servers and sends Update messages"), #ifdef ANJAY_WITH_MODULE_FW_UPDATE @@ -1286,6 +1366,17 @@ static const struct cmd_handler_def COMMAND_HANDLERS[] = { "Sets the path where the firmware package will be saved when " "Write /5/0/0 is performed"), #endif // ANJAY_WITH_MODULE_FW_UPDATE +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + CMD_HANDLER("set-afu-package-path", "", cmd_set_afu_package_path, + "Sets the path where the firmware package will be saved when " + "Write /" AVS_QUOTE_MACRO(ANJAY_ADVANCED_FW_UPDATE_OID) "/0/0 is performed. Only applied to instance 0."), + CMD_HANDLER("get-afu-deadline", "", cmd_get_afu_deadline, + "Gets the Advanced Firmware Update deadline (only for main APP " + "image)"), + CMD_HANDLER("set-afu-result", "RESULT", cmd_set_afu_result, + "Attempts to set Advanced Firmware Update Result of instance " + "/" AVS_QUOTE_MACRO(ANJAY_ADVANCED_FW_UPDATE_OID) "/0 (APP) at runtime"), +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE CMD_HANDLER("open-location-csv", "filename frequency=1", cmd_open_location_csv, "Opens a CSV file and starts using it for location information"), @@ -1525,18 +1616,27 @@ static void handle_command(avs_sched_t *sched, const void *invocation_) { const demo_command_invocation_t *invocation = (const demo_command_invocation_t *) invocation_; if (invocation->cmd[0]) { - demo_log(INFO, "command: %s", invocation->cmd); + const struct cmd_handler_def *cmd = NULL; for (size_t idx = 0; idx < AVS_ARRAY_SIZE(COMMAND_HANDLERS); ++idx) { - const struct cmd_handler_def *cmd = &COMMAND_HANDLERS[idx]; + const struct cmd_handler_def *candidate_cmd = + &COMMAND_HANDLERS[idx]; - if (strncmp(invocation->cmd, cmd->cmd_name, cmd->cmd_name_length) + if (strncmp(invocation->cmd, candidate_cmd->cmd_name, + candidate_cmd->cmd_name_length) == 0) { - cmd->handler(invocation->demo, - invocation->cmd + cmd->cmd_name_length); + cmd = candidate_cmd; break; } } + + if (cmd) { + demo_log(INFO, "command: %s", invocation->cmd); + cmd->handler(invocation->demo, + invocation->cmd + cmd->cmd_name_length); + } else { + demo_log(ERROR, "unrecognized command: %s", invocation->cmd); + } } fprintf(stdout, "(DEMO)>"); diff --git a/demo/demo_utils.c b/demo/demo_utils.c index da182ec72..834e97865 100644 --- a/demo/demo_utils.c +++ b/demo/demo_utils.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -141,10 +142,150 @@ int fetch_bytes(anjay_input_ctx_t *ctx, void **buffer, size_t *out_size) { return result; } -int open_temporary_file(char *path) { +static int open_temporary_file(char *path) { mode_t old_umask = (mode_t) umask(S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH); int fd = mkstemp(path); umask(old_umask); return fd; } + +char *generate_random_target_filepath(void) { + char *result = NULL; + if (!(result = avs_strdup("/tmp/anjay-fw-XXXXXX"))) { + return NULL; + } + + int fd = open_temporary_file(result); + if (fd == -1) { + demo_log(ERROR, "could not generate firmware filename: %s", + strerror(errno)); + avs_free(result); + return NULL; + } + close(fd); + return result; +} + +int copy_file_contents(FILE *dst, FILE *src) { + while (!feof(src)) { + char buf[4096]; + + size_t bytes_read = fread(buf, 1, sizeof(buf), src); + if (bytes_read == 0 && ferror(src)) { + return -1; + } + + if (fwrite(buf, 1, bytes_read, dst) != bytes_read) { + return -1; + } + } + return 0; +} + +// CRC32 code adapted from http://home.thep.lu.se/~bjorn/crc/ +static uint32_t crc32_for_byte(uint8_t value) { + uint32_t result = value; + for (int i = 0; i < 8; ++i) { + if (result & 1) { + result >>= 1; + } else { + result = (result >> 1) ^ (uint32_t) 0xEDB88320UL; + } + } + return result ^ (uint32_t) 0xFF000000UL; +} + +static void crc32(uint32_t *inout_crc, const uint8_t *data, size_t size) { + static uint32_t LOOKUP_TABLE[256]; + if (!*LOOKUP_TABLE) { + for (size_t i = 0; i < AVS_ARRAY_SIZE(LOOKUP_TABLE); ++i) { + LOOKUP_TABLE[i] = crc32_for_byte((uint8_t) i); + } + } + + for (size_t i = 0; i < size; ++i) { + *inout_crc = LOOKUP_TABLE[data[i] ^ (uint8_t) *inout_crc] + ^ (*inout_crc >> 8); + } +} + +int calc_file_crc32(const char *filename, uint32_t *out_crc) { + FILE *f = fopen(filename, "rb"); + if (!f) { + demo_log(ERROR, "could not open %s", filename); + return -1; + } + + *out_crc = 0; + unsigned char buf[4096]; + int result = -1; + + while (!feof(f)) { + size_t bytes_read = fread(buf, 1, sizeof(buf), f); + if (bytes_read == 0 && ferror(f)) { + demo_log(ERROR, "could not read from %s: %s", filename, + strerror(errno)); + goto cleanup; + } + + crc32(out_crc, buf, bytes_read); + } + + result = 0; + +cleanup: + fclose(f); + return result; +} + +#if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) \ + && defined(AVS_COMMONS_STREAM_WITH_FILE) +avs_error_t store_etag(avs_persistence_context_t *ctx, + const anjay_etag_t *etag) { + bool use_etag = (etag != NULL); + avs_error_t err; + + (void) (avs_is_err((err = avs_persistence_bool(ctx, &use_etag))) || !etag + || avs_is_err((err = avs_persistence_u8( + ctx, (uint8_t *) (intptr_t) &etag->size))) + || avs_is_err((err = avs_persistence_bytes( + ctx, (uint8_t *) (intptr_t) etag->value, + etag->size)))); + return err; +} + +avs_error_t restore_etag(avs_persistence_context_t *ctx, anjay_etag_t **etag) { + assert(etag && !*etag); + bool use_etag; + avs_error_t err; + uint8_t size8; + if (avs_is_err((err = avs_persistence_bool(ctx, &use_etag))) || !use_etag + || avs_is_err((err = avs_persistence_u8(ctx, &size8))) + || avs_is_err((err = ((*etag = anjay_etag_new(size8)) + ? avs_errno(AVS_NO_ERROR) + : avs_errno(AVS_ENOMEM))))) { + return err; + } + + if (avs_is_err(err = avs_persistence_bytes(ctx, (*etag)->value, size8))) { + avs_free(*etag); + *etag = NULL; + } + return err; +} +#endif // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && + // defined(AVS_COMMONS_STREAM_WITH_FILE) + +avs_coap_udp_tx_params_t g_tx_params; + +void fw_set_coap_tx_params(const avs_coap_udp_tx_params_t *tx_params) { + g_tx_params = *tx_params; +} + +avs_coap_udp_tx_params_t fw_get_coap_tx_params(void *user_ptr, + const char *download_uri) { + (void) user_ptr; + (void) download_uri; + return g_tx_params; +} \ No newline at end of file diff --git a/demo/demo_utils.h b/demo/demo_utils.h index 8bc657bea..6ae7210a8 100644 --- a/demo/demo_utils.h +++ b/demo/demo_utils.h @@ -65,6 +65,23 @@ int demo_parse_long(const char *str, long *out_value); int fetch_bytes(anjay_input_ctx_t *ctx, void **buffer, size_t *out_size); -int open_temporary_file(char *path); +char *generate_random_target_filepath(void); + +int copy_file_contents(FILE *dst, FILE *src); + +int calc_file_crc32(const char *filename, uint32_t *out_crc); + +#if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) \ + && defined(AVS_COMMONS_STREAM_WITH_FILE) +avs_error_t store_etag(avs_persistence_context_t *ctx, + const anjay_etag_t *etag); +avs_error_t restore_etag(avs_persistence_context_t *ctx, anjay_etag_t **etag); +#endif // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && + // defined(AVS_COMMONS_STREAM_WITH_FILE) + +void fw_set_coap_tx_params(const avs_coap_udp_tx_params_t *tx_params); + +avs_coap_udp_tx_params_t fw_get_coap_tx_params(void *user_ptr, + const char *download_uri); #endif // DEMO_UTILS_H diff --git a/demo/firmware_update.c b/demo/firmware_update.c index ac4669bdd..f40266098 100644 --- a/demo/firmware_update.c +++ b/demo/firmware_update.c @@ -34,23 +34,6 @@ #define FORCE_SET_FAILURE_FROM_PERFORM_UPGRADE 6 #define FORCE_DO_NOTHING 7 -static char *generate_random_target_filepath(void) { - char *result = NULL; - if (!(result = avs_strdup("/tmp/anjay-fw-XXXXXX"))) { - return NULL; - } - - int fd = open_temporary_file(result); - if (fd == -1) { - demo_log(ERROR, "could not generate firmware filename: %s", - strerror(errno)); - avs_free(result); - return NULL; - } - close(fd); - return result; -} - static int maybe_create_firmware_file(fw_update_logic_t *fw) { if (!fw->next_target_path) { if (fw->administratively_set_target_path) { @@ -77,8 +60,11 @@ static void maybe_delete_firmware_file(fw_update_logic_t *fw) { } void firmware_update_set_package_path(fw_update_logic_t *fw, const char *path) { - AVS_ASSERT(!fw->stream, - "cannot set package path while a download is in progress"); + if (fw->stream) { + demo_log(ERROR, + "cannot set package path while a download is in progress"); + return; + } char *new_target_path = avs_strdup(path); if (!new_target_path) { demo_log(ERROR, "out of memory"); @@ -114,23 +100,6 @@ static int read_fw_meta_from_file(FILE *f, fw_metadata_t *out_metadata) { return 0; } -static int copy_file_contents(FILE *dst, FILE *src) { - while (!feof(src)) { - char buf[4096]; - - size_t bytes_read = fread(buf, 1, sizeof(buf), src); - if (bytes_read == 0 && ferror(src)) { - return -1; - } - - if (fwrite(buf, 1, bytes_read, dst) != bytes_read) { - return -1; - } - } - - return 0; -} - static int unpack_fw_to_file(const char *fw_pkg_path, const char *target_path, fw_metadata_t *out_metadata) { @@ -215,63 +184,6 @@ static bool fw_magic_valid(const fw_metadata_t *meta) { return true; } -// CRC32 code adapted from http://home.thep.lu.se/~bjorn/crc/ - -static uint32_t crc32_for_byte(uint8_t value) { - uint32_t result = value; - for (int i = 0; i < 8; ++i) { - if (result & 1) { - result >>= 1; - } else { - result = (result >> 1) ^ (uint32_t) 0xEDB88320UL; - } - } - return result ^ (uint32_t) 0xFF000000UL; -} - -static void crc32(uint32_t *inout_crc, const uint8_t *data, size_t size) { - static uint32_t LOOKUP_TABLE[256]; - if (!*LOOKUP_TABLE) { - for (size_t i = 0; i < AVS_ARRAY_SIZE(LOOKUP_TABLE); ++i) { - LOOKUP_TABLE[i] = crc32_for_byte((uint8_t) i); - } - } - - for (size_t i = 0; i < size; ++i) { - *inout_crc = LOOKUP_TABLE[data[i] ^ (uint8_t) *inout_crc] - ^ (*inout_crc >> 8); - } -} - -static int get_file_crc32(const char *filename, uint32_t *out_crc) { - FILE *f = fopen(filename, "rb"); - if (!f) { - demo_log(ERROR, "could not open %s", filename); - return -1; - } - - *out_crc = 0; - unsigned char buf[4096]; - int result = -1; - - while (!feof(f)) { - size_t bytes_read = fread(buf, 1, sizeof(buf), f); - if (bytes_read == 0 && ferror(f)) { - demo_log(ERROR, "could not read from %s: %s", filename, - strerror(errno)); - goto cleanup; - } - - crc32(out_crc, buf, bytes_read); - } - - result = 0; - -cleanup: - fclose(f); - return result; -} - static bool fw_version_supported(const fw_metadata_t *meta) { if (meta->version != 1) { demo_log(ERROR, "unsupported firmware version: %u", meta->version); @@ -288,7 +200,7 @@ static int validate_firmware(fw_update_logic_t *fw) { } uint32_t actual_crc; - int result = get_file_crc32(fw->next_target_path, &actual_crc); + int result = calc_file_crc32(fw->next_target_path, &actual_crc); if (result) { demo_log(WARNING, "unable to check firmware CRC"); @@ -325,19 +237,6 @@ static int preprocess_firmware(fw_update_logic_t *fw) { #if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) \ && defined(AVS_COMMONS_STREAM_WITH_FILE) -static avs_error_t store_etag(avs_persistence_context_t *ctx, - const anjay_etag_t *etag) { - bool use_etag = (etag != NULL); - avs_error_t err; - - (void) (avs_is_err((err = avs_persistence_bool(ctx, &use_etag))) || !etag - || avs_is_err((err = avs_persistence_u8( - ctx, (uint8_t *) (intptr_t) &etag->size))) - || avs_is_err((err = avs_persistence_bytes( - ctx, (uint8_t *) (intptr_t) etag->value, - etag->size)))); - return err; -} static int write_persistence_file(const char *path, anjay_fw_update_initial_result_t result, @@ -413,8 +312,6 @@ static void fw_reset(void *fw_) { static int fw_stream_open(void *fw_, const char *package_uri, const struct anjay_etag *package_etag) { - (void) package_uri; - (void) package_etag; fw_update_logic_t *fw = (fw_update_logic_t *) fw_; assert(!fw->stream); @@ -565,15 +462,6 @@ static int fw_get_security_config(void *fw_, return 0; } -static avs_coap_udp_tx_params_t g_tx_params; - -static avs_coap_udp_tx_params_t -fw_get_coap_tx_params(void *user_ptr, const char *download_uri) { - (void) user_ptr; - (void) download_uri; - return g_tx_params; -} - static anjay_fw_update_handlers_t FW_UPDATE_HANDLERS = { .stream_open = fw_stream_open, .stream_write = fw_stream_write, @@ -585,31 +473,6 @@ static anjay_fw_update_handlers_t FW_UPDATE_HANDLERS = { .get_coap_tx_params = fw_get_coap_tx_params }; -#if defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) \ - && defined(AVS_COMMONS_STREAM_WITH_FILE) -static avs_error_t restore_etag(avs_persistence_context_t *ctx, - anjay_etag_t **etag) { - assert(etag && !*etag); - bool use_etag; - avs_error_t err; - uint8_t size8; - if (avs_is_err((err = avs_persistence_bool(ctx, &use_etag))) || !use_etag - || avs_is_err((err = avs_persistence_u8(ctx, &size8))) - || avs_is_err((err = ((*etag = anjay_etag_new(size8)) - ? avs_errno(AVS_NO_ERROR) - : avs_errno(AVS_ENOMEM))))) { - return err; - } - - if (avs_is_err(err = avs_persistence_bytes(ctx, (*etag)->value, size8))) { - avs_free(*etag); - *etag = NULL; - } - return err; -} -#endif // defined(AVS_COMMONS_WITH_AVS_PERSISTENCE) && - // defined(AVS_COMMONS_STREAM_WITH_FILE) - static bool is_valid_result(int8_t result) { switch (result) { case ANJAY_FW_UPDATE_INITIAL_DOWNLOADED: @@ -714,7 +577,7 @@ int firmware_update_install(anjay_t *anjay, } if (tx_params) { - g_tx_params = *tx_params; + fw_set_coap_tx_params(tx_params); } else { FW_UPDATE_HANDLERS.get_coap_tx_params = NULL; } @@ -791,6 +654,7 @@ int firmware_update_install(anjay_t *anjay, } void firmware_update_destroy(fw_update_logic_t *fw_update) { + assert(fw_update); if (fw_update->stream) { fclose(fw_update->stream); } diff --git a/deps/avs_coap/include_public/avsystem/coap/observe.h b/deps/avs_coap/include_public/avsystem/coap/observe.h index ce7dc7653..6551bf486 100644 --- a/deps/avs_coap/include_public/avsystem/coap/observe.h +++ b/deps/avs_coap/include_public/avsystem/coap/observe.h @@ -194,6 +194,38 @@ avs_coap_observe_start(avs_coap_ctx_t *ctx, avs_coap_observe_cancel_handler_t *cancel_handler, void *handler_arg); +/** + * Cancels an observation previously started using @ref avs_coap_observe_start . + * + * This function is called automatically by avs_coap when an observe + * cancellation request is received over the network, or when an attempt to + * deliver a confirmable notification times out. This function shall be called + * by the user only when an observation shall be cancelled manually. + * + * IMPORTANT: The remote endpoint is not notified of the + * cancellation in any way. Cancelling observations programmatically is likely + * to violate the RFC 7641 requirements, unless observe state is synchronized + * through a higher-layer protocol. + * + * NOTE: If notification exchanges started using + * @ref avs_coap_notify_async or @ref avs_coap_notify_streaming are still in + * progress, they will NOT be cancelled automatically. + * + * NOTE: This function will call the cancel_handler + * originally passed to @ref avs_coap_observe_start . + * + * @param ctx CoAP context to operate on. + * + * @param id Unique observation ID (request token). + * + * @returns + * - AVS_OK for success + * - avs_errno(AVS_EINVAL) if an invalid @p ctx has been passed, or + * @p id does not refer to any know observation + */ +avs_error_t avs_coap_observe_cancel(avs_coap_ctx_t *ctx, + avs_coap_observe_id_t id); + /** * Sends a CoAP Notification in an asynchronous mode. This function returns * immediately. diff --git a/deps/avs_coap/src/async/avs_coap_async_server.c b/deps/avs_coap/src/async/avs_coap_async_server.c index d930987d3..e20ed5838 100644 --- a/deps/avs_coap/src/async/avs_coap_async_server.c +++ b/deps/avs_coap/src/async/avs_coap_async_server.c @@ -245,7 +245,7 @@ static void cancel_notification_on_error(avs_coap_ctx_t *ctx, AVS_COAP_CODE_STRING(response_code)); #ifdef WITH_AVS_COAP_OBSERVE - _avs_coap_observe_cancel(ctx, &observe_id); + avs_coap_observe_cancel(ctx, observe_id); #endif // WITH_AVS_COAP_OBSERVE (void) ctx; (void) observe_id; @@ -303,6 +303,17 @@ send_result_handler(avs_coap_ctx_t *ctx, }, exchange->code); } +#ifdef WITH_AVS_COAP_OBSERVE + else if (fail_err.category == AVS_COAP_ERR_CATEGORY + && fail_err.code == AVS_COAP_ERR_TIMEOUT) { + // According to RFC 7641, when trying to send a Confirmable notification + // ends in a timeout, the client "is considered no longer interested in + // the resource and is removed by the server from the list of observers" + avs_coap_observe_cancel(ctx, (avs_coap_observe_id_t) { + .token = exchange->token + }); + } +#endif // WITH_AVS_COAP_OBSERVE // exchange may have been canceled by user handler if (!_avs_coap_find_server_exchange_ptr_by_id(ctx, exchange_id)) { @@ -392,9 +403,11 @@ avs_time_monotonic_t _avs_coap_async_server_abort_timedout_exchanges(avs_coap_ctx_t *ctx) { avs_coap_base_t *coap_base = _avs_coap_get_base(ctx); while (coap_base->server_exchanges - && avs_time_monotonic_before(coap_base->server_exchanges->by_type - .server.exchange_deadline, - avs_time_monotonic_now())) { + && avs_time_monotonic_valid(coap_base->server_exchanges->by_type + .server.exchange_deadline) + && !avs_time_monotonic_before(avs_time_monotonic_now(), + coap_base->server_exchanges->by_type + .server.exchange_deadline)) { LOG(DEBUG, _("exchange ") "%s" _(" timed out"), AVS_UINT64_AS_STRING(coap_base->server_exchanges->id.value)); @@ -719,7 +732,7 @@ handle_observe_option(avs_coap_ctx_t *ctx, const avs_coap_observe_id_t observe_id = { .token = request->token }; - _avs_coap_observe_cancel(ctx, &observe_id); + avs_coap_observe_cancel(ctx, observe_id); } return observe_value; } diff --git a/deps/avs_coap/src/avs_coap_ctx.c b/deps/avs_coap/src/avs_coap_ctx.c index 4581eee86..47ce25a1f 100644 --- a/deps/avs_coap/src/avs_coap_ctx.c +++ b/deps/avs_coap/src/avs_coap_ctx.c @@ -59,7 +59,7 @@ void avs_coap_ctx_cleanup(avs_coap_ctx_t **ctx) { } #ifdef WITH_AVS_COAP_OBSERVE while (coap_base->observes) { - _avs_coap_observe_cancel(*ctx, &coap_base->observes->id); + avs_coap_observe_cancel(*ctx, coap_base->observes->id); } #endif // WITH_AVS_COAP_OBSERVE #ifdef WITH_AVS_COAP_STREAMING_API @@ -226,9 +226,11 @@ static void retry_or_request_expired_job(avs_sched_t *sched, void _avs_coap_reschedule_retry_or_request_expired_job( avs_coap_ctx_t *ctx, avs_time_monotonic_t target_time) { avs_coap_base_t *coap_base = _avs_coap_get_base(ctx); - if (avs_time_monotonic_before( - avs_sched_time(&coap_base->retry_or_request_expired_job), - target_time)) { + if (coap_base->retry_or_request_expired_job + && !avs_time_monotonic_before( + target_time, + avs_sched_time( + &coap_base->retry_or_request_expired_job))) { return; } diff --git a/deps/avs_coap/src/avs_coap_observe.c b/deps/avs_coap/src/avs_coap_observe.c index 41c8b6fe0..a042ead0a 100644 --- a/deps/avs_coap/src/avs_coap_observe.c +++ b/deps/avs_coap/src/avs_coap_observe.c @@ -97,7 +97,7 @@ avs_coap_observe_start(avs_coap_ctx_t *ctx, } // make sure to *replace* existing observation with same ID if one exists - _avs_coap_observe_cancel(ctx, &id); + avs_coap_observe_cancel(ctx, id); LOG(DEBUG, _("Observe start: ") "%s", AVS_COAP_TOKEN_HEX(&id.token)); @@ -132,22 +132,26 @@ _avs_coap_observe_setup_notify(avs_coap_ctx_t *ctx, return AVS_OK; } -void _avs_coap_observe_cancel(avs_coap_ctx_t *ctx, - const avs_coap_observe_id_t *id) { - AVS_LIST(avs_coap_observe_t) *observe_ptr = find_observe_ptr_by_id(ctx, id); +avs_error_t avs_coap_observe_cancel(avs_coap_ctx_t *ctx, + avs_coap_observe_id_t id) { + AVS_LIST(avs_coap_observe_t) *observe_ptr = NULL; + if (ctx) { + observe_ptr = find_observe_ptr_by_id(ctx, &id); + } if (!observe_ptr) { LOG(TRACE, _("observation ") "%s" _(" does not exist"), - AVS_COAP_TOKEN_HEX(&id->token)); - return; + AVS_COAP_TOKEN_HEX(&id.token)); + return avs_errno(AVS_EINVAL); } - LOG(DEBUG, _("Observe cancel: ") "%s", AVS_COAP_TOKEN_HEX(&id->token)); + LOG(DEBUG, _("Observe cancel: ") "%s", AVS_COAP_TOKEN_HEX(&id.token)); avs_coap_observe_t *observe = AVS_LIST_DETACH(observe_ptr); if (observe->cancel_handler) { - observe->cancel_handler(*id, observe->cancel_handler_arg); + observe->cancel_handler(id, observe->cancel_handler_arg); } AVS_LIST_DELETE(&observe); + return AVS_OK; } # ifdef WITH_AVS_COAP_OBSERVE_PERSISTENCE diff --git a/deps/avs_coap/src/avs_coap_observe.h b/deps/avs_coap/src/avs_coap_observe.h index 1961e3fd3..86e7fb245 100644 --- a/deps/avs_coap/src/avs_coap_observe.h +++ b/deps/avs_coap/src/avs_coap_observe.h @@ -54,9 +54,6 @@ _avs_coap_observe_setup_notify(avs_coap_ctx_t *ctx, const avs_coap_observe_id_t *id, avs_coap_observe_notify_t *out_notify); -void _avs_coap_observe_cancel(avs_coap_ctx_t *ctx, - const avs_coap_observe_id_t *id); - VISIBILITY_PRIVATE_HEADER_END #endif // AVS_COAP_SRC_OBSERVE_H diff --git a/deps/avs_coap/src/streaming/avs_coap_streaming_server.c b/deps/avs_coap/src/streaming/avs_coap_streaming_server.c index 1c98210a9..9db85c554 100644 --- a/deps/avs_coap/src/streaming/avs_coap_streaming_server.c +++ b/deps/avs_coap/src/streaming/avs_coap_streaming_server.c @@ -294,6 +294,7 @@ static int request_handler(avs_coap_request_ctx_t *request_ctx, return 0; case AVS_COAP_SERVER_REQUEST_RECEIVED: { +# ifdef WITH_AVS_COAP_BLOCK avs_coap_option_block_t req_block2; switch (avs_coap_options_get_block(&request->header.options, AVS_COAP_BLOCK2, &req_block2)) { @@ -309,6 +310,7 @@ static int request_handler(avs_coap_request_ctx_t *request_ctx, AVS_UNREACHABLE("malformed options got through packet validation"); return AVS_COAP_CODE_INTERNAL_SERVER_ERROR; } +# endif // WITH_AVS_COAP_BLOCK streaming_req_ctx->server_ctx.state = AVS_COAP_STREAMING_SERVER_RECEIVED_LAST_REQUEST_CHUNK; // This will be continued in ensure_data_is_available_to_read() diff --git a/deps/avs_coap/src/tcp/avs_coap_tcp_ctx.c b/deps/avs_coap/src/tcp/avs_coap_tcp_ctx.c index f4513266c..b9f268a9b 100644 --- a/deps/avs_coap/src/tcp/avs_coap_tcp_ctx.c +++ b/deps/avs_coap/src/tcp/avs_coap_tcp_ctx.c @@ -139,15 +139,6 @@ static avs_error_t send_simple_msg(avs_coap_tcp_ctx_t *ctx, return _avs_coap_tcp_send_msg(ctx, &msg); } -static void send_release(avs_coap_tcp_ctx_t *ctx) { - avs_coap_borrowed_msg_t msg = { - .code = AVS_COAP_CODE_RELEASE - }; - - (void) _avs_coap_ctx_generate_token(ctx->base.prng_ctx, &msg.token); - (void) _avs_coap_tcp_send_msg(ctx, &msg); -} - static avs_error_t handle_cached_msg(avs_coap_tcp_ctx_t *ctx, avs_coap_borrowed_msg_t *out_request) { avs_coap_tcp_cached_msg_t *msg = &ctx->cached_msg; @@ -266,11 +257,8 @@ static inline void send_abort(avs_coap_tcp_ctx_t *ctx) { static void coap_tcp_cleanup(avs_coap_ctx_t *ctx_) { avs_coap_tcp_ctx_t *ctx = (avs_coap_tcp_ctx_t *) ctx_; - if (!ctx->aborted && ctx->base.socket) { - (void) send_release(ctx); - } // TODO T2262 - // Wait for completion of pending requests after sending release message. + // Send the Release message and wait for completion of pending requests. // "The peer responding to the Release message SHOULD delay the closing of // the connection until it has responded to all requests received by it // before the Release message." diff --git a/deps/avs_coap/src/tcp/avs_coap_tcp_pending_requests.c b/deps/avs_coap/src/tcp/avs_coap_tcp_pending_requests.c index c72259f89..1455c166f 100644 --- a/deps/avs_coap/src/tcp/avs_coap_tcp_pending_requests.c +++ b/deps/avs_coap/src/tcp/avs_coap_tcp_pending_requests.c @@ -52,8 +52,8 @@ is_list_ordered_by_expire_time(AVS_LIST(avs_coap_tcp_pending_request_t) list) { AVS_LIST(avs_coap_tcp_pending_request_t) next = list; AVS_LIST_ADVANCE(&next); if (next - && avs_time_monotonic_before(next->expire_time, - list->expire_time)) { + && !avs_time_monotonic_before(list->expire_time, + next->expire_time)) { return false; } } @@ -139,8 +139,9 @@ find_pending_request_ptr_by_token( avs_time_monotonic_t _avs_coap_tcp_fail_expired_pending_requests(avs_coap_tcp_ctx_t *ctx) { while (ctx->pending_requests - && avs_time_monotonic_before(ctx->pending_requests->expire_time, - avs_time_monotonic_now())) { + && avs_time_monotonic_valid(ctx->pending_requests->expire_time) + && !avs_time_monotonic_before(avs_time_monotonic_now(), + ctx->pending_requests->expire_time)) { finish_pending_request_with_error(ctx, &ctx->pending_requests, AVS_COAP_SEND_RESULT_FAIL, _avs_coap_err(AVS_COAP_ERR_TIMEOUT)); diff --git a/deps/avs_coap/src/udp/avs_coap_udp_ctx.c b/deps/avs_coap/src/udp/avs_coap_udp_ctx.c index c0db27fea..f5b199292 100644 --- a/deps/avs_coap/src/udp/avs_coap_udp_ctx.c +++ b/deps/avs_coap/src/udp/avs_coap_udp_ctx.c @@ -1353,7 +1353,7 @@ static avs_error_t handle_empty(avs_coap_udp_ctx_t *ctx, avs_coap_observe_id_t observe_id = { .token = *observe_token }; - _avs_coap_observe_cancel((avs_coap_ctx_t *) ctx, &observe_id); + avs_coap_observe_cancel((avs_coap_ctx_t *) ctx, observe_id); } # endif // WITH_AVS_COAP_OBSERVE diff --git a/deps/avs_coap/src/udp/avs_coap_udp_msg_cache.c b/deps/avs_coap/src/udp/avs_coap_udp_msg_cache.c index ce1aa245e..4e56e0400 100644 --- a/deps/avs_coap/src/udp/avs_coap_udp_msg_cache.c +++ b/deps/avs_coap/src/udp/avs_coap_udp_msg_cache.c @@ -210,7 +210,7 @@ static bool entry_valid(const avs_coap_udp_response_cache_t *cache, static bool entry_expired(const cache_entry_t *entry, const avs_time_monotonic_t *now) { - return avs_time_monotonic_before(entry->expiration_time, *now); + return !avs_time_monotonic_before(*now, entry->expiration_time); } /* returns total size of avs_coap_udp_msg_t, including length field diff --git a/deps/avs_coap/tests/mock_clock.c b/deps/avs_coap/tests/mock_clock.c index 2297b5402..77bcfd170 100644 --- a/deps/avs_coap/tests/mock_clock.c +++ b/deps/avs_coap/tests/mock_clock.c @@ -56,8 +56,6 @@ int clock_gettime(clockid_t clock, struct timespec *t) { // all clocks are equivalent for our purposes, so ignore clock t->tv_sec = (time_t) MOCK_CLOCK.since_monotonic_epoch.seconds; t->tv_nsec = MOCK_CLOCK.since_monotonic_epoch.nanoseconds; - MOCK_CLOCK = avs_time_monotonic_add( - MOCK_CLOCK, avs_time_duration_from_scalar(1, AVS_TIME_NS)); return 0; } else { return orig_clock_gettime(clock, t); diff --git a/deps/avs_coap/tests/tcp/env.h b/deps/avs_coap/tests/tcp/env.h index 1fbb2072a..dbb13864a 100644 --- a/deps/avs_coap/tests/tcp/env.h +++ b/deps/avs_coap/tests/tcp/env.h @@ -175,10 +175,6 @@ static inline test_env_t test_setup(void) { } static inline void test_teardown_impl(test_env_t *env) { - if (!env->aborted && env->mocksock) { - expect_send(env, COAP_MSG(RELEASE, TOKEN(current_token()))); - } - avs_coap_ctx_cleanup(&env->coap_ctx); if (env->mocksock) { avs_unit_mocksock_assert_expects_met(env->mocksock); diff --git a/deps/avs_commons b/deps/avs_commons index a4a25b6fa..a227958d8 160000 --- a/deps/avs_commons +++ b/deps/avs_commons @@ -1 +1 @@ -Subproject commit a4a25b6faf56f5359fb73193caca214f55f8bc76 +Subproject commit a227958d88cd9fc689a90d2c336ee0eaa1daf17f diff --git a/devconfig b/devconfig index 17b5cea6a..aa3d77c88 100755 --- a/devconfig +++ b/devconfig @@ -144,12 +144,14 @@ ${CMAKE_COMMAND} \ -D WITH_DOC_CHECK=ON \ -D WITH_URL_CHECK=ON \ -D WITH_STATIC_ANALYSIS=${WITH_STATIC_ANALYSIS} \ + -D WITH_MODULE_advanced_fw_update=ON \ -D DTLS_BACKEND="${DTLS_BACKEND}" \ -D AVS_LOG_WITH_TRACE=ON \ -D WITH_EXAMPLES=${WITH_EXAMPLES} \ -D CMAKE_C_FLAGS="${C_FLAGS} ${EXTRA_C_FLAGS}" \ -D CMAKE_CXX_FLAGS="${EXTRA_C_FLAGS}" \ -D ANJAY_VERSION="${ANJAY_VERSION}" \ + -D NPROC="$((3 * $(nproc)))" \ "${EXTRA_FLAGS[@]}" \ -H"$(dirname "$0")" -B. && make clean diff --git a/doc/sphinx/snippet_sources.md5 b/doc/sphinx/snippet_sources.md5 index 42a25496b..257e44bfc 100644 --- a/doc/sphinx/snippet_sources.md5 +++ b/doc/sphinx/snippet_sources.md5 @@ -1,4 +1,4 @@ -c2fdb37415d974d06387a40f7e87ed2d demo/demo_cmds.c +0afe7586298eaf52bf210557fe88a296 demo/demo_cmds.c cd5ba3adb34a01fdd2ceceb6a35d5be2 demo/objects/ipso_objects.c 90e172b4b00a21b7290510a21498e0b8 deps/avs_coap/include_public/avsystem/coap/tcp.h 4670a0997896b4ab2b973fb833c4a039 deps/avs_coap/include_public/avsystem/coap/udp.h @@ -7,7 +7,7 @@ b02b7236e89497c73496ab2e5503ed14 deps/avs_commons/include_public/avsystem/commo 045968e7cbf1891b418ed417b6b8d189 deps/avs_commons/include_public/avsystem/commons/avs_crypto_common.h 477cd70c144b9fba0f1be35f0c049f76 deps/avs_commons/include_public/avsystem/commons/avs_crypto_pki.h 75bb517f8386fd9c77e5982835eef537 deps/avs_commons/include_public/avsystem/commons/avs_crypto_psk.h -4844d08ca5c6546cf5752ee6f170847e deps/avs_commons/include_public/avsystem/commons/avs_socket.h +77120c9f2f2b754019b81cb8cfbd6646 deps/avs_commons/include_public/avsystem/commons/avs_socket.h 0691a3c8a2dbb7f8f22c74e09db381ce deps/avs_commons/include_public/avsystem/commons/avs_socket_v_table.h 1dc558d1f1b72af3da021cf9c1fca4f4 deps/avs_commons/include_public/avsystem/commons/avs_utils.h 6805fe6ea07bc0eabd90a52923abfc77 deps/avs_commons/src/net/avs_net_global.h @@ -35,18 +35,18 @@ d591e56ad78070b63f95e26cf3b84d2e examples/custom-network/minimal/src/net_impl.c 41aa341e9670d5b9e2ff07ed9bba7fff examples/custom-network/remote-host-port/src/net_impl.c c9fdbce9762414d5c4cd16460e8f568d examples/custom-network/shutdown-remote-hostname/src/net_impl.c 7516e0fd17e6b4de6eabb11dbc3b5c30 examples/custom-network/stats/src/net_impl.c -ee76a2b801b61d1dbbee2b07ef6877b6 examples/custom-tls/certificates-advanced-fake-dane/src/tls_impl.c -49cd520a0c24f6578ca9d181ba348cd3 examples/custom-tls/certificates-advanced/src/tls_impl.c +353eba4fd231e00cbf92b397b7a3a4fd examples/custom-tls/certificates-advanced-fake-dane/src/tls_impl.c +81394ffa9095e75222166a69ae6b39ba examples/custom-tls/certificates-advanced/src/tls_impl.c 82659bac1068896e5599908deeefbb4c examples/custom-tls/certificates-basic/src/main.c -4c4f94a03c682d535a2f70836bfa864a examples/custom-tls/certificates-basic/src/tls_impl.c -65bbefed7e5865daf405b7bd2a5f800c examples/custom-tls/config-features/src/tls_impl.c -3feb3c37bd3935a436440028c977a75b examples/custom-tls/minimal/src/tls_impl.c -57c687160f7c3e0910d5514c3f9f03e6 examples/custom-tls/resumption-buffer/src/tls_impl.c -44678235054e66f398302bc49c4afd72 examples/custom-tls/resumption-simple/src/tls_impl.c +ae76c7a143ba7da04165c7a1faaa2d56 examples/custom-tls/certificates-basic/src/tls_impl.c +bd818261132b20b7c5443c0b92be12ad examples/custom-tls/config-features/src/tls_impl.c +1907bca291e2788db48ec5a4deca84ed examples/custom-tls/minimal/src/tls_impl.c +28895a3432adbc23bcc5bba324672c84 examples/custom-tls/resumption-buffer/src/tls_impl.c +65d02858ff1c7cdb7eb041dbeed9e484 examples/custom-tls/resumption-simple/src/tls_impl.c 2115ce268f6142ba683987b7e45cf499 examples/custom-tls/stub/CMakeLists.txt b3a3d73e038d33e93e141e6edbbe801b examples/custom-tls/stub/src/tls_impl.c d983a72f9198ce620476cb1151a02ba0 examples/custom-tls/tcp-support/src/firmware_update.c -50d1bfe0ba1aafa503caad8964a855af examples/custom-tls/tcp-support/src/tls_impl.c +5b8470d2256e4eb1e60b06137cb2ea63 examples/custom-tls/tcp-support/src/tls_impl.c f2cabad50ff2b6a78b2fbb2eb992a720 examples/tutorial/AT-AccessControl/src/main.c 3a790986d9469133b8a00ab3c26006df examples/tutorial/AT-Certificates/src/main.c 12dee646ecc725624f6a45e78c94c3a5 examples/tutorial/AT-CustomEventLoop/src/main.c @@ -76,18 +76,21 @@ b9e1ed0eeaa7bf74af92278ce6e37fc5 examples/tutorial/BC-Send/src/time_object.h 0a07f4dddcbc6ff212a758846d78c3f3 examples/tutorial/BC-ThreadSafety/CMakeLists.txt 015f7222280dbe28e92a68d311991f16 examples/tutorial/BC-ThreadSafety/src/main.c 854fac3a121f2602b24f9912c9e217f8 examples/tutorial/BC-ThreadSafety/src/time_object.c +55313e5351782caf5bcb91190f07c974 examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c +3250ba9efc66031e101d2213e170f30a examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.h f971df7872a8f052bb72ae64610a8031 examples/tutorial/firmware-update/basic-implementation/src/firmware_update.c 8e79fe9e76c36fef28476e89140ec43b examples/tutorial/firmware-update/basic-implementation/src/firmware_update.h 1d85bb08f511fcc7b321f0be56ddb119 examples/tutorial/firmware-update/basic-implementation/src/main.c 844cd7d9d0ebe80007dc5ae8d6a719b0 examples/tutorial/firmware-update/download-resumption/src/firmware_update.c 077f6b89dad59ef3b9b58e2abe93aff6 examples/tutorial/firmware-update/secure-downloads/src/firmware_update.c +10310e97d55c12989ee56511cf0788c6 include_public/anjay/advanced_fw_update.h af2f0e0ac0de25dc04713f05a378d469 include_public/anjay/attr_storage.h -40556bc52ee8a6b3080840a40d200a88 include_public/anjay/core.h -01253f147521b96a08d08ff533b7d789 include_public/anjay/dm.h -bd04d42f0c35a3fdd16173a3003af577 include_public/anjay/fw_update.h -c14cc4edd6db240a0a6c5d93170a0010 include_public/anjay/io.h +9ffba29d019480c84e3ace840c6b51c7 include_public/anjay/core.h +0534e6f5f96ff707378f59e8236f0bad include_public/anjay/dm.h +f9fabfaff536cb7bbb4b2b10c549797a include_public/anjay/fw_update.h +7ef9abee6d8d8e689ddf467cce116347 include_public/anjay/io.h 33c77736e874a89e7dec6a4654eab3c9 include_public/anjay/ipso_objects.h e449359975f3fb404a50078630be399b include_public/anjay/security.h -af0053a405513ad8b9ba77de3adba98a tests/integration/framework/test_utils.py +f5479c311353f8f7f441494cbc97774f tests/integration/framework/test_utils.py 78596279ba96afc13d9cac52ebe8c707 tools/provisioning-tool/factory_prov/factory_prov.py bec6f7f8f8a559fa52ff0a6a224a01e7 tools/provisioning-tool/ptool.py diff --git a/doc/sphinx/source/AdvancedTopics/AT-AttributeStorage.rst b/doc/sphinx/source/AdvancedTopics/AT-AttributeStorage.rst index 59d72ce15..5465c0dbb 100644 --- a/doc/sphinx/source/AdvancedTopics/AT-AttributeStorage.rst +++ b/doc/sphinx/source/AdvancedTopics/AT-AttributeStorage.rst @@ -11,9 +11,9 @@ Attribute storage .. highlight:: c -The Write Attributes and Discover RPCs, as well as the Information Reporting -interface, use a concept of Attributes that may be set for Resources, Object -Instances or Objects. +The Write Attributes and Discover operations, as well as the Information +Reporting interface, use a concept of Attributes that may be set for Resources, +Object Instances or Objects. .. seealso:: :doc:`/LwM2M` chapter contains more information about LwM2M Attributes. diff --git a/doc/sphinx/source/AdvancedTopics/AT-CustomObjects/AT_CO_MultipleResourceInstances.rst b/doc/sphinx/source/AdvancedTopics/AT-CustomObjects/AT_CO_MultipleResourceInstances.rst index b8027c0bb..b229d237a 100644 --- a/doc/sphinx/source/AdvancedTopics/AT-CustomObjects/AT_CO_MultipleResourceInstances.rst +++ b/doc/sphinx/source/AdvancedTopics/AT-CustomObjects/AT_CO_MultipleResourceInstances.rst @@ -154,8 +154,8 @@ Note that the resource instances MUST be returned in a strictly ascending, sorted order. We will keep the resource instances in sorted order, so this implementation satisfies this contract. -Handling Multiple Instance Resources in Read RPC ------------------------------------------------- +Handling Multiple Instance Resources in Read operation +------------------------------------------------------ ``resource_read`` handler is being called by Anjay for each Resource Instance referenced by the server, giving the control to the user. Thus, the read handler @@ -225,8 +225,8 @@ as it only needs to clear the resource: } -Handling Multiple Instance Resources in Write RPC -------------------------------------------------- +Handling Multiple Instance Resources in Write operation +------------------------------------------------------- Now we are ready to actually implement the write operation. We will create a helper function for actually updating the Resource Instance list with a newly diff --git a/doc/sphinx/source/AdvancedTopics/AT-CustomObjects/AT_CO_SingleInstanceReadOnly.rst b/doc/sphinx/source/AdvancedTopics/AT-CustomObjects/AT_CO_SingleInstanceReadOnly.rst index 97b686114..477732751 100644 --- a/doc/sphinx/source/AdvancedTopics/AT-CustomObjects/AT_CO_SingleInstanceReadOnly.rst +++ b/doc/sphinx/source/AdvancedTopics/AT-CustomObjects/AT_CO_SingleInstanceReadOnly.rst @@ -86,8 +86,8 @@ and present in a given Object Instance. A LwM2M Client may be able to handle a Resource that has no default value - in that case, the Resource is always *supported*, but becomes *present* only after a LwM2M Server sets its value first. Before that, it can be treated as non-existent - it will not be reported -via the Discover RPC, for example. Examples include Default Minimum Period and -Default Maximum Period Resources of the LwM2M Server object. +via the Discover operation, for example. Examples include Default Minimum Period +and Default Maximum Period Resources of the LwM2M Server object. In our case, Resources 0 and 1 are always present in the only Instance we have, so we can implement the ``test_resource_preset`` handler simply as: diff --git a/doc/sphinx/source/AdvancedTopics/AT-NetworkErrorHandling.rst b/doc/sphinx/source/AdvancedTopics/AT-NetworkErrorHandling.rst index c1e967423..dfd54ec3c 100644 --- a/doc/sphinx/source/AdvancedTopics/AT-NetworkErrorHandling.rst +++ b/doc/sphinx/source/AdvancedTopics/AT-NetworkErrorHandling.rst @@ -13,23 +13,22 @@ Like any software that needs to communicate with other hosts over the network, Anjay needs to be prepared to handle communication errors. This page documents the library's behavior during various error conditions. -Outgoing RPC error handling table ---------------------------------- +Outgoing request error handling table +------------------------------------- The following table describes the behavior of Anjay when various error -conditions happen while performing each of the client-initiated RPC methods. +conditions happen while performing each of the client-initiated operations. +-----------------+------------------+------------------+------------------+-------------+-------------------+ | | Request | Register | Update | De-register | Notify | | | Bootstrap | | | | (confirmable) | +=================+==================+==================+==================+=============+===================+ -| **Timeout | Retry DTLS | Retry DTLS | Fall back | Ignored | Ignored; will be | -| (DTLS)** [#t]_ | handshake [#hs]_ | handshake [#hs]_ | to Register | | retried whenever | -+-----------------+------------------+------------------+ | | next notification | -| **Timeout | Abort all | :ref:`Abort | | | (either | -| (NoSec)** [#t]_ | communication | registration | | | confirmable or | -| | [#a]_ | ` | | | not) is scheduled | -+-----------------+ | +------------------+ +-------------------+ +| **Timeout | Retry DTLS | Retry DTLS | Fall back | Ignored | Cancel | +| (DTLS)** [#t]_ | handshake [#hs]_ | handshake [#hs]_ | to Register | | observation | ++-----------------+------------------+------------------+ | | | +| **Timeout | Abort all | :ref:`Abort | | | | +| (NoSec)** [#t]_ | communication | registration | | | | ++-----------------+ [#a]_ | ` +------------------+ +-------------------+ | **Network | | | Fall back to | | Fall back to | | (e.g. ICMP) | | | Client-Initiated | | Client-Initiated | | error** | | | Bootstrap [#bs]_ | | Bootstrap [#bs]_ | @@ -114,9 +113,9 @@ Other error conditions `anjay_configuration_t <../api/structanjay__configuration.html>`_. .. [#hs] To prevent infinite loop of handshakes, DTLS handshake is only retried - if the failed RPC was **not** performed immediately after the previous - handshake; otherwise the behavior described in "Timeout (NoSec)" is - used. + if the failed operation was **not** performed immediately after the + previous handshake; otherwise the behavior described in "Timeout + (NoSec)" is used. .. [#a] Communication with all servers will be aborted and `anjay_all_connections_failed() diff --git a/doc/sphinx/source/CommercialFeatures/CF-CorePersistence.rst b/doc/sphinx/source/CommercialFeatures/CF-CorePersistence.rst index efde86eef..d96ecd689 100644 --- a/doc/sphinx/source/CommercialFeatures/CF-CorePersistence.rst +++ b/doc/sphinx/source/CommercialFeatures/CF-CorePersistence.rst @@ -324,3 +324,18 @@ To see how this feature affects data sent during the connection, we encourage to not only analyze application's log output, but use some packet analyzer software like Wireshark. +.. important:: + + Because Core Persistence feature maintains some data that depends on real + time (time at which disabled servers should be reenabled, registration expiry + time), this feature requires a properly implemented real clock + (``avs_time_real_now()``, + :ref:`see porting article `) to work correctly. + Also, make sure to synchronize it first (by using e.g. RTC or NTP protocol) + before starting the client, otherwise disabled servers will be not reenabled + as expected and the registration will be incorrectly deemed to be up to date. + Alternatively, if it's not possible to synchronize the clock before, make + sure to manually enable those disabled servers back by using + ``anjay_enable_server()`` and schedule a registration update using + ``anjay_schedule_registration_update()``. + diff --git a/doc/sphinx/source/FirmwareUpdateTutorial.rst b/doc/sphinx/source/FirmwareUpdateTutorial.rst index 05928b2f4..b2608389f 100644 --- a/doc/sphinx/source/FirmwareUpdateTutorial.rst +++ b/doc/sphinx/source/FirmwareUpdateTutorial.rst @@ -19,3 +19,4 @@ Firmware Update Tutorial FirmwareUpdateTutorial/FU-SecureDownloads FirmwareUpdateTutorial/FU-PoorConnectivity FirmwareUpdateTutorial/FU-DownloadResumption + FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate.rst b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate.rst new file mode 100644 index 000000000..ff14bbc46 --- /dev/null +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate.rst @@ -0,0 +1,35 @@ +.. + Copyright 2017-2023 AVSystem + AVSystem Anjay LwM2M SDK + All rights reserved. + + Licensed under the AVSystem-5-clause License. + See the attached LICENSE file for details. + +Advanced Firmware Update +======================== + +**Advanced Firmware Update** object (``/33629``) is an optional object which +extends the definition of **Firmware Update** object (``/5``) and allows for +multiple instances, with each instance representing a separate “component” of +the device's firmware that can be upgraded independently. The significance of +such components is implementation-defined, however, the intention is that they +might refer to components such as: bootloaders, application code, cellular +modem firmwares, security processor firmwares, etc. + +It is expected that firmware components can be upgraded independently in most +cases, however, the object provides a mechanism for checking version +dependencies when a certain order of updates is required, or when multiple +components need to be upgraded in tandem. + + +:download:`Download: Advanced Firmware Update Object Definition XML ` + +.. toctree:: + :glob: + :titlesonly: + + FU-AdvancedFirmwareUpdate/FU-AFU-ResourceDefinitions.rst + FU-AdvancedFirmwareUpdate/FU-AFU-StateDiagram.rst + FU-AdvancedFirmwareUpdate/FU-AFU-Examples.rst + FU-AdvancedFirmwareUpdate/FU-AFU-BasicImplementation.rst diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-BasicImplementation.rst b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-BasicImplementation.rst new file mode 100644 index 000000000..a8ac57ab6 --- /dev/null +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-BasicImplementation.rst @@ -0,0 +1,850 @@ +.. + Copyright 2017-2023 AVSystem + AVSystem Anjay LwM2M SDK + All rights reserved. + + Licensed under the AVSystem-5-clause License. + See the attached LICENSE file for details. + +Implementation +============== + +.. contents:: :local: + +.. note:: + + Before approaching this tutorial, it is recommended to get familiar with the + :doc:`../FU-BasicImplementation` chapter. Both examples are very similar, + except for the additional elements resulting from the Advanced Firmware Update specification. + In this document we will not focus on those elements that are also present in basic Firmware Udpates. + +Project structure +^^^^^^^^^^^^^^^^^ + +.. code:: + + examples/tutorial/firmware-update/advanced-firmware-update/ + ├── CMakeLists.txt + └── src + ├── advanced_firmware_update.c + ├── advanced_firmware_update.h + ├── main.c + ├── time_object.c + └── time_object.h + +Advanced Firmware Update API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In order to install the module, we are going to use: + +.. highlight:: c +.. snippet-source:: include_public/anjay/advanced_fw_update.h + + int anjay_advanced_fw_update_install( + anjay_t *anjay, const anjay_advanced_fw_update_global_config_t *config); + +With this call we are passing a ``config`` that will affect all instances of the Advanced Firmware Update object. + +To add an instance of the Advanced Firmware Update object, we are going to use: + +.. highlight:: c +.. snippet-source:: include_public/anjay/advanced_fw_update.h + + int anjay_advanced_fw_update_instance_add( + anjay_t *anjay, + anjay_iid_t iid, + const char *component_name, + const anjay_advanced_fw_update_handlers_t *handlers, + void *user_arg, + const anjay_advanced_fw_update_initial_state_t *initial_state); + +The ``anjay``, ``handlers``, ``user_arg`` and ``initial_state`` arguments are similar +to their equivalents in ``anjay_fw_update_install`` function (see :ref:`anjay_fw_update_install`). +There are two major differences between ``anjay_advanced_fw_update_handlers_t`` and ``anjay_fw_update_handlers_t``: + + - each callback has an additional **iid** argument, which corresponds to the object instance number, + - new callback ``get_current_version`` that return the version of current firmware package. + +Remember that each instance must have a unique **iid** number. The **component_name** holds value of the /33629/x/14 resource. +**The string is NOT copied, so it needs to remain valid for the lifetime of the object instance.** + +Implementing handlers and installation routine +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Compared to a regular Firmware Updates, we need to store more information in the global structure. +For each AFU instance, we define: a file handle, an array storing the firmware version +(for the /33629/x/15 resource) and an array storing the instance name (**component_name** argument in +``anjay_advanced_fw_update_instance_add``). + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c + + #include "advanced_firmware_update.h" + + #include + #include + #include + #include + #include + + #define AFU_VERSION_STR_MAX_LEN 10 + #define AFU_INSTANCE_NAME_STR_MAX_LEN 10 + #define AFU_FILE_NAME_STR_MAX_LEN 50 + + typedef struct { + anjay_t *anjay; + char fw_version[AFU_NUMBER_OF_FIRMWARE_INSTANCES] + [AFU_VERSION_STR_MAX_LEN + 1]; + char instance_name[AFU_NUMBER_OF_FIRMWARE_INSTANCES] + [AFU_INSTANCE_NAME_STR_MAX_LEN + 1]; + FILE *new_firmware_file[AFU_NUMBER_OF_FIRMWARE_INSTANCES]; + } advanced_firmware_update_logic_t; + + static advanced_firmware_update_logic_t afu_logic; + +Number of the firmware instances is defined by ``AFU_NUMBER_OF_FIRMWARE_INSTANCES``. +Default instance (``AFU_DEFAULT_FIRMWARE_INSTANCE_IID``) is the built image of this software. +The other instances correspond to the files created by the ``afu_update_install`` function for +the purpose of this tutorial (those are equivalent of such software images as bootloader image, +modem image etc. used in embedded systems). + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.h + + #ifndef ADVANCED_FIRMWARE_UPDATE_H + #define ADVANCED_FIRMWARE_UPDATE_H + + #include + #include + + #include + + #define AFU_DEFAULT_FIRMWARE_VERSION "1.0.0" + #define AFU_ADD_FILE_DEFAULT_CONTENT "1.1.1" + + #define AFU_DEFAULT_FIRMWARE_INSTANCE_IID 0 + #define AFU_NUMBER_OF_FIRMWARE_INSTANCES 3 + + /** + * Buffer for the endpoint name that will be used when re-launching the client + * after firmware upgrade. + */ + extern const char *ENDPOINT_NAME; + + /** + * Installs the advanced firmware update module. + * + * @returns 0 on success, negative value otherwise. + */ + int afu_update_install(anjay_t *anjay); + + #endif // ADVANCED_FIRMWARE_UPDATE_H + +The implementation of ``fw_stream_open`` and ``fw_update_common_write`` is quite simple. +For each iid we open and write to a separate file. + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c + + static void get_firmware_download_name(int iid, char *buff) { + if (iid == AFU_DEFAULT_FIRMWARE_INSTANCE_IID) { + snprintf(buff, AFU_FILE_NAME_STR_MAX_LEN, "/tmp/firmware_image.bin"); + } else { + snprintf(buff, AFU_FILE_NAME_STR_MAX_LEN, "/tmp/add_image_%d", iid); + } + } + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c + + static int fw_stream_open(anjay_iid_t iid, void *user_ptr) { + (void) user_ptr; + + char file_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_firmware_download_name(iid, file_name); + + if (afu_logic.new_firmware_file[iid]) { + avs_log(advance_fu, ERROR, "Already open %s", file_name); + return -1; + } + afu_logic.new_firmware_file[iid] = fopen(file_name, "wb"); + if (!afu_logic.new_firmware_file[iid]) { + avs_log(advance_fu, ERROR, "Could not open %s", file_name); + return -1; + } + return 0; + } + + static int fw_update_common_write(anjay_iid_t iid, + void *user_ptr, + const void *data, + size_t length) { + (void) user_ptr; + + if (!afu_logic.new_firmware_file[iid]) { + avs_log(advance_fu, ERROR, "Stream not open: object %d", iid); + return -1; + } + if (length + && (fwrite(data, length, 1, afu_logic.new_firmware_file[iid]) != 1 + || fflush(afu_logic.new_firmware_file[iid]) != 0)) { + avs_log(advance_fu, ERROR, "fwrite or fflush failed: %s", + strerror(errno)); + return ANJAY_ADVANCED_FW_UPDATE_ERR_NOT_ENOUGH_SPACE; + } + return 0; + } + +In ``fw_update_common_finish`` after closing the stream, we check the other instances, +if any is in DOWNLOADED state then we link them with each other. **This is not a requirement, +the implementation is free do decide which instances are linked and which are not.** +With ``anjay_advanced_fw_update_set_linked_instances`` we set the **Linked Instances** (/33629/x/15) +resource to inform the server that the upgrade will be performed simultaneously on all linked instances. + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c + + static void add_linked_instance(anjay_iid_t iid, anjay_iid_t target_iid) { + const anjay_iid_t *linked_instances; + anjay_iid_t linked_target_iids[AFU_NUMBER_OF_FIRMWARE_INSTANCES - 1]; + size_t linked_iids_count = 0; + + // get linked instances + anjay_advanced_fw_update_get_linked_instances( + afu_logic.anjay, iid, &linked_instances, &linked_iids_count); + // add target_iid to the list + for (size_t i = 0; i < linked_iids_count; i++) { + linked_target_iids[i] = linked_instances[i]; + } + linked_target_iids[linked_iids_count++] = target_iid; + anjay_advanced_fw_update_set_linked_instances( + afu_logic.anjay, iid, linked_target_iids, linked_iids_count); + } + + static int fw_update_common_finish(anjay_iid_t iid, void *user_ptr) { + (void) user_ptr; + + if (!afu_logic.new_firmware_file[iid]) { + avs_log(advance_fu, ERROR, "Stream not open: object %d", iid); + return -1; + } + + if (fclose(afu_logic.new_firmware_file[iid])) { + avs_log(advance_fu, ERROR, "Closing firmware image failed: object %d", + iid); + afu_logic.new_firmware_file[iid] = NULL; + return -1; + } + afu_logic.new_firmware_file[iid] = NULL; + + /* + If other firmware instances are in downloaded state set linked instances, + based on them, the upgrade will be performed simultaneously in the + perform_upgrade callback. The reason for setting linked instances may be + different and depends on the user's implementation, but always mean + that instances will be updated in a batch if the Update resource is executed + with no arguments. + */ + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (i != iid) { + anjay_advanced_fw_update_state_t state; + anjay_advanced_fw_update_get_state(afu_logic.anjay, i, &state); + if (state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED) { + add_linked_instance(iid, i); + add_linked_instance(i, iid); + } + } + } + + return 0; + } + + +Three new paramteters are passed with ``fw_update_common_perform_upgrade`` function: + - ``iid``, + - ``requested_supplemental_iids``, + - ``requested_supplemental_iids_count``. + +The ``iid`` points to the instance on which `Update` (/33629/x/2) was called. The ``requested_supplemental_iids`` can +contain a list of instances for simultaneous upgrade, passed as an argument in the `Update` (/33629/x/2) command sent by +the server. If ``requested_supplemental_iids`` is present (different than `NULL`), specification forces us to upgrade +according to it. If this is not possible then we need to return a corresponding error. + +First, we check which instances are to be updated. To do this, we create ``update_iid`` array and for each +instance we set it to `true` if the conditions for upgrade are met. Conditions are checked inside +``is_update_requested`` function. In our example if ``requested_supplemental_iids == NULL``, we use ``linked_target_iids`` +instead. Then we retrieve the state of each instance that is involved in the upgrade, if it is other than `DOWNLOADED` +then we return a `CONFLICTING_STATE` error. Before leaving the function, we set conflicting instances to tell the server +which instance is causing the problem (``add_conflicting_instance`` function which calls ``anjay_advanced_fw_update_set_conflicting_instances``). + +In next step we check version compatibility. For the purposes of this tutorial, we have assumed that the first character +in each additional file must have the same value. If we are updating only some of the files, their new versions must +have the same value as the old ones. In the case of replacing all files, each new file must match. If a mismatch is detected, +an error `CONFLICTING_STATE` is returned and the ``add_conflicting_instance`` function is called, so that the server gets +information about the error and the instance that caused it. After that we update the state of each instance from `DOWNLOADED` +to `UPDATING`. + +.. note:: + + During upgrade you must remember to change the state of each instance. Anjay will only modify the state of the ``iid`` + instance. In a typical scenario, the state of each instance must be changed first from `DOWNLOADED` to `UPDATING` + and then, if reboot does not occur, to `SUCCESS`. + +The last step is to replace the firmware. We start with additional images, if ``update_iid[i] == true`` then we use the +``move_file`` function to swap files, if the main image does not change then we create a "marker" file for each instance +(logic carried over from :doc:`../FU-BasicImplementation`) and change its state from `UPDATING` to `SUCCESS`. +If the main image is to be replaced then at this point we create the corresponding "marker" file and start a new application. +Otherwise, we call ``refresh_fw_version`` to update the instance's firmware versions and we remove all information about +all linked and conflicting instances. + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c + + static void get_add_firmware_file_name(int iid, char *buff) { + snprintf(buff, AFU_FILE_NAME_STR_MAX_LEN, "ADD_FILE_%d", iid); + } + + static void get_marker_file_name(int iid, char *buff) { + snprintf(buff, AFU_FILE_NAME_STR_MAX_LEN, "/tmp/fw-updated-marker_%d", iid); + } + + static int move_file(const char *dest, const char *source) { + int ret_val = -1; + FILE *dest_stream = NULL; + FILE *source_stream = fopen(source, "r"); + + if (!source_stream) { + avs_log(advance_fu, ERROR, "Could not open file: %s", source); + goto cleanup; + } + dest_stream = fopen(dest, "w"); + if (!dest_stream) { + avs_log(advance_fu, ERROR, "Could not open file: %s", dest); + fclose(source_stream); + goto cleanup; + } + + while (!feof(source_stream)) { + char buff[1024]; + size_t bytes_read_1 = fread(buff, 1, sizeof(buff), source_stream); + if (fwrite(buff, 1, bytes_read_1, dest_stream) != bytes_read_1) { + avs_log(advance_fu, ERROR, "Error during write to file: %s", dest); + goto cleanup; + } + } + ret_val = 0; + + cleanup: + if (dest_stream) { + if (fclose(dest_stream)) { + avs_log(advance_fu, ERROR, "Could not close file: %s", dest); + ret_val = -1; + } + } + if (source_stream) { + if (fclose(source_stream)) { + avs_log(advance_fu, ERROR, "Could not close file: %s", source); + ret_val = -1; + } + } + unlink(source); + + return ret_val; + } + + static void add_conflicting_instance(anjay_iid_t iid, anjay_iid_t target_iid) { + const anjay_iid_t *conflicting_instances; + anjay_iid_t conflicting_target_iids[AFU_NUMBER_OF_FIRMWARE_INSTANCES - 1]; + size_t conflicting_iids_count = 0; + + // get conflicting instances + anjay_advanced_fw_update_get_conflicting_instances(afu_logic.anjay, iid, + &conflicting_instances, + &conflicting_iids_count); + // add target_iid to the list + for (size_t i = 0; i < conflicting_iids_count; i++) { + conflicting_target_iids[i] = conflicting_instances[i]; + } + conflicting_target_iids[conflicting_iids_count++] = target_iid; + anjay_advanced_fw_update_set_conflicting_instances(afu_logic.anjay, iid, + conflicting_target_iids, + conflicting_iids_count); + } + + static bool is_update_requested(anjay_iid_t iid, + anjay_iid_t target_iid, + const anjay_iid_t *requested_supplemental_iids, + size_t requested_supplemental_iids_count, + const anjay_iid_t *linked_target_iids, + size_t linked_iids_count) { + if (iid == target_iid) { + return true; + } + if (requested_supplemental_iids) { + for (size_t i = 0; i < requested_supplemental_iids_count; i++) { + if (iid == requested_supplemental_iids[i]) { + return true; + } + } + } else if (linked_target_iids) { + for (size_t i = 0; i < linked_iids_count; i++) { + if (iid == linked_target_iids[i]) { + return true; + } + } + } + return false; + } + + static char get_firmware_major_version(anjay_iid_t iid, bool is_upgrade) { + char file_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + + if (is_upgrade == false) { + return afu_logic.fw_version[iid][0]; + } + + get_firmware_download_name(iid, file_name); + + // get value from new file + char buff; + FILE *stream = fopen(file_name, "r"); + if (!stream) { + avs_log(advance_fu, ERROR, "Could not open file: %s", file_name); + return ' '; + } + if (!fread(&buff, 1, 1, stream)) { + avs_log(advance_fu, ERROR, "Could not read from file file: %s", + file_name); + fclose(stream); + return ' '; + } + if (fclose(stream)) { + avs_log(advance_fu, ERROR, "Could not close file: %s", file_name); + } + + return buff; + } + + static int refresh_fw_version() { + memcpy(afu_logic.fw_version[AFU_DEFAULT_FIRMWARE_INSTANCE_IID], + AFU_DEFAULT_FIRMWARE_VERSION, strlen(AFU_DEFAULT_FIRMWARE_VERSION)); + + for (int i = 1; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + char buff[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_add_firmware_file_name(i, buff); + FILE *stream = fopen(buff, "r"); + if (!stream) { + avs_log(advance_fu, ERROR, "Could not open file with iid: %d", i); + return -1; + } + if (!fread(afu_logic.fw_version[i], 1, AFU_VERSION_STR_MAX_LEN, + stream)) { + avs_log(advance_fu, ERROR, "Could not read file with iid: %d", i); + fclose(stream); + return -1; + } + if (fclose(stream)) { + avs_log(advance_fu, ERROR, "Could not close file with iid: %d", i); + return -1; + } + } + + for (int i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + avs_log(advance_fu, INFO, + "Firmware version for object with IID %d is: %s", i, + afu_logic.fw_version[i]); + } + + return 0; + } + + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c + + static int + fw_update_common_perform_upgrade(anjay_iid_t iid, + void *user_ptr, + const anjay_iid_t *requested_supplemental_iids, + size_t requested_supplemental_iids_count) { + (void) user_ptr; + + const anjay_iid_t *linked_target_iids; + bool update_iid[AFU_NUMBER_OF_FIRMWARE_INSTANCES]; + size_t linked_iids_count = 0; + + // get linked instances + anjay_advanced_fw_update_get_linked_instances( + afu_logic.anjay, iid, &linked_target_iids, &linked_iids_count); + + /* Prepare list of iid to update. If requested_supplemental_iids is present + * use it otherwise use linked_target_iids.*/ + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (is_update_requested(i, iid, requested_supplemental_iids, + requested_supplemental_iids_count, + linked_target_iids, linked_iids_count)) { + update_iid[i] = true; + // check if new file is already downloaded + anjay_advanced_fw_update_state_t state; + anjay_advanced_fw_update_get_state(afu_logic.anjay, i, &state); + if ((i != iid) + && (state != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED)) { + avs_log(advance_fu, ERROR, + "Upgrade can't be performed, firmware file with iid %d " + "is not ready", + i); + // set conflicting instance + add_conflicting_instance(iid, i); + return ANJAY_ADVANCED_FW_UPDATE_ERR_CONFLICTING_STATE; + } + } else { + update_iid[i] = false; + } + } + + /* + Check firmware version compatibility. + In this example major version number is compare - first character in every + additional image must have the same value. If new file is given (DOWNLOADED + STATE), get this value from them, otherwise use the old one. + */ + for (anjay_iid_t i = 1; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (update_iid[i] == true) { + for (anjay_iid_t j = i + 1; j < AFU_NUMBER_OF_FIRMWARE_INSTANCES; + j++) { + if (get_firmware_major_version(i, update_iid[i]) + != get_firmware_major_version(j, update_iid[j])) { + avs_log(advance_fu, ERROR, + "Upgrade can't be performed, conflicting firmware " + "version between instance %d and %d", + i, j); + // set conflicting instance due to firmware version + // incompatibility + add_conflicting_instance(i, j); + add_conflicting_instance(j, i); + return ANJAY_ADVANCED_FW_UPDATE_ERR_CONFLICTING_STATE; + } + } + } + } + + /* No errors found, change the status of all requested_supplemental_iids or + * linked_target_iids to UPDATING before the actual update process.*/ + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (update_iid[i] == true) { + if (i != iid) { + anjay_advanced_fw_update_set_state_and_result( + afu_logic.anjay, i, + ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + } + } + } + + // after firmware versions check, start firmware update, first with + // additional images + for (anjay_iid_t i = 1; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (update_iid[i] == true) { + avs_log(advance_fu, INFO, "Perform update on %d instance", i); + + char new_firm_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + char current_firm_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_firmware_download_name(i, new_firm_name); + get_add_firmware_file_name(i, current_firm_name); + + if (move_file(current_firm_name, new_firm_name)) { + avs_log(advance_fu, ERROR, + "Error during the %d additional image swapping", i); + return -1; + } + // if main application is restarted, set update marker + if (update_iid[AFU_DEFAULT_FIRMWARE_INSTANCE_IID] == true) { + char marker_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_marker_file_name(i, marker_name); + FILE *marker = fopen(marker_name, "w"); + if (!marker) { + avs_log(advance_fu, ERROR, + "Marker file could not be created"); + return -1; + } + if (fclose(marker)) { + avs_log(advance_fu, ERROR, + "Marker file could not be close"); + } + } // if main application is not restarted, update state + else { + anjay_advanced_fw_update_set_state_and_result( + afu_logic.anjay, i, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS); + } + } + } + + // update application + if (update_iid[AFU_DEFAULT_FIRMWARE_INSTANCE_IID] == true) { + avs_log(advance_fu, INFO, "Perform update on default instance"); + + char new_firm_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + char marker_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_firmware_download_name(AFU_DEFAULT_FIRMWARE_INSTANCE_IID, + new_firm_name); + get_marker_file_name(AFU_DEFAULT_FIRMWARE_INSTANCE_IID, marker_name); + + if (chmod(new_firm_name, 0700) == -1) { + avs_log(advance_fu, ERROR, "Could not make firmware executable: %s", + strerror(errno)); + return -1; + } + // Create a marker file, so that the new process knows it is the + // "upgraded" one + FILE *marker = fopen(marker_name, "w"); + if (!marker) { + avs_log(advance_fu, ERROR, "Marker file could not be created"); + return -1; + } + if (fclose(marker)) { + avs_log(advance_fu, ERROR, "Marker file could not be close"); + } + + assert(ENDPOINT_NAME); + + // If the call below succeeds, the firmware is considered as "upgraded", + // and we hope the newly started client registers to the Server. + (void) execl(new_firm_name, new_firm_name, ENDPOINT_NAME, NULL); + avs_log(advance_fu, ERROR, "execl() failed: %s", strerror(errno)); + // If we are here, it means execl() failed. Marker file MUST now be + // removed, as the firmware update failed. + unlink(marker_name); + return -1; + } + + // update firmware version + refresh_fw_version(); + + // clear conflicting and linked instances in the objects + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (update_iid[i] == true) { + anjay_advanced_fw_update_set_conflicting_instances(afu_logic.anjay, + i, NULL, 0); + anjay_advanced_fw_update_set_linked_instances(afu_logic.anjay, i, + NULL, 0); + // clear conflicting and linked instances about this object from + // other objects + for (anjay_iid_t j = 0; j < AFU_NUMBER_OF_FIRMWARE_INSTANCES; j++) { + if (i != j) { + remove_linked_instance(i, j); + remove_conflicting_instance(i, j); + } + } + } + } + + return 0; + } + +At the very end we will look at the implementation of ``fw_update_common_reset``. Note that this function will be called +not only in case of failure but also after a successful update if there is no reboot. In addition to removing the downloaded +files, we clean `linked` and `conflicting` resources from the instance and using the ``remove_conflicting_instance`` and +``remove_linked_instance`` functions, we remove the information about given instance from the other instances. +Please note that as in the case of ``fw_update_common_perform_upgrade`` we do not check the state of each instance, +perhaps in your implementation before clearing the `linked` and `conflicting` lists, you will need to include logic +to check the status of the update. + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c + + static void remove_linked_instance(anjay_iid_t iid, anjay_iid_t target_iid) { + const anjay_iid_t *linked_instances; + anjay_iid_t linked_target_iids[AFU_NUMBER_OF_FIRMWARE_INSTANCES - 1]; + size_t linked_iids_count = 0; + size_t new_linked_iids_count = 0; + + // get linked instances + anjay_advanced_fw_update_get_linked_instances( + afu_logic.anjay, iid, &linked_instances, &linked_iids_count); + // remove target_iid from the list + for (size_t i = 0; i < linked_iids_count; i++) { + if (linked_instances[i] != target_iid) { + linked_target_iids[new_linked_iids_count++] = linked_instances[i]; + } + } + // update linked list + anjay_advanced_fw_update_set_linked_instances( + afu_logic.anjay, iid, linked_target_iids, new_linked_iids_count); + } + + static void remove_conflicting_instance(anjay_iid_t iid, + anjay_iid_t target_iid) { + const anjay_iid_t *conflicting_instances; + anjay_iid_t conflicting_target_iids[AFU_NUMBER_OF_FIRMWARE_INSTANCES - 1]; + size_t conflicting_iids_count = 0; + size_t new_conflicting_iids_count = 0; + + // get conflicting instances + anjay_advanced_fw_update_get_conflicting_instances(afu_logic.anjay, iid, + &conflicting_instances, + &conflicting_iids_count); + // remove target_iid from the list + for (size_t i = 0; i < conflicting_iids_count; i++) { + if (conflicting_instances[i] != target_iid) { + conflicting_target_iids[new_conflicting_iids_count++] = + conflicting_instances[i]; + } + } + // update conflicting list + anjay_advanced_fw_update_set_conflicting_instances( + afu_logic.anjay, iid, conflicting_target_iids, + new_conflicting_iids_count); + } + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c + + void fw_update_common_reset(anjay_iid_t iid, void *user_ptr) { + (void) user_ptr; + + char new_firm_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_firmware_download_name(iid, new_firm_name); + + // Reset can be issued even if the download never started + if (afu_logic.new_firmware_file[iid]) { + // We ignore the result code of fclose(), as fw_reset() can't fail + (void) fclose(afu_logic.new_firmware_file[iid]); + // and reset our global state to initial value + afu_logic.new_firmware_file[iid] = NULL; + } + // Finally, let's remove any downloaded payload + unlink(new_firm_name); + + // clear conflicting and linked instances in the object + anjay_advanced_fw_update_set_conflicting_instances(afu_logic.anjay, iid, + NULL, 0); + anjay_advanced_fw_update_set_linked_instances(afu_logic.anjay, iid, NULL, + 0); + // clear conflicting and linked instances about this object from other + // objects + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (i != iid) { + remove_linked_instance(i, iid); + remove_conflicting_instance(i, iid); + } + } + } + +Installing the Advanced Firmware Update module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``afu_update_install`` function is similar to ``fw_update_install`` from :doc:`../FU-BasicImplementation`. +First we call ``anjay_advanced_fw_update_install`` then for each instance we check the existence of a "marker" file, +based on which we call ``anjay_advanced_fw_update_instance_add`` with the appropriate arguments. For additional files, +we create them if they do not exist and fill them with the default content (`AFU_ADD_FILE_DEFAULT_CONTENT`). +Finally, we call the ``refresh_fw_version`` function so that the ``fw_update_common_get_current_version`` callback +can return the correct value. + +.. highlight:: c +.. snippet-source:: examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c + + static const char *fw_update_common_get_current_version(anjay_iid_t iid, + void *user_ptr) { + (void) user_ptr; + + return (const char *) afu_logic.fw_version[iid]; + } + + static const anjay_advanced_fw_update_handlers_t handlers = { + .stream_open = fw_stream_open, + .stream_write = fw_update_common_write, + .stream_finish = fw_update_common_finish, + .reset = fw_update_common_reset, + .get_current_version = fw_update_common_get_current_version, + .perform_upgrade = fw_update_common_perform_upgrade + }; + + int afu_update_install(anjay_t *anjay) { + anjay_advanced_fw_update_initial_state_t state; + char marker_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + char file_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + + memset(&state, 0, sizeof(state)); + + afu_logic.anjay = anjay; + + anjay_advanced_fw_update_global_config_t config = { + .prefer_same_socket_downloads = true + }; + int result = anjay_advanced_fw_update_install(anjay, &config); + if (result) { + avs_log(advance_fu, ERROR, + "Could not install advanced firmware object: %d", result); + return -1; + } + + // check if application was updated + get_marker_file_name(AFU_DEFAULT_FIRMWARE_INSTANCE_IID, marker_name); + if (access(marker_name, F_OK) != -1) { + avs_log(advance_fu, INFO, "Application update succeded"); + state.result = ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS; + unlink(marker_name); + } + result = anjay_advanced_fw_update_instance_add( + anjay, AFU_DEFAULT_FIRMWARE_INSTANCE_IID, "application", &handlers, + NULL, &state); + if (result) { + avs_log(advance_fu, ERROR, + "Could not add default application instance: %d", result); + return -1; + } + + // check if additional files were updated, if not create it with default + // value + for (anjay_iid_t i = 1; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + memset(marker_name, 0, sizeof(marker_name)); + get_marker_file_name(i, marker_name); + if (access(marker_name, F_OK) != -1) { + avs_log(advance_fu, INFO, + "Additional file with idd: %d update succeded", i); + state.result = ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS; + unlink(marker_name); + } else { + state.result = ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL; + } + + memset(file_name, 0, sizeof(file_name)); + get_add_firmware_file_name(i, file_name); + // create file only if not exist + if (access(file_name, F_OK) != 0) { + FILE *stream = fopen(file_name, "wb"); + if (!stream) { + avs_log(advance_fu, ERROR, "Could not open %s", file_name); + return -1; + } + if (fwrite(AFU_ADD_FILE_DEFAULT_CONTENT, + strlen(AFU_ADD_FILE_DEFAULT_CONTENT), 1, stream) + != 1) { + avs_log(advance_fu, ERROR, "Could not write to %s", file_name); + fclose(stream); + return -1; + } + if (fclose(stream)) { + avs_log(advance_fu, ERROR, "Could not close %s", file_name); + return -1; + } + } + + snprintf(afu_logic.instance_name[i], AFU_INSTANCE_NAME_STR_MAX_LEN, + "add_img_%d", i); + result = anjay_advanced_fw_update_instance_add( + anjay, i, afu_logic.instance_name[i], &handlers, NULL, &state); + if (result) { + avs_log(advance_fu, ERROR, + "Could not add the additional image instance: %d", result); + return -1; + } + } + + if (refresh_fw_version()) { + return -1; + } + + return 0; + } diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-Examples.rst b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-Examples.rst new file mode 100644 index 000000000..a3252c2ef --- /dev/null +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-Examples.rst @@ -0,0 +1,446 @@ +.. + Copyright 2017-2023 AVSystem + AVSystem Anjay LwM2M SDK + All rights reserved. + + Licensed under the AVSystem-5-clause License. + See the attached LICENSE file for details. + +Examples +======== + +.. contents:: :local: + +Example client data model - initial state +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Lwm2M Firmware update Object - Application component [/33629/0] +*************************************************************** + +This component governs the package for the main application firmware that +implements the core device functionality. + +.. flat-table:: + :header-rows: 1 + :widths: 50 20 25 20 35 + + * - Resource Name + - Resource ID + - Resource Instance ID + - Value + - Notes + * - Package + - 0 + - + - + - + * - Package URI + - 1 + - + - + - + * - State + - 3 + - + - 0 + - Idle + * - Update Result + - 5 + - + - 0 + - Initial value + * - :rspan:`3` Firmware Update Protocol Support + - :rspan:`3` 8 + - 0 + - 0 + - CoAP + * - 1 + - 1 + - CoAPs + * - 2 + - 2 + - HTTP + * - 3 + - 3 + - HTTPS + * - Firmware Update Delivery Method + - 9 + - + - 2 + - Both push and pull + * - Component Name + - 14 + - + - Application + - + * - Current version + - 15 + - + - 1.0 + - + * - Linked Instances + - 16 + - + - + - + * - Conflicting Instances + - 17 + - + - + - + +Lwm2M Firmware update Object - Trusted firmware component [/33629/1] +******************************************************************** + +This component governs the TEE (Trusted Execution Environment) firmware package. + +.. flat-table:: + :header-rows: 1 + :widths: 50 20 25 20 35 + + * - Resource Name + - Resource ID + - Resource Instance ID + - Value + - Notes + * - Package + - 0 + - + - + - + * - Package URI + - 1 + - + - + - + * - State + - 3 + - + - 0 + - Idle + * - Update Result + - 5 + - + - 0 + - Initial value + * - :rspan:`3` Firmware Update Protocol Support + - :rspan:`3` 8 + - 0 + - 0 + - CoAP + * - 1 + - 1 + - CoAPs + * - 2 + - 2 + - HTTP + * - 3 + - 3 + - HTTPS + * - Firmware Update Delivery Method + - 9 + - + - 2 + - Both push and pull + * - Component Name + - 14 + - + - TEE + - + * - Current version + - 15 + - + - 1.1 + - + * - Linked Instances + - 16 + - + - + - + * - Conflicting Instances + - 17 + - + - + - + +Lwm2M Firmware update Object - Bootloader [/33629/2] +**************************************************** + +This component governs the package for the device's bootloader. + +.. flat-table:: + :header-rows: 1 + :widths: 50 20 25 20 35 + + * - Resource Name + - Resource ID + - Resource Instance ID + - Value + - Notes + * - Package + - 0 + - + - + - + * - Package URI + - 1 + - + - + - + * - State + - 3 + - + - 0 + - Idle + * - Update Result + - 5 + - + - 0 + - Initial value + * - :rspan:`3` Firmware Update Protocol Support + - :rspan:`3` 8 + - 0 + - 0 + - CoAP + * - 1 + - 1 + - CoAPs + * - 2 + - 2 + - HTTP + * - 3 + - 3 + - HTTPS + * - Firmware Update Delivery Method + - 9 + - + - 2 + - Both push and pull + * - Component Name + - 14 + - + - Bootloader + - + * - Current version + - 15 + - + - 2.1 + - + * - Linked Instances + - 16 + - + - + - + * - Conflicting Instances + - 17 + - + - + - + +Lwm2M Firmware update Object - Modem [/33629/3] +*********************************************** + +This component governs the firmware for the cellular module that the device +uses for radio communication. The module may be located on a separate IC +package from the main processor or microcontroller. + +.. flat-table:: + :header-rows: 1 + :widths: 50 20 25 20 35 + + * - Resource Name + - Resource ID + - Resource Instance ID + - Value + - Notes + * - Package + - 0 + - + - + - + * - Package URI + - 1 + - + - + - + * - State + - 3 + - + - 0 + - Idle + * - Update Result + - 5 + - + - 0 + - Initial value + * - :rspan:`3` Firmware Update Protocol Support + - :rspan:`3` 8 + - 0 + - 0 + - CoAP + * - 1 + - 1 + - CoAPs + * - 2 + - 2 + - HTTP + * - 3 + - 3 + - HTTPS + * - Firmware Update Delivery Method + - 9 + - + - 2 + - Both push and pull + * - Component Name + - 14 + - + - Modem + - + * - Current version + - 15 + - + - 22.01 + - + * - Linked Instances + - 16 + - + - + - + * - Conflicting Instances + - 17 + - + - + - + +Example upgrade scenarios +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Version conflict +**************** + +Let's assume that the Application firmware version 2.0 requires the TEE +firmware to also be updated to version 2.0 or later. + +#. The LwM2M Server writes ``http://example.com/app_2.0.bin`` to resource + ``/33629/0/1`` (Package URI of the Application component) +#. The instance enters the Downloading state, downloads the package, and enters + the Downloaded state +#. Resource ``/33629/0/17`` (Conflicting instances) changes the value to: ``[0] + = 33629:1``; this informs the server that instance ``/33629/1`` requires + attention due to dependency conflict +#. If the server nevertheless issues Execute on ``/33629/0/2``, the device + immediately reverts to the Downloaded state, with Update Result set to 13 + (Dependency error) +#. The LwM2M Server then writes ``http://example.com/tee_2.0.bin`` to resource + ``/33629/1/1`` (Package URI of the TEE component) +#. The instance enters the Downloading state, downloads the package, and enters + the Downloaded state +#. Resource ``/33629/0/17`` (Conflicting instances) changes value back to empty. +#. Resource ``/33629/0/16`` (Linked instances) changes the value to: ``[0] = + 33629:1``; resource ``/33629/1/16`` (Linked instances) changes the value to: + ``[0] = 33629:0``; this signifies that by default, the device will upgrade + both when the Update operation is executed on either of the instances +#. The LwM2M Server issues Execute on ``/33629/0/2`` +#. The device enters Updating state, performs an update of both components, + reboots, and reconnects to the server +#. The State on both ``/33629/0`` and ``/33629/1`` instances read 0 (idle), + Update Result on both is 1 (success), and the version numbers have been + updated so that ``/33629/0/15`` = 2.0 and ``/33629/0/16`` = 2.0 + +Multi-component package +*********************** + +#. The LwM2M Server writes ``http://example.com/app_tee_2.1.zip`` to + ``/33629/0/1`` (Package URI of the Application component) +#. Instance 0 enters the Downloading state, downloads the package, and the + device logic detects that the file contains packages for both the Application + and TEE components +#. Both instance 0 and instance 1 enter the Downloaded state + +Conflicting downloads +********************* + +#. The LwM2M Server writes ``http://example.com/tee_2.0.bin`` to ``/33629/1/1`` + (Package URI of the TEE component) +#. Instance 1 enters the Downloading state, downloads the package, and enters + the Downloaded state +#. The LwM2M Server writes ``http://example.com/app_tee_2.1.zip`` to + ``33629/0/1`` (Package URI of the Application component) +#. Instance 0 enters the Downloading state, downloads at least some part of the + package, and then the device logic detects that the file contains packages + for both the Application and TEE components +#. The device aborts the download; instance 0 reverts to the Idle state, with + Update Result set to 12 (Conflicting state); instance 1 remains in the + Downloaded state, with the 2.0 package in memory. +#. Resource ``/33629/0/17`` changes the value to: ``[0] = 33629:1``; this serves + as the detail for the “Conflicting state” error, informing on which instances + were conflicting + +Implicit and explicit linked updates +************************************ + +#. The LwM2M server writes the following values: + #. ``http://example.com/app_2.0.bin`` to resource ``/33629/0/1`` (Package + URI of the Application component) + #. ``http://example.com/tee_2.0.bin`` to resource ``/33629/1/1`` (Package + URI of the TEE component) + #. ``http://example.com/boot_2.2.bin`` to resource ``/33629/2/1`` (Package + URI of the Bootloader component) + #. ``http://example.com/modem_22.02.bin`` to resource ``/33629/3/1`` + (Package URI of the Modem component) +#. The device downloads the packages, eventually all the instances enter the + Downloaded state +#. Resource ``/33629/0/16`` (Linked instances) changes the value to: ``[0] = + 33629:1``; resource ``/33629/1/16`` (Linked instances) changes the value to: + ``[0] = 33629:0``; this signifies that by default, the device will upgrade + instances 0 and 1 in one go when the Update operation is executed on either + of them +#. Either of the sub-scenarios described below follows + +Implicit linked update +********************** + +#. The LwM2M server issues Execute with no arguments on ``/33629/0/2`` or + ``/33629/1/2`` +#. The device enters Updating state, performs update of the Application and TEE + components, reboots, and reconnects to the server +#. The State on both ``/33629/0`` and ``/33629/1`` instances read 0 (idle), + Update Result on both is 1 (success), and the version numbers have been + updated +#. Instances ``/33629/2`` and ``/33629/3`` remain in the Downloaded state if + possible + +Explicit linked update +********************** + +#. The LwM2M server issues Execute on ``/33629/0/2`` with argument payload: + ``“0=',,'”`` +#. The device enters Updating state, performs an update of all four components, + reboots, and reconnects to the server +#. The State on all instances read 0 (idle), Update Result on all of them is 1 + (success), and the version numbers have been updated + +Explicit single component update +******************************** + +#. The LwM2M server issues Execute on ``/33629/1/2`` with argument payload: + ``“0”`` +#. Instance 1 enters Updating state, the device performs update of only the TEE + component (ignoring the Linked Instances), and reconnects to the server +#. The State on instance ``/33629/1`` reads 0 (idle), corresponding Update + Result is 1 (success), and the version number has been updated +#. Instances ``/33629/0``, ``/33629/2`` and ``/33629/3`` remain in the + Downloaded state if possible + +Explicit single component update - unsuccessful +*********************************************** + +#. The LwM2M server issues Execute on ``/33629/0/2`` with argument payload: + ``“0”`` +#. Instance 0 immediately reverts to the Downloaded state, with Update Result + set to 13 (Dependency error), because application version 2.0 requires the + TEE component to be updated to version 2.0 first or at the same time +#. Resource ``/33629/0/17`` changes value to: ``[0] = 33629:1``; this serves as + the detail for the “Dependency error” result, informing on which instances + were conflicting diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-ResourceDefinitions.rst b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-ResourceDefinitions.rst new file mode 100644 index 000000000..ab1fba237 --- /dev/null +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-ResourceDefinitions.rst @@ -0,0 +1,413 @@ +.. + Copyright 2017-2023 AVSystem + AVSystem Anjay LwM2M SDK + All rights reserved. + + Licensed under the AVSystem-5-clause License. + See the attached LICENSE file for details. + +Resource definitions +==================== + +.. contents:: :local: + +Package (/33629/x/0) +^^^^^^^^^^^^^^^^^^^^ + +:ID: 0 +:Operations: W +:Instances: Single +:Mandatory: Yes +:Type: Opaque +:Range or Enumeration: \- +:Units: \- +:Description: + Firmware package + +Package URI (/33629/x/1) +^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 1 +:Operations: RW +:Instances: Single +:Mandatory: Yes +:Type: String +:Range or Enumeration: 0..255 +:Units: \- +:Description: + URI from where the device can download the firmware package by an alternative + mechanism. As soon as the device has received the Package URI it performs the + download at the next practical opportunity. The URI format is defined in + ``RFC 3986``. For example, ``coaps://example.org/firmware`` is a + syntactically valid URI. The URI scheme determines the protocol to be used. + For CoAP this endpoint MAY be an LwM2M Server but does not necessarily need to + be. A CoAP server implementing block-wise transfer is sufficient as a server + hosting a firmware repository and the expectation is that this server merely + serves as a separate file server making firmware packages available to LwM2M + Clients. + +Update (/33629/x/2) +^^^^^^^^^^^^^^^^^^^ + +:ID: 2 +:Operations: E +:Instances: Single +:Mandatory: Yes +:Type: \- +:Range or Enumeration: \- +:Units: \- +:Description: + Updates firmware by using the firmware package stored in Package, or, by + using the firmware downloaded from the Package URI. This Resource is only + executable when the value of the State Resource is Downloaded. + + If multiple instances of the Advanced Firmware Update object are in the + Downloaded state, the device MAY update multiple components in one go. In + this case, the Linked Instances resource MUST list all other components that + will be updated alongside the current one. + + The server MAY override this behavior by including an argument 0 in the + Execute operation. If the argument is present with no value, the client MUST + attempt to update only the component handled by the current instance. If the + argument is present with a value containing a list of Advanced Firmware + Update object instances specified as a Core Link Format (so that the argument + may read, for example: ``0=,``), the client MUST attempt + to update the components handled by the current instance and the instances + listed in the argument, and MUST NOT attempt to update any other components. + If the client is not able to satisfy such a request, the update process shall + fail with the Update Result resource set to 13. + + If the downloaded packages are incompatible with at least one of the packages + installed on other components, and compatible updates for them are not + downloaded (i.e., the State resource in an instance corresponding to the + conflicting component is not Downloaded), the update process shall also fail + with the Update Result resource set to 13. + + When multiple components are upgraded as part of a single Update operation, + the device SHOULD upgrade them in a transactional fashion (i.e., all are + updated successfully, or all are reverted in case of error), and MUST perform + the upgrade in a way that ensures that the device will not be rendered + unbootable due to partial errors. + +State (/33629/x/3) +^^^^^^^^^^^^^^^^^^ + +:ID: 3 +:Operations: R +:Instances: Single +:Mandatory: Yes +:Type: Integer +:Range or Enumeration: 0..3 +:Units: \- +:Description: + Indicates current state with respect to this firmware update. This value is + set by the LwM2M Client. + + | **0:** Idle (before downloading or after successful updating) + | **1:** Downloading (The data sequence is on the way) + | **2:** Downloaded + | **3:** Updating + + If writing the firmware package to Package Resource has completed, or, if the + device has downloaded the firmware package from the Package URI the state + changes to Downloaded. The device MAY support packages containing code for + multiple components in a single file, in which case downloading the package in + any instance of the Advanced Firmware Update object that is valid for it, + MUST set the State resource to 2 in instances handling all components that + are affected by the downloaded package; if the State of any of such instances + was different than 0, the package MUST be rejected and the Update Result + resource set to 12. + + Writing an empty string to Package URI Resource or setting the Package + Resource to **NULL** (``\0``), resets the Advanced Firmware Update State + Machine: the State Resource value is set to Idle and the Update Result + Resource value is set to 0. The device should remove the downloaded firmware + package when the state is reset to Idle. + + When in Downloaded state, and the executable Resource Update is triggered, + the state changes to Updating if the update starts immediately. For devices + that support a user interface and the deferred update functionality, the user + may be allowed to defer the firmware update to a later time. In this case, + the state stays in the Downloaded state and the Update Result is set to 11. + Once a user accepts the firmware update, the state changes to Updating. When + the user deferred the update, the device will continue operations normally + until the user approves the firmware update or an automatic update starts. It + will not block any operation on the device. + + If the Update Resource failed, the state may return to either Downloaded or + Idle depending on the underlying reason of update failure, e.g. Integrity + Check Failure results in the client moving to the Idle state. If performing + the Update or Cancel operation was successful, the state changes to Idle. The + Advanced Firmware Update state machine is illustrated in the respective LwM2M + specification. + +Update Result (/33629/x/5) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 5 +:Operations: R +:Instances: Single +:Mandatory: Yes +:Type: Integer +:Range or Enumeration: 0..13 +:Units: \- +:Description: + Contains the result of downloading or updating the firmware. + + | **0**: Initial value. Once the updating process is initiated (Download + /Update), this Resource MUST be reset to Initial value. + | **1:** Firmware updated successfully. + | **2:** Not enough flash memory for the new firmware package. + | **3:** Out of RAM during the downloading process. + | **4:** Connection lost during the downloading process. + | **5:** Integrity check failure for new downloaded package. + | **6:** Unsupported package type. + | **7:** Invalid URI. + | **8:** Firmware update failed. + | **9:** Unsupported protocol. An LwM2M client indicates the failure to + retrieve the firmware package using the URI provided in the Package URI + resource by writing the value 9 to the ``/33629/0/5`` (Update Result + resource) when the URI contained a URI scheme unsupported by the client. + Consequently, the LwM2M Client is unable to retrieve the firmware package + using the URI provided by the LwM2M Server in the Package URI when it + refers to an unsupported protocol. + | **10:** Firmware update cancelled. A Cancel operation has been executed + successfully. + | **11:** Firmware update deferred. + | **12:** Conflicting state. Multi-component firmware package download is + rejected before entering the Downloaded state because it conflicts with an + already downloaded package in a different object instance. + | **13:** Dependency error. The Update operation failed because the component + package requires some other component to be updated first or at the same + time. + +PkgName (/33629/x/6) +^^^^^^^^^^^^^^^^^^^^ + +:ID: 6 +:Operations: R +:Instances: Single +:Mandatory: No +:Type: String +:Range or Enumeration: \- +:Units: \- +:Description: + Name of the Firmware Package. If this resource is supported, it shall contain + the name of the downloaded package when the State is 2 (Downloaded) or 3 + (Updating); otherwise it MAY be empty. + +PkgVersion (/33629/x/7) +^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 7 +:Operations: R +:Instances: Single +:Mandatory: No +:Type: String +:Range or Enumeration: \- +:Units: \- +:Description: + Version of the Firmware package. If this resource is supported, it shall + contain the version of the downloaded package when the State is 2 + (Downloaded) or 3 (Updating); otherwise it MAY be empty. + +Firmware Update Protocol Support (/33629/x/8) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 8 +:Operations: R +:Instances: Multiple +:Mandatory: No +:Type: Integer +:Range or Enumeration: 0..5 +:Units: \- +:Description: + This resource indicates what protocols the LwM2M Client implements to + retrieve firmware packages. The LwM2M server uses this information to decide + what URI to include in the Package URI. An LwM2M Server MUST NOT include a URI + in the Package URI object that uses a protocol that is unsupported by the + LwM2M client. For example, if an LwM2M client indicates that it supports CoAP + and CoAPS then an LwM2M Server must not provide an HTTP URI in the Packet URI. + The following values are defined by this version of the specification: + + | **0:** CoAP (as defined in ``RFC 7252``) with the additional support for + block-wise transfer. CoAP is the default setting. + | **1:** CoAPS (as defined in ``RFC 7252``) with the additional support for + block-wise transfer + | **2:** HTTP 1.1 (as defined in ``RFC 7230``) + | **3:** HTTPS 1.1 (as defined in ``RFC 7230``) + | **4:** CoAP over TCP (as defined in ``RFC 8323``) + | **5:** CoAP over TLS (as defined in ``RFC 8323``) + + Additional values MAY be defined in the future. Any value not understood by + the LwM2M Server MUST be ignored. + + The value of this resource SHOULD be the same for all instances of the + Advanced Firmware Update object. + +Firmware Update Delivery Method (/33629/x/9) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 9 +:Operations: R +:Instances: Single +:Mandatory: Yes +:Type: Integer +:Range or Enumeration: 0..2 +:Units: \- +:Description: + The LwM2M Client uses this resource to indicate its support for transferring + firmware packages to the client either via the Package Resource (=push) or via + the Package URI Resource (=pull) mechanism. + + | **0:** Pull only + | **1:** Push only + | **2:** Both. In this case the LwM2M Server MAY choose the preferred + mechanism for conveying the firmware package to the LwM2M Client. + + The value of this resource SHOULD be the same for all instances of the + Advanced Firmware Update object. + +Cancel (/33629/x/10) +^^^^^^^^^^^^^^^^^^^^ + +:ID: 10 +:Operations: E +:Instances: Single +:Mandatory: No +:Type: \- +:Range or Enumeration: \- +:Units: \- +:Description: + Cancels firmware update. Cancel can be executed if the device has not + initiated the Update process. If the device is in the process of installing + the firmware or has already completed installation it MUST respond with + Method Not Allowed error code. Upon successful Cancel operation, Update + Result Resource is set to 10 and State is set to 0 by the device. + +Severity (/33629/x/11) +^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 11 +:Operations: RW +:Instances: Single +:Mandatory: No +:Type: Integer +:Range or Enumeration: 0..2 +:Units: \- +:Description: + Severity of the firmware package. + + | **0:** Critical + | **1:** Mandatory + | **2:** Optional + + This information is useful when the device provides an option for the + deferred update. Default value is 1. + +Last State Change Time (/33629/x/12) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 12 +:Operations: R +:Instances: Single +:Mandatory: No +:Type: Time +:Range or Enumeration: \- +:Units: \- +:Description: + This resource stores the time when the State resource is changed. Device + updates this resource before making any change to the State. + +Maximum Defer Period (/33629/x/13) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 13 +:Operations: RW +:Instances: Single +:Mandatory: No +:Type: Unsigned Integer +:Range or Enumeration: \- +:Units: s +:Description: + The number of seconds a user can defer the software update. When this time + period is over, the device will not prompt the user for update and install it + automatically. If the value is 0, a deferred update is not allowed. + +Component Name (/33629/x/14) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 14 +:Operations: R +:Instances: Single +:Mandatory: No +:Type: String +:Range or Enumeration: \- +:Units: \- +:Description: + Name of the component handled by this instance of the Advanced Firmware + Update object. + + This should be a name clearly identifying the component for both humans and + machines. The syntax of these names is implementation-specific, but might + refer to terms such as “bootloader”, “application”, “modem firmware” etc. + +Current Version (/33629/x/15) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 15 +:Operations: R +:Instances: Single +:Mandatory: Yes +:Type: String +:Range or Enumeration: \- +:Units: \- +:Description: + Version number of the package that is currently installed and running for the + component handled by this instance of the Advanced Firmware Update object. + + For the main component (the one that contains code for the core part of the + device's functionality), this value SHOULD be the same as the Firmware + Version resource in the Device object (``/3/0/3``). + +Linked Instances (/33629/x/16) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 16 +:Operations: R +:Instances: Multiple +:Mandatory: No +:Type: Objlnk +:Range or Enumeration: \- +:Units: \- +:Description: + When multiple instances of the Advanced Firmware Update object are in the + Downloaded state, this resource shall list all other instances that will be + updated in a batch if the Update resource is executed on this instance with + no arguments. Each of the instances listed MUST be in the Downloaded state. + + The resource MUST NOT contain references to any objects other than the + Advanced Firmware Update object. + +Conflicting Instances (/33629/x/17) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ID: 17 +:Operations: R +:Instances: Multiple +:Mandatory: No +:Type: Objlnk +:Range or Enumeration: \- +:Units: \- +:Description: + When the download or update fails and the Update Result resource is set to 12 + or 13, this resource MUST be present and contain references to the Advanced + Firmware Update object instances that caused the conflict. + + In other states, this resource MAY be absent or empty, or it MAY contain + references to the Advanced Firmware Update object instances which are in a + state conflicting with the possibility of successfully updating this + instance. + + The resource MUST NOT contain references to any objects other than the + Advanced Firmware Update object. diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-StateDiagram.rst b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-StateDiagram.rst new file mode 100644 index 000000000..6cf3897d1 --- /dev/null +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/FU-AFU-StateDiagram.rst @@ -0,0 +1,35 @@ +.. + Copyright 2017-2023 AVSystem + AVSystem Anjay LwM2M SDK + All rights reserved. + + Licensed under the AVSystem-5-clause License. + See the attached LICENSE file for details. + + +Firmware Update State Diagram +============================= + +.. figure:: _images/FU-AFU-StateDiagram.svg + +The figure above shows an implementation of the **Advanced Firmware Update** +mechanism. The state diagram consists of states, drawn as rounded rectangles, +and transitions, drawn as arrows connecting the states. The syntax of the +transition is trigger [guard] / behavior. A trigger is an event that may cause +a transition, a guard is a condition and behavior is an activity that executes +while the transition takes place. The states additionally contain a compartment +that includes assertions and variable assignments. For example, the assertion +in the IDLE state indicates the value of the “Update Result” resource +(abbreviated as “Res”) must be between 0 and 13. The State resource is set to +zero (0) when the program is in this IDLE state. + +Any operation to the Firmware Update Object that would result in undefined +behavior because that specific operation is not identified as a trigger in the +state machine (e.g. Write to Package URI while in DOWNLOADING state) SHOULD be +rejected. + +Errors during the Firmware Update process MUST be reported only by the “Update +Result” resource. For instance, when the LwM2M Server performs a Write +operation on resource “Package URI”, the LwM2M Client returns a Success or +Failure for the Write operation. Then if the URI is invalid, it changes its +“Update Result” resource value to 7. diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/_files/33629.xml b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/_files/33629.xml new file mode 100644 index 000000000..53d329771 --- /dev/null +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/_files/33629.xml @@ -0,0 +1,257 @@ + + + + + + + Advanced Firmware Update + + 33629 + urn:oma:lwm2m:oma:33629 + 1.1 + 1.0 + Multiple + Optional + + + Package + W + Single + Mandatory + Opaque + + + + + + Package URI + RW + Single + Mandatory + String + 0..255 + + + + + Update + E + Single + Mandatory + + + + ,'), the client MUST attempt to update the components handled by the current instance and the instances listed in the argument, and MUST NOT attempt to update any other components. If the client is not able to satisfy such a request, the update process shall fail with the Update Result resource set to 13. +If the downloaded packages are incompatible with at least one of the other currently installed components, and compatible updates for them are not downloaded (i.e., the State resource in an instance corresponding to the conflicting component is not Downloaded), the update process shall also fail with the Update Result resource set to 13. +When multiple components are upgraded as part of a single Update operation, the device SHOULD upgrade them in a transactional fashion (i.e., all are updated successfully, or all are reverted in case of error), and MUST perform the upgrade in a way that ensures that the device will not be rendered unbootable due to partial errors.]]> + + + State + R + Single + Mandatory + Integer + 0..3 + + + + + Update Result + R + Single + Mandatory + Integer + 0..13 + + + + + PkgName + R + Single + Optional + String + 0..255 + + + + + PkgVersion + R + Single + Optional + String + 0..255 + + + + + Firmware Update Protocol Support + R + Multiple + Optional + Integer + 0..5 + + + + + Firmware Update Delivery Method + R + Single + Mandatory + Integer + 0..2 + + + + + Cancel + E + Single + Optional + + + + + + + Severity + RW + Single + Optional + Integer + 0..2 + + + + + Last State Change Time + R + Single + Optional + Time + + + + + + Maximum Defer Period + RW + Single + Optional + Unsigned Integer + + s + + + + Component Name + R + Single + Mandatory + String + + + + + + Current Version + R + Single + Mandatory + String + + + + + + Linked Instances + R + Multiple + Optional + Objlnk + + + + + + Conflicting Instances + R + Multiple + Optional + Objlnk + + + + + + + + diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/_images/FU-AFU-StateDiagram.svg b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/_images/FU-AFU-StateDiagram.svg new file mode 100644 index 000000000..5ff7dab1d --- /dev/null +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-AdvancedFirmwareUpdate/_images/FU-AFU-StateDiagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-BasicImplementation.rst b/doc/sphinx/source/FirmwareUpdateTutorial/FU-BasicImplementation.rst index 96d2d8411..ebeab555f 100644 --- a/doc/sphinx/source/FirmwareUpdateTutorial/FU-BasicImplementation.rst +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-BasicImplementation.rst @@ -30,6 +30,8 @@ chapter. In the end, our project structure would look as follows: Note the ``firmware_update.c`` and ``firmware_update.h`` are introduced in this chapter. +.. _anjay_fw_update_install: + Installing the Firmware Update module ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-Introduction.rst b/doc/sphinx/source/FirmwareUpdateTutorial/FU-Introduction.rst index 0ff477663..084d6da5d 100644 --- a/doc/sphinx/source/FirmwareUpdateTutorial/FU-Introduction.rst +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-Introduction.rst @@ -118,7 +118,7 @@ give an idea on what the implementation of FOTA would take: anjay_fw_update_get_security_config_t *get_security_config; /** Queries CoAP transmission parameters to be used during firmware - * update. */ + * update; @ref anjay_fw_update_get_coap_tx_params_t */ anjay_fw_update_get_coap_tx_params_t *get_coap_tx_params; } anjay_fw_update_handlers_t; diff --git a/doc/sphinx/source/FirmwareUpdateTutorial/FU-SecureDownloads.rst b/doc/sphinx/source/FirmwareUpdateTutorial/FU-SecureDownloads.rst index a09311cbd..17202e27b 100644 --- a/doc/sphinx/source/FirmwareUpdateTutorial/FU-SecureDownloads.rst +++ b/doc/sphinx/source/FirmwareUpdateTutorial/FU-SecureDownloads.rst @@ -292,7 +292,7 @@ by the user: anjay_fw_update_get_security_config_t *get_security_config; /** Queries CoAP transmission parameters to be used during firmware - * update. */ + * update; @ref anjay_fw_update_get_coap_tx_params_t */ anjay_fw_update_get_coap_tx_params_t *get_coap_tx_params; } anjay_fw_update_handlers_t; diff --git a/doc/sphinx/source/Migrating.rst b/doc/sphinx/source/Migrating.rst index 6b18d6a3c..b26f52f1c 100644 --- a/doc/sphinx/source/Migrating.rst +++ b/doc/sphinx/source/Migrating.rst @@ -32,3 +32,5 @@ Migrating from older versions Migrating/MigratingFromAnjay214 Migrating/MigratingFromAnjay215 Migrating/MigratingFromAnjay30 + Migrating/MigratingFromAnjay32 + Migrating/MigratingFromAnjay33 diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay214.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay214.rst index 270964b91..bf436a031 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay214.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay214.rst @@ -263,3 +263,46 @@ This new API is used by the Security object implementation's features related to the ``anjay_security_object_install_with_hsm()``. If you don't use these features to store private keys in the hardware security engine, it is OK to provide a dummy implementation such as ``return avs_errno(AVS_ENOTSUP);``. + +Refactor of time handling in avs_sched and avs_coap +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + +Removal of avs_unit_memstream +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``avs_unit_memstream`` was a specific implementation of ``avs_stream_t`` within +the avs_unit module that implemented a simple FIFO stream in a fixed-size memory +area. + +This feature has been removed. Instead, you can use an +``avs_stream_inbuf``/``avs_stream_outbuf`` pair, or an ``avs_stream_membuf`` +object. diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay215.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay215.rst index b5190da0e..fb7dabbf1 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay215.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay215.rst @@ -158,3 +158,46 @@ function has also been replaced with function previously known as These changes are breaking for code that accesses the ``data.psk`` field of ``avs_net_security_info_t`` directly and for usages of the two changed types. + +Refactor of time handling in avs_sched and avs_coap +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + +Removal of avs_unit_memstream +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``avs_unit_memstream`` was a specific implementation of ``avs_stream_t`` within +the avs_unit module that implemented a simple FIFO stream in a fixed-size memory +area. + +This feature has been removed. Instead, you can use an +``avs_stream_inbuf``/``avs_stream_outbuf`` pair, or an ``avs_stream_membuf`` +object. diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay225.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay225.rst index ec3813430..6589d8efe 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay225.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay225.rst @@ -779,3 +779,46 @@ the raw CoAP APIs of that component, you will need to migrate to either the new serializing, sending and receiving raw, isolated messages (as opposed to proper, conformant CoAP exchanges), is not provided in the public API for this reason. + +Refactor of time handling in avs_sched and avs_coap +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + +Removal of avs_unit_memstream +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``avs_unit_memstream`` was a specific implementation of ``avs_stream_t`` within +the avs_unit module that implemented a simple FIFO stream in a fixed-size memory +area. + +This feature has been removed. Instead, you can use an +``avs_stream_inbuf``/``avs_stream_outbuf`` pair, or an ``avs_stream_membuf`` +object. diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay24.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay24.rst index 4e01d5387..a52846f0a 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay24.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay24.rst @@ -354,3 +354,46 @@ implementation, but now it is a ``static inline`` function that wraps ``avs_net_socket_*()`` APIs. Please remove your version of ``avs_net_local_address_for_target_host()`` from your socket implementation if you have one, as having two alternative variants may lead to conflicts. + +Refactor of time handling in avs_sched and avs_coap +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + +Removal of avs_unit_memstream +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``avs_unit_memstream`` was a specific implementation of ``avs_stream_t`` within +the avs_unit module that implemented a simple FIFO stream in a fixed-size memory +area. + +This feature has been removed. Instead, you can use an +``avs_stream_inbuf``/``avs_stream_outbuf`` pair, or an ``avs_stream_membuf`` +object. diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay26.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay26.rst index 59517f7ff..a32edb2cb 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay26.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay26.rst @@ -344,3 +344,46 @@ implementation, but now it is a ``static inline`` function that wraps ``avs_net_socket_*()`` APIs. Please remove your version of ``avs_net_local_address_for_target_host()`` from your socket implementation if you have one, as having two alternative variants may lead to conflicts. + +Refactor of time handling in avs_sched and avs_coap +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + +Removal of avs_unit_memstream +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``avs_unit_memstream`` was a specific implementation of ``avs_stream_t`` within +the avs_unit module that implemented a simple FIFO stream in a fixed-size memory +area. + +This feature has been removed. Instead, you can use an +``avs_stream_inbuf``/``avs_stream_outbuf`` pair, or an ``avs_stream_membuf`` +object. diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay27.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay27.rst index 3fb7b0d40..e6672a27e 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay27.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay27.rst @@ -317,3 +317,46 @@ loosened, making it possible to replace the reference implementation based on * Enabling the aforementioned flag is now a dependency for enabling ``WITH_OPENSSL_PKCS11_ENGINE`` (CMake) / ``AVS_COMMONS_WITH_OPENSSL_PKCS11_ENGINE`` (header) + +Refactor of time handling in avs_sched and avs_coap +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + +Removal of avs_unit_memstream +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``avs_unit_memstream`` was a specific implementation of ``avs_stream_t`` within +the avs_unit module that implemented a simple FIFO stream in a fixed-size memory +area. + +This feature has been removed. Instead, you can use an +``avs_stream_inbuf``/``avs_stream_outbuf`` pair, or an ``avs_stream_membuf`` +object. diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay28.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay28.rst index 77858c0f4..cf0ee710a 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay28.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay28.rst @@ -305,3 +305,46 @@ This new API is used by the Security object implementation's features related to the ``anjay_security_object_install_with_hsm()``. If you don't use these features to store private keys in the hardware security engine, it is OK to provide a dummy implementation such as ``return avs_errno(AVS_ENOTSUP);``. + +Refactor of time handling in avs_sched and avs_coap +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + +Removal of avs_unit_memstream +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``avs_unit_memstream`` was a specific implementation of ``avs_stream_t`` within +the avs_unit module that implemented a simple FIFO stream in a fixed-size memory +area. + +This feature has been removed. Instead, you can use an +``avs_stream_inbuf``/``avs_stream_outbuf`` pair, or an ``avs_stream_membuf`` +object. diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay30.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay30.rst index a2dc6cac5..fb972641c 100644 --- a/doc/sphinx/source/Migrating/MigratingFromAnjay30.rst +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay30.rst @@ -20,8 +20,11 @@ Since Anjay 3.0, some minor changes to the API has been done. These change are breaking in the strictest sense, but in practice should not require any changes to user code in typical usage. +Changes in Anjay proper +----------------------- + Behavior of anjay_attr_storage_restore() upon failure ------------------------------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Deprecated behavior of ``anjay_attr_storage_restore()`` which did clear the Attribute Storage if supplied source stream was invalid has been changed. From @@ -30,7 +33,7 @@ This change is breaking for code which relied on the old behavior, although such code is unlikely. Default (D)TLS version ----------------------- +^^^^^^^^^^^^^^^^^^^^^^ When the `anjay_configuration_t::dtls_version <../api/structanjay__configuration.html#ab32477e7370a36e02db5b7e7ccbdd89d>`_ @@ -50,3 +53,50 @@ version. Please explicitly set ``dtls_version`` to Please note that Mbed TLS 3.0 has dropped support for TLS 1.1 and earlier, so this change will not affect behavior with that library. + + +Changes in avs_commons +---------------------- + +Refactor of time handling in avs_sched and avs_coap +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + +Removal of avs_unit_memstream +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``avs_unit_memstream`` was a specific implementation of ``avs_stream_t`` within +the avs_unit module that implemented a simple FIFO stream in a fixed-size memory +area. + +This feature has been removed. Instead, you can use an +``avs_stream_inbuf``/``avs_stream_outbuf`` pair, or an ``avs_stream_membuf`` +object. diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay32.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay32.rst new file mode 100644 index 000000000..2f39170fd --- /dev/null +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay32.rst @@ -0,0 +1,71 @@ +.. + Copyright 2017-2023 AVSystem + AVSystem Anjay LwM2M SDK + All rights reserved. + + Licensed under the AVSystem-5-clause License. + See the attached LICENSE file for details. + +Migrating from Anjay 3.1 or 3.2 +=============================== + +.. contents:: :local: + +.. highlight:: c + +Introduction +------------ + +avs_commons 5.2.0 that is used by Anjay 3.3.0, has removed one specific +functionality of the avs_unit module that has not been used by Anjay. If you are +developing your own unit tests based on avs_unit, you may need to update them. + +Since Anjay 3.4.0 and avs_commons 5.4.0, time handling in avs_sched and avs_coap +has been refactored, which slightly redefined existing APIs. These changes +should not require any changes to user code in typical usage, but may break some +edge cases, especially on platforms where the system clock has a low resolution +and you are scheduling custom jobs through the avs_sched module. + +Refactor of time handling in avs_sched and avs_coap +--------------------------------------------------- + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + + +Removal of avs_unit_memstream +----------------------------- + +``avs_unit_memstream`` was a specific implementation of ``avs_stream_t`` within +the avs_unit module that implemented a simple FIFO stream in a fixed-size memory +area. + +This feature has been removed. Instead, you can use an +``avs_stream_inbuf``/``avs_stream_outbuf`` pair, or an ``avs_stream_membuf`` +object. diff --git a/doc/sphinx/source/Migrating/MigratingFromAnjay33.rst b/doc/sphinx/source/Migrating/MigratingFromAnjay33.rst new file mode 100644 index 000000000..176705f8c --- /dev/null +++ b/doc/sphinx/source/Migrating/MigratingFromAnjay33.rst @@ -0,0 +1,57 @@ +.. + Copyright 2017-2023 AVSystem + AVSystem Anjay LwM2M SDK + All rights reserved. + + Licensed under the AVSystem-5-clause License. + See the attached LICENSE file for details. + +Migrating from Anjay 3.3 +======================== + +.. contents:: :local: + +.. highlight:: c + +Introduction +------------ + +Since Anjay 3.4.0 and avs_commons 5.4.0, time handling in avs_sched and avs_coap +has been refactored, which slightly redefined existing APIs. + +These changes should not require any changes to user code in typical usage, but +may break some edge cases, especially on platforms where the system clock has a +low resolution and you are scheduling custom jobs through the avs_sched module. + +Refactor of time handling in avs_sched and avs_coap +--------------------------------------------------- + +It is now enforced more strictly that time-based events shall happen when the +clock reaches *at least* the expected value. Previously, the tasks scheduled via +avs_sched were executed only when the clock reached a value *later* than the +scheduled job execution time. + +This change will have no impact on your code if your platform has enough clock +resolution so that two subsequent calls to ``avs_time_real_now()`` or +``avs_time_monotonic_now()`` will *always* return different values. As a rule of +thumb, this should be the case if your clock has a resolution no worse than +about 1-2 orders of magnitude smaller than the CPU clock. For example, for a +100 MHz CPU, a clock resolution of around 100-1000 ns (i.e., 1-10 MHz) should be +sufficient, depending on the specific architecture. + +If your clock has a lower resolution, you may observe the following changes: + +* ``anjay_sched_run()`` is now properly guaranteed to execute at least one job + if the time reported by ``anjay_sched_time_to_next()`` passed. Previously this + could require waiting for another change of the numerical value of the clock, + which could cause undesirable active waiting in the event loop. This is the + motivating factor in introducing these changes. +* Jobs scheduled using ``AVS_SCHED_NOW()`` during an execution of + ``anjay_sched_run()`` before the numerical value of the clock changes, *will* + be executed during the same run. The previous behavior more strictly enforced + the policy to not execute such jobs in the same run. + +If you are scheduling custom jobs through the avs_sched module, you may want or +need to modify their logic accordingly to accommodate for these changes. In most +typical use cases, no changes are expected to be necessary. + diff --git a/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/CustomTLS/CustomTLS-CertificatesBasic.rst b/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/CustomTLS/CustomTLS-CertificatesBasic.rst index c16fa2d52..e71d2c276 100644 --- a/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/CustomTLS/CustomTLS-CertificatesBasic.rst +++ b/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/CustomTLS/CustomTLS-CertificatesBasic.rst @@ -506,11 +506,11 @@ The implementation above is a complete basic integration with the private key infrastructure, however it lacks a number of features that are supported by the ``avs_net`` API: -* Lack of DANE support. **This means that this the Server Public Key LwM2M - resource is not supported, and will cause a failure if used.** This is - because LwM2M does not use standard certificate validation logic based on a - trust store, using a custom mechanism instead. However, that mechanism is - almost identical to the one used by `DANE +* Lack of DANE support. **This means that the Server Public Key LwM2M resource + is not supported, and will cause a failure if used.** This is because LwM2M + does not use standard certificate validation logic based on a trust store, + using a custom mechanism instead. However, that mechanism is almost identical + to the one used by `DANE `_, so it is implemented in terms of that mechanism in Anjay and ``avs_net``. diff --git a/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/CustomTLS/CustomTLS-Minimal.rst b/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/CustomTLS/CustomTLS-Minimal.rst index 01ad22d1e..5d13f21c7 100644 --- a/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/CustomTLS/CustomTLS-Minimal.rst +++ b/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/CustomTLS/CustomTLS-Minimal.rst @@ -338,7 +338,7 @@ identity. static avs_error_t configure_psk(tls_socket_impl_t *sock, const avs_net_psk_info_t *psk) { - if (!psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER + if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) { return avs_errno(AVS_EINVAL); } diff --git a/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/TimeAPI.rst b/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/TimeAPI.rst index c3eb78282..cdf1b0f0b 100644 --- a/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/TimeAPI.rst +++ b/doc/sphinx/source/PortingGuideForNonPOSIXPlatforms/TimeAPI.rst @@ -32,6 +32,8 @@ The default implementation that uses POSIX ``clock_gettime()`` API can be used as a reference for writing your own integration layer. +.. _timeapi_avs_time_real_now: + avs_time_real_now() ^^^^^^^^^^^^^^^^^^^ diff --git a/doc/sphinx/source/conf.py.in b/doc/sphinx/source/conf.py.in index 4086cca82..8df4eee38 100644 --- a/doc/sphinx/source/conf.py.in +++ b/doc/sphinx/source/conf.py.in @@ -34,7 +34,8 @@ sys.path.insert(0, '@ANJAY_SOURCE_DIR@/doc/sphinx/extensions') extensions = ['snippet_source', 'builders.snippet_source_linter', 'builders.snippet_source_list_references', - 'sphinx_rtd_theme'] + 'sphinx_rtd_theme', + 'linuxdoc.rstFlatTable'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/example_configs/embedded_lwm2m10/anjay/anjay_config.h b/example_configs/embedded_lwm2m10/anjay/anjay_config.h index 2706392d7..a93c138e0 100644 --- a/example_configs/embedded_lwm2m10/anjay/anjay_config.h +++ b/example_configs/embedded_lwm2m10/anjay/anjay_config.h @@ -256,6 +256,12 @@ */ /* #undef ANJAY_WITH_CORE_PERSISTENCE */ +/** + * Disable automatic closing of server connection sockets after + * MAX_TRANSMIT_WAIT of inactivity. + */ +/* #undef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE */ + /** * Enable support for CoAP Content-Format numerical values 1541-1544 that have * been used before final LwM2M TS 1.0. @@ -500,6 +506,12 @@ */ #define ANJAY_WITH_MODULE_FW_UPDATE +/** + * Enable advanced_fw_update module (implementation of the 33629 custom + * Advanced Firmware Update object). + */ +/* #undef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE */ + /** * Disable support for PUSH mode Firmware Update. * @@ -545,7 +557,7 @@ * AVS_COMMONS_WITH_AVS_PERSISTENCE to be enabled in avs_commons * configuration. * - * IMPORTANT: Only available with the boostrapper feature. Ignored in the open + * IMPORTANT: Only available with the bootstrapper feature. Ignored in the open * source version. */ /* #undef ANJAY_WITH_MODULE_BOOTSTRAPPER */ diff --git a/example_configs/embedded_lwm2m11/anjay/anjay_config.h b/example_configs/embedded_lwm2m11/anjay/anjay_config.h index c748e24df..a7619d45b 100644 --- a/example_configs/embedded_lwm2m11/anjay/anjay_config.h +++ b/example_configs/embedded_lwm2m11/anjay/anjay_config.h @@ -256,6 +256,12 @@ */ /* #undef ANJAY_WITH_CORE_PERSISTENCE */ +/** + * Disable automatic closing of server connection sockets after + * MAX_TRANSMIT_WAIT of inactivity. + */ +/* #undef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE */ + /** * Enable support for CoAP Content-Format numerical values 1541-1544 that have * been used before final LwM2M TS 1.0. @@ -500,6 +506,12 @@ */ #define ANJAY_WITH_MODULE_FW_UPDATE +/** + * Enable advanced_fw_update module (implementation of the 33629 custom + * Advanced Firmware Update object). + */ +/* #undef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE */ + /** * Disable support for PUSH mode Firmware Update. * @@ -545,7 +557,7 @@ * AVS_COMMONS_WITH_AVS_PERSISTENCE to be enabled in avs_commons * configuration. * - * IMPORTANT: Only available with the boostrapper feature. Ignored in the open + * IMPORTANT: Only available with the bootstrapper feature. Ignored in the open * source version. */ /* #undef ANJAY_WITH_MODULE_BOOTSTRAPPER */ diff --git a/example_configs/linux_lwm2m10/anjay/anjay_config.h b/example_configs/linux_lwm2m10/anjay/anjay_config.h index ad30929e0..7d45e7f78 100644 --- a/example_configs/linux_lwm2m10/anjay/anjay_config.h +++ b/example_configs/linux_lwm2m10/anjay/anjay_config.h @@ -256,6 +256,12 @@ */ /* #undef ANJAY_WITH_CORE_PERSISTENCE */ +/** + * Disable automatic closing of server connection sockets after + * MAX_TRANSMIT_WAIT of inactivity. + */ +/* #undef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE */ + /** * Enable support for CoAP Content-Format numerical values 1541-1544 that have * been used before final LwM2M TS 1.0. @@ -500,6 +506,12 @@ */ #define ANJAY_WITH_MODULE_FW_UPDATE +/** + * Enable advanced_fw_update module (implementation of the 33629 custom + * Advanced Firmware Update object). + */ +/* #undef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE */ + /** * Disable support for PUSH mode Firmware Update. * @@ -545,7 +557,7 @@ * AVS_COMMONS_WITH_AVS_PERSISTENCE to be enabled in avs_commons * configuration. * - * IMPORTANT: Only available with the boostrapper feature. Ignored in the open + * IMPORTANT: Only available with the bootstrapper feature. Ignored in the open * source version. */ /* #undef ANJAY_WITH_MODULE_BOOTSTRAPPER */ diff --git a/example_configs/linux_lwm2m11/anjay/anjay_config.h b/example_configs/linux_lwm2m11/anjay/anjay_config.h index 7f140d55e..0754bde8e 100644 --- a/example_configs/linux_lwm2m11/anjay/anjay_config.h +++ b/example_configs/linux_lwm2m11/anjay/anjay_config.h @@ -256,6 +256,12 @@ */ /* #undef ANJAY_WITH_CORE_PERSISTENCE */ +/** + * Disable automatic closing of server connection sockets after + * MAX_TRANSMIT_WAIT of inactivity. + */ +/* #undef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE */ + /** * Enable support for CoAP Content-Format numerical values 1541-1544 that have * been used before final LwM2M TS 1.0. @@ -500,6 +506,12 @@ */ #define ANJAY_WITH_MODULE_FW_UPDATE +/** + * Enable advanced_fw_update module (implementation of the 33629 custom + * Advanced Firmware Update object). + */ +/* #undef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE */ + /** * Disable support for PUSH mode Firmware Update. * @@ -545,7 +557,7 @@ * AVS_COMMONS_WITH_AVS_PERSISTENCE to be enabled in avs_commons * configuration. * - * IMPORTANT: Only available with the boostrapper feature. Ignored in the open + * IMPORTANT: Only available with the bootstrapper feature. Ignored in the open * source version. */ /* #undef ANJAY_WITH_MODULE_BOOTSTRAPPER */ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 7f3ca3919..395683f72 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -27,6 +27,7 @@ add_custom_command(TARGET tutorial_examples -DCMAKE_INSTALL_PREFIX="${ANJAY_TUTORIALS_ANJAY_INSTALL_DIR}" -DWITH_LIBRARY_SHARED=OFF -DWITH_DEMO=OFF + -DWITH_MODULE_advanced_fw_update=ON -DCMAKE_BUILD_TYPE=Debug COMMAND ${CMAKE_COMMAND} --build . --target install -- -j${NPROC} WORKING_DIRECTORY ${ANJAY_TUTORIALS_ANJAY_BUILD_DIR}) diff --git a/examples/custom-tls/certificates-advanced-fake-dane/src/tls_impl.c b/examples/custom-tls/certificates-advanced-fake-dane/src/tls_impl.c index 1168b7078..5fb695bae 100644 --- a/examples/custom-tls/certificates-advanced-fake-dane/src/tls_impl.c +++ b/examples/custom-tls/certificates-advanced-fake-dane/src/tls_impl.c @@ -436,7 +436,7 @@ static unsigned int psk_client_cb(SSL *ssl, static avs_error_t configure_psk(tls_socket_impl_t *sock, const avs_net_psk_info_t *psk) { - if (!psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER + if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) { return avs_errno(AVS_EINVAL); } diff --git a/examples/custom-tls/certificates-advanced/src/tls_impl.c b/examples/custom-tls/certificates-advanced/src/tls_impl.c index 7a1a7b6b4..72e9b4e7b 100644 --- a/examples/custom-tls/certificates-advanced/src/tls_impl.c +++ b/examples/custom-tls/certificates-advanced/src/tls_impl.c @@ -423,7 +423,7 @@ static unsigned int psk_client_cb(SSL *ssl, static avs_error_t configure_psk(tls_socket_impl_t *sock, const avs_net_psk_info_t *psk) { - if (!psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER + if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) { return avs_errno(AVS_EINVAL); } diff --git a/examples/custom-tls/certificates-basic/src/tls_impl.c b/examples/custom-tls/certificates-basic/src/tls_impl.c index 2cfffbbd8..5e2a3bc91 100644 --- a/examples/custom-tls/certificates-basic/src/tls_impl.c +++ b/examples/custom-tls/certificates-basic/src/tls_impl.c @@ -346,7 +346,7 @@ static unsigned int psk_client_cb(SSL *ssl, static avs_error_t configure_psk(tls_socket_impl_t *sock, const avs_net_psk_info_t *psk) { - if (!psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER + if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) { return avs_errno(AVS_EINVAL); } diff --git a/examples/custom-tls/config-features/src/tls_impl.c b/examples/custom-tls/config-features/src/tls_impl.c index 385eda8f4..4071abf72 100644 --- a/examples/custom-tls/config-features/src/tls_impl.c +++ b/examples/custom-tls/config-features/src/tls_impl.c @@ -344,7 +344,7 @@ static unsigned int psk_client_cb(SSL *ssl, static avs_error_t configure_psk(tls_socket_impl_t *sock, const avs_net_psk_info_t *psk) { - if (!psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER + if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) { return avs_errno(AVS_EINVAL); } diff --git a/examples/custom-tls/minimal/src/tls_impl.c b/examples/custom-tls/minimal/src/tls_impl.c index 64a375c48..08da3a158 100644 --- a/examples/custom-tls/minimal/src/tls_impl.c +++ b/examples/custom-tls/minimal/src/tls_impl.c @@ -284,7 +284,7 @@ static unsigned int psk_client_cb(SSL *ssl, static avs_error_t configure_psk(tls_socket_impl_t *sock, const avs_net_psk_info_t *psk) { - if (!psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER + if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) { return avs_errno(AVS_EINVAL); } diff --git a/examples/custom-tls/resumption-buffer/src/tls_impl.c b/examples/custom-tls/resumption-buffer/src/tls_impl.c index bf307538c..c3e642119 100644 --- a/examples/custom-tls/resumption-buffer/src/tls_impl.c +++ b/examples/custom-tls/resumption-buffer/src/tls_impl.c @@ -302,7 +302,7 @@ static unsigned int psk_client_cb(SSL *ssl, static avs_error_t configure_psk(tls_socket_impl_t *sock, const avs_net_psk_info_t *psk) { - if (!psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER + if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) { return avs_errno(AVS_EINVAL); } diff --git a/examples/custom-tls/resumption-simple/src/tls_impl.c b/examples/custom-tls/resumption-simple/src/tls_impl.c index 90f79b169..ab602a56e 100644 --- a/examples/custom-tls/resumption-simple/src/tls_impl.c +++ b/examples/custom-tls/resumption-simple/src/tls_impl.c @@ -300,7 +300,7 @@ static unsigned int psk_client_cb(SSL *ssl, static avs_error_t configure_psk(tls_socket_impl_t *sock, const avs_net_psk_info_t *psk) { - if (!psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER + if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) { return avs_errno(AVS_EINVAL); } diff --git a/examples/custom-tls/tcp-support/src/tls_impl.c b/examples/custom-tls/tcp-support/src/tls_impl.c index 4cec5c508..01b02a3c9 100644 --- a/examples/custom-tls/tcp-support/src/tls_impl.c +++ b/examples/custom-tls/tcp-support/src/tls_impl.c @@ -464,7 +464,7 @@ static unsigned int psk_client_cb(SSL *ssl, static avs_error_t configure_psk(tls_socket_impl_t *sock, const avs_net_psk_info_t *psk) { - if (!psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER + if (psk->key.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER || psk->identity.desc.source != AVS_CRYPTO_DATA_SOURCE_BUFFER) { return avs_errno(AVS_EINVAL); } diff --git a/examples/tutorial/firmware-update/CMakeLists.txt b/examples/tutorial/firmware-update/CMakeLists.txt index 5c71ca594..279fc6d41 100644 --- a/examples/tutorial/firmware-update/CMakeLists.txt +++ b/examples/tutorial/firmware-update/CMakeLists.txt @@ -1,3 +1,4 @@ add_subdirectory(basic-implementation) add_subdirectory(secure-downloads) add_subdirectory(download-resumption) +add_subdirectory(advanced-firmware-update) diff --git a/examples/tutorial/firmware-update/advanced-firmware-update/CMakeLists.txt b/examples/tutorial/firmware-update/advanced-firmware-update/CMakeLists.txt new file mode 100644 index 000000000..3e9557531 --- /dev/null +++ b/examples/tutorial/firmware-update/advanced-firmware-update/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.1) +project(advanced-firmware-update C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_EXTENSIONS OFF) + +find_package(anjay REQUIRED) + +add_executable(${PROJECT_NAME} + src/main.c + src/advanced_firmware_update.c + src/advanced_firmware_update.h + src/time_object.c + src/time_object.h) +target_link_libraries(${PROJECT_NAME} PRIVATE anjay) diff --git a/examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c b/examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c new file mode 100644 index 000000000..a5bfc52e2 --- /dev/null +++ b/examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.c @@ -0,0 +1,643 @@ +#include "advanced_firmware_update.h" + +#include +#include +#include +#include +#include + +#define AFU_VERSION_STR_MAX_LEN 10 +#define AFU_INSTANCE_NAME_STR_MAX_LEN 10 +#define AFU_FILE_NAME_STR_MAX_LEN 50 + +typedef struct { + anjay_t *anjay; + char fw_version[AFU_NUMBER_OF_FIRMWARE_INSTANCES] + [AFU_VERSION_STR_MAX_LEN + 1]; + char instance_name[AFU_NUMBER_OF_FIRMWARE_INSTANCES] + [AFU_INSTANCE_NAME_STR_MAX_LEN + 1]; + FILE *new_firmware_file[AFU_NUMBER_OF_FIRMWARE_INSTANCES]; +} advanced_firmware_update_logic_t; + +static advanced_firmware_update_logic_t afu_logic; + +const char *ENDPOINT_NAME; + +static void get_firmware_download_name(int iid, char *buff) { + if (iid == AFU_DEFAULT_FIRMWARE_INSTANCE_IID) { + snprintf(buff, AFU_FILE_NAME_STR_MAX_LEN, "/tmp/firmware_image.bin"); + } else { + snprintf(buff, AFU_FILE_NAME_STR_MAX_LEN, "/tmp/add_image_%d", iid); + } +} + +static void get_add_firmware_file_name(int iid, char *buff) { + snprintf(buff, AFU_FILE_NAME_STR_MAX_LEN, "ADD_FILE_%d", iid); +} + +static void get_marker_file_name(int iid, char *buff) { + snprintf(buff, AFU_FILE_NAME_STR_MAX_LEN, "/tmp/fw-updated-marker_%d", iid); +} + +static int move_file(const char *dest, const char *source) { + int ret_val = -1; + FILE *dest_stream = NULL; + FILE *source_stream = fopen(source, "r"); + + if (!source_stream) { + avs_log(advance_fu, ERROR, "Could not open file: %s", source); + goto cleanup; + } + dest_stream = fopen(dest, "w"); + if (!dest_stream) { + avs_log(advance_fu, ERROR, "Could not open file: %s", dest); + fclose(source_stream); + goto cleanup; + } + + while (!feof(source_stream)) { + char buff[1024]; + size_t bytes_read_1 = fread(buff, 1, sizeof(buff), source_stream); + if (fwrite(buff, 1, bytes_read_1, dest_stream) != bytes_read_1) { + avs_log(advance_fu, ERROR, "Error during write to file: %s", dest); + goto cleanup; + } + } + ret_val = 0; + +cleanup: + if (dest_stream) { + if (fclose(dest_stream)) { + avs_log(advance_fu, ERROR, "Could not close file: %s", dest); + ret_val = -1; + } + } + if (source_stream) { + if (fclose(source_stream)) { + avs_log(advance_fu, ERROR, "Could not close file: %s", source); + ret_val = -1; + } + } + unlink(source); + + return ret_val; +} + +static void add_conflicting_instance(anjay_iid_t iid, anjay_iid_t target_iid) { + const anjay_iid_t *conflicting_instances; + anjay_iid_t conflicting_target_iids[AFU_NUMBER_OF_FIRMWARE_INSTANCES - 1]; + size_t conflicting_iids_count = 0; + + // get conflicting instances + anjay_advanced_fw_update_get_conflicting_instances(afu_logic.anjay, iid, + &conflicting_instances, + &conflicting_iids_count); + // add target_iid to the list + for (size_t i = 0; i < conflicting_iids_count; i++) { + conflicting_target_iids[i] = conflicting_instances[i]; + } + conflicting_target_iids[conflicting_iids_count++] = target_iid; + anjay_advanced_fw_update_set_conflicting_instances(afu_logic.anjay, iid, + conflicting_target_iids, + conflicting_iids_count); +} + +static bool is_update_requested(anjay_iid_t iid, + anjay_iid_t target_iid, + const anjay_iid_t *requested_supplemental_iids, + size_t requested_supplemental_iids_count, + const anjay_iid_t *linked_target_iids, + size_t linked_iids_count) { + if (iid == target_iid) { + return true; + } + if (requested_supplemental_iids) { + for (size_t i = 0; i < requested_supplemental_iids_count; i++) { + if (iid == requested_supplemental_iids[i]) { + return true; + } + } + } else if (linked_target_iids) { + for (size_t i = 0; i < linked_iids_count; i++) { + if (iid == linked_target_iids[i]) { + return true; + } + } + } + return false; +} + +static char get_firmware_major_version(anjay_iid_t iid, bool is_upgrade) { + char file_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + + if (is_upgrade == false) { + return afu_logic.fw_version[iid][0]; + } + + get_firmware_download_name(iid, file_name); + + // get value from new file + char buff; + FILE *stream = fopen(file_name, "r"); + if (!stream) { + avs_log(advance_fu, ERROR, "Could not open file: %s", file_name); + return ' '; + } + if (!fread(&buff, 1, 1, stream)) { + avs_log(advance_fu, ERROR, "Could not read from file file: %s", + file_name); + fclose(stream); + return ' '; + } + if (fclose(stream)) { + avs_log(advance_fu, ERROR, "Could not close file: %s", file_name); + } + + return buff; +} + +static int refresh_fw_version() { + memcpy(afu_logic.fw_version[AFU_DEFAULT_FIRMWARE_INSTANCE_IID], + AFU_DEFAULT_FIRMWARE_VERSION, strlen(AFU_DEFAULT_FIRMWARE_VERSION)); + + for (int i = 1; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + char buff[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_add_firmware_file_name(i, buff); + FILE *stream = fopen(buff, "r"); + if (!stream) { + avs_log(advance_fu, ERROR, "Could not open file with iid: %d", i); + return -1; + } + if (!fread(afu_logic.fw_version[i], 1, AFU_VERSION_STR_MAX_LEN, + stream)) { + avs_log(advance_fu, ERROR, "Could not read file with iid: %d", i); + fclose(stream); + return -1; + } + if (fclose(stream)) { + avs_log(advance_fu, ERROR, "Could not close file with iid: %d", i); + return -1; + } + } + + for (int i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + avs_log(advance_fu, INFO, + "Firmware version for object with IID %d is: %s", i, + afu_logic.fw_version[i]); + } + + return 0; +} + +static int fw_stream_open(anjay_iid_t iid, void *user_ptr) { + (void) user_ptr; + + char file_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_firmware_download_name(iid, file_name); + + if (afu_logic.new_firmware_file[iid]) { + avs_log(advance_fu, ERROR, "Already open %s", file_name); + return -1; + } + afu_logic.new_firmware_file[iid] = fopen(file_name, "wb"); + if (!afu_logic.new_firmware_file[iid]) { + avs_log(advance_fu, ERROR, "Could not open %s", file_name); + return -1; + } + return 0; +} + +static int fw_update_common_write(anjay_iid_t iid, + void *user_ptr, + const void *data, + size_t length) { + (void) user_ptr; + + if (!afu_logic.new_firmware_file[iid]) { + avs_log(advance_fu, ERROR, "Stream not open: object %d", iid); + return -1; + } + if (length + && (fwrite(data, length, 1, afu_logic.new_firmware_file[iid]) != 1 + || fflush(afu_logic.new_firmware_file[iid]) != 0)) { + avs_log(advance_fu, ERROR, "fwrite or fflush failed: %s", + strerror(errno)); + return ANJAY_ADVANCED_FW_UPDATE_ERR_NOT_ENOUGH_SPACE; + } + return 0; +} + +static void remove_linked_instance(anjay_iid_t iid, anjay_iid_t target_iid) { + const anjay_iid_t *linked_instances; + anjay_iid_t linked_target_iids[AFU_NUMBER_OF_FIRMWARE_INSTANCES - 1]; + size_t linked_iids_count = 0; + size_t new_linked_iids_count = 0; + + // get linked instances + anjay_advanced_fw_update_get_linked_instances( + afu_logic.anjay, iid, &linked_instances, &linked_iids_count); + // remove target_iid from the list + for (size_t i = 0; i < linked_iids_count; i++) { + if (linked_instances[i] != target_iid) { + linked_target_iids[new_linked_iids_count++] = linked_instances[i]; + } + } + // update linked list + anjay_advanced_fw_update_set_linked_instances( + afu_logic.anjay, iid, linked_target_iids, new_linked_iids_count); +} + +static void remove_conflicting_instance(anjay_iid_t iid, + anjay_iid_t target_iid) { + const anjay_iid_t *conflicting_instances; + anjay_iid_t conflicting_target_iids[AFU_NUMBER_OF_FIRMWARE_INSTANCES - 1]; + size_t conflicting_iids_count = 0; + size_t new_conflicting_iids_count = 0; + + // get conflicting instances + anjay_advanced_fw_update_get_conflicting_instances(afu_logic.anjay, iid, + &conflicting_instances, + &conflicting_iids_count); + // remove target_iid from the list + for (size_t i = 0; i < conflicting_iids_count; i++) { + if (conflicting_instances[i] != target_iid) { + conflicting_target_iids[new_conflicting_iids_count++] = + conflicting_instances[i]; + } + } + // update conflicting list + anjay_advanced_fw_update_set_conflicting_instances( + afu_logic.anjay, iid, conflicting_target_iids, + new_conflicting_iids_count); +} + +static void add_linked_instance(anjay_iid_t iid, anjay_iid_t target_iid) { + const anjay_iid_t *linked_instances; + anjay_iid_t linked_target_iids[AFU_NUMBER_OF_FIRMWARE_INSTANCES - 1]; + size_t linked_iids_count = 0; + + // get linked instances + anjay_advanced_fw_update_get_linked_instances( + afu_logic.anjay, iid, &linked_instances, &linked_iids_count); + // add target_iid to the list + for (size_t i = 0; i < linked_iids_count; i++) { + linked_target_iids[i] = linked_instances[i]; + } + linked_target_iids[linked_iids_count++] = target_iid; + anjay_advanced_fw_update_set_linked_instances( + afu_logic.anjay, iid, linked_target_iids, linked_iids_count); +} + +static int fw_update_common_finish(anjay_iid_t iid, void *user_ptr) { + (void) user_ptr; + + if (!afu_logic.new_firmware_file[iid]) { + avs_log(advance_fu, ERROR, "Stream not open: object %d", iid); + return -1; + } + + if (fclose(afu_logic.new_firmware_file[iid])) { + avs_log(advance_fu, ERROR, "Closing firmware image failed: object %d", + iid); + afu_logic.new_firmware_file[iid] = NULL; + return -1; + } + afu_logic.new_firmware_file[iid] = NULL; + + /* + If other firmware instances are in downloaded state set linked instances, + based on them, the upgrade will be performed simultaneously in the + perform_upgrade callback. The reason for setting linked instances may be + different and depends on the user's implementation, but always mean + that instances will be updated in a batch if the Update resource is executed + with no arguments. + */ + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (i != iid) { + anjay_advanced_fw_update_state_t state; + anjay_advanced_fw_update_get_state(afu_logic.anjay, i, &state); + if (state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED) { + add_linked_instance(iid, i); + add_linked_instance(i, iid); + } + } + } + + return 0; +} + +static int +fw_update_common_perform_upgrade(anjay_iid_t iid, + void *user_ptr, + const anjay_iid_t *requested_supplemental_iids, + size_t requested_supplemental_iids_count) { + (void) user_ptr; + + const anjay_iid_t *linked_target_iids; + bool update_iid[AFU_NUMBER_OF_FIRMWARE_INSTANCES]; + size_t linked_iids_count = 0; + + // get linked instances + anjay_advanced_fw_update_get_linked_instances( + afu_logic.anjay, iid, &linked_target_iids, &linked_iids_count); + + /* Prepare list of iid to update. If requested_supplemental_iids is present + * use it otherwise use linked_target_iids.*/ + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (is_update_requested(i, iid, requested_supplemental_iids, + requested_supplemental_iids_count, + linked_target_iids, linked_iids_count)) { + update_iid[i] = true; + // check if new file is already downloaded + anjay_advanced_fw_update_state_t state; + anjay_advanced_fw_update_get_state(afu_logic.anjay, i, &state); + if ((i != iid) + && (state != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED)) { + avs_log(advance_fu, ERROR, + "Upgrade can't be performed, firmware file with iid %d " + "is not ready", + i); + // set conflicting instance + add_conflicting_instance(iid, i); + return ANJAY_ADVANCED_FW_UPDATE_ERR_CONFLICTING_STATE; + } + } else { + update_iid[i] = false; + } + } + + /* + Check firmware version compatibility. + In this example major version number is compare - first character in every + additional image must have the same value. If new file is given (DOWNLOADED + STATE), get this value from them, otherwise use the old one. + */ + for (anjay_iid_t i = 1; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (update_iid[i] == true) { + for (anjay_iid_t j = i + 1; j < AFU_NUMBER_OF_FIRMWARE_INSTANCES; + j++) { + if (get_firmware_major_version(i, update_iid[i]) + != get_firmware_major_version(j, update_iid[j])) { + avs_log(advance_fu, ERROR, + "Upgrade can't be performed, conflicting firmware " + "version between instance %d and %d", + i, j); + // set conflicting instance due to firmware version + // incompatibility + add_conflicting_instance(i, j); + add_conflicting_instance(j, i); + return ANJAY_ADVANCED_FW_UPDATE_ERR_CONFLICTING_STATE; + } + } + } + } + + /* No errors found, change the status of all requested_supplemental_iids or + * linked_target_iids to UPDATING before the actual update process.*/ + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (update_iid[i] == true) { + if (i != iid) { + anjay_advanced_fw_update_set_state_and_result( + afu_logic.anjay, i, + ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + } + } + } + + // after firmware versions check, start firmware update, first with + // additional images + for (anjay_iid_t i = 1; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (update_iid[i] == true) { + avs_log(advance_fu, INFO, "Perform update on %d instance", i); + + char new_firm_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + char current_firm_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_firmware_download_name(i, new_firm_name); + get_add_firmware_file_name(i, current_firm_name); + + if (move_file(current_firm_name, new_firm_name)) { + avs_log(advance_fu, ERROR, + "Error during the %d additional image swapping", i); + return -1; + } + // if main application is restarted, set update marker + if (update_iid[AFU_DEFAULT_FIRMWARE_INSTANCE_IID] == true) { + char marker_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_marker_file_name(i, marker_name); + FILE *marker = fopen(marker_name, "w"); + if (!marker) { + avs_log(advance_fu, ERROR, + "Marker file could not be created"); + return -1; + } + if (fclose(marker)) { + avs_log(advance_fu, ERROR, + "Marker file could not be close"); + } + } // if main application is not restarted, update state + else { + anjay_advanced_fw_update_set_state_and_result( + afu_logic.anjay, i, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS); + } + } + } + + // update application + if (update_iid[AFU_DEFAULT_FIRMWARE_INSTANCE_IID] == true) { + avs_log(advance_fu, INFO, "Perform update on default instance"); + + char new_firm_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + char marker_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_firmware_download_name(AFU_DEFAULT_FIRMWARE_INSTANCE_IID, + new_firm_name); + get_marker_file_name(AFU_DEFAULT_FIRMWARE_INSTANCE_IID, marker_name); + + if (chmod(new_firm_name, 0700) == -1) { + avs_log(advance_fu, ERROR, "Could not make firmware executable: %s", + strerror(errno)); + return -1; + } + // Create a marker file, so that the new process knows it is the + // "upgraded" one + FILE *marker = fopen(marker_name, "w"); + if (!marker) { + avs_log(advance_fu, ERROR, "Marker file could not be created"); + return -1; + } + if (fclose(marker)) { + avs_log(advance_fu, ERROR, "Marker file could not be close"); + } + + assert(ENDPOINT_NAME); + + // If the call below succeeds, the firmware is considered as "upgraded", + // and we hope the newly started client registers to the Server. + (void) execl(new_firm_name, new_firm_name, ENDPOINT_NAME, NULL); + avs_log(advance_fu, ERROR, "execl() failed: %s", strerror(errno)); + // If we are here, it means execl() failed. Marker file MUST now be + // removed, as the firmware update failed. + unlink(marker_name); + return -1; + } + + // update firmware version + refresh_fw_version(); + + // clear conflicting and linked instances in the objects + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (update_iid[i] == true) { + anjay_advanced_fw_update_set_conflicting_instances(afu_logic.anjay, + i, NULL, 0); + anjay_advanced_fw_update_set_linked_instances(afu_logic.anjay, i, + NULL, 0); + // clear conflicting and linked instances about this object from + // other objects + for (anjay_iid_t j = 0; j < AFU_NUMBER_OF_FIRMWARE_INSTANCES; j++) { + if (i != j) { + remove_linked_instance(i, j); + remove_conflicting_instance(i, j); + } + } + } + } + + return 0; +} + +void fw_update_common_reset(anjay_iid_t iid, void *user_ptr) { + (void) user_ptr; + + char new_firm_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + get_firmware_download_name(iid, new_firm_name); + + // Reset can be issued even if the download never started + if (afu_logic.new_firmware_file[iid]) { + // We ignore the result code of fclose(), as fw_reset() can't fail + (void) fclose(afu_logic.new_firmware_file[iid]); + // and reset our global state to initial value + afu_logic.new_firmware_file[iid] = NULL; + } + // Finally, let's remove any downloaded payload + unlink(new_firm_name); + + // clear conflicting and linked instances in the object + anjay_advanced_fw_update_set_conflicting_instances(afu_logic.anjay, iid, + NULL, 0); + anjay_advanced_fw_update_set_linked_instances(afu_logic.anjay, iid, NULL, + 0); + // clear conflicting and linked instances about this object from other + // objects + for (anjay_iid_t i = 0; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + if (i != iid) { + remove_linked_instance(i, iid); + remove_conflicting_instance(i, iid); + } + } +} + +static const char *fw_update_common_get_current_version(anjay_iid_t iid, + void *user_ptr) { + (void) user_ptr; + + return (const char *) afu_logic.fw_version[iid]; +} + +static const anjay_advanced_fw_update_handlers_t handlers = { + .stream_open = fw_stream_open, + .stream_write = fw_update_common_write, + .stream_finish = fw_update_common_finish, + .reset = fw_update_common_reset, + .get_current_version = fw_update_common_get_current_version, + .perform_upgrade = fw_update_common_perform_upgrade +}; + +int afu_update_install(anjay_t *anjay) { + anjay_advanced_fw_update_initial_state_t state; + char marker_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + char file_name[AFU_FILE_NAME_STR_MAX_LEN] = { 0 }; + + memset(&state, 0, sizeof(state)); + + afu_logic.anjay = anjay; + + anjay_advanced_fw_update_global_config_t config = { + .prefer_same_socket_downloads = true + }; + int result = anjay_advanced_fw_update_install(anjay, &config); + if (result) { + avs_log(advance_fu, ERROR, + "Could not install advanced firmware object: %d", result); + return -1; + } + + // check if application was updated + get_marker_file_name(AFU_DEFAULT_FIRMWARE_INSTANCE_IID, marker_name); + if (access(marker_name, F_OK) != -1) { + avs_log(advance_fu, INFO, "Application update succeded"); + state.result = ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS; + unlink(marker_name); + } + result = anjay_advanced_fw_update_instance_add( + anjay, AFU_DEFAULT_FIRMWARE_INSTANCE_IID, "application", &handlers, + NULL, &state); + if (result) { + avs_log(advance_fu, ERROR, + "Could not add default application instance: %d", result); + return -1; + } + + // check if additional files were updated, if not create it with default + // value + for (anjay_iid_t i = 1; i < AFU_NUMBER_OF_FIRMWARE_INSTANCES; i++) { + memset(marker_name, 0, sizeof(marker_name)); + get_marker_file_name(i, marker_name); + if (access(marker_name, F_OK) != -1) { + avs_log(advance_fu, INFO, + "Additional file with idd: %d update succeded", i); + state.result = ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS; + unlink(marker_name); + } else { + state.result = ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL; + } + + memset(file_name, 0, sizeof(file_name)); + get_add_firmware_file_name(i, file_name); + // create file only if not exist + if (access(file_name, F_OK) != 0) { + FILE *stream = fopen(file_name, "wb"); + if (!stream) { + avs_log(advance_fu, ERROR, "Could not open %s", file_name); + return -1; + } + if (fwrite(AFU_ADD_FILE_DEFAULT_CONTENT, + strlen(AFU_ADD_FILE_DEFAULT_CONTENT), 1, stream) + != 1) { + avs_log(advance_fu, ERROR, "Could not write to %s", file_name); + fclose(stream); + return -1; + } + if (fclose(stream)) { + avs_log(advance_fu, ERROR, "Could not close %s", file_name); + return -1; + } + } + + snprintf(afu_logic.instance_name[i], AFU_INSTANCE_NAME_STR_MAX_LEN, + "add_img_%d", i); + result = anjay_advanced_fw_update_instance_add( + anjay, i, afu_logic.instance_name[i], &handlers, NULL, &state); + if (result) { + avs_log(advance_fu, ERROR, + "Could not add the additional image instance: %d", result); + return -1; + } + } + + if (refresh_fw_version()) { + return -1; + } + + return 0; +} diff --git a/examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.h b/examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.h new file mode 100644 index 000000000..ccb21a0c3 --- /dev/null +++ b/examples/tutorial/firmware-update/advanced-firmware-update/src/advanced_firmware_update.h @@ -0,0 +1,28 @@ +#ifndef ADVANCED_FIRMWARE_UPDATE_H +#define ADVANCED_FIRMWARE_UPDATE_H + +#include +#include + +#include + +#define AFU_DEFAULT_FIRMWARE_VERSION "1.0.0" +#define AFU_ADD_FILE_DEFAULT_CONTENT "1.1.1" + +#define AFU_DEFAULT_FIRMWARE_INSTANCE_IID 0 +#define AFU_NUMBER_OF_FIRMWARE_INSTANCES 3 + +/** + * Buffer for the endpoint name that will be used when re-launching the client + * after firmware upgrade. + */ +extern const char *ENDPOINT_NAME; + +/** + * Installs the advanced firmware update module. + * + * @returns 0 on success, negative value otherwise. + */ +int afu_update_install(anjay_t *anjay); + +#endif // ADVANCED_FIRMWARE_UPDATE_H diff --git a/examples/tutorial/firmware-update/advanced-firmware-update/src/main.c b/examples/tutorial/firmware-update/advanced-firmware-update/src/main.c new file mode 100644 index 000000000..e32b61eef --- /dev/null +++ b/examples/tutorial/firmware-update/advanced-firmware-update/src/main.c @@ -0,0 +1,141 @@ +#include +#include +#include +#include + +#include "advanced_firmware_update.h" +#include "time_object.h" + +typedef struct { + anjay_t *anjay; + const anjay_dm_object_def_t **time_object; +} notify_job_args_t; + +// Periodically notifies the library about Resource value changes +static void notify_job(avs_sched_t *sched, const void *args_ptr) { + const notify_job_args_t *args = (const notify_job_args_t *) args_ptr; + + time_object_notify(args->anjay, args->time_object); + + // Schedule run of the same function after 1 second + AVS_SCHED_DELAYED(sched, NULL, avs_time_duration_from_scalar(1, AVS_TIME_S), + notify_job, args, sizeof(*args)); +} + +// Installs Security Object and adds and instance of it. +// An instance of Security Object provides information needed to connect to +// LwM2M server. +static int setup_security_object(anjay_t *anjay) { + if (anjay_security_object_install(anjay)) { + return -1; + } + + static const char PSK_IDENTITY[] = "IDENTITY"; + static const char PSK_KEY[] = "KEY"; + + anjay_security_instance_t security_instance = { + .ssid = 1, + .server_uri = "coaps://eu.iot.avsystem.cloud:5684", + .security_mode = ANJAY_SECURITY_PSK, + .public_cert_or_psk_identity = (const uint8_t *) PSK_IDENTITY, + .public_cert_or_psk_identity_size = strlen(PSK_IDENTITY), + .private_cert_or_psk_key = (const uint8_t *) PSK_KEY, + .private_cert_or_psk_key_size = strlen(PSK_KEY) + }; + + // Anjay will assign Instance ID automatically + anjay_iid_t security_instance_id = ANJAY_ID_INVALID; + if (anjay_security_object_add_instance(anjay, &security_instance, + &security_instance_id)) { + return -1; + } + + return 0; +} + +// Installs Server Object and adds and instance of it. +// An instance of Server Object provides the data related to a LwM2M Server. +static int setup_server_object(anjay_t *anjay) { + if (anjay_server_object_install(anjay)) { + return -1; + } + + const anjay_server_instance_t server_instance = { + // Server Short ID + .ssid = 1, + // Client will send Update message often than every 60 seconds + .lifetime = 60, + // Disable Default Minimum Period resource + .default_min_period = -1, + // Disable Default Maximum Period resource + .default_max_period = -1, + // Disable Disable Timeout resource + .disable_timeout = -1, + // Sets preferred transport to UDP + .binding = "U" + }; + + // Anjay will assign Instance ID automatically + anjay_iid_t server_instance_id = ANJAY_ID_INVALID; + if (anjay_server_object_add_instance(anjay, &server_instance, + &server_instance_id)) { + return -1; + } + + return 0; +} + +int main(int argc, char *argv[]) { + if (argc != 2) { + avs_log(tutorial, ERROR, "usage: %s ENDPOINT_NAME", argv[0]); + return -1; + } + + ENDPOINT_NAME = argv[1]; + + const anjay_configuration_t CONFIG = { + .endpoint_name = ENDPOINT_NAME, + .in_buffer_size = 4000, + .out_buffer_size = 4000, + .msg_cache_size = 4000 + }; + + anjay_t *anjay = anjay_new(&CONFIG); + if (!anjay) { + avs_log(tutorial, ERROR, "Could not create Anjay object"); + return -1; + } + + int result = 0; + // Setup necessary objects + if (setup_security_object(anjay) || setup_server_object(anjay) + || afu_update_install(anjay)) { + result = -1; + } + + const anjay_dm_object_def_t **time_object = NULL; + if (!result) { + time_object = time_object_create(); + if (time_object) { + result = anjay_register_object(anjay, time_object); + } else { + result = -1; + } + } + + if (!result) { + // Run notify_job the first time; + // this will schedule periodic calls to itself via the scheduler + notify_job(anjay_get_scheduler(anjay), &(const notify_job_args_t) { + .anjay = anjay, + .time_object = time_object + }); + + result = anjay_event_loop_run( + anjay, avs_time_duration_from_scalar(1, AVS_TIME_S)); + } + + anjay_delete(anjay); + time_object_release(time_object); + return result; +} diff --git a/examples/tutorial/firmware-update/advanced-firmware-update/src/time_object.c b/examples/tutorial/firmware-update/advanced-firmware-update/src/time_object.c new file mode 120000 index 000000000..d93140e69 --- /dev/null +++ b/examples/tutorial/firmware-update/advanced-firmware-update/src/time_object.c @@ -0,0 +1 @@ +../../../BC-Notifications/src/time_object.c \ No newline at end of file diff --git a/examples/tutorial/firmware-update/advanced-firmware-update/src/time_object.h b/examples/tutorial/firmware-update/advanced-firmware-update/src/time_object.h new file mode 120000 index 000000000..270f11e70 --- /dev/null +++ b/examples/tutorial/firmware-update/advanced-firmware-update/src/time_object.h @@ -0,0 +1 @@ +../../../BC-Notifications/src/time_object.h \ No newline at end of file diff --git a/include_public/anjay/advanced_fw_update.h b/include_public/anjay/advanced_fw_update.h new file mode 100644 index 000000000..3a0cd2b68 --- /dev/null +++ b/include_public/anjay/advanced_fw_update.h @@ -0,0 +1,862 @@ +/* + * Copyright 2017-2023 AVSystem + * AVSystem Anjay LwM2M SDK + * All rights reserved. + * + * Licensed under the AVSystem-5-clause License. + * See the attached LICENSE file for details. + */ + +#ifndef ANJAY_INCLUDE_ANJAY_ADVANCED_FW_UPDATE_H +#define ANJAY_INCLUDE_ANJAY_ADVANCED_FW_UPDATE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define ANJAY_ADVANCED_FW_UPDATE_OID 33629 + +/** + * Numeric values of the Advanced Firmware Update State resource. + * See AVSystem specification of Advanced Firmware Update for details. + * + * Note: they SHOULD only be used with + * @ref anjay_advanced_fw_update_set_state_and_result . + */ +typedef enum { + ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE = 0, + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING, + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED, + ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING +} anjay_advanced_fw_update_state_t; + +/** + * Numeric values of the Advanced Firmware Update Result resource. + * See AVSystem specification of Advanced Firmware Update for details. + * + * Note: they SHOULD only be used with + * @ref anjay_advanced_fw_update_set_state_and_result . + */ +typedef enum { + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL = 0, + ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS = 1, + ANJAY_ADVANCED_FW_UPDATE_RESULT_NOT_ENOUGH_SPACE = 2, + ANJAY_ADVANCED_FW_UPDATE_RESULT_OUT_OF_MEMORY = 3, + ANJAY_ADVANCED_FW_UPDATE_RESULT_CONNECTION_LOST = 4, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INTEGRITY_FAILURE = 5, + ANJAY_ADVANCED_FW_UPDATE_RESULT_UNSUPPORTED_PACKAGE_TYPE = 6, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INVALID_URI = 7, + ANJAY_ADVANCED_FW_UPDATE_RESULT_FAILED = 8, + ANJAY_ADVANCED_FW_UPDATE_RESULT_UNSUPPORTED_PROTOCOL = 9, + ANJAY_ADVANCED_FW_UPDATE_RESULT_UPDATE_CANCELLED = 10, + ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED = 11, + ANJAY_ADVANCED_FW_UPDATE_RESULT_CONFLICTING_STATE = 12, + ANJAY_ADVANCED_FW_UPDATE_RESULT_DEPENDENCY_ERROR = 13, +} anjay_advanced_fw_update_result_t; + +/** @name Advanced Firmware Update result codes + * @{ + * The following result codes may be returned from + * @ref anjay_advanced_fw_update_stream_write_t, + * @ref anjay_advanced_fw_update_stream_finish_t or + * @ref anjay_advanced_fw_update_perform_upgrade_t to control the value of the + * Update Result Resource after the failure. + * + * Their values correspond to negated numeric values of that resource. However, + * attempting to use other negated value will be checked and cause a fall-back + * to a value default for a given handler. + */ +#define ANJAY_ADVANCED_FW_UPDATE_ERR_NOT_ENOUGH_SPACE \ + (-ANJAY_ADVANCED_FW_UPDATE_RESULT_NOT_ENOUGH_SPACE) +#define ANJAY_ADVANCED_FW_UPDATE_ERR_OUT_OF_MEMORY \ + (-ANJAY_ADVANCED_FW_UPDATE_RESULT_OUT_OF_MEMORY) +#define ANJAY_ADVANCED_FW_UPDATE_ERR_INTEGRITY_FAILURE \ + (-ANJAY_ADVANCED_FW_UPDATE_RESULT_INTEGRITY_FAILURE) +#define ANJAY_ADVANCED_FW_UPDATE_ERR_UNSUPPORTED_PACKAGE_TYPE \ + (-ANJAY_ADVANCED_FW_UPDATE_RESULT_UNSUPPORTED_PACKAGE_TYPE) +#define ANJAY_ADVANCED_FW_UPDATE_ERR_DEFERRED \ + (-ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED) +#define ANJAY_ADVANCED_FW_UPDATE_ERR_CONFLICTING_STATE \ + (-ANJAY_ADVANCED_FW_UPDATE_RESULT_CONFLICTING_STATE) +#define ANJAY_ADVANCED_FW_UPDATE_ERR_DEPENDENCY_ERROR \ + (-ANJAY_ADVANCED_FW_UPDATE_RESULT_DEPENDENCY_ERROR) +/** @} */ + +/** + * Numeric values of the Advanced Firmware Update Severity resource. + * See AVSystem specification of Advanced Firmware Update for details. + */ +typedef enum { + ANJAY_ADVANCED_FW_UPDATE_SEVERITY_CRITICAL = 0, + ANJAY_ADVANCED_FW_UPDATE_SEVERITY_MANDATORY, + ANJAY_ADVANCED_FW_UPDATE_SEVERITY_OPTIONAL +} anjay_advanced_fw_update_severity_t; + +/** + * Bool values of the Advanced Firmware Update object configuration. + * This Advanced Firmware Update object configuration affects all instances. + */ +typedef struct { + /** + * Informs the module to try reusing sockets of existing LwM2M Servers to + * download the firmware image if the download URI matches any of the LwM2M + * Servers. + */ + bool prefer_same_socket_downloads; +#ifdef ANJAY_WITH_SEND + /** + * Enables using LwM2M Send to report State, Update Result and Firmware + * Version to the LwM2M Server (if LwM2M Send is enabled) during firmware + * update. + */ + bool use_lwm2m_send; +#endif // ANJAY_WITH_SEND +} anjay_advanced_fw_update_global_config_t; + +/** + * Information about the state to initialize the instances of Advanced + * Firmware Update objects in. + */ +typedef struct { + /** + * Information about the state of update of particular instance of + * Advance Firmware Update object, at the moment of initialization. + */ + anjay_advanced_fw_update_state_t state; + + /** + * Information about the result of update of particular instance of + * Advance Firmware Update object, at the moment of initialization. + */ + anjay_advanced_fw_update_result_t result; + + /** + * Value to initialize the Severity resource with. + */ + anjay_advanced_fw_update_severity_t persisted_severity; + + /** + * Value to initialize the Last State Change Time resource with. + */ + avs_time_real_t persisted_last_state_change_time; + + /** + * Update deadline based on Maximum Defer Period resource value and time of + * executing Update resource. + */ + avs_time_real_t persisted_update_deadline; +} anjay_advanced_fw_update_initial_state_t; + +/** + * Opens the stream that will be used to write the firmware package to. + * + * The intended way of implementing this handler is to open a temporary file + * using fopen() or allocate some memory buffer that may then be used to + * store the downloaded data in. The library will not attempt to call + * @ref anjay_advanced_fw_update_stream_write_t without having previously called + * @ref anjay_advanced_fw_update_stream_open_t . Please see + * @ref anjay_advanced_fw_update_handlers_t for more information about state + * transitions. + * + * Note that this handler will NOT be called after initializing the object with + * the ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING option, so any + * necessary resources shall be already open before calling + * @ref anjay_advanced_fw_update_instance_add . + * + * @param iid Instance ID of an Advanced Firmware Object which tries to + * open a stream. + * + * @param user_ptr Opaque pointer to user data, as passed to + * @ref anjay_advanced_fw_update_instance_add + * + * @returns The callback shall return 0 if successful or a negative value in + * case of error. Error codes are NOT handled here, so + * attempting to return ANJAY_ADVANCED_FW_UPDATE_ERR_* values + * will NOT cause any effect different than any other + * negative value. + */ +typedef int anjay_advanced_fw_update_stream_open_t(anjay_iid_t iid, + void *user_ptr); +/** + * Writes data to the download stream. + * + * May be called multipled times after + * @ref anjay_advanced_fw_update_stream_open_t, once for each consecutive chunk + * of downloaded data. + * + * @param iid Instance ID of an Advanced Firmware Object which tries to + * write to a stream. + * + * @param user_ptr Opaque pointer to user data, as passed to + * @ref anjay_advanced_fw_update_instance_add + * + * @param data Pointer to a chunk of the firmware package being downloaded. + * Guaranteed to be non-NULL. + * + * @param length Number of bytes in the chunk pointed to by data. + * Guaranteed to be greater than zero. + * + * @returns The callback shall return 0 if successful or a negative value in + * case of error. If one of the ANJAY_ADVANCED_FW_UPDATE_ERR_* + * value is returned, an equivalent value will be set in the Update + * Result Resource. + */ +typedef int anjay_advanced_fw_update_stream_write_t(anjay_iid_t iid, + void *user_ptr, + const void *data, + size_t length); + +/** + * Closes the download stream and prepares the firmware package to be flashed. + * + * Will be called after a series of @ref anjay_advanced_fw_update_stream_write_t + * calls, after the whole package is downloaded. + * + * The intended way of implementing this handler is to e.g. call fclose() + * and perform integrity check on the downloaded file. It might also be + * uncompressed or decrypted as necessary, so that it is ready to be flashed. + * The exact split of responsibility between + * @ref anjay_advanced_fw_update_stream_finish_t and + * @ref anjay_advanced_fw_update_perform_upgrade_t is not clearly defined and up + * to the implementor. + * + * Note that regardless of the return value, the stream is considered to be + * closed. That is, upon successful return, the Advanced Firmware Update object + * is considered to be in the Downloaded state, and upon returning an + * error - in the Idle state. + * + * @param iid Instance ID of an Advanced Firmware Object which tries to + * finish a stream. + * + * @param user_ptr Opaque pointer to user data, as passed to + * @ref anjay_advanced_fw_update_instance_add + * + * @returns The callback shall return 0 if successful or a negative value in + * case of error. If one of the ANJAY_ADVANCED_FW_UPDATE_ERR_* + * value is returned, an equivalent value will be set in the Update + * Result Resource. + */ +typedef int anjay_advanced_fw_update_stream_finish_t(anjay_iid_t iid, + void *user_ptr); + +/** + * Resets the firmware update state and performs any applicable cleanup of + * temporary storage if necessary. + * + * Will be called at request of the server, or after a failed download. Note + * that it may be called without previously calling + * @ref anjay_advanced_fw_update_stream_finish_t, so it shall also close the + * currently open download stream, if any. + * + * @note If reset of particular instance is done while it is in + * ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED state, it is likely + * possible that it is listed as linked instance of another instance. + * If that is the case, it should be marked as Conflicting instance + * in every instance that it is linked with. + * + * @param iid Instance ID of an Advanced Firmware Object which performs + * reset. + * + * @param user_ptr Opaque pointer to user data, as passed to + * @ref anjay_advanced_fw_update_instance_add + */ +typedef void anjay_advanced_fw_update_reset_t(anjay_iid_t iid, void *user_ptr); + +/** + * Returns the name of downloaded firmware package. + * + * The name will be exposed in the data model as the PkgName Resource. If this + * callback returns NULL or is not implemented at all (with the + * corresponding field set to NULL), that Resource will not be present in + * the data model. + * + * It only makes sense for this handler to return non-NULL values if + * there is a valid package already downloaded. The library will not call this + * handler in any state other than Downloaded. + * + * The library will not attempt to deallocate the returned pointer. User code + * must assure that the pointer will remain valid at least until return from + * @ref anjay_serve or @ref anjay_sched_run . + * + * @param iid Instance ID of an Advanced Firmware Object which tries to get + * related package name. + * + * @param user_ptr Opaque pointer to user data, as passed to + * @ref anjay_advanced_fw_update_instance_add + * + * @returns The callback shall return a pointer to a null-terminated string + * containing the package name, or NULL if it is not currently + * available. + */ +typedef const char *anjay_advanced_fw_update_get_pkg_name_t(anjay_iid_t iid, + void *user_ptr); + +/** + * Returns the version of downloaded firmware package. + * + * The version will be exposed in the data model as the PkgVersion Resource. If + * this callback returns NULL or is not implemented at all (with the + * corresponding field set to NULL), that Resource will not be present in + * the data model. + * + * It only makes sense for this handler to return non-NULL values if + * there is a valid package already downloaded. The library will not call this + * handler in any state other than Downloaded. + * + * The library will not attempt to deallocate the returned pointer. User code + * must assure that the pointer will remain valid at least until return from + * @ref anjay_serve or @ref anjay_sched_run . + * + * @param iid Instance ID of an Advanced Firmware Object which tries to get + * related package version. + * + * @param user_ptr Opaque pointer to user data, as passed to + * @ref anjay_advanced_fw_update_instance_add + * + * @returns The callback shall return a pointer to a null-terminated string + * containing the package version, or NULL if it is not + * currently available. + */ +typedef const char *anjay_advanced_fw_update_get_pkg_version_t(anjay_iid_t iid, + void *user_ptr); + +/** + * Returns the current version of firmware represented by Advanced Firmware + * Update object instance. + * + * The version will be exposed in the data model as the Current Version + * Resource. If this callback returns NULL or is not implemented at all + * (with the corresponding field set to NULL), that Resource will not be + * present in the data model. + * + * @param iid Instance ID of an Advanced Firmware Object which tries to get + * related current version. + * + * @param user_ptr Opaque pointer to user data, as passed to + * @ref anjay_advanced_fw_update_instance_add + * + * @returns The callback shall return a pointer to a null-terminated string + * containing the package version, or NULL if it is not + * currently available. + */ +typedef const char * +anjay_advanced_fw_update_get_current_version_t(anjay_iid_t iid, void *user_ptr); + +/** + * Performs the actual upgrade with previously downloaded package. + * + * Will be called at request of the server, after a package has been downloaded. + * + * Most users will want to implement firmware update in a way that involves a + * reboot. In such case, it is expected that this callback will do either one of + * the following: + * + * - perform firmware upgrade, terminate outermost event loop and return, + * call reboot after @ref anjay_event_loop_run + * - perform the firmware upgrade internally and then reboot, it means that + * the return will never happen + * + * After rebooting, the result of the upgrade process may be passed to the + * library during initialization via the initial_result argument to + * @ref anjay_advanced_fw_update_instance_add . + * + * Alternatively, if the update can be performed without reinitializing Anjay, + * you can use @ref anjay_advanced_fw_update_set_state_and_result (either from + * within the handler or some time after returning from it) to pass the update + * result. + * + * @param iid Instance ID of an Advanced Firmware + * Object which tries to perform + * upgrade. + * + * @param user_ptr Opaque pointer to user data, as + * passed to + * @ref anjay_advanced_fw_update_instance_add + * + * @param requested_supplemental_iids Pointer to list of Advanced Firmware + * Object instances that server request + * to upgrade along with instance that + * this callback belongs to. + * + * @param requested_supplemental_iids_count Count of requested supplemental iids + * + * @returns The callback shall return a negative value if it can be determined + * without a reboot, that the firmware upgrade cannot be successfully + * performed. + * + * If one of the ANJAY_ADVANCED_FW_UPDATE_ERR_* values is + * returned, an equivalent value will be set in the Update Result + * Resource. Otherwise, if a non-zero value is returned, the Update + * Result Resource is set to generic "Firmware update failed" code. + * + */ +typedef int anjay_advanced_fw_update_perform_upgrade_t( + anjay_iid_t iid, + void *user_ptr, + const anjay_iid_t *requested_supplemental_iids, + size_t requested_supplemental_iids_count); + +/** + * Queries security information that shall be used for an encrypted connection + * with a PULL-mode download server. + * + * May be called before @ref anjay_advanced_fw_update_stream_open_t if the + * download is to be performed in PULL mode and the connection needs to use TLS + * or DTLS encryption. + * + * Note that the @ref anjay_security_config_t contains references to file paths, + * binary security keys, and/or ciphersuite lists. It is the user's + * responsibility to appropriately allocate them and ensure proper lifetime of + * the returned pointers. The returned security information may only be + * invalidated in a call to @ref anjay_advanced_fw_update_reset_t or after a + * call to @ref anjay_delete . + * + * If this handler is not implemented at all (with the corresponding field set + * to NULL), @ref anjay_security_config_from_dm will be used as a default + * way to get security information. + * + * In that (no user-defined handler) case, anjay_security_config_pkix() + * will be used as an additional fallback if ANJAY_WITH_LWM2M11 is + * enabled and a valid trust store is available (either specified through + * use_system_trust_store, trust_store_certs or + * trust_store_crls fields in anjay_configuration_t, or obtained + * via /est/crts request if est_cacerts_policy is set to + * ANJAY_EST_CACERTS_IF_EST_CONFIGURED or + * ANJAY_EST_CACERTS_ALWAYS). + * + * You may also use these functions yourself, for example as a fallback + * mechanism. + * + * @param iid Instance ID of an Advanced Firmware Object which + * tries to get security config. + * + * @param user_ptr Opaque pointer to user data, as passed to + * @ref anjay_advanced_fw_update_instance_add + * + * @param out_security_config Pointer in which the handler shall fill in + * security configuration to use for download. Note + * that leaving this value as empty without filling + * it in will result in a configuration that is + * valid, but very insecure: it will + * cause any server certificate to be accepted + * without validation. Any pointers used within the + * supplied structure shall remain valid until either + * a call to @ref anjay_advanced_fw_update_reset_t, + * or exit to the event loop (from either + * @ref anjay_serve, @ref anjay_sched_run or + * @ref anjay_advanced_fw_update_instance_add), + * whichever happens first. Anjay will + * not attempt to deallocate + * anything automatically. + * + * @param download_uri Target firmware URI. + * + * @returns The callback shall return 0 if successful or a negative value in + * case of error. If one of the ANJAY_ADVANCED_FW_UPDATE_ERR_* + * value is returned, an equivalent value will be set in the Update + * Result Resource. + */ +typedef int anjay_advanced_fw_update_get_security_config_t( + anjay_iid_t iid, + void *user_ptr, + anjay_security_config_t *out_security_info, + const char *download_uri); + +/** + * Returns tx_params used to override default ones. + * + * If this handler is not implemented at all (with the corresponding field set + * to NULL), udp_tx_params from anjay_t object are used. + * + * @param iid Instance ID of an Advanced Firmware Object which query + * tx_params. + * + * @param user_ptr Opaque pointer to user data, as passed to + * @ref anjay_advanced_fw_update_instance_add . + * + * @param download_uri Target firmware URI. + * + * @returns Object with CoAP transmission parameters. + */ +typedef avs_coap_udp_tx_params_t anjay_advanced_fw_update_get_coap_tx_params_t( + anjay_iid_t iid, void *user_ptr, const char *download_uri); + +/** + * Handler callbacks that shall implement the platform-specific part of firmware + * update process. + * + * The Firmware Update object logic may be in one of the following states: + * + * - Idle. This is the state in which the object is just after + * creation (unless initialized with either + * ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED or + * ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING). The following handlers + * may be called in this state: + * - stream_open - shall open the download stream; moves the object + * into the Downloading state + * - get_security_config - shall fill in security info that shall be + * used for a given URL + * - reset - shall free data allocated by get_security_config, + * if it was called and there is any + * - Downloading. The object might be initialized directly into + * this state by using ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING. In + * this state, the download stream is open and data may be transferred. The + * following handlers may be called in this state: + * - stream_write - shall write a chunk of data into the download + * stream; it normally does not change state - however, if it fails, it will + * be immediately followed by a call to reset + * - stream_finish - shall close the download stream and perform + * integrity check on the downloaded image; if successful, this moves the + * object into the Downloaded state. If failed - into the + * Idle state; note that reset will NOT be called in that + * case + * - reset - shall remove all downloaded data; moves the object into + * the Idle state + * - Downloaded. The object might be initialized directly into + * this state by using ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED. In + * this state, the firmware package has been downloaded and checked and is ready + * to be flashed. The following handlers may be called in this state: + * - reset - shall reset all downloaded data; moves the object into the + * Idle state + * - get_name - shall return the package name, if available + * - get_version - shall return the package version, if available + * - perform_upgrade - shall perform the actual upgrade; if it fails, + * it does not cause a state change and may be called again; upon success, + * it may be treated as a transition to a "terminal" state, after which the + * device is expected to reboot + */ +typedef struct { + /** Opens the stream that will be used to write the firmware package to; + * @ref anjay_advanced_fw_update_stream_open_t */ + anjay_advanced_fw_update_stream_open_t *stream_open; + /** Writes data to the download stream; + * @ref anjay_advanced_fw_update_stream_write_t */ + anjay_advanced_fw_update_stream_write_t *stream_write; + /** Closes the download stream and prepares the firmware package to be + * flashed; @ref anjay_advanced_fw_update_stream_finish_t */ + anjay_advanced_fw_update_stream_finish_t *stream_finish; + + /** Resets the firmware update state and performs any applicable cleanup of + * temporary storage if necessary; @ref anjay_advanced_fw_update_reset_t */ + anjay_advanced_fw_update_reset_t *reset; + + /** Returns the name of downloaded firmware package; + * @ref anjay_advanced_fw_update_get_pkg_name_t */ + anjay_advanced_fw_update_get_pkg_name_t *get_pkg_name; + /** Return the version of downloaded firmware package; + * @ref anjay_advanced_fw_update_get_pkg_version_t */ + anjay_advanced_fw_update_get_pkg_version_t *get_pkg_version; + /** Return the version of current firmware package; + * @ref anjay_advanced_fw_update_get_current_version_t */ + anjay_advanced_fw_update_get_current_version_t *get_current_version; + + /** Performs the actual upgrade with previously downloaded package; + * @ref anjay_advanced_fw_update_perform_upgrade_t */ + anjay_advanced_fw_update_perform_upgrade_t *perform_upgrade; + + /** Queries security configuration that shall be used for an encrypted + * connection; @ref anjay_advanced_fw_update_get_security_config_t */ + anjay_advanced_fw_update_get_security_config_t *get_security_config; + + /** Queries CoAP transmission parameters to be used during firmware + * update; @ref anjay_advanced_fw_update_get_coap_tx_params_t */ + anjay_advanced_fw_update_get_coap_tx_params_t *get_coap_tx_params; +} anjay_advanced_fw_update_handlers_t; + +/** + * Installs the Advanced Firmware Update object in an Anjay object. + * + * The Advanced Firmware Update module does not require explicit cleanup; all + * resources will be automatically freed up during the call to + * @ref anjay_delete. + * + * @param anjay Anjay object for which the Advanced Firmware Update + * Object is installed. + * + * @param config Provides configuration of preferred socked downloads and + * lwm2m send usage; + * @ref anjay_advanced_fw_update_global_config_t + * + * @returns 0 on success, or a negative value in case of error. + */ +int anjay_advanced_fw_update_install( + anjay_t *anjay, const anjay_advanced_fw_update_global_config_t *config); + +/** + * Adds the Advanced Firmware Update object instance in an Advanced Firmware + * Update object. + * + * The Advanced Firmware Update module does not require explicit cleanup; all + * resources will be automatically freed up during the call to + * @ref anjay_delete. + * + * @param anjay Anjay object for which the Advanced Firmware Update + * Object is installed. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @param component_name Pointer to null-terminated component name string. + * Note: String is NOT copied, so it needs to remain valid + * for the lifetime of the object instance. + * + * @param handlers Pointer to a set of handler functions that handle the + * platform-specific part of firmware update process. + * Note: Contents of the structure are NOT copied, so it + * needs to remain valid for the lifetime of the object + * instance. + * + * @param user_arg Opaque user pointer that will be passed as the first + * argument to handler functions. + * + * @param initial_state Information about the state to initialize the Advanced + * Firmware Update object instance in. It is intended to be + * used after either an orderly reboot caused by a firmware + * update attempt to report the update result, or by an + * unexpected reboot in the middle of the download process. + * If the object shall be initialized in a neutral initial + * state, NULL might be passed. + * + * @returns 0 on success, or a negative value in case of error. + */ +int anjay_advanced_fw_update_instance_add( + anjay_t *anjay, + anjay_iid_t iid, + const char *component_name, + const anjay_advanced_fw_update_handlers_t *handlers, + void *user_arg, + const anjay_advanced_fw_update_initial_state_t *initial_state); + +/** + * Sets the Advanced Firmware Update object instance State to @p state and + * Result to @p result , interrupting the update process. + * + * If the function fails, neither Update State nor Update Result are changed. + * + * Some state transitions are disallowed and cause this function to fail: + * + * - @ref ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL and + * @ref ANJAY_ADVANCED_FW_UPDATE_RESULT_UPDATE_CANCELLED are never allowed and + * cause this function to fail. + * + * - @ref ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS is only allowed if the + * firmware application process was started by the server (an Execute + * operation was already performed on the Update resource of the Firmware + * Update object or @ref ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING was used in a + * call to @ref anjay_advanced_fw_update_instance_add). Otherwise, the + * function fails. + * + * - Other values of @p result (various error codes) are only allowed if + * Advanced Firmware Update State is not Idle (0), i.e. firmware is being + * downloaded, was already downloaded or is being applied. + * + * WARNING: calling this in @ref anjay_advanced_fw_update_perform_upgrade_t + * handler is supported, but the result of using it from within any other of + * @ref anjay_advanced_fw_update_handlers_t handlers is undefined. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @param state Value of the State resource to set. + * + * @param result Value of the Update Result resource to set. + * + * @returns 0 on success, or a negative value in case of error. + */ +int anjay_advanced_fw_update_set_state_and_result( + anjay_t *anjay, + anjay_iid_t iid, + anjay_advanced_fw_update_state_t state, + anjay_advanced_fw_update_result_t result); + +/** + * Gets the Advanced Firmware Update object instance State. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @param out_state Pointer to where write output state. + * + * @returns 0 on success, or a negative value in case of error. + */ +int anjay_advanced_fw_update_get_state( + anjay_t *anjay, + anjay_iid_t iid, + anjay_advanced_fw_update_state_t *out_state); + +/** + * Gets the Advanced Firmware Update object instance Result. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @param out_result Pointer to where write output result. + * + * @returns 0 on success, or a negative value in case of error. + */ +int anjay_advanced_fw_update_get_result( + anjay_t *anjay_locked, + anjay_iid_t iid, + anjay_advanced_fw_update_result_t *out_result); + +/** + * Sets linked instances resource of Advance Firmware Update object instance. + * + * Linked instances mark instances that will be updated in a batch together when + * performing upgrade of a @p iid instance. See AVSystem specification of + * Advanced Firmware Update for details. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @param target_iids Points to array iids of linked instances in relation + * to Advanced Firmware Update object instance @p iid. + * NOTE: Only already added instances only of Advanced + * Firmware Update object are allowed. + * + * @param target_iids_count Count of target iids in an array. + * + * @returns 0 on success, or a negative value in case of error. + */ +int anjay_advanced_fw_update_set_linked_instances( + anjay_t *anjay, + anjay_iid_t iid, + const anjay_iid_t *target_iids, + size_t target_iids_count); + +/** + * Gets linked instances resource of Advance Firmware Update object instance. + * + * Linked instances mark instances that will be updated in a batch together when + * performing upgrade of a @p iid instance. See AVSystem specification of + * Advanced Firmware Update for details. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @param out_target_iids Points to memory where to write array of iids of + * linked instances. + * + * @param out_target_iids_count Point to memory where to write count of target + * iids in an array. + * + * @returns 0 on success, or a negative value in case of error. + */ +int anjay_advanced_fw_update_get_linked_instances( + anjay_t *anjay, + anjay_iid_t iid, + const anjay_iid_t **out_target_iids, + size_t *out_target_iids_count); + +/** + * Sets conflicting instances resource of Advance Firmware Update object + * instance. + * + * When the download or update fails and the Update Result resource is set to + * @ref ANJAY_ADVANCED_FW_UPDATE_RESULT_CONFLICTING_STATE or + * @ref ANJAY_ADVANCED_FW_UPDATE_RESULT_DEPENDENCY_ERROR + * this resource MUST be present and contain references to the Advanced + * Firmware Update object instances that caused the conflict. See LwM2M + * specification for details. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @param target_iids Points to array iids of conflicting instances in + * relation to Advanced Firmware Update object instance + * @p iid. + * NOTE: Only already added instances only of Advanced + * Firmware Update object are allowed. + * + * @param target_iids_count Count of target iids in an array. + * + * @returns 0 on success, or a negative value in case of error. + */ +int anjay_advanced_fw_update_set_conflicting_instances( + anjay_t *anjay, + anjay_iid_t iid, + const anjay_iid_t *target_iids, + size_t target_iids_count); + +/** + * Gets conflicting instances resource of Advance Firmware Update object + * instance. + * + * When the download or update fails and the Update Result resource is set to + * @ref ANJAY_ADVANCED_FW_UPDATE_RESULT_CONFLICTING_STATE or + * @ref ANJAY_ADVANCED_FW_UPDATE_RESULT_DEPENDENCY_ERROR + * this resource MUST be present and contain references to the Advanced + * Firmware Update object instances that caused the conflict. See LwM2M + * specification for details. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @param out_target_iids Points to memory where to write array of iids of + * conflicting instances. + * + * @param out_target_iids_count Point to memory where to write count of target + * iids in an array. + * + * @returns 0 on success, or a negative value in case of error. + */ +int anjay_advanced_fw_update_get_conflicting_instances( + anjay_t *anjay, + anjay_iid_t iid, + const anjay_iid_t **out_target_iids, + size_t *out_target_iids_count); + +/** + * Gets the update deadline based on Maximum Defer Period resource value and + * time of downloading full firmware. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @returns Real time of the update deadline. In case of not deferring + * update returns @c AVS_TIME_REAL_INVALID. + */ +avs_time_real_t anjay_advanced_fw_update_get_deadline(anjay_t *anjay, + anjay_iid_t iid); + +/** + * Gets the update severity. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @returns Severity resource value present in Firmware Update object on + * success, or @ref ANJAY_ADVANCED_FW_UPDATE_SEVERITY_MANDATORY on error. + */ +anjay_advanced_fw_update_severity_t +anjay_advanced_fw_update_get_severity(anjay_t *anjay, anjay_iid_t iid); + +/** + * Gets the value of Last State Change Time resource. + * + * @param anjay Anjay object to operate on. + * + * @param iid Instance ID of an Advanced Firmware Object. + * + * @returns Real time of last State resource change, or @ref + * AVS_TIME_REAL_INVALID on error. + */ +avs_time_real_t +anjay_advanced_fw_update_get_last_state_change_time(anjay_t *anjay, + anjay_iid_t iid); + +#ifdef __cplusplus +} +#endif + +#endif /* ANJAY_INCLUDE_ANJAY_ADVANCED_FW_UPDATE_H */ diff --git a/include_public/anjay/anjay_config.h.in b/include_public/anjay/anjay_config.h.in index 3e22486d3..2e285c75a 100644 --- a/include_public/anjay/anjay_config.h.in +++ b/include_public/anjay/anjay_config.h.in @@ -256,6 +256,12 @@ */ #cmakedefine ANJAY_WITH_CORE_PERSISTENCE +/** + * Disable automatic closing of server connection sockets after + * MAX_TRANSMIT_WAIT of inactivity. + */ +#cmakedefine ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE + /** * Enable support for CoAP Content-Format numerical values 1541-1544 that have * been used before final LwM2M TS 1.0. @@ -500,6 +506,12 @@ */ #cmakedefine ANJAY_WITH_MODULE_FW_UPDATE +/** + * Enable advanced_fw_update module (implementation of the 33629 custom + * Advanced Firmware Update object). + */ +#cmakedefine ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + /** * Disable support for PUSH mode Firmware Update. * @@ -545,7 +557,7 @@ * AVS_COMMONS_WITH_AVS_PERSISTENCE to be enabled in avs_commons * configuration. * - * IMPORTANT: Only available with the boostrapper feature. Ignored in the open + * IMPORTANT: Only available with the bootstrapper feature. Ignored in the open * source version. */ #cmakedefine ANJAY_WITH_MODULE_BOOTSTRAPPER diff --git a/include_public/anjay/core.h b/include_public/anjay/core.h index 2635d52e9..45ce424be 100644 --- a/include_public/anjay/core.h +++ b/include_public/anjay/core.h @@ -261,6 +261,30 @@ typedef struct anjay_configuration { */ bool use_connection_id; + /** + * Send the Update message immediately when Object Instances are created or + * deleted. + * + * NOTE: In case of Create and Delete operations, the Update message will be + * immediately sent to all the servers, including the one + * that initiated the operation. + * + * By default, such data model changes are reported in the next scheduled + * update message (or the message can be requested using + * @ref anjay_schedule_registration_update), but the Update is not triggered + * automatically. + */ + bool update_immediately_on_dm_change; + + /** + * Send the Notify messages as a result of a server action (e.g. Write) even + * to the initiating server. + * + * By default, notifications resulting from server actions are only sent to + * the servers other than the one which initiated the action. + */ + bool enable_self_notify; + /** * (D)TLS ciphersuites to use if the "DTLS/TLS Ciphersuite" Resource * (/0/x/16) is not available or empty. @@ -579,49 +603,49 @@ typedef uint16_t anjay_riid_t; * Request sent by the LwM2M Server was malformed or contained an invalid * value. */ -#define ANJAY_ERR_BAD_REQUEST (-ANJAY_COAP_STATUS(4, 0)) +#define ANJAY_ERR_BAD_REQUEST (-(int) ANJAY_COAP_STATUS(4, 0)) /** * LwM2M Server is not allowed to perform the operation due to lack of * necessary access rights. */ -#define ANJAY_ERR_UNAUTHORIZED (-ANJAY_COAP_STATUS(4, 1)) +#define ANJAY_ERR_UNAUTHORIZED (-(int) ANJAY_COAP_STATUS(4, 1)) /** * Low-level CoAP error code; used internally by Anjay when CoAP option values * were invalid. */ -#define ANJAY_ERR_BAD_OPTION (-ANJAY_COAP_STATUS(4, 2)) -#define ANJAY_ERR_FORBIDDEN (-ANJAY_COAP_STATUS(4, 3)) +#define ANJAY_ERR_BAD_OPTION (-(int) ANJAY_COAP_STATUS(4, 2)) +#define ANJAY_ERR_FORBIDDEN (-(int) ANJAY_COAP_STATUS(4, 3)) /** Target of the operation (Object/Instance/Resource) does not exist. */ -#define ANJAY_ERR_NOT_FOUND (-ANJAY_COAP_STATUS(4, 4)) +#define ANJAY_ERR_NOT_FOUND (-(int) ANJAY_COAP_STATUS(4, 4)) /** * Operation is not allowed in current device state or the attempted operation * is invalid for this target (Object/Instance/Resource) */ -#define ANJAY_ERR_METHOD_NOT_ALLOWED (-ANJAY_COAP_STATUS(4, 5)) +#define ANJAY_ERR_METHOD_NOT_ALLOWED (-(int) ANJAY_COAP_STATUS(4, 5)) /** * Low-level CoAP error code; used internally by Anjay when the client is * unable to encode response in requested content format. */ -#define ANJAY_ERR_NOT_ACCEPTABLE (-ANJAY_COAP_STATUS(4, 6)) +#define ANJAY_ERR_NOT_ACCEPTABLE (-(int) ANJAY_COAP_STATUS(4, 6)) /** * Low-level CoAP error code; used internally by Anjay in case of unrecoverable * problems during block-wise transfer. */ -#define ANJAY_ERR_REQUEST_ENTITY_INCOMPLETE (-ANJAY_COAP_STATUS(4, 8)) +#define ANJAY_ERR_REQUEST_ENTITY_INCOMPLETE (-(int) ANJAY_COAP_STATUS(4, 8)) /** * The server requested operation has a Content Format option that is * unsupported by Anjay. */ -#define ANJAY_ERR_UNSUPPORTED_CONTENT_FORMAT (-ANJAY_COAP_STATUS(4, 15)) +#define ANJAY_ERR_UNSUPPORTED_CONTENT_FORMAT (-(int) ANJAY_COAP_STATUS(4, 15)) /** Unspecified error, no other error code was suitable. */ -#define ANJAY_ERR_INTERNAL (-ANJAY_COAP_STATUS(5, 0)) +#define ANJAY_ERR_INTERNAL (-(int) ANJAY_COAP_STATUS(5, 0)) /** Operation is not implemented by the LwM2M Client. */ -#define ANJAY_ERR_NOT_IMPLEMENTED (-ANJAY_COAP_STATUS(5, 1)) +#define ANJAY_ERR_NOT_IMPLEMENTED (-(int) ANJAY_COAP_STATUS(5, 1)) /** * LwM2M Client is busy processing some other request; LwM2M Server may retry * sending the same request after some delay. */ -#define ANJAY_ERR_SERVICE_UNAVAILABLE (-ANJAY_COAP_STATUS(5, 3)) +#define ANJAY_ERR_SERVICE_UNAVAILABLE (-(int) ANJAY_COAP_STATUS(5, 3)) /** @} */ /** @@ -794,7 +818,7 @@ int anjay_event_loop_run_with_error_handling(anjay_t *anjay_locked, * instead wait until either of the following: * * - any ongoing scheduler tasks finish - * - any incoming RPC involving a blockwise transfer finishes + * - any incoming operation involving a blockwise transfer finishes * - the poll or select() operation finishes (see the * max_wait_time argument to @ref anjay_event_loop_run) * @@ -849,6 +873,39 @@ int anjay_event_loop_interrupt(anjay_t *anjay); int anjay_serve_any(anjay_t *anjay, avs_time_duration_t max_wait_time); #endif // ANJAY_WITH_EVENT_LOOP +/** + * Schedules sending a Register message to the server identified by given + * Short Server ID. + * + * For currently connected servers, the Register message will be sent during the + * next @ref anjay_sched_run call, without reconnecting (and thus without a new + * DTLS handshake, if applicable). Please additionally call + * @ref anjay_server_schedule_reconnect before or after this function (without + * running @ref anjay_sched_run in between - so in multi-threaded applications + * you may need to do that from within an intermediary scheduler job) if you + * want to force a reconnect. + * + * For servers that are disabled or in a failure state, this function will + * invalidate the registration state, so that a new Register message will + * definitely be sent once the server is re-enabled (even if the DTLS session + * is successfully resumed), but will not re-enable it itself. Please + * additionally call @ref anjay_enable_server before or after this funciton if + * you want the server reactivated immediately. + * + * Note: This function will not change the offline state of the server's + * transport. + * + * @param anjay Anjay object to operate on. + * @param ssid Short Server ID of the server to send Register to or + * @ref ANJAY_SSID_ANY to send Register to all known servers. + * NOTE: Since Register is not useful for the Bootstrap Server, + * this function does not send one for @ref ANJAY_SSID_BOOTSTRAP + * @p ssid . + * + * @returns 0 on success, a negative value in case of error. + */ +int anjay_schedule_register(anjay_t *anjay, anjay_ssid_t ssid); + /** * Schedules sending an Update message to the server identified by given * Short Server ID. @@ -898,6 +955,7 @@ int anjay_schedule_bootstrap_request(anjay_t *anjay); * If the server is already disabled, its re-enable action will be re-scheduled * according to the value of the Disable Timeout resource added to current time. * + * * @param anjay Anjay object to operate on. * @param ssid Short Server ID of the server to put in a disabled state. * NOTE: disabling a server requires a Server Object Instance @@ -924,6 +982,7 @@ int anjay_disable_server(anjay_t *anjay, anjay_ssid_t ssid); * communication channels. Shutting down only one of them requires changing * the Binding Resource in Server object. * + * * @param anjay Anjay object to operate on. * @param ssid Short Server ID of the server to put in a disabled state. * @param timeout Disable timeout. If set to @c AVS_TIME_DURATION_INVALID, @@ -949,6 +1008,27 @@ int anjay_disable_server_with_timeout(anjay_t *anjay, */ int anjay_enable_server(anjay_t *anjay, anjay_ssid_t ssid); +/** + * Reconnects sockets associated with a specific LwM2M Server. + * + * The reconnection will be performed during the next @ref anjay_sched_run call + * and will trigger sending any messages necessary to maintain valid + * registration (DTLS session resumption and/or Register or Update operations). + * + * If the server connection is disconnected due to queue mode, and there are no + * outstanding messages (Register, Update, Notify or Send), the socket will not + * be reconnected. + * + * If the server is in a disabled state, an error will be returned. Use + * @ref anjay_enable_server if you want to re-enable such a server. + * + * @param anjay Anjay object to operate on. + * @param ssid Short Server ID of the server to reconnect. + * + * @returns 0 on success, a negative value in case of error. + */ +int anjay_server_schedule_reconnect(anjay_t *anjay, anjay_ssid_t ssid); + /** * Structure defining the set of transports that * @ref anjay_transport_is_offline, @ref anjay_transport_enter_offline, @@ -1072,7 +1152,7 @@ int anjay_transport_set_online(anjay_t *anjay, * * The reconnection will be performed during the next @ref anjay_sched_run call * and will trigger sending any messages necessary to maintain valid - * registration (DTLS session resumption and/or Register or Update RPCs). + * registration (DTLS session resumption and/or Register or Update operations). * * In case of ongoing downloads (started via @ref anjay_download or the * fw_update module), if the reconnection fails, the download will be @@ -1602,7 +1682,7 @@ avs_error_t anjay_update_dtls_handshake_timeouts( avs_net_dtls_handshake_timeouts_t dtls_handshake_timeouts); #ifdef ANJAY_WITH_COMMUNICATION_TIMESTAMP_API -/* +/** * Gets the time at which the client has registered successfully to a given * LwM2M server for the last time. * @@ -1626,7 +1706,7 @@ avs_error_t anjay_get_server_last_registration_time(anjay_t *anjay, anjay_ssid_t ssid, avs_time_real_t *out_time); -/* +/** * Gets the time at which next registration update operation with a given * LwM2M Server is scheduled. * @@ -1658,7 +1738,7 @@ avs_error_t anjay_get_server_last_registration_time(anjay_t *anjay, avs_error_t anjay_get_server_next_update_time(anjay_t *anjay, anjay_ssid_t ssid, avs_time_real_t *out_time); -/* +/** * Gets the time at which the client has communicated with a given LwM2M Server * for the last time. * diff --git a/include_public/anjay/dm.h b/include_public/anjay/dm.h index e8a8dda08..54a5cdc47 100644 --- a/include_public/anjay/dm.h +++ b/include_public/anjay/dm.h @@ -84,7 +84,7 @@ extern const anjay_dm_r_attributes_t ANJAY_DM_R_ATTRIBUTES_EMPTY; * @param anjay Anjay object to operate on. * @param obj_ptr Object definition pointer, as passed to * @ref anjay_register_object . - * @param ssid Short Server ID of the server requesting the RPC. + * @param ssid Short Server ID of the server requesting the operation. * @param[out] out Attributes struct to be filled by the handler. * * @returns This handler should return: @@ -106,7 +106,7 @@ typedef int anjay_dm_object_read_default_attrs_t( * @param anjay Anjay object to operate on. * @param obj_ptr Object definition pointer, as passed to * @ref anjay_register_object . - * @param ssid Short Server ID of the server requesting the RPC. + * @param ssid Short Server ID of the server requesting the operation. * @param attrs Attributes struct to be set for the Object. * * @returns This handler should return: @@ -242,7 +242,7 @@ anjay_dm_instance_create_t(anjay_t *anjay, * @param obj_ptr Object definition pointer, as passed to * @ref anjay_register_object . * @param iid Checked Object Instance ID. - * @param ssid Short Server ID of the server requesting the RPC. + * @param ssid Short Server ID of the server requesting the operation. * @param[out] out Returned attributes. * * @returns This handler should return: @@ -266,7 +266,7 @@ typedef int anjay_dm_instance_read_default_attrs_t( * @param obj_ptr Object definition pointer, as passed to * @ref anjay_register_object . * @param iid Checked Object Instance ID. - * @param ssid Short Server ID of the server requesting the RPC. + * @param ssid Short Server ID of the server requesting the operation. * @param attrs Attributes to set for the Object Instance. * * @returns This handler should return: @@ -1106,7 +1106,8 @@ anjay_resource_observation_status_t anjay_resource_observation_status( #endif // ANJAY_WITH_OBSERVATION_STATUS /** - * Registers the Object in the data model, making it available for RPC calls. + * Registers the Object in the data model, making it available for access by the + * LwM2M Servers. * * NOTE: def_ptr MUST stay valid up to and including the corresponding * @ref anjay_delete or @ref anjay_unregister_object call. @@ -1123,7 +1124,7 @@ int anjay_register_object(anjay_t *anjay, /** * Unregisters an Object in the data model, so that it is no longer available - * for RPC calls. + * for access by the LwM2M Servers. * * def_ptr MUST be a pointer previously passed to * @ref anjay_register_object for the same anjay object. diff --git a/include_public/anjay/factory_provisioning.h b/include_public/anjay/factory_provisioning.h index 8df51fba6..b2896bc6e 100644 --- a/include_public/anjay/factory_provisioning.h +++ b/include_public/anjay/factory_provisioning.h @@ -9,6 +9,7 @@ #ifndef ANJAY_INCLUDE_ANJAY_FACTORY_PROVISIONING_H #define ANJAY_INCLUDE_ANJAY_FACTORY_PROVISIONING_H +#include #include #include diff --git a/include_public/anjay/fw_update.h b/include_public/anjay/fw_update.h index df1bbdb95..55aa5c13c 100644 --- a/include_public/anjay/fw_update.h +++ b/include_public/anjay/fw_update.h @@ -48,13 +48,13 @@ typedef enum { * to a value default for a given handler. */ #define ANJAY_FW_UPDATE_ERR_NOT_ENOUGH_SPACE \ - (-ANJAY_FW_UPDATE_RESULT_NOT_ENOUGH_SPACE) + (-(int) ANJAY_FW_UPDATE_RESULT_NOT_ENOUGH_SPACE) #define ANJAY_FW_UPDATE_ERR_OUT_OF_MEMORY \ - (-ANJAY_FW_UPDATE_RESULT_OUT_OF_MEMORY) + (-(int) ANJAY_FW_UPDATE_RESULT_OUT_OF_MEMORY) #define ANJAY_FW_UPDATE_ERR_INTEGRITY_FAILURE \ - (-ANJAY_FW_UPDATE_RESULT_INTEGRITY_FAILURE) + (-(int) ANJAY_FW_UPDATE_RESULT_INTEGRITY_FAILURE) #define ANJAY_FW_UPDATE_ERR_UNSUPPORTED_PACKAGE_TYPE \ - (-ANJAY_FW_UPDATE_RESULT_UNSUPPORTED_PACKAGE_TYPE) + (-(int) ANJAY_FW_UPDATE_RESULT_UNSUPPORTED_PACKAGE_TYPE) /** @} */ @@ -358,10 +358,10 @@ typedef const char *anjay_fw_update_get_version_t(void *user_ptr); * reboot. In such case, it is expected that this callback will do either one of * the following: * - * - return, causing the outermost event loop to terminate, shutdown the library - * and then perform the firmware upgrade and then the device to reboot - * - perform the firmware upgrade internally and never return, causing a reboot - * in the process + * - perform firmware upgrade, terminate outermost event loop and return, + * call reboot after @ref anjay_event_loop_run() + * - perform the firmware upgrade internally and then reboot, it means that + * the return will never happen * * After rebooting, the result of the upgrade process may be passed to the * library during initialization via the initial_result argument to @@ -383,8 +383,6 @@ typedef const char *anjay_fw_update_get_version_t(void *user_ptr); * Otherwise, if a non-zero value is returned, the Update Result * Resource is set to generic "Firmware update failed" code. * - * If an update is to be attempted, it shall either return 0 or - * perform a reboot internally without returning. */ typedef int anjay_fw_update_perform_upgrade_t(void *user_ptr); @@ -437,6 +435,8 @@ typedef int anjay_fw_update_perform_upgrade_t(void *user_ptr); * first. Anjay will not attempt to * deallocate anything automatically. * + * @param download_uri Target firmware URI. + * * @returns The callback shall return 0 if successful or a negative value in * case of error. If one of the ANJAY_FW_UPDATE_ERR_* value is * returned, an equivalent value will be set in the Update Result @@ -538,7 +538,7 @@ typedef struct { anjay_fw_update_get_security_config_t *get_security_config; /** Queries CoAP transmission parameters to be used during firmware - * update. */ + * update; @ref anjay_fw_update_get_coap_tx_params_t */ anjay_fw_update_get_coap_tx_params_t *get_coap_tx_params; } anjay_fw_update_handlers_t; @@ -605,6 +605,8 @@ int anjay_fw_update_install( * @param anjay Anjay object to operate on. * * @param result Value of the Update Result resource to set. + * + * @returns 0 on success, or a negative value in case of error. */ int anjay_fw_update_set_result(anjay_t *anjay, anjay_fw_update_result_t result); diff --git a/include_public/anjay/io.h b/include_public/anjay/io.h index e7cc7ca97..ebc876e27 100644 --- a/include_public/anjay/io.h +++ b/include_public/anjay/io.h @@ -119,12 +119,12 @@ void anjay_dm_emit_res(anjay_dm_resource_list_ctx_t *ctx, anjay_dm_resource_kind_t kind, anjay_dm_resource_presence_t presence); -/** Type used to return some content in response to a RPC. */ +/** Type used to return some content in response to a request from server. */ typedef struct anjay_output_ctx_struct anjay_output_ctx_t; -/** Type used to return a chunked blob of data in response to a RPC. Useful in - * cases where the application needs to send more data than it can fit in - * the memory. */ +/** Type used to return a chunked blob of data in response to a request from + * server. Useful in cases where the application needs to send more data than it + * can fit in the memory. */ typedef struct anjay_ret_bytes_ctx_struct anjay_ret_bytes_ctx_t; /** @@ -132,7 +132,7 @@ typedef struct anjay_ret_bytes_ctx_struct anjay_ret_bytes_ctx_t; * in conjunction with @ref anjay_ret_bytes_append to return a large blob of * data in multiple chunks. * - * Example: file content in an RPC response. + * Example: file content in the response. * * @code * FILE *file; @@ -271,7 +271,7 @@ static inline int anjay_ret_u32(anjay_output_ctx_t *ctx, uint32_t value) { } #endif // ANJAY_WITH_LWM2M11 -/* +/** * Returns a 64-bit floating-point value from the data model handler. * * Note: the @p value will be sent as a 32-bit floating-point value if it is @@ -409,7 +409,7 @@ int anjay_ret_psk_key_info(anjay_output_ctx_t *ctx, avs_crypto_psk_key_info_t psk_key_info); #endif // ANJAY_WITH_SECURITY_STRUCTURED -/** Type used to retrieve RPC content. */ +/** Type used to retrieve request content. */ typedef struct anjay_input_ctx_struct anjay_input_ctx_t; #define ANJAY_EXECUTE_GET_ARG_END 1 @@ -482,7 +482,7 @@ int anjay_execute_get_arg_value(anjay_execute_ctx_t *ctx, size_t buf_size); /** - * Reads a chunk of data blob from the RPC request message. + * Reads a chunk of data blob from the request message. * * Consecutive calls to this function will return successive chunks of * the data blob. Reaching end of the data is signaled by setting the @@ -526,10 +526,10 @@ int anjay_get_bytes(anjay_input_ctx_t *ctx, #define ANJAY_BUFFER_TOO_SHORT 1 /** - * Reads a null-terminated string from the RPC request content. On success or - * even when @ref ANJAY_BUFFER_TOO_SHORT is returned, the content inside - * @p out_buf is always null-terminated. On failure, the contents of @p out_buf - * are undefined. + * Reads a null-terminated string from the request content. On success or even + * when @ref ANJAY_BUFFER_TOO_SHORT is returned, the content inside @p out_buf + * is always null-terminated. On failure, the contents of @p out_buf are + * undefined. * * When the input buffer is not big enough to contain whole message content + * terminating nullbyte, ANJAY_BUFFER_TOO_SHORT is returned, after which further @@ -547,7 +547,7 @@ int anjay_get_bytes(anjay_input_ctx_t *ctx, int anjay_get_string(anjay_input_ctx_t *ctx, char *out_buf, size_t buf_size); /** - * Reads an integer as a 32-bit signed value from the RPC request content. + * Reads an integer as a 32-bit signed value from the request content. * * @param ctx Input context to operate on. * @param[out] out Returned value. If the call is not successful, it is @@ -558,7 +558,7 @@ int anjay_get_string(anjay_input_ctx_t *ctx, char *out_buf, size_t buf_size); int anjay_get_i32(anjay_input_ctx_t *ctx, int32_t *out); /** - * Reads an integer as a 64-bit signed value from the RPC request content. + * Reads an integer as a 64-bit signed value from the request content. * * @param ctx Input context to operate on. * @param[out] out Returned value. If the call is not successful, it is @@ -570,7 +570,7 @@ int anjay_get_i64(anjay_input_ctx_t *ctx, int64_t *out); #ifdef ANJAY_WITH_LWM2M11 /** - * Reads an unsigned integer as a 32-bit unsigned value from the RPC request + * Reads an unsigned integer as a 32-bit unsigned value from the request * content. * * @param ctx Input context to operate on. @@ -582,7 +582,7 @@ int anjay_get_i64(anjay_input_ctx_t *ctx, int64_t *out); int anjay_get_u32(anjay_input_ctx_t *ctx, uint32_t *out); /** - * Reads an unsigned integer as a 64-bit unsigned value from the RPC request + * Reads an unsigned integer as a 64-bit unsigned value from the request * content. * * @param ctx Input context to operate on. @@ -595,7 +595,7 @@ int anjay_get_u64(anjay_input_ctx_t *ctx, uint64_t *out); #endif // ANJAY_WITH_LWM2M11 /** - * Reads a floating-point value as a float from the RPC request content. + * Reads a floating-point value as a float from the request content. * * @param ctx Input context to operate on. * @param[out] out Returned value. If the call is not successful, it is @@ -606,7 +606,7 @@ int anjay_get_u64(anjay_input_ctx_t *ctx, uint64_t *out); int anjay_get_float(anjay_input_ctx_t *ctx, float *out); /** - * Reads a floating-point value as a double from the RPC request content. + * Reads a floating-point value as a double from the request content. * * @param ctx Input context to operate on. * @param[out] out Returned value. If the call is not successful, it is @@ -617,7 +617,7 @@ int anjay_get_float(anjay_input_ctx_t *ctx, float *out); int anjay_get_double(anjay_input_ctx_t *ctx, double *out); /** - * Reads a boolean value from the RPC request content. + * Reads a boolean value from the request content. * * @param ctx Input context to operate on. * @param[out] out Returned value. If the call is not successful, it is @@ -628,8 +628,8 @@ int anjay_get_double(anjay_input_ctx_t *ctx, double *out); int anjay_get_bool(anjay_input_ctx_t *ctx, bool *out); /** - * Reads an object link (Object ID/Object Instance ID pair) from the RPC - * request content. + * Reads an object link (Object ID/Object Instance ID pair) from the request + * content. * * @param ctx Input context to operate on. * @param[out] out_oid Object ID part of the returned value. diff --git a/src/anjay_config_log.h b/src/anjay_config_log.h index 2282e3128..067713e26 100644 --- a/src/anjay_config_log.h +++ b/src/anjay_config_log.h @@ -44,6 +44,11 @@ static inline void _anjay_log_feature_list(void) { #else // ANJAY_WITHOUT_PLAINTEXT _anjay_log(anjay, TRACE, "ANJAY_WITHOUT_PLAINTEXT = OFF"); #endif // ANJAY_WITHOUT_PLAINTEXT +#ifdef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE + _anjay_log(anjay, TRACE, "ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE = ON"); +#else // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE + _anjay_log(anjay, TRACE, "ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE = OFF"); +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE #ifdef ANJAY_WITHOUT_TLV _anjay_log(anjay, TRACE, "ANJAY_WITHOUT_TLV = ON"); #else // ANJAY_WITHOUT_TLV @@ -149,6 +154,11 @@ static inline void _anjay_log_feature_list(void) { #else // ANJAY_WITH_MODULE_ACCESS_CONTROL _anjay_log(anjay, TRACE, "ANJAY_WITH_MODULE_ACCESS_CONTROL = OFF"); #endif // ANJAY_WITH_MODULE_ACCESS_CONTROL +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + _anjay_log(anjay, TRACE, "ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE = ON"); +#else // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + _anjay_log(anjay, TRACE, "ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE = OFF"); +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE #ifdef ANJAY_WITH_MODULE_AT_SMS _anjay_log(anjay, TRACE, "ANJAY_WITH_MODULE_AT_SMS = ON"); #else // ANJAY_WITH_MODULE_AT_SMS diff --git a/src/anjay_modules/anjay_bootstrap.h b/src/anjay_modules/anjay_bootstrap.h index bfb3ec138..375967ad1 100644 --- a/src/anjay_modules/anjay_bootstrap.h +++ b/src/anjay_modules/anjay_bootstrap.h @@ -20,6 +20,8 @@ VISIBILITY_PRIVATE_HEADER_BEGIN bool _anjay_bootstrap_in_progress(anjay_unlocked_t *anjay); +void _anjay_bootstrap_cleanup(anjay_unlocked_t *anjay); + # if defined(ANJAY_WITH_MODULE_FACTORY_PROVISIONING) avs_error_t _anjay_bootstrap_delete_everything(anjay_unlocked_t *anjay); @@ -40,6 +42,8 @@ int _anjay_schedule_bootstrap_request_unlocked(anjay_unlocked_t *anjay); # define _anjay_bootstrap_in_progress(anjay) ((void) (anjay), false) +# define _anjay_bootstrap_cleanup(anjay) ((void) 0) + #endif VISIBILITY_PRIVATE_HEADER_END diff --git a/src/anjay_modules/anjay_dm_utils.h b/src/anjay_modules/anjay_dm_utils.h index 19d429153..f200905ad 100644 --- a/src/anjay_modules/anjay_dm_utils.h +++ b/src/anjay_modules/anjay_dm_utils.h @@ -963,6 +963,18 @@ int _anjay_get_objlnk_unlocked(anjay_unlocked_input_ctx_t *ctx, anjay_oid_t *out_oid, anjay_iid_t *out_iid); +int _anjay_execute_get_next_arg_unlocked(anjay_unlocked_execute_ctx_t *ctx, + int *out_arg, + bool *out_has_value); + +int _anjay_execute_get_arg_value_unlocked(anjay_unlocked_execute_ctx_t *ctx, + size_t *out_bytes_read, + char *out_buf, + size_t buf_size); + +AVS_LIST(const anjay_ssid_t) +_anjay_server_get_ssids_unlocked(anjay_unlocked_t *anjay); + VISIBILITY_PRIVATE_HEADER_END #endif /* ANJAY_INCLUDE_ANJAY_MODULES_DM_H */ diff --git a/src/core/anjay_bootstrap_core.c b/src/core/anjay_bootstrap_core.c index 051189902..09348842a 100644 --- a/src/core/anjay_bootstrap_core.c +++ b/src/core/anjay_bootstrap_core.c @@ -771,11 +771,11 @@ int _anjay_bootstrap_notify_regular_connection_available( return 0; } int result = 0; + anjay_connection_ref_t bootstrap_connection = { + .server = _anjay_servers_find_active(anjay, ANJAY_SSID_BOOTSTRAP), + .conn_type = ANJAY_CONNECTION_PRIMARY + }; if (anjay->bootstrap.in_progress) { - anjay_connection_ref_t bootstrap_connection = { - .server = _anjay_servers_find_active(anjay, ANJAY_SSID_BOOTSTRAP), - .conn_type = ANJAY_CONNECTION_PRIMARY - }; (void) ((result = validate_bootstrap_configuration( anjay, bootstrap_connection)) || (result = bootstrap_finish_impl( diff --git a/src/core/anjay_bootstrap_core.h b/src/core/anjay_bootstrap_core.h index 7628319b4..8f826b40d 100644 --- a/src/core/anjay_bootstrap_core.h +++ b/src/core/anjay_bootstrap_core.h @@ -61,8 +61,6 @@ int _anjay_perform_bootstrap_action_if_appropriate( void _anjay_bootstrap_init(anjay_bootstrap_t *bootstrap, bool allow_legacy_server_initiated_bootstrap); -void _anjay_bootstrap_cleanup(anjay_unlocked_t *anjay); - #else # define _anjay_bootstrap_notify_regular_connection_available(anjay) \ @@ -76,8 +74,6 @@ void _anjay_bootstrap_cleanup(anjay_unlocked_t *anjay); # define _anjay_perform_bootstrap_action_if_appropriate(...) (-1) -# define _anjay_bootstrap_cleanup(anjay) ((void) 0) - #endif VISIBILITY_PRIVATE_HEADER_END diff --git a/src/core/anjay_core.c b/src/core/anjay_core.c index caa465764..cfa066c51 100644 --- a/src/core/anjay_core.c +++ b/src/core/anjay_core.c @@ -47,7 +47,7 @@ VISIBILITY_SOURCE_BEGIN #ifndef ANJAY_VERSION -# define ANJAY_VERSION "3.3.1" +# define ANJAY_VERSION "3.4.0" #endif // ANJAY_VERSION #ifdef ANJAY_WITH_LWM2M11 @@ -228,6 +228,9 @@ static int init_anjay(anjay_unlocked_t *anjay, #endif // ANJAY_WITH_DOWNLOADER anjay->prefer_hierarchical_formats = config->prefer_hierarchical_formats; + anjay->update_immediately_on_dm_change = + config->update_immediately_on_dm_change; + anjay->enable_self_notify = config->enable_self_notify; anjay->use_connection_id = config->use_connection_id; anjay->additional_tls_config_clb = config->additional_tls_config_clb; @@ -859,7 +862,7 @@ static int handle_request(anjay_connection_ref_t connection, result = _anjay_bootstrap_perform_action(connection, request); } else { result = _anjay_dm_perform_action(connection, request); - _anjay_observe_sched_flush(connection); + (void) _anjay_observe_sched_flush(connection); } return result; } diff --git a/src/core/anjay_core.h b/src/core/anjay_core.h index 3b7a174fa..ad3770a90 100644 --- a/src/core/anjay_core.h +++ b/src/core/anjay_core.h @@ -141,6 +141,8 @@ struct anjay_downloader_t downloader; #endif // ANJAY_WITH_DOWNLOADER bool prefer_hierarchical_formats; + bool update_immediately_on_dm_change; + bool enable_self_notify; #ifdef ANJAY_WITH_NET_STATS closed_connections_stats_t closed_connections_stats; #endif // ANJAY_WITH_NET_STATS diff --git a/src/core/anjay_lwm2m_send.c b/src/core/anjay_lwm2m_send.c index cd7e8f50c..b84fb9501 100644 --- a/src/core/anjay_lwm2m_send.c +++ b/src/core/anjay_lwm2m_send.c @@ -825,6 +825,7 @@ static void retry_deferred_job(avs_sched_t *sched, const void *ssid_) { ANJAY_MUTEX_UNLOCK(anjay_locked); } +# ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE bool _anjay_send_has_deferred(anjay_unlocked_t *anjay, anjay_ssid_t ssid) { assert(ssid != ANJAY_SSID_ANY); AVS_LIST(anjay_send_entry_t) it; @@ -840,6 +841,7 @@ bool _anjay_send_has_deferred(anjay_unlocked_t *anjay, anjay_ssid_t ssid) { } return false; } +# endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE int _anjay_send_sched_retry_deferred(anjay_unlocked_t *anjay, anjay_ssid_t ssid) { diff --git a/src/core/anjay_lwm2m_send.h b/src/core/anjay_lwm2m_send.h index 6481e1884..ab36ff483 100644 --- a/src/core/anjay_lwm2m_send.h +++ b/src/core/anjay_lwm2m_send.h @@ -26,7 +26,9 @@ void _anjay_send_interrupt(anjay_connection_ref_t ref); void _anjay_send_cleanup(anjay_sender_t *sender); +#ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE bool _anjay_send_has_deferred(anjay_unlocked_t *anjay, anjay_ssid_t ssid); +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE int _anjay_send_sched_retry_deferred(anjay_unlocked_t *anjay, anjay_ssid_t ssid); diff --git a/src/core/anjay_notify.c b/src/core/anjay_notify.c index 2f1781e35..5700ce690 100644 --- a/src/core/anjay_notify.c +++ b/src/core/anjay_notify.c @@ -64,7 +64,10 @@ security_modified_notify(anjay_unlocked_t *anjay, last_iid = it->iid; } } - if (security->instance_set_changes.instance_set_changed) { + // NOTE: If anjay->update_immediately_on_dm_change is true, + // then this will be called from anjay_notify_perform_impl() itself + if (!anjay->update_immediately_on_dm_change + && security->instance_set_changes.instance_set_changed) { _anjay_update_ret(&ret, _anjay_schedule_reload_servers(anjay)); } return ret; @@ -74,7 +77,11 @@ static int server_modified_notify(anjay_unlocked_t *anjay, anjay_notify_queue_object_entry_t *server) { int ret = 0; if (server->instance_set_changes.instance_set_changed) { - _anjay_update_ret(&ret, _anjay_schedule_reload_servers(anjay)); + // NOTE: If anjay->update_immediately_on_dm_change is true, + // then this will be called from anjay_notify_perform_impl() itself + if (!anjay->update_immediately_on_dm_change) { + _anjay_update_ret(&ret, _anjay_schedule_reload_servers(anjay)); + } #ifdef ANJAY_WITH_SEND // servers may have been removed from data model // if so, abort their Send requests as well @@ -125,20 +132,29 @@ static int anjay_notify_perform_impl(anjay_unlocked_t *anjay, if (!queue_ptr || !*queue_ptr) { return 0; } + bool instances_modified = false; int ret = 0; _anjay_update_ret(&ret, _anjay_sync_access_control(anjay, origin_ssid, queue_ptr)); AVS_LIST(anjay_notify_queue_object_entry_t) it; AVS_LIST_FOREACH(it, *queue_ptr) { - if (it->oid > ANJAY_DM_OID_SERVER) { - break; - } else if (it->oid == ANJAY_DM_OID_SECURITY) { + if (it->instance_set_changes.instance_set_changed) { + instances_modified = true; + } + if (it->oid == ANJAY_DM_OID_SECURITY) { _anjay_update_ret(&ret, security_modified_notify(anjay, it)); } else if (server_notify && it->oid == ANJAY_DM_OID_SERVER) { _anjay_update_ret(&ret, server_modified_notify(anjay, it)); } } - _anjay_update_ret(&ret, observe_notify(anjay, origin_ssid, *queue_ptr)); + if (instances_modified && anjay->update_immediately_on_dm_change) { + _anjay_update_ret(&ret, _anjay_schedule_reload_servers(anjay)); + } + _anjay_update_ret(&ret, observe_notify(anjay, + anjay->enable_self_notify + ? ANJAY_SSID_BOOTSTRAP + : origin_ssid, + *queue_ptr)); #ifdef ANJAY_WITH_ATTR_STORAGE _anjay_update_ret(&ret, _anjay_attr_storage_notify(anjay, *queue_ptr)); #endif // ANJAY_WITH_ATTR_STORAGE diff --git a/src/core/anjay_servers_private.h b/src/core/anjay_servers_private.h index 688fa92dc..d6b86a82b 100644 --- a/src/core/anjay_servers_private.h +++ b/src/core/anjay_servers_private.h @@ -17,6 +17,7 @@ #include #include +#include #include @@ -80,16 +81,10 @@ _anjay_conn_session_tokens_equal(anjay_conn_session_token_t left, return avs_time_monotonic_equal(left.value, right.value); } -// copied from deps/avs_commons/http/src/headers.h -// see description there for rationale; TODO: move this to public Commons API? -// note that this _includes_ the terminating null byte -#define ANJAY_UINT_STR_BUF_SIZE(type) ((12 * sizeof(type)) / 5 + 2) - // 6.2.2 Object Version format: // "The Object Version of an Object is composed of 2 digits separated by a dot" // However, we're a bit lenient to support proper numbers and not just digits. -#define ANJAY_DM_OBJECT_VERSION_BUF_LENGTH \ - (2 * ANJAY_UINT_STR_BUF_SIZE(unsigned)) +#define ANJAY_DM_OBJECT_VERSION_BUF_LENGTH (2 * AVS_UINT_STR_BUF_SIZE(unsigned)) typedef struct { anjay_oid_t oid; @@ -463,12 +458,19 @@ void _anjay_server_on_fatal_coap_error(anjay_connection_ref_t conn_ref, */ const anjay_url_t *_anjay_connection_uri(anjay_connection_ref_t ref); +#ifdef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE +# define _anjay_connection_schedule_queue_mode_close(...) ((void) 0) +#else // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE /** - * This function is called from _anjay_release_connection() - if the - * connection is in queue mode, it schedules closing of the socket (suspending - * the connection) after MAX_TRANSMIT_WAIT passes. + * This function schedules closing of the socket (suspending the connection) + * after MAX_TRANSMIT_WAIT passes. It is supposed to be called after finishing + * each interaction with a server - generally after each call to + * avs_coap_streaming_handle_incoming_packet(), + * avs_coap_client_send_async_request(), avs_coap_notify_async(), as well as + * after (re)connecting a socket even if no outgoing message is being sent. */ void _anjay_connection_schedule_queue_mode_close(anjay_connection_ref_t ref); +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE /** * Returns the socket associated with a given connection, if it exists and is in @@ -503,10 +505,9 @@ void _anjay_connection_mark_stable(anjay_connection_ref_t ref); void _anjay_connection_bring_online(anjay_connection_ref_t ref); /** - * Suspends the specified connection (or all connections in the server if - * conn_ref.conn_type == ANJAY_CONNECTION_UNSET). Suspending the connection - * means closing the socket, but not cleaning it up. The connection (and server) - * is then still considered active, but not online. + * Suspends the specified connection. Suspending the connection means closing + * the socket, but not cleaning it up. The connection (and server) is then still + * considered active, but not online. */ void _anjay_connection_suspend(anjay_connection_ref_t conn_ref); diff --git a/src/core/anjay_servers_utils.h b/src/core/anjay_servers_utils.h index d79ad7a20..03f54d911 100644 --- a/src/core/anjay_servers_utils.h +++ b/src/core/anjay_servers_utils.h @@ -41,6 +41,8 @@ bool _anjay_server_registration_expired(anjay_server_info_t *server); int _anjay_schedule_socket_update(anjay_unlocked_t *anjay, anjay_iid_t security_iid); +bool _anjay_server_connection_active(anjay_connection_ref_t ref); + VISIBILITY_PRIVATE_HEADER_END #endif // ANJAY_SERVERS_H diff --git a/src/core/anjay_utils_core.c b/src/core/anjay_utils_core.c index b5b818713..84c10c5fb 100644 --- a/src/core/anjay_utils_core.c +++ b/src/core/anjay_utils_core.c @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -818,6 +819,8 @@ int _anjay_safe_strtod(const char *in, double *value) { return map_str_conversion_result(in, endptr); } +// || defined(ANJAY_WITH_CORE_PERSISTENCE)) + #ifdef ANJAY_TEST # include "tests/core/utils.c" #endif // ANJAY_TEST diff --git a/src/core/anjay_utils_private.h b/src/core/anjay_utils_private.h index d49d2478f..a2a57863f 100644 --- a/src/core/anjay_utils_private.h +++ b/src/core/anjay_utils_private.h @@ -11,6 +11,7 @@ #define ANJAY_UTILS_PRIVATE_H #include +#include #include #include @@ -127,6 +128,8 @@ bool _anjay_socket_transport_included(anjay_transport_set_t set, bool _anjay_socket_transport_is_online(anjay_unlocked_t *anjay, anjay_socket_transport_t transport); +// || defined(ANJAY_WITH_CORE_PERSISTENCE)) + #define ANJAY_SMS_URI_SCHEME "tel" VISIBILITY_PRIVATE_HEADER_END diff --git a/src/core/dm/anjay_dm_execute.c b/src/core/dm/anjay_dm_execute.c index f3f01c404..74ad50130 100644 --- a/src/core/dm/anjay_dm_execute.c +++ b/src/core/dm/anjay_dm_execute.c @@ -99,7 +99,7 @@ static int try_reading_next_arg(anjay_unlocked_execute_ctx_t *ctx) { return 0; } -static int execute_get_arg_value_unlocked(anjay_unlocked_execute_ctx_t *ctx, +int _anjay_execute_get_arg_value_unlocked(anjay_unlocked_execute_ctx_t *ctx, size_t *out_bytes_read, char *out_buf, size_t buf_size) { @@ -151,13 +151,14 @@ static int skip_value(anjay_unlocked_execute_ctx_t *ctx) { if (ctx->state == STATE_READ_VALUE) { char buf[64]; do { - ret = execute_get_arg_value_unlocked(ctx, NULL, buf, sizeof(buf)); + ret = _anjay_execute_get_arg_value_unlocked( + ctx, NULL, buf, sizeof(buf)); } while (ret == ANJAY_BUFFER_TOO_SHORT); } return ret; } -static int execute_get_next_arg_unlocked(anjay_unlocked_execute_ctx_t *ctx, +int _anjay_execute_get_next_arg_unlocked(anjay_unlocked_execute_ctx_t *ctx, int *out_arg, bool *out_has_value) { if (skip_value(ctx)) { @@ -186,7 +187,7 @@ int anjay_execute_get_next_arg(anjay_execute_ctx_t *ctx, #ifdef ANJAY_WITH_THREAD_SAFETY ANJAY_MUTEX_LOCK(anjay, ctx->anjay_locked); #endif // ANJAY_WITH_THREAD_SAFETY - result = execute_get_next_arg_unlocked( + result = _anjay_execute_get_next_arg_unlocked( _anjay_execute_get_unlocked(ctx), out_arg, out_has_value); #ifdef ANJAY_WITH_THREAD_SAFETY ANJAY_MUTEX_UNLOCK(ctx->anjay_locked); @@ -202,10 +203,11 @@ int anjay_execute_get_arg_value(anjay_execute_ctx_t *ctx, #ifdef ANJAY_WITH_THREAD_SAFETY ANJAY_MUTEX_LOCK(anjay, ctx->anjay_locked); #endif // ANJAY_WITH_THREAD_SAFETY - result = execute_get_arg_value_unlocked(_anjay_execute_get_unlocked(ctx), - out_bytes_read, - out_buf, - buf_size); + result = _anjay_execute_get_arg_value_unlocked(_anjay_execute_get_unlocked( + ctx), + out_bytes_read, + out_buf, + buf_size); #ifdef ANJAY_WITH_THREAD_SAFETY ANJAY_MUTEX_UNLOCK(ctx->anjay_locked); #endif // ANJAY_WITH_THREAD_SAFETY diff --git a/src/core/downloader/anjay_coap.c b/src/core/downloader/anjay_coap.c index 0ad29ad71..b72387c5f 100644 --- a/src/core/downloader/anjay_coap.c +++ b/src/core/downloader/anjay_coap.c @@ -282,7 +282,7 @@ get_coap_socket_transport(anjay_download_ctx_t *ctx) { } # ifdef ANJAY_TEST -# include "tests/core/downloader/downloader_mock.h" +# include "tests/core/socket_mock.h" # endif // ANJAY_TEST static void start_download_job(avs_sched_t *sched, const void *id_ptr) { diff --git a/src/core/io/anjay_batch_builder.c b/src/core/io/anjay_batch_builder.c index d01339540..095103532 100644 --- a/src/core/io/anjay_batch_builder.c +++ b/src/core/io/anjay_batch_builder.c @@ -12,6 +12,7 @@ #if defined(ANJAY_WITH_OBSERVE) || defined(ANJAY_WITH_SEND) # include "../anjay_access_utils_private.h" +# include "../anjay_utils_private.h" # include "../dm/anjay_dm_read.h" # include "anjay_batch_builder.h" # include "anjay_vtable.h" diff --git a/src/core/io/anjay_senml_in.c b/src/core/io/anjay_senml_in.c index bb095c207..6d359dcbb 100644 --- a/src/core/io/anjay_senml_in.c +++ b/src/core/io/anjay_senml_in.c @@ -48,7 +48,6 @@ typedef struct { } senml_deserialization_vtable_t; typedef struct { - /* NOTE: empty state of cached entry is represented by 0-length path */ char path[MAX_PATH_STRING_SIZE]; anjay_json_like_value_type_t type; union { @@ -63,9 +62,6 @@ typedef struct { } senml_cached_entry_t; static void cached_entry_reset(senml_cached_entry_t *entry) { - if (!*entry->path) { - return; - } if (entry->type == ANJAY_JSON_LIKE_VALUE_TEXT_STRING || entry->type == ANJAY_JSON_LIKE_VALUE_BYTE_STRING) { avs_free(entry->value.bytes.data); diff --git a/src/core/observe/anjay_observe_core.c b/src/core/observe/anjay_observe_core.c index 0c204aae6..79b406c60 100644 --- a/src/core/observe/anjay_observe_core.c +++ b/src/core/observe/anjay_observe_core.c @@ -230,18 +230,27 @@ detach_observation(anjay_observe_connection_entry_t *conn, remove_from_observed_paths(conn, observation); } -void _anjay_observe_cleanup_connection(anjay_observe_connection_entry_t *conn) { +static void +cleanup_observation(anjay_observe_connection_entry_t *conn, + AVS_SORTED_SET_ELEM(anjay_observation_t) observation) { + remove_from_observed_paths(conn, observation); + avs_sched_del(&observation->notify_task); + if (observation->last_sent) { + delete_value(_anjay_from_server(conn->conn_ref.server), + &observation->last_sent); + } + assert(!observation->last_sent); +} + +static void cleanup_connection_without_observations_set( + anjay_observe_connection_entry_t *conn) { anjay_unlocked_t *anjay = _anjay_from_server(conn->conn_ref.server); while (conn->unsent) { delete_value(anjay, &conn->unsent); } - AVS_SORTED_SET_DELETE(&conn->observations) { - remove_from_observed_paths(conn, *conn->observations); - avs_sched_del(&(*conn->observations)->notify_task); - if ((*conn->observations)->last_sent) { - delete_value(anjay, &(*conn->observations)->last_sent); - } - assert(!(*conn->observations)->last_sent); + AVS_SORTED_SET_ELEM(anjay_observation_t) observation; + AVS_SORTED_SET_FOREACH(observation, conn->observations) { + cleanup_observation(conn, observation); } if (conn->observed_paths) { assert(!AVS_SORTED_SET_FIRST(conn->observed_paths)); @@ -252,6 +261,55 @@ void _anjay_observe_cleanup_connection(anjay_observe_connection_entry_t *conn) { } } +void _anjay_observe_cleanup_connection(anjay_observe_connection_entry_t *conn) { + cleanup_connection_without_observations_set(conn); + AVS_SORTED_SET_DELETE(&conn->observations); +} + +static void +cancel_ongoing_notify_exchange(anjay_observe_connection_entry_t *conn) { + avs_coap_ctx_t *coap = _anjay_connection_get_coap(conn->conn_ref); + if (coap && avs_coap_exchange_id_valid(conn->notify_exchange_id)) { + avs_coap_exchange_cancel(coap, conn->notify_exchange_id); + } + assert(!avs_coap_exchange_id_valid(conn->notify_exchange_id)); + assert(!conn->serialization_state.membuf_stream); + assert(!conn->serialization_state.out_ctx); +} + +void _anjay_observe_invalidate(anjay_connection_ref_t ref) { + AVS_LIST(anjay_observe_connection_entry_t) *conn_ptr = + _anjay_observe_find_connection_state(ref); + if (!conn_ptr) { + return; + } + cancel_ongoing_notify_exchange(*conn_ptr); + + // We clean up the observations but not remove the actual sorted set + // elements, so that we still have the tokens ready for the later phase. + cleanup_connection_without_observations_set(*conn_ptr); + + // Detach the connection entry so that it won't be found by + // _anjay_observe_find_connection_state() in _anjay_observe_cancel_handler() + AVS_LIST(anjay_observe_connection_entry_t) conn = AVS_LIST_DETACH(conn_ptr); + avs_coap_ctx_t *coap = _anjay_connection_get_coap(ref); + + // Now cancel the observations. We're doing it after having cleaned up all + // the state first - otherwise cancelling each observation would reschedule + // things related to the next scheduled one, might re-read attributes and + // such. That would be very suboptimal considering that we're removing all + // of them. + AVS_SORTED_SET_DELETE(&conn->observations) { + if (coap) { + avs_coap_observe_cancel(coap, + (avs_coap_observe_id_t) { + .token = (*conn->observations)->token + }); + } + } + AVS_LIST_DELETE(&conn); +} + void _anjay_observe_cleanup(anjay_observe_state_t *observe) { AVS_LIST_CLEAR(&observe->connection_entries) { _anjay_observe_cleanup_connection(observe->connection_entries); @@ -806,14 +864,7 @@ static void observe_remove_entry(anjay_connection_ref_t connection, if (!conn_ptr) { return; } - if (avs_coap_exchange_id_valid((*conn_ptr)->notify_exchange_id)) { - avs_coap_exchange_cancel(_anjay_connection_get_coap( - (*conn_ptr)->conn_ref), - (*conn_ptr)->notify_exchange_id); - } - assert(!avs_coap_exchange_id_valid((*conn_ptr)->notify_exchange_id)); - assert(!(*conn_ptr)->serialization_state.membuf_stream); - assert(!(*conn_ptr)->serialization_state.out_ctx); + cancel_ongoing_notify_exchange(*conn_ptr); AVS_SORTED_SET_ELEM(anjay_observation_t) observation = AVS_SORTED_SET_FIND((*conn_ptr)->observations, _anjay_observation_query(token)); @@ -1555,11 +1606,13 @@ static void flush_next_unsent(anjay_observe_connection_entry_t *conn) { // defined(ANJAY_WITH_CORE_PERSISTENCE) } avs_coap_options_cleanup(&response.options); +# ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE // on_entry_flushed() may have closed the socket already, // so we need to check if it's still open if (_anjay_connection_get_online_socket(conn_ref)) { _anjay_connection_schedule_queue_mode_close(conn_ref); } +# endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE # ifdef ANJAY_WITH_COMMUNICATION_TIMESTAMP_API if (avs_is_ok(err)) { @@ -1599,6 +1652,7 @@ bool _anjay_observe_confirmable_in_delivery(anjay_connection_ref_t ref) { && avs_coap_exchange_id_valid((*conn_ptr)->notify_exchange_id); } +# ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE bool _anjay_observe_needs_flushing(anjay_connection_ref_t ref) { AVS_LIST(anjay_observe_connection_entry_t) *conn_ptr = _anjay_observe_find_connection_state(ref); @@ -1608,6 +1662,7 @@ bool _anjay_observe_needs_flushing(anjay_connection_ref_t ref) { return (*conn_ptr)->unsent && !(*conn_ptr)->flush_task && !avs_coap_exchange_id_valid((*conn_ptr)->notify_exchange_id); } +# endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE int _anjay_observe_sched_flush(anjay_connection_ref_t ref) { anjay_log(TRACE, @@ -1732,32 +1787,40 @@ static void trigger_observe(avs_sched_t *sched, const void *args_) { && _anjay_socket_transport_is_online( _anjay_from_server(args->conn_state->conn_ref.server), _anjay_connection_transport(args->conn_state->conn_ref)); - if (ready_for_notifying - || notification_storing_enabled(args->conn_state->conn_ref)) { - int result = - update_notification_value(args->conn_state, args->observation); - if (result) { - insert_error(args->conn_state, args->observation, result); - } - } - if (args->conn_state->unsent) { + if (!ready_for_notifying + && _anjay_server_registration_expired( + args->conn_state->conn_ref.server)) { + // Registration expired - notifications would be cleared at the time of + // Register anyway, so we might as well do it here to conserve memory + _anjay_observe_invalidate(args->conn_state->conn_ref); + } else { if (ready_for_notifying - && !avs_coap_exchange_id_valid( - args->conn_state->notify_exchange_id)) { - avs_sched_del(&args->conn_state->flush_task); - assert(!args->conn_state->flush_task); - if (_anjay_connection_get_online_socket( - args->conn_state->conn_ref)) { - flush_next_unsent(args->conn_state); - } else if (_anjay_server_registration_info( - args->conn_state->conn_ref.server) - ->queue_mode) { - _anjay_connection_bring_online(args->conn_state->conn_ref); - // once the connection is up, _anjay_observe_sched_flush() - // will be called; we're done here - } else if (!notification_storing_enabled( - args->conn_state->conn_ref)) { - remove_all_unsent_values(args->conn_state); + || notification_storing_enabled(args->conn_state->conn_ref)) { + int result = update_notification_value(args->conn_state, + args->observation); + if (result) { + insert_error(args->conn_state, args->observation, result); + } + } + if (args->conn_state->unsent) { + if (ready_for_notifying + && !avs_coap_exchange_id_valid( + args->conn_state->notify_exchange_id)) { + avs_sched_del(&args->conn_state->flush_task); + assert(!args->conn_state->flush_task); + if (_anjay_connection_get_online_socket( + args->conn_state->conn_ref)) { + flush_next_unsent(args->conn_state); + } else if (_anjay_server_registration_info( + args->conn_state->conn_ref.server) + ->queue_mode) { + _anjay_connection_bring_online(args->conn_state->conn_ref); + // once the connection is up, _anjay_observe_sched_flush() + // will be called; we're done here + } else if (!notification_storing_enabled( + args->conn_state->conn_ref)) { + remove_all_unsent_values(args->conn_state); + } } } } diff --git a/src/core/observe/anjay_observe_core.h b/src/core/observe/anjay_observe_core.h index a7858aa4d..d39fd50e4 100644 --- a/src/core/observe/anjay_observe_core.h +++ b/src/core/observe/anjay_observe_core.h @@ -71,9 +71,13 @@ int _anjay_observe_composite_handle(anjay_connection_ref_t ref, void _anjay_observe_interrupt(anjay_connection_ref_t ref); +void _anjay_observe_invalidate(anjay_connection_ref_t ref); + bool _anjay_observe_confirmable_in_delivery(anjay_connection_ref_t ref); +# ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE bool _anjay_observe_needs_flushing(anjay_connection_ref_t ref); +# endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE int _anjay_observe_sched_flush(anjay_connection_ref_t ref); @@ -96,6 +100,7 @@ _anjay_observe_status(anjay_unlocked_t *anjay, # define _anjay_observe_cleanup(...) ((void) 0) # define _anjay_observe_gc(...) ((void) 0) # define _anjay_observe_interrupt(...) ((void) 0) +# define _anjay_observe_invalidate(...) ((void) 0) # define _anjay_observe_confirmable_in_delivery(...) false # define _anjay_observe_needs_flushing(...) false # define _anjay_observe_sched_flush(...) 0 diff --git a/src/core/observe/anjay_observe_planning.c b/src/core/observe/anjay_observe_planning.c index bfa84b11f..7dade9db9 100644 --- a/src/core/observe/anjay_observe_planning.c +++ b/src/core/observe/anjay_observe_planning.c @@ -41,7 +41,7 @@ static int foreach_relevant_connection_helper(anjay_unlocked_t *anjay, }; AVS_LIST(anjay_observe_connection_entry_t) *conn_ptr = _anjay_observe_find_connection_state(ref); - if (conn_ptr && *conn_ptr + if (conn_ptr && *conn_ptr && _anjay_server_connection_active(ref) && _anjay_socket_transport_included( arg->transport_set, _anjay_connection_transport(ref))) { diff --git a/src/core/servers/anjay_activate.c b/src/core/servers/anjay_activate.c index 5b7cf467b..ef723ac63 100644 --- a/src/core/servers/anjay_activate.c +++ b/src/core/servers/anjay_activate.c @@ -39,13 +39,14 @@ static int deactivate_server(anjay_server_info_t *server) { assert(server); #ifndef ANJAY_WITHOUT_DEREGISTER if (server->ssid != ANJAY_SSID_BOOTSTRAP - && !_anjay_bootstrap_in_progress(server->anjay) - && _anjay_server_active(server) - && !_anjay_server_registration_expired(server)) { - // Return value intentionally ignored. - // There isn't much we can do in case it fails and De-Register is - // optional anyway. _anjay_serve_deregister logs the error cause. - _anjay_server_deregister(server); + && !_anjay_bootstrap_in_progress(server->anjay)) { + if (_anjay_server_active(server) + && !_anjay_server_registration_expired(server)) { + // Return value intentionally ignored. + // There isn't much we can do in case it fails and De-Register is + // optional anyway. _anjay_serve_deregister logs the error cause. + _anjay_server_deregister(server); + } } #endif // ANJAY_WITHOUT_DEREGISTER _anjay_server_clean_active_data(server); @@ -239,6 +240,7 @@ void _anjay_server_on_server_communication_timeout( server->anjay, server->ssid, AVS_TIME_DURATION_ZERO)) { server->refresh_failed = true; } else { + // defined(ANJAY_WITHOUT_TELIT_STABLE_CONNECTION_TIMEOUT_REG_FAILURE) _anjay_server_on_server_communication_error(server, avs_errno(AVS_EBADF)); } @@ -274,7 +276,8 @@ void _anjay_server_on_refreshed(anjay_server_info_t *server, _anjay_get_server_connection(primary_ref); if (state == ANJAY_SERVER_CONNECTION_OFFLINE) { if (avs_is_err(err)) { - anjay_log(TRACE, _("could not initialize sockets for SSID ") "%u", + anjay_log(TRACE, + _("could not initialize sockets for SSID ") "%" PRIu16, server->ssid); _anjay_server_on_server_communication_error(server, err); } else if (_anjay_socket_transport_supported(server->anjay, @@ -283,12 +286,12 @@ void _anjay_server_on_refreshed(anjay_server_info_t *server, server->anjay, primary_conn->transport)) { assert(server->registration_info.queue_mode); anjay_log(TRACE, - _("Server with SSID ") "%u" _( + _("Server with SSID ") "%" PRIu16 _( " is suspended due to queue mode"), server->ssid); _anjay_server_reschedule_update_job(server); } else { - anjay_log(TRACE, _("Server with SSID ") "%u" _(" is offline"), + anjay_log(TRACE, _("Server with SSID ") "%" PRIu16 _(" is offline"), server->ssid); if (!avs_time_real_valid(server->reactivate_time)) { // make the server reactivate when it comes back online @@ -556,7 +559,7 @@ void _anjay_disable_server_with_timeout_from_dm_sync( anjay_iid_t server_iid; if (_anjay_find_server_iid(server->anjay, server->ssid, &server_iid)) { anjay_log(DEBUG, - _("no Server Object Instance with SSID = ") "%u" _( + _("no Server Object Instance with SSID = ") "%" PRIu16 _( ", disabling skipped"), server->ssid); } else { @@ -565,7 +568,15 @@ void _anjay_disable_server_with_timeout_from_dm_sync( server_iid); server->reactivate_time = avs_time_real_add(avs_time_real_now(), disable_timeout); - deactivate_server(server); + // defined(ANJAY_WITH_TELIT_CUSTOM_FEATURES) + server->disabled_explicitly = true; + if (deactivate_server(server)) { + anjay_log(ERROR, _("unable to deactivate server: ") "%" PRIu16, + server->ssid); + } else { + anjay_log(INFO, _("server ") "%" PRIu16 _(" deactivated"), + server->ssid); + } } } @@ -579,7 +590,7 @@ static int disable_server_impl(anjay_unlocked_t *anjay, || disable_action == ANJAY_SERVER_NEXT_ACTION_DISABLE_WITH_EXPLICIT_TIMEOUT); if (ssid == ANJAY_SSID_ANY) { - anjay_log(WARNING, _("invalid SSID: ") "%u", ssid); + anjay_log(WARNING, _("invalid SSID: ") "%" PRIu16, ssid); return -1; } @@ -629,6 +640,8 @@ int anjay_disable_server(anjay_t *anjay_locked, anjay_ssid_t ssid) { void _anjay_disable_server_with_explicit_timeout_sync( anjay_server_info_t *server) { + // defined(ANJAY_WITH_TELIT_CUSTOM_FEATURES) + server->disabled_explicitly = true; if (deactivate_server(server)) { anjay_log(ERROR, _("unable to deactivate server: ") "%" PRIu16, server->ssid); @@ -667,7 +680,7 @@ int anjay_disable_server_with_timeout(anjay_t *anjay_locked, int _anjay_enable_server_unlocked(anjay_unlocked_t *anjay, anjay_ssid_t ssid) { if (ssid == ANJAY_SSID_ANY) { - anjay_log(WARNING, _("invalid SSID: ") "%u", ssid); + anjay_log(WARNING, _("invalid SSID: ") "%" PRIu16, ssid); return -1; } @@ -675,7 +688,7 @@ int _anjay_enable_server_unlocked(anjay_unlocked_t *anjay, anjay_ssid_t ssid) { _anjay_servers_find_ptr(&anjay->servers, ssid); if (!server_ptr || !*server_ptr || _anjay_server_active(*server_ptr)) { - anjay_log(TRACE, _("not an inactive server: SSID = ") "%u", ssid); + anjay_log(TRACE, _("not an inactive server: SSID = ") "%" PRIu16, ssid); return -1; } @@ -691,6 +704,7 @@ int _anjay_enable_server_unlocked(anjay_unlocked_t *anjay, anjay_ssid_t ssid) { } (*server_ptr)->reactivate_time = avs_time_real_now(); + // defined(ANJAY_WITH_CORE_PERSISTENCE) return _anjay_server_sched_activate(*server_ptr); } @@ -701,3 +715,32 @@ int anjay_enable_server(anjay_t *anjay_locked, anjay_ssid_t ssid) { ANJAY_MUTEX_UNLOCK(anjay_locked); return result; } + +int anjay_server_schedule_reconnect(anjay_t *anjay_locked, anjay_ssid_t ssid) { + if (ssid == ANJAY_SSID_ANY) { + anjay_log(WARNING, _("invalid SSID: ") "%" PRIu16, ssid); + return -1; + } + + int result = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + AVS_LIST(anjay_server_info_t) *server_ptr = + _anjay_servers_find_ptr(&anjay->servers, ssid); + + if (!server_ptr || !*server_ptr || !_anjay_server_active(*server_ptr)) { + anjay_log(TRACE, _("not an active server: SSID = ") "%" PRIu16, ssid); + } else { + anjay_connection_type_t conn_type; + ANJAY_CONNECTION_TYPE_FOREACH(conn_type) { + const anjay_connection_ref_t ref = { + .server = *server_ptr, + .conn_type = conn_type + }; + _anjay_connection_suspend(ref); + } + result = _anjay_schedule_refresh_server(*server_ptr, + AVS_TIME_DURATION_ZERO); + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return result; +} diff --git a/src/core/servers/anjay_connection_ip.c b/src/core/servers/anjay_connection_ip.c index 0094e233b..469e60a71 100644 --- a/src/core/servers/anjay_connection_ip.c +++ b/src/core/servers/anjay_connection_ip.c @@ -27,6 +27,10 @@ #include "anjay_connections_internal.h" #include "anjay_servers_internal.h" +#ifdef ANJAY_TEST +# include "tests/core/socket_mock.h" +#endif // ANJAY_TEST + VISIBILITY_SOURCE_BEGIN #if defined(WITH_AVS_COAP_UDP) \ diff --git a/src/core/servers/anjay_connections.c b/src/core/servers/anjay_connections.c index 3a1498bf1..08f2944e4 100644 --- a/src/core/servers/anjay_connections.c +++ b/src/core/servers/anjay_connections.c @@ -62,7 +62,9 @@ void _anjay_connection_internal_clean_socket( #endif // ANJAY_WITH_DOWNLOADER _anjay_coap_ctx_cleanup(anjay, &connection->coap_ctx); _anjay_socket_cleanup(anjay, &connection->conn_socket_); +#ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE avs_sched_del(&connection->queue_mode_close_socket_clb); +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE } typedef struct { @@ -361,10 +363,7 @@ avs_error_t _anjay_server_connection_internal_bring_online( return AVS_OK; } - // defined(ANJAY_WITH_CORE_PERSISTENCE) - bool session_resumed; - bool should_attempt_context_wrapping; avs_error_t err = def->connect_socket(server->anjay, connection); if (avs_is_err(err)) { goto error; @@ -374,6 +373,8 @@ avs_error_t _anjay_server_connection_internal_bring_online( _anjay_was_session_resumed(connection->conn_socket_))) { _anjay_conn_session_token_reset(&connection->session_token); // Clean up and recreate the CoAP context to discard observations + // NOTE: In old versions of avs_coap, this was sending the Release + // message. This may need to be revised if it's ever reintroduced. _anjay_coap_ctx_cleanup(server->anjay, &connection->coap_ctx); } @@ -529,6 +530,9 @@ ensure_socket_connected(anjay_server_info_t *server, return _anjay_server_connection_internal_bring_online(server, conn_type); } +#ifdef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE +# define should_primary_connection_be_online(...) true +#else // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE static bool should_primary_connection_be_online(anjay_server_info_t *server) { anjay_connection_ref_t ref = { .server = server, @@ -550,12 +554,13 @@ static bool should_primary_connection_be_online(anjay_server_info_t *server) { || !server->registration_info.queue_mode // if there are notifications to be sent, we need to send them || _anjay_observe_needs_flushing(ref) -#ifdef ANJAY_WITH_SEND +# ifdef ANJAY_WITH_SEND // if there are Send messages to be sent, we need to send them || _anjay_send_has_deferred(server->anjay, server->ssid) -#endif // ANJAY_WITH_SEND +# endif // ANJAY_WITH_SEND ; } +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE static avs_error_t refresh_connection(anjay_server_info_t *server, anjay_connection_type_t conn_type, @@ -658,7 +663,9 @@ void _anjay_server_connections_refresh( anjay_server_connection_t *connection = _anjay_connection_get(&server->connections, conn_type); connection->state = ANJAY_SERVER_CONNECTION_IN_PROGRESS; +#ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE avs_sched_del(&connection->queue_mode_close_socket_clb); +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE } avs_error_t err = refresh_connection( server, ANJAY_CONNECTION_PRIMARY, diff --git a/src/core/servers/anjay_connections.h b/src/core/servers/anjay_connections.h index 28549e326..eb9e62c83 100644 --- a/src/core/servers/anjay_connections.h +++ b/src/core/servers/anjay_connections.h @@ -184,11 +184,15 @@ typedef struct { */ anjay_server_connection_nontransient_state_t nontransient_state; +#ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE /** - * Handle to scheduled queue_mode_close_socket() scheduler job. Scheduled - * by _anjay_connection_schedule_queue_mode_close(). + * Handle to scheduled queue_mode_close_socket() scheduler job. Generally + * scheduled by _anjay_connection_schedule_queue_mode_close(), although it + * can also be rescheduled by itself (queue_mode_close_socket() function) to + * defer the action if CoAP exchanges are in progress. */ avs_sched_handle_t queue_mode_close_socket_clb; +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE } anjay_server_connection_t; typedef struct { diff --git a/src/core/servers/anjay_register.c b/src/core/servers/anjay_register.c index 14bead8fa..504a245d8 100644 --- a/src/core/servers/anjay_register.c +++ b/src/core/servers/anjay_register.c @@ -44,6 +44,50 @@ VISIBILITY_SOURCE_BEGIN * seconds. */ #define ANJAY_MIN_UPDATE_INTERVAL_S 1 +static int schedule_register_for_server(anjay_server_info_t *server) { + int result = 0; + if (_anjay_server_active(server)) { + result = _anjay_schedule_refresh_server(server, AVS_TIME_DURATION_ZERO); + } + if (!result) { + anjay_server_connection_t *connection = + _anjay_connection_get(&server->connections, + ANJAY_CONNECTION_PRIMARY); + _anjay_conn_session_token_reset(&connection->session_token); + } + return result; +} + +static int schedule_register_for_all_servers(anjay_unlocked_t *anjay) { + int result = 0; + + AVS_LIST(anjay_server_info_t) it; + AVS_LIST_FOREACH(it, anjay->servers) { + _anjay_update_ret(&result, schedule_register_for_server(it)); + } + + return result; +} + +int anjay_schedule_register(anjay_t *anjay_locked, anjay_ssid_t ssid) { + int result = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + if (ssid == ANJAY_SSID_ANY) { + result = schedule_register_for_all_servers(anjay); + } else { + AVS_LIST(anjay_server_info_t) *server_ptr = + _anjay_servers_find_ptr(&anjay->servers, ssid); + if (!server_ptr || !*server_ptr) { + anjay_log(WARNING, _("no known server with SSID = ") "%u", ssid); + result = -1; + } else { + result = schedule_register_for_server(*server_ptr); + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return result; +} + static avs_time_real_t calculate_time_of_next_update(anjay_server_info_t *server) { avs_time_real_t expire_time = _anjay_registration_expire_time(server); @@ -110,14 +154,6 @@ static int schedule_next_update(anjay_server_info_t *server) { server, delay, ANJAY_SERVER_NEXT_ACTION_SEND_UPDATE); } -bool _anjay_server_primary_connection_valid(anjay_server_info_t *server) { - return _anjay_server_active(server) - && _anjay_connection_get_online_socket((anjay_connection_ref_t) { - .server = server, - .conn_type = ANJAY_CONNECTION_PRIMARY - }) != NULL; -} - int _anjay_server_reschedule_update_job(anjay_server_info_t *server) { if (schedule_next_update(server)) { anjay_log(ERROR, _("could not schedule next Update for server ") "%u", @@ -356,10 +392,23 @@ get_binding_mode_for_version(anjay_server_info_t *server, } } -static int update_parameters_init(anjay_server_info_t *server, - anjay_lwm2m_version_t lwm2m_version, - anjay_update_parameters_t *out_params) { +static avs_error_t +update_parameters_init(anjay_server_info_t *server, + anjay_lwm2m_version_t lwm2m_version, + anjay_update_parameters_t *out_params) { + avs_error_t err = avs_errno(AVS_UNKNOWN_ERROR); memset(out_params, 0, sizeof(*out_params)); + if (!_anjay_server_connection_active((anjay_connection_ref_t) { + .server = server, + .conn_type = ANJAY_CONNECTION_PRIMARY + })) { + anjay_log(ERROR, + _("No valid connection to Registration Interface for " + "SSID = ") "%u", + server->ssid); + err = avs_errno(AVS_EBADF); + goto error; + } if (query_dm(server->anjay, lwm2m_version, &out_params->dm)) { goto error; } @@ -369,10 +418,10 @@ static int update_parameters_init(anjay_server_info_t *server, } get_binding_mode_for_version(server, lwm2m_version, &out_params->binding_mode); - return 0; + return AVS_OK; error: update_parameters_cleanup(out_params); - return -1; + return err; } void _anjay_registration_info_cleanup(anjay_registration_info_t *info) { @@ -576,7 +625,7 @@ handle_register_response(anjay_server_info_t *server, if (result != ANJAY_REGISTRATION_SUCCESS) { anjay_log(WARNING, _("could not register to server ") "%u", _anjay_server_ssid(server)); - AVS_LIST_CLEAR(move_endpoint_path); + AVS_LIST_CLEAR(move_endpoint_path) {} } else { _anjay_server_update_registration_info( server, move_endpoint_path, attempted_version, @@ -595,8 +644,8 @@ handle_register_response(anjay_server_info_t *server, // NOTE: update_parameters format may differ slightly between // LwM2M versions, so we need to rebuild them update_parameters_cleanup(move_params); - if (update_parameters_init(server, attempted_version, - move_params)) { + if (avs_is_err(update_parameters_init(server, attempted_version, + move_params))) { result = ANJAY_REGISTRATION_ERROR_OTHER; } else { register_with_version(server, attempted_version, move_params); @@ -775,6 +824,13 @@ static void register_with_version(anjay_server_info_t *server, static void do_register(anjay_server_info_t *server, anjay_update_parameters_t *move_params) { + anjay_connection_type_t conn_type; + ANJAY_CONNECTION_TYPE_FOREACH(conn_type) { + _anjay_observe_invalidate((anjay_connection_ref_t) { + .server = server, + .conn_type = conn_type + }); + } anjay_lwm2m_version_t attempted_version = #ifdef ANJAY_WITH_LWM2M11 server->anjay->lwm2m_version_config.maximum_version; @@ -1061,19 +1117,11 @@ void _anjay_server_ensure_valid_registration(anjay_server_info_t *server) { } anjay_update_parameters_t new_params; - if (update_parameters_init(server, server->registration_info.lwm2m_version, - &new_params)) { - on_registration_update_result(server, &new_params, - ANJAY_REGISTRATION_ERROR_OTHER, - avs_errno(AVS_UNKNOWN_ERROR)); - } else if (!_anjay_server_primary_connection_valid(server)) { - anjay_log(ERROR, - _("No valid connection to Registration Interface for " - "SSID = ") "%u", - server->ssid); + avs_error_t err = update_parameters_init( + server, server->registration_info.lwm2m_version, &new_params); + if (avs_is_err(err)) { on_registration_update_result(server, &new_params, - ANJAY_REGISTRATION_ERROR_OTHER, - avs_errno(AVS_EBADF)); + ANJAY_REGISTRATION_ERROR_OTHER, err); } else { bool registration_or_update_in_progress = avs_coap_exchange_id_valid( server->registration_exchange_state.exchange_id); @@ -1098,6 +1146,7 @@ void _anjay_server_ensure_valid_registration(anjay_server_info_t *server) { if (!registration_or_update_in_progress) { _anjay_server_on_updated_registration( server, ANJAY_REGISTRATION_SUCCESS, AVS_OK); +#ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE anjay_connection_ref_t ref = { .server = server, .conn_type = ANJAY_CONNECTION_PRIMARY @@ -1107,6 +1156,7 @@ void _anjay_server_ensure_valid_registration(anjay_server_info_t *server) { if (!connection->queue_mode_close_socket_clb) { _anjay_connection_schedule_queue_mode_close(ref); } +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE } } else { update_registration(server, &new_params); @@ -1271,7 +1321,7 @@ static bool server_state_stable(anjay_server_info_t *server) { // does not expire. return !_anjay_bootstrap_scheduled(server->anjay); } else if (!_anjay_server_active(server)) { - return false; + return server->disabled_explicitly; } else { // Management servers connections are considered stable when they have // a valid, non-expired registration. diff --git a/src/core/servers/anjay_register.h b/src/core/servers/anjay_register.h index bc2b8dce2..f10cbcceb 100644 --- a/src/core/servers/anjay_register.h +++ b/src/core/servers/anjay_register.h @@ -25,8 +25,6 @@ void _anjay_registration_info_cleanup(anjay_registration_info_t *info); void _anjay_registration_exchange_state_cleanup( anjay_registration_async_exchange_state_t *state); -bool _anjay_server_primary_connection_valid(anjay_server_info_t *server); - typedef enum { /** Successfully registered/updated */ ANJAY_REGISTRATION_SUCCESS, diff --git a/src/core/servers/anjay_server_connections.c b/src/core/servers/anjay_server_connections.c index c4db7af92..4a001914d 100644 --- a/src/core/servers/anjay_server_connections.c +++ b/src/core/servers/anjay_server_connections.c @@ -256,6 +256,11 @@ static int read_server_sni(anjay_unlocked_t *anjay, void _anjay_active_server_refresh(anjay_server_info_t *server) { anjay_log(TRACE, _("refreshing SSID ") "%u", server->ssid); + // defined(ANJAY_WITH_TELIT_CUSTOM_FEATURES) + + // Refreshed server is not deemed explicitly disabled anymore + server->disabled_explicitly = false; + int result = 0; anjay_iid_t security_iid = ANJAY_ID_INVALID; avs_url_t *uri = NULL; @@ -451,6 +456,7 @@ void _anjay_connection_bring_online(anjay_connection_ref_t ref) { } } +#ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE static void queue_mode_close_socket(avs_sched_t *sched, const void *ref_ptr) { static const long RETRY_DELAY_S = 1; anjay_t *anjay_locked = _anjay_get_from_sched(sched); @@ -502,6 +508,7 @@ void _anjay_connection_schedule_queue_mode_close(anjay_connection_ref_t ref) { anjay_log(ERROR, _("could not schedule queue mode operations")); } } +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE const anjay_url_t *_anjay_connection_uri(anjay_connection_ref_t ref) { return &_anjay_get_server_connection(ref)->uri; diff --git a/src/core/servers/anjay_servers_internal.c b/src/core/servers/anjay_servers_internal.c index 6c874b5c3..b810c2c89 100644 --- a/src/core/servers/anjay_servers_internal.c +++ b/src/core/servers/anjay_servers_internal.c @@ -112,7 +112,7 @@ bool _anjay_connection_ready_for_outgoing_message(anjay_connection_ref_t ref) { // server as inactive for notification purposes. anjay_unlocked_t *anjay = _anjay_from_server(ref.server); return !_anjay_bootstrap_in_progress(anjay) - && _anjay_server_active(ref.server) + && _anjay_server_connection_active(ref) && !_anjay_server_registration_expired(ref.server) && !_anjay_server_registration_info(ref.server)->update_forced; } @@ -214,6 +214,14 @@ bool _anjay_server_is_disable_scheduled(anjay_server_info_t *server) { == ANJAY_SERVER_NEXT_ACTION_DISABLE_WITH_EXPLICIT_TIMEOUT); } +bool _anjay_server_connection_active(anjay_connection_ref_t ref) { + if (_anjay_server_is_disable_scheduled(ref.server)) { + return false; + } + return !!_anjay_connection_internal_get_socket( + _anjay_get_server_connection(ref)); +} + bool _anjay_server_active(anjay_server_info_t *server) { if (_anjay_server_is_disable_scheduled(server)) { return false; diff --git a/src/core/servers/anjay_servers_internal.h b/src/core/servers/anjay_servers_internal.h index 482fe0533..305b79650 100644 --- a/src/core/servers/anjay_servers_internal.h +++ b/src/core/servers/anjay_servers_internal.h @@ -166,6 +166,17 @@ struct anjay_server_info_struct { */ avs_time_real_t reactivate_time; + /** + * True if the server is explicitly disabled by: + * - LwM2M Server executing Disable resource on Server object + * - user calling @ref anjay_disable_server + * - user calling @ref anjay_disable_server_with_timeout + * + * This allows to find out whether server's reactivate_time + * should be loaded from core persistence or not. + */ + bool disabled_explicitly; + /** * True if, and only if, the last activation attempt was unsuccessful, for * whatever reason - not necessarily those included in num_icmp_failures diff --git a/src/modules/advanced_fw_update/anjay_advanced_fw_update.c b/src/modules/advanced_fw_update/anjay_advanced_fw_update.c new file mode 100644 index 000000000..2ce404bf1 --- /dev/null +++ b/src/modules/advanced_fw_update/anjay_advanced_fw_update.c @@ -0,0 +1,2209 @@ +/* + * Copyright 2017-2023 AVSystem + * AVSystem Anjay LwM2M SDK + * All rights reserved. + * + * Licensed under the AVSystem-5-clause License. + * See the attached LICENSE file for details. + */ + +#include + +#ifdef ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE + +# include + +# include +# include +# include + +# include + +# include +# include + +# ifdef ANJAY_WITH_SEND +# include +# endif // ANJAY_WITH_SEND + +# include +# include +# include +# include +# include + +# define fw_log(level, ...) avs_log(advanced_fw_update, level, __VA_ARGS__) +# define _(Arg) AVS_DISPOSABLE_LOG(Arg) + +# define ADV_FW_RES_PACKAGE 0 +# define ADV_FW_RES_PACKAGE_URI 1 +# define ADV_FW_RES_UPDATE 2 +# define ADV_FW_RES_STATE 3 +# define ADV_FW_RES_UPDATE_RESULT 5 +# define ADV_FW_RES_PKG_NAME 6 +# define ADV_FW_RES_PKG_VERSION 7 +# define ADV_FW_RES_UPDATE_PROTOCOL_SUPPORT 8 +# define ADV_FW_RES_UPDATE_DELIVERY_METHOD 9 +# define ADV_FW_RES_CANCEL 10 +# define ADV_FW_RES_SEVERITY 11 +# define ADV_FW_RES_LAST_STATE_CHANGE_TIME 12 +# define ADV_FW_RES_MAX_DEFER_PERIOD 13 +# define ADV_FW_RES_COMPONENT_NAME 14 +# define ADV_FW_RES_CURRENT_VERSION 15 +# define ADV_FW_RES_LINKED_INSTANCES 16 +# define ADV_FW_RES_CONFLICTING_INSTANCES 17 + +VISIBILITY_SOURCE_BEGIN + +typedef struct { + const anjay_advanced_fw_update_handlers_t *handlers; + void *arg; + anjay_advanced_fw_update_state_t state; +} advanced_fw_user_state_t; + +typedef struct { + anjay_iid_t iid; + + const char *component_name; + + advanced_fw_user_state_t user_state; + + anjay_advanced_fw_update_state_t state; + anjay_advanced_fw_update_result_t result; + const char *package_uri; + bool retry_download_on_expired; + avs_sched_handle_t update_job; + avs_sched_handle_t resume_download_job; + avs_time_monotonic_t resume_download_deadline; + anjay_advanced_fw_update_severity_t severity; + avs_time_real_t last_state_change_time; + int max_defer_period; + avs_time_real_t update_deadline; + + anjay_iid_t *linked_instances; + size_t linked_instances_count; + + anjay_iid_t *conflicting_instances; + size_t conflicting_instances_count; +} advanced_fw_instance_t; + +typedef struct { + anjay_dm_installed_object_t def_ptr; + const anjay_unlocked_dm_object_def_t *def; + + bool prefer_same_socket_downloads; +# ifdef ANJAY_WITH_SEND + bool use_lwm2m_send; +# endif // ANJAY_WITH_SEND + + anjay_iid_t *supplemental_iid_cache; + size_t supplemental_iid_cache_count; + + anjay_download_handle_t download_handle; + AVS_LIST(anjay_download_config_t) download_queue; + + AVS_LIST(advanced_fw_instance_t) instances; +} advanced_fw_repr_t; + +static inline advanced_fw_repr_t * +get_fw(const anjay_dm_installed_object_t obj_ptr) { + return AVS_CONTAINER_OF(_anjay_dm_installed_object_get_unlocked(&obj_ptr), + advanced_fw_repr_t, def); +} + +# ifdef ANJAY_WITH_SEND +# define SEND_RES_PATH(Oid, Iid, Rid) \ + { \ + .oid = (Oid), \ + .iid = (Iid), \ + .rid = (Rid) \ + } + +# define SEND_FW_RES_PATH(Iid, Res) \ + SEND_RES_PATH(ANJAY_ADVANCED_FW_UPDATE_OID, Iid, ADV_FW_RES_##Res) + +static void send_batch_to_all_servers(anjay_unlocked_t *anjay, + anjay_send_batch_t *batch) { + AVS_LIST(const anjay_ssid_t) ssids = + _anjay_server_get_ssids_unlocked(anjay); + AVS_LIST(const anjay_ssid_t) ssid; + AVS_LIST_FOREACH(ssid, ssids) { + if (_anjay_send_deferrable_unlocked(anjay, *ssid, batch, NULL, NULL) + != ANJAY_SEND_OK) { + fw_log(WARNING, + _("failed to perform Send, SSID: ") "%" PRIu16, + *ssid); + } + } +} + +static void perform_lwm2m_send(anjay_unlocked_t *anjay, + const anjay_send_resource_path_t *paths, + size_t paths_len) { + assert(paths); + + anjay_send_batch_builder_t *batch_builder = anjay_send_batch_builder_new(); + if (!batch_builder) { + fw_log(ERROR, _("out of memory")); + return; + } + if (_anjay_send_batch_data_add_current_multiple_unlocked( + batch_builder, anjay, paths, paths_len, true)) { + fw_log(ERROR, _("failed to add data to batch")); + anjay_send_batch_builder_cleanup(&batch_builder); + return; + } + anjay_send_batch_t *batch = + anjay_send_batch_builder_compile(&batch_builder); + if (!batch) { + anjay_send_batch_builder_cleanup(&batch_builder); + fw_log(ERROR, _("out of memory")); + return; + } + send_batch_to_all_servers(anjay, batch); + anjay_send_batch_release(&batch); +} + +static void send_state_and_update_result(anjay_unlocked_t *anjay, + const advanced_fw_repr_t *fw, + anjay_iid_t iid, + bool with_version_info) { + if (!fw->use_lwm2m_send) { + return; + } + + anjay_send_resource_path_t paths[5] = { SEND_FW_RES_PATH(iid, STATE), + SEND_FW_RES_PATH(iid, + UPDATE_RESULT) }; + size_t path_count = 2; + if (with_version_info) { + paths[path_count++] = + (anjay_send_resource_path_t) SEND_FW_RES_PATH(iid, + CURRENT_VERSION); + paths[path_count++] = + (anjay_send_resource_path_t) SEND_RES_PATH(3, 0, 3); + paths[path_count++] = + (anjay_send_resource_path_t) SEND_RES_PATH(3, 0, 19); + } + perform_lwm2m_send(anjay, paths, path_count); +} +# endif // ANJAY_WITH_SEND + +static int set_update_result(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst, + anjay_advanced_fw_update_result_t new_result) { + if (inst->result != new_result) { + fw_log(DEBUG, + _("Firmware Update Instance ") "%" PRIu16 _( + " Result change: ") "%d" _(" -> ") "%d", + inst->iid, (int) inst->result, (int) new_result); + inst->result = new_result; + _anjay_notify_changed_unlocked(anjay, ANJAY_ADVANCED_FW_UPDATE_OID, + inst->iid, ADV_FW_RES_UPDATE_RESULT); + return 0; + } + return -1; +} + +static int set_state(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst, + anjay_advanced_fw_update_state_t new_state) { + if (inst->state != new_state) { + inst->last_state_change_time = avs_time_real_now(); + fw_log(DEBUG, + _("Firmware Update Instance ") "%" PRIu16 _( + " State change: ") "%d" _(" -> ") "%d", + inst->iid, (int) inst->state, (int) new_state); + inst->state = new_state; + _anjay_notify_changed_unlocked(anjay, ANJAY_ADVANCED_FW_UPDATE_OID, + inst->iid, ADV_FW_RES_STATE); + return 0; + } + return -1; +} + +static void +update_state_and_update_result(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst, + anjay_advanced_fw_update_state_t new_state, + anjay_advanced_fw_update_result_t new_result) { + int set_res = set_update_result(anjay, inst, new_result); + int set_st = set_state(anjay, inst, new_state); +# ifdef ANJAY_WITH_SEND + bool send_version_info = + inst->state == ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING + && new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE + && new_result == ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS; + if (!set_res || !set_st) { + send_state_and_update_result(anjay, fw, inst->iid, send_version_info); + } +# else + (void) fw; + (void) set_res; + (void) set_st; +# endif // ANJAY_WITH_SEND +} + +static void set_user_state(advanced_fw_user_state_t *user, + anjay_advanced_fw_update_state_t new_state) { + fw_log(DEBUG, _("user->state change: ") "%d" _(" -> ") "%d", + (int) user->state, (int) new_state); + user->state = new_state; +} + +static int user_state_ensure_stream_open(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst) { + advanced_fw_user_state_t *const user = &inst->user_state; + + if (user->state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING) { + return 0; + } + assert(user->state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE); + + int result = -1; + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + result = user->handlers->stream_open(inst->iid, user->arg); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + if (!result) { + set_user_state(user, ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING); + } + return result; +} + +static int user_state_stream_write(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst, + const void *data, + size_t length) { + assert(inst->user_state.state + == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING); + int result = -1; + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + result = inst->user_state.handlers->stream_write( + inst->iid, inst->user_state.arg, data, length); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + return result; +} + +static const char *user_state_get_pkg_name(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst) { + if (!inst->user_state.handlers->get_pkg_name + || inst->user_state.state + != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED) { + return NULL; + } + const char *result = NULL; + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + result = inst->user_state.handlers->get_pkg_name(inst->iid, + inst->user_state.arg); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + return result; +} + +static const char *user_state_get_pkg_version(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst) { + if (!inst->user_state.handlers->get_pkg_version + || inst->user_state.state + != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED) { + return NULL; + } + const char *result = NULL; + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + result = inst->user_state.handlers->get_pkg_version(inst->iid, + inst->user_state.arg); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + return result; +} + +static const char * +user_state_get_current_version(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst) { + if (!inst->user_state.handlers->get_current_version) { + return NULL; + } + const char *result = NULL; + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + result = inst->user_state.handlers->get_current_version( + inst->iid, inst->user_state.arg); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + return result; +} + +static int user_state_perform_upgrade(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst, + const anjay_iid_t *supplemental_iids, + size_t supplemental_iids_count) { + advanced_fw_user_state_t *user = &inst->user_state; + + int result = -1; + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + result = user->handlers->perform_upgrade( + inst->iid, user->arg, supplemental_iids, supplemental_iids_count); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + // If the state was changed during perform_upgrade handler, this means + // @ref anjay_advanced_fw_update_set_state_and_result was called and + // has overwritten the State and Result. In that case, change State to + // Updating if update was not deferred or to Downloaded if failed due to + // dependency error. + if (!result && user->state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED + && inst->result != ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED + && inst->result + != ANJAY_ADVANCED_FW_UPDATE_RESULT_DEPENDENCY_ERROR) { + set_user_state(user, ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING); + } + return result; +} + +static int finish_user_stream(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst) { + assert(inst->user_state.state + == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING); + int result = -1; + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + result = inst->user_state.handlers->stream_finish(inst->iid, + inst->user_state.arg); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + if (result) { + set_user_state(&inst->user_state, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE); + } else { + set_user_state(&inst->user_state, + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED); + } + return result; +} + +static void reset_user_state(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst) { + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + inst->user_state.handlers->reset(inst->iid, inst->user_state.arg); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + set_user_state(&inst->user_state, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE); +} + +# ifdef ANJAY_WITH_DOWNLOADER + +static int get_security_config(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst, + anjay_security_config_t *out_security_config) { + assert(inst->user_state.state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE + || inst->user_state.state + == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING); + if (inst->user_state.handlers->get_security_config) { + int result = -1; + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + result = inst->user_state.handlers->get_security_config( + inst->iid, inst->user_state.arg, out_security_config, + inst->package_uri); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + return result; + } else { + if (!_anjay_security_config_from_dm_unlocked(anjay, out_security_config, + inst->package_uri)) { + return 0; + } +# ifdef ANJAY_WITH_LWM2M11 + *out_security_config = _anjay_security_config_pkix_unlocked(anjay); + if (out_security_config->security_info.data.cert + .server_cert_validation) { + return 0; + } +# endif // ANJAY_WITH_LWM2M11 + return -1; + } +} + +static int get_coap_tx_params(anjay_unlocked_t *anjay, + advanced_fw_instance_t *inst, + avs_coap_udp_tx_params_t *out_tx_params) { + if (inst->user_state.handlers->get_coap_tx_params) { + ANJAY_MUTEX_UNLOCK_FOR_CALLBACK(anjay_locked, anjay); + *out_tx_params = inst->user_state.handlers->get_coap_tx_params( + inst->iid, inst->user_state.arg, inst->package_uri); + ANJAY_MUTEX_LOCK_AFTER_CALLBACK(anjay_locked); + return 0; + } + return -1; +} +# endif // ANJAY_WITH_DOWNLOADER + +static void +handle_err_result(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst, + anjay_advanced_fw_update_state_t new_state, + int result, + anjay_advanced_fw_update_result_t default_result) { + anjay_advanced_fw_update_result_t new_result; + + switch (result) { + case -ANJAY_ADVANCED_FW_UPDATE_RESULT_NOT_ENOUGH_SPACE: + case -ANJAY_ADVANCED_FW_UPDATE_RESULT_OUT_OF_MEMORY: + case -ANJAY_ADVANCED_FW_UPDATE_RESULT_INTEGRITY_FAILURE: + case -ANJAY_ADVANCED_FW_UPDATE_RESULT_UNSUPPORTED_PACKAGE_TYPE: + case -ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED: + case -ANJAY_ADVANCED_FW_UPDATE_RESULT_CONFLICTING_STATE: + case -ANJAY_ADVANCED_FW_UPDATE_RESULT_DEPENDENCY_ERROR: + new_result = (anjay_advanced_fw_update_result_t) (-result); + break; + default: + new_result = default_result; + } + update_state_and_update_result(anjay, fw, inst, new_state, new_result); +} + +static void reset_state(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst) { + reset_user_state(anjay, inst); + update_state_and_update_result(anjay, fw, inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + fw_log(INFO, _("Firmware Object Instance ") "%" PRIu16 _(" state reset"), + inst->iid); +} + +static int fw_list_instances(anjay_unlocked_t *anjay, + const anjay_dm_installed_object_t obj_ptr, + anjay_unlocked_dm_list_ctx_t *ctx) { + (void) anjay; + + advanced_fw_repr_t *fw = get_fw(obj_ptr); + AVS_LIST(advanced_fw_instance_t) inst; + AVS_LIST_FOREACH(inst, fw->instances) { + _anjay_dm_emit_unlocked(ctx, inst->iid); + } + return 0; +} + +static advanced_fw_instance_t *get_fw_instance(advanced_fw_repr_t *fw, + anjay_iid_t iid) { + assert(fw); + + AVS_LIST(advanced_fw_instance_t) inst; + AVS_LIST_FOREACH(inst, fw->instances) { + if (inst->iid > iid) { + break; + } else if (inst->iid == iid) { + return inst; + } + } + return NULL; +} + +static int fw_list_resources(anjay_unlocked_t *anjay, + const anjay_dm_installed_object_t obj_ptr, + anjay_iid_t iid, + anjay_unlocked_dm_resource_list_ctx_t *ctx) { + advanced_fw_repr_t *fw = get_fw(obj_ptr); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + assert(inst); + + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_PACKAGE, ANJAY_DM_RES_W, + ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_PACKAGE_URI, ANJAY_DM_RES_RW, + ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_UPDATE, ANJAY_DM_RES_E, + ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_STATE, ANJAY_DM_RES_R, + ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_UPDATE_RESULT, ANJAY_DM_RES_R, + ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_PKG_NAME, ANJAY_DM_RES_R, + user_state_get_pkg_name(anjay, inst) + ? ANJAY_DM_RES_PRESENT + : ANJAY_DM_RES_ABSENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_PKG_VERSION, ANJAY_DM_RES_R, + user_state_get_pkg_version(anjay, inst) + ? ANJAY_DM_RES_PRESENT + : ANJAY_DM_RES_ABSENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_UPDATE_PROTOCOL_SUPPORT, + ANJAY_DM_RES_RM, ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_UPDATE_DELIVERY_METHOD, + ANJAY_DM_RES_R, ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_CANCEL, ANJAY_DM_RES_E, + ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_SEVERITY, ANJAY_DM_RES_RW, + ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_LAST_STATE_CHANGE_TIME, + ANJAY_DM_RES_R, ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_MAX_DEFER_PERIOD, + ANJAY_DM_RES_RW, ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_COMPONENT_NAME, ANJAY_DM_RES_R, + inst->component_name ? ANJAY_DM_RES_PRESENT + : ANJAY_DM_RES_ABSENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_CURRENT_VERSION, ANJAY_DM_RES_R, + user_state_get_current_version(anjay, inst) + ? ANJAY_DM_RES_PRESENT + : ANJAY_DM_RES_ABSENT); + if (AVS_LIST_NEXT(fw->instances)) { + // at least 2 instances exist + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_LINKED_INSTANCES, + ANJAY_DM_RES_RM, ANJAY_DM_RES_PRESENT); + _anjay_dm_emit_res_unlocked(ctx, ADV_FW_RES_CONFLICTING_INSTANCES, + ANJAY_DM_RES_RM, ANJAY_DM_RES_PRESENT); + } + return 0; +} + +static const int32_t SUPPORTED_PROTOCOLS[] = { +# ifdef WITH_AVS_COAP_UDP + 0, /* CoAP */ +# ifndef AVS_COMMONS_WITHOUT_TLS + 1, /* CoAPS */ +# endif // AVS_COMMONS_WITHOUT_TLS +# endif // WITH_AVS_COAP_UDP +# ifdef ANJAY_WITH_HTTP_DOWNLOAD + 2, /* HTTP 1.1 */ +# ifndef AVS_COMMONS_WITHOUT_TLS + 3, /* HTTPS 1.1 */ +# endif // AVS_COMMONS_WITHOUT_TLS +# endif // ANJAY_WITH_HTTP_DOWNLOAD +# ifdef WITH_AVS_COAP_TCP + 4, /* CoAP over TCP */ +# ifndef AVS_COMMONS_WITHOUT_TLS + 5, /* CoAP over TLS */ +# endif // AVS_COMMONS_WITHOUT_TLS +# endif // WITH_AVS_COAP_TCP +}; + +static int fw_read(anjay_unlocked_t *anjay, + const anjay_dm_installed_object_t obj_ptr, + anjay_iid_t iid, + anjay_rid_t rid, + anjay_riid_t riid, + anjay_unlocked_output_ctx_t *ctx) { + advanced_fw_repr_t *fw = get_fw(obj_ptr); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + assert(inst); + switch (rid) { + case ADV_FW_RES_PACKAGE_URI: + return _anjay_ret_string_unlocked( + ctx, inst->package_uri ? inst->package_uri : ""); + case ADV_FW_RES_STATE: + return _anjay_ret_i64_unlocked(ctx, (int32_t) inst->state); + case ADV_FW_RES_UPDATE_RESULT: + return _anjay_ret_i64_unlocked(ctx, (int32_t) inst->result); + case ADV_FW_RES_PKG_NAME: { + const char *name = user_state_get_pkg_name(anjay, inst); + + if (name) { + return _anjay_ret_string_unlocked(ctx, name); + } else { + return ANJAY_ERR_NOT_FOUND; + } + } + case ADV_FW_RES_PKG_VERSION: { + const char *version = user_state_get_pkg_version(anjay, inst); + + if (version) { + return _anjay_ret_string_unlocked(ctx, version); + } else { + return ANJAY_ERR_NOT_FOUND; + } + } + case ADV_FW_RES_UPDATE_PROTOCOL_SUPPORT: + assert(riid < AVS_ARRAY_SIZE(SUPPORTED_PROTOCOLS)); + return _anjay_ret_i64_unlocked(ctx, SUPPORTED_PROTOCOLS[riid]); + case ADV_FW_RES_UPDATE_DELIVERY_METHOD: +# ifdef ANJAY_WITH_DOWNLOADER + return _anjay_ret_i64_unlocked(ctx, 2); // 2 -> pull && push +# else // ANJAY_WITH_DOWNLOADER + return _anjay_ret_i64_unlocked(ctx, 1); // 1 -> push only +# endif // ANJAY_WITH_DOWNLOADER + case ADV_FW_RES_SEVERITY: + return _anjay_ret_i64_unlocked(ctx, (int32_t) inst->severity); + case ADV_FW_RES_LAST_STATE_CHANGE_TIME: { + int64_t last_state_change_timestamp = 0; + + avs_time_real_to_scalar(&last_state_change_timestamp, AVS_TIME_S, + inst->last_state_change_time); + return _anjay_ret_i64_unlocked(ctx, last_state_change_timestamp); + } + case ADV_FW_RES_MAX_DEFER_PERIOD: + return _anjay_ret_i64_unlocked(ctx, inst->max_defer_period); + case ADV_FW_RES_COMPONENT_NAME: + return _anjay_ret_string_unlocked(ctx, inst->component_name); + case ADV_FW_RES_CURRENT_VERSION: { + const char *version = user_state_get_current_version(anjay, inst); + + if (version) { + return _anjay_ret_string_unlocked(ctx, version); + } else { + return ANJAY_ERR_NOT_FOUND; + } + } + case ADV_FW_RES_LINKED_INSTANCES: + return _anjay_ret_objlnk_unlocked(ctx, ANJAY_ADVANCED_FW_UPDATE_OID, + riid); + case ADV_FW_RES_CONFLICTING_INSTANCES: + return _anjay_ret_objlnk_unlocked(ctx, ANJAY_ADVANCED_FW_UPDATE_OID, + riid); + default: + AVS_UNREACHABLE("Read called on unknown or non-readable Firmware " + "Update resource"); + return ANJAY_ERR_METHOD_NOT_ALLOWED; + } +} + +# if defined(ANJAY_WITH_COAP_DOWNLOAD) || defined(ANJAY_WITH_HTTP_DOWNLOAD) +static anjay_transport_security_t +transport_security_from_protocol(const char *protocol) { +# ifdef ANJAY_WITH_COAP_DOWNLOAD + const anjay_transport_info_t *info = + _anjay_transport_info_by_uri_scheme(protocol); + if (info) { + return info->security; + } +# endif // ANJAY_WITH_COAP_DOWNLOAD + +# ifdef ANJAY_WITH_HTTP_DOWNLOAD + if (avs_strcasecmp(protocol, "http") == 0) { + return ANJAY_TRANSPORT_NOSEC; + } + if (avs_strcasecmp(protocol, "https") == 0) { + return ANJAY_TRANSPORT_ENCRYPTED; + } +# endif // ANJAY_WITH_HTTP_DOWNLOAD + + return ANJAY_TRANSPORT_SECURITY_UNDEFINED; +} + +static anjay_transport_security_t transport_security_from_uri(const char *uri) { + avs_url_t *parsed_url = avs_url_parse_lenient(uri); + if (!parsed_url) { + return ANJAY_TRANSPORT_SECURITY_UNDEFINED; + } + anjay_transport_security_t result = ANJAY_TRANSPORT_SECURITY_UNDEFINED; + + const char *protocol = avs_url_protocol(parsed_url); + if (protocol) { + result = transport_security_from_protocol(protocol); + } + avs_url_free(parsed_url); + return result; +} +# else // ANJAY_WITH_COAP_DOWNLOAD || ANJAY_WITH_HTTP_DOWNLOAD +static anjay_transport_security_t transport_security_from_uri(const char *uri) { + (void) uri; + return ANJAY_TRANSPORT_SECURITY_UNDEFINED; +} +# endif // ANJAY_WITH_COAP_DOWNLOAD || ANJAY_WITH_HTTP_DOWNLOAD + +static void set_update_deadline(advanced_fw_instance_t *inst) { + if (inst->max_defer_period <= 0) { + inst->update_deadline = AVS_TIME_REAL_INVALID; + return; + } + inst->update_deadline = avs_time_real_add( + avs_time_real_now(), + avs_time_duration_from_scalar(inst->max_defer_period, AVS_TIME_S)); +} + +# ifdef ANJAY_WITH_DOWNLOADER + +static avs_error_t download_write_block(anjay_t *anjay_locked, + const uint8_t *data, + size_t data_size, + const anjay_etag_t *etag, + void *inst_) { + (void) etag; + int result = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + advanced_fw_instance_t *inst = (advanced_fw_instance_t *) inst_; + result = user_state_ensure_stream_open(anjay, inst); + if (!result && data_size > 0) { + result = user_state_stream_write(anjay, inst, data, data_size); + } + if (result) { + fw_log(ERROR, _("could not write firmware")); + + handle_err_result(anjay, fw, inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, result, + ANJAY_ADVANCED_FW_UPDATE_RESULT_NOT_ENOUGH_SPACE); + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return result ? avs_errno(AVS_UNKNOWN_ERROR) : AVS_OK; +} + +static int schedule_background_anjay_download(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst); + +static int schedule_download_now(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst, + anjay_download_config_t *cfg) { + if (transport_security_from_uri(cfg->url) == ANJAY_TRANSPORT_ENCRYPTED) { + int result = get_security_config(anjay, inst, &cfg->security_config); + + if (result) { + handle_err_result( + anjay, fw, inst, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + result, + ANJAY_ADVANCED_FW_UPDATE_RESULT_UNSUPPORTED_PROTOCOL); + return -1; + } + } + avs_error_t err = + _anjay_download_unlocked(anjay, cfg, &fw->download_handle); + if (avs_is_err(err)) { + anjay_advanced_fw_update_result_t update_result = + ANJAY_ADVANCED_FW_UPDATE_RESULT_CONNECTION_LOST; + if (err.category == AVS_ERRNO_CATEGORY) { + switch (err.code) { + case AVS_EADDRNOTAVAIL: + case AVS_EINVAL: + update_result = ANJAY_ADVANCED_FW_UPDATE_RESULT_INVALID_URI; + break; + case AVS_ENOMEM: + update_result = ANJAY_ADVANCED_FW_UPDATE_RESULT_OUT_OF_MEMORY; + break; + case AVS_EPROTONOSUPPORT: + update_result = + ANJAY_ADVANCED_FW_UPDATE_RESULT_UNSUPPORTED_PROTOCOL; + break; + } + } + reset_user_state(anjay, inst); + set_update_result(anjay, inst, update_result); +# ifdef ANJAY_WITH_SEND + send_state_and_update_result(anjay, fw, inst->iid, false); +# endif // ANJAY_WITH_SEND + return -1; + } + inst->retry_download_on_expired = (false); + update_state_and_update_result(anjay, fw, inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + fw_log(INFO, _("IID ") "%" PRIu16 _(": download started: ") "%s", inst->iid, + inst->package_uri); + return 0; +} + +static void start_next_download_if_waiting(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw) { + if (fw->download_queue != NULL) { + if (schedule_download_now( + anjay, fw, + (advanced_fw_instance_t *) fw->download_queue->user_data, + fw->download_queue)) { + fw_log(WARNING, _("Scheduling next waiting download failed")); + } + avs_free((void *) (intptr_t) fw->download_queue->url); + avs_free((void *) fw->download_queue->coap_tx_params); + AVS_LIST_DELETE(&fw->download_queue); + } +} + +static void download_finished(anjay_t *anjay_locked, + anjay_download_status_t status, + void *inst_) { + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + advanced_fw_instance_t *inst = (advanced_fw_instance_t *) inst_; + fw->download_handle = NULL; + if (inst->state != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING) { + // something already failed in download_write_block() + reset_user_state(anjay, inst); + start_next_download_if_waiting(anjay, fw); + } else if (status.result != ANJAY_DOWNLOAD_FINISHED) { + anjay_advanced_fw_update_result_t update_result = + ANJAY_ADVANCED_FW_UPDATE_RESULT_CONNECTION_LOST; + + if (status.result == ANJAY_DOWNLOAD_ERR_FAILED) { + if (status.details.error.category == AVS_ERRNO_CATEGORY) { + if (status.details.error.code == AVS_ENOMEM) { + update_result = + ANJAY_ADVANCED_FW_UPDATE_RESULT_OUT_OF_MEMORY; + } else if (status.details.error.code == AVS_EADDRNOTAVAIL) { + update_result = + ANJAY_ADVANCED_FW_UPDATE_RESULT_INVALID_URI; + } + } + } else if (status.result == ANJAY_DOWNLOAD_ERR_INVALID_RESPONSE + && (status.details.status_code == AVS_COAP_CODE_NOT_FOUND + || status.details.status_code == 404)) { + // NOTE: We should only check for the status code appropriate + // for the download protocol, but 132 (AVS_COAP_CODE_NOT_FOUND) + // is unlikely as a HTTP status code, and 12.20 (404 according + // to CoAP convention) is not representable on a single byte, so + // this is good enough. + update_result = ANJAY_ADVANCED_FW_UPDATE_RESULT_INVALID_URI; + } + reset_user_state(anjay, inst); + if (inst->retry_download_on_expired + && status.result == ANJAY_DOWNLOAD_ERR_EXPIRED) { + fw_log(INFO, + _("Could not resume firmware download (result = ") "%" + "d" _("), retrying from the beginning"), + (int) status.result); + if (schedule_background_anjay_download(anjay, fw, inst)) { + fw_log(WARNING, _("Could not retry firmware download")); + set_state(anjay, inst, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE); +# ifdef ANJAY_WITH_SEND + send_state_and_update_result(anjay, fw, inst->iid, false); +# endif // ANJAY_WITH_SEND + } + } else { + fw_log(WARNING, _("download aborted: result = ") "%d", + (int) status.result); + update_state_and_update_result( + anjay, fw, inst, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + update_result); + } + } else { + int result = user_state_ensure_stream_open(anjay, inst); + + if (!result) { + result = finish_user_stream(anjay, inst); + } + if (result) { + handle_err_result( + anjay, fw, inst, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + result, + ANJAY_ADVANCED_FW_UPDATE_RESULT_NOT_ENOUGH_SPACE); + } else { + update_state_and_update_result( + anjay, fw, inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + } + start_next_download_if_waiting(anjay, fw); + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); +} + +static bool is_any_download_in_progress(advanced_fw_repr_t *fw) { + return fw->download_handle || fw->download_queue; +} + +static int enqueue_download(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst, + anjay_download_config_t *cfg) { +# ifndef NDEBUG + AVS_LIST(anjay_download_config_t) queued_cfg; + AVS_LIST_FOREACH(queued_cfg, fw->download_queue) { + advanced_fw_instance_t *queued_inst = + (advanced_fw_instance_t *) queued_cfg->user_data; + assert(queued_inst->iid != inst->iid); + } +# endif // NDEBUG + AVS_LIST(anjay_download_config_t) new_download = + AVS_LIST_APPEND_NEW(anjay_download_config_t, &fw->download_queue); + if (!new_download) { + goto cleanup; + } + memcpy(new_download, cfg, sizeof(anjay_download_config_t)); + new_download->url = avs_strdup(cfg->url); + if (!new_download->url) { + goto cleanup; + } + if (cfg->coap_tx_params) { + new_download->coap_tx_params = (avs_coap_udp_tx_params_t *) avs_malloc( + sizeof(avs_coap_udp_tx_params_t)); + if (!new_download->coap_tx_params) { + goto cleanup; + } + memcpy(new_download->coap_tx_params, cfg->coap_tx_params, + sizeof(avs_coap_udp_tx_params_t)); + } + + update_state_and_update_result(anjay, fw, inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + fw_log(INFO, + _("There is a download in progress. New download from ") "%s" _( + " added to queue"), + inst->package_uri); + return 0; + +cleanup: + fw_log(ERROR, _("Out of memory")); + avs_free((void *) (intptr_t) new_download->url); + if (new_download) { + AVS_LIST_DELETE(&new_download); + } + return -1; +} + +static int schedule_download(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst) { + anjay_download_config_t cfg = { + .url = inst->package_uri, + .on_next_block = download_write_block, + .on_download_finished = download_finished, + .user_data = inst, + .prefer_same_socket_downloads = fw->prefer_same_socket_downloads + }; + avs_coap_udp_tx_params_t tx_params; + if (!get_coap_tx_params(anjay, inst, &tx_params)) { + cfg.coap_tx_params = &tx_params; + } + if (is_any_download_in_progress(fw)) { + return enqueue_download(anjay, fw, inst, &cfg); + } + return schedule_download_now(anjay, fw, inst, &cfg); +} + +struct schedule_download_args { + anjay_t *anjay; + advanced_fw_repr_t *fw; + advanced_fw_instance_t *inst; + size_t start_offset; + // actually a FAM +}; + +static int schedule_background_anjay_download(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst) { + return schedule_download(anjay, fw, inst); +} +# endif // ANJAY_WITH_DOWNLOADER + +static int write_firmware_to_stream(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst, + anjay_unlocked_input_ctx_t *ctx, + bool *out_is_reset_request) { + int result = 0; + size_t written = 0; + bool finished = false; + int first_byte = EOF; + + *out_is_reset_request = false; + while (!finished) { + size_t bytes_read; + char buffer[1024]; + + result = _anjay_get_bytes_unlocked(ctx, &bytes_read, &finished, buffer, + sizeof(buffer)); + if (result) { + fw_log(ERROR, _("anjay_get_bytes() failed")); + + update_state_and_update_result( + anjay, fw, inst, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + ANJAY_ADVANCED_FW_UPDATE_RESULT_CONNECTION_LOST); + return result; + } + if (bytes_read > 0) { + if (first_byte == EOF) { + first_byte = (unsigned char) buffer[0]; + } + result = user_state_stream_write(anjay, inst, buffer, bytes_read); + } + if (result) { + handle_err_result(anjay, fw, inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, result, + ANJAY_ADVANCED_FW_UPDATE_RESULT_NOT_ENOUGH_SPACE); + return ANJAY_ERR_INTERNAL; + } + written += bytes_read; + } + *out_is_reset_request = (written == 1 && first_byte == '\0'); + fw_log(INFO, _("write finished, ") "%lu" _(" B written"), + (unsigned long) written); + return 0; +} + +static int expect_single_nullbyte(anjay_unlocked_input_ctx_t *ctx) { + char bytes[2]; + size_t bytes_read; + bool finished = false; + + if (_anjay_get_bytes_unlocked(ctx, &bytes_read, &finished, bytes, + sizeof(bytes))) { + fw_log(ERROR, _("anjay_get_bytes() failed")); + return ANJAY_ERR_INTERNAL; + } else if (bytes_read != 1 || !finished || bytes[0] != '\0') { + return ANJAY_ERR_BAD_REQUEST; + } + return 0; +} + +static int write_firmware(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst, + anjay_unlocked_input_ctx_t *ctx, + bool *out_is_reset_request) { + assert(inst->state != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING); + if (user_state_ensure_stream_open(anjay, inst)) { + return -1; + } + + int result = write_firmware_to_stream(anjay, fw, inst, ctx, + out_is_reset_request); + + if (result) { + reset_user_state(anjay, inst); + } else if (!*out_is_reset_request) { + // stream_finish_result deliberately not propagated up: + // write itself succeeded + int stream_finish_result = finish_user_stream(anjay, inst); + + if (stream_finish_result) { + handle_err_result(anjay, fw, inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + stream_finish_result, + ANJAY_ADVANCED_FW_UPDATE_RESULT_NOT_ENOUGH_SPACE); + } else { + update_state_and_update_result( + anjay, fw, inst, ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + } + } + return result; +} + +static void +cancel_existing_download_if_in_progress(anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + advanced_fw_instance_t *inst) { + if (inst->state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING) { + if (inst->resume_download_job) { + assert(!fw->download_handle); + avs_sched_del(&inst->resume_download_job); + return; + } + AVS_ASSERT(fw->download_handle, + "download_handle is NULL - another Write handler called " + "during a PUSH-mode download?!"); + _anjay_download_abort_unlocked(anjay, fw->download_handle); + assert(!fw->download_handle); + } +} + +static int fw_write(anjay_unlocked_t *anjay, + const anjay_dm_installed_object_t obj_ptr, + anjay_iid_t iid, + anjay_rid_t rid, + anjay_riid_t riid, + anjay_unlocked_input_ctx_t *ctx) { + (void) riid; + + advanced_fw_repr_t *fw = get_fw(obj_ptr); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + assert(inst); + + switch (rid) { + case ADV_FW_RES_PACKAGE: { + assert(riid == ANJAY_ID_INVALID); + + int result = 0; + bool is_any_in_progress = is_any_download_in_progress(fw); + + if (inst->state == ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING) { + fw_log(WARNING, _("cannot set Package resource while updating")); + return ANJAY_ERR_METHOD_NOT_ALLOWED; + } else if (inst->state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE + && !is_any_in_progress) { + bool is_reset_request = false; + + result = write_firmware(anjay, fw, inst, ctx, &is_reset_request); + if (!result && is_reset_request) { + reset_state(anjay, fw, inst); + } + } else { + result = expect_single_nullbyte(ctx); + if (!result) { + cancel_existing_download_if_in_progress(anjay, fw, inst); + reset_state(anjay, fw, inst); + } else if (is_any_in_progress) { + fw_log(ERROR, _("There is a download already in progress or in " + "queue. Rejecting push mode download due do " + "implementation limitation")); + return ANJAY_ERR_METHOD_NOT_ALLOWED; + } + } + return result; + } + case ADV_FW_RES_PACKAGE_URI: { + assert(riid == ANJAY_ID_INVALID); + char *new_uri = NULL; + int result = _anjay_io_fetch_string(ctx, &new_uri); + size_t len = (new_uri ? strlen(new_uri) : 0); + + if (!result && len == 0) { + avs_free(new_uri); + + if (inst->state == ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING) { + fw_log(WARNING, + _("cannot set Package URI resource while updating")); + return ANJAY_ERR_METHOD_NOT_ALLOWED; + } + + cancel_existing_download_if_in_progress(anjay, fw, inst); + + avs_free((void *) (intptr_t) inst->package_uri); + inst->package_uri = NULL; + reset_state(anjay, fw, inst); + return 0; + } + + if (!result && inst->state != ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE) { + result = ANJAY_ERR_BAD_REQUEST; + } + + if (!result + && transport_security_from_uri(new_uri) + == ANJAY_TRANSPORT_SECURITY_UNDEFINED) { + fw_log(WARNING, + _("unsupported download protocol required for uri ") "%s", + new_uri); + set_update_result( + anjay, inst, + ANJAY_ADVANCED_FW_UPDATE_RESULT_UNSUPPORTED_PROTOCOL); +# ifdef ANJAY_WITH_SEND + send_state_and_update_result(anjay, fw, iid, false); +# endif // ANJAY_WITH_SEND + result = ANJAY_ERR_BAD_REQUEST; + } + +# ifdef ANJAY_WITH_DOWNLOADER + if (!result) { + avs_free((void *) (intptr_t) inst->package_uri); + inst->package_uri = new_uri; + new_uri = NULL; + + int dl_res = schedule_background_anjay_download(anjay, fw, inst); + + if (dl_res) { + fw_log(WARNING, + _("schedule_download_in_background failed: ") "%d", + dl_res); + } + // write itself succeeded; do not propagate error + } +# endif // ANJAY_WITH_DOWNLOADER + + avs_free(new_uri); + + return result; + } + case ADV_FW_RES_SEVERITY: { + assert(riid == ANJAY_ID_INVALID); + + int32_t severity = ANJAY_ADVANCED_FW_UPDATE_SEVERITY_MANDATORY; + + if (_anjay_get_i32_unlocked(ctx, &severity) + || severity < (int32_t) + ANJAY_ADVANCED_FW_UPDATE_SEVERITY_CRITICAL + || severity > (int32_t) + ANJAY_ADVANCED_FW_UPDATE_SEVERITY_OPTIONAL) { + return ANJAY_ERR_BAD_REQUEST; + } + inst->severity = (anjay_advanced_fw_update_severity_t) severity; + return 0; + } + case ADV_FW_RES_MAX_DEFER_PERIOD: { + assert(riid == ANJAY_ID_INVALID); + + int max_defer_period = 0; + + if (_anjay_get_i32_unlocked(ctx, &max_defer_period) + || max_defer_period < 0) { + return ANJAY_ERR_BAD_REQUEST; + } + inst->max_defer_period = max_defer_period; + return 0; + } + default: + // Bootstrap Server may try to write to other resources, + // so no AVS_UNREACHABLE() here + return ANJAY_ERR_METHOD_NOT_ALLOWED; + } +} + +static int fw_resource_instances(anjay_unlocked_t *anjay, + const anjay_dm_installed_object_t obj_ptr, + anjay_iid_t iid, + anjay_rid_t rid, + anjay_unlocked_dm_list_ctx_t *ctx) { + (void) anjay; + advanced_fw_repr_t *fw = get_fw(obj_ptr); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + assert(inst); + + switch (rid) { + case ADV_FW_RES_UPDATE_PROTOCOL_SUPPORT: { + for (anjay_riid_t i = 0; i < AVS_ARRAY_SIZE(SUPPORTED_PROTOCOLS); ++i) { + _anjay_dm_emit_unlocked(ctx, i); + } + return 0; + } + case ADV_FW_RES_LINKED_INSTANCES: { + for (size_t i = 0; i < inst->linked_instances_count; ++i) { + _anjay_dm_emit_unlocked(ctx, inst->linked_instances[i]); + } + return 0; + } + case ADV_FW_RES_CONFLICTING_INSTANCES: { + for (size_t i = 0; i < inst->conflicting_instances_count; ++i) { + _anjay_dm_emit_unlocked(ctx, inst->conflicting_instances[i]); + } + return 0; + } + default: + AVS_UNREACHABLE( + "Attempted to list instances in a single-instance resource"); + return ANJAY_ERR_INTERNAL; + } +} + +static void reset_supplemental_iid_cache(advanced_fw_repr_t *fw) { + avs_free(fw->supplemental_iid_cache); + fw->supplemental_iid_cache = NULL; + fw->supplemental_iid_cache_count = 0; +} + +struct upgrade_job_args { + advanced_fw_repr_t *fw; + advanced_fw_instance_t *inst; +}; + +static void perform_upgrade(avs_sched_t *sched, const void *args_) { + struct upgrade_job_args *args = + (struct upgrade_job_args *) (intptr_t) args_; + + set_update_deadline(args->inst); + anjay_t *anjay_locked = _anjay_get_from_sched(sched); + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + int result = + user_state_perform_upgrade(anjay, + args->inst, + args->fw->supplemental_iid_cache, + args->fw->supplemental_iid_cache_count); + reset_supplemental_iid_cache(args->fw); + if (result) { + fw_log(ERROR, _("user_state_perform_upgrade() failed: ") "%d", result); + handle_err_result(anjay, args->fw, args->inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED, result, + ANJAY_ADVANCED_FW_UPDATE_RESULT_FAILED); + } + ANJAY_MUTEX_UNLOCK(anjay_locked); +} + +static void schedule_upgrade(avs_sched_t *sched, const void *args_) { + struct upgrade_job_args *args = + (struct upgrade_job_args *) (intptr_t) args_; + anjay_t *anjay_locked = _anjay_get_from_sched(sched); + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + // Let's defer actually performing the upgrade to yet another scheduler run + // - the notification for the UPDATING state is probably being scheduled in + // the current one. + if (args->inst->state == ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING + && args->inst->user_state.state + != ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING + && AVS_SCHED_NOW(sched, &args->inst->update_job, perform_upgrade, + args, sizeof(*args))) { + reset_supplemental_iid_cache(args->fw); + update_state_and_update_result( + anjay, args->fw, args->inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED, + ANJAY_ADVANCED_FW_UPDATE_RESULT_OUT_OF_MEMORY); + } + ANJAY_MUTEX_UNLOCK(anjay_locked); +} + +static int sort_supplemental_iid_cache(advanced_fw_repr_t *fw) { + // Simple insertion sort. + // The count should be usually low enough for it to not be a performance + // hog. + for (size_t i = 1; i < fw->supplemental_iid_cache_count; ++i) { + for (ptrdiff_t j = (ptrdiff_t) i; j >= 0; --j) { + if (fw->supplemental_iid_cache[j - 1] + == fw->supplemental_iid_cache[j]) { + fw_log(ERROR, _("Duplicate instances specified in " + "Firmare Update arguments")); + return ANJAY_ERR_BAD_REQUEST; + } else if (fw->supplemental_iid_cache[j - 1] + < fw->supplemental_iid_cache[j]) { + break; + } + + anjay_iid_t tmp = fw->supplemental_iid_cache[j - 1]; + + fw->supplemental_iid_cache[j - 1] = fw->supplemental_iid_cache[j]; + fw->supplemental_iid_cache[j] = tmp; + } + } + return 0; +} + +static int handle_fw_execute_args(advanced_fw_repr_t *fw, + anjay_iid_t main_iid, + anjay_unlocked_execute_ctx_t *ctx) { + int arg; + bool arg_has_value; + + reset_supplemental_iid_cache(fw); + + if (_anjay_execute_get_next_arg_unlocked(ctx, &arg, &arg_has_value)) { + return 0; + } + + if (arg != 0) { + fw_log(ERROR, _("Invalid Firmware Update argument: ") "%d", arg); + return ANJAY_ERR_BAD_REQUEST; + } + + avs_stream_t *supplemental_iid_cache_membuf = avs_stream_membuf_create(); + + if (!supplemental_iid_cache_membuf) { + fw_log(ERROR, _("Out of memory")); + return ANJAY_ERR_INTERNAL; + } + + char arg_buf[sizeof(",")]; + size_t arg_buf_offset = 0; + size_t arg_buf_bytes_read; + int result = ANJAY_BUFFER_TOO_SHORT; + void *supplemental_iid_cache = NULL; + size_t supplemental_iid_cache_size = 0; + + while (true) { + if (result == ANJAY_BUFFER_TOO_SHORT + && sizeof(arg_buf) - arg_buf_offset > 1) { + result = _anjay_execute_get_arg_value_unlocked( + ctx, &arg_buf_bytes_read, arg_buf + arg_buf_offset, + sizeof(arg_buf) - arg_buf_offset); + if (result && result != ANJAY_BUFFER_TOO_SHORT) { + fw_log(ERROR, + _("Error while reading Firmware Update arguments")); + goto finish; + } + } + + if (arg_buf_offset == 0 && arg_buf_bytes_read == 0) { + break; + } + + anjay_oid_t supplemental_oid; + anjay_iid_t supplemental_iid; + advanced_fw_instance_t *supplemental_inst = NULL; + int char_count = 0; + int scanf_result = + sscanf(arg_buf, "%n", + &supplemental_oid, &supplemental_iid, &char_count); + + if (scanf_result == 2 && char_count + && supplemental_oid == ANJAY_ADVANCED_FW_UPDATE_OID + && supplemental_iid != main_iid + && supplemental_iid != ANJAY_ID_INVALID) { + supplemental_inst = get_fw_instance(fw, supplemental_iid); + } + + if (!supplemental_inst) { + fw_log(ERROR, _("Invalid argument for Firmware Update")); + result = ANJAY_ERR_BAD_REQUEST; + goto finish; + } + + if (supplemental_inst->state + != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED) { + fw_log(WARNING, + _("Firmware Update including supplemental instance ") "%" PRIu16 + _(" requested, but firmware not yet downloaded " + "(state = ") "%d" _(")"), + supplemental_iid, supplemental_inst->state); + result = ANJAY_ERR_METHOD_NOT_ALLOWED; + goto finish; + } + + if (avs_is_err(avs_stream_write(supplemental_iid_cache_membuf, + &supplemental_iid, + sizeof(supplemental_iid)))) { + fw_log(ERROR, _("Out of memory")); + result = ANJAY_ERR_INTERNAL; + goto finish; + } + + if (!result && !arg_buf[char_count]) { + break; + } else if (arg_buf[char_count] == ',') { + memmove(arg_buf, arg_buf + char_count + 1, + sizeof(arg_buf) - (size_t) char_count - 1); + arg_buf_offset += arg_buf_bytes_read; + arg_buf_offset -= (size_t) char_count + 1; + } else { + fw_log(ERROR, _("Invalid argument for Firmware Update")); + result = ANJAY_ERR_BAD_REQUEST; + goto finish; + } + } + + if (!fw->supplemental_iid_cache_count) { + // empty list is different from non-existing one in this case + // let's allocated a dummy byte so that the resulting pointer is not + // NULL + avs_stream_write(supplemental_iid_cache_membuf, &(uint8_t) { 0 }, 1); + } + + avs_stream_membuf_fit(supplemental_iid_cache_membuf); + + if (avs_is_err( + avs_stream_membuf_take_ownership(supplemental_iid_cache_membuf, + &supplemental_iid_cache, + &supplemental_iid_cache_size)) + || !supplemental_iid_cache) { + fw_log(ERROR, _("Could not take ownership of the buffer")); + result = ANJAY_ERR_INTERNAL; + goto finish; + } + + fw->supplemental_iid_cache = (anjay_iid_t *) supplemental_iid_cache; + fw->supplemental_iid_cache_count = + supplemental_iid_cache_size / sizeof(anjay_iid_t); + + result = sort_supplemental_iid_cache(fw); + + if (!result + && _anjay_execute_get_next_arg_unlocked(ctx, &arg, &arg_has_value) + != ANJAY_EXECUTE_GET_ARG_END) { + fw_log(ERROR, _("Superfluous Firmware Update argument: ") "%d", arg); + result = ANJAY_ERR_BAD_REQUEST; + } + +finish: + avs_stream_cleanup(&supplemental_iid_cache_membuf); + + if (result) { + reset_supplemental_iid_cache(fw); + } + return result; +} + +static int fw_execute(anjay_unlocked_t *anjay, + const anjay_dm_installed_object_t obj_ptr, + anjay_iid_t iid, + anjay_rid_t rid, + anjay_unlocked_execute_ctx_t *ctx) { + advanced_fw_repr_t *fw = get_fw(obj_ptr); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + assert(inst); + + switch (rid) { + case ADV_FW_RES_UPDATE: { + if (inst->state != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED) { + fw_log(WARNING, + _("Firmware Update for instance ") "%" PRIu16 _( + " requested, but firmware not yet downloaded " + "(state = ") "%d" _(")"), + iid, inst->state); + return ANJAY_ERR_METHOD_NOT_ALLOWED; + } + + int result = handle_fw_execute_args(fw, iid, ctx); + + if (result) { + return result; + } + + update_state_and_update_result(anjay, fw, inst, + ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING, + ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + // NOTE: This has to be called after update_state_and_update_result(), + // to make sure that schedule_upgrade() is called after notify_clb() + // and consequently, perform_upgrade() is called after trigger_observe() + // (if it's not delayed due to pmin). + const struct upgrade_job_args upgrade_job_args = { + .fw = fw, + .inst = inst + }; + + if (AVS_SCHED_NOW(_anjay_get_scheduler_unlocked(anjay), + &inst->update_job, schedule_upgrade, + &upgrade_job_args, sizeof(upgrade_job_args))) { + fw_log(WARNING, _("Could not schedule the upgrade job")); + update_state_and_update_result( + anjay, fw, inst, ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED, + ANJAY_ADVANCED_FW_UPDATE_RESULT_OUT_OF_MEMORY); + reset_supplemental_iid_cache(fw); + return ANJAY_ERR_INTERNAL; + } + return 0; + } + case ADV_FW_RES_CANCEL: + if (inst->state != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING + && inst->state != ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED) { + fw_log(WARNING, + _("Firmware Update Cancel requested, but the firmware is " + "being installed or has already been installed " + "(state = ") "%d" _(")"), + inst->state); + return ANJAY_ERR_METHOD_NOT_ALLOWED; + } + cancel_existing_download_if_in_progress(anjay, fw, inst); + reset_user_state(anjay, inst); + update_state_and_update_result( + anjay, fw, inst, ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE, + ANJAY_ADVANCED_FW_UPDATE_RESULT_UPDATE_CANCELLED); + return 0; + default: + return ANJAY_ERR_METHOD_NOT_ALLOWED; + } +} + +static int fw_transaction_noop(anjay_unlocked_t *anjay, + const anjay_dm_installed_object_t obj_ptr) { + (void) anjay; + (void) obj_ptr; + return 0; +} + +static const anjay_unlocked_dm_object_def_t FIRMWARE_UPDATE = { + .oid = ANJAY_ADVANCED_FW_UPDATE_OID, + .handlers = { + .list_instances = fw_list_instances, + .list_resources = fw_list_resources, + .resource_read = fw_read, + .resource_write = fw_write, + .list_resource_instances = fw_resource_instances, + .resource_execute = fw_execute, + .transaction_begin = fw_transaction_noop, + .transaction_validate = fw_transaction_noop, + .transaction_commit = fw_transaction_noop, + .transaction_rollback = fw_transaction_noop + } +}; + +static AVS_LIST(advanced_fw_instance_t) initialize_fw_instance( + anjay_unlocked_t *anjay, + advanced_fw_repr_t *fw, + anjay_iid_t iid, + const char *component_name, + const anjay_advanced_fw_update_handlers_t *handlers, + void *user_arg, + const anjay_advanced_fw_update_initial_state_t *initial_state) { + AVS_LIST(advanced_fw_instance_t) inst = + AVS_LIST_NEW_ELEMENT(advanced_fw_instance_t); + + if (!inst) { + fw_log(ERROR, _("Out of memory")); + return NULL; + } + + inst->iid = iid; + inst->component_name = component_name; + inst->user_state.handlers = handlers; + inst->user_state.arg = user_arg; + + if (!initial_state) { + return inst; + } + + inst->severity = initial_state->persisted_severity; + inst->last_state_change_time = + initial_state->persisted_last_state_change_time; + inst->update_deadline = initial_state->persisted_update_deadline; + + if ((initial_state->state != ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE + && initial_state->result != ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL) + || (initial_state->state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE + && initial_state->result + != ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL + && initial_state->result + != ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS + && initial_state->result + != ANJAY_ADVANCED_FW_UPDATE_RESULT_INTEGRITY_FAILURE + && initial_state->result + != ANJAY_ADVANCED_FW_UPDATE_RESULT_FAILED + && initial_state->result + != ANJAY_ADVANCED_FW_UPDATE_RESULT_DEPENDENCY_ERROR)) { + fw_log(ERROR, + _("Invalid initial_state->result for the specified " + "initial_state->state")); + goto fail; + } + + switch (initial_state->state) { + case ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE: + inst->result = initial_state->result; + return inst; + case ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING: { +# ifdef ANJAY_WITH_DOWNLOADER + inst->user_state.state = ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING; + reset_user_state(anjay, inst); + if (inst->result == ANJAY_ADVANCED_FW_UPDATE_RESULT_CONNECTION_LOST + && schedule_background_anjay_download(anjay, fw, inst)) { + fw_log(WARNING, _("Could not retry firmware download")); + } +# else // ANJAY_WITH_DOWNLOADER + (void) anjay; + fw_log(WARNING, + _("Unable to resume download: PULL download not supported")); +# endif // ANJAY_WITH_DOWNLOADER + return inst; + } + case ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED: + inst->user_state.state = ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED; + inst->state = ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED; + return inst; + case ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING: + inst->user_state.state = ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING; + inst->state = ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING; + inst->result = ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL; + return inst; + } + + fw_log(ERROR, _("Invalid initial_state->state")); +fail: + AVS_LIST_DELETE(&inst); + return NULL; +} + +static void fw_delete(void *fw_) { + advanced_fw_repr_t *fw = (advanced_fw_repr_t *) fw_; + AVS_LIST_CLEAR(&fw->instances) { + AVS_LIST(advanced_fw_instance_t) inst = fw->instances; + avs_sched_del(&inst->update_job); + avs_sched_del(&inst->resume_download_job); + avs_free(inst->linked_instances); + avs_free(inst->conflicting_instances); + avs_free((void *) (intptr_t) inst->package_uri); + } + AVS_LIST_CLEAR(&fw->download_queue) { + avs_free((void *) (intptr_t) fw->download_queue->url); + avs_free((void *) fw->download_queue->coap_tx_params); + } + // NOTE: fw itself will be freed when cleaning the objects list +} + +int anjay_advanced_fw_update_install( + anjay_t *anjay_locked, + const anjay_advanced_fw_update_global_config_t *config) { + assert(anjay_locked); + int result = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + AVS_LIST(advanced_fw_repr_t) repr = + AVS_LIST_NEW_ELEMENT(advanced_fw_repr_t); + if (!repr) { + fw_log(ERROR, _("out of memory")); + } else { + repr->def = &FIRMWARE_UPDATE; + if (config) { + repr->prefer_same_socket_downloads = + config->prefer_same_socket_downloads; +# ifdef ANJAY_WITH_SEND + repr->use_lwm2m_send = config->use_lwm2m_send; +# endif // ANJAY_WITH_SEND + } + _anjay_dm_installed_object_init_unlocked(&repr->def_ptr, &repr->def); + if (!_anjay_dm_module_install(anjay, fw_delete, repr)) { + AVS_STATIC_ASSERT(offsetof(advanced_fw_repr_t, def_ptr) == 0, + def_ptr_is_first_field); + AVS_LIST(anjay_dm_installed_object_t) entry = &repr->def_ptr; + if (_anjay_register_object_unlocked(anjay, &entry)) { + result = _anjay_dm_module_uninstall(anjay, fw_delete); + assert(!result); + result = -1; + } else { + result = 0; + } + } + if (result) { + AVS_LIST_CLEAR(&repr); + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return result; +} + +int anjay_advanced_fw_update_instance_add( + anjay_t *anjay_locked, + anjay_iid_t iid, + const char *component_name, + const anjay_advanced_fw_update_handlers_t *handlers, + void *user_arg, + const anjay_advanced_fw_update_initial_state_t *initial_state) { + assert(anjay_locked); + assert(handlers); + int retval = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t **inst_insert_ptr = &fw->instances; + + AVS_LIST_ITERATE_PTR(inst_insert_ptr) { + if ((*inst_insert_ptr)->iid >= iid) { + break; + } + } + + if (*inst_insert_ptr && (*inst_insert_ptr)->iid == iid) { + fw_log(ERROR, _("Instance already initialized")); + } else if ((fw->instances || iid != 0) + && (!component_name || !handlers->get_current_version)) { + fw_log(ERROR, + _("Component Name and Current Version is mandatory if " + "multiple instances are present")); + } else { + AVS_LIST(advanced_fw_instance_t) inst = + initialize_fw_instance(anjay, fw, iid, component_name, + handlers, user_arg, initial_state); + + if (inst) { + AVS_LIST_INSERT(inst_insert_ptr, inst); + retval = 0; + +# ifdef ANJAY_WITH_SEND + if (initial_state->state != ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE + || initial_state->result + != ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL) { + send_state_and_update_result(anjay, fw, iid, true); + } +# endif // ANJAY_WITH_SEND + } + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return retval; +} + +static bool +is_state_change_allowed(anjay_advanced_fw_update_state_t current_state, + anjay_advanced_fw_update_state_t new_state, + anjay_advanced_fw_update_result_t new_result) { + switch (current_state) { + case ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE: + return (((new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING + || new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED) + && new_result == ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL) + || (new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED + && new_result == ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED)); + case ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADING: + return (new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE + && new_result != ANJAY_ADVANCED_FW_UPDATE_RESULT_SUCCESS + && new_result != ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED) + || (new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED + && (new_result == ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL + || new_result + == ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED)); + case ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED: + return (new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE + && (new_result == ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL + || new_result + == ANJAY_ADVANCED_FW_UPDATE_RESULT_UPDATE_CANCELLED)) + || (new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED + && new_result == ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED) + || (new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING + && new_result == ANJAY_ADVANCED_FW_UPDATE_RESULT_INITIAL); + case ANJAY_ADVANCED_FW_UPDATE_STATE_UPDATING: + return (new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE + && new_result + != ANJAY_ADVANCED_FW_UPDATE_RESULT_UPDATE_CANCELLED + && new_result != ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED + && new_result + != ANJAY_ADVANCED_FW_UPDATE_RESULT_CONFLICTING_STATE) + || (new_state == ANJAY_ADVANCED_FW_UPDATE_STATE_DOWNLOADED + && (new_result == ANJAY_ADVANCED_FW_UPDATE_RESULT_FAILED + || new_result == ANJAY_ADVANCED_FW_UPDATE_RESULT_DEFERRED + || new_result + == ANJAY_ADVANCED_FW_UPDATE_RESULT_DEPENDENCY_ERROR)); + } + AVS_UNREACHABLE("invalid enum value"); + return false; +} + +int anjay_advanced_fw_update_set_state_and_result( + anjay_t *anjay_locked, + anjay_iid_t iid, + anjay_advanced_fw_update_state_t state, + anjay_advanced_fw_update_result_t result) { + assert(anjay_locked); + int retval = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else if (!is_state_change_allowed(inst->state, state, result)) { + fw_log(WARNING, + _("Firmware Update State and Result change from ") "%d/%d" _( + " to ") "%d/%d" _(" is not allowed"), + (int) inst->state, (int) inst->result, (int) state, + (int) result); + } else { + if (state == ANJAY_ADVANCED_FW_UPDATE_STATE_IDLE) { + reset_user_state(anjay, inst); + } + update_state_and_update_result(anjay, fw, inst, state, result); + retval = 0; + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return retval; +} + +int anjay_advanced_fw_update_get_state( + anjay_t *anjay_locked, + anjay_iid_t iid, + anjay_advanced_fw_update_state_t *out_state) { + assert(anjay_locked); + assert(out_state); + int retval = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else { + *out_state = inst->state; + retval = 0; + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return retval; +} + +int anjay_advanced_fw_update_get_result( + anjay_t *anjay_locked, + anjay_iid_t iid, + anjay_advanced_fw_update_result_t *out_result) { + assert(anjay_locked); + assert(out_result); + int retval = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else { + *out_result = inst->result; + retval = 0; + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return retval; +} + +static int validate_target_iid_list(advanced_fw_repr_t *fw, + anjay_iid_t iid, + const anjay_iid_t *target_iids, + size_t target_iids_count) { + for (size_t i = 1; i < target_iids_count; ++i) { + if (target_iids[i - 1] == target_iids[i]) { + fw_log(ERROR, _("Duplicate target instance")); + return -1; + } else if (target_iids[i - 1] > target_iids[i]) { + fw_log(ERROR, _("Target instance list not sorted")); + return -1; + } + } + + AVS_LIST(advanced_fw_instance_t) inst = fw->instances; + + for (size_t i = 0; i < target_iids_count; ++i) { + if (target_iids[i] == iid) { + fw_log(ERROR, _("Linked Instances or Conflicting Instances " + "cannot reference self")); + return -1; + } + while (inst && inst->iid < target_iids[i]) { + AVS_LIST_ADVANCE(&inst); + } + if (!inst || inst->iid != target_iids[i]) { + fw_log(ERROR, _("Target instance does not exist")); + return -1; + } + } + + return 0; +} + +static int copy_target_iid_list(anjay_iid_t **out_instances, + size_t *out_size, + const anjay_iid_t *target_iids, + size_t target_iids_count) { + anjay_iid_t *new_ptr = NULL; + + if (!target_iids_count) { + avs_free(*out_instances); + } else { + new_ptr = (anjay_iid_t *) avs_realloc( + *out_instances, target_iids_count * sizeof(anjay_iid_t)); + } + + if (target_iids_count && !new_ptr) { + fw_log(ERROR, _("Out of memory")); + return -1; + } + + *out_instances = new_ptr; + *out_size = target_iids_count; + if (target_iids_count) { + memcpy(*out_instances, target_iids, + target_iids_count * sizeof(anjay_iid_t)); + } + + return 0; +} + +int anjay_advanced_fw_update_set_linked_instances( + anjay_t *anjay_locked, + anjay_iid_t iid, + const anjay_iid_t *target_iids, + size_t target_iids_count) { + assert(anjay_locked); + int retval = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else { + retval = validate_target_iid_list(fw, iid, target_iids, + target_iids_count); + + if (!retval) { + retval = copy_target_iid_list(&inst->linked_instances, + &inst->linked_instances_count, + target_iids, target_iids_count); + } + + if (!retval) { + _anjay_notify_changed_unlocked(anjay, + ANJAY_ADVANCED_FW_UPDATE_OID, + iid, + ADV_FW_RES_LINKED_INSTANCES); + } + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return retval; +} + +int anjay_advanced_fw_update_get_linked_instances( + anjay_t *anjay_locked, + anjay_iid_t iid, + const anjay_iid_t **out_target_iids, + size_t *out_target_iids_count) { + assert(anjay_locked); + + int retval = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else { + *out_target_iids = inst->linked_instances; + *out_target_iids_count = inst->linked_instances_count; + retval = 0; + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return retval; +} + +int anjay_advanced_fw_update_set_conflicting_instances( + anjay_t *anjay_locked, + anjay_iid_t iid, + const anjay_iid_t *target_iids, + size_t target_iids_count) { + assert(anjay_locked); + int retval = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else { + retval = validate_target_iid_list(fw, iid, target_iids, + target_iids_count); + + if (!retval) { + retval = + copy_target_iid_list(&inst->conflicting_instances, + &inst->conflicting_instances_count, + target_iids, target_iids_count); + } + + if (!retval) { + _anjay_notify_changed_unlocked( + anjay, ANJAY_ADVANCED_FW_UPDATE_OID, iid, + ADV_FW_RES_CONFLICTING_INSTANCES); + } + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return retval; +} + +int anjay_advanced_fw_update_get_conflicting_instances( + anjay_t *anjay_locked, + anjay_iid_t iid, + const anjay_iid_t **out_target_iids, + size_t *out_target_iids_count) { + assert(anjay_locked); + int retval = -1; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else { + *out_target_iids = inst->conflicting_instances; + *out_target_iids_count = inst->conflicting_instances_count; + retval = 0; + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return retval; +} + +avs_time_real_t anjay_advanced_fw_update_get_deadline(anjay_t *anjay_locked, + anjay_iid_t iid) { + assert(anjay_locked); + avs_time_real_t result = AVS_TIME_REAL_INVALID; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else { + result = inst->update_deadline; + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return result; +} + +anjay_advanced_fw_update_severity_t +anjay_advanced_fw_update_get_severity(anjay_t *anjay_locked, anjay_iid_t iid) { + assert(anjay_locked); + anjay_advanced_fw_update_severity_t result = + ANJAY_ADVANCED_FW_UPDATE_SEVERITY_MANDATORY; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else { + result = inst->severity; + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return result; +} + +avs_time_real_t +anjay_advanced_fw_update_get_last_state_change_time(anjay_t *anjay_locked, + anjay_iid_t iid) { + assert(anjay_locked); + avs_time_real_t result = AVS_TIME_REAL_INVALID; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + const anjay_dm_installed_object_t *obj = + _anjay_dm_find_object_by_oid(anjay, ANJAY_ADVANCED_FW_UPDATE_OID); + if (!obj) { + fw_log(WARNING, _("Firmware Update object not installed")); + } else { + advanced_fw_repr_t *fw = get_fw(*obj); + + assert(fw); + advanced_fw_instance_t *inst = get_fw_instance(fw, iid); + + if (!inst) { + fw_log(ERROR, _("Instance does not exist")); + } else { + result = inst->last_state_change_time; + } + } + ANJAY_MUTEX_UNLOCK(anjay_locked); + return result; +} + +#endif // ANJAY_WITH_MODULE_ADVANCED_FW_UPDATE diff --git a/src/modules/factory_provisioning/anjay_provisioning.c b/src/modules/factory_provisioning/anjay_provisioning.c index 10c853d11..49dff322c 100644 --- a/src/modules/factory_provisioning/anjay_provisioning.c +++ b/src/modules/factory_provisioning/anjay_provisioning.c @@ -14,6 +14,7 @@ # error "CBOR content format must be enabled to use factory provisioning" # endif // ANJAY_WITH_CBOR +# include # include # include @@ -25,7 +26,6 @@ # include # include "core/dm/anjay_dm_write.h" -# include VISIBILITY_SOURCE_BEGIN @@ -48,26 +48,29 @@ static avs_error_t factory_provisioning_unlocked(anjay_unlocked_t *anjay, if (_anjay_input_senml_cbor_create( &input_ctx, data_stream, &MAKE_ROOT_PATH())) { provisioning_log(ERROR, _("Cannot create CBOR context")); - return avs_errno(AVS_ENOMEM); + err = avs_errno(AVS_ENOMEM); } else { if (_anjay_bootstrap_write_composite(anjay, input_ctx)) { + provisioning_log(ERROR, + _("Error occured during writing bootstrap " + "information")); err = avs_errno(AVS_EPROTO); } (void) _anjay_input_ctx_destroy(&input_ctx); } } - if (avs_is_err(err)) { - provisioning_log( - ERROR, _("Error occured during writing bootstrap information")); - return err; - } else if (_anjay_bootstrap_finish(anjay)) { + if (avs_is_ok(err) && _anjay_bootstrap_finish(anjay)) { provisioning_log(ERROR, _("Could not apply bootstrap information")); - return avs_errno(AVS_EBADMSG); + err = avs_errno(AVS_EBADMSG); } - provisioning_log(INFO, _("Finished factory provisioning")); - return AVS_OK; + if (avs_is_ok(err)) { + provisioning_log(INFO, _("Finished factory provisioning")); + } else { + _anjay_bootstrap_cleanup(anjay); + } + return err; } avs_error_t anjay_factory_provision(anjay_t *anjay_locked, diff --git a/src/modules/server/anjay_mod_server.c b/src/modules/server/anjay_mod_server.c index 42a043644..b7febc41d 100644 --- a/src/modules/server/anjay_mod_server.c +++ b/src/modules/server/anjay_mod_server.c @@ -45,6 +45,7 @@ static const struct { .rid = SERV_RES_DEFAULT_MAX_PERIOD, .kind = ANJAY_DM_RES_RW }, +# ifndef ANJAY_WITHOUT_DEREGISTER { .rid = SERV_RES_DISABLE, .kind = ANJAY_DM_RES_E @@ -53,6 +54,7 @@ static const struct { .rid = SERV_RES_DISABLE_TIMEOUT, .kind = ANJAY_DM_RES_RW }, +# endif // ANJAY_WITHOUT_DEREGISTER { .rid = SERV_RES_NOTIFICATION_STORING_WHEN_DISABLED_OR_OFFLINE, .kind = ANJAY_DM_RES_RW @@ -698,10 +700,10 @@ void anjay_server_object_purge(anjay_t *anjay_locked) { ANJAY_MUTEX_UNLOCK(anjay_locked); } -AVS_LIST(const anjay_ssid_t) anjay_server_get_ssids(anjay_t *anjay_locked) { - assert(anjay_locked); +AVS_LIST(const anjay_ssid_t) +_anjay_server_get_ssids_unlocked(anjay_unlocked_t *anjay) { + assert(anjay); AVS_LIST(server_instance_t) source = NULL; - ANJAY_MUTEX_LOCK(anjay, anjay_locked); const anjay_dm_installed_object_t *server_obj = _anjay_dm_find_object_by_oid(anjay, SERVER.oid); server_repr_t *repr = _anjay_serv_get(*server_obj); @@ -710,7 +712,6 @@ AVS_LIST(const anjay_ssid_t) anjay_server_get_ssids(anjay_t *anjay_locked) { } else { source = repr->instances; } - ANJAY_MUTEX_UNLOCK(anjay_locked); // We rely on the fact that the "ssid" field is first in server_instance_t, // which means that both "source" and "&source->ssid" point to exactly the // same memory location. The "next" pointer location in AVS_LIST is @@ -720,6 +721,15 @@ AVS_LIST(const anjay_ssid_t) anjay_server_get_ssids(anjay_t *anjay_locked) { return &source->ssid; } +AVS_LIST(const anjay_ssid_t) anjay_server_get_ssids(anjay_t *anjay_locked) { + assert(anjay_locked); + AVS_LIST(const anjay_ssid_t) source = NULL; + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + source = _anjay_server_get_ssids_unlocked(anjay); + ANJAY_MUTEX_UNLOCK(anjay_locked); + return source; +} + bool anjay_server_object_is_modified(anjay_t *anjay_locked) { assert(anjay_locked); bool result = false; diff --git a/src/modules/server/anjay_mod_server.h b/src/modules/server/anjay_mod_server.h index 41343dfb8..db093ecdc 100644 --- a/src/modules/server/anjay_mod_server.h +++ b/src/modules/server/anjay_mod_server.h @@ -21,8 +21,10 @@ typedef enum { SERV_RES_LIFETIME = 1, SERV_RES_DEFAULT_MIN_PERIOD = 2, SERV_RES_DEFAULT_MAX_PERIOD = 3, +#ifndef ANJAY_WITHOUT_DEREGISTER SERV_RES_DISABLE = 4, SERV_RES_DISABLE_TIMEOUT = 5, +#endif // ANJAY_WITHOUT_DEREGISTER SERV_RES_NOTIFICATION_STORING_WHEN_DISABLED_OR_OFFLINE = 6, SERV_RES_BINDING = 7, SERV_RES_REGISTRATION_UPDATE_TRIGGER = 8, diff --git a/tests/core/anjay.c b/tests/core/anjay.c index 6d95bb8a0..5d4a0a996 100644 --- a/tests/core/anjay.c +++ b/tests/core/anjay.c @@ -9,6 +9,8 @@ #include +#include + #define AVS_UNIT_ENABLE_SHORT_ASSERTS #include @@ -17,8 +19,11 @@ #include +#include "src/core/anjay_servers_inactive.h" +#include "src/core/anjay_servers_reload.h" #include "src/core/servers/anjay_server_connections.h" #include "tests/core/coap/utils.h" +#include "tests/core/socket_mock.h" #include "tests/utils/dm.h" #include "tests/utils/utils.h" @@ -620,7 +625,9 @@ AVS_UNIT_TEST(queue_mode, change) { ASSERT_EQ(entries->ssid, 1); ASSERT_FALSE(entries->queue_mode); } +#ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE ASSERT_NULL(connection->queue_mode_close_socket_clb); +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE ////// REFRESH BINDING MODE ////// // query SSID in Server @@ -805,9 +812,7 @@ AVS_UNIT_TEST(queue_mode, change) { PAYLOAD(",")); avs_unit_mocksock_expect_output(mocksocks[0], update->content, update->length); - while (anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) == 0) { - anjay_sched_run(anjay); - } + anjay_sched_run(anjay); const coap_test_msg_t *update_response = COAP_MSG(ACK, CHANGED, ID_TOKEN_RAW(0x0000, nth_token(0)), @@ -817,6 +822,7 @@ AVS_UNIT_TEST(queue_mode, change) { expect_has_buffered_data_check(mocksocks[0], false); ASSERT_OK(anjay_serve(anjay, mocksocks[0])); +#ifndef ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE ASSERT_NOT_NULL(connection->queue_mode_close_socket_clb); // After 93s from now, the socket should be closed. We first wait for 92s, // ensure that the socket is still not closed, then wait one more second, @@ -832,6 +838,7 @@ AVS_UNIT_TEST(queue_mode, change) { ASSERT_NULL(anjay_get_sockets(anjay)); ASSERT_NULL(anjay_get_socket_entries(anjay)); ASSERT_NULL(connection->queue_mode_close_socket_clb); +#endif // ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE DM_TEST_FINISH; } @@ -845,7 +852,13 @@ AVS_UNIT_TEST(anjay_new, no_endpoint_name) { ASSERT_NULL(anjay_new(&configuration)); } -static const anjay_iid_t FAKE_SERVER_INSTANCES[] = { 1, ANJAY_ID_INVALID }; +static const anjay_mock_dm_res_entry_t FAKE_SECURITY_RESOURCES[] = { + { ANJAY_DM_RID_SECURITY_SERVER_URI, ANJAY_DM_RES_R, ANJAY_DM_RES_PRESENT }, + { ANJAY_DM_RID_SECURITY_MODE, ANJAY_DM_RES_R, ANJAY_DM_RES_PRESENT }, + { ANJAY_DM_RID_SECURITY_SSID, ANJAY_DM_RES_R, ANJAY_DM_RES_PRESENT }, + ANJAY_MOCK_DM_RES_END +}; + static const anjay_mock_dm_res_entry_t FAKE_SERVER_RESOURCES[] = { { ANJAY_DM_RID_SERVER_SSID, ANJAY_DM_RES_R, ANJAY_DM_RES_PRESENT }, { ANJAY_DM_RID_SERVER_LIFETIME, ANJAY_DM_RES_RW, ANJAY_DM_RES_PRESENT }, @@ -853,37 +866,59 @@ static const anjay_mock_dm_res_entry_t FAKE_SERVER_RESOURCES[] = { ANJAY_MOCK_DM_RES_END }; -static void expect_refresh_server(anjay_t *anjay) { +typedef enum { + RECONNECT_VERIFY = 0, + RECONNECT_SUSPENDED, + RECONNECT_FULL, + RECONNECT_NONE +} expect_refresh_server_reconnect_mode_t; + +typedef struct { + int dummy; + expect_refresh_server_reconnect_mode_t with_reconnect; + anjay_ssid_t ssid; + size_t server_count; +} expect_refresh_server_additional_args_t; + +static void +expect_refresh_server__(anjay_t *anjay, + const expect_refresh_server_additional_args_t *args) { + anjay_ssid_t ssid = args->ssid ? args->ssid : 1; + size_t server_count = args->server_count ? args->server_count : 1; + AVS_UNIT_ASSERT_TRUE(ssid <= server_count); + + anjay_iid_t fake_server_instances[server_count + 1]; + for (size_t i = 0; i < server_count; ++i) { + fake_server_instances[i] = (anjay_iid_t) (i + 1); + } + fake_server_instances[server_count] = ANJAY_ID_INVALID; + _anjay_mock_dm_expect_list_instances(anjay, &FAKE_SERVER, 0, - FAKE_SERVER_INSTANCES); + fake_server_instances); // Read SSID - _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, 1, 0, - FAKE_SERVER_RESOURCES); - _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SERVER, 1, - ANJAY_DM_RID_SERVER_SSID, - ANJAY_ID_INVALID, 0, - ANJAY_MOCK_DM_INT(0, 1)); + for (anjay_ssid_t i = 1; i <= ssid; ++i) { + _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, i, 0, + FAKE_SERVER_RESOURCES); + _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SERVER, i, + ANJAY_DM_RID_SERVER_SSID, + ANJAY_ID_INVALID, 0, + ANJAY_MOCK_DM_INT(0, i)); + } // Read Binding - _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, 1, 0, + _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, ssid, 0, FAKE_SERVER_RESOURCES); - _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SERVER, 1, + _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SERVER, ssid, ANJAY_DM_RID_SERVER_BINDING, ANJAY_ID_INVALID, 0, ANJAY_MOCK_DM_STRING(0, "U")); #ifdef ANJAY_WITH_LWM2M11 // attempt to read Preferred Transport - _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, 1, 0, + _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, ssid, 0, FAKE_SERVER_RESOURCES); #endif // ANJAY_WITH_LWM2M11 _anjay_mock_dm_expect_list_instances( anjay, &FAKE_SECURITY2, 0, (const anjay_iid_t[]) { 1, ANJAY_ID_INVALID }); - const anjay_mock_dm_res_entry_t FAKE_SECURITY_RESOURCES[] = { - { ANJAY_DM_RID_SECURITY_SERVER_URI, ANJAY_DM_RES_R, - ANJAY_DM_RES_PRESENT }, - { ANJAY_DM_RID_SECURITY_SSID, ANJAY_DM_RES_R, ANJAY_DM_RES_PRESENT }, - ANJAY_MOCK_DM_RES_END - }; // attempt to read Bootstrap _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SECURITY2, 1, 0, FAKE_SECURITY_RESOURCES); @@ -893,7 +928,7 @@ static void expect_refresh_server(anjay_t *anjay) { _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SECURITY2, 1, ANJAY_DM_RID_SECURITY_SSID, ANJAY_ID_INVALID, 0, - ANJAY_MOCK_DM_INT(0, 1)); + ANJAY_MOCK_DM_INT(0, ssid)); // Read Server URI _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SECURITY2, 1, 0, FAKE_SECURITY_RESOURCES); @@ -905,46 +940,99 @@ static void expect_refresh_server(anjay_t *anjay) { _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SECURITY2, 1, 0, FAKE_SECURITY_RESOURCES); #endif // ANJAY_WITH_LWM2M11 + if (args->with_reconnect == RECONNECT_NONE) { + return; + } + if (args->with_reconnect >= RECONNECT_FULL) { + _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SECURITY2, 1, 0, + FAKE_SECURITY_RESOURCES); + _anjay_mock_dm_expect_resource_read( + anjay, &FAKE_SECURITY2, 1, ANJAY_DM_RID_SECURITY_MODE, + ANJAY_ID_INVALID, 0, + ANJAY_MOCK_DM_INT(0, ANJAY_SECURITY_NOSEC)); + } // Query the data model _anjay_mock_dm_expect_list_instances(anjay, &FAKE_SERVER, 0, - FAKE_SERVER_INSTANCES); + fake_server_instances); // attempt to read Bootstrap _anjay_mock_dm_expect_list_instances(anjay, &FAKE_SERVER, 0, - FAKE_SERVER_INSTANCES); + fake_server_instances); // Read SSID - _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, 1, 0, - FAKE_SERVER_RESOURCES); - _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SERVER, 1, - ANJAY_DM_RID_SERVER_SSID, - ANJAY_ID_INVALID, 0, - ANJAY_MOCK_DM_INT(0, 1)); + for (anjay_ssid_t i = 1; i <= ssid; ++i) { + _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, i, 0, + FAKE_SERVER_RESOURCES); + _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SERVER, i, + ANJAY_DM_RID_SERVER_SSID, + ANJAY_ID_INVALID, 0, + ANJAY_MOCK_DM_INT(0, i)); + } // Read Lifetime - _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, 1, 0, + _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, ssid, 0, FAKE_SERVER_RESOURCES); - _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SERVER, 1, + _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SERVER, ssid, ANJAY_DM_RID_SERVER_LIFETIME, ANJAY_ID_INVALID, 0, ANJAY_MOCK_DM_INT(0, 86400)); } -static void force_update(anjay_t *anjay, avs_net_socket_t *mocksock) { - AVS_UNIT_ASSERT_SUCCESS(anjay_schedule_registration_update(anjay, 1)); - expect_refresh_server(anjay); - anjay_sched_run(anjay); +#define expect_refresh_server(...) \ + expect_refresh_server__(AVS_VARARG0(__VA_ARGS__), \ + &(const expect_refresh_server_additional_args_t) { \ + .dummy = 0 AVS_VARARG_REST(__VA_ARGS__) \ + }) + +typedef struct { + int dummy; + anjay_ssid_t ssid; + size_t server_count; + uint64_t token_seed; +} force_update_additional_args_t; + +static void force_update__(anjay_t *anjay, + avs_net_socket_t *mocksock, + const force_update_additional_args_t *args) { + anjay_ssid_t ssid = args->ssid ? args->ssid : 1; + size_t server_count = args->server_count ? args->server_count : 1; + AVS_UNIT_ASSERT_TRUE(ssid <= server_count); + + AVS_UNIT_ASSERT_SUCCESS(anjay_schedule_registration_update(anjay, ssid)); + expect_refresh_server(anjay, + .ssid = ssid, + .server_count = server_count); + avs_stream_t *payload_memstream = avs_stream_membuf_create(); + AVS_UNIT_ASSERT_NOT_NULL(payload_memstream); + for (anjay_iid_t i = 1; i <= server_count; ++i) { + AVS_UNIT_ASSERT_SUCCESS(avs_stream_write_f( + payload_memstream, "%s", i > 1 ? "," : "", i)); + } + void *payload = NULL; + size_t payload_size = 0; + AVS_UNIT_ASSERT_SUCCESS(avs_stream_membuf_take_ownership( + payload_memstream, &payload, &payload_size)); + avs_stream_cleanup(&payload_memstream); const coap_test_msg_t *update_request = - COAP_MSG(CON, POST, ID_TOKEN_RAW(0x0000, nth_token(0)), + COAP_MSG(CON, POST, + ID_TOKEN_RAW(0x0000, nth_token(args->token_seed)), CONTENT_FORMAT(LINK_FORMAT), QUERY("lt=86400", "b=U"), - PAYLOAD("")); + PAYLOAD_EXTERNAL(payload, payload_size)); avs_unit_mocksock_expect_output(mocksock, update_request->content, update_request->length); anjay_sched_run(anjay); - DM_TEST_REQUEST(mocksock, ACK, CHANGED, ID_TOKEN_RAW(0x0000, nth_token(0)), + avs_free(payload); + DM_TEST_REQUEST(mocksock, ACK, CHANGED, + ID_TOKEN_RAW(0x0000, nth_token(args->token_seed)), NO_PAYLOAD); expect_has_buffered_data_check(mocksock, false); AVS_UNIT_ASSERT_SUCCESS(anjay_serve(anjay, mocksock)); anjay_sched_run(anjay); } +#define force_update(Anjay, ...) \ + force_update__((Anjay), AVS_VARARG0(__VA_ARGS__), \ + &(const force_update_additional_args_t) { \ + .dummy = 0 AVS_VARARG_REST(__VA_ARGS__) \ + }) + AVS_UNIT_TEST(reconnect_after_update, test) { DM_TEST_INIT_WITH_OBJECTS(&FAKE_SECURITY2, &FAKE_SERVER); // Do an initial Update first, to update the registration expire time @@ -965,8 +1053,9 @@ AVS_UNIT_TEST(reconnect_after_update, test) { .flag = true }); // reload_servers_sched_job - _anjay_mock_dm_expect_list_instances(anjay, &FAKE_SERVER, 0, - FAKE_SERVER_INSTANCES); + _anjay_mock_dm_expect_list_instances( + anjay, &FAKE_SERVER, 0, + (const anjay_iid_t[]) { 1, ANJAY_ID_INVALID }); _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SERVER, 1, 0, FAKE_SERVER_RESOURCES); _anjay_mock_dm_expect_resource_read(anjay, &FAKE_SERVER, 1, @@ -979,7 +1068,6 @@ AVS_UNIT_TEST(reconnect_after_update, test) { _anjay_mock_dm_expect_list_resources(anjay, &FAKE_SECURITY2, 1, 0, (const anjay_mock_dm_res_entry_t[]) { ANJAY_MOCK_DM_RES_END }); - anjay_sched_run(anjay); // retry_or_request_expired_job const coap_test_msg_t *update_request = @@ -1002,3 +1090,317 @@ AVS_UNIT_TEST(reconnect_after_update, test) { DM_TEST_FINISH; } + +#ifdef ANJAY_WITH_LWM2M11 +# define DM_REGISTER_TEST_CONFIGURATION \ + DM_TEST_CONFIGURATION(.lwm2m_version_config = &( \ + const anjay_lwm2m_version_config_t) { \ + .minimum_version = ANJAY_LWM2M_VERSION_1_0, \ + .maximum_version = ANJAY_LWM2M_VERSION_1_0 \ + }) +#else // ANJAY_WITH_LWM2M11 +# define DM_REGISTER_TEST_CONFIGURATION DM_TEST_CONFIGURATION() +#endif // ANJAY_WITH_LWM2M11 + +#define DM_REGISTER_TEST_INIT_WITH_SSIDS(...) \ + const anjay_dm_object_def_t *const *obj_defs[] = { &FAKE_SECURITY2, \ + &FAKE_SERVER }; \ + anjay_ssid_t ssids[] = { __VA_ARGS__ }; \ + DM_TEST_INIT_GENERIC(obj_defs, ssids, DM_REGISTER_TEST_CONFIGURATION); \ + do { \ + /* Do initial Updates first to update the registration expire times */ \ + for (size_t _i = 0; _i < AVS_ARRAY_SIZE(ssids); ++_i) { \ + force_update(anjay, mocksocks[_i], \ + .ssid = (anjay_ssid_t) (_i + 1), \ + .server_count = AVS_ARRAY_SIZE(ssids), \ + .token_seed = _i); \ + } \ + } while (false) + +static void make_server_inactive(anjay_t *anjay, + anjay_ssid_t ssid, + avs_net_socket_t *mocksock) { + // Schedule server refresh and make it fail + ANJAY_MUTEX_LOCK(anjay_unlocked, anjay); + anjay_server_info_t *server = _anjay_servers_find(anjay_unlocked, ssid); + AVS_UNIT_ASSERT_NOT_NULL(server); + _anjay_schedule_refresh_server(server, AVS_TIME_DURATION_ZERO); + ANJAY_MUTEX_UNLOCK(anjay); + _anjay_mock_dm_expect_list_instances(anjay, &FAKE_SERVER, -1, + (const anjay_iid_t[]) { + ANJAY_ID_INVALID }); + avs_unit_mocksock_expect_shutdown(mocksock); +#ifdef ANJAY_WITH_NET_STATS + avs_unit_mocksock_expect_get_opt(mocksock, AVS_NET_SOCKET_OPT_BYTES_SENT, + (avs_net_socket_opt_value_t) { + .bytes_sent = 0 + }); + avs_unit_mocksock_expect_get_opt(mocksock, + AVS_NET_SOCKET_OPT_BYTES_RECEIVED, + (avs_net_socket_opt_value_t) { + .bytes_received = 0 + }); +#endif // ANJAY_WITH_NET_STATS +#ifdef ANJAY_WITH_LWM2M11 + _anjay_mock_dm_expect_list_instances(anjay, &FAKE_SERVER, -1, + (const anjay_iid_t[]) { + ANJAY_ID_INVALID }); +#endif // ANJAY_WITH_LWM2M11 + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); +} + +AVS_UNIT_TEST(reconnect_server, failures) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + // ANJAY_SSID_ANY + AVS_UNIT_ASSERT_FAILED( + anjay_server_schedule_reconnect(anjay, ANJAY_SSID_ANY)); + // Nonexistent server + AVS_UNIT_ASSERT_FAILED(anjay_server_schedule_reconnect(anjay, 42)); + // Inactive server + make_server_inactive(anjay, 1, mocksocks[0]); + AVS_UNIT_ASSERT_FAILED(anjay_server_schedule_reconnect(anjay, 1)); + DM_TEST_FINISH; +} + +AVS_UNIT_TEST(reconnect_server, fresh_session) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + avs_unit_mocksock_expect_shutdown(mocksocks[0]); + AVS_UNIT_ASSERT_SUCCESS(anjay_server_schedule_reconnect(anjay, 1)); + expect_refresh_server(anjay, + .with_reconnect = RECONNECT_SUSPENDED); + avs_unit_mocksock_expect_connect(mocksocks[0], "", ""); + avs_unit_mocksock_expect_local_port(mocksocks[0], "5683"); + avs_unit_mocksock_expect_get_opt(mocksocks[0], + AVS_NET_SOCKET_OPT_SESSION_RESUMED, + (avs_net_socket_opt_value_t) { + .flag = false + }); + const coap_test_msg_t *register_request = + COAP_MSG(CON, POST, ID_TOKEN_RAW(0x0000, nth_token(1)), + CONTENT_FORMAT(LINK_FORMAT), PATH("rd"), + QUERY("lwm2m=1.0", "ep=urn:dev:os:anjay-test", "lt=86400"), + PAYLOAD("")); + avs_unit_mocksock_expect_output(mocksocks[0], register_request->content, + register_request->length); + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + DM_TEST_FINISH; +} + +AVS_UNIT_TEST(reconnect_server, resumed_session) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + avs_unit_mocksock_expect_shutdown(mocksocks[0]); + AVS_UNIT_ASSERT_SUCCESS(anjay_server_schedule_reconnect(anjay, 1)); + expect_refresh_server(anjay); + avs_unit_mocksock_expect_connect(mocksocks[0], "", ""); + avs_unit_mocksock_expect_local_port(mocksocks[0], "5683"); + avs_unit_mocksock_expect_get_opt(mocksocks[0], + AVS_NET_SOCKET_OPT_SESSION_RESUMED, + (avs_net_socket_opt_value_t) { + .flag = true + }); + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + DM_TEST_FINISH; +} + +AVS_UNIT_TEST(schedule_register, nonexistent) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + AVS_UNIT_ASSERT_FAILED(anjay_schedule_register(anjay, 42)); + DM_TEST_FINISH; +} + +AVS_UNIT_TEST(schedule_register, active_server) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + AVS_UNIT_ASSERT_SUCCESS(anjay_schedule_register(anjay, 1)); + expect_refresh_server(anjay); + const coap_test_msg_t *register_request = + COAP_MSG(CON, POST, ID_TOKEN_RAW(0x0001, nth_token(1)), + CONTENT_FORMAT(LINK_FORMAT), PATH("rd"), + QUERY("lwm2m=1.0", "ep=urn:dev:os:anjay-test", "lt=86400"), + PAYLOAD("")); + avs_unit_mocksock_expect_output(mocksocks[0], register_request->content, + register_request->length); + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + DM_TEST_FINISH; +} + +AVS_UNIT_TEST(schedule_register, reconnect_and_register) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + avs_unit_mocksock_expect_shutdown(mocksocks[0]); + AVS_UNIT_ASSERT_SUCCESS(anjay_server_schedule_reconnect(anjay, 1)); + AVS_UNIT_ASSERT_SUCCESS(anjay_schedule_register(anjay, 1)); + expect_refresh_server(anjay); + avs_unit_mocksock_expect_connect(mocksocks[0], "", ""); + avs_unit_mocksock_expect_local_port(mocksocks[0], "5683"); + avs_unit_mocksock_expect_get_opt(mocksocks[0], + AVS_NET_SOCKET_OPT_SESSION_RESUMED, + (avs_net_socket_opt_value_t) { + .flag = true + }); + const coap_test_msg_t *register_request = + COAP_MSG(CON, POST, ID_TOKEN_RAW(0x0001, nth_token(1)), + CONTENT_FORMAT(LINK_FORMAT), PATH("rd"), + QUERY("lwm2m=1.0", "ep=urn:dev:os:anjay-test", "lt=86400"), + PAYLOAD("")); + avs_unit_mocksock_expect_output(mocksocks[0], register_request->content, + register_request->length); + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + DM_TEST_FINISH; +} + +AVS_UNIT_TEST(schedule_register, register_and_reconnect) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + AVS_UNIT_ASSERT_SUCCESS(anjay_schedule_register(anjay, 1)); + avs_unit_mocksock_expect_shutdown(mocksocks[0]); + AVS_UNIT_ASSERT_SUCCESS(anjay_server_schedule_reconnect(anjay, 1)); + expect_refresh_server(anjay); + avs_unit_mocksock_expect_connect(mocksocks[0], "", ""); + avs_unit_mocksock_expect_local_port(mocksocks[0], "5683"); + avs_unit_mocksock_expect_get_opt(mocksocks[0], + AVS_NET_SOCKET_OPT_SESSION_RESUMED, + (avs_net_socket_opt_value_t) { + .flag = true + }); + const coap_test_msg_t *register_request = + COAP_MSG(CON, POST, ID_TOKEN_RAW(0x0001, nth_token(1)), + CONTENT_FORMAT(LINK_FORMAT), PATH("rd"), + QUERY("lwm2m=1.0", "ep=urn:dev:os:anjay-test", "lt=86400"), + PAYLOAD("")); + avs_unit_mocksock_expect_output(mocksocks[0], register_request->content, + register_request->length); + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + DM_TEST_FINISH; +} + +static avs_net_socket_t **recreate_mocksock_once_ptr; + +static avs_error_t +recreate_udp_mocksock_once(avs_net_socket_t **socket, + const avs_net_socket_configuration_t *config) { + (void) config; + AVS_UNIT_ASSERT_NULL(*socket); + *recreate_mocksock_once_ptr = _anjay_test_dm_create_socket(false); + *socket = *recreate_mocksock_once_ptr; + AVS_UNIT_MOCK(avs_net_udp_socket_create) = NULL; + recreate_mocksock_once_ptr = NULL; + avs_unit_mocksock_expect_connect(*socket, "127.0.0.1", "5683"); + avs_unit_mocksock_expect_local_port(*socket, "5683"); + avs_unit_mocksock_expect_get_opt(*socket, + AVS_NET_SOCKET_OPT_SESSION_RESUMED, + (avs_net_socket_opt_value_t) { + .flag = true + }); + return AVS_OK; +} + +AVS_UNIT_TEST(schedule_register, + inactive_server_doesnt_automatically_reregister) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + make_server_inactive(anjay, 1, mocksocks[0]); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + AVS_UNIT_ASSERT_SUCCESS(anjay_enable_server(anjay, 1)); + expect_refresh_server(anjay, + .with_reconnect = RECONNECT_FULL); + recreate_mocksock_once_ptr = &mocksocks[0]; + AVS_UNIT_MOCK(avs_net_udp_socket_create) = recreate_udp_mocksock_once; + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_EQUAL(AVS_UNIT_MOCK_INVOCATIONS(avs_net_udp_socket_create), + 1); + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + DM_TEST_FINISH; +} + +static avs_error_t recreate_udp_mocksock_once_and_expect_register( + avs_net_socket_t **socket, + const avs_net_socket_configuration_t *config) { + AVS_UNIT_ASSERT_SUCCESS(recreate_udp_mocksock_once(socket, config)); + const coap_test_msg_t *register_request = + COAP_MSG(CON, POST, ID_TOKEN_RAW(0x0000, nth_token(1)), + CONTENT_FORMAT(LINK_FORMAT), PATH("rd"), + QUERY("lwm2m=1.0", "ep=urn:dev:os:anjay-test", "lt=86400"), + PAYLOAD("")); + avs_unit_mocksock_expect_output(*socket, register_request->content, + register_request->length); + return AVS_OK; +} + +AVS_UNIT_TEST(schedule_register, inactive_server) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + make_server_inactive(anjay, 1, mocksocks[0]); + AVS_UNIT_ASSERT_SUCCESS(anjay_schedule_register(anjay, 1)); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + AVS_UNIT_ASSERT_SUCCESS(anjay_enable_server(anjay, 1)); + expect_refresh_server(anjay, + .with_reconnect = RECONNECT_FULL); + recreate_mocksock_once_ptr = &mocksocks[0]; + AVS_UNIT_MOCK(avs_net_udp_socket_create) = + recreate_udp_mocksock_once_and_expect_register; + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + AVS_UNIT_ASSERT_EQUAL(AVS_UNIT_MOCK_INVOCATIONS(avs_net_udp_socket_create), + 1); + DM_TEST_FINISH; +} + +AVS_UNIT_TEST(schedule_register, inactive_server_enable_first) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1); + make_server_inactive(anjay, 1, mocksocks[0]); + AVS_UNIT_ASSERT_SUCCESS(anjay_enable_server(anjay, 1)); + AVS_UNIT_ASSERT_SUCCESS(anjay_schedule_register(anjay, 1)); + expect_refresh_server(anjay, + .with_reconnect = RECONNECT_FULL); + recreate_mocksock_once_ptr = &mocksocks[0]; + AVS_UNIT_MOCK(avs_net_udp_socket_create) = + recreate_udp_mocksock_once_and_expect_register; + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + AVS_UNIT_ASSERT_EQUAL(AVS_UNIT_MOCK_INVOCATIONS(avs_net_udp_socket_create), + 1); + DM_TEST_FINISH; +} + +AVS_UNIT_TEST(schedule_register, all_servers) { + DM_REGISTER_TEST_INIT_WITH_SSIDS(1, 2); + AVS_UNIT_ASSERT_SUCCESS(anjay_schedule_register(anjay, ANJAY_SSID_ANY)); + expect_refresh_server(anjay, + .ssid = 1, + .server_count = 2); + expect_refresh_server(anjay, + .ssid = 2, + .server_count = 2); + const coap_test_msg_t *register_request1 = + COAP_MSG(CON, POST, ID_TOKEN_RAW(0x0001, nth_token(2)), + CONTENT_FORMAT(LINK_FORMAT), PATH("rd"), + QUERY("lwm2m=1.0", "ep=urn:dev:os:anjay-test", "lt=86400"), + PAYLOAD(",")); + avs_unit_mocksock_expect_output(mocksocks[0], register_request1->content, + register_request1->length); + const coap_test_msg_t *register_request2 = + COAP_MSG(CON, POST, ID_TOKEN_RAW(0x0001, nth_token(3)), + CONTENT_FORMAT(LINK_FORMAT), PATH("rd"), + QUERY("lwm2m=1.0", "ep=urn:dev:os:anjay-test", "lt=86400"), + PAYLOAD(",")); + avs_unit_mocksock_expect_output(mocksocks[1], register_request2->content, + register_request2->length); + anjay_sched_run(anjay); + AVS_UNIT_ASSERT_TRUE(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) + >= 1000); + DM_TEST_FINISH; +} diff --git a/tests/core/bootstrap.c b/tests/core/bootstrap.c index 1f12dd8c6..0926f7401 100644 --- a/tests/core/bootstrap.c +++ b/tests/core/bootstrap.c @@ -293,15 +293,21 @@ AVS_UNIT_TEST(bootstrap_write, resource_with_mismatched_tlv_rid) { } AVS_UNIT_TEST(bootstrap_write, resource_error_with_create) { - DM_TEST_INIT_WITH_SSIDS(ANJAY_SSID_BOOTSTRAP); - DM_TEST_REQUEST(mocksocks[0], CON, PUT, ID(0xFA3E), PATH("42", "514", "7"), + const anjay_dm_object_def_t *const *obj_defs[] = { &OBJ_WITH_TRANSACTION, + &FAKE_SECURITY, + &FAKE_SERVER }; + anjay_ssid_t ssids[] = { ANJAY_SSID_BOOTSTRAP }; + DM_TEST_INIT_GENERIC(obj_defs, ssids, DM_TEST_CONFIGURATION()); + + DM_TEST_REQUEST(mocksocks[0], CON, PUT, ID(0xFA3E), PATH("69", "514", "7"), CONTENT_FORMAT(PLAINTEXT), PAYLOAD("Hello")); _anjay_mock_dm_expect_list_instances( - anjay, &OBJ, 0, + anjay, &OBJ_WITH_TRANSACTION, 0, (const anjay_iid_t[]) { 14, 42, 69, ANJAY_ID_INVALID }); - _anjay_mock_dm_expect_instance_create(anjay, &OBJ, 514, 0); + _anjay_mock_dm_expect_transaction_begin(anjay, &OBJ_WITH_TRANSACTION, 0); + _anjay_mock_dm_expect_instance_create(anjay, &OBJ_WITH_TRANSACTION, 514, 0); _anjay_mock_dm_expect_list_resources( - anjay, &OBJ, 514, 0, + anjay, &OBJ_WITH_TRANSACTION, 514, 0, (const anjay_mock_dm_res_entry_t[]) { { 0, ANJAY_DM_RES_RW, ANJAY_DM_RES_ABSENT }, { 1, ANJAY_DM_RES_RW, ANJAY_DM_RES_ABSENT }, @@ -311,11 +317,11 @@ AVS_UNIT_TEST(bootstrap_write, resource_error_with_create) { { 5, ANJAY_DM_RES_RW, ANJAY_DM_RES_ABSENT }, { 6, ANJAY_DM_RES_RW, ANJAY_DM_RES_ABSENT }, ANJAY_MOCK_DM_RES_END }); - // TODO: should expect transaction_rollback here DM_TEST_EXPECT_RESPONSE(mocksocks[0], ACK, NOT_FOUND, ID(0xFA3E), NO_PAYLOAD); expect_has_buffered_data_check(mocksocks[0], false); AVS_UNIT_ASSERT_SUCCESS(anjay_serve(anjay, mocksocks[0])); + _anjay_mock_dm_expect_transaction_rollback(anjay, &OBJ_WITH_TRANSACTION, 0); DM_TEST_FINISH; } diff --git a/tests/core/observe/observe.c b/tests/core/observe/observe.c index b1a0f627d..802c3d7f8 100644 --- a/tests/core/observe/observe.c +++ b/tests/core/observe/observe.c @@ -895,7 +895,6 @@ AVS_UNIT_TEST(notify, min_period) { expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); anjay_sched_run(anjay); - anjay_sched_run(anjay); ////// PMIN REACHED ////// _anjay_mock_clock_advance(avs_time_duration_from_scalar(5, AVS_TIME_S)); @@ -914,7 +913,6 @@ AVS_UNIT_TEST(notify, min_period) { _anjay_mock_clock_advance(avs_time_duration_from_scalar(10, AVS_TIME_S)); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_STRING(0, "Hi!")); anjay_sched_run(anjay); @@ -1013,8 +1011,6 @@ AVS_UNIT_TEST(notify, epmin_less_than_pmax) { _anjay_mock_clock_advance(avs_time_duration_from_scalar(10, AVS_TIME_S)); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); - // We don't send notification yet because epmin didn't expire ////// EPMIN EXPIRED BUT RESOURCE VALUE REMAINS THE SAME ////// _anjay_mock_clock_advance(avs_time_duration_from_scalar(10, AVS_TIME_S)); @@ -1027,7 +1023,6 @@ AVS_UNIT_TEST(notify, epmin_less_than_pmax) { _anjay_mock_clock_advance(avs_time_duration_from_scalar(10, AVS_TIME_S)); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); ////// EPMIN EXPIRED AND RESOURCE VALUE CHANGED ////// _anjay_mock_clock_advance(avs_time_duration_from_scalar(10, AVS_TIME_S)); @@ -1075,7 +1070,6 @@ AVS_UNIT_TEST(notify, confirmable) { _anjay_mock_clock_advance(avs_time_duration_from_scalar(5, AVS_TIME_S)); DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 42)); const coap_test_msg_t *notify_response = @@ -1126,7 +1120,6 @@ AVS_UNIT_TEST(notify, extremes) { ////// LESS ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_FLOAT(0, 42.43)); const coap_test_msg_t *notify_response = @@ -1147,7 +1140,6 @@ AVS_UNIT_TEST(notify, extremes) { ////// EVEN LESS ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_FLOAT(0, 14.7)); anjay_sched_run(anjay); @@ -1163,7 +1155,6 @@ AVS_UNIT_TEST(notify, extremes) { ////// IN BETWEEN ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 695)); const coap_test_msg_t *notify_response2 = @@ -1184,7 +1175,6 @@ AVS_UNIT_TEST(notify, extremes) { ////// EQUAL - STILL NOT CROSSING ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 69)); anjay_sched_run(anjay); @@ -1200,7 +1190,6 @@ AVS_UNIT_TEST(notify, extremes) { ////// GREATER ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 1024)); const coap_test_msg_t *notify_response3 = @@ -1221,7 +1210,6 @@ AVS_UNIT_TEST(notify, extremes) { ////// STILL GREATER ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 999)); anjay_sched_run(anjay); @@ -1237,7 +1225,6 @@ AVS_UNIT_TEST(notify, extremes) { ////// LESS AGAIN ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_FLOAT(0, -69.75)); const coap_test_msg_t *notify_response4 = @@ -1288,7 +1275,6 @@ AVS_UNIT_TEST(notify, greater_only) { ////// STILL GREATER ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 9001)); anjay_sched_run(anjay); @@ -1304,7 +1290,6 @@ AVS_UNIT_TEST(notify, greater_only) { ////// LESS ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 42)); const coap_test_msg_t *notify_response = @@ -1325,7 +1310,6 @@ AVS_UNIT_TEST(notify, greater_only) { ////// GREATER AGAIN ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 77)); const coap_test_msg_t *notify_response2 = @@ -1376,7 +1360,6 @@ AVS_UNIT_TEST(notify, less_only) { ////// LESS ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 42)); const coap_test_msg_t *notify_response = @@ -1397,7 +1380,6 @@ AVS_UNIT_TEST(notify, less_only) { ////// STILL LESS ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 514)); anjay_sched_run(anjay); @@ -1413,7 +1395,6 @@ AVS_UNIT_TEST(notify, less_only) { ////// GREATER ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 9001)); const coap_test_msg_t *notify_response2 = @@ -1434,7 +1415,6 @@ AVS_UNIT_TEST(notify, less_only) { ////// LESS AGAIN ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 69)); const coap_test_msg_t *notify_response3 = @@ -1484,7 +1464,6 @@ AVS_UNIT_TEST(notify, step) { ////// TOO LITTLE INCREASE ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_FLOAT(0, 523.5)); anjay_sched_run(anjay); @@ -1500,7 +1479,6 @@ AVS_UNIT_TEST(notify, step) { ////// INCREASE BY EXACTLY stp ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 524)); const coap_test_msg_t *notify_response0 = @@ -1521,7 +1499,6 @@ AVS_UNIT_TEST(notify, step) { ////// INCREASE BY OVER stp ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_FLOAT(0, 540.048)); const coap_test_msg_t *notify_response1 = @@ -1542,7 +1519,6 @@ AVS_UNIT_TEST(notify, step) { ////// NON-NUMERIC VALUE ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_STRING(0, "trololo")); const coap_test_msg_t *notify_response2 = @@ -1563,7 +1539,6 @@ AVS_UNIT_TEST(notify, step) { ////// BACK TO NUMBERS ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 42)); const coap_test_msg_t *notify_response3 = @@ -1584,7 +1559,6 @@ AVS_UNIT_TEST(notify, step) { ////// TOO LITTLE DECREASE ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_FLOAT(0, 32.001)); anjay_sched_run(anjay); @@ -1600,7 +1574,6 @@ AVS_UNIT_TEST(notify, step) { ////// DECREASE BY EXACTLY stp ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 31)); const coap_test_msg_t *notify_response4 = @@ -1621,7 +1594,6 @@ AVS_UNIT_TEST(notify, step) { ////// DECREASE BY MORE THAN stp ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 20)); const coap_test_msg_t *notify_response5 = @@ -1642,7 +1614,6 @@ AVS_UNIT_TEST(notify, step) { ////// INCREASE BY EXACTLY stp ////// expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); expect_read_res_attrs(anjay, &OBJ, 14, 69, 4, &ATTRS); expect_read_res(anjay, &OBJ, 69, 4, ANJAY_MOCK_DM_INT(0, 30)); const coap_test_msg_t *notify_response6 = @@ -1806,7 +1777,6 @@ AVS_UNIT_TEST(notify, storing_when_inactive) { DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); DM_TEST_EXPECT_READ_NULL_ATTRS(34, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -1857,7 +1827,6 @@ AVS_UNIT_TEST(notify, storing_when_inactive) { DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); DM_TEST_EXPECT_READ_NULL_ATTRS(34, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -1922,7 +1891,6 @@ AVS_UNIT_TEST(notify, storing_when_inactive) { CONTENT_FORMAT(PLAINTEXT), PAYLOAD("Rin")); avs_unit_mocksock_expect_output(mocksocks[0], notify_response3->content, notify_response3->length); - anjay_sched_run(anjay); const coap_test_msg_t *notify_response4 = COAP_MSG(NON, CONTENT, ID_TOKEN(0x0001, "SuccsTkn"), OBSERVE(2), @@ -1960,7 +1928,6 @@ AVS_UNIT_TEST(notify, no_storing_when_disabled) { DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); DM_TEST_EXPECT_READ_NULL_ATTRS(34, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -1993,7 +1960,6 @@ AVS_UNIT_TEST(notify, no_storing_when_disabled) { DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); DM_TEST_EXPECT_READ_NULL_ATTRS(34, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -2048,7 +2014,6 @@ AVS_UNIT_TEST(notify, storing_on_send_error) { // first notification DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -2076,7 +2041,6 @@ AVS_UNIT_TEST(notify, storing_on_send_error) { // second notification DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -2128,7 +2092,6 @@ AVS_UNIT_TEST(notify, storing_on_send_error) { OBSERVE(3), CONTENT_FORMAT(PLAINTEXT), PAYLOAD("Meiko")); avs_unit_mocksock_expect_output(mocksocks[0], notify_response->content, notify_response->length); - anjay_sched_run(anjay); const coap_test_msg_t *notify_response2 = COAP_MSG(NON, CONTENT, ID_TOKEN(MSG_ID_BASE + 3, "SuccsTkn"), @@ -2147,7 +2110,6 @@ AVS_UNIT_TEST(notify, no_storing_on_send_error) { // first notification DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -2175,7 +2137,6 @@ AVS_UNIT_TEST(notify, no_storing_on_send_error) { // second notification DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -2235,7 +2196,6 @@ static void storing_of_errors_test_impl(bool storing_resource_value) { // first notification DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -2250,7 +2210,6 @@ static void storing_of_errors_test_impl(bool storing_resource_value) { // second notification - should not actually do anything DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -2291,7 +2250,6 @@ AVS_UNIT_TEST(notify, send_error) { // first notification DM_TEST_EXPECT_READ_NULL_ATTRS(14, 69, 4); AVS_UNIT_ASSERT_SUCCESS(anjay_notify_changed(anjay, 42, 69, 4)); - anjay_sched_run(anjay); _anjay_mock_clock_advance(avs_time_duration_from_scalar(1, AVS_TIME_S)); @@ -2313,7 +2271,6 @@ AVS_UNIT_TEST(notify, send_error) { _anjay_mock_dm_expect_resource_read(anjay, &OBJ, 69, 4, ANJAY_ID_INVALID, 0, ANJAY_MOCK_DM_STRING(0, "Meiko")); avs_unit_mocksock_output_fail(mocksocks[0], avs_errno(AVS_ECONNRESET)); - anjay_sched_run(anjay); _anjay_mocksock_expect_stats_zero(mocksocks[0]); #if defined(ANJAY_WITH_LWM2M11) && defined(ANJAY_WITH_BOOTSTRAP) diff --git a/tests/core/socket_mock.c b/tests/core/socket_mock.c new file mode 100644 index 000000000..7fe48a31a --- /dev/null +++ b/tests/core/socket_mock.c @@ -0,0 +1,17 @@ +/* + * Copyright 2017-2023 AVSystem + * AVSystem Anjay LwM2M SDK + * All rights reserved. + * + * Licensed under the AVSystem-5-clause License. + * See the attached LICENSE file for details. + */ + +#include + +#include "socket_mock.h" + +AVS_UNIT_MOCK_DEFINE(avs_net_tcp_socket_create) +AVS_UNIT_MOCK_DEFINE(avs_net_udp_socket_create) +AVS_UNIT_MOCK_DEFINE(avs_net_ssl_socket_create) +AVS_UNIT_MOCK_DEFINE(avs_net_dtls_socket_create) diff --git a/tests/core/downloader/downloader_mock.h b/tests/core/socket_mock.h similarity index 67% rename from tests/core/downloader/downloader_mock.h rename to tests/core/socket_mock.h index cce8f4216..178e7070f 100644 --- a/tests/core/downloader/downloader_mock.h +++ b/tests/core/socket_mock.h @@ -7,25 +7,26 @@ * See the attached LICENSE file for details. */ -#ifndef ANJAY_TEST_DOWNLOADER_MOCK_H -#define ANJAY_TEST_DOWNLOADER_MOCK_H +#ifndef SOCKET_MOCK_H +#define SOCKET_MOCK_H +#include #include -AVS_UNIT_MOCK_CREATE(avs_net_tcp_socket_create) +extern AVS_UNIT_MOCK_DECLARE(avs_net_tcp_socket_create); #define avs_net_tcp_socket_create(...) \ AVS_UNIT_MOCK_WRAPPER(avs_net_tcp_socket_create)(__VA_ARGS__) -AVS_UNIT_MOCK_CREATE(avs_net_udp_socket_create) +extern AVS_UNIT_MOCK_DECLARE(avs_net_udp_socket_create); #define avs_net_udp_socket_create(...) \ AVS_UNIT_MOCK_WRAPPER(avs_net_udp_socket_create)(__VA_ARGS__) -AVS_UNIT_MOCK_CREATE(avs_net_ssl_socket_create) +extern AVS_UNIT_MOCK_DECLARE(avs_net_ssl_socket_create); #define avs_net_ssl_socket_create(...) \ AVS_UNIT_MOCK_WRAPPER(avs_net_ssl_socket_create)(__VA_ARGS__) -AVS_UNIT_MOCK_CREATE(avs_net_dtls_socket_create) +extern AVS_UNIT_MOCK_DECLARE(avs_net_dtls_socket_create); #define avs_net_dtls_socket_create(...) \ AVS_UNIT_MOCK_WRAPPER(avs_net_dtls_socket_create)(__VA_ARGS__) -#endif /* ANJAY_TEST_DOWNLOADER_MOCK_H */ +#endif /* SOCKET_MOCK_H */ diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt index ed9087269..e80b16f92 100644 --- a/tests/integration/CMakeLists.txt +++ b/tests/integration/CMakeLists.txt @@ -20,30 +20,41 @@ else() endmacro() endif() -set(DEFAULT_TEST_SUITE "default") -set(INTEGRATION_TEST_SUITE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/suites") set(INTEGRATION_TEST_PREFIX "test_demo_") set(PYMBEDTLS_MODULE_DIR ${ANJAY_BUILD_OUTPUT_DIR}/pymbedtls) -file(GLOB_RECURSE DEMO_TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/suites "${INTEGRATION_TEST_SUITE_ROOT}/*.py") -list(SORT DEMO_TESTS) -foreach(DEMO_TEST ${DEMO_TESTS}) - string(REGEX REPLACE "^${CMAKE_CURRENT_SOURCE_DIR}" "" DEMO_TEST "${DEMO_TEST}") - string(REGEX REPLACE "\\.py" "" DEMO_TEST "${DEMO_TEST}") - string(REGEX REPLACE "/" "." DEMO_TEST "${DEMO_TEST}") +if(TEST_KEEP_SUCCESS_LOGS) + set(ADDITIONAL_RUNTEST_ARGS "--keep-success-logs") +else() + set(ADDITIONAL_RUNTEST_ARGS) +endif() - if (NOT DEMO_TEST MATCHES "__init__$") - add_test(${INTEGRATION_TEST_PREFIX}${DEMO_TEST} - ${CMAKE_CURRENT_SOURCE_DIR}/runtest.py "^${DEMO_TEST}\\\$" - --client=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/demo) +execute_process(COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/runtest.py" "-l" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE RUNTEST_LIST_RESULT + OUTPUT_VARIABLE RUNTEST_LIST_OUTPUT) + +if(NOT RUNTEST_LIST_RESULT EQUAL 0) + message(FATAL_ERROR "runtest.py -l failed") +endif() + +string(REPLACE "\n" ";" RUNTEST_LIST_OUTPUT "${RUNTEST_LIST_OUTPUT}") +list(SORT RUNTEST_LIST_OUTPUT) +foreach(TEST_CASE_ENTRY IN LISTS RUNTEST_LIST_OUTPUT) + if(TEST_CASE_ENTRY MATCHES "^\\* .*$") + string(REGEX REPLACE "^\\* " "" TEST_CASE_ENTRY "${TEST_CASE_ENTRY}") + add_test(${INTEGRATION_TEST_PREFIX}${TEST_CASE_ENTRY} + ${CMAKE_CURRENT_SOURCE_DIR}/runtest.py "^${TEST_CASE_ENTRY}\\\$" + --client=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/demo + ${ADDITIONAL_RUNTEST_ARGS}) # Tell python tests where to look for pymbedtls*.so - set_property(TEST ${INTEGRATION_TEST_PREFIX}${DEMO_TEST} + set_property(TEST ${INTEGRATION_TEST_PREFIX}${TEST_CASE_ENTRY} APPEND PROPERTY ENVIRONMENT "PYTHONPATH=${PYMBEDTLS_MODULE_DIR}") - # Limit each test suite to 15 minutes - set_property(TEST ${INTEGRATION_TEST_PREFIX}${DEMO_TEST} PROPERTY TIMEOUT 900) + # The longest test is 5 minutes, so set the timeout to 6 minutes + set_property(TEST ${INTEGRATION_TEST_PREFIX}${TEST_CASE_ENTRY} PROPERTY TIMEOUT 360) - add_valgrind(${INTEGRATION_TEST_PREFIX}${DEMO_TEST}) + add_valgrind(${INTEGRATION_TEST_PREFIX}${TEST_CASE_ENTRY}) endif() endforeach() @@ -74,16 +85,14 @@ endif() unset(_MISSING_PYTHON_MSG) add_custom_target(pymbedtls COMMAND + env "MBEDTLS_ROOT_DIR=${MBEDTLS_ROOT_DIR}" python3 -m pip install --target ${PYMBEDTLS_MODULE_DIR} "${NSH_LWM2M_DIR}/pymbedtls") set(WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") configure_file("run_tests.sh.in" "run_tests.sh") -add_custom_target(integration_check_st - COMMAND ./run_tests.sh -s - DEPENDS demo pymbedtls) add_custom_target(integration_check COMMAND ./run_tests.sh - DEPENDS integration_check_st) + DEPENDS demo pymbedtls) add_custom_target(integration_check_hsm COMMAND ./run_tests.sh -h DEPENDS demo pymbedtls) diff --git a/tests/integration/framework/asserts.py b/tests/integration/framework/asserts.py index 26f8b07b1..a5c875a34 100644 --- a/tests/integration/framework/asserts.py +++ b/tests/integration/framework/asserts.py @@ -12,7 +12,7 @@ from urllib import response from .lwm2m.messages import * -from .test_utils import DEMO_ENDPOINT_NAME, ResponseFilter +from .test_utils import DEMO_ENDPOINT_NAME from framework.lwm2m.coap.transport import Transport @@ -97,6 +97,13 @@ def assertMsgEqual(self, expected, actual, msg=None): DEFAULT_REGISTER_ENDPOINT = '/rd/demo' + def assertTcpCsm(self, server=None): + serv = server or self.serv + pkt = serv.recv() + self.assertEqual(coap.Code.SIGNALING_CSM, pkt.code) + serv.send(coap.Packet(code=coap.Code.SIGNALING_CSM, token=b'')) + return pkt + @staticmethod def _expected_register_message(version, endpoint, lifetime, binding, lwm2m11_queue_mode): # Note: the specific order of Uri-Query options does not matter, but @@ -122,12 +129,12 @@ def assertDemoRegisters(self, location=DEFAULT_REGISTER_ENDPOINT, endpoint=DEMO_ENDPOINT_NAME, lifetime=None, - timeout_s=2, + timeout_s=-1, respond=True, binding=None, lwm2m11_queue_mode=False, reject=False, - response_filter: ResponseFilter = None): + response_filter=None): # passing a float instead of an integer results in a disaster # (serializes as e.g. lt=4.0 instead of lt=4), which makes the # assertion fail @@ -137,10 +144,7 @@ def assertDemoRegisters(self, serv = server or self.serv - if response_filter: - pkt = response_filter.filtered_recv(serv, timeout_s) - else: - pkt = serv.recv(timeout_s=timeout_s) + pkt = serv.recv(timeout_s=timeout_s, filter=response_filter) self.assertMsgEqual(self._expected_register_message( version, endpoint, lifetime, binding, lwm2m11_queue_mode), pkt) self.assertIsNotNone(pkt.content) @@ -161,9 +165,9 @@ def assertDemoUpdatesRegistration(self, binding: Optional[str] = None, sms_number: Optional[str] = None, content: bytes = b'', - timeout_s: float = 1, + timeout_s: float = -1, respond: bool = True, - response_filter: ResponseFilter = None): + response_filter=None): serv = server or self.serv query_args = (([('lt', lifetime)] if lifetime is not None else []) @@ -175,16 +179,13 @@ def assertDemoUpdatesRegistration(self, if query_string: path += '?' + query_string - if response_filter: - pkt = response_filter.filtered_recv(serv, timeout_s) - else: - pkt = serv.recv(timeout_s=timeout_s) + pkt = serv.recv(timeout_s=timeout_s, filter=response_filter) self.assertMsgEqual(Lwm2mUpdate(path, content=content), pkt) if respond: serv.send(Lwm2mChanged.matching(pkt)()) return pkt - def assertDemoDeregisters(self, server=None, path=DEFAULT_REGISTER_ENDPOINT, timeout_s=2, reset=True): + def assertDemoDeregisters(self, server=None, path=DEFAULT_REGISTER_ENDPOINT, timeout_s=-1, reset=True): serv = server or self.serv pkt = serv.recv(timeout_s=timeout_s) @@ -207,20 +208,11 @@ def assertDemoRequestsBootstrap(self, uri_path='', uri_query=None, respond_with_ self.bootstrap_server.send(Lwm2mErrorResponse.matching( pkt)(code=respond_with_error_code)) - def assertDemoReleases(self, server=None, timeout_s=1): - serv = server or self.serv - if serv.transport != Transport.TCP: - raise ValueError('Expected Release on non-TCP server') - - pkt = serv.recv(timeout_s=timeout_s) - self.assertMsgEqual(coap.Packet( - code=coap.Code.SIGNALING_RELEASE, token=ANY), pkt) - - def assertDtlsReconnect(self, server=None, timeout_s=1): + def assertDtlsReconnect(self, server=None, timeout_s=-1, deadline=None): serv = server or self.serv with self.assertRaises(RuntimeError) as raised: - serv.recv(timeout_s=timeout_s) + serv.recv(timeout_s=timeout_s, deadline=deadline) # -0x6780 == MBEDTLS_ERR_SSL_CLIENT_RECONNECT self.assertIn('0x6780', raised.exception.args[0]) diff --git a/tests/integration/framework/firmware_package.py b/tests/integration/framework/firmware_package.py index 4135029da..726814724 100644 --- a/tests/integration/framework/firmware_package.py +++ b/tests/integration/framework/firmware_package.py @@ -10,7 +10,7 @@ import binascii import enum import struct -from typing import Optional +from typing import List, Optional @enum.unique @@ -26,24 +26,51 @@ class FirmwareUpdateForcedError(enum.IntEnum): Defer = 8 +LINKED_SLOTS = 8 +IMG_VER_STRLEN_MAX = 24 # 255.255.65535.4294967295 + + def make_firmware_package(binary: bytes, magic: bytes = b'ANJAY_FW', crc: Optional[int] = None, force_error: FirmwareUpdateForcedError = FirmwareUpdateForcedError.NoError, - version: int = 1): + version: int = 1, + linked: list = [], + pkg_version=b'1.0'): assert len(magic) == 8 if crc is None: crc = binascii.crc32(binary) - meta = struct.pack('>8sHHI', magic, version, force_error, crc) + if version == 2: + assert len(linked) <= LINKED_SLOTS + assert len(pkg_version) <= IMG_VER_STRLEN_MAX + # Fill remaining linked slots with 0xFF or all slots if nothing provided. + # Demo understands 0xFF as no linked instance. + meta = struct.pack(f'>8sHHI{LINKED_SLOTS}sB{len(pkg_version)}s', magic, version, force_error, crc, + bytes(linked + [0xFF] * (LINKED_SLOTS - len(linked))), len(pkg_version), pkg_version) + else: + assert not linked, 'Linked instances provided with wrong version of package format' + meta = struct.pack('>8sHHI', magic, version, force_error, crc) + return meta + binary +def make_multiple_firmware_package(binary: List[bytes], + magic: bytes = b'MULTIPKG', + version: int = 3): + meta = struct.pack(f'>8sHH{"I" * len(binary)}', magic, version, len(binary), *[len(pkg) for pkg in binary]) + package = meta + for pkg in binary: + package += pkg + return package + + if __name__ == '__main__': import argparse - parser = argparse.ArgumentParser(description='Create firmware package from executable.') + parser = argparse.ArgumentParser(description='Create firmware package from executable. Use -v option to switch ' + 'between Firmware Update and Advanced Firmware Update packages.') parser.add_argument('-i', '--in-file', help='Path to the input executable. Default: stdin', default='/dev/stdin') @@ -52,7 +79,10 @@ def make_firmware_package(binary: bytes, default='/dev/stdout') parser.add_argument('-m', '--magic', type=str, - help='Set firmware magic (must be exactly 8 bytes in length). Default: "ANJAY_FW"', + help='Set firmware magic (must be exactly 8 bytes in length). Default: "ANJAY_FW". If Anjay ' + 'demo is used with Advanced Firmware Update, possible magics are: "AJAY_APP", "AJAY_TEE", ' + '"AJAYBOOT", "AJAYMODE", which corresponds respectively with instances 1, 2, 3, and 4 of ' + 'object /33629. ', default='ANJAY_FW') parser.add_argument('-c', '--crc', type=int, @@ -63,17 +93,26 @@ def make_firmware_package(binary: bytes, help=('Create a firmware that causes update failure. Possible values: ' + ', '.join(e.name for e in FirmwareUpdateForcedError.__members__.values())), default='NoError') + parser.add_argument('-l', '--linked-instances', + type=int, + nargs='+', + help='Add instances that are linked to package. Possible usage: -l 1 2 3, means that instances ' + f'/33629/1, /33629/2 and /33629/3 are linked to package. Max amount of instances is {LINKED_SLOTS}.', + default=0) parser.add_argument('-v', '--version', type=int, - help='Set firmware package version.', + help='Set version of firmware package format. Two formats are supported. -v 1 corresponds to standard ' + 'Firmware Update, -v 2 corresponds to Advanced Firmware Update. Setting this argument to any ' + 'other value leds to use version 1, which is default value.', default=1) args = parser.parse_args() args.force_error = FirmwareUpdateForcedError.__members__[args.force_error] - + linked = args.linked_instances if type(args.linked_instances) == list else [] with open(args.in_file, 'rb') as in_file, open(args.out_file, 'wb') as out_file: out_file.write(make_firmware_package(in_file.read(), magic=args.magic.encode('ascii'), crc=args.crc, force_error=args.force_error, - version=args.version)) + version=args.version, + linked=linked)) diff --git a/tests/integration/framework/lwm2m/coap/server.py b/tests/integration/framework/lwm2m/coap/server.py index 3ec28e685..4cbba76ca 100644 --- a/tests/integration/framework/lwm2m/coap/server.py +++ b/tests/integration/framework/lwm2m/coap/server.py @@ -8,15 +8,13 @@ # See the attached LICENSE file for details. import contextlib +import enum import socket -import errno +import time from typing import Tuple, Optional from .packet import Packet from .transport import Transport -from .code import Code - -import enum class SecurityMode(enum.Enum): @@ -39,17 +37,27 @@ def __str__(self): return 'nosec' +def _calculate_deadline(timeout_s=-1, deadline=None): + if deadline is None and timeout_s is not None and timeout_s >= 0: + return time.time() + timeout_s + else: + return deadline + + @contextlib.contextmanager -def _override_timeout(sock, timeout_s=-1): - skip_override = sock is None or (timeout_s is not None and timeout_s < 0) +def _override_timeout(sock, *args, **kwargs): + deadline = _calculate_deadline(*args, **kwargs) + skip_override = sock is None or deadline is None if skip_override: yield else: orig_timeout_s = sock.gettimeout() - sock.settimeout(timeout_s) - yield - sock.settimeout(orig_timeout_s) + try: + sock.settimeout(max(deadline - time.time(), 0)) + yield + finally: + sock.settimeout(orig_timeout_s) def _disconnect_socket(old_sock, family): @@ -118,28 +126,28 @@ def __init__(self, listen_port=0, use_ipv6=False, reuse_port=False, transport=Tr self.transport = transport self.reuse_port = reuse_port self.accepted_connection = False + self._filtered_messages = [] self.reset(listen_port) - def listen(self, timeout_s=-1): + def listen(self, *args, **kwargs): + deadline = _calculate_deadline(*args, **kwargs) + if self.transport == Transport.UDP: assert self.get_remote_addr() is None - with _override_timeout(self.socket, timeout_s): - _, remote_addr_port = self.socket.recvfrom(1, socket.MSG_PEEK) + with _override_timeout(self.socket, deadline=deadline): + raw_pkt, remote_addr_port = self.socket.recvfrom(65536) self.connect_to_client(remote_addr_port) + self._filtered_messages.append(raw_pkt) elif self.transport == Transport.TCP: self.socket.listen() - with _override_timeout(self.socket, timeout_s): + with _override_timeout(self.socket, deadline=deadline): client_socket, _ = self.socket.accept() if self.server_socket is not None: self.server_socket.close() self.server_socket = self.socket self.socket = client_socket - pkt = self.recv() - - assert pkt.code == Code.SIGNALING_CSM - self.send(Packet(code=Code.SIGNALING_CSM, token=b'')) else: raise ValueError("Invalid transport: %r" % (self.transport,)) @@ -189,6 +197,7 @@ def _fake_unclose(self) -> None: self._raw_udp_socket, self.family) def _flush_recv_queue(self) -> None: + self._filtered_messages.clear() with _override_timeout(self._raw_udp_socket, 0): try: while True: @@ -212,6 +221,7 @@ def close(self) -> None: if self.socket: self.socket.close() self.socket = None + self._filtered_messages.clear() def reset(self, listen_port=None) -> None: if listen_port is None: @@ -221,30 +231,64 @@ def reset(self, listen_port=None) -> None: if self.server_socket is not None: self.socket = self.server_socket else: - self.socket = socket.socket(self.family, socket.SOCK_STREAM if self.transport == Transport.TCP else socket.SOCK_DGRAM) + self.socket = socket.socket(self.family, + socket.SOCK_STREAM if self.transport == Transport.TCP else socket.SOCK_DGRAM) self.socket.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEPORT, 1 if self.reuse_port else 0) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.setsockopt( + socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 if self.reuse_port else 0) self.socket.bind(('', listen_port)) self.accepted_connection = False def send(self, coap_packet: Packet) -> None: self.socket.send(coap_packet.serialize(transport=self.transport)) - def recv_raw(self, timeout_s: float = -1): + def recv_raw(self, timeout_s=-1, deadline=None, peek=False): + deadline = _calculate_deadline(timeout_s, deadline) + # NOTE: get_remote_addr() can sometimes return None, if someone # decided to "unconnect" the socket from a certain client. It is # only done for testing connection_id. if not self.get_remote_addr() and not self.accepted_connection: - self.listen(timeout_s=timeout_s) + self.listen(deadline=deadline) self.accepted_connection = True - with _override_timeout(self.socket, timeout_s): - return self.socket.recv(65536) + if len(self._filtered_messages) > 0: + if peek: + return self._filtered_messages[0] + else: + return self._filtered_messages.pop(0) + + with _override_timeout(self.socket, deadline=deadline): + raw_pkt = self.socket.recv(65536) + + if peek: + self._filtered_messages.append(raw_pkt) + + return raw_pkt - def recv(self, timeout_s: float = -1) -> Packet: - return Packet.parse(self.recv_raw(timeout_s), transport=self.transport) + def recv(self, timeout_s: float = -1, deadline=None, filter=None, peek=False) -> Packet: + deadline = _calculate_deadline(timeout_s, deadline) + + if filter is None: + filter = lambda _: True + + filtered_messages = [] + while True: + try: + raw_pkt = self.recv_raw(deadline=deadline) + except BlockingIOError: + raise socket.timeout('timed out') + pkt = Packet.parse(raw_pkt, transport=self.transport) + filter_result = filter(pkt) + if peek or not filter_result: + filtered_messages.append(raw_pkt) + if filter_result: + break + + self._filtered_messages += filtered_messages + return pkt def set_timeout(self, timeout_s: float) -> None: self.socket_timeout = timeout_s @@ -327,14 +371,16 @@ def reset(self, listen_port=None) -> None: if not isinstance(self.socket, ServerSocket): self.socket = ServerSocket(self._pymbedtls_context, self.socket) - def listen(self, timeout_s: float = -1) -> None: + def listen(self, *args, **kwargs) -> None: + deadline = _calculate_deadline(*args, **kwargs) + from pymbedtls import ServerSocket assert isinstance(self.socket, ServerSocket) if self.transport == Transport.TCP: self.socket.py_socket.listen() - with _override_timeout(self.socket, timeout_s): + with _override_timeout(self.socket, deadline=deadline): client_socket = self.socket.accept() if self.socket_timeout is not None: client_socket.settimeout(self.socket_timeout) @@ -347,10 +393,6 @@ def listen(self, timeout_s: float = -1) -> None: self.server_socket.close() self.server_socket = self.socket self.socket = client_socket - - pkt = self.recv() - assert pkt.code == Code.SIGNALING_CSM - self.send(Packet(code=Code.SIGNALING_CSM, token=b'')) else: raise ValueError("Invalid transport: %r" % (self.transport,)) diff --git a/tests/integration/framework/lwm2m/server.py b/tests/integration/framework/lwm2m/server.py index c40676a36..857711d60 100644 --- a/tests/integration/framework/lwm2m/server.py +++ b/tests/integration/framework/lwm2m/server.py @@ -14,7 +14,7 @@ class Lwm2mServer: def __init__(self, coap_server=None): super().__setattr__('_coap_server', coap_server or coap.Server()) - self.set_timeout(timeout_s=5) + self.set_timeout(timeout_s=15) def send(self, pkt: coap.Packet): if not isinstance(pkt, coap.Packet): @@ -22,8 +22,12 @@ def send(self, pkt: coap.Packet): 'valid syntax: Lwm2mSomething.matching(pkt)()') % (type(pkt),)) self._coap_server.send(pkt.fill_placeholders()) - def recv(self, timeout_s=-1): - pkt = self._coap_server.recv(timeout_s=timeout_s) + def recv(self, timeout_s: float = -1, deadline=None, filter=None, peek=False): + lwm2m_filter = None + if filter is not None: + lwm2m_filter = lambda pkt: filter(get_lwm2m_msg(pkt)) + pkt = self._coap_server.recv(timeout_s=timeout_s, deadline=deadline, filter=lwm2m_filter, + peek=peek) return get_lwm2m_msg(pkt) def __getattr__(self, name): diff --git a/tests/integration/framework/lwm2m/tlv.py b/tests/integration/framework/lwm2m/tlv.py index f8516b1be..217de799f 100644 --- a/tests/integration/framework/lwm2m/tlv.py +++ b/tests/integration/framework/lwm2m/tlv.py @@ -10,6 +10,7 @@ import struct import typing from textwrap import indent +from ..test_utils import Objlink class TLVType: @@ -82,6 +83,10 @@ def encode_double(data): def encode_float(data): return struct.pack('>f', float(data)) + @staticmethod + def encode_objlink(data: Objlink): + return struct.pack('>HH', data.ObjID, data.ObjInstID) + @staticmethod def make_instance(instance_id: int, content: typing.Iterable['TLV'] = None): @@ -93,7 +98,7 @@ def make_instance(instance_id: int, return TLV(TLVType.INSTANCE, instance_id, content or []) @staticmethod - def _encode_resource_value(content: int or float or str or bytes): + def _encode_resource_value(content: int or float or str or bytes or Objlink): if isinstance(content, int): content = TLV.encode_int(content) elif isinstance(content, float): @@ -104,6 +109,8 @@ def _encode_resource_value(content: int or float or str or bytes): content = TLV.encode_double(content) elif isinstance(content, str): content = content.encode('ascii') + elif isinstance(content, Objlink): + content = TLV.encode_objlink(content) if not isinstance(content, bytes): raise ValueError('Unsupported resource value type: ' + type(content).__name__) @@ -236,6 +243,7 @@ def _get_resource_value(self): if len(self.value) == 4: value += ', float: %f' % struct.unpack('>f', self.value)[0] + value += ', objlink: %d:%d' % struct.unpack('>HH', self.value) elif len(self.value) == 8: value += ', double: %f' % struct.unpack('>d', self.value)[0] @@ -272,9 +280,11 @@ def __eq__(self, other): def full_description(self): if self.tlv_type == TLVType.INSTANCE: return ('instance %d (%d resources)\n%s' - % (self.identifier, len(self.value), indent('\n'.join(x.full_description() for x in self.value), ' '))) + % (self.identifier, len(self.value), + indent('\n'.join(x.full_description() for x in self.value), ' '))) elif self.tlv_type == TLVType.MULTIPLE_RESOURCE: return ('multiple resource %d (%d instances)\n%s' - % (self.identifier, len(self.value), indent('\n'.join(x.full_description() for x in self.value), ' '))) + % (self.identifier, len(self.value), + indent('\n'.join(x.full_description() for x in self.value), ' '))) else: return str(self) diff --git a/tests/integration/framework/nsh-lwm2m/pymbedtls/src/socket.cpp b/tests/integration/framework/nsh-lwm2m/pymbedtls/src/socket.cpp index d5a2ed88e..f726c1e89 100644 --- a/tests/integration/framework/nsh-lwm2m/pymbedtls/src/socket.cpp +++ b/tests/integration/framework/nsh-lwm2m/pymbedtls/src/socket.cpp @@ -340,8 +340,12 @@ Socket::~Socket() { void Socket::perform_handshake(py::tuple host_port, py::object handshake_timeouts_s_, bool py_connect) { - client_host_and_port_ = host_port_to_std_tuple(host_port); - last_recv_host_and_port_ = host_port_to_std_tuple(host_port); + if (py_connect) { + call_method(py_socket_, "connect", host_port); + } + + last_recv_host_and_port_ = client_host_and_port_ = host_port_to_std_tuple( + call_method(py_socket_, "getpeername")); if (!handshake_timeouts_s_.is_none()) { auto handshake_timeouts_s = py::cast(handshake_timeouts_s_); @@ -359,16 +363,21 @@ void Socket::perform_handshake(py::tuple host_port, throw mbedtls_error("mbedtls_ssl_sssion_reset failed", result); } string address = std::get<0>(client_host_and_port_); - result = mbedtls_ssl_set_client_transport_id( - &mbedtls_context_, - reinterpret_cast(address.c_str()), - address.length()); - if (result) { - throw mbedtls_error("mbedtls_ssl_set_client_transport_id failed", - result); - } - if (py_connect) { - call_method(py_socket_, "connect", client_host_and_port_); + if (type_ == SocketType::Client) { + result = mbedtls_ssl_set_hostname(&mbedtls_context_, + address.c_str()); + if (result) { + throw mbedtls_error("mbedtls_ssl_set_hostname failed", result); + } + } else { + result = mbedtls_ssl_set_client_transport_id( + &mbedtls_context_, + reinterpret_cast(address.c_str()), + address.length()); + if (result) { + throw mbedtls_error( + "mbedtls_ssl_set_client_transport_id failed", result); + } } hs_result = do_handshake(); } while (hs_result == HandshakeResult::HelloVerifyRequired); diff --git a/tests/integration/framework/test_suite.py b/tests/integration/framework/test_suite.py index 087e989a1..f55cba977 100644 --- a/tests/integration/framework/test_suite.py +++ b/tests/integration/framework/test_suite.py @@ -8,6 +8,7 @@ # See the attached LICENSE file for details. import inspect +import io import logging import os import re @@ -117,19 +118,19 @@ def __exit__(self, _type, value, _traceback): class Lwm2mDmOperations(Lwm2mAsserts): - DEFAULT_OPERATION_TIMEOUT_S = 5 - - def _perform_action(self, server, request, expected_response, timeout_s=None, - response_filter: ResponseFilter = None): + def _perform_action(self, server, request, expected_response, timeout_s=None, deadline=None): server.send(request) - if timeout_s is None: - timeout_s = self.DEFAULT_OPERATION_TIMEOUT_S + if timeout_s is None and deadline is None: + timeout_s = -1 - if response_filter: - res = response_filter.filtered_recv(server, timeout_s) + if server.transport == Transport.UDP and request.msg_id is not None and request.msg_id is not ANY: + filter = lambda pkt: pkt.msg_id == request.msg_id + elif expected_response.token is not ANY: + filter = lambda pkt: pkt.token == expected_response.token else: - res = server.recv(timeout_s=timeout_s) + filter = lambda pkt: isinstance(pkt, type(expected_response)) + res = server.recv(timeout_s=timeout_s, deadline=deadline, filter=filter) self.assertMsgEqual(expected_response, res) return res @@ -288,12 +289,12 @@ def discover(self, server, oid=None, iid=None, rid=None, depth=None, expect_erro return self._perform_action(server, req, expected_res, **kwargs) def observe(self, server, oid=None, iid=None, rid=None, riid=None, expect_error_code=None, - response_filter=None, **kwargs): + **kwargs): req = Lwm2mObserve( Lwm2mDmOperations.make_path(oid, iid, rid, riid), **kwargs) expected_res = self._make_expected_res( req, Lwm2mContent, expect_error_code) - return self._perform_action(server, req, expected_res, response_filter=response_filter) + return self._perform_action(server, req, expected_res) def write_attributes(self, server, oid=None, iid=None, rid=None, query=[], expect_error_code=None, **kwargs): @@ -384,6 +385,8 @@ def make_demo_args(self, minimum_version, maximum_version, fw_updated_marker_path, + afu_marker_path=None, + afu_original_img_file_path=None, tls_version='TLSv1.2', ciphersuites=(0xC030, 0xC0A8, 0xC0AE)): """ @@ -410,6 +413,10 @@ def make_demo_args(self, '--security-mode', security_mode] if fw_updated_marker_path is not None: args += ['--fw-updated-marker-path', fw_updated_marker_path] + if afu_marker_path is not None: + args += ['--afu-marker-path', afu_marker_path] + if afu_original_img_file_path is not None: + args += ['--afu-original-img-file-path', afu_original_img_file_path] if tls_version is not None: args += ['--tls-version', tls_version] if ciphersuites is not None: @@ -434,6 +441,7 @@ def logs_path(self, log_type, log_root=None, **kwargs): return log_path def read_log_until_match(self, regex, timeout_s): + orig_offset = self.demo_process.log_file.tell() deadline = time.time() + timeout_s out = bytearray() while True: @@ -453,8 +461,15 @@ def read_log_until_match(self, regex, timeout_s): match = re.search(regex, out) if match: + # Move the file pointer to just after the match, in case we've read more + move_offset = match.end() - len(out) + if move_offset != 0: + assert move_offset < 0 + self.demo_process.log_file.seek(move_offset, io.SEEK_CUR) + return match elif partial_timeout <= 0.0: + self.demo_process.log_file.seek(orig_offset, io.SEEK_SET) return None def _get_valgrind_args(self): @@ -480,7 +495,7 @@ def is_file_executable(file_path): return demo_executable - def _start_demo(self, cmdline_args, timeout_s=30, prepend_args=None): + def _start_demo(self, cmdline_args, timeout_s=60, prepend_args=None): """ Starts the demo executable with given CMDLINE_ARGS. """ @@ -723,6 +738,8 @@ def setup_demo_with_servers(self, for serv in all_servers_passed: if serv.security_mode() != 'nosec': serv.listen() + if serv.transport == Transport.TCP: + self.assertTcpCsm(serv) for serv in servers_passed: self.assertDemoRegisters(serv, version=maximum_version, @@ -911,8 +928,6 @@ def request_demo_shutdown(self, deregister_servers=[], timeout_s=-1, *args, **kw for serv in deregister_servers: self.assertDemoDeregisters(serv, reset=False, timeout_s=timeout_s, *args, **kwargs) - if serv.transport == Transport.TCP: - self.assertDemoReleases(serv) logging.debug('demo terminated') @@ -1076,8 +1091,8 @@ def _count_packets(self, condition: lambda pkts: True): @staticmethod def is_icmp_unreachable(pkt): return isinstance(pkt, dpkt.ip.IP) \ - and isinstance(pkt.data, dpkt.icmp.ICMP) \ - and isinstance(pkt.data.data, dpkt.icmp.ICMP.Unreach) + and isinstance(pkt.data, dpkt.icmp.ICMP) \ + and isinstance(pkt.data.data, dpkt.icmp.ICMP.Unreach) @staticmethod def is_dtls_client_hello(pkt): diff --git a/tests/integration/framework/test_utils.py b/tests/integration/framework/test_utils.py index 6d64681d3..6ed95900f 100644 --- a/tests/integration/framework/test_utils.py +++ b/tests/integration/framework/test_utils.py @@ -18,7 +18,7 @@ from typing import Optional from .lwm2m import coap -from .firmware_package import FirmwareUpdateForcedError, make_firmware_package +from .firmware_package import FirmwareUpdateForcedError, make_firmware_package, make_multiple_firmware_package if sys.version_info[0] == 3 and sys.version_info[1] < 7: # based on https://stackoverflow.com/a/18348004/2339636 @@ -99,6 +99,7 @@ class OID: IpPing = 33607 GeoPoints = 33608 DownloadDiagnostics = 33609 + AdvancedFirmwareUpdate = 33629 class RID: @@ -321,6 +322,25 @@ class Test: BoolArray = 22 ResInitBoolArray = 23 + class AdvancedFirmwareUpdate: + Package = 0 + PackageURI = 1 + Update = 2 + State = 3 + UpdateResult = 5 + PackageName = 6 + PackageVersion = 7 + FirmwareUpdateProtocolSupport = 8 + FirmwareUpdateDeliveryMethod = 9 + Cancel = 10 + Severity = 11 + LastStateChangeTime = 12 + MaxDeferPeriod = 13 + PartitionName = 14 + CurrentVersion = 15 + LinkedInstances = 16 + ConflictingInstances = 17 + class Portfolio: Identity = 0 GetAuthData = 1 @@ -462,6 +482,8 @@ class ResPath: RID.GeoPoints, oid=OID.GeoPoints, multi_instance=True) DownloadDiagnostics = _Lwm2mResourcePathHelper.from_rid_object(RID.DownloadDiagnostics, oid=OID.DownloadDiagnostics) + AdvancedFirmwareUpdate = _Lwm2mResourcePathHelper.from_rid_object( + RID.AdvancedFirmwareUpdate, oid=OID.AdvancedFirmwareUpdate, multi_instance=True) BinaryAppDataContainer = _Lwm2mResourcePathHelper.from_rid_object( RID.BinaryAppDataContainer, oid=OID.BinaryAppDataContainer, multi_instance=True) EventLog = _Lwm2mResourcePathHelper.from_rid_object(RID.EventLog, oid=OID.EventLog) @@ -482,10 +504,10 @@ class TxParams(namedtuple('TxParams', 'max_latency'], defaults=(2.0, 1.5, 4.0, 100.0))): def max_transmit_wait(self): - return self.ack_timeout * self.ack_random_factor * (2**(self.max_retransmit + 1) - 1) + return self.ack_timeout * self.ack_random_factor * (2 ** (self.max_retransmit + 1) - 1) def max_transmit_span(self): - return self.ack_timeout * (2**self.max_retransmit - 1) * self.ack_random_factor + return self.ack_timeout * (2 ** self.max_retransmit - 1) * self.ack_random_factor def exchange_lifetime(self): """ @@ -502,31 +524,7 @@ def first_retransmission_timeout(self): return self.ack_random_factor * self.ack_timeout def last_retransmission_timeout(self): - return self.first_retransmission_timeout() * 2**self.max_retransmit - - -class ResponseFilter: - def __init__(self, *filtered_types): - self.filtered_types = filtered_types - self.filtered_messages = [] - - def add_if_filtered(self, message): - if type(message) in self.filtered_types: - self.filtered_messages.append(message) - return True - return False - - def filtered_recv(self, server, timeout_s): - begin = time.time() - res = server.recv(timeout_s=timeout_s) - - while self.add_if_filtered(res): - timeout_left = timeout_s - (time.time() - begin) - if timeout_left < 0: - raise socket.timeout() - res = server.recv(timeout_s=timeout_left) - - return res + return self.first_retransmission_timeout() * 2 ** self.max_retransmit DEMO_ENDPOINT_NAME = 'urn:dev:os:0023C7-000001' diff --git a/tests/integration/run_tests.sh.in b/tests/integration/run_tests.sh.in index 761333bc6..92e34bf51 100755 --- a/tests/integration/run_tests.sh.in +++ b/tests/integration/run_tests.sh.in @@ -8,30 +8,17 @@ # See the attached LICENSE file for details. -COMMAND="@CMAKE_CTEST_COMMAND@ -E sensitive"; +COMMAND="@CMAKE_CTEST_COMMAND@"; RERUNS=@TEST_RERUNS@; -if [ "$1" == "-s" ]; then - COMMAND="@CMAKE_CTEST_COMMAND@ -R sensitive"; - RERUNS=@TEST_SENSITIVE_RERUNS@; - if [ $RERUNS == 0 ]; then - $COMMAND --output-on-failure && exit 0; - else - $COMMAND && exit 0; - fi -elif [ "$1" == "-h" ]; then +if [ "$1" == "-h" ]; then COMMAND="@CMAKE_CTEST_COMMAND@ -R hsm"; - if [ $RERUNS == 0 ]; then - $COMMAND --output-on-failure && exit 0; - else - $COMMAND && exit 0; - fi +fi + +if [ $RERUNS == 0 ]; then + $COMMAND -j@NPROC@ --output-on-failure && exit 0; else - if [ $RERUNS == 0 ]; then - $COMMAND -j@NPROC@ --output-on-failure && exit 0; - else - $COMMAND -j@NPROC@ && exit 0; - fi + $COMMAND -j@NPROC@ && exit 0; fi if [ $RERUNS -gt 0 ]; then diff --git a/tests/integration/runtest.py b/tests/integration/runtest.py index 3acd14b1e..a74c916fe 100755 --- a/tests/integration/runtest.py +++ b/tests/integration/runtest.py @@ -32,7 +32,6 @@ ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) UNITTEST_PATH = os.path.join(ROOT_DIR, 'suites') -DEFAULT_SUITE_REGEX = r'^default\.' def traverse(tree, cls=None): @@ -46,8 +45,22 @@ def traverse(tree, cls=None): def discover_test_suites(test_config): + if getattr(test_config, 'demo_path', None) is None: + try: + import pymbedtls + except ModuleNotFoundError: + # Fake pymbedtls module so that "runtest.py -l" works without setting PYTHONPATH + class FakePymbedtlsModule: + class Context: + @staticmethod + def supports_connection_id(): + return False + + sys.modules['pymbedtls'] = FakePymbedtlsModule + loader = unittest.TestLoader() loader.testMethodPrefix = 'runTest' + suite = loader.discover(UNITTEST_PATH, pattern='*.py', top_level_dir=UNITTEST_PATH) for error in loader.errors: @@ -88,12 +101,10 @@ def run_tests(suites, config): errors = sum(r.testsErrors for r in test_runner.results) failures = sum(r.testsFailed for r in test_runner.results) - print('\nFinished in %f s; %s%d/%d successes%s, %s%d/%d errors%s, %s%d/%d failures%s\n' - % (seconds_elapsed, - COLOR_GREEN if successes == all_tests else COLOR_YELLOW, successes, all_tests, - COLOR_DEFAULT, - COLOR_RED if errors else COLOR_GREEN, errors, all_tests, COLOR_DEFAULT, - COLOR_RED if failures else COLOR_GREEN, failures, all_tests, COLOR_DEFAULT)) + print('\nFinished in %f s; %s%d/%d successes%s, %s%d/%d errors%s, %s%d/%d failures%s\n' % ( + seconds_elapsed, COLOR_GREEN if successes == all_tests else COLOR_YELLOW, successes, + all_tests, COLOR_DEFAULT, COLOR_RED if errors else COLOR_GREEN, errors, all_tests, + COLOR_DEFAULT, COLOR_RED if failures else COLOR_GREEN, failures, all_tests, COLOR_DEFAULT)) return test_runner.results @@ -177,42 +188,36 @@ def remove_tests_logs(tests): ================= {regex_match_rules_help} '''.format(regex_match_rules_help=textwrap.indent( - textwrap.dedent( - test_or_suite_matches_query_regex.__doc__), - prefix=' ' * 8))), + textwrap.dedent(test_or_suite_matches_query_regex.__doc__), prefix=' ' * 8))), formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument('--list', '-l', - action='store_true', + parser.add_argument('--list', '-l', action='store_true', help='only list matching test cases, do not execute them') - parser.add_argument('--client', '-c', - type=str, required=True, - help='path to the demo application to use') - parser.add_argument('--keep-success-logs', - action='store_true', + parser.add_argument('--client', '-c', type=str, help='path to the demo application to use') + parser.add_argument('--keep-success-logs', action='store_true', help='keep logs from all tests, including ones that passed') - parser.add_argument('--target-logs-path', type=str, - help='path where to leave the logs stored') - parser.add_argument('query_regex', - type=str, default=DEFAULT_SUITE_REGEX, nargs='?', + parser.add_argument('--target-logs-path', type=str, help='path where to leave the logs stored') + parser.add_argument('query_regex', type=str, default='', nargs='?', help='regex used to filter test cases. See REGEX MATCH RULES for details.') cmdline_args = parser.parse_args(sys.argv[1:]) with tempfile.TemporaryDirectory() as tmp_log_dir: class TestConfig: - demo_cmd = os.path.basename(cmdline_args.client) - demo_path = os.path.abspath(os.path.dirname(cmdline_args.client)) + if cmdline_args.client is not None: + demo_cmd = os.path.basename(cmdline_args.client) + demo_path = os.path.abspath(os.path.dirname(cmdline_args.client)) + target_logs_path = os.path.abspath( + cmdline_args.target_logs_path or os.path.join(demo_path, + '../test/integration/log')) + logs_path = tmp_log_dir suite_root_path = os.path.abspath(UNITTEST_PATH) - target_logs_path = os.path.abspath( - cmdline_args.target_logs_path or os.path.join(demo_path, '../test/integration/log')) - def config_to_string(cfg): - config = sorted((k, v) for k, v in cfg.__dict__.items() - if not k.startswith('_')) # skip builtins + config = sorted( + (k, v) for k, v in cfg.__dict__.items() if not k.startswith('_')) # skip builtins max_key_len = max(len(k) for k, _ in config) @@ -232,6 +237,9 @@ def config_to_string(cfg): result = None if not cmdline_args.list: + if cmdline_args.client is None: + parser.error('--client/c is required unless -l/--list is specified') + sys.stderr.write('%s\n\n' % config_to_string(TestConfig)) try: diff --git a/tests/integration/suites/default/advanced_firmware_update.py b/tests/integration/suites/default/advanced_firmware_update.py new file mode 100644 index 000000000..58940fd52 --- /dev/null +++ b/tests/integration/suites/default/advanced_firmware_update.py @@ -0,0 +1,3700 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017-2023 AVSystem +# AVSystem Anjay LwM2M SDK +# All rights reserved. +# +# Licensed under the AVSystem-5-clause License. +# See the attached LICENSE file for details. + +import asyncio +import http +import http.server +import os +import re +import ssl +import threading +import unittest + +from .firmware_update import FirmwareUpdate, UpdateState +from .firmware_update import UpdateResult as FU_UpdateResult +from framework.coap_file_server import CoapFileServer +from framework.lwm2m_test import * +from .access_control import AccessMask +from .block_write import Block, equal_chunk_splitter, msg_id_generator + + +class UpdateSeverity: + CRITICAL = 0 + MANDATORY = 1 + OPTIONAL = 2 + + +class UpdateResult(FU_UpdateResult): + CONFLICTING_STATE = 12 + DEPENDENCY_ERROR = 13 + + +class Instances: + APP = 0 + TEE = 1 + BOOT = 2 + MODEM = 3 + + +def packets_from_chunks(chunks, process_options=None, + path=ResPath.AdvancedFirmwareUpdate[ + Instances.APP].Package, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM, + code=coap.Code.REQ_PUT): + for idx, chunk in enumerate(chunks): + has_more = (idx != len(chunks) - 1) + + options = ((uri_path_to_options(path) if path is not None else []) + + [coap.Option.CONTENT_FORMAT(format), + coap.Option.BLOCK1(seq_num=chunk.idx, has_more=has_more, + block_size=chunk.size)]) + + if process_options is not None: + options = process_options(options, idx) + + yield coap.Packet(type=coap.Type.CONFIRMABLE, + code=code, + token=random_stuff(size=5), + msg_id=next(msg_id_generator), + options=options, + content=chunk.content) + + +GARBAGE_FILE = b'GARBAGE' +DUMMY_FILE = os.urandom(1 * 1024) +DUMMY_LONG_FILE = os.urandom(128 * 1024) +FIRMWARE_PATH = '/firmware' +FIRMWARE_SCRIPT_TEMPLATE = '#!/bin/sh\n%secho updated > "%s"\nrm "$0"\n' + + +# +# Test cases below are derived from test cases used to test Firmware Update +# + +class AdvancedFirmwareUpdate: + class Test(test_suite.Lwm2mSingleServerTest): + FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, 'linked': []} + + def set_auto_deregister(self, auto_deregister): + self.auto_deregister = auto_deregister + + def set_check_marker(self, check_marker): + self.check_marker = check_marker + + def set_reset_machine(self, reset_machine): + self.reset_machine = reset_machine + + def set_expect_send_after_state_machine_reset( + self, expect_send_after_state_machine_reset): + self.expect_send_after_state_machine_reset = expect_send_after_state_machine_reset + + def setUp(self, garbage=0, *args, **kwargs): + garbage_lines = '' + while garbage > 0: + garbage_line = '#' * (min(garbage, 80) - 1) + '\n' + garbage_lines += garbage_line + garbage -= len(garbage_line) + self.ANJAY_MARKER_FILE = generate_temp_filename( + dir='/tmp', prefix='anjay-afu-marked-') + self.ORIGINAL_IMG_FILE = generate_temp_filename( + dir='/tmp', prefix='anjay-afu-bootloader-') + with open(self.ORIGINAL_IMG_FILE, 'wb') as f: + f.write(GARBAGE_FILE) + self.FIRMWARE_SCRIPT_CONTENT = \ + (FIRMWARE_SCRIPT_TEMPLATE % + (garbage_lines, self.ANJAY_MARKER_FILE)).encode('ascii') + super().setUp(afu_marker_path=self.ANJAY_MARKER_FILE, + afu_original_img_file_path=self.ORIGINAL_IMG_FILE, + *args, **kwargs) + + def tearDown(self): + auto_deregister = getattr(self, 'auto_deregister', True) + check_marker = getattr(self, 'check_marker', False) + reset_machine = getattr(self, 'reset_machine', True) + expect_send_after_state_machine_reset = getattr(self, + 'expect_send_after_state_machine_reset', + False) + try: + if check_marker: + for _ in range(10): + time.sleep(0.5) + + if os.path.isfile(self.ANJAY_MARKER_FILE): + break + else: + self.fail('firmware marker not created') + with open(self.ANJAY_MARKER_FILE, "rb") as f: + line = f.readline()[:-1] + self.assertEqual(line, b"updated") + os.unlink(self.ANJAY_MARKER_FILE) + finally: + os.unlink(self.ORIGINAL_IMG_FILE) + if reset_machine: + # reset the state machine + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[ + Instances.APP].PackageURI, '') + self.serv.send(req) + self.assertMsgEqual( + Lwm2mChanged.matching(req)(), self.serv.recv()) + if expect_send_after_state_machine_reset: + pkt = self.serv.recv() + self.assertMsgEqual(Lwm2mSend(), pkt) + CBOR.parse(pkt.content).verify_values(test=self, + expected_value_map={ + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State: UpdateState.IDLE, + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].UpdateResult: UpdateResult.INITIAL + }) + self.serv.send(Lwm2mChanged.matching(pkt)()) + super().tearDown(auto_deregister=auto_deregister) + + def read_update_result(self, inst: int): + req = Lwm2mRead(ResPath.AdvancedFirmwareUpdate[inst].UpdateResult) + self.serv.send(req) + res = self.serv.recv() + self.assertMsgEqual(Lwm2mContent.matching(req)(), res) + return int(res.content) + + def read_state(self, inst: int): + req = Lwm2mRead(ResPath.AdvancedFirmwareUpdate[inst].State) + self.serv.send(req) + res = self.serv.recv() + self.assertMsgEqual(Lwm2mContent.matching(req)(), res) + return int(res.content) + + def read_linked(self, inst: int): + req = Lwm2mRead( + ResPath.AdvancedFirmwareUpdate[inst].LinkedInstances) + self.serv.send(req) + res = self.serv.recv() + self.assertMsgEqual(Lwm2mContent.matching(req)(), res) + return res.content + + def read_linked_and_check(self, inst: int, expected_inst: list): + """ + expected_inst -- list of tuples, each of form (Resource Instance ID, Value) + """ + received = self.read_linked(inst) + expected = TLV.make_multires( + RID.AdvancedFirmwareUpdate.LinkedInstances, + expected_inst) + self.assertEqual(expected.serialize(), received) + + def read_conflicting(self, inst: int): + req = Lwm2mRead( + ResPath.AdvancedFirmwareUpdate[inst].ConflictingInstances) + self.serv.send(req) + res = self.serv.recv() + self.assertMsgEqual(Lwm2mContent.matching(req)(), res) + return res.content + + def read_conflicting_and_check(self, inst: int, + expected_inst: list): + """ + expected_inst -- list of tuples, each of form (Resource Instance ID, Value) + """ + received = self.read_conflicting(inst) + expected = TLV.make_multires( + RID.AdvancedFirmwareUpdate.ConflictingInstances, + expected_inst) + self.assertEqual(expected.serialize(), received) + + def write_firmware_and_wait_for_download(self, inst: int, + firmware_uri: str, + download_timeout_s=20): + # Write /33629/inst/1 (Firmware URI) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[inst].PackageURI, + firmware_uri) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # wait until client downloads the firmware + deadline = time.time() + download_timeout_s + while time.time() < deadline: + time.sleep(0.5) + + if self.read_state(inst) == UpdateState.DOWNLOADED: + return + + self.fail('firmware still not downloaded') + + def wait_until_state_is(self, inst, state, timeout_s=2): + deadline = time.time() + timeout_s + while time.time() < deadline: + time.sleep(0.1) + if self.read_state(inst) == state: + return + + self.fail(f'state still is not {state}') + + def execute_update_and_check_success(self, inst): + # Execute /33629/inst/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[inst].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.wait_until_state_is(inst, UpdateState.IDLE) + + # Check /33629/inst result + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(inst)) + + def prepare_package(self, firmware: bytes): + self.PACKAGE = make_firmware_package( + firmware, **self.FW_PKG_OPTS) + + def prepare_package_app_img(self, use_real_app=False): + if use_real_app: + with open(os.path.join(self.config.demo_path, + self.config.demo_cmd), 'rb') as f: + firmware = f.read() + else: + firmware = self.FIRMWARE_SCRIPT_CONTENT + self.prepare_package(firmware) + + def prepare_package_additional_img(self, content: bytes): + with open(self.ORIGINAL_IMG_FILE, 'wb') as f: + f.write(content) + self.prepare_package(content) + + class TestWithHttpServer(FirmwareUpdate.TestWithHttpServerMixin, Test): + def provide_response_app_img(self, use_real_app=False): + super().provide_response(use_real_app) + + def provide_response_additional_img(self, content: bytes, overwrite_original_img=True): + if overwrite_original_img: + with open(self.ORIGINAL_IMG_FILE, 'wb') as f: + f.write(content) + super().provide_response(other_content=content) + + class TestWithTlsServer(FirmwareUpdate.TestWithTlsServerMixin, Test): + def setUp(self, pass_cert_to_demo=True, cmd_arg='', **kwargs): + super().setUp(pass_cert_to_demo, cmd_arg='--afu-cert-file', + **kwargs) + + class TestWithHttpsServer(TestWithTlsServer, + FirmwareUpdate.TestWithHttpsServerMixin, + TestWithHttpServer): + pass + + class TestWithCoapServer(FirmwareUpdate.TestWithCoapServerMixin, Test): + pass + + class TestWithPartialDownload(Test): + GARBAGE_SIZE = 8000 + + def wait_for_half_download(self): + # roughly twice the time expected as per SlowServer + deadline = time.time() + self.GARBAGE_SIZE / 500 + fsize = 0 + while time.time() < deadline: + time.sleep(0.5) + fsize = os.stat(self.fw_file_name).st_size + if fsize * 2 > self.GARBAGE_SIZE: + break + if fsize * 2 <= self.GARBAGE_SIZE: + self.fail('firmware image not downloaded fast enough') + elif fsize > self.GARBAGE_SIZE: + self.fail('firmware image downloaded too quickly') + + def setUp(self, *args, **kwargs): + super().setUp(garbage=self.GARBAGE_SIZE, *args, **kwargs) + + import tempfile + + with tempfile.NamedTemporaryFile(delete=False) as f: + self.fw_file_name = f.name + self.communicate('set-afu-package-path %s' % + (os.path.abspath(self.fw_file_name))) + + class TestWithPartialDownloadAndRestart( + FirmwareUpdate.DemoArgsExtractorMixin, TestWithPartialDownload): + pass + + class TestWithPartialCoapDownloadAndRestart( + TestWithPartialDownloadAndRestart, + TestWithCoapServer): + def setUp(self): + class SlowServer(coap.Server): + def send(self, *args, **kwargs): + time.sleep(0.5) + result = super().send(*args, **kwargs) + self.reset() # allow requests from other ports + return result + + super().setUp(coap_server=SlowServer()) + + with self.file_server as file_server: + file_server.set_resource('/firmware', + make_firmware_package( + self.FIRMWARE_SCRIPT_CONTENT, + **self.FW_PKG_OPTS)) + self.fw_uri = file_server.get_resource_uri('/firmware') + + class TestWithPartialHttpDownloadAndRestart( + FirmwareUpdate.TestWithPartialHttpDownloadAndRestartMixin, + TestWithPartialDownloadAndRestart, + TestWithHttpServer): + pass + + class BlockTest(Test, Block.Test): + def block_init_file(self): + import tempfile + + with tempfile.NamedTemporaryFile(delete=False) as f: + fw_file_name = f.name + self.communicate( + 'set-afu-package-path %s' % (os.path.abspath(fw_file_name))) + return fw_file_name + + def block_send(self, data, splitter, **make_firmware_package_args): + fw_file_name = self.block_init_file() + + make_firmware_package_args.update(self.FW_PKG_OPTS) + chunks = list(splitter( + make_firmware_package(data, **make_firmware_package_args))) + + for request in packets_from_chunks(chunks): + self.serv.send(request) + response = self.serv.recv() + self.assertIsSuccessResponse(response, request) + + with open(fw_file_name, 'rb') as fw_file: + self.assertEqual(fw_file.read(), data) + + self.files_to_cleanup.append(fw_file_name) + + def tearDown(self): + for file in self.files_to_cleanup: + try: + os.unlink(file) + except FileNotFoundError: + pass + + # now reset the state machine + self.write_resource(self.serv, OID.AdvancedFirmwareUpdate, 0, + RID.AdvancedFirmwareUpdate.Package, b'\0', + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + super(Block.Test, self).tearDown() + + +class AdvancedFirmwareUpdatePackageTest(AdvancedFirmwareUpdate.Test): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + # Write /33629/0/0 (Firmware): script content + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + make_firmware_package(self.FIRMWARE_SCRIPT_CONTENT, + **self.FW_PKG_OPTS), + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdateUriTest(AdvancedFirmwareUpdate.TestWithHttpServer): + + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + self.provide_response_app_img() + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdateStateChangeTest( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + # disable minimum notification period + write_attrs_req = Lwm2mWriteAttributes( + ResPath.AdvancedFirmwareUpdate[Instances.APP].State, + query=['pmin=0']) + self.serv.send(write_attrs_req) + self.assertMsgEqual(Lwm2mChanged.matching( + write_attrs_req)(), self.serv.recv()) + + # initial state should be 0 + observe_req = Lwm2mObserve( + ResPath.AdvancedFirmwareUpdate[Instances.APP].State) + self.serv.send(observe_req) + self.assertMsgEqual(Lwm2mContent.matching( + observe_req)(content=b'0'), self.serv.recv()) + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # notification should be sent before downloading + self.assertMsgEqual(Lwm2mNotify(observe_req.token, b'1'), + self.serv.recv()) + + self.provide_response_app_img() + + # ... and after it finishes + self.assertMsgEqual(Lwm2mNotify(observe_req.token, b'2'), + self.serv.recv()) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # ... and when update starts + self.assertMsgEqual(Lwm2mNotify(observe_req.token, b'3'), + self.serv.recv()) + + # there should be exactly one request + self.assertEqual(['/firmware'], self.requests) + + +class AdvancedFirmwareUpdateSendStateChangeTest( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp(minimum_version='1.1', maximum_version='1.1', + extra_cmdline_args=['--afu-use-send']) + self.set_reset_machine(False) + self.set_expect_send_after_state_machine_reset(True) + + def runTest(self): + self.assertEqual(self.read_state(Instances.APP), UpdateState.IDLE) + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + pkt = self.serv.recv() + self.assertMsgEqual(Lwm2mSend(), pkt) + CBOR.parse(pkt.content).verify_values(test=self, + expected_value_map={ + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].UpdateResult: UpdateResult.INITIAL, + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State: UpdateState.DOWNLOADING + }) + self.serv.send(Lwm2mChanged.matching(pkt)()) + + self.provide_response_app_img(use_real_app=True) + + pkt = self.serv.recv() + self.assertMsgEqual(Lwm2mSend(), pkt) + CBOR.parse(pkt.content).verify_values(test=self, + expected_value_map={ + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].UpdateResult: UpdateResult.INITIAL, + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State: UpdateState.DOWNLOADED + }) + self.serv.send(Lwm2mChanged.matching(pkt)()) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + pkt = self.serv.recv() + self.assertMsgEqual(Lwm2mSend(), pkt) + CBOR.parse(pkt.content).verify_values(test=self, + expected_value_map={ + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].UpdateResult: UpdateResult.INITIAL, + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State: UpdateState.UPDATING + }) + self.serv.send(Lwm2mChanged.matching(pkt)()) + + # there should be exactly one request + self.assertEqual(['/firmware'], self.requests) + + self.serv.reset() + self.assertDemoRegisters(version='1.1') + + pkt = self.serv.recv() + self.assertMsgEqual(Lwm2mSend(), pkt) + parsed_cbor = CBOR.parse(pkt.content) + parsed_cbor.verify_values(test=self, + expected_value_map={ + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].UpdateResult: UpdateResult.SUCCESS, + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State: UpdateState.IDLE + }) + # Check if Send contains firmware and software version + self.assertEqual(parsed_cbor[3].get(SenmlLabel.NAME), '/3/0/3') + self.assertEqual(parsed_cbor[4].get(SenmlLabel.NAME), '/3/0/19') + self.serv.send(Lwm2mChanged.matching(pkt)()) + + +class AdvancedFirmwareUpdateBadBase64(AdvancedFirmwareUpdate.Test): + def runTest(self): + # Write /33629/0/0 (Firmware): some random text to see how it makes the world burn + # (as text context does not implement some_bytes handler). + data = bytes(b'\x01' * 16) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + data, + format=coap.ContentFormat.TEXT_PLAIN) + self.serv.send(req) + self.assertMsgEqual( + Lwm2mErrorResponse.matching(req)(coap.Code.RES_BAD_REQUEST), + self.serv.recv()) + + +class AdvancedFirmwareUpdateGoodBase64(AdvancedFirmwareUpdate.Test): + def runTest(self): + import base64 + data = base64.encodebytes(bytes(b'\x01' * 16)).replace(b'\n', b'') + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + data, + format=coap.ContentFormat.TEXT_PLAIN) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdateNullPkg(AdvancedFirmwareUpdate.TestWithHttpServer): + def runTest(self): + self.provide_response_app_img() + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + b'\0', + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateEmptyPkgUri( + AdvancedFirmwareUpdate.TestWithHttpServer): + def runTest(self): + self.provide_response_app_img() + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, '') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateInvalidUri(AdvancedFirmwareUpdate.Test): + def runTest(self): + # observe Result + observe_req = Lwm2mObserve( + ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult) + self.serv.send(observe_req) + self.assertMsgEqual(Lwm2mContent.matching( + observe_req)(content=b'0'), self.serv.recv()) + + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + b'http://invalidfirmware.exe') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + while True: + notify = self.serv.recv() + self.assertMsgEqual(Lwm2mNotify(observe_req.token), notify) + if int(notify.content) != UpdateResult.INITIAL: + break + self.assertEqual(UpdateResult.INVALID_URI, int(notify.content)) + self.serv.send(Lwm2mReset(msg_id=notify.msg_id)) + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + + +class AdvancedFirmwareUpdateUnsupportedUri(AdvancedFirmwareUpdate.Test): + def runTest(self): + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + b'unsupported://uri.exe') + self.serv.send(req) + self.assertMsgEqual( + Lwm2mErrorResponse.matching(req)(coap.Code.RES_BAD_REQUEST), + self.serv.recv()) + # This does not even change state or anything, because according to the LwM2M spec + # Server can't feed us with unsupported URI type + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.UNSUPPORTED_PROTOCOL, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateOfflineUriTest( + AdvancedFirmwareUpdate.TestWithHttpServer): + def runTest(self): + self.communicate('enter-offline tcp') + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.CONNECTION_LOST, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateReplacingPkgUri( + AdvancedFirmwareUpdate.TestWithHttpServer): + def runTest(self): + self.provide_response_app_img() + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # This isn't specified anywhere as a possible transition, therefore + # it is most likely a bad request. + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + 'http://something') + self.serv.send(req) + self.assertMsgEqual( + Lwm2mErrorResponse.matching(req)(coap.Code.RES_BAD_REQUEST), + self.serv.recv()) + + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateReplacingPkg( + AdvancedFirmwareUpdate.TestWithHttpServer): + def runTest(self): + self.provide_response_app_img() + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # This isn't specified anywhere as a possible transition, therefore + # it is most likely a bad request. + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + b'trololo', + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual( + Lwm2mErrorResponse.matching(req)(coap.Code.RES_BAD_REQUEST), + self.serv.recv()) + + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + + + +class AdvancedFirmwareUpdateHttpsCancelPackageTest( + AdvancedFirmwareUpdate.TestWithPartialDownload, + AdvancedFirmwareUpdate.TestWithHttpServer): + RESPONSE_DELAY = 0.5 + CHUNK_SIZE = 1000 + ETAGS = True + + def runTest(self): + self.provide_response_app_img() + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.wait_for_half_download() + + self.assertEqual(self.get_socket_count(), 2) + + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + b'\0', + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.wait_until_socket_count(expected=1, timeout_s=5) + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateHttpsCancelPackageUriTest( + AdvancedFirmwareUpdate.TestWithPartialDownload, + AdvancedFirmwareUpdate.TestWithHttpServer): + RESPONSE_DELAY = 0.5 + CHUNK_SIZE = 1000 + ETAGS = True + + def runTest(self): + self.provide_response_app_img() + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.wait_for_half_download() + + self.assertEqual(self.get_socket_count(), 2) + + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, b'') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.wait_until_socket_count(expected=1, timeout_s=5) + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateCoapCancelPackageUriTest( + AdvancedFirmwareUpdate.TestWithPartialDownload, + AdvancedFirmwareUpdate.TestWithCoapServer): + def runTest(self): + with self.file_server as file_server: + file_server.set_resource('/firmware', + make_firmware_package( + self.FIRMWARE_SCRIPT_CONTENT)) + fw_uri = file_server.get_resource_uri('/firmware') + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + fw_uri) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Handle one GET + file_server.handle_request() + + self.assertEqual(self.get_socket_count(), 2) + + # Cancel download + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, b'') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.wait_until_socket_count(expected=1, timeout_s=5) + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateHttpsOfflineTest( + AdvancedFirmwareUpdate.TestWithPartialDownloadAndRestart, + AdvancedFirmwareUpdate.TestWithHttpServer): + RESPONSE_DELAY = 0.5 + CHUNK_SIZE = 1000 + ETAGS = True + + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + self.provide_response_app_img() + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.wait_for_half_download() + + self.assertEqual(self.get_socket_count(), 2) + self.communicate('enter-offline tcp') + self.wait_until_socket_count(expected=1, timeout_s=5) + self.provide_response_app_img() + self.communicate('exit-offline tcp') + + deadline = time.time() + 20 + while time.time() < deadline: + time.sleep(0.5) + + if self.read_state(Instances.APP) == UpdateState.DOWNLOADED: + break + else: + raise + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdateHttpsTest( + AdvancedFirmwareUpdate.TestWithHttpsServer): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + self.provide_response_app_img() + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri(), + download_timeout_s=20) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdateUnconfiguredHttpsTest( + AdvancedFirmwareUpdate.TestWithHttpsServer): + def setUp(self): + super().setUp(pass_cert_to_demo=False) + + def runTest(self): + # disable minimum notification period + write_attrs_req = Lwm2mWriteAttributes( + ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult, + query=['pmin=0']) + self.serv.send(write_attrs_req) + self.assertMsgEqual(Lwm2mChanged.matching( + write_attrs_req)(), self.serv.recv()) + + # initial result should be 0 + observe_req = Lwm2mObserve( + ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult) + self.serv.send(observe_req) + self.assertMsgEqual(Lwm2mContent.matching( + observe_req)(content=b'0'), self.serv.recv()) + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # even before reaching the server, we should get an error + notify_msg = self.serv.recv() + # no security information => "Unsupported protocol" + self.assertMsgEqual(Lwm2mNotify(observe_req.token, + str(UpdateResult.UNSUPPORTED_PROTOCOL).encode()), + notify_msg) + self.serv.send(Lwm2mReset(msg_id=notify_msg.msg_id)) + self.assertEqual(0, self.read_state(Instances.APP)) + + +class AdvancedFirmwareUpdateUnconfiguredHttpsWithFallbackAttemptTest( + AdvancedFirmwareUpdate.TestWithHttpsServer): + def setUp(self): + super().setUp(pass_cert_to_demo=False, + psk_identity=b'test-identity', psk_key=b'test-key') + + def runTest(self): + # disable minimum notification period + write_attrs_req = Lwm2mWriteAttributes( + ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult, + query=['pmin=0']) + self.serv.send(write_attrs_req) + self.assertMsgEqual(Lwm2mChanged.matching( + write_attrs_req)(), self.serv.recv()) + + # initial result should be 0 + observe_req = Lwm2mObserve( + ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult) + self.serv.send(observe_req) + self.assertMsgEqual(Lwm2mContent.matching( + observe_req)(content=b'0'), self.serv.recv()) + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # even before reaching the server, we should get an error + notify_msg = self.serv.recv() + # no security information => client will attempt PSK from data model + # and fail handshake => "Connection lost" + self.assertMsgEqual(Lwm2mNotify(observe_req.token, + str(UpdateResult.CONNECTION_LOST).encode()), + notify_msg) + self.serv.send(Lwm2mReset(msg_id=notify_msg.msg_id)) + self.assertEqual(0, self.read_state(Instances.APP)) + + +class AdvancedFirmwareUpdateInvalidHttpsTest( + AdvancedFirmwareUpdate.TestWithHttpsServer): + def setUp(self): + super().setUp(cn='invalid_cn', alt_ip=None) + + def runTest(self): + # disable minimum notification period + write_attrs_req = Lwm2mWriteAttributes( + ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult, + query=['pmin=0']) + self.serv.send(write_attrs_req) + self.assertMsgEqual(Lwm2mChanged.matching( + write_attrs_req)(), self.serv.recv()) + + # initial result should be 0 + observe_req = Lwm2mObserve( + ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult) + self.serv.send(observe_req) + self.assertMsgEqual(Lwm2mContent.matching( + observe_req)(content=b'0'), self.serv.recv()) + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # even before reaching the server, we should get an error + notify_msg = self.serv.recv() + # handshake failure => "Connection lost" + self.assertMsgEqual(Lwm2mNotify(observe_req.token, + str(UpdateResult.CONNECTION_LOST).encode()), + notify_msg) + self.serv.send(Lwm2mReset(msg_id=notify_msg.msg_id)) + self.assertEqual(0, self.read_state(Instances.APP)) + + +class AdvancedFirmwareUpdateResetInIdleState(AdvancedFirmwareUpdate.Test): + def runTest(self): + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, b'') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + b'\0', + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateCoapUri(AdvancedFirmwareUpdate.TestWithCoapServer): + def tearDown(self): + super().tearDown() + + # there should be exactly one request + with self.file_server as file_server: + self.assertEqual(1, len(file_server.requests)) + self.assertMsgEqual(CoapGet('/firmware'), + file_server.requests[0]) + + def runTest(self): + with self.file_server as file_server: + file_server.set_resource('/firmware', + make_firmware_package( + self.FIRMWARE_SCRIPT_CONTENT, + **self.FW_PKG_OPTS)) + fw_uri = file_server.get_resource_uri('/firmware') + self.write_firmware_and_wait_for_download(Instances.APP, fw_uri) + + +class AdvancedFirmwareUpdateRestartWithDownloaded(AdvancedFirmwareUpdate.Test): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + # Write /33629/0/0 (Firmware): script content + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + make_firmware_package(self.FIRMWARE_SCRIPT_CONTENT, + **self.FW_PKG_OPTS), + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # restart the app + self.teardown_demo_with_servers() + self.setup_demo_with_servers( + afu_marker_path=self.ANJAY_MARKER_FILE, + afu_original_img_file_path=self.ORIGINAL_IMG_FILE) + + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + + + +class AdvancedFirmwareUpdateResumeDownloadingOverHttpWithReconnect( + AdvancedFirmwareUpdate.TestWithPartialHttpDownloadAndRestart): + def _get_valgrind_args(self): + # we don't kill the process here, so we want Valgrind + return AdvancedFirmwareUpdate.TestWithHttpServer._get_valgrind_args( + self) + + def send_headers(self, handler, response_content, response_etag): + if 'Range' in handler.headers: + self.assertEqual(handler.headers['If-Match'], response_etag) + match = re.fullmatch(r'bytes=([0-9]+)-', handler.headers['Range']) + self.assertIsNotNone(match) + offset = int(match.group(1)) + handler.send_header('Content-range', + 'bytes %d-%d/*' % ( + offset, len(response_content) - 1)) + return offset + + def runTest(self): + self.provide_response_app_img() + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.wait_for_half_download() + + # reconnect + self.serv.reset() + self.communicate('reconnect') + self.provide_response_app_img() + self.assertDemoRegisters(self.serv, timeout_s=5) + + # wait until client downloads the firmware + deadline = time.time() + 20 + state = None + while time.time() < deadline: + fsize = os.stat(self.fw_file_name).st_size + self.assertGreater(fsize * 2, self.GARBAGE_SIZE) + state = self.read_state(Instances.APP) + self.assertIn( + state, {UpdateState.DOWNLOADING, UpdateState.DOWNLOADED}) + if state == UpdateState.DOWNLOADED: + break + # prevent test from reading Result hundreds of times per second + time.sleep(0.5) + + self.assertEqual(state, UpdateState.DOWNLOADED) + + self.assertEqual(len(self.requests), 2) + + + + +class AdvancedFirmwareUpdateWithDelayedResultTest: + class TestMixin: + def runTest(self, forced_error, result): + with open(os.path.join(self.config.demo_path, self.config.demo_cmd), + 'rb') as f: + firmware = f.read() + + # Write /33629/0/0 (Firmware) + self.block_send(firmware, + equal_chunk_splitter(chunk_size=1024), + force_error=forced_error) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute( + ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.serv.reset() + self.assertDemoRegisters() + self.assertEqual( + self.read_path(self.serv, ResPath.AdvancedFirmwareUpdate[ + Instances.APP].UpdateResult).content, + str(result).encode()) + self.assertEqual(self.read_path(self.serv, + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State).content, + str(UpdateState.IDLE).encode()) + + +class AdvancedFirmwareUpdateWithDelayedSuccessTest( + AdvancedFirmwareUpdateWithDelayedResultTest.TestMixin, + AdvancedFirmwareUpdate.BlockTest): + def runTest(self): + super().runTest(FirmwareUpdateForcedError.DelayedSuccess, + UpdateResult.SUCCESS) + + +class AdvancedFirmwareUpdateWithDelayedFailureTest( + AdvancedFirmwareUpdateWithDelayedResultTest.TestMixin, + AdvancedFirmwareUpdate.BlockTest): + def runTest(self): + super().runTest(FirmwareUpdateForcedError.DelayedFailedUpdate, + UpdateResult.FAILED) + + +class AdvancedFirmwareUpdateWithSetSuccessInPerformUpgrade( + AdvancedFirmwareUpdate.BlockTest): + def runTest(self): + with open(os.path.join(self.config.demo_path, self.config.demo_cmd), + 'rb') as f: + firmware = f.read() + + # Write /33629/0/0 (Firmware) + self.block_send(firmware, + equal_chunk_splitter(chunk_size=1024), + force_error=FirmwareUpdateForcedError.SetSuccessInPerformUpgrade) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # perform_upgrade handler is called via scheduler, so there is a small + # window during which reading the Firmware Update State still returns + # Updating. Wait for a while for State to actually change. + observed_states = [] + deadline = time.time() + 5 # arbitrary limit + while not observed_states or observed_states[-1] == str( + UpdateState.UPDATING): + if time.time() > deadline: + self.fail( + 'Firmware Update did not finish on time, last state = %s' % ( + observed_states[-1] if observed_states else 'NONE')) + observed_states.append( + self.read_path(self.serv, ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State).content.decode()) + time.sleep(0.5) + + self.assertNotEqual([], observed_states) + self.assertEqual(observed_states[-1], str(UpdateState.IDLE)) + self.assertEqual(self.read_path(self.serv, + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].UpdateResult).content, + str(UpdateResult.SUCCESS).encode()) + + +class AdvancedFirmwareUpdateWithSetFailureInPerformUpgrade( + AdvancedFirmwareUpdate.BlockTest): + def runTest(self): + with open(os.path.join(self.config.demo_path, self.config.demo_cmd), + 'rb') as f: + firmware = f.read() + + # Write /33629/0/0 (Firmware) + self.block_send(firmware, + equal_chunk_splitter(chunk_size=1024), + force_error=FirmwareUpdateForcedError.SetFailureInPerformUpgrade) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # perform_upgrade handler is called via scheduler, so there is a small + # window during which reading the Firmware Update State still returns + # Updating. Wait for a while for State to actually change. + observed_states = [] + deadline = time.time() + 5 # arbitrary limit + while not observed_states or observed_states[-1] == str( + UpdateState.UPDATING): + if time.time() > deadline: + self.fail( + 'Firmware Update did not finish on time, last state = %s' % ( + observed_states[-1] if observed_states else 'NONE')) + observed_states.append( + self.read_path(self.serv, ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State).content.decode()) + time.sleep(0.5) + + self.assertNotEqual([], observed_states) + self.assertEqual(observed_states[-1], str(UpdateState.IDLE)) + self.assertEqual(self.read_path(self.serv, + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].UpdateResult).content, + str(UpdateResult.FAILED).encode()) + + +try: + import aiocoap + import aiocoap.resource + import aiocoap.transports.tls +except ImportError: + # FirmwareUpdateCoapTlsTest requires a bleeding-edge version of aiocoap, that at the time of + # writing this code, is not available even in the prerelease channel. + # So we're not enforcing this dependency for now. + pass + + +@unittest.skipIf('aiocoap.transports.tls' not in sys.modules, + 'aiocoap.transports.tls not available') +@unittest.skipIf(sys.version_info < (3, 5, 3), + 'SSLContext signature changed in Python 3.5.3') +class AdvancedFirmwareUpdateCoapTlsTest( + AdvancedFirmwareUpdate.TestWithTlsServer, test_suite.Lwm2mDmOperations): + def setUp(self): + super().setUp(garbage=8000) + + class FirmwareResource(aiocoap.resource.Resource): + async def render_get(resource, request): + return aiocoap.Message(payload=make_firmware_package( + self.FIRMWARE_SCRIPT_CONTENT, **self.FW_PKG_OPTS)) + + serversite = aiocoap.resource.Site() + serversite.add_resource(('firmware',), FirmwareResource()) + + sslctx = ssl.SSLContext() + sslctx.load_cert_chain(self._cert_file, self._key_file) + + class EphemeralTlsServer(aiocoap.transports.tls.TLSServer): + _default_port = 0 + + class CoapTcpFileServerThread(threading.Thread): + def __init__(self): + super().__init__() + self.loop = asyncio.new_event_loop() + ctx = aiocoap.Context(loop=self.loop, serversite=serversite) + self.loop.run_until_complete(ctx._append_tokenmanaged_transport( + lambda tman: EphemeralTlsServer.create_server( + ('127.0.0.1', 0), tman, ctx.log, + self.loop, sslctx))) + + socket = \ + ctx.request_interfaces[0].token_interface.server.sockets[0] + self.server_address = socket.getsockname() + + def run(self): + asyncio.set_event_loop(self.loop) + try: + self.loop.run_forever() + finally: + self.loop.run_until_complete( + self.loop.shutdown_asyncgens()) + self.loop.close() + + self.server_thread = CoapTcpFileServerThread() + self.server_thread.start() + + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def tearDown(self): + try: + super().tearDown() + finally: + self.server_thread.loop.call_soon_threadsafe( + self.server_thread.loop.stop) + self.server_thread.join() + + def get_firmware_uri(self): + return 'coaps+tcp://127.0.0.1:%d/firmware' % ( + self.server_thread.server_address[1],) + + def runTest(self): + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdateWeakEtagTest( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + orig_end_headers = self.http_server.RequestHandlerClass.end_headers + + def updated_end_headers(request_handler): + request_handler.send_header('ETag', 'W/"weaketag"') + orig_end_headers(request_handler) + + self.http_server.RequestHandlerClass.end_headers = updated_end_headers + + def runTest(self): + self.provide_response_app_img() + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + with open(self.ANJAY_MARKER_FILE, 'rb') as f: + marker_data = f.read() + + self.assertNotIn(b'weaketag', marker_data) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class SameSocketDownload: + class Test(test_suite.Lwm2mDtlsSingleServerTest, + test_suite.Lwm2mDmOperations, + AdvancedFirmwareUpdate.Test): + GARBAGE_SIZE = 2048 + # Set to be able to comfortably test interleaved requests + ACK_TIMEOUT = 10 + MAX_RETRANSMIT = 4 + # Sometimes we want to allow the client to send more than one request at a time + # e.g. Update and GET. + NSTART = 1 + LIFETIME = 86400 + BINDING = 'U' + BLK_SZ = 1024 + + def setUp(self, *args, **kwargs): + if 'extra_cmdline_args' not in kwargs: + kwargs['extra_cmdline_args'] = [] + + kwargs['extra_cmdline_args'] += [ + '--prefer-same-socket-downloads', + '--ack-timeout', str(self.ACK_TIMEOUT), + '--max-retransmit', str(self.MAX_RETRANSMIT), + '--ack-random-factor', str(1.0), + '--nstart', str(self.NSTART) + ] + kwargs['lifetime'] = self.LIFETIME + super().setUp(*args, **kwargs) + self.file_server = CoapFileServer( + self.serv._coap_server, binding=self.BINDING) + self.file_server.set_resource(path=FIRMWARE_PATH, + data=make_firmware_package( + b'a' * self.GARBAGE_SIZE + , **self.FW_PKG_OPTS)) + + def read_state(self, serv=None): + return int(self.read_resource(serv or self.serv, + oid=OID.AdvancedFirmwareUpdate, + iid=0, + rid=RID.AdvancedFirmwareUpdate.State).content) + + def read_result(self, serv=None): + return int(self.read_resource(serv or self.serv, + oid=OID.AdvancedFirmwareUpdate, + iid=0, + rid=RID.AdvancedFirmwareUpdate.UpdateResult).content) + + def start_download(self): + self.write_resource(self.serv, + oid=OID.AdvancedFirmwareUpdate, + iid=0, + rid=RID.AdvancedFirmwareUpdate.PackageURI, + content=self.file_server.get_resource_uri( + FIRMWARE_PATH)) + + def handle_get(self, pkt=None): + if pkt is None: + pkt = self.serv.recv() + block2 = pkt.get_options(coap.Option.BLOCK2) + if block2: + self.assertEqual(block2[0].block_size(), self.BLK_SZ) + self.file_server.handle_recvd_request(pkt) + + def num_blocks(self): + return (len( + self.file_server._resources[ + FIRMWARE_PATH].data) + self.BLK_SZ - 1) // self.BLK_SZ + + +class AdvancedFirmwareDownloadSameSocket(SameSocketDownload.Test): + def runTest(self): + self.start_download() + + for _ in range(self.num_blocks()): + pkt = self.serv.recv() + self.handle_get(pkt) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketAndOngoingBlockwiseWrite( + SameSocketDownload.Test): + def runTest(self): + self.create_instance(self.serv, oid=OID.Test, iid=1) + self.start_download() + + resource_num_blocks = 10 + self.assertGreaterEqual(resource_num_blocks, self.num_blocks()) + for seq_num in range(resource_num_blocks): + if seq_num < self.num_blocks(): + dl_req_get = self.serv.recv() + + has_more = seq_num < resource_num_blocks - 1 + # Server does blockwise write on the unrelated resource + pkt = Lwm2mWrite(ResPath.Test[1].ResRawBytes, b'x' * 16, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM, + options=[ + coap.Option.BLOCK1(seq_num, has_more, 16)]) + self.serv.send(pkt) + if has_more: + self.assertMsgEqual( + Lwm2mContinue.matching(pkt)(), self.serv.recv()) + else: + self.assertMsgEqual( + Lwm2mChanged.matching(pkt)(), self.serv.recv()) + + if seq_num < self.num_blocks(): + # Client waits for next chunk of Raw Bytes, but gets firmware + # block instead + self.handle_get(dl_req_get) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketAndOngoingBlockwiseRead( + SameSocketDownload.Test): + BYTES = 160 + + def runTest(self): + self.create_instance(self.serv, oid=OID.Test, iid=1) + self.write_resource(self.serv, oid=OID.Test, iid=1, + rid=RID.Test.ResBytesSize, + content=bytes(str(self.BYTES), 'ascii')) + self.start_download() + + block_size = 16 + self.assertGreaterEqual(self.BYTES // block_size, self.num_blocks()) + for seq_num in range(self.BYTES // block_size): + if seq_num < self.num_blocks(): + dl_req_get = self.serv.recv() + + # Server does blockwise write on the unrelated resource + pkt = Lwm2mRead(ResPath.Test[1].ResBytes, + accept=coap.ContentFormat.APPLICATION_OCTET_STREAM, + options=[ + coap.Option.BLOCK2(seq_num, 0, block_size)]) + self.serv.send(pkt) + res = self.serv.recv() + self.assertEqual(pkt.msg_id, res.msg_id) + self.assertTrue(len(res.get_options(coap.Option.BLOCK2)) > 0) + + if seq_num < self.num_blocks(): + # Client waits for next chunk of Raw Bytes, but gets firmware + # block instead + self.handle_get(dl_req_get) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketUpdateDuringDownloadNstart2( + SameSocketDownload.Test): + NSTART = 2 + + def runTest(self): + self.start_download() + + for _ in range(self.num_blocks()): + dl_req_get = self.serv.recv() + # rather than responding to a request, force Update + self.communicate('send-update') + self.assertDemoUpdatesRegistration(timeout_s=5) + # and only then respond with next block + self.handle_get(dl_req_get) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketUpdateDuringDownloadNstart1( + SameSocketDownload.Test): + def runTest(self): + self.start_download() + + for _ in range(self.num_blocks()): + dl_req_get = self.serv.recv() + # rather than responding to a request, force Update + self.communicate('send-update') + # the Update won't be received, because of NSTART=1 + with self.assertRaises(socket.timeout): + self.serv.recv(timeout_s=3) + # so we respond to a block + self.handle_get(dl_req_get) + # and only then the Update arrives + self.assertDemoUpdatesRegistration(timeout_s=5) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketAndReconnectNstart1( + SameSocketDownload.Test): + def runTest(self): + self.start_download() + + for _ in range(self.num_blocks()): + # get dl request and ignore it + self.serv.recv() + # rather than responding to a request force reconnect + self.communicate('reconnect') + self.serv.reset() + # demo will resume DTLS session without sending any LwM2M messages + self.serv.listen() + # download request is retried + dl_req_get = self.serv.recv() + # and finally we respond to a block + self.handle_get(dl_req_get) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketUpdateTimeoutNstart2( + SameSocketDownload.Test): + NSTART = 2 + LIFETIME = 5 + MAX_RETRANSMIT = 1 + ACK_TIMEOUT = 2 + + def runTest(self): + time.sleep(self.LIFETIME + 1) + self.assertMsgEqual( + Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, content=b''), + self.serv.recv()) + self.assertMsgEqual( + Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, content=b''), + self.serv.recv()) + self.start_download() + + dl_get0_0 = self.serv.recv(filter=CoapGet._pkt_matches) + dl_get0_1 = self.serv.recv(filter=CoapGet._pkt_matches) # retry + self.handle_get(dl_get0_0) + self.assertEqual(dl_get0_0.msg_id, dl_get0_1.msg_id) + dl_get1_0 = self.serv.recv(filter=CoapGet._pkt_matches) + + # lifetime expired, demo re-registers + self.assertDemoRegisters(lifetime=self.LIFETIME) + + # this is a retransmission + dl_get1_1 = self.serv.recv() + self.assertEqual(dl_get1_0.msg_id, dl_get1_1.msg_id) + self.handle_get(dl_get1_1) + self.handle_get(self.serv.recv()) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketUpdateTimeoutNstart1( + SameSocketDownload.Test): + LIFETIME = 5 + MAX_RETRANSMIT = 1 + ACK_TIMEOUT = 2 + + def runTest(self): + time.sleep(self.LIFETIME + 1) + self.assertMsgEqual( + Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, content=b''), + self.serv.recv()) + self.assertMsgEqual( + Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, content=b''), + self.serv.recv()) + self.start_download() + + # registration temporarily held due to ongoing download + self.handle_get(self.serv.recv()) + # and only after handling the GET, it can be sent finally + self.assertDemoRegisters(lifetime=self.LIFETIME) + self.handle_get(self.serv.recv()) + self.handle_get(self.serv.recv()) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketDontCare(SameSocketDownload.Test): + def runTest(self): + self.start_download() + self.serv.recv() # recv GET and ignore it + + +class AdvancedFirmwareDownloadSameSocketSuspendDueToOffline( + SameSocketDownload.Test): + ACK_TIMEOUT = 1.5 + + def runTest(self): + self.start_download() + self.serv.recv() # ignore first GET + self.communicate('enter-offline') + with self.assertRaises(socket.timeout): + self.serv.recv(timeout_s=2 * self.ACK_TIMEOUT) + self.serv.reset() + self.communicate('exit-offline') + + # demo will resume DTLS session without sending any LwM2M messages + self.serv.listen() + + for _ in range(self.num_blocks()): + self.handle_get(self.serv.recv()) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketSuspendDueToOfflineDuringUpdate( + SameSocketDownload.Test): + ACK_TIMEOUT = 1.5 + + def runTest(self): + self.start_download() + self.serv.recv() # ignore first GET + self.communicate('send-update', match_regex=re.escape('Update sent')) + # actual Update message will not arrive due to NSTART + with self.serv.fake_close(): + self.communicate('enter-offline') + self.wait_until_socket_count(expected=0, timeout_s=5) + self.serv.reset() + self.communicate('exit-offline') + # demo will resume DTLS session before sending Register + self.serv.listen() + self.assertDemoUpdatesRegistration() + + for _ in range(self.num_blocks()): + self.handle_get(self.serv.recv()) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketSuspendDueToOfflineDuringUpdateNoMessagesCheck( + SameSocketDownload.Test): + ACK_TIMEOUT = 1.5 + + def runTest(self): + # Note: This test is almost identical to the one above, but does not close the socket + # during the offline period. This is to check that the client does not attempt to send any + # packets during that time. With the bug that triggered the addition of these test cases, + # these were two distinct code flow paths. + + self.start_download() + self.serv.recv() # ignore first GET + self.communicate('send-update', match_regex=re.escape('Update sent')) + # actual Update message will not arrive due to NSTART + self.communicate('enter-offline') + with self.assertRaises(socket.timeout): + self.serv.recv(timeout_s=2 * self.ACK_TIMEOUT) + self.serv.reset() + self.communicate('exit-offline') + # demo will resume DTLS session before sending Update + self.serv.listen() + self.assertDemoUpdatesRegistration() + + for _ in range(self.num_blocks()): + self.handle_get(self.serv.recv()) + + self.assertEqual(self.read_state(), UpdateState.DOWNLOADED) + + +class AdvancedFirmwareDownloadSameSocketAndBootstrap(SameSocketDownload.Test): + def setUp(self): + super().setUp(bootstrap_server=True) + + def tearDown(self): + super(test_suite.Lwm2mSingleServerTest, self).tearDown( + deregister_servers=[self.new_server]) + + def runTest(self): + self.start_download() + self.serv.recv() # recv GET and ignore it + + self.execute_resource(self.serv, + oid=OID.Server, + iid=2, + rid=RID.Server.RequestBootstrapTrigger) + + self.assertDemoRequestsBootstrap() + + self.new_server = Lwm2mServer() + self.write_resource(self.bootstrap_server, + oid=OID.Security, + iid=2, + rid=RID.Security.ServerURI, + content=bytes( + 'coap://127.0.0.1:%d' % self.new_server.get_listen_port(), + 'ascii')) + self.write_resource(self.bootstrap_server, + oid=OID.Security, + iid=2, + rid=RID.Security.Mode, + content=str( + coap.server.SecurityMode.NoSec.value).encode()) + req = Lwm2mBootstrapFinish() + self.bootstrap_server.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.bootstrap_server.recv()) + + self.assertDemoRegisters(self.new_server) + self.assertEqual(self.read_state(self.new_server), UpdateState.IDLE) + + + + +class AdvancedFirmwareUpdateCancelDuringIdleTest(AdvancedFirmwareUpdate.Test): + def runTest(self): + # Execute /33629/0/10 (Cancel) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Cancel) + self.serv.send(req) + self.assertMsgEqual(Lwm2mErrorResponse.matching(req)( + code=coap.Code.RES_METHOD_NOT_ALLOWED), + self.serv.recv()) + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateCancelDuringDownloadingTest( + AdvancedFirmwareUpdate.TestWithPartialDownload, + AdvancedFirmwareUpdate.TestWithHttpServer): + RESPONSE_DELAY = 0.5 + CHUNK_SIZE = 3000 + + def runTest(self): + self.provide_response_app_img() + + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + self.wait_for_half_download() + + # Execute /33629/0/10 (Cancel) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Cancel) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.CANCELLED, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateCancelDuringDownloadedTest( + AdvancedFirmwareUpdate.TestWithHttpServer): + def runTest(self): + self.provide_response_app_img() + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Execute /33629/0/10 (Cancel) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Cancel) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.CANCELLED, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateCancelDuringUpdatingTest( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_reset_machine(False) + # Don't run the downloaded package to be able to process Cancel + self.FW_PKG_OPTS = { + "force_error": FirmwareUpdateForcedError.DoNothing, + 'magic': b'AJAY_APP', 'version': 2, 'linked': [] + } + + def runTest(self): + self.provide_response_app_img() + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Execute /33629/0/10 (Cancel) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Cancel) + self.serv.send(req) + self.assertMsgEqual(Lwm2mErrorResponse.matching(req)( + code=coap.Code.RES_METHOD_NOT_ALLOWED), + self.serv.recv()) + + + + +class AdvancedFirmwareUpdateMaxDeferPeriodInvalidValueTest( + AdvancedFirmwareUpdate.Test): + def runTest(self): + # Write /33629/0/13 (Maximum Defer Period) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].MaxDeferPeriod, b'-5') + self.serv.send(req) + self.assertMsgEqual( + Lwm2mErrorResponse.matching(req)(coap.Code.RES_BAD_REQUEST), + self.serv.recv()) + + +class AdvancedFirmwareUpdateMaxDeferPeriodValidValueTest( + AdvancedFirmwareUpdate.Test): + def runTest(self): + for max_defer_period_value in [b'0', b'30']: + # Write /33629/0/13 (Maximum Defer Period) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].MaxDeferPeriod, + max_defer_period_value) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + +class AdvancedFirmwareUpdateWithDefer(AdvancedFirmwareUpdate.BlockTest): + def runTest(self): + with open(os.path.join(self.config.demo_path, self.config.demo_cmd), + 'rb') as f: + firmware = f.read() + + # Write /33629/0/13 (Maximum Defer Period) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].MaxDeferPeriod, b'30') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Write /33629/0/0 (Firmware) + self.block_send(firmware, + equal_chunk_splitter(chunk_size=1024), + force_error=FirmwareUpdateForcedError.Defer) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # perform_upgrade handler is called via scheduler, so there is a small + # window during which reading the Firmware Update State still returns + # Updating. Wait for a while for State to actually change. + observed_states = [] + deadline = time.time() + 5 # arbitrary limit + while not observed_states or observed_states[-1] == str( + UpdateState.UPDATING): + if time.time() > deadline: + self.fail( + 'Firmware Update did not finish on time, last state = %s' % ( + observed_states[-1] if observed_states else 'NONE')) + observed_states.append( + self.read_path(self.serv, ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State).content.decode()) + time.sleep(0.5) + + self.assertNotEqual([], observed_states) + self.assertEqual(observed_states[-1], str(UpdateState.DOWNLOADED)) + self.assertEqual(self.read_path(self.serv, + ResPath.AdvancedFirmwareUpdate[ + Instances.APP].UpdateResult).content, + str(UpdateResult.DEFERRED).encode()) + + +class AdvancedFirmwareUpdateSeverityWriteInvalidValueTest( + AdvancedFirmwareUpdate.Test): + def runTest(self): + for invalid_severity in [b'-1', b'3']: + # Write /33629/0/11 (Severity) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].Severity, + invalid_severity) + self.serv.send(req) + self.assertMsgEqual( + Lwm2mErrorResponse.matching(req)(coap.Code.RES_BAD_REQUEST), + self.serv.recv()) + + +class AdvancedFirmwareUpdateSeverityWriteValidValueTest( + AdvancedFirmwareUpdate.Test): + def runTest(self): + valid_severity_values = [ + UpdateSeverity.CRITICAL, + UpdateSeverity.MANDATORY, + UpdateSeverity.OPTIONAL + ] + for severity in [str(i).encode() for i in valid_severity_values]: + # Write /33629/0/11 (Severity) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].Severity, + severity) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + +class AdvancedFirmwareUpdateSeverityReadTest(AdvancedFirmwareUpdate.Test): + def runTest(self): + # Read default /33629/0/11 (Severity) + req = Lwm2mRead(ResPath.AdvancedFirmwareUpdate[Instances.APP].Severity) + self.serv.send(req) + self.assertMsgEqual( + Lwm2mContent.matching(req)( + content=str(UpdateSeverity.MANDATORY).encode()), + self.serv.recv()) + + +class AdvancedFirmwareUpdateLastStateChangeTime: + class Test: + def observe_state(self): + # Observe /33629/0/3 (State) + observe_req = Lwm2mObserve('/%d/0/%d' % ( + OID.AdvancedFirmwareUpdate, RID.AdvancedFirmwareUpdate.State)) + self.serv.send(observe_req) + self.assertMsgEqual(Lwm2mContent.matching(observe_req)(), + self.serv.recv()) + return observe_req.token + + def cancel_observe_state(self, token): + cancel_req = Lwm2mObserve('/%d/0/%d' % ( + OID.AdvancedFirmwareUpdate, RID.AdvancedFirmwareUpdate.State), + observe=1, token=token) + self.serv.send(cancel_req) + self.assertMsgEqual(Lwm2mContent.matching(cancel_req)(), + self.serv.recv()) + + def get_states_and_timestamp(self, token, deadline=None): + # Receive a notification from /33629/0/3 and read /33629/0/12 + notification_responses = [self.serv.recv(deadline=deadline)] + self.assertMsgEqual(Lwm2mNotify(token), notification_responses[0]) + + read_response = self.read_path(self.serv, + ResPath.AdvancedFirmwareUpdate[0].LastStateChangeTime, + deadline=deadline) + while True: + try: + notification_responses.append( + self.serv.recv(timeout_s=0, + filter=lambda pkt: isinstance(pkt, + Lwm2mNotify))) + except socket.timeout: + break + return [r.content.decode() for r in + notification_responses], read_response.content.decode() + + +class AdvancedFirmwareUpdateLastStateChangeTimeWithDelayedSuccessTest( + AdvancedFirmwareUpdate.BlockTest, + AdvancedFirmwareUpdateLastStateChangeTime.Test): + def runTest(self): + with open(os.path.join(self.config.demo_path, self.config.demo_cmd), + 'rb') as f: + firmware = f.read() + + observe_token = self.observe_state() + + # Write /33629/0/0 (Firmware) + self.block_send(firmware, + equal_chunk_splitter(chunk_size=1024), + force_error=FirmwareUpdateForcedError.DelayedSuccess) + + _, before_update_timestamp = self.get_states_and_timestamp( + observe_token) + + time.sleep(1) + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + state_notification = self.serv.recv() + self.assertMsgEqual(Lwm2mNotify(observe_token), state_notification) + + self.serv.reset() + self.assertDemoRegisters() + + req = Lwm2mRead( + ResPath.AdvancedFirmwareUpdate[Instances.APP].LastStateChangeTime) + self.serv.send(req) + after_update_response = self.serv.recv() + self.assertMsgEqual(Lwm2mContent.matching(req)(), after_update_response) + all_timestamps = [before_update_timestamp, + after_update_response.content.decode()] + self.assertEqual(all_timestamps, sorted(all_timestamps)) + + +class AdvancedFirmwareUpdateLastStateChangeTimeWithDeferTest( + AdvancedFirmwareUpdate.BlockTest, + AdvancedFirmwareUpdateLastStateChangeTime.Test): + def observe_after_update(self, token): + observed_states = [] + observed_timestamps = [] + deadline = time.time() + 5 # arbitrary limit + while not observed_states or observed_states[-1] == str( + UpdateState.UPDATING): + states, timestamp = self.get_states_and_timestamp(token, deadline=deadline) + observed_states += states + observed_timestamps.append(timestamp) + self.assertNotEqual([], observed_timestamps) + return observed_timestamps + + def runTest(self): + with open(os.path.join(self.config.demo_path, self.config.demo_cmd), + 'rb') as f: + firmware = f.read() + + # Write /33629/0/13 (Maximum Defer Period) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].MaxDeferPeriod, b'30') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + observe_token = self.observe_state() + + # Write /33629/0/0 (Firmware) + self.block_send(firmware, + equal_chunk_splitter(chunk_size=1024), + force_error=FirmwareUpdateForcedError.Defer) + + _, after_write_timestamp = self.get_states_and_timestamp(observe_token) + + time.sleep(1) + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + observed_timestamps = self.observe_after_update(observe_token) + + all_timestamps = [after_write_timestamp] + observed_timestamps + self.assertEqual(all_timestamps, sorted(all_timestamps)) + + self.cancel_observe_state(observe_token) + + +class AdvancedFirmwareUpdateSeverityPersistenceTest( + AdvancedFirmwareUpdate.Test): + def restart(self): + self.teardown_demo_with_servers() + self.setup_demo_with_servers( + afu_marker_path=self.ANJAY_MARKER_FILE, + afu_original_img_file_path=self.ORIGINAL_IMG_FILE) + + def runTest(self): + severity_values = [ + UpdateSeverity.CRITICAL, + UpdateSeverity.MANDATORY, + UpdateSeverity.OPTIONAL + ] + for severity in [str(i).encode() for i in severity_values]: + # Write /33629/0/11 (Severity) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].Severity, + severity) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + make_firmware_package(self.FIRMWARE_SCRIPT_CONTENT, + **self.FW_PKG_OPTS), + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + self.restart() + + req = Lwm2mRead( + ResPath.AdvancedFirmwareUpdate[Instances.APP].Severity) + self.serv.send(req) + res = self.serv.recv() + self.assertMsgEqual(Lwm2mContent.matching(req)(), res) + + self.assertEqual(severity, res.content) + self.restart() + + +class AdvancedFirmwareUpdateDeadlinePersistenceTest( + FirmwareUpdate.DemoArgsExtractorMixin, + AdvancedFirmwareUpdate.BlockTest): + def get_deadline(self): + return int(self.communicate('get-afu-deadline', + match_regex='AFU_APP_UPDATE_DEADLINE==([0-9]+)\n').group( + 1)) + + def runTest(self): + with open(os.path.join(self.config.demo_path, self.config.demo_cmd), + 'rb') as f: + firmware = f.read() + + # Write /33629/0/13 (Maximum Defer Period) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.APP].MaxDeferPeriod, b'30') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Write /33629/0/0 (Firmware) + self.block_send(firmware, + equal_chunk_splitter(chunk_size=1024), + force_error=FirmwareUpdateForcedError.Defer) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # perform_upgrade handler is called via scheduler, so there is a small + # window during which reading the Firmware Update State still returns + # Updating. Wait for a while for State to actually change. + observed_states = [] + deadline = time.time() + 5 # arbitrary limit + while not observed_states or observed_states[-1] == str( + UpdateState.UPDATING): + if time.time() > deadline: + self.fail( + 'Firmware Update did not finish on time, last state = %s' % ( + observed_states[-1] if observed_states else 'NONE')) + observed_states.append( + self.read_path(self.serv, ResPath.AdvancedFirmwareUpdate[ + Instances.APP].State).content.decode()) + time.sleep(0.5) + + self.assertNotEqual([], observed_states) + self.assertEqual(observed_states[-1], str(UpdateState.DOWNLOADED)) + + saved_deadline = self.get_deadline() + + self.demo_process.kill() + self.serv.reset() + self._start_demo(self.cmdline_args) + self.assertDemoRegisters() + + restored_deadline = self.get_deadline() + + self.assertEqual(saved_deadline, restored_deadline) + + +# +# Below test cases are specific for Advanced Firmware Update +# + +class AdvancedFirmwareUpdatePackageTestTwoNotLinkedImages( + AdvancedFirmwareUpdate.Test): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.prepare_package_app_img() + + # Write /33629/0/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/0 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write /33629/1/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.TEE].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/1 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.TEE)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + self.execute_update_and_check_success(Instances.TEE) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdateUriTestTwoNotLinkedImages( + AdvancedFirmwareUpdate.TestWithHttpServer): + + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.provide_response_app_img() + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/1/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + self.execute_update_and_check_success(Instances.TEE) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdatePackageTestFourNotLinkedImages( + AdvancedFirmwareUpdate.Test): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.prepare_package_app_img() + + # Write /33629/0/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/0 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write /33629/1/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.TEE].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/1 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.TEE)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + # Prepare package for /33629/2 + self.FW_PKG_OPTS = {'magic': b'AJAYBOOT', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write /33629/2/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.BOOT].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/2 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + + # Prepare package for /33629/3 + self.FW_PKG_OPTS = {'magic': b'AJAYMODE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write /33629/3/0 (Firmware) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.MODEM].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/3 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.MODEM)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.MODEM)) + + self.execute_update_and_check_success(Instances.TEE) + self.execute_update_and_check_success(Instances.BOOT) + self.execute_update_and_check_success(Instances.MODEM) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdateUriTestFourNotLinkedImages( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.provide_response_app_img() + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + # Prepare package for /33629/2 + self.FW_PKG_OPTS = {'magic': b'AJAYBOOT', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.BOOT, + self.get_firmware_uri()) + + # Check /33629/2 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + + # Prepare package for /33629/3 + self.FW_PKG_OPTS = {'magic': b'AJAYMODE', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.MODEM, + self.get_firmware_uri()) + + # Check /33629/3 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.MODEM)) + + self.execute_update_and_check_success(Instances.TEE) + self.execute_update_and_check_success(Instances.BOOT) + self.execute_update_and_check_success(Instances.MODEM) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + +class AdvancedFirmwareUpdateUriTestFourNotLinkedImagesAPPFirst( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.provide_response_app_img(use_real_app=True) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + # Prepare package for /33629/2 + self.FW_PKG_OPTS = {'magic': b'AJAYBOOT', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.BOOT, + self.get_firmware_uri()) + + # Check /33629/2 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + + # Prepare package for /33629/3 + self.FW_PKG_OPTS = {'magic': b'AJAYMODE', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.MODEM, + self.get_firmware_uri()) + + # Check /33629/3 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.MODEM)) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.serv.reset() + self.assertDemoRegisters() + self.execute_update_and_check_success(Instances.TEE) + self.execute_update_and_check_success(Instances.BOOT) + self.execute_update_and_check_success(Instances.MODEM) + + +class AdvancedFirmwareUpdateTestLinkedTeeToApp( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, + 'linked': [Instances.TEE]} + self.provide_response_app_img() + + # Check /33629/0/16 (LinkedInstances), there should not be any linked instances + self.read_linked_and_check(Instances.APP, []) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result and linked instances + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + self.read_linked_and_check(Instances.APP, [(1, Objlink(33629, 1))]) + + +class AdvancedFirmwareUpdateTestLinkedOthersToApp( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, + 'linked': [Instances.TEE, + Instances.BOOT, + Instances.MODEM]} + self.provide_response_app_img() + + # Check /33629/0/16 (LinkedInstances), there should not be any linked instances + self.read_linked_and_check(Instances.APP, []) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result linked and conflicting instances + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + self.read_linked_and_check(Instances.APP, [(1, Objlink(33629, 1)), + (2, Objlink(33629, 2)), + (3, Objlink(33629, 3)), ]) + self.read_conflicting_and_check(Instances.APP, [(1, Objlink(33629, 1)), + (2, Objlink(33629, 2)), + (3, Objlink(33629, 3))]) + + # Try to Update /33629/0 + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.wait_until_state_is(Instances.APP, UpdateState.DOWNLOADED) + + # Check /33629/0 result + self.assertEqual(UpdateResult.DEPENDENCY_ERROR, + self.read_update_result(Instances.APP)) + + +class AdvancedFirmwareUpdateTestConflictingAppAndTee( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, + 'linked': [Instances.TEE]} + self.provide_response_app_img() + + # Check /33629/0/17, there should not be any conflicting instances + self.read_conflicting_and_check(Instances.APP, []) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result and conflicting instances + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + self.read_conflicting_and_check(Instances.APP, [(1, Objlink(33629, 1))]) + + +class AdvancedFirmwareUpdateTestResolveConflictingAppAndTee( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, + 'linked': [Instances.TEE]} + self.provide_response_app_img() + + # Check /33629/0/17, there should not be any conflicting instances + self.read_conflicting_and_check(Instances.APP, []) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result and conflicting instances + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + self.read_conflicting_and_check(Instances.APP, [(1, Objlink(33629, 1))]) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/1/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + self.read_linked_and_check(Instances.TEE, []) + + # Check again /33629/0/17, conflicting instances should disappear + self.read_conflicting_and_check(Instances.APP, []) + + +class AdvancedFirmwareUpdateTestResolveConflictingAndUpdateTeeAndBoot( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2, + 'linked': [Instances.BOOT]} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Check /33629/1/17, there should not be any conflicting instances + self.read_conflicting_and_check(Instances.TEE, []) + + # Write /33629/1/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result and conflicting instances + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + self.read_conflicting_and_check(Instances.TEE, [(2, Objlink(33629, 2))]) + + # Prepare package for /33629/2 + self.FW_PKG_OPTS = {'magic': b'AJAYBOOT', 'version': 2, + 'linked': [Instances.TEE]} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/2/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.BOOT, + self.get_firmware_uri()) + + # Check /33629/2 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + self.read_linked_and_check(Instances.BOOT, [(1, Objlink(33629, 1))]) + + # Check again /33629/1/17, conflicting instances should disappear + self.read_conflicting_and_check(Instances.TEE, []) + + # Update 33629/1 TEE + self.execute_update_and_check_success(Instances.TEE) + + # Check /33629/2 BOOT state and result, which should also be updated already + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.BOOT)) + + # Check linked and conflicting are cleared + self.read_linked_and_check(Instances.TEE, []) + self.read_conflicting_and_check(Instances.TEE, []) + self.read_linked_and_check(Instances.BOOT, []) + self.read_conflicting_and_check(Instances.BOOT, []) + + +class AdvancedFirmwareUpdateTestNoConflictWithDownloadedEarlier( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + self.read_linked_and_check(Instances.TEE, []) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, + 'linked': [Instances.TEE]} + self.provide_response_app_img() + + # Check /33629/0/17, there should not be any conflicting instances + self.read_conflicting_and_check(Instances.APP, []) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Check again /33629/0/17, TEE already downloaded so should not be any conflict + self.read_conflicting_and_check(Instances.APP, []) + + +class AdvancedFirmwareUpdateTestFailedUpdate( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2, + 'linked': [Instances.BOOT]} + self.provide_response_additional_img(content=DUMMY_FILE, overwrite_original_img=False) + + # Check /33629/1/17, there should not be any conflicting instances + self.read_conflicting_and_check(Instances.TEE, []) + + # Write /33629/1/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result and conflicting instances + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + self.read_conflicting_and_check(Instances.TEE, [(2, Objlink(33629, 2))]) + + # Prepare package for /33629/2 + self.FW_PKG_OPTS = {'magic': b'AJAYBOOT', 'version': 2, + 'linked': [Instances.TEE]} + self.provide_response_additional_img(content=DUMMY_FILE, overwrite_original_img=False) + + # Write /33629/2/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.BOOT, + self.get_firmware_uri()) + + # Check /33629/2 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + self.read_linked_and_check(Instances.BOOT, [(1, Objlink(33629, 1))]) + + # Check again /33629/1/17, conflicting instances should disappear + self.read_conflicting_and_check(Instances.TEE, []) + + # Execute /33629/1/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.TEE].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # This execute is not going to make successful update it is forced by + # option 'prep_additional=False' while calling `provide_response`. + # It means that this test just not going to prepare proper image to compare with by demo. + self.wait_until_state_is(Instances.TEE, UpdateState.DOWNLOADED) + + # Check /33629/1 result + self.assertEqual(UpdateResult.FAILED, + self.read_update_result(Instances.TEE)) + + # Check /33629/2 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.FAILED, + self.read_update_result(Instances.BOOT)) + + +class AdvancedFirmwareUpdateTestUpdateBootWithLinkedTee( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/2 + self.FW_PKG_OPTS = {'magic': b'AJAYBOOT', 'version': 2, + 'linked': [Instances.TEE]} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Check /33629/2/17, there should not be any conflicting instances + self.read_conflicting_and_check(Instances.BOOT, []) + + # Write /33629/2/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.BOOT, + self.get_firmware_uri()) + + # Check /33629/2 result and conflicting instances + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + self.read_conflicting_and_check(Instances.BOOT, + [(1, Objlink(33629, 1))]) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + self.read_linked_and_check(Instances.TEE, []) + + # Check again /33629/2 state, result and conflicting instances + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + self.read_conflicting_and_check(Instances.BOOT, []) + + # Execute /33629/2/2 (Update) + req = Lwm2mExecute( + ResPath.AdvancedFirmwareUpdate[Instances.BOOT].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.wait_until_state_is(Instances.TEE, UpdateState.IDLE) + + # Check /33629/1 state, result, linked and conflicting + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.TEE)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.TEE)) + self.read_linked_and_check(Instances.TEE, []) + self.read_conflicting_and_check(Instances.TEE, []) + + # Check /33629/2 state, result, linked and conflicting + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.BOOT)) + self.read_linked_and_check(Instances.BOOT, []) + self.read_conflicting_and_check(Instances.BOOT, []) + + +class AdvancedFirmwareUpdateTestSetConflictAfterCancelOfLinkedImage( + AdvancedFirmwareUpdate.TestWithHttpServer): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, + 'linked': [Instances.TEE]} + self.provide_response_app_img() + + # Check /33629/0/17, there should not be any conflicting instances + self.read_conflicting_and_check(Instances.APP, []) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result and conflicting instances + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + self.read_conflicting_and_check(Instances.APP, [(1, Objlink(33629, 1))]) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + self.read_linked_and_check(Instances.TEE, []) + + # Check again /33629/0/17, conflicting instances should disappear + self.read_conflicting_and_check(Instances.APP, []) + + # Execute /33629/1/10 (Cancel) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.TEE].Cancel) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check again /33629/0/17, conflicting instances should be there again + self.read_conflicting_and_check(Instances.APP, [(1, Objlink(33629, 1))]) + + +class AdvancedFirmwareUpdatePackageTestWithMultiPackage( + AdvancedFirmwareUpdate.Test): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.prepare_package_app_img() + app_pkg = self.PACKAGE + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + tee_pkg = self.PACKAGE + + # Prepare multiple package + multi_pkg = make_multiple_firmware_package([app_pkg, tee_pkg]) + + # Write multiple package to /33629/0/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + multi_pkg, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/0 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Check /33629/1 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.TEE)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + self.execute_update_and_check_success(Instances.TEE) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdatePackageTestWithMultiPackageAllImages( + AdvancedFirmwareUpdate.Test): + def setUp(self): + super().setUp() + self.set_check_marker(True) + self.set_auto_deregister(False) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.prepare_package_app_img() + app_pkg = self.PACKAGE + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2, + 'linked': [Instances.BOOT, Instances.MODEM]} + self.prepare_package_additional_img(content=DUMMY_FILE) + tee_pkg = self.PACKAGE + + # Prepare package for /33629/2 + self.FW_PKG_OPTS = {'magic': b'AJAYBOOT', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + boot_pkg = self.PACKAGE + + # Prepare package for /33629/3 + self.FW_PKG_OPTS = {'magic': b'AJAYMODE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + modem_pkg = self.PACKAGE + + # Prepare multiple package + multi_pkg = make_multiple_firmware_package( + [app_pkg, tee_pkg, boot_pkg, modem_pkg]) + + # Write multiple package to /33629/3/0 (Firmware) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.MODEM].Package, + multi_pkg, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/0 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Check /33629/1 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.TEE)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + # Check /33629/2 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + + # Check /33629/3 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.MODEM)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.MODEM)) + + # Execute /33629/1/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.TEE].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.wait_until_state_is(Instances.TEE, UpdateState.IDLE) + + # Check /33629/1 result + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.TEE)) + + # Check /33629/2 state and result + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.BOOT)) + + # Check /33629/3 state and result + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.MODEM)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.MODEM)) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + +class AdvancedFirmwareUpdatePackageTestWithMultiPackageConflictingDownloads( + AdvancedFirmwareUpdate.Test): + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write multiple package to /33629/1/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.TEE].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/1 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.TEE)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.prepare_package_app_img() + app_pkg = self.PACKAGE + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + tee_pkg = self.PACKAGE + + # Prepare multiple package + multi_pkg = make_multiple_firmware_package([app_pkg, tee_pkg]) + + # Write multiple package to /33629/3/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + multi_pkg, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/0 state and result + self.assertEqual(UpdateState.IDLE, self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.CONFLICTING_STATE, + self.read_update_result(Instances.APP)) + self.read_conflicting_and_check(Instances.APP, [(1, Objlink(33629, 1))]) + + # Check /33629/1 state and result + self.assertEqual(UpdateState.DOWNLOADED, self.read_state(Instances.TEE)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + +class AdvancedFirmwareUpdateUriTestExplicitLinkedUpdate( + AdvancedFirmwareUpdate.TestWithHttpServer): + + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, 'linked': [1]} + self.provide_response_app_img(use_real_app=True) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + self.read_linked_and_check(Instances.APP, [(1, Objlink(33629, 1))]) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2, 'linked': [0]} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/1/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + self.read_linked_and_check(Instances.TEE, [(0, Objlink(33629, 0))]) + + # Prepare package for /33629/2 + self.FW_PKG_OPTS = {'magic': b'AJAYBOOT', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write /33629/2/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.BOOT].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/2 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + + # Prepare package for /33629/3 + self.FW_PKG_OPTS = {'magic': b'AJAYMODE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write /33629/3/0 (Firmware) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.MODEM].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/3 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.MODEM)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.MODEM)) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update, + content=b'0=\',,\'') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.serv.reset() + self.assertDemoRegisters() + + # Check /33629/0 state and result + self.assertEqual(UpdateState.IDLE, + self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.APP)) + + # Check /33629/1 state and result + self.assertEqual(UpdateState.IDLE, + self.read_state(Instances.TEE)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.TEE)) + + # Check /33629/2 state and result + self.assertEqual(UpdateState.IDLE, + self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.BOOT)) + + # Check /33629/3 state and result + self.assertEqual(UpdateState.IDLE, + self.read_state(Instances.MODEM)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.MODEM)) + + +class AdvancedFirmwareUpdateUriTestExplicitSinglePartitionUpdate( + AdvancedFirmwareUpdate.TestWithHttpServer): + + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, 'linked': [1]} + self.provide_response_app_img(use_real_app=True) + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + self.read_linked_and_check(Instances.APP, [(1, Objlink(33629, 1))]) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2, 'linked': [0]} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/1/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + # Check /33629/1 result and linked + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + self.read_linked_and_check(Instances.TEE, [(0, Objlink(33629, 0))]) + + # Prepare package for /33629/2 + self.FW_PKG_OPTS = {'magic': b'AJAYBOOT', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write /33629/2/0 (Firmware) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.BOOT].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/2 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.BOOT)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.BOOT)) + + # Prepare package for /33629/3 + self.FW_PKG_OPTS = {'magic': b'AJAYMODE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write /33629/3/0 (Firmware) + req = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.MODEM].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Check /33629/3 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.MODEM)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.MODEM)) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update, + content=b'0') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.serv.reset() + self.assertDemoRegisters() + + # Check /33629/0 state and result + self.assertEqual(UpdateState.IDLE, + self.read_state(Instances.APP)) + self.assertEqual(UpdateResult.SUCCESS, + self.read_update_result(Instances.APP)) + + # Check /33629/1 state and result + self.assertEqual(UpdateState.DOWNLOADED, + self.read_state(Instances.TEE)) + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.TEE)) + + +class AdvancedFirmwareUpdateUriTestCheckPkgVersion( + AdvancedFirmwareUpdate.TestWithHttpServer): + + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, + 'pkg_version': b'2.0.1'} + self.provide_response_app_img() + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # Check /33629/7 pkg_version + req = Lwm2mRead( + ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageVersion) + self.serv.send(req) + res = self.serv.recv() + self.assertMsgEqual(Lwm2mContent.matching(req)(), res) + self.assertEqual(res.content, b'2.0.1') + + +class AdvancedFirmwareUpdateVersionConflictTest( + AdvancedFirmwareUpdate.TestWithHttpServer): + + def setUp(self): + super().setUp() + self.set_check_marker(False) + self.set_auto_deregister(True) + self.set_reset_machine(False) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, + 'pkg_version': b'2.0.1'} + self.provide_response_app_img() + + # Write /33629/0/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.APP, + self.get_firmware_uri()) + + # Check /33629/0 result + self.assertEqual(UpdateResult.INITIAL, + self.read_update_result(Instances.APP)) + + # In demo there is requirement that major version of TEE has to be equal or above new version of APP + # There should be conflicting instance + self.read_conflicting_and_check(Instances.APP, [(1, Objlink(33629, 1))]) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + self.wait_until_state_is(Instances.APP, UpdateState.DOWNLOADED) + + # Check /33629/0 result + self.assertEqual(UpdateResult.DEPENDENCY_ERROR, + self.read_update_result(Instances.APP)) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2, + 'pkg_version': b'2.0.1'} + self.provide_response_additional_img(content=DUMMY_FILE) + + # Write /33629/1/0 (Firmware) + self.write_firmware_and_wait_for_download(Instances.TEE, + self.get_firmware_uri()) + + self.execute_update_and_check_success(Instances.TEE) + + # Check again conflicting, it should disappear + self.read_conflicting_and_check(Instances.APP, []) + + +class AdvancedFirmwareUpdateQueueParallelPull(AdvancedFirmwareUpdate.TestWithCoapServer): + def setUp(self, coap_server=None, *args, **kwargs): + super().setUp(coap_server=[None, None], *args, **kwargs) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_LONG_FILE) + with self.get_file_server(serv=0) as file_server: + file_server.set_resource('/firmwareAPP', + self.PACKAGE) + fw_uri = file_server.get_resource_uri('/firmwareAPP') + + # Write /33629/inst/1 (Firmware URI) + req1 = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + fw_uri) + self.serv.send(req1) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + with self.get_file_server(serv=1) as file_server: + file_server.set_resource('/firmwareTEE', + self.PACKAGE) + fw_uri = file_server.get_resource_uri('/firmwareTEE') + + # Write /33629/inst/1 (Firmware URI) + req2 = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.TEE].PackageURI, + fw_uri) + self.serv.send(req2) + + # This case finally tests if two strings occur one after another to ensure that + # download schedule for TEE is done just after the APP finish downloading + if self.read_log_until_match(regex=re.escape(b'instance /33629/0 downloaded successfully'), + timeout_s=5) is None: + raise self.failureException( + 'string not found') + + if self.read_log_until_match(regex=re.escape(b'download scheduled: ' + fw_uri.encode()), + timeout_s=1) is None: + raise self.failureException( + 'string not found') + + # There should be two request already received as we didn't check it before, to not wait for client + self.assertMsgEqual(Lwm2mChanged.matching(req1)(), + self.serv.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(req2)(), + self.serv.recv()) + + # Make sure that queued download finished properly + self.wait_until_state_is(Instances.TEE, UpdateState.DOWNLOADED) + + +class AdvancedFirmwareUpdateRejectPushWhilePull(AdvancedFirmwareUpdate.TestWithCoapServer): + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_LONG_FILE) + with self.file_server as file_server: + file_server.set_resource('/firmwareAPP', + self.PACKAGE) + fw_uri = file_server.get_resource_uri('/firmwareAPP') + + # Write /33629/inst/1 (Firmware URI) + req1 = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + fw_uri) + self.serv.send(req1) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + + # Write /33629/3/0 (Firmware) + req2 = Lwm2mWrite( + ResPath.AdvancedFirmwareUpdate[Instances.MODEM].Package, + self.PACKAGE, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + self.serv.send(req2) + + # There should be request already received as we didn't check it before, to not wait for client + self.assertMsgEqual(Lwm2mChanged.matching(req1)(), + self.serv.recv()) + + # There is pull ongoing on another instance so push should return bad request + expected_res = Lwm2mChanged.matching(req2)() + expected_res.code = coap.Code.RES_METHOD_NOT_ALLOWED + self.assertMsgEqual(expected_res, self.serv.recv()) + + # Make sure that ongoing download is finished properly + self.wait_until_state_is(Instances.APP, UpdateState.DOWNLOADED) + + +class AdvancedFirmwareUpdateHandleTooManyPulls(AdvancedFirmwareUpdate.TestWithCoapServer): + def setUp(self, coap_server=None, *args, **kwargs): + super().setUp(coap_server=[None, None], *args, **kwargs) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_LONG_FILE) + with self.get_file_server(serv=0) as file_server: + file_server.set_resource('/firmwareAPP', + self.PACKAGE) + fw_uri = file_server.get_resource_uri('/firmwareAPP') + + # Write /33629/inst/0 (Firmware URI) + req1 = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + fw_uri) + self.serv.send(req1) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + with self.get_file_server(serv=1) as file_server: + file_server.set_resource('/firmwareTEE', + self.PACKAGE) + fw_uri = file_server.get_resource_uri('/firmwareTEE') + + # Write /33629/inst/1 (Firmware URI) + req2 = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.TEE].PackageURI, + fw_uri) + self.serv.send(req2) + + # Write /33629/inst/1 (Firmware URI) + req3 = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.TEE].PackageURI, + fw_uri) + self.serv.send(req3) + + # There should be three requests already received as we didn't check it before, to not wait for client + self.assertMsgEqual(Lwm2mChanged.matching(req1)(), + self.serv.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(req2)(), + self.serv.recv()) + + # There is pull already ongoing so second one should return bad request + expected_res = Lwm2mChanged.matching(req3)() + expected_res.code = coap.Code.RES_BAD_REQUEST + self.assertMsgEqual(expected_res, self.serv.recv()) + + # Both instances still should end with DOWNLOADED state + self.wait_until_state_is(Instances.APP, UpdateState.DOWNLOADED) + self.wait_until_state_is(Instances.TEE, UpdateState.DOWNLOADED) + + +class AdvancedFirmwareUpdateHandleTooManyPullsWithSecureConnection(AdvancedFirmwareUpdate.TestWithCoapServer, + test_suite.Lwm2mDtlsSingleServerTest): + def setUp(self, coap_server=None, *args, **kwargs): + dtlserv = [coap.DtlsServer(psk_key=self.PSK_KEY, psk_identity=self.PSK_IDENTITY), + coap.DtlsServer(psk_key=self.PSK_KEY, psk_identity=self.PSK_IDENTITY)] + super().setUp(coap_server=dtlserv, *args, **kwargs) + + def runTest(self): + # Prepare package for /33629/0 + self.FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + with self.get_file_server(serv=0) as file_server: + file_server.set_resource('/firmwareAPP', + self.PACKAGE) + fw_uri = file_server.get_resource_uri('/firmwareAPP') + + # Write /33629/inst/0 (Firmware URI) + req1 = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + fw_uri) + self.serv.send(req1) + + # Prepare package for /33629/1 + self.FW_PKG_OPTS = {'magic': b'AJAY_TEE', 'version': 2} + self.prepare_package_additional_img(content=DUMMY_FILE) + with self.get_file_server(serv=1) as file_server: + file_server.set_resource('/firmwareTEE', + self.PACKAGE) + fw_uri = file_server.get_resource_uri('/firmwareTEE') + + # Write /33629/inst/1 (Firmware URI) + req2 = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.TEE].PackageURI, + fw_uri) + self.serv.send(req2) + + # Write /33629/inst/1 (Firmware URI) + req3 = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.TEE].PackageURI, + fw_uri) + self.serv.send(req3) + + # There should be three requests already received as we didn't check it before, to not wait for client + self.assertMsgEqual(Lwm2mChanged.matching(req1)(), + self.serv.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(req2)(), + self.serv.recv()) + + # There is pull already ongoing so second one should return bad request + expected_res = Lwm2mChanged.matching(req3)() + expected_res.code = coap.Code.RES_BAD_REQUEST + self.assertMsgEqual(expected_res, self.serv.recv()) + + # Both instances still should end with DOWNLOADED state + self.wait_until_state_is(Instances.APP, UpdateState.DOWNLOADED) + self.wait_until_state_is(Instances.TEE, UpdateState.DOWNLOADED) diff --git a/tests/integration/suites/default/async.py b/tests/integration/suites/default/async.py index 93a17eca7..23abf3c44 100644 --- a/tests/integration/suites/default/async.py +++ b/tests/integration/suites/default/async.py @@ -7,11 +7,7 @@ # Licensed under the AVSystem-5-clause License. # See the attached LICENSE file for details. -import socket -import time - from framework.lwm2m_test import * -from framework.test_utils import ResponseFilter class DmChangeDuringRegistration(test_suite.Lwm2mTest): @@ -146,34 +142,26 @@ def runTest(self): class AsyncNotifications: - class Test(test_suite.Lwm2mSingleServerTest, - test_suite.Lwm2mDmOperations): - def clearObservation(self, respond=False): - notify_filter = ResponseFilter(Lwm2mNotify) - self.observe(self.serv, - OID.Device, - 0, - RID.Device.CurrentTime, - observe=1, - response_filter=notify_filter) - - if respond: - for message in notify_filter.filtered_messages: - self.serv.send(Lwm2mEmpty.matching(message)()) + class Test(test_suite.Lwm2mSingleServerTest, test_suite.Lwm2mDmOperations): + def clearObservation(self, notif, respond=False): + self.observe(self.serv, OID.Device, 0, RID.Device.CurrentTime, token=notif.token, + observe=1) # consume possibly outstanding Notify interrupting clean deregister - try: - pkt = self.serv.recv(timeout_s=1.5) - self.assertIsInstance(pkt, Lwm2mNotify) - if respond: - self.serv.send(Lwm2mEmpty.matching(pkt)()) - except socket.timeout: - pass + deadline = time.time() + 1.5 + while True: + try: + pkt = self.serv.recv(deadline=deadline) + self.assertIsInstance(pkt, Lwm2mNotify) + if respond: + self.serv.send(Lwm2mEmpty.matching(pkt)()) + except socket.timeout: + break class NonconfirmableNotificationsDuringUpdate(AsyncNotifications.Test): def runTest(self): - self.observe(self.serv, OID.Device, 0, RID.Device.CurrentTime) + notif = self.observe(self.serv, OID.Device, 0, RID.Device.CurrentTime) self.communicate('send-update') received_update_requests = 0 @@ -204,9 +192,9 @@ def runTest(self): break end_time = time.time() - self.assertAlmostEqual(received_notifications, end_time - start_time, delta=1.1) + self.assertAlmostEqual(received_notifications, end_time - start_time, delta=1.5) - self.clearObservation(respond=False) + self.clearObservation(notif, respond=False) class ConfirmableNotificationsDuringUpdate(AsyncNotifications.Test): @@ -214,7 +202,7 @@ def setUp(self): super().setUp(extra_cmdline_args=['--confirmable-notifications']) def runTest(self): - self.observe(self.serv, OID.Device, 0, RID.Device.CurrentTime) + notif = self.observe(self.serv, OID.Device, 0, RID.Device.CurrentTime) self.communicate('send-update') # Confirmable notifications honor NSTART @@ -230,6 +218,7 @@ def runTest(self): if not early_notify_checked_for: early_notify_checked_for = True if isinstance(pkt, Lwm2mNotify): + self.serv.send(Lwm2mEmpty.matching(pkt)()) continue self.assertIsInstance(pkt, Lwm2mUpdate) @@ -253,55 +242,6 @@ def runTest(self): break end_time = time.time() - self.assertAlmostEqual(received_notifications, end_time - start_time, delta=1.1) - - self.clearObservation(respond=True) - - -class NonconfirmableNotificationsDuringRegister(AsyncNotifications.Test): - def runTest(self, respond_to_notifications=False): - self.observe(self.serv, OID.Device, 0, RID.Device.CurrentTime) - notify_filter = ResponseFilter(Lwm2mNotify) - - # force a Register - self.communicate('send-update') - update = self.assertDemoUpdatesRegistration(respond=False, response_filter=notify_filter) - self.serv.send(Lwm2mErrorResponse.matching(update)(coap.Code.RES_FORBIDDEN)) - register = self.assertDemoRegisters(respond=False, response_filter=notify_filter) - - if respond_to_notifications: - for message in notify_filter.filtered_messages: - self.serv.send(Lwm2mEmpty.matching(message)()) - - start_time = time.time() - received_register_requests = 1 - while received_register_requests < 3: - pkt = self.serv.recv(timeout_s=10) - self.assertMsgEqual(pkt, register) - received_register_requests += 1 - - self.serv.send(Lwm2mCreated.matching(pkt)(location=self.DEFAULT_REGISTER_ENDPOINT)) - - received_notifications = 0 - while True: - try: - pkt = self.serv.recv(timeout_s=0.5) - self.assertIsInstance(pkt, Lwm2mNotify) - received_notifications += 1 - if respond_to_notifications: - self.serv.send(Lwm2mEmpty.matching(pkt)()) - except socket.timeout: - break - - end_time = time.time() - self.assertAlmostEqual(received_notifications, end_time - start_time, delta=1.1) - - self.clearObservation(respond=respond_to_notifications) - + self.assertAlmostEqual(received_notifications, end_time - start_time, delta=1.5) -class ConfirmableNotificationsDuringRegister(NonconfirmableNotificationsDuringRegister): - def setUp(self): - super().setUp(extra_cmdline_args=['--confirmable-notifications']) - - def runTest(self): - super().runTest(respond_to_notifications=True) + self.clearObservation(notif, respond=True) diff --git a/tests/integration/suites/default/bootstrap_client.py b/tests/integration/suites/default/bootstrap_client.py index 1e7a3b78d..1294dcbb4 100644 --- a/tests/integration/suites/default/bootstrap_client.py +++ b/tests/integration/suites/default/bootstrap_client.py @@ -93,10 +93,10 @@ def perform_typical_bootstrap(self, server_iid, security_iid, server_uri, lifeti bootstrap_request_timeout_s=None): # For the first holdoff_s seconds, the client should wait for # 1.0-style Server Initiated Bootstrap. Note that we subtract - # 1 second to take into account code execution delays. + # 2 seconds to take into account code execution delays. if holdoff_s is None: holdoff_s = self.holdoff_s or 0 - no_message_s = max(0, holdoff_s - 1) + no_message_s = max(0, holdoff_s - 2) if no_message_s > 0: with self.assertRaises(socket.timeout): print(self.bootstrap_server.recv(timeout_s=no_message_s)) diff --git a/tests/integration/suites/default/bootstrap_transaction.py b/tests/integration/suites/default/bootstrap_transaction.py index 7741cf7ce..4969c0a14 100644 --- a/tests/integration/suites/default/bootstrap_transaction.py +++ b/tests/integration/suites/default/bootstrap_transaction.py @@ -13,9 +13,7 @@ class BootstrapTransactionTest(test_suite.Lwm2mTest): def setUp(self): - self.setup_demo_with_servers(servers=1, - num_servers_passed=0, - bootstrap_server=True, + self.setup_demo_with_servers(servers=1, num_servers_passed=0, bootstrap_server=True, extra_cmdline_args=['--bootstrap-timeout', '-1']) def tearDown(self): @@ -26,37 +24,34 @@ def runTest(self): # Create Server object req = Lwm2mWrite('/%d/1' % (OID.Server,), - TLV.make_resource(RID.Server.Lifetime, 60).serialize() - + TLV.make_resource(RID.Server.ShortServerID, 1).serialize() - + TLV.make_resource(RID.Server.NotificationStoring, True).serialize() - + TLV.make_resource(RID.Server.Binding, "U").serialize(), + TLV.make_resource(RID.Server.Lifetime, 60).serialize() + TLV.make_resource( + RID.Server.ShortServerID, 1).serialize() + TLV.make_resource( + RID.Server.NotificationStoring, True).serialize() + TLV.make_resource( + RID.Server.Binding, "U").serialize(), format=coap.ContentFormat.APPLICATION_LWM2M_TLV) self.bootstrap_server.send(req) - self.assertMsgEqual(Lwm2mChanged.matching(req)(), - self.bootstrap_server.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.bootstrap_server.recv()) # Create Security object regular_serv_uri = 'coap://127.0.0.1:%d' % self.servers[0].get_listen_port() - req = Lwm2mWrite('/%d/2' % (OID.Security,), - TLV.make_resource(RID.Security.ServerURI, regular_serv_uri).serialize() - + TLV.make_resource(RID.Security.Bootstrap, 0).serialize() - + TLV.make_resource(RID.Security.Mode, 3).serialize() - + TLV.make_resource(RID.Security.ShortServerID, 1).serialize() - + TLV.make_resource(RID.Security.PKOrIdentity, "").serialize() - + TLV.make_resource(RID.Security.SecretKey, "").serialize(), + req = Lwm2mWrite('/%d/2' % (OID.Security,), TLV.make_resource(RID.Security.ServerURI, + regular_serv_uri).serialize() + TLV.make_resource( + RID.Security.Bootstrap, 0).serialize() + TLV.make_resource(RID.Security.Mode, + 3).serialize() + TLV.make_resource( + RID.Security.ShortServerID, 1).serialize() + TLV.make_resource( + RID.Security.PKOrIdentity, "").serialize() + TLV.make_resource(RID.Security.SecretKey, + "").serialize(), format=coap.ContentFormat.APPLICATION_LWM2M_TLV) self.bootstrap_server.send(req) - self.assertMsgEqual(Lwm2mChanged.matching(req)(), - self.bootstrap_server.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.bootstrap_server.recv()) # create incomplete Geo-Points object req = Lwm2mWrite('/%d/42' % (OID.GeoPoints,), TLV.make_resource(RID.GeoPoints.Latitude, 42.0).serialize(), format=coap.ContentFormat.APPLICATION_LWM2M_TLV) self.bootstrap_server.send(req) - self.assertMsgEqual(Lwm2mChanged.matching(req)(), - self.bootstrap_server.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.bootstrap_server.recv()) # send Bootstrap Finish req = Lwm2mBootstrapFinish() @@ -73,17 +68,16 @@ def runTest(self): TLV.make_resource(RID.GeoPoints.Longitude, 69.0).serialize(), format=coap.ContentFormat.APPLICATION_LWM2M_TLV) self.bootstrap_server.send(req) - self.assertMsgEqual(Lwm2mChanged.matching(req)(), - self.bootstrap_server.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.bootstrap_server.recv()) class BootstrapTransactionPersistenceTest(test_suite.Lwm2mTest, test_suite.Lwm2mDmOperations): def setUp(self): self._dm_persistence_file = tempfile.NamedTemporaryFile() - self.setup_demo_with_servers( - servers=0, bootstrap_server=True, - extra_cmdline_args=['--bootstrap-timeout', '-1', - '--dm-persistence-file', self._dm_persistence_file.name]) + self.setup_demo_with_servers(servers=0, bootstrap_server=True, + extra_cmdline_args=['--bootstrap-timeout', '-1', + '--dm-persistence-file', + self._dm_persistence_file.name]) def tearDown(self): try: @@ -96,25 +90,23 @@ def runTest(self): # Create Server object without Binding req = Lwm2mWrite('/%d/1' % (OID.Server,), - TLV.make_resource(RID.Server.Lifetime, 60).serialize() - + TLV.make_resource(RID.Server.ShortServerID, 1).serialize() - + TLV.make_resource(RID.Server.NotificationStoring, True).serialize(), + TLV.make_resource(RID.Server.Lifetime, 60).serialize() + TLV.make_resource( + RID.Server.ShortServerID, 1).serialize() + TLV.make_resource( + RID.Server.NotificationStoring, True).serialize(), format=coap.ContentFormat.APPLICATION_LWM2M_TLV) self.bootstrap_server.send(req) - self.assertMsgEqual(Lwm2mChanged.matching(req)(), - self.bootstrap_server.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.bootstrap_server.recv()) # Create Security object without URI - req = Lwm2mWrite('/%d/2' % (OID.Security,), - TLV.make_resource(RID.Security.Bootstrap, 0).serialize() - + TLV.make_resource(RID.Security.Mode, 3).serialize() - + TLV.make_resource(RID.Security.ShortServerID, 1).serialize() - + TLV.make_resource(RID.Security.PKOrIdentity, "").serialize() - + TLV.make_resource(RID.Security.SecretKey, "").serialize(), + req = Lwm2mWrite('/%d/2' % (OID.Security,), TLV.make_resource(RID.Security.Bootstrap, + 0).serialize() + TLV.make_resource( + RID.Security.Mode, 3).serialize() + TLV.make_resource(RID.Security.ShortServerID, + 1).serialize() + TLV.make_resource( + RID.Security.PKOrIdentity, "").serialize() + TLV.make_resource(RID.Security.SecretKey, + "").serialize(), format=coap.ContentFormat.APPLICATION_LWM2M_TLV) self.bootstrap_server.send(req) - self.assertMsgEqual(Lwm2mChanged.matching(req)(), - self.bootstrap_server.recv()) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.bootstrap_server.recv()) # send Bootstrap Finish req = Lwm2mBootstrapFinish() @@ -127,10 +119,9 @@ def runTest(self): self.bootstrap_server.reset() - self._start_demo(['--dm-persistence-file', self._dm_persistence_file.name] - + self.make_demo_args(DEMO_ENDPOINT_NAME, [], - '1.0', '1.0', - None)) + self._start_demo( + ['--dm-persistence-file', self._dm_persistence_file.name] + self.make_demo_args( + DEMO_ENDPOINT_NAME, [], '1.0', '1.0', None)) # Demo shall launch, with the initial server configuration self.assertDemoRequestsBootstrap() @@ -166,10 +157,21 @@ def runTest(self): self.execute_resource(self.serv, OID.Server, 2, RID.Server.RequestBootstrapTrigger) self.assertDemoRequestsBootstrap() - self.serv.reset() + start_time = time.time() + notifications = 0 + # Notifications generated before Request Bootstrap might still be sent + deadline = time.time() + 1 + while True: + try: + self.assertIsInstance(self.serv.recv(deadline=deadline), Lwm2mNotify) + notifications += 1 + except socket.timeout: + break + + deadline += 4 with self.assertRaises(socket.timeout): - print(self.serv.recv(timeout_s=5)) + print(self.serv.recv(deadline=deadline)) self.serv.reset() @@ -184,16 +186,17 @@ def runTest(self): self.assertIsInstance(self.serv.recv(timeout_s=0.8), Lwm2mNotify) notifications += 1 except socket.timeout: + end_time = time.time() break - self.assertTrue(4 <= notifications <= 6) + self.assertTrue( + round(end_time - start_time - 2) <= notifications <= round(end_time - start_time + 2)) class NotificationDuringBootstrapInQueueMode(BootstrapTest.Test, test_suite.Lwm2mDtlsSingleServerTest): def setUp(self): - super().setUp(num_servers_passed=1, - extra_cmdline_args=['--binding=UQ'], + super().setUp(num_servers_passed=1, extra_cmdline_args=['--binding=UQ'], auto_register=False) # demo will perform all DTLS handshakes before sending Register self.serv.listen() @@ -205,10 +208,21 @@ def runTest(self): self.execute_resource(self.serv, OID.Server, 2, RID.Server.RequestBootstrapTrigger) self.assertDemoRequestsBootstrap() - self.serv.reset() + start_time = time.time() + + notifications = 0 + # Notifications generated before Request Bootstrap might still be sent + deadline = time.time() + 1 + while True: + try: + self.assertIsInstance(self.serv.recv(deadline=deadline), Lwm2mNotify) + notifications += 1 + except socket.timeout: + break + deadline += 4 with self.assertRaises(socket.timeout): - print(self.serv.recv(timeout_s=5)) + print(self.serv.recv(deadline=deadline)) self.serv.reset() @@ -217,15 +231,16 @@ def runTest(self): # demo will resume DTLS session without sending any LwM2M messages self.serv.listen() - notifications = 0 while True: try: self.assertIsInstance(self.serv.recv(timeout_s=0.8), Lwm2mNotify) notifications += 1 except socket.timeout: + end_time = time.time() break - self.assertTrue(4 <= notifications <= 6) + self.assertTrue( + round(end_time - start_time - 2) <= notifications <= round(end_time - start_time + 2)) class ChangeServersDuringBootstrap(BootstrapTest.Test): @@ -243,9 +258,10 @@ def runTest(self): iid = i + 2 self.write_instance(self.bootstrap_server, OID.AccessControl, i + 1000, TLV.make_resource(RID.AccessControl.TargetOID, - OID.Server).serialize() + - TLV.make_resource(RID.AccessControl.TargetIID, iid).serialize() + - TLV.make_resource(RID.AccessControl.Owner, iid).serialize()) + OID.Server).serialize() + TLV.make_resource( + RID.AccessControl.TargetIID, + iid).serialize() + TLV.make_resource(RID.AccessControl.Owner, + iid).serialize()) self.perform_bootstrap_finish() self.assertDemoRegisters(self.servers[0]) diff --git a/tests/integration/suites/default/client_block_request.py b/tests/integration/suites/default/client_block_request.py index 0dbd70332..9849ad593 100644 --- a/tests/integration/suites/default/client_block_request.py +++ b/tests/integration/suites/default/client_block_request.py @@ -373,7 +373,6 @@ def runTest(self): self.block_recv_next(expected_seq_num=0) # ignore packet - client should retry - self.serv.set_timeout(timeout_s=5) self.block_recv() @@ -390,7 +389,6 @@ def runTest(self): self.block_recv_next(expected_seq_num=(self.expected_num_blocks // 2)) # ignore packet - client should retry - self.serv.set_timeout(timeout_s=5) self.block_recv(seq_num_begin=(self.expected_num_blocks // 2)) @@ -407,7 +405,6 @@ def runTest(self): self.block_recv_next(expected_seq_num=(self.expected_num_blocks - 1)) # ignore packet - client should retry - self.serv.set_timeout(timeout_s=5) self.block_recv(seq_num_begin=(self.expected_num_blocks - 1)) diff --git a/tests/integration/suites/default/crash.py b/tests/integration/suites/default/crash.py index 61ee9f5ed..8de488b5c 100644 --- a/tests/integration/suites/default/crash.py +++ b/tests/integration/suites/default/crash.py @@ -47,8 +47,6 @@ def runTest(self): class CrashAfterRequestWithTokenFollowedByNoToken(test_suite.Lwm2mSingleServerTest): def runTest(self): - self.serv.set_timeout(timeout_s=1) - # failure to clear the token from last message remembered by output # buffer combined with not overriding cached token length in case of # a zero-length token caused anjay to incorrectly assume a non-empty diff --git a/tests/integration/suites/default/disable_server.py b/tests/integration/suites/default/disable_server.py index 838292017..9ef888dea 100644 --- a/tests/integration/suites/default/disable_server.py +++ b/tests/integration/suites/default/disable_server.py @@ -16,8 +16,6 @@ def assertSocketsPolled(self, num): self.assertEqual(num, self.get_socket_count()) def runTest(self): - self.serv.set_timeout(timeout_s=1) - # Write Disable Timeout req = Lwm2mWrite(ResPath.Server[1].DisableTimeout, '6') self.serv.send(req) @@ -35,6 +33,7 @@ def runTest(self): print(self.serv.recv(timeout_s=5)) self.assertSocketsPolled(0) + self.assertFalse(self.ongoing_registration_exists()) # we should get another Register self.assertDemoRegisters(timeout_s=3) @@ -84,6 +83,8 @@ def runTest(self): with self.assertRaises(socket.timeout): print(self.servers[0].recv(timeout_s=5)) + self.assertFalse(self.ongoing_registration_exists()) + # only now the server should re-register self.assertDemoRegisters(server=self.servers[0], timeout_s=3) register_timestamp = time.time() diff --git a/tests/integration/suites/default/downloader.py b/tests/integration/suites/default/downloader.py index 8313038c2..da2724265 100644 --- a/tests/integration/suites/default/downloader.py +++ b/tests/integration/suites/default/downloader.py @@ -64,7 +64,7 @@ def setUp(self, coap_server: coap.Server = None): self.communicate('trim-servers 0') self.assertDemoDeregisters() self.file_server = (coap_server or coap.Server()) - self.file_server.set_timeout(5) + self.file_server.set_timeout(15) self.tempfile = tempfile.NamedTemporaryFile() def tearDown(self): diff --git a/tests/integration/suites/default/firmware_update.py b/tests/integration/suites/default/firmware_update.py index 4b705b990..3fb4a5100 100644 --- a/tests/integration/suites/default/firmware_update.py +++ b/tests/integration/suites/default/firmware_update.py @@ -51,6 +51,8 @@ class UpdateResult: class FirmwareUpdate: class Test(test_suite.Lwm2mSingleServerTest): + FW_PKG_OPTS = {} + def set_auto_deregister(self, auto_deregister): self.auto_deregister = auto_deregister @@ -148,17 +150,16 @@ def write_firmware_and_wait_for_download(self, firmware_uri: str, self.fail('firmware still not downloaded') - class TestWithHttpServer(Test): + class TestWithHttpServerMixin: RESPONSE_DELAY = 0 CHUNK_SIZE = sys.maxsize ETAGS = False - FW_PKG_OPTS = {} def get_firmware_uri(self): return 'http://127.0.0.1:%d%s' % ( self.http_server.server_address[1], FIRMWARE_PATH) - def provide_response(self, use_real_app=False): + def provide_response(self, use_real_app=False, other_content: bytes = None): with self._response_cv: self.assertIsNone(self._response_content) if use_real_app: @@ -166,6 +167,9 @@ def provide_response(self, use_real_app=False): firmware = f.read() self._response_content = make_firmware_package( firmware, **self.FW_PKG_OPTS) + elif other_content: + self._response_content = make_firmware_package( + other_content, **self.FW_PKG_OPTS) else: self._response_content = make_firmware_package( self.FIRMWARE_SCRIPT_CONTENT, **self.FW_PKG_OPTS) @@ -199,7 +203,10 @@ def chunks(data): for chunk in chunks(response_content): time.sleep(test_case.RESPONSE_DELAY) - self.wfile.write(chunk) + try: + self.wfile.write(chunk) + except BrokenPipeError: + pass self.wfile.flush() def log_request(self, code='-', size='-'): @@ -233,7 +240,10 @@ def tearDown(self): self.http_server.shutdown() self.server_thread.join() - class TestWithTlsServer(Test): + class TestWithHttpServer(TestWithHttpServerMixin, Test): + pass + + class TestWithTlsServerMixin: @staticmethod def _generate_key(): from cryptography.hazmat.backends import default_backend @@ -275,9 +285,9 @@ def _generate_cert(private_key, public_key, issuer_cn, cn='127.0.0.1', alt_ip=No @staticmethod def _generate_cert_and_key( cn='127.0.0.1', alt_ip='127.0.0.1', ca=False): - key = FirmwareUpdate.TestWithTlsServer._generate_key() - cert = FirmwareUpdate.TestWithTlsServer._generate_cert(key, key.public_key(), cn, cn, - alt_ip, ca) + key = FirmwareUpdate.TestWithTlsServerMixin._generate_key() + cert = FirmwareUpdate.TestWithTlsServerMixin._generate_cert(key, key.public_key(), cn, cn, + alt_ip, ca) return cert, key @staticmethod @@ -285,7 +295,7 @@ def _generate_pem_cert_and_key( cn='127.0.0.1', alt_ip='127.0.0.1', ca=False): from cryptography.hazmat.primitives import serialization - cert, key = FirmwareUpdate.TestWithTlsServer._generate_cert_and_key( + cert, key = FirmwareUpdate.TestWithTlsServerMixin._generate_cert_and_key( cn, alt_ip, ca) cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM) key_pem = key.private_bytes(encoding=serialization.Encoding.PEM, @@ -293,7 +303,7 @@ def _generate_pem_cert_and_key( encryption_algorithm=serialization.NoEncryption()) return cert_pem, key_pem - def setUp(self, pass_cert_to_demo=True, **kwargs): + def setUp(self, pass_cert_to_demo=True, cmd_arg='', **kwargs): cert_kwargs = {} for key in ('cn', 'alt_ip'): if key in kwargs: @@ -317,7 +327,7 @@ def setUp(self, pass_cert_to_demo=True, **kwargs): extra_cmdline_args += kwargs['extra_cmdline_args'] del kwargs['extra_cmdline_args'] if pass_cert_to_demo: - extra_cmdline_args += ['--fw-cert-file', self._cert_file] + extra_cmdline_args += [cmd_arg, self._cert_file] super().setUp(extra_cmdline_args=extra_cmdline_args, **kwargs) def tearDown(self): @@ -334,7 +344,11 @@ def unlink_without_err(fname): unlink_without_err(self._cert_file) unlink_without_err(self._key_file) - class TestWithHttpsServer(TestWithTlsServer, TestWithHttpServer): + class TestWithTlsServer(TestWithTlsServerMixin, Test): + def setUp(self, pass_cert_to_demo=True, cmd_arg='', **kwargs): + super().setUp(pass_cert_to_demo, cmd_arg='--fw-cert-file', **kwargs) + + class TestWithHttpsServerMixin: def get_firmware_uri(self): http_uri = super().get_firmware_uri() assert http_uri[:5] == 'http:' @@ -347,22 +361,37 @@ def _create_server(self): server_side=True) return http_server - class TestWithCoapServer(Test): + class TestWithHttpsServer(TestWithTlsServer, TestWithHttpsServerMixin, TestWithHttpServer): + pass + + class TestWithCoapServerMixin: def setUp(self, coap_server=None, *args, **kwargs): super().setUp(*args, **kwargs) - self.server_thread = CoapFileServerThread(coap_server=coap_server) - self.server_thread.start() + if type(coap_server) is not list: + coap_server = [coap_server] + + self.server_thread = [] + for i, server in enumerate(coap_server): + self.server_thread.append(CoapFileServerThread(coap_server=server)) + self.server_thread[i].start() @property def file_server(self): - return self.server_thread.file_server + return self.server_thread[0].file_server + + def get_file_server(self, serv): + return self.server_thread[serv].file_server def tearDown(self): try: super().tearDown() finally: - self.server_thread.join() + for s in self.server_thread: + s.join() + + class TestWithCoapServer(TestWithCoapServerMixin, Test): + pass class DemoArgsExtractorMixin: def _get_valgrind_args(self): @@ -425,8 +454,7 @@ def send(self, *args, **kwargs): make_firmware_package(self.FIRMWARE_SCRIPT_CONTENT)) self.fw_uri = file_server.get_resource_uri('/firmware') - class TestWithPartialHttpDownloadAndRestart(TestWithPartialDownloadAndRestart, - TestWithHttpServer): + class TestWithPartialHttpDownloadAndRestartMixin: def get_etag(self, response_content): return '"%d"' % zlib.crc32(response_content) @@ -477,7 +505,10 @@ def do_GET(self): while offset < len(response_content): chunk = response_content[offset:offset + 1024] - self.wfile.write(chunk) + try: + self.wfile.write(chunk) + except BrokenPipeError: + pass offset += len(chunk) time.sleep(0.5) @@ -493,6 +524,11 @@ def handle_error(self, *args, **kwargs): return SilentServer(('', 0), FirmwareRequestHandler) + class TestWithPartialHttpDownloadAndRestart(TestWithPartialHttpDownloadAndRestartMixin, + TestWithPartialDownloadAndRestart, + TestWithHttpServer): + pass + class FirmwareUpdatePackageTest(FirmwareUpdate.Test): def setUp(self): @@ -543,8 +579,6 @@ def setUp(self): self.set_reset_machine(False) def runTest(self): - self.serv.set_timeout(timeout_s=1) - # disable minimum notification period write_attrs_req = Lwm2mWriteAttributes( ResPath.FirmwareUpdate.State, query=['pmin=0']) @@ -596,8 +630,6 @@ def setUp(self): self.set_expect_send_after_state_machine_reset(True) def runTest(self): - self.serv.set_timeout(timeout_s=3) - self.assertEqual(self.read_state(), UpdateState.IDLE) # Write /5/0/1 (Firmware URI) @@ -1387,7 +1419,7 @@ def runTest(self): self.serv.reset() self.communicate('reconnect') self.provide_response() - self.assertDemoRegisters(self.serv) + self.assertDemoRegisters(self.serv, timeout_s=5) # wait until client downloads the firmware deadline = time.time() + 20 @@ -1935,11 +1967,11 @@ def runTest(self): self.serv.recv()) self.start_download() - dl_get0_0 = self.serv.recv() - dl_get0_1 = self.serv.recv() # retry + dl_get0_0 = self.serv.recv(filter=CoapGet._pkt_matches) + dl_get0_1 = self.serv.recv(filter=CoapGet._pkt_matches) # retry self.handle_get(dl_get0_0) self.assertEqual(dl_get0_0.msg_id, dl_get0_1.msg_id) - dl_get1_0 = self.serv.recv() + dl_get1_0 = self.serv.recv(filter=CoapGet._pkt_matches) # lifetime expired, demo re-registers self.assertDemoRegisters(lifetime=self.LIFETIME) diff --git a/tests/integration/suites/default/msg_cache.py b/tests/integration/suites/default/msg_cache.py index 58ebe8769..31f7d24b5 100644 --- a/tests/integration/suites/default/msg_cache.py +++ b/tests/integration/suites/default/msg_cache.py @@ -17,8 +17,6 @@ class CacheTest(test_suite.Lwm2mSingleServerTest, test_suite.Lwm2mDmOperations): def setUp(self): super().setUp(extra_cmdline_args=['--cache-size', '4096']) - - self.serv.set_timeout(timeout_s=1) self.create_instance(self.serv, oid=OID.Test, iid=1) def runTest(self): @@ -65,9 +63,6 @@ def setUp(self): self.setup_demo_with_servers(servers=2, extra_cmdline_args=['--cache-size', '4096']) - self.servers[0].set_timeout(timeout_s=1) - self.servers[1].set_timeout(timeout_s=1) - def tearDown(self): self.teardown_demo_with_servers() diff --git a/tests/integration/suites/default/notification_timestamps.py b/tests/integration/suites/default/notification_timestamps.py index d9ffe953d..539b73168 100644 --- a/tests/integration/suites/default/notification_timestamps.py +++ b/tests/integration/suites/default/notification_timestamps.py @@ -28,7 +28,7 @@ def runTest(self): self.assertEqual(len(res['e']), 1) self.assertNotIn('n', res['e'][0]) self.assertEqual(res['e'][0]['v'], 0) - self.assertAlmostEqual(res['e'][0]['t'], time.time(), 0) + self.assertAlmostEqual(res['e'][0]['t'], time.time(), delta=2) self.execute_resource( self.serv, @@ -39,7 +39,7 @@ def runTest(self): self.assertIsInstance(notification, Lwm2mNotify) res2 = as_json(notification) self.assertEqual(res2['e'][0]['v'], 1) - self.assertAlmostEqual(res2['e'][0]['t'], time.time(), 0) + self.assertAlmostEqual(res2['e'][0]['t'], time.time(), delta=2) self.assertGreater(res2['e'][0]['t'], res['e'][0]['t']) # Check if the responses have identical structure @@ -59,7 +59,7 @@ def runTest(self): self.assertEqual(res[0]['bn'], ResPath.Test[0].Counter) self.assertNotIn('n', res[0]) self.assertEqual(res[0]['v'], 0) - self.assertAlmostEqual(res[0]['bt'], time.time(), 0) + self.assertAlmostEqual(res[0]['bt'], time.time(), delta=2) self.execute_resource( self.serv, @@ -70,7 +70,7 @@ def runTest(self): self.assertIsInstance(notification, Lwm2mNotify) res2 = as_json(notification) self.assertEqual(res2[0]['v'], 1) - self.assertAlmostEqual(res2[0]['bt'], time.time(), 0) + self.assertAlmostEqual(res2[0]['bt'], time.time(), delta=2) self.assertGreater(res2[0]['bt'], res[0]['bt']) # Check if the responses have identical structure @@ -92,7 +92,7 @@ def runTest(self): self.assertNotIn(SenmlLabel.NAME.value, res[0]) self.assertEqual(res[0][SenmlLabel.VALUE.value], 0) self.assertAlmostEqual( - res[0][SenmlLabel.BASE_TIME.value], time.time(), 0) + res[0][SenmlLabel.BASE_TIME.value], time.time(), delta=2) self.execute_resource( self.serv, @@ -104,7 +104,7 @@ def runTest(self): res2 = cbor2.loads(notification.content) self.assertEqual(res2[0][SenmlLabel.VALUE.value], 1) self.assertAlmostEqual( - res2[0][SenmlLabel.BASE_TIME.value], time.time(), 0) + res2[0][SenmlLabel.BASE_TIME.value], time.time(), delta=2) self.assertGreater(res2[0][SenmlLabel.BASE_TIME.value], res[0][SenmlLabel.BASE_TIME.value]) diff --git a/tests/integration/suites/default/notifications.py b/tests/integration/suites/default/notifications.py index 510ac0227..de8a1156f 100644 --- a/tests/integration/suites/default/notifications.py +++ b/tests/integration/suites/default/notifications.py @@ -60,3 +60,132 @@ def runTest(self): self.assertEqual(bytes(notify2.token), bytes(observe2.token)) self.assertNotEqual(notify1.content, notify2.content) self.serv.send(Lwm2mReset.matching(notify2)()) + + +class SelfNotifyDisabled(test_suite.Lwm2mSingleServerTest, test_suite.Lwm2mDmOperations): + def runTest(self): + self.create_instance(self.serv, oid=OID.Test, iid=42) + + self.observe(self.serv, oid=OID.Test, iid=42, rid=RID.Test.ResInt) + + self.write_resource(self.serv, oid=OID.Test, iid=42, rid=RID.Test.ResInt, content=b'42') + # no notification expected in this case + with self.assertRaises(socket.timeout): + self.serv.recv(timeout_s=3) + + +class SelfNotifyEnabled(test_suite.Lwm2mSingleServerTest, test_suite.Lwm2mDmOperations): + def setUp(self): + super().setUp(extra_cmdline_args=['--enable-self-notify']) + + def runTest(self): + self.create_instance(self.serv, oid=OID.Test, iid=42) + + observe = self.observe(self.serv, oid=OID.Test, iid=42, rid=RID.Test.ResInt) + + self.write_resource(self.serv, oid=OID.Test, iid=42, rid=RID.Test.ResInt, content=b'42') + notify = self.serv.recv() + self.assertIsInstance(notify, Lwm2mNotify) + self.assertEqual(bytes(notify.token), bytes(observe.token)) + self.assertEqual(notify.content, b'42') + + +class RegisterCancelsNotifications(test_suite.Lwm2mSingleServerTest, test_suite.Lwm2mDmOperations): + def runTest(self): + self.create_instance(self.serv, oid=OID.Test, iid=1) + self.create_instance(self.serv, oid=OID.Test, iid=2) + + self.observe(self.serv, oid=OID.Test, iid=1, rid=RID.Test.Counter) + self.execute_resource(self.serv, oid=OID.Test, iid=1, rid=RID.Test.IncrementCounter) + self.assertIsInstance(self.serv.recv(), Lwm2mNotify) + + self.observe(self.serv, oid=OID.Test, iid=2, rid=RID.Test.Counter) + self.execute_resource(self.serv, oid=OID.Test, iid=2, rid=RID.Test.IncrementCounter) + self.assertIsInstance(self.serv.recv(), Lwm2mNotify) + + self.communicate('send-update') + update = self.assertDemoUpdatesRegistration(respond=False, content=ANY) + + # Notifications shall work while Updating + self.execute_resource(self.serv, oid=OID.Test, iid=1, rid=RID.Test.IncrementCounter) + self.execute_resource(self.serv, oid=OID.Test, iid=2, rid=RID.Test.IncrementCounter) + notifications = 0 + while True: + pkt = self.serv.recv() + if notifications < 2 and isinstance(pkt, Lwm2mNotify): + notifications += 1 + if notifications == 2: + break + else: + self.assertMsgEqual(pkt, update) + + # force a Register + self.serv.send(Lwm2mErrorResponse.matching(update)(coap.Code.RES_FORBIDDEN)) + register = self.assertDemoRegisters(respond=False) + + # Notifications shall no longer work here + self.execute_resource(self.serv, oid=OID.Test, iid=1, rid=RID.Test.IncrementCounter) + self.execute_resource(self.serv, oid=OID.Test, iid=2, rid=RID.Test.IncrementCounter) + pkt = self.serv.recv() + self.assertMsgEqual(pkt, register) + + self.serv.send(Lwm2mCreated.matching(pkt)(location=self.DEFAULT_REGISTER_ENDPOINT)) + + # Check that notifications still don't work + self.execute_resource(self.serv, oid=OID.Test, iid=1, rid=RID.Test.IncrementCounter) + self.execute_resource(self.serv, oid=OID.Test, iid=2, rid=RID.Test.IncrementCounter) + with self.assertRaises(socket.timeout): + print(self.serv.recv(timeout_s=5)) + + +class LifetimeExpirationCancelsObserveWhileStoring(test_suite.Lwm2mDtlsSingleServerTest, + test_suite.Lwm2mDmOperations): + def setUp(self): + super().setUp(lifetime=30) + + def runTest(self): + expiration_time = float(self.communicate('registration-expiration-time 1', + match_regex='REGISTRATION_EXPIRATION_TIME=(.*)\n').group( + 1)) + + observe = self.observe(self.serv, oid=OID.Device, iid=0, rid=RID.Device.CurrentTime) + observe_start_time = time.time() + + notifications_generated = 0 + self.communicate('enter-offline') + # Receive the notifications that might have managed to be sent before entering offline mode + deadline = time.time() + 5 + while time.time() < deadline: + try: + self.assertIsInstance(self.serv.recv(deadline=deadline), Lwm2mNotify) + notifications_generated += 1 + except socket.timeout: + break + + # Check that the notifications are stored alright during offline mode + deadline = expiration_time + 5 + while True: + timeout_s = max(0.0, deadline - time.time()) + if self.read_log_until_match( + b'Notify for token %s scheduled:' % (binascii.hexlify(observe.token),), + timeout_s=timeout_s) is not None: + notifications_generated += 1 + else: + break + + # Assert that the notification has been implicitly cancelled + self.assertIsNotNone( + self.read_log_until_match(b'Observe cancel: %s\n' % (binascii.hexlify(observe.token),), + timeout_s=5)) + + self.assertAlmostEqual(notifications_generated, expiration_time - observe_start_time, + delta=2) + + self.communicate('exit-offline') + + # The client will register again + self.assertDtlsReconnect() + self.assertDemoRegisters(lifetime=30) + # Check that no notifications are sent + with self.assertRaises(socket.timeout): + print(self.serv.recv(timeout_s=5)) diff --git a/tests/integration/suites/default/offline.py b/tests/integration/suites/default/offline.py index 5c0f12733..2caa97529 100644 --- a/tests/integration/suites/default/offline.py +++ b/tests/integration/suites/default/offline.py @@ -51,18 +51,19 @@ def runTest(self): # client reconnects with DTLS session resumption self.assertDtlsReconnect() - exit_offline_time = time.time() notifications = 0 while True: try: - timestamp_pkt = self.serv.recv(timeout_s=0.2) + timestamp_pkt = self.serv.recv(timeout_s=0.9) self.assertEqual(timestamp_pkt.token, observe_req.token) notifications += 1 except socket.timeout: break - self.assertGreaterEqual(notifications, exit_offline_time - enter_offline_time - 1) - self.assertLessEqual(notifications, exit_offline_time - enter_offline_time + 1) + end_time = time.time() + + self.assertGreaterEqual(notifications, end_time - enter_offline_time - 1) + self.assertLessEqual(notifications, end_time - enter_offline_time + 1) # Cancel Observe req = Lwm2mObserve(ResPath.Test[0].Timestamp, observe=1, token=observe_req.token) @@ -143,11 +144,23 @@ def runTest(self): self.assertDemoUpdatesRegistration() -class OfflineWithQueueMode(retransmissions.RetransmissionTest.TestMixin, - test_suite.Lwm2mDtlsSingleServerTest): - def setUp(self): - super().setUp(extra_cmdline_args=['--binding=UQ'], binding='UQ') +class OfflineWithQueueMode: + class Test(retransmissions.RetransmissionTest.TestMixin, test_suite.Lwm2mDtlsSingleServerTest): + def setUp(self, extra_cmdline_args=None, binding='UQ', *args, **kwargs): + import subprocess + import unittest + output = subprocess.run([self._get_demo_executable(), '-e', 'dummy', '-u', 'invalid'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode( + 'utf-8') + + if 'ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE = ON' in output: + raise unittest.SkipTest('Queue mode autoclose disabled') + + super().setUp(*args, extra_cmdline_args=(extra_cmdline_args or []) + ['--binding=UQ'], + binding=binding, **kwargs) + +class OfflineWithQueueModeTest(OfflineWithQueueMode.Test): def runTest(self): self.wait_until_socket_count(0, timeout_s=self.max_transmit_wait() + 2) self.communicate('enter-offline') @@ -164,10 +177,9 @@ def runTest(self): self.assertDemoUpdatesRegistration() -class OfflineWithQueueModeScheduledUpdate(retransmissions.RetransmissionTest.TestMixin, - test_suite.Lwm2mDtlsSingleServerTest): +class OfflineWithQueueModeScheduledUpdate(OfflineWithQueueMode.Test): def setUp(self): - super().setUp(extra_cmdline_args=['--binding=UQ'], lifetime=45, auto_register=False) + super().setUp(lifetime=45, auto_register=False) def runTest(self): self.assertDemoRegisters(lifetime=45, binding='UQ') @@ -189,12 +201,7 @@ def runTest(self): self.assertDemoUpdatesRegistration() -class OfflineWithQueueModeNotify(retransmissions.RetransmissionTest.TestMixin, - test_suite.Lwm2mDtlsSingleServerTest, - test_suite.Lwm2mDmOperations): - def setUp(self): - super().setUp(extra_cmdline_args=['--binding=UQ'], binding='UQ') - +class OfflineWithQueueModeNotify(OfflineWithQueueMode.Test, test_suite.Lwm2mDmOperations): def runTest(self): token = self.observe(self.serv, OID.EventLog, 0, RID.EventLog.LogData).token self.wait_until_socket_count(0, timeout_s=self.max_transmit_wait() + 2) @@ -212,12 +219,7 @@ def runTest(self): self.assertMsgEqual(Lwm2mNotify(token=token), self.serv.recv()) -class OfflineWithQueueModeScheduledNotify(retransmissions.RetransmissionTest.TestMixin, - test_suite.Lwm2mDtlsSingleServerTest, - test_suite.Lwm2mDmOperations): - def setUp(self): - super().setUp(extra_cmdline_args=['--binding=UQ'], binding='UQ') - +class OfflineWithQueueModeScheduledNotify(OfflineWithQueueMode.Test, test_suite.Lwm2mDmOperations): def runTest(self): token = self.observe(self.serv, OID.EventLog, 0, RID.EventLog.LogData).token self.wait_until_socket_count(0, timeout_s=self.max_transmit_wait() + 2) @@ -232,12 +234,9 @@ def runTest(self): self.assertMsgEqual(Lwm2mNotify(token=token), self.serv.recv()) -class OfflineWithQueueModeScheduledSend(retransmissions.RetransmissionTest.TestMixin, - test_suite.Lwm2mDtlsSingleServerTest, - test_suite.Lwm2mDmOperations): +class OfflineWithQueueModeScheduledSend(OfflineWithQueueMode.Test, test_suite.Lwm2mDmOperations): def setUp(self): - super().setUp(extra_cmdline_args=['--binding=UQ'], auto_register=False, - minimum_version='1.1', maximum_version='1.1') + super().setUp(auto_register=False, minimum_version='1.1', maximum_version='1.1') self.assertDemoRegisters(version='1.1', lwm2m11_queue_mode=True) def runTest(self): @@ -301,3 +300,14 @@ def runTest(self): # observation got canceled, no new messages shall arrive with self.assertRaises(socket.timeout): self.serv.recv(timeout_s=OFFLINE_INTERVAL) + + +class ForceReregisterDuringOffline(test_suite.Lwm2mDtlsSingleServerTest): + def runTest(self): + self.communicate('enter-offline') + self.wait_until_socket_count(0, timeout_s=5) + self.communicate('send-register') + time.sleep(1) + self.communicate('exit-offline') + self.assertDtlsReconnect() + self.assertDemoRegisters() diff --git a/tests/integration/suites/default/queue_mode.py b/tests/integration/suites/default/queue_mode.py index 53c87f825..53324c9d7 100644 --- a/tests/integration/suites/default/queue_mode.py +++ b/tests/integration/suites/default/queue_mode.py @@ -12,12 +12,30 @@ from . import access_control, retransmissions, firmware_update +class QueueMode: + @staticmethod + def autoclose_disabled(test_case): + import subprocess + output = subprocess.run([test_case._get_demo_executable(), '-e', 'dummy', '-u', 'invalid'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode( + 'utf-8') + return 'ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE = ON' in output + + class Test(retransmissions.RetransmissionTest.TestMixin): + def setUp(self, *args, **kwargs): + self.autoclose_disabled = QueueMode.autoclose_disabled(self) + super().setUp(*args, **kwargs) + + class QueueModeBehaviour(retransmissions.RetransmissionTest.TestMixin, access_control.AccessControl.Test): PSK_IDENTITY = b'test-identity' PSK_KEY = b'test-key' def setUp(self): + import unittest + if QueueMode.autoclose_disabled(self): + raise unittest.SkipTest('Queue mode autoclose disabled') super().setUp(servers=[ Lwm2mServer(coap.DtlsServer(psk_key=self.PSK_KEY, psk_identity=self.PSK_IDENTITY)), Lwm2mServer(coap.DtlsServer(psk_key=self.PSK_KEY, psk_identity=self.PSK_IDENTITY))], @@ -81,10 +99,11 @@ def runTest(self): super().runTest() -class ForceQueueMode(retransmissions.RetransmissionTest.TestMixin, - framework.test_suite.Lwm2mSingleServerTest): +class ForceQueueMode(QueueMode.Test, framework.test_suite.Lwm2mSingleServerTest): def tearDown(self): - super().tearDown(auto_deregister=False) + auto_deregister = False + auto_deregister = self.autoclose_disabled + super().tearDown(auto_deregister=auto_deregister) def runTest(self): self.communicate('set-queue-mode-preference FORCE_QUEUE_MODE') @@ -98,6 +117,8 @@ def runTest(self): self.communicate('send-update') self.assertDemoUpdatesRegistration() + if self.autoclose_disabled: + return # effectively queue mode, even though binding is "U" time.sleep(self.max_transmit_wait() - 2) self.assertEqual(self.get_socket_count(), 1) @@ -126,6 +147,9 @@ def runTest(self): class Lwm2m11QueueMode(retransmissions.RetransmissionTest.TestMixin, framework.test_suite.Lwm2mDtlsSingleServerTest): def setUp(self): + import unittest + if QueueMode.autoclose_disabled(self): + raise unittest.SkipTest('Queue mode autoclose disabled') super().setUp(maximum_version='1.1') def runTest(self): @@ -176,8 +200,7 @@ def runTest(self): self.assertEqual(self.get_socket_count(), 1) -class Lwm2m11UQBinding(retransmissions.RetransmissionTest.TestMixin, - framework.test_suite.Lwm2mDtlsSingleServerTest): +class Lwm2m11UQBinding(QueueMode.Test, framework.test_suite.Lwm2mDtlsSingleServerTest): def setUp(self): # UQ binding is not LwM2M 1.1-compliant, but we support it anyway super().setUp(extra_cmdline_args=['--binding=UQ'], auto_register=False, @@ -185,6 +208,8 @@ def setUp(self): self.assertDemoRegisters(self.serv, version='1.1', lwm2m11_queue_mode=True) def runTest(self): + if self.autoclose_disabled: + return # default: Prefer Online Mode, queue mode time.sleep(self.max_transmit_wait() - 2) self.assertEqual(self.get_socket_count(), 1) @@ -226,6 +251,9 @@ def runTest(self): class QueueModeAfterManualReconnect(retransmissions.RetransmissionTest.TestMixin, firmware_update.SameSocketDownload.Test): def setUp(self): + import unittest + if QueueMode.autoclose_disabled(self): + raise unittest.SkipTest('Queue mode autoclose disabled') super().setUp(extra_cmdline_args=['--binding=UQ'], maximum_version='1.1', binding=None, lwm2m11_queue_mode=True, psk_identity=b'test-identity', psk_key=b'test-key') @@ -248,7 +276,16 @@ def runTest(self): self.assertEqual(self.get_socket_count(), 0) -class QueueModeAfterTimedOutSend(QueueModeAfterManualReconnect): +class QueueModeAfterTimedOutSend(QueueMode.Test, firmware_update.SameSocketDownload.Test): + def setUp(self): + super().setUp(extra_cmdline_args=['--binding=UQ'], maximum_version='1.1', binding=None, + lwm2m11_queue_mode=True, psk_identity=b'test-identity', psk_key=b'test-key') + + def tearDown(self): + auto_deregister = False + auto_deregister = self.autoclose_disabled + super().tearDown(auto_deregister=auto_deregister) + def runTest(self): self.start_download() self.serv.recv() @@ -275,4 +312,98 @@ def runTest(self): time.sleep(timeout) self.assertEqual(self.get_socket_count(), 1) time.sleep(4) + if self.autoclose_disabled: + return + self.assertEqual(self.get_socket_count(), 0) + + +class TlsQueueMode(framework.test_suite.Lwm2mSingleTcpServerTest): + PSK_IDENTITY = b'test-identity' + PSK_KEY = b'test-key' + + def setUp(self, *args, **kwargs): + import unittest + if QueueMode.autoclose_disabled(self): + raise unittest.SkipTest('Queue mode autoclose disabled') + super().setUp(extra_cmdline_args=['--tcp-request-timeout', '5'], + psk_identity=self.PSK_IDENTITY, psk_key=self.PSK_KEY, maximum_version='1.1', + binding='T') + + def runTest(self): + self.communicate('set-queue-mode-preference PREFER_QUEUE_MODE') + self.communicate('send-update') + self.assertDemoRegisters(version='1.1', binding='T', lwm2m11_queue_mode=True) + self.wait_until_socket_count(0, timeout_s=6) + + time.sleep(1) + self.serv.reset() + self.communicate('send-update') + self.serv.listen(timeout_s=5) + self.read_log_until_match(b'statefully resumed connection', timeout_s=5) + # no CSM message here + self.assertDemoUpdatesRegistration() + + +class NosecTcpQueueMode(framework.test_suite.Lwm2mSingleTcpServerTest): + def setUp(self, *args, **kwargs): + import unittest + if QueueMode.autoclose_disabled(self): + raise unittest.SkipTest('Queue mode autoclose disabled') + super().setUp(extra_cmdline_args=['--tcp-request-timeout', '5'], maximum_version='1.1', + binding='T') + + def runTest(self): + self.communicate('set-queue-mode-preference PREFER_QUEUE_MODE') + self.communicate('send-update') + self.assertDemoRegisters(version='1.1', binding='T', lwm2m11_queue_mode=True) + self.wait_until_socket_count(0, timeout_s=6) + + time.sleep(1) + self.serv.reset() + self.communicate('send-update') + self.assertTcpCsm() + self.assertDemoRegisters(version='1.1', binding='T', lwm2m11_queue_mode=True) + + +class ReconnectServerIgnoredDuringQueueMode(QueueModeAfterManualReconnect): + def runTest(self): + time.sleep(self.max_transmit_wait() - 2) + self.assertEqual(self.get_socket_count(), 1) + time.sleep(4) + self.assertEqual(self.get_socket_count(), 0) + + self.communicate('reconnect-server 1') + with self.assertRaises(socket.timeout): + self.serv.recv(timeout_s=5) + + +class ReconnectServerDuringQueueMode(retransmissions.RetransmissionTest.TestMixin, + firmware_update.SameSocketDownload.Test): + def setUp(self): + import unittest + if not QueueMode.autoclose_disabled(self): + raise unittest.SkipTest('Queue mode autoclose enabled') + super().setUp(extra_cmdline_args=['--binding=UQ'], maximum_version='1.1', binding=None, + lwm2m11_queue_mode=True, psk_identity=b'test-identity', psk_key=b'test-key') + + def runTest(self): + time.sleep(self.max_transmit_wait() + 2) + self.assertEqual(self.get_socket_count(), 1) + self.communicate('reconnect-server 1') + self.assertDtlsReconnect() + + +class ForceReregisterDuringQueueMode(QueueModeAfterManualReconnect): + def runTest(self): + time.sleep(self.max_transmit_wait() - 2) + self.assertEqual(self.get_socket_count(), 1) + time.sleep(4) + self.assertEqual(self.get_socket_count(), 0) + + self.communicate('send-register 1') + self.assertDtlsReconnect() + self.assertDemoRegisters(version='1.1', lwm2m11_queue_mode=True) + time.sleep(self.max_transmit_wait() - 2) + self.assertEqual(self.get_socket_count(), 1) + time.sleep(4) self.assertEqual(self.get_socket_count(), 0) diff --git a/tests/integration/suites/default/reboot.py b/tests/integration/suites/default/reboot.py index c1cd44ac0..285f9f8fd 100644 --- a/tests/integration/suites/default/reboot.py +++ b/tests/integration/suites/default/reboot.py @@ -16,8 +16,6 @@ def _get_valgrind_args(self): return [] def runTest(self): - self.serv.set_timeout(timeout_s=1) - # should send a response before rebooting req = Lwm2mExecute(ResPath.Device.Reboot) self.serv.send(req) diff --git a/tests/integration/suites/default/register.py b/tests/integration/suites/default/register.py index b23ad497b..0a2335852 100644 --- a/tests/integration/suites/default/register.py +++ b/tests/integration/suites/default/register.py @@ -345,6 +345,7 @@ def setUp(self): super().setUp(auto_register=False) def runTest(self): + self.assertTcpCsm() pkt = self.serv.recv() self.assertMsgEqual( Lwm2mRegister( @@ -480,10 +481,11 @@ def runTest(self): class ConcurrentRequestWhileWaitingForResponse: class TestMixin: def runTest(self): - pkt = self.serv.recv() path = '/rd?lwm2m=1.0&ep=%s<=86400' % DEMO_ENDPOINT_NAME if self.serv.transport == Transport.TCP: path += '&b=T' + self.assertTcpCsm() + pkt = self.serv.recv() self.assertMsgEqual(Lwm2mRegister(path, content=expected_content()), pkt) self.read_path(self.serv, ResPath.Device.Manufacturer) self.serv.send(Lwm2mCreated.matching(pkt)(location='/rd/demo')) @@ -509,10 +511,11 @@ def make_demo_args(self, *args, **kwargs): return args def runTest(self): - pkt = self.serv.recv() path = '/i/am/crazy/and/rd?lwm2m=i&ep=know<=it&lwm2m=1.0&ep=%s<=86400' % DEMO_ENDPOINT_NAME if self.serv.transport == Transport.TCP: path += '&b=T' + self.assertTcpCsm() + pkt = self.serv.recv() self.assertMsgEqual(Lwm2mRegister(path, content=expected_content()), pkt) self.serv.send(Lwm2mCreated.matching(pkt)(location='/some/weird/rd/point')) @@ -606,12 +609,16 @@ def runTest(self): self.serv.send(Lwm2mCreated.matching(pkt)(location=self.DEFAULT_REGISTER_ENDPOINT)) # change binding and register with TCP binding - # receive register packet in background to avoid race condition + # receive CSM packet in background to avoid race condition with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(self.tcp_serv.recv, timeout_s=10) self.write_resource(self.serv, OID.Server, 100, RID.Server.Binding, 'TU') pkt = future.result() + assert pkt.code == coap.Code.SIGNALING_CSM + self.tcp_serv.send(coap.Packet(code=coap.Code.SIGNALING_CSM, token=b'')) + + pkt = self.tcp_serv.recv() self.assertIsInstance(pkt, Lwm2mRegister) self.tcp_serv.send(Lwm2mCreated.matching(pkt)(location=self.DEFAULT_REGISTER_ENDPOINT)) diff --git a/tests/integration/suites/default/retransmissions.py b/tests/integration/suites/default/retransmissions.py index dda139d0d..c3fb37bc7 100644 --- a/tests/integration/suites/default/retransmissions.py +++ b/tests/integration/suites/default/retransmissions.py @@ -76,7 +76,7 @@ def tearDown(self): def runTest(self): # wait until ultimate failure - self.wait_until_icmp_unreachable_count(1, timeout_s=3) + self.wait_until_icmp_unreachable_count(1, timeout_s=10) # Ensure that the control is given back to the user. self.assertTrue(self.get_all_connections_failed()) @@ -84,7 +84,7 @@ def runTest(self): # attempt reconnection self._server_close_stack.close() # unclose the server socket self.communicate('reconnect') - self.assertDemoRegisters(self.serv, timeout_s=8) + self.assertDemoRegisters(self.serv, timeout_s=10) self.assertEqual(1, self.count_icmp_unreachable_packets()) @@ -151,7 +151,7 @@ def runTest(self): # Ignore register requests. for _ in range(self.MAX_RETRANSMIT + 1): self.assertDemoRegisters(respond=False, - timeout_s=self.last_retransmission_timeout()) + timeout_s=self.last_retransmission_timeout() + 5) self.wait_for_retransmission_response_timeout() @@ -362,7 +362,7 @@ def runTest(self): content=str(new_lifetime)) self.assertDemoUpdatesRegistration(lifetime=new_lifetime) # Give dumpcap a little bit of time to write to dump file. - time.sleep(self.ACK_TIMEOUT / 2) + time.sleep(self.ACK_TIMEOUT) num_initial_dtls_hs_packets = self.count_dtls_client_hello_packets() self.serv.close() @@ -442,7 +442,7 @@ def setUp(self, bootstrap_server=True, *args, **kwargs): def runTest(self): # Ignore Request Bootstrap requests. for _ in range(self.MAX_RETRANSMIT + 1): - pkt = self.bootstrap_server.recv(timeout_s=self.last_retransmission_timeout()) + pkt = self.bootstrap_server.recv(timeout_s=self.last_retransmission_timeout() + 5) self.assertIsInstance(pkt, Lwm2mRequestBootstrap) self.wait_for_retransmission_response_timeout() @@ -597,7 +597,7 @@ def runTest(self): self.assertTrue(self.get_all_connections_failed()) -class NotificationTimeoutIsIgnored: +class NotificationTimeoutCancelsObservation: class TestMixin(RetransmissionTest.TestMixin, test_suite.Lwm2mDmOperations): CONFIRMABLE_NOTIFICATIONS = True @@ -629,22 +629,19 @@ def runTest(self): time.sleep(self.last_retransmission_timeout() + 1) - # check that following notifications still trigger attempts to send the value + # check that the observation is really cancelled self.execute_resource(self.serv, oid=OID.Test, iid=1, rid=RID.Test.IncrementCounter) - pkt = self.serv.recv(timeout_s=self.last_retransmission_timeout() + 2) - self.assertIsInstance(pkt, Lwm2mNotify) - self.assertEqual(pkt.content, first_pkt.content) - self.serv.send(Lwm2mReset.matching(pkt)()) - + with self.assertRaises(socket.timeout): + print(self.serv.recv(timeout_s=self.last_retransmission_timeout() + 5)) -class NotificationDtlsTimeoutIsIgnoredTest(NotificationTimeoutIsIgnored.TestMixin, - test_suite.Lwm2mDtlsSingleServerTest): +class NotificationDtlsTimeoutCancelsObservationTest(NotificationTimeoutCancelsObservation.TestMixin, + test_suite.Lwm2mDtlsSingleServerTest): pass -class NotificationTimeoutIsIgnoredTest(NotificationTimeoutIsIgnored.TestMixin, - test_suite.Lwm2mSingleServerTest): +class NotificationTimeoutCancelsObservationTest(NotificationTimeoutCancelsObservation.TestMixin, + test_suite.Lwm2mSingleServerTest): pass diff --git a/tests/integration/suites/default/security.py b/tests/integration/suites/default/security.py index b812fe449..3fab1d351 100644 --- a/tests/integration/suites/default/security.py +++ b/tests/integration/suites/default/security.py @@ -9,10 +9,8 @@ import base64 import datetime import os -import socket import subprocess import threading -import time import cryptography import cryptography.hazmat diff --git a/tests/integration/suites/default/send.py b/tests/integration/suites/default/send.py index a325bfcae..07e25837f 100644 --- a/tests/integration/suites/default/send.py +++ b/tests/integration/suites/default/send.py @@ -235,14 +235,14 @@ def runTest(self): # demo retries with lwm2m=1.0 self.assertDemoRegisters(self.serv, version='1.0') - self.assertIsNotNone(self.read_log_until_match(b'SEND FINISHED HANDLER: -3', 1)) + self.assertIsNotNone(self.read_log_until_match(b'SEND FINISHED HANDLER: -3', 60)) class CleanupWhileThereIsDeferredSend(Send.Test): def tearDown(self): try: self.request_demo_shutdown() - self.assertIsNotNone(self.read_log_until_match(b'SEND FINISHED HANDLER: -2', 1)) + self.assertIsNotNone(self.read_log_until_match(b'SEND FINISHED HANDLER: -2', 60)) finally: super().tearDown(auto_deregister=False) @@ -265,7 +265,7 @@ def runTest(self): self.communicate('send_deferrable 1 %s' % ResPath.Device.ModelNumber) self.communicate('trim-servers 0') - self.assertIsNotNone(self.read_log_until_match(b'SEND FINISHED HANDLER: -3', 1)) + self.assertIsNotNone(self.read_log_until_match(b'SEND FINISHED HANDLER: -3', 60)) class ServerRemovedWhileAwaitingResponseToDeferredSend(Send.Test): @@ -287,7 +287,7 @@ def runTest(self): self.communicate('trim-servers 0') self.assertDemoDeregisters() - self.assertIsNotNone(self.read_log_until_match(b'SEND FINISHED HANDLER: -2', 1)) + self.assertIsNotNone(self.read_log_until_match(b'SEND FINISHED HANDLER: -2', 60)) class QueueModeSend(retransmissions.RetransmissionTest.TestMixin, Send.Test): @@ -295,6 +295,15 @@ class QueueModeSend(retransmissions.RetransmissionTest.TestMixin, Send.Test): PSK_KEY = b'test-key' def setUp(self): + import subprocess + import unittest + output = subprocess.run([self._get_demo_executable(), '-e', 'dummy', '-u', 'invalid'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode( + 'utf-8') + + if 'ANJAY_WITHOUT_QUEUE_MODE_AUTOCLOSE = ON' in output: + raise unittest.SkipTest('Queue mode autoclose disabled') + super().setUp(psk_identity=self.PSK_IDENTITY, psk_key=self.PSK_KEY, extra_cmdline_args=['--binding=UQ'], auto_register=False) self.assertDemoRegisters(version='1.1', lwm2m11_queue_mode=True) diff --git a/tests/integration/suites/default/separate_response.py b/tests/integration/suites/default/separate_response.py index 9f4a7683a..7debf1cc9 100644 --- a/tests/integration/suites/default/separate_response.py +++ b/tests/integration/suites/default/separate_response.py @@ -99,7 +99,12 @@ def runTest(self): class SeparateResponseToSendTimeoutTest(test_suite.Lwm2mSingleServerTest): def setUp(self): - super().setUp(maximum_version='1.1') + super().setUp(maximum_version='1.1', + extra_cmdline_args=[ + '--ack-random-factor', '1', + '--ack-timeout', '1', + '--max-retransmit', '1' + ]) def runTest(self): self.communicate('send 1 %s' % (ResPath.Server[1].Lifetime,)) @@ -109,9 +114,12 @@ def runTest(self): # Separate Response: empty ACK self.serv.send(Lwm2mEmpty.matching(req)()) - # Separate Response timeout in CoAP2 is arbitrarily set to 5 minutes + # Separate Response timeout in CoAP2 is EXCHANGE_LIFETIME, which is effectively: + # ACK_TIMEOUT * (2 ** MAX_RETRANSMIT) * ACK_RANDOM_FACTOR + 200 + # Unfortunately the 200 part is hardcoded, + # based on the value of MAX_LATENCY given in RFC 7252 with self.assertRaises(socket.timeout, msg='unexpected message'): - print(self.serv.recv(timeout_s=305)) + print(self.serv.recv(timeout_s=210)) # Separate Response req = Lwm2mChanged(msg_id=(req.msg_id * 2) % (2 ** 16), token=req.token) diff --git a/tests/integration/suites/default/test_object.py b/tests/integration/suites/default/test_object.py index 2159e74ee..d61574381 100644 --- a/tests/integration/suites/default/test_object.py +++ b/tests/integration/suites/default/test_object.py @@ -18,8 +18,6 @@ class TestCase(test_suite.Lwm2mSingleServerTest): def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) - self.serv.set_timeout(timeout_s=1) - req = Lwm2mCreate('/%d' % (OID.Test,)) self.serv.send(req) self.assertMsgEqual(Lwm2mCreated.matching(req)(), diff --git a/tests/integration/suites/default/update.py b/tests/integration/suites/default/update.py index cd90159c8..be7f9fba7 100644 --- a/tests/integration/suites/default/update.py +++ b/tests/integration/suites/default/update.py @@ -7,24 +7,15 @@ # Licensed under the AVSystem-5-clause License. # See the attached LICENSE file for details. -import socket -import time -import unittest - from framework.lwm2m_test import * class UpdateTest(test_suite.Lwm2mSingleServerTest): def runTest(self): - self.serv.set_timeout(timeout_s=1) - # should send a correct Update self.communicate('send-update') pkt = self.serv.recv() - self.assertMsgEqual(Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, - query=[], - content=b''), - pkt) + self.assertMsgEqual(Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, query=[], content=b''), pkt) self.serv.send(Lwm2mChanged.matching(pkt)()) @@ -64,8 +55,6 @@ def tearDown(self): class ReconnectTest(test_suite.Lwm2mDtlsSingleServerTest): def runTest(self): - self.serv.set_timeout(timeout_s=1) - self.communicate('reconnect') # server is connected, so only a packet from the same remote port will pass this assertion @@ -76,22 +65,17 @@ class UpdateFallbacksToRegisterAfterLifetimeExpiresTest(test_suite.Lwm2mSingleSe LIFETIME = 4 def setUp(self): - super().setUp(auto_register=False, - lifetime=self.LIFETIME, - extra_cmdline_args=['--ack-random-factor', '1', - '--ack-timeout', '1', + super().setUp(auto_register=False, lifetime=self.LIFETIME, + extra_cmdline_args=['--ack-random-factor', '1', '--ack-timeout', '1', '--max-retransmit', '1']) self.assertDemoRegisters(lifetime=self.LIFETIME) def runTest(self): self.assertDemoUpdatesRegistration(timeout_s=self.LIFETIME / 2 + 1) - self.assertDemoUpdatesRegistration( - timeout_s=self.LIFETIME / 2 + 1, respond=False) - self.assertDemoUpdatesRegistration( - timeout_s=self.LIFETIME / 2 + 1, respond=False) - self.assertDemoRegisters( - lifetime=self.LIFETIME, timeout_s=self.LIFETIME / 2 + 1) + self.assertDemoUpdatesRegistration(timeout_s=self.LIFETIME / 2 + 1, respond=False) + self.assertDemoUpdatesRegistration(timeout_s=self.LIFETIME / 2 + 1, respond=False) + self.assertDemoRegisters(lifetime=self.LIFETIME, timeout_s=self.LIFETIME / 2 + 1) class UpdateFallbacksToRegisterAfterCoapClientErrorResponse(test_suite.Lwm2mSingleServerTest): @@ -100,8 +84,7 @@ def check(code: coap.Code): self.communicate('send-update') req = self.serv.recv() - self.assertMsgEqual(Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, - content=b''), req) + self.assertMsgEqual(Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, content=b''), req) self.serv.send(Lwm2mErrorResponse.matching(req)(code)) self.assertDemoRegisters() @@ -119,8 +102,6 @@ def tearDown(self): super().tearDown(auto_deregister=False) def runTest(self): - self.serv.set_timeout(timeout_s=1) - # should send an Update with reconnect self.communicate('reconnect') self.serv.reset() @@ -128,8 +109,7 @@ def runTest(self): pkt = self.serv.recv() self.assertMsgEqual(Lwm2mRegister('/rd?lwm2m=1.0&ep=%s<=86400' % (DEMO_ENDPOINT_NAME,)), pkt) - self.serv.send(Lwm2mErrorResponse.matching(pkt)( - code=coap.Code.RES_INTERNAL_SERVER_ERROR)) + self.serv.send(Lwm2mErrorResponse.matching(pkt)(code=coap.Code.RES_INTERNAL_SERVER_ERROR)) # client should abort with self.assertRaises(socket.timeout): @@ -143,8 +123,6 @@ def tearDown(self): super().tearDown(auto_deregister=False) def runTest(self): - self.serv.set_timeout(timeout_s=1) - # should try to resume DTLS session with self.serv.fake_close(): self.communicate('reconnect') @@ -164,10 +142,7 @@ def runTest(self): self.communicate('send-update') pkt = self.serv.recv() - self.assertMsgEqual(Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, - query=[], - content=b''), - pkt) + self.assertMsgEqual(Lwm2mUpdate(self.DEFAULT_REGISTER_ENDPOINT, query=[], content=b''), pkt) self.read_path(self.serv, ResPath.Device.Manufacturer) @@ -192,9 +167,9 @@ def runTest(self): class NoUpdateDuringShutdownTest(test_suite.Lwm2mSingleServerTest): def runTest(self): - self.communicate('schedule-update-on-exit') - # tearDown() expects a De-Register operation and will fail on - # unexpected Update + self.communicate( + 'schedule-update-on-exit') # tearDown() expects a De-Register operation and will fail on # unexpected Update + class ExternalSetLifetimeForcesUpdate(test_suite.Lwm2mSingleServerTest): def runTest(self): @@ -208,3 +183,22 @@ def runTest(self): self.communicate('set-lifetime 1 86400') with self.assertRaises(socket.timeout): self.serv.recv(timeout_s=3) + + +class UpdateImmediatelyDisabledTest(test_suite.Lwm2mSingleServerTest, test_suite.Lwm2mDmOperations): + def runTest(self): + self.create_instance(self.serv, OID.Test, iid=1) + # no Update message expected in this case + with self.assertRaises(socket.timeout): + self.serv.recv(timeout_s=3) + + +class UpdateImmediatelyEnabledTest(test_suite.Lwm2mSingleServerTest, test_suite.Lwm2mDmOperations): + def setUp(self): + super().setUp(extra_cmdline_args=['--update-immediately-on-dm-change']) + + def runTest(self): + self.create_instance(self.serv, OID.Test, iid=42) + # Update message shall be automatically generated + pkt = self.assertDemoUpdatesRegistration(self.serv, content=ANY) + self.assertIn(f''.encode(), pkt.content) diff --git a/tests/integration/suites/default/write_composite.py b/tests/integration/suites/default/write_composite.py index a3849cebb..1d2e22504 100644 --- a/tests/integration/suites/default/write_composite.py +++ b/tests/integration/suites/default/write_composite.py @@ -6,7 +6,7 @@ # # Licensed under the AVSystem-5-clause License. # See the attached LICENSE file for details. - +import base64 import json from framework.lwm2m_test import * @@ -106,6 +106,46 @@ def runTest(self): ]) +class WriteCompositeBasenameOnly(Test.WriteComposite): + def runTest(self): + self.create_instance(self.serv, oid=OID.Test, iid=IID + 1) + self.create_instance(self.serv, oid=OID.Test, iid=IID + 2) + request = [ + { + 'vd': base64.encodebytes(b'test').strip().rstrip(b'=').decode(), + 'bn': '/%d/%d/%d' % (OID.Test, IID, RID.Test.ResRawBytes) + }, + { + 'vd': base64.encodebytes(b'hurrdurr').strip().rstrip(b'=').decode(), + 'bn': '/%d/%d/%d' % (OID.Test, IID + 1, RID.Test.ResRawBytes) + }, + { + 'vd': base64.encodebytes(b'herpderp').strip().rstrip(b'=').decode(), + 'bn': '/%d/%d/%d' % (OID.Test, IID + 2, RID.Test.ResRawBytes) + } + ] + + self.write_composite(self.serv, content=json.dumps(request), + format=coap.ContentFormat.APPLICATION_LWM2M_SENML_JSON) + res = self.read_composite(self.serv, paths=[entry['bn'] for entry in request], + accept=coap.ContentFormat.APPLICATION_LWM2M_SENML_JSON) + self.assertEqual(json.loads(res.content.decode()), [ + { + 'bn': f'/{OID.Test}', + 'n': f'/{IID}/{RID.Test.ResRawBytes}', + 'vd': request[0]['vd'] + }, + { + 'n': f'/{IID + 1}/{RID.Test.ResRawBytes}', + 'vd': request[1]['vd'] + }, + { + 'n': f'/{IID + 2}/{RID.Test.ResRawBytes}', + 'vd': request[2]['vd'] + } + ]) + + class WriteCompositeUnsupportedContentFromat(Test.WriteComposite): def runTest(self): self.write_composite(self.serv, content='nothing special', diff --git a/tests/integration/suites/sensitive/advanced_firmware_update.py b/tests/integration/suites/sensitive/advanced_firmware_update.py new file mode 100644 index 000000000..db52b5c06 --- /dev/null +++ b/tests/integration/suites/sensitive/advanced_firmware_update.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017-2023 AVSystem +# AVSystem Anjay LwM2M SDK +# All rights reserved. +# +# Licensed under the AVSystem-5-clause License. +# See the attached LICENSE file for details. + +import os +import re + +from framework.lwm2m_test import * + +from suites.default.advanced_firmware_update import AdvancedFirmwareUpdate, equal_chunk_splitter +from suites.default.advanced_firmware_update import UpdateResult, UpdateState, Instances + + +class AdvancedFirmwareUpdateWithoutReboot(AdvancedFirmwareUpdate.BlockTest): + def runTest(self): + with open(os.path.join(self.config.demo_path, self.config.demo_cmd), 'rb') as f: + firmware = f.read() + + # Write /33629/0/0 (Package) + self.block_send(firmware, + equal_chunk_splitter(chunk_size=1024), + force_error=FirmwareUpdateForcedError.DoNothing) + + # Execute /33629/0/2 (Update) + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + # Wait until internal state machine is updated + # We cannot rely on FirmwareUpdate.State resource because it is updated first + # and the user code is only notified later, via a scheduler job + self.read_log_until_match(regex=re.escape(b'*** FIRMWARE UPDATE:'), timeout_s=5) + + self.assertEqual( + self.read_path(self.serv, ResPath.AdvancedFirmwareUpdate[Instances.APP].State).content.decode(), + str(UpdateState.UPDATING)) + + self.communicate('set-afu-result ' + str(UpdateResult.SUCCESS)) + + self.assertEqual( + self.read_path(self.serv, ResPath.AdvancedFirmwareUpdate[Instances.APP].State).content.decode(), + str(UpdateState.IDLE)) + self.assertEqual(self.read_path(self.serv, ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult).content, + str(UpdateResult.SUCCESS).encode()) diff --git a/tests/integration/suites/sensitive/bootstrap_client.py b/tests/integration/suites/sensitive/bootstrap_client.py index 39807ec79..11d8a0716 100644 --- a/tests/integration/suites/sensitive/bootstrap_client.py +++ b/tests/integration/suites/sensitive/bootstrap_client.py @@ -41,6 +41,7 @@ def test_bootstrap_backoff(self, num_attempts): # discarded just before we get the new Client Hello. holdoff_s = 0 + last_time = time.time() for attempt in range(num_attempts): # Create Security Object instance with deliberately wrong keys self.perform_typical_bootstrap(server_iid=1, @@ -50,7 +51,8 @@ def test_bootstrap_backoff(self, num_attempts): secure_key=self.PSK_KEY + b'durr', security_mode=SecurityMode.PreSharedKey, finish=False, - holdoff_s=holdoff_s) + holdoff_s=max(last_time + holdoff_s - time.time(), 0)) + last_time = time.time() self.serv.reset() self.perform_bootstrap_finish() diff --git a/tests/integration/suites/sensitive/update.py b/tests/integration/suites/sensitive/update.py index 7a883424c..f653c2100 100644 --- a/tests/integration/suites/sensitive/update.py +++ b/tests/integration/suites/sensitive/update.py @@ -20,7 +20,6 @@ def setUp(self): self.setup_demo_with_servers(servers=0, bootstrap_server=True) def runTest(self): - self.bootstrap_server.set_timeout(timeout_s=1) pkt = self.bootstrap_server.recv() self.assertMsgEqual(Lwm2mRequestBootstrap(endpoint_name=DEMO_ENDPOINT_NAME), pkt) diff --git a/tests/integration/suites/testfest/dm/advanced_firmware_update.py b/tests/integration/suites/testfest/dm/advanced_firmware_update.py new file mode 100644 index 000000000..fbe531b99 --- /dev/null +++ b/tests/integration/suites/testfest/dm/advanced_firmware_update.py @@ -0,0 +1,1267 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017-2023 AVSystem +# AVSystem Anjay LwM2M SDK +# All rights reserved. +# +# Licensed under the AVSystem-5-clause License. +# See the attached LICENSE file for details. + +import contextlib +import http +import os +import resource +import socket +import threading +import time + +from framework.lwm2m_test import * +from .utils import DataModel, ValueValidator as VV + + +class Instances: + APP = 0 + TEE = 1 + BOOT = 2 + MODEM = 3 + + +# All test cases in this file are derived from their counterparts placed in +# dm/firmware_update.py. They are based directly on requirements and cases +# contained in OMA-ETS-Lwm2m document. Note that Advanced Firmware Update object +# (33629) is not standardized by OMA and as such it is not mentioned in +# OMA-ETS-Lwm2m document. That is why tests in this file are based on cases +# created for standard Firmware Update object (OID = 5). + + +class AdvancedFirmwareUpdate: + class Test(DataModel.Test): + FW_PKG_OPTS = {'magic': b'AJAY_APP', 'version': 2, 'linked': []} + + def collect_values(self, path: Lwm2mPath, final_value, max_iterations=300, step_time=0.1): + observed_values = [] + orig_timeout = self.serv.get_timeout() + try: + deadline = time.time() + max_iterations * step_time + while True: + timeout = max(deadline - time.time(), 0.0) + self.serv.set_timeout(timeout) + try: + state = self.test_read(path) + except socket.timeout: + break + observed_values.append(state) + if state == final_value: + break + time.sleep(step_time) + return observed_values + finally: + self.serv.set_timeout(orig_timeout) + + def setUp(self, extra_cmdline_args=[]): + self.ANJAY_MARKER_FILE = generate_temp_filename(dir='/tmp', prefix='anjay-afu-marked-') + self.ORIGINAL_IMG_FILE = generate_temp_filename(dir='/tmp', prefix='anjay-afu-bootloader-') + super().setUp(afu_marker_path=self.ANJAY_MARKER_FILE, + afu_original_img_file_path=self.ORIGINAL_IMG_FILE, + extra_cmdline_args=extra_cmdline_args) + + def tearDown(self): + # reset the state machine + # Write /33629/0/1 (Firmware URI) + req = Lwm2mWrite(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, '') + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), self.serv.recv()) + super().tearDown() + + class TestWithCoapServer(Test): + def setUp(self, coap_server=None, extra_cmdline_args=[]): + super().setUp(extra_cmdline_args=extra_cmdline_args) + + from framework.coap_file_server import CoapFileServerThread + self.server_thread = CoapFileServerThread(coap_server=coap_server) + self.server_thread.start() + + @property + def file_server(self): + return self.server_thread.file_server + + def tearDown(self): + try: + super().tearDown() + finally: + self.server_thread.join() + + +class AdvancedFirmwareUpdateWithHttpServer: + class Test(AdvancedFirmwareUpdate.Test): + FIRMWARE_PATH = '/firmware' + HTTP_SERVER_CLASS = http.server.HTTPServer + + def get_firmware_uri(self): + return 'http://127.0.0.1:%d%s' % (self.http_server.server_address[1], self.FIRMWARE_PATH) + + def before_download(self): + pass + + def during_download(self, request_handler): + pass + + def setUp(self, firmware_package): + super().setUp() + + test_case = self + + class FirmwareRequestHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + test_case.requests.append(self.path) + test_case.before_download() + + self.send_response(http.HTTPStatus.OK) + self.send_header('Content-type', 'application/octet-stream') + self.send_header('Content-length', len(firmware_package)) + self.end_headers() + + # give the test some time to read "Downloading" state + time.sleep(1) + + test_case.during_download(self) + try: + self.wfile.write(firmware_package) + except BrokenPipeError: + pass + + def log_request(code='-', size='-'): + # don't display logs on successful request + pass + + self.requests = [] + self.http_server = self.HTTP_SERVER_CLASS(('', 0), FirmwareRequestHandler) + + self.server_thread = threading.Thread(target=lambda: self.http_server.serve_forever()) + self.server_thread.start() + + def tearDown(self): + try: + super().tearDown() + finally: + self.http_server.shutdown() + self.server_thread.join() + + # there should be exactly one request + self.assertEqual([self.FIRMWARE_PATH], self.requests) + + +class Test751_AdvancedFirmwareUpdate_QueryingTheReadableResources(AdvancedFirmwareUpdate.Test): + def runTest(self): + # 1. READ (CoAP GET) operation is performed on the Advanced Firmware Update + # Object Instance + # + # A. In test step 1, the Server receives the status code "2.05" for + # READ operation success + # B. In test step 1, the returned values regarding State (ID:3) and + # Update Result (ID:5) prove the Client FW update Capability is in + # initial state (State=Idle & Update Result= Initial Value). + # C. In test step 1, the returned values regarding Advanced Firmware Update + # Protocol Support (ID:8) & Advanced Firmware Update Delivery Method + # (ID:9) allow to determine the supported characteristics of the + # Client FW Update Capability. + self.test_read('/%d/0' % OID.AdvancedFirmwareUpdate, + VV.tlv_instance( + resource_validators={ + RID.AdvancedFirmwareUpdate.State: VV.from_raw_int(0), + RID.AdvancedFirmwareUpdate.UpdateResult: VV.from_raw_int(0), + RID.AdvancedFirmwareUpdate.FirmwareUpdateProtocolSupport: VV.multiple_resource( + VV.from_values(b'\x00', b'\x01', b'\x02', b'\x03', b'\x04', b'\x05')), + RID.AdvancedFirmwareUpdate.FirmwareUpdateDeliveryMethod: VV.from_raw_int(2), + }, + ignore_extra=True)) + + +class Test755_AdvancedFirmwareUpdate_SettingTheWritableResourcePackage(AdvancedFirmwareUpdate.Test): + def runTest(self): + # 1. A WRITE (CoAP PUT) operation with a NULL value ('\0') is + # performed by the Server on the Package Resource (ID:0) of the + # FW Update Object Instance + # + # A. In test step 1, the Server receives the success message "2.04" + # associated with the WRITE operation + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, b'\0', + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + + # 2. The Server READs (CoAP GET) the FW Object Instance to get + # the values of the State (ID:3) and Update Result (ID:5) Resources + # + # B. In test step 2, the Server receives the success message "2.05" along + # with the value of State and Update Result Resources values. + # C. In test step 2, the queried State and Update Result Resources values + # are both 0 (Idle / Initial value): FW Update Object Instance is in the + # Initial state. + self.test_read('/%d/0' % OID.AdvancedFirmwareUpdate, + VV.tlv_instance( + resource_validators={ + RID.AdvancedFirmwareUpdate.State: VV.from_raw_int(0), + RID.AdvancedFirmwareUpdate.UpdateResult: VV.from_raw_int(0), + }, + ignore_extra=True)) + + # 3. A WRITE (CoAP PUT) operation with a valid image is + # performed by the Server on the Package Resource (ID:0) of the + # FW Update Object Instance + # + # D. In test step 3, the Server receives the success message "2.04" + # associated with the WRITE request for loading the firmware image. + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + make_firmware_package(b'', **self.FW_PKG_OPTS), + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + + # 4. The Server READs (CoAP GET) the FW Object Instance to get + # the values of the State (ID:3) and Update Result (ID:5) Resources + # + # E. In test step 4, the Server receives the success message "2.05" along + # with the State and Update Result Resources values. + # F. In test step 4, the queried value of State resource is 2 (Downloaded) + # and the value of Update Result value is still 0 (Initial Value) + self.test_read('/%d/0' % OID.AdvancedFirmwareUpdate, + VV.tlv_instance( + resource_validators={ + RID.AdvancedFirmwareUpdate.State: VV.from_raw_int(2), + RID.AdvancedFirmwareUpdate.UpdateResult: VV.from_raw_int(0), + }, + ignore_extra=True)) + + +class Test756_AdvancedFirmwareUpdate_SettingTheWritableResourcePackageURI(AdvancedFirmwareUpdateWithHttpServer.Test): + def setUp(self): + super().setUp(make_firmware_package(b'', **self.FW_PKG_OPTS)) + + def runTest(self): + # 1. A WRITE (CoAP PUT) operation with an empty string value is + # performed by the Server on the Package Resource (ID:0) of the FW + # Update Object Instance + # + # A. In test step 1, the Server receives the success message "2.04" + # associated with the WRITE operation + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, b'') + + # 2. The Server READs (CoAP GET) the FW Object Instance to get the + # values of the State (ID:3) and Update Result (ID:5) Resources + # + # B. In test step 2, the Server receives the success message "2.05" along + # with the value of State and Update Result Resources values. + # C. In test step 2, the queried State and Update Result Resources values + # are both 0 (Idle / Initial value): FW Update Object Instance is in the + # Initial state. + self.test_read('/%d/0' % OID.AdvancedFirmwareUpdate, + VV.tlv_instance( + resource_validators={ + RID.AdvancedFirmwareUpdate.State: VV.from_raw_int(0), + RID.AdvancedFirmwareUpdate.UpdateResult: VV.from_raw_int(0), + }, + ignore_extra=True)) + + # 3. A WRITE (CoAP PUT) operation with a valid image is performed by + # the Server on the Package Resource (ID:0) of the FW Update Object + # Instance + # + # D. In test step 3, the Server receives the success message "2.04" + # associated with the WRITE request for the loadded image. + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + + # give the client some time to download firmware + time.sleep(3) + + # 4. The Server READs (CoAP GET) the FW Object Instance to get the + # values of the State (ID:3) and Update Result (ID:5) Resources + # + # E. In test step 4, the Server receives the success message "2.05" along + # with the State and Update Result Resources values. + # F. In test step 4, the queried value of State resource is 2 (Downloaded) + # and the value of Update Result value is still 0 (Initial Value) + self.test_read('/%d/0' % OID.AdvancedFirmwareUpdate, + VV.tlv_instance( + resource_validators={ + RID.AdvancedFirmwareUpdate.State: VV.from_raw_int(2), + RID.AdvancedFirmwareUpdate.UpdateResult: VV.from_raw_int(0), + }, + ignore_extra=True)) + + +class Test760_AdvancedFirmwareUpdate_BasicObservationAndNotificationOnFirmwareUpdateObjectResources( + AdvancedFirmwareUpdate.Test): + def runTest(self): + # 1. The Server communicates to the Client pmin=2 and pmax=10 + # periods with a WRITE-ATTRIBUTE (CoAP PUT) operation at + # the FW Update Object Instance level. + # + # A. In test step 1, the Server receives the success message "2.04" associated + # with the WRITE-ATTRIBUTE operation. + self.test_write_attributes('/%d/0' % OID.AdvancedFirmwareUpdate, + pmin=2, pmax=10) + + # 2. The Server Sends OBSERVE (CoAP Observe Option) message + # to activate reporting on the State Resource (/33629/0/3) of the FW + # Update Object Instance. + # + # B. In test step 2, the Server receives the success message "2.05" associated + # with the OBSERVE operation, along with the value of State =Idle + req = Lwm2mObserve(ResPath.AdvancedFirmwareUpdate[Instances.APP].State) + self.serv.send(req) + res = self.serv.recv() + self.assertMsgEqual(Lwm2mContent.matching(req)(content=b'0'), res) + + # 3. The Server delivers the firmware to the Client through a WRITE + # (CoAP PUT) operation on the Package Resource (/33629/0/0) + # + # C. In test step 3, the Server receives the success message "2.04" associated + # with the WRITE operation delivering the firmaware image. + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + make_firmware_package(b'', **self.FW_PKG_OPTS), + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + + # 4. The Client reports requested information with a NOTIFY + # message (CoAP response) + # + # D. In test step 4, the State Resource value returned by the Client in NOTIFY + # message is set to "Downloaded" + req = Lwm2mObserve(ResPath.AdvancedFirmwareUpdate[Instances.APP].State) + self.serv.send(req) + res = self.serv.recv(timeout_s=3) + self.assertMsgEqual(Lwm2mContent.matching(req)(content=b'2'), res) + + +class Test770_AdvancedFirmwareUpdate_SuccessfulFirmwareUpdateViaCoAP(AdvancedFirmwareUpdate.Test): + def runTest(self): + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + payload = f.read() + + prev_version = self.test_read(ResPath.Device.FirmwareVersion) + + # 1. Step 1 – Package Delivery + # a. The Server places the Client in the initial state of the FW Update + # process : A WRITE (CoAP PUT) operation with a NULL value + # (‘\0’) is performed by the Server on the Package Resource + # (ID:0) of the FW Update Object Instance + # + # A. Step 1 – Package Delivery + # a. In the test step 1.a, the Server receives the status code "2.04" for + # the WRITE success setting the Client in the FW update initial + # state. + # d. Update Result is "0" (Initial Value) during the whole step + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, b'\0', + coap.ContentFormat.APPLICATION_OCTET_STREAM) + + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 1. Step 1 – Package Delivery + # b. The Server delivers the firmware to the Client through a WRITE + # (CoAP PUT) operation on the Package Resource (/33629/0/0) + # + # A. Step 1 – Package Delivery + # b. In the test step 1.b, The Server receives success message with + # either a "2.31" status code (Continue) or a final "2.04" status + # code. + self.test_write_block(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + make_firmware_package(payload, **self.FW_PKG_OPTS), + coap.ContentFormat.APPLICATION_OCTET_STREAM) + + # 1. Step 1 – Package Delivery + # c. Polling (READ command) or Notification on Update Result + # and State Resources is performed, up to the time State Resource + # takes the ‘Downloaded’ value (2) + # + # A. Step 1 – Package Delivery + # c. In the test step 1.c State Resource can take the value "1" + # (Downloading) during this sub-step and will take the value "2" at + # the end (Downloaded) + # d. Update Result is "0" (Initial Value) during the whole step + self.assertEqual(b'2', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + # 2. Step 2 – Advanced Firmware Update + # a. When the download is completed (State Resource value is ‘2’ + # Downloaded) , the Server initiates a firmware update by + # triggering EXECUTE command on Update Resource (CoAP + # POST /33629/0/2) + # + # B. Step 2 – Advanced Firmware Update + # a. In test step 2.a, the Server receives a success message "2.04" + # (Changed) in response to the EXECUTE command + self.test_execute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + # not supported: Updating state only observable via Observe + # self.assertEqual(b'3', self.test_read(ResPath.FirmwareUpdate.State)) + + # 2. Step 2 – Advanced Firmware Update + # b. Polling (READ command) or Notification on Update Result and + # State Resources is performed, up to the time State Resource is + # turned back to Idle value (0) or Update Result Resource contains + # an other value than the Initial one (0) + # + # B. Step 2 – Advanced Firmware Update + # b. In test step 2.b, the Server receives success message(s) "2.05" + # Contents along with a State Resource value of "3" (Updating) + # or "0" (Idle) and an Update Ressource value of "0" (Initial + # Value) or "1" (Firmware updated successfully) + self.serv.reset() + self.assertDemoRegisters() + + # 3. Step 3 – Process verification + # a. The Server READs Update Result ("/33629/0/5") and State ("/33629/0/3") + # Resources to know the result of the advanced firmware update procedure. + # + # C. Step 3 – Process verification + # a. In test step 3.a, the Server receives success message(s) "2.05" + # Content" along with a State Resource value of "0" (Idle) and an + # Update Ressource value of "1" (Firmware updated successfully) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + self.assertEqual(b'1', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + # 3. Step 3 – Process verification + # b. The Server READs the Resource "Advanced Firmware Update" from the + # Object Device Instance ("/3/0/3") + # + # C. Step 3 – Process verification + # b. In test step 3.b, the Server receives success message "2.05" + # Content" along with the expected value of the Resource + # Firmware Version from the Object Device Instance + # + # TODO: we currently update firmware with an identical executable, + # so the version does not change + self.assertEqual(prev_version, self.test_read(ResPath.Device.FirmwareVersion)) + + +class Test771_AdvancedFirmwareUpdate_SuccessfulFirmwareUpdateViaAlternateMechanism( + AdvancedFirmwareUpdateWithHttpServer.Test): + def setUp(self): + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + pkg = make_firmware_package(f.read(), **self.FW_PKG_OPTS) + + super().setUp(pkg) + + def runTest(self): + # In this test the package version stays the same after update + prev_version = self.test_read(ResPath.Device.FirmwareVersion) + + # 1. Step 1 – Package Delivery + # a. The Server places the Client in the initial state of the FW + # Update process : A WRITE (CoAP PUT) operation with an + # empty string value is performed by the Server on the Package + # URI Resource (ID:1) of the FW Update Object Instance + # + # A. Step 1 – Package Delivery + # a. In the test step 1.a, the Server receives the status code "2.04" + # for the WRITE success setting the Client in the FW update + # initial state. + # e. Update Result is "0" (Initial Value) during the whole test + # step 1 + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, '') + self.assertEqual(b'0', self.test_read(ResPath.FirmwareUpdate.UpdateResult)) + + # 1. Step 1 – Package Delivery + # b. The Server delivers the Package URI to the Client through a + # WRITE (CoAP PUT) operation on the Package URI Resource + # (/33629/0/1) + # + # A. Step 1 – Package Delivery + # b. In the test step 1.b, the Server receives the status code "2.04" + # for the WRITE success setting the Package URI Client in the + # FW update Object Instance + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + # 1. Step 1 – Package Delivery + # c. The Client downloads the firmware from the provided URI via + # an alternative mechanism (not CoAP) + # d. Polling ( successive READ commands) or Notification on + # Update Result and State Resources is performed, up to the time + # State Resource takes the ‘Downloaded’ value (2) + # + # A. Step 1 – Package Delivery + # c. In the test step 1.c, The Server receives success message + # with either a "2.31" status code (Continue) or a final "2.04" + # status code. + # d. In the test step 1.d State Resource can take the value "1" + # (Downloading) during this sub-step and will take the value + # "2" at the end (Downloaded) + # e. Update Result is "0" (Initial Value) during the whole test + # step 1 + observed_states = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'2') + self.assertEqual(b'2', observed_states[-1]) + self.assertIn(set(observed_states), [{b'0', b'1', b'2'}, {b'1', b'2'}]) + + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + # 2. Step 2 – Advanced Firmware Update + # a. When the download is completed (State Resource value is ‘2’ + # Downloaded) , the Server initiates a advanced firmware update by + # triggering EXECUTE command on Update Resource (CoAP + # POST /33629/0/2 ) + # + # B. Step 2 – Advanced Firmware Update + # a. In test step 2.a, the Server receives a success message "2.04" + # (Changed) in response to the EXECUTE command + self.test_execute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + # not supported: Updating state only observable via Observe + # self.assertEqual(b'3', self.test_read(ResPath.FirmwareUpdate.State)) + + # 2. Step 2 – Advanced Firmware Update + # b. Polling (READ command) or Notification on Update Result and + # State Resources is performed, up to the time State Resource is + # turned back to Idle value (0) or Update Result Resource contains + # an other value than the Initial one (0) + # + # B. Step 2 – Advanced Firmware Update + # b. In test step 2.b, the Server receives success message(s) "2.05" + # Contents along with a State Resource value of "3" (Updating) + # or "0" (Idle) and an Update Ressource value of "0" (Initial + # Value) or "1" (Firmware updated successfully) + self.serv.reset() + self.assertDemoRegisters() + + # 3. Step 3 – Process verification + # a. The Server READs Update Result ("/33629/0/5") and State ("/33629/0/3") + # Resources to know the result of the advanced firmware update procedure. + # + # C. Step 3 – Process verification + # a. In test step 3.a, the Server receives success message(s) "2.05" + # Content" along with a State Resource value of "0" (Idle) and an + # Update Ressource value of "1" (Firmware updated successfully) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + self.assertEqual(b'1', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + # 3. Step 3 – Process verification + # b. The Server READs the Resource "Advanced Firmware Update" from the + # Object Device Instance ("/3/0/3") + # + # C. Step 3 – Process verification + # b. In test step 3.b, the Server receives success message "2.05" + # Content" along with the expected value of the Resource + # Firmware Version from the Object Device Instance + # + # TODO: we currently update firmware with an identical executable, + # so the version does not change + self.assertEqual(prev_version, self.test_read(ResPath.Device.FirmwareVersion)) + + +class Test772_AdvancedFirmwareUpdate_ErrorCase_FirmwarePackageNotDownloaded(AdvancedFirmwareUpdate.Test): + def runTest(self): + # 1. The Server send a READ operation (CoAP GET /33629/0) to the Client on the + # FW Update Object Instance to obtain the values of the State and Update + # Resources. + # + # A. In test step 1, the Server receives a success message ("2.05" Content) + # associated to its READ command along with a State Resource value + # which is not "2" (Downloaded) and a valid (0..9) Update Resource + # value + state = self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State) + self.assertNotEqual(b'2', state) + + update_result = self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult) + self.assertIn(int(update_result.decode('ascii')), range(10)) + + # 2. the Client receives an EXECUTE operation on the Update Resource + # (CoAP POST /33629/0/2 ) of the FW Update Object Instance + # + # B. In test step 2, the Server receives the status code "4.05" for method + # not allowed associated to its EXECUTE command + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + res = self.serv.recv() + self.assertMsgEqual(Lwm2mErrorResponse.matching(req)(coap.Code.RES_METHOD_NOT_ALLOWED), + res) + + # 3. The Server send a READ operation again (CoAP GET /33629/0/3) to the Client + # on the FW Update Object Instance to obtain the State and the Update + # Resource values + # + # C. In test step 3, the Server receives a success message ("2.05" Content) + # associated to its READ command along with a State Resource value + # and an Update Result Resource value, identical to the ones retrieved + # in Pass-Criteria A. The firmware has not bee installed. + self.assertEqual(state, self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + self.assertEqual(update_result, self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class Test773_AdvancedFirmwareUpdate_ErrorCase_NotEnoughStorage_FirmwareURI(AdvancedFirmwareUpdateWithHttpServer.Test): + def setUp(self): + @contextlib.contextmanager + def temporary_soft_fsize_limit(limit_bytes): + prev_limit = resource.getrlimit(resource.RLIMIT_FSIZE) + + try: + resource.setrlimit(resource.RLIMIT_FSIZE, (limit_bytes, prev_limit[1])) + yield + finally: + resource.setrlimit(resource.RLIMIT_FSIZE, prev_limit) + + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + payload = f.read() + + # Limit file size for demo so that full firmware is too much. + # After demo starts, we can safely restore original limit, as + # the client already inherited smaller one. + with temporary_soft_fsize_limit(len(payload) // 2): + super().setUp(payload) + + def runTest(self): + # 1. The Server verifies through a READ (CoAP GET) command on + # /33629/0/3 (State) the FW Update Object Instance of the Client is in + # Idle State + # + # A. In test step 1, the Server receives the status code "2.05 " (Content) for + # the READ success command, along with the State Resource value of + # "0" (Idle) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 2. The Server delivers the firmware package to the Client either + # through a WRITE (CoAP PUT) operation in the Package + # Resource (/33629/0/0) or through a WRITE operation of an URI in the + # Package URI Resource. + # + # B. In test step 2., the Server receives the status code "2.04" (Changed) + # for the WRITE command setting either the Package URI Resource or + # setting the Package Resource, according to the chosen firmware + # delivery method. + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + self.get_firmware_uri()) + + # 3. The firmware downloading process is runing The Server sends + # repeated READs or OBSERVE on State and Update Result + # Resources (CoAP GET /33629/0) of the FW Update Object Instance + # to determine when the download is completed or if an error + # occured.Before the end of download, the device runs out of + # storage and cannot finish the download + # 4. When the Package delivery is stopped the server READs Update + # Result to know the result of the firmware update procedure. + # + # C. In test step 3., the State Resource retrieved with a value of "1" from + # successive Server READs or Client NOTIFY messages, indicates the + # download stage of the Package Delivery is engaged + # D. In test step 3., the Update Result Resource (/33629/0/5) retrieved from + # successive Server READs or Client NOTIFY messages will take the + # value "2" indicating an error occurred during the downloading + # process related to shortage of storage memory The State Resource + # value never reaches the Downloaded value ("2") + # E. In test step 4., the success READ message(s) (status code "2.05" + # Content) on State Resource with value "0" (Idle) and Update Result + # Resource with value "2" indicates the firmware Package Delivery + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'0') + self.assertEqual(b'0', observed_values[-1]) + self.assertEqual({b'0', b'1'}, set(observed_values)) + self.assertEqual(b'2', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class Test773_AdvancedFirmwareUpdate_ErrorCase_NotEnoughStorage_FirmwareURI_CoAP( + AdvancedFirmwareUpdate.TestWithCoapServer): + def setUp(self): + # limit file size to 100K; enough for persistence file, not + # enough for firmware + import resource + self.prev_limit = resource.getrlimit(resource.RLIMIT_FSIZE) + new_limit_b = 100 * 1024 + resource.setrlimit(resource.RLIMIT_FSIZE, (new_limit_b, self.prev_limit[1])) + + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + self._payload = f.read() + + super().setUp() + + def tearDown(self): + import resource + resource.setrlimit(resource.RLIMIT_FSIZE, self.prev_limit) + + super().tearDown() + + def runTest(self): + with self.file_server as file_server: + file_server.set_resource('/firmware', self._payload) + uri = file_server.get_resource_uri('/firmware') + + # 1. The Server verifies through a READ (CoAP GET) command on + # /33629/0/3 (State) the FW Update Object Instance of the Client is in + # Idle State + # + # A. In test step 1, the Server receives the status code "2.05 " (Content) for + # the READ success command, along with the State Resource value of + # "0" (Idle) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 2. The Server delivers the firmware package to the Client either + # through a WRITE (CoAP PUT) operation in the Package + # Resource (/33629/0/0) or through a WRITE operation of an URI in the + # Package URI Resource. + # + # B. In test step 2., the Server receives the status code "2.04" (Changed) + # for the WRITE command setting either the Package URI Resource or + # setting the Package Resource, according to the chosen firmware + # delivery method. + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, uri) + + # 3. The firmware downloading process is runing The Server sends + # repeated READs or OBSERVE on State and Update Result + # Resources (CoAP GET /33629/0) of the FW Update Object Instance + # to determine when the download is completed or if an error + # occured.Before the end of download, the device runs out of + # storage and cannot finish the download + # 4. When the Package delivery is stopped the server READs Update + # Result to know the result of the firmware update procedure. + # + # C. In test step 3., the State Resource retrieved with a value of "1" from + # successive Server READs or Client NOTIFY messages, indicates the + # download stage of the Package Delivery is engaged + # D. In test step 3., the Update Result Resource (/33629/0/5) retrieved from + # successive Server READs or Client NOTIFY messages will take the + # value "2" indicating an error occurred during the downloading + # process related to shortage of storage memory The State Resource + # value never reaches the Downloaded value ("2") + # E. In test step 4., the success READ message(s) (status code "2.05" + # Content) on State Resource with value "0" (Idle) and Update Result + # Resource with value "2" indicates the firmware Package Delivery + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'0') + self.assertEqual(b'0', observed_values[-1]) + self.assertEqual({b'0', b'1'}, set(observed_values)) + self.assertEqual(b'2', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class Test774_AdvancedFirmwareUpdate_ErrorCase_OutOfMemory(AdvancedFirmwareUpdate.Test): + def runTest(self): + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + payload = f.read() + + # 1. The Server verifies through a READ (CoAP GET) command on + # /33629/0/3 (State) the FW Update Object Instance of the Client is in Idle + # State + # + # A. In test step 1, the Server receives the status code "2.05" (Content) for + # the READ success command, along with the State Resource value of + # "0" (Idle) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 2. The Server delivers the firmware package to the Client either through + # a WRITE (CoAP PUT) operation in the Package Resource (/33629/0/0) or + # through a WRITE operation of an URI in the Package URI Resource. + # + # B. In test step 2., the Server receives the status code "2.04" (Changed) + # for the WRITE command setting either the Package URI Resource or + # setting the Package Resource, according to the chosen firmware + # delivery method. + self.test_write_block(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + make_firmware_package(payload, + **self.FW_PKG_OPTS, + force_error=FirmwareUpdateForcedError.OutOfMemory), + coap.ContentFormat.APPLICATION_OCTET_STREAM) + + # 3. The firmware download process is runing The Server sends repeated + # READs or OBSERVE on State and Update Result Resources (CoAP + # GET /33629/0) of the FW Update Object Instance to determine when the + # download is completed or if an error occured.Before the end of + # download, the Client runs out of RAM and cannot finish the + # download + # 4. When the Package delivery is stopped the server READs Update + # Result to know the result of the firmware update procedure. + # + # C. In test step 3., the State Resource retrieved with a value of "1" from + # successive Server READs or Client NOTIFY messages, indicates the + # download stage of the Package Delivery is engaged + # D. In test step 3., the Update Result Resource (/33629/0/5) retrieved from + # successive Server READs or Client NOTIFY messages will take the + # value "2" indicating an error occurred during the download process + # related to shortage of RAM The State Resource value never reaches + # the Downloaded value ("3") + # E. In test step 4., the success READ message(s) (status code "2.05" + # Content) on State Resource with value "0" (Idle) and Update Result + # Resource with value "3" indicates the firmware Package Delivery + # aborted due to shortage of RAM. + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + self.assertEqual(b'3', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class AdvancedFirmwareUpdate_ErrorCase_OutOfMemory_PackageURI(AdvancedFirmwareUpdateWithHttpServer.Test): + def setUp(self): + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + pkg = make_firmware_package(f.read(), + **self.FW_PKG_OPTS, + force_error=FirmwareUpdateForcedError.OutOfMemory) + + super().setUp(pkg) + + def runTest(self): + # Test 774, but with Package URI + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, self.get_firmware_uri()) + + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'0') + self.assertEqual(b'0', observed_values[-1]) + self.assertEqual({b'0', b'1'}, set(observed_values)) + self.assertEqual(b'3', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class Test775_AdvancedFirmwareUpdate_ErrorCase_ConnectionLostDuringDownloadPackageURI( + AdvancedFirmwareUpdateWithHttpServer.Test): + class NoShutdownHttpServer(http.server.HTTPServer): + def shutdown_request(self, request): + pass + + def close_request(self, request): + pass + + HTTP_SERVER_CLASS = NoShutdownHttpServer + + def during_download(self, req_handler): + self._dangling_http_socket = req_handler.request + # HACK to ignore any calls on .wfile afterwards + req_handler.wfile = ANY + + def setUp(self): + self._dangling_http_socket = None + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + pkg = make_firmware_package(f.read(), + **self.FW_PKG_OPTS, + force_error=FirmwareUpdateForcedError.OutOfMemory) + + super().setUp(pkg) + + def tearDown(self): + try: + if self._dangling_http_socket is not None: + self._dangling_http_socket.close() + finally: + super().tearDown() + + def runTest(self): + # 1. The Server verifies through a READ (CoAP GET) command on + # /33629/0/3 (State) the FW Update Object Instance of the Client is in Idle + # State + # + # A. In test step 1., the Server receives the status code "2.05 " (Content) + # for the READ success command, along with the State Resource value + # of "0" (Idle) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 2. The Server delivers the firmware package to the Client through a + # WRITE operation of an URI in the Package URI Resource. + # + # B. In test step 2., the Server receives the status code "2.04" (Changed) + # for the WRITE command setting the Package URI Resource + # according to the PULL firmware delivery method. + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, self.get_firmware_uri()) + + # 3. The Server sends repeated READs or OBSERVE on State and Update + # Result Resources (CoAP GET /33629/0) of the FW Update Object + # Instance to determine when the download is completed or if an error + # occured.Before the end of download, the connection is intentionnaly + # lost and the download cannot be finished. + # 4. When the Package delivery is stopped the Server READs Update + # Result to know the result of the firmware update procedure. + # + # C. In test step 3., the State Resource value set to "1" retrieved from + # successive Server READs or Client NOTIFY messages, indicates the + # Package Delivery process is engaged in a Download stage + # D. In test step 3., the Update Result Resource (/33629/0/5) retrieved from + # successive Server READs or Client NOTIFY messages will take the + # value "4" indicating an error occurred during the downloading + # process related to connection lost + # E. In test step 4., the success READ message(s) (status code "2.05" + # Content) on State Resource with value "0" (Idle) and Update Result + # Resource with value "4" indicates the firmware Package Delivery + # aborted due to connection lost dur the Package delivery. + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'0', + max_iterations=600) # wait up to 60 seconds + self.assertEqual(b'0', observed_values[-1]) + self.assertEqual({b'0', b'1'}, set(observed_values)) + self.assertEqual(b'4', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class Test775_AdvancedFirmwareUpdate_ErrorCase_ConnectionLostDuringDownloadPackageURI_CoAP( + AdvancedFirmwareUpdate.TestWithCoapServer): + def setUp(self): + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + pkg = make_firmware_package(f.read(), + **self.FW_PKG_OPTS, + force_error=FirmwareUpdateForcedError.OutOfMemory) + + class MuteServer(coap.Server): + def send(self, *args, **kwargs): + pass + + super().setUp(coap_server=MuteServer(), extra_cmdline_args=['--afu-ack-timeout', '1']) + + with self.file_server as file_server: + file_server.set_resource('/firmware', pkg) + self._uri = file_server.get_resource_uri('/firmware') + + def runTest(self): + # 1. The Server verifies through a READ (CoAP GET) command on + # /33629/0/3 (State) the FW Update Object Instance of the Client is in Idle + # State + # + # A. In test step 1., the Server receives the status code "2.05 " (Content) + # for the READ success command, along with the State Resource value + # of "0" (Idle) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 2. The Server delivers the firmware package to the Client through a + # WRITE operation of an URI in the Package URI Resource. + # + # B. In test step 2., the Server receives the status code "2.04" (Changed) + # for the WRITE command setting the Package URI Resource + # according to the PULL firmware delivery method. + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, self._uri) + + # 3. The Server sends repeated READs or OBSERVE on State and Update + # Result Resources (CoAP GET /33629/0) of the FW Update Object + # Instance to determine when the download is completed or if an error + # occured.Before the end of download, the connection is intentionnaly + # lost and the download cannot be finished. + # 4. When the Package delivery is stopped the Server READs Update + # Result to know the result of the firmware update procedure. + # + # C. In test step 3., the State Resource value set to "1" retrieved from + # successive Server READs or Client NOTIFY messages, indicates the + # Package Delivery process is engaged in a Download stage + # D. In test step 3., the Update Result Resource (/33629/0/5) retrieved from + # successive Server READs or Client NOTIFY messages will take the + # value "4" indicating an error occurred during the downloading + # process related to connection lost + # E. In test step 4., the success READ message(s) (status code "2.05" + # Content) on State Resource with value "0" (Idle) and Update Result + # Resource with value "4" indicates the firmware Package Delivery + # aborted due to connection lost dur the Package delivery. + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'0', + max_iterations=50, step_time=1) + self.assertEqual(b'0', observed_values[-1]) + self.assertEqual({b'0', b'1'}, set(observed_values)) + self.assertEqual(b'4', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class Test776_AdvancedFirmwareUpdate_ErrorCase_CRCCheckFail(AdvancedFirmwareUpdate.Test): + def runTest(self): + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + payload = f.read() + + # 1. The Server verifies through a READ (CoAP GET) command on + # /33629/0/3 (State) the FW Update Object Instance of the Client is in Idle + # State + # + # A. In test step 1, the Server receives the status code "2.05 " (Content) for + # the READ success command, along with the State Resource value of + # "0" (Idle) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 2. The Server delivers the firmware package to the Client either through + # a WRITE (CoAP PUT) operation in the Package Resource (/33629/0/0) or + # through a WRITE operation of an URI in the Package URI Resource. + # + # B. In test step 2., the Server receives the status code "2.04" (Changed) + # for the WRITE command setting either the Package URI Resource or + # setting the Package Resource, according to the chosen firmware + # delivery method. + self.test_write_block(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + make_firmware_package(payload, + **self.FW_PKG_OPTS, + crc=0), + coap.ContentFormat.APPLICATION_OCTET_STREAM) + + # 3. The Server sends repeated READs or OBSERVE on State and Update + # Result Resources (CoAP GET /33629/0) of the FW Update Object + # Instance to determine when the download is completed or if an error + # occured. The firmware package Integry Check failure stopped the + # download process. + # 4. When the Package delivery is stopped the server READs Update + # Result to know the result of the firmware update procedure. + # + # C. In test step 3., the State Resource value set to "1" retrieved from + # successive Server READs or Client NOTIFY messages, indicates the + # Package Delivery process is maintained in Downloading stage + # D. In test step 3., the Update Result Resource (/33629/0/5) retrieved from + # successive Server READs or Client NOTIFY messages will take the + # value "5" indicating an error occurred during the downloading + # process related to the failure of the firmware package integrity check + # E. In test step 4., the success READ message(s) (status code "2.05" + # Content) on State Resource with value "0" (Idle) and Update Result + # Resource with value "5" indicates the firmware Package Delivery + # aborted due to a Firmware Package Integrity failure. + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + self.assertEqual(b'5', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class AdvancedFirmwareUpdate_ErrorCase_CRCCheckFail_PackageURI(AdvancedFirmwareUpdateWithHttpServer.Test): + def setUp(self): + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + pkg = make_firmware_package(f.read(), + **self.FW_PKG_OPTS, + crc=0) + + super().setUp(pkg) + + def runTest(self): + # Test 776, but with Package URI + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, self.get_firmware_uri()) + + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'0') + self.assertEqual(b'0', observed_values[-1]) + self.assertEqual({b'0', b'1'}, set(observed_values)) + self.assertEqual(b'5', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class Test777_AdvancedFirmwareUpdate_ErrorCase_UnsupportedPackageType(AdvancedFirmwareUpdate.Test): + def runTest(self): + # 1. The Server verifies through a READ (CoAP GET) command on + # /33629/0/3 (State) the FW Update Object Instance of the Client is in Idle + # State + # + # A. In test step 1, the Server receives the status code "2.05 " (Content) for + # the READ success command, along with the State Resource value of + # "0" (Idle) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 2. The Server delivers the firmware package to the Client either through + # a WRITE (CoAP PUT) operation in the Package Resource (/33629/0/0 ) or + # through a WRITE operation of an URI in the Package URI Resource. + # + # B. In test step 2., the Server receives the status code "2.04" (Changed) + # for the WRITE command setting either the Package URI Resource or + # setting the Package Resource, according to the chosen firmware + # delivery method. + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, b'A' * 1024, + format=coap.ContentFormat.APPLICATION_OCTET_STREAM) + + # 3. The Server sends repeated READs or OBSERVE on State and Update + # Result Resources (CoAP GET /33629/0) of the FW Update Object + # Instance to determine when the download is completed or if an error + # occured. The Download cannot be finished since the firmware + # package type is not supported by the Client + # 4. When the Package delivery is stopped the server READs Update + # Result to know the result of the firmware update procedure. + # + # C. In test step 3., the State Resource value set to "1" retrieved from + # successive Server READs or Client NOTIFY messages, indicates the + # Package Delivery process is in Downloading stage + # D. In test step 3., the Update Result Resource (/33629/0/5) retrieved from + # successive Server READs or Client NOTIFY messages will take the + # value "6" indicating an error occurred during the downloading + # process related to the firmware package type not supported by the + # Client. + # E. In test step 4., the success READ message(s) (status code "2.05" + # Content) on State Resource with value "1" (Downloading) and + # Update Result Resource with value "6 indicates the firmware Package + # Delivery aborted due to a firmware package type not supported by the + # Client. + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + self.assertEqual(b'6', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class AdvancedFirmwareUpdate_ErrorCase_UnsupportedPackageType_PackageURI(AdvancedFirmwareUpdateWithHttpServer.Test): + def setUp(self): + super().setUp(b'A' * 1024) + + def runTest(self): + # Test 777, but with Package URI + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, self.get_firmware_uri()) + + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'0') + self.assertEqual(b'0', observed_values[-1]) + self.assertEqual({b'0', b'1'}, set(observed_values)) + self.assertEqual(b'6', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class Test778_AdvancedFirmwareUpdate_ErrorCase_InvalidURI(AdvancedFirmwareUpdate.Test): + def runTest(self): + # 1. The Server verifies through a READ (CoAP GET) command on + # /33629/0/3 (State) the FW Update Object Instance of the Client is in Idle + # State + # + # A. In test step 1, the Server receives the status code "2.05 " (Content) for + # the READ success command, along with the State Resource value of + # "0" (Idle) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 2. The Server initiates a firmware package delivery to the Client through + # a WRITE operation of an invalid URI in the Package URI Resource. + # + # B. In test step 2., the Server receives the status code "2.04" (Changed) + # for the WRITE command setting the Package URI Resource + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, + 'http://mylovelyfwserver/') + + # 3. The Server sends repeated READs or OBSERVE on State and Update + # Result Resources (CoAP GET /33629/0) of the FW Update Object + # Instance to determine when the download is completed or if an error + # occured. The download process is stopped by the Client due to the + # usage of a bad URI. + # 4. When the Package delivery is stopped the server READs Update + # Result to know the result of the firmware update procedure. + # C. In test step 3., the State Resource value set to "1" retrieved from + # successive Server READs or Client NOTIFY messages, indicates the + # Package Delivery process is maintained in Downloading stage + # D. In test step 3., the Update Result Resource (/33629/0/5) retrieved from + # successive Server READs or Client NOTIFY messages will take the + # value "7" indicating an error occurred during the downloading + # process related to the usage of a bad URI + # E. In test step 4., the success READ message(s) (status code "2.05" + # Content) on State Resource with value "0" (Idle) and Update Result + # Resource with value "7" indicates the firmware Package Delivery + # aborted due to the connection to an Invalid URI for the firmware + # package delivery. + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'0') + self.assertEqual(b'0', observed_values[-1]) + # TODO? client does not report "Downloading" state + # self.assertEqual({b'0', b'1'}, set(observed_values)) + self.assertEqual({b'0'}, set(observed_values)) + self.assertEqual(b'7', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + +class Test779_AdvancedFirmwareUpdate_ErrorCase_UnsuccessfulFirmwareUpdate(AdvancedFirmwareUpdate.Test): + def runTest(self): + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + payload = f.read() + + prev_version = self.test_read(ResPath.Device.FirmwareVersion) + + # 1. Step 1 – Package Delivery + # a. The Server verifies through a READ (CoAP GET) command on + # /33629/0/3 (State) the FW Update Object Instance of the Client is in Idle + # State + # + # A. Package Delivery + # a. In test step 1.a, the Server receives the status code "2.05 " (Content) + # for the READ success command, along with the State Resource value + # of "0" (Idle) + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 1. Step 1 – Package Delivery + # b. The Server retrieves (CoAP GET) the initial value of the Firmware + # Version Resource from the Object Device Instance for verification in + # the Pass Criteria (C) + # + # A. Package Delivery + # b. In test step 1.b, the Server receives the status code "2.05 " (Content) + # for the READ success command, along with the initial value of the + # Firmware version Resource available from the Object Device + # Instance. + prev_version = self.test_read(ResPath.Device.FirmwareVersion) + + # 1. Step 1 – Package Delivery + # c. The Server delivers the firmware package to the Client either + # through a WRITE (CoAP PUT) operation in the Package Resource + # (/33629/0/0 ) or through a WRITE operation of an URI in the Package URI + # Resource. + # + # A. Package Delivery + # c. In test step 1.c, the Server receives the status code "2.04" (Changed) + # for the WRITE command setting either the Package URI Resource or + # setting the Package Resource, according to the chosen firmware + # delivery method. + self.test_write_block(ResPath.AdvancedFirmwareUpdate[Instances.APP].Package, + make_firmware_package(payload, + **self.FW_PKG_OPTS, + force_error=FirmwareUpdateForcedError.FailedUpdate), + coap.ContentFormat.APPLICATION_OCTET_STREAM) + + # 1. Step 1 – Package Delivery + # d. Polling ( successive READ commands) or Notification on Update + # Result and State Resources is performed, up to the time State + # Resource takes the ‘Downloaded’ value (2) + # + # A. Package Delivery + # d. In at this end of test step 1.d, the State Resource take the value "2" + # (Downloaded) + self.assertEqual(b'2', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + + # 2. Step 2 – Installation Failure + # a. When the download is completed (State Resource value is ‘2’ + # Downloaded) , the Server initiates a firmware update by triggering + # EXECUTE command on Update Resource (CoAP POST /33629/0/2 ) + # + # B. Installation failure + # a. In test step 2.a, the Server receives a success message "2.04" + # (Changed) in response to the EXECUTE command + self.test_execute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + + # 2. Step 2 – Installation Failure + # b. Polling (READ command) or Notification on Update Result and + # State Resources is performed, up to the time State Resource is + # turned back to 2 (Downloaded) or the Update Result Resource + # contains the value "8" (Firmware update failed ) + # + # B. Installation failure + # b. In test step 2.b, the Server receives success message(s) "2.05" + # Contents along with a State Resource value of "3" (Updating) or "2" + # (Downloaded) and an Update Ressource value of "0" (Initial Value) + # and "8" at the end (Firmware updated failure) + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'2') + self.assertEqual(b'2', observed_values[-1]) + # state == 3 may or may not not be observed + self.assertTrue(set(observed_values).issubset({b'2', b'3'})) + self.assertEqual(b'8', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + # 3. Step 3 – Process Verification + # a. The server READs Update Result & State Resources to know the + # result of the firmware update procedure. + # + # C. Process Verification + # a. In test step 3.a, the Server receives success message(s) "2.05" + # Content" along with a State Resource value of "2" (Downloaded) and + # an Update Ressource value of "8" (Firmware updated failed) + self.assertEqual(b'2', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].State)) + self.assertEqual(b'8', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + # 3. Step 3 – Process Verification + # b. The Server READs the Firmware Version Resource from the + # Object Device Instance + # + # C. Process Verification + # b. In test step 3.b the Server receives success message(s) "2.05" Content" + # along with a Firmware Version Resource value form the Object + # Device Instance which has not changed compared to the one retrieved + # in Pass Criteria A.b + self.assertEqual(prev_version, self.test_read(ResPath.Device.FirmwareVersion)) + + +class AdvancedFirmwareUpdate_ErrorCase_UnsuccessfulFirmwareUpdate_PackageURI(AdvancedFirmwareUpdateWithHttpServer.Test): + def setUp(self): + demo_executable = os.path.join(self.config.demo_path, self.config.demo_cmd) + with open(demo_executable, 'rb') as f: + pkg = make_firmware_package(f.read(), + **self.FW_PKG_OPTS, + force_error=FirmwareUpdateForcedError.FailedUpdate) + + super().setUp(pkg) + + def runTest(self): + # Test 777, but with Package URI + self.assertEqual(b'0', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + prev_version = self.test_read(ResPath.Device.FirmwareVersion) + + self.test_write(ResPath.AdvancedFirmwareUpdate[Instances.APP].PackageURI, self.get_firmware_uri()) + + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'2') + self.assertEqual(b'2', observed_values[-1]) + self.assertEqual({b'1', b'2'}, set(observed_values)) + + req = Lwm2mExecute(ResPath.AdvancedFirmwareUpdate[Instances.APP].Update) + self.serv.send(req) + self.assertMsgEqual(Lwm2mChanged.matching(req)(), + self.serv.recv()) + + observed_values = self.collect_values(ResPath.AdvancedFirmwareUpdate[Instances.APP].State, b'2') + self.assertEqual(b'2', observed_values[-1]) + # state == 3 may or may not not be observed + self.assertTrue(set(observed_values).issubset({b'2', b'3'})) + self.assertEqual(b'8', self.test_read(ResPath.AdvancedFirmwareUpdate[Instances.APP].UpdateResult)) + + self.assertEqual(prev_version, self.test_read(ResPath.Device.FirmwareVersion)) diff --git a/tests/integration/suites/testfest/dm/firmware_update.py b/tests/integration/suites/testfest/dm/firmware_update.py index 4a4883acf..8ff4f27ae 100644 --- a/tests/integration/suites/testfest/dm/firmware_update.py +++ b/tests/integration/suites/testfest/dm/firmware_update.py @@ -21,7 +21,7 @@ class FirmwareUpdate: class Test(DataModel.Test): - def collect_values(self, path: Lwm2mPath, final_value, max_iterations=100, step_time=0.1): + def collect_values(self, path: Lwm2mPath, final_value, max_iterations=300, step_time=0.1): observed_values = [] orig_timeout = self.serv.get_timeout() try: diff --git a/tests/integration/suites/testfest/management.py b/tests/integration/suites/testfest/management.py index 86da3aa69..ecb023243 100644 --- a/tests/integration/suites/testfest/management.py +++ b/tests/integration/suites/testfest/management.py @@ -217,7 +217,7 @@ def runTest(self): # successfully with the Server again (see LightweightM2M-1.0- # int-101) self.serv.reset() - self.assertDemoRegisters(timeout_s=3) + self.assertDemoRegisters() class Test260_DiscoverCommand(DataModel.Test): diff --git a/tests/modules/factory_provisioning/provisioning.c b/tests/modules/factory_provisioning/provisioning.c new file mode 100644 index 000000000..65c60953f --- /dev/null +++ b/tests/modules/factory_provisioning/provisioning.c @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2023 AVSystem + * AVSystem Anjay LwM2M SDK + * All rights reserved. + * + * Licensed under the AVSystem-5-clause License. + * See the attached LICENSE file for details. + */ + +#include + +#include +#include + +#include + +#include "tests/utils/dm.h" + +// NOTE: Success case is tested by tests/integration/factory_provisioning.py + +AVS_UNIT_TEST(factory_provisioning, fail_rollback) { + DM_TEST_INIT_WITH_OBJECTS(&OBJ_WITH_TRANSACTION, &FAKE_SECURITY, + &FAKE_SERVER); + avs_stream_t *stream = avs_stream_membuf_create(); + AVS_UNIT_ASSERT_NOT_NULL(stream); + static const char PROVISIONING_DATA[] = "\x82" // array(2) + "\xa2" // map(2) + "\x00\x69" + "/69/420/2" // name: "/69/420/2" + "\x02\x01" // value: 1 + "\xa2" // map(2) + "\x00\x69" + "/69/420/3" // name: "/69/420/3" + "\x02\x07"; // value: 7 + AVS_UNIT_ASSERT_SUCCESS(avs_stream_write(stream, PROVISIONING_DATA, + sizeof(PROVISIONING_DATA) - 1)); + avs_unit_mocksock_expect_shutdown(mocksocks[0]); + // Implicit DELETE / + _anjay_mock_dm_expect_list_instances( + anjay, &FAKE_SERVER, 0, (const anjay_iid_t[]) { ANJAY_ID_INVALID }); + _anjay_mock_dm_expect_list_instances(anjay, &OBJ_WITH_TRANSACTION, 0, + (const anjay_iid_t[]) { + ANJAY_ID_INVALID }); + // actual write + _anjay_mock_dm_expect_list_instances(anjay, &OBJ_WITH_TRANSACTION, 0, + (const anjay_iid_t[]) { + ANJAY_ID_INVALID }); + _anjay_mock_dm_expect_transaction_begin(anjay, &OBJ_WITH_TRANSACTION, 0); + _anjay_mock_dm_expect_instance_create(anjay, &OBJ_WITH_TRANSACTION, 420, 0); + _anjay_mock_dm_expect_resource_write(anjay, &OBJ_WITH_TRANSACTION, 420, 2, + ANJAY_ID_INVALID, + ANJAY_MOCK_DM_INT(0, 1), 0); + _anjay_mock_dm_expect_list_instances( + anjay, &OBJ_WITH_TRANSACTION, 0, + (const anjay_iid_t[]) { 420, ANJAY_ID_INVALID }); + _anjay_mock_dm_expect_resource_write(anjay, &OBJ_WITH_TRANSACTION, 420, 3, + ANJAY_ID_INVALID, + ANJAY_MOCK_DM_INT(0, 7), 0); + // fail transaction validation + _anjay_mock_dm_expect_transaction_validate(anjay, &OBJ_WITH_TRANSACTION, + -1); + _anjay_mock_dm_expect_transaction_rollback(anjay, &OBJ_WITH_TRANSACTION, 0); + AVS_UNIT_ASSERT_FAILED(anjay_factory_provision(anjay, stream)); + avs_stream_cleanup(&stream); + DM_TEST_FINISH; +} diff --git a/tests/modules/server/persistence.c b/tests/modules/server/persistence.c index 79086dc62..dcaab043b 100644 --- a/tests/modules/server/persistence.c +++ b/tests/modules/server/persistence.c @@ -73,7 +73,9 @@ static void assert_instances_equal(const server_instance_t *a, AVS_UNIT_ASSERT_EQUAL(a->lifetime, b->lifetime); AVS_UNIT_ASSERT_EQUAL(a->default_min_period, b->default_min_period); AVS_UNIT_ASSERT_EQUAL(a->default_max_period, b->default_max_period); +#ifndef ANJAY_WITHOUT_DEREGISTER AVS_UNIT_ASSERT_EQUAL(a->disable_timeout, b->disable_timeout); +#endif // ANJAY_WITHOUT_DEREGISTER AVS_UNIT_ASSERT_EQUAL(a->notification_storing, b->notification_storing); #ifdef ANJAY_WITH_LWM2M11 AVS_UNIT_ASSERT_EQUAL(a->last_alert, b->last_alert); @@ -141,7 +143,9 @@ AVS_UNIT_TEST(server_persistence, nonempty_store_restore_version_1) { .lifetime = 9001, .default_min_period = -1, .default_max_period = -1, +#ifndef ANJAY_WITHOUT_DEREGISTER .disable_timeout = -1, +#endif // ANJAY_WITHOUT_DEREGISTER .binding = { .data = "UQ", }, @@ -152,7 +156,9 @@ AVS_UNIT_TEST(server_persistence, nonempty_store_restore_version_1) { .present_resources = { [SERV_RES_SSID] = true, [SERV_RES_LIFETIME] = true, +#ifndef ANJAY_WITHOUT_DEREGISTER [SERV_RES_DISABLE] = true, +#endif // ANJAY_WITHOUT_DEREGISTER [SERV_RES_NOTIFICATION_STORING_WHEN_DISABLED_OR_OFFLINE] = true, [SERV_RES_BINDING] = true, [SERV_RES_REGISTRATION_UPDATE_TRIGGER] = true, @@ -216,7 +222,9 @@ AVS_UNIT_TEST(server_persistence, nonempty_store_restore) { .present_resources = { [SERV_RES_SSID] = true, [SERV_RES_LIFETIME] = true, +#ifndef ANJAY_WITHOUT_DEREGISTER [SERV_RES_DISABLE] = true, +#endif // ANJAY_WITHOUT_DEREGISTER [SERV_RES_NOTIFICATION_STORING_WHEN_DISABLED_OR_OFFLINE] = true, [SERV_RES_BINDING] = true, [SERV_RES_REGISTRATION_UPDATE_TRIGGER] = true, diff --git a/tests/utils/dm.c b/tests/utils/dm.c index 369028994..74ab94dd1 100644 --- a/tests/utils/dm.c +++ b/tests/utils/dm.c @@ -35,23 +35,41 @@ anjay_t *_anjay_test_dm_init(const anjay_configuration_t *config) { return anjay; } +void _anjay_test_dm_unsched_notify_clb(anjay_t *anjay_locked) { + ANJAY_MUTEX_LOCK(anjay, anjay_locked); + _anjay_notify_clear_queue(&anjay->scheduled_notify.queue); + avs_sched_del(&anjay->scheduled_notify.handle); + ANJAY_MUTEX_UNLOCK(anjay_locked); +} + void _anjay_test_dm_unsched_reload_sockets(anjay_t *anjay_locked) { ANJAY_MUTEX_LOCK(anjay, anjay_locked); avs_sched_del(&anjay->reload_servers_sched_job_handle); ANJAY_MUTEX_UNLOCK(anjay_locked); } +avs_net_socket_t *_anjay_test_dm_create_socket(bool connected) { + avs_net_socket_t *socket = NULL; + _anjay_mocksock_create(&socket, 1252, 1252); + if (connected) { + avs_unit_mocksock_expect_connect(socket, "", ""); + AVS_UNIT_ASSERT_SUCCESS(avs_net_socket_connect(socket, "", "")); + } + avs_unit_mocksock_enable_recv_timeout_getsetopt( + socket, avs_time_duration_from_scalar(1, AVS_TIME_S)); + avs_unit_mocksock_enable_inner_mtu_getopt(socket, 1252); + avs_unit_mocksock_enable_state_getopt(socket); + return socket; +} + avs_net_socket_t *_anjay_test_dm_install_socket(anjay_t *anjay_locked, anjay_ssid_t ssid) { - avs_net_socket_t *socket = NULL; + avs_net_socket_t *socket = _anjay_test_dm_create_socket(true); ANJAY_MUTEX_LOCK(anjay, anjay_locked); AVS_UNIT_ASSERT_NOT_NULL( AVS_LIST_INSERT_NEW(anjay_server_info_t, &anjay->servers)); anjay->servers->anjay = anjay; anjay->servers->ssid = ssid; - _anjay_mocksock_create(&socket, 1252, 1252); - avs_unit_mocksock_expect_connect(socket, "", ""); - AVS_UNIT_ASSERT_SUCCESS(avs_net_socket_connect(socket, "", "")); anjay->servers->registration_info.expire_time.since_real_epoch.seconds = INT64_MAX; anjay_server_connection_t *connection = @@ -89,9 +107,9 @@ void _anjay_test_dm_finish(anjay_t *anjay_locked) { AVS_LIST_CLEAR(&anjay->servers) { _anjay_server_cleanup(anjay->servers); } - _anjay_mock_dm_expect_clean(); ANJAY_MUTEX_UNLOCK(anjay_locked); anjay_delete(anjay_locked); + _anjay_mock_dm_expect_clean(); _anjay_mock_clock_finish(); } diff --git a/tests/utils/dm.h b/tests/utils/dm.h index a66e15471..bbe897c90 100644 --- a/tests/utils/dm.h +++ b/tests/utils/dm.h @@ -21,8 +21,12 @@ anjay_t *_anjay_test_dm_init(const anjay_configuration_t *config); +void _anjay_test_dm_unsched_notify_clb(anjay_t *anjay); + void _anjay_test_dm_unsched_reload_sockets(anjay_t *anjay); +avs_net_socket_t *_anjay_test_dm_create_socket(bool connected); + avs_net_socket_t *_anjay_test_dm_install_socket(anjay_t *anjay, anjay_ssid_t ssid); void _anjay_test_dm_finish(anjay_t *anjay); @@ -76,6 +80,14 @@ static const anjay_dm_object_def_t *const OBJ_WITH_RESET = .instance_reset = _anjay_mock_dm_instance_reset } }; +static const anjay_dm_object_def_t *const OBJ_WITH_TRANSACTION = &( + const anjay_dm_object_def_t) { + .oid = 69, + .handlers = { ANJAY_MOCK_DM_HANDLERS_BASIC, ANJAY_MOCK_DM_HANDLERS_REST, + ANJAY_MOCK_DM_HANDLERS_TRANSACTION, + .instance_reset = _anjay_test_dm_instance_reset_NOOP } +}; + static anjay_dm_object_def_t *const EXECUTE_OBJ = &(anjay_dm_object_def_t) { .oid = 128, .handlers = { ANJAY_MOCK_DM_HANDLERS } @@ -88,10 +100,7 @@ static const anjay_dm_object_def_t *const FAKE_SECURITY = .list_instances = _anjay_test_dm_fake_security_list_instances, .list_resources = _anjay_test_dm_fake_security_list_resources, .resource_read = _anjay_test_dm_fake_security_read, - .transaction_begin = anjay_dm_transaction_NOOP, - .transaction_validate = anjay_dm_transaction_NOOP, - .transaction_commit = anjay_dm_transaction_NOOP, - .transaction_rollback = anjay_dm_transaction_NOOP + ANJAY_MOCK_DM_HANDLERS_TRANSACTION_NOOP } }; @@ -114,44 +123,30 @@ static const anjay_dm_object_def_t *const FAKE_SERVER = .out_buffer_size = 4096, __VA_ARGS__ \ } -#define DM_TEST_INIT_OBJECTS__(ObjDefs, ...) \ - reset_token_generator(); \ - anjay_t *anjay = _anjay_test_dm_init((__VA_ARGS__)); \ - do { \ - for (size_t _i = 0; _i < AVS_ARRAY_SIZE((ObjDefs)); ++_i) { \ - AVS_UNIT_ASSERT_SUCCESS( \ - anjay_register_object(anjay, (ObjDefs)[_i])); \ - if (((*(ObjDefs)[_i])->oid == ANJAY_DM_OID_SECURITY \ - || (*(ObjDefs)[_i])->oid == ANJAY_DM_OID_SERVER) \ - && (*(ObjDefs)[_i])->handlers.list_instances \ - == _anjay_mock_dm_list_instances) { \ - _anjay_mock_dm_expect_list_instances( \ - anjay, \ - (ObjDefs)[_i], \ - 0, \ - (const anjay_iid_t[]) { ANJAY_ID_INVALID }); \ - } \ - } \ +#define DM_TEST_INIT_OBJECTS__(ObjDefs, ...) \ + reset_token_generator(); \ + anjay_t *anjay = _anjay_test_dm_init((__VA_ARGS__)); \ + do { \ + for (size_t _i = 0; _i < AVS_ARRAY_SIZE((ObjDefs)); ++_i) { \ + AVS_UNIT_ASSERT_SUCCESS( \ + anjay_register_object(anjay, (ObjDefs)[_i])); \ + } \ } while (false) -#define DM_TEST_POST_INIT__ \ - do { \ - anjay_sched_run(anjay); \ - _anjay_test_dm_unsched_reload_sockets(anjay); \ - } while (anjay_sched_calculate_wait_time_ms(anjay, INT_MAX) == 0) - -#define DM_TEST_INIT_GENERIC(ObjDefs, Ssids, ...) \ - DM_TEST_INIT_OBJECTS__(ObjDefs, __VA_ARGS__); \ - avs_net_socket_t *mocksocks[AVS_ARRAY_SIZE((Ssids))]; \ - for (size_t _i = AVS_ARRAY_SIZE((Ssids)) - 1; \ - _i < AVS_ARRAY_SIZE((Ssids)); \ - --_i) { \ - mocksocks[_i] = _anjay_test_dm_install_socket(anjay, (Ssids)[_i]); \ - avs_unit_mocksock_enable_recv_timeout_getsetopt( \ - mocksocks[_i], avs_time_duration_from_scalar(1, AVS_TIME_S)); \ - avs_unit_mocksock_enable_inner_mtu_getopt(mocksocks[_i], 1252); \ - avs_unit_mocksock_enable_state_getopt(mocksocks[_i]); \ - } \ +#define DM_TEST_POST_INIT__ \ + _anjay_test_dm_unsched_notify_clb(anjay); \ + AVS_UNIT_ASSERT_EQUAL(anjay_sched_calculate_wait_time_ms(anjay, INT_MAX), \ + INT_MAX) + +#define DM_TEST_INIT_GENERIC(ObjDefs, Ssids, ...) \ + DM_TEST_INIT_OBJECTS__(ObjDefs, __VA_ARGS__); \ + avs_net_socket_t *mocksocks[AVS_ARRAY_SIZE((Ssids))]; \ + for (size_t _i = AVS_ARRAY_SIZE((Ssids)) - 1; \ + _i < AVS_ARRAY_SIZE((Ssids)); \ + --_i) { \ + mocksocks[_i] = _anjay_test_dm_install_socket(anjay, (Ssids)[_i]); \ + } \ + (void) mocksocks; \ DM_TEST_POST_INIT__ #define DM_TEST_DEFAULT_OBJECTS \ diff --git a/tests/utils/mock_clock.c b/tests/utils/mock_clock.c index cad9e3c8c..2e1913a47 100644 --- a/tests/utils/mock_clock.c +++ b/tests/utils/mock_clock.c @@ -61,8 +61,6 @@ int clock_gettime(clockid_t clock, struct timespec *t) { // all clocks are equivalent for our purposes, so ignore clock t->tv_sec = (time_t) MOCK_CLOCK.since_monotonic_epoch.seconds; t->tv_nsec = MOCK_CLOCK.since_monotonic_epoch.nanoseconds; - MOCK_CLOCK = avs_time_monotonic_add( - MOCK_CLOCK, avs_time_duration_from_scalar(1, AVS_TIME_NS)); return 0; } else { return orig_clock_gettime(clock, t); diff --git a/tests/utils/mock_dm.c b/tests/utils/mock_dm.c index d41007d80..c1cdcd110 100644 --- a/tests/utils/mock_dm.c +++ b/tests/utils/mock_dm.c @@ -36,7 +36,11 @@ typedef enum { MOCK_DM_RESOURCE_READ_ATTRS, MOCK_DM_RESOURCE_WRITE_ATTRS, MOCK_DM_RESOURCE_INSTANCE_READ_ATTRS, - MOCK_DM_RESOURCE_INSTANCE_WRITE_ATTRS + MOCK_DM_RESOURCE_INSTANCE_WRITE_ATTRS, + MOCK_DM_TRANSACTION_BEGIN, + MOCK_DM_TRANSACTION_VALIDATE, + MOCK_DM_TRANSACTION_COMMIT, + MOCK_DM_TRANSACTION_ROLLBACK } anjay_mock_dm_expected_command_type_t; typedef struct { @@ -480,6 +484,18 @@ int _anjay_mock_dm_resource_write_attrs( DM_ACTION_RETURN; } +#define TRANSACTION_ACTION(LName, UName) \ + int _anjay_mock_dm_transaction_##LName( \ + anjay_t *anjay, const anjay_dm_object_def_t *const *obj_ptr) { \ + DM_ACTION_COMMON(TRANSACTION_##UName); \ + DM_ACTION_RETURN; \ + } + +TRANSACTION_ACTION(begin, BEGIN) +TRANSACTION_ACTION(validate, VALIDATE) +TRANSACTION_ACTION(commit, COMMIT) +TRANSACTION_ACTION(rollback, ROLLBACK) + #ifdef ANJAY_WITH_LWM2M11 int _anjay_mock_dm_resource_instance_read_attrs( anjay_t *anjay, @@ -850,6 +866,22 @@ void _anjay_mock_dm_expect_resource_instance_write_attrs( command->value.resource_attributes = *attrs; } +#define EXPECT_TRANSACTION_ACTION(LName, UName) \ + void _anjay_mock_dm_expect_transaction_##LName( \ + anjay_t *anjay, const anjay_dm_object_def_t *const *obj_ptr, \ + int retval) { \ + anjay_mock_dm_expected_command_t *command = \ + NEW_EXPECTED_COMMAND(MOCK_DM_TRANSACTION_##UName); \ + command->anjay = anjay; \ + command->obj_ptr = obj_ptr; \ + command->retval = retval; \ + } + +EXPECT_TRANSACTION_ACTION(begin, BEGIN) +EXPECT_TRANSACTION_ACTION(validate, VALIDATE) +EXPECT_TRANSACTION_ACTION(commit, COMMIT) +EXPECT_TRANSACTION_ACTION(rollback, ROLLBACK) + void _anjay_mock_dm_expect_clean(void) { AVS_UNIT_ASSERT_NULL(EXPECTED_COMMANDS); } diff --git a/tests/utils/mock_dm.h b/tests/utils/mock_dm.h index ad0330958..0590d8097 100644 --- a/tests/utils/mock_dm.h +++ b/tests/utils/mock_dm.h @@ -174,6 +174,10 @@ anjay_dm_resource_reset_t _anjay_mock_dm_resource_reset; anjay_dm_list_resource_instances_t _anjay_mock_dm_list_resource_instances; anjay_dm_resource_read_attrs_t _anjay_mock_dm_resource_read_attrs; anjay_dm_resource_write_attrs_t _anjay_mock_dm_resource_write_attrs; +anjay_dm_transaction_begin_t _anjay_mock_dm_transaction_begin; +anjay_dm_transaction_validate_t _anjay_mock_dm_transaction_validate; +anjay_dm_transaction_commit_t _anjay_mock_dm_transaction_commit; +anjay_dm_transaction_rollback_t _anjay_mock_dm_transaction_rollback; #ifdef ANJAY_WITH_LWM2M11 anjay_dm_resource_instance_read_attrs_t _anjay_mock_dm_resource_instance_read_attrs; @@ -193,45 +197,49 @@ anjay_dm_resource_instance_write_attrs_t .list_resource_instances = _anjay_mock_dm_list_resource_instances #if defined(ANJAY_WITH_LWM2M11) -# define ANJAY_MOCK_DM_HANDLERS \ - ANJAY_MOCK_DM_HANDLERS_BASIC, \ - .object_read_default_attrs = \ - _anjay_mock_dm_object_read_default_attrs, \ - .object_write_default_attrs = \ - _anjay_mock_dm_object_write_default_attrs, \ - .instance_read_default_attrs = \ - _anjay_mock_dm_instance_read_default_attrs, \ - .instance_write_default_attrs = \ - _anjay_mock_dm_instance_write_default_attrs, \ - .resource_read_attrs = _anjay_mock_dm_resource_read_attrs, \ - .resource_write_attrs = _anjay_mock_dm_resource_write_attrs, \ - .resource_instance_read_attrs = \ - _anjay_mock_dm_resource_instance_read_attrs, \ - .resource_instance_write_attrs = \ - _anjay_mock_dm_resource_instance_write_attrs, \ - .transaction_begin = anjay_dm_transaction_NOOP, \ - .transaction_validate = anjay_dm_transaction_NOOP, \ - .transaction_commit = anjay_dm_transaction_NOOP, \ - .transaction_rollback = anjay_dm_transaction_NOOP +# define ANJAY_MOCK_DM_HANDLERS_REST \ + .object_read_default_attrs = _anjay_mock_dm_object_read_default_attrs, \ + .object_write_default_attrs = \ + _anjay_mock_dm_object_write_default_attrs, \ + .instance_read_default_attrs = \ + _anjay_mock_dm_instance_read_default_attrs, \ + .instance_write_default_attrs = \ + _anjay_mock_dm_instance_write_default_attrs, \ + .resource_read_attrs = _anjay_mock_dm_resource_read_attrs, \ + .resource_write_attrs = _anjay_mock_dm_resource_write_attrs, \ + .resource_instance_read_attrs = \ + _anjay_mock_dm_resource_instance_read_attrs, \ + .resource_instance_write_attrs = \ + _anjay_mock_dm_resource_instance_write_attrs #else // defined(ANJAY_WITH_LWM2M11) -# define ANJAY_MOCK_DM_HANDLERS \ - ANJAY_MOCK_DM_HANDLERS_BASIC, \ - .object_read_default_attrs = \ - _anjay_mock_dm_object_read_default_attrs, \ - .object_write_default_attrs = \ - _anjay_mock_dm_object_write_default_attrs, \ - .instance_read_default_attrs = \ - _anjay_mock_dm_instance_read_default_attrs, \ - .instance_write_default_attrs = \ - _anjay_mock_dm_instance_write_default_attrs, \ - .resource_read_attrs = _anjay_mock_dm_resource_read_attrs, \ - .resource_write_attrs = _anjay_mock_dm_resource_write_attrs, \ - .transaction_begin = anjay_dm_transaction_NOOP, \ - .transaction_validate = anjay_dm_transaction_NOOP, \ - .transaction_commit = anjay_dm_transaction_NOOP, \ - .transaction_rollback = anjay_dm_transaction_NOOP +# define ANJAY_MOCK_DM_HANDLERS_REST \ + .object_read_default_attrs = _anjay_mock_dm_object_read_default_attrs, \ + .object_write_default_attrs = \ + _anjay_mock_dm_object_write_default_attrs, \ + .instance_read_default_attrs = \ + _anjay_mock_dm_instance_read_default_attrs, \ + .instance_write_default_attrs = \ + _anjay_mock_dm_instance_write_default_attrs, \ + .resource_read_attrs = _anjay_mock_dm_resource_read_attrs, \ + .resource_write_attrs = _anjay_mock_dm_resource_write_attrs #endif // ANJAY_WITH_LWM2M11 +#define ANJAY_MOCK_DM_HANDLERS_TRANSACTION_NOOP \ + .transaction_begin = anjay_dm_transaction_NOOP, \ + .transaction_validate = anjay_dm_transaction_NOOP, \ + .transaction_commit = anjay_dm_transaction_NOOP, \ + .transaction_rollback = anjay_dm_transaction_NOOP + +#define ANJAY_MOCK_DM_HANDLERS_TRANSACTION \ + .transaction_begin = _anjay_mock_dm_transaction_begin, \ + .transaction_validate = _anjay_mock_dm_transaction_validate, \ + .transaction_commit = _anjay_mock_dm_transaction_commit, \ + .transaction_rollback = _anjay_mock_dm_transaction_rollback + +#define ANJAY_MOCK_DM_HANDLERS \ + ANJAY_MOCK_DM_HANDLERS_BASIC, ANJAY_MOCK_DM_HANDLERS_REST, \ + ANJAY_MOCK_DM_HANDLERS_TRANSACTION_NOOP + void _anjay_mock_dm_expect_object_read_default_attrs( anjay_t *anjay, const anjay_dm_object_def_t *const *obj_ptr, @@ -354,6 +362,22 @@ void _anjay_mock_dm_expect_resource_instance_write_attrs( anjay_ssid_t ssid, const anjay_dm_r_attributes_t *attrs, int retval); +void _anjay_mock_dm_expect_transaction_begin( + anjay_t *anjay, + const anjay_dm_object_def_t *const *obj_ptr, + int retval); +void _anjay_mock_dm_expect_transaction_validate( + anjay_t *anjay, + const anjay_dm_object_def_t *const *obj_ptr, + int retval); +void _anjay_mock_dm_expect_transaction_commit( + anjay_t *anjay, + const anjay_dm_object_def_t *const *obj_ptr, + int retval); +void _anjay_mock_dm_expect_transaction_rollback( + anjay_t *anjay, + const anjay_dm_object_def_t *const *obj_ptr, + int retval); void _anjay_mock_dm_expect_clean(void); void _anjay_mock_dm_expected_commands_clear(void); diff --git a/tools/ci/rockylinux-9/Dockerfile b/tools/ci/rockylinux-9/Dockerfile index 8c38e13f3..6e2f30e65 100644 --- a/tools/ci/rockylinux-9/Dockerfile +++ b/tools/ci/rockylinux-9/Dockerfile @@ -7,13 +7,11 @@ FROM rockylinux/rockylinux:9 RUN dnf update -y && \ - dnf install -y python3-pip git openssl-devel zlib-devel \ + dnf install -y --allowerasing python3-pip git openssl-devel zlib-devel \ python3 python3-devel wget python3-cryptography openssl \ python3-requests python3-packaging valgrind curl cmake \ gcc gcc-c++ wireshark-cli which 'dnf-command(config-manager)' RUN dnf config-manager --set-enabled crb && \ dnf install -y epel-release && \ dnf install -y mbedtls-devel -RUN pip3 install sphinx sphinx-rtd-theme cbor2 aiocoap wheel -# Solve issues with EPERM when running dumpcap -RUN setcap '' $(which dumpcap) +RUN pip3 install sphinx sphinx-rtd-theme cbor2 aiocoap wheel linuxdoc diff --git a/tools/ci/ubuntu-18.04/Dockerfile b/tools/ci/ubuntu-18.04/Dockerfile index 33e9a92a5..ccc9b8151 100644 --- a/tools/ci/ubuntu-18.04/Dockerfile +++ b/tools/ci/ubuntu-18.04/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update && \ apt-get install -y python3-pip git build-essential libmbedtls-dev \ libssl-dev zlib1g-dev python3 libpython3-dev wget python3-cryptography \ python3-requests python3-packaging valgrind curl cmake tshark -RUN pip3 install sphinx sphinx-rtd-theme +RUN pip3 install sphinx sphinx-rtd-theme linuxdoc # NOTE: Newer versions don't install cleanly on Python 3.6 RUN pip3 install aiocoap==0.4b3 cbor2==4.1.2 # Solve issues with EPERM when running dumpcap diff --git a/tools/ci/ubuntu-20.04/Dockerfile b/tools/ci/ubuntu-20.04/Dockerfile index 45265cebc..309b1a7a9 100644 --- a/tools/ci/ubuntu-20.04/Dockerfile +++ b/tools/ci/ubuntu-20.04/Dockerfile @@ -11,6 +11,6 @@ RUN apt-get update && \ apt-get install -y python3-pip git libmbedtls-dev libssl-dev zlib1g-dev \ python3 libpython3-dev wget python3-cryptography python3-requests \ python3-packaging valgrind curl cmake build-essential tshark -RUN pip3 install sphinx sphinx-rtd-theme cbor2 aiocoap +RUN pip3 install sphinx sphinx-rtd-theme cbor2 aiocoap linuxdoc # Solve issues with EPERM when running dumpcap RUN setcap '' $(which dumpcap) diff --git a/tools/ci/ubuntu-22.04/Dockerfile b/tools/ci/ubuntu-22.04/Dockerfile index 71171e38e..641b488d3 100644 --- a/tools/ci/ubuntu-22.04/Dockerfile +++ b/tools/ci/ubuntu-22.04/Dockerfile @@ -11,6 +11,6 @@ RUN apt-get update && \ apt-get install -y python3-pip git libmbedtls-dev libssl-dev zlib1g-dev \ python3 libpython3-dev wget python3-cryptography python3-requests \ python3-packaging valgrind curl cmake build-essential tshark -RUN pip3 install sphinx sphinx-rtd-theme cbor2 aiocoap +RUN pip3 install sphinx sphinx-rtd-theme cbor2 aiocoap linuxdoc # Solve issues with EPERM when running dumpcap RUN setcap '' $(which dumpcap) diff --git a/tools/symlink-check.sh b/tools/symlink-check.sh index 8e820b41d..84c88835b 100755 --- a/tools/symlink-check.sh +++ b/tools/symlink-check.sh @@ -10,6 +10,7 @@ set -e EXCEPTIONS=( + "^\./\.git/" "^\./examples/" "^\./test_ghactions" "/doc/sphinx/html/"