NSBundle pathForResource failing in shell tool - objective-c

I've noticed some weird behavior with NSBundle when using it in a
command-line program. If, in my program, I take an existing bundle and
make a copy of it and then try to use pathForResource to look up
something in the Resources folder, nil is always returned unless the
bundle I'm looking up existed before my program started. I created a
sample app that replicates the issue and the relevant code is:
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *exePath = [NSString stringWithCString:argv[0]
encoding:NSASCIIStringEncoding];
NSString *path = [exePath stringByDeletingLastPathComponent];
NSString *templatePath = [path stringByAppendingPathComponent:#"TestApp.app"];
// This call works because TestApp.app exists before this program is run
NSString *resourcePath = [NSBundle pathForResource:#"InfoPlist"
ofType:#"strings"
inDirectory:templatePath];
NSLog(#"NOCOPY: %#", resourcePath);
NSString *copyPath = [path stringByAppendingPathComponent:#"TestAppCopy.app"];
[[NSFileManager defaultManager] removeItemAtPath:copyPath
error:nil];
if ([[NSFileManager defaultManager] copyItemAtPath:templatePath
toPath:copyPath
error:nil])
{
// This call will fail if TestAppCopy.app does not exist before
// this program is run
NSString *resourcePath2 = [NSBundle pathForResource:#"InfoPlist"
ofType:#"strings"
inDirectory:copyPath];
NSLog(#"COPY: %#", resourcePath2);
[[NSFileManager defaultManager] removeItemAtPath:copyPath
error:nil];
}
[pool release];
}
For the purpose of this test app, let's assume that TestApp.app
already exists in the same directory as my test app. If I run this,
the 2nd NSLog call will output: COPY: (null)
Now, if I comment out the final removeItemAtPath call in the if
statement so that when my program exits TestAppCopy.app still exists
and then re-run, the program will work as expected.
I've tried this in a normal Cocoa application and I can't reproduce
the behavior. It only happens in a shell tool target.
Can anyone think of a reason why this is failing?
BTW: I'm trying this on 10.6.4 and I haven't tried on any other
versions of Mac OS X.

I can confirm that it is a bug in CoreFoundation, not Foundation. The bug is due to CFBundle code relying on a directory contents cache containing stale data. The code apparently assumes that neither the bundle directories nor their immediate parent directories will change during application runtime.
The CoreFoundation call corresponding to +[NSBundle pathForResource:ofType:inDirectory:] is CFBundleCopyResourceURLInDirectory(), and it exhibits the same misbehavior. (This is unsurprising, as -pathForResource:ofType:inDirectory: itself uses this call.)
The problem ultimately lies with _CFBundleCopyDirectoryContentsAtPath(). This is called during bundle loading and during all resource lookup. It caches information about the directories it looks up in contentsCache.
Here's the problem: When it comes time to get the contents of TestAppCopy.app, the cached contents of the directory containing TestApp.app don't include TestAppCopy.app. Because the cache ostensibly has the contents of that directory, only the cached contents are searched for TestAppCopy.app. When TestAppCopy.app is not found, the function takes that as a definitive "this path does not exist" and doesn't bother trying to open the directory:
__CFSpinLock(&CFBundleResourceGlobalDataLock);
if (contentsCache) dirDirContents = (CFArrayRef)CFDictionaryGetValue(contentsCache, dirName);
if (dirDirContents) {
Boolean foundIt = false;
CFIndex dirDirIdx, dirDirLength = CFArrayGetCount(dirDirContents);
for (dirDirIdx = 0; !foundIt && dirDirIdx < dirDirLength; dirDirIdx++) if (kCFCompareEqualTo == CFStringCompare(name, CFArrayGetValueAtIndex(dirDirContents, dirDirIdx), kCFCompareCaseInsensitive)) foundIt = true;
if (!foundIt) tryToOpen = false;
}
__CFSpinUnlock(&CFBundleResourceGlobalDataLock);
So, the contents array remains empty, gets cached for this path, and lookup continues. We now have cached the (incorrectly empty) contents of TestAppCopy.app, and as lookup drills down into this directory, we keep hitting bad cached information. Language lookup takes a stab when it finds nothing and hopes there's an en.lproj hanging around, but we still won't find anything, because we're looking in a stale cache.
CoreFoundation includes SPI functions to flush the CFBundle caches. The only place public API calls into them in CoreFoundation is __CFBundleDeallocate(). This flushes all cached information about the bundle's directory itself, but not its parent directory: _CFBundleFlushContentsCacheForPath(), which actually removes the data from the cache, removes only keys matching an anchored, case-insensitive search for the bundle path.
It would seem the only public way a client of CoreFoundation could flush bad information about TestApp.app's parent directory would be to make the parent directory a bundle directory (so TestApp.app lived alongside Contents), create a CFBundle for the parent bundle directory, then release that CFBundle. But, it seems that if you made the mistake of trying to work with the TestAppCopy.app bundle prior to flushing it, the bad data about TestAppCopy.app would not be flushed.

That sounds like a bug in the Foundation. The one key difference between a command line tool like that one and a Cocoa application is the run loop. Try refactoring the above into something like:
#interface Foo:NSObject
#end
#implementation Foo
- (void) doIt { .... your code from main() here .... }
#end
... main(...) {
Foo *f = [Foo new];
[f performSelector: #selector(doIt) withObject: nil afterDelay: 0.1 ...];
[[NSRunLoop currentRunLoop] run];
return 0; // not reached, I'd bet.
}
And see if that "fixes" it. It might. It might not (there are couple of other significant differences, obviously). In any case, do please file a bug via http://bugreport.apple.com/ and add the bug # as a comment.

Related

While loop with NSFileManager directory enumerator not running

I am seeking help to understand why a tutorial I am following is not working for me. I am running macOS 12.3.1, Xcode 13.3.1. The project is in Objective-C and using XIB.
This is a view-based NSTableView, using a folder of PNGs stored on my SSD for the imageView and the stringByDeletingPathExtension as stringValue for the cell's text field. I filled my code with NSLog calls to try and catch what could have been going awry.
Most setup is happening in applicationDidFinishLaunching:, where I initialise an NSMutableArray for the table's content, an NSString for the file path, then set up the file manager and the directory enumerator with said path (note: all working up to here).
Now comes the loop to populate the table contents' mutable array. I cannot understand why said loop gets skipped entirely! Its condition is to set an NSString equal to the nextObject of the directory enumerator. I am sure the loop gets skipped because the NSLog call after the loop runs!
Here is the entire code of applicationDidFinishLaunching:, including my comments and logs (I have just replaced my account name with ):
-(void)applicationDidFinishLaunching:(NSNotification *)aNotification {
_tableContents = [[NSMutableArray alloc] init];
NSString *path = #"/Users/<myUsername>/Developer/Apple-Programming-YT/Cocoa Programming/Flags/PNG/40x30";
// MARK: Debug 1
NSLog(#"path found: %#", path); // the correct path gets printed, as expected
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDirectoryEnumerator *directoryEnum = [fileManager enumeratorAtPath:path];
NSString *file;
// MARK: Debug 2
NSLog(#"Checking that file is empty: %#", file); // (null) gets printed, as expected
// MARK: Debug 3
if (file != directoryEnum.nextObject) {
NSLog(#"File cannot be assigned to the Directory Enumerator");
} else if (file == directoryEnum.nextObject) {
NSLog(#"File properly assigned. Proceed!"); // this gets printed! Is it correct?
} else {
NSLog(#"Something went wrong during assignment of nextObject to file");
}
while (file = [directoryEnum nextObject]) {
NSLog(#"While loop entered!"); // this doesn't get printed! Why?!
// MARK: Debug 4
NSLog(#"File: %#", file);
NSString *filePath = [path stringByAppendingFormat:#"/%#", file];
// MARK: Debug 5
NSLog(#"Image filepath: %#", filePath);
NSDictionary *obj = #{#"image": [[NSImage alloc] initByReferencingFile:filePath],
#"name": [file stringByDeletingPathExtension]};
[self.tableContents addObject:obj];
}
[self.tableView reloadData];
NSLog(#"Table View Reloaded"); // This gets printed!
}
I have uploaded the full app to GitHub, in case you may want to look at it and see if something else could be wrong, but every outlet, delegate, data source is connected.
Now for my diagnosis & ideas:
The Debug 3 mark is what I find most interesting. AFAIK file should still be (null), so how checking if it is equal to directoryEnum.nextObject returns YES?
I created Debug 3 because the NSLog checking whether the loop had been entered didn't get printed. I therefore assumed the condition for the while loop had a problem.
I then tried to create a do-while loop instead of this while loop and, of course, the code ran. For the log with "Image filepath" it returned the address above followed by (null), as if it didn't find the file. But how is it possible if the file is indeed there? Do I require some sort of permission to access it? Being the object empty, the next line in the console was quite clear: "attempt to insert nil object from objects[1]".
But now, how do I solve this?
Any help here is much appreciated. If you download it from GitHub, please replaces the *path string with a folder of PNGs on your SSD.
Thank you.
I don't think you can access the filesystem directly with a path like that any more. If you check the value of file in your code, it is nil, which means that file == directoryEnum.nextObject will evaluate to true.
You have to create a path starting with NSHomeDirectory() or similar and add components to it. This makes a path that goes via your application support folder, which contains an alias to the desktop. I'm not sure why that's OK and accessing it directly is not, but I'm not a Mac developer.
I'd have to say following a tutorial as old as that, you're going to struggle with a lot of things.

OS X faster file system API than repetitively calling [NSFileManager attributesOfItemAtPath...]?

Is there a faster file system API that I can use if I only need to know if a file is a folder/symlink and its size. I'm currently using [NSFileManager attributesOfItemAtPath...] and only NSFileSize and NSFileType.
Are there any bulk filesystem enumeration APIs I should be using? I suspect this could be faster without having to jump in and out of user code.
My goal is to quickly recurse through directories to get a folders true file size and currently calling attributesOfItemAtPath is my 95% bottleneck.
Some of the code I'm currently using:
NSDictionary* properties = [fileManager attributesOfItemAtPath:filePath error:&error];
long long fileSize = [[properties objectForKey:NSFileSize] longLongValue];
NSObject* fileType = [[properties objectForKey:NSFileType isEqual:NSFileTypeDirectory];
If you want to get really hairy, the Mac OS kernel implements a unique getdirentriesattr() system call which will return a list of files and attributes from a specified directory. It's messy to set up and call, and it's not supported on all filesystems (!), but if you can get it to work for you, it can speed things up significantly.
There's also a closely related searchfs() system call which can be used to rapidly search for files. It's subject to most of the same gotchas.
You can use stat and lstat. Take a look at this answer for calculating directory size.
CPU raises with attributesOfItemAtPath:error:
Whether it's faster or not I'm not certain, but NSURL will give you this information via getResourceValue:forKey:error:
NSError * e;
NSNumber * isDirectory;
BOOL success = [URLToFile getResourceValue:&isDirectory
forKey:NSURLIsDirectoryKey
error:&e];
if( !success ){
// error
}
NSNumber * fileSize;
BOOL success = [URLToFile getResourceValue:&fileSize
forKey:NSURLFileSizeKey
error:&e];
You might also find it convenient to wrap this up if you don't really care about the error:
#implementation NSURL (WSSSimpleResourceValueRetrieval)
- (id)WSSResourceValueForKey: (NSString *)key
{
id value = nil;
BOOL success = [self getResourceValue:&value
forKey:key
error:nil];
if( !success ){
value = nil;
}
return value;
}
#end
This is given as the substitute for the deprecated File Manager function FSGetCatalogInfo(), which is used in a solution in an old Cocoa-dev thread that Dave DeLong gives the thumbs up to.
For the enumeration part, the File System Programming Guide has a section "Getting the Contents of a Directory in a Single Batch Operation", which discusses using contentsOfDirectoryAtURL:includingPropertiesForKeys:options:error:

Memory warning after downloading and unzip files with ARC

Im downloading some gzipped xml files from a server, save it to the documents folder and unzip every file. After that I delete the .gz file. I do that in a loop for more or less 500 files. When im using instruments, I see that the live bytes more or less are 470MB after this process. When Im waiting some seconds, the ARC clears it and the application is going to 5mb live bytes. But because its a synchronization process my app gets a memory warning right after that when I dont stop after the downloading and unzipping. At least I think it should be possible to force the ARC to release the memory? Or do I have a real bad code and I am just still dont see that?
Any help or hint is really appreciated.
Downloading and unzipping:
for(NSString *filePath in filePaths){
NSString *localPath = [[DownloadManager sharedInstance] downloadFile:filePath];
if(localPath){
//downloaded correctly
if([self unzipFileAtPath:localPath]){
[FileUtility deleteFileAtPath:localPath];
}
}
}
Unzip method:
+ (BOOL)unzipFileAtPath:(NSString *)path
{
NSData *gzData = [NSData dataWithContentsOfFile:path];
NSData *ungzippedData = [gzData gunzippedData];
BOOL success = [ungzippedData writeToFile:[FormatUtility pathWithoutGz:path] atomically:NO];
ungzippedData = nil;
gzData = nil;
return success;
}
Wrap the inside of your for-loop with an autorelease pool:
for (NSString* filePath in filePaths) {
#autoreleasepool {
// do work
}
}
The problem actually has nothing to do with ARC. Methods like dataWithContentsOfFile: will return a new autoreleased object instance. These objects will not be released until the enclosing autorelease pool is drained, which by default only happens at the end of your thread/operation or when you return to the run-loop.
When you allocate many temporary objects in a loop, like you're doing, you should use your own autorelease pool to ensure that these temporary objects do not accumulate needlessly.
I am facing a similar problem when downloading a very large zip file (of 5GB!) using AFNetworking 2.6.4 and AFDownloadRequestOperation 2.0.1. I find the such memory issue is caused by the Flipboard FLEX 2.1.1 framework. After I commented out the line [[FLEXManager sharedManager] setNetworkDebuggingEnabled:YES]; it works perfectly fine.

Confused on creating new directories and changing directories (Objective-C)

When I changed the current directory path of main.m to newDir why does it still say that there are no files in newDir? Also I ran this program multiple times with no errors. Does that mean I ended up creating multiple newDir?
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[])
{
#autoreleasepool {
NSString *newDir = #"newDir";
NSFileManager *manager = [NSFileManager defaultManager];
if ([manager createDirectoryAtPath:newDir withIntermediateDirectories:YES attributes:nil error:NULL] == NO) {
NSLog(#"couldnt create new directory");
return 1;
}
if ([manager changeCurrentDirectoryPath: newDir] == NO) {
NSLog(#"couldnt change directory path");
return 2;
}
NSLog(#"%#", [manager currentDirectoryPath]);
NSLog(#"%#", [manager contentsOfDirectoryAtPath:newDir error:NULL]);
}
return 0;
}
Output:
2012-08-07 10:27:20.428 Test[853:707] /Users/ss/Library/Developer/Xcode/DerivedData/Test-bfrqtnrhaafmdzghoyirjnfqjbfc/Build/Products/Debug/newDir
2012-08-07 10:36:47.832 Test[885:707] (null)
The path to main.m does not play into what happens when you run your program: the only question is whether the directory has any files or not, and from the log it appears that it doesn't.
To create some files in the directory, run these commands in the terminal window:
touch /Users/ss/Library/Developer/Xcode/DerivedData/Test-bfrqtnrhaafmdzghoyirjnfqjbfc/Build/Products/Debug/newDir/quick.txt
touch /Users/ss/Library/Developer/Xcode/DerivedData/Test-bfrqtnrhaafmdzghoyirjnfqjbfc/Build/Products/Debug/newDir/brown.txt
touch /Users/ss/Library/Developer/Xcode/DerivedData/Test-bfrqtnrhaafmdzghoyirjnfqjbfc/Build/Products/Debug/newDir/fox.txt
This will create three empty files. Now run your program, and see if it discovers the newly created txt files; it should.
On your second question, the operating system would not let you create multiple file system objects with identical names, so the answer is no, you created only one newDir.
Since you're ignoring errors on your contentsOfDirectoryAtPath call, it's entirely possible that the call is failing -- hence the null.
Without looking at references, it appears that you're looking for directory .../newDir/newDir, a directory that likely does not exist.
In any event, since newDir is new it wouldn't contain any entries (other than . and ..).

Checking File sizes for changes

I have an app that watches a folder for incoming jobs and then processes them. A job consists of a Folder with several job files inside the folder. Jobs are usually copied over the internet so when a folder is added to my Watched Folder I'm having my app get the attributes for the files inside the job folder, wait 20 seconds, and compare the current attributes for NSFileSize to see if there have been any changes. When everything matches, and no changes are detected it can pass the job folder along to be processed. This is the code I have:
while (fileSizes == NO && fileCount == NO) {
NSLog(#"going for a loop");
NSArray *jobFiles = [fm subpathsAtPath:jobPath];
NSMutableArray *jobFileAttrs = [[NSMutableArray alloc] init];
int i = 0;
while (i < [jobFiles count]) {
NSString *filePath = [jobPath stringByAppendingPathComponent:[jobFiles objectAtIndex:i]];
[jobFileAttrs addObject:[fm attributesOfItemAtPath:filePath error:nil]];
++i;
}
sleep(20);
NSArray *jobFiles2 = [fm subpathsAtPath:jobPath];
NSMutableArray *jobFileAttrs2 = [[NSMutableArray alloc] init];
i = 0;
while (i < [jobFiles2 count]) {
NSString *filePath = [jobPath stringByAppendingPathComponent:[jobFiles2 objectAtIndex:i]];
[jobFileAttrs2 addObject:[fm attributesOfItemAtPath:filePath error:nil]];
++i;
}
if ([jobFiles count] == [jobFiles2 count]) {
i = 0;
fileSizes = YES;
while (i < [jobFiles count]) {
NSLog(#"Does %ul = %ul", [[jobFileAttrs objectAtIndex:i] objectForKey:NSFileSize], [[jobFileAttrs2 objectAtIndex:i] objectForKey:NSFileSize]);
if ([[jobFileAttrs objectAtIndex:i] objectForKey:NSFileSize] != [[jobFileAttrs2 objectAtIndex:i] objectForKey:NSFileSize]){
fileSizes = NO;
}
++i;
}
if (fileSizes)
fileCount = YES;
}
This code works as intended in Lion, but when I run the App on Snow Leopard I get inconsistent values for the NSFileSize attribute. Every time the loop runs I get a completely different set of value than even the previous loop. This is obviously for a folder full of files that is no longer being copied and should give matching values for the file sizes.
Why doesn't this work in Snow Leopard, and what do I need to do to fix this? Part of my problem is I'm only set up for development on a Lion Machine, so I have to make build, and then transfer it to a snow leopard machine with out a debugger to test. It's making it hard for me to trouble shoot.
There are a few problems with this but for one thing, you are comparing the value of two objects using a boolean operator (!=). This is comparing the pointer locations of the two objects, not their value.
To compare two objects, you must use the isEqual: method:
if (![[[jobFileAttrs objectAtIndex:i] objectForKey:NSFileSize] isEqual:[[jobFileAttrs2 objectAtIndex:i] objectForKey:NSFileSize]])
{
fileSizes = NO;
}
Secondly, this is fundamentally bad design. What your code is doing is called polling, and the presence of sleep() in the code is a bad sign. You should never use sleep in Cocoa code, especially if your code is executing on the main thread as it will block the main run loop.
If you absolutely must use polling, you should use an NSTimer object.
However, in this particular case you don't need to use polling to determine when there has been a change to the folder contents, you can use the FSEvents API instead. The API is C-based and a bit obtuse so you might want to use Stu Connolly's SCEvents Objective-C wrapper.
What you should be doing is maintaining an array of the current files and their file sizes (probably as an NSArray instance variable containing dictionaries with keys for file name and file attributes) and then when you are notified of a change on disk, get the current status of the files and compare that with the information in your stored array. You would then replace your stored array with the updated file information.
The other file monitoring option is kqueues. These differ from FSEvents in that they are specific to a particular file, whereas FSEvents monitors directories, not individual files. In your case, kqueues may actually be more appropriate. Uli Kusterer has written a great Objective-C wrapper for kqueues. If you use this, you just need to start monitoring a particular file and you'll be notified whenever it changes via a delegate. This would be a simpler option if you only need to check one file at a time.
I hope this makes sense.