/** Implementation for NSTask for GNUStep Copyright (C) 1998,1999 Free Software Foundation, Inc. Written by: Richard Frith-Macdonald Date: 1998 This file is part of the GNUstep Base Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 31 Milk Street #960789 Boston, MA 02196 USA. NSTask class reference */ #import "common.h" #import "GSPThread.h" #define EXPOSE_NSTask_IVARS 1 #import "Foundation/FoundationErrors.h" #import "Foundation/NSAutoreleasePool.h" #import "Foundation/NSCharacterSet.h" #import "Foundation/NSData.h" #import "Foundation/NSDate.h" #import "Foundation/NSEnumerator.h" #import "Foundation/NSError.h" #import "Foundation/NSException.h" #import "Foundation/NSFileHandle.h" #import "Foundation/NSFileManager.h" #import "Foundation/NSMapTable.h" #import "Foundation/NSProcessInfo.h" #import "Foundation/NSRunLoop.h" #import "Foundation/NSLock.h" #import "Foundation/NSNotification.h" #import "Foundation/NSNotificationQueue.h" #import "Foundation/NSTask.h" #import "Foundation/NSThread.h" #import "Foundation/NSTimer.h" #import "Foundation/NSURL.h" #import "GNUstepBase/NSString+GNUstepBase.h" #import "GNUstepBase/NSTask+GNUstepBase.h" #import "GSPrivate.h" #include #if defined(__FreeBSD__) || defined(__OpenBSD__) #include #endif #ifdef HAVE_WINDOWS_H # include #endif #if defined(HAVE_SYS_SIGNAL_H) # include #elif defined(HAVE_SIGNAL_H) # include #endif #if defined(HAVE_SYS_FILE_H) # include #endif #if defined(HAVE_SYS_FCNTL_H) # include #elif defined(HAVE_FCNTL_H) # include #endif #ifdef HAVE_SYS_IOCTL_H #include #endif #ifdef HAVE_SYS_WAIT_H #include #endif #ifdef HAVE_SYS_PARAM_H #include #endif /* * If we are on a streams based system, we need to include stropts.h * for definitions needed to set up slave pseudo-terminal stream. */ #ifdef HAVE_SYS_STROPTS_H #include #endif /* * If we don't have NOFILE, default to 2048 open descriptors. */ #ifndef NOFILE #define NOFILE 2048 #endif static NSRecursiveLock *tasksLock = nil; static NSMapTable *activeTasks = 0; static BOOL hadChildSignal = NO; static void handleSignal(int sig) { hadChildSignal = YES; #ifndef _WIN32 signal(SIGCHLD, handleSignal); #endif } #ifdef _WIN32 /* We use exit code 10 to denote a process termination. * Windows does nt have an exit code to denote termination this way. */ #define WIN_SIGNALLED 10 @interface NSConcreteWindowsTask : NSTask { @public PROCESS_INFORMATION procInfo; HANDLE wThread; } @end #define NSConcreteTask NSConcreteWindowsTask #else @interface NSConcreteUnixTask : NSTask { char slave_name[32]; BOOL _usePseudoTerminal; } @end #define NSConcreteTask NSConcreteUnixTask static int pty_master(char* name, int len) { int master; /* * If we have grantpt(), assume we are using sysv-style pseudo-terminals, * otherwise assume bsd style. */ #ifdef HAVE_GRANTPT master = open("/dev/ptmx", O_RDWR); if (master >= 0) { const char *slave; grantpt(master); /* Change permission of slave. */ unlockpt(master); /* Unlock slave. */ slave = (const char*)ptsname(master); if (slave == 0 || strlen(slave) >= len) { close(master); master = -1; } else { strncpy(name, (char*)slave, len); } } else #endif { const char *groups = "pqrstuvwxyzPQRSTUVWXYZ"; master = -1; if (len > 10) { strncpy(name, "/dev/ptyXX", len); while (master < 0 && *groups != '\0') { int i; name[8] = *groups++; for (i = 0; i < 16; i++) { name[9] = "0123456789abcdef"[i]; master = open(name, O_RDWR); if (master >= 0) { name[5] = 't'; break; } } } } } return master; } static int pty_slave(const char* name) { int slave; slave = open(name, O_RDWR); #ifdef HAVE_SYS_STROPTS_H #ifdef HAVE_PTS_STREAM_MODULES if (slave >= 0 && isastream(slave)) { if (ioctl(slave, I_PUSH, "ptem") < 0) { perror("unable to push 'ptem' streams module"); } else if (ioctl(slave, I_PUSH, "ldterm") < 0) { perror("unable to push 'ldterm' streams module"); } } #endif #endif return slave; } #endif @interface NSTask (Private) - (NSString *) _fullLaunchPath; - (void) _collectChild; - (void) _notifyOfTermination; - (void) _terminatedChild: (int)status reason: (NSTaskTerminationReason)reason; @end /** * The NSTask class provides a mechanism to run separate tasks * under (limited) control of your program. */ @implementation NSTask + (id) allocWithZone: (NSZone*)zone { NSTask *task; if (self == [NSTask class]) task = (NSTask *)NSAllocateObject([NSConcreteTask class], 0, zone); else task = (NSTask *)NSAllocateObject(self, 0, zone); return task; } + (void) initialize { if (self == [NSTask class]) { static gs_mutex_t classLock = GS_MUTEX_INIT_STATIC; GS_MUTEX_LOCK(classLock); if (tasksLock == nil) { tasksLock = [NSRecursiveLock new]; [[NSObject leakAt: &tasksLock] release]; /* The activeTasks map contains the NSTask objects corresponding * to running subtasks, and retains them until the subprocess * actually terminates. * The previous implementation stored non-retained objects and * the -finalize method removed them from the table, but this * caused a thread safety issue even though table access was * lock protected: * If thread t1 releases the task at the same time that the subtask * dies and is 'reaped' by t2, then there is a window such * that t1 can enter -dealloc while t2 is dealing with * subprocess termination notification. * So t1 completes deallocation while t2 performs * the notification, and then t2 tries to release the object... * t1: enter dealloc * t2: lookup task in activeTasks and retain/autorelease it * t2: create NSNotification (retaining task) * t2: post notification * t1: remove task from activeTasks * t1: release memory occupied by task object. * t2: complete notification ... attempt to release task object * but it's already deallocated. */ activeTasks = NSCreateMapTable(NSIntegerMapKeyCallBacks, NSObjectMapValueCallBacks, 0); [[NSObject leakAt: &activeTasks] release]; } GS_MUTEX_UNLOCK(classLock); #ifndef _WIN32 signal(SIGCHLD, handleSignal); #endif } } /** * Creates and launches a task, returning an autoreleased task object. * Supplies the path to the executable and an array of argument. * The task inherits the parents environment and I/O. */ + (NSTask*) launchedTaskWithLaunchPath: (NSString*)path arguments: (NSArray*)args { NSTask* task = [NSTask new]; [task setLaunchPath: path]; [task setArguments: args]; [task launch]; return AUTORELEASE(task); } - (void) finalize { return; } - (void) dealloc { [self finalize]; RELEASE(_arguments); RELEASE(_environment); RELEASE(_launchPath); RELEASE(_currentDirectoryPath); RELEASE(_standardError); RELEASE(_standardInput); RELEASE(_standardOutput); RELEASE(_launchingThread); [super dealloc]; } /** * Returns the arguments set for the task. */ - (NSArray*) arguments { return _arguments; } /** * Returns the working directory set for the task. */ - (NSString*) currentDirectoryPath { if (_currentDirectoryPath == nil) { [self setCurrentDirectoryPath: [[NSFileManager defaultManager] currentDirectoryPath]]; } return _currentDirectoryPath; } /** * Returns the environment set for the task. */ - (NSDictionary*) environment { if (_environment == nil) { [self setEnvironment: [[NSProcessInfo processInfo] environment]]; } return _environment; } /** * Sends an interrupt signal to the receiver and any subtasks.
* If the task has not been launched, raises an * NSInvalidArgumentException.
* Has no effect on a task that has already terminated.
* This is rather like the terminate method, but the child * process may not choose to terminate in response to an interrupt. */ - (void) interrupt { if (_hasLaunched == NO) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has not yet launched"]; } if (_hasTerminated) { return; } #ifndef _WIN32 #ifdef HAVE_KILLPG killpg(_taskId, SIGINT); #else kill(-_taskId, SIGINT); #endif #endif } /** * Checks to see if the task is currently running. */ - (BOOL) isRunning { if (_hasLaunched == NO) { return NO; } if (_hasCollected == NO) { [self _collectChild]; } if (_hasTerminated == YES) { return NO; } return YES; } /** * Launches the task.
* Raises an NSInvalidArgumentException if the launch path is not * set or if the subtask cannot be started for some reason * (eg. the executable does not exist or the task has already been launched). * The actual launching is done in a concrete subclass; this method just * takes care of actions common to all subclasses. */ - (void) launch { NSError *e; if (NO == [self launchAndReturnError: &e]) { [NSException raise: NSInvalidArgumentException format: @"%@", e ? [e description] : @"Unable to launch"]; } } /** * Returns the launch path set for the task. */ - (NSString*) launchPath { return _launchPath; } /** * Returns the number identifying the child process on this system. */ - (int) processIdentifier { return _taskId; } /** * Sends a cont signal to the receiver and any subtasks.
* If the task has not been launched, raises an * NSInvalidArgumentException.
*/ - (BOOL) resume { if (_hasLaunched == NO) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has not yet launched"]; } #ifndef _WIN32 #ifdef HAVE_KILLPG killpg(_taskId, SIGCONT); #else kill(-_taskId, SIGCONT); #endif #endif return YES; } /** * Sets an array of arguments to be supplied to the task when it * is launched. The default is an empty array. This method cannot * be used after a task is launched ... * it raises an NSInvalidArgumentException. */ - (void) setArguments: (NSArray*)args { if (_hasLaunched) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has been launched"]; } ASSIGN(_arguments, args); } /** * Sets the home directory in which the task is to be run. * The default is the parent processes directory. * This method cannot be used after a task is launched ... * it raises an NSInvalidArgumentException. */ - (void) setCurrentDirectoryPath: (NSString*)path { if (_hasLaunched) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has been launched"]; } ASSIGN(_currentDirectoryPath, path); } /** * Sets the environment variables for the task to be run. * The default is the parent processes environment. * This method cannot be used after a task is launched ... * it raises an NSInvalidArgumentException. */ - (void) setEnvironment: (NSDictionary*)env { if (_hasLaunched) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has been launched"]; } ASSIGN(_environment, env); } /** * Sets the path to the executable file to be run. * There is no default for this - you must set the launch path. * This method cannot be used after a task is launched ... * it raises an NSInvalidArgumentException. */ - (void) setLaunchPath: (NSString*)path { if (_hasLaunched) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has been launched"]; } ASSIGN(_launchPath, path); } /** * Sets the standard error stream for the task.
* This is normally a writable NSFileHandle object. * If this is an NSPipe, the write end of the pipe is * automatically closed on launching.
* The default behavior is to inherit the parent processes * stderr output.
* This method cannot be used after a task is launched ... * it raises an NSInvalidArgumentException. */ - (void) setStandardError: (id)hdl { if (_hasLaunched) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has been launched"]; } NSAssert(hdl != nil && ([hdl isKindOfClass: [NSFileHandle class]] || [hdl isKindOfClass: [NSPipe class]]), NSInvalidArgumentException); ASSIGN(_standardError, hdl); } /** * Sets the standard input stream for the task.
* This is normally a readable NSFileHandle object. * If this is an NSPipe, the read end of the pipe is * automatically closed on launching.
* The default behavior is to inherit the parent processes * stdin stream.
* This method cannot be used after a task is launched ... * it raises an NSInvalidArgumentException. */ - (void) setStandardInput: (id)hdl { if (_hasLaunched) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has been launched"]; } NSAssert(hdl != nil && ([hdl isKindOfClass: [NSFileHandle class]] || [hdl isKindOfClass: [NSPipe class]]), NSInvalidArgumentException); ASSIGN(_standardInput, hdl); } /** * Sets the standard output stream for the task.
* This is normally a writable NSFileHandle object. * If this is an NSPipe, the write end of the pipe is * automatically closed on launching.
* The default behavior is to inherit the parent processes * stdout stream.
* This method cannot be used after a task is launched ... * it raises an NSInvalidArgumentException. */ - (void) setStandardOutput: (id)hdl { if (_hasLaunched) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has been launched"]; } NSAssert(hdl != nil && ([hdl isKindOfClass: [NSFileHandle class]] || [hdl isKindOfClass: [NSPipe class]]), NSInvalidArgumentException); ASSIGN(_standardOutput, hdl); } /** * Returns the standard error stream for the task - an NSFileHandle * unless an NSPipe was passed to -setStandardError: */ - (id) standardError { if (_standardError == nil) { [self setStandardError: [NSFileHandle fileHandleWithStandardError]]; } return _standardError; } /** * Returns the standard input stream for the task - an NSFileHandle * unless an NSPipe was passed to -setStandardInput: */ - (id) standardInput { if (_standardInput == nil) { [self setStandardInput: [NSFileHandle fileHandleWithStandardInput]]; } return _standardInput; } /** * Returns the standard output stream for the task - an NSFileHandle * unless an NSPipe was passed to -setStandardOutput: */ - (id) standardOutput { if (_standardOutput == nil) { [self setStandardOutput: [NSFileHandle fileHandleWithStandardOutput]]; } return _standardOutput; } /** * Sends a stop signal to the receiver and any subtasks.
* If the task has not been launched, raises an * NSInvalidArgumentException.
*/ - (BOOL) suspend { if (_hasLaunched == NO) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has not yet launched"]; } #ifndef _WIN32 #ifdef HAVE_KILLPG killpg(_taskId, SIGSTOP); #else kill(-_taskId, SIGSTOP); #endif #endif return YES; } /** * Sends a terminate signal to the receiver and any subtasks.
* If the task has not been launched, raises an * NSInvalidArgumentException.
* Has no effect on a task that has already terminated.
* When a task terminates, either due to this method being called, * or normal termination, an NSTaskDidTerminateNotification is * posted. */ - (void) terminate { if (_hasLaunched == NO) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has not yet launched"]; } if (_hasTerminated) { return; } _hasTerminated = YES; #ifndef _WIN32 #ifdef HAVE_KILLPG killpg(_taskId, SIGTERM); #else kill(-_taskId, SIGTERM); #endif #endif } /** * Returns the termination reason of the task.
* If the task has not completed running, raises an * NSInvalidArgumentException. */ - (NSTaskTerminationReason) terminationReason { if (_hasLaunched == NO) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has not yet launched"]; } if (_hasCollected == NO) { [self _collectChild]; } if (_hasTerminated == NO) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has not yet terminated"]; } return _terminationReason; } /** * Returns the termination status of the task.
* If the task has not completed running, raises an * NSInvalidArgumentException. */ - (int) terminationStatus { if (_hasLaunched == NO) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has not yet launched"]; } if (_hasCollected == NO) { [self _collectChild]; } if (_hasTerminated == NO) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has not yet terminated"]; } return _terminationStatus; } /** * If the system supports it, this method sets the standard * input, output, and error streams to a pseudo-terminal so * that, when launched, the child task will act as if it was * running interactively on a terminal. The file handles * can then be used to communicate with the child.
* This method cannot be used after a task is launched ... * it raises an NSInvalidArgumentException.
* The standard input, output and error streams cannot be * changed after calling this method.
* The method returns YES on success, NO on failure. */ - (BOOL) usePseudoTerminal { return NO; } /** * Returns a validated launch path or nil.
* Allows for the GNUstep host/operating system, and library combination * subdirectories in a path, appending them as necessary to try to locate * the actual binary to be used.
* Checks that the binary file exists and is executable.
* Even tries searching the directories in the PATH environment variable * to locate a binary if the original launch path set was not absolute. */ - (NSString*) validatedLaunchPath { NSString *libs; NSString *target_dir; NSString *prog; NSString *lpath; NSString *base_path; NSString *arch_path; NSString *full_path; if (_launchPath == nil) { return nil; } libs = [NSBundle _library_combo]; target_dir = [NSBundle _gnustep_target_dir]; /* * Set lpath to the actual path to use for the executable. * First choice - base_path/architecture/library_combo/prog. * Second choice - base_path/architecture/prog. * Third choice - base_path/prog. * Otherwise - try using PATH environment variable if possible. */ prog = [_launchPath lastPathComponent]; base_path = [_launchPath stringByDeletingLastPathComponent]; if ([[base_path lastPathComponent] isEqualToString: libs] == YES) base_path = [base_path stringByDeletingLastPathComponent]; if (nil == target_dir) arch_path = base_path; else { if ([[base_path lastPathComponent] isEqualToString: target_dir] == YES) base_path = [base_path stringByDeletingLastPathComponent]; arch_path = [base_path stringByAppendingPathComponent: target_dir]; } full_path = [arch_path stringByAppendingPathComponent: libs]; lpath = [full_path stringByAppendingPathComponent: prog]; if (nil == (lpath = [NSTask executablePath: lpath])) { lpath = [arch_path stringByAppendingPathComponent: prog]; if (nil == (lpath = [NSTask executablePath: lpath])) { lpath = [base_path stringByAppendingPathComponent: prog]; if (nil == (lpath = [NSTask executablePath: lpath])) { /* * Last resort - if the launch path was simply a program name * get NSBundle to try using the PATH environment * variable to find the executable. */ if ([base_path isEqualToString: @""] == YES) { lpath = [NSBundle _absolutePathOfExecutable: prog]; } if (lpath != nil) { lpath = [NSTask executablePath: lpath]; } } } } if (lpath != nil) { /* Make sure we have a standardised absolute path to pass to execve() */ if ([lpath isAbsolutePath] == NO) { NSString *current; current = [[NSFileManager defaultManager] currentDirectoryPath]; lpath = [current stringByAppendingPathComponent: lpath]; } lpath = [lpath stringByStandardizingPath]; #if defined(_WIN32) if ([lpath rangeOfString: @"/"].length > 0) { lpath = [lpath stringByReplacingString: @"/" withString: @"\\"]; } #endif } return lpath; } /** * Suspends the current thread until the task terminates, by * waiting in NSRunLoop (NSDefaultRunLoopMode) for the task * termination.
* Returns immediately if the task is not running. */ - (void) waitUntilExit { ENTER_POOL NSRunLoop *loop = [NSRunLoop currentRunLoop]; NSTimer *timer = nil; NSDate *limit = nil; IF_NO_ARC([[self retain] autorelease];) while ([self isRunning]) { /* Poll at 0.1 second intervals. */ limit = [[NSDate alloc] initWithTimeIntervalSinceNow: 0.1]; if (timer == nil) { timer = [NSTimer scheduledTimerWithTimeInterval: 0.1 target: nil selector: @selector(class) userInfo: nil repeats: YES]; } [loop runMode: NSDefaultRunLoopMode beforeDate: limit]; DESTROY(limit); } [timer invalidate]; /* Run loop twice more (with limit date in past) so that if the * notification about the task ending is to be delivered in this * thread it is delivered before this method completes. (Twice * because the first time runs the _notifyOfTermination performer, * which queues the notification, and the second dequeues and posts * the notification.) * If the task was launched in another thread, the notification * is delivered when that thread executes its run loop. */ limit = [NSDate dateWithTimeIntervalSinceNow: 0.0]; [loop runMode: NSDefaultRunLoopMode beforeDate: limit]; [loop runMode: NSDefaultRunLoopMode beforeDate: limit]; LEAVE_POOL } // macOS 10.13 methods... + (NSTask *) launchedTaskWithExecutableURL: (NSURL *)url arguments: (NSArray *)arguments error: (NSError **)error terminationHandler: (GSTaskTerminationHandler)terminationHandler { NSTask *task = [self launchedTaskWithLaunchPath: [url path] arguments: arguments]; task->_handler = terminationHandler; if (error) { *error = nil; } return task; } - (BOOL) launchAndReturnError: (NSError **)error { NSFileManager *mgr = [NSFileManager defaultManager]; NSString *cwd; BOOL ok; ASSIGN(_launchingThread, [NSThread currentThread]); cwd = [self currentDirectoryPath]; if (NO == [mgr fileExistsAtPath: cwd isDirectory: &ok]) { if (error) { NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: @"does not exist", NSLocalizedDescriptionKey, cwd, NSFilePathErrorKey, nil]; *error = [NSError errorWithDomain: NSCocoaErrorDomain code: NSFileNoSuchFileError userInfo: info]; } return NO; } if (NO == ok) { if (error) { *error = [NSError _systemError: ENOTDIR]; [*error _setObject: cwd forKey: NSFilePathErrorKey]; } return NO; } if (NO == [mgr isExecutableFileAtPath: cwd]) { if (error) { *error = [NSError _systemError: EACCES]; [*error _setObject: cwd forKey: NSFilePathErrorKey]; } return NO; } return YES; } - (NSURL *) executableURL { return [NSURL URLWithString: [self launchPath]];; } - (void) setExecutableURL: (NSURL *)url { [self setLaunchPath: [url path]]; } - (NSURL *) currentDirectoryURL { return [NSURL URLWithString: [self currentDirectoryPath]]; } - (void) setCurrentDirectoryURL: (NSURL *)url { [self setCurrentDirectoryPath: [url path]]; } @end @implementation NSTask (Private) - (NSString *) _fullLaunchPath { NSString *val; if (_launchPath == nil) { [NSException raise: NSInvalidArgumentException format: @"NSTask - no launch path set"]; } val = [self validatedLaunchPath]; if (val == nil) { [NSException raise: NSInvalidArgumentException format: @"NSTask - launch path (%@) not valid", _launchPath]; } return val; } - (void) _collectChild { [self subclassResponsibility: _cmd]; } - (void) _notifyOfTermination { NSNotificationQueue *q; NSNotification *n; n = [NSNotification notificationWithName: NSTaskDidTerminateNotification object: self userInfo: nil]; q = [NSNotificationQueue defaultQueue]; [q enqueueNotification: n postingStyle: NSPostASAP coalesceMask: NSNotificationNoCoalescing forModes: nil]; if (_handler != NULL) { CALL_BLOCK_NO_ARGS(_handler); } } - (void) _terminatedChild: (int)status reason: (NSTaskTerminationReason)reason { [tasksLock lock]; IF_NO_ARC([[self retain] autorelease];) NSMapRemove(activeTasks, (void*)(intptr_t)_taskId); [tasksLock unlock]; _terminationStatus = status; _terminationReason = reason; _hasCollected = YES; _hasTerminated = YES; if (_hasNotified == NO) { _hasNotified = YES; if (_launchingThread != [NSThread currentThread] && [_launchingThread isExecuting] == YES) { [self performSelector: @selector(_notifyOfTermination) onThread: _launchingThread withObject: nil waitUntilDone: NO]; } else { [self _notifyOfTermination]; } } } @end #ifdef _WIN32 @implementation NSConcreteWindowsTask BOOL GSPrivateCheckTasks() { BOOL found = NO; if (YES == hadChildSignal) { NSArray *a; unsigned c; hadChildSignal = NO; [tasksLock lock]; a = NSAllMapTableValues(activeTasks); [tasksLock unlock]; c = [a count]; while (c-- > 0) { NSConcreteWindowsTask *t = [a objectAtIndex: c]; DWORD eCode; if (GetExitCodeProcess(t->procInfo.hProcess, &eCode) != 0) { if (eCode != STILL_ACTIVE) { [t _terminatedChild: eCode reason: (WIN_SIGNALLED == eCode) ? NSTaskTerminationReasonUncaughtSignal : NSTaskTerminationReasonExit]; found = YES; } } } } return found; } - (void) finalize { [super finalize]; if (wThread != NULL) { CloseHandle(wThread); } if (procInfo.hProcess != NULL) { CloseHandle(procInfo.hProcess); } if (procInfo.hThread != NULL) { CloseHandle(procInfo.hThread); } } - (void) interrupt { } - (void) terminate { if (_hasLaunched == NO) { [NSException raise: NSInvalidArgumentException format: @"NSTask - task has not yet launched"]; } if (_hasTerminated) { return; } /* We use exit code 10 to denote a process termination. * Windows does nt have an exit code to denote termination this way. */ _hasTerminated = YES; TerminateProcess(procInfo.hProcess, WIN_SIGNALLED); } static NSString* endSlashesDoubledFromString(NSString *aString) { int i = [aString length] - 2; NSMutableString *returnString; if (![aString hasSuffix:@"\\"]) { return aString; } returnString = [NSMutableString stringWithFormat: @"%@\\", aString]; while ([aString characterAtIndex: i] == '\\' && i >= 0) { [returnString appendString:@"\\"]; i--; } return returnString; } static NSString* quotedFromString(NSString *aString) { NSString *resultString; NSMutableArray *components; int i; /* First split on "'s */ components = [NSMutableArray arrayWithArray: [aString componentsSeparatedByString: @"\""]]; /* Iterate over all but the last component and double slashes if needed */ i = [components count]; while (i-- > 0) { [components replaceObjectAtIndex: i withObject: endSlashesDoubledFromString([components objectAtIndex: i])]; } /* Join them again with \" as separator */ resultString = [components componentsJoinedByString: @"\\\""]; /* Put in in "'s if it contains spaces */ if ([resultString rangeOfCharacterFromSet: [NSCharacterSet whitespaceCharacterSet]].length > 0) { resultString = [NSString stringWithFormat: @"\"%@\"", resultString]; } return resultString; } /* Wait for child process completion. The task object is retained by the * current thread until it exits, but may be removed from the table of * active tasks by another thread, so we must check that it's still * active at intervals. */ - (void) _windowsTaskWatcher { void *taskId = (void*)(intptr_t)[self processIdentifier]; for (;;) { NSConcreteWindowsTask *task; /* Check that this task is still active. */ [tasksLock lock]; task = (NSConcreteWindowsTask*)NSMapGet(activeTasks, taskId); [tasksLock unlock]; if (task != self) { /* Task has been reaped by another thread ... so we can exit. */ [NSThread exit]; } /* Wait for up to 1 minute (60,000 milliseconds) to get a notification * of an event from the subprocess. */ if (WaitForSingleObject(procInfo.hProcess, (DWORD)60000) != WAIT_TIMEOUT) { /* The subprocess has terminated or failed in some way ... * Set flag so child task can be reaped, then exit since this * thread no longer needs to watch for events. */ handleSignal(0); [NSThread exit]; } /* Timed out ... repeat wait attempt. */ } } // 10.13 method... - (BOOL) launchAndReturnError: (NSError **)error { STARTUPINFOW start_info; NSString *lpath; NSString *arg; NSEnumerator *arg_enum; NSMutableString *args; wchar_t *w_args; int result; const wchar_t *wexecutable; LPVOID envp = 0; NSDictionary *env; NSMutableArray *toClose; NSFileHandle *hdl; HANDLE hIn; HANDLE hOut; HANDLE hErr; id last = nil; if (_hasLaunched) { if (error) { NSDictionary *info; info = [NSDictionary dictionaryWithObjectsAndKeys: @"task has already been launched", NSLocalizedDescriptionKey, nil]; *error = [NSError errorWithDomain: NSCocoaErrorDomain code: 0 userInfo: info]; } return NO; } if (NO == [super launchAndReturnError: error]) { return NO; } lpath = [self _fullLaunchPath]; wexecutable = (const unichar*)[lpath fileSystemRepresentation]; args = [[NSMutableString alloc] initWithString: quotedFromString(lpath)]; arg_enum = [[self arguments] objectEnumerator]; while ((arg = [arg_enum nextObject])) { [args appendString: @" "]; [args appendString: quotedFromString(arg)]; } // NSLog(@"ARGS: '%@'", args); w_args = NSZoneMalloc(NSDefaultMallocZone(), sizeof(wchar_t) * ([args length] + 1)); [args getCharacters: (unichar*)w_args]; w_args[[args length]] = 0; env = [self environment]; if ([env count] > 0) { NSMutableData *data = [NSMutableData dataWithCapacity: 10240]; NSEnumerator *enumerator; NSString *key; unichar terminator = 0; NSAutoreleasePool *pool = [NSAutoreleasePool new]; // Win32 environment variables must be sorted by name enumerator = [[[env allKeys] sortedArrayUsingSelector: @selector(compare:)] objectEnumerator]; while ((key = [enumerator nextObject])) { NSString *value = [env objectForKey:key]; NSString *setting; unsigned l; NSRange r = NSMakeRange(0,0); unichar buffer[1024]; setting = [NSString stringWithFormat: @"%@=%@", key, value]; l = [setting length]; while (r.location < l) { r.length = l - r.location; if (r.length > 1024) { r.length = 1024; } [setting getCharacters: buffer range: r]; [data appendBytes: buffer length: (r.length)*sizeof(unichar)]; r.location += r.length; } [data appendBytes: &terminator length: 2]; // end of setting } [data appendBytes: &terminator length: 2]; // end of environment [pool drain]; envp = [data mutableBytes]; } memset (&start_info, 0, sizeof(start_info)); start_info.cb = sizeof(start_info); start_info.dwFlags |= STARTF_USESTDHANDLES; toClose = [NSMutableArray arrayWithCapacity: 3]; if (_standardInput == nil) { start_info.hStdInput = GetStdHandle(STD_INPUT_HANDLE); } else { hdl = [self standardInput]; if ([hdl isKindOfClass: [NSPipe class]]) { hdl = [(NSPipe*)hdl fileHandleForReading]; [toClose addObject: hdl]; } start_info.hStdInput = [hdl nativeHandle]; } hIn = start_info.hStdInput; if (_standardOutput == nil) { start_info.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); } else { hdl = [self standardOutput]; if ([hdl isKindOfClass: [NSPipe class]]) { hdl = [(NSPipe*)hdl fileHandleForWriting]; [toClose addObject: hdl]; } start_info.hStdOutput = [hdl nativeHandle]; } hOut = start_info.hStdOutput; if (_standardError == nil) { start_info.hStdError = GetStdHandle(STD_ERROR_HANDLE); } else { hdl = [self standardError]; if ([hdl isKindOfClass: [NSPipe class]]) { hdl = [(NSPipe*)hdl fileHandleForWriting]; /* * If we have the same pipe twice we don't want to close it twice */ if ([toClose indexOfObjectIdenticalTo: hdl] == NSNotFound) { [toClose addObject: hdl]; } } start_info.hStdError = [hdl nativeHandle]; } hErr = start_info.hStdError; /* Tell the system not to show a window for the subtask. */ start_info.wShowWindow = SW_HIDE; start_info.dwFlags |= STARTF_USESHOWWINDOW; /* Make the handles inheritable only temporarily while launching the * child task. This section must be lock protected so we don't have * another thread trying to launch at the same time and get handles * inherited by the wrong threads. */ [tasksLock lock]; SetHandleInformation(hIn, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); SetHandleInformation(hOut, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); SetHandleInformation(hErr, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); result = CreateProcessW(wexecutable, w_args, NULL, /* proc attrs */ NULL, /* thread attrs */ 1, /* inherit handles */ 0 /* We don't want a subtask to be run min a window since that would prevent * startup of background tasks. If a subtask wants a window, it should * create it itself. */ |CREATE_NO_WINDOW /* We don't set the DETACHED_PROCESS flag as it actually means that the * child task should get a new Console allocated ... and that means it * will pop up a console window ... which looks really bad. */ // |DETACHED_PROCESS |CREATE_UNICODE_ENVIRONMENT, envp, /* env block */ (const unichar*)[[self currentDirectoryPath] fileSystemRepresentation], &start_info, &procInfo); if (0 == result) { last = [NSError _last]; } NSZoneFree(NSDefaultMallocZone(), w_args); SetHandleInformation(hIn, HANDLE_FLAG_INHERIT, 0); SetHandleInformation(hOut, HANDLE_FLAG_INHERIT, 0); SetHandleInformation(hErr, HANDLE_FLAG_INHERIT, 0); [tasksLock unlock]; if (0 == result) { if (error) { *error = last; } return NO; } _taskId = procInfo.dwProcessId; _hasLaunched = YES; ASSIGN(_launchPath, lpath); // Actual path used. [tasksLock lock]; NSMapInsert(activeTasks, (void*)(intptr_t) _taskId, (void*)self); [tasksLock unlock]; /* * Create thread to watch for termination of process. */ [NSThread detachNewThreadSelector: @selector(_windowsTaskWatcher) toTarget: self withObject: nil]; /* * Close the ends of any pipes used by the child. */ while ([toClose count] > 0) { hdl = [toClose objectAtIndex: 0]; [hdl closeFile]; [toClose removeObjectAtIndex: 0]; } return YES; } - (void) _collectChild { if (_hasCollected == NO) { DWORD eCode; if (GetExitCodeProcess(procInfo.hProcess, &eCode) == 0) { NSLog(@"Error getting exit code for process %d", _taskId); } else if (eCode != STILL_ACTIVE) { [self _terminatedChild: eCode reason: (WIN_SIGNALLED == eCode) ? NSTaskTerminationReasonUncaughtSignal : NSTaskTerminationReasonExit]; } } } @end #else /* !_WIN32 */ @implementation NSConcreteUnixTask BOOL GSPrivateCheckTasks() { BOOL found = NO; if (YES == hadChildSignal) { int result; int status; hadChildSignal = NO; do { NSTask *t; errno = 0; result = waitpid(-1, &status, WNOHANG); if (result < 0) { #if defined(WAITDEBUG) [tasksLock lock]; t = (NSTask*)NSMapGet(activeTasks, (void*)(intptr_t)result); IF_NO_ARC([[t retain] autorelease];) [tasksLock unlock]; if (t != nil) { NSLog(@"waitpid result %d, error %@", result, [NSError _last]); } #endif } else if (result > 0) { [tasksLock lock]; t = (NSTask*)NSMapGet(activeTasks, (void*)(intptr_t)result); IF_NO_ARC([[t retain] autorelease];) [tasksLock unlock]; if (t != nil) { if (WIFEXITED(status)) { #if defined(WAITDEBUG) NSLog(@"waitpid %d, exit status = %d", result, status); #endif [t _terminatedChild: WEXITSTATUS(status) reason: NSTaskTerminationReasonExit]; found = YES; } else if (WIFSIGNALED(status)) { #if defined(WAITDEBUG) NSLog(@"waitpid %d, termination status = %d", result, status); #endif [t _terminatedChild: WTERMSIG(status) reason: NSTaskTerminationReasonUncaughtSignal]; found = YES; } else { NSLog(@"Warning ... task %d neither exited nor signalled", result); } } #if defined(WAITDEBUG) else { NSLog(@"Received signal for unknown child %d", result); } #endif } } while (result > 0); } return found; } // 10.13 method... - (BOOL) launchAndReturnError: (NSError **)error { NSDictionary *info; NSMutableArray *toClose; NSString *lpath; int pid; const char *executable; const char *path; int idesc; int odesc; int edesc; NSDictionary *e = [self environment]; NSArray *k = [e allKeys]; NSArray *a = [self arguments]; int ec = [e count]; int ac = [a count]; const char *args[ac+2]; const char *envl[ec+1]; id hdl; int i; if (_hasLaunched) { if (error) { info = [NSDictionary dictionaryWithObjectsAndKeys: @"task has already been launched", NSLocalizedDescriptionKey, nil]; *error = [NSError errorWithDomain: NSCocoaErrorDomain code: 0 userInfo: info]; } return NO; } if (NO == [super launchAndReturnError: error]) { return NO; } lpath = [self _fullLaunchPath]; executable = [lpath fileSystemRepresentation]; args[0] = executable; for (i = 0; i < ac; i++) { args[i+1] = [[[a objectAtIndex: i] description] lossyCString]; } args[ac+1] = 0; for (i = 0; i < ec; i++) { NSString *s; id key = [k objectAtIndex: i]; id val = [e objectForKey: key]; if (val) { s = [NSString stringWithFormat: @"%@=%@", key, val]; } else { s = [NSString stringWithFormat: @"%@=", key]; } envl[i] = [s lossyCString]; } envl[ec] = 0; path = [[self currentDirectoryPath] fileSystemRepresentation]; toClose = [NSMutableArray arrayWithCapacity: 3]; hdl = [self standardInput]; if ([hdl isKindOfClass: [NSPipe class]]) { hdl = [(NSPipe*)hdl fileHandleForReading]; [toClose addObject: hdl]; } idesc = [hdl fileDescriptor]; hdl = [self standardOutput]; if ([hdl isKindOfClass: [NSPipe class]]) { hdl = [(NSPipe*)hdl fileHandleForWriting]; [toClose addObject: hdl]; } odesc = [hdl fileDescriptor]; hdl = [self standardError]; if ([hdl isKindOfClass: [NSPipe class]]) { hdl = [(NSPipe*)hdl fileHandleForWriting]; /* * If we have the same pipe twice we don't want to close it twice */ if ([toClose indexOfObjectIdenticalTo: hdl] == NSNotFound) { [toClose addObject: hdl]; } } edesc = [hdl fileDescriptor]; /* NB. we use fork() rather than vfork() because the bahavior of vfork() * is undefined when we assign to variables or make system calls (as we * do below) other than a very limited set. * For performance it might be possible to use vfork on systems where * there is a guarantee that vfork() is safe, but when in doubt we must * assume the standard POSIX behavior. */ pid = fork(); if (pid < 0) { if (error) { *error = [NSError _last]; } return NO; } if (pid == 0) { int i; /* Make sure the task gets default signal setup. */ for (i = 0; i < 32; i++) { signal(i, SIG_DFL); } /* Make sure task is session leader in it's own process group * and with no controlling terminal. * This allows us to use killpg() to put the task to sleep etc, * and have the signal effect forked children of the subtask. */ #if defined(HAVE_SETSID) setsid(); #else #if defined(HAVE_SETPGRP) #if defined(SETPGRP_VOID) setpgrp(); #else setpgrp(getpid(), getpid()); #endif #else #if defined(HAVE_SETPGID) #if defined(_WIN32) pid = (int)GetCurrentProcessId(), #else pid = (int)getpid(); #endif setpgid(pid, pid); #endif /* HAVE_SETPGID */ #endif /* HAVE_SETPGRP */ /* Detach from controlling terminal. */ #if defined(TIOCNOTTY) i = open("/dev/tty", O_RDWR); if (i >= 0) { (void)ioctl(i, TIOCNOTTY, 0); (void)close(i); } #endif /* TIOCNOTTY */ #endif /* HAVE_SETSID */ if (_usePseudoTerminal == YES) { int s; s = pty_slave(slave_name); if (s < 0) { exit(1); /* Failed to open slave! */ } /* Set up stdin, stdout and stderr by duplicating descriptor as * necessary and closing the original. */ if (dup2(s, 0) != 0) exit(1); if (dup2(s, 1) != 1) exit(1); if (dup2(s, 2) != 2) exit(1); if (s != 0 && s != 1 && s != 2) { (void)close(s); } } else { /* Set up stdin, stdout and stderr by duplicating descriptors as * necessary and closing the originals (to ensure we won't have a * pipe left with two write descriptors etc). */ if (idesc != 0) { if (dup2(idesc, 0) != 0) exit(1); } if (odesc != 1) { if (dup2(odesc, 1) != 1) exit(1); } if (edesc != 2) { if (dup2(edesc, 2) != 2) exit(1); } } /* * Close any extra descriptors. */ #if defined(HAVE_CLOSEFROM) closefrom(3); #elif defined(F_CLOSEM) (void)fcntl(3, F_CLOSEM, 0); #elif defined(_SC_OPEN_MAX) i = sysconf(_SC_OPEN_MAX); while (i-- > 3) { (void)close(i); } #else for (i = 3; i < NOFILE; i++) { (void)close(i); } #endif if (0 != chdir(path)) { _exit(-1); } (void)execve(executable, (char**)args, (char**)envl); _exit(-1); } else { _taskId = pid; _hasLaunched = YES; ASSIGN(_launchPath, lpath); // Actual path used. [tasksLock lock]; NSMapInsert(activeTasks, (void*)(intptr_t)_taskId, (void*)self); [tasksLock unlock]; /* * Close the ends of any pipes used by the child. */ while ([toClose count] > 0) { hdl = [toClose objectAtIndex: 0]; [hdl closeFile]; [toClose removeObjectAtIndex: 0]; } } return YES; } - (void) setStandardError: (id)hdl { if (_usePseudoTerminal == YES) { [NSException raise: NSInvalidArgumentException format: @"NSTask - set error for task on pseudo terminal"]; } [super setStandardError: hdl]; } - (void) setStandardInput: (id)hdl { if (_usePseudoTerminal == YES) { [NSException raise: NSInvalidArgumentException format: @"NSTask - set input for task on pseudo terminal"]; } [super setStandardInput: hdl]; } - (void) setStandardOutput: (id)hdl { if (_usePseudoTerminal == YES) { [NSException raise: NSInvalidArgumentException format: @"NSTask - set output for task on pseudo terminal"]; } [super setStandardOutput: hdl]; } - (void) _collectChild { if (_hasCollected == NO) { int result; int status; errno = 0; result = waitpid(_taskId, &status, WNOHANG); if (result > 0) { if (WIFEXITED(status)) { #if defined(WAITDEBUG) NSLog(@"waitpid %d, exit status = %d", result, status); #endif [self _terminatedChild: WEXITSTATUS(status) reason: NSTaskTerminationReasonExit]; } else if (WIFSIGNALED(status)) { #if defined(WAITDEBUG) NSLog(@"waitpid %d, termination status = %d", result, status); #endif [self _terminatedChild: WTERMSIG(status) reason: NSTaskTerminationReasonUncaughtSignal]; } else { NSLog(@"Warning ... task %d neither exited nor signalled", result); } } } } - (BOOL) usePseudoTerminal { int desc; int master; NSFileHandle *fh; if (_usePseudoTerminal == YES) { return YES; } master = pty_master(slave_name, sizeof(slave_name)); if (master < 0) { return NO; } fh = [[NSFileHandle alloc] initWithFileDescriptor: master closeOnDealloc: YES]; [self setStandardInput: fh]; RELEASE(fh); if ((desc = dup(master)) < 0) { return NO; } fh = [[NSFileHandle alloc] initWithFileDescriptor: desc closeOnDealloc: YES]; [self setStandardOutput: fh]; RELEASE(fh); if ((desc = dup(master)) < 0) { return NO; } fh = [[NSFileHandle alloc] initWithFileDescriptor: desc closeOnDealloc: YES]; [self setStandardError: fh]; RELEASE(fh); _usePseudoTerminal = YES; return YES; } @end #endif /* !_WIN32 */