currentDirectoryPath and NSTask

1.3k views Asked by At

OK, let's say I'm creating a (Bash) Terminal emulator - I'm not actually, but it's pretty close in terms of description.

I've managed to get (almost) everything working, however I'm facing one simple issue: maintaining the current directory.

I mean... let's say the user runs pwd and we execute this via NSTask and /usr/bin/env bash. This outputs the current app's directory. That's fine.

Now, let's say the user enters cd ... The path is changing right? (OK, even for that particular session, but it is changing, nope?)

So, I though of storing the task's currentDirectoryPath when the Task is terminated and then re-set it when starting another Bash-related task.

However, it keeps getting the very same path (the one the app bundle is in).

What am I missing?

Any ideas on how to get this working?


The code

- (NSString*)exec:(NSArray *)args environment:(NSDictionary*)env action:(void (^)(NSString*))action completed:(void (^)(NSString*))completed
{
    _task                           = [NSTask new];
    _output                         = [NSPipe new];
    _error                          = [NSPipe new];
    _input                          = [NSPipe new];
    NSFileHandle* outputF           = [_output fileHandleForReading];
    NSFileHandle* errorF            = [_error fileHandleForReading];

    __block NSString* fullOutput    = @"";

    NSMutableDictionary* envs = [NSMutableDictionary dictionary];

    envs[@"PATH"] = [[APP environment] envPath];

    [_task setLaunchPath:@"/usr/bin/env"];
    if (env)
    {
        for (NSString* key in env)
        {
            envs[key] = env[key];
        }
    }
    [_task setEnvironment:envs];

    [_task setArguments:args];
    [_task setStandardOutput:_output];
    [_task setStandardError:_error];
    [_task setStandardInput:_input];

    void (^outputter)(NSFileHandle*) = ^(NSFileHandle *file){
        NSData *data = [file availableData];
        NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

        action(str);

        fullOutput = [fullOutput stringByAppendingString:str];
    };

    [outputF setReadabilityHandler:outputter];
    [errorF setReadabilityHandler:outputter];

    [_task setTerminationHandler:^(NSTask* task){
        completed(fullOutput);

        dispatch_async(dispatch_get_main_queue(), ^{
            [[APP environment] setPwd:[task currentDirectoryPath]];
        });

        [task.standardOutput fileHandleForReading].readabilityHandler = nil;
        [task.standardError fileHandleForReading].readabilityHandler = nil;
        [task.standardInput fileHandleForWriting].writeabilityHandler = nil;
        [task terminate];
        task = nil;
    }];

    if (![[[APP environment] pwd] isEqualToString:@""])
        [_task setCurrentDirectoryPath:[[APP environment] pwd]];

    [_task launch];

    return @"";
}
2

There are 2 answers

2
Lawrence H On

I am fairly certain that running cd in NSTask does not change the value of task.currentDirectoryPath. Have you tried setting a break point in your dispatch call to see if that value is actually being set correctly?

Edit:
From your termination handler try doing [[APP environment] setPwd:[[[NSProcessInfo processInfo]environment]objectForKey:@"PATH"]];

0
Ken Thomases On

As a general matter, it is difficult to impossible to modify another process's environment and other properties from the outside. Similarly, it is not generally possible to query those from the outside. Debuggers and ps can do it by using special privileges.

The parent process which created the process in question has the opportunity to set the initial environment and properties at the point where it spawns the subprocess.

The cd command is necessarily a shell built-in command precisely because it has to modify the shell process's state. That change does not directly affect any other existing process's state. It will be inherited by subprocesses that are subsequently created.

The currentDirectoryPath property of NSTask is only meaningful at the point where the task is launched. It is the current directory that the new process will inherit. It does not track the subprocess's current directory, because it can't. Querying it only returns the value that the NSTask object was configured to use (or the default value which is the current directory of the process which created the NSTask object).

If you're trying to write something like a terminal emulator, you will need to create a long-running interactive shell subprocess with communication pipes between the parent and the shell. Don't run individual commands in separate processes. Instead, write the commands to the interactive shell over the pipe and read the subsequent output. It probably doesn't make sense to try to interpret that output since it can be general in form and not easily parsable. Just display it directly to the user.

Alternatively, you will have to interpret some commands locally in the parent process. This will be analogous to shell built-ins. So, you would have to recognize a cd command and, instead of launching an NSTask to execute it, you would just modify the state of the parent process in such a way that the new current directory will be used for subsequent tasks. That is, you could track the new current directory in a variable and set currentDirectoryPath for all subsequent NSTask objects before launching them or you could modify the parent process's current directory using -[NSFileManager changeCurrentDirectoryPath:] and that will automatically be inherited by future subprocesses.