Implement app streams: stdio for apps (#640)
Implement file IO for applications to facilitate text input/output capturing. Apps can now launch apps and: - Read their stdio output - Write to their stdio input
This commit is contained in:
committed by
GitHub
parent
64fb1a9f52
commit
6e5e35610b
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Which readiness condition to block for in AppFileOps::await(). */
|
||||
typedef enum {
|
||||
APP_FILE_WAIT_READABLE,
|
||||
APP_FILE_WAIT_WRITABLE,
|
||||
} AppFileWait;
|
||||
|
||||
/** Bits returned by AppFileOps::poll(). */
|
||||
#define APP_FILE_READABLE (1u << 0)
|
||||
#define APP_FILE_WRITABLE (1u << 1)
|
||||
|
||||
/**
|
||||
* Operations table for one kind of file-like object (stream, file, device, ...). Every function
|
||||
* takes the type-erased `object` a concrete AppFile instance was constructed with.
|
||||
*/
|
||||
struct AppFileOps {
|
||||
ssize_t (*read)(void* object, void* buffer, size_t size);
|
||||
ssize_t (*write)(void* object, const void* buffer, size_t size);
|
||||
error_t (*close)(void* object);
|
||||
error_t (*await)(void* object, AppFileWait wait, TickType_t timeout);
|
||||
/** @return bitmask of APP_FILE_READABLE / APP_FILE_WRITABLE. */
|
||||
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. */
|
||||
struct AppFile {
|
||||
const struct AppFileOps* ops;
|
||||
void* object;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define STDIN_FILENO 0
|
||||
#define STDOUT_FILENO 1
|
||||
#define STDERR_FILENO 2
|
||||
|
||||
/** Fixed size of an app instance's fd table. FD allocation uses the lowest unused index >= 3. */
|
||||
#define APP_MAX_FDS 16
|
||||
|
||||
/**
|
||||
* FD-table dispatch for read()/write()/close(). When the calling task belongs to a running app
|
||||
* instance and @a fd is one that instance bound/allocated itself, it's looked up in that
|
||||
* instance's own fd table. Otherwise (no app instance, e.g. a kernel service task; or an app
|
||||
* instance's own fd that was never bound through app-module, e.g. a real file from fopen()/
|
||||
* open(), which this layer never intercepts) @a fd is a real underlying fd and the call falls
|
||||
* through to the real syscall unchanged.
|
||||
* @return number of bytes transferred, 0 on EOF (read) or a closed peer (write), or -1 with
|
||||
* errno set on failure. This is the fd-layer translation of AppFileOps::read()/write()'s ssize_t
|
||||
* and AppFileOps::close()'s error_t.
|
||||
*/
|
||||
ssize_t app_io_read(int fd, void* buffer, size_t size);
|
||||
ssize_t app_io_write(int fd, const void* buffer, size_t size);
|
||||
int app_io_close(int fd);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <app/instance.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/stream.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
@@ -86,6 +87,30 @@ error_t app_manager_start_with_parameters(const char* id, int argc, const char*
|
||||
*/
|
||||
error_t app_manager_start_for_result(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
|
||||
|
||||
/** One fd-to-stream binding for app_manager_start_with_streams(). Every field is passed through
|
||||
* to app_stream_subscribe() as-is; see its own doc for the ownership contracts. */
|
||||
struct AppStreamBinding {
|
||||
int producer_fd;
|
||||
struct AppStream* stream;
|
||||
void* buffer;
|
||||
size_t buffer_capacity;
|
||||
struct TaskEventGroup* event_group;
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as app_manager_start(), but installs @a bindings into the new instance's fd table before
|
||||
* its task begins executing (e.g. a child's stdio, piped through parent-owned AppStreams; see
|
||||
* app/stream.h). Writes the new instance's id into each bound stream's producer_id itself, since
|
||||
* the caller cannot know it in advance.
|
||||
* @param[in] bindings @a binding_count entries; each stream and buffer must stay alive (see
|
||||
* app_stream_subscribe()) until unsubscribed or the child exits.
|
||||
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
|
||||
* @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range
|
||||
* @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_start_with_streams(const char* id, const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id);
|
||||
|
||||
/**
|
||||
* Stop an app instance permanently. Emits APP_EVENT_CLOSE and bound-waits for its task to exit
|
||||
* if it was running.
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/file.h>
|
||||
#include <app/instance.h>
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @warning This is internal data. Do not read/write to it directly. */
|
||||
struct AppStreamBuffer {
|
||||
uint8_t* data;
|
||||
size_t capacity;
|
||||
size_t read_pos;
|
||||
size_t write_pos;
|
||||
size_t count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Buffered byte-oriented communication between one producer task and one consumer task. The
|
||||
* consumer owns the AppStream object and its lifetime. See app_stream_subscribe().
|
||||
*
|
||||
* @warning This is internal data. Do not read/write to it directly.
|
||||
*/
|
||||
struct AppStream {
|
||||
struct AppStreamBuffer buffer;
|
||||
AppInstanceId producer_id;
|
||||
TaskHandle_t producer_task;
|
||||
/** fd this stream is installed at in producer_id's fd table; set by
|
||||
* app_stream_subscribe()/app_manager_start_with_streams(), used by
|
||||
* app_stream_unsubscribe() to find it again. */
|
||||
int producer_fd;
|
||||
|
||||
struct Mutex mutex;
|
||||
/** Caller-owned group readiness is signalled on; see app_stream_subscribe(). */
|
||||
struct TaskEventGroup* event_group;
|
||||
uint32_t readable_bit;
|
||||
uint32_t writable_bit;
|
||||
bool closed;
|
||||
/** Count of AppFileOps calls currently executing against this stream; app_stream_unsubscribe()
|
||||
* waits for this to reach 0 before destructing `mutex`, since a call already blocked in
|
||||
* app_stream_await() when unsubscribe starts only wakes (it doesn't vanish) when closed. */
|
||||
int active_operations;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers @a stream as the file-like object bound to @a producer_id's fd table at
|
||||
* @a producer_fd, claiming two bits from @a event_group for its readable/writable readiness.
|
||||
* There is at most one subscriber for a given app fd. Subscribing over an existing one
|
||||
* atomically closes it first (any task blocked on it wakes; already-buffered bytes remain
|
||||
* readable until drained) and installs @a stream in its place.
|
||||
* @param[in,out] stream caller-owned. Storage must remain valid until app_stream_unsubscribe()
|
||||
* returns. That is the only operation guaranteeing both that the fd-table binding is gone and
|
||||
* that no AppFileOps call is still executing against @a stream; app_stream_close() alone does
|
||||
* neither.
|
||||
* @param[in] buffer ring buffer storage; caller-owned, same validity requirement as @a stream.
|
||||
* @param[in] buffer_capacity size of @a buffer in bytes.
|
||||
* @param[in] event_group caller-owned; must outlive @a stream (i.e. be destructed only after
|
||||
* app_stream_unsubscribe()). Lets a task wait on this stream's readiness together with other
|
||||
* event sources sharing the same group via task_event_group_wait()/task_event_group_wait_any().
|
||||
* @retval ERROR_NOT_FOUND no instance with this id is running
|
||||
* @retval ERROR_OUT_OF_RANGE @a producer_fd is out of range
|
||||
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_stream_subscribe(struct AppStream* stream, void* buffer, size_t buffer_capacity, struct TaskEventGroup* event_group, AppInstanceId producer_id, int producer_fd);
|
||||
|
||||
/**
|
||||
* Removes @a stream's binding from whichever fd table it was installed in (further use of that
|
||||
* fd then fails), wakes and waits for every AppFileOps call currently in flight against
|
||||
* @a stream to finish, then releases its two bits back to the event_group given to
|
||||
* app_stream_subscribe() and destructs its internal mutex. Only after this returns is
|
||||
* @a stream's storage safe to free or reuse.
|
||||
*/
|
||||
error_t app_stream_unsubscribe(struct AppStream* stream);
|
||||
|
||||
/**
|
||||
* Blocks until @a stream becomes readable/writable (per @a wait) or @a timeout elapses. Checks
|
||||
* the current state directly before blocking, so a permanently-true condition (e.g. closed)
|
||||
* keeps returning immediately on every call, even though the underlying event bit itself is
|
||||
* one-shot (task_event_group_wait() clears a matched bit on exit).
|
||||
* @retval ERROR_NONE the condition is true
|
||||
* @retval ERROR_TIMEOUT @a timeout elapsed
|
||||
* @retval ERROR_ISR_STATUS called from an ISR
|
||||
*/
|
||||
error_t app_stream_await(struct AppStream* stream, AppFileWait wait, TickType_t timeout);
|
||||
|
||||
/** Non-blocking: copies up to @a buffer_size currently-available bytes out of @a stream. */
|
||||
size_t app_stream_read(struct AppStream* stream, void* buffer, size_t buffer_size);
|
||||
|
||||
/** Non-blocking: copies up to @a buffer_size bytes into @a stream, as much as currently fits.
|
||||
* Copies nothing and returns 0 once @a stream is closed. */
|
||||
size_t app_stream_write(struct AppStream* stream, const void* buffer, size_t buffer_size);
|
||||
|
||||
/**
|
||||
* Marks @a stream closed: wakes any task blocked in app_stream_await(), and causes the other side
|
||||
* to observe EOF (reader, once drained) or a write error (writer). Safe to call while another
|
||||
* task may be blocked on @a stream, unlike app_stream_unsubscribe() (see its own doc); does not
|
||||
* by itself make @a stream's storage safe to free.
|
||||
*/
|
||||
error_t app_stream_close(struct AppStream* stream);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user