blob: c596213e9aafe8ec9b312c89d96897cc64997aa3 [file] [log] [blame]
/*
* idevicedebug.c
* Interact with the debugserver service of a device.
*
* Copyright (c) 2014-2015 Martin Szulecki All Rights Reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#define TOOL_NAME "idevicedebug"
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <libgen.h>
#ifdef WIN32
#include <windows.h>
#define sleep(x) Sleep(x*1000)
#endif
#include <libimobiledevice/installation_proxy.h>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/debugserver.h>
#include <plist/plist.h>
#include "common/debug.h"
static int debug_level = 0;
#define log_debug(...) if (debug_level > 0) { printf(__VA_ARGS__); fputc('\n', stdout); }
enum cmd_mode {
CMD_NONE = 0,
CMD_RUN
};
static int quit_flag = 0;
static void on_signal(int sig)
{
fprintf(stderr, "Exiting...\n");
quit_flag++;
}
static void cancel_receive()
{
return quit_flag;
}
static instproxy_error_t instproxy_client_get_object_by_key_from_info_dictionary_for_bundle_identifier(instproxy_client_t client, const char* appid, const char* key, plist_t* node)
{
if (!client || !appid || !key)
return INSTPROXY_E_INVALID_ARG;
plist_t apps = NULL;
// create client options for any application types
plist_t client_opts = instproxy_client_options_new();
instproxy_client_options_add(client_opts, "ApplicationType", "Any", NULL);
// only return attributes we need
instproxy_client_options_set_return_attributes(client_opts, "CFBundleIdentifier", "CFBundleExecutable", key, NULL);
// only query for specific appid
const char* appids[] = {appid, NULL};
// query device for list of apps
instproxy_error_t ierr = instproxy_lookup(client, appids, client_opts, &apps);
instproxy_client_options_free(client_opts);
if (ierr != INSTPROXY_E_SUCCESS) {
return ierr;
}
plist_t app_found = plist_access_path(apps, 1, appid);
if (!app_found) {
if (apps)
plist_free(apps);
*node = NULL;
return INSTPROXY_E_OP_FAILED;
}
plist_t object = plist_dict_get_item(app_found, key);
if (object) {
*node = plist_copy(object);
} else {
log_debug("key %s not found", key);
return INSTPROXY_E_OP_FAILED;
}
plist_free(apps);
return INSTPROXY_E_SUCCESS;
}
static debugserver_error_t debugserver_client_handle_response(debugserver_client_t client, char** response, int* exit_status)
{
debugserver_error_t dres = DEBUGSERVER_E_SUCCESS;
char* o = NULL;
char* r = *response;
/* Documentation of response codes can be found here:
https://github.com/llvm/llvm-project/blob/4fe839ef3a51e0ea2e72ea2f8e209790489407a2/lldb/docs/lldb-gdb-remote.txt#L1269 */
if (r[0] == 'O') {
/* stdout/stderr */
debugserver_decode_string(r + 1, strlen(r) - 1, &o);
printf("%s", o);
fflush(stdout);
} else if (r[0] == 'T') {
/* thread stopped information */
log_debug("Thread stopped. Details:\n%s", r + 1);
/* "Thread stopped" seems to happen when assert() fails. Use bash convention where signals cause an exit status of 128 + signal */
*exit_status = 128 + SIGABRT;
/* Break out of the loop. */
dres = DEBUGSERVER_E_UNKNOWN_ERROR;
} else if (r[0] == 'E') {
printf("ERROR: %s\n", r + 1);
} else if (r[0] == 'W' || r[0] == 'X') {
/* process exited */
debugserver_decode_string(r + 1, strlen(r) - 1, &o);
if (o != NULL) {
printf("Exit %s: %u\n", (r[0] == 'W' ? "status" : "due to signal"), o[0]);
/* Use bash convention where signals cause an exit status of 128 + signal */
*exit_status = o[0] + (r[0] == 'W' ? 0 : 128);
} else {
debug_info("Unable to decode exit status from %s", r);
dres = DEBUGSERVER_E_UNKNOWN_ERROR;
}
} else if (r && strlen(r) == 0) {
log_debug("empty response");
} else {
log_debug("ERROR: unhandled response '%s'", r);
}
if (o != NULL) {
free(o);
o = NULL;
}
free(*response);
*response = NULL;
return dres;
}
static void print_usage(int argc, char **argv)
{
char *name = NULL;
name = strrchr(argv[0], '/');
printf("Usage: %s [OPTIONS] COMMAND\n", (name ? name + 1: argv[0]));
printf("\n");
printf("Interact with the debugserver service of a device.\n");
printf("\n");
printf("Where COMMAND is one of:\n");
printf(" run BUNDLEID [ARGS...]\trun app with BUNDLEID and optional ARGS on device.\n");
printf("\n");
printf("The following OPTIONS are accepted:\n");
printf(" -u, --udid UDID\ttarget specific device by UDID\n");
printf(" -n, --network\t\tconnect to network device\n");
printf(" --detach\t\tdetach from app after launch, keeping it running\n");
printf(" -e, --env NAME=VALUE\tset environment variable NAME to VALUE\n");
printf(" -d, --debug\t\tenable communication debugging\n");
printf(" -h, --help\t\tprints usage information\n");
printf(" -v, --version\t\tprints version information\n");
printf("\n");
printf("Homepage: <" PACKAGE_URL ">\n");
printf("Bug Reports: <" PACKAGE_BUGREPORT ">\n");
}
int main(int argc, char *argv[])
{
int res = -1;
idevice_t device = NULL;
idevice_error_t ret = IDEVICE_E_UNKNOWN_ERROR;
instproxy_client_t instproxy_client = NULL;
debugserver_client_t debugserver_client = NULL;
int i;
int cmd = CMD_NONE;
const char* udid = NULL;
int use_network = 0;
int detach_after_start = 0;
const char* bundle_identifier = NULL;
char* path = NULL;
char* working_directory = NULL;
char **newlist = NULL;
char** environment = NULL;
int environment_index = 0;
int environment_count = 0;
char* response = NULL;
debugserver_command_t command = NULL;
debugserver_error_t dres = DEBUGSERVER_E_UNKNOWN_ERROR;
/* map signals */
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
#ifndef WIN32
signal(SIGQUIT, on_signal);
signal(SIGPIPE, SIG_IGN);
#endif
/* parse command line arguments */
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "-d") || !strcmp(argv[i], "--debug")) {
debug_level++;
idevice_set_debug_level(debug_level);
continue;
} else if (!strcmp(argv[i], "-u") || !strcmp(argv[i], "--udid")) {
i++;
if (!argv[i] || !*argv[i]) {
print_usage(argc, argv);
res = 0;
goto cleanup;
}
udid = argv[i];
continue;
} else if (!strcmp(argv[i], "-n") || !strcmp(argv[i], "--network")) {
use_network = 1;
continue;
} else if (!strcmp(argv[i], "--detach")) {
detach_after_start = 1;
continue;
} else if (!strcmp(argv[i], "-e") || !strcmp(argv[i], "--env")) {
i++;
if (!argv[i] || (strlen(argv[i]) <= 1) || strchr(argv[i], '=') == NULL) {
print_usage(argc, argv);
res = 0;
goto cleanup;
}
/* add environment variable */
if (!newlist)
newlist = malloc((environment_count + 1) * sizeof(char*));
else
newlist = realloc(environment, (environment_count + 1) * sizeof(char*));
newlist[environment_count++] = strdup(argv[i]);
environment = newlist;
continue;
} else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
print_usage(argc, argv);
res = 0;
goto cleanup;
} else if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--version")) {
printf("%s %s\n", TOOL_NAME, PACKAGE_VERSION);
res = 0;
goto cleanup;
} else if (!strcmp(argv[i], "run")) {
cmd = CMD_RUN;
i++;
if (!argv[i]) {
/* make sure at least the bundle identifier was provided */
printf("Please supply the bundle identifier of the app to run.\n");
print_usage(argc, argv);
res = 0;
goto cleanup;
}
/* read bundle identifier */
bundle_identifier = argv[i];
break;
} else {
print_usage(argc, argv);
res = 0;
goto cleanup;
}
}
if (environment) {
newlist = realloc(environment, (environment_count + 1) * sizeof(char*));
newlist[environment_count] = NULL;
environment = newlist;
}
/* verify options */
if (cmd == CMD_NONE) {
print_usage(argc, argv);
goto cleanup;
}
/* connect to the device */
ret = idevice_new_with_options(&device, udid, (use_network) ? IDEVICE_LOOKUP_NETWORK : IDEVICE_LOOKUP_USBMUX);
if (ret != IDEVICE_E_SUCCESS) {
if (udid) {
printf("No device found with udid %s.\n", udid);
} else {
printf("No device found.\n");
}
goto cleanup;
}
switch (cmd) {
case CMD_RUN:
default:
/* get the path to the app and it's working directory */
if (instproxy_client_start_service(device, &instproxy_client, TOOL_NAME) != INSTPROXY_E_SUCCESS) {
fprintf(stderr, "Could not start installation proxy service.\n");
goto cleanup;
}
instproxy_client_get_path_for_bundle_identifier(instproxy_client, bundle_identifier, &path);
if (!path) {
fprintf(stderr, "Invalid bundle identifier: %s\n", bundle_identifier);
goto cleanup;
}
plist_t container = NULL;
instproxy_client_get_object_by_key_from_info_dictionary_for_bundle_identifier(instproxy_client, bundle_identifier, "Container", &container);
instproxy_client_free(instproxy_client);
instproxy_client = NULL;
if (container && (plist_get_node_type(container) == PLIST_STRING)) {
plist_get_string_val(container, &working_directory);
log_debug("working_directory: %s\n", working_directory);
plist_free(container);
} else {
plist_free(container);
fprintf(stderr, "Could not determine container path for bundle identifier %s.\n", bundle_identifier);
goto cleanup;
}
/* start and connect to debugserver */
if (debugserver_client_start_service(device, &debugserver_client, TOOL_NAME) != DEBUGSERVER_E_SUCCESS) {
fprintf(stderr,
"Could not start com.apple.debugserver!\n"
"Please make sure to mount the developer disk image first:\n"
" 1) Get the iOS version from `ideviceinfo -k ProductVersion`.\n"
" 2) Find the matching iPhoneOS DeveloperDiskImage.dmg files.\n"
" 3) Run `ideviceimagemounter` with the above path.\n");
goto cleanup;
}
/* set receive params */
if (debugserver_client_set_receive_params(debugserver_client, cancel_receive, 250) != DEBUGSERVER_E_SUCCESS) {
fprintf(stderr, "Error in debugserver_client_set_receive_params\n");
goto cleanup;
}
/* enable logging for the session in debug mode */
if (debug_level) {
log_debug("Setting logging bitmask...");
debugserver_command_new("QSetLogging:bitmask=LOG_ALL|LOG_RNB_REMOTE|LOG_RNB_PACKETS;", 0, NULL, &command);
dres = debugserver_client_send_command(debugserver_client, command, &response, NULL);
debugserver_command_free(command);
command = NULL;
if (response) {
if (strncmp(response, "OK", 2)) {
debugserver_client_handle_response(debugserver_client, &response, NULL);
goto cleanup;
}
free(response);
response = NULL;
}
}
/* set maximum packet size */
log_debug("Setting maximum packet size...");
char* packet_size[2] = {strdup("1024"), NULL};
debugserver_command_new("QSetMaxPacketSize:", 1, packet_size, &command);
free(packet_size[0]);
dres = debugserver_client_send_command(debugserver_client, command, &response, NULL);
debugserver_command_free(command);
command = NULL;
if (response) {
if (strncmp(response, "OK", 2)) {
debugserver_client_handle_response(debugserver_client, &response, NULL);
goto cleanup;
}
free(response);
response = NULL;
}
/* set working directory */
log_debug("Setting working directory...");
char* working_dir[2] = {working_directory, NULL};
debugserver_command_new("QSetWorkingDir:", 1, working_dir, &command);
dres = debugserver_client_send_command(debugserver_client, command, &response, NULL);
debugserver_command_free(command);
command = NULL;
if (response) {
if (strncmp(response, "OK", 2)) {
debugserver_client_handle_response(debugserver_client, &response, NULL);
goto cleanup;
}
free(response);
response = NULL;
}
/* set environment */
if (environment) {
log_debug("Setting environment...");
for (environment_index = 0; environment_index < environment_count; environment_index++) {
log_debug("setting environment variable: %s", environment[environment_index]);
debugserver_client_set_environment_hex_encoded(debugserver_client, environment[environment_index], NULL);
}
}
/* set arguments and run app */
log_debug("Setting argv...");
i++; /* i is the offset of the bundle identifier, thus skip it */
int app_argc = (argc - i + 2);
char **app_argv = (char**)malloc(sizeof(char*) * app_argc);
app_argv[0] = path;
log_debug("app_argv[%d] = %s", 0, app_argv[0]);
app_argc = 1;
while (i < argc && argv && argv[i]) {
log_debug("app_argv[%d] = %s", app_argc, argv[i]);
app_argv[app_argc++] = argv[i];
i++;
}
app_argv[app_argc] = NULL;
debugserver_client_set_argv(debugserver_client, app_argc, app_argv, NULL);
free(app_argv);
/* check if launch succeeded */
log_debug("Checking if launch succeeded...");
debugserver_command_new("qLaunchSuccess", 0, NULL, &command);
dres = debugserver_client_send_command(debugserver_client, command, &response, NULL);
debugserver_command_free(command);
command = NULL;
if (response) {
if (strncmp(response, "OK", 2)) {
debugserver_client_handle_response(debugserver_client, &response, NULL);
goto cleanup;
}
free(response);
response = NULL;
}
if (detach_after_start) {
log_debug("Detaching from app");
debugserver_command_new("D", 0, NULL, &command);
dres = debugserver_client_send_command(debugserver_client, command, &response, NULL);
debugserver_command_free(command);
command = NULL;
res = (dres == DEBUGSERVER_E_SUCCESS) ? 0: -1;
break;
}
/* set thread */
log_debug("Setting thread...");
debugserver_command_new("Hc0", 0, NULL, &command);
dres = debugserver_client_send_command(debugserver_client, command, &response, NULL);
debugserver_command_free(command);
command = NULL;
if (response) {
if (strncmp(response, "OK", 2)) {
debugserver_client_handle_response(debugserver_client, &response, NULL);
goto cleanup;
}
free(response);
response = NULL;
}
/* continue running process */
log_debug("Continue running process...");
debugserver_command_new("c", 0, NULL, &command);
dres = debugserver_client_send_command(debugserver_client, command, &response, NULL);
debugserver_command_free(command);
command = NULL;
/* main loop which is parsing/handling packets during the run */
log_debug("Entering run loop...");
while (!quit_flag) {
if (dres != DEBUGSERVER_E_SUCCESS) {
log_debug("failed to receive response; error %d", dres);
break;
}
if (response) {
log_debug("response: %s", response);
if (strncmp(response, "OK", 2)) {
dres = debugserver_client_handle_response(debugserver_client, &response, &res);
if (dres != DEBUGSERVER_E_SUCCESS) {
debug_info("failed to process response; error %d; %s", dres, response);
break;
}
}
}
if (res >= 0) {
goto cleanup;
}
dres = debugserver_client_receive_response(debugserver_client, &response, NULL);
}
/* kill process after we finished */
log_debug("Killing process...");
debugserver_command_new("k", 0, NULL, &command);
dres = debugserver_client_send_command(debugserver_client, command, &response, NULL);
debugserver_command_free(command);
command = NULL;
if (response) {
if (strncmp(response, "OK", 2)) {
debugserver_client_handle_response(debugserver_client, &response, NULL);
goto cleanup;
}
free(response);
response = NULL;
}
if (res < 0) {
res = (dres == DEBUGSERVER_E_SUCCESS) ? 0: -1;
}
break;
}
cleanup:
/* cleanup the house */
if (environment) {
for (environment_index = 0; environment_index < environment_count; environment_index++) {
free(environment[environment_index]);
}
free(environment);
}
if (working_directory)
free(working_directory);
if (path)
free(path);
if (response)
free(response);
if (debugserver_client)
debugserver_client_free(debugserver_client);
if (device)
idevice_free(device);
return res;
}