Bug 1830725: Synchronous `nsWindowsPackageManager::GetCampaignId` blocks main thread for many seconds r=nrishel

Made GetCampaignId asynchronous to not block anything.
Also fix Bug 1872933 (broken build)

Differential Revision: https://phabricator.services.mozilla.com/D188520
This commit is contained in:
Michael Hughes 2024-01-05 21:26:39 +00:00
Родитель 05f593b0cc
Коммит 3b64a17a14
5 изменённых файлов: 297 добавлений и 146 удалений

Просмотреть файл

@ -64,10 +64,12 @@ export var AttributionCode = {
* Wrapper to pull campaign IDs from MSIX builds.
* This function solely exists to make it easy to mock out for tests.
*/
get msixCampaignId() {
return Cc["@mozilla.org/windows-package-manager;1"]
.createInstance(Ci.nsIWindowsPackageManager)
.getCampaignId();
async msixCampaignId() {
const windowsPackageManager = Cc[
"@mozilla.org/windows-package-manager;1"
].createInstance(Ci.nsIWindowsPackageManager);
return windowsPackageManager.campaignId();
},
/**
@ -346,7 +348,7 @@ export var AttributionCode = {
)}`
);
let encoder = new TextEncoder();
bytes = encoder.encode(encodeURIComponent(this.msixCampaignId));
bytes = encoder.encode(encodeURIComponent(await this.msixCampaignId()));
} else {
bytes = await AttributionIOUtils.read(attributionFile.path);
}

Просмотреть файл

@ -80,9 +80,17 @@ add_task(async function test_read_error() {
throw new Error("read_error");
};
// On MSIX builds, AttributionIOUtils.read is not used; AttributionCode.msixCampaignId is.
// Ensure we override that as well.
let oldMsixCampaignId = AttributionCode.msixCampaignId;
AttributionCode.msixCampaignId = async () => {
throw new Error("read_error");
};
registerCleanupFunction(() => {
AttributionIOUtils.exists = oldExists;
AttributionIOUtils.read = oldRead;
AttributionCode.msixCampaignId = oldMsixCampaignId;
});
// Try to read the file

Просмотреть файл

@ -20,6 +20,8 @@ add_task(async () => {
* to make sure we reject bad ones and accept good ones.
*/
add_task(async function testValidAttrCodes() {
let msixCampaignIdStub = sinon.stub(AttributionCode, "msixCampaignId");
let currentCode = null;
for (let entry of validAttrCodes) {
currentCode = entry.code;
@ -36,14 +38,13 @@ add_task(async function testValidAttrCodes() {
// In real life, the attribution codes returned from Microsoft APIs
// are not URI encoded, and the AttributionCode code that deals with
// them expects that - so we have to simulate that as well.
sinon
.stub(AttributionCode, "msixCampaignId")
.get(() => decodeURIComponent(currentCode));
msixCampaignIdStub.callsFake(async () => decodeURIComponent(currentCode));
} else {
await AttributionCode.writeAttributionFile(currentCode);
}
AttributionCode._clearCache();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
entry.parsed,
@ -51,13 +52,18 @@ add_task(async function testValidAttrCodes() {
);
}
AttributionCode._clearCache();
// Restore the msixCampaignId stub so that other tests don't fail stubbing it
msixCampaignIdStub.restore();
});
/**
* Make sure codes with various formatting errors are not seen as valid.
*/
add_task(async function testInvalidAttrCodes() {
let msixCampaignIdStub = sinon.stub(AttributionCode, "msixCampaignId");
let currentCode = null;
for (let code of invalidAttrCodes) {
currentCode = code;
@ -73,9 +79,7 @@ add_task(async function testInvalidAttrCodes() {
continue;
}
sinon
.stub(AttributionCode, "msixCampaignId")
.get(() => decodeURIComponent(currentCode));
msixCampaignIdStub.callsFake(async () => decodeURIComponent(currentCode));
} else {
await AttributionCode.writeAttributionFile(currentCode);
}
@ -88,6 +92,9 @@ add_task(async function testInvalidAttrCodes() {
);
}
AttributionCode._clearCache();
// Restore the msixCampaignId stub so that other tests don't fail stubbing it
msixCampaignIdStub.restore();
});
/**

Просмотреть файл

@ -23,7 +23,7 @@ interface nsIWindowsPackageManager : nsISupports
*/
unsigned long long getInstalledDate();
/* Retrieves the campaignId, if any, a user's Microsoft Store install is
/* Asynchronously retrieves the campaignId, if any, a user's Microsoft Store install is
* associated with. These are present if the user clicked a "ms-window-store://"
* or "https://" link that included a "cid" query argument the very first time
* they installed the app. (This value appears to be cached forever, so
@ -37,5 +37,6 @@ interface nsIWindowsPackageManager : nsISupports
* a non-packaged build.
* @throw NS_ERROR_FAILURE for any other errors
*/
AString getCampaignId();
[implicit_jscontext]
Promise campaignId();
};

Просмотреть файл

@ -4,10 +4,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsWindowsPackageManager.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/Logging.h"
#include "mozilla/WinHeaderOnlyUtils.h"
#include "mozilla/mscom/EnsureMTA.h"
#ifndef __MINGW32__
# include "nsProxyRelease.h"
# include <comutil.h>
# include <wrl.h>
# include <windows.applicationmodel.store.h>
@ -141,16 +142,72 @@ nsWindowsPackageManager::GetInstalledDate(uint64_t* ts) {
#endif // __MINGW32__
}
static HRESULT RejectOnMainThread(
const char* aName, nsMainThreadPtrHandle<dom::Promise> promiseHolder,
nsresult result) {
DebugOnly<nsresult> rv = NS_DispatchToMainThread(NS_NewRunnableFunction(
aName, [promiseHolder = std::move(promiseHolder), result]() {
promiseHolder.get()->MaybeReject(result);
}));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed");
return S_OK;
}
#ifndef __MINGW32__
// forward declarations
static void GetCampaignIdFromStoreProductOnBackgroundThread(
ComPtr<IAsyncOperation<StoreProductResult*> > asyncSpr,
ComPtr<IStoreContext> storeContext,
nsMainThreadPtrHandle<dom::Promise> promiseHolder);
static void GetCampaignIdFromLicenseOnBackgroundThread(
ComPtr<IAsyncOperation<StoreAppLicense*> > asyncSal,
nsMainThreadPtrHandle<dom::Promise> promiseHolder,
nsAutoString aCampaignId);
#endif // __MINGW32__
static std::tuple<nsMainThreadPtrHolder<dom::Promise>*, nsresult>
InitializePromise(JSContext* aCx, dom::Promise** aPromise) {
*aPromise = nullptr;
nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
if (NS_WARN_IF(!global)) {
return std::make_tuple(nullptr, NS_ERROR_FAILURE);
}
ErrorResult erv;
RefPtr<dom::Promise> outer = dom::Promise::Create(global, erv);
if (NS_WARN_IF(erv.Failed())) {
return std::make_tuple(nullptr, erv.StealNSResult());
}
auto promiseHolder = new nsMainThreadPtrHolder<dom::Promise>(
"nsWindowsPackageManager::CampaignId Promise", outer);
outer.forget(aPromise);
return std::make_tuple(promiseHolder, NS_OK);
}
NS_IMETHODIMP
nsWindowsPackageManager::GetCampaignId(nsAString& aCampaignId) {
nsWindowsPackageManager::CampaignId(JSContext* aCx, dom::Promise** aPromise) {
NS_ENSURE_ARG_POINTER(aPromise);
#ifdef __MINGW32__
return NS_ERROR_NOT_IMPLEMENTED;
#else
// This is only relevant for MSIX packaged builds.
if (!mozilla::HasPackageIdentity()) {
return NS_ERROR_NOT_IMPLEMENTED;
}
auto [unwrappedPromiseHolder, result] = InitializePromise(aCx, aPromise);
NS_ENSURE_SUCCESS(result, result);
nsMainThreadPtrHandle<dom::Promise> promiseHolder(unwrappedPromiseHolder);
ComPtr<IStoreContextStatics> scStatics = nullptr;
HRESULT hr = RoGetActivationFactory(
HStringReference(RuntimeClass_Windows_Services_Store_StoreContext).Get(),
@ -169,160 +226,236 @@ nsWindowsPackageManager::GetCampaignId(nsAString& aCampaignId) {
* supporting that scenario.
*
*/
if (!SUCCEEDED(hr) || scStatics == nullptr) return NS_ERROR_FAILURE;
ComPtr<IStoreContext> storeContext = nullptr;
hr = scStatics->GetDefault(&storeContext);
if (!SUCCEEDED(hr) || storeContext == nullptr) return NS_ERROR_FAILURE;
ComPtr<IAsyncOperation<StoreProductResult*> > asyncSpr = nullptr;
{
nsAutoHandle event(CreateEventW(nullptr, true, false, nullptr));
bool asyncOpSucceeded = false;
// Despite the documentation indicating otherwise, the async operations
// and callbacks used here don't seem to work outside of a COM MTA.
mozilla::mscom::EnsureMTA(
[&event, &asyncOpSucceeded, &hr, &storeContext, &asyncSpr]() -> void {
auto callback =
Callback<IAsyncOperationCompletedHandler<StoreProductResult*> >(
[&asyncOpSucceeded, &event](
IAsyncOperation<StoreProductResult*>* asyncInfo,
AsyncStatus status) -> HRESULT {
asyncOpSucceeded = status == AsyncStatus::Completed;
return SetEvent(event.get());
});
hr = storeContext->GetStoreProductForCurrentAppAsync(&asyncSpr);
if (!SUCCEEDED(hr) || asyncSpr == nullptr) {
asyncOpSucceeded = false;
return;
}
hr = asyncSpr->put_Completed(callback.Get());
if (!SUCCEEDED(hr)) {
asyncOpSucceeded = false;
return;
}
DWORD ret = WaitForSingleObject(event.get(), 30000);
if (ret != WAIT_OBJECT_0) {
asyncOpSucceeded = false;
}
});
if (!asyncOpSucceeded) return NS_ERROR_FAILURE;
if (!SUCCEEDED(hr) || scStatics == nullptr) {
promiseHolder.get()->MaybeReject(NS_ERROR_FAILURE);
return NS_OK;
}
ComPtr<IStoreContext> storeContext = nullptr;
hr = scStatics->GetDefault(&storeContext);
if (!SUCCEEDED(hr) || storeContext == nullptr) {
promiseHolder.get()->MaybeReject(NS_ERROR_FAILURE);
return NS_OK;
}
/* Despite the documentation not saying otherwise, these don't work
* consistently when called from the main thread. I tried the two scenarios
* described above multiple times, and couldn't consistently get the campaign
* id when running this code async on the main thread. So instead, this
* dispatches to a background task to do the work, and then dispatches to the
* main thread to call back into the Javascript asynchronously
*
*/
result = NS_DispatchBackgroundTask(
NS_NewRunnableFunction(
__func__,
[storeContext = std::move(storeContext),
promiseHolder = std::move(promiseHolder)]() -> void {
ComPtr<IAsyncOperation<StoreProductResult*> > asyncSpr = nullptr;
auto hr =
storeContext->GetStoreProductForCurrentAppAsync(&asyncSpr);
if (!SUCCEEDED(hr) || asyncSpr == nullptr) {
RejectOnMainThread(__func__, std::move(promiseHolder),
NS_ERROR_FAILURE);
return;
}
auto callback =
Callback<IAsyncOperationCompletedHandler<StoreProductResult*> >(
[promiseHolder, asyncSpr,
storeContext = std::move(storeContext)](
IAsyncOperation<StoreProductResult*>* asyncInfo,
AsyncStatus status) -> HRESULT {
bool asyncOpSucceeded = status == AsyncStatus::Completed;
if (!asyncOpSucceeded) {
return RejectOnMainThread(__func__,
std::move(promiseHolder),
NS_ERROR_FAILURE);
}
GetCampaignIdFromStoreProductOnBackgroundThread(
std::move(asyncSpr), std::move(storeContext),
std::move(promiseHolder));
return S_OK;
});
hr = asyncSpr->put_Completed(callback.Get());
if (!SUCCEEDED(hr)) {
RejectOnMainThread(__func__, std::move(promiseHolder),
NS_ERROR_FAILURE);
return;
}
}),
NS_DISPATCH_EVENT_MAY_BLOCK);
if (NS_FAILED(result)) {
promiseHolder.get()->MaybeReject(NS_ERROR_FAILURE);
return NS_OK;
}
return NS_OK;
#endif // __MINGW32__
}
#ifndef __MINGW32__
static void GetCampaignIdFromStoreProductOnBackgroundThread(
ComPtr<IAsyncOperation<StoreProductResult*> > asyncSpr,
ComPtr<IStoreContext> storeContext,
nsMainThreadPtrHandle<dom::Promise> promiseHolder) {
ComPtr<IStoreProductResult> productResult = nullptr;
hr = asyncSpr->GetResults(&productResult);
if (!SUCCEEDED(hr) || productResult == nullptr) return NS_ERROR_FAILURE;
auto hr = asyncSpr->GetResults(&productResult);
if (!SUCCEEDED(hr) || productResult == nullptr) {
RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
return;
}
nsAutoString campaignId;
ComPtr<IStoreProduct> product = nullptr;
hr = productResult->get_Product(&product);
if (!SUCCEEDED(hr) || product == nullptr) return NS_ERROR_FAILURE;
ComPtr<Collections::IVectorView<StoreSku*> > skus = nullptr;
hr = product->get_Skus(&skus);
if (!SUCCEEDED(hr) || skus == nullptr) return NS_ERROR_FAILURE;
unsigned int size;
hr = skus->get_Size(&size);
if (!SUCCEEDED(hr)) return NS_ERROR_FAILURE;
for (unsigned int i = 0; i < size; i++) {
ComPtr<IStoreSku> sku = nullptr;
hr = skus->GetAt(i, &sku);
if (!SUCCEEDED(hr) || sku == nullptr) return NS_ERROR_FAILURE;
boolean isInUserCollection = false;
hr = sku->get_IsInUserCollection(&isInUserCollection);
if (!SUCCEEDED(hr) || !isInUserCollection) continue;
ComPtr<IStoreCollectionData> scd = nullptr;
hr = sku->get_CollectionData(&scd);
if (!SUCCEEDED(hr) || scd == nullptr) continue;
HString campaignId;
hr = scd->get_CampaignId(campaignId.GetAddressOf());
if (!SUCCEEDED(hr)) continue;
unsigned int tmp;
aCampaignId.Assign(campaignId.GetRawBuffer(&tmp));
if (aCampaignId.Length() > 0) {
aCampaignId.AppendLiteral("&msstoresignedin=true");
if (SUCCEEDED(hr) && (product != nullptr)) {
ComPtr<Collections::IVectorView<StoreSku*> > skus = nullptr;
hr = product->get_Skus(&skus);
if (!SUCCEEDED(hr) || skus == nullptr) {
RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
return;
}
unsigned int size;
hr = skus->get_Size(&size);
if (!SUCCEEDED(hr)) {
RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
return;
}
for (unsigned int i = 0; i < size; i++) {
ComPtr<IStoreSku> sku = nullptr;
hr = skus->GetAt(i, &sku);
if (!SUCCEEDED(hr) || sku == nullptr) {
RejectOnMainThread(__func__, std::move(promiseHolder),
NS_ERROR_FAILURE);
return;
}
boolean isInUserCollection = false;
hr = sku->get_IsInUserCollection(&isInUserCollection);
if (!SUCCEEDED(hr) || !isInUserCollection) continue;
ComPtr<IStoreCollectionData> scd = nullptr;
hr = sku->get_CollectionData(&scd);
if (!SUCCEEDED(hr) || scd == nullptr) continue;
HString msCampaignId;
hr = scd->get_CampaignId(msCampaignId.GetAddressOf());
if (!SUCCEEDED(hr)) continue;
unsigned int tmp;
campaignId.Assign(msCampaignId.GetRawBuffer(&tmp));
if (campaignId.Length() > 0) {
campaignId.AppendLiteral("&msstoresignedin=true");
}
}
}
if (!campaignId.IsEmpty()) {
// If we got here, it means that campaignId has been processed and can be
// sent back via the promise
DebugOnly<nsresult> rv = NS_DispatchToMainThread(NS_NewRunnableFunction(
__func__, [promiseHolder = std::move(promiseHolder),
campaignId = std::move(campaignId)]() {
promiseHolder.get()->MaybeResolve(campaignId);
}));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed");
return;
}
// There's various points above that could exit without a failure.
// If we get here without a campaignId we may as well just check
// the AppStoreLicense.
if (aCampaignId.IsEmpty()) {
ComPtr<IAsyncOperation<StoreAppLicense*> > asyncSal = nullptr;
bool asyncOpSucceeded = false;
nsAutoHandle event(CreateEventW(nullptr, true, false, nullptr));
mozilla::mscom::EnsureMTA(
[&event, &asyncOpSucceeded, &hr, &storeContext, &asyncSal]() -> void {
auto callback =
Callback<IAsyncOperationCompletedHandler<StoreAppLicense*> >(
[&asyncOpSucceeded, &event](
IAsyncOperation<StoreAppLicense*>* asyncInfo,
AsyncStatus status) -> HRESULT {
asyncOpSucceeded = status == AsyncStatus::Completed;
return SetEvent(event.get());
});
ComPtr<IAsyncOperation<StoreAppLicense*> > asyncSal = nullptr;
hr = storeContext->GetAppLicenseAsync(&asyncSal);
if (!SUCCEEDED(hr) || asyncSal == nullptr) {
asyncOpSucceeded = false;
return;
}
hr = asyncSal->put_Completed(callback.Get());
if (!SUCCEEDED(hr)) {
asyncOpSucceeded = false;
return;
}
hr = storeContext->GetAppLicenseAsync(&asyncSal);
if (!SUCCEEDED(hr) || asyncSal == nullptr) {
RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
return;
}
DWORD ret = WaitForSingleObject(event.get(), 30000);
if (ret != WAIT_OBJECT_0) {
asyncOpSucceeded = false;
}
});
if (!asyncOpSucceeded) return NS_ERROR_FAILURE;
auto callback = Callback<IAsyncOperationCompletedHandler<StoreAppLicense*> >(
[asyncSal, promiseHolder, campaignId = std::move(campaignId)](
IAsyncOperation<StoreAppLicense*>* asyncInfo,
AsyncStatus status) -> HRESULT {
bool asyncOpSucceeded = status == AsyncStatus::Completed;
if (!asyncOpSucceeded) {
return RejectOnMainThread(__func__, std::move(promiseHolder),
NS_ERROR_FAILURE);
}
ComPtr<IStoreAppLicense> license = nullptr;
hr = asyncSal->GetResults(&license);
if (!SUCCEEDED(hr) || license == nullptr) return NS_ERROR_FAILURE;
GetCampaignIdFromLicenseOnBackgroundThread(std::move(asyncSal),
std::move(promiseHolder),
std::move(campaignId));
HString extendedData;
hr = license->get_ExtendedJsonData(extendedData.GetAddressOf());
if (!SUCCEEDED(hr)) return NS_ERROR_FAILURE;
return S_OK;
});
Json::Value jsonData;
Json::Reader jsonReader;
hr = asyncSal->put_Completed(callback.Get());
if (!SUCCEEDED(hr)) {
RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
return;
}
}
unsigned int tmp;
nsAutoString key(extendedData.GetRawBuffer(&tmp));
if (!jsonReader.parse(NS_ConvertUTF16toUTF8(key).get(), jsonData, false)) {
return NS_ERROR_FAILURE;
}
static void GetCampaignIdFromLicenseOnBackgroundThread(
ComPtr<IAsyncOperation<StoreAppLicense*> > asyncSal,
nsMainThreadPtrHandle<dom::Promise> promiseHolder,
nsAutoString aCampaignId) {
ComPtr<IStoreAppLicense> license = nullptr;
auto hr = asyncSal->GetResults(&license);
if (!SUCCEEDED(hr) || license == nullptr) {
RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
return;
}
if (jsonData.isMember(CAMPAIGN_ID_JSON_FIELD_NAME) &&
jsonData[CAMPAIGN_ID_JSON_FIELD_NAME].isString()) {
aCampaignId.Assign(
NS_ConvertUTF8toUTF16(
jsonData[CAMPAIGN_ID_JSON_FIELD_NAME].asString().c_str())
.get());
if (aCampaignId.Length() > 0) {
aCampaignId.AppendLiteral("&msstoresignedin=false");
}
HString extendedData;
hr = license->get_ExtendedJsonData(extendedData.GetAddressOf());
if (!SUCCEEDED(hr)) {
RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
return;
}
Json::Value jsonData;
Json::Reader jsonReader;
unsigned int tmp;
nsAutoString key(extendedData.GetRawBuffer(&tmp));
if (!jsonReader.parse(NS_ConvertUTF16toUTF8(key).get(), jsonData, false)) {
RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
return;
}
if (jsonData.isMember(CAMPAIGN_ID_JSON_FIELD_NAME) &&
jsonData[CAMPAIGN_ID_JSON_FIELD_NAME].isString()) {
aCampaignId.Assign(
NS_ConvertUTF8toUTF16(
jsonData[CAMPAIGN_ID_JSON_FIELD_NAME].asString().c_str())
.get());
if (aCampaignId.Length() > 0) {
aCampaignId.AppendLiteral("&msstoresignedin=false");
}
}
// No matter what happens in either block above, if they don't exit with a
// failure we managed to successfully pull the campaignId from somewhere
// (even if its empty).
return NS_OK;
#endif // __MINGW32__
DebugOnly<nsresult> rv = NS_DispatchToMainThread(NS_NewRunnableFunction(
__func__, [promiseHolder = std::move(promiseHolder),
aCampaignId = std::move(aCampaignId)]() {
promiseHolder.get()->MaybeResolve(aCampaignId);
}));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed");
}
#endif // __MINGW32__
} // namespace system
} // namespace toolkit