Add a 'symbols' command line flag that takes a path to a directory. This command will download the dyld cache files containing the system libraries compressed into one file and store them in the given directory. This commit does the same thing that Xcode does when a device is plugged in and Xcode is 'Preparing debugger support for iPhone'. This is only the first part of downloading symbols from a device. A subsequent commit will extract the libraries the dyld cache files. (#534)

Co-authored-by: Ivan Hernandez <ivanhernandez@Ivans-MacBook-Pro.local>
diff --git a/src/ios-deploy/ios-deploy.m b/src/ios-deploy/ios-deploy.m
index 7792ba8..721776d 100644
--- a/src/ios-deploy/ios-deploy.m
+++ b/src/ios-deploy/ios-deploy.m
@@ -3,6 +3,7 @@
 #import <CoreFoundation/CoreFoundation.h>
 #import <Foundation/Foundation.h>
 #include <unistd.h>
+#include <sys/mman.h>
 #include <sys/socket.h>
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -13,6 +14,7 @@
 #include <getopt.h>
 #include <pwd.h>
 #include <dlfcn.h>
+#include <time.h>
 
 #include <netinet/in.h>
 #include <netinet/tcp.h>
@@ -92,6 +94,8 @@
 
 int AMDServiceConnectionSend(ServiceConnRef con, const void * data, size_t size);
 int AMDServiceConnectionReceive(ServiceConnRef con, void * data, size_t size);
+uint64_t AMDServiceConnectionReceiveMessage(ServiceConnRef con, CFPropertyListRef message, CFPropertyListFormat *format);
+uint64_t AMDServiceConnectionSendMessage(ServiceConnRef con, CFPropertyListRef message, CFPropertyListFormat format);
 
 bool found_device = false, debug = false, verbose = false, unbuffered = false, nostart = false, debugserver_only = false, detect_only = false, install = true, uninstall = false, no_wifi = false;
 bool faster_path_search = false;
@@ -112,6 +116,7 @@
 char *envs = NULL;
 char *list_root = NULL;
 const char * custom_script_path = NULL;
+char *symbols_download_directory = NULL;
 int _timeout = 0;
 int _detectDeadlockTimeout = 0;
 bool _json_output = false;
@@ -129,6 +134,12 @@
 CFRunLoopSourceRef server_socket_runloop;
 CFRunLoopSourceRef fdvendor_runloop;
 
+uint32_t symbols_file_paths_command = 0x30303030;
+uint32_t symbols_download_file_command = 0x01000000;
+CFStringRef symbols_service_name = CFSTR("com.apple.dt.fetchsymbols");
+
+const size_t sizeof_uint32_t = sizeof(uint32_t);
+
 // Error codes we report on different failures, so scripts can distinguish between user app exit
 // codes and our exit codes. For non app errors we use codes in reserved 128-255 range.
 const int exitcode_timeout = 252;
@@ -229,6 +240,9 @@
     }
 }
 
+uint64_t get_current_time_in_milliseconds() {
+    return clock_gettime_nsec_np(CLOCK_REALTIME) / (1000 * 1000);
+}
 
 BOOL mkdirp(NSString* path) {
     NSError* error = nil;
@@ -713,16 +727,17 @@
         
         on_error(@"Unable to mount developer disk image. (%x)", result);
     }
-	
+  
     CFStringRef symbols_path = copy_device_support_path(device, CFSTR("Symbols"));
     if (symbols_path != NULL)
     {
         NSLogOut(@"Symbol Path: %@", symbols_path);
         NSLogJSON(@{@"Event": @"MountDeveloperImage",
                     @"SymbolsPath": (__bridge NSString *)symbols_path
-                    });		CFRelease(symbols_path);
+                    });
+        CFRelease(symbols_path);
     }
-	
+  
     CFRelease(image_path);
     CFRelease(options);
 }
@@ -1922,7 +1937,7 @@
         afc_conn_p = start_afc_service(device);
     } else {
         afc_conn_p = start_house_arrest_service(device);
-    } 
+    }
     assert(afc_conn_p);
 
     if (!target_filename)
@@ -2084,6 +2099,153 @@
     }
 }
 
