diff --git a/Buildscripts/TactilitySDK/TactilitySDK.esp32.cmake b/Buildscripts/TactilitySDK/TactilitySDK.esp32.cmake index 1492ce71b..b1da6de34 100644 --- a/Buildscripts/TactilitySDK/TactilitySDK.esp32.cmake +++ b/Buildscripts/TactilitySDK/TactilitySDK.esp32.cmake @@ -24,6 +24,12 @@ macro(tactility_project_post project_name) idf_build_set_property(COMPILE_OPTIONS "-fno-omit-frame-pointer" APPEND) endif() + # ESP-IDF's own (esp_libc/platform_include) is found before app-module's, which adds + # the terminal ioctls (TIOCGWINSZ) Tactility's ioctl() supports for app fds. + # C and C++ only: assembly sources can't include a C header. + idf_build_set_property(C_COMPILE_OPTIONS "SHELL:-include ${TACTILITY_SDK_PATH}/Modules/app-module/include/sys/ioctl.h" APPEND) + idf_build_set_property(CXX_COMPILE_OPTIONS "SHELL:-include ${TACTILITY_SDK_PATH}/Modules/app-module/include/sys/ioctl.h" APPEND) + include("${TACTILITY_SDK_PATH}/Libraries/elf_loader/elf_loader.cmake") project_elf($project_name) diff --git a/Buildscripts/TactilitySDK/TactilitySDK.posix.cmake b/Buildscripts/TactilitySDK/TactilitySDK.posix.cmake index 68e4e63ec..ae4ebe95f 100644 --- a/Buildscripts/TactilitySDK/TactilitySDK.posix.cmake +++ b/Buildscripts/TactilitySDK/TactilitySDK.posix.cmake @@ -32,6 +32,12 @@ macro(tactility_project_post project_name) ${TACTILITY_SDK_PATH}/Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/utils ) target_compile_definitions(TactilitySDK INTERFACE LV_LVGL_H_INCLUDE_SIMPLE) + # An app's references to its own functions must bind to its own definitions, not to a same-named + # libc function already loaded in the Tactility process (e.g. an app shipping its own regcomp()). + # ESP32 apps get the same through -fvisibility=hidden. Mach-O binds within the image by default. + if (NOT APPLE) + target_link_options(TactilitySDK INTERFACE -Wl,-Bsymbolic) + endif () # ESP-IDF's project() auto-discovers the "main" component; plain CMake doesn't. add_subdirectory(main) diff --git a/CMakeLists.txt b/CMakeLists.txt index 051bb1de5..f1fc9fa75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) # Wraps newlib's reentrant syscall stubs, not the plain read()/write()/close() newlib itself. # Implements as thin wrappers around them. newlib's own stdio (fflush()'s buffer-flush path in particular) # calls these _r stubs directly, bypassing the plain names entirely. - # See Modules/app-module/source/stdio_wrap_esp32.cpp's own comment for the exact call chain. + # See Modules/app-esp32-module/source/stdio_wrap.cpp's own comment for the exact call chain. idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_read_r" APPEND) idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_write_r" APPEND) idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_close_r" APPEND) @@ -82,6 +82,13 @@ if (DEFINED ENV{ESP_IDF_VERSION}) idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=tcgetattr" APPEND) idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=tcsetattr" APPEND) + # elf_loader's full D-cache write-back must not run concurrently with a cache operation on the other core. + # See Modules/app-esp32-module/source/elf_cache.cpp. + idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_elf_arch_flush" APPEND) + + # elf_loader's Xtensa relocation drops GLOB_DAT/JMP_SLOT addends. See Modules/app-esp32-module/source/elf_relocate.cpp. + idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_elf_arch_relocate" APPEND) + # exit() from an app ends only that app (see app_scheduler_exit_current()) instead of aborting. idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=exit" APPEND) diff --git a/Devices/cl32/source/cl32_v2_keyboard.cpp b/Devices/cl32/source/cl32_v2_keyboard.cpp index 03f969d9a..e8eccd44b 100644 --- a/Devices/cl32/source/cl32_v2_keyboard.cpp +++ b/Devices/cl32/source/cl32_v2_keyboard.cpp @@ -17,31 +17,31 @@ static constexpr uint32_t KEYMAP_LC[80] = { 37, 49, 50, 51, 52, 53, 54, 55, 56, 0, // % 1 2 3 4 5 6 7 8 57, 48, 8, 91, 93, 43, 34, 39, 27, 0, // 9 0 BKSP [ ] + " ' EXIT 9, 113, 119, 101, 114, 116, 121, 117, 105, 0, // TAB q w e r t y u i - 111, 112, 13, 40, 41, 45, 59, 58, 3, 0, // o p ENTER ( ) - ; : STOP + 111, 112, 10, 40, 41, 45, 59, 58, 3, 0, // o p ENTER ( ) - ; : STOP 0, 97, 115, 100, 102, 103, 104, 106, 107, 0, // a s d f g h j k 108, 17, 35, 123, 125, 42, 44, 46, 2, 0, // l UP # { } * , . MENU 122, 120, 99, 118, 98, 32, 32, 110, 109, 0, // z x c v b n m - CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 13, 0, // LEFT DOWN RIGHT < > / \ = RUN + CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 10, 0, // LEFT DOWN RIGHT < > / \ = RUN }; static constexpr uint32_t KEYMAP_UC[80] = { 37, 49, 50, 51, 52, 53, 54, 55, 56, 0, // % 1 2 3 4 5 6 7 8 57, 48, 8, 91, 93, 43, 34, 39, 27, 0, // 9 0 BKSP [ ] + " ' EXIT 9, 81, 87, 69, 82, 84, 89, 85, 73, 0, // TAB Q W E R T Y U I - 79, 80, 13, 40, 41, 45, 59, 58, 3, 0, // O P ENTER ( ) - ; : STOP + 79, 80, 10, 40, 41, 45, 59, 58, 3, 0, // O P ENTER ( ) - ; : STOP 0, 65, 83, 68, 70, 71, 72, 74, 75, 0, // A S D F G H J K 76, 17, 35, 123, 125, 42, 44, 46, 2, 0, // L UP # { } * , . MENU 90, 88, 67, 86, 66, 32, 32, 78, 77, 0, // Z X C V B N M - CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 13, 0, // LEFT DOWN RIGHT < > / \ = RUN + CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 10, 0, // LEFT DOWN RIGHT < > / \ = RUN }; static constexpr uint32_t KEYMAP_SY[80] = { 37, 49, 50, 51, 52, 53, 54, 55, 56, 0, // % 1 2 3 4 5 6 7 8 57, 48, 8, 91, 93, 43, 34, 39, 27, 0, // 9 0 BKSP [ ] + " ' EXIT 9, 113, 119, 101, 114, 116, 121, 117, 105, 0, // TAB q w e r t y u i - 111, 112, 13, 40, 41, 45, 59, 58, 3, 0, // o p ENTER ( ) - ; : STOP + 111, 112, 10, 40, 41, 45, 59, 58, 3, 0, // o p ENTER ( ) - ; : STOP 0, 97, 115, 100, 102, 103, 104, 106, 107, 0, // a s d f g h j k 108, 17, 35, 123, 125, 42, 44, 46, 2, 0, // l UP # { } * , . MENU 122, 120, 99, 118, 98, 32, 32, 110, 109, 0, // z x c v b n m - CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 13, 0, // LEFT DOWN RIGHT < > / \ = RUN + CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 10, 0, // LEFT DOWN RIGHT < > / \ = RUN }; static Tca8418Config cl32_keyboard_config {}; diff --git a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts index 85e44adf0..385f71417 100644 --- a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts +++ b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts @@ -88,19 +88,19 @@ keymap-lc = [ 113 119 101 114 116 121 117 105 111 112 // q w e r t y u i o p 97 115 100 102 103 104 106 107 108 8 // a s d f g h j k l BACKSPACE - 0 122 120 99 118 98 110 109 36 13 // z x c v b n m $ ENTER + 0 122 120 99 118 98 110 109 36 10 // z x c v b n m $ ENTER 0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT ]; keymap-uc = [ 81 87 69 82 84 89 85 73 79 80 // Q W E R T Y U I O P 65 83 68 70 71 72 74 75 76 8 // A S D F G H J K L BACKSPACE - 0 90 88 67 86 66 78 77 36 13 // Z X C V B N M $ ENTER + 0 90 88 67 86 66 78 77 36 10 // Z X C V B N M $ ENTER 0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT ]; keymap-sy = [ 49 50 51 52 53 54 55 56 57 48 // 1 2 3 4 5 6 7 8 9 0 64 35 43 45 42 47 40 41 95 8 // @ # + - * / ( ) _ BACKSPACE - 0 33 63 59 58 39 34 44 46 13 // ! ? ; : ' " , . ENTER + 0 33 63 59 58 39 34 44 46 10 // ! ? ; : ' " , . ENTER 0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT ]; shift-row = <2>; diff --git a/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts b/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts index eed3ea5d8..8358b5e4e 100644 --- a/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts +++ b/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts @@ -60,19 +60,19 @@ keymap-lc = [ 113 119 101 114 116 121 117 105 111 112 // q w e r t y u i o p 97 115 100 102 103 104 106 107 108 8 // a s d f g h j k l BACKSPACE - 0 122 120 99 118 98 110 109 36 13 // z x c v b n m $ ENTER + 0 122 120 99 118 98 110 109 36 10 // z x c v b n m $ ENTER 0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT ]; keymap-uc = [ 81 87 69 82 84 89 85 73 79 80 // Q W E R T Y U I O P 65 83 68 70 71 72 74 75 76 8 // A S D F G H J K L BACKSPACE - 0 90 88 67 86 66 78 77 36 13 // Z X C V B N M $ ENTER + 0 90 88 67 86 66 78 77 36 10 // Z X C V B N M $ ENTER 0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT ]; keymap-sy = [ 49 50 51 52 53 54 55 56 57 48 // 1 2 3 4 5 6 7 8 9 0 64 35 43 45 42 47 40 41 95 8 // @ # + - * / ( ) _ BACKSPACE - 0 33 63 59 58 39 34 44 46 13 // ! ? ; : ' " , . ENTER + 0 33 63 59 58 39 34 44 46 10 // ! ? ; : ' " , . ENTER 0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT ]; shift-row = <2>; diff --git a/Devices/lilygo-tlora-pager/CMakeLists.txt b/Devices/lilygo-tlora-pager/CMakeLists.txt deleted file mode 100644 index 1afb97ba9..000000000 --- a/Devices/lilygo-tlora-pager/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -file(GLOB_RECURSE SOURCE_FILES source/*.c*) - -idf_component_register( - SRCS ${SOURCE_FILES} - INCLUDE_DIRS "source" - REQUIRES TactilityKernel lvgl-module gpio-encoder-module -) diff --git a/Devices/lilygo-tlora-pager/device.properties b/Devices/lilygo-tlora-pager/device.properties index e9cfa96ee..38626e85d 100644 --- a/Devices/lilygo-tlora-pager/device.properties +++ b/Devices/lilygo-tlora-pager/device.properties @@ -21,3 +21,4 @@ display.dpi=227 lvgl.colorDepth=16 lvgl.dpi=150 +lvgl.uiDensity=compact diff --git a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts index 8f7293f13..058ed13f0 100644 --- a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts +++ b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts @@ -58,21 +58,21 @@ columns = <10>; keymap-lc = [ 113 119 101 114 116 121 117 105 111 112 // q w e r t y u i o p - 97 115 100 102 103 104 106 107 108 13 // a s d f g h j k l ENTER + 97 115 100 102 103 104 106 107 108 10 // a s d f g h j k l ENTER 0 122 120 99 118 98 110 109 0 8 // z x c v b n m BACKSPACE 32 0 0 0 0 0 0 0 0 0 // SPC ]; keymap-uc = [ 81 87 69 82 84 89 85 73 79 80 // Q W E R T Y U I O P - 65 83 68 70 71 72 74 75 76 13 // A S D F G H J K L ENTER + 65 83 68 70 71 72 74 75 76 10 // A S D F G H J K L ENTER 0 90 88 67 86 66 78 77 0 8 // Z X C V B N M BACKSPACE 32 0 0 0 0 0 0 0 0 0 // SPC ]; keymap-sy = [ 49 50 51 52 53 54 55 56 57 48 // 1 2 3 4 5 6 7 8 9 0 - 46 47 43 45 61 58 39 34 64 9 // . / + - = : ' " @ TAB + 46 47 43 45 61 58 39 34 64 27 // . / + - = : ' " @ ESC 0 95 36 59 63 33 44 46 0 8 // _ $ ; ? ! , . BACKSPACE - 32 0 0 0 0 0 0 0 0 0 // SPC + 9 0 0 0 0 0 0 0 0 0 // TAB ]; shift-row = <2>; shift-col = <8>; diff --git a/Devices/lilygo-tlora-pager/module.yaml b/Devices/lilygo-tlora-pager/module.yaml index 2ed8e0a22..acebfa42e 100644 --- a/Devices/lilygo-tlora-pager/module.yaml +++ b/Devices/lilygo-tlora-pager/module.yaml @@ -8,3 +8,4 @@ dependencies: - Drivers/drv2605-module - Drivers/gpio-encoder-module dts: lilygo,tlora-pager.dts +dts-only: true diff --git a/Devices/lilygo-tlora-pager/source/module.cpp b/Devices/lilygo-tlora-pager/source/module.cpp deleted file mode 100644 index 27797e5f7..000000000 --- a/Devices/lilygo-tlora-pager/source/module.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include -#include -#include - -constexpr auto* TAG = "T-Lora Pager"; - -extern "C" { - -static void on_boot_completed(struct SystemEvent* /*event*/, void* /*context*/) { - // The kernel gpio_encoder device is already started by kernel_init(); this just - // registers it as an LVGL input device, which requires LVGL to be up first. - lvgl_lock(); - lvgl_unlock(); -} - -static error_t start() { - system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr); - return ERROR_NONE; -} - -static error_t stop() { - system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed); - return ERROR_NONE; -} - -Module lilygo_tlora_pager_module = { - .name = "lilygo-tlora-pager", - .start = start, - .stop = stop, - .drivers = nullptr, - .symbols = nullptr, - .internal = nullptr -}; - -} diff --git a/Documentation/ideas.md b/Documentation/ideas.md index eb633a905..4435ce04d 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -11,7 +11,8 @@ - CrashDiagnostics shouldn't show a QR when there's no callstack - Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp - Add bold fonts for e-ink readability improvement -- Improve Setup: Add keyboard/keypad navigation explanation +- Improve Setup: Add keyboard/keypad/encoder navigation explanation. + e.g. encoder long press on t-lora pager to get out of textarea - Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module. - Improve SPI kernel driver (implement read, write, transactions) - TCA9534 keyboards should use interrupts diff --git a/Drivers/m5stack-module/source/cardputer_keyboard.cpp b/Drivers/m5stack-module/source/cardputer_keyboard.cpp index 704fe4cc2..f6f3191d1 100644 --- a/Drivers/m5stack-module/source/cardputer_keyboard.cpp +++ b/Drivers/m5stack-module/source/cardputer_keyboard.cpp @@ -54,7 +54,7 @@ static const CardputerKeyDef cardputer_key_map[CARDPUTER_ROWS][CARDPUTER_COLS] = { K('\t', '\t'), K('q', 'Q'), K('w', 'W'), K('e', 'E'), K('r', 'R'), K('t', 'T'), K('y', 'Y'), K('u', 'U'), K('i', 'I'), K('o', 'O'), K('p', 'P'), K('[', '{'), K(']', '}'), K('\\', '|') }, { { CARDPUTER_KEY_FN, 0, 0 }, { CARDPUTER_KEY_SHIFT, 0, 0 }, K('a', 'A'), K('s', 'S'), K('d', 'D'), K('f', 'F'), K('g', 'G'), - K('h', 'H'), K('j', 'J'), K('k', 'K'), K('l', 'L'), K(';', ':'), K('\'', '"'), K('\r', '\r') }, + K('h', 'H'), K('j', 'J'), K('k', 'K'), K('l', 'L'), K(';', ':'), K('\'', '"'), K('\n', '\n') }, { { CARDPUTER_KEY_CTRL, 0, 0 }, { CARDPUTER_KEY_OPT, 0, 0 }, { CARDPUTER_KEY_ALT, 0, 0 }, K('z', 'Z'), K('x', 'X'), K('c', 'C'), K('v', 'V'), K('b', 'B'), K('n', 'N'), K('m', 'M'), K(',', '<'), K('.', '>'), K('/', '?'), K(' ', ' ') }, }; diff --git a/Modules/app-esp32-module/CMakeLists.txt b/Modules/app-esp32-module/CMakeLists.txt index f1ff7b929..30424fa71 100644 --- a/Modules/app-esp32-module/CMakeLists.txt +++ b/Modules/app-esp32-module/CMakeLists.txt @@ -8,4 +8,6 @@ tactility_add_module(app-esp32-module SRCS ${SOURCE_FILES} INCLUDE_DIRS include/ REQUIRES TactilityKernel app-module service-module elf_loader + # The libc wraps in source/stdio_wrap.cpp are only reached through -Wl,--wrap=, never by direct reference + WHOLE_ARCHIVE ) diff --git a/Modules/app-esp32-module/source/app_esp32_loader_service.cpp b/Modules/app-esp32-module/source/app_esp32_loader_service.cpp index 6d8a51109..12add2c2f 100644 --- a/Modules/app-esp32-module/source/app_esp32_loader_service.cpp +++ b/Modules/app-esp32-module/source/app_esp32_loader_service.cpp @@ -150,6 +150,10 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) { return ERROR_RESOURCE; } + // Relocation copied everything it needs (segments, symbol names) out of the file buffer + free(runtime->file_data); + runtime->file_data = nullptr; + *out_runtime = runtime; return ERROR_NONE; } diff --git a/Modules/app-esp32-module/source/elf_cache.cpp b/Modules/app-esp32-module/source/elf_cache.cpp new file mode 100644 index 000000000..c39e4d76b --- /dev/null +++ b/Modules/app-esp32-module/source/elf_cache.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#ifdef CONFIG_ELF_LOADER_LOAD_PSRAM + +#include +#include + +#include + +// elf_loader writes back the whole D-cache with the ROM's Cache_WriteBack_All(), which doesn't take +// esp_cache_msync()'s lock. A cache operation running at the same time on the other core (e.g. a display +// driver syncing a PSRAM framebuffer) then fails with "Dcache sync parameter configuration error". +// The write-back therefore runs in core 0's IPC task while core 1 is parked in its own IPC task. An IPC task +// only runs when its core is not in a critical section, which is where esp_cache_msync() does its work. +extern "C" { + +void __real_esp_elf_arch_flush(void); +void Cache_WriteBack_All(void); +void spi_flash_disable_interrupts_caches_and_other_cpu(void); +void spi_flash_enable_interrupts_caches_and_other_cpu(void); + +} + +#if !CONFIG_FREERTOS_UNICORE && !CONFIG_IDF_TARGET_ESP32S31 + +namespace { + +std::atomic other_core_parked { false }; +std::atomic other_core_release { false }; + +void IRAM_ATTR park_core(void*) { + const uint32_t state = portSET_INTERRUPT_MASK_FROM_ISR(); + other_core_parked.store(true, std::memory_order_release); + while (!other_core_release.load(std::memory_order_acquire)) { + } + portCLEAR_INTERRUPT_MASK_FROM_ISR(state); +} + +void IRAM_ATTR write_back_with_core1_parked(void*) { + other_core_parked.store(false, std::memory_order_relaxed); + other_core_release.store(false, std::memory_order_relaxed); + esp_ipc_call(1, park_core, nullptr); + while (!other_core_parked.load(std::memory_order_acquire)) { + } + const uint32_t state = portSET_INTERRUPT_MASK_FROM_ISR(); + Cache_WriteBack_All(); + portCLEAR_INTERRUPT_MASK_FROM_ISR(state); + other_core_release.store(true, std::memory_order_release); +} + +} // namespace + +// IRAM: spi_flash_disable_interrupts_caches_and_other_cpu() returns here with the caches disabled +extern "C" void IRAM_ATTR __wrap_esp_elf_arch_flush(void) { + esp_ipc_call_blocking(0, write_back_with_core1_parked, nullptr); + // Same as elf_loader's own esp_elf_arch_flush() after its write-back + spi_flash_disable_interrupts_caches_and_other_cpu(); + spi_flash_enable_interrupts_caches_and_other_cpu(); +} + +#else + +extern "C" void IRAM_ATTR __wrap_esp_elf_arch_flush(void) { + __real_esp_elf_arch_flush(); +} + +#endif + +#endif // CONFIG_ELF_LOADER_LOAD_PSRAM diff --git a/Modules/app-esp32-module/source/elf_relocate.cpp b/Modules/app-esp32-module/source/elf_relocate.cpp new file mode 100644 index 000000000..0a7c82132 --- /dev/null +++ b/Modules/app-esp32-module/source/elf_relocate.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include + +// elf_loader's Xtensa relocation ignores the addend of R_XTENSA_GLOB_DAT/R_XTENSA_JMP_SLOT, which is +// S + A per the ABI. The compiler does emit addends there: newlib's ctype macros read (_ctype_ + 1)[c], +// which becomes "R_XTENSA_GLOB_DAT _ctype_ + 1". Without the addend every ctype lookup in an app reads +// the previous character's entry (e.g. isspace('!') is true). +extern "C" { + +int __real_esp_elf_arch_relocate(esp_elf_t* elf, const elf32_rela_t* rela, const elf32_sym_t* sym, uint32_t addr); + +int __wrap_esp_elf_arch_relocate(esp_elf_t* elf, const elf32_rela_t* rela, const elf32_sym_t* sym, uint32_t addr) { +#if CONFIG_IDF_TARGET_ARCH_XTENSA + constexpr uint32_t R_XTENSA_GLOB_DAT = 3; + constexpr uint32_t R_XTENSA_JMP_SLOT = 4; + const uint32_t type = ELF_R_TYPE(rela->info); + if (type == R_XTENSA_GLOB_DAT || type == R_XTENSA_JMP_SLOT) { + addr += rela->addend; + } +#endif + return __real_esp_elf_arch_relocate(elf, rela, sym, addr); +} + +} diff --git a/Modules/app-module/source/stdio_wrap_esp32.cpp b/Modules/app-esp32-module/source/stdio_wrap.cpp similarity index 65% rename from Modules/app-module/source/stdio_wrap_esp32.cpp rename to Modules/app-esp32-module/source/stdio_wrap.cpp index 0f42c4af1..648618a9d 100644 --- a/Modules/app-module/source/stdio_wrap_esp32.cpp +++ b/Modules/app-esp32-module/source/stdio_wrap.cpp @@ -1,11 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 -#ifdef ESP_PLATFORM - -#include +// libc wraps (-Wl,--wrap=, see the top-level CMakeLists.txt) that route an app instance's calls to +// its own fds, cwd and exit(). App-instance behavior itself lives in app-module (app/libc.h). #include +#include #include +#include +#include +#include +#include +#include + #include #include @@ -39,10 +45,9 @@ int __wrap_ioctl(int fd, int request, ...) { void* arg = va_arg(args, void*); va_end(args); - struct winsize windowSize {}; - if (tryAppWindowSize(fd, static_cast(request), arg, &windowSize)) { - *static_cast(arg) = windowSize; - return 0; + int result; + if (app_libc_try_window_size(fd, static_cast(request), arg, &result)) { + return result; } return __real_ioctl(fd, request, arg); } @@ -52,11 +57,7 @@ int __real_chdir(const char* path); char* __wrap_getcwd(char* buf, size_t size) { char* result; - int err; - if (tryAppGetCwd(buf, size, &result, &err)) { - if (result == nullptr) { - errno = err; - } + if (app_libc_try_getcwd(buf, size, &result)) { return result; } return __real_getcwd(buf, size); @@ -64,11 +65,7 @@ char* __wrap_getcwd(char* buf, size_t size) { int __wrap_chdir(const char* path) { int result; - int err; - if (tryAppChdir(path, &result, &err)) { - if (result != 0) { - errno = err; - } + if (app_libc_try_chdir(path, &result)) { return result; } return __real_chdir(path); @@ -79,7 +76,7 @@ int __real__fstat_r(struct _reent* r, int fd, struct stat* st); int __wrap__fstat_r(struct _reent* r, int fd, struct stat* st) { int result; - if (tryAppFstat(fd, st, &result)) { + if (app_libc_try_fstat(fd, st, &result)) { return result; } return __real__fstat_r(r, fd, st); @@ -89,7 +86,7 @@ int __real_poll(struct pollfd* fds, nfds_t nfds, int timeout); int __wrap_poll(struct pollfd* fds, nfds_t nfds, int timeout) { int result; - if (tryAppPoll(fds, nfds, timeout, __real_poll, &result)) { + if (app_libc_try_poll(fds, nfds, timeout, __real_poll, &result)) { return result; } return __real_poll(fds, nfds, timeout); @@ -100,7 +97,7 @@ int __real_tcsetattr(int fd, int optional_actions, const struct termios* p); int __wrap_tcgetattr(int fd, struct termios* p) { int result; - if (tryAppTcgetattr(fd, p, &result)) { + if (app_libc_try_tcgetattr(fd, p, &result)) { return result; } return __real_tcgetattr(fd, p); @@ -108,12 +105,32 @@ int __wrap_tcgetattr(int fd, struct termios* p) { int __wrap_tcsetattr(int fd, int optional_actions, const struct termios* p) { int result; - if (tryAppTcsetattr(fd, &result)) { + if (app_libc_try_tcsetattr(fd, p, &result)) { return result; } return __real_tcsetattr(fd, optional_actions, p); } +// ESP-IDF's newlib has no signal() of its own, so this is the only definition +_sig_func_ptr signal(int sig, _sig_func_ptr handler) { + AppLibcSignalHandler previous; + if (app_libc_try_signal(sig, handler, &previous)) { + return previous; + } + errno = ENOSYS; + return SIG_ERR; +} + +// Replaces the libc kill(), which reaches a _kill_r stub that aborts the device on some libc builds +int kill(pid_t pid, int sig) { + int result; + if (app_libc_try_kill(static_cast(pid), sig, &result)) { + return result; + } + errno = ENOSYS; + return -1; +} + // Called by an app, newlib's exit() would reach _exit(), which aborts the whole device [[noreturn]] void __real_exit(int status); @@ -123,5 +140,3 @@ int __wrap_tcsetattr(int fd, int optional_actions, const struct termios* p) { } } - -#endif // ESP_PLATFORM diff --git a/Modules/app-module/CMakeLists.txt b/Modules/app-module/CMakeLists.txt index 3f7f3a43a..63b67bb58 100644 --- a/Modules/app-module/CMakeLists.txt +++ b/Modules/app-module/CMakeLists.txt @@ -14,10 +14,9 @@ tactility_add_module(app-module ) # Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close() rather -# than calling ::read/::write/::close() directly, on every platform where those are wrapped: via -# -Wl,--wrap= on ESP-IDF (see the top-level CMakeLists.txt), or via source/stdio_wrap_posix.cpp's own -# read/write/close and printf-family definitions on POSIX (dyld interpose on Apple, plain -# symbol overrides elsewhere - see that file for why POSIX doesn't use --wrap). +# than calling ::read/::write/::close() directly, since those are wrapped by the platform module: +# app-esp32-module via -Wl,--wrap= (the linker provides __real_*, see the top-level CMakeLists.txt), +# app-posix-module via its own read/write/close overrides (it defines __real_*, see its stdio_wrap.cpp). tactility_get_module_name(app-module MODULE_NAME) target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO) diff --git a/Modules/app-module/include/app/dir.h b/Modules/app-module/include/app/dir.h index acdc14f7a..c1be863a3 100644 --- a/Modules/app-module/include/app/dir.h +++ b/Modules/app-module/include/app/dir.h @@ -13,7 +13,7 @@ * instance's cwd. * * Matches POSIX getcwd()/chdir() semantics, and is what their real-syscall wraps - * (Modules/app-module/source/stdio_wrap.cpp) route to for a calling task that is an app instance. + * (the platform modules' stdio_wrap.cpp, via app/libc.h) route to for a calling task that is an app instance. */ #ifdef __cplusplus diff --git a/Modules/app-module/include/app/file.h b/Modules/app-module/include/app/file.h index c82f44663..e65a654ab 100644 --- a/Modules/app-module/include/app/file.h +++ b/Modules/app-module/include/app/file.h @@ -47,7 +47,6 @@ struct AppFileOps { struct AppFile { const struct AppFileOps* ops; void* object; - bool suppress_console_tee; }; #ifdef __cplusplus diff --git a/Modules/app-module/include/app/io.h b/Modules/app-module/include/app/io.h index fa9e56888..4d4cc75d8 100644 --- a/Modules/app-module/include/app/io.h +++ b/Modules/app-module/include/app/io.h @@ -53,7 +53,6 @@ error_t app_io_ioctl(int fd, AppIoctlRequest request, void* arg); /** * Installs a custom AppFileOps at @a fd in the calling task's own app instance fd table. - * Unlike AppStreamBinding, writes are never teed to the real underlying fd. * @warning Must be called from the app instance's own task. * @retval ERROR_NOT_FOUND the calling task isn't a running app instance * @retval ERROR_OUT_OF_RANGE @a fd is outside [0, APP_MAX_FDS) diff --git a/Modules/app-module/include/app/libc.h b/Modules/app-module/include/app/libc.h new file mode 100644 index 000000000..8b66ec481 --- /dev/null +++ b/Modules/app-module/include/app/libc.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct stat; +struct termios; + +/** + * App-instance behavior for libc calls, used by the platform modules' libc wraps + * (app-esp32-module, app-posix-module). + * + * Each app_libc_try_*() function returns false when the call has nothing to do with an app instance + * (not called from an app's task, or not an app fd), so the caller falls through to the real libc + * function. When it returns true, the call was handled: *out_result holds the return value, and errno + * is set on failure. A closed app fd fails with EBADF, so it never reaches a real fd of the same number. + */ + +/** Handles ioctl(fd, TIOCGWINSZ, struct winsize*) for an app fd. */ +bool app_libc_try_window_size(int fd, unsigned long request, void* arg, int* out_result); + +/** @param[out] out_result @a buf on success, NULL on failure */ +bool app_libc_try_getcwd(char* buf, size_t size, char** out_result); + +bool app_libc_try_chdir(const char* path, int* out_result); + +/** App fds report as character devices, which also makes isatty() true for them. */ +bool app_libc_try_fstat(int fd, struct stat* st, int* out_result); + +/** + * App fd input is always raw and unechoed, and a written '\n' also returns the cursor. + * The input flags are per app instance: ICRNL (on by default, like a terminal) makes stdin read '\r' as '\n'. + */ +bool app_libc_try_tcgetattr(int fd, struct termios* t, int* out_result); + +/** Stores the input flags (c_iflag, of which ICRNL is applied). Other settings are accepted without effect. */ +bool app_libc_try_tcsetattr(int fd, const struct termios* t, int* out_result); + +/** Signal numbers an app can register a handler for are 1 to APP_LIBC_SIGNAL_COUNT - 1. */ +#define APP_LIBC_SIGNAL_COUNT 32 + +typedef void (*AppLibcSignalHandler)(int sig); + +/** + * Records @a handler for @a sig in the calling app instance. Signals aren't delivered to apps yet, + * so the handler is never called. SIGKILL, SIGSTOP and out-of-range signals fail with EINVAL. + * @param[out] out_previous the previously registered handler, or SIG_ERR on failure + */ +bool app_libc_try_signal(int sig, AppLibcSignalHandler handler, AppLibcSignalHandler* out_previous); + +/** Apps can't send signals: kill() from an app fails with ENOSYS rather than reaching the process. */ +bool app_libc_try_kill(int pid, int sig, int* out_result); + +typedef int (*AppLibcPollFunction)(struct pollfd* fds, nfds_t nfds, int timeout); + +/** + * Handles poll() when @a fds contains an app fd. Other fds are checked with @a real_poll. + * Closed app fds report POLLNVAL. + */ +bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPollFunction real_poll, int* out_result); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/app-module/include/app/package_manifest.h b/Modules/app-module/include/app/package_manifest.h index 28518fe23..e7590c664 100644 --- a/Modules/app-module/include/app/package_manifest.h +++ b/Modules/app-module/include/app/package_manifest.h @@ -5,6 +5,7 @@ #include +#include #include #ifdef __cplusplus @@ -59,6 +60,9 @@ struct PackageManifest { */ char requires_device_id[PACKAGE_MANIFEST_REQUIRES_DEVICE_ID_LENGTH + 1]; + /** RAM in megabytes (1024 * 1024 bytes) the package needs to run ("requires.ram", e.g. "2M"). 0 means no requirement. */ + uint8_t requires_ram; + /** How many AppManifest entries this package's manifest.properties declared. */ uint32_t app_manifest_count; }; @@ -86,6 +90,12 @@ struct AppManifestBinding { */ error_t app_package_manifest_parse(const char* path, struct PackageManifest* out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity); +/** + * @return true if this device can run the package: it is listed in requires_device_id (or that is empty), + * and its total RAM (internal plus external) is at least requires_ram megabytes + */ +bool app_package_manifest_is_compatible(const struct PackageManifest* const manifest); + #ifdef __cplusplus } #endif diff --git a/Modules/app-module/include/poll.h b/Modules/app-module/include/poll.h new file mode 100644 index 000000000..f79342432 --- /dev/null +++ b/Modules/app-module/include/poll.h @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// ESP-IDF only provides . Apps include this through app-module's include path. +#pragma once + +#include diff --git a/Modules/app-module/include/sys/ioctl.h b/Modules/app-module/include/sys/ioctl.h new file mode 100644 index 000000000..78cb89983 --- /dev/null +++ b/Modules/app-module/include/sys/ioctl.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// ESP-IDF's has no terminal ioctls. Apps include this through app-module's include path. +// For an app fd, ioctl(fd, TIOCGWINSZ, &winsize) reports the terminal size (see app/libc.h). +#pragma once + +#include_next + +#ifndef TIOCGWINSZ +#define TIOCGWINSZ 0x5413 +struct winsize { + unsigned short ws_row; + unsigned short ws_col; + unsigned short ws_xpixel; + unsigned short ws_ypixel; +}; +#endif diff --git a/Modules/app-module/private/app/private/fd_table.h b/Modules/app-module/private/app/private/fd_table.h index 66ed2e787..d1ee41d9e 100644 --- a/Modules/app-module/private/app/private/fd_table.h +++ b/Modules/app-module/private/app/private/fd_table.h @@ -58,10 +58,9 @@ void app_fd_table_teardown(struct AppFdTable* table); /** * Installs {ops, object} at @a fd, closing whatever was previously there first (the null device * counts as "previously there" for fds 0-2, so this doubles as their initial stdio binding). - * @param[in] suppress_console_tee see AppFile::suppress_console_tee * @retval ERROR_OUT_OF_RANGE @a fd is outside [0, APP_MAX_FDS) */ -error_t app_fd_table_bind(struct AppFdTable* table, int fd, const struct AppFileOps* ops, void* object, bool suppress_console_tee); +error_t app_fd_table_bind(struct AppFdTable* table, int fd, const struct AppFileOps* ops, void* object); /** * Installs {ops, object} at the lowest unused fd >= 3. diff --git a/Modules/app-module/private/app/private/ledger.h b/Modules/app-module/private/app/private/ledger.h index 20d8f2404..5d26317bf 100644 --- a/Modules/app-module/private/app/private/ledger.h +++ b/Modules/app-module/private/app/private/ledger.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include +#include #include #include #include @@ -71,6 +73,12 @@ struct AppInstanceRecord { /** Inherited from the parent instance, like `env`. Mutable via app_dir_set_cwd() (app/dir.h). * Always absolute. */ std::string cwd = "/"; + + /** Handlers registered via signal() (app/libc.h), indexed by signal number. Null means SIG_DFL. */ + AppLibcSignalHandler signal_handlers[APP_LIBC_SIGNAL_COUNT] {}; + + /** Terminal input flags set via tcsetattr() (app/libc.h). ICRNL is applied when reading stdin. */ + tcflag_t termios_iflag = ICRNL; }; /** A registered installed package - see app_manager_add_package() (app/manager.h). */ diff --git a/Modules/app-module/private/app/private/stdio_wrap.h b/Modules/app-module/private/app/private/stdio_wrap.h deleted file mode 100644 index 37a02627f..000000000 --- a/Modules/app-module/private/app/private/stdio_wrap.h +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -// Neither platform's headers declare TIOCGWINSZ/struct winsize (ESP-IDF's sys/ioctl.h has no -// terminal ioctls at all; the guard is only for POSIX, where already provides them). -#ifndef TIOCGWINSZ -#define TIOCGWINSZ 0x5413 -struct winsize { - unsigned short ws_row; - unsigned short ws_col; - unsigned short ws_xpixel; - unsigned short ws_ypixel; -}; -#endif - -using PollFunction = int (*)(struct pollfd*, nfds_t, int); - -/** @return true (with *out filled) if this was a window-size query on an app-owned fd */ -bool tryAppWindowSize(int fd, unsigned long request, void* arg, struct winsize* out); - -/** @return false if the caller isn't an app instance and should fall through to the real getcwd() */ -bool tryAppGetCwd(char* buf, size_t size, char** out, int* outErrno); - -/** @return false if the caller isn't an app instance and should fall through to the real chdir() */ -bool tryAppChdir(const char* path, int* outResult, int* outErrno); - -enum class AppFdState { - Bound, - /** An app fd that was already closed. Fails with EBADF rather than reaching a real fd of the same number. */ - Closed, - /** Not an app fd. Falls through to the real syscall. */ - NotAppFd, -}; - -AppFdState getAppFdState(int fd); - -/** - * The tryApp*() fd helpers below set *outResult (and errno on failure) when they return true. - * @return false if @a fd isn't an app fd and the caller should fall through to the real call - */ -bool tryAppFstat(int fd, struct stat* st, int* outResult); -bool tryAppTcgetattr(int fd, struct termios* t, int* outResult); -bool tryAppTcsetattr(int fd, int* outResult); - -/** - * Closed app fds report POLLNVAL. - * @return false if no fd in @a fds is an app fd and the caller should fall through to @a realPoll - */ -bool tryAppPoll(struct pollfd* fds, nfds_t nfds, int timeout, PollFunction realPoll, int* outResult); diff --git a/Modules/app-module/source/fd_table.cpp b/Modules/app-module/source/fd_table.cpp index 1435a24b1..0a016f2c8 100644 --- a/Modules/app-module/source/fd_table.cpp +++ b/Modules/app-module/source/fd_table.cpp @@ -71,7 +71,7 @@ void app_fd_table_teardown(AppFdTable* table) { mutex_destruct(&table->mutex); } -error_t app_fd_table_bind(AppFdTable* table, int fd, const AppFileOps* ops, void* object, bool suppress_console_tee) { +error_t app_fd_table_bind(AppFdTable* table, int fd, const AppFileOps* ops, void* object) { if (!fd_in_range(fd)) { return ERROR_OUT_OF_RANGE; } @@ -81,7 +81,7 @@ error_t app_fd_table_bind(AppFdTable* table, int fd, const AppFileOps* ops, void AppFdSlot& slot = table->slots[fd]; AppFile old = slot.file; bool had_old = slot.in_use; - slot.file = { .ops = ops, .object = object, .suppress_console_tee = suppress_console_tee }; + slot.file = { .ops = ops, .object = object }; slot.in_use = true; slot.ever_used = true; mutex_unlock(&table->mutex); diff --git a/Modules/app-module/source/io.cpp b/Modules/app-module/source/io.cpp index 6c2fd57e4..be7775c35 100644 --- a/Modules/app-module/source/io.cpp +++ b/Modules/app-module/source/io.cpp @@ -48,6 +48,20 @@ int real_close(int fd) { return ::close(fd); } namespace { +// Whether the calling app instance has ICRNL set (see AppInstanceRecord::termios_iflag). +bool current_app_translates_cr() { + AppInstanceId app_id = app_scheduler_current_app_id(); + if (app_id == 0) { + return false; + } + auto& ledger = app_ledger(); + mutex_lock(&ledger.mutex); + auto iterator = ledger.instances.find(app_id); + bool result = (iterator != ledger.instances.end()) && (iterator->second.termios_iflag & ICRNL) != 0; + mutex_unlock(&ledger.mutex); + return result; +} + // NULL for a caller not running as an app instance (e.g. a kernel service task). @a fd is then // a real underlying fd, handled by the caller falling through to the real syscall. AppFdTable* current_app_fd_table() { @@ -75,6 +89,15 @@ ssize_t app_io_read(int fd, void* buffer, size_t size) { if (file.ops->release != nullptr) { file.ops->release(file.object); } + // Like a terminal with ICRNL: the Enter key's '\r' reads as '\n' + if (fd == STDIN_FILENO && result > 0 && current_app_translates_cr()) { + auto* bytes = static_cast(buffer); + for (ssize_t i = 0; i < result; i++) { + if (bytes[i] == '\r') { + bytes[i] = '\n'; + } + } + } return result; } // @a fd isn't currently bound. If this table has never touched it either, it's a real @@ -96,17 +119,6 @@ ssize_t app_io_write(int fd, const void* buffer, size_t size) { if (file.ops->release != nullptr) { file.ops->release(file.object); } - // Tee to the real fd too: a bound stream normally exists because a parent explicitly - // asked to capture this app instance's own output (see AppStreamBinding), but generic - // code running on that same instance's thread - most commonly the platform's own logging - // (LOG_I/etc, which calls write() the same as anything else) - has no way to know its - // output is currently being intercepted. Without this, a log line emitted while any app - // instance has its stdout captured would vanish from the console entirely instead of - // just also being visible to the capturing parent. An instance that bound its own fd - // (see app_io_bind_self()) opts out - it wants exclusive ownership, not a silent tap. - if (!file.suppress_console_tee) { - real_write(fd, buffer, size); - } return result; } if (table != nullptr && app_fd_table_is_app_owned(table, fd)) { @@ -184,7 +196,7 @@ error_t app_io_bind_self(int fd, const AppFileOps* ops, void* object) { if (table == nullptr) { return ERROR_NOT_FOUND; } - return app_fd_table_bind(table, fd, ops, object, /*suppress_console_tee=*/true); + return app_fd_table_bind(table, fd, ops, object); } } // extern "C" diff --git a/Modules/app-module/source/libc.cpp b/Modules/app-module/source/libc.cpp new file mode 100644 index 000000000..298a4689b --- /dev/null +++ b/Modules/app-module/source/libc.cpp @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +enum class AppFdState { + Bound, + /** An app fd that was already closed. Fails with EBADF rather than reaching a real fd of the same number. */ + Closed, + /** Not an app fd. Falls through to the real syscall. */ + NotAppFd, +}; + +AppFdState get_app_fd_state(int fd) { + uint32_t bits; + switch (app_io_poll(fd, &bits)) { + case ERROR_NONE: + return AppFdState::Bound; + case ERROR_INVALID_STATE: + return AppFdState::Closed; + default: + return AppFdState::NotAppFd; + } +} + +// Upper bound on wake latency while waiting on more than one fd: app streams can only be awaited one at a time. +constexpr TickType_t POLL_INTERVAL_TICKS = pdMS_TO_TICKS(10); + +} // namespace + +extern "C" { + +bool app_libc_try_window_size(int fd, unsigned long request, void* arg, int* out_result) { + if (request != TIOCGWINSZ) { + return false; + } + const AppFdState state = get_app_fd_state(fd); + if (state == AppFdState::Closed) { + errno = EBADF; + *out_result = -1; + return true; + } + if (arg == nullptr || state == AppFdState::NotAppFd) { + return false; + } + AppWindowSize size {}; + if (app_io_ioctl(fd, APP_IOCTL_GET_WINDOW_SIZE, &size) != ERROR_NONE) { + return false; + } + auto* out = static_cast(arg); + out->ws_row = size.rows; + out->ws_col = size.columns; + out->ws_xpixel = 0; + out->ws_ypixel = 0; + *out_result = 0; + return true; +} + +// app_dir_get_cwd() distinguishes "not an app instance" (ERROR_NOT_FOUND, caller falls through to the +// real syscall) from "buffer too small" (ERROR_BUFFER_OVERFLOW, a real failure to report). +bool app_libc_try_getcwd(char* buf, size_t size, char** out_result) { + if (buf == nullptr) { + return false; + } + const error_t result = app_dir_get_cwd(buf, size); + if (result == ERROR_NONE) { + *out_result = buf; + return true; + } + if (result == ERROR_BUFFER_OVERFLOW) { + errno = ERANGE; + *out_result = nullptr; + return true; + } + return false; // ERROR_NOT_FOUND: not an app instance. +} + +// app_dir_set_cwd() requires an already-absolute path and reports both "not an app instance" and +// "no such directory" as ERROR_NOT_FOUND, so app_dir_get_cwd() is used first as an unambiguous +// "is this an app instance" probe (it's needed anyway, to resolve a relative path). +bool app_libc_try_chdir(const char* path, int* out_result) { + if (path == nullptr || path[0] == '\0') { + return false; + } + char cwd[FILE_MAX_PATH_STRING_LENGTH]; + if (app_dir_get_cwd(cwd, sizeof(cwd)) != ERROR_NONE) { + return false; // not an app instance + } + + char resolved[FILE_MAX_PATH_STRING_LENGTH]; + const int written = (path[0] == '/') ? snprintf(resolved, sizeof(resolved), "%s", path) + : (strcmp(cwd, "/") == 0) ? snprintf(resolved, sizeof(resolved), "/%s", path) + : snprintf(resolved, sizeof(resolved), "%s/%s", cwd, path); + if (written < 0 || static_cast(written) >= sizeof(resolved)) { + errno = ENAMETOOLONG; + *out_result = -1; + return true; + } + + if (app_dir_set_cwd(resolved) == ERROR_NONE) { + *out_result = 0; + } else { + errno = ENOENT; + *out_result = -1; + } + return true; +} + +bool app_libc_try_fstat(int fd, struct stat* st, int* out_result) { + const AppFdState state = get_app_fd_state(fd); + if (state == AppFdState::NotAppFd) { + return false; + } + if (state == AppFdState::Closed) { + errno = EBADF; + *out_result = -1; + return true; + } + if (st == nullptr) { + errno = EFAULT; + *out_result = -1; + return true; + } + memset(st, 0, sizeof(*st)); + st->st_mode = S_IFCHR | 0666; + *out_result = 0; + return true; +} + +bool app_libc_try_tcgetattr(int fd, struct termios* t, int* out_result) { + const AppFdState state = get_app_fd_state(fd); + if (state == AppFdState::NotAppFd) { + return false; + } + if (state == AppFdState::Closed) { + errno = EBADF; + *out_result = -1; + return true; + } + if (t == nullptr) { + errno = EFAULT; + *out_result = -1; + return true; + } + tcflag_t iflag = ICRNL; + auto& ledger = app_ledger(); + mutex_lock(&ledger.mutex); + auto iterator = ledger.instances.find(app_scheduler_current_app_id()); + if (iterator != ledger.instances.end()) { + iflag = iterator->second.termios_iflag; + } + mutex_unlock(&ledger.mutex); + memset(t, 0, sizeof(*t)); + t->c_iflag = iflag; + t->c_oflag = OPOST | ONLCR; + t->c_cflag = CS8 | CREAD; + t->c_cc[VMIN] = 1; + t->c_cc[VTIME] = 0; + *out_result = 0; + return true; +} + +bool app_libc_try_tcsetattr(int fd, const struct termios* t, int* out_result) { + const AppFdState state = get_app_fd_state(fd); + if (state == AppFdState::NotAppFd) { + return false; + } + if (state == AppFdState::Closed) { + errno = EBADF; + *out_result = -1; + return true; + } + if (t == nullptr) { + errno = EFAULT; + *out_result = -1; + return true; + } + auto& ledger = app_ledger(); + mutex_lock(&ledger.mutex); + auto iterator = ledger.instances.find(app_scheduler_current_app_id()); + if (iterator != ledger.instances.end()) { + iterator->second.termios_iflag = t->c_iflag; + } + mutex_unlock(&ledger.mutex); + *out_result = 0; + return true; +} + +bool app_libc_try_signal(int sig, AppLibcSignalHandler handler, AppLibcSignalHandler* out_previous) { + const AppInstanceId app_instance_id = app_scheduler_current_app_id(); + if (app_instance_id == 0) { + return false; + } + if (sig <= 0 || sig >= APP_LIBC_SIGNAL_COUNT || sig == SIGKILL || sig == SIGSTOP) { + errno = EINVAL; + *out_previous = SIG_ERR; + return true; + } + auto& ledger = app_ledger(); + mutex_lock(&ledger.mutex); + auto iterator = ledger.instances.find(app_instance_id); + if (iterator == ledger.instances.end()) { + mutex_unlock(&ledger.mutex); + return false; + } + AppLibcSignalHandler previous = iterator->second.signal_handlers[sig]; + iterator->second.signal_handlers[sig] = handler; + mutex_unlock(&ledger.mutex); + *out_previous = previous; + return true; +} + +bool app_libc_try_kill(int pid, int sig, int* out_result) { + if (app_scheduler_current_app_id() == 0) { + return false; + } + errno = ENOSYS; + *out_result = -1; + return true; +} + +// Non-app fds in @a fds are checked with a zero timeout real poll() on every iteration. +bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPollFunction real_poll, int* out_result) { + if (fds == nullptr) { + return false; + } + // A closed app fd is always ready (POLLNVAL), so the wait below always has a bound fd to await + int first_app_index = -1; + bool has_app_fd = false; + nfds_t active_count = 0; + for (nfds_t i = 0; i < nfds; i++) { + if (fds[i].fd < 0) { + continue; + } + active_count++; + const AppFdState state = get_app_fd_state(fds[i].fd); + has_app_fd = has_app_fd || state != AppFdState::NotAppFd; + if (first_app_index < 0 && state == AppFdState::Bound) { + first_app_index = static_cast(i); + } + } + if (!has_app_fd) { + return false; + } + + const TickType_t start = get_ticks(); + const TickType_t timeout_ticks = (timeout < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout); + while (true) { + int ready = 0; + for (nfds_t i = 0; i < nfds; i++) { + fds[i].revents = 0; + if (fds[i].fd < 0) { + continue; + } + uint32_t bits; + const error_t poll_result = app_io_poll(fds[i].fd, &bits); + if (poll_result == ERROR_NONE) { + if ((fds[i].events & POLLIN) && (bits & APP_FILE_READABLE)) { + fds[i].revents |= POLLIN; + } + if ((fds[i].events & POLLOUT) && (bits & APP_FILE_WRITABLE)) { + fds[i].revents |= POLLOUT; + } + } else if (poll_result == ERROR_INVALID_STATE) { + fds[i].revents = POLLNVAL; + } else { + real_poll(&fds[i], 1, 0); + } + if (fds[i].revents != 0) { + ready++; + } + } + if (ready > 0) { + *out_result = ready; + return true; + } + + TickType_t remaining = (timeout < 0) ? portMAX_DELAY : get_timeout_remaining_ticks(timeout_ticks, start); + if (remaining == 0) { + *out_result = 0; + return true; + } + if (active_count > 1 && remaining > POLL_INTERVAL_TICKS) { + remaining = POLL_INTERVAL_TICKS; + } + const struct pollfd& first = fds[first_app_index]; + if (first.events & POLLIN) { + app_io_await(first.fd, APP_FILE_WAIT_READABLE, remaining); + } else if (first.events & POLLOUT) { + app_io_await(first.fd, APP_FILE_WAIT_WRITABLE, remaining); + } else { + delay_ticks(remaining < POLL_INTERVAL_TICKS ? remaining : POLL_INTERVAL_TICKS); + } + } +} + +} // extern "C" diff --git a/Modules/app-module/source/module.cpp b/Modules/app-module/source/module.cpp index 25596b8f5..515770b05 100644 --- a/Modules/app-module/source/module.cpp +++ b/Modules/app-module/source/module.cpp @@ -78,6 +78,7 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(app_manifest_stack_size_is_valid), // app/package_manifest DEFINE_MODULE_SYMBOL(app_package_manifest_parse), + DEFINE_MODULE_SYMBOL(app_package_manifest_is_compatible), // app/paths DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory), DEFINE_MODULE_SYMBOL(app_paths_get_user_data_path), diff --git a/Modules/app-module/source/package_compatibility.cpp b/Modules/app-module/source/package_compatibility.cpp new file mode 100644 index 000000000..4e9d776f3 --- /dev/null +++ b/Modules/app-module/source/package_compatibility.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#ifdef ESP_PLATFORM +#include +#endif + +#include + +namespace { + +// requires_device_id is a comma-separated list without spaces (see app_package_manifest_is_valid_device_id_list()) +bool is_device_listed(const char* device_ids, const char* device_id) { + const size_t device_id_length = strlen(device_id); + const char* item = device_ids; + while (*item != '\0') { + const char* comma = strchr(item, ','); + const size_t item_length = (comma != nullptr) ? static_cast(comma - item) : strlen(item); + if (item_length == device_id_length && strncmp(item, device_id, item_length) == 0) { + return true; + } + if (comma == nullptr) { + break; + } + item = comma + 1; + } + return false; +} + +} // namespace + +extern "C" { + +bool app_package_manifest_is_compatible(const struct PackageManifest* const manifest) { + if (manifest->requires_device_id[0] != '\0' && !is_device_listed(manifest->requires_device_id, CONFIG_TT_DEVICE_ID)) { + return false; + } + // Internal and external memory together, as an app's allocations can come from either + const uint64_t total_ram = static_cast(memory_heap_total()) + memory_external_total(); + const uint64_t required_ram = static_cast(manifest->requires_ram) * 1024 * 1024; + return total_ram >= required_ram; +} + +} // extern "C" diff --git a/Modules/app-module/source/package_manifest_parsing_v3.cpp b/Modules/app-module/source/package_manifest_parsing_v3.cpp index 733982267..80a316af4 100644 --- a/Modules/app-module/source/package_manifest_parsing_v3.cpp +++ b/Modules/app-module/source/package_manifest_parsing_v3.cpp @@ -210,6 +210,21 @@ error_t package_manifest_parse_v3(const std::map& prop } } + // requires.ram: whole megabytes with an "M" suffix, e.g. "2M" + auto ram_iterator = properties.find("requires.ram"); + if (ram_iterator != properties.end()) { + const std::string& ram = ram_iterator->second; + uint32_t megabytes = 0; + const char* first = ram.data(); + const char* last = first + ram.size(); + const auto [end, error] = std::from_chars(first, last, megabytes); + if (error != std::errc {} || end != last - 1 || *end != 'M' || megabytes > UINT8_MAX) { + LOG_E(TAG, "Invalid requires.ram"); + return ERROR_INVALID_ARGUMENT; + } + out_package.requires_ram = static_cast(megabytes); + } + // app..* blocks: 0-indexed, contiguous - the first missing "app..id" ends the list. size_t count = 0; while (properties.contains(std::format("app.{}.id", count))) { diff --git a/Modules/app-module/source/scheduler.cpp b/Modules/app-module/source/scheduler.cpp index 7cc064464..653a8adc7 100644 --- a/Modules/app-module/source/scheduler.cpp +++ b/Modules/app-module/source/scheduler.cpp @@ -34,6 +34,11 @@ constexpr auto* TAG = "app_scheduler"; constexpr size_t APP_INSTANCE_ID_THREAD_SLOT_INDEX = 1; AppInstanceId get_current_app_id() { + // No current task before the scheduler starts (e.g. newlib's stdio setup calling fstat()), + // where pvTaskGetThreadLocalStoragePointer(nullptr, ...) would dereference a null task. + if (xTaskGetCurrentTaskHandle() == nullptr) { + return 0; + } return reinterpret_cast(pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX)); } diff --git a/Modules/app-module/source/stdio_wrap.cpp b/Modules/app-module/source/stdio_wrap.cpp deleted file mode 100644 index 5a431d4d1..000000000 --- a/Modules/app-module/source/stdio_wrap.cpp +++ /dev/null @@ -1,239 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Platform-independent helpers for the syscall wraps in stdio_wrap_esp32.cpp and stdio_wrap_posix.cpp. -#include - -#include -#include - -#include -#include -#include - -#include -#include - -bool tryAppWindowSize(int fd, unsigned long request, void* arg, struct winsize* out) { - if (request != TIOCGWINSZ || arg == nullptr) { - return false; - } - AppWindowSize size {}; - if (app_io_ioctl(fd, APP_IOCTL_GET_WINDOW_SIZE, &size) != ERROR_NONE) { - return false; - } - out->ws_row = size.rows; - out->ws_col = size.columns; - out->ws_xpixel = 0; - out->ws_ypixel = 0; - return true; -} - -// getcwd(): app_dir_get_cwd() distinguishes "not an app instance" (ERROR_NOT_FOUND, caller falls -// through to the real syscall) from "buffer too small" (ERROR_BUFFER_OVERFLOW, a real failure to -// report), so no separate probe is needed here. -bool tryAppGetCwd(char* buf, size_t size, char** out, int* outErrno) { - if (buf == nullptr) { - return false; - } - const error_t result = app_dir_get_cwd(buf, size); - if (result == ERROR_NONE) { - *out = buf; - return true; - } - if (result == ERROR_BUFFER_OVERFLOW) { - *out = nullptr; - *outErrno = ERANGE; - return true; - } - return false; // ERROR_NOT_FOUND: not an app instance. -} - -// chdir(): app_dir_set_cwd() requires an already-absolute path and reports both "not an app -// instance" and "no such directory" as ERROR_NOT_FOUND, so app_dir_get_cwd() is used first as a -// cheap, unambiguous "is this an app instance" probe (it's needed anyway, to resolve a relative -// path) - only once that confirms an app instance is calling is app_dir_set_cwd()'s own result -// treated as a real success/failure to report, rather than a reason to fall through. -bool tryAppChdir(const char* path, int* outResult, int* outErrno) { - if (path == nullptr || path[0] == '\0') { - return false; - } - char cwd[FILE_MAX_PATH_STRING_LENGTH]; - if (app_dir_get_cwd(cwd, sizeof(cwd)) != ERROR_NONE) { - return false; // not an app instance - } - - char resolved[FILE_MAX_PATH_STRING_LENGTH]; - const int written = (path[0] == '/') ? snprintf(resolved, sizeof(resolved), "%s", path) - : (strcmp(cwd, "/") == 0) ? snprintf(resolved, sizeof(resolved), "/%s", path) - : snprintf(resolved, sizeof(resolved), "%s/%s", cwd, path); - if (written < 0 || static_cast(written) >= sizeof(resolved)) { - *outResult = -1; - *outErrno = ENAMETOOLONG; - return true; - } - - if (app_dir_set_cwd(resolved) == ERROR_NONE) { - *outResult = 0; - } else { - *outResult = -1; - *outErrno = ENOENT; - } - return true; -} - -AppFdState getAppFdState(int fd) { - uint32_t bits; - switch (app_io_poll(fd, &bits)) { - case ERROR_NONE: - return AppFdState::Bound; - case ERROR_INVALID_STATE: - return AppFdState::Closed; - default: - return AppFdState::NotAppFd; - } -} - -// App fds report as character devices, which also makes isatty() true for them. -bool tryAppFstat(int fd, struct stat* st, int* outResult) { - const AppFdState state = getAppFdState(fd); - if (state == AppFdState::NotAppFd) { - return false; - } - if (state == AppFdState::Closed) { - errno = EBADF; - *outResult = -1; - return true; - } - if (st == nullptr) { - errno = EFAULT; - *outResult = -1; - return true; - } - memset(st, 0, sizeof(*st)); - st->st_mode = S_IFCHR | 0666; - *outResult = 0; - return true; -} - -// App fd input is always raw and unechoed, and a written '\n' also returns the cursor. -bool tryAppTcgetattr(int fd, struct termios* t, int* outResult) { - const AppFdState state = getAppFdState(fd); - if (state == AppFdState::NotAppFd) { - return false; - } - if (state == AppFdState::Closed) { - errno = EBADF; - *outResult = -1; - return true; - } - if (t == nullptr) { - errno = EFAULT; - *outResult = -1; - return true; - } - memset(t, 0, sizeof(*t)); - t->c_oflag = OPOST | ONLCR; - t->c_cflag = CS8 | CREAD; - t->c_cc[VMIN] = 1; - t->c_cc[VTIME] = 0; - *outResult = 0; - return true; -} - -// Accepts any settings without applying them. -bool tryAppTcsetattr(int fd, int* outResult) { - const AppFdState state = getAppFdState(fd); - if (state == AppFdState::NotAppFd) { - return false; - } - if (state == AppFdState::Closed) { - errno = EBADF; - *outResult = -1; - return true; - } - *outResult = 0; - return true; -} - -namespace { - -// Upper bound on wake latency while waiting on more than one fd: app streams can only be awaited one at a time. -constexpr TickType_t POLL_INTERVAL_TICKS = pdMS_TO_TICKS(10); - -} // namespace - -// Non-app fds in @a fds are checked with a zero timeout real poll() on every iteration. -bool tryAppPoll(struct pollfd* fds, nfds_t nfds, int timeout, PollFunction realPoll, int* outResult) { - if (fds == nullptr) { - return false; - } - // A closed app fd is always ready (POLLNVAL), so the wait below always has a bound fd to await - int firstAppIndex = -1; - bool hasAppFd = false; - nfds_t activeCount = 0; - for (nfds_t i = 0; i < nfds; i++) { - if (fds[i].fd < 0) { - continue; - } - activeCount++; - const AppFdState state = getAppFdState(fds[i].fd); - hasAppFd = hasAppFd || state != AppFdState::NotAppFd; - if (firstAppIndex < 0 && state == AppFdState::Bound) { - firstAppIndex = static_cast(i); - } - } - if (!hasAppFd) { - return false; - } - - const TickType_t start = get_ticks(); - const TickType_t timeoutTicks = (timeout < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout); - while (true) { - int ready = 0; - for (nfds_t i = 0; i < nfds; i++) { - fds[i].revents = 0; - if (fds[i].fd < 0) { - continue; - } - uint32_t bits; - const error_t pollResult = app_io_poll(fds[i].fd, &bits); - if (pollResult == ERROR_NONE) { - if ((fds[i].events & POLLIN) && (bits & APP_FILE_READABLE)) { - fds[i].revents |= POLLIN; - } - if ((fds[i].events & POLLOUT) && (bits & APP_FILE_WRITABLE)) { - fds[i].revents |= POLLOUT; - } - } else if (pollResult == ERROR_INVALID_STATE) { - fds[i].revents = POLLNVAL; - } else { - realPoll(&fds[i], 1, 0); - } - if (fds[i].revents != 0) { - ready++; - } - } - if (ready > 0) { - *outResult = ready; - return true; - } - - TickType_t remaining = (timeout < 0) ? portMAX_DELAY : get_timeout_remaining_ticks(timeoutTicks, start); - if (remaining == 0) { - *outResult = 0; - return true; - } - if (activeCount > 1 && remaining > POLL_INTERVAL_TICKS) { - remaining = POLL_INTERVAL_TICKS; - } - const struct pollfd& first = fds[firstAppIndex]; - if (first.events & POLLIN) { - app_io_await(first.fd, APP_FILE_WAIT_READABLE, remaining); - } else if (first.events & POLLOUT) { - app_io_await(first.fd, APP_FILE_WAIT_WRITABLE, remaining); - } else { - delay_ticks(remaining < POLL_INTERVAL_TICKS ? remaining : POLL_INTERVAL_TICKS); - } - } -} - diff --git a/Modules/app-module/source/stream.cpp b/Modules/app-module/source/stream.cpp index 10436ee23..861c26359 100644 --- a/Modules/app-module/source/stream.cpp +++ b/Modules/app-module/source/stream.cpp @@ -217,7 +217,7 @@ error_t app_stream_subscribe(AppStream* stream, void* buffer, size_t buffer_capa return ERROR_NOT_FOUND; } stream->producer_task = iterator->second.task; - error_t bind_result = app_fd_table_bind(&iterator->second.fd_table, producer_fd, &STREAM_OPS, stream, /*suppress_console_tee=*/false); + error_t bind_result = app_fd_table_bind(&iterator->second.fd_table, producer_fd, &STREAM_OPS, stream); mutex_unlock(&ledger.mutex); if (bind_result != ERROR_NONE) { @@ -242,7 +242,7 @@ error_t app_stream_bind_alias_fd(AppStream* stream, int alias_fd) { mutex_unlock(&ledger.mutex); return ERROR_NOT_FOUND; } - error_t result = app_fd_table_bind(&iterator->second.fd_table, alias_fd, &STREAM_OPS, stream, /*suppress_console_tee=*/false); + error_t result = app_fd_table_bind(&iterator->second.fd_table, alias_fd, &STREAM_OPS, stream); mutex_unlock(&ledger.mutex); return result; } diff --git a/Modules/app-module/tests/CMakeLists.txt b/Modules/app-module/tests/CMakeLists.txt index 78bc313bc..6f6f4d242 100644 --- a/Modules/app-module/tests/CMakeLists.txt +++ b/Modules/app-module/tests/CMakeLists.txt @@ -10,13 +10,14 @@ target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC} ${CMAKE_CURRENT_ add_test(NAME AppModuleTests COMMAND AppModuleTests) -# No --wrap flags or stdio_wrap sources needed here: linking app-module below already brings -# both along (see its own CMakeLists.txt). +# app-posix-module provides the __real_read/__real_write/__real_close that app-module's io.cpp falls +# through to. Tests of the libc wraps themselves live in app-posix-module's own tests. target_link_libraries(AppModuleTests PUBLIC TactilityKernel TactilityKernelCpp app-module + app-posix-module service-module platform-posix freertos_kernel diff --git a/Modules/app-module/tests/source/execute_test.cpp b/Modules/app-module/tests/source/execute_test.cpp index 0737c77f7..9e1326d28 100644 --- a/Modules/app-module/tests/source/execute_test.cpp +++ b/Modules/app-module/tests/source/execute_test.cpp @@ -111,11 +111,6 @@ int32_t stream_writer_app_main(int, char*[]) { return 7; } -int32_t printf_stream_writer_app_main(int, char*[]) { - printf("loc"); - return 7; -} - // Writes argv[0] to its own stdout, for the app_execute_with_streams() argv-delivery test. int32_t argv_echo_app_main(int argc, char* argv[]) { if (argc < 1) { @@ -346,57 +341,3 @@ TEST_CASE("app_execute_for_result_with_streams delivers both the stream data and app_manager_remove("test.app.execute.parent_streams"); } -TEST_CASE("app_execute_for_result_with_streams pipes a child's plain printf() calls too") { - ensure_memory_loader_registered(); - - AppManifest parent_manifest { "test.app.execute.printf_parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(location_app_main) } }; - REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE); - - uint32_t parent_id = 0; - AppStartContext parent_context; - REQUIRE_EQ(app_start_context_from_id("test.app.execute.printf_parent", &parent_context), ERROR_NONE); - REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE); - CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000)); - - TaskEventGroup parent_event_group {}; - task_event_group_construct(&parent_event_group); - AppEventSubscription parent_sub {}; - REQUIRE_EQ(app_event_subscribe_with_app_id(&parent_sub, &parent_event_group, parent_id), ERROR_NONE); - - uint8_t storage[64]; - AppStream child_stdout {}; - AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &parent_event_group, {}, -1 }; - - AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast(printf_stream_writer_app_main) }; - uint32_t child_id = 0; - AppStartContext context = app_start_context_for_location(location); - app_start_context_set_streams(&context, &binding, 1); - app_start_context_set_parent(&context, parent_id); - REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE); - - std::vector received; - while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) { - uint8_t chunk[16]; - size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk)); - if (n == 0) { - break; // EOF - } - received.insert(received.end(), chunk, chunk + n); - } - REQUIRE_EQ(received.size(), 3u); - CHECK_EQ(std::memcmp(received.data(), "loc", 3), 0); - - REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); - AppEvent event {}; - REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE); - CHECK_EQ(event.type, APP_EVENT_RESULT); - CHECK_EQ(event.result.launch_id, child_id); - CHECK_EQ(event.result.result, 7); - - app_stream_unsubscribe(&child_stdout); - app_event_unsubscribe(&parent_sub); - task_event_group_destruct(&parent_event_group); - app_manager_stop(child_id); - app_manager_stop(parent_id); - app_manager_remove("test.app.execute.printf_parent"); -} diff --git a/Modules/app-module/tests/source/io_test.cpp b/Modules/app-module/tests/source/io_test.cpp index b787ba674..68f7e4704 100644 --- a/Modules/app-module/tests/source/io_test.cpp +++ b/Modules/app-module/tests/source/io_test.cpp @@ -13,15 +13,10 @@ #include #include -#include -#include -#include #include #include -#include #include -#include #include #include @@ -208,160 +203,8 @@ int32_t double_close_app_main(int, char*[]) { return 0; } -std::atomic g_posix_stdin_is_char_device { false }; -std::atomic g_posix_tcgetattr_result { -2 }; -std::atomic g_posix_stdin_is_raw { false }; -std::atomic g_posix_tcsetattr_result { -2 }; -std::atomic g_posix_poll_before_write { -2 }; -std::atomic g_posix_poll_after_write { -2 }; -std::atomic g_posix_poll_first_done { false }; -std::atomic g_posix_null_buffers_efault { false }; - -int32_t posix_calls_app_main(int, char*[]) { - struct stat st {}; - g_posix_stdin_is_char_device.store(fstat(STDIN_FILENO, &st) == 0 && S_ISCHR(st.st_mode), std::memory_order_release); - - struct termios t {}; - g_posix_tcgetattr_result.store(tcgetattr(STDIN_FILENO, &t), std::memory_order_release); - g_posix_stdin_is_raw.store((t.c_lflag & (ICANON | ECHO)) == 0, std::memory_order_release); - g_posix_tcsetattr_result.store(tcsetattr(STDIN_FILENO, TCSANOW, &t), std::memory_order_release); - - struct stat* volatile null_stat = nullptr; - struct termios* volatile null_termios = nullptr; - const bool fstat_efault = fstat(STDIN_FILENO, null_stat) == -1 && errno == EFAULT; - const bool tcgetattr_efault = tcgetattr(STDIN_FILENO, null_termios) == -1 && errno == EFAULT; - g_posix_null_buffers_efault.store(fstat_efault && tcgetattr_efault, std::memory_order_release); - - struct pollfd fds { STDIN_FILENO, POLLIN, 0 }; - g_posix_poll_before_write.store(poll(&fds, 1, 50), std::memory_order_release); - g_posix_poll_first_done.store(true, std::memory_order_release); - g_posix_poll_after_write.store(poll(&fds, 1, 1000), std::memory_order_release); - return 0; -} - -std::atomic g_closed_fstat_ebadf { false }; -std::atomic g_closed_tcgetattr_ebadf { false }; -std::atomic g_closed_tcsetattr_ebadf { false }; -std::atomic g_closed_poll_result { -2 }; -std::atomic g_closed_poll_revents { -2 }; - -// Must never reach the process's real fd 0 (the host terminal on the simulator). -int32_t closed_stdin_posix_calls_app_main(int, char*[]) { - app_io_close(STDIN_FILENO); - - struct stat st {}; - g_closed_fstat_ebadf.store(fstat(STDIN_FILENO, &st) == -1 && errno == EBADF, std::memory_order_release); - struct termios t {}; - g_closed_tcgetattr_ebadf.store(tcgetattr(STDIN_FILENO, &t) == -1 && errno == EBADF, std::memory_order_release); - g_closed_tcsetattr_ebadf.store(tcsetattr(STDIN_FILENO, TCSANOW, &t) == -1 && errno == EBADF, std::memory_order_release); - - struct pollfd fds { STDIN_FILENO, POLLIN, 0 }; - g_closed_poll_result.store(poll(&fds, 1, 1000), std::memory_order_release); - g_closed_poll_revents.store(fds.revents, std::memory_order_release); - return 0; -} - -std::atomic g_exit_before { false }; -std::atomic g_exit_after { false }; - -int32_t exit_app_main(int, char*[]) { - g_exit_before.store(true, std::memory_order_release); - exit(42); - g_exit_after.store(true, std::memory_order_release); - return 0; -} - } // namespace -TEST_CASE("exit() in an app ends only that app instance") { - ensure_memory_loader_registered(); - g_exit_before.store(false, std::memory_order_relaxed); - g_exit_after.store(false, std::memory_order_relaxed); - - AppManifest manifest { "test.io.exit", "Exit", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(exit_app_main) } }; - REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); - - AppInstanceId instance_id = 0; - AppStartContext context; - REQUIRE_EQ(app_start_context_from_id("test.io.exit", &context), ERROR_NONE); - REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE); - REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 2000)); - - CHECK(g_exit_before.load(std::memory_order_acquire)); - CHECK_FALSE(g_exit_after.load(std::memory_order_acquire)); - - app_manager_remove("test.io.exit"); -} - -TEST_CASE("fstat, termios and poll on a closed app stdin fail with EBADF/POLLNVAL instead of reaching the real fd") { - ensure_memory_loader_registered(); - g_closed_fstat_ebadf.store(false, std::memory_order_relaxed); - g_closed_tcgetattr_ebadf.store(false, std::memory_order_relaxed); - g_closed_tcsetattr_ebadf.store(false, std::memory_order_relaxed); - g_closed_poll_result.store(-2, std::memory_order_relaxed); - g_closed_poll_revents.store(-2, std::memory_order_relaxed); - - AppManifest manifest { "test.io.closed_posix_calls", "ClosedPosixCalls", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(closed_stdin_posix_calls_app_main) } }; - REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); - - AppInstanceId instance_id = 0; - AppStartContext context; - REQUIRE_EQ(app_start_context_from_id("test.io.closed_posix_calls", &context), ERROR_NONE); - REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE); - REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 2000)); - - CHECK(g_closed_fstat_ebadf.load(std::memory_order_acquire)); - CHECK(g_closed_tcgetattr_ebadf.load(std::memory_order_acquire)); - CHECK(g_closed_tcsetattr_ebadf.load(std::memory_order_acquire)); - CHECK_EQ(g_closed_poll_result.load(std::memory_order_acquire), 1); - CHECK_EQ(g_closed_poll_revents.load(std::memory_order_acquire), POLLNVAL); - - app_manager_remove("test.io.closed_posix_calls"); -} - -TEST_CASE("fstat, termios and poll on a bound app stdin report a raw character device that becomes readable") { - ensure_memory_loader_registered(); - g_posix_stdin_is_char_device.store(false, std::memory_order_relaxed); - g_posix_tcgetattr_result.store(-2, std::memory_order_relaxed); - g_posix_stdin_is_raw.store(false, std::memory_order_relaxed); - g_posix_tcsetattr_result.store(-2, std::memory_order_relaxed); - g_posix_poll_before_write.store(-2, std::memory_order_relaxed); - g_posix_poll_after_write.store(-2, std::memory_order_relaxed); - g_posix_poll_first_done.store(false, std::memory_order_relaxed); - g_posix_null_buffers_efault.store(false, std::memory_order_relaxed); - - AppManifest manifest { "test.io.posix_calls", "PosixCalls", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(posix_calls_app_main) } }; - REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); - - TaskEventGroup event_group {}; - task_event_group_construct(&event_group); - - uint8_t storage[16]; - AppStream child_stdin {}; - AppStreamBinding binding { STDIN_FILENO, &child_stdin, storage, sizeof(storage), &event_group, {}, -1 }; - AppInstanceId child_id = 0; - AppStartContext context; - REQUIRE_EQ(app_start_context_from_id("test.io.posix_calls", &context), ERROR_NONE); - app_start_context_set_streams(&context, &binding, 1); - REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE); - - REQUIRE(wait_for_flag(g_posix_poll_first_done, 1000)); - app_stream_write(&child_stdin, "x", 1); - - REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000)); - CHECK(g_posix_stdin_is_char_device.load(std::memory_order_acquire)); - CHECK_EQ(g_posix_tcgetattr_result.load(std::memory_order_acquire), 0); - CHECK(g_posix_stdin_is_raw.load(std::memory_order_acquire)); - CHECK_EQ(g_posix_tcsetattr_result.load(std::memory_order_acquire), 0); - CHECK(g_posix_null_buffers_efault.load(std::memory_order_acquire)); - CHECK_EQ(g_posix_poll_before_write.load(std::memory_order_acquire), 0); - CHECK_EQ(g_posix_poll_after_write.load(std::memory_order_acquire), 1); - - app_stream_unsubscribe(&child_stdin); - task_event_group_destruct(&event_group); - app_manager_remove("test.io.posix_calls"); -} - TEST_CASE("an app's stdio fds default to the null device: write succeeds and discards, read reports EOF") { ensure_memory_loader_registered(); g_stdio_write_result.store(-2, std::memory_order_relaxed); diff --git a/Modules/app-module/tests/source/package_manifest_test.cpp b/Modules/app-module/tests/source/package_manifest_test.cpp new file mode 100644 index 000000000..e49bed908 --- /dev/null +++ b/Modules/app-module/tests/source/package_manifest_test.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "doctest.h" + +#include +#include + +#include +#include +#include + +namespace { + +std::map minimal_v3_properties() { + return { + { "manifest.version", "0.3" }, + { "id", "test.package" }, + { "version.name", "1.0.0" }, + { "version.code", "1" }, + { "target.sdk", "0.8.0" }, + { "app.0.id", "test.package.app" }, + { "app.0.name", "App" }, + { "app.0.binary", "app" }, + }; +} + +error_t parse(const std::map& properties, PackageManifest& package) { + AppManifestBinding bindings[1]; + return package_manifest_parse_v3(properties, package, bindings, 1); +} + +} // namespace + +TEST_CASE("package manifest v3: requires.ram defaults to 0") { + PackageManifest package {}; + REQUIRE_EQ(parse(minimal_v3_properties(), package), ERROR_NONE); + CHECK_EQ(package.requires_ram, 0); +} + +TEST_CASE("package manifest v3: requires.ram parses whole megabytes with an M suffix") { + auto properties = minimal_v3_properties(); + properties["requires.ram"] = "2M"; + PackageManifest package {}; + REQUIRE_EQ(parse(properties, package), ERROR_NONE); + CHECK_EQ(package.requires_ram, 2); +} + +TEST_CASE("package manifest v3: an invalid requires.ram rejects the manifest") { + for (const char* value : { "2", "2m", "M", "256M", "-1M", "2MB", "" }) { + CAPTURE(value); + auto properties = minimal_v3_properties(); + properties["requires.ram"] = value; + PackageManifest package {}; + CHECK_EQ(parse(properties, package), ERROR_INVALID_ARGUMENT); + } +} + +TEST_CASE("package manifest compatibility: requires_device_id must list this device, if set") { + PackageManifest package {}; + CHECK(app_package_manifest_is_compatible(&package)); + + strcpy(package.requires_device_id, CONFIG_TT_DEVICE_ID); + CHECK(app_package_manifest_is_compatible(&package)); + + strcpy(package.requires_device_id, "other-device," CONFIG_TT_DEVICE_ID); + CHECK(app_package_manifest_is_compatible(&package)); + + strcpy(package.requires_device_id, "other-device"); + CHECK_FALSE(app_package_manifest_is_compatible(&package)); + + // A listed id that only starts with this device's id is a different device + strcpy(package.requires_device_id, CONFIG_TT_DEVICE_ID "-v2"); + CHECK_FALSE(app_package_manifest_is_compatible(&package)); +} + +TEST_CASE("package manifest compatibility: requires_ram must fit in total RAM") { + PackageManifest package {}; + package.requires_ram = 1; + CHECK(app_package_manifest_is_compatible(&package)); +} diff --git a/Modules/app-posix-module/CMakeLists.txt b/Modules/app-posix-module/CMakeLists.txt index f818c9e7d..df523aa03 100644 --- a/Modules/app-posix-module/CMakeLists.txt +++ b/Modules/app-posix-module/CMakeLists.txt @@ -7,8 +7,12 @@ file(GLOB_RECURSE SOURCE_FILES "source/*.c*") tactility_add_module(app-posix-module SRCS ${SOURCE_FILES} INCLUDE_DIRS include/ + PRIV_INCLUDE_DIRS private/ REQUIRES TactilityKernel app-module service-module PRIV_REQUIRES ${CMAKE_DL_LIBS} + # The libc overrides in source/stdio_wrap*.cpp replace libc's own by name and are never referenced + # directly, and app-module's io.cpp calls back into their __real_read/__real_write/__real_close + WHOLE_ARCHIVE ) # Baked in at compile time, mirroring ESP32's CONFIG_IDF_TARGET - names the installed-app binary diff --git a/Modules/app-module/private/app/private/stdio_wrap_posix.h b/Modules/app-posix-module/private/app_posix/stdio_wrap.h similarity index 83% rename from Modules/app-module/private/app/private/stdio_wrap_posix.h rename to Modules/app-posix-module/private/app_posix/stdio_wrap.h index a54aeb262..11ee9017b 100644 --- a/Modules/app-module/private/app/private/stdio_wrap_posix.h +++ b/Modules/app-posix-module/private/app_posix/stdio_wrap.h @@ -7,10 +7,13 @@ #include #include #include +#include + +#include #include -// Implemented in stdio_wrap_posix.cpp, installed under the real names by stdio_wrap_elf.cpp or stdio_wrap_apple.cpp. +// Implemented in stdio_wrap.cpp, installed under the real names by stdio_wrap_elf.cpp or stdio_wrap_apple.cpp. extern "C" { ssize_t __wrap_read(int fd, void* buffer, size_t size); ssize_t __wrap_write(int fd, const void* buffer, size_t size); @@ -22,6 +25,8 @@ int __wrap_fstat(int fd, struct stat* st); int __wrap_poll(struct pollfd* fds, nfds_t nfds, int timeout); int __wrap_tcgetattr(int fd, struct termios* p); int __wrap_tcsetattr(int fd, int optional_actions, const struct termios* p); +AppLibcSignalHandler __wrap_signal(int sig, AppLibcSignalHandler handler); +int __wrap_kill(pid_t pid, int sig); [[noreturn]] void __wrap_exit(int status); int __wrap_vprintf(const char* format, va_list args); diff --git a/Modules/app-module/source/stdio_wrap_posix.cpp b/Modules/app-posix-module/source/stdio_wrap.cpp similarity index 83% rename from Modules/app-module/source/stdio_wrap_posix.cpp rename to Modules/app-posix-module/source/stdio_wrap.cpp index 24b0d8181..da566209d 100644 --- a/Modules/app-module/source/stdio_wrap_posix.cpp +++ b/Modules/app-posix-module/source/stdio_wrap.cpp @@ -1,13 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 -#ifndef ESP_PLATFORM -// POSIX can't use -Wl,--wrap= like ESP32: --wrap doesn't reach a dlopen()ed app's own printf/write -// calls, so these wraps are installed under their real names instead - dyld interpose on Apple -// (stdio_wrap_apple.cpp), plain strong definitions elsewhere (stdio_wrap_elf.cpp). -#include -#include +// libc wraps that route an app instance's calls to its own fds, cwd and exit(). App-instance behavior +// itself lives in app-module (app/libc.h). POSIX can't use -Wl,--wrap= like ESP32: --wrap doesn't reach +// a dlopen()ed app's own printf/write calls, so these wraps are installed under their real names instead +// - dyld interpose on Apple (stdio_wrap_apple.cpp), plain strong definitions elsewhere (stdio_wrap_elf.cpp). +// app-module's io.cpp falls through to the __real_read/__real_write/__real_close defined here. +#include #include +#include #include #include @@ -72,6 +73,16 @@ int __real_tcsetattr(int fd, int optional_actions, const struct termios* p) { return real(fd, optional_actions, p); } +AppLibcSignalHandler __real_signal(int sig, AppLibcSignalHandler handler) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "signal")); + return real(sig, handler); +} + +int __real_kill(pid_t pid, int sig) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "kill")); + return real(pid, sig); +} + [[noreturn]] void __real_exit(int status) { static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "exit")); real(status); @@ -96,21 +107,16 @@ int __wrap_ioctl(int fd, unsigned long request, ...) { void* arg = va_arg(args, void*); va_end(args); - struct winsize windowSize {}; - if (tryAppWindowSize(fd, request, arg, &windowSize)) { - *static_cast(arg) = windowSize; - return 0; + int result; + if (app_libc_try_window_size(fd, request, arg, &result)) { + return result; } return __real_ioctl(fd, request, arg); } char* __wrap_getcwd(char* buf, size_t size) { char* result; - int err; - if (tryAppGetCwd(buf, size, &result, &err)) { - if (result == nullptr) { - errno = err; - } + if (app_libc_try_getcwd(buf, size, &result)) { return result; } return __real_getcwd(buf, size); @@ -118,11 +124,7 @@ char* __wrap_getcwd(char* buf, size_t size) { int __wrap_chdir(const char* path) { int result; - int err; - if (tryAppChdir(path, &result, &err)) { - if (result != 0) { - errno = err; - } + if (app_libc_try_chdir(path, &result)) { return result; } return __real_chdir(path); @@ -130,7 +132,7 @@ int __wrap_chdir(const char* path) { int __wrap_fstat(int fd, struct stat* st) { int result; - if (tryAppFstat(fd, st, &result)) { + if (app_libc_try_fstat(fd, st, &result)) { return result; } return __real_fstat(fd, st); @@ -138,7 +140,7 @@ int __wrap_fstat(int fd, struct stat* st) { int __wrap_poll(struct pollfd* fds, nfds_t nfds, int timeout) { int result; - if (tryAppPoll(fds, nfds, timeout, __real_poll, &result)) { + if (app_libc_try_poll(fds, nfds, timeout, __real_poll, &result)) { return result; } return __real_poll(fds, nfds, timeout); @@ -146,7 +148,7 @@ int __wrap_poll(struct pollfd* fds, nfds_t nfds, int timeout) { int __wrap_tcgetattr(int fd, struct termios* p) { int result; - if (tryAppTcgetattr(fd, p, &result)) { + if (app_libc_try_tcgetattr(fd, p, &result)) { return result; } return __real_tcgetattr(fd, p); @@ -154,12 +156,30 @@ int __wrap_tcgetattr(int fd, struct termios* p) { int __wrap_tcsetattr(int fd, int optional_actions, const struct termios* p) { int result; - if (tryAppTcsetattr(fd, &result)) { + if (app_libc_try_tcsetattr(fd, p, &result)) { return result; } return __real_tcsetattr(fd, optional_actions, p); } +// Called by an app, the real signal() would install a process-wide handler pointing into the app's binary +AppLibcSignalHandler __wrap_signal(int sig, AppLibcSignalHandler handler) { + AppLibcSignalHandler previous; + if (app_libc_try_signal(sig, handler, &previous)) { + return previous; + } + return __real_signal(sig, handler); +} + +// Called by an app, the real kill() would signal the whole simulator (e.g. SIGSTOP) +int __wrap_kill(pid_t pid, int sig) { + int result; + if (app_libc_try_kill(pid, sig, &result)) { + return result; + } + return __real_kill(pid, sig); +} + // Called by an app, the real exit() would end the whole simulator void __wrap_exit(int status) { app_scheduler_exit_current(status); @@ -387,5 +407,3 @@ char* __wrap_fgets(char* buffer, int size, FILE* stream) { } // endregion - -#endif // ESP_PLATFORM diff --git a/Modules/app-module/source/stdio_wrap_apple.cpp b/Modules/app-posix-module/source/stdio_wrap_apple.cpp similarity index 93% rename from Modules/app-module/source/stdio_wrap_apple.cpp rename to Modules/app-posix-module/source/stdio_wrap_apple.cpp index 693cd5f66..ef4e79181 100644 --- a/Modules/app-module/source/stdio_wrap_apple.cpp +++ b/Modules/app-posix-module/source/stdio_wrap_apple.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #ifdef __APPLE__ -#include +#include #include #include @@ -25,6 +25,8 @@ TT_DYLD_INTERPOSE(__wrap_poll, poll) TT_DYLD_INTERPOSE(__wrap_tcgetattr, tcgetattr) TT_DYLD_INTERPOSE(__wrap_tcsetattr, tcsetattr) TT_DYLD_INTERPOSE(__wrap_exit, exit) +TT_DYLD_INTERPOSE(__wrap_signal, signal) +TT_DYLD_INTERPOSE(__wrap_kill, kill) TT_DYLD_INTERPOSE(__wrap_vprintf, vprintf) TT_DYLD_INTERPOSE(__wrap_printf, printf) diff --git a/Modules/app-module/source/stdio_wrap_elf.cpp b/Modules/app-posix-module/source/stdio_wrap_elf.cpp similarity index 91% rename from Modules/app-module/source/stdio_wrap_elf.cpp rename to Modules/app-posix-module/source/stdio_wrap_elf.cpp index 4c0375b13..e2f1fa574 100644 --- a/Modules/app-module/source/stdio_wrap_elf.cpp +++ b/Modules/app-posix-module/source/stdio_wrap_elf.cpp @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 -#if !defined(ESP_PLATFORM) && !defined(__APPLE__) +#ifndef __APPLE__ // Plain strong definitions: ELF gives the main executable's symbols priority process-wide, // including for a dlopen()ed app's own calls. -#include +#include extern "C" { @@ -51,6 +51,14 @@ int tcsetattr(int fd, int optional_actions, const struct termios* p) { return __wrap_tcsetattr(fd, optional_actions, p); } +AppLibcSignalHandler signal(int sig, AppLibcSignalHandler handler) { + return __wrap_signal(sig, handler); +} + +int kill(pid_t pid, int sig) { + return __wrap_kill(pid, sig); +} + void exit(int status) { __wrap_exit(status); } diff --git a/Modules/app-posix-module/tests/CMakeLists.txt b/Modules/app-posix-module/tests/CMakeLists.txt index df85980c4..ae0559c87 100644 --- a/Modules/app-posix-module/tests/CMakeLists.txt +++ b/Modules/app-posix-module/tests/CMakeLists.txt @@ -13,7 +13,7 @@ set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPEND # Fixture: calls printf() with no app-module linkage of its own, so its call must resolve against # the host process's own printf - proves a dlopen()ed app's stdio actually reaches app_io_write() -# rather than bypassing straight to libc (see stdio_wrap_posix.cpp's own comment for why that's not a +# rather than bypassing straight to libc (see ../source/stdio_wrap.cpp's own comment for why that's not a # given on POSIX). add_library(printf_fixture SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_LIST_DIR}/fixtures/printf_fixture.cpp) set_target_properties(printf_fixture PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/Modules/app-posix-module/tests/source/libc_test.cpp b/Modules/app-posix-module/tests/source/libc_test.cpp new file mode 100644 index 000000000..b7e170007 --- /dev/null +++ b/Modules/app-posix-module/tests/source/libc_test.cpp @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: Apache-2.0 +// libc calls made by an app instance (fstat, termios, poll, printf, exit), routed by this module's +// libc wraps (../source/stdio_wrap.cpp) to the app instance's own fds and lifecycle. +#include "doctest.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +extern ServiceManifest app_internal_loader_service_manifest; + +namespace { + +// See manager_test.cpp's own copy of this helper for why this checks the registry directly +// rather than a per-translation-unit static bool. +void ensure_memory_loader_registered() { + if (service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID) == nullptr) { + service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true); + } +} + +bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_ms) { + uint32_t waited = 0; + while (waited < timeout_ms) { + if (app_manager_get_state(id) == target) { + return true; + } + delay_millis(10); + waited += 10; + } + return app_manager_get_state(id) == target; +} + +bool wait_for_flag(std::atomic& flag, uint32_t timeout_ms) { + uint32_t waited = 0; + while (waited < timeout_ms) { + if (flag.load(std::memory_order_acquire)) { + return true; + } + delay_millis(10); + waited += 10; + } + return flag.load(std::memory_order_acquire); +} + +int32_t location_app_main(int argc, char* argv[]) { + if (argc == 1) { + return static_cast(strtol(argv[0], nullptr, 10)); + } + + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + AppEventSubscription sub {}; + app_event_subscribe(&sub, &event_group); + + while (true) { + if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) { + break; // safety net so a bug here can't hang the test suite + } + bool done = false; + AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + done = true; + break; + } + } + if (done) { + break; + } + } + + app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); + return 0; +} + +int32_t printf_stream_writer_app_main(int, char*[]) { + printf("loc"); + return 7; +} + +std::atomic g_posix_stdin_is_char_device { false }; +std::atomic g_posix_tcgetattr_result { -2 }; +std::atomic g_posix_stdin_is_raw { false }; +std::atomic g_posix_tcsetattr_result { -2 }; +std::atomic g_posix_poll_before_write { -2 }; +std::atomic g_posix_poll_after_write { -2 }; +std::atomic g_posix_poll_first_done { false }; +std::atomic g_posix_null_buffers_efault { false }; + +int32_t posix_calls_app_main(int, char*[]) { + struct stat st {}; + g_posix_stdin_is_char_device.store(fstat(STDIN_FILENO, &st) == 0 && S_ISCHR(st.st_mode), std::memory_order_release); + + struct termios t {}; + g_posix_tcgetattr_result.store(tcgetattr(STDIN_FILENO, &t), std::memory_order_release); + g_posix_stdin_is_raw.store((t.c_lflag & (ICANON | ECHO)) == 0, std::memory_order_release); + g_posix_tcsetattr_result.store(tcsetattr(STDIN_FILENO, TCSANOW, &t), std::memory_order_release); + + struct stat* volatile null_stat = nullptr; + struct termios* volatile null_termios = nullptr; + const bool fstat_efault = fstat(STDIN_FILENO, null_stat) == -1 && errno == EFAULT; + const bool tcgetattr_efault = tcgetattr(STDIN_FILENO, null_termios) == -1 && errno == EFAULT; + g_posix_null_buffers_efault.store(fstat_efault && tcgetattr_efault, std::memory_order_release); + + struct pollfd fds { STDIN_FILENO, POLLIN, 0 }; + g_posix_poll_before_write.store(poll(&fds, 1, 50), std::memory_order_release); + g_posix_poll_first_done.store(true, std::memory_order_release); + g_posix_poll_after_write.store(poll(&fds, 1, 1000), std::memory_order_release); + return 0; +} + +std::atomic g_closed_fstat_ebadf { false }; +std::atomic g_closed_tcgetattr_ebadf { false }; +std::atomic g_closed_tcsetattr_ebadf { false }; +std::atomic g_closed_ioctl_ebadf { false }; +std::atomic g_closed_poll_result { -2 }; +std::atomic g_closed_poll_revents { -2 }; + +// Must never reach the process's real fd 0 (the host terminal on the simulator). +int32_t closed_stdin_posix_calls_app_main(int, char*[]) { + app_io_close(STDIN_FILENO); + + struct stat st {}; + g_closed_fstat_ebadf.store(fstat(STDIN_FILENO, &st) == -1 && errno == EBADF, std::memory_order_release); + struct termios t {}; + g_closed_tcgetattr_ebadf.store(tcgetattr(STDIN_FILENO, &t) == -1 && errno == EBADF, std::memory_order_release); + g_closed_tcsetattr_ebadf.store(tcsetattr(STDIN_FILENO, TCSANOW, &t) == -1 && errno == EBADF, std::memory_order_release); + struct winsize ws {}; + g_closed_ioctl_ebadf.store(ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == -1 && errno == EBADF, std::memory_order_release); + + struct pollfd fds { STDIN_FILENO, POLLIN, 0 }; + g_closed_poll_result.store(poll(&fds, 1, 1000), std::memory_order_release); + g_closed_poll_revents.store(fds.revents, std::memory_order_release); + return 0; +} + +std::atomic g_exit_before { false }; +std::atomic g_exit_after { false }; + +int32_t exit_app_main(int, char*[]) { + g_exit_before.store(true, std::memory_order_release); + exit(42); + g_exit_after.store(true, std::memory_order_release); + return 0; +} + +void test_signal_handler(int) { +} + +std::atomic g_signal_first_default { false }; +std::atomic g_signal_returns_previous { false }; +std::atomic g_signal_sigkill_rejected { false }; +std::atomic g_kill_enosys { false }; + +int32_t signal_app_main(int, char*[]) { + g_signal_first_default.store(signal(SIGWINCH, test_signal_handler) == SIG_DFL, std::memory_order_release); + g_signal_returns_previous.store(signal(SIGWINCH, SIG_IGN) == test_signal_handler, std::memory_order_release); + g_signal_sigkill_rejected.store(signal(SIGKILL, test_signal_handler) == SIG_ERR && errno == EINVAL, std::memory_order_release); + // Would stop the whole test process if it reached the real kill() + g_kill_enosys.store(kill(0, SIGSTOP) == -1 && errno == ENOSYS, std::memory_order_release); + return 0; +} + +std::atomic g_icrnl_default_on { false }; +std::atomic g_icrnl_first_read { -2 }; +std::atomic g_icrnl_first_done { false }; +std::atomic g_icrnl_second_read { -2 }; + +int32_t icrnl_app_main(int, char*[]) { + struct termios t {}; + tcgetattr(STDIN_FILENO, &t); + g_icrnl_default_on.store((t.c_iflag & ICRNL) != 0, std::memory_order_release); + + char c = 0; + g_icrnl_first_read.store(read(STDIN_FILENO, &c, 1) == 1 ? c : -1, std::memory_order_release); + + t.c_iflag &= ~ICRNL; + tcsetattr(STDIN_FILENO, TCSANOW, &t); + g_icrnl_first_done.store(true, std::memory_order_release); + g_icrnl_second_read.store(read(STDIN_FILENO, &c, 1) == 1 ? c : -1, std::memory_order_release); + return 0; +} + +} // namespace + +TEST_CASE("ICRNL is on by default and makes stdin read the Enter key's \\r as \\n, until an app clears it") { + ensure_memory_loader_registered(); + g_icrnl_default_on.store(false, std::memory_order_relaxed); + g_icrnl_first_read.store(-2, std::memory_order_relaxed); + g_icrnl_first_done.store(false, std::memory_order_relaxed); + g_icrnl_second_read.store(-2, std::memory_order_relaxed); + + AppManifest manifest { "test.libc.icrnl", "Icrnl", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(icrnl_app_main) } }; + REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); + + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + uint8_t storage[16]; + AppStream child_stdin {}; + AppStreamBinding binding { STDIN_FILENO, &child_stdin, storage, sizeof(storage), &event_group, {}, -1 }; + AppInstanceId child_id = 0; + AppStartContext context; + REQUIRE_EQ(app_start_context_from_id("test.libc.icrnl", &context), ERROR_NONE); + app_start_context_set_streams(&context, &binding, 1); + REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE); + + app_stream_write(&child_stdin, "\r", 1); + REQUIRE(wait_for_flag(g_icrnl_first_done, 1000)); + app_stream_write(&child_stdin, "\r", 1); + + REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000)); + CHECK(g_icrnl_default_on.load(std::memory_order_acquire)); + CHECK_EQ(g_icrnl_first_read.load(std::memory_order_acquire), '\n'); + CHECK_EQ(g_icrnl_second_read.load(std::memory_order_acquire), '\r'); + + app_stream_unsubscribe(&child_stdin); + task_event_group_destruct(&event_group); + app_manager_remove("test.libc.icrnl"); +} + +TEST_CASE("signal() in an app records handlers per app instance, and kill() never reaches the process") { + ensure_memory_loader_registered(); + g_signal_first_default.store(false, std::memory_order_relaxed); + g_signal_returns_previous.store(false, std::memory_order_relaxed); + g_signal_sigkill_rejected.store(false, std::memory_order_relaxed); + g_kill_enosys.store(false, std::memory_order_relaxed); + + AppManifest manifest { "test.libc.signal", "Signal", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(signal_app_main) } }; + REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); + + AppInstanceId instance_id = 0; + AppStartContext context; + REQUIRE_EQ(app_start_context_from_id("test.libc.signal", &context), ERROR_NONE); + REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE); + REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 2000)); + + CHECK(g_signal_first_default.load(std::memory_order_acquire)); + CHECK(g_signal_returns_previous.load(std::memory_order_acquire)); + CHECK(g_signal_sigkill_rejected.load(std::memory_order_acquire)); + CHECK(g_kill_enosys.load(std::memory_order_acquire)); + + app_manager_remove("test.libc.signal"); +} + +TEST_CASE("exit() in an app ends only that app instance") { + ensure_memory_loader_registered(); + g_exit_before.store(false, std::memory_order_relaxed); + g_exit_after.store(false, std::memory_order_relaxed); + + AppManifest manifest { "test.io.exit", "Exit", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(exit_app_main) } }; + REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); + + AppInstanceId instance_id = 0; + AppStartContext context; + REQUIRE_EQ(app_start_context_from_id("test.io.exit", &context), ERROR_NONE); + REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE); + REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 2000)); + + CHECK(g_exit_before.load(std::memory_order_acquire)); + CHECK_FALSE(g_exit_after.load(std::memory_order_acquire)); + + app_manager_remove("test.io.exit"); +} + +TEST_CASE("fstat, ioctl, termios and poll on a closed app stdin fail with EBADF/POLLNVAL instead of reaching the real fd") { + ensure_memory_loader_registered(); + g_closed_fstat_ebadf.store(false, std::memory_order_relaxed); + g_closed_tcgetattr_ebadf.store(false, std::memory_order_relaxed); + g_closed_tcsetattr_ebadf.store(false, std::memory_order_relaxed); + g_closed_ioctl_ebadf.store(false, std::memory_order_relaxed); + g_closed_poll_result.store(-2, std::memory_order_relaxed); + g_closed_poll_revents.store(-2, std::memory_order_relaxed); + + AppManifest manifest { "test.io.closed_posix_calls", "ClosedPosixCalls", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(closed_stdin_posix_calls_app_main) } }; + REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); + + AppInstanceId instance_id = 0; + AppStartContext context; + REQUIRE_EQ(app_start_context_from_id("test.io.closed_posix_calls", &context), ERROR_NONE); + REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE); + REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 2000)); + + CHECK(g_closed_fstat_ebadf.load(std::memory_order_acquire)); + CHECK(g_closed_tcgetattr_ebadf.load(std::memory_order_acquire)); + CHECK(g_closed_tcsetattr_ebadf.load(std::memory_order_acquire)); + CHECK(g_closed_ioctl_ebadf.load(std::memory_order_acquire)); + CHECK_EQ(g_closed_poll_result.load(std::memory_order_acquire), 1); + CHECK_EQ(g_closed_poll_revents.load(std::memory_order_acquire), POLLNVAL); + + app_manager_remove("test.io.closed_posix_calls"); +} + +TEST_CASE("fstat, termios and poll on a bound app stdin report a raw character device that becomes readable") { + ensure_memory_loader_registered(); + g_posix_stdin_is_char_device.store(false, std::memory_order_relaxed); + g_posix_tcgetattr_result.store(-2, std::memory_order_relaxed); + g_posix_stdin_is_raw.store(false, std::memory_order_relaxed); + g_posix_tcsetattr_result.store(-2, std::memory_order_relaxed); + g_posix_poll_before_write.store(-2, std::memory_order_relaxed); + g_posix_poll_after_write.store(-2, std::memory_order_relaxed); + g_posix_poll_first_done.store(false, std::memory_order_relaxed); + g_posix_null_buffers_efault.store(false, std::memory_order_relaxed); + + AppManifest manifest { "test.io.posix_calls", "PosixCalls", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(posix_calls_app_main) } }; + REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); + + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + uint8_t storage[16]; + AppStream child_stdin {}; + AppStreamBinding binding { STDIN_FILENO, &child_stdin, storage, sizeof(storage), &event_group, {}, -1 }; + AppInstanceId child_id = 0; + AppStartContext context; + REQUIRE_EQ(app_start_context_from_id("test.io.posix_calls", &context), ERROR_NONE); + app_start_context_set_streams(&context, &binding, 1); + REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE); + + REQUIRE(wait_for_flag(g_posix_poll_first_done, 1000)); + app_stream_write(&child_stdin, "x", 1); + + REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000)); + CHECK(g_posix_stdin_is_char_device.load(std::memory_order_acquire)); + CHECK_EQ(g_posix_tcgetattr_result.load(std::memory_order_acquire), 0); + CHECK(g_posix_stdin_is_raw.load(std::memory_order_acquire)); + CHECK_EQ(g_posix_tcsetattr_result.load(std::memory_order_acquire), 0); + CHECK(g_posix_null_buffers_efault.load(std::memory_order_acquire)); + CHECK_EQ(g_posix_poll_before_write.load(std::memory_order_acquire), 0); + CHECK_EQ(g_posix_poll_after_write.load(std::memory_order_acquire), 1); + + app_stream_unsubscribe(&child_stdin); + task_event_group_destruct(&event_group); + app_manager_remove("test.io.posix_calls"); +} + +TEST_CASE("app_execute_for_result_with_streams pipes a child's plain printf() calls too") { + ensure_memory_loader_registered(); + + AppManifest parent_manifest { "test.app.execute.printf_parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(location_app_main) } }; + REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE); + + uint32_t parent_id = 0; + AppStartContext parent_context; + REQUIRE_EQ(app_start_context_from_id("test.app.execute.printf_parent", &parent_context), ERROR_NONE); + REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE); + CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000)); + + TaskEventGroup parent_event_group {}; + task_event_group_construct(&parent_event_group); + AppEventSubscription parent_sub {}; + REQUIRE_EQ(app_event_subscribe_with_app_id(&parent_sub, &parent_event_group, parent_id), ERROR_NONE); + + uint8_t storage[64]; + AppStream child_stdout {}; + AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &parent_event_group, {}, -1 }; + + AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast(printf_stream_writer_app_main) }; + uint32_t child_id = 0; + AppStartContext context = app_start_context_for_location(location); + app_start_context_set_streams(&context, &binding, 1); + app_start_context_set_parent(&context, parent_id); + REQUIRE_EQ(app_start_with_context(&context, &child_id), ERROR_NONE); + + std::vector received; + while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) { + uint8_t chunk[16]; + size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk)); + if (n == 0) { + break; // EOF + } + received.insert(received.end(), chunk, chunk + n); + } + REQUIRE_EQ(received.size(), 3u); + CHECK_EQ(std::memcmp(received.data(), "loc", 3), 0); + + REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); + AppEvent event {}; + REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE); + CHECK_EQ(event.type, APP_EVENT_RESULT); + CHECK_EQ(event.result.launch_id, child_id); + CHECK_EQ(event.result.result, 7); + + app_stream_unsubscribe(&child_stdout); + app_event_unsubscribe(&parent_sub); + task_event_group_destruct(&parent_event_group); + app_manager_stop(child_id); + app_manager_stop(parent_id); + app_manager_remove("test.app.execute.printf_parent"); +} diff --git a/Modules/c-symbols-module/source/module.cpp b/Modules/c-symbols-module/source/module.cpp index 84cf6d705..63f7c07c6 100644 --- a/Modules/c-symbols-module/source/module.cpp +++ b/Modules/c-symbols-module/source/module.cpp @@ -124,6 +124,7 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(getc), DEFINE_MODULE_SYMBOL(putc), DEFINE_MODULE_SYMBOL(putchar), + DEFINE_MODULE_SYMBOL(getchar), DEFINE_MODULE_SYMBOL(puts), DEFINE_MODULE_SYMBOL(printf), DEFINE_MODULE_SYMBOL(sscanf), diff --git a/Modules/posix-symbols-module/source/module.cpp b/Modules/posix-symbols-module/source/module.cpp index 918dd76a1..4bfc4dfae 100644 --- a/Modules/posix-symbols-module/source/module.cpp +++ b/Modules/posix-symbols-module/source/module.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,9 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(ftruncate), DEFINE_MODULE_SYMBOL(fsync), DEFINE_MODULE_SYMBOL(getuid), + // signal.h - handlers are recorded per app, see app/libc.h + DEFINE_MODULE_SYMBOL(signal), + DEFINE_MODULE_SYMBOL(kill), // sys/ioctl.h DEFINE_MODULE_SYMBOL(ioctl), // sys/poll.h diff --git a/Tactility/Private/Tactility/app/apphub/AppHubEntry.h b/Tactility/Private/Tactility/app/apphub/AppHubEntry.h index e498d52c6..c40d49f4b 100644 --- a/Tactility/Private/Tactility/app/apphub/AppHubEntry.h +++ b/Tactility/Private/Tactility/app/apphub/AppHubEntry.h @@ -15,6 +15,10 @@ struct AppHubEntry { std::string appDescription; std::string targetSdk; std::vector targetPlatforms; + /** Device ids the app is restricted to (manifest "requires.device.id"). Empty means unrestricted. */ + std::vector requiresDeviceId; + /** RAM in megabytes the app needs (manifest "requires.ram"). 0 means no requirement. */ + int32_t requiresRam = 0; std::string file; }; @@ -24,4 +28,7 @@ using AppHubEntryList = std::vector +#include #include #include @@ -32,6 +33,10 @@ class Reader { if (!readNumber(key, buffer)) { return false; } + if (buffer != std::trunc(buffer)) { + LOG_E(TAG, "%s is not an integer", key); + return false; + } output = static_cast(buffer); return true; } diff --git a/Tactility/Source/app/apphub/AppHubApp.cpp b/Tactility/Source/app/apphub/AppHubApp.cpp index f944a914f..64a71ef6a 100644 --- a/Tactility/Source/app/apphub/AppHubApp.cpp +++ b/Tactility/Source/app/apphub/AppHubApp.cpp @@ -121,17 +121,8 @@ void showApps(Context* ctx) { lv_obj_clean(ctx->contentWrapper); ctx->mutex.lock(); if (parseJson(ctx->cachedAppsJsonFile, ctx->entries)) { - // An empty targetPlatforms list means the entry runs everywhere; otherwise it must name - // this build's own target to be installable here. The simulator isn't a real MCU target, - // so it has nothing to match against and skips this filter entirely. std::erase_if(ctx->entries, [](const AppHubEntry& entry) { -#ifdef ESP_PLATFORM - return !entry.targetPlatforms.empty() && - std::ranges::find(entry.targetPlatforms, std::string_view(CONFIG_IDF_TARGET)) == entry.targetPlatforms.end(); -#else - (void)entry; - return false; -#endif + return !isCompatible(entry); }); std::ranges::sort(ctx->entries, [](auto left, auto right) { diff --git a/Tactility/Source/app/apphub/AppHubEntry.cpp b/Tactility/Source/app/apphub/AppHubEntry.cpp index 57b0e626e..f0986ba2d 100644 --- a/Tactility/Source/app/apphub/AppHubEntry.cpp +++ b/Tactility/Source/app/apphub/AppHubEntry.cpp @@ -2,14 +2,35 @@ #include #include +#include + +#ifdef ESP_PLATFORM +#include +#endif + #include +#include +#include +#include +#include + namespace tt::app::apphub { constexpr auto* TAG = "AppHubJson"; static bool parseEntry(const cJSON* object, AppHubEntry& entry) { const json::Reader reader(object); + // Optional: absent in apps.json files from before these fields existed + // Reset because parseJson() reuses entries from a previous parse + entry.requiresDeviceId.clear(); + entry.requiresRam = 0; + if (cJSON_HasObjectItem(object, "requiresDeviceId") && !reader.readStringArray("requiresDeviceId", entry.requiresDeviceId)) { + return false; + } + if (cJSON_HasObjectItem(object, "requiresRam") && !reader.readInt32("requiresRam", entry.requiresRam)) { + return false; + } return reader.readString("appId", entry.appId) && reader.readString("appVersionName", entry.appVersionName) && reader.readInt32("appVersionCode", entry.appVersionCode) && @@ -57,4 +78,35 @@ bool parseJson(const std::string& filePath, AppHubEntryList& entries) { return true; } +bool isCompatible(const AppHubEntry& entry) { + // The simulator isn't a real MCU target, so it has no platform to match against +#ifdef ESP_PLATFORM + if (!entry.targetPlatforms.empty() && + std::ranges::find(entry.targetPlatforms, std::string_view(CONFIG_IDF_TARGET)) == entry.targetPlatforms.end()) { + return false; + } +#endif + + // Same device and RAM rules as for installed packages + PackageManifest package {}; + std::string deviceIds; + for (const auto& deviceId : entry.requiresDeviceId) { + if (!deviceIds.empty()) { + deviceIds += ','; + } + deviceIds += deviceId; + } + if (deviceIds.size() >= sizeof(package.requires_device_id)) { + LOG_W(TAG, "%s: requiresDeviceId too long", entry.appId.c_str()); + return false; + } + strcpy(package.requires_device_id, deviceIds.c_str()); + if (entry.requiresRam < 0 || entry.requiresRam > UINT8_MAX) { + LOG_W(TAG, "%s: invalid requiresRam %d", entry.appId.c_str(), static_cast(entry.requiresRam)); + return false; + } + package.requires_ram = static_cast(entry.requiresRam); + return app_package_manifest_is_compatible(&package); +} + } diff --git a/Tactility/Source/app/applist/AppList.cpp b/Tactility/Source/app/applist/AppList.cpp index 91e94defb..7a9614b6a 100644 --- a/Tactility/Source/app/applist/AppList.cpp +++ b/Tactility/Source/app/applist/AppList.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -14,6 +15,7 @@ #include #include +#include #include #include @@ -137,6 +139,17 @@ void collectManifest(const ::AppManifest* manifest, void* context) { manifests->push_back(*manifest); } +// Apps from installed packages this device can't run (wrong device, too little RAM) +void collectIncompatibleAppIds(const AppPackage* package, void* context) { + if (app_package_manifest_is_compatible(&package->package)) { + return; + } + auto* ids = static_cast*>(context); + for (size_t i = 0; i < package->app_id_count; i++) { + ids->emplace_back(package->app_ids[i]); + } +} + void populateList(lv_obj_t* list) { // Captured before lv_obj_clean() deletes the currently focused button below: LVGL moves // focus elsewhere as it leaves the group, and creating replacement buttons doesn't restore @@ -160,6 +173,11 @@ void populateList(lv_obj_t* list) { std::vector<::AppManifest> collected; app_manager_for_each_manifest(collectManifest, &collected); + std::vector incompatibleIds; + app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds); + std::erase_if(collected, [&](const ::AppManifest& manifest) { + return std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end(); + }); std::ranges::sort(collected, [&](const ::AppManifest& a, const ::AppManifest& b) { const bool aFavourite = Favourites::contains(favouriteIds, a.id); const bool bFavourite = Favourites::contains(favouriteIds, b.id); diff --git a/Tactility/Source/app/apppackagelist/AppPackageList.cpp b/Tactility/Source/app/apppackagelist/AppPackageList.cpp index c9aafb10a..e7d416125 100644 --- a/Tactility/Source/app/apppackagelist/AppPackageList.cpp +++ b/Tactility/Source/app/apppackagelist/AppPackageList.cpp @@ -13,6 +13,8 @@ #include +#include + #include #include #include @@ -25,15 +27,18 @@ extern const ::AppManifest manifest; namespace { +// Prefers PSRAM, like the other app list buffers +using PackageManifestList = std::vector>; + struct Context { uint32_t appInstanceId; // Must outlive the widgets - button user-data points into this, not a createWidgets()-local vector. - std::vector packageIds = {}; + PackageManifestList packages = {}; }; void onPackagePressed(lv_event_t* e) { - auto* packageId = static_cast(lv_event_get_user_data(e)); - apppackagedetails::start(packageId); + const auto* package = static_cast(lv_event_get_user_data(e)); + apppackagedetails::start(package->id); } void onBackPressed(lv_event_t* event) { @@ -41,16 +46,19 @@ void onBackPressed(lv_event_t* event) { app_event_emit_close(ctx->appInstanceId); } -void createPackageWidget(const char* packageId, lv_obj_t* list) { - lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_DEPLOYED_CODE, packageId); +void createPackageWidget(const PackageManifest* package, lv_obj_t* list) { + // A v2 package's single app shares its id, and its name is the nicer label. Otherwise show the package id. + AppManifest appManifest; + const char* label = (app_manager_find_manifest(package->id, &appManifest) == ERROR_NONE) ? appManifest.name : package->id; + lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_DEPLOYED_CODE, label); lv_obj_t* image = lv_obj_get_child(btn, 0); lv_obj_set_style_text_font(image, lvgl_get_shared_icon_font(), LV_PART_MAIN); - lv_obj_add_event_cb(btn, &onPackagePressed, LV_EVENT_SHORT_CLICKED, const_cast(packageId)); + lv_obj_add_event_cb(btn, &onPackagePressed, LV_EVENT_SHORT_CLICKED, const_cast(package)); } -void collectPackageId(const ::AppPackage* pkg, void* context) { - auto* packageIds = static_cast*>(context); - packageIds->emplace_back(pkg->package.id); +void collectPackage(const ::AppPackage* pkg, void* context) { + auto* packages = static_cast(context); + packages->push_back(pkg->package); } void createWidgets(lv_obj_t* parent, void* userData) { @@ -70,15 +78,17 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_grow(list, 1); // createWidgets() can rerun for this same Context (window rebuild-on-remove). - ctx->packageIds.clear(); - app_manager_for_each_package(collectPackageId, &ctx->packageIds); - std::ranges::sort(ctx->packageIds); - - for (const auto& packageId : ctx->packageIds) { - createPackageWidget(packageId.c_str(), list); + ctx->packages.clear(); + app_manager_for_each_package(collectPackage, &ctx->packages); + std::ranges::sort(ctx->packages, [](const PackageManifest& left, const PackageManifest& right) { + return strcmp(left.id, right.id) < 0; + }); + + for (const auto& package : ctx->packages) { + createPackageWidget(&package, list); } - if (ctx->packageIds.empty()) { + if (ctx->packages.empty()) { // lv_obj_align() is ignored for children of a flex-managed parent, so the empty-state // label needs its own flex-growing wrapper to center within; the (empty) list is hidden // rather than deleted so the wrapper can just take its place in the flex flow. diff --git a/Tactility/Source/app/fileselection/FileSelection.cpp b/Tactility/Source/app/fileselection/FileSelection.cpp index 2d77ddf82..a7bacc70c 100644 --- a/Tactility/Source/app/fileselection/FileSelection.cpp +++ b/Tactility/Source/app/fileselection/FileSelection.cpp @@ -85,7 +85,7 @@ int32_t appMain(int argc, char* argv[]) { if (ctx.resultCode == 0) { // The parent captures this via an AppStream bound to our stdout (see startWithMode()) - - // see Modules/app-module/source/stdio_wrap.cpp for how printf() itself gets routed there on POSIX. + // see Modules/app-posix-module/source/stdio_wrap.cpp for how printf() itself gets routed there on POSIX. LOG_I(TAG, "Result: %s", ctx.resultPath.c_str()); printf("%s", ctx.resultPath.c_str()); } diff --git a/Tactility/Source/app/inputdialog/InputDialog.cpp b/Tactility/Source/app/inputdialog/InputDialog.cpp index 83ab5343b..e26d26cfb 100644 --- a/Tactility/Source/app/inputdialog/InputDialog.cpp +++ b/Tactility/Source/app/inputdialog/InputDialog.cpp @@ -146,7 +146,7 @@ int32_t appMain(int argc, char* argv[]) { if (ctx.resultCode == 0) { // The caller captures this via an AppStream bound to our stdout (see start()); see - // Modules/app-module/source/stdio_wrap.cpp for how printf() itself gets routed there on POSIX. + // Modules/app-posix-module/source/stdio_wrap.cpp for how printf() itself gets routed there on POSIX. printf("%s", ctx.resultText.c_str()); } return ctx.resultCode; diff --git a/Tactility/Source/app/poweroff/PowerOff.cpp b/Tactility/Source/app/poweroff/PowerOff.cpp index fb66ef0e7..375c66aaa 100644 --- a/Tactility/Source/app/poweroff/PowerOff.cpp +++ b/Tactility/Source/app/poweroff/PowerOff.cpp @@ -118,6 +118,12 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_border_width(button_wrapper, 0, 0); lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + // Ensures showing selection state for keyboard/encoder devices + lv_obj_set_style_pad_all(button_wrapper, 0, LV_ALIGN_DEFAULT); + lv_obj_set_style_pad_column(button_wrapper, 12, LV_STATE_DEFAULT); + lv_obj_set_style_pad_ver(button_wrapper, 6, LV_STATE_DEFAULT); + lv_obj_set_style_pad_hor(button_wrapper, 6, LV_STATE_DEFAULT); + auto* yes_button = lv_button_create(button_wrapper); auto* yes_label = lv_label_create(yes_button); lv_label_set_text(yes_label, "Yes"); diff --git a/Tactility/Source/app/shell/LineEditor.cpp b/Tactility/Source/app/shell/LineEditor.cpp index 9ad45420d..e4d9b2451 100644 --- a/Tactility/Source/app/shell/LineEditor.cpp +++ b/Tactility/Source/app/shell/LineEditor.cpp @@ -13,7 +13,7 @@ namespace { // and cursor movement belong to the terminal itself and must never end up in a command's redirect // target. sh_redir.c only ever swaps the stdout/stderr/stdin FILE*, never fd 1 itself, which is // always piped to the terminal app running it. fwrite() would also miss this app's own stdio -// wrapping entirely (see stdio_wrap_posix.cpp), leaving nothing for that pipe to carry. +// wrapping entirely (see app-posix-module's stdio_wrap.cpp), leaving nothing for that pipe to carry. void write(const char* text, size_t length) { const char* bytes = text; size_t remaining = length; diff --git a/Tactility/Source/app/terminal/vterm/vterm.c b/Tactility/Source/app/terminal/vterm/vterm.c index ebdba1490..0e06d8934 100644 --- a/Tactility/Source/app/terminal/vterm/vterm.c +++ b/Tactility/Source/app/terminal/vterm/vterm.c @@ -61,6 +61,11 @@ typedef struct { */ int pending_wrap; + /* Scroll region (DECSTBM, ESC[top;bottom r): line feeds, IND, RI, IL and DL only move rows + * scroll_top up to (not including) scroll_bottom. scroll_bottom 0 means the whole screen. */ + int scroll_top; + int scroll_bottom; + } vterm_t; static vterm_t *s_vterms = NULL; @@ -146,6 +151,64 @@ static void vterm_scroll(vterm_t *vt) vt->cursor_y = rows - 1; } +/* Exclusive end row of the scroll region, within the visible rows. */ +static int region_bottom(const vterm_t *vt) +{ + const int rows = effective_rows(); + return (vt->scroll_bottom > 0 && vt->scroll_bottom < rows) ? vt->scroll_bottom : rows; +} + +static void clear_rows(vterm_t *vt, int from, int to) +{ + for (int y = from; y < to; y++) { + vterm_cell_t *row = &vt->cells[y * VTERM_COLS]; + for (int x = 0; x < VTERM_COLS; x++) { + row[x].ch = ' '; + row[x].attr = VTERM_DEFAULT_ATTR; + } + } +} + +/* Moves the scroll region's rows up by one and clears its last row. The whole screen scrolls + * through vterm_scroll(), so its top line still reaches the scrollback history. */ +static void vterm_scroll_region_up(vterm_t *vt) +{ + const int top = vt->scroll_top; + const int bottom = region_bottom(vt); + if (top == 0 && bottom == effective_rows()) { + vterm_scroll(vt); + return; + } + if (bottom - top > 1) { + memmove(&vt->cells[top * VTERM_COLS], &vt->cells[(top + 1) * VTERM_COLS], + (bottom - top - 1) * VTERM_COLS * sizeof(vterm_cell_t)); + } + clear_rows(vt, bottom - 1, bottom); +} + +/* Moves the scroll region's rows down by one and clears its first row. */ +static void vterm_scroll_region_down(vterm_t *vt) +{ + const int top = vt->scroll_top; + const int bottom = region_bottom(vt); + if (bottom - top > 1) { + memmove(&vt->cells[(top + 1) * VTERM_COLS], &vt->cells[top * VTERM_COLS], + (bottom - top - 1) * VTERM_COLS * sizeof(vterm_cell_t)); + } + clear_rows(vt, top, top + 1); +} + +/* Moves the cursor down one row. At the bottom of the scroll region, the region scrolls instead. + * Below the region, the cursor stops at the last row. */ +static void vterm_line_feed(vterm_t *vt) +{ + if (vt->cursor_y == region_bottom(vt) - 1) { + vterm_scroll_region_up(vt); + } else if (vt->cursor_y < effective_rows() - 1) { + vt->cursor_y++; + } +} + static void vterm_putchar_internal(vterm_t *vt, char c) { /* @@ -158,8 +221,7 @@ static void vterm_putchar_internal(vterm_t *vt, char c) if (vt->pending_wrap && c >= 32 && c < 127) { vt->pending_wrap = 0; vt->cursor_x = 0; - vt->cursor_y++; - if (vt->cursor_y >= effective_rows()) vterm_scroll(vt); + vterm_line_feed(vt); } // Direct pointer access for speed @@ -169,8 +231,7 @@ static void vterm_putchar_internal(vterm_t *vt, char c) case '\n': vt->pending_wrap = 0; vt->cursor_x = 0; - vt->cursor_y++; - if (vt->cursor_y >= effective_rows()) vterm_scroll(vt); + vterm_line_feed(vt); break; case '\r': vt->pending_wrap = 0; @@ -360,30 +421,17 @@ static int vterm_handle_escape(vterm_t *vt, char c) } // Non-CSI escape sequences: ESC if (c == 'D') { - // IND - Index: move cursor down, scroll if at bottom - if (vt->cursor_y >= effective_rows() - 1) { - vterm_scroll(vt); - } else { - vt->cursor_y++; - } + // IND - Index: move cursor down, scroll the region if at its bottom + vterm_line_feed(vt); vt->escape_state = 0; return 1; } if (c == 'M') { - // RI - Reverse Index: move cursor up, scroll down if at top - if (vt->cursor_y <= 0) { - // Scroll down: move lines 0..N-2 to 1..N-1 - memmove(&vt->cells[VTERM_COLS], &vt->cells[0], - (effective_rows() - 1) * VTERM_COLS * sizeof(vterm_cell_t)); - - // Clear top line - vterm_cell_t *top_row = &vt->cells[0]; - for (int x = 0; x < VTERM_COLS; x++) { - top_row[x].ch = ' '; - top_row[x].attr = VTERM_DEFAULT_ATTR; - } - } else { + // RI - Reverse Index: move cursor up, scroll the region down if at its top + if (vt->cursor_y == vt->scroll_top) { + vterm_scroll_region_down(vt); + } else if (vt->cursor_y > 0) { vt->cursor_y--; } @@ -393,11 +441,7 @@ static int vterm_handle_escape(vterm_t *vt, char c) if (c == 'E') { // NEL - Next Line: move to column 1 of next line, scroll if needed vt->cursor_x = 0; - if (vt->cursor_y >= effective_rows() - 1) { - vterm_scroll(vt); - } else { - vt->cursor_y++; - } + vterm_line_feed(vt); vt->escape_state = 0; return 1; @@ -528,53 +572,66 @@ static int vterm_handle_escape(vterm_t *vt, char c) break; } case 'L': { - // IL - Insert Lines: insert N blank lines at cursor row, scroll down + // IL - Insert Lines: insert N blank lines at the cursor row, within the scroll region + const int bottom = region_bottom(vt); + if (vt->cursor_y < vt->scroll_top || vt->cursor_y >= bottom) break; int n = 1; if (vt->escape_buf[0]) n = atoi(vt->escape_buf); if (n < 1) n = 1; - if (n > VTERM_ROWS - vt->cursor_y) n = VTERM_ROWS - vt->cursor_y; + if (n > bottom - vt->cursor_y) n = bottom - vt->cursor_y; - // Move lines down - int lines_to_move = VTERM_ROWS - vt->cursor_y - n; + int lines_to_move = bottom - vt->cursor_y - n; if (lines_to_move > 0) { memmove(&vt->cells[(vt->cursor_y + n) * VTERM_COLS], &vt->cells[vt->cursor_y * VTERM_COLS], lines_to_move * VTERM_COLS * sizeof(vterm_cell_t)); } - - // Clear inserted lines - for (int y = vt->cursor_y; y < vt->cursor_y + n; y++) { - vterm_cell_t *row = &vt->cells[y * VTERM_COLS]; - for (int x = 0; x < VTERM_COLS; x++) { - row[x].ch = ' '; - row[x].attr = VTERM_DEFAULT_ATTR; - } - } + clear_rows(vt, vt->cursor_y, vt->cursor_y + n); break; } case 'M': { - // DL - Delete Lines: delete N lines at cursor row, scroll up + // DL - Delete Lines: delete N lines at the cursor row, within the scroll region + const int bottom = region_bottom(vt); + if (vt->cursor_y < vt->scroll_top || vt->cursor_y >= bottom) break; int n = 1; if (vt->escape_buf[0]) n = atoi(vt->escape_buf); if (n < 1) n = 1; - if (n > VTERM_ROWS - vt->cursor_y) n = VTERM_ROWS - vt->cursor_y; + if (n > bottom - vt->cursor_y) n = bottom - vt->cursor_y; - // Move lines up - int lines_to_move = VTERM_ROWS - vt->cursor_y - n; + int lines_to_move = bottom - vt->cursor_y - n; if (lines_to_move > 0) { memmove(&vt->cells[vt->cursor_y * VTERM_COLS], &vt->cells[(vt->cursor_y + n) * VTERM_COLS], lines_to_move * VTERM_COLS * sizeof(vterm_cell_t)); } - - // Clear vacated lines at bottom - for (int y = VTERM_ROWS - n; y < VTERM_ROWS; y++) { - vterm_cell_t *row = &vt->cells[y * VTERM_COLS]; - for (int x = 0; x < VTERM_COLS; x++) { - row[x].ch = ' '; - row[x].attr = VTERM_DEFAULT_ATTR; - } + clear_rows(vt, bottom - n, bottom); + break; + } + case 'r': { + // DECSTBM - Set Scroll Region: ESC[top;bottom r (1-based), no parameters is the whole screen + const int rows = effective_rows(); + int top = 1, bottom = rows; + // Each parameter falls back to its own default when omitted, e.g. ESC[;4r + const char *separator = strchr(vt->escape_buf, ';'); + if (vt->escape_buf[0] && vt->escape_buf[0] != ';') top = atoi(vt->escape_buf); + if (separator && separator[1]) bottom = atoi(separator + 1); + if (top < 1) top = 1; + if (bottom > rows || bottom < 1) bottom = rows; + if (top < bottom) { + vt->scroll_top = top - 1; + vt->scroll_bottom = (bottom == rows) ? 0 : bottom; } + vt->cursor_x = 0; + vt->cursor_y = 0; + break; + } + case 'G': { + // CHA - Cursor Horizontal Absolute: move to column N (1-based) on the current row + int n = 1; + if (vt->escape_buf[0]) n = atoi(vt->escape_buf); + if (n < 1) n = 1; + vt->cursor_x = n - 1; + if (vt->cursor_x >= effective_cols()) vt->cursor_x = effective_cols() - 1; break; } case 'n': @@ -783,12 +840,10 @@ void vterm_write(int vt_id, const char *data, size_t len) */ if (pending_wrap) { pending_wrap = 0; - cx = 0; cy++; - if (cy >= effective_rows()) { - vt->cursor_x = cx; vt->cursor_y = cy; - vterm_scroll(vt); - cy = vt->cursor_y; - } + cx = 0; + vt->cursor_x = cx; vt->cursor_y = cy; + vterm_line_feed(vt); + cy = vt->cursor_y; cursor_ptr = &cells_base[cy * VTERM_COLS + cx]; row_end = &cells_base[cy * VTERM_COLS + cols]; } diff --git a/Tactility/Source/lvgl/wrappers/obj.cpp b/Tactility/Source/lvgl/wrappers/obj.cpp index 914808665..5df116501 100644 --- a/Tactility/Source/lvgl/wrappers/obj.cpp +++ b/Tactility/Source/lvgl/wrappers/obj.cpp @@ -19,7 +19,8 @@ void __wrap_lv_obj_set_flex_flow(lv_obj_t* obj, lv_flex_flow_t flow) { lv_obj_t* __wrap_lv_obj_create(lv_obj_t* parent) { auto obj = __real_lv_obj_create(parent); - if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) { + // Screens and display layers (a null parent, also used by LVGL's own display setup) must stay borderless and unpadded + if (parent != nullptr && lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) { lv_obj_set_style_pad_all(obj, 2, LV_STATE_DEFAULT); lv_obj_set_style_pad_gap(obj, 2, LV_STATE_DEFAULT); lv_obj_set_style_radius(obj, 3, LV_STATE_DEFAULT); diff --git a/Tactility/Tests/Source/AppHubEntryTest.cpp b/Tactility/Tests/Source/AppHubEntryTest.cpp new file mode 100644 index 000000000..5b4cf4978 --- /dev/null +++ b/Tactility/Tests/Source/AppHubEntryTest.cpp @@ -0,0 +1,84 @@ +#include "doctest.h" + +#include + +#include +#include + +#include +#include + +using namespace tt::app::apphub; + +namespace { + +std::string writeAppsJson(const char* contents) { + char temp[FILE_MAX_PATH_STRING_LENGTH]; + REQUIRE_EQ(paths_get_temp_path(temp, sizeof(temp)), ERROR_NONE); + REQUIRE_EQ(directory_make(temp, true), ERROR_NONE); + const std::string path = std::string(temp) + "/apphub_test_apps.json"; + FILE* file = fopen(path.c_str(), "w"); + REQUIRE_NE(file, nullptr); + fputs(contents, file); + fclose(file); + return path; +} + +AppHubEntry entryWith(std::vector requiresDeviceId, int32_t requiresRam) { + AppHubEntry entry {}; + entry.requiresDeviceId = std::move(requiresDeviceId); + entry.requiresRam = requiresRam; + return entry; +} + +} // namespace + +TEST_CASE("app hub: requiresDeviceId and requiresRam are parsed, and optional") { + const auto path = writeAppsJson(R"({"apps": [ + {"appId": "a", "appVersionName": "1", "appVersionCode": 1, "appName": "A", "appDescription": "", "targetSdk": "0.8.0", "targetPlatforms": [], "file": "a-1.app", + "requiresDeviceId": ["m5stack-tab5", "lilygo-tdeck"], "requiresRam": 2}, + {"appId": "b", "appVersionName": "1", "appVersionCode": 1, "appName": "B", "appDescription": "", "targetSdk": "0.8.0", "targetPlatforms": [], "file": "b-1.app"} + ]})"); + AppHubEntryList entries; + REQUIRE(parseJson(path, entries)); + REQUIRE_EQ(entries.size(), 2u); + CHECK_EQ(entries[0].requiresDeviceId, std::vector { "m5stack-tab5", "lilygo-tdeck" }); + CHECK_EQ(entries[0].requiresRam, 2); + CHECK(entries[1].requiresDeviceId.empty()); + CHECK_EQ(entries[1].requiresRam, 0); + remove(path.c_str()); +} + +TEST_CASE("app hub: reparsing into the same list resets optional requirements") { + AppHubEntryList entries; + const auto restricted = writeAppsJson(R"({"apps": [ + {"appId": "a", "appVersionName": "1", "appVersionCode": 1, "appName": "A", "appDescription": "", "targetSdk": "0.8.0", "targetPlatforms": [], "file": "a-1.app", + "requiresDeviceId": ["m5stack-tab5"], "requiresRam": 2} + ]})"); + REQUIRE(parseJson(restricted, entries)); + const auto unrestricted = writeAppsJson(R"({"apps": [ + {"appId": "b", "appVersionName": "1", "appVersionCode": 1, "appName": "B", "appDescription": "", "targetSdk": "0.8.0", "targetPlatforms": [], "file": "b-1.app"} + ]})"); + REQUIRE(parseJson(unrestricted, entries)); + REQUIRE_EQ(entries.size(), 1u); + CHECK(entries[0].requiresDeviceId.empty()); + CHECK_EQ(entries[0].requiresRam, 0); + remove(unrestricted.c_str()); +} + +TEST_CASE("app hub: a fractional requiresRam rejects the entry") { + const auto path = writeAppsJson(R"({"apps": [ + {"appId": "a", "appVersionName": "1", "appVersionCode": 1, "appName": "A", "appDescription": "", "targetSdk": "0.8.0", "targetPlatforms": [], "file": "a-1.app", + "requiresRam": 1.5} + ]})"); + AppHubEntryList entries; + CHECK_FALSE(parseJson(path, entries)); + remove(path.c_str()); +} + +TEST_CASE("app hub: isCompatible applies the device and RAM requirements") { + CHECK(isCompatible(entryWith({}, 0))); + CHECK(isCompatible(entryWith({ "other-device", CONFIG_TT_DEVICE_ID }, 1))); + CHECK_FALSE(isCompatible(entryWith({ "other-device" }, 0))); + CHECK_FALSE(isCompatible(entryWith({}, 256))); +} diff --git a/Tactility/Tests/Source/VtermTest.cpp b/Tactility/Tests/Source/VtermTest.cpp new file mode 100644 index 000000000..a26afbb0b --- /dev/null +++ b/Tactility/Tests/Source/VtermTest.cpp @@ -0,0 +1,86 @@ +#include "doctest.h" + +extern "C" { +#include +} + +#include +#include + +namespace { + +void write(const char* text) { + vterm_write(0, text, strlen(text)); +} + +char cell(int row, int col) { + return vterm_get_direct_buffer()[row * VTERM_COLS + col].ch; +} + +std::string column0(int rows) { + std::string result; + for (int row = 0; row < rows; row++) { + result += cell(row, 0); + } + return result; +} + +} // namespace + +TEST_CASE("vterm: a scroll region limits line feeds and inserted lines to its rows") { + REQUIRE_EQ(vterm_init(), ERROR_NONE); + vterm_set_size_override(5, 10); + + write("\x1B[2J\x1B[H1\r\n2\r\n3\r\n4\r\n5"); + CHECK_EQ(column0(5), "12345"); + + // Rows 2-4 (1-based) scroll, rows 1 and 5 stay put + write("\x1B[2;4r"); + write("\x1B[4;1H\n"); + CHECK_EQ(column0(5), "134 5"); + + write("\x1B[2;1H\x1B[L"); + CHECK_EQ(column0(5), "1 345"); + + write("\x1B[2;1H\x1B[M"); + CHECK_EQ(column0(5), "134 5"); + + // A full clear keeps the scroll region + write("\x1B[2J\x1B[H1\r\n2\r\n3\r\n4\x1B[5;1H5"); + CHECK_EQ(column0(5), "12345"); + write("\x1B[4;1H\n"); + CHECK_EQ(column0(5), "134 5"); + + // DECSTBM without parameters restores the whole screen as scroll region + write("\x1B[r\x1B[5;1H\n"); + CHECK_EQ(column0(5), "34 5 "); + + vterm_clear_size_override(); + vterm_deinit(); +} + +TEST_CASE("vterm: an omitted scroll region top defaults to the first row") { + REQUIRE_EQ(vterm_init(), ERROR_NONE); + vterm_set_size_override(5, 10); + + write("\x1B[2J\x1B[H1\r\n2\r\n3\r\n4\r\n5"); + write("\x1B[;4r\x1B[4;1H\n"); + CHECK_EQ(column0(5), "234 5"); + + vterm_clear_size_override(); + vterm_deinit(); +} + +TEST_CASE("vterm: CHA moves the cursor to a column on the current row") { + REQUIRE_EQ(vterm_init(), ERROR_NONE); + vterm_set_size_override(5, 10); + + write("\x1B[2J\x1B[3;2H\x1B[5G"); + int col = -1, row = -1, visible = 0; + vterm_get_cursor(0, &col, &row, &visible); + CHECK_EQ(col, 4); + CHECK_EQ(row, 2); + + vterm_clear_size_override(); + vterm_deinit(); +} diff --git a/TactilityKernel/include/tactility/drivers/keyboard.h b/TactilityKernel/include/tactility/drivers/keyboard.h index 7682ff815..9f9e1b127 100644 --- a/TactilityKernel/include/tactility/drivers/keyboard.h +++ b/TactilityKernel/include/tactility/drivers/keyboard.h @@ -19,7 +19,7 @@ extern "C" { * Subsystems like LVGL can translate these special codepoints to specific actions. */ typedef enum { - CODEPOINT_ENTER = '\r', + CODEPOINT_ENTER = 10, CODEPOINT_ESCAPE = '\x1B', CODEPOINT_BACKSPACE = '\b', CODEPOINT_DELETE = '\x7F', @@ -58,7 +58,7 @@ struct KeyboardKeyData { * * Reported separately rather than folded into `key` because the two encodings collide: the C0 * control codes a terminal expects for Ctrl chords (Ctrl+C is 0x03, Ctrl+K is 0x0B, ...) overlap - * the four CodePoint values that remain in true C0 range (CODEPOINT_ENTER is Ctrl+M/13, + * the four CodePoint values that remain in true C0 range (CODEPOINT_ENTER is Ctrl+J/10, * CODEPOINT_BACKSPACE is Ctrl+H/8, CODEPOINT_TAB is Ctrl+I/9, CODEPOINT_ESCAPE is Ctrl+[/27), so * a single uint32_t cannot express both. Consumers that want control codes derive them here, e.g. * `((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z')) ? (key & 0x1F) : key` diff --git a/TactilityKernel/include/tactility/memory.h b/TactilityKernel/include/tactility/memory.h index 58a426548..d388cebab 100644 --- a/TactilityKernel/include/tactility/memory.h +++ b/TactilityKernel/include/tactility/memory.h @@ -56,6 +56,12 @@ void memory_log_stats(void); */ size_t memory_heap_total(void); +/** + * @brief Total size of external memory (PSRAM), in bytes. + * @return the external memory's total size, or 0 if the device or platform has none + */ +size_t memory_external_total(void); + /** * @brief Currently free space in the main heap, in bytes. * @note See memory_heap_total()'s own note - the same system-wide-vs-process-heap caveat applies. diff --git a/TactilityKernel/source/drivers/keyboard.cpp b/TactilityKernel/source/drivers/keyboard.cpp index dc7ef9c05..5a1a1aa56 100644 --- a/TactilityKernel/source/drivers/keyboard.cpp +++ b/TactilityKernel/source/drivers/keyboard.cpp @@ -50,7 +50,7 @@ static const uint8_t hid_keycode_to_ascii[57][2] = { {'z', 'Z'}, {'1', '!'}, {'2', '@'}, {'3', '#'}, {'4', '$'}, {'5', '%'}, {'6', '^'}, {'7', '&'}, {'8', '*'}, {'9', '('}, {'0', ')'}, - {'\r', '\r'}, {0, 0}, {'\b', 0}, {'\t', '\t'}, {' ', ' '}, + {'\n', '\n'}, {0, 0}, {'\b', 0}, {'\t', '\t'}, {' ', ' '}, {'-', '_'}, {'=', '+'}, {'[', '{'}, {']', '}'}, {'\\', '|'}, {'\\', '|'}, {';', ':'}, {'\'', '"'}, {'`', '~'}, {',', '<'}, {'.', '>'}, {'/', '?'}, diff --git a/TactilityKernel/source/memory.cpp b/TactilityKernel/source/memory.cpp index 8b1e0d8cf..2afb71c1c 100644 --- a/TactilityKernel/source/memory.cpp +++ b/TactilityKernel/source/memory.cpp @@ -44,6 +44,14 @@ size_t memory_heap_total() { #endif } +size_t memory_external_total() { +#ifdef ESP_PLATFORM + return heap_caps_get_total_size(MALLOC_CAP_SPIRAM); +#else + return 0; +#endif +} + size_t memory_heap_free() { #ifdef ESP_PLATFORM return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 601d28a64..e769c4b26 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -189,6 +189,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(memory_free), DEFINE_MODULE_SYMBOL(memory_heap_free), DEFINE_MODULE_SYMBOL(memory_heap_total), + DEFINE_MODULE_SYMBOL(memory_external_total), // drivers/gpio_controller DEFINE_MODULE_SYMBOL(gpio_descriptor_acquire), DEFINE_MODULE_SYMBOL(gpio_descriptor_release),