ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
query.c
Go to the documentation of this file.
1
13#include <ascii-chat/tooling/query/query.h>
14#include <ascii-chat/util/time.h>
15#include <ascii-chat/common.h>
16
17#include <stdbool.h>
18#include <stdint.h>
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22
23#ifdef _WIN32
24#define WIN32_LEAN_AND_MEAN
25#include <windows.h>
26#include <winsock2.h>
27#pragma comment(lib, "ws2_32.lib")
28#else
29#include <arpa/inet.h>
30#include <errno.h>
31#include <netinet/in.h>
32#include <signal.h>
33#include <sys/socket.h>
34#include <sys/types.h>
35#include <sys/wait.h>
36#include <unistd.h>
37#endif
38
39// Timeouts for HTTP health check (in nanoseconds)
40#define HEALTH_CHECK_TIMEOUT_NS (10000LL * NS_PER_MS_INT)
41#define HEALTH_CHECK_INTERVAL_NS (100LL * NS_PER_MS_INT)
42#define HEALTH_CHECK_CONNECT_TIMEOUT_NS (500LL * NS_PER_MS_INT)
43
44// Global state for the query tool runtime
45static bool g_query_active = false;
46static int g_query_port = -1;
47
48#ifdef _WIN32
49static HANDLE g_controller_handle = NULL;
50static DWORD g_controller_pid = 0;
51#else
52static pid_t g_controller_pid = -1;
53#endif
54
61static bool try_http_connect(int port, int timeout_ms) {
62#ifdef _WIN32
63 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
64 if (sock == INVALID_SOCKET) {
65 return false;
66 }
67
68 // Set non-blocking mode for timeout
69 u_long mode = 1;
70 ioctlsocket(sock, FIONBIO, &mode);
71
72 struct sockaddr_in addr;
73 memset(&addr, 0, sizeof(addr));
74 addr.sin_family = AF_INET;
75 addr.sin_port = htons((u_short)port);
76 addr.sin_addr.s_addr = inet_addr("127.0.0.1");
77
78 connect(sock, (struct sockaddr *)&addr, sizeof(addr));
79
80 // Use select for timeout
81 fd_set writefds;
82 FD_ZERO(&writefds);
83 FD_SET(sock, &writefds);
84
85 struct timeval tv;
86 tv.tv_sec = timeout_ms / 1000;
87 tv.tv_usec = (timeout_ms % 1000) * 1000;
88
89 int result = select(0, NULL, &writefds, NULL, &tv);
90 closesocket(sock);
91
92 return result > 0;
93#else
94 int sock = socket(AF_INET, SOCK_STREAM, 0);
95 if (sock < 0) {
96 return false;
97 }
98
99 // Set socket timeout
100 struct timeval tv;
101 tv.tv_sec = timeout_ms / 1000;
102 tv.tv_usec = (timeout_ms % 1000) * 1000;
103 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
104 setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
105
106 struct sockaddr_in addr;
107 memset(&addr, 0, sizeof(addr));
108 addr.sin_family = AF_INET;
109 addr.sin_port = htons((uint16_t)port);
110 addr.sin_addr.s_addr = inet_addr("127.0.0.1");
111
112 int result = connect(sock, (struct sockaddr *)&addr, sizeof(addr));
113 close(sock);
114
115 return result == 0;
116#endif
117}
118
125static bool wait_for_http_ready(int port, uint64_t timeout_ns) {
126 uint64_t elapsed_ns = 0;
127
128 while (elapsed_ns < timeout_ns) {
129 if (try_http_connect(port, (int)(HEALTH_CHECK_CONNECT_TIMEOUT_NS / NS_PER_MS_INT))) {
130 return true;
131 }
132
133// Check if controller process is still alive
134#ifdef _WIN32
135 if (g_controller_handle != NULL) {
136 DWORD exit_code;
137 if (GetExitCodeProcess(g_controller_handle, &exit_code)) {
138 if (exit_code != STILL_ACTIVE) {
139 // Controller exited unexpectedly
140 return false;
141 }
142 }
143 }
144#else
145 if (g_controller_pid > 0) {
146 int status;
147 pid_t result = waitpid(g_controller_pid, &status, WNOHANG);
148 if (result == g_controller_pid) {
149 // Controller exited unexpectedly
150 g_controller_pid = -1;
151 return false;
152 }
153 }
154#endif
155
156 // Sleep between checks
157 platform_sleep_ns(HEALTH_CHECK_INTERVAL_NS);
158 elapsed_ns += HEALTH_CHECK_INTERVAL_NS;
159 }
160
161 return false;
162}
163
170static bool find_query_server_path(char *buffer, size_t buffer_size) {
171 // Try common locations relative to the executable
172 const char *search_paths[] = {// Relative to build directory
173 ".deps-cache/query-tool/ascii-query-server",
174 "../.deps-cache/query-tool/ascii-query-server",
175 "../../.deps-cache/query-tool/ascii-query-server",
176 // In PATH
177 "ascii-query-server",
178 // Absolute paths for development
179 NULL};
180
181 for (int i = 0; search_paths[i] != NULL; i++) {
182#ifdef _WIN32
183 // On Windows, append .exe
184 safe_snprintf(buffer, buffer_size, "%s.exe", search_paths[i]);
185 if (GetFileAttributesA(buffer) != INVALID_FILE_ATTRIBUTES) {
186 return true;
187 }
188#else
189 safe_snprintf(buffer, buffer_size, "%s", search_paths[i]);
190 if (access(buffer, X_OK) == 0) {
191 return true;
192 }
193#endif
194 }
195
196 // Try to find via environment variable
197 const char *query_server_path = getenv("ASCIICHAT_QUERY_SERVER");
198 if (query_server_path != NULL) {
199 safe_snprintf(buffer, buffer_size, "%s", query_server_path);
200#ifdef _WIN32
201 if (GetFileAttributesA(buffer) != INVALID_FILE_ATTRIBUTES) {
202 return true;
203 }
204#else
205 if (access(buffer, X_OK) == 0) {
206 return true;
207 }
208#endif
209 }
210
211 return false;
212}
213
214int query_init(int preferred_port) {
215 // Already initialized?
216 if (g_query_active) {
217 return g_query_port;
218 }
219
220 // Find the query server executable
221 char server_path[PLATFORM_MAX_PATH_LENGTH];
222 if (!find_query_server_path(server_path, sizeof(server_path))) {
223 fprintf(stderr, "[query] Could not find ascii-query-server executable\n");
224 fprintf(stderr, "[query] Set ASCIICHAT_QUERY_SERVER environment variable or ensure "
225 "it's in .deps-cache/query-tool/\n");
226 return -1;
227 }
228
229#ifdef _WIN32
230 // Windows implementation using CreateProcess
231 WSADATA wsa_data;
232 if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
233 fprintf(stderr, "[query] WSAStartup failed\n");
234 return -1;
235 }
236
237 char cmdline[2048];
238 safe_snprintf(cmdline, sizeof(cmdline), "\"%s\" --attach %lu --port %d", server_path,
239 (unsigned long)GetCurrentProcessId(), preferred_port);
240
241 STARTUPINFOA si;
242 PROCESS_INFORMATION pi;
243 memset(&si, 0, sizeof(si));
244 si.cb = sizeof(si);
245 memset(&pi, 0, sizeof(pi));
246
247 // Create the controller process
248 if (!CreateProcessA(NULL, // Use command line
249 cmdline, // Command line
250 NULL, // Process security attributes
251 NULL, // Thread security attributes
252 FALSE, // Don't inherit handles
253 CREATE_NEW_CONSOLE, // Creation flags
254 NULL, // Use parent's environment
255 NULL, // Use parent's directory
256 &si, &pi)) {
257 fprintf(stderr, "[query] Failed to start query server: error %lu\n", GetLastError());
258 return -1;
259 }
260
261 g_controller_handle = pi.hProcess;
262 g_controller_pid = pi.dwProcessId;
263 CloseHandle(pi.hThread); // Don't need the thread handle
264
265 fprintf(stderr, "[query] Started query server (PID %lu) on port %d\n", (unsigned long)g_controller_pid,
266 preferred_port);
267
268#else
269 // Unix implementation using fork/exec
270 pid_t self_pid = getpid();
271
272 char port_str[16];
273 char pid_str[16];
274 safe_snprintf(port_str, sizeof(port_str), "%d", preferred_port);
275 safe_snprintf(pid_str, sizeof(pid_str), "%d", self_pid);
276
277 pid_t child = fork();
278 if (child < 0) {
279 fprintf(stderr, "[query] fork() failed: %s\n", strerror(errno));
280 return -1;
281 }
282
283 if (child == 0) {
284 // Child process: exec the controller
285 // Redirect stdout/stderr to /dev/null or a log file to avoid clutter
286 // (The controller has its own logging)
287
288 execl(server_path, "ascii-query-server", "--attach", pid_str, "--port", port_str, (char *)NULL);
289
290 // If exec fails
291 fprintf(stderr, "[query] exec(%s) failed: %s\n", server_path, strerror(errno));
292 exit(1);
293 }
294
295 // Parent process
296 g_controller_pid = child;
297 fprintf(stderr, "[query] Started query server (PID %d) on port %d\n", child, preferred_port);
298#endif
299
300 // Wait for the HTTP server to become ready
301 fprintf(stderr, "[query] Waiting for HTTP server to be ready...\n");
302 if (!wait_for_http_ready(preferred_port, HEALTH_CHECK_TIMEOUT_NS)) {
303 fprintf(stderr, "[query] Timeout waiting for query server to start\n");
305 return -1;
306 }
307
308 g_query_active = true;
309 g_query_port = preferred_port;
310
311 fprintf(stderr, "[query] Query server ready at http://localhost:%d\n", preferred_port);
312 return preferred_port;
313}
314
315void query_destroy(void) {
316 if (!g_query_active && g_controller_pid <= 0) {
317 return;
318 }
319
320 fprintf(stderr, "[query] Shutting down query server...\n");
321
322#ifdef _WIN32
323 if (g_controller_handle != NULL) {
324 // Send termination signal
325 TerminateProcess(g_controller_handle, 0);
326
327 // Wait for process to exit (with timeout)
328 WaitForSingleObject(g_controller_handle, 3000);
329
330 CloseHandle(g_controller_handle);
331 g_controller_handle = NULL;
332 g_controller_pid = 0;
333 }
334
335 // Clean up Winsock (matches WSAStartup in query_init)
336 WSACleanup();
337#else
338 if (g_controller_pid > 0) {
339 // Send SIGTERM for graceful shutdown
340 kill(g_controller_pid, SIGTERM);
341
342 // Wait for process to exit (with timeout)
343 int status;
344 int wait_count = 0;
345 while (wait_count < 30) { // 3 second timeout
346 pid_t result = waitpid(g_controller_pid, &status, WNOHANG);
347 if (result == g_controller_pid) {
348 break;
349 }
350 if (result < 0) {
351 break;
352 }
353 usleep(100 * US_PER_MS_INT); // 100ms
354 wait_count++;
355 }
356
357 // If still running, force kill
358 if (wait_count >= 30) {
359 kill(g_controller_pid, SIGKILL);
360 waitpid(g_controller_pid, &status, 0);
361 }
362
363 g_controller_pid = -1;
364 }
365#endif
366
367 g_query_active = false;
368 g_query_port = -1;
369
370 fprintf(stderr, "[query] Query server stopped\n");
371}
372
373bool query_is_active(void) {
374 if (!g_query_active) {
375 return false;
376 }
377
378// Verify the controller is still running
379#ifdef _WIN32
380 if (g_controller_handle != NULL) {
381 DWORD exit_code;
382 if (GetExitCodeProcess(g_controller_handle, &exit_code)) {
383 if (exit_code != STILL_ACTIVE) {
384 g_query_active = false;
385 g_controller_handle = NULL;
386 g_controller_pid = 0;
387 return false;
388 }
389 }
390 }
391#else
392 if (g_controller_pid > 0) {
393 // Check if process is still alive
394 if (kill(g_controller_pid, 0) != 0) {
395 g_query_active = false;
396 g_controller_pid = -1;
397 return false;
398 }
399 }
400#endif
401
402 return g_query_active;
403}
404
405int query_get_port(void) {
406 return g_query_port;
407}
int buffer_size
Size of circular buffer.
Definition grep.c:84
int query_init(int preferred_port)
Definition query.c:214
void query_destroy(void)
Definition query.c:315
bool query_is_active(void)
Definition query.c:373
int query_get_port(void)
Definition query.c:405
#define HEALTH_CHECK_TIMEOUT_NS
Definition query.c:40
#define HEALTH_CHECK_INTERVAL_NS
Definition query.c:41
#define HEALTH_CHECK_CONNECT_TIMEOUT_NS
Definition query.c:42
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64