restructure, clean up and try to do a better job tracking down locations

This commit is contained in:
Casey Fleser 2014-11-03 05:48:59 -06:00
parent 99fc6abf0b
commit 3e8236883f
12 changed files with 809 additions and 296 deletions

View file

@ -12,6 +12,9 @@
C9194D3019C09A03004178EB /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9194D2F19C09A03004178EB /* Images.xcassets */; };
C9194D3319C09A03004178EB /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = C9194D3119C09A03004178EB /* MainMenu.xib */; };
C9194D3F19C09A03004178EB /* SimDirsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C9194D3E19C09A03004178EB /* SimDirsTests.m */; };
C9B7078A1A03A5C70001CB77 /* QSSimAppInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = C9B707891A03A5C70001CB77 /* QSSimAppInfo.m */; };
C9B707901A03A63E0001CB77 /* QSSimDeviceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = C9B7078F1A03A63E0001CB77 /* QSSimDeviceInfo.m */; };
C9B707931A03B6C30001CB77 /* QSSimViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C9B707921A03B6C30001CB77 /* QSSimViewController.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -35,6 +38,12 @@
C9194D3819C09A03004178EB /* SimDirsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimDirsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C9194D3D19C09A03004178EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C9194D3E19C09A03004178EB /* SimDirsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SimDirsTests.m; sourceTree = "<group>"; };
C9B707881A03A5C70001CB77 /* QSSimAppInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QSSimAppInfo.h; sourceTree = "<group>"; };
C9B707891A03A5C70001CB77 /* QSSimAppInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QSSimAppInfo.m; sourceTree = "<group>"; };
C9B7078E1A03A63E0001CB77 /* QSSimDeviceInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QSSimDeviceInfo.h; sourceTree = "<group>"; };
C9B7078F1A03A63E0001CB77 /* QSSimDeviceInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QSSimDeviceInfo.m; sourceTree = "<group>"; };
C9B707911A03B6C30001CB77 /* QSSimViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QSSimViewController.h; sourceTree = "<group>"; };
C9B707921A03B6C30001CB77 /* QSSimViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QSSimViewController.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -78,6 +87,12 @@
children = (
C9194D2C19C09A03004178EB /* AppDelegate.h */,
C9194D2D19C09A03004178EB /* AppDelegate.m */,
C9B707881A03A5C70001CB77 /* QSSimAppInfo.h */,
C9B707891A03A5C70001CB77 /* QSSimAppInfo.m */,
C9B7078E1A03A63E0001CB77 /* QSSimDeviceInfo.h */,
C9B7078F1A03A63E0001CB77 /* QSSimDeviceInfo.m */,
C9B707911A03B6C30001CB77 /* QSSimViewController.h */,
C9B707921A03B6C30001CB77 /* QSSimViewController.m */,
C9194D2F19C09A03004178EB /* Images.xcassets */,
C9194D3119C09A03004178EB /* MainMenu.xib */,
C9194D2819C09A03004178EB /* Supporting Files */,
@ -211,6 +226,9 @@
buildActionMask = 2147483647;
files = (
C9194D2E19C09A03004178EB /* AppDelegate.m in Sources */,
C9B707901A03A63E0001CB77 /* QSSimDeviceInfo.m in Sources */,
C9B7078A1A03A5C70001CB77 /* QSSimAppInfo.m in Sources */,
C9B707931A03B6C30001CB77 /* QSSimViewController.m in Sources */,
C9194D2B19C09A03004178EB /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -398,6 +416,7 @@
C9194D4419C09A03004178EB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C9194D4519C09A03004178EB /* Build configuration list for PBXNativeTarget "SimDirsTests" */ = {
isa = XCConfigurationList;
@ -406,6 +425,7 @@
C9194D4719C09A03004178EB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};

View file

@ -7,12 +7,12 @@
//
#import "AppDelegate.h"
#import "QSSimDeviceInfo.h"
#import "QSSimViewController.h"
@interface AppDelegate ()
@property (nonatomic, weak) IBOutlet NSWindow *window;
@property (nonatomic, weak) IBOutlet NSOutlineView *locationOutline;
@property (nonatomic, strong) NSMutableArray *simLocations;
@end
@ -20,274 +20,11 @@
- (void) applicationDidFinishLaunching: (NSNotification *) inNotification
{
[self.locationOutline setDoubleAction: @selector(handleRowSelect:)];
[self updateLocations];
}
- (void) applicationWillTerminate: (NSNotification *) inNotification
-(BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) inSender
{
}
-(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
return YES;
}
- (void) updateLocations
{
[self discoverSimLocations];
[self.locationOutline reloadData];
}
- (void) discoverSimLocations
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *libraryDirURL = [[fileManager URLsForDirectory: NSLibraryDirectory inDomains: NSUserDomainMask] firstObject];
self.simLocations = [NSMutableArray array];
if (libraryDirURL != nil) {
libraryDirURL = [libraryDirURL URLByAppendingPathComponent: @"Developer/CoreSimulator/Devices"];
if (libraryDirURL != nil) {
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtURL: libraryDirURL includingPropertiesForKeys: nil options: NSDirectoryEnumerationSkipsSubdirectoryDescendants | NSDirectoryEnumerationSkipsHiddenFiles errorHandler: nil];
NSURL *baseInfoURL;
while ((baseInfoURL = [dirEnum nextObject])) {
NSURL *deviceInfoURL = [baseInfoURL URLByAppendingPathComponent: @"device.plist"];
if (deviceInfoURL != nil && [fileManager fileExistsAtPath: [deviceInfoURL path]]) {
NSData *plistData = [NSData dataWithContentsOfURL: deviceInfoURL];
if (plistData != nil) {
NSDictionary *plistInfo;
plistInfo = [NSPropertyListSerialization propertyListWithData: plistData options: NSPropertyListImmutable format: nil error: nil];
if (plistInfo != nil) {
NSMutableDictionary *deviceInfo = [NSMutableDictionary dictionary];
NSString *name = plistInfo[@"name"];
NSString *runtime = plistInfo[@"runtime"];
NSRange runtimeRange;
runtimeRange = [runtime rangeOfString: @"iOS.*" options: NSRegularExpressionSearch];
if (runtimeRange.location != NSNotFound) {
NSArray *versionComponents = [[runtime substringWithRange: runtimeRange] componentsSeparatedByString: @"-"];
deviceInfo[@"path"] = [baseInfoURL path];
deviceInfo[@"title"] = [NSString stringWithFormat: @"%@: %@ %@.%@", name, versionComponents[0], versionComponents[1], versionComponents[2]];
[self updateDeviceInfoForApps: deviceInfo];
[self updateDeviceInfoForAppsFromLogs: deviceInfo];
[self.simLocations addObject: deviceInfo];
}
}
}
}
}
}
}
}
- (void) addPath: (NSString *) inPath
withTitle: (NSString *) inTitle
withBundleInfo: (NSMutableDictionary *) inBundleInfo
{
NSMutableArray *pathList = inBundleInfo[@"children"];
NSMutableDictionary *pathInfo;
NSInteger pathIndex;
if (pathList == nil) {
pathList = [NSMutableArray array];
inBundleInfo[@"children"] = pathList;
}
pathIndex = [pathList indexOfObjectPassingTest: ^BOOL(id inObject, NSUInteger inIndex, BOOL *outStop) {
*outStop = [inObject[@"path"] isEqualToString: inPath];
return *outStop;
}];
if (pathIndex == NSNotFound) {
pathInfo = [NSMutableDictionary dictionary];
pathInfo[@"title"] = inTitle;
pathInfo[@"path"] = inPath;
[pathList addObject: pathInfo];
}
// we already have this path
}
- (void) addPath: (NSString *) inPath
withTitle: (NSString *) inTitle
forBundleID: (NSString *) inBundleID
withDeviceInfo: (NSMutableDictionary *) inDeviceInfo
{
NSMutableArray *bundleList = inDeviceInfo[@"children"];
NSMutableDictionary *bundleInfo;
NSInteger bundleIndex;
if (bundleList == nil) {
bundleList = [NSMutableArray array];
inDeviceInfo[@"children"] = bundleList;
}
bundleIndex = [bundleList indexOfObjectPassingTest: ^BOOL(id inObject, NSUInteger inIndex, BOOL *outStop) {
*outStop = [inObject[@"title"] isEqualToString: inBundleID];
return *outStop;
}];
if (bundleIndex == NSNotFound) {
bundleInfo = [NSMutableDictionary dictionary];
bundleInfo[@"title"] = inBundleID;
[bundleList addObject: bundleInfo];
}
else {
bundleInfo = [bundleList objectAtIndex: bundleIndex];
}
[self addPath: inPath withTitle: inTitle withBundleInfo: bundleInfo];
}
- (void) updateDeviceInfoForApps: (NSMutableDictionary *) inDeviceInfo
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *launchMapInfoURL = [[NSURL alloc] initFileURLWithPath: inDeviceInfo[@"path"]];
launchMapInfoURL = [launchMapInfoURL URLByAppendingPathComponent: @"data/Library/MobileInstallation/LastLaunchServicesMap.plist"];
if (launchMapInfoURL != nil && [fileManager fileExistsAtPath: [launchMapInfoURL path]]) {
NSData *plistData = [NSData dataWithContentsOfURL: launchMapInfoURL];
NSDictionary *launchInfo;
NSDictionary *userInfo;
launchInfo = [NSPropertyListSerialization propertyListWithData: plistData options: NSPropertyListImmutable format: nil error: nil];
userInfo = launchInfo[@"User"];
for (NSString *bundleID in userInfo) {
NSDictionary *appInfo = userInfo[bundleID];
if (appInfo != nil) {
NSArray *pathKeys = @[
@{ @"title" : @"Bundle Location", @"pathKey" : @"BundleContainer" },
@{ @"title" : @"Sandbox Location", @"pathKey" : @"Container" } ];
[pathKeys enumerateObjectsUsingBlock: ^(id inObject, NSUInteger inIndex, BOOL *outStop) {
NSString *appPath = appInfo[inObject[@"pathKey"]];
//NSLog(@"test %@ - %@", appPath, [fileManager fileExistsAtPath: appPath] ? @"YES" : @"NO");
if (appPath != nil && [fileManager fileExistsAtPath: appPath]) {
[self addPath: appPath withTitle: inObject[@"title"] forBundleID: bundleID withDeviceInfo: inDeviceInfo];
}
}];
}
}
}
}
- (void) updateDeviceInfoForAppsFromLogs: (NSMutableDictionary *) inDeviceInfo
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *installLogURL = [[NSURL alloc] initFileURLWithPath: inDeviceInfo[@"path"]];
installLogURL = [installLogURL URLByAppendingPathComponent: @"data/Library/Logs/MobileInstallation/mobile_installation.log.0"];
if (installLogURL != nil && [fileManager fileExistsAtPath: [installLogURL path]]) {
NSString *installLog = [[NSString alloc] initWithContentsOfURL: installLogURL usedEncoding: nil error: nil];
if (installLog != nil) {
NSRange logMentionRange;
for (NSString *line in [installLog componentsSeparatedByCharactersInSet: [NSCharacterSet newlineCharacterSet]]) {
logMentionRange = [line rangeOfString: @"Made container live for "];
if (logMentionRange.location != NSNotFound) {
NSArray *installParts = [[line substringFromIndex: logMentionRange.location + logMentionRange.length] componentsSeparatedByString: @" "];
if ([installParts count] == 3) { // expecting com.foo.bar at UUID
NSString *bundleID = [installParts objectAtIndex: 0];
if ([bundleID rangeOfString: @"com.apple"].location == NSNotFound) {
NSString *path = [installParts objectAtIndex: 2];
if (path != nil && [fileManager fileExistsAtPath: path]) {
NSString *pathTitle;
if ([path rangeOfString: @"Data/Application"].location != NSNotFound) {
pathTitle = @"Sandbox Location";
}
else if ([path rangeOfString: @"Bundle/Application"].location != NSNotFound) {
pathTitle = @"Bundle Location";
}
else {
pathTitle = @"???";
}
[self addPath: path withTitle: pathTitle forBundleID: bundleID withDeviceInfo: inDeviceInfo];
}
}
}
}
}
}
}
}
#pragma mark - Handlers
- (IBAction) handleRowSelect: (id) inSender
{
if (inSender == self.locationOutline) {
id item = [self.locationOutline itemAtRow: [self.locationOutline clickedRow]];
if (item != nil) {
if (item[@"path"] != nil) {
NSURL *itemPathURL = [[NSURL alloc] initFileURLWithPath: item[@"path"]];
if (itemPathURL != nil) {
[[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: @[ itemPathURL ]];
}
}
else {
if ([self.locationOutline isItemExpanded: item]) {
[self.locationOutline collapseItem: item];
}
else {
[self.locationOutline expandItem: item];
}
}
}
}
}
- (IBAction) handleUpdate: (id) inSender
{
[self updateLocations];
}
#pragma mark - NSOutlineViewDataSource
- (NSInteger) outlineView: (NSOutlineView *) inOutlineView
numberOfChildrenOfItem: (id) inItem
{
NSArray *target = inItem == nil ? self.simLocations : inItem[@"children"];
return [target count];
}
- (id) outlineView: (NSOutlineView *) inOutlineView
child: (NSInteger) inIndex
ofItem: (id) inItem
{
NSArray *target = inItem == nil ? self.simLocations : inItem[@"children"];
return [target objectAtIndex: inIndex];
}
- (BOOL) outlineView: (NSOutlineView *) inOutlineView
isItemExpandable: (id) inItem
{
NSArray *target = inItem == nil ? self.simLocations : inItem[@"children"];
return [target count] ? YES : NO;
}
- (id) outlineView: (NSOutlineView *) inOutlineView
objectValueForTableColumn: (NSTableColumn *) inTableColumn
byItem: (id) inItem
{
return [inItem objectForKey: @"title"];
}
@end

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6245" systemVersion="13E28" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6250" systemVersion="13F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6245"/>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6250"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -13,11 +14,14 @@
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate">
<connections>
<outlet property="locationOutline" destination="hie-77-RjD" id="vaX-Cy-hrm"/>
<outlet property="window" destination="nYB-8M-ZYr" id="B0K-mf-fOD"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="SyX-BE-Buh" userLabel="Main Controller" customClass="QSSimViewController">
<connections>
<outlet property="locationOutline" destination="hie-77-RjD" id="xmZ-A6-jSF"/>
</connections>
</customObject>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="SimDirs" id="1Xt-HY-uBw">
@ -103,21 +107,31 @@
</menu>
<window title="SimDirs" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" frameAutosaveName="main" animationBehavior="default" id="nYB-8M-ZYr">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<windowPositionMask key="initialPositionMask" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="100" y="1000" width="192" height="320"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1418"/>
<view key="contentView" id="5rQ-mH-cgE">
<rect key="frame" x="0.0" y="0.0" width="192" height="320"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eiG-1E-Kei">
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="iz9-Ye-hd1">
<rect key="frame" x="54" y="13" width="85" height="32"/>
<buttonCell key="cell" type="push" title="Update" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="QEX-ht-ahg">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="handleUpdate:" target="SyX-BE-Buh" id="yt1-0o-8m2"/>
</connections>
</button>
<scrollView autohidesScrollers="YES" horizontalLineScroll="20" horizontalPageScroll="10" verticalLineScroll="20" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eiG-1E-Kei">
<rect key="frame" x="0.0" y="61" width="192" height="259"/>
<clipView key="contentView" id="Tuz-5T-KKT">
<rect key="frame" x="1" y="17" width="238" height="117"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" indentationPerLevel="16" outlineTableColumn="dVq-2Q-Cyd" id="hie-77-RjD">
<rect key="frame" x="0.0" y="0.0" width="238" height="117"/>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" rowHeight="18" indentationPerLevel="16" outlineTableColumn="dVq-2Q-Cyd" id="hie-77-RjD">
<rect key="frame" x="0.0" y="0.0" width="190" height="19"/>
<autoresizingMask key="autoresizingMask"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
@ -129,7 +143,7 @@
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="3Wd-eV-7Zx">
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" allowsUndo="NO" alignment="left" title="Text Cell" id="3Wd-eV-7Zx">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -138,7 +152,7 @@
</tableColumn>
</tableColumns>
<connections>
<outlet property="dataSource" destination="Voe-Tx-rLC" id="Gmi-Hc-w3X"/>
<outlet property="dataSource" destination="SyX-BE-Buh" id="PQC-0Q-20T"/>
</connections>
</outlineView>
</subviews>
@ -153,16 +167,6 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="iz9-Ye-hd1">
<rect key="frame" x="54" y="13" width="85" height="32"/>
<buttonCell key="cell" type="push" title="Update" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="QEX-ht-ahg">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="handleUpdate:" target="Voe-Tx-rLC" id="OGg-PQ-qdt"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="eiG-1E-Kei" firstAttribute="leading" secondItem="5rQ-mH-cgE" secondAttribute="leading" id="2Sc-05-Xvd"/>
@ -173,15 +177,7 @@
<constraint firstAttribute="bottom" secondItem="iz9-Ye-hd1" secondAttribute="bottom" constant="20" id="lw2-pT-lqw"/>
</constraints>
</view>
<point key="canvasLocation" x="145" y="261"/>
<point key="canvasLocation" x="150" y="313"/>
</window>
<button verticalHuggingPriority="750" id="0gu-fg-9qr">
<rect key="frame" x="0.0" y="0.0" width="82" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Button" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ucj-7g-fYh">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
</objects>
</document>

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>0.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>

13
SimDirs/QSDeviceInfo.h Normal file
View file

@ -0,0 +1,13 @@
//
// QSDeviceInfo.h
// SimDirs
//
// Created by Casey Fleser on 10/31/14.
// Copyright (c) 2014 Quiet Spark. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface QSDeviceInfo : NSObject
@end

13
SimDirs/QSDeviceInfo.m Normal file
View file

@ -0,0 +1,13 @@
//
// QSDeviceInfo.m
// SimDirs
//
// Created by Casey Fleser on 10/31/14.
// Copyright (c) 2014 Quiet Spark. All rights reserved.
//
#import "QSDeviceInfo.h"
@implementation QSDeviceInfo
@end

31
SimDirs/QSSimAppInfo.h Normal file
View file

@ -0,0 +1,31 @@
//
// QSSimAppInfo.h
// SimDirs
//
// Created by Casey Fleser on 10/31/14.
// Copyright (c) 2014 Quiet Spark. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "QSSimViewController.h"
@interface QSSimAppInfo : NSObject <QSOutlineProvider>
- (id) initWithBundleID: (NSString *) inBundleID;
- (void) updateFromLastLaunchMapInfo: (NSDictionary *) inMapInfo;
- (void) updateFromAppStateInfo: (NSDictionary *) inStateInfo;
- (void) refinePaths;
@property (nonatomic, strong) NSString *bundleID;
@property (nonatomic, strong) NSString *appName;
@property (nonatomic, strong) NSString *appShortVersion;
@property (nonatomic, strong) NSString *appVersion;
@property (nonatomic, strong) NSString *bundlePath;
@property (nonatomic, strong) NSString *sandBoxPath;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) BOOL hasValidPaths;
@end