+void start_symbols_service_with_command(AMDeviceRef device, uint32_t command) {
+    connect_and_start_session(device);
+    int start_err = AMDeviceSecureStartService(device, symbols_service_name, NULL,
+                                               &dbgServiceConnection);
+    if (start_err != 0) {
+        on_error(@"Failed to start service: %x %s", start_err,
+               symbols_service_name);
+    }
+
+    uint32_t bytes_sent = AMDServiceConnectionSend(dbgServiceConnection, &command,
+                                                    sizeof_uint32_t);
+    if (bytes_sent != sizeof_uint32_t) {
+        on_error(@"Sent %d bytes but was expecting %d.", bytes_sent, sizeof_uint32_t);
+    }
+
+    uint32_t response;
+    uint32_t bytes_read = AMDServiceConnectionReceive(dbgServiceConnection,
+                                                        &response, sizeof_uint32_t);
+    if (bytes_read != sizeof_uint32_t) {
+        on_error(@"Read %d bytes but was expecting %d.", bytes_read, sizeof_uint32_t);
+    } else if (response != command) {
+        on_error(@"Failed to get confirmation response for: %s", command);
+    }
+}
+
+CFArrayRef get_dyld_file_paths(AMDeviceRef device) {
+    start_symbols_service_with_command(device, symbols_file_paths_command);
+
+    CFPropertyListFormat format;
+    CFDictionaryRef dict = NULL;
+    uint64_t bytes_read =
+        AMDServiceConnectionReceiveMessage(dbgServiceConnection, &dict, &format);
+    if (bytes_read == -1) {
+        on_error(@"Received %d bytes after succesfully starting command %d.", bytes_read,
+                 symbols_file_paths_command);
+    }
+    AMDeviceStopSession(device);
+    AMDeviceDisconnect(device);
+
+    CFStringRef files_key = CFSTR("files");
+    if (!CFDictionaryContainsKey(dict, files_key)) {
+        on_error(@"Incoming messasge did not contain key '%@', %@", files_key, dict);
+    }
+    return CFDictionaryGetValue(dict, files_key);
+}
+
+void write_dyld_file(CFStringRef dest, uint64_t file_size) {
+    // Prepare the destination file by mapping it into memory.
+    int fd = open(CFStringGetCStringPtr(dest, kCFStringEncodingUTF8),
+                  O_RDWR | O_CREAT, 0644);
+    if (fd == -1) {
+        on_sys_error(@"Failed to open %@.", dest);
+    }
+    if (lseek(fd, file_size - 1, SEEK_SET) == -1) {
+        on_sys_error(@"Failed to lseek to last byte.");
+    }
+    if (write(fd, "", 1) == -1) {
+        on_sys_error(@"Failed to write to last byte.");
+    }
+    void *map = mmap(NULL, file_size, PROT_WRITE, MAP_SHARED, fd, 0);
+    if (map == MAP_FAILED) {
+        on_sys_error(@"Failed to mmap %@.", dest);
+    }
+    close(fd);
+  
+    // Read the file content packet by packet until we've copied the entire file
+    // to disk.
+    uint64_t total_bytes_read = 0;
+    uint64_t last_time = get_current_time_in_milliseconds() / 250;
+    while (total_bytes_read < file_size) {
+        uint64_t bytes_remaining = file_size - total_bytes_read;
+        // This fails for some reason if we try to download more than
+        // INT_MAX bytes at a time.
+        uint64_t bytes_to_download = MIN(bytes_remaining, INT_MAX - 1);
+        uint64_t bytes_read = AMDServiceConnectionReceive(
+            dbgServiceConnection, map + total_bytes_read, bytes_to_download);
+        total_bytes_read += bytes_read;
+
+        uint64_t current_time = get_current_time_in_milliseconds() / 250;
+        // We can process several packets per second which would result
+        // in spamming output so only log if any of the following are
+        // true:
+        //    - Running in verbose mode.
+        //    - It's been at least a quarter second since the last log.
+        //    - We finished processing the last packet.
+        if (verbose || last_time != current_time || total_bytes_read == file_size) {
+            last_time = current_time;
+            int percent = (double)total_bytes_read / file_size * 100;
+            NSLogOut(@"%llu/%llu (%d%%)", total_bytes_read, file_size, percent);
+            NSLogJSON(@{@"Event": @"DyldCacheDownloadProgress",
+                         @"BytesRead": @(total_bytes_read),
+                         @"Percent": @(percent),
+                      });
+        }
+    }
+  
+    munmap(map, file_size);
+}
+
+void download_dyld_file(AMDeviceRef device, uint32_t dyld_index,
+                        CFStringRef filepath) {
+    start_symbols_service_with_command(device, symbols_download_file_command);
+
+    uint32_t index = CFSwapInt32HostToBig(dyld_index);
+    uint64_t bytes_sent =
+        AMDServiceConnectionSend(dbgServiceConnection, &index, sizeof_uint32_t);
+    if (bytes_sent != sizeof_uint32_t) {
+        on_error(@"Sent %d bytes but was expecting %d.", bytes_sent, sizeof_uint32_t);
+    }
+
+    uint64_t file_size = 0;
+    uint64_t bytes_read = AMDServiceConnectionReceive(dbgServiceConnection,
+                                                    &file_size, sizeof(uint64_t));
+    if (bytes_read != sizeof(uint64_t)) {
+        on_error(@"Read %d bytes but was expecting %d.", bytes_read, sizeof(uint64_t));
+    }
+    file_size = CFSwapInt64BigToHost(file_size);
+  
+    CFStringRef download_path = CFStringCreateWithFormat(
+        NULL, NULL, CFSTR("%s%@"), symbols_download_directory, filepath);
+    mkdirp(
+        ((__bridge NSString *)download_path).stringByDeletingLastPathComponent);
+    NSLogOut(@"Downloading %@ to %@.", filepath, download_path);
+    NSLogJSON(@{@"Event": @"DyldCacheDownload",
+                 @"Source": (__bridge NSString *)filepath,
+                 @"Destination": (__bridge NSString *)download_path,
+                 @"Size": @(file_size),
+              });
+
+    write_dyld_file(download_path, file_size);
+  
+    CFRelease(download_path);
+    AMDeviceStopSession(device);
+    AMDeviceDisconnect(device);
+}
+
+void download_device_symbols(AMDeviceRef device) {
+    dbgServiceConnection = NULL;
+    CFArrayRef files = get_dyld_file_paths(device);
+    CFIndex files_count = CFArrayGetCount(files);
+
+    for (uint32_t i = 0; i < files_count; ++i) {
+        CFStringRef filepath = (CFStringRef)CFArrayGetValueAtIndex(files, i);
+        download_dyld_file(device, i, filepath);
+    }
+}
+
 void handle_device(AMDeviceRef device) {
     NSLogVerbose(@"Already found device? %d", found_device);
 
@@ -2144,6 +2306,8 @@
             list_bundle_id(device);
         } else if (strcmp("get_battery_level", command) == 0) {
             get_battery_level(device);
+        } else if (strcmp("symbols", command) == 0) {
+            download_device_symbols(device);
         }
         exit(0);
     }
