App Manager: per-entry traversal fallback and stable list

This commit is contained in:
admin committed 2026-11-12 13:39:00 +08:00
1 parent 0e5851c521
commit dc51a3a3c7
4 files changed
+234 -24

No files matched your search

+215 -24
View File
@@ -50,6 +50,30 @@ static NSMutableDictionary<NSString *, NSString *> *gDataMap;
static BOOL gBundleMapAttempted;
static NSObject *gBundleMapLock;
static dispatch_queue_t gAppsQueue;
static BOOL gBuildingApps;
static void *kWQAppsQueueKey = &kWQAppsQueueKey;
static dispatch_queue_t WQAppsQueue(void)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gAppsQueue = dispatch_queue_create(
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
dispatch_queue_set_specific(gAppsQueue, kWQAppsQueueKey,
(void *)1, NULL);
});
return gAppsQueue;
}
static void WQAppsQueueSync(dispatch_block_t block)
{
if (!block) return;
if (dispatch_get_specific(kWQAppsQueueKey)) {
block();
return;
}
dispatch_sync(WQAppsQueue(), block);
}
static dispatch_queue_t gFamiliesQueue;
static NSMutableDictionary<NSString *, NSArray<NSString *> *> *gPathleakCache;
static dispatch_group_t gStartupGroup;
@@ -358,9 +382,7 @@ void WQRefreshDiscovery(void)
{
dispatch_async(gFamiliesQueue ?: dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
WQDiscoverPerContainerFamilies();
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
dispatch_sync(gAppsQueue, ^{
WQAppsQueueSync(^{
gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date];
});
@@ -1123,6 +1145,143 @@ static void WQAppendMobileInstallationApps(NSMutableArray *result,
(unsigned long)apps.count, (unsigned long)result.count);
}
static BOOL gForceAppTraversal;
#pragma mark - Per-entry app scan
static void WQAddCandidateBundleID(NSMutableOrderedSet<NSString *> *ids,
id value)
{
if ([value isKindOfClass:NSString.class] &&
WQLooksLikeBundleIdentifier(value))
[ids addObject:value];
}
static void WQAddProxyBundleID(NSMutableOrderedSet<NSString *> *ids, id proxy)
{
if (!proxy) return;
NSArray<NSString *> *selectors = @[
@"applicationIdentifier", @"bundleIdentifier", @"bundleId",
@"applicationID",
];
for (NSString *name in selectors) {
SEL selector = NSSelectorFromString(name);
if (![proxy respondsToSelector:selector]) continue;
id value = ((id(*)(id, SEL))objc_msgSend)(proxy, selector);
WQAddCandidateBundleID(ids, value);
}
for (NSString *key in @[@"bundleIdentifier", @"applicationIdentifier",
@"_bundleIdentifier", @"_applicationIdentifier"]) {
@try {
WQAddCandidateBundleID(ids, [proxy valueForKey:key]);
} @catch (__unused NSException *exception) {
}
}
SEL urlSelector = NSSelectorFromString(@"bundleURL");
if ([proxy respondsToSelector:urlSelector]) {
id url = ((id(*)(id, SEL))objc_msgSend)(proxy, urlSelector);
if ([url isKindOfClass:NSURL.class]) {
NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:
[[(NSURL *)url path] stringByAppendingPathComponent:@"Info.plist"]];
WQAddCandidateBundleID(ids, plist[@"CFBundleIdentifier"]);
}
}
}
/// Bundle ids exposed by LaunchServices without relying on allApplications.
/// This is the fallback when the CSStore parser/layout is unavailable.
static NSArray<NSString *> *WQCandidateBundleIDs(void)
{
NSMutableOrderedSet<NSString *> *ids = [NSMutableOrderedSet orderedSet];
Class workspaceClass = NSClassFromString(@"LSApplicationWorkspace");
id workspace = nil;
SEL defaultSelector = NSSelectorFromString(@"defaultWorkspace");
if ([workspaceClass respondsToSelector:defaultSelector])
workspace = ((id(*)(id, SEL))objc_msgSend)(workspaceClass,
defaultSelector);
NSArray<NSString *> *arraySelectors = @[
@"installedApplications", @"allInstalledApplications",
@"allApplications", @"unrestrictedApplications",
@"directionsApplications", @"placeholderApplications",
];
if (workspace) {
for (NSString *name in arraySelectors) {
SEL selector = NSSelectorFromString(name);
if (![workspace respondsToSelector:selector]) continue;
id value = ((id(*)(id, SEL))objc_msgSend)(workspace, selector);
if (![value isKindOfClass:NSArray.class]) continue;
for (id item in value) {
if ([item isKindOfClass:NSString.class])
WQAddCandidateBundleID(ids, item);
else
WQAddProxyBundleID(ids, item);
}
}
NSArray<NSString *> *typedSelectors = @[
@"applicationsOfType:", @"legacyApplicationProxiesListWithType:",
];
for (NSString *name in typedSelectors) {
SEL selector = NSSelectorFromString(name);
if (![workspace respondsToSelector:selector]) continue;
for (NSUInteger type = 0; type <= 1; type++) {
id value = ((id(*)(id, SEL, NSUInteger))objc_msgSend)(
workspace, selector, type);
if (![value isKindOfClass:NSArray.class]) continue;
for (id item in value) WQAddProxyBundleID(ids, item);
}
}
SEL enumerateSelector = NSSelectorFromString(
@"enumerateApplicationsOfType:block:");
if ([workspace respondsToSelector:enumerateSelector]) {
void (^block)(id) = ^(id proxy) {
WQAddProxyBundleID(ids, proxy);
};
((void(*)(id, SEL, NSUInteger, id))objc_msgSend)(workspace,
enumerateSelector, 0, block);
}
}
// MobileInstallation is absent on some iOS 27 builds, but present on
// others; use it when available.
NSDictionary<NSString *, NSDictionary *> *installation =
WQMobileInstallationApps();
for (NSString *key in installation) {
WQAddCandidateBundleID(ids, key);
NSDictionary *entry = installation[key];
if ([entry isKindOfClass:NSDictionary.class])
WQAddCandidateBundleID(ids, entry[@"CFBundleIdentifier"]);
}
return ids.array;
}
static NSArray<NSString *> *WQContainersForBundleIDs(NSArray<NSString *> *ids)
{
NSMutableArray<NSString *> *paths = [NSMutableArray array];
for (NSString *bundleID in ids) {
if (!bundleID.length) continue;
NSString *error = nil;
NSString *path = MCMFilzaDataContainerPath(bundleID, &error);
if (!path.length) continue;
path = WQCanonicalPath(path);
if (path.length && WQPathExists(path)) [paths addObject:path];
}
return paths;
}
void WQRequestAppManagerRescan(void)
{
gForceAppTraversal = YES;
WQAppsQueueSync(^{
gAppsCache = nil;
gAppsCacheDate = nil;
});
if (gPathleakCache) [gPathleakCache removeAllObjects];
NSLog(@"[WQ] App Manager requested a fresh per-container scan");
}
static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
{
NSString *root = @"/private/var/mobile/Containers/Data/Application";
@@ -1133,27 +1292,44 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
WQPostStartupProgress(0.20, @"正在读取 LaunchServices 应用数据库…");
WQLoadLaunchServicesDatabase();
NSMutableArray<NSString *> *lsContainers = [NSMutableArray array];
BOOL forceTraversal = gForceAppTraversal;
gForceAppTraversal = NO;
NSMutableArray<NSString *> *candidates = [NSMutableArray array];
// Traditional path traversal: whenever the App Manager opens, enumerate
// the data-container root again through worse_query/pathleak. This does
// not depend on the LaunchServices database being readable.
if (forceTraversal) {
WQPostStartupProgress(0.24, @"正在逐个遍历应用数据容器…");
[candidates addObjectsFromArray:WQEnumerateContainers(root)];
}
// Names/container hints from the LaunchServices database. These are a
// supplement, never the only source.
[candidates addObjectsFromArray:WQLSContainerPaths() ?: @[]];
// Per-app container lookup by bundle id. This is the fallback for devices
// where the data-container root cannot be listed at all.
NSArray<NSString *> *bundleIDs = WQCandidateBundleIDs();
[candidates addObjectsFromArray:WQContainersForBundleIDs(bundleIDs)];
// If neither source found anything, do the traditional enumeration even
// when this was not an App Manager-triggered scan.
if (candidates.count == 0) {
[candidates addObjectsFromArray:WQEnumerateContainers(root)];
}
NSString *lsPrefix = [root stringByAppendingString:@"/"];
for (NSString *path in WQLSContainerPaths() ?: @[]) {
NSMutableOrderedSet<NSString *> *unique = [NSMutableOrderedSet orderedSet];
for (NSString *path in candidates) {
NSString *canonical = WQCanonicalPath(path);
if (![canonical hasPrefix:lsPrefix]) continue;
if (canonical.length != lsPrefix.length + 36) continue;
if (![[NSUUID alloc] initWithUUIDString:
[canonical substringFromIndex:lsPrefix.length]]) continue;
[lsContainers addObject:canonical];
}
NSArray<NSString *> *containers = nil;
if (lsContainers.count >= 2) {
containers = lsContainers;
WQPostStartupProgress(0.30, [NSString stringWithFormat:
@"应用数据库已列出 %lu 个数据容器,正在授权…",
(unsigned long)containers.count]);
} else {
WQPostStartupProgress(0.26, @"正在扫描应用数据容器…");
containers = WQEnumerateContainers(root);
[unique addObject:canonical];
}
NSArray<NSString *> *containers = unique.array;
WQPostStartupProgress(0.40, [NSString stringWithFormat:
@"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]);
NSUInteger granted = 0;
@@ -1176,17 +1352,21 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
NSArray<NSDictionary<NSString *, NSString *> *> *WQInstalledApplications(void)
{
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
__block NSArray *value = nil;
dispatch_sync(gAppsQueue, ^{
WQAppsQueueSync(^{
NSTimeInterval age = gAppsCacheDate
? -[gAppsCacheDate timeIntervalSinceNow] : 1e9;
if (gBuildingApps) {
value = gAppsCache ?: @[];
return;
}
// Never cache an empty result: the first call can happen before the
// worse_query grants and app-container scan have finished.
if (!gAppsCache || gAppsCache.count == 0 || age > 120) {
gBuildingApps = YES;
gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date];
gBuildingApps = NO;
}
value = gAppsCache;
});
@@ -1507,15 +1687,14 @@ void WQFilzaStart(void)
WQScheduleFavorites();
// Build the app-container index before the user opens Apps manager.
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
WQAppsQueue();
if (!gFamiliesQueue) gFamiliesQueue = dispatch_queue_create(
"com.filzaslop.worsequery.families", DISPATCH_QUEUE_SERIAL);
// App containers are the priority: the Apps manager must populate before
// the slower system-group sweep begins.
dispatch_group_enter(gStartupGroup);
dispatch_async(gAppsQueue, ^{
dispatch_async(WQAppsQueue(), ^{
WQPostStartupProgress(0.24, @"正在枚举应用数据容器…");
if (!gAppsCache) gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date];
@@ -1587,6 +1766,18 @@ void WQFilzaStart(void)
- (NSNumber *)dynamicDiskUsage { return @0; }
- (BOOL)isDeletable { return YES; }
- (BOOL)isPlaceholder { return NO; }
- (BOOL)isSystem { return NO; }
- (BOOL)isSystem {
return [_bundleIdentifier hasPrefix:@"com.apple."];
}
- (BOOL)isInstalled { return YES; }
- (BOOL)isNewsstandApp { return NO; }
- (BOOL)isRestricted { return NO; }
- (BOOL)isLaunchProhibited { return NO; }
- (BOOL)isHidden { return NO; }
- (NSString *)minimumSystemVersion { return @"15.0"; }
- (NSString *)teamID { return @""; }
- (long long)bundleModTime {
return (long long)NSDate.date.timeIntervalSinceReferenceDate;
}
@end