ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
http_renderer_process.c
Go to the documentation of this file.
1
2/* Existing buffer budgets in bytes, including NUL for text storage. */
3#define MCP_RENDER_ENV_CAPACITY 16
4
5#include "http_renderer.h"
6#include <libplatform/filesystem.h>
8#include <uv.h>
9#include <signal.h>
10#include <string.h>
11
12// libuv supplies portable process and pipe I/O; each renderer owns its event loop.
13typedef struct {
14 uv_loop_t loop;
15 uv_process_t process;
16 uv_pipe_t output, errors;
17 uv_timer_t timer;
18 atomic_t *cancelled;
21 unsigned timeout;
29static void stop_worker(render_worker_t *w, const char *reason) {
30 if (w->failed)
31 return;
32 w->failed = true;
33 w->stopped = true;
34 SAFE_STRNCPY(w->result->error, reason, sizeof(w->result->error));
35 if (!w->exited)
36 uv_process_kill(&w->process, SIGKILL);
37}
38static void allocate(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) {
39 (void)handle;
40 (void)suggested;
41 buf->len = 16384;
42 buf->base = SAFE_MALLOC(buf->len, char *);
43}
44static void read_output(uv_stream_t *stream, ssize_t count, const uv_buf_t *buf) {
45 render_worker_t *w = stream->data;
46 if (count > 0) {
47 if (stream == (uv_stream_t *)&w->output) {
48 size_t n = (size_t)count;
50 stop_worker(w, "Rendered text exceeds 8 MiB");
51 else {
52 memcpy(w->result->text + w->result->length, buf->base, n);
53 w->result->length += n;
54 w->result->text[w->result->length] = '\0';
55 }
56 } else if (!w->stopped) {
57 // Keep the tail, which normally contains the most useful decoder error.
58 size_t old = strlen(w->result->error), n = (size_t)count, cap = sizeof(w->result->error) - 1;
59 if (n >= cap) {
60 memcpy(w->result->error, buf->base + n - cap, cap);
61 w->result->error[cap] = '\0';
62 } else {
63 if (old + n > cap) {
64 size_t drop = old + n - cap;
65 memmove(w->result->error, w->result->error + drop, old - drop);
66 old -= drop;
67 }
68 memcpy(w->result->error + old, buf->base, n);
69 w->result->error[old + n] = '\0';
70 }
71 }
72 }
73 char *allocation = buf->base;
74 SAFE_FREE(allocation);
75 if (count < 0 && !uv_is_closing((uv_handle_t *)stream))
76 uv_close((uv_handle_t *)stream, NULL);
77}
78static void exited(uv_process_t *process, int64_t status, int signal) {
79 render_worker_t *w = process->data;
80 w->exited = true;
81 w->exit_time = time_get_ns();
82 if (!w->failed && (status || signal)) {
83 w->failed = true;
84 if (!w->result->error[0])
85 SAFE_SNPRINTF(w->result->error, sizeof(w->result->error), "Renderer failed (exit %lld, signal %d)",
86 (long long)status, signal);
87 }
88 uv_close((uv_handle_t *)process, NULL);
89}
90static void tick(uv_timer_t *timer) {
91 render_worker_t *w = timer->data;
92 if (atomic_load_bool(w->cancelled))
93 stop_worker(w, "Rendering cancelled");
94 if (!w->failed && time_get_ns() - w->started > (uint64_t)w->timeout * 1000000) {
95 w->result->timed_out = true;
96 stop_worker(w, "Rendering exceeded timeout_ms");
97 }
98 if (w->exited) {
99 bool drained = uv_is_closing((uv_handle_t *)&w->output) && uv_is_closing((uv_handle_t *)&w->errors);
100 // Allow pending pipe reads to drain after process exit, but bound inherited-descriptor stalls.
101 if (drained || time_get_ns() - w->exit_time > 1000000000) {
102 if (!drained)
103 stop_worker(w, "Renderer output pipes did not close");
104 uv_close((uv_handle_t *)&w->timer, NULL);
105 if (!uv_is_closing((uv_handle_t *)&w->output))
106 uv_close((uv_handle_t *)&w->output, NULL);
107 if (!uv_is_closing((uv_handle_t *)&w->errors))
108 uv_close((uv_handle_t *)&w->errors, NULL);
109 }
110 }
111}
114 memset(result, 0, sizeof(*result));
116 SAFE_STRNCPY(result->error, "Renderer output must select text or files", sizeof(result->error));
117 return;
118 }
119 render_worker_t w = {.cancelled = cancelled, .result = result, .started = time_get_ns(), .timeout = r->timeout_ms};
120 result->text = SAFE_CALLOC(HTTP_RENDERER_MAX_OUTPUT + 1, 1, char *);
121 if (uv_loop_init(&w.loop)) {
122 SAFE_STRNCPY(result->error, "Cannot initialize renderer event loop", sizeof(result->error));
123 return;
124 }
125 uv_pipe_init(&w.loop, &w.output, 0);
126 uv_pipe_init(&w.loop, &w.errors, 0);
127 uv_timer_init(&w.loop, &w.timer);
128 w.output.data = w.errors.data = w.timer.data = w.process.data = &w;
129 uv_stdio_container_t streams[3] = {0};
130 streams[0].flags = UV_IGNORE;
131 streams[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE;
132 streams[1].data.stream = (uv_stream_t *)&w.output;
133 streams[2].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE;
134 streams[2].data.stream = (uv_stream_t *)&w.errors;
135 // Only runtime essentials are inherited; user config, ASCII_CHAT_* and credentials are excluded.
136 const char *keys[] = {
137 "PATH", "SystemRoot", "WINDIR", "TEMP", "TMP", "TMPDIR", "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "TESTING", NULL};
138 char *env[MCP_RENDER_ENV_CAPACITY] = {0};
139 size_t ne = 0;
140 for (size_t i = 0; keys[i]; ++i) {
141 const char *v = platform_getenv(keys[i]);
142 if (v) {
143 size_t n = strlen(keys[i]) + strlen(v) + 2;
144 env[ne] = SAFE_MALLOC(n, char *);
145 SAFE_SNPRINTF(env[ne], n, "%s=%s", keys[i], v);
146 ++ne;
147 }
148 }
149 // The private staging directory provides an isolated home for media libraries.
150 if (r->stage_dir[0]) {
151 size_t n = strlen(r->stage_dir) + sizeof("HOME=");
152 env[ne] = SAFE_MALLOC(n, char *);
153 SAFE_SNPRINTF(env[ne], n, "HOME=%s", r->stage_dir);
154 ++ne;
155 }
156 env[ne++] = platform_strdup("ASCII_CHAT_MCP_WORKER=1");
157 env[ne++] = platform_strdup("ASCII_CHAT_OTEL_ENABLED_TELEMETRY=none");
158 if (r->capture_webcam)
159 env[ne++] = platform_strdup("ASCII_CHAT_MCP_WEBCAM=1");
160 uv_process_options_t opts = {
161 .exit_cb = exited, .file = r->argv[0], .args = r->argv, .env = env, .stdio_count = 3, .stdio = streams};
162 int rc = uv_spawn(&w.loop, &w.process, &opts);
163 for (size_t i = 0; i < ne; ++i)
164 SAFE_FREE(env[i]);
165 if (rc) {
166 SAFE_SNPRINTF(result->error, sizeof(result->error), "Cannot start renderer: %s", uv_strerror(rc));
167 uv_close((uv_handle_t *)&w.output, NULL);
168 uv_close((uv_handle_t *)&w.errors, NULL);
169 uv_close((uv_handle_t *)&w.timer, NULL);
170 } else {
171 uv_read_start((uv_stream_t *)&w.output, allocate, read_output);
172 uv_read_start((uv_stream_t *)&w.errors, allocate, read_output);
173 uv_timer_start(&w.timer, tick, 20, 20);
174 }
175 uv_run(&w.loop, UV_RUN_DEFAULT);
176 uv_loop_close(&w.loop);
177 result->elapsed_ms = (time_get_ns() - w.started) / 1000000;
178 if (rc || w.failed)
179 return;
180 result->error[0] = '\0';
181 if (!result->length && !r->stage_file[0]) {
182 SAFE_STRNCPY(result->error, "Renderer returned no ASCII art", sizeof(result->error));
183 return;
184 }
185 if (r->stage_file[0]) {
186 uv_fs_t fs;
187 int e = uv_fs_stat(NULL, &fs, r->stage_file, NULL);
188 bool valid = e == 0 && fs.statbuf.st_size > 0;
189 uv_fs_req_cleanup(&fs);
190 if (!valid) {
191 SAFE_STRNCPY(result->error, "Renderer did not create an output file", sizeof(result->error));
192 return;
193 }
194 // Hard-link publication is atomic and fails if any file/symlink already owns the destination.
195 // Staging beside the destination keeps this on the same filesystem.
196 e = uv_fs_link(NULL, &fs, r->stage_file, r->output_path, NULL);
197 uv_fs_req_cleanup(&fs);
198 if (e) {
199 SAFE_SNPRINTF(result->error, sizeof(result->error), "Cannot save without replacing an existing file: %s",
200 uv_strerror(e));
201 return;
202 }
203 SAFE_STRNCPY(result->output_path, r->output_path, sizeof(result->output_path));
204 }
205 http_render_result_t frames = {0};
206 if (result->text && result->length)
207 (void)http_render_result_add_text_frame(&frames, result->text, result->length, 0);
208 if (result->output_path[0])
209 (void)http_render_result_add_file_frame(&frames, result->output_path, 0);
210 result->frames = frames.frames;
211 result->frame_count = frames.frame_count;
212}
213
215 if (!result)
216 return;
217 for (size_t i = 0; i < result->frame_count; ++i)
218 SAFE_FREE(result->frames[i].text);
219 SAFE_FREE(result->frames);
220 SAFE_FREE(result->text);
221 memset(result, 0, sizeof(*result));
222}
bool valid
Definition fft.c:27
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:507
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
const char * platform_getenv(const char *name)
Get an environment variable value.
asciichat_error_t http_render_result_add_file_frame(http_render_result_t *result, const char *path, uint64_t timestamp_ms)
asciichat_error_t http_render_result_add_text_frame(http_render_result_t *result, const char *text, size_t length, uint64_t timestamp_ms)
#define HTTP_RENDERER_MAX_OUTPUT
@ HTTP_RENDER_OUTPUT_FILE
@ HTTP_RENDER_OUTPUT_TEXT
void http_renderer_process(http_renderer_process_request_t *r, atomic_t *cancelled, http_renderer_process_result_t *result)
void http_renderer_process_result_destroy(http_renderer_process_result_t *result)
#define MCP_RENDER_ENV_CAPACITY
🔤 String Manipulation and Shell Escaping Utilities
http_render_frame_t * frames
http_render_output_type_t output_type
char stage_file[PLATFORM_MAX_PATH_LENGTH]
char stage_dir[PLATFORM_MAX_PATH_LENGTH]
char output_path[PLATFORM_MAX_PATH_LENGTH]
char output_path[PLATFORM_MAX_PATH_LENGTH]
http_render_frame_t * frames
http_renderer_process_result_t * result
⏱️ High-precision timing utilities using sokol_time.h and uthash