| 741 | } |
| 742 | |
| 743 | extern "C" int hook_NotificationDirect(void* pThis, const char* methodName, void* body, int* flags) |
| 744 | { |
| 745 | HookGuard guard; |
| 746 | if (g_shuttingDown.load(std::memory_order_acquire)) { |
| 747 | auto fn = g_origNotificationDirect.load(std::memory_order_acquire); |
| 748 | return fn ? fn(pThis, methodName, body, flags) : 0; |
| 749 | } |
| 750 | CrashContextScope crashContext("NotificationDirect:entry", methodName, 0); |
| 751 | auto origFn = g_origNotificationDirect.load(std::memory_order_acquire); |
| 752 | for (int i = 0; !origFn && i < 1000; ++i) { |
| 753 | usleep(100); |
| 754 | origFn = g_origNotificationDirect.load(std::memory_order_acquire); |
| 755 | } |
| 756 | if (!origFn) return 0; |
| 757 | |
| 758 | // Only intercept Cloud.* notifications |
| 759 | if (!IsCloudRpc(methodName)) { |
| 760 | return origFn(pThis, methodName, body, flags); |
| 761 | } |
| 762 | |
| 763 | uint32_t appId = CheckNotificationNamespaceApp(methodName, body); |
| 764 | CR_SetCrashContext("NotificationDirect:checked", methodName, appId); |
| 765 | if (appId == 0) { |
| 766 | // Not a namespace app - pass through to Steam servers |
| 767 | LOG("[Hook-Notif] %s: not namespace, passing through", methodName); |
| 768 | return origFn(pThis, methodName, body, flags); |
| 769 | } |
| 770 | |
| 771 | // ExitSyncDone: let Steam process it (updates remotecache.vdf CN). |
| 772 | if (strcmp(methodName, CloudIntercept::RPC_EXIT_SYNC) == 0) { |
| 773 | CR_SetCrashContext("NotificationDirect:exit-sync", methodName, appId); |
| 774 | auto bodyBytes = SerializeMessage(body); |
| 775 | auto fields = PB::Parse(bodyBytes.data(), bodyBytes.size()); |
| 776 | uint64_t clientId = 0; |
| 777 | bool uploadsCompleted = false; |
| 778 | bool uploadsRequired = false; |
| 779 | if (auto* f = PB::FindField(fields, 2)) clientId = f->varintVal; |
| 780 | if (auto* f = PB::FindField(fields, 3)) uploadsCompleted = f->varintVal != 0; |
| 781 | if (auto* f = PB::FindField(fields, 4)) uploadsRequired = f->varintVal != 0; |
| 782 | uint32_t accountId = CloudIntercept::GetAccountId(); |
| 783 | if (accountId != 0) { |
| 784 | PendingOpsJournal::RecordExitSyncState(accountId, appId, |
| 785 | uploadsCompleted, uploadsRequired, clientId); |
| 786 | // Native-faithful: ExitSyncDone is fire-and-forget (notification, no |
| 787 | // response). Dispatch session release off Steam's thread. |
| 788 | std::thread([accountId, appId, clientId] { |
| 789 | CloudStorage::InflightSyncScope guard; |
| 790 | if (!guard.entered) return; // shutting down, skip session release |
| 791 | CloudStorage::ReleaseCloudSession(accountId, appId, clientId); |
| 792 | }).detach(); |
| 793 | } |
| 794 | LOG("[Hook-Notif] %s app=%u: letting Steam process internally", methodName, appId); |
| 795 | return origFn(pThis, methodName, body, flags); |
| 796 | } |
| 797 | |
| 798 | // ConflictResolution: parse chose_local_files so HandleLaunchIntent |
| 799 | // knows whether to skip pre-restore (user chose "keep local files"). |
| 800 | if (strcmp(methodName, CloudIntercept::RPC_CONFLICT) == 0) { |
nothing calls this directly
no test coverage detected