diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 4435ce04d..79bb1e8f5 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -7,7 +7,7 @@ ## Higher Priority -- coreutils apps: Only emit colour escapes when stdout is the terminal (command_support.h color() function) +- NimBLE looses pairing key after reboot - 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 @@ -18,7 +18,7 @@ - TCA9534 keyboards should use interrupts - External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility Check during installation process, but also when starting (SD card might have old app install from before Tactility OS update) -- Support direct installation of an `.app` file with `tactility.py install helloworld.app ` +- Support direct installation of an `.app` file with `tactility.py install --app helloworld.app` - minitar/untarFile(): "entry->metadata.path" can escape its confined path (e.g. "../something") ## Medium Priority diff --git a/Drivers/lilygo-module/source/tdeck_keyboard.cpp b/Drivers/lilygo-module/source/tdeck_keyboard.cpp index 0afd4c820..1b94fc166 100644 --- a/Drivers/lilygo-module/source/tdeck_keyboard.cpp +++ b/Drivers/lilygo-module/source/tdeck_keyboard.cpp @@ -66,6 +66,10 @@ static error_t tdeck_keyboard_read_key(Device* device, KeyboardKeyData* data) { data->key = internal->last_key; data->pressed = false; } else if (read_buffer != 0) { + // Return key is keycode 10, but keyboard passes 13 + if (read_buffer == 13) { + read_buffer = 10; + } data->key = read_buffer; data->pressed = true; } else { diff --git a/Modules/app-esp32-module/source/app_esp32_loader_service.cpp b/Modules/app-esp32-module/source/app_esp32_loader_service.cpp index 12add2c2f..0c17a3827 100644 --- a/Modules/app-esp32-module/source/app_esp32_loader_service.cpp +++ b/Modules/app-esp32-module/source/app_esp32_loader_service.cpp @@ -146,7 +146,7 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) { // esp_elf_relocate() already frees elf->pdata/ptext itself on a relocation failure free(runtime->file_data); delete runtime; - LOG_E(TAG, "Failed to map elf"); + LOG_E(TAG, "Failed to map elf. Possibly out of memory."); return ERROR_RESOURCE; } diff --git a/Modules/app-esp32-module/source/app_symbols.cpp b/Modules/app-esp32-module/source/app_symbols.cpp new file mode 100644 index 000000000..d258a9b76 --- /dev/null +++ b/Modules/app-esp32-module/source/app_symbols.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Symbols resolved for ELF apps ahead of every other module's (see app_esp32_symbol_resolver()). +// Firmware code calling the same functions never gets here. +// Pending signals are delivered here, at the entry and again when a call was interrupted by one, +// since the app holds no lock of the system itself there. +// Allocations are counted per app instance (see app/memory.h). +#include +#include +#include + +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +ssize_t app_read(int fd, void* buffer, size_t size) { + app_signal_deliver_pending(); + const ssize_t result = read(fd, buffer, size); + if (result < 0 && errno == EINTR) { + app_signal_deliver_pending(); + } + return result; +} + +ssize_t app_write(int fd, const void* buffer, size_t size) { + app_signal_deliver_pending(); + const ssize_t result = write(fd, buffer, size); + if (result < 0 && errno == EINTR) { + app_signal_deliver_pending(); + } + return result; +} + +int app_poll(struct pollfd* fds, nfds_t nfds, int timeout) { + app_signal_deliver_pending(); + const int result = poll(fds, nfds, timeout); + if (result < 0 && errno == EINTR) { + app_signal_deliver_pending(); + } + return result; +} + +int app_usleep(useconds_t usec) { + app_signal_deliver_pending(); + int result; + if (!app_libc_try_usleep(usec, &result)) { + return usleep(usec); + } + app_signal_deliver_pending(); + return result; +} + +unsigned int app_sleep(unsigned int seconds) { + app_signal_deliver_pending(); + unsigned int result; + if (!app_libc_try_sleep(seconds, &result)) { + return sleep(seconds); + } + app_signal_deliver_pending(); + return result; +} + +int app_kill(pid_t pid, int sig) { + const int result = kill(pid, sig); + // A signal sent to the app itself is delivered before kill() returns + app_signal_deliver_pending(); + return result; +} + +pid_t app_getpid() { + int result; + return app_libc_try_getpid(&result) ? result : 0; +} + +pid_t app_getppid() { + int result; + return app_libc_try_getppid(&result) ? result : 0; +} + +void* record_alloc(void* ptr) { + if (ptr != nullptr) { + app_memory_record_alloc(heap_caps_get_allocated_size(ptr)); + } + return ptr; +} + +void record_free(void* ptr) { + if (ptr != nullptr) { + app_memory_record_free(heap_caps_get_allocated_size(ptr)); + } +} + +void* app_malloc(size_t size) { + return record_alloc(malloc(size)); +} + +void* app_calloc(size_t count, size_t size) { + return record_alloc(calloc(count, size)); +} + +void* app_realloc(void* ptr, size_t size) { + const size_t old_size = (ptr != nullptr) ? heap_caps_get_allocated_size(ptr) : 0; + void* result = realloc(ptr, size); + // A failed realloc() leaves the old block allocated + if (result != nullptr || size == 0) { + if (ptr != nullptr) { + app_memory_record_free(old_size); + } + record_alloc(result); + } + return result; +} + +void app_free(void* ptr) { + record_free(ptr); + free(ptr); +} + +char* app_strdup(const char* s) { + return static_cast(record_alloc(strdup(s))); +} + +char* app_strndup(const char* s, size_t n) { + return static_cast(record_alloc(strndup(s, n))); +} + +void* app_operator_new(size_t size) { + return record_alloc(::operator new(size)); +} + +void* app_operator_new_array(size_t size) { + return record_alloc(::operator new[](size)); +} + +void app_operator_delete(void* ptr) { + record_free(ptr); + ::operator delete(ptr); +} + +void app_operator_delete_array(void* ptr) { + record_free(ptr); + ::operator delete[](ptr); +} + +void app_operator_delete_sized(void* ptr, size_t) { + app_operator_delete(ptr); +} + +void app_operator_delete_array_sized(void* ptr, size_t) { + app_operator_delete_array(ptr); +} + +} // namespace + +extern "C" { + +extern const ModuleSymbol app_esp32_symbols[] = { + { "read", reinterpret_cast(app_read) }, + { "write", reinterpret_cast(app_write) }, + { "poll", reinterpret_cast(app_poll) }, + { "usleep", reinterpret_cast(app_usleep) }, + { "sleep", reinterpret_cast(app_sleep) }, + { "kill", reinterpret_cast(app_kill) }, + { "getpid", reinterpret_cast(app_getpid) }, + { "getppid", reinterpret_cast(app_getppid) }, + { "malloc", reinterpret_cast(app_malloc) }, + { "calloc", reinterpret_cast(app_calloc) }, + { "realloc", reinterpret_cast(app_realloc) }, + { "free", reinterpret_cast(app_free) }, + { "strdup", reinterpret_cast(app_strdup) }, + { "strndup", reinterpret_cast(app_strndup) }, + { "_Znwj", reinterpret_cast(app_operator_new) }, // operator new(unsigned int) + { "_Znaj", reinterpret_cast(app_operator_new_array) }, // operator new[](unsigned int) + { "_ZdlPv", reinterpret_cast(app_operator_delete) }, // operator delete(void*) + { "_ZdaPv", reinterpret_cast(app_operator_delete_array) }, // operator delete[](void*) + { "_ZdlPvj", reinterpret_cast(app_operator_delete_sized) }, // operator delete(void*, unsigned int) + { "_ZdaPvj", reinterpret_cast(app_operator_delete_array_sized) }, // operator delete[](void*, unsigned int) + MODULE_SYMBOL_TERMINATOR +}; + +} diff --git a/Modules/app-esp32-module/source/module.cpp b/Modules/app-esp32-module/source/module.cpp index 85c7a014c..188ddc2fd 100644 --- a/Modules/app-esp32-module/source/module.cpp +++ b/Modules/app-esp32-module/source/module.cpp @@ -11,12 +11,16 @@ extern "C" { extern ServiceManifest loader_service_manifest; +extern const ModuleSymbol app_esp32_symbols[]; +extern Module app_esp32_module; // Overrides elf_loader's default KConfig-based symbol resolver with one that looks up symbols -// across every started kernel module's own symbol table instead. +// across every started kernel module's own symbol table instead. This module's own symbols come +// first, as they replace libc functions for apps (see app_symbols.cpp). uintptr_t app_esp32_symbol_resolver(const char* symbolName) { uintptr_t symbol_address; - if (module_resolve_symbol_global(symbolName, &symbol_address)) { + if (module_resolve_symbol(&app_esp32_module, symbolName, &symbol_address) || + module_resolve_symbol_global(symbolName, &symbol_address)) { return symbol_address; } return 0; @@ -37,7 +41,7 @@ Module app_esp32_module = { .start = start, .stop = stop, .drivers = nullptr, - .symbols = nullptr, + .symbols = app_esp32_symbols, .internal = nullptr, }; diff --git a/Modules/app-module/include/app/event.h b/Modules/app-module/include/app/event.h index 2cd793a2b..cb49a60d9 100644 --- a/Modules/app-module/include/app/event.h +++ b/Modules/app-module/include/app/event.h @@ -17,6 +17,7 @@ extern "C" { enum AppEventType { APP_EVENT_RESULT, // struct AppResultEventData APP_EVENT_CLOSE, // no data - terminate now, permanently + APP_EVENT_SIGNAL, // struct AppSignalEventData, see app/signal.h }; /** Data for APP_EVENT_RESULT. */ @@ -29,12 +30,22 @@ struct AppResultEventData { int32_t result; }; +/** Data for APP_EVENT_SIGNAL. */ +struct AppSignalEventData { + /** A POSIX signal number, e.g. SIGHUP */ + int sig; +}; + struct AppEvent { enum AppEventType type; /** Stamped by app_event_emit(); any value passed in by the caller is ignored. */ uint64_t timestamp; - /** Valid only when type == APP_EVENT_RESULT. */ - struct AppResultEventData result; + union { + /** Valid only when type == APP_EVENT_RESULT. */ + struct AppResultEventData result; + /** Valid only when type == APP_EVENT_SIGNAL. */ + struct AppSignalEventData signal; + }; }; /** diff --git a/Modules/app-module/include/app/libc.h b/Modules/app-module/include/app/libc.h index 8b66ec481..5710df409 100644 --- a/Modules/app-module/include/app/libc.h +++ b/Modules/app-module/include/app/libc.h @@ -48,20 +48,40 @@ bool app_libc_try_tcsetattr(int fd, const struct termios* t, int* out_result); 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. + * Records @a handler for @a sig in the calling app instance, called when a signal is delivered (see + * app/signal.h). 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. */ +/** + * Sends @a sig to the app instance @a pid via app_signal_send(), never to the process. + * A @a sig of 0 only checks that @a pid exists. SIGKILL, SIGSTOP and out-of-range signals fail + * with EINVAL. A @a pid that is not a running app instance fails with ESRCH, including pid <= 0, + * since there are no process groups. + */ bool app_libc_try_kill(int pid, int sig, int* out_result); +/** The calling app instance's AppInstanceId. */ +bool app_libc_try_getpid(int* out_result); + +/** The AppInstanceId of the calling app instance's parent, or 0 for a top-level launch. */ +bool app_libc_try_getppid(int* out_result); + +/** Sleeps the calling app instance. Fails with EINTR when a signal is pending or arrives (see app/signal.h). */ +bool app_libc_try_usleep(unsigned long usec, int* out_result); + +/** + * Sleeps the calling app instance. Returns early when a signal is pending or arrives (see app/signal.h). + * @param[out] out_result the whole seconds not slept, 0 when not interrupted + */ +bool app_libc_try_sleep(unsigned int seconds, unsigned 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. + * Closed app fds report POLLNVAL. Fails with EINTR when a signal is pending (see app/signal.h). */ bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPollFunction real_poll, int* out_result); diff --git a/Modules/app-module/include/app/memory.h b/Modules/app-module/include/app/memory.h new file mode 100644 index 000000000..cb0b973f3 --- /dev/null +++ b/Modules/app-module/include/app/memory.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Counts an allocation made by the calling app instance's own code. When the app ends with + * allocations left, a warning is logged. Does nothing outside an app's own task. + * @param[in] size the allocated block's size + */ +void app_memory_record_alloc(size_t size); + +/** + * Counts a free by the calling app instance's own code. Does nothing outside an app's own task. + * @param[in] size the freed block's size + */ +void app_memory_record_free(size_t size); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/app-module/include/app/signal.h b/Modules/app-module/include/app/signal.h new file mode 100644 index 000000000..39aa9b597 --- /dev/null +++ b/Modules/app-module/include/app/signal.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "instance.h" + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Sends a POSIX signal to an app instance. A signal's pid is its AppInstanceId. + * + * An app with an event subscription (app/event.h) receives the signal as an AppEvent: SIGTERM as + * APP_EVENT_CLOSE, any other signal as APP_EVENT_SIGNAL. Its signal() handlers are not used. + * + * Any other app gets the signal queued, and delivered on its own task at its next libc call (see + * app_signal_deliver_pending()). A blocking read, write, poll or sleep it is waiting in returns -1 + * with errno EINTR within about 100 ms. + * A signal the app ignores is discarded right away. + * + * @param[in] sig 1 to APP_LIBC_SIGNAL_COUNT - 1 (app/libc.h) + * @retval ERROR_NONE the signal was sent or discarded + * @retval ERROR_INVALID_ARGUMENT @a sig is out of range + * @retval ERROR_NOT_FOUND no such app instance + * @retval ERROR_RESOURCE an event queue of the app was full + */ +error_t app_signal_send(AppInstanceId app_instance_id, int sig); + +/** @return true when the calling app instance has a signal that was not yet delivered */ +bool app_signal_is_pending(void); + +/** + * Delivers the calling app instance's pending signals: calls signal() handlers, and ends the app + * (as exit(128 + sig)) for a signal without a handler whose default action terminates. + * Does nothing outside an app task. + * @warning Only call where the app holds no lock of the system itself, e.g. at the entry of a libc wrap. + */ +void app_signal_deliver_pending(void); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/app-module/private/app/private/ledger.h b/Modules/app-module/private/app/private/ledger.h index 5d26317bf..f284566a7 100644 --- a/Modules/app-module/private/app/private/ledger.h +++ b/Modules/app-module/private/app/private/ledger.h @@ -77,6 +77,9 @@ struct AppInstanceRecord { /** Handlers registered via signal() (app/libc.h), indexed by signal number. Null means SIG_DFL. */ AppLibcSignalHandler signal_handlers[APP_LIBC_SIGNAL_COUNT] {}; + /** Signals sent via app_signal_send() but not yet delivered, one bit per signal number (app/signal.h). */ + uint32_t pending_signals = 0; + /** Terminal input flags set via tcsetattr() (app/libc.h). ICRNL is applied when reading stdin. */ tcflag_t termios_iflag = ICRNL; }; diff --git a/Modules/app-module/source/libc.cpp b/Modules/app-module/source/libc.cpp index 298a4689b..9025ac885 100644 --- a/Modules/app-module/source/libc.cpp +++ b/Modules/app-module/source/libc.cpp @@ -3,8 +3,10 @@ #include #include +#include #include #include +#include #include #include @@ -44,6 +46,28 @@ AppFdState get_app_fd_state(int fd) { // 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); +// Upper bound on how long a sleep or poll takes to notice a signal (see app/signal.h) +constexpr TickType_t SIGNAL_CHECK_INTERVAL_TICKS = pdMS_TO_TICKS(100); + +/** + * @param[out] out_remaining the ticks not slept + * @return true when interrupted by a signal + */ +bool sleep_unless_signalled(TickType_t ticks, TickType_t* out_remaining) { + const TickType_t start = get_ticks(); + while (true) { + const TickType_t remaining = get_timeout_remaining_ticks(ticks, start); + *out_remaining = remaining; + if (app_signal_is_pending()) { + return true; + } + if (remaining == 0) { + return false; + } + delay_ticks(remaining < SIGNAL_CHECK_INTERVAL_TICKS ? remaining : SIGNAL_CHECK_INTERVAL_TICKS); + } +} + } // namespace extern "C" { @@ -232,8 +256,79 @@ 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; + if (sig < 0 || sig >= APP_LIBC_SIGNAL_COUNT || sig == SIGKILL || sig == SIGSTOP) { + errno = EINVAL; + *out_result = -1; + return true; + } + const bool exists = pid > 0 && app_manager_get_state(static_cast(pid)) != APP_INSTANCE_STATE_STOPPED; + if (!exists) { + errno = ESRCH; + *out_result = -1; + return true; + } + const error_t result = (sig == 0) ? ERROR_NONE : app_signal_send(static_cast(pid), sig); + switch (result) { + case ERROR_NONE: + *out_result = 0; + break; + case ERROR_NOT_FOUND: + errno = ESRCH; + *out_result = -1; + break; + default: + errno = EAGAIN; + *out_result = -1; + break; + } + return true; +} + +bool app_libc_try_getpid(int* out_result) { + const AppInstanceId app_instance_id = app_scheduler_current_app_id(); + if (app_instance_id == 0) { + return false; + } + *out_result = static_cast(app_instance_id); + return true; +} + +bool app_libc_try_getppid(int* out_result) { + const AppInstanceId app_instance_id = app_scheduler_current_app_id(); + if (app_instance_id == 0) { + return false; + } + auto& ledger = app_ledger(); + mutex_lock(&ledger.mutex); + auto iterator = ledger.instances.find(app_instance_id); + const uint32_t parent_id = (iterator != ledger.instances.end()) ? iterator->second.parent_id : 0; + mutex_unlock(&ledger.mutex); + *out_result = static_cast(parent_id); + return true; +} + +bool app_libc_try_usleep(unsigned long usec, int* out_result) { + if (app_scheduler_current_app_id() == 0) { + return false; + } + const auto ticks = static_cast((static_cast(usec) * configTICK_RATE_HZ + 999999) / 1000000); + TickType_t remaining; + if (sleep_unless_signalled(ticks, &remaining)) { + errno = EINTR; + *out_result = -1; + } else { + *out_result = 0; + } + return true; +} + +bool app_libc_try_sleep(unsigned int seconds, unsigned int* out_result) { + if (app_scheduler_current_app_id() == 0) { + return false; + } + TickType_t remaining; + sleep_unless_signalled(static_cast(static_cast(seconds) * configTICK_RATE_HZ), &remaining); + *out_result = static_cast(remaining / configTICK_RATE_HZ); return true; } @@ -264,6 +359,11 @@ bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPoll const TickType_t start = get_ticks(); const TickType_t timeout_ticks = (timeout < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout); while (true) { + if (app_signal_is_pending()) { + errno = EINTR; + *out_result = -1; + return true; + } int ready = 0; for (nfds_t i = 0; i < nfds; i++) { fds[i].revents = 0; @@ -302,10 +402,11 @@ bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPoll remaining = POLL_INTERVAL_TICKS; } const struct pollfd& first = fds[first_app_index]; + const TickType_t wait_ticks = remaining < SIGNAL_CHECK_INTERVAL_TICKS ? remaining : SIGNAL_CHECK_INTERVAL_TICKS; if (first.events & POLLIN) { - app_io_await(first.fd, APP_FILE_WAIT_READABLE, remaining); + app_io_await(first.fd, APP_FILE_WAIT_READABLE, wait_ticks); } else if (first.events & POLLOUT) { - app_io_await(first.fd, APP_FILE_WAIT_WRITABLE, remaining); + app_io_await(first.fd, APP_FILE_WAIT_WRITABLE, wait_ticks); } else { delay_ticks(remaining < POLL_INTERVAL_TICKS ? remaining : POLL_INTERVAL_TICKS); } diff --git a/Modules/app-module/source/module.cpp b/Modules/app-module/source/module.cpp index 515770b05..ae43a998e 100644 --- a/Modules/app-module/source/module.cpp +++ b/Modules/app-module/source/module.cpp @@ -7,9 +7,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -76,6 +78,9 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(app_manifest_id_is_valid), DEFINE_MODULE_SYMBOL(app_manifest_name_is_valid), DEFINE_MODULE_SYMBOL(app_manifest_stack_size_is_valid), + // app/memory + DEFINE_MODULE_SYMBOL(app_memory_record_alloc), + DEFINE_MODULE_SYMBOL(app_memory_record_free), // app/package_manifest DEFINE_MODULE_SYMBOL(app_package_manifest_parse), DEFINE_MODULE_SYMBOL(app_package_manifest_is_compatible), @@ -86,6 +91,8 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(app_paths_get_assets_path), // app/scheduler DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id), + // app/signal + DEFINE_MODULE_SYMBOL(app_signal_send), // app/start DEFINE_MODULE_SYMBOL(app_start_context_for_manifest), DEFINE_MODULE_SYMBOL(app_start_context_for_location), diff --git a/Modules/app-module/source/scheduler.cpp b/Modules/app-module/source/scheduler.cpp index 653a8adc7..4e05d5ecd 100644 --- a/Modules/app-module/source/scheduler.cpp +++ b/Modules/app-module/source/scheduler.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include #include #include @@ -8,6 +9,7 @@ #include #include #include +#include #include #include @@ -20,6 +22,7 @@ #include #include +#include #include #include #include @@ -82,6 +85,9 @@ struct TaskContext { AppCompletionSignal* completion; StackType_t* stackBuffer; StaticTask_t* taskTcb; + // Live allocations of the app's own code, see app/memory.h. Only updated by the app's own task. + int32_t allocCount; + int64_t allocBytes; }; struct ReaperContext { @@ -306,6 +312,10 @@ void finish_app_task(TaskContext* ctx, int32_t result, bool exiting) { ctx->loader->unload(ctx->runtime); #endif + if (ctx->allocCount > 0 || ctx->allocBytes > 0) { + LOG_W(TAG, "[instance %lu] %ld allocations (%lld bytes) not freed", ctx->app_instance_id, static_cast(ctx->allocCount), static_cast(ctx->allocBytes)); + } + deliver_result_to_parent_if_any(ctx->app_instance_id, result); // The terminal marker for every exit path: an app instance is Stopped exactly when its @@ -454,6 +464,8 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, const AppStartContext .completion = completion, .stackBuffer = stack_buffer, .taskTcb = task_tcb, + .allocCount = 0, + .allocBytes = 0, }; if (context == nullptr) { @@ -515,8 +527,8 @@ error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeou AppCompletionSignal* completion = acquire_completion_signal(app_instance_id); if (completion != nullptr) { - AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; - app_event_emit(app_instance_id, &event); + // An app with an event subscription receives this as APP_EVENT_CLOSE, see app/signal.h + app_signal_send(app_instance_id, SIGTERM); // Marked as soon as the app has been told to close, not once its task has actually // unwound - so app_manager_get_state()/app_manager_get_topmost_instance_id() reflect the @@ -549,6 +561,29 @@ AppInstanceId app_scheduler_current_app_id(void) { return get_current_app_id(); } +void app_memory_record_alloc(size_t size) { + // Checked first: thread_local can't be read before the scheduler starts on ESP32 + if (get_current_app_id() == 0) { + return; + } + TaskContext* ctx = current_task_context; + if (ctx != nullptr) { + ctx->allocCount++; + ctx->allocBytes += static_cast(size); + } +} + +void app_memory_record_free(size_t size) { + if (get_current_app_id() == 0) { + return; + } + TaskContext* ctx = current_task_context; + if (ctx != nullptr) { + ctx->allocCount--; + ctx->allocBytes -= static_cast(size); + } +} + void app_scheduler_exit_current(int32_t status) { // Checked first: thread_local can't be read before the scheduler starts on ESP32 if (get_current_app_id() == 0) { diff --git a/Modules/app-module/source/signal.cpp b/Modules/app-module/source/signal.cpp new file mode 100644 index 000000000..96e0ffbbf --- /dev/null +++ b/Modules/app-module/source/signal.cpp @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include + +#include + +namespace { + +// Signals whose default action is to do nothing, so they never interrupt or end an app. +// There is no job control, so the stop signals are discarded like for an orphaned process group, +// and SIGCONT never finds a stopped app. +bool is_ignored_by_default(int sig) { + switch (sig) { + case SIGCHLD: + case SIGWINCH: + case SIGURG: + case SIGCONT: + case SIGTSTP: + case SIGTTIN: + case SIGTTOU: + return true; + default: + return false; + } +} + +bool is_ignored(AppLibcSignalHandler handler, int sig) { + if (handler == SIG_IGN) { + return true; + } + return handler == SIG_DFL && is_ignored_by_default(sig); +} + +} // namespace + +extern "C" { + +error_t app_signal_send(AppInstanceId app_instance_id, int sig) { + if (sig <= 0 || sig >= APP_LIBC_SIGNAL_COUNT) { + return ERROR_INVALID_ARGUMENT; + } + + if (app_manager_get_state(app_instance_id) == APP_INSTANCE_STATE_STOPPED) { + return ERROR_NOT_FOUND; + } + + AppEvent event {}; + if (sig == SIGTERM) { + event.type = APP_EVENT_CLOSE; + } else { + event.type = APP_EVENT_SIGNAL; + event.signal.sig = sig; + } + const error_t emit_result = app_event_emit(app_instance_id, &event); + if (emit_result != ERROR_NOT_FOUND) { + return emit_result; + } + + 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 ERROR_NOT_FOUND; + } + AppInstanceRecord& record = iterator->second; + if (is_ignored(record.signal_handlers[sig], sig)) { + mutex_unlock(&ledger.mutex); + return ERROR_NONE; + } + record.pending_signals |= 1u << sig; + mutex_unlock(&ledger.mutex); + return ERROR_NONE; +} + +bool app_signal_is_pending(void) { + const AppInstanceId app_instance_id = app_scheduler_current_app_id(); + if (app_instance_id == 0) { + return false; + } + auto& ledger = app_ledger(); + mutex_lock(&ledger.mutex); + auto iterator = ledger.instances.find(app_instance_id); + const bool pending = iterator != ledger.instances.end() && iterator->second.pending_signals != 0; + mutex_unlock(&ledger.mutex); + return pending; +} + +void app_signal_deliver_pending(void) { + const AppInstanceId app_instance_id = app_scheduler_current_app_id(); + if (app_instance_id == 0) { + return; + } + auto& ledger = app_ledger(); + while (true) { + mutex_lock(&ledger.mutex); + auto iterator = ledger.instances.find(app_instance_id); + if (iterator == ledger.instances.end() || iterator->second.pending_signals == 0) { + mutex_unlock(&ledger.mutex); + return; + } + AppInstanceRecord& record = iterator->second; + const int sig = __builtin_ctz(record.pending_signals); + record.pending_signals &= ~(1u << sig); + // Read at delivery rather than when sent, as the app may have changed it since + const AppLibcSignalHandler handler = record.signal_handlers[sig]; + mutex_unlock(&ledger.mutex); + + if (is_ignored(handler, sig)) { + continue; + } + if (handler == SIG_DFL) { + app_scheduler_exit_current(128 + sig); + return; + } + handler(sig); + } +} + +} // extern "C" diff --git a/Modules/app-module/source/stream.cpp b/Modules/app-module/source/stream.cpp index 861c26359..b8c9deaca 100644 --- a/Modules/app-module/source/stream.cpp +++ b/Modules/app-module/source/stream.cpp @@ -4,12 +4,18 @@ #include #include #include +#include #include #include +#include + namespace { +// Upper bound on how long a blocked read or write takes to notice a signal (see app/signal.h) +constexpr TickType_t SIGNAL_CHECK_INTERVAL_TICKS = pdMS_TO_TICKS(100); + // Caller must hold stream->mutex. bool is_readable_locked(AppStream* stream) { return stream->buffer.count > 0 || stream->closed; @@ -44,11 +50,28 @@ class StreamOperationGuard { AppStream* stream_; }; +/** + * Waits until @a stream is ready, or a signal is pending for the calling app (see app/signal.h). + * @return false when the wait failed, with errno EINTR when interrupted by a signal + */ +bool await_unless_signalled(AppStream* stream, AppFileWait wait) { + while (true) { + if (app_signal_is_pending()) { + errno = EINTR; + return false; + } + const error_t result = app_stream_await(stream, wait, SIGNAL_CHECK_INTERVAL_TICKS); + if (result != ERROR_TIMEOUT) { + return result == ERROR_NONE; + } + } +} + ssize_t stream_file_read(void* object, void* buffer, size_t size) { auto* stream = static_cast(object); StreamOperationGuard guard(stream); while (true) { - if (app_stream_await(stream, APP_FILE_WAIT_READABLE, portMAX_DELAY) != ERROR_NONE) { + if (!await_unless_signalled(stream, APP_FILE_WAIT_READABLE)) { return -1; } size_t read = app_stream_read(stream, buffer, size); @@ -69,7 +92,7 @@ ssize_t stream_file_read(void* object, void* buffer, size_t size) { ssize_t stream_file_write(void* object, const void* buffer, size_t size) { auto* stream = static_cast(object); StreamOperationGuard guard(stream); - if (app_stream_await(stream, APP_FILE_WAIT_WRITABLE, portMAX_DELAY) != ERROR_NONE) { + if (!await_unless_signalled(stream, APP_FILE_WAIT_WRITABLE)) { return -1; } // app_stream_write() itself refuses to copy anything once closed (checked under the same diff --git a/Modules/app-posix-module/private/app_posix/malloc_wrap.h b/Modules/app-posix-module/private/app_posix/malloc_wrap.h new file mode 100644 index 000000000..68d685bfa --- /dev/null +++ b/Modules/app-posix-module/private/app_posix/malloc_wrap.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifndef __APPLE__ + +/** + * Sets the address range of the app image running on the calling thread. Allocations made by code + * in [start, end) are counted for the current app instance (see app/memory.h). + */ +void app_posix_set_current_image(uintptr_t start, uintptr_t end); + +#else + +// Allocations aren't counted on Apple platforms +inline void app_posix_set_current_image(uintptr_t, uintptr_t) {} + +#endif diff --git a/Modules/app-posix-module/private/app_posix/stdio_wrap.h b/Modules/app-posix-module/private/app_posix/stdio_wrap.h index 11ee9017b..32997e9a8 100644 --- a/Modules/app-posix-module/private/app_posix/stdio_wrap.h +++ b/Modules/app-posix-module/private/app_posix/stdio_wrap.h @@ -11,6 +11,7 @@ #include #include +#include // Implemented in stdio_wrap.cpp, installed under the real names by stdio_wrap_elf.cpp or stdio_wrap_apple.cpp. @@ -27,6 +28,10 @@ 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); +pid_t __wrap_getpid(); +pid_t __wrap_getppid(); +int __wrap_usleep(useconds_t usec); +unsigned int __wrap_sleep(unsigned int seconds); [[noreturn]] void __wrap_exit(int status); int __wrap_vprintf(const char* format, va_list args); diff --git a/Modules/app-posix-module/source/app_posix_loader_service.cpp b/Modules/app-posix-module/source/app_posix_loader_service.cpp index 44b892524..7fe7e0a11 100644 --- a/Modules/app-posix-module/source/app_posix_loader_service.cpp +++ b/Modules/app-posix-module/source/app_posix_loader_service.cpp @@ -1,4 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include #include #include @@ -11,6 +13,10 @@ #include #include +#ifndef __APPLE__ +#include +#endif + #include #include @@ -21,8 +27,52 @@ namespace { /** load()-allocated state, passed back through run()/unload(). */ struct PosixAppRuntime { void* handle = nullptr; + // Address range of the loaded image, see app_posix_set_current_image() + uintptr_t image_start = 0; + uintptr_t image_end = 0; }; +#ifndef __APPLE__ +struct ImageRangeSearch { + ElfW(Addr) base; + uintptr_t start; + uintptr_t end; +}; + +int find_image_range(struct dl_phdr_info* info, size_t, void* context) { + auto* search = static_cast(context); + if (info->dlpi_addr != search->base) { + return 0; + } + for (ElfW(Half) i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr)& header = info->dlpi_phdr[i]; + if (header.p_type != PT_LOAD) { + continue; + } + const uintptr_t start = info->dlpi_addr + header.p_vaddr; + const uintptr_t end = start + header.p_memsz; + if (search->start == 0 || start < search->start) { + search->start = start; + } + if (end > search->end) { + search->end = end; + } + } + return 1; +} + +void resolve_image_range(PosixAppRuntime* runtime) { + struct link_map* map = nullptr; + if (dlinfo(runtime->handle, RTLD_DI_LINKMAP, &map) != 0 || map == nullptr) { + return; + } + ImageRangeSearch search { .base = map->l_addr, .start = 0, .end = 0 }; + dl_iterate_phdr(find_image_range, &search); + runtime->image_start = search.start; + runtime->image_end = search.end; +} +#endif + bool is_regular_file(const std::string& path) { struct stat path_stat {}; return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode); @@ -109,6 +159,10 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) { return ERROR_OUT_OF_MEMORY; } +#ifndef __APPLE__ + resolve_image_range(runtime); +#endif + *out_runtime = runtime; return ERROR_NONE; } @@ -126,7 +180,10 @@ int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, } auto* main_fn = reinterpret_cast(symbol); - return main_fn(argc, argv); + app_posix_set_current_image(runtime->image_start, runtime->image_end); + const int32_t result = main_fn(argc, argv); + app_posix_set_current_image(0, 0); + return result; } void api_unload(AppRuntime runtime_ptr) { diff --git a/Modules/app-posix-module/source/malloc_wrap.cpp b/Modules/app-posix-module/source/malloc_wrap.cpp new file mode 100644 index 000000000..ab1e34ec3 --- /dev/null +++ b/Modules/app-posix-module/source/malloc_wrap.cpp @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef __APPLE__ + +// Counts the allocations made by a dlopen()ed app's own code (see app/memory.h). A .so resolves +// malloc through the dynamic linker, so these replace the process-wide functions and forward to +// glibc's own allocator. A call is counted only when its caller lies inside the image of the app +// running on the calling thread. +#include + +#include + +#include + +#include +#include +#include + +extern "C" { +void* __libc_malloc(size_t size); +void* __libc_calloc(size_t count, size_t size); +void* __libc_realloc(void* ptr, size_t size); +void __libc_free(void* ptr); +} + +namespace { + +// initial-exec, since the default TLS model of position independent code can call malloc itself +__attribute__((tls_model("initial-exec"))) thread_local uintptr_t image_start = 0; +__attribute__((tls_model("initial-exec"))) thread_local uintptr_t image_end = 0; + +inline bool is_app_caller(void* caller) { + const auto address = reinterpret_cast(caller); + return address >= image_start && address < image_end; +} + +inline void* record_alloc(void* ptr, void* caller) { + if (ptr != nullptr && is_app_caller(caller)) { + app_memory_record_alloc(malloc_usable_size(ptr)); + } + return ptr; +} + +inline void record_free(void* ptr, void* caller) { + if (ptr != nullptr && is_app_caller(caller)) { + app_memory_record_free(malloc_usable_size(ptr)); + } +} + +void* new_or_throw(size_t size) { + while (true) { + void* ptr = __libc_malloc(size); + if (ptr != nullptr) { + return ptr; + } + std::new_handler handler = std::get_new_handler(); + if (handler == nullptr) { + // Required by operator new's contract, so callers in libraries behave as before +#if __cpp_exceptions + throw std::bad_alloc(); +#else + abort(); +#endif + } + handler(); + } +} + +} // namespace + +void app_posix_set_current_image(uintptr_t start, uintptr_t end) { + image_start = start; + image_end = end; +} + +extern "C" { + +void* malloc(size_t size) { + return record_alloc(__libc_malloc(size), __builtin_return_address(0)); +} + +void* calloc(size_t count, size_t size) { + return record_alloc(__libc_calloc(count, size), __builtin_return_address(0)); +} + +void* realloc(void* ptr, size_t size) { + void* caller = __builtin_return_address(0); + if (!is_app_caller(caller)) { + return __libc_realloc(ptr, size); + } + const size_t old_size = (ptr != nullptr) ? malloc_usable_size(ptr) : 0; + void* result = __libc_realloc(ptr, size); + // A failed realloc() leaves the old block allocated + if (result != nullptr || size == 0) { + if (ptr != nullptr) { + app_memory_record_free(old_size); + } + if (result != nullptr) { + app_memory_record_alloc(malloc_usable_size(result)); + } + } + return result; +} + +void free(void* ptr) { + record_free(ptr, __builtin_return_address(0)); + __libc_free(ptr); +} + +char* strdup(const char* s) { + const size_t length = strlen(s) + 1; + auto* copy = static_cast(__libc_malloc(length)); + if (copy != nullptr) { + memcpy(copy, s, length); + } + return static_cast(record_alloc(copy, __builtin_return_address(0))); +} + +char* strndup(const char* s, size_t n) { + const size_t length = strnlen(s, n); + auto* copy = static_cast(__libc_malloc(length + 1)); + if (copy != nullptr) { + memcpy(copy, s, length); + copy[length] = '\0'; + } + return static_cast(record_alloc(copy, __builtin_return_address(0))); +} + +} // extern "C" + +void* operator new(size_t size) { + return record_alloc(new_or_throw(size), __builtin_return_address(0)); +} + +void* operator new[](size_t size) { + return record_alloc(new_or_throw(size), __builtin_return_address(0)); +} + +void* operator new(size_t size, const std::nothrow_t&) noexcept { + return record_alloc(__libc_malloc(size), __builtin_return_address(0)); +} + +void* operator new[](size_t size, const std::nothrow_t&) noexcept { + return record_alloc(__libc_malloc(size), __builtin_return_address(0)); +} + +void operator delete(void* ptr) noexcept { + record_free(ptr, __builtin_return_address(0)); + __libc_free(ptr); +} + +void operator delete[](void* ptr) noexcept { + record_free(ptr, __builtin_return_address(0)); + __libc_free(ptr); +} + +void operator delete(void* ptr, size_t) noexcept { + record_free(ptr, __builtin_return_address(0)); + __libc_free(ptr); +} + +void operator delete[](void* ptr, size_t) noexcept { + record_free(ptr, __builtin_return_address(0)); + __libc_free(ptr); +} + +void operator delete(void* ptr, const std::nothrow_t&) noexcept { + record_free(ptr, __builtin_return_address(0)); + __libc_free(ptr); +} + +void operator delete[](void* ptr, const std::nothrow_t&) noexcept { + record_free(ptr, __builtin_return_address(0)); + __libc_free(ptr); +} + +#endif diff --git a/Modules/app-posix-module/source/stdio_wrap.cpp b/Modules/app-posix-module/source/stdio_wrap.cpp index da566209d..4a294fc6f 100644 --- a/Modules/app-posix-module/source/stdio_wrap.cpp +++ b/Modules/app-posix-module/source/stdio_wrap.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -83,18 +84,51 @@ int __real_kill(pid_t pid, int sig) { return real(pid, sig); } +pid_t __real_getpid() { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "getpid")); + return real(); +} + +pid_t __real_getppid() { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "getppid")); + return real(); +} + +int __real_usleep(useconds_t usec) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "usleep")); + return real(usec); +} + +unsigned int __real_sleep(unsigned int seconds) { + static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "sleep")); + return real(seconds); +} + [[noreturn]] void __real_exit(int status) { static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "exit")); real(status); __builtin_unreachable(); } +// Pending signals are delivered at the entry of these wraps, and again when a call was interrupted +// by one, since the app holds no lock of the system itself there. + ssize_t __wrap_read(int fd, void* buffer, size_t size) { - return app_io_read(fd, buffer, size); + app_signal_deliver_pending(); + const ssize_t result = app_io_read(fd, buffer, size); + if (result < 0 && errno == EINTR) { + app_signal_deliver_pending(); + } + return result; } ssize_t __wrap_write(int fd, const void* buffer, size_t size) { - return app_io_write(fd, buffer, size); + app_signal_deliver_pending(); + const ssize_t result = app_io_write(fd, buffer, size); + if (result < 0 && errno == EINTR) { + app_signal_deliver_pending(); + } + return result; } int __wrap_close(int fd) { @@ -139,8 +173,12 @@ int __wrap_fstat(int fd, struct stat* st) { } int __wrap_poll(struct pollfd* fds, nfds_t nfds, int timeout) { + app_signal_deliver_pending(); int result; if (app_libc_try_poll(fds, nfds, timeout, __real_poll, &result)) { + if (result < 0 && errno == EINTR) { + app_signal_deliver_pending(); + } return result; } return __real_poll(fds, nfds, timeout); @@ -175,11 +213,49 @@ AppLibcSignalHandler __wrap_signal(int sig, AppLibcSignalHandler handler) { int __wrap_kill(pid_t pid, int sig) { int result; if (app_libc_try_kill(pid, sig, &result)) { + // A signal sent to the app itself is delivered before kill() returns + app_signal_deliver_pending(); return result; } return __real_kill(pid, sig); } +pid_t __wrap_getpid() { + int result; + if (app_libc_try_getpid(&result)) { + return result; + } + return __real_getpid(); +} + +pid_t __wrap_getppid() { + int result; + if (app_libc_try_getppid(&result)) { + return result; + } + return __real_getppid(); +} + +int __wrap_usleep(useconds_t usec) { + app_signal_deliver_pending(); + int result; + if (app_libc_try_usleep(usec, &result)) { + app_signal_deliver_pending(); + return result; + } + return __real_usleep(usec); +} + +unsigned int __wrap_sleep(unsigned int seconds) { + app_signal_deliver_pending(); + unsigned int result; + if (app_libc_try_sleep(seconds, &result)) { + app_signal_deliver_pending(); + return result; + } + return __real_sleep(seconds); +} + // Called by an app, the real exit() would end the whole simulator void __wrap_exit(int status) { app_scheduler_exit_current(status); @@ -238,11 +314,15 @@ namespace { /** @return the number of bytes written, less than `size` if the fd stopped accepting data */ size_t writeAllTo(int fd, const void* data, size_t size) { + app_signal_deliver_pending(); const auto* bytes = static_cast(data); size_t remaining = size; while (remaining > 0) { ssize_t written = app_io_write(fd, bytes, remaining); if (written <= 0) { + if (written < 0 && errno == EINTR) { + app_signal_deliver_pending(); + } break; } bytes += written; @@ -274,7 +354,7 @@ int formatTo(int fd, const char* format, va_list args) { } int readOneFromStdin(char& out) { - return static_cast(app_io_read(STDIN_FILENO, &out, 1)); + return static_cast(__wrap_read(STDIN_FILENO, &out, 1)); } // The process' own streams, captured before anything can reassign stdin/stdout/stderr. A caller diff --git a/Modules/app-posix-module/source/stdio_wrap_apple.cpp b/Modules/app-posix-module/source/stdio_wrap_apple.cpp index ef4e79181..048e203cd 100644 --- a/Modules/app-posix-module/source/stdio_wrap_apple.cpp +++ b/Modules/app-posix-module/source/stdio_wrap_apple.cpp @@ -27,6 +27,10 @@ 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_getpid, getpid) +TT_DYLD_INTERPOSE(__wrap_getppid, getppid) +TT_DYLD_INTERPOSE(__wrap_usleep, usleep) +TT_DYLD_INTERPOSE(__wrap_sleep, sleep) TT_DYLD_INTERPOSE(__wrap_vprintf, vprintf) TT_DYLD_INTERPOSE(__wrap_printf, printf) diff --git a/Modules/app-posix-module/source/stdio_wrap_elf.cpp b/Modules/app-posix-module/source/stdio_wrap_elf.cpp index e2f1fa574..e2b70a1b3 100644 --- a/Modules/app-posix-module/source/stdio_wrap_elf.cpp +++ b/Modules/app-posix-module/source/stdio_wrap_elf.cpp @@ -59,6 +59,22 @@ int kill(pid_t pid, int sig) { return __wrap_kill(pid, sig); } +pid_t getpid() { + return __wrap_getpid(); +} + +pid_t getppid() { + return __wrap_getppid(); +} + +int usleep(useconds_t usec) { + return __wrap_usleep(usec); +} + +unsigned int sleep(unsigned int seconds) { + return __wrap_sleep(seconds); +} + void exit(int status) { __wrap_exit(status); } diff --git a/Modules/app-posix-module/tests/source/libc_test.cpp b/Modules/app-posix-module/tests/source/libc_test.cpp index b7e170007..87c07fd2b 100644 --- a/Modules/app-posix-module/tests/source/libc_test.cpp +++ b/Modules/app-posix-module/tests/source/libc_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -175,14 +176,14 @@ void test_signal_handler(int) { std::atomic g_signal_first_default { false }; std::atomic g_signal_returns_previous { false }; std::atomic g_signal_sigkill_rejected { false }; -std::atomic g_kill_enosys { false }; +std::atomic g_kill_sigstop_rejected { 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); + g_kill_sigstop_rejected.store(kill(getpid(), SIGSTOP) == -1 && errno == EINVAL, std::memory_order_release); return 0; } @@ -249,7 +250,7 @@ TEST_CASE("signal() in an app records handlers per app instance, and kill() neve 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); + g_kill_sigstop_rejected.store(false, std::memory_order_relaxed); AppManifest manifest { "test.libc.signal", "Signal", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(signal_app_main) } }; REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); @@ -263,7 +264,7 @@ TEST_CASE("signal() in an app records handlers per app instance, and kill() neve 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)); + CHECK(g_kill_sigstop_rejected.load(std::memory_order_acquire)); app_manager_remove("test.libc.signal"); } @@ -413,3 +414,281 @@ TEST_CASE("app_execute_for_result_with_streams pipes a child's plain printf() ca app_manager_stop(parent_id); app_manager_remove("test.app.execute.printf_parent"); } + +namespace { + +std::atomic g_signal_handled { 0 }; +std::atomic g_signal_app_blocked { false }; +std::atomic g_signal_call_result { -2 }; +std::atomic g_signal_call_errno { 0 }; +std::atomic g_signal_app_continued { false }; + +void record_signal_handler(int sig) { + g_signal_handled.store(sig, std::memory_order_release); +} + +void reset_signal_flags() { + g_signal_handled.store(0, std::memory_order_relaxed); + g_signal_app_blocked.store(false, std::memory_order_relaxed); + g_signal_call_result.store(-2, std::memory_order_relaxed); + g_signal_call_errno.store(0, std::memory_order_relaxed); + g_signal_app_continued.store(false, std::memory_order_relaxed); +} + +int32_t handled_signal_app_main(int, char*[]) { + signal(SIGUSR1, record_signal_handler); + g_signal_app_blocked.store(true, std::memory_order_release); + char c; + const ssize_t result = read(STDIN_FILENO, &c, 1); + g_signal_call_errno.store(errno, std::memory_order_release); + g_signal_call_result.store(static_cast(result), std::memory_order_release); + return 0; +} + +int32_t default_signal_app_main(int, char*[]) { + g_signal_app_blocked.store(true, std::memory_order_release); + char c; + read(STDIN_FILENO, &c, 1); + g_signal_app_continued.store(true, std::memory_order_release); + return 0; +} + +int32_t ignored_signal_app_main(int, char*[]) { + signal(SIGHUP, SIG_IGN); + g_signal_app_blocked.store(true, std::memory_order_release); + g_signal_call_result.store(usleep(300 * 1000), std::memory_order_release); + g_signal_app_continued.store(true, std::memory_order_release); + return 0; +} + +int32_t default_sleep_app_main(int, char*[]) { + g_signal_app_blocked.store(true, std::memory_order_release); + g_signal_call_result.store(usleep(300 * 1000), std::memory_order_release); + g_signal_app_continued.store(true, std::memory_order_release); + return 0; +} + +int32_t interrupted_sleep_app_main(int, char*[]) { + signal(SIGUSR1, record_signal_handler); + g_signal_app_blocked.store(true, std::memory_order_release); + const int result = usleep(10 * 1000 * 1000); + g_signal_call_errno.store(errno, std::memory_order_release); + g_signal_call_result.store(result, std::memory_order_release); + return 0; +} + +int32_t interrupted_poll_app_main(int, char*[]) { + signal(SIGUSR1, record_signal_handler); + g_signal_app_blocked.store(true, std::memory_order_release); + struct pollfd fd { STDIN_FILENO, POLLIN, 0 }; + const int result = poll(&fd, 1, -1); + g_signal_call_errno.store(errno, std::memory_order_release); + g_signal_call_result.store(result, std::memory_order_release); + return 0; +} + +std::atomic g_kill_probe_ok { false }; +std::atomic g_kill_missing_esrch { false }; +std::atomic g_kill_group_esrch { false }; +std::atomic g_getppid_top_level { false }; +std::atomic g_kill_self_handled_before_return { false }; + +int32_t kill_app_main(int, char*[]) { + signal(SIGUSR2, record_signal_handler); + g_kill_probe_ok.store(kill(getpid(), 0) == 0, std::memory_order_release); + g_kill_missing_esrch.store(kill(0x7FFFFFF0, SIGTERM) == -1 && errno == ESRCH, std::memory_order_release); + g_kill_group_esrch.store(kill(0, SIGTERM) == -1 && errno == ESRCH, std::memory_order_release); + g_getppid_top_level.store(getppid() == 0, std::memory_order_release); + kill(getpid(), SIGUSR2); + g_kill_self_handled_before_return.store(g_signal_handled.load(std::memory_order_acquire) == SIGUSR2, std::memory_order_release); + return 0; +} + +std::atomic g_event_signal { 0 }; +std::atomic g_event_closed { false }; + +int32_t evented_signal_app_main(int, char*[]) { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; + app_event_subscribe(&sub, &event_group); + g_signal_app_blocked.store(true, std::memory_order_release); + bool closed = false; + while (!closed) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_SIGNAL) { + g_event_signal.store(event.signal.sig, std::memory_order_release); + } else if (event.type == APP_EVENT_CLOSE) { + closed = true; + } + } + } + g_event_closed.store(true, std::memory_order_release); + app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); + return 0; +} + +/** Runs @a app_main with a stdin that is never written to, so a read() of it blocks. */ +struct BlockedStdinApp { + TaskEventGroup event_group {}; + uint8_t storage[16] {}; + AppStream stdin_stream {}; + AppInstanceId id = 0; + const char* manifest_id; + AppManifest manifest { "", "Signal", APP_CATEGORY_USER, {} }; + + BlockedStdinApp(const char* manifestId, int32_t (*app_main)(int, char*[])) : manifest_id(manifestId) { + ensure_memory_loader_registered(); + reset_signal_flags(); + strncpy(manifest.id, manifestId, sizeof(manifest.id) - 1); + manifest.location = { APP_LOCATION_MEMORY, reinterpret_cast(app_main) }; + REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); + task_event_group_construct(&event_group); + AppStreamBinding binding { STDIN_FILENO, &stdin_stream, storage, sizeof(storage), &event_group, {}, -1 }; + AppStartContext context; + REQUIRE_EQ(app_start_context_from_id(manifest_id, &context), ERROR_NONE); + app_start_context_set_streams(&context, &binding, 1); + REQUIRE_EQ(app_start_with_context(&context, &id), ERROR_NONE); + REQUIRE(wait_for_flag(g_signal_app_blocked, 1000)); + // Lets the app reach the blocking call after raising its flag + delay_millis(50); + } + + ~BlockedStdinApp() { + app_manager_stop(id); + app_stream_unsubscribe(&stdin_stream); + task_event_group_destruct(&event_group); + app_manager_remove(manifest_id); + } +}; + +} // namespace + +TEST_CASE("A signal with a handler interrupts a blocked read() with EINTR and calls the handler") { + BlockedStdinApp app("test.libc.signal_handled", handled_signal_app_main); + + CHECK_EQ(app_signal_send(app.id, SIGUSR1), ERROR_NONE); + + REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000)); + CHECK_EQ(g_signal_handled.load(std::memory_order_acquire), SIGUSR1); + CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), -1); + CHECK_EQ(g_signal_call_errno.load(std::memory_order_acquire), EINTR); +} + +TEST_CASE("A signal without a handler ends an app blocked in read()") { + BlockedStdinApp app("test.libc.signal_default", default_signal_app_main); + + CHECK_EQ(app_signal_send(app.id, SIGHUP), ERROR_NONE); + + REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000)); + CHECK_FALSE(g_signal_app_continued.load(std::memory_order_acquire)); +} + +TEST_CASE("app_manager_stop() ends an app without an event subscription via SIGTERM") { + BlockedStdinApp app("test.libc.signal_stop", default_signal_app_main); + + CHECK_EQ(app_manager_stop(app.id), ERROR_NONE); + + CHECK_EQ(app_manager_get_state(app.id), APP_INSTANCE_STATE_STOPPED); + CHECK_FALSE(g_signal_app_continued.load(std::memory_order_acquire)); +} + +TEST_CASE("An ignored signal does not interrupt the app") { + BlockedStdinApp app("test.libc.signal_ignored", ignored_signal_app_main); + + CHECK_EQ(app_signal_send(app.id, SIGHUP), ERROR_NONE); + + REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000)); + CHECK(g_signal_app_continued.load(std::memory_order_acquire)); + CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), 0); +} + +TEST_CASE("SIGCONT and the stop signals without a handler neither interrupt nor end the app") { + BlockedStdinApp app("test.libc.signal_job_control", default_sleep_app_main); + + CHECK_EQ(app_signal_send(app.id, SIGCONT), ERROR_NONE); + CHECK_EQ(app_signal_send(app.id, SIGTSTP), ERROR_NONE); + CHECK_EQ(app_signal_send(app.id, SIGTTIN), ERROR_NONE); + CHECK_EQ(app_signal_send(app.id, SIGTTOU), ERROR_NONE); + + REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000)); + CHECK(g_signal_app_continued.load(std::memory_order_acquire)); + CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), 0); +} + +TEST_CASE("A signal interrupts usleep() with EINTR") { + BlockedStdinApp app("test.libc.signal_sleep", interrupted_sleep_app_main); + + CHECK_EQ(app_signal_send(app.id, SIGUSR1), ERROR_NONE); + + REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000)); + CHECK_EQ(g_signal_handled.load(std::memory_order_acquire), SIGUSR1); + CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), -1); + CHECK_EQ(g_signal_call_errno.load(std::memory_order_acquire), EINTR); +} + +TEST_CASE("A signal interrupts poll() without a timeout with EINTR") { + BlockedStdinApp app("test.libc.signal_poll", interrupted_poll_app_main); + + CHECK_EQ(app_signal_send(app.id, SIGUSR1), ERROR_NONE); + + REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000)); + CHECK_EQ(g_signal_handled.load(std::memory_order_acquire), SIGUSR1); + CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), -1); + CHECK_EQ(g_signal_call_errno.load(std::memory_order_acquire), EINTR); +} + +TEST_CASE("An app with an event subscription receives signals as events") { + g_event_signal.store(0, std::memory_order_relaxed); + g_event_closed.store(false, std::memory_order_relaxed); + BlockedStdinApp app("test.libc.signal_evented", evented_signal_app_main); + + CHECK_EQ(app_signal_send(app.id, SIGUSR1), ERROR_NONE); + uint32_t waited = 0; + while (g_event_signal.load(std::memory_order_acquire) == 0 && waited < 1000) { + delay_millis(10); + waited += 10; + } + CHECK_EQ(g_event_signal.load(std::memory_order_acquire), SIGUSR1); + CHECK_FALSE(g_event_closed.load(std::memory_order_acquire)); + + CHECK_EQ(app_signal_send(app.id, SIGTERM), ERROR_NONE); + REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000)); + CHECK(g_event_closed.load(std::memory_order_acquire)); +} + +TEST_CASE("kill() and getpid() address app instances") { + ensure_memory_loader_registered(); + reset_signal_flags(); + g_kill_probe_ok.store(false, std::memory_order_relaxed); + g_kill_missing_esrch.store(false, std::memory_order_relaxed); + g_kill_group_esrch.store(false, std::memory_order_relaxed); + g_getppid_top_level.store(false, std::memory_order_relaxed); + g_kill_self_handled_before_return.store(false, std::memory_order_relaxed); + + AppManifest manifest { "test.libc.kill", "Kill", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(kill_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.kill", &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_kill_probe_ok.load(std::memory_order_acquire)); + CHECK(g_kill_missing_esrch.load(std::memory_order_acquire)); + CHECK(g_kill_group_esrch.load(std::memory_order_acquire)); + CHECK(g_getppid_top_level.load(std::memory_order_acquire)); + CHECK(g_kill_self_handled_before_return.load(std::memory_order_acquire)); + + app_manager_remove("test.libc.kill"); +} + +TEST_CASE("app_signal_send() rejects an out-of-range signal and an unknown app") { + CHECK_EQ(app_signal_send(1, 0), ERROR_INVALID_ARGUMENT); + CHECK_EQ(app_signal_send(1, 32), ERROR_INVALID_ARGUMENT); + CHECK_EQ(app_signal_send(0x7FFFFFF0, SIGTERM), ERROR_NOT_FOUND); +} diff --git a/Tactility/Private/Tactility/app/terminal/KeyboardInput.h b/Tactility/Private/Tactility/app/terminal/KeyboardInput.h index 878477303..beda05d39 100644 --- a/Tactility/Private/Tactility/app/terminal/KeyboardInput.h +++ b/Tactility/Private/Tactility/app/terminal/KeyboardInput.h @@ -21,6 +21,12 @@ class KeyboardInput { /** @return true if onKey returned true for any key read */ bool pump(KeyHandler onKey); + /** + * While exclusive, LVGL's own indevs for these keyboards are disabled, since they would + * otherwise consume keys from the same devices. No effect while LVGL is not running. + */ + void setExclusive(bool exclusive); + private: static constexpr uint32_t REFRESH_INTERVAL_MS = 1000; @@ -28,7 +34,9 @@ class KeyboardInput { void refresh(); void rescan(); + void applyExclusive(); std::vector devices_; TickType_t lastRefresh_ = 0; + bool exclusive_ = false; }; diff --git a/Tactility/Private/Tactility/app/terminal/Terminal.h b/Tactility/Private/Tactility/app/terminal/Terminal.h index f462b63b6..7dd5dc7d2 100644 --- a/Tactility/Private/Tactility/app/terminal/Terminal.h +++ b/Tactility/Private/Tactility/app/terminal/Terminal.h @@ -1,9 +1,13 @@ #pragma once struct Device; +class TerminalRenderer; /** - * Runs the terminal until the user exits. Must be called after module_stop(&lvgl_module), since it - * drives the display and keyboards directly. Tracks keyboards and the touch-to-exit gesture itself. + * Runs the terminal until the user exits. Unless the renderer is a TerminalRendererLvgl, must be + * called after module_stop(&lvgl_module), since it then drives the display directly. Tracks + * keyboards itself. + * @param[in] renderer not yet begun, begun and ended by this call + * @param[in] touchToExit whether touching the screen exits a keyboard-less terminal */ -void runTerminal(struct Device* display); +void runTerminal(struct Device* display, TerminalRenderer& renderer, bool touchToExit); diff --git a/Tactility/Private/Tactility/app/terminal/TerminalRenderer.h b/Tactility/Private/Tactility/app/terminal/TerminalRenderer.h index 6c2d2dbba..b1552a261 100644 --- a/Tactility/Private/Tactility/app/terminal/TerminalRenderer.h +++ b/Tactility/Private/Tactility/app/terminal/TerminalRenderer.h @@ -3,6 +3,8 @@ #include #include +#include + struct Device; struct PixelBuffer; @@ -18,7 +20,8 @@ struct PixelBuffer; * per-row push would show a mix of stale and fresh rows, since a hw double buffer's zero-copy * path flips on any write regardless of the sub-rect given. * - * Must be used only while LVGL is stopped, since it writes to the display directly. + * Must be used only while LVGL is stopped, since it writes to the display directly. The exception + * is TerminalRendererLvgl, which draws into an LVGL canvas instead. */ class TerminalRenderer { public: @@ -40,6 +43,9 @@ class TerminalRenderer { int columns() const { return cols; } int rows() const { return rowCount; } + /** False while the terminal is not on screen, e.g. when another app's window covers it. */ + virtual bool isShown() const { return true; } + protected: void paintCell(int row, int col, char ch, uint8_t attr); void paintCursor(int row, int col); @@ -57,6 +63,9 @@ class TerminalRenderer { */ bool allocateCommon(Device* display); + /** Like allocateCommon(Device*), but with frameBuffer in the given format instead of the display's. */ + bool allocateCommon(Device* display, enum DisplayColorFormat frameColorFormat); + /** Frees frameBuffer, fullFrameBuffer, shadow, and the borrowed hw double buffer, if any. */ void freeCommon(); diff --git a/Tactility/Private/Tactility/app/terminal/TerminalRendererLvgl.h b/Tactility/Private/Tactility/app/terminal/TerminalRendererLvgl.h new file mode 100644 index 000000000..8e63aeb39 --- /dev/null +++ b/Tactility/Private/Tactility/app/terminal/TerminalRendererLvgl.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include + +#include + +/** + * Draws the terminal into an LVGL canvas, so it can live in a regular window while LVGL keeps + * running. The frame is kept in a canvas-sized buffer that survives the canvas itself being deleted + * and recreated by the window manager. + */ +class TerminalRendererLvgl : public TerminalRenderer { +public: + ~TerminalRendererLvgl() override; + + /** Sizes the grid to the canvas given to attachCanvas(), which must happen first. */ + bool begin(Device* display) override; + + /** Releases all resources and deletes the attached canvas, since it would still show the freed buffer. */ + void end() override; + + bool isShown() const override { return canvas_ != nullptr; } + + /** + * Shows the frame on the given canvas, or detaches from it when nullptr. + * @warning Caller must hold the LVGL lock. + */ + void attachCanvas(lv_obj_t* canvas); + +protected: + void present(int yStart, int yEnd) override; + +private: + // Owned by the LVGL lock, but read without it by isShown(). + std::atomic canvas_ = nullptr; + + // The whole frame, as shown by the canvas. + PixelBuffer* canvasBuffer_ = nullptr; +}; diff --git a/Tactility/Source/app/shell/Run.cpp b/Tactility/Source/app/shell/Run.cpp index e3eb8f6fe..fcdcdc52f 100644 --- a/Tactility/Source/app/shell/Run.cpp +++ b/Tactility/Source/app/shell/Run.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ #include #include +#include #include #include #include @@ -215,6 +217,8 @@ int runApp(AppStartContext& context) { } else { // Our own stdin hung up (touch-to-exit): propagate to the child the same way. app_stream_close(&stdinStream); + // A child that doesn't stop at the end of its input, like an editor, stops at the hangup + app_signal_send(childId, SIGHUP); ownStdinClosed = true; } } diff --git a/Tactility/Source/app/terminal/KeyboardInput.cpp b/Tactility/Source/app/terminal/KeyboardInput.cpp index 4da717a5e..368f1120b 100644 --- a/Tactility/Source/app/terminal/KeyboardInput.cpp +++ b/Tactility/Source/app/terminal/KeyboardInput.cpp @@ -1,5 +1,8 @@ #include +#include +#include + #include constexpr auto* TAG = "terminal-keyb"; @@ -9,6 +12,7 @@ KeyboardInput::KeyboardInput() { } KeyboardInput::~KeyboardInput() { + setExclusive(false); for (Device* device : devices_) { device_put(device); } @@ -39,6 +43,28 @@ bool KeyboardInput::pump(KeyHandler onKey) { return handled; } +void KeyboardInput::setExclusive(bool exclusive) { + if (exclusive == exclusive_) { + return; + } + exclusive_ = exclusive; + applyExclusive(); +} + +void KeyboardInput::applyExclusive() { + if (!lvgl_is_running()) { + return; + } + lvgl_lock(); + for (Device* device : devices_) { + lv_indev_t* indev = lvgl_keyboard_find_by_device(device); + if (indev != nullptr) { + lv_indev_enable(indev, !exclusive_); + } + } + lvgl_unlock(); +} + bool KeyboardInput::collect(Device* device, void* context) { static_cast*>(context)->push_back(device); return true; @@ -76,4 +102,9 @@ void KeyboardInput::rescan() { ++it; } } + + // A keyboard connected since the last scan gets an enabled LVGL indev of its own. + if (exclusive_) { + applyExclusive(); + } } diff --git a/Tactility/Source/app/terminal/Shell.cpp b/Tactility/Source/app/terminal/Shell.cpp index ed9c50f86..f8bd2dfe3 100644 --- a/Tactility/Source/app/terminal/Shell.cpp +++ b/Tactility/Source/app/terminal/Shell.cpp @@ -119,6 +119,8 @@ void runShell(int columns, int rows, volatile bool* stopRequested, TaskHandle_t while (app_event_poll(&eventSub, &event) == ERROR_NONE) { if (event.type == APP_EVENT_RESULT && event.result.launch_id == shellId) { shellDone = true; + } else if (event.type == APP_EVENT_CLOSE) { + *stopRequested = true; } } } diff --git a/Tactility/Source/app/terminal/Terminal.cpp b/Tactility/Source/app/terminal/Terminal.cpp index 0bbec14cb..c4c85591c 100644 --- a/Tactility/Source/app/terminal/Terminal.cpp +++ b/Tactility/Source/app/terminal/Terminal.cpp @@ -3,8 +3,6 @@ #include #include #include -#include -#include #include #include @@ -46,7 +44,7 @@ constexpr int SCROLL_STEP_LINES = 5; namespace { -/** Set once the shell should wind down (touch-to-exit; see ioTask()). Read by runTerminal()'s own +/** Set once the shell should wind down (touch-to-exit in ioTask(), or APP_EVENT_CLOSE in runShell()). Read by runTerminal()'s own * pump loop, which closes the shell app's stdin to unstick it (see that loop's own comment). */ volatile bool stopRequested = false; @@ -160,12 +158,16 @@ void ioTask(void* arg) { auto* params = static_cast(arg); while (!shellFinished) { + // Keys belong to whichever window is on top, so they are left alone while that is not this one. + const bool shown = params->renderer->isShown(); + params->keyboards->setExclusive(shown); + // A scroll replaces every row at once, so the renderer is told to repaint rather than rely // on its per-cell comparison. - const bool viewMoved = params->keyboards->pump(handleKey); + const bool viewMoved = shown && params->keyboards->pump(handleKey); params->renderer->render(viewMoved); - if (!stopRequested && params->touch->touched(!params->keyboards->empty())) { + if (!stopRequested && params->touch != nullptr && params->touch->touched(!params->keyboards->empty())) { LOG_I(TAG, "Touch detected - stopping"); stopRequested = true; } @@ -183,7 +185,7 @@ void ioTask(void* arg) { } // namespace -void runTerminal(Device* display) { +void runTerminal(Device* display, TerminalRenderer& renderer, bool touchToExit) { stopRequested = false; shellFinished = false; @@ -195,11 +197,6 @@ void runTerminal(Device* display) { return; } - TerminalRendererPpa ppaRenderer; - TerminalRendererGeneric genericRenderer; - TerminalRenderer& renderer = TerminalRendererPpa::isSupported() - ? static_cast(ppaRenderer) - : static_cast(genericRenderer); if (!renderer.begin(display)) { LOG_E(TAG, "Renderer failed to start"); vterm_deinit(); @@ -221,7 +218,7 @@ void runTerminal(Device* display) { // working while this one is blocked draining the shell app. See ioTask(). IoTaskParams ioParams { .keyboards = &keyboards, - .touch = &touch, + .touch = touchToExit ? &touch : nullptr, .renderer = &renderer, .doneSem = xSemaphoreCreateBinary(), }; diff --git a/Tactility/Source/app/terminal/TerminalRenderer.cpp b/Tactility/Source/app/terminal/TerminalRenderer.cpp index 68b3fbc30..7e7825ed3 100644 --- a/Tactility/Source/app/terminal/TerminalRenderer.cpp +++ b/Tactility/Source/app/terminal/TerminalRenderer.cpp @@ -41,6 +41,10 @@ inline uint16_t paletteColour(uint8_t index) { } // namespace bool TerminalRenderer::allocateCommon(Device* displayDevice) { + return allocateCommon(displayDevice, display_get_color_format(displayDevice)); +} + +bool TerminalRenderer::allocateCommon(Device* displayDevice, enum DisplayColorFormat frameColorFormat) { display = displayDevice; monochrome = display_get_color_format(display) == DISPLAY_COLOR_FORMAT_MONOCHROME; @@ -65,8 +69,7 @@ bool TerminalRenderer::allocateCommon(Device* displayDevice) { originX = (frameWidth - cols * cellWidth) / 2; originY = (frameHeight - rowCount * cellHeight) / 2; - const enum DisplayColorFormat colorFormat = display_get_color_format(display); - frameBuffer = pixel_buffer_create(colorFormat, frameWidth, cellHeight); + frameBuffer = pixel_buffer_create(frameColorFormat, frameWidth, cellHeight); if (frameBuffer == nullptr) { LOG_E(TAG, "Failed to allocate frame buffer"); return false; diff --git a/Tactility/Source/app/terminal/TerminalRendererLvgl.cpp b/Tactility/Source/app/terminal/TerminalRendererLvgl.cpp new file mode 100644 index 000000000..3cf678f5f --- /dev/null +++ b/Tactility/Source/app/terminal/TerminalRendererLvgl.cpp @@ -0,0 +1,91 @@ +#include + +#include + +#include + +#include + +constexpr auto* TAG = "TermRenderLvgl"; + +TerminalRendererLvgl::~TerminalRendererLvgl() { + end(); +} + +bool TerminalRendererLvgl::begin(Device* displayDevice) { + lvgl_lock(); + lv_obj_t* canvas = canvas_; + if (canvas == nullptr) { + lvgl_unlock(); + LOG_E(TAG, "No canvas attached"); + return false; + } + lv_obj_update_layout(canvas); + frameWidth = lv_obj_get_width(canvas); + frameHeight = lv_obj_get_height(canvas); + lvgl_unlock(); + + panelWidth = frameWidth; + panelHeight = frameHeight; + + // LVGL converts to the display's own format when flushing. + if (!allocateCommon(displayDevice, DISPLAY_COLOR_FORMAT_RGB565)) { + return false; + } + + canvasBuffer_ = pixel_buffer_create(DISPLAY_COLOR_FORMAT_RGB565, frameWidth, frameHeight); + if (canvasBuffer_ == nullptr) { + LOG_E(TAG, "Failed to allocate canvas buffer"); + freeCommon(); + return false; + } + + clearPanelOnce(); + + lvgl_lock(); + attachCanvas(canvas_); + lvgl_unlock(); + + LOG_I(TAG, "Terminal %dx%d cells (%dx%d px) on %dx%d canvas", + cols, rowCount, cellWidth, cellHeight, frameWidth, frameHeight); + return true; +} + +void TerminalRendererLvgl::end() { + if (canvasBuffer_ != nullptr) { + lvgl_lock(); + lv_obj_t* canvas = canvas_; + if (canvas != nullptr) { + lv_obj_delete(canvas); + canvas_ = nullptr; + } + lvgl_unlock(); + + pixel_buffer_free(canvasBuffer_); + canvasBuffer_ = nullptr; + } + freeCommon(); +} + +void TerminalRendererLvgl::attachCanvas(lv_obj_t* canvas) { + canvas_ = canvas; + if (canvas != nullptr && canvasBuffer_ != nullptr) { + lv_canvas_set_buffer(canvas, pixel_buffer_get_data(canvasBuffer_), frameWidth, frameHeight, LV_COLOR_FORMAT_RGB565); + } +} + +void TerminalRendererLvgl::present(int yStart, int yEnd) { + // Locked during the blit too, as LVGL may be drawing the canvas from this buffer + lvgl_lock(); + pixel_buffer_blit(canvasBuffer_, 0, yStart, frameBuffer, 0, 0, frameWidth, yEnd - yStart, PIXEL_BUFFER_CONVERSION_EXACT_BLACK); + + lv_obj_t* canvas = canvas_; + if (canvas != nullptr) { + lv_area_t area; + lv_obj_get_coords(canvas, &area); + area.y2 = area.y1 + yEnd - 1; + area.y1 += yStart; + lv_obj_invalidate_area(canvas, &area); + } + lvgl_unlock(); +} diff --git a/Tactility/Source/app/terminal/main.cpp b/Tactility/Source/app/terminal/main.cpp index 6d66f2a32..2bcc86944 100644 --- a/Tactility/Source/app/terminal/main.cpp +++ b/Tactility/Source/app/terminal/main.cpp @@ -2,6 +2,9 @@ #include +#include +#include +#include #include @@ -14,33 +17,107 @@ #include #include #include +#include +#include + +#include constexpr auto* TAG = "terminal"; namespace tt::app::terminal { -int main(int argc, char* argv[]) { - Device* display_device = nullptr; - if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) { - LOG_E(TAG, "No display device found"); - return 0; +namespace { + +struct WindowContext { + TerminalRendererLvgl renderer; + AppInstanceId appInstanceId; +}; + +// The toolbar's default action closes the topmost app instance, which is the shell app rather than this one. +void onClosePressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + app_event_emit_close(ctx->appInstanceId); +} + +void createWidgets(lv_obj_t* root, void* userData) { + auto* ctx = static_cast(userData); + + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(root, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(root, 0, LV_STATE_DEFAULT); + + auto* toolbar = lvgl_toolbar_create(root, "Terminal"); + lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClosePressed, ctx); + + auto* canvas = lv_canvas_create(root); + lv_obj_set_width(canvas, LV_PCT(100)); + lv_obj_set_flex_grow(canvas, 1); + + ctx->renderer.attachCanvas(canvas); +} + +void destroyWidgets(void* userData) { + static_cast(userData)->renderer.attachCanvas(nullptr); +} + +bool hasArgument(int argc, char* argv[], const char* argument) { + for (int i = 0; i < argc; i++) { + if (strcmp(argv[i], argument) == 0) { + return true; + } } + return false; +} + +void runInWindow(Device* display) { + WindowContext ctx { .appInstanceId = app_scheduler_current_app_id() }; + WindowId window = window_manager_create_ext(ctx.appInstanceId, createWidgets, destroyWidgets, &ctx); + runTerminal(display, ctx.renderer, false); + window_manager_remove(window); +} +void runFullscreen(Device* display) { // Stop LVGL, because terminal has custom rendering auto lvgl_active = module_is_started(&lvgl_module); if (lvgl_active) { module_stop(&lvgl_module); } - runTerminal(display_device); - - device_put(display_device); + TerminalRendererPpa ppaRenderer; + TerminalRendererGeneric genericRenderer; + TerminalRenderer& renderer = TerminalRendererPpa::isSupported() + ? static_cast(ppaRenderer) + : static_cast(genericRenderer); + runTerminal(display, renderer, true); // If needed, restart LVGL if (lvgl_active && !module_is_started(&lvgl_module)) { LOG_I(TAG, "Restarting LVGL"); module_start(&lvgl_module); } +} + +} // namespace + +int main(int argc, char* argv[]) { + Device* display_device = nullptr; + if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) { + LOG_E(TAG, "No display device found"); + return 0; + } + + // The canvas holds a full frame, which needs external RAM on larger displays. + const bool useLvgl = module_is_started(&lvgl_module) && + memory_external_total() > 0 && + !hasArgument(argc, argv, "--no-lvgl"); + + if (useLvgl) { + runInWindow(display_device); + } else { + runFullscreen(display_device); + } + + device_put(display_device); return 0; }