ascii-chat 0.8.38
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 <ascii-chat/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), static_cast<uint32_t>(actual_expand));
274
275 if (!var) {
276 JsonObject obj;
277 obj.set("status", "error");
278 obj.set("error", "not_found");
279 obj.set("message", "Variable '" + name + "' not found");
280 obj.set("stopped", is_stopped);
281 return HttpResponse::json(obj.toString());
282 }
283
284 JsonObject obj;
285 obj.set("status", "ok");
286 obj.set("stopped", is_stopped);
287 obj.set("result", variableToJson(*var, actual_expand > 0 ? actual_expand : 3));
288 return HttpResponse::json(obj.toString());
289 });
290
291 // GET /breakpoints - List breakpoints
292 server.addRoute("GET", "/breakpoints", [&controller](const HttpRequest &) {
293 auto breakpoints = controller.getBreakpoints();
294 JsonArray arr;
295 for (const auto &bp : breakpoints) {
296 arr.add(breakpointToJson(bp));
297 }
298 JsonObject obj;
299 obj.set("count", static_cast<int64_t>(breakpoints.size()));
300 obj.set("breakpoints", arr);
301 return HttpResponse::json(obj.toString());
302 });
303
304 // POST /breakpoints - Set breakpoint
305 server.addRoute("POST", "/breakpoints", [&controller](const HttpRequest &req) {
306 std::string file = req.param("file");
307 int line = req.paramInt("line", 0);
308 std::string condition = req.param("condition");
309
310 if (file.empty() || line <= 0) {
311 return HttpResponse::badRequest("Missing 'file' and 'line' parameters");
312 }
313
314 int bp_id = controller.setBreakpoint(file, static_cast<uint32_t>(line), condition);
315 if (bp_id < 0) {
316 JsonObject obj;
317 obj.set("status", "error");
318 obj.set("message", controller.lastError());
319 return HttpResponse::json(obj.toString());
320 }
321
322 auto bp = controller.getBreakpoint(bp_id);
323 JsonObject obj;
324 obj.set("status", "ok");
325 if (bp) {
326 obj.set("breakpoint", breakpointToJson(*bp));
327 }
328 return HttpResponse::json(obj.toString());
329 });
330
331 // DELETE /breakpoints - Remove breakpoint
332 server.addRoute("DELETE", "/breakpoints", [&controller](const HttpRequest &req) {
333 int bp_id = req.paramInt("id", -1);
334 if (bp_id < 0) {
335 return HttpResponse::badRequest("Missing 'id' parameter");
336 }
337 bool removed = controller.removeBreakpoint(static_cast<int32_t>(bp_id));
338 JsonObject obj;
339 obj.set("status", removed ? "ok" : "error");
340 if (!removed) {
341 obj.set("message", "Breakpoint not found");
342 }
343 return HttpResponse::json(obj.toString());
344 });
345
346 // POST /continue - Resume execution
347 server.addRoute("POST", "/continue", [&controller](const HttpRequest &) {
348 bool resumed = controller.resume();
349 JsonObject obj;
350 obj.set("status", resumed ? "running" : "error");
351 if (!resumed) {
352 obj.set("error", controller.lastError());
353 }
354 return HttpResponse::json(obj.toString());
355 });
356
357 // POST /stop - Stop execution
358 server.addRoute("POST", "/stop", [&controller](const HttpRequest &) {
359 bool stopped = controller.stop();
360 JsonObject obj;
361 obj.set("status", stopped ? "stopped" : "error");
362 if (!stopped) {
363 obj.set("error", controller.lastError());
364 }
365 return HttpResponse::json(obj.toString());
366 });
367
368 // POST /step - Single step
369 server.addRoute("POST", "/step", [&controller](const HttpRequest &req) {
370 bool over = req.hasParam("over");
371 bool out = req.hasParam("out");
372 bool success = out ? controller.stepOut() : (over ? controller.stepOver() : controller.stepInto());
373 JsonObject obj;
374 obj.set("status", success ? "ok" : "error");
375 if (!success) {
376 obj.set("error", controller.lastError());
377 }
378 return HttpResponse::json(obj.toString());
379 });
380
381 // POST /detach - Detach from process
382 server.addRoute("POST", "/detach", [&controller](const HttpRequest &) {
383 controller.detach();
384 JsonObject obj;
385 obj.set("status", "detached");
386 return HttpResponse::json(obj.toString());
387 });
388}
389
390} // namespace
391
392int main(int argc, char *argv[]) {
393 // Parse command line arguments
394 pid_t attach_pid = 0;
395 std::string attach_name;
396 bool wait_for = false;
397 uint16_t port = 9999;
398
399 for (int i = 1; i < argc; i++) {
400 if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
401 printUsage(argv[0]);
402 return 0;
403 } else if (strcmp(argv[i], "--attach") == 0 && i + 1 < argc) {
404 // Parse PID using safe parsing utility
405 unsigned long pid_val = 0;
406 if (parse_ulong(argv[++i], &pid_val, 1, ULONG_MAX) != ASCIICHAT_OK) {
407 fprintf(stderr, "Error: Invalid PID value\n");
408 return 1;
409 }
410 attach_pid = static_cast<pid_t>(pid_val);
411 } else if (strcmp(argv[i], "--attach-name") == 0 && i + 1 < argc) {
412 attach_name = argv[++i];
413 } else if (strcmp(argv[i], "--wait") == 0) {
414 wait_for = true;
415 } else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
416 // Parse port using safe parsing utility with proper range validation
417 if (parse_port(argv[++i], &port) != ASCIICHAT_OK) {
418 fprintf(stderr, "Error: Invalid port number (must be 1-65535)\n");
419 return 1;
420 }
421 } else {
422 fprintf(stderr, "Unknown option: %s\n", argv[i]);
423 printUsage(argv[0]);
424 return 1;
425 }
426 }
427
428 // Validate arguments
429 if (attach_pid == 0 && attach_name.empty()) {
430 fprintf(stderr, "Error: Must specify --attach <pid> or --attach-name <name>\n\n");
431 printUsage(argv[0]);
432 return 1;
433 }
434
435 // Setup signal handlers
436 signal(SIGINT, signalHandler);
437 signal(SIGTERM, signalHandler);
438#ifndef _WIN32
439 signal(SIGPIPE, SIG_IGN);
440#endif
441
442 // Initialize LLDB controller
444 g_controller = &controller;
445
446 if (!controller.initialize()) {
447 fprintf(stderr, "Error: Failed to initialize LLDB: %s\n", controller.lastError().c_str());
448 return 1;
449 }
450
451 // Attach to target
452 fprintf(stderr, "[ascii-query-server] Attaching to ");
453 if (attach_pid > 0) {
454 fprintf(stderr, "PID %llu...\n", static_cast<unsigned long long>(attach_pid));
455 if (!controller.attach(attach_pid)) {
456 fprintf(stderr, "Error: Failed to attach: %s\n", controller.lastError().c_str());
457 return 1;
458 }
459 } else {
460 fprintf(stderr, "process '%s'%s...\n", attach_name.c_str(), wait_for ? " (waiting)" : "");
461 if (!controller.attachByName(attach_name, wait_for)) {
462 fprintf(stderr, "Error: Failed to attach: %s\n", controller.lastError().c_str());
463 return 1;
464 }
465 }
466
467 fprintf(stderr, "[ascii-query-server] Attached to %s (PID %llu)\n", controller.targetName().c_str(),
468 static_cast<unsigned long long>(controller.targetPid()));
469
470 // Resume the target (it's stopped after attach)
471 if (controller.state() == ascii_query::ProcessState::Stopped) {
472 fprintf(stderr, "[ascii-query-server] Resuming target...\n");
473 controller.resume();
474 }
475
476 // Setup HTTP server
478 g_server = &server;
479
480 setupRoutes(server, controller);
481
482 // Start HTTP server
483 if (!server.start(port)) {
484 fprintf(stderr, "Error: Failed to start HTTP server: %s\n", server.lastError().c_str());
485 controller.detach();
486 return 1;
487 }
488
489 fprintf(stderr, "[ascii-query-server] HTTP server listening on http://localhost:%d\n", port);
490 fprintf(stderr, "[ascii-query-server] Press Ctrl+C to stop\n");
491
492 // Wait for shutdown
493 while (!g_shutdown_requested && controller.isAttached()) {
494#ifdef _WIN32
495 Sleep(100);
496#else
497 usleep(100000);
498#endif
499 ascii_query::ProcessState state = controller.state();
501 fprintf(stderr, "[ascii-query-server] Target %s, shutting down\n",
502 state == ascii_query::ProcessState::Exited ? "exited" : "crashed");
503 break;
504 }
505 }
506
507 // Cleanup
508 fprintf(stderr, "[ascii-query-server] Shutting down...\n");
509 server.stop();
510 controller.detach();
511 controller.shutdown();
512
513 fprintf(stderr, "[ascii-query-server] Done\n");
514 return 0;
515}
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
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:392
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)
Definition parsing.c:139
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
Definition parsing.c:251
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.