mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-18 03:15:05 +00:00
stdio, keyboard, compact UI, package manifest, and more (#667)
This commit is contained in:
parent
25bcb20c05
commit
5bac1ada82
@ -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 <sys/ioctl.h> (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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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 {};
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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
|
||||
)
|
||||
@ -21,3 +21,4 @@ display.dpi=227
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.dpi=150
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -8,3 +8,4 @@ dependencies:
|
||||
- Drivers/drv2605-module
|
||||
- Drivers/gpio-encoder-module
|
||||
dts: lilygo,tlora-pager.dts
|
||||
dts-only: true
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <tactility/module.h>
|
||||
#include <tactility/system_event.h>
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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(' ', ' ') },
|
||||
};
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
70
Modules/app-esp32-module/source/elf_cache.cpp
Normal file
70
Modules/app-esp32-module/source/elf_cache.cpp
Normal file
@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <sdkconfig.h>
|
||||
|
||||
#ifdef CONFIG_ELF_LOADER_LOAD_PSRAM
|
||||
|
||||
#include <esp_ipc.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
// 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<bool> other_core_parked { false };
|
||||
std::atomic<bool> 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
|
||||
27
Modules/app-esp32-module/source/elf_relocate.cpp
Normal file
27
Modules/app-esp32-module/source/elf_relocate.cpp
Normal file
@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <sdkconfig.h>
|
||||
|
||||
#include <esp_elf.h>
|
||||
#include <private/elf_platform.h>
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,11 +1,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <app/private/stdio_wrap.h>
|
||||
|
||||
// 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 <app/io.h>
|
||||
#include <app/libc.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <signal.h>
|
||||
#include <sys/poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <termios.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdarg>
|
||||
|
||||
@ -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<unsigned long>(request), arg, &windowSize)) {
|
||||
*static_cast<struct winsize*>(arg) = windowSize;
|
||||
return 0;
|
||||
int result;
|
||||
if (app_libc_try_window_size(fd, static_cast<unsigned long>(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<int>(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
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -47,7 +47,6 @@ struct AppFileOps {
|
||||
struct AppFile {
|
||||
const struct AppFileOps* ops;
|
||||
void* object;
|
||||
bool suppress_console_tee;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@ -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)
|
||||
|
||||
70
Modules/app-module/include/app/libc.h
Normal file
70
Modules/app-module/include/app/libc.h
Normal file
@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <sys/poll.h>
|
||||
|
||||
#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
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
|
||||
5
Modules/app-module/include/poll.h
Normal file
5
Modules/app-module/include/poll.h
Normal file
@ -0,0 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// ESP-IDF only provides <sys/poll.h>. Apps include this through app-module's include path.
|
||||
#pragma once
|
||||
|
||||
#include <sys/poll.h>
|
||||
16
Modules/app-module/include/sys/ioctl.h
Normal file
16
Modules/app-module/include/sys/ioctl.h
Normal file
@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// ESP-IDF's <sys/ioctl.h> 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 <sys/ioctl.h>
|
||||
|
||||
#ifndef TIOCGWINSZ
|
||||
#define TIOCGWINSZ 0x5413
|
||||
struct winsize {
|
||||
unsigned short ws_row;
|
||||
unsigned short ws_col;
|
||||
unsigned short ws_xpixel;
|
||||
unsigned short ws_ypixel;
|
||||
};
|
||||
#endif
|
||||
@ -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.
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <app/instance.h>
|
||||
#include <app/libc.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/package_manifest.h>
|
||||
#include <app/private/fd_table.h>
|
||||
@ -14,6 +15,7 @@
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <termios.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@ -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). */
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <termios.h>
|
||||
|
||||
// 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 <sys/ioctl.h> 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);
|
||||
@ -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);
|
||||
|
||||
@ -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<char*>(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"
|
||||
|
||||
315
Modules/app-module/source/libc.cpp
Normal file
315
Modules/app-module/source/libc.cpp
Normal file
@ -0,0 +1,315 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/libc.h>
|
||||
|
||||
#include <app/dir.h>
|
||||
#include <app/io.h>
|
||||
#include <app/private/ledger.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/paths.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <signal.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <termios.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
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<struct winsize*>(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<size_t>(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<int>(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"
|
||||
@ -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),
|
||||
|
||||
46
Modules/app-module/source/package_compatibility.cpp
Normal file
46
Modules/app-module/source/package_compatibility.cpp
Normal file
@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/package_manifest.h>
|
||||
|
||||
#include <tactility/memory.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
|
||||
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<size_t>(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<uint64_t>(memory_heap_total()) + memory_external_total();
|
||||
const uint64_t required_ram = static_cast<uint64_t>(manifest->requires_ram) * 1024 * 1024;
|
||||
return total_ram >= required_ram;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@ -210,6 +210,21 @@ error_t package_manifest_parse_v3(const std::map<std::string, std::string>& 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<uint8_t>(megabytes);
|
||||
}
|
||||
|
||||
// app.<index>.* blocks: 0-indexed, contiguous - the first missing "app.<index>.id" ends the list.
|
||||
size_t count = 0;
|
||||
while (properties.contains(std::format("app.{}.id", count))) {
|
||||
|
||||
@ -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<uintptr_t>(pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX));
|
||||
}
|
||||
|
||||
|
||||
@ -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 <app/private/stdio_wrap.h>
|
||||
|
||||
#include <app/dir.h>
|
||||
#include <app/io.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/paths.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
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<size_t>(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<int>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<void*>(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<void*>(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<uint8_t> 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");
|
||||
}
|
||||
|
||||
@ -13,15 +13,10 @@
|
||||
#include <tactility/delay.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
@ -208,160 +203,8 @@ int32_t double_close_app_main(int, char*[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::atomic<bool> g_posix_stdin_is_char_device { false };
|
||||
std::atomic<int> g_posix_tcgetattr_result { -2 };
|
||||
std::atomic<bool> g_posix_stdin_is_raw { false };
|
||||
std::atomic<int> g_posix_tcsetattr_result { -2 };
|
||||
std::atomic<int> g_posix_poll_before_write { -2 };
|
||||
std::atomic<int> g_posix_poll_after_write { -2 };
|
||||
std::atomic<bool> g_posix_poll_first_done { false };
|
||||
std::atomic<bool> 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<bool> g_closed_fstat_ebadf { false };
|
||||
std::atomic<bool> g_closed_tcgetattr_ebadf { false };
|
||||
std::atomic<bool> g_closed_tcsetattr_ebadf { false };
|
||||
std::atomic<int> g_closed_poll_result { -2 };
|
||||
std::atomic<int> 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<bool> g_exit_before { false };
|
||||
std::atomic<bool> 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<void*>(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<void*>(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<void*>(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);
|
||||
|
||||
79
Modules/app-module/tests/source/package_manifest_test.cpp
Normal file
79
Modules/app-module/tests/source/package_manifest_test.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "doctest.h"
|
||||
|
||||
#include <app/package_manifest.h>
|
||||
#include <app/private/package_manifest_parsing.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::map<std::string, std::string> 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<std::string, std::string>& 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));
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -7,10 +7,13 @@
|
||||
#include <sys/poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <app/libc.h>
|
||||
|
||||
#include <signal.h>
|
||||
#include <termios.h>
|
||||
|
||||
|
||||
// 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);
|
||||
@ -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 <app/private/stdio_wrap.h>
|
||||
#include <app/private/stdio_wrap_posix.h>
|
||||
// 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 <app_posix/stdio_wrap.h>
|
||||
|
||||
#include <app/io.h>
|
||||
#include <app/libc.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <cerrno>
|
||||
@ -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<AppLibcSignalHandler (*)(int, AppLibcSignalHandler)>(dlsym(RTLD_NEXT, "signal"));
|
||||
return real(sig, handler);
|
||||
}
|
||||
|
||||
int __real_kill(pid_t pid, int sig) {
|
||||
static auto real = reinterpret_cast<int (*)(pid_t, int)>(dlsym(RTLD_NEXT, "kill"));
|
||||
return real(pid, sig);
|
||||
}
|
||||
|
||||
[[noreturn]] void __real_exit(int status) {
|
||||
static auto real = reinterpret_cast<void (*)(int)>(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<struct winsize*>(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
|
||||
@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifdef __APPLE__
|
||||
|
||||
#include <app/private/stdio_wrap_posix.h>
|
||||
#include <app_posix/stdio_wrap.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <sys/ioctl.h>
|
||||
@ -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)
|
||||
@ -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 <app/private/stdio_wrap_posix.h>
|
||||
#include <app_posix/stdio_wrap.h>
|
||||
|
||||
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);
|
||||
}
|
||||
@ -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)
|
||||
|
||||
415
Modules/app-posix-module/tests/source/libc_test.cpp
Normal file
415
Modules/app-posix-module/tests/source/libc_test.cpp
Normal file
@ -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 <app/event.h>
|
||||
#include <app/io.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
#include <app/stream.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
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<bool>& 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<int32_t>(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<bool> g_posix_stdin_is_char_device { false };
|
||||
std::atomic<int> g_posix_tcgetattr_result { -2 };
|
||||
std::atomic<bool> g_posix_stdin_is_raw { false };
|
||||
std::atomic<int> g_posix_tcsetattr_result { -2 };
|
||||
std::atomic<int> g_posix_poll_before_write { -2 };
|
||||
std::atomic<int> g_posix_poll_after_write { -2 };
|
||||
std::atomic<bool> g_posix_poll_first_done { false };
|
||||
std::atomic<bool> 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<bool> g_closed_fstat_ebadf { false };
|
||||
std::atomic<bool> g_closed_tcgetattr_ebadf { false };
|
||||
std::atomic<bool> g_closed_tcsetattr_ebadf { false };
|
||||
std::atomic<bool> g_closed_ioctl_ebadf { false };
|
||||
std::atomic<int> g_closed_poll_result { -2 };
|
||||
std::atomic<int> 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<bool> g_exit_before { false };
|
||||
std::atomic<bool> 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<bool> g_signal_first_default { false };
|
||||
std::atomic<bool> g_signal_returns_previous { false };
|
||||
std::atomic<bool> g_signal_sigkill_rejected { false };
|
||||
std::atomic<bool> 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<bool> g_icrnl_default_on { false };
|
||||
std::atomic<int> g_icrnl_first_read { -2 };
|
||||
std::atomic<bool> g_icrnl_first_done { false };
|
||||
std::atomic<int> 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<void*>(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<void*>(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<void*>(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<void*>(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<void*>(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<void*>(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<void*>(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<uint8_t> 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");
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include <ctime>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <strings.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/poll.h>
|
||||
@ -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
|
||||
|
||||
@ -15,6 +15,10 @@ struct AppHubEntry {
|
||||
std::string appDescription;
|
||||
std::string targetSdk;
|
||||
std::vector<std::string> targetPlatforms;
|
||||
/** Device ids the app is restricted to (manifest "requires.device.id"). Empty means unrestricted. */
|
||||
std::vector<std::string> 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<AppHubEntry, OptExternalAllocator<AppHubEntr
|
||||
|
||||
bool parseJson(const std::string& filePath, AppHubEntryList& entries);
|
||||
|
||||
/** @return true if this device can install and run the entry's app (target platform, device and RAM requirements) */
|
||||
bool isCompatible(const AppHubEntry& entry);
|
||||
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cJSON.h>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@ -32,6 +33,10 @@ public:
|
||||
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<int32_t>(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -2,14 +2,35 @@
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/json/Reader.h>
|
||||
|
||||
#include <app/package_manifest.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
|
||||
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<int>(entry.requiresRam));
|
||||
return false;
|
||||
}
|
||||
package.requires_ram = static_cast<uint8_t>(entry.requiresRam);
|
||||
return app_package_manifest_is_compatible(&package);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/package_manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
|
||||
@ -14,6 +15,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <lvgl/icons/shared.h>
|
||||
@ -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<std::vector<std::string>*>(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<std::string> 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);
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
|
||||
#include <tactility/check.h>
|
||||
|
||||
#include <TactilityCpp/Allocator.h>
|
||||
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
#include <lvgl.h>
|
||||
#include <algorithm>
|
||||
@ -25,15 +27,18 @@ extern const ::AppManifest manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
// Prefers PSRAM, like the other app list buffers
|
||||
using PackageManifestList = std::vector<PackageManifest, OptExternalAllocator<PackageManifest>>;
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
// Must outlive the widgets - button user-data points into this, not a createWidgets()-local vector.
|
||||
std::vector<std::string> packageIds = {};
|
||||
PackageManifestList packages = {};
|
||||
};
|
||||
|
||||
void onPackagePressed(lv_event_t* e) {
|
||||
auto* packageId = static_cast<char*>(lv_event_get_user_data(e));
|
||||
apppackagedetails::start(packageId);
|
||||
const auto* package = static_cast<const PackageManifest*>(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<char*>(packageId));
|
||||
lv_obj_add_event_cb(btn, &onPackagePressed, LV_EVENT_SHORT_CLICKED, const_cast<PackageManifest*>(package));
|
||||
}
|
||||
|
||||
void collectPackageId(const ::AppPackage* pkg, void* context) {
|
||||
auto* packageIds = static_cast<std::vector<std::string>*>(context);
|
||||
packageIds->emplace_back(pkg->package.id);
|
||||
void collectPackage(const ::AppPackage* pkg, void* context) {
|
||||
auto* packages = static_cast<PackageManifestList*>(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);
|
||||
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& packageId : ctx->packageIds) {
|
||||
createPackageWidget(packageId.c_str(), list);
|
||||
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.
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 <letter>
|
||||
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];
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
84
Tactility/Tests/Source/AppHubEntryTest.cpp
Normal file
84
Tactility/Tests/Source/AppHubEntryTest.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <Tactility/app/apphub/AppHubEntry.h>
|
||||
|
||||
#include <tactility/filesystem/fs.h>
|
||||
#include <tactility/paths.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
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<std::string> 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<std::string> { "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)));
|
||||
}
|
||||
86
Tactility/Tests/Source/VtermTest.cpp
Normal file
86
Tactility/Tests/Source/VtermTest.cpp
Normal file
@ -0,0 +1,86 @@
|
||||
#include "doctest.h"
|
||||
|
||||
extern "C" {
|
||||
#include <Tactility/app/terminal/vterm/vterm.h>
|
||||
}
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
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();
|
||||
}
|
||||
@ -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`
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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'}, {' ', ' '},
|
||||
{'-', '_'}, {'=', '+'}, {'[', '{'}, {']', '}'},
|
||||
{'\\', '|'}, {'\\', '|'}, {';', ':'}, {'\'', '"'},
|
||||
{'`', '~'}, {',', '<'}, {'.', '>'}, {'/', '?'},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user