18#include <ascii-chat/util/parsing.h>
38volatile sig_atomic_t g_shutdown_requested = 0;
40void signalHandler(
int sig) {
42 g_shutdown_requested = 1;
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");
92 obj.
set(
"id", thread.
id);
93 obj.
set(
"index",
static_cast<int64_t
>(thread.
index));
98 obj.
set(
"line",
static_cast<int64_t
>(thread.
line));
106 obj.
set(
"index",
static_cast<int64_t
>(frame.
index));
109 obj.
set(
"line",
static_cast<int64_t
>(frame.
line));
110 obj.
set(
"pc", frame.
pc);
111 obj.
set(
"fp", frame.
fp);
125 obj.
set(
"size",
static_cast<int64_t
>(var.
size));
130 if (!var.
children.empty() && max_depth > 0) {
132 for (
const auto &child : var.children) {
133 children.
add(variableToJson(child, max_depth - 1));
135 obj.
set(
"children", children);
144 obj.
set(
"id",
static_cast<int64_t
>(bp.
id));
146 obj.
set(
"line",
static_cast<int64_t
>(bp.
line));
148 obj.
set(
"hit_count",
static_cast<int64_t
>(bp.
hit_count));
162 std::string state_str = processStateToString(state);
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";
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";
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";
188 return HttpResponse::text(text);
194 obj.
set(
"pid",
static_cast<int64_t
>(controller.
targetPid()));
196 obj.
set(
"state", processStateToString(controller.
state()));
201 return HttpResponse::json(obj.
toString());
208 for (
const auto &thread : threads) {
209 arr.
add(threadToJson(thread));
212 obj.
set(
"count",
static_cast<int64_t
>(threads.size()));
213 obj.
set(
"threads", arr);
214 return HttpResponse::json(obj.
toString());
219 int max_frames = req.
paramInt(
"max", 50);
220 auto frames = controller.
getFrames(
static_cast<uint32_t
>(max_frames));
222 for (
const auto &frame : frames) {
223 arr.
add(frameToJson(frame));
226 obj.
set(
"count",
static_cast<int64_t
>(frames.size()));
227 obj.
set(
"frames", arr);
228 return HttpResponse::json(obj.
toString());
233 std::string file = req.
param(
"file");
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);
242 return HttpResponse::badRequest(
"Missing 'name' parameter");
246 bool is_stopped = (state == ProcessState::Stopped);
249 if (!file.empty() && line > 0 && should_break && !is_stopped) {
250 int bp_id = controller.
setBreakpoint(file,
static_cast<uint32_t
>(line));
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());
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());
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));
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());
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());
295 for (
const auto &bp : breakpoints) {
296 arr.
add(breakpointToJson(bp));
299 obj.
set(
"count",
static_cast<int64_t
>(breakpoints.size()));
300 obj.
set(
"breakpoints", arr);
301 return HttpResponse::json(obj.
toString());
306 std::string file = req.
param(
"file");
308 std::string condition = req.
param(
"condition");
310 if (file.empty() || line <= 0) {
311 return HttpResponse::badRequest(
"Missing 'file' and 'line' parameters");
314 int bp_id = controller.
setBreakpoint(file,
static_cast<uint32_t
>(line), condition);
317 obj.
set(
"status",
"error");
319 return HttpResponse::json(obj.
toString());
324 obj.
set(
"status",
"ok");
326 obj.
set(
"breakpoint", breakpointToJson(*bp));
328 return HttpResponse::json(obj.
toString());
335 return HttpResponse::badRequest(
"Missing 'id' parameter");
339 obj.
set(
"status", removed ?
"ok" :
"error");
341 obj.
set(
"message",
"Breakpoint not found");
343 return HttpResponse::json(obj.
toString());
348 bool resumed = controller.
resume();
350 obj.
set(
"status", resumed ?
"running" :
"error");
354 return HttpResponse::json(obj.
toString());
359 bool stopped = controller.
stop();
361 obj.
set(
"status", stopped ?
"stopped" :
"error");
365 return HttpResponse::json(obj.
toString());
374 obj.
set(
"status", success ?
"ok" :
"error");
378 return HttpResponse::json(obj.
toString());
385 obj.
set(
"status",
"detached");
386 return HttpResponse::json(obj.
toString());
392int main(
int argc,
char *argv[]) {
394 pid_t attach_pid = 0;
395 std::string attach_name;
396 bool wait_for =
false;
397 uint16_t port = 9999;
399 for (
int i = 1; i < argc; i++) {
400 if (strcmp(argv[i],
"--help") == 0 || strcmp(argv[i],
"-h") == 0) {
403 }
else if (strcmp(argv[i],
"--attach") == 0 && i + 1 < argc) {
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");
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) {
415 }
else if (strcmp(argv[i],
"--port") == 0 && i + 1 < argc) {
417 if (
parse_port(argv[++i], &port) != ASCIICHAT_OK) {
418 fprintf(stderr,
"Error: Invalid port number (must be 1-65535)\n");
422 fprintf(stderr,
"Unknown option: %s\n", argv[i]);
429 if (attach_pid == 0 && attach_name.empty()) {
430 fprintf(stderr,
"Error: Must specify --attach <pid> or --attach-name <name>\n\n");
436 signal(SIGINT, signalHandler);
437 signal(SIGTERM, signalHandler);
439 signal(SIGPIPE, SIG_IGN);
444 g_controller = &controller;
447 fprintf(stderr,
"Error: Failed to initialize LLDB: %s\n", controller.
lastError().c_str());
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());
460 fprintf(stderr,
"process '%s'%s...\n", attach_name.c_str(), wait_for ?
" (waiting)" :
"");
462 fprintf(stderr,
"Error: Failed to attach: %s\n", controller.
lastError().c_str());
467 fprintf(stderr,
"[ascii-query-server] Attached to %s (PID %llu)\n", controller.
targetName().c_str(),
468 static_cast<unsigned long long>(controller.
targetPid()));
472 fprintf(stderr,
"[ascii-query-server] Resuming target...\n");
480 setupRoutes(server, controller);
483 if (!server.
start(port)) {
484 fprintf(stderr,
"Error: Failed to start HTTP server: %s\n", server.
lastError().c_str());
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");
493 while (!g_shutdown_requested && controller.
isAttached()) {
501 fprintf(stderr,
"[ascii-query-server] Target %s, shutting down\n",
508 fprintf(stderr,
"[ascii-query-server] Shutting down...\n");
513 fprintf(stderr,
"[ascii-query-server] Done\n");
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.
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.
JsonArray & add(const JsonValue &value)
std::string toString() const
JsonObject & set(const std::string &key, const JsonValue &value)
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[])
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)
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
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.
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.
int paramInt(const std::string &name, int default_value=0) const
Get a query parameter as integer.
std::string param(const std::string &name, const std::string &default_value="") const
Get a query parameter value.
bool hasParam(const std::string &name) const
Check if a query parameter exists (flag-style, like &break)
std::string name
Thread name (may be empty)
uint32_t line
Current line number.
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.