@@ -2399,6 +2563,7 @@
         @"  --detect_deadlocks <sec>     start printing backtraces for all threads periodically after specific amount of seconds\n"
         @"  -f, --file_system            specify file system for mkdir / list / upload / download / rm\n"
         @"  -F, --non-recursively        specify non-recursively walk directory\n"
+        @"  -S, --symbols                download OS symbols. must specify a directory to store the downloaded symbols\n"
         @"  -j, --json                   format output as JSON\n"
         @"  -k, --key                    keys for the properties of the bundle. Joined by ',' and used only with -B <list_bundle_id> and -j <json> \n"
         @"  --custom-script <script>     path to custom python script to execute in lldb\n"
@@ -2460,6 +2625,7 @@
         { "file_system", no_argument, NULL, 'f'},
         { "non-recursively", no_argument, NULL, 'F'},
         { "key", optional_argument, NULL, 'k' },
+        { "symbols", required_argument, NULL, 'S' },
         { "custom-script", required_argument, NULL, 1001},
         { "custom-command", required_argument, NULL, 1002},
         { "faster-path-search", no_argument, NULL, 1003},
@@ -2467,7 +2633,7 @@
     };
     int ch;
 
-    while ((ch = getopt_long(argc, argv, "VmcdvunrILefFD:R:X:i:b:a:t:p:1:2:o:l:w:9BWjNs:OE:CA:k:", longopts, NULL)) != -1)
+    while ((ch = getopt_long(argc, argv, "VmcdvunrILefFD:R:X:i:b:a:t:p:1:2:o:l:w:9BWjNs:OE:CA:k:S:", longopts, NULL)) != -1)
     {
         switch (ch) {
         case 'm':
@@ -2489,6 +2655,11 @@
         case 's':
             envs = optarg;
             break;
+        case 'S':
+            symbols_download_directory = optarg;
+            command = "symbols";
+            command_only = true;
+            break;
         case 'v':
             verbose = true;
             break;
@@ -2633,7 +2804,7 @@
 
     if (!app_path && !detect_only && !debugserver_only && !command_only) {
         usage(argv[0]);
-        on_error(@"One of -[b|c|o|l|w|D|N|R|X|e|B|C|9] is required to proceed!");
+        on_error(@"One of -[b|c|o|l|w|D|N|R|X|e|B|C|S|9] is required to proceed!");
     }
 
     if (unbuffered) {