Fix first launch and package hook as a signed framework

This commit is contained in:
admin committed 2026-11-12 03:27:18 +08:00
1 parent c1d0293404
commit 97a4fad71f
6 files changed
+147 -20

No files matched your search

+19
View File
@@ -90,6 +90,25 @@ group.<your-team-id>.<your-app-id>.01
The exact value must match your own profile. Do not reuse someone else's certificate, profile, or App Group.
The hook is stored as a nested framework at:
```text
Payload/AppsManager.app/Frameworks/AppsManagerHook.framework
```
Standard signing tools must sign `Frameworks` recursively. If a tool signs only the main executable, the app will fail to launch after re-signing. The published build uses the framework layout specifically so tools such as AltStore, SideStore, Sideloadly, and similar signers handle the hook as nested code.
### 重签名 / Re-signing
- 必须保留 App Group 权限;如果新的描述文件没有 App Group,App 会提示 `No usable App Group`,无法访问其他 App 容器。
The new provisioning profile must include an App Group. Without it the app reports `No usable App Group` and cannot access other app containers.
- 修改 Bundle ID 时,必须同时使用与该 Bundle ID 匹配的描述文件和 entitlements。
When changing the Bundle ID, use a matching provisioning profile and entitlements.
- 签名工具必须递归签名 `Frameworks` 目录;不要只签名主程序。
The signing tool must recursively sign the `Frameworks` directory, not only the main executable.
- 免费 Apple ID 通常不支持 App Group,因此无法让 worse_query 正常工作。
Free Apple ID profiles generally do not support App Groups, so worse_query will not work with them.
## 第三方代码 / Third-party code
This source includes and links code from:
+43 -3
View File
@@ -25,6 +25,7 @@ OUT_DIR=${OUT_DIR:-"$ROOT/out"}
BUNDLE_ID=${BUNDLE_ID:-com.jbjf.appsmanager}
DYLIB="$ROOT/hook/AppsManagerHook.dylib"
APP="$OUT_DIR/AppsManager.app"
FRAMEWORK="$APP/Frameworks/AppsManagerHook.framework"
if [ ! -f "$DYLIB" ]; then
echo "Missing $DYLIB; run build_dylib.sh first." >&2
@@ -42,17 +43,55 @@ fi
rm -rf "$APP"
mkdir -p "$OUT_DIR"
cp -R "$APP_SOURCE" "$APP"
cp "$DYLIB" "$APP/AppsManagerHook.dylib"
mkdir -p "$FRAMEWORK"
cp "$DYLIB" "$FRAMEWORK/AppsManagerHook"
cp "$PROVISION" "$APP/embedded.mobileprovision"
plutil -replace CFBundleIdentifier -string "$BUNDLE_ID" "$APP/Info.plist"
plutil -replace CFBundleDisplayName -string "Apps Manager" "$APP/Info.plist"
plutil -replace CFBundleName -string "AppsManager" "$APP/Info.plist"
cat > "$FRAMEWORK/Info.plist" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>AppsManagerHook</string>
<key>CFBundleIdentifier</key>
<string>com.jbjf.appsmanager.hook</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>AppsManagerHook</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>MinimumOSVersion</key>
<string>15.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
</dict>
</plist>
EOF
# Keep the hook in Frameworks so standard signing tools treat it as nested
# code and re-sign it together with the app.
install_name_tool -id "@rpath/AppsManagerHook.framework/AppsManagerHook" "$FRAMEWORK/AppsManagerHook"
cp "$APP/ADManager" "$APP/ADManager.orig"
"$INSERT_DYLIB" --inplace --overwrite --strip-codesig --all-yes \
"@executable_path/AppsManagerHook.dylib" "$APP/ADManager"
"@rpath/AppsManagerHook.framework/AppsManagerHook" "$APP/ADManager"
rm -f "$APP/ADManager.orig"
if ! otool -l "$APP/ADManager" | grep -q "@executable_path/Frameworks"; then
install_name_tool -add_rpath "@executable_path/Frameworks" "$APP/ADManager"
fi
set -- -f -s "$SIGN_IDENTITY"
if [ -n "$KEYCHAIN" ]; then
@@ -62,8 +101,9 @@ if [ -n "$ENTITLEMENTS" ]; then
set -- "$@" --entitlements "$ENTITLEMENTS" --generate-entitlement-der
fi
codesign "$@" "$APP/AppsManagerHook.dylib"
codesign "$@" "$FRAMEWORK"
codesign "$@" "$APP"
echo "built $APP"
otool -L "$APP/ADManager" | head -8
otool -l "$APP/ADManager" | grep -A2 LC_RPATH | head -6
+67 -10
View File
@@ -36,6 +36,7 @@ static NSString *WBLocalized(NSString *key)
@"scanning_containers_short": @"Scanning system containers…", @"ls_found": @"LaunchServices database located",
@"init_failed": @"Initialization Failed", @"ok": @"OK",
@"no_group": @"No usable App Group; worse_query cannot run",
@"no_group_hint": @"Re-sign with a certificate and provisioning profile that include an App Group entitlement.",
@"no_ls": @"LaunchServices database not found",
@"no_containers": @"The LaunchServices database returned no containers",
},
@@ -47,6 +48,7 @@ static NSString *WBLocalized(NSString *key)
@"scanning_containers_short": @"正在扫描系统容器…", @"ls_found": @"已定位 LaunchServices 数据库",
@"init_failed": @"初始化失败", @"ok": @"好",
@"no_group": @"未找到可用的 App Group,无法执行 worse_query",
@"no_group_hint": @"请使用包含 App Group 权限的证书和描述文件重新签名。",
@"no_ls": @"未找到 LaunchServices 数据库",
@"no_containers": @"LaunchServices 数据库没有返回容器列表",
},
@@ -780,9 +782,17 @@ static void (*gOrigLoad)(id, SEL);
static NSString *(*gOrigFileSizeString)(id, SEL);
static void WBStartPreparation(void);
static BOOL (*gOrigDidFinishLaunching)(id, SEL, UIApplication *, NSDictionary *);
static CFAbsoluteTime gLaunchStarted;
static BOOL WBHookDidFinishLaunching(id self, SEL _cmd, UIApplication *application, NSDictionary *launchOptions)
{
gLaunchStarted = CFAbsoluteTimeGetCurrent();
// The original app shows a one-time "Message From Developer" alert that
// competes with the launch progress sheet and makes the first run look
// blank. The hook has its own launch status UI, so skip it.
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
[defaults setBool:YES forKey:@"FirstLaunch"];
[defaults synchronize];
BOOL result = gOrigDidFinishLaunching ? gOrigDidFinishLaunching(self, _cmd, application, launchOptions) : YES;
WBStartPreparation();
return result;
@@ -790,13 +800,29 @@ static BOOL WBHookDidFinishLaunching(id self, SEL _cmd, UIApplication *applicati
static __weak id gAppsTableViewController;
static WBProgressViewController *gProgressController;
static dispatch_semaphore_t gReadySemaphore;
static NSCondition *gReadyCondition;
static BOOL gPrepared;
static BOOL gPreparing;
static BOOL gWaitingForRoot;
static BOOL gReloadScheduled;
static BOOL WBWaitForRegistry(void)
{
if (WBRegistry.shared.ready) return YES;
if (!gReadyCondition) return NO;
[gReadyCondition lock];
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:120];
while (!WBRegistry.shared.ready && !gPrepared && deadline.timeIntervalSinceNow > 0) {
[gReadyCondition waitUntilDate:deadline];
}
BOOL ready = WBRegistry.shared.ready;
[gReadyCondition unlock];
return ready;
}
static NSArray *WBHookAllInstalled(id self, SEL _cmd)
{
if (!WBRegistry.shared.ready) WBWaitForRegistry();
NSArray *proxies = WBRegistry.shared.proxies;
if (WBRegistry.shared.ready && proxies.count) return proxies;
return gOrigAllInstalled ? gOrigAllInstalled(self, _cmd) : @[];
@@ -804,6 +830,7 @@ static NSArray *WBHookAllInstalled(id self, SEL _cmd)
static NSArray *WBHookAllApplications(id self, SEL _cmd)
{
if (!WBRegistry.shared.ready) WBWaitForRegistry();
NSArray *proxies = WBRegistry.shared.proxies;
if (WBRegistry.shared.ready && proxies.count) return proxies;
return gOrigAllApplications ? gOrigAllApplications(self, _cmd) : @[];
@@ -872,9 +899,6 @@ static void WBHookDetailReload(id self, SEL _cmd)
static void WBHookLoad(id self, SEL _cmd)
{
gAppsTableViewController = self;
if (gReadySemaphore && !WBRegistry.shared.ready) {
dispatch_semaphore_wait(gReadySemaphore, dispatch_time(DISPATCH_TIME_NOW, 120 * NSEC_PER_SEC));
}
gOrigLoad(self, _cmd);
}
@@ -1467,7 +1491,7 @@ static void WBInstallHooks(void)
method_setImplementation(didFinish, (IMP)WBHookDidFinishLaunching);
}
gReadySemaphore = dispatch_semaphore_create(0);
gReadyCondition = [NSCondition new];
[[NSNotificationCenter defaultCenter] addObserverForName:@"WBAppSizeDidUpdate" object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *note) {
if (gReloadScheduled) return;
gReloadScheduled = YES;
@@ -1661,7 +1685,6 @@ static void WBRunDebugRefresh(void)
static void WBStartPreparation(void)
{
if (gPreparing || gPrepared) return;
gPreparing = YES;
UIWindow *window = UIApplication.sharedApplication.keyWindow;
if (!window) {
@@ -1673,25 +1696,58 @@ static void WBStartPreparation(void)
}
}
UIViewController *root = window.rootViewController;
if (root && !gProgressController) {
if (!root) {
// On a cold launch the window/root view may not exist yet. Wait for it
// instead of running the preparation with no visible progress sheet.
if (!gWaitingForRoot && CFAbsoluteTimeGetCurrent() - gLaunchStarted < 10) {
gWaitingForRoot = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
gWaitingForRoot = NO;
WBStartPreparation();
});
return;
}
if (!gWaitingForRoot) {
// Last-resort fallback: start even without a presenter.
gPreparing = YES;
[WBRegistry.shared prepareWithProgress:nil completion:^(BOOL ok, NSString *message) {
gPrepared = YES;
[gReadyCondition lock];
[gReadyCondition broadcast];
[gReadyCondition unlock];
}];
}
return;
}
gPreparing = YES;
[root loadViewIfNeeded];
UIViewController *presenter = root;
while (presenter.presentedViewController) presenter = presenter.presentedViewController;
if (!gProgressController) {
gProgressController = [WBProgressViewController new];
gProgressController.modalPresentationStyle = UIModalPresentationOverFullScreen;
[root presentViewController:gProgressController animated:YES completion:nil];
[presenter presentViewController:gProgressController animated:YES completion:nil];
}
[WBRegistry.shared prepareWithProgress:^(double fraction, NSString *status) {
[gProgressController updateFraction:fraction status:status];
} completion:^(BOOL ok, NSString *message) {
gPrepared = YES;
if (gReadySemaphore) dispatch_semaphore_signal(gReadySemaphore);
[gReadyCondition lock];
[gReadyCondition broadcast];
[gReadyCondition unlock];
dispatch_async(dispatch_get_main_queue(), ^{
if (gProgressController) {
[gProgressController dismissViewControllerAnimated:YES completion:nil];
gProgressController = nil;
}
if (!ok && message.length) {
NSString *detail = message;
if ([message containsString:@"App Group"]) {
detail = [message stringByAppendingFormat:@"\n\n%@", WBLocalized(@"no_group_hint")];
}
UIAlertController *alert = [UIAlertController alertControllerWithTitle:WBLocalized(@"init_failed")
message:message
message:detail
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:WBLocalized(@"ok") style:UIAlertActionStyleDefault handler:nil]];
UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow;
@@ -1711,6 +1767,7 @@ __attribute__((constructor))
static void WBHookInit(void)
{
@autoreleasepool {
gLaunchStarted = CFAbsoluteTimeGetCurrent();
WBInstallHooks();
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification