#import #import #import #import #import #import #import #include #include #include #include "builtin_debugger_cmds.h" @interface Delegate : NSObject - (NSString *)replEval:(NSString *)expression; @end static bool spec_mode = false; static int debug_mode = -1; #define DEBUG_GDB 1 #define DEBUG_REPL 2 #define DEBUG_NOTHING 0 static Delegate *delegate = nil; static NSTask *gdb_task = nil; static BOOL debugger_killed_session = NO; static id current_session = nil; static NSString *xcode_path = nil; static NSString *sdk_version = nil; static NSString *replSocketPath = nil; static NSRect simulator_app_bounds = { {0, 0}, {0, 0} }; 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:NSASCIIStringEncoding 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:NSASCIIStringEncoding 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) { // requestEndWithTimeout: must be called only once. assert(current_session != nil); ((void (*)(id, SEL, double))objc_msgSend)(current_session, @selector(requestEndWithTimeout:), 0.0); terminated = true; } } static void sigterminate(int sig) { terminate_session(); exit(1); } static void sigcleanup(int sig) { if (debug_mode == DEBUG_REPL) { save_repl_history(); } exit(1); } static void sigforwarder(int sig) { if (gdb_task != nil) { kill([gdb_task processIdentifier], sig); } } @implementation Delegate static void locate_simulator_app_bounds(void) { if (!CGRectEqualToRect(simulator_app_bounds, CGRectZero)) { return; } CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID); NSRect bounds = NSZeroRect; 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); 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], nil]; } bool found = false; for (NSString *pattern in patterns) { if ([name rangeOfString:pattern].location != NSNotFound) { found = true; break; } } if (!found) { 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 bounds_ok = true; break; } CFRelease(windows); if (!bounds_ok) { static bool error_printed = false; if (!error_printed) { fprintf(stderr, "Cannot locate the Simulator app, mouse over disabled\n"); error_printed = true; } return; } // Inset the main view frame. bounds.origin.x += 30; bounds.size.width -= 60; bounds.origin.y += 120; bounds.size.height -= 240; simulator_app_bounds = bounds; } 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) { 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); } #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]]; if (len < sizeof buf) { break; } } 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) { break; } printf("=> %s\n", [res UTF8String]); expr = nil; expr_level = 0; } } - (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; NSNumber *pidNumber = ((id (*)(id, SEL))objc_msgSend)(session, @selector(simulatedApplicationPID)); if (pidNumber != nil && [pidNumber isKindOfClass:[NSNumber class]]) { NSString *path = [NSString stringWithFormat: @"/tmp/.rubymotion_process_exited.%@", [pidNumber description]]; 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); } if (debug_mode == DEBUG_GDB) { NSNumber *pidNumber = ((id (*)(id, SEL))objc_msgSend)(session, @selector(simulatedApplicationPID)); if (pidNumber == nil || ![pidNumber isKindOfClass:[NSNumber class]]) { fprintf(stderr, "can't get simulated application PID\n"); exit(1); } // Forward ^C to gdb. signal(SIGINT, sigforwarder); // Create the gdb commands file (used to 'continue' the process). NSString *cmds_path = [NSString pathWithComponents: [NSArray arrayWithObjects:NSTemporaryDirectory(), @"_simgdbcmds", nil]]; //if (![[NSFileManager defaultManager] fileExistsAtPath:cmds_path]) { 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:@"continue\n"]; } NSError *error = nil; if (![cmds writeToFile:cmds_path atomically:YES encoding:NSASCIIStringEncoding error:&error]) { fprintf(stderr, "can't write gdb commands file into path %s: %s\n", [cmds_path UTF8String], [[error description] UTF8String]); exit(1); } //} // Run the gdb process. NSString *gdb_path = [xcode_path stringByAppendingPathComponent:@"Platforms/iPhoneSimulator.platform/Developer/usr/libexec/gdb/gdb-i386-apple-darwin"]; gdb_task = [[NSTask launchedTaskWithLaunchPath:gdb_path arguments:[NSArray arrayWithObjects:@"--arch", @"i386", @"-q", @"--pid", [pidNumber description], @"-x", cmds_path, nil]] retain]; [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"); } @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 (argc != 6) { usage(); } spec_mode = getenv("SIM_SPEC_MODE") != NULL; debug_mode = atoi(argv[1]); NSNumber *device_family = [NSNumber numberWithInt:atoi(argv[2])]; sdk_version = [[NSString stringWithUTF8String:argv[3]] retain]; xcode_path = [[NSString stringWithUTF8String:argv[4]] retain]; NSString *app_path = [NSString stringWithUTF8String:realpath(argv[5], NULL)]; // Load the framework. [[NSBundle bundleWithPath:[xcode_path stringByAppendingPathComponent:@"Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/iPhoneSimulatorRemoteClient.framework"]] load]; 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); // Prepare app environment. NSMutableDictionary *appEnvironment = [[[NSProcessInfo processInfo] environment] mutableCopy]; if (debug_mode == DEBUG_REPL) { // Prepare repl socket path. 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 stringByAppendingPathComponent:@"data"]; replPath = [replPath stringByAppendingPathComponent:sdk_version]; replPath = [replPath stringByAppendingPathComponent:@"iPhoneSimulator"]; replPath = [replPath stringByAppendingPathComponent:@"libmacruby-repl.dylib"]; [appEnvironment setObject:replPath forKey:@"REPL_DYLIB_PATH"]; } //[NSDictionary dictionaryWithObjectsAndKeys:@"/usr/lib/libgmalloc.dylib", @"DYLD_INSERT_LIBRARIES", nil]); // 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); assert(system_root != nil); // 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:), [NSArray array]); ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedApplicationLaunchEnvironment:), appEnvironment); ((void (*)(id, SEL, BOOL))objc_msgSend)(config, @selector(setSimulatedApplicationShouldWaitForDebugger:), debug_mode == DEBUG_GDB); ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedDeviceFamily:), device_family); ((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'}; if (fcntl(STDOUT_FILENO, F_GETPATH, &path) == -1 && fcntl(STDERR_FILENO, F_GETPATH, &path) == -1) { fprintf(stderr, "*** stdout/stderr unavailable, process output is disabled\n"); } else { ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedApplicationStdOutPath:), [NSString stringWithUTF8String:path]); ((void (*)(id, SEL, id))objc_msgSend)(config, @selector(setSimulatedApplicationStdErrPath:), [NSString stringWithUTF8String:path]); } // Create session. id session = [[Session alloc] init]; delegate = [[Delegate alloc] init]; ((void (*)(id, SEL, id))objc_msgSend)(session, @selector(setDelegate:), delegate); // Start session. NSError *error = nil; if (!((BOOL (*)(id, SEL, id, double, id *))objc_msgSend)(session, @selector(requestStartWithConfig:timeout:error:), config, 0.0, &error)) { fprintf(stderr, "*** can't 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); } // Open simulator to the foreground. if (!spec_mode) { system("/usr/bin/open -a \"iPhone Simulator\""); } [[NSRunLoop mainRunLoop] run]; return 0; }