224
SimDirs/QSSimAppInfo.m Normal file
View file

@ -0,0 +1,224 @@
//
// QSSimAppInfo.m
// SimDirs
//
// Created by Casey Fleser on 10/31/14.
// Copyright (c) 2014 Quiet Spark. All rights reserved.
//
#import "QSSimAppInfo.h"
@interface QSSimAppInfo ()
@property (nonatomic, strong) NSArray *childItems;
@end
@implementation QSSimAppInfo
- (id) initWithBundleID: (NSString *) inBundleID
{
if ((self = [super init]) != nil) {
self.bundleID = inBundleID;
}
return self;
}
- (NSString *) description
{
// return [NSString stringWithFormat: @"%@: bundle %@ sandbox %@", self.bundleID, [self.bundlePath lastPathComponent], [self.sandBoxPath lastPathComponent]];
return self.bundleID;
}
- (BOOL) testPath: (NSString *) inPath
{
return inPath != nil && [[NSFileManager defaultManager] fileExistsAtPath: inPath] ? YES : NO;
}
- (void) updateFromLastLaunchMapInfo: (NSDictionary *) inMapInfo
{
NSString *path;
path = inMapInfo[@"BundleContainer"];
if (self.bundlePath == nil && [self testPath: path]) {
self.bundlePath = path;
}
path = inMapInfo[@"Container"];
if (self.sandBoxPath == nil && [self testPath: path]) {
self.sandBoxPath = path;
}
}
- (void) updateFromAppStateInfo: (NSDictionary *) inStateInfo
{
NSDictionary *compatInfo = inStateInfo[@"compatibilityInfo"];
if (compatInfo != nil) {
NSString *path;
path = compatInfo[@"bundlePath"];
if (self.bundlePath == nil && [self testPath: path]) {
self.bundlePath = path;
}
path = compatInfo[@"sandboxPath"];
if (self.sandBoxPath == nil && [self testPath: path]) {
self.sandBoxPath = path;
}
}
}
- (void) refinePaths
{
NSURL *infoURL;
if (self.bundlePath != nil && [[self.bundlePath lastPathComponent] rangeOfString: @".app"].location == NSNotFound) {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *bundleURL = [[NSURL alloc] initFileURLWithPath: self.bundlePath];
NSURL *appURL;
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtURL: bundleURL includingPropertiesForKeys: nil
options: NSDirectoryEnumerationSkipsSubdirectoryDescendants | NSDirectoryEnumerationSkipsHiddenFiles errorHandler: nil];
while ((appURL = [dirEnum nextObject])) {
NSString *appPath = [appURL path];
if ([[appPath lastPathComponent] rangeOfString: @".app"].location != NSNotFound) {
self.bundlePath = appPath;
break;
}
}
}
infoURL = [[NSURL alloc] initFileURLWithPath: self.bundlePath];
infoURL = [infoURL URLByAppendingPathComponent: @"Info.plist"];
if (infoURL != nil && [[NSFileManager defaultManager] fileExistsAtPath: [infoURL path]]) {
NSData *plistData = [NSData dataWithContentsOfURL: infoURL];
if (plistData != nil) {
NSDictionary *plistInfo;
plistInfo = [NSPropertyListSerialization propertyListWithData: plistData options: NSPropertyListImmutable format: nil error: nil];
if (plistInfo != nil) {
self.appName = plistInfo[(__bridge NSString *)kCFBundleNameKey];
self.appShortVersion = plistInfo[@"CFBundleShortVersionString"];
self.appVersion = plistInfo[(__bridge NSString *)kCFBundleVersionKey];
}
}
}
}
#pragma mark - QSOutlineProvider
- (NSInteger) outlineChildCount
{
return [self.childItems count];
}
- (id) outlineChildAtIndex: (NSInteger) inIndex
{
NSDictionary *pathInfo = [self.childItems objectAtIndex: inIndex];
return pathInfo[@"title"];
}
- (BOOL) outlineItemIsExpanable
{
return [self outlineChildCount] ? YES : NO;
}
- (NSString *) outlineItemValueForColumn: (NSTableColumn *) inTableColumn
{
return [inTableColumn.identifier isEqualToString: @"title"] ? self.title : nil;
}
- (BOOL) outlineItemPerformAction
{
return NO;
}
- (BOOL) outlineItemPerformActionForChild: (id) inChild
{
NSInteger pathIndex;
BOOL handled = NO;
pathIndex = [self.childItems indexOfObjectPassingTest: ^(id inObject, NSUInteger inIndex, BOOL *outStop) {
NSDictionary *pathInfo = inObject;
*outStop = [pathInfo[@"title"] isEqualToString: inChild];
return *outStop;
}];
if (pathIndex != NSNotFound) {
NSDictionary *pathInfo = [self.childItems objectAtIndex: pathIndex];
NSURL *itemPathURL = [[NSURL alloc] initFileURLWithPath: pathInfo[@"path"]];
if (itemPathURL != nil) {
[[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: @[ itemPathURL ]];
}
handled = YES;
}
return handled;
}
#pragma mark - Setters / Getters
- (NSArray *) childItems
{
// any more than these two items and perhaps a dedicated class would be better
if (_childItems == nil) {
NSMutableArray *childItems = [NSMutableArray array];
if (self.bundlePath != nil) {
[childItems addObject: @{ @"title" : @"Bundle Location", @"path" : self.bundlePath }];
}
if (self.sandBoxPath != nil) {
[childItems addObject: @{ @"title" : @"Sandbox Location", @"path" : self.sandBoxPath }];
}
_childItems = childItems;
}
return _childItems;
}
- (void) setBundlePath: (NSString *) inBundlePath
{
_bundlePath = inBundlePath;
_childItems = nil;
}
- (void) setSandBoxPath: (NSString *) inSandBoxPath
{
_sandBoxPath = inSandBoxPath;
_childItems = nil;
}
- (NSString *) title
{
NSString *title;
if (self.appName != nil) {
title = [NSString stringWithFormat: @"%@ v%@", self.appName, self.appShortVersion];
if (![self.appShortVersion isEqualToString: self.appVersion]) {
title = [title stringByAppendingString: [NSString stringWithFormat: @" (%@)", self.appVersion]];
}
title = [title stringByAppendingString: [NSString stringWithFormat: @" - %@", self.bundleID]];
}
else {
title = self.bundleID;
}
return title;
}
- (BOOL) hasValidPaths
{
return [self.childItems count] ? YES : NO;
}
@end

22
SimDirs/QSSimDeviceInfo.h Normal file
View file

@ -0,0 +1,22 @@
//
// QSSimDeviceInfo.h
// SimDirs
//
// Created by Casey Fleser on 10/31/14.
// Copyright (c) 2014 Quiet Spark. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "QSSimViewController.h"
@interface QSSimDeviceInfo : NSObject <QSOutlineProvider>
+ (NSArray *) gatherDeviceLocations;
@property (nonatomic, strong) NSURL *baseURL;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *model;
@property (nonatomic, strong) NSString *version;
@property (nonatomic, readonly) NSString *title;
@end

288
SimDirs/QSSimDeviceInfo.m Normal file
View file

@ -0,0 +1,288 @@
//
// QSSimDeviceInfo.m
// SimDirs
//
// Created by Casey Fleser on 10/31/14.
// Copyright (c) 2014 Quiet Spark. All rights reserved.
//
#import "QSSimDeviceInfo.h"
#import "QSSimAppInfo.h"
@interface QSSimDeviceInfo ()
@property (nonatomic, strong) NSMutableArray *appList;
@end
@implementation QSSimDeviceInfo
+ (NSArray *) gatherDeviceLocations
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *libraryDirURL = [[fileManager URLsForDirectory: NSLibraryDirectory inDomains: NSUserDomainMask] firstObject];
NSMutableArray *deviceList = [NSMutableArray array];
if (libraryDirURL != nil) {
libraryDirURL = [libraryDirURL URLByAppendingPathComponent: @"Developer/CoreSimulator/Devices"];
if (libraryDirURL != nil) {
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtURL: libraryDirURL includingPropertiesForKeys: nil
options: NSDirectoryEnumerationSkipsSubdirectoryDescendants | NSDirectoryEnumerationSkipsHiddenFiles errorHandler: nil];
NSURL *baseInfoURL;
QSSimDeviceInfo *deviceInfo;
while ((baseInfoURL = [dirEnum nextObject])) {
deviceInfo = [[QSSimDeviceInfo alloc] initWithURL: baseInfoURL];
if (deviceInfo != nil) {
[deviceList addObject: deviceInfo];
}
}
}
}
return deviceList;
}
- (id) initWithURL: (NSURL *) inDeviceURL
{
if ((self = [super init]) != nil) {
NSURL *deviceInfoURL = [inDeviceURL URLByAppendingPathComponent: @"device.plist"];
BOOL initOK = NO;
self.baseURL = inDeviceURL;
if (deviceInfoURL != nil && [[NSFileManager defaultManager] fileExistsAtPath: [deviceInfoURL path]]) {
NSData *plistData = [NSData dataWithContentsOfURL: deviceInfoURL];
if (plistData != nil) {
NSDictionary *plistInfo;
plistInfo = [NSPropertyListSerialization propertyListWithData: plistData options: NSPropertyListImmutable format: nil error: nil];
if (plistInfo != nil) {
NSString *runtime = plistInfo[@"runtime"];
NSRange runtimeRange;
runtimeRange = [runtime rangeOfString: @"iOS.*" options: NSRegularExpressionSearch];
if (runtimeRange.location != NSNotFound) {
NSArray *versionComponents = [[runtime substringWithRange: runtimeRange] componentsSeparatedByString: @"-"];
self.name = plistInfo[@"name"];
self.model = versionComponents[0];
self.version = [NSString stringWithFormat: @"%@.%@", versionComponents[1], versionComponents[2]];
self.appList = [NSMutableArray array];
[self gatherAppInfoFromLastLaunchMap];
[self gatherAppInfoFromAppState];
[self cleanupAndRefineAppList];
initOK = YES;
}
}
}
}
if (!initOK) {
self = nil;
}
}
return self;
}
- (NSString *) description
{
NSMutableArray *childDescriptions = [NSMutableArray array];
for (QSSimAppInfo *appInfo in self.appList) {
[childDescriptions addObject: [appInfo description]];
}
return [NSString stringWithFormat: @"%@: %@ %@ %@", self.name, self.model, self.version, childDescriptions];
}
// LastLaunchServicesMap.plist seems to be the most reliable location to gather app info
- (void) gatherAppInfoFromLastLaunchMap
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *launchMapInfoURL = [self.baseURL URLByAppendingPathComponent: @"data/Library/MobileInstallation/LastLaunchServicesMap.plist"];
if (launchMapInfoURL != nil && [fileManager fileExistsAtPath: [launchMapInfoURL path]]) {
NSData *plistData = [NSData dataWithContentsOfURL: launchMapInfoURL];
NSDictionary *launchInfo;
NSDictionary *userInfo;
launchInfo = [NSPropertyListSerialization propertyListWithData: plistData options: NSPropertyListImmutable format: nil error: nil];
userInfo = launchInfo[@"User"];
for (NSString *bundleID in userInfo) {
QSSimAppInfo *appInfo = [self appInfoWithBundleID: bundleID];
if (appInfo != nil) {
[appInfo updateFromLastLaunchMapInfo: userInfo[bundleID]];
}
}
}
}
// applicationState.plist sometimes has info that LastLaunchServicesMap.plist doesn't
- (void) gatherAppInfoFromAppState
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *apStateInfoURL = [self.baseURL URLByAppendingPathComponent: @"data/Library/BackBoard/applicationState.plist"];
if (apStateInfoURL != nil && [fileManager fileExistsAtPath: [apStateInfoURL path]]) {
NSData *plistData = [NSData dataWithContentsOfURL: apStateInfoURL];
NSDictionary *stateInfo;
stateInfo = [NSPropertyListSerialization propertyListWithData: plistData options: NSPropertyListImmutable format: nil error: nil];
for (NSString *bundleID in stateInfo) {
if ([bundleID rangeOfString: @"com.apple"].location == NSNotFound) {
QSSimAppInfo *appInfo = [self appInfoWithBundleID: bundleID];
if (appInfo != nil) {
[appInfo updateFromAppStateInfo: stateInfo[bundleID]];
}
}
}
}
}
- (void) cleanupAndRefineAppList
{
NSMutableArray *mysteryApps = [NSMutableArray array];
for (QSSimAppInfo *appInfo in self.appList) {
if (!appInfo.hasValidPaths) {
[mysteryApps addObject: appInfo];
}
}
[self.appList removeObjectsInArray: mysteryApps];
[self.appList sortUsingDescriptors: @[ [NSSortDescriptor sortDescriptorWithKey: @"title" ascending:YES ]]];
for (QSSimAppInfo *appInfo in self.appList) {
[appInfo refinePaths];
}
}
// Used to also scan installation logs for clues but uncertain how useful this is.
// Leaving it here in case I decide to put it back
#if 0
- (void) updateDeviceInfoForAppsFromLogs: (NSMutableDictionary *) inDeviceInfo
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *installLogURL = [[NSURL alloc] initFileURLWithPath: inDeviceInfo[@"path"]];
installLogURL = [installLogURL URLByAppendingPathComponent: @"data/Library/Logs/MobileInstallation/mobile_installation.log.0"];
if (installLogURL != nil && [fileManager fileExistsAtPath: [installLogURL path]]) {
NSString *installLog = [[NSString alloc] initWithContentsOfURL: installLogURL usedEncoding: nil error: nil];
if (installLog != nil) {
NSRange logMentionRange;
for (NSString *line in [installLog componentsSeparatedByCharactersInSet: [NSCharacterSet newlineCharacterSet]]) {
logMentionRange = [line rangeOfString: @"Made container live for "];
if (logMentionRange.location != NSNotFound) {
NSArray *installParts = [[line substringFromIndex: logMentionRange.location + logMentionRange.length] componentsSeparatedByString: @" "];
if ([installParts count] == 3) { // expecting com.foo.bar at UUID
NSString *bundleID = [installParts objectAtIndex: 0];
if ([bundleID rangeOfString: @"com.apple"].location == NSNotFound) {
NSString *path = [installParts objectAtIndex: 2];
if (path != nil && [fileManager fileExistsAtPath: path]) {
NSString *pathTitle;
if ([path rangeOfString: @"Data/Application"].location != NSNotFound) {
pathTitle = @"Sandbox Location";
}
else if ([path rangeOfString: @"Bundle/Application"].location != NSNotFound) {
pathTitle = @"Bundle Location";
}
else {
pathTitle = @"???";
}
[self addPath: path withTitle: pathTitle forBundleID: bundleID withDeviceInfo: inDeviceInfo];
}
}
}
}
}
}
}
}
#endif
- (QSSimAppInfo *) appInfoWithBundleID: (NSString *) inBundleID
{
QSSimAppInfo *appInfo = nil;
NSInteger appIndex;
appIndex = [self.appList indexOfObjectPassingTest: ^(id inObject, NSUInteger inIndex, BOOL *outStop) {
QSSimAppInfo *appInfo = inObject;
*outStop = [appInfo.bundleID isEqualToString: inBundleID];
return *outStop;
}];
if (appIndex == NSNotFound) {
appInfo = [[QSSimAppInfo alloc] initWithBundleID: inBundleID];
[self.appList addObject: appInfo];
}
else {
appInfo = [self.appList objectAtIndex: appIndex];
}
return appInfo;
}
#pragma mark - QSOutlineProvider
- (NSInteger) outlineChildCount
{
return [self.appList count];
}
- (id) outlineChildAtIndex: (NSInteger) inIndex
{
return [self.appList objectAtIndex: inIndex];
}
- (BOOL) outlineItemIsExpanable
{
return [self.appList count] ? YES : NO;
}
- (NSString *) outlineItemValueForColumn: (NSTableColumn *) inTableColumn
{
return [inTableColumn.identifier isEqualToString: @"title"] ? self.title : nil;
}
- (BOOL) outlineItemPerformAction
{
if (self.baseURL != nil) {
[[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: @[ self.baseURL ]];
}
return YES;
}
- (BOOL) outlineItemPerformActionForChild: (id) inChild
{
return NO;
}
#pragma mark - Setters / Getters
- (NSString *) title
{
return [NSString stringWithFormat: @"%@: %@ %@", self.name, self.model, self.version];
}
@end

View file

@ -0,0 +1,25 @@
//
// QSSimViewController.h
// SimDirs
//
// Created by Casey Fleser on 10/31/14.
// Copyright (c) 2014 Quiet Spark. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@protocol QSOutlineProvider <NSObject>
- (NSInteger) outlineChildCount;
- (id) outlineChildAtIndex: (NSInteger) inIndex;
- (BOOL) outlineItemIsExpanable;
- (NSString *) outlineItemValueForColumn: (NSTableColumn *) inTableColumn;
- (BOOL) outlineItemPerformAction;
- (BOOL) outlineItemPerformActionForChild: (id) inChild;
@end
@interface QSSimViewController : NSObject
@end

View file

@ -0,0 +1,144 @@
//
// QSSimViewController.m
// SimDirs
//
// Created by Casey Fleser on 10/31/14.
// Copyright (c) 2014 Quiet Spark. All rights reserved.
//
#import "QSSimViewController.h"
#import "QSSimDeviceInfo.h"
@interface QSSimViewController ()
@property (nonatomic, weak) IBOutlet NSOutlineView *locationOutline;
@property (nonatomic, strong) NSArray *deviceList;
@end
@implementation QSSimViewController
- (void) awakeFromNib
{
[self reloadOutine];
[self.locationOutline setTarget: self];
[self.locationOutline setDoubleAction: @selector(handleRowSelect:)];
}
- (void) reloadOutine
{
NSArray *deviceList = [QSSimDeviceInfo gatherDeviceLocations];
self.deviceList = [deviceList sortedArrayUsingDescriptors: @[ [NSSortDescriptor sortDescriptorWithKey: @"title" ascending:YES ]]];
[self.locationOutline reloadData];
}
#pragma mark - NSOutlineViewDataSource
- (NSInteger) outlineView: (NSOutlineView *) inOutlineView
numberOfChildrenOfItem: (id) inItem
{
NSInteger childCount = 0;
if (inItem == nil) {
childCount = [self.deviceList count];
}
else {
if ([inItem conformsToProtocol: @protocol(QSOutlineProvider)]) {
childCount = [inItem outlineChildCount];
}
}
return childCount;
}
- (id) outlineView: (NSOutlineView *) inOutlineView
child: (NSInteger) inIndex
ofItem: (id) inItem
{
id child = nil;
if (inItem == nil) {
child = [self.deviceList objectAtIndex: inIndex];
}
else {
if ([inItem conformsToProtocol: @protocol(QSOutlineProvider)]) {
child = [inItem outlineChildAtIndex: inIndex];
}
}
return child;
}
- (BOOL) outlineView: (NSOutlineView *) inOutlineView
isItemExpandable: (id) inItem
{
BOOL expandable = NO;
if (inItem == nil) {
expandable = [self.deviceList count] ? YES : NO;
}
else {
if ([inItem conformsToProtocol: @protocol(QSOutlineProvider)]) {
expandable = [inItem outlineItemIsExpanable];
}
}
return expandable;
}
- (id) outlineView: (NSOutlineView *) inOutlineView
objectValueForTableColumn: (NSTableColumn *) inTableColumn
byItem: (id) inItem
{
if ([inItem conformsToProtocol: @protocol(QSOutlineProvider)]) {
inItem = [inItem outlineItemValueForColumn: inTableColumn];
}
else if (![inTableColumn.identifier isEqualToString: @"title"]) {
inItem = nil;
}
return inItem;
}
#pragma mark - Handlers
- (void) handleRowSelect: (id) inSender
{
if (inSender == self.locationOutline) {
id item = [self.locationOutline itemAtRow: [self.locationOutline clickedRow]];
BOOL handled = NO;
if (item != nil) {
if ([item conformsToProtocol: @protocol(QSOutlineProvider)]) {
handled = [item outlineItemPerformAction];
}
else {
id parentItem = [self.locationOutline parentForItem: item];
if ([parentItem conformsToProtocol: @protocol(QSOutlineProvider)]) {
handled = [parentItem outlineItemPerformActionForChild: item];
}
}
if (!handled) {
if ([self.locationOutline isItemExpanded: item]) {
[self.locationOutline collapseItem: item];
}
else {
[self.locationOutline expandItem: item];
}
}
}
}
}
- (IBAction) handleUpdate: (id) inSender
{
[self reloadOutine];
}
@end