219 lines
7.7 KiB
C++
219 lines
7.7 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "mozilla/glean/bindings/Ping.h"
|
|
|
|
#include "jsapi.h"
|
|
#include "mozilla/AppShutdown.h"
|
|
#include "mozilla/ClearOnShutdown.h"
|
|
#include "mozilla/Components.h"
|
|
#include "mozilla/dom/Promise.h"
|
|
#include "mozilla/dom/Promise-inl.h"
|
|
#include "nsIClassInfoImpl.h"
|
|
#include "nsTHashMap.h"
|
|
#include "nsString.h"
|
|
|
|
#include <memory>
|
|
|
|
namespace mozilla::glean {
|
|
|
|
namespace impl {
|
|
|
|
using CallbackMapType = nsTHashMap<uint32_t, FalliblePingTestCallback>;
|
|
using MetricIdToCallbackMutex = StaticDataMutex<UniquePtr<CallbackMapType>>;
|
|
static Maybe<MetricIdToCallbackMutex::AutoLock> GetCallbackMapLock() {
|
|
static MetricIdToCallbackMutex sCallbacks("sCallbacks");
|
|
auto lock = sCallbacks.Lock();
|
|
// Test callbacks will continue to work until the end of AppShutdownTelemetry
|
|
if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) {
|
|
return Nothing();
|
|
}
|
|
if (!*lock) {
|
|
*lock = MakeUnique<CallbackMapType>();
|
|
RunOnShutdown(
|
|
[&] {
|
|
auto lock = sCallbacks.Lock();
|
|
*lock = nullptr; // deletes, see UniquePtr.h
|
|
},
|
|
ShutdownPhase::XPCOMWillShutdown);
|
|
}
|
|
return Some(std::move(lock));
|
|
}
|
|
|
|
void Ping::Submit(const nsACString& aReason) const {
|
|
(void)SubmitInternal(aReason);
|
|
}
|
|
|
|
nsresult Ping::SubmitInternal(const nsACString& aReason) const {
|
|
nsresult rv = NS_OK;
|
|
{
|
|
auto callback = Maybe<FalliblePingTestCallback>();
|
|
GetCallbackMapLock().apply(
|
|
[&](const auto& lock) { callback = lock.ref()->Extract(mId); });
|
|
// Calling the callback outside of the lock allows it to register a new
|
|
// callback itself.
|
|
if (callback) {
|
|
rv = callback.extract()(aReason);
|
|
}
|
|
}
|
|
fog_submit_ping_by_id(mId, &aReason);
|
|
return rv;
|
|
}
|
|
|
|
void Ping::SetEnabled(bool aValue) const {
|
|
fog_set_ping_enabled_by_id(mId, aValue);
|
|
}
|
|
|
|
void Ping::TestBeforeNextSubmit(PingTestCallback&& aCallback) const {
|
|
TestBeforeNextSubmitFallible(
|
|
[callback = std::move(aCallback)](const nsACString& aReason) -> nsresult {
|
|
callback(aReason);
|
|
return NS_OK;
|
|
});
|
|
}
|
|
|
|
void Ping::TestBeforeNextSubmitFallible(
|
|
FalliblePingTestCallback&& aCallback) const {
|
|
{
|
|
GetCallbackMapLock().apply(
|
|
[&](const auto& lock) { lock.ref()->InsertOrUpdate(mId, aCallback); });
|
|
}
|
|
}
|
|
|
|
bool Ping::TestSubmission(PingTestCallback&& aTestCallback,
|
|
PingSubmitCallback&& aSubmitCallback) const {
|
|
// We could probably get away with passing a reference to the callbacks, but
|
|
// if aSubmitCallback does not actually submit the ping, then we will have
|
|
// created a reference to stack memory that will persist after this function
|
|
// returns.
|
|
auto didCall = std::make_shared<bool>(false);
|
|
|
|
TestBeforeNextSubmit([callback = std::move(aTestCallback),
|
|
didCall](const nsACString& aReason) {
|
|
*didCall = true;
|
|
callback(aReason);
|
|
});
|
|
|
|
aSubmitCallback();
|
|
|
|
return *didCall;
|
|
}
|
|
|
|
} // namespace impl
|
|
|
|
NS_IMPL_CLASSINFO(GleanPing, nullptr, 0, {0})
|
|
NS_IMPL_ISUPPORTS_CI(GleanPing, nsIGleanPing)
|
|
|
|
NS_IMETHODIMP
|
|
GleanPing::Submit(const nsACString& aReason) {
|
|
return mPing.SubmitInternal(aReason);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
GleanPing::SetEnabled(bool aValue) {
|
|
mPing.SetEnabled(aValue);
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
GleanPing::TestBeforeNextSubmit(nsIGleanPingTestCallback* aCallback) {
|
|
if (NS_WARN_IF(!aCallback)) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
// Throw the bare ptr into a COM ptr to keep it around in the lambda.
|
|
mPing.TestBeforeNextSubmitFallible(
|
|
[callback = nsCOMPtr(aCallback)](const nsACString& aReason) -> nsresult {
|
|
return callback->Call(aReason);
|
|
});
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
GleanPing::TestSubmission(nsIGleanPingTestCallback* aTestCallback,
|
|
nsIGleanPingSubmitCallback* aSubmitCallback,
|
|
uint32_t aSubmitTimeoutMs, JSContext* aCx,
|
|
dom::Promise** aOutPromise) {
|
|
if (NS_WARN_IF(!aTestCallback || !aSubmitCallback)) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
IgnoredErrorResult err;
|
|
// This promise will be resolved if `aTestCallback` is called (i.e., the ping
|
|
// is submitted) and doesn't throw.
|
|
RefPtr<dom::Promise> submittedPromise =
|
|
dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), err);
|
|
if (err.Failed()) {
|
|
return err.StealNSResult();
|
|
}
|
|
|
|
// Wrap the callback with one that will resolve or reject `submittedPromise`.
|
|
mPing.TestBeforeNextSubmit([testCallback = nsCOMPtr{aTestCallback},
|
|
submittedPromise](const nsACString& aReason) {
|
|
nsresult rv = testCallback->Call(aReason);
|
|
if (NS_SUCCEEDED(rv)) {
|
|
submittedPromise->MaybeResolveWithUndefined();
|
|
} else {
|
|
// If the callback threw we need to pass that along as a promise
|
|
// rejection.
|
|
submittedPromise->MaybeReject(rv);
|
|
}
|
|
});
|
|
|
|
// Call `aSubmitCallback` to trigger ping submission. This function may or may
|
|
// not be async, but becuase it *can* be, XPConnect will always promise wrap
|
|
// its return value.
|
|
RefPtr<dom::Promise> submitPromise;
|
|
MOZ_TRY(aSubmitCallback->Call(getter_AddRefs(submitPromise)));
|
|
|
|
RefPtr<dom::Promise> thenPromise;
|
|
MOZ_TRY_VAR(thenPromise,
|
|
submitPromise->ThenWithCycleCollectedArgs(
|
|
[aSubmitTimeoutMs](JSContext*, JS::Handle<JS::Value>,
|
|
ErrorResult& aRv,
|
|
RefPtr<dom::Promise> aSubmittedPromise)
|
|
-> already_AddRefed<dom::Promise> {
|
|
// The submit callback has finished successfully now
|
|
// (whether or not it was async).
|
|
if (aSubmitTimeoutMs) {
|
|
// We have a submit timeout, which means that
|
|
// `aSubmitCallback` has triggered the submission through
|
|
// some async means that does not complete before it
|
|
// returns (e.g., idle dispatch from c++). Reject
|
|
// `submittedPromise` after the given timeout to make sure
|
|
// we don't hang forever waiting for a submission that
|
|
// might not happen.
|
|
nsresult rv = NS_DelayedDispatchToCurrentThread(
|
|
NS_NewRunnableFunction(
|
|
__func__,
|
|
[submittedPromise = RefPtr(aSubmittedPromise)]() {
|
|
submittedPromise->MaybeRejectWithTimeoutError(
|
|
"Ping was not submitted after timeout");
|
|
}),
|
|
aSubmitTimeoutMs);
|
|
|
|
if (NS_FAILED(rv)) {
|
|
aSubmittedPromise->MaybeReject(rv);
|
|
}
|
|
} else {
|
|
// If there is no timeout then the ping should have been
|
|
// submitted already. We attempt to reject the promise,
|
|
// which will have no affect if the promise is already
|
|
// settled.
|
|
aSubmittedPromise->MaybeRejectWithOperationError(
|
|
"Ping did not submit immediately");
|
|
}
|
|
|
|
// Chain into `submittedPromise` so that we will
|
|
// resolve/reject appropriately.
|
|
return aSubmittedPromise.forget();
|
|
},
|
|
std::move(submittedPromise)));
|
|
|
|
thenPromise.forget(aOutPromise);
|
|
return NS_OK;
|
|
}
|
|
|
|
} // namespace mozilla::glean
|