mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Fixes
This commit is contained in:
parent
7ffb901eda
commit
7780860446
@ -33,6 +33,12 @@ struct AppFileOps {
|
|||||||
error_t (*await)(void* object, AppFileWait wait, TickType_t timeout);
|
error_t (*await)(void* object, AppFileWait wait, TickType_t timeout);
|
||||||
/** @return bitmask of APP_FILE_READABLE / APP_FILE_WRITABLE. */
|
/** @return bitmask of APP_FILE_READABLE / APP_FILE_WRITABLE. */
|
||||||
uint32_t (*poll)(void* object);
|
uint32_t (*poll)(void* object);
|
||||||
|
/** Optional (may be NULL). Called by app_fd_table_get_and_retain() atomically with the fd
|
||||||
|
* lookup, before handing `object` to a caller about to dispatch into it, so a concurrent
|
||||||
|
* teardown of `object` can wait for that dispatch to finish instead of racing it. The caller
|
||||||
|
* calls release() once done, regardless of what the dispatched call returned. */
|
||||||
|
void (*retain)(void* object);
|
||||||
|
void (*release)(void* object);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A file-descriptor-table entry: an operations table paired with the object it operates on. */
|
/** A file-descriptor-table entry: an operations table paired with the object it operates on. */
|
||||||
|
|||||||
@ -56,6 +56,15 @@ error_t app_fd_table_allocate(struct AppFdTable* table, const struct AppFileOps*
|
|||||||
/** @return true and fills @a out_file if @a fd is currently in use, false otherwise. */
|
/** @return true and fills @a out_file if @a fd is currently in use, false otherwise. */
|
||||||
bool app_fd_table_get(struct AppFdTable* table, int fd, struct AppFile* out_file);
|
bool app_fd_table_get(struct AppFdTable* table, int fd, struct AppFile* out_file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like app_fd_table_get(), but also calls the returned AppFile's AppFileOps::retain() (if any)
|
||||||
|
* while still holding `mutex`, atomically with the lookup. A caller that gets true back can
|
||||||
|
* therefore never have @a fd closed/torn down out from under it between this call and actually
|
||||||
|
* dispatching into the returned AppFile. Caller must call AppFileOps::release() once done
|
||||||
|
* dispatching.
|
||||||
|
*/
|
||||||
|
bool app_fd_table_get_and_retain(struct AppFdTable* table, int fd, struct AppFile* out_file);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes @a fd (via AppFileOps::close()) and frees the slot for reuse.
|
* Closes @a fd (via AppFileOps::close()) and frees the slot for reuse.
|
||||||
* @retval ERROR_NOT_FOUND @a fd is out of range or not currently in use
|
* @retval ERROR_NOT_FOUND @a fd is out of range or not currently in use
|
||||||
|
|||||||
@ -12,6 +12,24 @@ bool fd_in_range(int fd) {
|
|||||||
return fd >= 0 && fd < APP_MAX_FDS;
|
return fd >= 0 && fd < APP_MAX_FDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get_internal(AppFdTable* table, int fd, AppFile* out_file, bool retain) {
|
||||||
|
if (!fd_in_range(fd)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutex_lock(&table->mutex);
|
||||||
|
check(!table->shutting_down);
|
||||||
|
bool in_use = table->fds[fd] != nullptr;
|
||||||
|
if (in_use) {
|
||||||
|
*out_file = table->slots[fd];
|
||||||
|
if (retain && out_file->ops->retain != nullptr) {
|
||||||
|
out_file->ops->retain(out_file->object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mutex_unlock(&table->mutex);
|
||||||
|
return in_use;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@ -86,18 +104,11 @@ error_t app_fd_table_allocate(AppFdTable* table, const AppFileOps* ops, void* ob
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool app_fd_table_get(AppFdTable* table, int fd, AppFile* out_file) {
|
bool app_fd_table_get(AppFdTable* table, int fd, AppFile* out_file) {
|
||||||
if (!fd_in_range(fd)) {
|
return get_internal(table, fd, out_file, /*retain=*/false);
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
mutex_lock(&table->mutex);
|
bool app_fd_table_get_and_retain(AppFdTable* table, int fd, AppFile* out_file) {
|
||||||
check(!table->shutting_down);
|
return get_internal(table, fd, out_file, /*retain=*/true);
|
||||||
bool in_use = table->fds[fd] != nullptr;
|
|
||||||
if (in_use) {
|
|
||||||
*out_file = table->slots[fd];
|
|
||||||
}
|
|
||||||
mutex_unlock(&table->mutex);
|
|
||||||
return in_use;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t app_fd_table_close(AppFdTable* table, int fd) {
|
error_t app_fd_table_close(AppFdTable* table, int fd) {
|
||||||
|
|||||||
@ -49,11 +49,15 @@ ssize_t app_io_read(int fd, void* buffer, size_t size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppFile file {};
|
AppFile file {};
|
||||||
if (!app_fd_table_get(table, fd, &file)) {
|
if (!app_fd_table_get_and_retain(table, fd, &file)) {
|
||||||
errno = EBADF;
|
errno = EBADF;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return file.ops->read(file.object, buffer, size);
|
ssize_t result = file.ops->read(file.object, buffer, size);
|
||||||
|
if (file.ops->release != nullptr) {
|
||||||
|
file.ops->release(file.object);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssize_t app_io_write(int fd, const void* buffer, size_t size) {
|
ssize_t app_io_write(int fd, const void* buffer, size_t size) {
|
||||||
@ -67,11 +71,15 @@ ssize_t app_io_write(int fd, const void* buffer, size_t size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppFile file {};
|
AppFile file {};
|
||||||
if (!app_fd_table_get(table, fd, &file)) {
|
if (!app_fd_table_get_and_retain(table, fd, &file)) {
|
||||||
errno = EBADF;
|
errno = EBADF;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return file.ops->write(file.object, buffer, size);
|
ssize_t result = file.ops->write(file.object, buffer, size);
|
||||||
|
if (file.ops->release != nullptr) {
|
||||||
|
file.ops->release(file.object);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int app_io_close(int fd) {
|
int app_io_close(int fd) {
|
||||||
|
|||||||
@ -126,6 +126,12 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg
|
|||||||
error_t bind_result = app_stream_subscribe(bindings[i].stream, bindings[i].buffer, bindings[i].buffer_capacity, bindings[i].event_group, target_id, bindings[i].producer_fd);
|
error_t bind_result = app_stream_subscribe(bindings[i].stream, bindings[i].buffer, bindings[i].buffer_capacity, bindings[i].event_group, target_id, bindings[i].producer_fd);
|
||||||
if (bind_result != ERROR_NONE) {
|
if (bind_result != ERROR_NONE) {
|
||||||
LOG_E(TAG, "[instance %d] Failed to bind stream at fd %d: %s", target_id, bindings[i].producer_fd, error_to_string(bind_result));
|
LOG_E(TAG, "[instance %d] Failed to bind stream at fd %d: %s", target_id, bindings[i].producer_fd, error_to_string(bind_result));
|
||||||
|
// Undo bindings[0..i): app_fd_table_teardown() below only closes each stream. It
|
||||||
|
// doesn't release the event bits app_stream_subscribe() claimed or destruct
|
||||||
|
// stream->internal.mutex; only app_stream_unsubscribe() does that.
|
||||||
|
for (size_t j = 0; j < i; j++) {
|
||||||
|
app_stream_unsubscribe(bindings[j].stream);
|
||||||
|
}
|
||||||
mutex_lock(&ledger.mutex);
|
mutex_lock(&ledger.mutex);
|
||||||
app_fd_table_teardown(&ledger.instances[target_id].fd_table);
|
app_fd_table_teardown(&ledger.instances[target_id].fd_table);
|
||||||
ledger.instances.erase(target_id);
|
ledger.instances.erase(target_id);
|
||||||
@ -137,6 +143,11 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg
|
|||||||
|
|
||||||
error_t error = app_scheduler_start(target_id, manifest->location, manifest->stack, argc, argv);
|
error_t error = app_scheduler_start(target_id, manifest->location, manifest->stack, argc, argv);
|
||||||
if (error != ERROR_NONE) {
|
if (error != ERROR_NONE) {
|
||||||
|
// Every binding succeeded before app_scheduler_start() failed. Unsubscribe all of them,
|
||||||
|
// same reasoning as the bind-failure path above.
|
||||||
|
for (size_t j = 0; j < binding_count; j++) {
|
||||||
|
app_stream_unsubscribe(bindings[j].stream);
|
||||||
|
}
|
||||||
mutex_lock(&ledger.mutex);
|
mutex_lock(&ledger.mutex);
|
||||||
app_fd_table_teardown(&ledger.instances[target_id].fd_table);
|
app_fd_table_teardown(&ledger.instances[target_id].fd_table);
|
||||||
ledger.instances.erase(target_id);
|
ledger.instances.erase(target_id);
|
||||||
|
|||||||
@ -113,12 +113,31 @@ uint32_t stream_file_poll(void* object) {
|
|||||||
return bits;
|
return bits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fused with app_fd_table_get_and_retain()'s own lock, so a caller that gets a live AppFile back
|
||||||
|
// is already counted here before it can be paused and raced by app_stream_unsubscribe()'s drain
|
||||||
|
// wait (see StreamOperationGuard above for the equivalent per-call bracketing).
|
||||||
|
void stream_file_retain(void* object) {
|
||||||
|
auto* stream = static_cast<AppStream*>(object);
|
||||||
|
mutex_lock(&stream->internal.mutex);
|
||||||
|
stream->internal.active_operations++;
|
||||||
|
mutex_unlock(&stream->internal.mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void stream_file_release(void* object) {
|
||||||
|
auto* stream = static_cast<AppStream*>(object);
|
||||||
|
mutex_lock(&stream->internal.mutex);
|
||||||
|
stream->internal.active_operations--;
|
||||||
|
mutex_unlock(&stream->internal.mutex);
|
||||||
|
}
|
||||||
|
|
||||||
constexpr AppFileOps STREAM_OPS = {
|
constexpr AppFileOps STREAM_OPS = {
|
||||||
.read = stream_file_read,
|
.read = stream_file_read,
|
||||||
.write = stream_file_write,
|
.write = stream_file_write,
|
||||||
.close = stream_file_close,
|
.close = stream_file_close,
|
||||||
.await = stream_file_await,
|
.await = stream_file_await,
|
||||||
.poll = stream_file_poll,
|
.poll = stream_file_poll,
|
||||||
|
.retain = stream_file_retain,
|
||||||
|
.release = stream_file_release,
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@ -170,3 +170,39 @@ TEST_CASE("a write blocked on a full stream wakes with an error once the consume
|
|||||||
task_event_group_destruct(&event_group);
|
task_event_group_destruct(&event_group);
|
||||||
app_manager_remove("test.io.blocked");
|
app_manager_remove("test.io.blocked");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("app_stream_unsubscribe is safe to call while a write is actively blocked") {
|
||||||
|
ensure_memory_loader_registered();
|
||||||
|
g_blocked_writer_saw_error.store(false, std::memory_order_relaxed);
|
||||||
|
g_blocked_writer_done.store(false, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
AppManifest manifest { "test.io.unsub_race", "UnsubRace", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(blocked_writer_app_main) } };
|
||||||
|
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||||
|
|
||||||
|
TaskEventGroup event_group {};
|
||||||
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
|
uint8_t storage[4]; // smaller than the 10 bytes blocked_writer_app_main sends
|
||||||
|
AppStream child_stdout {};
|
||||||
|
|
||||||
|
AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group };
|
||||||
|
AppInstanceId child_id = 0;
|
||||||
|
REQUIRE_EQ(app_manager_start_with_streams("test.io.unsub_race", &binding, 1, &child_id), ERROR_NONE);
|
||||||
|
|
||||||
|
// Give the child time to fill the 4-byte buffer and block inside app_io_write(), already
|
||||||
|
// dispatched through app_fd_table_get_and_retain() and currently waiting in
|
||||||
|
// app_stream_await(). This is the exact state app_stream_unsubscribe() must be safe to run
|
||||||
|
// against, with no prior app_stream_close() or wait for the child to stop first.
|
||||||
|
delay_millis(200);
|
||||||
|
REQUIRE_FALSE(g_blocked_writer_done.load(std::memory_order_acquire));
|
||||||
|
|
||||||
|
// Regression: unsubscribing directly here used to be able to destruct
|
||||||
|
// stream->internal.mutex while the blocked write above was still executing against it.
|
||||||
|
REQUIRE_EQ(app_stream_unsubscribe(&child_stdout), ERROR_NONE);
|
||||||
|
|
||||||
|
REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000));
|
||||||
|
CHECK(g_blocked_writer_saw_error.load(std::memory_order_acquire));
|
||||||
|
|
||||||
|
task_event_group_destruct(&event_group);
|
||||||
|
app_manager_remove("test.io.unsub_race");
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user