Using /dev/stdout in a sandboxed app


Graham Cox
 

Hi all,

In my app I use NSTask to launch a copy of ffmpeg as a child process to do some work in the background on a video stream.

I pass parameters to this task via the command line arguments through NSTask. One of the arguments is where to send its ‘progress’ output, which I direct to /dev/stdout, which in turn I open as a file handle so my app receives these notifications. I parse what comes back to update my UI in various ways.

This all works fine when the app is built unsandboxed.

When it is sandboxed, the NSTask fails to launch because it doesn’t have permission to open /dev/stdout

I’m wondering how to deal with this. If I just create a URL for /dev/stdout and then call -startAccessingSecurityScopedResource on it, that has no effect, but it’s probably naive to think it would. The launched task is responsible for opening the stream, and I have no idea what exactly it does to do that anyway - it’s all internal to ffmpeg.

Can this even be done in a sandboxed app?


—Graham


Chris Hanson
 

Sure, just use NSTask’s own standardInput, standardOutput, and standardError support with NSPipe. No need to tell the task to send its stdout to some intermediate file that your app then opens and reads from. (Whether it’s in /dev or anywhere else.)

-- Chris


Bill Pitcher
 

Yes, NSTask (I think) and Process() inherent the sandbox

"Important
In a sandboxed application, child processes created with the Process class inherit the sandbox of the parent app. You should generally write helper applications as XPC Services instead, because XPC Services allows you to specify different sandbox entitlements for helper apps. See Daemons and Services Programming Guide and XPC for more information.”

I ran into this and just decided to not use sandbox nor sell the app on the App Store, just not worth the extra hassle.

cheers
Bill

On 9 Jul 2019, at 11:47 am, Graham Cox <graham@...> wrote:

Hi all,

In my app I use NSTask to launch a copy of ffmpeg as a child process to do some work in the background on a video stream.

I pass parameters to this task via the command line arguments through NSTask. One of the arguments is where to send its ‘progress’ output, which I direct to /dev/stdout, which in turn I open as a file handle so my app receives these notifications. I parse what comes back to update my UI in various ways.

This all works fine when the app is built unsandboxed.

When it is sandboxed, the NSTask fails to launch because it doesn’t have permission to open /dev/stdout

I’m wondering how to deal with this. If I just create a URL for /dev/stdout and then call -startAccessingSecurityScopedResource on it, that has no effect, but it’s probably naive to think it would. The launched task is responsible for opening the stream, and I have no idea what exactly it does to do that anyway - it’s all internal to ffmpeg.

Can this even be done in a sandboxed app?


—Graham





Graham Cox
 

Hi Chris,

actually I think I am doing that.

it’s a while since i put this code together, and exactly what the arguments are is not so clear now. I set the NSTask.standardOutput to an NSPipe, and I have a callback block that deals with that.

However, I’m also passing the -progress argument to ffmpeg as /dev/stdout. Now ffmpeg has a million options, and the documentation is rather terse. All it says for the -progress option is:


I pass /dev/stdout as the url argument because what else could it be? My assumption was that NSTask’s standardOutput basically read from there, it was just a convenient place to hook in. The fact it worked led me never to further question that until now. If you don’t pass the -progress parameter, ffmpeg doesn’t send anything.

So the problem isn’t so much the NSTask, but what to pass as a url parameter to the -progress option.

The gist of the code is (with irrelevant stuff cut for clarity and brevity)

NSPipe* progressPipe = [NSPipe pipe];


progressPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle* handle )
{
// parse progress reports to update UI with certain info
};


// create the FFMPEG task:


NSString* execPath = [[[self class] ffMPEGURL] path];
NSArray* codecArgs = [[[self class] ffMPEGCodecForOption:self.format] componentsSeparatedByString:@" "];


if( codecArgs.count > 0 )
{
if( self.outputFileURL == nil )
[self requestDelegateCreatesFile];


NSAssert( self.outputFileURL, @"unable to proceed - no output file");


NSMutableArray* args = [NSMutableArray array];


[args addObjectsFromArray:@[@"-loglevel", @"error", @"-i", url.absoluteString]];
[args addObjectsFromArray:codecArgs];
[args addObjectsFromArray:@[@"-progress", @"/dev/stdout", self.outputFileURL.path]];  /<——————————————— set up -progress option


XVLOG(@"ffmpeg arguments: %@", args);


NSTask* theTask = [[NSTask alloc] init];


NSAssert( theTask, @"unable to create NSTask for ffmpeg subprocess");


theTask.launchPath = execPath;
theTask.arguments = args;
theTask.standardOutput = progressPipe;
theTask.qualityOfService = NSQualityOfServiceUserInitiated;

theTask.terminationHandler = ^(NSTask* task)
{
// termination handler
};


self.ffMPEGTask = theTask;


[theTask release];


if( self.ffMPEGTask )
{
[self.ffMPEGTask launch]; // GO!!!
self.status = XVMediaDownloaderRunning;
self.filesize = 0;
}
}



—Graham



On 9 Jul 2019, at 4:01 pm, Chris Hanson <cmhanson@...> wrote:

Sure, just use NSTask’s own standardInput, standardOutput, and standardError support with NSPipe. No need to tell the task to send its stdout to some intermediate file that your app then opens and reads from. (Whether it’s in /dev or anywhere else.)

 -- Chris






Sandor Szatmari
 

I think you would pass

-progress pipe:1 will write out to stdout, 

Or

pipe:2 to stderr.

Then just read from the pipe you associate.

I haven’t tried it myself, google told me to do it.

It might mess up your parsing if other messages are interspersed in the progress messages, but hopefully you can distinguish between the two types of information.  


Sandor

On Jul 9, 2019, at 02:42, Graham Cox <graham@...> wrote:

Hi Chris,

actually I think I am doing that.

it’s a while since i put this code together, and exactly what the arguments are is not so clear now. I set the NSTask.standardOutput to an NSPipe, and I have a callback block that deals with that.

However, I’m also passing the -progress argument to ffmpeg as /dev/stdout. Now ffmpeg has a million options, and the documentation is rather terse. All it says for the -progress option is:

<Screen Shot 2019-07-09 at 4.26.06 pm.png>

I pass /dev/stdout as the url argument because what else could it be? My assumption was that NSTask’s standardOutput basically read from there, it was just a convenient place to hook in. The fact it worked led me never to further question that until now. If you don’t pass the -progress parameter, ffmpeg doesn’t send anything.

So the problem isn’t so much the NSTask, but what to pass as a url parameter to the -progress option.

The gist of the code is (with irrelevant stuff cut for clarity and brevity)

NSPipe* progressPipe = [NSPipe pipe];


progressPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle* handle )
{
// parse progress reports to update UI with certain info
};


// create the FFMPEG task:


NSString* execPath = [[[self class] ffMPEGURL] path];
NSArray* codecArgs = [[[self class] ffMPEGCodecForOption:self.format] componentsSeparatedByString:@" "];


if( codecArgs.count > 0 )
{
if( self.outputFileURL == nil )
[self requestDelegateCreatesFile];


NSAssert( self.outputFileURL, @"unable to proceed - no output file");


NSMutableArray* args = [NSMutableArray array];


[args addObjectsFromArray:@[@"-loglevel", @"error", @"-i", url.absoluteString]];
[args addObjectsFromArray:codecArgs];
[args addObjectsFromArray:@[@"-progress", @"/dev/stdout", self.outputFileURL.path]];  /<——————————————— set up -progress option


XVLOG(@"ffmpeg arguments: %@", args);


NSTask* theTask = [[NSTask alloc] init];


NSAssert( theTask, @"unable to create NSTask for ffmpeg subprocess");


theTask.launchPath = execPath;
theTask.arguments = args;
theTask.standardOutput = progressPipe;
theTask.qualityOfService = NSQualityOfServiceUserInitiated;

theTask.terminationHandler = ^(NSTask* task)
{
// termination handler
};


self.ffMPEGTask = theTask;


[theTask release];


if( self.ffMPEGTask )
{
[self.ffMPEGTask launch]; // GO!!!
self.status = XVMediaDownloaderRunning;
self.filesize = 0;
}
}



—Graham



On 9 Jul 2019, at 4:01 pm, Chris Hanson <cmhanson@...> wrote:

Sure, just use NSTask’s own standardInput, standardOutput, and standardError support with NSPipe. No need to tell the task to send its stdout to some intermediate file that your app then opens and reads from. (Whether it’s in /dev or anywhere else.)

 -- Chris






Graham Cox
 

Ah, that worked a treat!!! Thanks so much Sandor.

—Graham




On 9 Jul 2019, at 9:21 pm, Sandor Szatmari <admin.szatmari.net@...> wrote:

I think you would pass

-progress pipe:1 will write out to stdout, 

Or

pipe:2 to stderr.

Then just read from the pipe you associate.

