#import #import #import #import #import #import #import #import #include #include #include #include "builtin_debugger_cmds.h" #define DEVICE_FAMILY_IPHONE 1 #define DEVICE_FAMILY_IPAD 2 #define DEVICE_RETINA_FALSE 0 #define DEVICE_RETINA_TRUE 1 #define DEVICE_RETINA_3_5 2 #define DEVICE_RETINA_4 4 @interface Delegate : NSObject - (NSString *)replEval:(NSString *)expression; @end #include "rmtask.m" static bool spec_mode = false; static bool debug_sim_window_offsets = false; static int debug_mode = -1; #define DEBUG_GDB 1 #define DEBUG_REPL 2 #define DEBUG_NOTHING 0 static Delegate *delegate = nil; static NSArray *app_windows_ids = nil; static NSRunningApplication *running_app = nil; #if defined(SIMULATOR_IOS) static RMTask *gdb_task = nil; static id current_session = nil; static BOOL debugger_killed_session = NO; static NSString *xcode_path = nil; static int simulator_retina_type = DEVICE_RETINA_FALSE; static int simulator_device_family = DEVICE_FAMILY_IPHONE; #endif static NSString *sdk_version = nil; static NSString *replSocketPath = nil; static int repl_fd = -1; static NSLock *repl_fd_lock = nil; #define HISTORY_FILE @".repl_history" static void save_repl_history(void) { NSMutableArray *lines = [NSMutableArray array]; for (int i = 0; i < history_length; i++) { HIST_ENTRY *entry = history_get(history_base + i); if (entry == NULL) { break; } [lines addObject:[NSString stringWithUTF8String:entry->line]]; } NSString *data = [lines componentsJoinedByString:@"\n"]; NSError *error = nil; if (![data writeToFile:HISTORY_FILE atomically:YES encoding:NSUTF8StringEncoding error:&error]) { fprintf(stderr, "*** Cannot save REPL history file to `%s': %s\n", [HISTORY_FILE UTF8String], [[error description] UTF8String]); } } static void load_repl_history(void) { NSString *data = [NSString stringWithContentsOfFile:HISTORY_FILE encoding:NSUTF8StringEncoding error:nil]; if (data != nil) { NSArray *lines = [data componentsSeparatedByString:@"\n"]; for (NSString *line in lines) { line = [line stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; add_history([line UTF8String]); } } } static void terminate_session(void) { static bool terminated = false; if (!terminated) { #if defined(SIMULATOR_IOS) // requestEndWithTimeout: must be called only once. assert(current_session != nil); ((void (*)(id, SEL, double))objc_msgSend)(current_session, @selector(requestEndWithTimeout:), 0.0); #else save_repl_history(); #endif terminated = true; } } #if defined(SIMULATOR_IOS) static void sigterminate(int sig) { terminate_session(); exit(0); } static void sigforwarder(int sig) { if (gdb_task != nil) { kill([gdb_task processIdentifier], sig); } } #endif static void sigcleanup(int sig) { if (debug_mode == DEBUG_REPL) { save_repl_history(); } exit(1); } #if defined(SIMULATOR_OSX) static NSTask *osx_task = nil; static void sigint_osx(int sig) { if (osx_task != nil) { kill([osx_task processIdentifier], sig); } if (debug_mode == DEBUG_REPL) { save_repl_history(); } exit(0); } #endif @implementation Delegate static int expr_level = 0; static NSString * current_repl_prompt(NSString *top_level) { char question = '?'; if (top_level == nil) { static bool first_time = true; if (first_time) { top_level = @"main"; first_time = false; } else { top_level = [delegate replEval:@"self"]; } question = '>'; } if ([top_level length] > 30) { top_level = [[top_level substringToIndex:30] stringByAppendingString:@"..."]; } NSString *prompt = [NSString stringWithFormat:@"(%@)%c ", top_level, question]; for (int i = 0; i < expr_level; i++) { prompt = [prompt stringByAppendingString:@" "]; } return prompt; } #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1060 // This readline function is not implemented in Snow Leopard. // Code copied from http://cvsweb.netbsd.org/bsdweb.cgi/src/lib/libedit/readline.c?only_with_tag=MAIN #if !defined(RL_PROMPT_START_IGNORE) # define RL_PROMPT_START_IGNORE '\1' #endif #if !defined(RL_PROMPT_END_IGNORE) # define RL_PROMPT_END_IGNORE '\2' #endif int rl_set_prompt(const char *prompt) { char *p; if (!prompt) prompt = ""; if (rl_prompt != NULL && strcmp(rl_prompt, prompt) == 0) return 0; if (rl_prompt) /*el_*/free(rl_prompt); rl_prompt = strdup(prompt); if (rl_prompt == NULL) return -1; while ((p = strchr(rl_prompt, RL_PROMPT_END_IGNORE)) != NULL) *p = RL_PROMPT_START_IGNORE; return 0; } #endif static void refresh_repl_prompt(NSString *top_level, bool clear) { dispatch_async(dispatch_get_main_queue(), ^{ static int previous_prompt_length = 0; rl_set_prompt([current_repl_prompt(top_level) UTF8String]); if (clear) { putchar('\r'); for (int i = 0; i < previous_prompt_length; i++) { putchar(' '); } putchar('\r'); //printf("\n\033[F\033[J"); // Clear. rl_forced_update_display(); } previous_prompt_length = strlen(rl_prompt); }); } static void locate_app_windows_ids(void) { if (app_windows_ids != nil) { return; } NSMutableArray *ids = [[NSMutableArray alloc] init]; CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID); bool bounds_ok = false; for (NSDictionary *dict in (NSArray *)windows) { #define validate(obj, klass) \ if (obj == nil || ![obj isKindOfClass:[klass class]]) { \ continue; \ } id name = [dict objectForKey:@"kCGWindowName"]; validate(name, NSString); #if defined(SIMULATOR_IOS) static NSArray *patterns = nil; if (patterns == nil) { patterns = [[NSArray alloc] initWithObjects: [NSString stringWithFormat:@"iPhone - iOS %@", sdk_version], [NSString stringWithFormat:@"iPad - iOS %@", sdk_version], [NSString stringWithFormat:@"iPhone / iOS %@", sdk_version], [NSString stringWithFormat:@"iPad / iOS %@", sdk_version], [NSString stringWithFormat:@"iPhone (Retina 3.5-inch) - iOS %@", sdk_version], [NSString stringWithFormat:@"iPhone (Retina 3.5-inch) / iOS %@", sdk_version], [NSString stringWithFormat:@"iPhone Retina (3.5-inch) / iOS %@", sdk_version], [NSString stringWithFormat:@"iPhone (Retina 4-inch) - iOS %@", sdk_version], [NSString stringWithFormat:@"iPhone (Retina 4-inch) / iOS %@", sdk_version], [NSString stringWithFormat:@"iPhone Retina (4-inch) / iOS %@", sdk_version], [NSString stringWithFormat:@"iPhone Retina (4-inch 64-bit) / iOS %@", sdk_version], [NSString stringWithFormat:@"iPad (Retina) - iOS %@", sdk_version], [NSString stringWithFormat:@"iPad (Retina) / iOS %@", sdk_version], [NSString stringWithFormat:@"iPad Retina / iOS %@", sdk_version], nil]; } bool found = false; for (NSString *pattern in patterns) { if ([name rangeOfString:pattern].location != NSNotFound) { found = true; break; } } if (!found) { continue; } if ([name rangeOfString:@"Retina"].location != NSNotFound) { simulator_retina_type = DEVICE_RETINA_TRUE; if ([name rangeOfString:@"3.5-inch"].location != NSNotFound) { simulator_retina_type = DEVICE_RETINA_3_5; } else if ([name rangeOfString:@"4-inch"].location != NSNotFound) { simulator_retina_type = DEVICE_RETINA_4; } } if ([name rangeOfString:@"iPad"].location != NSNotFound) { simulator_device_family = DEVICE_FAMILY_IPAD; } #else // !SIMULATOR_IOS int window_pid = [[dict objectForKey:@"kCGWindowOwnerPID"] intValue]; if (window_pid != [osx_task processIdentifier]) { continue; } if ([[dict objectForKey:@"kCGWindowName"] isEqualToString:@"__HIGHLIGHT_OVERLAY__"]) { continue; } #endif #undef validate [ids addObject:[dict objectForKey:@"kCGWindowNumber"]]; bounds_ok = true; #if defined(SIMULATOR_IOS) // On iOS there is only one app window (the simulator). break; #endif } if (ids.count > 0) { app_windows_ids = (NSArray *)ids; } else { [ids release]; } CFRelease(windows); if (!bounds_ok) { #if defined(SIMULATOR_IOS) static bool error_printed = false; if (!error_printed) { fprintf(stderr, "*** Cannot locate the Simulator app, mouse-over disabled\n"); error_printed = true; } #endif } } static NSArray * get_app_windows_bounds(void) { if (app_windows_ids == nil) { locate_app_windows_ids(); if (app_windows_ids == nil) return nil; } // TODO if need be, we can use KVO on -[NSRunningApplication active] to // completely disable getting these events when the app is inactive. if (running_app == nil || !running_app.isActive) { return nil; } NSMutableArray *app_windows_bounds = [NSMutableArray arrayWithCapacity:app_windows_ids.count]; int count = app_windows_ids.count; uint32_t ids[count]; for (int i = 0; i < count; i++) { ids[i] = [[app_windows_ids objectAtIndex:i] intValue]; } CFArrayRef windowArray = CFArrayCreate(NULL, (const void **)ids, count, NULL); NSArray *windows = (NSArray *)CGWindowListCreateDescriptionFromArray(windowArray); CFRelease(windowArray); // The window(s) are gone. Need to find them again. // // This happens, for instance, when the iOS Simulator changes the devices // scale. if (windows.count != app_windows_ids.count) { app_windows_ids = nil; locate_app_windows_ids(); if (app_windows_ids == nil) return nil; } NSRect bounds = NSZeroRect; for (NSDictionary *dict in windows) { // TODO is validation here really needed? //#define validate(obj, klass) \ //if (obj == nil || ![obj isKindOfClass:[klass class]]) { \ //continue; \ //} id bounds_dict = [dict objectForKey:@"kCGWindowBounds"]; // validate(bounds_dict, NSDictionary); id x = [bounds_dict objectForKey:@"X"]; id y = [bounds_dict objectForKey:@"Y"]; id width = [bounds_dict objectForKey:@"Width"]; id height = [bounds_dict objectForKey:@"Height"]; //validate(x, NSNumber); //validate(y, NSNumber); //validate(width, NSNumber); //validate(height, NSNumber); bounds.origin.x = [x intValue]; bounds.origin.y = [y intValue]; bounds.size.width = [width intValue]; bounds.size.height = [height intValue]; // #undef validate #if defined(SIMULATOR_IOS) // Inset the main view frame. // // Devices that (at a scale of 100%) have thick borders and their // respective frames. // // On Retina Mac, the Retina iOS devices are shown at their native size // while on a Non-Retina Mac they are twice as large. // // Non-Retina Mac // =========================================================== // DEVICE | PORTRAIT FRAME // =========================================================== // Original iPhone | 368x716 // ----------------------------------------------------------- // iPhone Retina 3.5" | 724x1044 // ----------------------------------------------------------- // iPhone Retina 4" | 724x1220 // ----------------------------------------------------------- // Original iPad | 852x1108 // ----------------------------------------------------------- // iPad Retina | 1704x2216 (TODO This is the screenshot!) // ----------------------------------------------------------- // Retina Mac // // =========================================================== // DEVICE | PORTRAIT FRAME // =========================================================== // Original iPhone | 368x716 // ----------------------------------------------------------- // iPhone Retina 3.5" | 368x716 // ----------------------------------------------------------- // iPhone Retina 4" | 386x806 // ----------------------------------------------------------- // Original iPad | 852x1108 // ----------------------------------------------------------- // iPad Retina | 852x1108 // ----------------------------------------------------------- int w = (int)CGRectGetWidth(bounds); int h = (int)CGRectGetHeight(bounds); // NSLog(@"W: %d H: %d", w, h); bool portrait = w < h; bool retina_mac = (int)[[NSScreen mainScreen] backingScaleFactor] > 1; bool found_thick_border = false; #define MATCHES_FRAME(ww, hh) (portrait ? (w == ww && h == hh) : (w == hh && h == ww)) #define LOG_MATCH(desc) \ if (debug_sim_window_offsets) { \ fprintf(stderr, "*** Recognized current Simulator window as %s.\n", desc); \ } // Inset to account for the border and offset for the window drop shadow. #define BORDER_INSET(x, y) \ bounds = CGRectOffset(portrait ? CGRectInset(bounds, x, y) : CGRectInset(bounds, y, x), 1, 2); \ found_thick_border = true; if (simulator_device_family == DEVICE_FAMILY_IPHONE) { if (simulator_retina_type == DEVICE_RETINA_FALSE) { if (MATCHES_FRAME(368, 716)) { LOG_MATCH("a Non-Retina iPhone with thick iPhone border"); // Same size on *Non* Retina and Retina Mac *with* iPhone border. // State: Portrait & Landscape perfect! BORDER_INSET(24, 118); } } else if (simulator_retina_type == DEVICE_RETINA_3_5) { if (retina_mac) { if (MATCHES_FRAME(368, 716)) { LOG_MATCH("a Retina iPhone 3.5\" with thick iPhone border " \ "running on a Retina Mac"); // Retina Mac with enough space for the full iPhone border. // State: Portrait & Landscape perfect! BORDER_INSET(24, 118); } else if (MATCHES_FRAME(366, 526)) { LOG_MATCH("a Retina iPhone 3.5\" with thick iPad border " \ "running on a Retina Mac"); // Retina Mac with not enough space for the full iPhone border, // so gets an iPad border. // State: Untested! Does it even exist? BORDER_INSET(23, 23); } } else { if (MATCHES_FRAME(368, 716)) { LOG_MATCH("a Retina iPhone 3.5\" with thick iPhone border " \ "running on a Non-Retina Mac"); // Non-Retina Mac with enough space for the full iPhone border. // State: Untested! BORDER_INSET(24, 118); } else if (MATCHES_FRAME(724, 1044)) { LOG_MATCH("a Retina iPhone 3.5\" with thick iPad border " \ "running on a Non-Retina Mac"); // Non-Retina Mac with not enough space for the full iPhone // border, so gets an iPad border. // State: Portrait & Landscape perfect! BORDER_INSET(42, 42); } } } else if (simulator_retina_type == DEVICE_RETINA_4) { if (retina_mac) { if (MATCHES_FRAME(386, 806)) { LOG_MATCH("a Retina iPhone 4\" with thick iPhone border " \ "running on a Retina Mac"); // Retina Mac with enough space for the full iPhone border. // State: Untested! BORDER_INSET(33, 119); } else if (MATCHES_FRAME(366, 614)) { LOG_MATCH("a Retina iPhone 4\" with thick iPad border " \ "running on a Retina Mac"); // Retina Mac with not enough space for the full iPhone border, // so gets an iPad border. // State: Untested! Does it even exist? BORDER_INSET(23, 23); } } else { if (MATCHES_FRAME(724, 1220)) { LOG_MATCH("a Retina iPhone 4\" with thick iPad border " \ "running on a Non-Retina Mac"); // Non-Retina Mac with not enough space for the full iPhone // border, so gets an iPad border. // State: Portrait & Landscape perfect! BORDER_INSET(42, 42); } else if (MATCHES_FRAME(706, 1374)) { LOG_MATCH("a Retina iPhone 4\" with thick iPhone border " \ "running on a Retina Mac"); // Non-Retina Mac with enough space for the full iPhone border. // State: Untested! Does it even exist? BORDER_INSET(33, 119); } } } } // iPad else { if (MATCHES_FRAME(852, 1108)) { LOG_MATCH("a Non-Retina iPad with thick iPad border"); // Non-Retina iPad is the same size on Non-Retina and Retina Mac, // as is the Retina iPad on a Retina Mac. // State: Only Non-Retina iPad + Non-Retina Mac: Portrait & Landscape perfect! BORDER_INSET(42, 42); } else if (simulator_retina_type == DEVICE_RETINA_TRUE && !retina_mac && MATCHES_FRAME(1582, 2216)) { LOG_MATCH("a Retina iPad with thick border on a Non-Retina Mac"); // Retina iPad on a Non-Retina Mac is roughly twice as large. // State: Untested! Does it even exist? BORDER_INSET(42, 42); } } if (!found_thick_border) { if (debug_sim_window_offsets) { fprintf(stderr, "*** Current Simulator window NOT recognized as " \ "having thick borders (%dx%d).\n", w, h); } bounds.origin.y += 24; bounds.size.height -= 24; } #undef MATCHES_FRAME #undef LOG_MATCH #undef BORDER_INSET #endif [app_windows_bounds addObject:[NSValue valueWithRect:bounds]]; #if defined(SIMULATOR_IOS) // On iOS there is only one app window (the simulator). break; #endif } [windows release]; return (NSArray *)app_windows_bounds; } #define CONCURRENT_BEGIN dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ #define CONCURRENT_END }); static CGEventRef event_tap_cb(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *ctx) { static bool previousHighlight = false; if (!(CGEventGetFlags(event) & kCGEventFlagMaskCommand)) { CONCURRENT_BEGIN if (previousHighlight) { [delegate replEval:[NSString stringWithFormat: @"< 0) { buf[len] = '\0'; [res appendString:[NSString stringWithUTF8String:buf]]; } else { if ([res length] == 0) { [res release]; return nil; } } received_something = true; } return [res autorelease]; } - (NSString *)replEval:(NSString *)expression { if (repl_fd <= 0) { return nil; } if (repl_fd_lock == nil) { repl_fd_lock = [NSLock new]; } [repl_fd_lock lock]; NSString *res = nil; if (send_string(expression)) { res = receive_string(); } [repl_fd_lock unlock]; return res; } static NSArray * repl_complete_data(const char *text) { // Determine if we want to complete a method or not. size_t len = strlen(text); if (len == 0) { return NULL; } bool method = false; int i; for (i = len - 1; i >= 1; i--) { if (text[i] == ' ' || text[i] == '\t') { break; } else if (text[i] == '.') { method = true; break; } } // Prepare the REPL expression to evaluate. char buf[1024]; strlcpy(buf, "<= sizeof(buf) - strlen(buf)) { return NULL; } strncat(buf, text, i); strlcat(buf, ".methods", sizeof buf); } else { if (isupper(text[0])) { strlcat(buf, "Object.constants", sizeof buf); } else if (text[0] == '@') { strlcat(buf, "instance_variables", sizeof buf); } else { strlcat(buf, "local_variables", sizeof buf); } } // Evaluate the expression. NSString *list = [delegate replEval: [NSString stringWithUTF8String:buf]]; if ([list characterAtIndex:0] != '[') { // Not an array, likely an exception. return NULL; } // Ignore trailing '[' and ']'. list = [list substringWithRange:NSMakeRange(1, [list length] - 2)]; // Split tokens. NSMutableArray *data = [[NSMutableArray alloc] init]; NSArray *all = [list componentsSeparatedByString:@", "]; // Prepare first part of completion. const char *p = &text[i]; if (method) { p++; } NSString *last = [NSString stringWithUTF8String:p]; const size_t last_length = [last length]; // Filter all tokens based on the first part of completion. for (NSString *res in all) { size_t res_length = [res length]; int skip_beg = 1; // Results are symbols, so we skip ':'. int skip_end = 0; if (res_length < last_length + 1) { continue; } if ([res characterAtIndex:skip_beg] == '"') { skip_beg++; // Special symbol, :"foo:bar:". skip_end++; } if (res_length < last_length + skip_beg + skip_end) { continue; } if (last_length == 0) { if (method && [res characterAtIndex:skip_beg] == '_') { // Skip 'private' methods if we are searching for all // methods. continue; } } else { NSString *first = [res substringWithRange:NSMakeRange(skip_beg, last_length)]; if (![first isEqualToString:last]) { continue; } } res = [res substringWithRange:NSMakeRange(skip_beg, [res length] - skip_beg - skip_end)]; [data addObject:res]; } // Now prepare the suggested completion result. int data_count = [data count]; if (data_count >= 1) { NSString *suggested = nil; if (method) { suggested = [NSString stringWithUTF8String:text]; } else if (data_count == 1) { suggested = [data objectAtIndex:0]; } else { int i = 0, low = 100000; while (i < data_count) { int si = 0; while (true) { if (i + 1 >= data_count) { break; } NSString *s1 = [data objectAtIndex:i]; NSString *s2 = [data objectAtIndex:i + 1]; if (si >= [s1 length] || si >= [s2 length]) { break; } if ([s1 characterAtIndex:si] != [s2 characterAtIndex:si]) { break; } si++; } if (low > si) { low = si; } i++; } suggested = [[data objectAtIndex:0] substringToIndex:low]; } [data insertObject:suggested atIndex:0]; } return [data autorelease]; } static char ** repl_complete(const char *text, int start, int end) { NSArray *data = repl_complete_data(text); if (data == nil) { return NULL; } int data_count = [data count]; if (data_count == 0) { return NULL; } char **res = (char **)malloc(sizeof(char *) * (data_count + 1)); for (int i = 0; i < data_count; i++) { res[i + 0] = strdup([[data objectAtIndex:i] UTF8String]); } res[[data count]] = NULL; return res; } - (void)readEvalPrintLoop { [[NSAutoreleasePool alloc] init]; // Wait until the socket file is created. while (true) { if ([[NSFileManager defaultManager] fileExistsAtPath:replSocketPath]) { break; } usleep(500000); } // Create the socket. const int fd = socket(PF_LOCAL, SOCK_STREAM, 0); if (fd == -1) { perror("socket()"); terminate_session(); return; } fcntl(fd, F_SETFL, O_NONBLOCK); // Prepare the name. struct sockaddr_un name; name.sun_family = PF_LOCAL; strncpy(name.sun_path, [replSocketPath fileSystemRepresentation], sizeof(name.sun_path)); // Connect. if (connect(fd, (struct sockaddr *)&name, SUN_LEN(&name)) == -1) { perror("connect()"); terminate_session(); return; } repl_fd = fd; rl_readline_name = (char *)"RubyMotionRepl"; using_history(); load_repl_history(); rl_attempted_completion_function = repl_complete; rl_basic_word_break_characters = strdup(" \t\n`<;|&("); NSString *expr = nil; while (true) { // Read expression from stdin. NSString *prompt = current_repl_prompt(nil); char *line_cstr = readline([prompt UTF8String]); if (line_cstr == NULL) { terminate_session(); break; } NSString *line = [NSString stringWithUTF8String:line_cstr]; free(line_cstr); line_cstr = NULL; if ([line length] == 0) { continue; } // Parse the expression to see if it's complete. static NSDictionary *begin_tokens = nil; if (begin_tokens == nil) { begin_tokens = [[NSDictionary alloc] initWithObjectsAndKeys: @"1", @"class", @"1", @"module", @"1", @"def", @"1", @"begin", @"1", @"if", @"1", @"unless", @"1", @"case", @"1", @"while", @"1", @"for", @"1", @"do", nil]; } NSMutableString *parse_data = [line mutableCopy]; while (true) { NSUInteger i, count; again: i = 0; count = [parse_data length]; for (i = 0; i < count; i++) { UniChar c = [parse_data characterAtIndex:i]; switch (c) { case '\'': case '"': case '/': case '`': for (NSUInteger k = i + 1; k < count; k++) { UniChar c2 = [parse_data characterAtIndex:k]; if (c2 == '\\') { k++; } else if (c2 == c) { NSRange range = { i, k - i }; [parse_data deleteCharactersInRange:range]; goto again; } } break; } } break; } NSArray *tokens = [parse_data componentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; [parse_data release]; int old_expr_level = expr_level; for (NSString *token in tokens) { if ([begin_tokens objectForKey:token] != nil) { expr_level++; } else if ([token isEqualToString:@"end"]) { expr_level--; } } if (expr == nil) { expr = line; } else { expr = [expr stringByAppendingString:@"\n"]; expr = [expr stringByAppendingString:line]; } if (old_expr_level - 1 == expr_level) { printf("\e[1A\r\e[0K%s%s\n", [[prompt substringToIndex:[prompt length] - 2] UTF8String], [line UTF8String]); } // The expression is not complete yet. if (expr_level > 0) { continue; } // The expression is complete, add to history, eval it and print // the result. add_history([expr UTF8String]); NSString *res = [self replEval:expr]; if (res == nil) { if ([line compare:@"quit"] == NSOrderedSame || [line compare:@"exit"] == NSOrderedSame) { terminate_session(); } break; } printf("=> %s\n", [res UTF8String]); expr = nil; expr_level = 0; } } static NSString * save_debugger_command(NSString *cmds) { #if defined(SIMULATOR_IOS) # define SIMGDBCMDS_BASE @"_simgdbcmds_ios" #else # define SIMGDBCMDS_BASE @"_simgdbcmds_osx" #endif NSString *cmds_path = [NSString pathWithComponents: [NSArray arrayWithObjects:NSTemporaryDirectory(), SIMGDBCMDS_BASE, nil]]; NSError *error = nil; if (![cmds writeToFile:cmds_path atomically:YES encoding:NSASCIIStringEncoding error:&error]) { fprintf(stderr, "*** Cannot write gdb commands file into path %s: %s\n", [cmds_path UTF8String], [[error description] UTF8String]); exit(1); } return cmds_path; } static NSString * gdb_commands_file(void) { NSString *cmds = @""\ "set breakpoint pending on\n"\ "break rb_exc_raise\n"\ "break malloc_error_break\n"; cmds = [cmds stringByAppendingFormat:@"%s\n", BUILTIN_DEBUGGER_CMDS]; NSString *user_cmds = [NSString stringWithContentsOfFile: @"debugger_cmds" encoding:NSUTF8StringEncoding error:nil]; if (user_cmds != nil) { cmds = [cmds stringByAppendingString:user_cmds]; cmds = [cmds stringByAppendingString:@"\n"]; } if (getenv("no_continue") == NULL) { cmds = [cmds stringByAppendingString: #if defined(SIMULATOR_IOS) @"continue\n" #else @"run\n" #endif ]; } return save_debugger_command(cmds); } static NSString * lldb_commands_file(int pid, NSString *app_path) { NSString *cmds = @""; if (pid >= 0) { cmds = [cmds stringByAppendingFormat:@"process attach -p %d\n", pid]; } else if (app_path != nil) { cmds = [cmds stringByAppendingFormat:@"target create \"%@\"\n", app_path]; } else { abort(); } cmds = [cmds stringByAppendingString:@""\ "command script import /Library/RubyMotion/lldb/lldb.py\n"\ "breakpoint set --name rb_exc_raise\n"\ "breakpoint set --name malloc_error_break\n"]; NSString *user_cmds = [NSString stringWithContentsOfFile: @"debugger_cmds" encoding:NSUTF8StringEncoding error:nil]; if (user_cmds != nil) { cmds = [cmds stringByAppendingString:user_cmds]; cmds = [cmds stringByAppendingString:@"\n"]; } if (getenv("no_continue") == NULL) { cmds = [cmds stringByAppendingString: #if defined(SIMULATOR_IOS) @"continue\n" #else @"run\n" #endif ]; } return save_debugger_command(cmds); } #if defined(SIMULATOR_IOS) - (void)session:(id)session didEndWithError:(NSError *)error { if (gdb_task != nil) { [gdb_task terminate]; [gdb_task waitUntilExit]; } if (debug_mode == DEBUG_REPL) { save_repl_history(); } if (spec_mode || error == nil || debugger_killed_session) { int status = 0; int pidNumber = ((int (*)(id, SEL))objc_msgSend)(session, @selector(simulatedApplicationPID)); if (pidNumber != 0) { NSString *path = [NSString stringWithFormat: @"/tmp/.rubymotion_process_exited.%d", pidNumber]; NSString *res = [NSString stringWithContentsOfFile:path encoding:NSASCIIStringEncoding error:nil]; if (res != nil) { status = [res intValue]; } [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; } exit(status); } else { fprintf(stderr, "*** Simulator session ended with error: %s\n", [[error description] UTF8String]); exit(1); } } - (void)session:(id)session didStart:(BOOL)flag withError:(NSError *)error { if (!flag || error != nil) { fprintf(stderr, "*** Simulator session started with error: %s\n", [[error description] UTF8String]); exit(1); } // Open simulator to the foreground. if (!spec_mode) { NSArray *ary = [NSRunningApplication runningApplicationsWithBundleIdentifier: @"com.apple.iphonesimulator"]; if ([ary count] == 1) { running_app = [ary[0] retain]; // When launching in ‘background fetch’ mode, most users will log // debug output, only a few apps will update the UI (icon badge // or local notification). if (getenv("background_fetch") == NULL) { [running_app activateWithOptions: NSApplicationActivateIgnoringOtherApps]; } } } if (debug_mode == DEBUG_GDB) { int pidNumber = ((int (*)(id, SEL))objc_msgSend)(session, @selector(simulatedApplicationPID)); if (pidNumber == 0) { fprintf(stderr, "*** Cannot get simulated application PID\n"); exit(1); } // Forward ^C to gdb. signal(SIGINT, sigforwarder); // Run the debugger process. NSString *gdb_path = [xcode_path stringByAppendingPathComponent:@"Platforms/iPhoneSimulator.platform/Developer/usr/libexec/gdb/gdb-i386-apple-darwin"]; NSString *lldb_path = [xcode_path stringByAppendingPathComponent:@"Platforms/iPhoneSimulator.platform/Developer/usr/bin/lldb"]; if ([[NSFileManager defaultManager] fileExistsAtPath:gdb_path]) { gdb_task = [[RMTask launchedTaskWithLaunchPath:gdb_path arguments:[NSArray arrayWithObjects:@"--arch", @"i386", @"-q", @"--pid", [NSString stringWithFormat:@"%d", pidNumber], @"-x", gdb_commands_file(), nil]] retain]; } else if ([[NSFileManager defaultManager] fileExistsAtPath:lldb_path]) { gdb_task = [[RMTask launchedTaskWithLaunchPath:lldb_path arguments:[NSArray arrayWithObjects:@"-a", @"i386", @"-s", lldb_commands_file(pidNumber, nil), nil]] retain]; } else { fprintf(stderr, "*** Cannot locate a debugger (either gdb `%s' or lldb `%s')\n", [gdb_path UTF8String], [lldb_path UTF8String]); exit(1); } [gdb_task waitUntilExit]; gdb_task = nil; debugger_killed_session = YES; ((void (*)(id, SEL, NSTimeInterval))objc_msgSend)(session, @selector(requestEndWithTimeout:), 0); } else if (debug_mode == DEBUG_REPL) { [NSThread detachNewThreadSelector:@selector(readEvalPrintLoop) toTarget:self withObject:nil]; start_capture(self); } //fprintf(stderr, "*** simulator session started\n"); } #endif @end static void usage(void) { system("open http://www.youtube.com/watch?v=1orMXD_Ijbs&feature=fvst"); exit(1); } int main(int argc, char **argv) { [[NSAutoreleasePool alloc] init]; #if defined(SIMULATOR_IOS) # define MIN_ARGS 7 #else # define MIN_ARGS 4 #endif if (argc < MIN_ARGS) { usage(); } spec_mode = getenv("SIM_SPEC_MODE") != NULL; debug_sim_window_offsets = getenv("DEBUG_SIM_WINDOW_OFFSETS") != NULL; int argv_n = 1; debug_mode = atoi(argv[argv_n++]); #if defined(SIMULATOR_IOS) NSNumber *device_family = [NSNumber numberWithInt:atoi(argv[argv_n++])]; NSString *device_name = [[NSString stringWithUTF8String:argv[argv_n++]] retain]; #endif sdk_version = [[NSString stringWithUTF8String:argv[argv_n++]] retain]; #if defined(SIMULATOR_IOS) xcode_path = [[NSString stringWithUTF8String:argv[argv_n++]] retain]; #endif NSString *app_path = [NSString stringWithUTF8String:realpath(argv[argv_n++], NULL)]; NSMutableArray *app_args = [NSMutableArray new]; for (unsigned i = MIN_ARGS; i < argc; i++) { [app_args addObject:[NSString stringWithUTF8String:argv[i]]]; } #if defined(SIMULATOR_IOS) // Load the frameworks. [[NSBundle bundleWithPath:[xcode_path stringByAppendingPathComponent:@"../SharedFrameworks/DVTFoundation.framework"]] load]; [[NSBundle bundleWithPath:[xcode_path stringByAppendingPathComponent:@"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTiPhoneSimulatorRemoteClient.framework"]] load]; Class Platform = NSClassFromString(@"DVTPlatform"); if (Platform == NULL) { fprintf(stderr, "*** RubyMotion requires Xcode 5 or higher.\n\n"); exit(1); } Class AppSpecifier = NSClassFromString(@"DTiPhoneSimulatorApplicationSpecifier"); assert(AppSpecifier != nil); Class SystemRoot = NSClassFromString(@"DTiPhoneSimulatorSystemRoot"); assert(SystemRoot != nil); Class SessionConfig = NSClassFromString(@"DTiPhoneSimulatorSessionConfig"); assert(SessionConfig != nil); Class Session = NSClassFromString(@"DTiPhoneSimulatorSession"); assert(Session != nil); #endif // Prepare app environment. NSMutableDictionary *appEnvironment = [[[NSProcessInfo processInfo] environment] mutableCopy]; if (debug_mode != DEBUG_NOTHING) { // Prepare repl socket path. replSocketPath = [[appEnvironment objectForKey:@"REPL_SOCKET_PATH"] retain]; if (replSocketPath == nil) { NSString *tmpdir = NSTemporaryDirectory(); assert(tmpdir != nil); char path[PATH_MAX]; snprintf(path, sizeof path, "%s/rubymotion-repl-XXXXXX", [tmpdir fileSystemRepresentation]); assert(mktemp(path) != NULL); replSocketPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:path length:strlen(path)] retain]; [appEnvironment setObject:replSocketPath forKey:@"REPL_SOCKET_PATH"]; } // Make sure the unix socket path does not exist. [[NSFileManager defaultManager] removeItemAtPath:replSocketPath error:nil]; // Prepare repl dylib path. NSString *replPath = nil; replPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[0] length:strlen(argv[0])]; replPath = [replPath stringByDeletingLastPathComponent]; replPath = [replPath stringByDeletingLastPathComponent]; replPath = [replPath stringByDeletingLastPathComponent]; replPath = [replPath stringByAppendingPathComponent:@"data"]; #if defined(SIMULATOR_IOS) replPath = [replPath stringByAppendingPathComponent:@"ios"]; #else replPath = [replPath stringByAppendingPathComponent:@"osx"]; #endif replPath = [replPath stringByAppendingPathComponent:@"librubymotion-repl.dylib"]; [appEnvironment setObject:replPath forKey:@"REPL_DYLIB_PATH"]; } char *malloc_debug_level = NULL; if ((malloc_debug_level = getenv("malloc_debug")) != NULL) { int level = atoi(malloc_debug_level); if (level >= 1) { [appEnvironment setObject:@"1" forKey:@"MallocStackLoggingNoCompact"]; } if (level >= 2) { [appEnvironment setObject:@"/usr/lib/libgmalloc.dylib" forKey:@"DYLD_INSERT_LIBRARIES"]; } } #if defined(SIMULATOR_IOS) NSError *error = nil; // Initialize the DevTools environment. if (!((BOOL (*)(id, SEL, id *))objc_msgSend)(Platform, @selector(loadAllPlatformsReturningError:), &error)) { fprintf(stderr, "*** Cannot load simulator platforms: %s\n", [[error description] UTF8String]); exit(1); } // Create application specifier. id app_spec = ((id (*)(id, SEL, id))objc_msgSend)(AppSpecifier, @selector(specifierWithApplicationPath:), app_path); assert(app_spec != nil); // Create system root. id system_root = ((id (*)(id, SEL, id))objc_msgSend)(SystemRoot, @selector(rootWithSDKVersion:), sdk_version); if (system_root == nil) { fprintf(stderr, "*** iOS simulator for %s SDK not found.\n\n", [sdk_version UTF8String]); exit(1); } // Create session config. id config = [[SessionConfig alloc] init]; ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setApplicationToSimulateOnStart:), app_spec); ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedApplicationLaunchArgs:), app_args); ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedApplicationLaunchEnvironment:), appEnvironment); ((void (*)(id, SEL, BOOL))objc_msgSend)(config, @selector(setSimulatedApplicationShouldWaitForDebugger:), (debug_mode == DEBUG_GDB || getenv("SIM_WAIT_FOR_DEBUGGER") != NULL)); ((void (*)(id, SEL, BOOL))objc_msgSend)(config, @selector(setLaunchForBackgroundFetch:), getenv("background_fetch") != NULL); ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedDeviceFamily:), device_family); ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedDeviceInfoName:), device_name); ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedSystemRoot:), system_root); ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setLocalizedClientName:), @"NYANCAT"); char path[MAXPATHLEN] = {'\0'}; const char *stdout_path = getenv("SIM_STDOUT_PATH"); if (stdout_path == NULL) { if (fcntl(STDOUT_FILENO, F_GETPATH, &path) == -1) { printf("*** stdout unavailable, output disabled\n"); } else { stdout_path = path; } } if (stdout_path != NULL) { ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedApplicationStdOutPath:), [NSString stringWithUTF8String:stdout_path]); } const char *stderr_path = getenv("SIM_STDERR_PATH"); if (stderr_path == NULL) { if (fcntl(STDERR_FILENO, F_GETPATH, &path) == -1) { printf("*** stderr unavailable, output disabled\n"); } else { stderr_path = path; } } if (stderr_path != NULL) { ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedApplicationStdErrPath:), [NSString stringWithUTF8String:stderr_path]); } // Create session. id session = [[Session alloc] init]; delegate = [[Delegate alloc] init]; ((void (*)(id, SEL, id))objc_msgSend)(session, @selector(setDelegate:), delegate); // Start session. if (!((BOOL (*)(id, SEL, id, double, id *))objc_msgSend)(session, @selector(requestStartWithConfig:timeout:error:), config, 0.0, &error)) { fprintf(stderr, "*** Cannot start simulator: %s\n", [[error description] UTF8String]); exit(1); } if (debug_mode != DEBUG_GDB) { // ^C should terminate the request. current_session = session; signal(SIGINT, sigterminate); signal(SIGPIPE, sigcleanup); } [[NSRunLoop mainRunLoop] run]; #else // !SIMULATOR_IOS if (debug_mode != DEBUG_GDB) { signal(SIGINT, sigint_osx); signal(SIGPIPE, sigcleanup); delegate = [[Delegate alloc] init]; [NSThread detachNewThreadSelector:@selector(readEvalPrintLoop) toTarget:delegate withObject:nil]; osx_task = [[NSTask alloc] init]; [osx_task setEnvironment:appEnvironment]; [osx_task setLaunchPath:app_path]; [osx_task setArguments:app_args]; [osx_task launch]; // Capture running_app and move to the foreground. while (osx_task.isRunning) { usleep(0.1 * 1000000); running_app = [[NSRunningApplication runningApplicationWithProcessIdentifier: [osx_task processIdentifier]] retain]; if (running_app) { [running_app activateWithOptions: NSApplicationActivateIgnoringOtherApps]; break; } } start_capture(delegate); [osx_task waitUntilExit]; int status = [osx_task terminationStatus]; exit(status); } else { // Run the gdb process. // XXX using system(3) as NSTask isn't working well (termios issue). NSString *gdb_path = @"/usr/bin/gdb"; NSString *lldb_path = @"/usr/bin/lldb"; NSString *line = nil; if ([[NSFileManager defaultManager] fileExistsAtPath:gdb_path]) { line = [NSString stringWithFormat:@"%@ -x \"%@\" \"%@\"", gdb_path, gdb_commands_file(), app_path]; } else if ([[NSFileManager defaultManager] fileExistsAtPath:lldb_path]) { line = [NSString stringWithFormat:@"%@ -s \"%@\"", lldb_path, lldb_commands_file(-1, app_path)]; } else { fprintf(stderr, "*** Cannot locate a debugger (either gdb `%s' or lldb `%s')\n", [gdb_path UTF8String], [lldb_path UTF8String]); exit(1); } system([line UTF8String]); } #endif return 0; }