mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
LVGL terminal rendering, elf memleak fixes and posix support (#669)
This commit is contained in:
parent
ffab897fe4
commit
34ca63ec3c
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
192
Modules/app-esp32-module/source/app_symbols.cpp
Normal file
192
Modules/app-esp32-module/source/app_symbols.cpp
Normal 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
|
||||
};
|
||||
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
25
Modules/app-module/include/app/memory.h
Normal file
25
Modules/app-module/include/app/memory.h
Normal 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
|
||||
46
Modules/app-module/include/app/signal.h
Normal file
46
Modules/app-module/include/app/signal.h
Normal 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
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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) {
|
||||
|
||||
125
Modules/app-module/source/signal.cpp
Normal file
125
Modules/app-module/source/signal.cpp
Normal 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"
|
||||
@ -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
|
||||
|
||||
19
Modules/app-posix-module/private/app_posix/malloc_wrap.h
Normal file
19
Modules/app-posix-module/private/app_posix/malloc_wrap.h
Normal 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
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
176
Modules/app-posix-module/source/malloc_wrap.cpp
Normal file
176
Modules/app-posix-module/source/malloc_wrap.cpp
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
91
Tactility/Source/app/terminal/TerminalRendererLvgl.cpp
Normal file
91
Tactility/Source/app/terminal/TerminalRendererLvgl.cpp
Normal 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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user