I haven’t tried it myself, google told me to do it.

It might mess up your parsing if other messages are interspersed in the progress messages, but hopefully you can distinguish between the two types of information.  


Sandor


Bill Pitcher
 

looking back I went with

//! Connect the Output Pipes
[aTask setStandardOutput:[NSPipe pipe]];
[aTask setStandardError:[aTask standardOutput]];


//! Tell the Pipe to notify to read in the background and post a notification
[[[aTask standardOutput] fileHandleForReading] readInBackgroundAndNotify];

then grab the data as it comes in.

Cheers
Bill

On 9 Jul 2019, at 4:42 pm, Graham Cox <graham@...> wrote:

Hi Chris,

actually I think I am doing that.

it’s a while since i put this code together, and exactly what the arguments are is not so clear now. I set the NSTask.standardOutput to an NSPipe, and I have a callback block that deals with that.

However, I’m also passing the -progress argument to ffmpeg as /dev/stdout. Now ffmpeg has a million options, and the documentation is rather terse. All it says for the -progress option is:

<Screen Shot 2019-07-09 at 4.26.06 pm.png>

I pass /dev/stdout as the url argument because what else could it be? My assumption was that NSTask’s standardOutput basically read from there, it was just a convenient place to hook in. The fact it worked led me never to further question that until now. If you don’t pass the -progress parameter, ffmpeg doesn’t send anything.

So the problem isn’t so much the NSTask, but what to pass as a url parameter to the -progress option.

The gist of the code is (with irrelevant stuff cut for clarity and brevity)

NSPipe* progressPipe = [NSPipe pipe];

progressPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle* handle )
{
// parse progress reports to update UI with certain info
};

// create the FFMPEG task:

NSString* execPath = [[[self class] ffMPEGURL] path];
NSArray* codecArgs = [[[self class] ffMPEGCodecForOption:self.format] componentsSeparatedByString:@" "];

if( codecArgs.count > 0 )
{
if( self.outputFileURL == nil )
[self requestDelegateCreatesFile];

NSAssert( self.outputFileURL, @"unable to proceed - no output file");

NSMutableArray* args = [NSMutableArray array];

[args addObjectsFromArray:@[@"-loglevel", @"error", @"-i", url.absoluteString]];
[args addObjectsFromArray:codecArgs];
[args addObjectsFromArray:@[@"-progress", @"/dev/stdout", self.outputFileURL.path]]; /<——————————————— set up -progress option

XVLOG(@"ffmpeg arguments: %@", args);

NSTask* theTask = [[NSTask alloc] init];

NSAssert( theTask, @"unable to create NSTask for ffmpeg subprocess");

theTask.launchPath = execPath;
theTask.arguments = args;
theTask.standardOutput = progressPipe;
theTask.qualityOfService = NSQualityOfServiceUserInitiated;

theTask.terminationHandler = ^(NSTask* task)
{
// termination handler
};

self.ffMPEGTask = theTask;

[theTask release];

if( self.ffMPEGTask )
{
[self.ffMPEGTask launch]; // GO!!!
self.status = XVMediaDownloaderRunning;
self.filesize = 0;
}
}



—Graham



On 9 Jul 2019, at 4:01 pm, Chris Hanson <cmhanson@...> wrote:

Sure, just use NSTask’s own standardInput, standardOutput, and standardError support with NSPipe. No need to tell the task to send its stdout to some intermediate file that your app then opens and reads from. (Whether it’s in /dev or anywhere else.)

-- Chris




Sandor Szatmari
 

Awesome!  

After several rereads I realize that Ffmpeg documentation does state this, but not in clear terms.

It states that any file/stream can be referenced as a pair, <file_no:stream_no>, hence pipe:1 == stdout

It talks about pipe almost within the same breath, but doesn’t explicitly connect the dots and say, to reference stdout/stdin/stderr you literally use the word pipe.

Reference section 2 (Description)

Sandor

On Jul 9, 2019, at 07:38, Graham Cox <graham@...> wrote:

Ah, that worked a treat!!! Thanks so much Sandor.

—Graham




On 9 Jul 2019, at 9:21 pm, Sandor Szatmari <admin.szatmari.net@...> wrote:

I think you would pass

-progress pipe:1 will write out to stdout, 

Or

pipe:2 to stderr.

Then just read from the pipe you associate.

I haven’t tried it myself, google told me to do it.

It might mess up your parsing if other messages are interspersed in the progress messages, but hopefully you can distinguish between the two types of information.  


Sandor