diff options
Diffstat (limited to 'tools/idevicecrashreport.c')
-rw-r--r-- | tools/idevicecrashreport.c | 529 |
1 files changed, 529 insertions, 0 deletions
diff --git a/tools/idevicecrashreport.c b/tools/idevicecrashreport.c new file mode 100644 index 0000000..09bd537 --- /dev/null +++ b/tools/idevicecrashreport.c | |||
@@ -0,0 +1,529 @@ | |||
1 | /* | ||
2 | * idevicecrashreport.c | ||
3 | * Simple utility to move crash reports from a device to a local directory. | ||
4 | * | ||
5 | * Copyright (c) 2014 Martin Szulecki. All Rights Reserved. | ||
6 | * Copyright (c) 2014 Nikias Bassen. All Rights Reserved. | ||
7 | * | ||
8 | * This library is free software; you can redistribute it and/or | ||
9 | * modify it under the terms of the GNU Lesser General Public | ||
10 | * License as published by the Free Software Foundation; either | ||
11 | * version 2.1 of the License, or (at your option) any later version. | ||
12 | * | ||
13 | * This library is distributed in the hope that it will be useful, | ||
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
16 | * Lesser General Public License for more details. | ||
17 | * | ||
18 | * You should have received a copy of the GNU Lesser General Public | ||
19 | * License along with this library; if not, write to the Free Software | ||
20 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
21 | */ | ||
22 | |||
23 | #ifdef HAVE_CONFIG_H | ||
24 | #include <config.h> | ||
25 | #endif | ||
26 | |||
27 | #define TOOL_NAME "idevicecrashreport" | ||
28 | |||
29 | #include <stdio.h> | ||
30 | #include <stdlib.h> | ||
31 | #include <string.h> | ||
32 | #include <unistd.h> | ||
33 | #include <getopt.h> | ||
34 | #ifndef WIN32 | ||
35 | #include <signal.h> | ||
36 | #endif | ||
37 | #include <libimobiledevice-glue/utils.h> | ||
38 | |||
39 | #include <libimobiledevice/libimobiledevice.h> | ||
40 | #include <libimobiledevice/lockdown.h> | ||
41 | #include <libimobiledevice/service.h> | ||
42 | #include <libimobiledevice/afc.h> | ||
43 | #include <plist/plist.h> | ||
44 | |||
45 | #ifdef WIN32 | ||
46 | #include <windows.h> | ||
47 | #define S_IFLNK S_IFREG | ||
48 | #define S_IFSOCK S_IFREG | ||
49 | #endif | ||
50 | |||
51 | #define CRASH_REPORT_MOVER_SERVICE "com.apple.crashreportmover" | ||
52 | #define CRASH_REPORT_COPY_MOBILE_SERVICE "com.apple.crashreportcopymobile" | ||
53 | |||
54 | const char* target_directory = NULL; | ||
55 | static int extract_raw_crash_reports = 0; | ||
56 | static int keep_crash_reports = 0; | ||
57 | |||
58 | static int file_exists(const char* path) | ||
59 | { | ||
60 | struct stat tst; | ||
61 | #ifdef WIN32 | ||
62 | return (stat(path, &tst) == 0); | ||
63 | #else | ||
64 | return (lstat(path, &tst) == 0); | ||
65 | #endif | ||
66 | } | ||
67 | |||
68 | static int extract_raw_crash_report(const char* filename) | ||
69 | { | ||
70 | int res = 0; | ||
71 | plist_t report = NULL; | ||
72 | char* raw = NULL; | ||
73 | char* raw_filename = strdup(filename); | ||
74 | |||
75 | /* create filename with '.crash' extension */ | ||
76 | char* p = strrchr(raw_filename, '.'); | ||
77 | if ((p == NULL) || (strcmp(p, ".plist") != 0)) { | ||
78 | free(raw_filename); | ||
79 | return res; | ||
80 | } | ||
81 | strcpy(p, ".crash"); | ||
82 | |||
83 | /* read plist crash report */ | ||
84 | if (plist_read_from_file(filename, &report, NULL)) { | ||
85 | plist_t description_node = plist_dict_get_item(report, "description"); | ||
86 | if (description_node && plist_get_node_type(description_node) == PLIST_STRING) { | ||
87 | plist_get_string_val(description_node, &raw); | ||
88 | |||
89 | if (raw != NULL) { | ||
90 | /* write file */ | ||
91 | buffer_write_to_filename(raw_filename, raw, strlen(raw)); | ||
92 | free(raw); | ||
93 | res = 1; | ||
94 | } | ||
95 | } | ||
96 | } | ||
97 | |||
98 | if (report) | ||
99 | plist_free(report); | ||
100 | |||
101 | if (raw_filename) | ||
102 | free(raw_filename); | ||
103 | |||
104 | return res; | ||
105 | } | ||
106 | |||
107 | static int afc_client_copy_and_remove_crash_reports(afc_client_t afc, const char* device_directory, const char* host_directory, const char* filename_filter) | ||
108 | { | ||
109 | afc_error_t afc_error; | ||
110 | int k; | ||
111 | int res = -1; | ||
112 | int crash_report_count = 0; | ||
113 | uint64_t handle; | ||
114 | char source_filename[512]; | ||
115 | char target_filename[512]; | ||
116 | |||
117 | if (!afc) | ||
118 | return res; | ||
119 | |||
120 | char** list = NULL; | ||
121 | afc_error = afc_read_directory(afc, device_directory, &list); | ||
122 | if (afc_error != AFC_E_SUCCESS) { | ||
123 | fprintf(stderr, "ERROR: Could not read device directory '%s'\n", device_directory); | ||
124 | return res; | ||
125 | } | ||
126 | |||
127 | /* ensure we have a trailing slash */ | ||
128 | strcpy(source_filename, device_directory); | ||
129 | if (source_filename[strlen(source_filename)-1] != '/') { | ||
130 | strcat(source_filename, "/"); | ||
131 | } | ||
132 | int device_directory_length = strlen(source_filename); | ||
133 | |||
134 | /* ensure we have a trailing slash */ | ||
135 | strcpy(target_filename, host_directory); | ||
136 | if (target_filename[strlen(target_filename)-1] != '/') { | ||
137 | strcat(target_filename, "/"); | ||
138 | } | ||
139 | int host_directory_length = strlen(target_filename); | ||
140 | |||
141 | /* loop over file entries */ | ||
142 | for (k = 0; list[k]; k++) { | ||
143 | if (!strcmp(list[k], ".") || !strcmp(list[k], "..")) { | ||
144 | continue; | ||
145 | } | ||
146 | |||
147 | char **fileinfo = NULL; | ||
148 | struct stat stbuf; | ||
149 | memset(&stbuf, '\0', sizeof(struct stat)); | ||
150 | |||
151 | /* assemble absolute source filename */ | ||
152 | strcpy(((char*)source_filename) + device_directory_length, list[k]); | ||
153 | |||
154 | /* assemble absolute target filename */ | ||
155 | #ifdef WIN32 | ||
156 | /* replace every ':' with '-' since ':' is an illegal character for file names in windows */ | ||
157 | char* current_pos = strchr(list[k], ':'); | ||
158 | while (current_pos) { | ||
159 | *current_pos = '-'; | ||
160 | current_pos = strchr(current_pos, ':'); | ||
161 | } | ||
162 | #endif | ||
163 | char* p = strrchr(list[k], '.'); | ||
164 | if (p != NULL && !strncmp(p, ".synced", 7)) { | ||
165 | /* make sure to strip ".synced" extension as seen on iOS 5 */ | ||
166 | size_t newlen = p - list[k]; | ||
167 | strncpy(((char*)target_filename) + host_directory_length, list[k], newlen); | ||
168 | target_filename[host_directory_length + newlen] = '\0'; | ||
169 | } else { | ||
170 | strcpy(((char*)target_filename) + host_directory_length, list[k]); | ||
171 | } | ||
172 | |||
173 | /* get file information */ | ||
174 | afc_get_file_info(afc, source_filename, &fileinfo); | ||
175 | if (!fileinfo) { | ||
176 | printf("Failed to read information for '%s'. Skipping...\n", source_filename); | ||
177 | continue; | ||
178 | } | ||
179 | |||
180 | /* parse file information */ | ||
181 | int i; | ||
182 | for (i = 0; fileinfo[i]; i+=2) { | ||
183 | if (!strcmp(fileinfo[i], "st_size")) { | ||
184 | stbuf.st_size = atoll(fileinfo[i+1]); | ||
185 | } else if (!strcmp(fileinfo[i], "st_ifmt")) { | ||
186 | if (!strcmp(fileinfo[i+1], "S_IFREG")) { | ||
187 | stbuf.st_mode = S_IFREG; | ||
188 | } else if (!strcmp(fileinfo[i+1], "S_IFDIR")) { | ||
189 | stbuf.st_mode = S_IFDIR; | ||
190 | } else if (!strcmp(fileinfo[i+1], "S_IFLNK")) { | ||
191 | stbuf.st_mode = S_IFLNK; | ||
192 | } else if (!strcmp(fileinfo[i+1], "S_IFBLK")) { | ||
193 | stbuf.st_mode = S_IFBLK; | ||
194 | } else if (!strcmp(fileinfo[i+1], "S_IFCHR")) { | ||
195 | stbuf.st_mode = S_IFCHR; | ||
196 | } else if (!strcmp(fileinfo[i+1], "S_IFIFO")) { | ||
197 | stbuf.st_mode = S_IFIFO; | ||
198 | } else if (!strcmp(fileinfo[i+1], "S_IFSOCK")) { | ||
199 | stbuf.st_mode = S_IFSOCK; | ||
200 | } | ||
201 | } else if (!strcmp(fileinfo[i], "st_nlink")) { | ||
202 | stbuf.st_nlink = atoi(fileinfo[i+1]); | ||
203 | } else if (!strcmp(fileinfo[i], "st_mtime")) { | ||
204 | stbuf.st_mtime = (time_t)(atoll(fileinfo[i+1]) / 1000000000); | ||
205 | } else if (!strcmp(fileinfo[i], "LinkTarget")) { | ||
206 | /* report latest crash report filename */ | ||
207 | printf("Link: %s\n", (char*)target_filename + strlen(target_directory)); | ||
208 | |||
209 | /* remove any previous symlink */ | ||
210 | if (file_exists(target_filename)) { | ||
211 | remove(target_filename); | ||
212 | } | ||
213 | |||
214 | #ifndef WIN32 | ||
215 | /* use relative filename */ | ||
216 | char* b = strrchr(fileinfo[i+1], '/'); | ||
217 | if (b == NULL) { | ||
218 | b = fileinfo[i+1]; | ||
219 | } else { | ||
220 | b++; | ||
221 | } | ||
222 | |||
223 | /* create a symlink pointing to latest log */ | ||
224 | if (symlink(b, target_filename) < 0) { | ||
225 | fprintf(stderr, "Can't create symlink to %s\n", b); | ||
226 | } | ||
227 | #endif | ||
228 | |||
229 | if (!keep_crash_reports) | ||
230 | afc_remove_path(afc, source_filename); | ||
231 | |||
232 | res = 0; | ||
233 | } | ||
234 | } | ||
235 | |||
236 | /* free file information */ | ||
237 | afc_dictionary_free(fileinfo); | ||
238 | |||
239 | /* recurse into child directories */ | ||
240 | if (S_ISDIR(stbuf.st_mode)) { | ||
241 | #ifdef WIN32 | ||
242 | mkdir(target_filename); | ||
243 | #else | ||
244 | mkdir(target_filename, 0755); | ||
245 | #endif | ||
246 | res = afc_client_copy_and_remove_crash_reports(afc, source_filename, target_filename, filename_filter); | ||
247 | |||
248 | /* remove directory from device */ | ||
249 | if (!keep_crash_reports) | ||
250 | afc_remove_path(afc, source_filename); | ||
251 | } else if (S_ISREG(stbuf.st_mode)) { | ||
252 | if (filename_filter != NULL && strstr(source_filename, filename_filter) == NULL) { | ||
253 | continue; | ||
254 | } | ||
255 | |||
256 | /* copy file to host */ | ||
257 | afc_error = afc_file_open(afc, source_filename, AFC_FOPEN_RDONLY, &handle); | ||
258 | if(afc_error != AFC_E_SUCCESS) { | ||
259 | if (afc_error == AFC_E_OBJECT_NOT_FOUND) { | ||
260 | continue; | ||
261 | } | ||
262 | fprintf(stderr, "Unable to open device file '%s' (%d). Skipping...\n", source_filename, afc_error); | ||
263 | continue; | ||
264 | } | ||
265 | |||
266 | FILE* output = fopen(target_filename, "wb"); | ||
267 | if(output == NULL) { | ||
268 | fprintf(stderr, "Unable to open local file '%s'. Skipping...\n", target_filename); | ||
269 | afc_file_close(afc, handle); | ||
270 | continue; | ||
271 | } | ||
272 | |||
273 | printf("%s: %s\n", (keep_crash_reports ? "Copy": "Move") , (char*)target_filename + strlen(target_directory)); | ||
274 | |||
275 | uint32_t bytes_read = 0; | ||
276 | uint32_t bytes_total = 0; | ||
277 | unsigned char data[0x1000]; | ||
278 | |||
279 | afc_error = afc_file_read(afc, handle, (char*)data, 0x1000, &bytes_read); | ||
280 | while(afc_error == AFC_E_SUCCESS && bytes_read > 0) { | ||
281 | fwrite(data, 1, bytes_read, output); | ||
282 | bytes_total += bytes_read; | ||
283 | afc_error = afc_file_read(afc, handle, (char*)data, 0x1000, &bytes_read); | ||
284 | } | ||
285 | afc_file_close(afc, handle); | ||
286 | fclose(output); | ||
287 | |||
288 | if ((uint32_t)stbuf.st_size != bytes_total) { | ||
289 | fprintf(stderr, "File size mismatch. Skipping...\n"); | ||
290 | continue; | ||
291 | } | ||
292 | |||
293 | /* remove file from device */ | ||
294 | if (!keep_crash_reports) { | ||
295 | afc_remove_path(afc, source_filename); | ||
296 | } | ||
297 | |||
298 | /* extract raw crash information into separate '.crash' file */ | ||
299 | if (extract_raw_crash_reports) { | ||
300 | extract_raw_crash_report(target_filename); | ||
301 | } | ||
302 | |||
303 | crash_report_count++; | ||
304 | |||
305 | res = 0; | ||
306 | } | ||
307 | } | ||
308 | afc_dictionary_free(list); | ||
309 | |||
310 | /* no reports, no error */ | ||
311 | if (crash_report_count == 0) | ||
312 | res = 0; | ||
313 | |||
314 | return res; | ||
315 | } | ||
316 | |||
317 | static void print_usage(int argc, char **argv, int is_error) | ||
318 | { | ||
319 | char *name = strrchr(argv[0], '/'); | ||
320 | fprintf(is_error ? stderr : stdout, "Usage: %s [OPTIONS] DIRECTORY\n", (name ? name + 1: argv[0])); | ||
321 | fprintf(is_error ? stderr : stdout, | ||
322 | "\n" | ||
323 | "Move crash reports from device to a local DIRECTORY.\n" | ||
324 | "\n" | ||
325 | "OPTIONS:\n" | ||
326 | " -u, --udid UDID target specific device by UDID\n" | ||
327 | " -n, --network connect to network device\n" | ||
328 | " -e, --extract extract raw crash report into separate '.crash' file\n" | ||
329 | " -k, --keep copy but do not remove crash reports from device\n" | ||
330 | " -d, --debug enable communication debugging\n" | ||
331 | " -f, --filter NAME filter crash reports by NAME (case sensitive)\n" | ||
332 | " -h, --help prints usage information\n" | ||
333 | " -v, --version prints version information\n" | ||
334 | "\n" | ||
335 | "Homepage: <" PACKAGE_URL ">\n" | ||
336 | "Bug Reports: <" PACKAGE_BUGREPORT ">\n" | ||
337 | ); | ||
338 | } | ||
339 | |||
340 | int main(int argc, char* argv[]) | ||
341 | { | ||
342 | idevice_t device = NULL; | ||
343 | lockdownd_client_t lockdownd = NULL; | ||
344 | afc_client_t afc = NULL; | ||
345 | |||
346 | idevice_error_t device_error = IDEVICE_E_SUCCESS; | ||
347 | lockdownd_error_t lockdownd_error = LOCKDOWN_E_SUCCESS; | ||
348 | afc_error_t afc_error = AFC_E_SUCCESS; | ||
349 | |||
350 | const char* udid = NULL; | ||
351 | int use_network = 0; | ||
352 | const char* filename_filter = NULL; | ||
353 | |||
354 | int c = 0; | ||
355 | const struct option longopts[] = { | ||
356 | { "debug", no_argument, NULL, 'd' }, | ||
357 | { "help", no_argument, NULL, 'h' }, | ||
358 | { "udid", required_argument, NULL, 'u' }, | ||
359 | { "network", no_argument, NULL, 'n' }, | ||
360 | { "version", no_argument, NULL, 'v' }, | ||
361 | { "filter", required_argument, NULL, 'f' }, | ||
362 | { "extract", no_argument, NULL, 'e' }, | ||
363 | { "keep", no_argument, NULL, 'k' }, | ||
364 | { NULL, 0, NULL, 0} | ||
365 | }; | ||
366 | |||
367 | #ifndef WIN32 | ||
368 | signal(SIGPIPE, SIG_IGN); | ||
369 | #endif | ||
370 | |||
371 | /* parse cmdline args */ | ||
372 | while ((c = getopt_long(argc, argv, "dhu:nvf:ek", longopts, NULL)) != -1) { | ||
373 | switch (c) { | ||
374 | case 'd': | ||
375 | idevice_set_debug_level(1); | ||
376 | break; | ||
377 | case 'u': | ||
378 | if (!*optarg) { | ||
379 | fprintf(stderr, "ERROR: UDID argument must not be empty!\n"); | ||
380 | print_usage(argc, argv, 1); | ||
381 | return 2; | ||
382 | } | ||
383 | udid = optarg; | ||
384 | break; | ||
385 | case 'n': | ||
386 | use_network = 1; | ||
387 | break; | ||
388 | case 'h': | ||
389 | print_usage(argc, argv, 0); | ||
390 | return 0; | ||
391 | case 'v': | ||
392 | printf("%s %s\n", TOOL_NAME, PACKAGE_VERSION); | ||
393 | return 0; | ||
394 | case 'f': | ||
395 | if (!*optarg) { | ||
396 | fprintf(stderr, "ERROR: filter argument must not be empty!\n"); | ||
397 | print_usage(argc, argv, 1); | ||
398 | return 2; | ||
399 | } | ||
400 | filename_filter = optarg; | ||
401 | break; | ||
402 | case 'e': | ||
403 | extract_raw_crash_reports = 1; | ||
404 | break; | ||
405 | case 'k': | ||
406 | keep_crash_reports = 1; | ||
407 | break; | ||
408 | default: | ||
409 | print_usage(argc, argv, 1); | ||
410 | return 2; | ||
411 | } | ||
412 | } | ||
413 | argc -= optind; | ||
414 | argv += optind; | ||
415 | |||
416 | /* ensure a target directory was supplied */ | ||
417 | if (!argv[0]) { | ||
418 | fprintf(stderr, "ERROR: missing target directory.\n"); | ||
419 | print_usage(argc+optind, argv-optind, 1); | ||
420 | return 2; | ||
421 | } | ||
422 | target_directory = argv[0]; | ||
423 | |||
424 | /* check if target directory exists */ | ||
425 | if (!file_exists(target_directory)) { | ||
426 | fprintf(stderr, "ERROR: Directory '%s' does not exist.\n", target_directory); | ||
427 | return 1; | ||
428 | } | ||
429 | |||
430 | device_error = idevice_new_with_options(&device, udid, (use_network) ? IDEVICE_LOOKUP_NETWORK : IDEVICE_LOOKUP_USBMUX); | ||
431 | if (device_error != IDEVICE_E_SUCCESS) { | ||
432 | if (udid) { | ||
433 | printf("No device found with udid %s.\n", udid); | ||
434 | } else { | ||
435 | printf("No device found.\n"); | ||
436 | } | ||
437 | return -1; | ||
438 | } | ||
439 | |||
440 | lockdownd_error = lockdownd_client_new_with_handshake(device, &lockdownd, TOOL_NAME); | ||
441 | if (lockdownd_error != LOCKDOWN_E_SUCCESS) { | ||
442 | fprintf(stderr, "ERROR: Could not connect to lockdownd, error code %d\n", lockdownd_error); | ||
443 | idevice_free(device); | ||
444 | return -1; | ||
445 | } | ||
446 | |||
447 | /* start crash log mover service */ | ||
448 | lockdownd_service_descriptor_t service = NULL; | ||
449 | lockdownd_error = lockdownd_start_service(lockdownd, CRASH_REPORT_MOVER_SERVICE, &service); | ||
450 | if (lockdownd_error != LOCKDOWN_E_SUCCESS) { | ||
451 | fprintf(stderr, "ERROR: Could not start service %s: %s\n", CRASH_REPORT_MOVER_SERVICE, lockdownd_strerror(lockdownd_error)); | ||
452 | lockdownd_client_free(lockdownd); | ||
453 | idevice_free(device); | ||
454 | return -1; | ||
455 | } | ||
456 | |||
457 | /* trigger move operation on device */ | ||
458 | service_client_t svcmove = NULL; | ||
459 | service_error_t service_error = service_client_new(device, service, &svcmove); | ||
460 | lockdownd_service_descriptor_free(service); | ||
461 | service = NULL; | ||
462 | if (service_error != SERVICE_E_SUCCESS) { | ||
463 | lockdownd_client_free(lockdownd); | ||
464 | idevice_free(device); | ||
465 | return -1; | ||
466 | } | ||
467 | |||
468 | /* read "ping" message which indicates the crash logs have been moved to a safe harbor */ | ||
469 | char *ping = malloc(4); | ||
470 | memset(ping, '\0', 4); | ||
471 | int attempts = 0; | ||
472 | while ((strncmp(ping, "ping", 4) != 0) && (attempts < 10)) { | ||
473 | uint32_t bytes = 0; | ||
474 | service_error = service_receive_with_timeout(svcmove, ping, 4, &bytes, 2000); | ||
475 | if (service_error == SERVICE_E_SUCCESS || service_error == SERVICE_E_TIMEOUT) { | ||
476 | attempts++; | ||
477 | continue; | ||
478 | } | ||
479 | |||
480 | fprintf(stderr, "ERROR: Crash logs could not be moved. Connection interrupted (%d).\n", service_error); | ||
481 | break; | ||
482 | } | ||
483 | service_client_free(svcmove); | ||
484 | free(ping); | ||
485 | |||
486 | if (device_error != IDEVICE_E_SUCCESS || attempts > 10) { | ||
487 | fprintf(stderr, "ERROR: Failed to receive ping message from crash report mover.\n"); | ||
488 | lockdownd_client_free(lockdownd); | ||
489 | idevice_free(device); | ||
490 | return -1; | ||
491 | } | ||
492 | |||
493 | lockdownd_error = lockdownd_start_service(lockdownd, CRASH_REPORT_COPY_MOBILE_SERVICE, &service); | ||
494 | if (lockdownd_error != LOCKDOWN_E_SUCCESS) { | ||
495 | fprintf(stderr, "ERROR: Could not start service %s: %s\n", CRASH_REPORT_COPY_MOBILE_SERVICE, lockdownd_strerror(lockdownd_error)); | ||
496 | lockdownd_client_free(lockdownd); | ||
497 | idevice_free(device); | ||
498 | return -1; | ||
499 | } | ||
500 | lockdownd_client_free(lockdownd); | ||
501 | |||
502 | afc = NULL; | ||
503 | afc_error = afc_client_new(device, service, &afc); | ||
504 | if(afc_error != AFC_E_SUCCESS) { | ||
505 | lockdownd_client_free(lockdownd); | ||
506 | idevice_free(device); | ||
507 | return -1; | ||
508 | } | ||
509 | |||
510 | if (service) { | ||
511 | lockdownd_service_descriptor_free(service); | ||
512 | service = NULL; | ||
513 | } | ||
514 | |||
515 | /* recursively copy crash reports from the device to a local directory */ | ||
516 | if (afc_client_copy_and_remove_crash_reports(afc, ".", target_directory, filename_filter) < 0) { | ||
517 | fprintf(stderr, "ERROR: Failed to get crash reports from device.\n"); | ||
518 | afc_client_free(afc); | ||
519 | idevice_free(device); | ||
520 | return -1; | ||
521 | } | ||
522 | |||
523 | printf("Done.\n"); | ||
524 | |||
525 | afc_client_free(afc); | ||
526 | idevice_free(device); | ||
527 | |||
528 | return 0; | ||
529 | } | ||