ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
15#include "http_server.h"
16#include "json.h"
17#include "lldb_controller.h"
18#include "util/parsing.h"
19
20#include <csignal>
21#include <cstdio>
22#include <cstdlib>
23#include <cstring>
24#include <string>
25#include <climits>
26
27#ifdef _WIN32
28#include <windows.h>
29#else
30#include <unistd.h>
31#endif
32
33namespace {
34
35// Global controller and server (for signal handling)
36ascii_query::LLDBController *g_controller = nullptr;
37ascii_query::HttpServer *g_server = nullptr;
38volatile sig_atomic_t g_shutdown_requested = 0;
39
40void signalHandler(int sig) {
41 (void)sig;
42 g_shutdown_requested = 1;
43 if (g_server) {
44 g_server->stop();
45 }
46}
47
48void printUsage(const char *program) {
49 fprintf(stderr, "Usage: %s [options]\n\n", program);
50 fprintf(stderr, "Options:\n");
51 fprintf(stderr, " --attach <pid> Attach to process by PID\n");
52 fprintf(stderr, " --attach-name <name> Attach to process by name\n");
53 fprintf(stderr, " --wait Wait for process to start (with --attach-name)\n");
54 fprintf(stderr, " --port <port> HTTP server port (default: 9999)\n");
55 fprintf(stderr, " --help Show this help\n");
56 fprintf(stderr, "\nExamples:\n");
57 fprintf(stderr, " %s --attach 12345 --port 9999\n", program);
58 fprintf(stderr, " %s --attach-name ascii-chat --wait\n", program);
59 fprintf(stderr, "\nQuery endpoints:\n");
60 fprintf(stderr, " GET / Status page\n");
61 fprintf(stderr, " GET /process Process information\n");
62 fprintf(stderr, " GET /threads Thread list\n");
63 fprintf(stderr, " GET /frames Stack frames (when stopped)\n");
64 fprintf(stderr, " GET /query Query a variable\n");
65 fprintf(stderr, " POST /continue Resume execution\n");
66 fprintf(stderr, " POST /step Single step\n");
67 fprintf(stderr, " POST /detach Detach from process\n");
68}
69
70// Convert ProcessState to string
71const char *processStateToString(ascii_query::ProcessState state) {
72 switch (state) {
74 return "invalid";
76 return "running";
78 return "stopped";
80 return "exited";
82 return "crashed";
84 return "detached";
85 }
86 return "unknown";
87}
88
89// Build JSON for ThreadInfo
92 obj.set("id", thread.id);
93 obj.set("index", static_cast<int64_t>(thread.index));
94 obj.set("name", thread.name);
95 obj.set("stop_reason", thread.stop_reason);
96 obj.set("function", thread.function);
97 obj.set("file", thread.file);
98 obj.set("line", static_cast<int64_t>(thread.line));
99 obj.set("selected", thread.is_selected);
100 return obj;
101}
102
103// Build JSON for FrameInfo
106 obj.set("index", static_cast<int64_t>(frame.index));
107 obj.set("function", frame.function);
108 obj.set("file", frame.file);
109 obj.set("line", static_cast<int64_t>(frame.line));
110 obj.set("pc", frame.pc);
111 obj.set("fp", frame.fp);
112 return obj;
113}
114
115// Build JSON for VariableInfo
116ascii_query::json::JsonObject variableToJson(const ascii_query::VariableInfo &var, int max_depth = 3) {
118 obj.set("name", var.name);
119 obj.set("type", var.type);
120 obj.set("value", var.value);
121 if (!var.summary.empty()) {
122 obj.set("summary", var.summary);
123 }
124 obj.set("address", var.address);
125 obj.set("size", static_cast<int64_t>(var.size));
126 obj.set("valid", var.is_valid);
127 obj.set("pointer", var.is_pointer);
128 obj.set("aggregate", var.is_aggregate);
129
130 if (!var.children.empty() && max_depth > 0) {
132 for (const auto &child : var.children) {
133 children.add(variableToJson(child, max_depth - 1));
134 }
135 obj.set("children", children);
136 }
137
138 return obj;
139}
140
141// Build JSON for BreakpointInfo
144 obj.set("id", static_cast<int64_t>(bp.id));
145 obj.set("file", bp.file);
146 obj.set("line", static_cast<int64_t>(bp.line));
147 obj.set("condition", bp.condition);
148 obj.set("hit_count", static_cast<int64_t>(bp.hit_count));
149 obj.set("enabled", bp.enabled);
150 obj.set("resolved", bp.resolved);
151 return obj;
152}
153
154// Setup HTTP routes
155void setupRoutes(ascii_query::HttpServer &server, ascii_query::LLDBController &controller) {
156 using namespace ascii_query;
157 using namespace ascii_query::json;
158
159 // GET / - Status page (plain text for terminal users)
160 server.addRoute("GET", "/", [&controller](const HttpRequest &) {
161 ProcessState state = controller.state();
162 std::string state_str = processStateToString(state);
163
164 std::string text = "ascii-query-server\n";
165 text += "==================\n\n";
166 text += "Target PID: " + std::to_string(controller.targetPid()) + "\n";
167 text += "Target Name: " + controller.targetName() + "\n";
168 text += "State: " + state_str + "\n";
169 text += "\n";
170 text += "Endpoints:\n";
171 text += " GET / Status page (this)\n";
172 text += " GET /process Process information (JSON)\n";
173 text += " GET /threads Thread list (JSON)\n";
174 text += " GET /frames Stack frames when stopped (JSON)\n";
175 text += " GET /query?name=VAR Query a variable (JSON)\n";
176 text += " GET /breakpoints List breakpoints (JSON)\n";
177 text += " POST /continue Resume execution\n";
178 text += " POST /stop Stop execution\n";
179 text += " POST /step Single step (add ?over or ?out)\n";
180 text += " POST /detach Detach from process\n";
181 text += "\n";
182 text += "Examples:\n";
183 text += " curl localhost:9999/process\n";
184 text += " curl localhost:9999/query?name=frame.width\n";
185 text += " curl 'localhost:9999/query?file=server.c&line=100&name=client_count&break'\n";
186 text += " curl -X POST localhost:9999/continue\n";
187
188 return HttpResponse::text(text);
189 });
190
191 // GET /process - Process information
192 server.addRoute("GET", "/process", [&controller](const HttpRequest &) {
193 JsonObject obj;
194 obj.set("pid", static_cast<int64_t>(controller.targetPid()));
195 obj.set("name", controller.targetName());
196 obj.set("state", processStateToString(controller.state()));
197 obj.set("attached", controller.isAttached());
198 if (!controller.lastError().empty()) {
199 obj.set("last_error", controller.lastError());
200 }
201 return HttpResponse::json(obj.toString());
202 });
203
204 // GET /threads - Thread list
205 server.addRoute("GET", "/threads", [&controller](const HttpRequest &) {
206 auto threads = controller.getThreads();
207 JsonArray arr;
208 for (const auto &thread : threads) {
209 arr.add(threadToJson(thread));
210 }
211 JsonObject obj;
212 obj.set("count", static_cast<int64_t>(threads.size()));
213 obj.set("threads", arr);
214 return HttpResponse::json(obj.toString());
215 });
216
217 // GET /frames - Stack frames
218 server.addRoute("GET", "/frames", [&controller](const HttpRequest &req) {
219 int max_frames = req.paramInt("max", 50);
220 auto frames = controller.getFrames(static_cast<uint32_t>(max_frames));
221 JsonArray arr;
222 for (const auto &frame : frames) {
223 arr.add(frameToJson(frame));
224 }
225 JsonObject obj;
226 obj.set("count", static_cast<int64_t>(frames.size()));
227 obj.set("frames", arr);
228 return HttpResponse::json(obj.toString());
229 });
230
231 // GET /query - Query a variable
232 server.addRoute("GET", "/query", [&controller](const HttpRequest &req) {
233 std::string file = req.param("file");
234 int line = req.paramInt("line", 0);
235 std::string name = req.param("name");
236 int frame_index = req.paramInt("frame", 0);
237 int expand_depth = req.paramInt("depth", 0);
238 bool should_break = req.hasParam("break");
239 int timeout_ms = req.paramInt("timeout", 5000);
240
241 if (name.empty()) {
242 return HttpResponse::badRequest("Missing 'name' parameter");
243 }
244
245 ProcessState state = controller.state();
246 bool is_stopped = (state == ProcessState::Stopped);
247
248 // If we have file:line and need to break, set breakpoint
249 if (!file.empty() && line > 0 && should_break && !is_stopped) {
250 int bp_id = controller.setBreakpoint(file, static_cast<uint32_t>(line));
251 if (bp_id < 0) {
252 JsonObject obj;
253 obj.set("status", "error");
254 obj.set("error", "breakpoint_failed");
255 obj.set("message", "Failed to set breakpoint at " + file + ":" + std::to_string(line));
256 return HttpResponse::json(obj.toString());
257 }
258
259 bool hit = controller.waitForBreakpoint(static_cast<uint32_t>(timeout_ms));
260 if (!hit) {
261 controller.removeBreakpoint(bp_id);
262 JsonObject obj;
263 obj.set("status", "error");
264 obj.set("error", "timeout");
265 obj.set("message", "Breakpoint not hit within " + std::to_string(timeout_ms) + "ms");
266 return HttpResponse::json(obj.toString());
267 }
268 is_stopped = true;
269 }
270
271 // Read the variable
272 int actual_expand = expand_depth > 0 ? expand_depth : (req.hasParam("expand") ? 3 : 0);
273 auto var = controller.readVariable(name, static_cast<uint32_t>(frame_index),
274 static_cast<uint32_t>(actual_expand));
275
276 if (!var) {
277 JsonObject obj;
278 obj.set("status", "error");
279 obj.set("error", "not_found");
280 obj.set("message", "Variable '" + name + "' not found");
281 obj.set("stopped", is_stopped);
282 return HttpResponse::json(obj.toString());
283 }
284
285 JsonObject obj;
286 obj.set("status", "ok");
287 obj.set("stopped", is_stopped);
288 obj.set("result", variableToJson(*var, actual_expand > 0 ? actual_expand : 3));
289 return HttpResponse::json(obj.toString());
290 });
291
292 // GET /breakpoints - List breakpoints
293 server.addRoute("GET", "/breakpoints", [&controller](const HttpRequest &) {
294 auto breakpoints = controller.getBreakpoints();
295 JsonArray arr;
296 for (const auto &bp : breakpoints) {
297 arr.add(breakpointToJson(bp));
298 }
299 JsonObject obj;
300 obj.set("count", static_cast<int64_t>(breakpoints.size()));
301 obj.set("breakpoints", arr);
302 return HttpResponse::json(obj.toString());
303 });
304
305 // POST /breakpoints - Set breakpoint
306 server.addRoute("POST", "/breakpoints", [&controller](const HttpRequest &req) {
307 std::string file = req.param("file");
308 int line = req.paramInt("line", 0);
309 std::string condition = req.param("condition");
310
311 if (file.empty() || line <= 0) {
312 return HttpResponse::badRequest("Missing 'file' and 'line' parameters");
313 }
314
315 int bp_id = controller.setBreakpoint(file, static_cast<uint32_t>(line), condition);
316 if (bp_id < 0) {
317 JsonObject obj;
318 obj.set("status", "error");
319 obj.set("message", controller.lastError());
320 return HttpResponse::json(obj.toString());
321 }
322
323 auto bp = controller.getBreakpoint(bp_id);
324 JsonObject obj;
325 obj.set("status", "ok");
326 if (bp) {
327 obj.set("breakpoint", breakpointToJson(*bp));
328 }
329 return HttpResponse::json(obj.toString());
330 });
331
332 // DELETE /breakpoints - Remove breakpoint
333 server.addRoute("DELETE", "/breakpoints", [&controller](const HttpRequest &req) {
334 int bp_id = req.paramInt("id", -1);
335 if (bp_id < 0) {
336 return HttpResponse::badRequest("Missing 'id' parameter");
337 }
338 bool removed = controller.removeBreakpoint(static_cast<int32_t>(bp_id));
339 JsonObject obj;
340 obj.set("status", removed ? "ok" : "error");
341 if (!removed) {
342 obj.set("message", "Breakpoint not found");
343 }
344 return HttpResponse::json(obj.toString());
345 });
346
347 // POST /continue - Resume execution
348 server.addRoute("POST", "/continue", [&controller](const HttpRequest &) {
349 bool resumed = controller.resume();
350 JsonObject obj;
351 obj.set("status", resumed ? "running" : "error");
352 if (!resumed) {
353 obj.set("error", controller.lastError());
354 }
355 return HttpResponse::json(obj.toString());
356 });
357
358 // POST /stop - Stop execution
359 server.addRoute("POST", "/stop", [&controller](const HttpRequest &) {
360 bool stopped = controller.stop();
361 JsonObject obj;
362 obj.set("status", stopped ? "stopped" : "error");
363 if (!stopped) {
364 obj.set("error", controller.lastError());
365 }
366 return HttpResponse::json(obj.toString());
367 });
368
369 // POST /step - Single step
370 server.addRoute("POST", "/step", [&controller](const HttpRequest &req) {
371 bool over = req.hasParam("over");
372 bool out = req.hasParam("out");
373 bool success = out ? controller.stepOut() : (over ? controller.stepOver() : controller.stepInto());
374 JsonObject obj;
375 obj.set("status", success ? "ok" : "error");
376 if (!success) {
377 obj.set("error", controller.lastError());
378 }
379 return HttpResponse::json(obj.toString());
380 });
381
382 // POST /detach - Detach from process
383 server.addRoute("POST", "/detach", [&controller](const HttpRequest &) {
384 controller.detach();
385 JsonObject obj;
386 obj.set("status", "detached");
387 return HttpResponse::json(obj.toString());
388 });
389}
390
391} // namespace
392
393int main(int argc, char *argv[]) {
394 // Parse command line arguments
395 pid_t attach_pid = 0;
396 std::string attach_name;
397 bool wait_for = false;
398 uint16_t port = 9999;
399
400 for (int i = 1; i < argc; i++) {
401 if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
402 printUsage(argv[0]);
403 return 0;
404 } else if (strcmp(argv[i], "--attach") == 0 && i + 1 < argc) {
405 // Parse PID using safe parsing utility
406 unsigned long pid_val = 0;
407 if (parse_ulong(argv[++i], &pid_val, 1, ULONG_MAX) != ASCIICHAT_OK) {
408 fprintf(stderr, "Error: Invalid PID value\n");
409 return 1;
410 }
411 attach_pid = static_cast<pid_t>(pid_val);
412 } else if (strcmp(argv[i], "--attach-name") == 0 && i + 1 < argc) {
413 attach_name = argv[++i];
414 } else if (strcmp(argv[i], "--wait") == 0) {
415 wait_for = true;
416 } else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
417 // Parse port using safe parsing utility with proper range validation
418 if (parse_port(argv[++i], &port) != ASCIICHAT_OK) {
419 fprintf(stderr, "Error: Invalid port number (must be 1-65535)\n");
420 return 1;
421 }
422 } else {
423 fprintf(stderr, "Unknown option: %s\n", argv[i]);
424 printUsage(argv[0]);
425 return 1;
426 }
427 }
428
429 // Validate arguments
430 if (attach_pid == 0 && attach_name.empty()) {
431 fprintf(stderr, "Error: Must specify --attach <pid> or --attach-name <name>\n\n");
432 printUsage(argv[0]);
433 return 1;
434 }
435
436 // Setup signal handlers
437 signal(SIGINT, signalHandler);
438 signal(SIGTERM, signalHandler);
439#ifndef _WIN32
440 signal(SIGPIPE, SIG_IGN);
441#endif
442
443 // Initialize LLDB controller
445 g_controller = &controller;
446
447 if (!controller.initialize()) {
448 fprintf(stderr, "Error: Failed to initialize LLDB: %s\n", controller.lastError().c_str());
449 return 1;
450 }
451
452 // Attach to target
453 fprintf(stderr, "[ascii-query-server] Attaching to ");
454 if (attach_pid > 0) {
455 fprintf(stderr, "PID %llu...\n", static_cast<unsigned long long>(attach_pid));
456 if (!controller.attach(attach_pid)) {
457 fprintf(stderr, "Error: Failed to attach: %s\n", controller.lastError().c_str());
458 return 1;
459 }
460 } else {
461 fprintf(stderr, "process '%s'%s...\n", attach_name.c_str(), wait_for ? " (waiting)" : "");
462 if (!controller.attachByName(attach_name, wait_for)) {
463 fprintf(stderr, "Error: Failed to attach: %s\n", controller.lastError().c_str());
464 return 1;
465 }
466 }
467
468 fprintf(stderr, "[ascii-query-server] Attached to %s (PID %llu)\n", controller.targetName().c_str(),
469 static_cast<unsigned long long>(controller.targetPid()));
470
471 // Resume the target (it's stopped after attach)
472 if (controller.state() == ascii_query::ProcessState::Stopped) {
473 fprintf(stderr, "[ascii-query-server] Resuming target...\n");
474 controller.resume();
475 }
476
477 // Setup HTTP server
479 g_server = &server;
480
481 setupRoutes(server, controller);
482
483 // Start HTTP server
484 if (!server.start(port)) {
485 fprintf(stderr, "Error: Failed to start HTTP server: %s\n", server.lastError().c_str());
486 controller.detach();
487 return 1;
488 }
489
490 fprintf(stderr, "[ascii-query-server] HTTP server listening on http://localhost:%d\n", port);
491 fprintf(stderr, "[ascii-query-server] Press Ctrl+C to stop\n");
492
493 // Wait for shutdown
494 while (!g_shutdown_requested && controller.isAttached()) {
495#ifdef _WIN32
496 Sleep(100);
497#else
498 usleep(100000);
499#endif
500 ascii_query::ProcessState state = controller.state();
502 fprintf(stderr, "[ascii-query-server] Target %s, shutting down\n",
503 state == ascii_query::ProcessState::Exited ? "exited" : "crashed");
504 break;
505 }
506 }
507
508 // Cleanup
509 fprintf(stderr, "[ascii-query-server] Shutting down...\n");
510 server.stop();
511 controller.detach();
512 controller.shutdown();
513
514 fprintf(stderr, "[ascii-query-server] Done\n");
515 return 0;
516}
Simple single-threaded HTTP server.
void addRoute(const std::string &method, const std::string &path, RouteHandler handler)
Add a route handler.
bool start(uint16_t port)
Start the server.
const std::string & lastError() const
Get the last error message.
void stop()
Stop the server.
LLDB process controller.
int32_t setBreakpoint(const std::string &file, uint32_t line, const std::string &condition="")
Set a breakpoint at file:line.
bool attachByName(const std::string &process_name, bool wait_for=false)
Attach to a process by name.
bool removeBreakpoint(int32_t breakpoint_id)
Remove a breakpoint.
bool stepInto()
Single step the current thread (step into)
pid_t targetPid() const
Get the PID of the attached process.
std::string targetName() const
Get the name of the attached process.
std::optional< VariableInfo > readVariable(const std::string &name, uint32_t frame_index=0, uint32_t expand_depth=0) const
Read a variable from the current frame.
const std::string & lastError() const
Get the last error message.
bool initialize()
Initialize LLDB. Must be called before any other methods.
void shutdown()
Shutdown LLDB and release resources.
bool stop()
Stop the target process.
bool isAttached() const
Check if attached to a process.
std::vector< ThreadInfo > getThreads() const
Get list of all threads.
std::vector< BreakpointInfo > getBreakpoints() const
Get all breakpoints.
bool resume()
Resume the target process.
std::vector< FrameInfo > getFrames(uint32_t max_frames=0) const
Get stack frames for the selected thread.
bool stepOver()
Step over the current line.
std::optional< BreakpointInfo > getBreakpoint(int32_t breakpoint_id) const
Get information about a specific breakpoint.
bool stepOut()
Step out of the current function.
void detach()
Detach from the current process.
bool attach(pid_t pid)
Attach to a process by PID.
ProcessState state() const
Get the current process state.
bool waitForBreakpoint(uint32_t timeout_ms=0)
Wait for any breakpoint to be hit.
JSON array builder.
Definition json.h:146
JsonArray & add(const JsonValue &value)
Definition json.h:150
JSON object builder.
Definition json.h:189
std::string toString() const
Definition json.h:211
JsonObject & set(const std::string &key, const JsonValue &value)
Definition json.h:193
unsigned short uint16_t
Definition common.h:57
unsigned int uint32_t
Definition common.h:58
@ ASCIICHAT_OK
Definition error_codes.h:48
Minimal single-threaded HTTP/1.1 server.
Minimal JSON serialization helpers (header-only)
LLDB process attachment and control wrapper.
int main(int argc, char *argv[])
Definition main.cpp:393
ProcessState
Process state enumeration.
@ Exited
Process has exited.
@ Crashed
Process crashed.
@ Detached
Detached from process.
@ Invalid
No valid process attached.
@ Running
Process is running normally.
@ Stopped
Process is stopped (breakpoint, signal, etc.)
asciichat_error_t parse_ulong(const char *str, unsigned long *out_value, unsigned long min_value, unsigned long max_value)
Parse unsigned long integer with range validation.
Definition parsing.c:137
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
Parse port number (1-65535) from string.
Definition parsing.c:221
🔍 Safe Parsing Utilities
Breakpoint information.
int32_t id
LLDB breakpoint ID.
std::string condition
Condition expression (may be empty)
bool enabled
Is breakpoint enabled?
uint32_t line
Line number.
bool resolved
Is breakpoint resolved (has valid location)?
uint32_t hit_count
Number of times hit.
std::string file
Source file.
Stack frame information.
uint32_t line
Line number.
std::string function
Function name.
uint64_t pc
Program counter.
uint64_t fp
Frame pointer.
uint32_t index
Frame index (0 = innermost)
std::string file
Source file.
Parsed HTTP request.
Definition http_server.h:37
int paramInt(const std::string &name, int default_value=0) const
Get a query parameter as integer.
Definition http_server.h:71
std::string param(const std::string &name, const std::string &default_value="") const
Get a query parameter value.
Definition http_server.h:51
bool hasParam(const std::string &name) const
Check if a query parameter exists (flag-style, like &break)
Definition http_server.h:61
Thread information.
std::string name
Thread name (may be empty)
uint32_t line
Current line number.
uint64_t id
Thread ID.
bool is_selected
Is this the selected thread?
std::string stop_reason
Why thread is stopped (if stopped)
std::string function
Current function name.
uint32_t index
Index in thread list.
std::string file
Current source file.
Variable information (from LLDB SBValue)
std::vector< VariableInfo > children
Struct members / array elements.
std::string value
Value as string.
bool is_aggregate
Whether this is struct/class/array.
size_t size
Size in bytes.
std::string type
Type name.
std::string name
Variable name.
bool is_pointer
Whether this is a pointer type.
std::string summary
LLDB summary (for complex types)
bool is_valid
Whether value could be read.
uint64_t address
Memory address.