/* GNUstepAppLauncher.m Implementation of the GNUstepAppLauncher project module for the ProjectManager application. Copyright (C) 2005, 2006 Saso Kiselkov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #import "GNUstepAppLauncher.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "GNUstepAppLauncherDelegate.h" #import "../../NSImageAdditions.h" #import "../../ProjectDocument.h" /** * Notification sent when the launcher's project will be launched. * The userInfo dictionary contains these keys: * { * Project = (ProjectDocument *) originatingProject; * Target = ""; * } */ NSString * const GNUstepAppLauncherProjectWillLaunchNotification = @"GNUstepAppLauncherProjectWillLaunchNotification"; /** * Notification sent when the launcher's project has been successfuly * launched. The userInfo dictionary contains these keys: * { * Project = (ProjectDocument *) originatingProject; * Target = ""; * Task = (NSTask *) launchedProgramTask; * } */ NSString * const GNUstepAppLauncherProjectDidLaunchNotification = @"GNUstepAppLauncherProjectDidLaunchNotification"; /** * Notification sent when the launcher's project has failed to launch * or launching has been stopped by the user. The userInfo dictionary * contains these keys: * { * Project = (ProjectDocument *) originatingProject; * Target = ""; * } */ NSString * const GNUstepAppLauncherProjectDidFailToLaunchNotification = @"GNUstepAppLauncherProjectDidFailToLaunchNotification"; /** * Notification sent when the launcher's project has terminated. * The userInfo dictionary contains these keys: * { * Project = (ProjectDocument *) originatingProject; * Target = ""; * Task = (NSTask *) terminatedProgramTask; * } */ NSString * const GNUstepAppLauncherProjectDidTerminateNotification = @"GNUstepAppLauncherProjectDidTerminateNotification"; @interface GNUstepAppLauncher (Private) - (void) clearState; - (void) setLauncherState: (GNUstepAppLauncherState) state; - (BOOL) validateControl: (id) control; @end @implementation GNUstepAppLauncher (Private) /** * This method clears internal ivars which refer to the running * process, such as stdin, stdout and stderr pipes, and deregister * us at the notification center for NSTask-related notifications. */ - (void) clearState { NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; [nc removeObserver: self name: NSTaskDidTerminateNotification object: task]; [nc removeObserver: self name: NSFileHandleDataAvailableNotification object: stdoutHandle]; [nc removeObserver: self name: NSFileHandleDataAvailableNotification object: stderrHandle]; DESTROY(task); DESTROY(stdinHandle); DESTROY(stdoutHandle); DESTROY(stderrHandle); } /** * Puts the receiver into the a provided launcher state and sets up it's * interface to represent the state correctly. * * @param setup The state to which to set up the receiver. */ - (void) setLauncherState: (GNUstepAppLauncherState) state { launcherState = state; switch (launcherState) { case GNUstepAppLauncherReadyState: [workingDirectory setEditable: YES]; [workingDirectoryButton setEnabled: YES]; [targets setEnabled: YES]; break; case GNUstepAppLauncherDelayedLaunchState: [workingDirectory setEditable: NO]; [workingDirectoryButton setEnabled: NO]; [targets setEnabled: NO]; break; case GNUstepAppLauncherLaunchedState: [workingDirectory setEditable: NO]; [workingDirectoryButton setEnabled: NO]; [targets setEnabled: NO]; break; } [[[view window] toolbar] validateVisibleItems]; } /** * Validates a control based on it's action. This method is used by * both -validateMenuItem: and -validateToolbarItem:. * * @param control An object which responds to the -action message. * * @return YES if the control is valid, NO otherwise. */ - (BOOL) validateControl: (id) control { SEL action = [control action]; if (sel_eq(action, @selector(launch:))) { if (launcherState == GNUstepAppLauncherReadyState) { return YES; } else { return NO; } } else if (sel_eq(action, @selector(stopLaunch:))) { if (launcherState == GNUstepAppLauncherDelayedLaunchState) { return YES; } else { return NO; } } else if (sel_eq(action, @selector(kill:))) { if (launcherState == GNUstepAppLauncherLaunchedState) { return YES; } else { return NO; } } else { // allow other controls only when we're currently visible return [document currentProjectModule] == self; } } @end @implementation GNUstepAppLauncher + (NSString *) moduleName { return @"GNUstepAppLauncher"; } + (NSString *) humanReadableModuleName { return _(@"Launcher"); } - (NSToolbarItem *) toolbarItemForItemIdentifier: (NSString *) identifier { NSToolbarItem * item = [[[NSToolbarItem alloc] initWithItemIdentifier: identifier] autorelease]; [item setTarget: self]; if ([identifier isEqualToString: @"GNUstepAppLauncherLaunchToolbarItem"]) { [item setAction: @selector(launch:)]; [item setImage: [NSImage imageNamed: @"Launch" owner: self]]; [item setLabel: _(@"Launch")]; [item setToolTip: _(@"Launch the project")]; } else if ([identifier isEqualToString: @"GNUstepAppLauncherStopToolbarItem"]) { [item setAction: @selector(stopLaunch:)]; [item setImage: [NSImage imageNamed: @"Stop" owner: self]]; [item setLabel: _(@"Stop")]; [item setToolTip: _(@"Stop a launch in progress")]; } else if ([identifier isEqualToString: @"GNUstepAppLauncherKillToolbarItem"]) { [item setAction: @selector(kill:)]; [item setImage: [NSImage imageNamed: @"Kill" owner: self]]; [item setLabel: _(@"Kill")]; [item setToolTip: _(@"Kill the running project")]; } else if ([identifier isEqualToString: @"GNUstepAppLauncherShowArgumentsToolbarItem"]) { [item setAction: @selector(showArguments:)]; [item setImage: [NSImage imageNamed: @"Arguments" owner: self]]; [item setLabel: _(@"Arguments")]; [item setToolTip: _(@"Show the project's launch arguments")]; } else if ([identifier isEqualToString: @"GNUstepAppLauncherShowEnvironmentToolbarItem"]) { [item setAction: @selector(showEnvironment:)]; [item setImage: [NSImage imageNamed: @"Environment" owner: self]]; [item setLabel: _(@"Environment")]; [item setToolTip: _(@"Show the project's environment variables")]; } return item; } - (void) dealloc { NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; [nc removeObserver: self]; TEST_RELEASE(view); if (task != nil && [task isRunning]) { NSDictionary * userInfo; [task terminate]; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", task, @"Task", nil]; [nc postNotificationName: GNUstepAppLauncherProjectDidTerminateNotification object: self userInfo: userInfo]; } [self clearState]; TEST_RELEASE(arguments); TEST_RELEASE(environment); TEST_RELEASE(sortedEnvironmentNames); TEST_RELEASE(target); [super dealloc]; } - initWithDocument: (ProjectDocument *) aDocument infoDictionary: (NSDictionary *) infoDict { if ([self init]) { document = aDocument; environment = [[[NSProcessInfo processInfo] environment] mutableCopy]; ASSIGN(sortedEnvironmentNames, [[environment allKeys] sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)]); arguments = [NSMutableArray new]; // watch for current module changes in order to handle Arguments // and Environment panel closing and opening [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(moduleChanged:) name: CurrentProjectModuleDidChangeNotification object: nil]; return self; } else { return nil; } } - (void) finishInit { delegate = (id ) [document projectType]; } - (NSView *) view { if (view == nil) { [NSBundle loadNibNamed: @"GNUstepAppLauncher" owner: self]; } return view; } - (NSDictionary *) infoDictionary { return nil; } - (BOOL) regenerateDerivedFiles { return YES; } - (ProjectDocument *) document { return document; } - (void) awakeFromNib { [view retain]; [view removeFromSuperview]; DESTROY(bogusWindow); [stdout setFont: [NSFont userFixedPitchFontOfSize: 0]]; [stderr setFont: [NSFont userFixedPitchFontOfSize: 0]]; [stderr setTextColor: [NSColor whiteColor]]; [targets removeAllItems]; [targets addItemsWithTitles: [delegate launchTargetsForAppLauncher: self]]; if ([targets numberOfItems] > 0) { [targets selectItemAtIndex: 0]; } } /** * Appends a message to the stdout log of the receiver. After the message * is appended the log is scrolled to it's end. * * @param aMessage The message to append. */ - (void) appendStdoutMessage: (NSString *) aMessage { NSRange r; r = NSMakeRange([[stdout textStorage] length], 0); [stdout replaceCharactersInRange: r withString: aMessage]; r = NSMakeRange([[stdout textStorage] length], 0); [stdout scrollRangeToVisible: r]; } /** * Appends a message to the stderr log of the receiver. After the message * is appended the log is scrolled to it's end. * * @param aMessage The message to append. */ - (void) appendStderrMessage: (NSString *) aMessage { NSRange r; r = NSMakeRange([[stderr textStorage] length], 0); [stderr replaceCharactersInRange: r withString: aMessage]; r = NSMakeRange([[stderr textStorage] length], 0); [stderr scrollRangeToVisible: r]; } /** * Action invoked by the `Launch' button when the app is to be killed. */ - (void) kill: (id) sender { NSDictionary * userInfo; NSAssert(launcherState == GNUstepAppLauncherLaunchedState, _(@"Tried to kill task when not running.")); userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", task, @"Task", nil]; [task terminate]; [self clearState]; [self setLauncherState: GNUstepAppLauncherReadyState]; [[NSNotificationCenter defaultCenter] postNotificationName: GNUstepAppLauncherProjectDidTerminateNotification object: self userInfo: userInfo]; } /** * Action invoked by the `Launch' button when delayed launching of the * app is to be stopped. */ - (void) stopLaunch: (id) sender { NSDictionary * userInfo; NSAssert(launcherState == GNUstepAppLauncherDelayedLaunchState, _(@"Tried to stop delayed launch when not in the according state.")); [delegate stopDelayedLaunchForAppLauncher: self]; [self setLauncherState: GNUstepAppLauncherReadyState]; [document logMessage: _(@"Stopping launch")]; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", nil]; [[NSNotificationCenter defaultCenter] postNotificationName: GNUstepAppLauncherProjectDidFailToLaunchNotification object: self userInfo: userInfo]; } /** * Action invoked by the `Launch' button when the app is to be launched. * This method asks the delegate whether the receiver should delay * launching, and if the delegate answers NO, it immediatelly invokes * -[self proceedWithLaunch: YES], otherwise the method sets up the * user interface to allow stopping the delayed launch. */ - (void) launch: (id) sender { NSDictionary * userInfo; NSAssert(launcherState == GNUstepAppLauncherReadyState, _(@"Tried to launch app when not ready.")); // clear the output logs [stdout setString: @""]; [stderr setString: @""]; ASSIGN(target, [targets titleOfSelectedItem]); [document logMessage: [NSString stringWithFormat: _(@"Launching target %@"), target]]; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", nil]; [[NSNotificationCenter defaultCenter] postNotificationName: GNUstepAppLauncherProjectWillLaunchNotification object: self userInfo: userInfo]; if ([delegate appLauncher: self shouldDelayLaunchWithTarget: target] == NO) { [self proceedWithLaunch: YES]; } else { [self setLauncherState: GNUstepAppLauncherDelayedLaunchState]; } } /** * Proceeds with launching if the sender sets so using the `flag' argument. * The launch is separated into two stages - preparation (from the * -launch: method) and actual launching (performed by this method). This * allows the delegate of the receiver to perform any additional processing * before the actual launch (such as building the project if necessary). * In case the delegate delayed the launch, it must afterwards, at some * point, invoke this method to finalize the launch process. * * @param flag A flag which tells the receiver whether to proceed with * the launch or to abort. Passing YES contines the launch, NO * aborts it. */ - (void) proceedWithLaunch: (BOOL) flag { NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; if (flag == YES) { NSPipe * stdinPipe, * stdoutPipe, * stderrPipe; NSDictionary * userInfo; task = [NSTask new]; [task setLaunchPath: [delegate appLauncher: self pathToProjectBinaryOfType: target]]; [task setArguments: arguments]; [task setEnvironment: environment]; if ([[workingDirectory stringValue] length] > 0) { [task setCurrentDirectoryPath: [workingDirectory stringValue]]; } [nc addObserver: self selector: @selector(taskTerminated) name: NSTaskDidTerminateNotification object: task]; stdinPipe = [NSPipe pipe]; stdoutPipe = [NSPipe pipe]; stderrPipe = [NSPipe pipe]; [task setStandardInput: stdinPipe]; [task setStandardOutput: stdoutPipe]; [task setStandardError: stderrPipe]; ASSIGN(stdinHandle, [stdinPipe fileHandleForWriting]); ASSIGN(stdoutHandle, [stdoutPipe fileHandleForReading]); ASSIGN(stderrHandle, [stderrPipe fileHandleForReading]); [nc addObserver: self selector: @selector(readStdout) name: NSFileHandleDataAvailableNotification object: stdoutHandle]; [nc addObserver: self selector: @selector(readStderr) name: NSFileHandleDataAvailableNotification object: stderrHandle]; NS_DURING [task launch]; NS_HANDLER NSRunAlertPanel(_(@"Failed to launch"), _(@"Failed to launch the project.\n" @"%@"), nil, nil, nil, [localException reason]); [self clearState]; [self setLauncherState: GNUstepAppLauncherReadyState]; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", nil]; [nc postNotificationName: GNUstepAppLauncherProjectDidFailToLaunchNotification object: self userInfo: userInfo]; return; NS_ENDHANDLER [stdoutHandle waitForDataInBackgroundAndNotify]; [stderrHandle waitForDataInBackgroundAndNotify]; [self setLauncherState: GNUstepAppLauncherLaunchedState]; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", task, @"Task", nil]; [nc postNotificationName: GNUstepAppLauncherProjectDidLaunchNotification object: self userInfo: userInfo]; } else { NSDictionary * userInfo; [self setLauncherState: GNUstepAppLauncherReadyState]; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", nil]; [nc postNotificationName: GNUstepAppLauncherProjectDidFailToLaunchNotification object: self userInfo: userInfo]; } } /** * Action invoked by the `Choose...' button in the `Working Directory' box. */ - (void) chooseWorkingDirectory: (id)sender { NSOpenPanel * op = [NSOpenPanel openPanel]; [op setCanChooseDirectories: YES]; [op setCanChooseFiles: NO]; if ([op runModalForTypes: nil] == NSOKButton) { [workingDirectory setStringValue: [op filename]]; } } /** * Action which opens up the receiver's arguments panel. */ - (void) showArguments: sender { [argsPanel makeKeyAndOrderFront: nil]; } /** * Action which opens up the receiver's environment variables panel. */ - (void) showEnvironment: sender { [envPanel makeKeyAndOrderFront: nil]; } /** * Action invoked when the user enter something into the `Standard Input' * text field and hits 'Return'. */ - (void) writeStdin: sender { if (stdinHandle != nil) { NSString * string = [stdin stringValue]; // append a trailing newline automagically string = [string stringByAppendingString: @"\n"]; [stdinHandle writeData: [string dataUsingEncoding: NSUTF8StringEncoding]]; [stdinHandle synchronizeFile]; // reset the text field again [stdin setStringValue: nil]; } else { NSRunAlertPanel(_(@"Not running"), _(@"The application is not running. You must launch\n" @"it before providing it any standard input."), nil, nil, nil); } } /** * Action to add an argument to the argument list. */ - (void) addArg: sender { [arguments addObject: @"New Argument"]; [args reloadData]; } /** * Action to remove the selected argument from the argument list. */ - (void) removeArg: sender { int row = [args selectedRow]; if (row >= 0) { [arguments removeObjectAtIndex: row]; [args reloadData]; } } /** * Action to move the selected argument upwards in the list of arguments. */ - (void) moveArgUp: sender { int row = [args selectedRow]; if (row > 0) { id object; object = [arguments objectAtIndex: row - 1]; [object retain]; [arguments removeObjectAtIndex: row - 1]; [arguments insertObject: object atIndex: row]; [object release]; [args reloadData]; [args selectRow: row - 1 byExtendingSelection: NO]; } } /** * Action to move the selected argument downwards in the list of arguments. */ - (void) moveArgDown: sender { int row = [args selectedRow]; if (row >= 0 && row + 1 < (int) [arguments count]) { id object; object = [arguments objectAtIndex: row + 1]; [object retain]; [arguments removeObjectAtIndex: row + 1]; [arguments insertObject: object atIndex: row]; [object release]; [args reloadData]; [args selectRow: row + 1 byExtendingSelection: NO]; } } /** * Action to add an environment variable. */ - (void) addEnv: sender { NSString * newName = _(@"NewVariable"); int i; for (i = 1; [[environment allKeys] containsObject: newName]; newName = [NSString stringWithFormat: _(@"NewVariable%i"), i]); [environment setObject: _(@"A value") forKey: newName]; ASSIGN(sortedEnvironmentNames, [[environment allKeys] sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)]); [env reloadData]; [env selectRow: [sortedEnvironmentNames indexOfObject: newName] byExtendingSelection: NO]; } /** * Action to remove an environment variable. */ - (void) removeEnv: sender { int row = [env selectedRow]; if (row >= 0) { [environment removeObjectForKey: [sortedEnvironmentNames objectAtIndex: row]]; ASSIGN(sortedEnvironmentNames, [[environment allKeys] sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)]); [env reloadData]; } } /** * Notification method invoked when the task terminates itself. */ - (void) taskTerminated { NSDictionary * userInfo; NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", task, @"Task", nil]; [document logMessage: [NSString stringWithFormat: _(@"Application terminated with code %i"), [task terminationStatus]]]; [self clearState]; [self setLauncherState: GNUstepAppLauncherReadyState]; [nc postNotificationName: GNUstepAppLauncherProjectDidTerminateNotification object: self userInfo: userInfo]; } /** * Notification method invoked when we collect the subprocess' stdout. */ - (void) readStdout { NSString * string = [[[NSString alloc] initWithData: [stdoutHandle availableData] encoding: NSUTF8StringEncoding] autorelease]; [self appendStdoutMessage: string]; [stdoutHandle waitForDataInBackgroundAndNotify]; } /** * Notification method invoked when we collect the subprocess' stderr. */ - (void) readStderr { NSString * string = [[[NSString alloc] initWithData: [stderrHandle availableData] encoding: NSUTF8StringEncoding] autorelease]; [self appendStderrMessage: string]; [stderrHandle waitForDataInBackgroundAndNotify]; } - (void) moduleChanged: (NSNotification *) notif { // if we became the current module, open the additional panels // as they were left in their previous state if ([[notif userInfo] objectForKey: @"Module"] == self) { if (argsPanelWasOpen) { [argsPanel orderFront: nil]; } if (envPanelWasOpen) { [envPanel orderFront: nil]; } } // otherwise close them, saving whether they were open or closed else { argsPanelWasOpen = [argsPanel isVisible]; [argsPanel close]; envPanelWasOpen = [envPanel isVisible]; [envPanel close]; } } - (NSArray *) moduleMenuItems { return [NSArray arrayWithObjects: PMMakeMenuItem (_(@"Launch"), @selector(launch:), @"L", self), PMMakeMenuItem (_(@"Stop"), @selector(stopLaunch:), nil, self), PMMakeMenuItem (_(@"Kill"), @selector(kill:), nil, self), PMMakeMenuItem (_(@"Show Arguments..."), @selector(showArguments:), nil, self), PMMakeMenuItem (_(@"Show Environment..."), @selector(showEnvironment:), nil, self), nil]; } - (NSArray *) toolbarItemIdentifiers { return [NSArray arrayWithObjects: @"GNUstepAppLauncherLaunchToolbarItem", @"GNUstepAppLauncherStopToolbarItem", @"GNUstepAppLauncherKillToolbarItem", @"GNUstepAppLauncherShowArgumentsToolbarItem", @"GNUstepAppLauncherShowEnvironmentToolbarItem", nil]; } - (BOOL) validateMenuItem: (id ) item { return [self validateControl: item]; } - (BOOL) validateToolbarItem: (NSToolbarItem *) item { return [self validateControl: item]; } - (int) numberOfRowsInTableView: (NSTableView *)aTableView { if (aTableView == args) { return [arguments count]; } else { return [environment count]; } } - (id) tableView: (NSTableView *)aTableView objectValueForTableColumn: (NSTableColumn *)aTableColumn row: (int)rowIndex { NSString * identifier = [aTableColumn identifier]; if (aTableView == args) { if ([identifier isEqualToString: @"Number"]) { return [NSNumber numberWithInt: rowIndex + 1]; } else { return [arguments objectAtIndex: rowIndex]; } } else { if ([identifier isEqualToString: @"Name"]) { return [sortedEnvironmentNames objectAtIndex: rowIndex]; } else { return [environment objectForKey: [sortedEnvironmentNames objectAtIndex: rowIndex]]; } } } - (void) tableView: (NSTableView *)aTableView setObjectValue: (id)anObject forTableColumn: (NSTableColumn *)aTableColumn row: (int)rowIndex { if (aTableView == args) { [arguments replaceObjectAtIndex: rowIndex withObject: anObject]; } else { NSString * identifier = [aTableColumn identifier]; if ([identifier isEqualToString: @"Name"]) { if ([environment objectForKey: anObject] != nil) { NSRunAlertPanel(_(@"Environment variable already present"), _(@"An environment variable named \"%@\"\n" @"is already present. Delete the variable first."), nil, nil, nil, anObject); } else { id oldKey = [sortedEnvironmentNames objectAtIndex: rowIndex]; id value = [environment objectForKey: oldKey]; [environment setObject: value forKey: anObject]; [environment removeObjectForKey: oldKey]; ASSIGN(sortedEnvironmentNames, [[environment allKeys] sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)]); [env reloadData]; [env selectRow: [sortedEnvironmentNames indexOfObject: anObject] byExtendingSelection: NO]; } } else { id key = [sortedEnvironmentNames objectAtIndex: rowIndex]; [environment setObject: anObject forKey: key]; } } } @end