Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix NSTask race condition #90

Merged
merged 2 commits into from
Feb 14, 2022
Merged

Conversation

arthuralee
Copy link
Contributor

@arthuralee arthuralee commented Feb 14, 2022

Fixes #88, fixes #89

According to https://stackoverflow.com/questions/49184623/nstask-race-condition-with-readabilityhandler-block, there seems to be a possible race condition that causes the NSTask termination handler to fire before the data is finished being read. Additionally, there seems to be some issues with reading from fileHandle.availableData directly.

This PR implements the two fixes suggested by the stackoverflow post, and subsequently fixes #88 and #89 from my personal testing (I was able to reliably repro the first issue at least)

Comment on lines 58 to 59
outPipe.fileHandleForReading.readabilityHandler = nil;
errPipe.fileHandleForReading.readabilityHandler = nil;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally had these in the blocks themselves, but moved it out to avoid retain cycles. But I just saw this comment in that stackoverflow page:

However, from testing, it looks like if you avoid -availableData and instead use -readDataOfLength in the handler block, you will eventually get a call to that block with zero-length data. If you don't nil the handler at that point, you'll get called with repeated zero-length data objects. That may be the intention, it's just not documented.

I wonder if it's worth moving these back into readabilityHandler blocks (I think we will need to create weak references to the pipes)

Copy link
Contributor

@asafkorem asafkorem Feb 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes more sense to me:

self.standardOutput = nil
self.standardError = nil

WDYT?

Then I expect the entire pipes to be released after exiting the method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense. based on the stackoverflow comment, I'm just thinking about what could go wrong if the readabilityHandler gets called multiple times with no data. Maybe I should create separate semaphores for stdout and stderr in case it gets dispatched twice?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes more sense to me:

self.standardOutput = nil
self.standardError = nil

WDYT?

Then I expect the entire pipes to be released after exiting the method.

This actually doesn't work. Apparently this method raises NSInvalidArgumentException once this has been set https://developer.apple.com/documentation/foundation/nstask/1407627-standardoutput?language=objc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😮

Copy link
Contributor

@asafkorem asafkorem Feb 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So yeah, let's put the ...readabilityHandler = nil; inside the blocks (using a weak reference of each pipe) as you suggested in the first place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I'm not sure if it really matters here..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially now that we have two separated semaphores 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with setting the handler to nil in the block, we can make sure there are no extra calls to the handler (which makes it more technically correct), but it would not really have any effect on the outcome and the code is slightly messier. i'll leave it up to you 😄

Copy link
Contributor

@asafkorem asafkorem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arthuralee thanks for this AMAZING contribution and the excellent investigation!

Comment on lines 50 to 61
[self launch];

dispatch_semaphore_wait(waitForTermination, DISPATCH_TIME_FOREVER);
// Wait twice, once for stdout and once for stderr
dispatch_semaphore_wait(waitForOutput, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(waitForOutput, DISPATCH_TIME_FOREVER);
// also wait for the NSTask to terminate, otherwise we can't read terminationStatus
dispatch_semaphore_wait(waitForTaskTermination, DISPATCH_TIME_FOREVER);

outPipe.fileHandleForReading.readabilityHandler = nil;
errPipe.fileHandleForReading.readabilityHandler = nil;

NSString* stdOutStr = [[[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excuse my OCD, please fix the indentations (new lines are not aligned) 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah due to my tab settings, they line up. will fix!

Comment on lines 58 to 59
outPipe.fileHandleForReading.readabilityHandler = nil;
errPipe.fileHandleForReading.readabilityHandler = nil;
Copy link
Contributor

@asafkorem asafkorem Feb 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes more sense to me:

self.standardOutput = nil
self.standardError = nil

WDYT?

Then I expect the entire pipes to be released after exiting the method.

Copy link
Contributor

@asafkorem asafkorem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @arthuralee, good game! 😄

@asafkorem asafkorem merged commit eaa58fd into wix:master Feb 14, 2022
@arthuralee arthuralee deleted the fix/nstask-issue branch February 14, 2022 16:21
@arthuralee
Copy link
Contributor Author

Thanks for the quick responses! I think we might be able to close quite a few issues in this repo

@arthuralee
Copy link
Contributor Author

Any chance we can get a new version release some time soon? 🙂

@asafkorem
Copy link
Contributor

Of course, I’ll release a new version in a few hours (tomorrow in my timezone:smile:).

@asafkorem
Copy link
Contributor

@arthuralee AppleSimulatorUtils v0.9.5 released (https://github.com/wix/AppleSimulatorUtils/releases/tag/0.9.5).
Thanks again for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants