Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobile-dev",
"version": "0.1.161",
"version": "0.1.162",
"description": "Keep a simulator beside the chat while building, running, or debugging iOS and Android apps. Stream devices, read native and Metro logs, analyze CPU and React profiles with TraceSift, render interactive CPU, memory, and FPS recordings, and control apps with Mobile Dev tools.",
"author": {
"name": "Callstack"
Expand Down
2 changes: 1 addition & 1 deletion docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
| `mobile_read_logs` | Read a log batch and source status |
| `mobile_logs_keep_alive` | Keep background collection alive without sending logs (app only) |
| `mobile_logs_close` | Stop a session's log readers |
| `mobile_crashes_session` | Watch one device's crash reports for the panel's crash toast; read and close it like a log session (app only) |
| `mobile_crashes_session` | Watch one simulator, connected iPhone, or Android device for crashes for the panel's crash toast; read and close it like a log session (app only) |
| `mobile_performance_sources` | Read running apps and foreground identity on the selected iOS or Android device |
| `mobile_cpu_session` | Connect a native process and thread CPU plus memory monitor |
| `mobile_read_cpu` | Read live CPU and memory samples and connection status |
Expand Down
2 changes: 2 additions & 0 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ Since 0.1.99, `ui.logs.query_parse` measures query compilation in milliseconds o

Since 0.1.161, each visible simulator panel opens an app-only crash session for its booted iOS simulator or Android device, independent of Logs; log sessions run the same crash watcher scoped to their app or process. `logs.crash.detected` counts crash records, with `device_platform` and `crash_source`: `ios_exit` for SpringBoard's immediate crash-signal exit line, `ios_log` for the app's own crash message read from the unified log about a second later (uncaught exceptions, Swift fatal errors, React Native fatal JS errors; none for an external signal), `ios_report` for the `.ips` report written 30-45 seconds later, and `android_logcat` for `FATAL EXCEPTION` blocks and libc fatal signals. One iOS crash therefore usually records several sources. `logs.crash.parse` measures `.ips` parsing in milliseconds, the only expensive parse. Both use the `logs` surface and are recorded per crash because crashes are rare. In the UI, `ui.crash.toast_shown`, `ui.crash.attached`, `ui.crash.sent_to_chat`, `ui.crash.dismissed`, `ui.crash.report_attached` (a stack arriving for a toast already shown) and `ui.crash.watch_retry` count toast activity and session recovery; chat delivery time stays under `ui.logs.send`. Crash sessions long-poll for up to 20 seconds and pause reads while the document is hidden, sending only keep-alives. Crash messages, stack traces, app IDs, process names, report paths and device IDs stay local.

Since 0.1.162, the crash session also covers a connected physical iPhone. A second relay stream from the bundled log reader, filtered to SpringBoard, gives the immediate `ios_exit`; the reader's `--crash-reports` mode then copies the phone's new `.ips` reports through `com.apple.crashreportmover` and `com.apple.crashreportcopymobile` every 3 seconds for up to 2 minutes, recording `ios_report` when one matches the exit's PID and bundle ID. Physical iPhones record no `ios_log`. iOS crash metrics now carry `device_kind` (`simulator` or `physical`); earlier iOS crash metrics were all simulators. `logs.crash.report_wait` measures, in milliseconds, how long a physical iPhone's report took to become readable after its exit was seen, so slow reports can be told apart from slow parsing. The report mode records no native timing, which keeps the reader's `connect` timing limited to log streaming becoming ready. Report contents stay local.

`ui.logs.session_expired` counts expired sessions that trigger automatic recovery. It uses the active Logs context without sending session IDs, error text or log content.

`ui.logs.retention` measures bounded buffer updates in milliseconds. `ui.logs.evicted` counts rows removed locally to meet the shared row and text limits; `ui.logs.dropped` still counts only rows lost from the server buffer. Existing filter timing and row gauges cover the resulting list. These measurements contain no log text or source metadata.
Expand Down
7 changes: 7 additions & 0 deletions native/ios-logs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ are capped at 16 KiB, sender paths at 4 KiB, and subsystem/category values at 1
UTC date and microseconds. An optional exact executable-name filter operates
before JSON serialization and survives PID changes across app restarts.

`--crash-reports <since-unix-ms>` replaces the executable name for a one-shot
report copy. It asks `com.apple.crashreportmover` to move pending reports, then
lists the `com.apple.crashreportcopymobile` directory over AFC and prints one
`{"name","modified","text"}` line per top-level `.ips` report modified at or after
that time in the phone's clock. Reports over 8 MiB are skipped, and nothing is
removed from the phone.

The reader needs an existing trusted USB or Wi-Fi connection. It does not launch
an app, attach a debugger, install an SDK, or change signing requirements. It
cannot capture ordinary stdout/stderr print statements or recover values that
Expand Down
182 changes: 152 additions & 30 deletions native/ios-logs/collector.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <libimobiledevice/afc.h>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
#include <libimobiledevice/ostrace.h>
Expand All @@ -18,6 +19,7 @@

#define MAX_PACKET (1024 * 1024)
#define MAX_MESSAGE 16384
#define MAX_REPORT (8 * 1024 * 1024)

static volatile sig_atomic_t stopping = 0;

Expand Down Expand Up @@ -214,36 +216,115 @@ static int emit_record(const char *bytes, uint32_t length, const char *process_f
return failed ? -1 : 0;
}

int main(int argc, char **argv) {
mobile_dev_telemetry_init("ios-logs");
double connected_at = mobile_dev_telemetry_now();
if (argc < 4 || argc > 5 || strcmp(argv[1], "--device") != 0
|| (strcmp(argv[3], "usb") != 0 && strcmp(argv[3], "network") != 0)) {
fprintf(stderr, "Usage: mobile-dev-ios-logs --device <hardware-udid> <usb|network> [executable-name]\n");
return 2;
// The mover replies "ping" once pending crash reports are in the directory the copy service reads.
static int move_crash_reports(idevice_t device, lockdownd_client_t lockdown) {
lockdownd_service_descriptor_t descriptor = NULL;
lockdownd_error_t started = lockdownd_start_service(lockdown, "com.apple.crashreportmover", &descriptor);
if (started != LOCKDOWN_E_SUCCESS) {
fprintf(stderr, "Cannot start the crash report mover: %s. Unlock the iPhone and retry.\n", lockdownd_strerror(started));
return -1;
}
signal(SIGTERM, stop);
signal(SIGINT, stop);
signal(SIGPIPE, stop);
setvbuf(stdout, NULL, _IOLBF, 0);
idevice_t device = NULL;
lockdownd_client_t lockdown = NULL;
service_client_t mover = NULL;
service_error_t connected = service_client_new(device, descriptor, &mover);
lockdownd_service_descriptor_free(descriptor);
if (connected != SERVICE_E_SUCCESS) {
fprintf(stderr, "Cannot open the crash report mover (%d).\n", connected);
return -1;
}
char ping[4] = { 0 };
uint32_t received = 0;
for (int attempt = 0; attempt < 10 && received < sizeof(ping) && stopping == 0; attempt++) {
uint32_t bytes = 0;
service_error_t result = service_receive_with_timeout(mover, ping + received, sizeof(ping) - received, &bytes, 2000);
received += bytes;
if (result != SERVICE_E_SUCCESS && result != SERVICE_E_TIMEOUT) break;
}
service_client_free(mover);
if (received == sizeof(ping) && memcmp(ping, "ping", sizeof(ping)) == 0) return 0;
fprintf(stderr, "The iPhone did not finish moving its crash reports.\n");
return -1;
}

static char *read_report(afc_client_t afc, const char *path, uint64_t size) {
uint64_t handle = 0;
if (afc_file_open(afc, path, AFC_FOPEN_RDONLY, &handle) != AFC_E_SUCCESS) return NULL;
char *text = malloc(size + 1);
uint64_t offset = 0;
while (text != NULL && offset < size) {
uint32_t bytes = 0;
afc_error_t result = afc_file_read(afc, handle, text + offset, (uint32_t)(size - offset), &bytes);
if (result != AFC_E_SUCCESS || bytes == 0) break;
offset += bytes;
}
afc_file_close(afc, handle);
if (text == NULL || offset != size) {
free(text);
return NULL;
}
text[size] = '\0';
return text;
}

// Print each .ips report modified at or after since_ms as {"name","modified","text"}.
static int copy_crash_reports(idevice_t device, lockdownd_client_t lockdown, uint64_t since_ms) {
lockdownd_service_descriptor_t descriptor = NULL;
service_client_t client = NULL;
int exit_code = 1;
enum idevice_options lookup = strcmp(argv[3], "network") == 0 ? IDEVICE_LOOKUP_NETWORK : IDEVICE_LOOKUP_USBMUX;
idevice_error_t found = idevice_new_with_options(&device, argv[2], lookup);
if (found != IDEVICE_E_SUCCESS) {
fprintf(stderr, "The selected iPhone is unavailable to libimobiledevice over %s (%d).\n", argv[3], found);
goto cleanup;
lockdownd_error_t started = lockdownd_start_service(lockdown, "com.apple.crashreportcopymobile", &descriptor);
if (started != LOCKDOWN_E_SUCCESS) {
fprintf(stderr, "Cannot open crash reports: %s. Unlock the iPhone and retry.\n", lockdownd_strerror(started));
return -1;
}
lockdownd_error_t paired = lockdownd_client_new_with_handshake(device, &lockdown, "mobile-dev-ios-logs");
if (paired != LOCKDOWN_E_SUCCESS) {
const char *description = lockdownd_strerror(paired);
fprintf(stderr, "Cannot connect to the iPhone: %s. Unlock it and trust this Mac.\n", description);
goto cleanup;
afc_client_t afc = NULL;
afc_error_t connected = afc_client_new(device, descriptor, &afc);
lockdownd_service_descriptor_free(descriptor);
if (connected != AFC_E_SUCCESS) {
fprintf(stderr, "Cannot read crash reports (%d).\n", connected);
return -1;
}
char **names = NULL;
afc_error_t listed = afc_read_directory(afc, "/", &names);
int result = listed == AFC_E_SUCCESS ? 0 : -1;
if (result != 0) fprintf(stderr, "Cannot list crash reports (%d).\n", listed);
for (size_t index = 0; result == 0 && names != NULL && names[index] != NULL && stopping == 0; index++) {
const char *name = names[index];
size_t length = strlen(name);
if (length < 5 || strcmp(name + length - 4, ".ips") != 0) continue;
char path[1024];
if (snprintf(path, sizeof(path), "/%s", name) >= (int)sizeof(path)) continue;
plist_t info = NULL;
if (afc_get_file_info_plist(afc, path, &info) != AFC_E_SUCCESS || info == NULL) continue;
const char *format = plist_get_string_ptr(plist_dict_get_item(info, "st_ifmt"), NULL);
int regular = format != NULL && strcmp(format, "S_IFREG") == 0;
uint64_t size = plist_dict_get_uint(info, "st_size");
uint64_t modified_ms = plist_dict_get_uint(info, "st_mtime") / 1000000;
plist_free(info);
if (regular == 0 || modified_ms < since_ms) continue;
if (size == 0 || size > MAX_REPORT) {
fprintf(stderr, "Skipping crash report %s: %llu bytes.\n", name, (unsigned long long)size);
continue;
}
char *text = read_report(afc, path, size);
if (text == NULL) {
fprintf(stderr, "Cannot read crash report %s.\n", name);
continue;
}
fputs("{\"name\":", stdout);
json_string(name, length, 1024);
fprintf(stdout, ",\"modified\":%llu", (unsigned long long)modified_ms);
json_field("text", text, MAX_REPORT);
fputs("}\n", stdout);
free(text);
if (ferror(stdout)) result = -1;
}
lockdownd_error_t started = lockdownd_start_service(lockdown, OSTRACE_SERVICE_NAME, &descriptor);
if (names != NULL) afc_dictionary_free(names);
afc_client_free(afc);
return result;
}

static int stream_logs(idevice_t device, lockdownd_client_t *lockdown, const char *filter, double connected_at) {
lockdownd_service_descriptor_t descriptor = NULL;
service_client_t client = NULL;
int result = -1;
lockdownd_error_t started = lockdownd_start_service(*lockdown, OSTRACE_SERVICE_NAME, &descriptor);
if (started != LOCKDOWN_E_SUCCESS) {
const char *description = lockdownd_strerror(started);
fprintf(stderr, "Cannot start unified logs: %s. Unlock the iPhone and retry.\n", description);
Expand All @@ -256,15 +337,14 @@ int main(int argc, char **argv) {
}
lockdownd_service_descriptor_free(descriptor);
descriptor = NULL;
lockdownd_client_free(lockdown);
lockdown = NULL;
int result = start_activity(client);
lockdownd_client_free(*lockdown);
*lockdown = NULL;
result = start_activity(client);
if (result != 0) goto cleanup;
puts("{\"ready\":true}");
double ready_at = mobile_dev_telemetry_now();
double connect_ms = ready_at - connected_at;
mobile_dev_telemetry_timing(MOBILE_DEV_CONNECT, connect_ms);
const char *filter = argc == 5 ? argv[4] : NULL;
while (stopping == 0) {
char *bytes = NULL;
uint32_t length = 0;
Expand All @@ -285,6 +365,48 @@ int main(int argc, char **argv) {
cleanup:
if (client != NULL) service_client_free(client);
if (descriptor != NULL) lockdownd_service_descriptor_free(descriptor);
return result;
}

int main(int argc, char **argv) {
mobile_dev_telemetry_init("ios-logs");
double connected_at = mobile_dev_telemetry_now();
int reports = argc == 6 && strcmp(argv[4], "--crash-reports") == 0;
char *since_end = NULL;
unsigned long long since_ms = reports ? strtoull(argv[5], &since_end, 10) : 0;
if (argc < 4 || argc > 6 || strcmp(argv[1], "--device") != 0
|| (strcmp(argv[3], "usb") != 0 && strcmp(argv[3], "network") != 0)
|| (argc == 6 && (reports == 0 || since_end == argv[5] || *since_end != '\0'))) {
fprintf(stderr, "Usage: mobile-dev-ios-logs --device <hardware-udid> <usb|network> [executable-name | --crash-reports <since-unix-ms>]\n");
return 2;
}
signal(SIGTERM, stop);
signal(SIGINT, stop);
signal(SIGPIPE, stop);
setvbuf(stdout, NULL, _IOLBF, 0);
idevice_t device = NULL;
lockdownd_client_t lockdown = NULL;
int exit_code = 1;
enum idevice_options lookup = strcmp(argv[3], "network") == 0 ? IDEVICE_LOOKUP_NETWORK : IDEVICE_LOOKUP_USBMUX;
idevice_error_t found = idevice_new_with_options(&device, argv[2], lookup);
if (found != IDEVICE_E_SUCCESS) {
fprintf(stderr, "The selected iPhone is unavailable to libimobiledevice over %s (%d).\n", argv[3], found);
goto cleanup;
}
lockdownd_error_t paired = lockdownd_client_new_with_handshake(device, &lockdown, "mobile-dev-ios-logs");
if (paired != LOCKDOWN_E_SUCCESS) {
const char *description = lockdownd_strerror(paired);
fprintf(stderr, "Cannot connect to the iPhone: %s. Unlock it and trust this Mac.\n", description);
goto cleanup;
}
if (reports) {
int moved = move_crash_reports(device, lockdown);
if (moved == 0 && copy_crash_reports(device, lockdown, since_ms) == 0) exit_code = 0;
} else {
const char *filter = argc == 5 ? argv[4] : NULL;
stream_logs(device, &lockdown, filter, connected_at);
}
cleanup:
if (lockdown != NULL) lockdownd_client_free(lockdown);
if (device != NULL) idevice_free(device);
if (stopping) exit_code = 0;
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-mobile-dev-plugin",
"version": "0.1.161",
"version": "0.1.162",
"private": true,
"type": "module",
"engines": {
Expand Down
Loading
Loading