LVGL terminal rendering, elf memleak fixes and posix support (#669)

This commit is contained in:
Ken Van Hoeylandt 2026-10-03 19:02:07 +02:00 committed by GitHub
parent ffab897fe4
commit 34ca63ec3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1557 additions and 58 deletions

View File

@ -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 <ip>`
- 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

View File

@ -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 {

View File

@ -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;
}

View File

@ -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 <app/libc.h>
#include <app/memory.h>
#include <app/signal.h>
#include <esp_heap_caps.h>
#include <tactility/module.h>
#include <sys/poll.h>
#include <unistd.h>
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <new>
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<char*>(record_alloc(strdup(s)));
}
char* app_strndup(const char* s, size_t n) {
return static_cast<char*>(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<void*>(app_read) },
{ "write", reinterpret_cast<void*>(app_write) },
{ "poll", reinterpret_cast<void*>(app_poll) },
{ "usleep", reinterpret_cast<void*>(app_usleep) },
{ "sleep", reinterpret_cast<void*>(app_sleep) },
{ "kill", reinterpret_cast<void*>(app_kill) },
{ "getpid", reinterpret_cast<void*>(app_getpid) },
{ "getppid", reinterpret_cast<void*>(app_getppid) },
{ "malloc", reinterpret_cast<void*>(app_malloc) },
{ "calloc", reinterpret_cast<void*>(app_calloc) },
{ "realloc", reinterpret_cast<void*>(app_realloc) },
{ "free", reinterpret_cast<void*>(app_free) },
{ "strdup", reinterpret_cast<void*>(app_strdup) },
{ "strndup", reinterpret_cast<void*>(app_strndup) },
{ "_Znwj", reinterpret_cast<void*>(app_operator_new) }, // operator new(unsigned int)
{ "_Znaj", reinterpret_cast<void*>(app_operator_new_array) }, // operator new[](unsigned int)
{ "_ZdlPv", reinterpret_cast<void*>(app_operator_delete) }, // operator delete(void*)
{ "_ZdaPv", reinterpret_cast<void*>(app_operator_delete_array) }, // operator delete[](void*)
{ "_ZdlPvj", reinterpret_cast<void*>(app_operator_delete_sized) }, // operator delete(void*, unsigned int)
{ "_ZdaPvj", reinterpret_cast<void*>(app_operator_delete_array_sized) }, // operator delete[](void*, unsigned int)
MODULE_SYMBOL_TERMINATOR
};
}

View File

@ -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,
};

View File

@ -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;
};
};
/**

View File

@ -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);

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#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

View File

@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "instance.h"
#include <stdbool.h>
#include <tactility/error.h>
#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

View File

@ -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;
};

View File

@ -3,8 +3,10 @@
#include <app/dir.h>
#include <app/io.h>
#include <app/manager.h>
#include <app/private/ledger.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <tactility/delay.h>
#include <tactility/paths.h>
@ -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<AppInstanceId>(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<AppInstanceId>(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<int>(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<int>(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<TickType_t>((static_cast<uint64_t>(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<TickType_t>(static_cast<uint64_t>(seconds) * configTICK_RATE_HZ), &remaining);
*out_result = static_cast<unsigned int>(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);
}

View File

@ -7,9 +7,11 @@
#include <app/io.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/memory.h>
#include <app/package_manifest.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <app/start.h>
#include <app/stream.h>
@ -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),

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/instance.h>
#include <app/loader.h>
#include <app/memory.h>
#include <app/private/arguments.h>
#include <app/private/event.h>
#include <app/private/fd_table.h>
@ -8,6 +9,7 @@
#include <app/private/scheduler.h>
#include <app/private/stream_internal.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <app/stream.h>
#include <service/instance.h>
@ -20,6 +22,7 @@
#include <tactility/time.h>
#include <atomic>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <new>
@ -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<long>(ctx->allocCount), static_cast<long long>(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<int64_t>(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<int64_t>(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) {

View File

@ -0,0 +1,125 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/signal.h>
#include <app/libc.h>
#include <app/manager.h>
#include <app/private/event.h>
#include <app/private/ledger.h>
#include <app/scheduler.h>
#include <signal.h>
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"

View File

@ -4,12 +4,18 @@
#include <app/private/fd_table.h>
#include <app/private/ledger.h>
#include <app/private/stream_internal.h>
#include <app/signal.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
#include <cerrno>
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 @@ private:
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<AppStream*>(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<AppStream*>(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

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <cstdint>
#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

View File

@ -11,6 +11,7 @@
#include <signal.h>
#include <termios.h>
#include <unistd.h>
// 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);

View File

@ -1,4 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#include <app_posix/malloc_wrap.h>
#include <app/elf_check.h>
#include <app/loader.h>
#include <app/location.h>
@ -11,6 +13,10 @@
#include <dlfcn.h>
#include <sys/stat.h>
#ifndef __APPLE__
#include <link.h>
#endif
#include <new>
#include <string>
@ -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<ImageRangeSearch*>(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<AppMainFn>(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) {

View File

@ -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 <app_posix/malloc_wrap.h>
#include <app/memory.h>
#include <malloc.h>
#include <cstdlib>
#include <cstring>
#include <new>
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<uintptr_t>(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<char*>(__libc_malloc(length));
if (copy != nullptr) {
memcpy(copy, s, length);
}
return static_cast<char*>(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<char*>(__libc_malloc(length + 1));
if (copy != nullptr) {
memcpy(copy, s, length);
copy[length] = '\0';
}
return static_cast<char*>(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

View File

@ -10,6 +10,7 @@
#include <app/io.h>
#include <app/libc.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <cerrno>
#include <cstring>
@ -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<pid_t (*)()>(dlsym(RTLD_NEXT, "getpid"));
return real();
}
pid_t __real_getppid() {
static auto real = reinterpret_cast<pid_t (*)()>(dlsym(RTLD_NEXT, "getppid"));
return real();
}
int __real_usleep(useconds_t usec) {
static auto real = reinterpret_cast<int (*)(useconds_t)>(dlsym(RTLD_NEXT, "usleep"));
return real(usec);
}
unsigned int __real_sleep(unsigned int seconds) {
static auto real = reinterpret_cast<unsigned int (*)(unsigned int)>(dlsym(RTLD_NEXT, "sleep"));
return real(seconds);
}
[[noreturn]] void __real_exit(int status) {
static auto real = reinterpret_cast<void (*)(int)>(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<const char*>(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<int>(app_io_read(STDIN_FILENO, &out, 1));
return static_cast<int>(__wrap_read(STDIN_FILENO, &out, 1));
}
// The process' own streams, captured before anything can reassign stdin/stdout/stderr. A caller

View File

@ -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)

View File

@ -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);
}

View File

@ -8,6 +8,7 @@
#include <app/loader.h>
#include <app/manager.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <app/start.h>
#include <app/stream.h>
@ -175,14 +176,14 @@ 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 };
std::atomic<bool> 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<void*>(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<int> g_signal_handled { 0 };
std::atomic<bool> g_signal_app_blocked { false };
std::atomic<int> g_signal_call_result { -2 };
std::atomic<int> g_signal_call_errno { 0 };
std::atomic<bool> 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<int>(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<bool> g_kill_probe_ok { false };
std::atomic<bool> g_kill_missing_esrch { false };
std::atomic<bool> g_kill_group_esrch { false };
std::atomic<bool> g_getppid_top_level { false };
std::atomic<bool> 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<int> g_event_signal { 0 };
std::atomic<bool> 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<void*>(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<void*>(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);
}

View File

@ -21,6 +21,12 @@ public:
/** @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 @@ private:
void refresh();
void rescan();
void applyExclusive();
std::vector<Device*> devices_;
TickType_t lastRefresh_ = 0;
bool exclusive_ = false;
};

View File

@ -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);

View File

@ -3,6 +3,8 @@
#include <cstddef>
#include <cstdint>
#include <tactility/drivers/display.h>
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 @@ public:
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 @@ protected:
*/
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();

View File

@ -0,0 +1,41 @@
#pragma once
#include <Tactility/app/terminal/TerminalRenderer.h>
#include <lvgl.h>
#include <atomic>
/**
* 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<lv_obj_t*> canvas_ = nullptr;
// The whole frame, as shown by the canvas.
PixelBuffer* canvasBuffer_ = nullptr;
};

View File

@ -9,6 +9,7 @@
#include <app/io.h>
#include <app/manager.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <app/start.h>
#include <app/stream.h>
@ -19,6 +20,7 @@
#include <sys/stat.h>
#include <unistd.h>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@ -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;
}
}

View File

@ -1,5 +1,8 @@
#include <Tactility/app/terminal/KeyboardInput.h>
#include <lvgl/devices/keyboard.h>
#include <lvgl/lvgl.h>
#include <algorithm>
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<std::vector<Device*>*>(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();
}
}

View File

@ -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;
}
}
}

View File

@ -3,8 +3,6 @@
#include <Tactility/app/terminal/Shell.h>
#include <Tactility/app/terminal/Terminal.h>
#include <Tactility/app/terminal/TerminalRenderer.h>
#include <Tactility/app/terminal/TerminalRendererGeneric.h>
#include <Tactility/app/terminal/TerminalRendererPpa.h>
#include <Tactility/app/terminal/TouchInput.h>
#include <tactility/device.h>
@ -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<IoTaskParams*>(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<TerminalRenderer&>(ppaRenderer)
: static_cast<TerminalRenderer&>(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(),
};

View File

@ -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;

View File

@ -0,0 +1,91 @@
#include <Tactility/app/terminal/TerminalRendererLvgl.h>
#include <graphics/pixel_buffer.h>
#include <lvgl/lvgl.h>
#include <tactility/log.h>
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();
}

View File

@ -2,6 +2,9 @@
#include <Tactility/app/terminal/Terminal.h>
#include <Tactility/app/terminal/TerminalRendererGeneric.h>
#include <Tactility/app/terminal/TerminalRendererLvgl.h>
#include <Tactility/app/terminal/TerminalRendererPpa.h>
#include <tactility/log.h>
@ -14,11 +17,88 @@
#include <tactility/drivers/display.h>
#include <tactility/module.h>
#include <lvgl/module.h>
#include <lvgl/widgets/toolbar.h>
#include <lvgl_window_manager/window_manager.h>
#include <cstring>
constexpr auto* TAG = "terminal";
namespace tt::app::terminal {
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<WindowContext*>(lv_event_get_user_data(event));
app_event_emit_close(ctx->appInstanceId);
}
void createWidgets(lv_obj_t* root, void* userData) {
auto* ctx = static_cast<WindowContext*>(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<WindowContext*>(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);
}
TerminalRendererPpa ppaRenderer;
TerminalRendererGeneric genericRenderer;
TerminalRenderer& renderer = TerminalRendererPpa::isSupported()
? static_cast<TerminalRenderer&>(ppaRenderer)
: static_cast<TerminalRenderer&>(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) {
@ -26,22 +106,19 @@ int main(int argc, char* argv[]) {
return 0;
}
// Stop LVGL, because terminal has custom rendering
auto lvgl_active = module_is_started(&lvgl_module);
if (lvgl_active) {
module_stop(&lvgl_module);
}
// 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");
runTerminal(display_device);
if (useLvgl) {
runInWindow(display_device);
} else {
runFullscreen(display_device);
}
device_put(display_device);
// If needed, restart LVGL
if (lvgl_active && !module_is_started(&lvgl_module)) {
LOG_I(TAG, "Restarting LVGL");
module_start(&lvgl_module);
}
return 0;
}