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