/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 "GMPServiceParent.h" #include "GMPService.h" #include "prio.h" #include "base/task.h" #include "mozilla/Logging.h" #include "mozilla/dom/ContentParent.h" #include "GMPParent.h" #include "GMPVideoDecoderParent.h" #include "nsAutoPtr.h" #include "nsIObserverService.h" #include "GeckoChildProcessHost.h" #include "mozilla/Preferences.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/SyncRunnable.h" #include "nsXPCOMPrivate.h" #include "mozilla/Services.h" #include "nsNativeCharsetUtils.h" #include "nsIConsoleService.h" #include "mozilla/Unused.h" #include "GMPDecryptorParent.h" #include "GMPAudioDecoderParent.h" #include "nsComponentManagerUtils.h" #include "runnable_utils.h" #include "VideoUtils.h" #include "nsAppDirectoryServiceDefs.h" #include "nsDirectoryServiceUtils.h" #include "nsDirectoryServiceDefs.h" #include "nsHashKeys.h" #include "nsIFile.h" #include "nsISimpleEnumerator.h" #include "nsIXULRuntime.h" #include "GMPDecoderModule.h" #include #include "MediaPrefs.h" using mozilla::ipc::Transport; namespace mozilla { #ifdef LOG #undef LOG #endif #define LOGD(msg) MOZ_LOG(GetGMPLog(), mozilla::LogLevel::Debug, msg) #define LOG(level, msg) MOZ_LOG(GetGMPLog(), (level), msg) #ifdef __CLASS__ #undef __CLASS__ #endif #define __CLASS__ "GMPService" namespace gmp { static const uint32_t NodeIdSaltLength = 32; already_AddRefed GeckoMediaPluginServiceParent::GetSingleton() { MOZ_ASSERT(XRE_IsParentProcess()); RefPtr service( GeckoMediaPluginServiceParent::GetGeckoMediaPluginService()); #ifdef DEBUG if (service) { nsCOMPtr chromeService; CallQueryInterface(service.get(), getter_AddRefs(chromeService)); MOZ_ASSERT(chromeService); } #endif return service.forget().downcast(); } NS_IMPL_ISUPPORTS_INHERITED(GeckoMediaPluginServiceParent, GeckoMediaPluginService, mozIGeckoMediaPluginChromeService, nsIAsyncShutdownBlocker) GeckoMediaPluginServiceParent::GeckoMediaPluginServiceParent() : mShuttingDown(false) , mScannedPluginOnDisk(false) , mWaitingForPluginsSyncShutdown(false) , mInitPromiseMonitor("GeckoMediaPluginServiceParent::mInitPromiseMonitor") , mLoadPluginsFromDiskComplete(false) , mServiceUserCount(0) { MOZ_ASSERT(NS_IsMainThread()); mInitPromise.SetMonitor(&mInitPromiseMonitor); } GeckoMediaPluginServiceParent::~GeckoMediaPluginServiceParent() { MOZ_ASSERT(mPlugins.IsEmpty()); MOZ_ASSERT(mAsyncShutdownPlugins.IsEmpty()); } int32_t GeckoMediaPluginServiceParent::AsyncShutdownTimeoutMs() { return MediaPrefs::GMPAsyncShutdownTimeout(); } nsresult GeckoMediaPluginServiceParent::Init() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr obsService = mozilla::services::GetObserverService(); MOZ_ASSERT(obsService); MOZ_ALWAYS_SUCCEEDS(obsService->AddObserver(this, "profile-change-teardown", false)); MOZ_ALWAYS_SUCCEEDS(obsService->AddObserver(this, "last-pb-context-exited", false)); MOZ_ALWAYS_SUCCEEDS(obsService->AddObserver(this, "browser:purge-session-history", false)); #ifdef DEBUG MOZ_ALWAYS_SUCCEEDS(obsService->AddObserver(this, "mediakeys-request", false)); #endif nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); if (prefs) { prefs->AddObserver("media.gmp.plugin.crash", this, false); } nsresult rv = InitStorage(); if (NS_FAILED(rv)) { return rv; } // Kick off scanning for plugins nsCOMPtr thread; rv = GetThread(getter_AddRefs(thread)); if (NS_FAILED(rv)) { return rv; } // Detect if GMP storage has an incompatible version, and if so nuke it. int32_t version = Preferences::GetInt("media.gmp.storage.version.observed", 0); int32_t expected = Preferences::GetInt("media.gmp.storage.version.expected", 0); if (version != expected) { Preferences::SetInt("media.gmp.storage.version.observed", expected); return GMPDispatch(NewRunnableMethod( this, &GeckoMediaPluginServiceParent::ClearStorage)); } return NS_OK; } already_AddRefed CloneAndAppend(nsIFile* aFile, const nsAString& aDir) { nsCOMPtr f; nsresult rv = aFile->Clone(getter_AddRefs(f)); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } rv = f->Append(aDir); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } return f.forget(); } static void MoveAndOverwrite(nsIFile* aOldParentDir, nsIFile* aNewParentDir, const nsAString& aSubDir) { nsresult rv; nsCOMPtr srcDir(CloneAndAppend(aOldParentDir, aSubDir)); if (NS_WARN_IF(!srcDir)) { return; } if (!FileExists(srcDir)) { // No sub-directory to be migrated. return; } // Ensure destination parent directory exists. rv = aNewParentDir->Create(nsIFile::DIRECTORY_TYPE, 0700); if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr dstDir(CloneAndAppend(aNewParentDir, aSubDir)); if (FileExists(dstDir)) { // We must have migrated before already, and then ran an old version // of Gecko again which created storage at the old location. Overwrite // the previously migrated storage. rv = dstDir->Remove(true); if (NS_WARN_IF(NS_FAILED(rv))) { // MoveTo will fail. return; } } rv = srcDir->MoveTo(aNewParentDir, EmptyString()); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } static nsresult GMPPlatformString(nsAString& aOutPlatform) { // Append the OS and arch so that we don't reuse the storage if the profile is // copied or used under a different bit-ness, or copied to another platform. nsCOMPtr runtime = do_GetService("@mozilla.org/xre/runtime;1"); if (!runtime) { return NS_ERROR_FAILURE; } nsAutoCString OS; nsresult rv = runtime->GetOS(OS); if (NS_FAILED(rv)) { return rv; } nsAutoCString arch; rv = runtime->GetXPCOMABI(arch); if (NS_FAILED(rv)) { return rv; } nsCString platform; platform.Append(OS); platform.AppendLiteral("_"); platform.Append(arch); aOutPlatform = NS_ConvertUTF8toUTF16(platform); return NS_OK; } nsresult GeckoMediaPluginServiceParent::InitStorage() { MOZ_ASSERT(NS_IsMainThread()); // GMP storage should be used in the chrome process only. if (!XRE_IsParentProcess()) { return NS_OK; } // Directory service is main thread only, so cache the profile dir here // so that we can use it off main thread. nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mStorageBaseDir)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mStorageBaseDir->AppendNative(NS_LITERAL_CSTRING("gmp")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mStorageBaseDir->Create(nsIFile::DIRECTORY_TYPE, 0700); if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS)) { return rv; } nsCOMPtr gmpDirWithoutPlatform; rv = mStorageBaseDir->Clone(getter_AddRefs(gmpDirWithoutPlatform)); if (NS_FAILED(rv)) { return rv; } nsAutoString platform; rv = GMPPlatformString(platform); if (NS_FAILED(rv)) { return rv; } rv = mStorageBaseDir->Append(platform); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mStorageBaseDir->Create(nsIFile::DIRECTORY_TYPE, 0700); if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS)) { return rv; } return GeckoMediaPluginService::Init(); } NS_IMETHODIMP GeckoMediaPluginServiceParent::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aSomeData) { LOGD(("%s::%s topic='%s' data='%s'", __CLASS__, __FUNCTION__, aTopic, NS_ConvertUTF16toUTF8(aSomeData).get())); if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { nsCOMPtr branch( do_QueryInterface(aSubject) ); if (branch) { bool crashNow = false; if (NS_LITERAL_STRING("media.gmp.plugin.crash").Equals(aSomeData)) { branch->GetBoolPref("media.gmp.plugin.crash", &crashNow); } if (crashNow) { nsCOMPtr gmpThread; { MutexAutoLock lock(mMutex); gmpThread = mGMPThread; } if (gmpThread) { gmpThread->Dispatch(WrapRunnable(this, &GeckoMediaPluginServiceParent::CrashPlugins), NS_DISPATCH_NORMAL); } } } } else if (!strcmp("profile-change-teardown", aTopic)) { // How shutdown works: // // Some GMPs require time to do bookkeeping upon shutdown. These GMPs // need to be given time to access storage during shutdown. To signal // that time to shutdown is required, those GMPs implement the // GMPAsyncShutdown interface. // // When we startup the child process, we query the GMP for the // GMPAsyncShutdown interface, and if it's present, we send a message // back to the GMPParent, which then registers the GMPParent by calling // GMPService::AsyncShutdownNeeded(). // // On shutdown, we set mWaitingForPluginsSyncShutdown to true, and then // call UnloadPlugins on the GMPThread, and process events on the main // thread until 1. An event sets mWaitingForPluginsSyncShutdown=false on // the main thread; then 2. All async-shutdown plugins have indicated // they have completed shutdown. // // UnloadPlugins() sends close messages for all plugins' API objects to // the GMP interfaces in the child process, and then sends the async // shutdown notifications to child GMPs. When a GMP has completed its // shutdown, it calls GMPAsyncShutdownHost::ShutdownComplete(), which // sends a message back to the parent, which calls // GMPService::AsyncShutdownComplete(). If all plugins requiring async // shutdown have called AsyncShutdownComplete() we stick a dummy event on // the main thread, where the list of pending plugins is checked. We must // use an event to do this, as we must ensure the main thread processes an // event to run its loop. This will unblock the main thread, and shutdown // of other components will proceed. // // During shutdown, each GMPParent starts a timer, and pretends shutdown // is complete if it is taking too long. // // We shutdown in "profile-change-teardown", as the profile dir is // still writable then, and it's required for GMPStorage. We block the // shutdown process by spinning the main thread event loop until all GMPs // have shutdown, or timeout has occurred. // // GMPStorage needs to work up until the shutdown-complete notification // arrives from the GMP process. mWaitingForPluginsSyncShutdown = true; nsCOMPtr gmpThread; { MutexAutoLock lock(mMutex); MOZ_ASSERT(!mShuttingDown); mShuttingDown = true; gmpThread = mGMPThread; } if (gmpThread) { LOGD(("%s::%s Starting to unload plugins, waiting for first sync shutdown..." , __CLASS__, __FUNCTION__)); gmpThread->Dispatch( NewRunnableMethod(this, &GeckoMediaPluginServiceParent::UnloadPlugins), NS_DISPATCH_NORMAL); // Wait for UnloadPlugins() to do initial sync shutdown... while (mWaitingForPluginsSyncShutdown) { NS_ProcessNextEvent(NS_GetCurrentThread(), true); } // Wait for other plugins (if any) to do async shutdown... auto syncShutdownPluginsRemaining = std::numeric_limits::max(); for (;;) { { MutexAutoLock lock(mMutex); if (mAsyncShutdownPlugins.IsEmpty()) { LOGD(("%s::%s Finished unloading all plugins" , __CLASS__, __FUNCTION__)); break; } else if (mAsyncShutdownPlugins.Length() < syncShutdownPluginsRemaining) { // First time here, or number of pending plugins has decreased. // -> Update list of pending plugins in crash report. syncShutdownPluginsRemaining = mAsyncShutdownPlugins.Length(); LOGD(("%s::%s Still waiting for %d plugins to shutdown..." , __CLASS__, __FUNCTION__, (int)syncShutdownPluginsRemaining)); } } NS_ProcessNextEvent(NS_GetCurrentThread(), true); } } else { // GMP thread has already shutdown. MOZ_ASSERT(mPlugins.IsEmpty()); mWaitingForPluginsSyncShutdown = false; } } else if (!strcmp(NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, aTopic)) { MOZ_ASSERT(mShuttingDown); ShutdownGMPThread(); } else if (!strcmp("last-pb-context-exited", aTopic)) { // When Private Browsing mode exits, all we need to do is clear // mTempNodeIds. This drops all the node ids we've cached in memory // for PB origin-pairs. If we try to open an origin-pair for non-PB // mode, we'll get the NodeId salt stored on-disk, and if we try to // open a PB mode origin-pair, we'll re-generate new salt. mTempNodeIds.Clear(); } else if (!strcmp("browser:purge-session-history", aTopic)) { // Clear everything! if (!aSomeData || nsDependentString(aSomeData).IsEmpty()) { return GMPDispatch(NewRunnableMethod( this, &GeckoMediaPluginServiceParent::ClearStorage)); } // Clear nodeIds/records modified after |t|. nsresult rv; PRTime t = nsDependentString(aSomeData).ToInteger64(&rv, 10); if (NS_FAILED(rv)) { return rv; } return GMPDispatch(NewRunnableMethod( this, &GeckoMediaPluginServiceParent::ClearRecentHistoryOnGMPThread, t)); } return NS_OK; } RefPtr GeckoMediaPluginServiceParent::EnsureInitialized() { MonitorAutoLock lock(mInitPromiseMonitor); if (mLoadPluginsFromDiskComplete) { return GenericPromise::CreateAndResolve(true, __func__); } // We should have an init promise in flight. MOZ_ASSERT(!mInitPromise.IsEmpty()); return mInitPromise.Ensure(__func__); } bool GeckoMediaPluginServiceParent::GetContentParentFrom(GMPCrashHelper* aHelper, const nsACString& aNodeId, const nsCString& aAPI, const nsTArray& aTags, UniquePtr&& aCallback) { RefPtr thread(GetAbstractGMPThread()); if (!thread) { return false; } RefPtr self(this); nsCString nodeId(aNodeId); nsTArray tags(aTags); nsCString api(aAPI); GetGMPContentParentCallback* rawCallback = aCallback.release(); RefPtr helper(aHelper); EnsureInitialized()->Then(thread, __func__, [self, tags, api, nodeId, rawCallback, helper]() -> void { UniquePtr callback(rawCallback); RefPtr gmp = self->SelectPluginForAPI(nodeId, api, tags); LOGD(("%s: %p returning %p for api %s", __FUNCTION__, (void *)self, (void *)gmp, api.get())); if (!gmp) { NS_WARNING("GeckoMediaPluginServiceParent::GetContentParentFrom failed"); callback->Done(nullptr); return; } self->ConnectCrashHelper(gmp->GetPluginId(), helper); gmp->GetGMPContentParent(Move(callback)); }, [rawCallback]() -> void { UniquePtr callback(rawCallback); NS_WARNING("GMPService::EnsureInitialized failed."); callback->Done(nullptr); }); return true; } void GeckoMediaPluginServiceParent::InitializePlugins( AbstractThread* aAbstractGMPThread) { MOZ_ASSERT(aAbstractGMPThread); MonitorAutoLock lock(mInitPromiseMonitor); if (mLoadPluginsFromDiskComplete) { return; } RefPtr self(this); RefPtr p = mInitPromise.Ensure(__func__); InvokeAsync(aAbstractGMPThread, this, __func__, &GeckoMediaPluginServiceParent::LoadFromEnvironment) ->Then(aAbstractGMPThread, __func__, [self]() -> void { MonitorAutoLock lock(self->mInitPromiseMonitor); self->mLoadPluginsFromDiskComplete = true; self->mInitPromise.Resolve(true, __func__); }, [self]() -> void { MonitorAutoLock lock(self->mInitPromiseMonitor); self->mLoadPluginsFromDiskComplete = true; self->mInitPromise.Reject(NS_ERROR_FAILURE, __func__); }); } void GeckoMediaPluginServiceParent::AsyncShutdownNeeded(GMPParent* aParent) { LOGD(("%s::%s %p", __CLASS__, __FUNCTION__, aParent)); MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); MutexAutoLock lock(mMutex); MOZ_ASSERT(!mAsyncShutdownPlugins.Contains(aParent)); mAsyncShutdownPlugins.AppendElement(aParent); } void GeckoMediaPluginServiceParent::AsyncShutdownComplete(GMPParent* aParent) { LOGD(("%s::%s %p '%s'", __CLASS__, __FUNCTION__, aParent, aParent->GetDisplayName().get())); MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); { MutexAutoLock lock(mMutex); mAsyncShutdownPlugins.RemoveElement(aParent); } if (mShuttingDownOnGMPThread) { // The main thread may be waiting for async shutdown of plugins, // one of which has completed. Wake up the main thread by sending a task. nsCOMPtr task(NewRunnableMethod( this, &GeckoMediaPluginServiceParent::NotifyAsyncShutdownComplete)); NS_DispatchToMainThread(task); } } void GeckoMediaPluginServiceParent::NotifyAsyncShutdownComplete() { MOZ_ASSERT(NS_IsMainThread()); // Nothing to do, this task is just used to wake up the event loop in Observe(). } void GeckoMediaPluginServiceParent::NotifySyncShutdownComplete() { MOZ_ASSERT(NS_IsMainThread()); mWaitingForPluginsSyncShutdown = false; } bool GeckoMediaPluginServiceParent::IsShuttingDown() { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); return mShuttingDownOnGMPThread; } void GeckoMediaPluginServiceParent::UnloadPlugins() { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); MOZ_ASSERT(!mShuttingDownOnGMPThread); mShuttingDownOnGMPThread = true; nsTArray> plugins; { MutexAutoLock lock(mMutex); // Move all plugins references to a local array. This way mMutex won't be // locked when calling CloseActive (to avoid inter-locking). Swap(plugins, mPlugins); } LOGD(("%s::%s plugins:%u including async:%u", __CLASS__, __FUNCTION__, plugins.Length(), mAsyncShutdownPlugins.Length())); #ifdef DEBUG for (const auto& plugin : plugins) { LOGD(("%s::%s plugin: '%s'", __CLASS__, __FUNCTION__, plugin->GetDisplayName().get())); } for (const auto& plugin : mAsyncShutdownPlugins) { LOGD(("%s::%s async plugin: '%s'", __CLASS__, __FUNCTION__, plugin->GetDisplayName().get())); } #endif // Note: CloseActive may be async; it could actually finish // shutting down when all the plugins have unloaded. for (const auto& plugin : plugins) { plugin->CloseActive(true); } nsCOMPtr task(NewRunnableMethod( this, &GeckoMediaPluginServiceParent::NotifySyncShutdownComplete)); NS_DispatchToMainThread(task); } void GeckoMediaPluginServiceParent::CrashPlugins() { LOGD(("%s::%s", __CLASS__, __FUNCTION__)); MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); MutexAutoLock lock(mMutex); for (size_t i = 0; i < mPlugins.Length(); i++) { mPlugins[i]->Crash(); } } RefPtr GeckoMediaPluginServiceParent::LoadFromEnvironment() { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); RefPtr thread(GetAbstractGMPThread()); if (!thread) { return GenericPromise::AllPromiseType::CreateAndReject(NS_ERROR_FAILURE, __func__); } const char* env = PR_GetEnv("MOZ_GMP_PATH"); if (!env || !*env) { return GenericPromise::AllPromiseType::CreateAndResolve(true, __func__); } nsString allpaths; if (NS_WARN_IF(NS_FAILED(NS_CopyNativeToUnicode(nsDependentCString(env), allpaths)))) { return GenericPromise::AllPromiseType::CreateAndReject(NS_ERROR_FAILURE, __func__); } nsTArray> promises; uint32_t pos = 0; while (pos < allpaths.Length()) { // Loop over multiple path entries separated by colons (*nix) or // semicolons (Windows) int32_t next = allpaths.FindChar(XPCOM_ENV_PATH_SEPARATOR[0], pos); if (next == -1) { promises.AppendElement(AddOnGMPThread(nsString(Substring(allpaths, pos)))); break; } else { promises.AppendElement(AddOnGMPThread(nsString(Substring(allpaths, pos, next - pos)))); pos = next + 1; } } mScannedPluginOnDisk = true; return GenericPromise::All(thread, promises); } class NotifyObserversTask final : public mozilla::Runnable { public: explicit NotifyObserversTask(const char* aTopic, nsString aData = EmptyString()) : mTopic(aTopic) , mData(aData) {} NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr obsService = mozilla::services::GetObserverService(); MOZ_ASSERT(obsService); if (obsService) { obsService->NotifyObservers(nullptr, mTopic, mData.get()); } return NS_OK; } private: ~NotifyObserversTask() {} const char* mTopic; const nsString mData; }; NS_IMETHODIMP GeckoMediaPluginServiceParent::PathRunnable::Run() { mService->RemoveOnGMPThread(mPath, mOperation == REMOVE_AND_DELETE_FROM_DISK, mDefer); mService->UpdateContentProcessGMPCapabilities(); return NS_OK; } void GeckoMediaPluginServiceParent::UpdateContentProcessGMPCapabilities() { if (!NS_IsMainThread()) { nsCOMPtr task = NewRunnableMethod(this, &GeckoMediaPluginServiceParent::UpdateContentProcessGMPCapabilities); NS_DispatchToMainThread(task); return; } typedef mozilla::dom::GMPCapabilityData GMPCapabilityData; typedef mozilla::dom::GMPAPITags GMPAPITags; typedef mozilla::dom::ContentParent ContentParent; nsTArray caps; { MutexAutoLock lock(mMutex); for (const RefPtr& gmp : mPlugins) { // We have multiple instances of a GMPParent for a given GMP in the // list, one per origin. So filter the list so that we don't include // the same GMP's capabilities twice. NS_ConvertUTF16toUTF8 name(gmp->GetPluginBaseName()); bool found = false; for (const GMPCapabilityData& cap : caps) { if (cap.name().Equals(name)) { found = true; break; } } if (found) { continue; } GMPCapabilityData x; x.name() = name; x.version() = gmp->GetVersion(); for (const GMPCapability& tag : gmp->GetCapabilities()) { x.capabilities().AppendElement(GMPAPITags(tag.mAPIName, tag.mAPITags)); } caps.AppendElement(Move(x)); } } for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { Unused << cp->SendGMPsChanged(caps); } // For non-e10s, we must fire a notification so that any MediaKeySystemAccess // requests waiting on a CDM to download will retry. nsCOMPtr obsService = mozilla::services::GetObserverService(); MOZ_ASSERT(obsService); if (obsService) { obsService->NotifyObservers(nullptr, "gmp-changed", nullptr); } } RefPtr GeckoMediaPluginServiceParent::AsyncAddPluginDirectory(const nsAString& aDirectory) { RefPtr thread(GetAbstractGMPThread()); if (!thread) { return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } nsString dir(aDirectory); RefPtr self = this; return InvokeAsync(thread, this, __func__, &GeckoMediaPluginServiceParent::AddOnGMPThread, dir) ->Then(AbstractThread::MainThread(), __func__, [dir, self]() -> void { LOGD(("GeckoMediaPluginServiceParent::AsyncAddPluginDirectory %s succeeded", NS_ConvertUTF16toUTF8(dir).get())); MOZ_ASSERT(NS_IsMainThread()); self->UpdateContentProcessGMPCapabilities(); }, [dir]() -> void { LOGD(("GeckoMediaPluginServiceParent::AsyncAddPluginDirectory %s failed", NS_ConvertUTF16toUTF8(dir).get())); }) ->CompletionPromise(); } NS_IMETHODIMP GeckoMediaPluginServiceParent::AddPluginDirectory(const nsAString& aDirectory) { MOZ_ASSERT(NS_IsMainThread()); RefPtr p = AsyncAddPluginDirectory(aDirectory); Unused << p; return NS_OK; } NS_IMETHODIMP GeckoMediaPluginServiceParent::RemovePluginDirectory(const nsAString& aDirectory) { MOZ_ASSERT(NS_IsMainThread()); return GMPDispatch(new PathRunnable(this, aDirectory, PathRunnable::EOperation::REMOVE)); } NS_IMETHODIMP GeckoMediaPluginServiceParent::RemoveAndDeletePluginDirectory( const nsAString& aDirectory, const bool aDefer) { MOZ_ASSERT(NS_IsMainThread()); return GMPDispatch( new PathRunnable(this, aDirectory, PathRunnable::EOperation::REMOVE_AND_DELETE_FROM_DISK, aDefer)); } NS_IMETHODIMP GeckoMediaPluginServiceParent::HasPluginForAPI(const nsACString& aAPI, nsTArray* aTags, bool* aHasPlugin) { NS_ENSURE_ARG(aTags && aTags->Length() > 0); NS_ENSURE_ARG(aHasPlugin); nsresult rv = EnsurePluginsOnDiskScanned(); if (NS_FAILED(rv)) { NS_WARNING("Failed to load GMPs from disk."); return rv; } { MutexAutoLock lock(mMutex); nsCString api(aAPI); size_t index = 0; RefPtr gmp = FindPluginForAPIFrom(index, api, *aTags, &index); *aHasPlugin = !!gmp; } return NS_OK; } nsresult GeckoMediaPluginServiceParent::EnsurePluginsOnDiskScanned() { const char* env = nullptr; if (!mScannedPluginOnDisk && (env = PR_GetEnv("MOZ_GMP_PATH")) && *env) { // We have a MOZ_GMP_PATH environment variable which may specify the // location of plugins to load, and we haven't yet scanned the disk to // see if there are plugins there. Get the GMP thread, which will // cause an event to be dispatched to which scans for plugins. We // dispatch a sync event to the GMP thread here in order to wait until // after the GMP thread has scanned any paths in MOZ_GMP_PATH. nsresult rv = GMPDispatch(new mozilla::Runnable(), NS_DISPATCH_SYNC); NS_ENSURE_SUCCESS(rv, rv); MOZ_ASSERT(mScannedPluginOnDisk, "Should have scanned MOZ_GMP_PATH by now"); } return NS_OK; } already_AddRefed GeckoMediaPluginServiceParent::FindPluginForAPIFrom(size_t aSearchStartIndex, const nsCString& aAPI, const nsTArray& aTags, size_t* aOutPluginIndex) { mMutex.AssertCurrentThreadOwns(); for (size_t i = aSearchStartIndex; i < mPlugins.Length(); i++) { RefPtr gmp = mPlugins[i]; if (!GMPCapability::Supports(gmp->GetCapabilities(), aAPI, aTags)) { continue; } if (aOutPluginIndex) { *aOutPluginIndex = i; } return gmp.forget(); } return nullptr; } already_AddRefed GeckoMediaPluginServiceParent::SelectPluginForAPI(const nsACString& aNodeId, const nsCString& aAPI, const nsTArray& aTags) { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread, "Can't clone GMP plugins on non-GMP threads."); GMPParent* gmpToClone = nullptr; { MutexAutoLock lock(mMutex); size_t index = 0; RefPtr gmp; while ((gmp = FindPluginForAPIFrom(index, aAPI, aTags, &index))) { if (aNodeId.IsEmpty()) { if (gmp->CanBeSharedCrossNodeIds()) { return gmp.forget(); } } else if (gmp->CanBeUsedFrom(aNodeId)) { return gmp.forget(); } if (!gmpToClone || (gmpToClone->IsMarkedForDeletion() && !gmp->IsMarkedForDeletion())) { // This GMP has the correct type but has the wrong nodeId; hold on to it // in case we need to clone it. // Prefer GMPs in-use for the case where an upgraded plugin version is // waiting for the old one to die. If the old plugin is in use, we // should continue using it so that any persistent state remains // consistent. Otherwise, just check that the plugin isn't scheduled // for deletion. gmpToClone = gmp; } // Loop around and try the next plugin; it may be usable from aNodeId. index++; } } // Plugin exists, but we can't use it due to cross-origin separation. Create a // new one. if (gmpToClone) { RefPtr clone = ClonePlugin(gmpToClone); { MutexAutoLock lock(mMutex); mPlugins.AppendElement(clone); } if (!aNodeId.IsEmpty()) { clone->SetNodeId(aNodeId); } return clone.forget(); } return nullptr; } RefPtr CreateGMPParent() { return new GMPParent(); } already_AddRefed GeckoMediaPluginServiceParent::ClonePlugin(const GMPParent* aOriginal) { MOZ_ASSERT(aOriginal); RefPtr gmp = CreateGMPParent(); nsresult rv = gmp ? gmp->CloneFrom(aOriginal) : NS_ERROR_NOT_AVAILABLE; if (NS_FAILED(rv)) { NS_WARNING("Can't Create GMPParent"); return nullptr; } return gmp.forget(); } RefPtr GeckoMediaPluginServiceParent::AddOnGMPThread(nsString aDirectory) { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); nsCString dir = NS_ConvertUTF16toUTF8(aDirectory); RefPtr thread(GetAbstractGMPThread()); if (!thread) { LOGD(("%s::%s: %s No GMP Thread", __CLASS__, __FUNCTION__, dir.get())); return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } LOGD(("%s::%s: %s", __CLASS__, __FUNCTION__, dir.get())); nsCOMPtr directory; nsresult rv = NS_NewLocalFile(aDirectory, false, getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } RefPtr gmp = CreateGMPParent(); if (!gmp) { NS_WARNING("Can't Create GMPParent"); return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } RefPtr self(this); return gmp->Init(this, directory)->Then(thread, __func__, [gmp, self, dir]() -> void { LOGD(("%s::%s: %s Succeeded", __CLASS__, __FUNCTION__, dir.get())); { MutexAutoLock lock(self->mMutex); self->mPlugins.AppendElement(gmp); } }, [dir]() -> void { LOGD(("%s::%s: %s Failed", __CLASS__, __FUNCTION__, dir.get())); }) ->CompletionPromise(); } void GeckoMediaPluginServiceParent::RemoveOnGMPThread(const nsAString& aDirectory, const bool aDeleteFromDisk, const bool aCanDefer) { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); LOGD(("%s::%s: %s", __CLASS__, __FUNCTION__, NS_LossyConvertUTF16toASCII(aDirectory).get())); nsCOMPtr directory; nsresult rv = NS_NewLocalFile(aDirectory, false, getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } // Plugin destruction can modify |mPlugins|. Put them aside for now and // destroy them once we're done with |mPlugins|. nsTArray> deadPlugins; bool inUse = false; MutexAutoLock lock(mMutex); for (size_t i = mPlugins.Length(); i-- > 0; ) { nsCOMPtr pluginpath = mPlugins[i]->GetDirectory(); bool equals; if (NS_FAILED(directory->Equals(pluginpath, &equals)) || !equals) { continue; } RefPtr gmp = mPlugins[i]; if (aDeleteFromDisk && gmp->State() != GMPStateNotLoaded) { // We have to wait for the child process to release its lib handle // before we can delete the GMP. inUse = true; gmp->MarkForDeletion(); if (!mPluginsWaitingForDeletion.Contains(aDirectory)) { mPluginsWaitingForDeletion.AppendElement(aDirectory); } } if (gmp->State() == GMPStateNotLoaded || !aCanDefer) { // GMP not in use or shutdown is being forced; can shut it down now. deadPlugins.AppendElement(gmp); mPlugins.RemoveElementAt(i); } } { MutexAutoUnlock unlock(mMutex); for (auto& gmp : deadPlugins) { gmp->AbortAsyncShutdown(); gmp->CloseActive(true); } } if (aDeleteFromDisk && !inUse) { // Ensure the GMP dir and all files in it are writable, so we have // permission to delete them. directory->SetPermissions(0700); DirectoryEnumerator iter(directory, DirectoryEnumerator::FilesAndDirs); for (nsCOMPtr dirEntry; (dirEntry = iter.Next()) != nullptr;) { dirEntry->SetPermissions(0700); } if (NS_SUCCEEDED(directory->Remove(true))) { mPluginsWaitingForDeletion.RemoveElement(aDirectory); NS_DispatchToMainThread(new NotifyObserversTask("gmp-directory-deleted", nsString(aDirectory)), NS_DISPATCH_NORMAL); } } } // May remove when Bug 1043671 is fixed static void Dummy(RefPtr& aOnDeathsDoor) { // exists solely to do nothing and let the Runnable kill the GMPParent // when done. } void GeckoMediaPluginServiceParent::PluginTerminated(const RefPtr& aPlugin) { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); if (aPlugin->IsMarkedForDeletion()) { nsCString path8; RefPtr dir = aPlugin->GetDirectory(); nsresult rv = dir->GetNativePath(path8); NS_ENSURE_SUCCESS_VOID(rv); nsString path = NS_ConvertUTF8toUTF16(path8); if (mPluginsWaitingForDeletion.Contains(path)) { RemoveOnGMPThread(path, true /* delete */, true /* can defer */); } } } void GeckoMediaPluginServiceParent::ReAddOnGMPThread(const RefPtr& aOld) { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); LOGD(("%s::%s: %p", __CLASS__, __FUNCTION__, (void*) aOld)); RefPtr gmp; if (!mShuttingDownOnGMPThread) { // We're not shutting down, so replace the old plugin in the list with a // clone which is in a pristine state. Note: We place the plugin in // the same slot in the array as a hack to ensure if we re-request with // the same capabilities we get an instance of the same plugin. gmp = ClonePlugin(aOld); MutexAutoLock lock(mMutex); MOZ_ASSERT(mPlugins.Contains(aOld)); if (mPlugins.Contains(aOld)) { mPlugins[mPlugins.IndexOf(aOld)] = gmp; } } else { // We're shutting down; don't re-add plugin, let the old plugin die. MutexAutoLock lock(mMutex); mPlugins.RemoveElement(aOld); } // Schedule aOld to be destroyed. We can't destroy it from here since we // may be inside ActorDestroyed() for it. NS_DispatchToCurrentThread(WrapRunnableNM(&Dummy, aOld)); } NS_IMETHODIMP GeckoMediaPluginServiceParent::GetStorageDir(nsIFile** aOutFile) { if (NS_WARN_IF(!mStorageBaseDir)) { return NS_ERROR_FAILURE; } return mStorageBaseDir->Clone(aOutFile); } static nsresult WriteToFile(nsIFile* aPath, const nsCString& aFileName, const nsCString& aData) { nsCOMPtr path; nsresult rv = aPath->Clone(getter_AddRefs(path)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = path->AppendNative(aFileName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } PRFileDesc* f = nullptr; rv = path->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, PR_IRWXU, &f); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int32_t len = PR_Write(f, aData.get(), aData.Length()); PR_Close(f); if (NS_WARN_IF(len < 0 || (size_t)len != aData.Length())) { return NS_ERROR_FAILURE; } return NS_OK; } static nsresult ReadFromFile(nsIFile* aPath, const nsACString& aFileName, nsACString& aOutData, int32_t aMaxLength) { nsCOMPtr path; nsresult rv = aPath->Clone(getter_AddRefs(path)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = path->AppendNative(aFileName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } PRFileDesc* f = nullptr; rv = path->OpenNSPRFileDesc(PR_RDONLY | PR_CREATE_FILE, PR_IRWXU, &f); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } auto size = PR_Seek(f, 0, PR_SEEK_END); PR_Seek(f, 0, PR_SEEK_SET); if (size > aMaxLength) { return NS_ERROR_FAILURE; } aOutData.SetLength(size); auto len = PR_Read(f, aOutData.BeginWriting(), size); PR_Close(f); if (NS_WARN_IF(len != size)) { return NS_ERROR_FAILURE; } return NS_OK; } nsresult ReadSalt(nsIFile* aPath, nsACString& aOutData) { return ReadFromFile(aPath, NS_LITERAL_CSTRING("salt"), aOutData, NodeIdSaltLength); } already_AddRefed GeckoMediaPluginServiceParent::GetMemoryStorageFor(const nsACString& aNodeId) { RefPtr s; if (!mTempGMPStorage.Get(aNodeId, getter_AddRefs(s))) { s = CreateGMPMemoryStorage(); mTempGMPStorage.Put(aNodeId, s); } return s.forget(); } NS_IMETHODIMP GeckoMediaPluginServiceParent::IsPersistentStorageAllowed(const nsACString& aNodeId, bool* aOutAllowed) { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); NS_ENSURE_ARG(aOutAllowed); // We disallow persistent storage for the NodeId used for shared GMP // decoding, to prevent GMP decoding being used to track what a user // watches somehow. *aOutAllowed = !aNodeId.Equals(SHARED_GMP_DECODING_NODE_ID) && mPersistentStorageAllowed.Get(aNodeId); return NS_OK; } nsresult GeckoMediaPluginServiceParent::GetNodeId(const nsAString& aOrigin, const nsAString& aTopLevelOrigin, const nsAString& aGMPName, bool aInPrivateBrowsing, nsACString& aOutId) { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); LOGD(("%s::%s: (%s, %s), %s", __CLASS__, __FUNCTION__, NS_ConvertUTF16toUTF8(aOrigin).get(), NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(), (aInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing"))); nsresult rv; if (aOrigin.EqualsLiteral("null") || aOrigin.IsEmpty() || aTopLevelOrigin.EqualsLiteral("null") || aTopLevelOrigin.IsEmpty()) { // (origin, topLevelOrigin) is null or empty; this is for an anonymous // origin, probably a local file, for which we don't provide persistent storage. // Generate a random node id, and don't store it so that the GMP's storage // is temporary and the process for this GMP is not shared with GMP // instances that have the same nodeId. nsAutoCString salt; rv = GenerateRandomPathName(salt, NodeIdSaltLength); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } aOutId = salt; mPersistentStorageAllowed.Put(salt, false); return NS_OK; } const uint32_t hash = AddToHash(HashString(aOrigin), HashString(aTopLevelOrigin)); if (aInPrivateBrowsing) { // For PB mode, we store the node id, indexed by the origin pair and GMP name, // so that if the same origin pair is opened for the same GMP in this session, // it gets the same node id. const uint32_t pbHash = AddToHash(HashString(aGMPName), hash); nsCString* salt = nullptr; if (!(salt = mTempNodeIds.Get(pbHash))) { // No salt stored, generate and temporarily store some for this id. nsAutoCString newSalt; rv = GenerateRandomPathName(newSalt, NodeIdSaltLength); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } salt = new nsCString(newSalt); mTempNodeIds.Put(pbHash, salt); mPersistentStorageAllowed.Put(*salt, false); } aOutId = *salt; return NS_OK; } // Otherwise, try to see if we've previously generated and stored salt // for this origin pair. nsCOMPtr path; // $profileDir/gmp/$platform/ rv = GetStorageDir(getter_AddRefs(path)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = path->Append(aGMPName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // $profileDir/gmp/$platform/$gmpName/ rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700); if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = path->AppendNative(NS_LITERAL_CSTRING("id")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // $profileDir/gmp/$platform/$gmpName/id/ rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700); if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoCString hashStr; hashStr.AppendInt((int64_t)hash); // $profileDir/gmp/$platform/$gmpName/id/$hash rv = path->AppendNative(hashStr); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700); if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr saltFile; rv = path->Clone(getter_AddRefs(saltFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = saltFile->AppendNative(NS_LITERAL_CSTRING("salt")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoCString salt; bool exists = false; rv = saltFile->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!exists) { // No stored salt for this origin. Generate salt, and store it and // the origin on disk. nsresult rv = GenerateRandomPathName(salt, NodeIdSaltLength); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(salt.Length() == NodeIdSaltLength); // $profileDir/gmp/$platform/$gmpName/id/$hash/salt rv = WriteToFile(path, NS_LITERAL_CSTRING("salt"), salt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // $profileDir/gmp/$platform/$gmpName/id/$hash/origin rv = WriteToFile(path, NS_LITERAL_CSTRING("origin"), NS_ConvertUTF16toUTF8(aOrigin)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // $profileDir/gmp/$platform/$gmpName/id/$hash/topLevelOrigin rv = WriteToFile(path, NS_LITERAL_CSTRING("topLevelOrigin"), NS_ConvertUTF16toUTF8(aTopLevelOrigin)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { rv = ReadSalt(path, salt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } aOutId = salt; mPersistentStorageAllowed.Put(salt, true); return NS_OK; } NS_IMETHODIMP GeckoMediaPluginServiceParent::GetNodeId(const nsAString& aOrigin, const nsAString& aTopLevelOrigin, const nsAString& aGMPName, bool aInPrivateBrowsing, UniquePtr&& aCallback) { nsCString nodeId; nsresult rv = GetNodeId(aOrigin, aTopLevelOrigin, aGMPName, aInPrivateBrowsing, nodeId); aCallback->Done(rv, nodeId); return rv; } static bool ExtractHostName(const nsACString& aOrigin, nsACString& aOutData) { nsCString str; str.Assign(aOrigin); int begin = str.Find("://"); // The scheme is missing! if (begin == -1) { return false; } int end = str.RFind(":"); // Remove the port number if (end != begin) { str.SetLength(end); } nsDependentCSubstring host(str, begin + 3); aOutData.Assign(host); return true; } bool MatchOrigin(nsIFile* aPath, const nsACString& aSite, const mozilla::OriginAttributesPattern& aPattern) { // http://en.wikipedia.org/wiki/Domain_Name_System#Domain_name_syntax static const uint32_t MaxDomainLength = 253; nsresult rv; nsCString str; nsCString originNoSuffix; mozilla::PrincipalOriginAttributes originAttributes; rv = ReadFromFile(aPath, NS_LITERAL_CSTRING("origin"), str, MaxDomainLength); if (!originAttributes.PopulateFromOrigin(str, originNoSuffix)) { // Fails on parsing the originAttributes, treat this as a non-match. return false; } if (NS_SUCCEEDED(rv) && ExtractHostName(originNoSuffix, str) && str.Equals(aSite) && aPattern.Matches(originAttributes)) { return true; } mozilla::PrincipalOriginAttributes topLevelOriginAttributes; rv = ReadFromFile(aPath, NS_LITERAL_CSTRING("topLevelOrigin"), str, MaxDomainLength); if (!topLevelOriginAttributes.PopulateFromOrigin(str, originNoSuffix)) { // Fails on paring the originAttributes, treat this as a non-match. return false; } if (NS_SUCCEEDED(rv) && ExtractHostName(originNoSuffix, str) && str.Equals(aSite) && aPattern.Matches(topLevelOriginAttributes)) { return true; } return false; } template static void KillPlugins(const nsTArray>& aPlugins, Mutex& aMutex, T&& aFilter) { // Shutdown the plugins when |aFilter| evaluates to true. // After we clear storage data, node IDs will become invalid and shouldn't be // used anymore. We need to kill plugins with such nodeIDs. // Note: we can't shut them down while holding the lock, // as the lock is not re-entrant and shutdown requires taking the lock. // The plugin list is only edited on the GMP thread, so this should be OK. nsTArray> pluginsToKill; { MutexAutoLock lock(aMutex); for (size_t i = 0; i < aPlugins.Length(); i++) { RefPtr parent(aPlugins[i]); if (aFilter(parent)) { pluginsToKill.AppendElement(parent); } } } for (size_t i = 0; i < pluginsToKill.Length(); i++) { pluginsToKill[i]->CloseActive(false); // Abort async shutdown because we're going to wipe the plugin's storage, // so we don't want it writing more data in its async shutdown path. pluginsToKill[i]->AbortAsyncShutdown(); } } static nsresult DeleteDir(nsIFile* aPath) { bool exists = false; nsresult rv = aPath->Exists(&exists); if (NS_FAILED(rv)) { return rv; } if (exists) { return aPath->Remove(true); } return NS_OK; } struct NodeFilter { explicit NodeFilter(const nsTArray& nodeIDs) : mNodeIDs(nodeIDs) {} bool operator()(GMPParent* aParent) { return mNodeIDs.Contains(aParent->GetNodeId()); } private: const nsTArray& mNodeIDs; }; void GeckoMediaPluginServiceParent::ClearNodeIdAndPlugin(DirectoryFilter& aFilter) { // $profileDir/gmp/$platform/ nsCOMPtr path; nsresult rv = GetStorageDir(getter_AddRefs(path)); if (NS_FAILED(rv)) { return; } // Iterate all sub-folders of $profileDir/gmp/$platform/, i.e. the dirs in which // specific GMPs store their data. DirectoryEnumerator iter(path, DirectoryEnumerator::DirsOnly); for (nsCOMPtr pluginDir; (pluginDir = iter.Next()) != nullptr;) { ClearNodeIdAndPlugin(pluginDir, aFilter); } } void GeckoMediaPluginServiceParent::ClearNodeIdAndPlugin(nsIFile* aPluginStorageDir, DirectoryFilter& aFilter) { // $profileDir/gmp/$platform/$gmpName/id/ nsCOMPtr path = CloneAndAppend(aPluginStorageDir, NS_LITERAL_STRING("id")); if (!path) { return; } // Iterate all sub-folders of $profileDir/gmp/$platform/$gmpName/id/ nsTArray nodeIDsToClear; DirectoryEnumerator iter(path, DirectoryEnumerator::DirsOnly); for (nsCOMPtr dirEntry; (dirEntry = iter.Next()) != nullptr;) { // dirEntry is the hash of origins, i.e.: // $profileDir/gmp/$platform/$gmpName/id/$originHash/ if (!aFilter(dirEntry)) { continue; } nsAutoCString salt; if (NS_SUCCEEDED(ReadSalt(dirEntry, salt))) { // Keep node IDs to clear data/plugins associated with them later. nodeIDsToClear.AppendElement(salt); // Also remove node IDs from the table. mPersistentStorageAllowed.Remove(salt); } // Now we can remove the directory for the origin pair. if (NS_FAILED(dirEntry->Remove(true))) { NS_WARNING("Failed to delete the directory for the origin pair"); } } // Kill plugin instances that have node IDs being cleared. KillPlugins(mPlugins, mMutex, NodeFilter(nodeIDsToClear)); // Clear all storage in $profileDir/gmp/$platform/$gmpName/storage/$nodeId/ path = CloneAndAppend(aPluginStorageDir, NS_LITERAL_STRING("storage")); if (!path) { return; } for (const nsCString& nodeId : nodeIDsToClear) { nsCOMPtr dirEntry; nsresult rv = path->Clone(getter_AddRefs(dirEntry)); if (NS_FAILED(rv)) { continue; } rv = dirEntry->AppendNative(nodeId); if (NS_FAILED(rv)) { continue; } if (NS_FAILED(DeleteDir(dirEntry))) { NS_WARNING("Failed to delete GMP storage directory for the node"); } } } void GeckoMediaPluginServiceParent::ForgetThisSiteOnGMPThread(const nsACString& aSite, const mozilla::OriginAttributesPattern& aPattern) { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); LOGD(("%s::%s: origin=%s", __CLASS__, __FUNCTION__, aSite.Data())); struct OriginFilter : public DirectoryFilter { explicit OriginFilter(const nsACString& aSite, const mozilla::OriginAttributesPattern& aPattern) : mSite(aSite) , mPattern(aPattern) { } bool operator()(nsIFile* aPath) override { return MatchOrigin(aPath, mSite, mPattern); } private: const nsACString& mSite; const mozilla::OriginAttributesPattern& mPattern; } filter(aSite, aPattern); ClearNodeIdAndPlugin(filter); } void GeckoMediaPluginServiceParent::ClearRecentHistoryOnGMPThread(PRTime aSince) { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); LOGD(("%s::%s: since=%lld", __CLASS__, __FUNCTION__, (int64_t)aSince)); struct MTimeFilter : public DirectoryFilter { explicit MTimeFilter(PRTime aSince) : mSince(aSince) {} // Return true if any files under aPath is modified after |mSince|. bool IsModifiedAfter(nsIFile* aPath) { PRTime lastModified; nsresult rv = aPath->GetLastModifiedTime(&lastModified); if (NS_SUCCEEDED(rv) && lastModified >= mSince) { return true; } DirectoryEnumerator iter(aPath, DirectoryEnumerator::FilesAndDirs); for (nsCOMPtr dirEntry; (dirEntry = iter.Next()) != nullptr;) { if (IsModifiedAfter(dirEntry)) { return true; } } return false; } // |aPath| is $profileDir/gmp/$platform/$gmpName/id/$originHash/ bool operator()(nsIFile* aPath) override { if (IsModifiedAfter(aPath)) { return true; } nsAutoCString salt; if (NS_FAILED(ReadSalt(aPath, salt))) { return false; } // $profileDir/gmp/$platform/$gmpName/id/ nsCOMPtr idDir; if (NS_FAILED(aPath->GetParent(getter_AddRefs(idDir)))) { return false; } // $profileDir/gmp/$platform/$gmpName/ nsCOMPtr temp; if (NS_FAILED(idDir->GetParent(getter_AddRefs(temp)))) { return false; } // $profileDir/gmp/$platform/$gmpName/storage/ if (NS_FAILED(temp->Append(NS_LITERAL_STRING("storage")))) { return false; } // $profileDir/gmp/$platform/$gmpName/storage/$originSalt return NS_SUCCEEDED(temp->AppendNative(salt)) && IsModifiedAfter(temp); } private: const PRTime mSince; } filter(aSince); ClearNodeIdAndPlugin(filter); NS_DispatchToMainThread(new NotifyObserversTask("gmp-clear-storage-complete"), NS_DISPATCH_NORMAL); } NS_IMETHODIMP GeckoMediaPluginServiceParent::ForgetThisSite(const nsAString& aSite, const nsAString& aPattern) { MOZ_ASSERT(NS_IsMainThread()); mozilla::OriginAttributesPattern pattern; if (!pattern.Init(aPattern)) { return NS_ERROR_INVALID_ARG; } return ForgetThisSiteNative(aSite, pattern); } nsresult GeckoMediaPluginServiceParent::ForgetThisSiteNative(const nsAString& aSite, const mozilla::OriginAttributesPattern& aPattern) { MOZ_ASSERT(NS_IsMainThread()); return GMPDispatch(NewRunnableMethod( this, &GeckoMediaPluginServiceParent::ForgetThisSiteOnGMPThread, NS_ConvertUTF16toUTF8(aSite), aPattern)); } static bool IsNodeIdValid(GMPParent* aParent) { return !aParent->GetNodeId().IsEmpty(); } static nsCOMPtr GetShutdownBarrier() { nsCOMPtr svc = services::GetAsyncShutdown(); MOZ_RELEASE_ASSERT(svc); nsCOMPtr barrier; nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(barrier)); MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); MOZ_RELEASE_ASSERT(barrier); return barrier.forget(); } NS_IMETHODIMP GeckoMediaPluginServiceParent::GetName(nsAString& aName) { aName = NS_LITERAL_STRING("GeckoMediaPluginServiceParent: shutdown"); return NS_OK; } NS_IMETHODIMP GeckoMediaPluginServiceParent::GetState(nsIPropertyBag**) { return NS_OK; } NS_IMETHODIMP GeckoMediaPluginServiceParent::BlockShutdown(nsIAsyncShutdownClient*) { return NS_OK; } void GeckoMediaPluginServiceParent::ServiceUserCreated() { MOZ_ASSERT(mServiceUserCount >= 0); if (++mServiceUserCount == 1) { nsresult rv = GetShutdownBarrier()->AddBlocker( this, NS_LITERAL_STRING(__FILE__), __LINE__, NS_LITERAL_STRING("GeckoMediaPluginServiceParent shutdown")); MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); } } void GeckoMediaPluginServiceParent::ServiceUserDestroyed() { MOZ_ASSERT(mServiceUserCount > 0); if (--mServiceUserCount == 0) { nsresult rv = GetShutdownBarrier()->RemoveBlocker(this); MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); } } void GeckoMediaPluginServiceParent::ClearStorage() { MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread); LOGD(("%s::%s", __CLASS__, __FUNCTION__)); // Kill plugins with valid nodeIDs. KillPlugins(mPlugins, mMutex, &IsNodeIdValid); nsCOMPtr path; // $profileDir/gmp/$platform/ nsresult rv = GetStorageDir(getter_AddRefs(path)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } if (NS_FAILED(DeleteDir(path))) { NS_WARNING("Failed to delete GMP storage directory"); } // Clear private-browsing storage. mTempGMPStorage.Clear(); NS_DispatchToMainThread(new NotifyObserversTask("gmp-clear-storage-complete"), NS_DISPATCH_NORMAL); } already_AddRefed GeckoMediaPluginServiceParent::GetById(uint32_t aPluginId) { MutexAutoLock lock(mMutex); for (const RefPtr& gmp : mPlugins) { if (gmp->GetPluginId() == aPluginId) { return do_AddRef(gmp); } } return nullptr; } GMPServiceParent::~GMPServiceParent() { NS_DispatchToMainThread( NewRunnableMethod(mService.get(), &GeckoMediaPluginServiceParent::ServiceUserDestroyed)); } bool GMPServiceParent::RecvSelectGMP(const nsCString& aNodeId, const nsCString& aAPI, nsTArray&& aTags, uint32_t* aOutPluginId, nsresult* aOutRv) { if (mService->IsShuttingDown()) { *aOutRv = NS_ERROR_ILLEGAL_DURING_SHUTDOWN; return true; } RefPtr gmp = mService->SelectPluginForAPI(aNodeId, aAPI, aTags); if (gmp) { *aOutPluginId = gmp->GetPluginId(); *aOutRv = NS_OK; } else { *aOutRv = NS_ERROR_FAILURE; } nsCString api = aTags[0]; LOGD(("%s: %p returning %p for api %s", __FUNCTION__, (void *)this, (void *)gmp, api.get())); return true; } bool GMPServiceParent::RecvLaunchGMP(const uint32_t& aPluginId, nsTArray&& aAlreadyBridgedTo, ProcessId* aOutProcessId, nsCString* aOutDisplayName, nsresult* aOutRv) { *aOutRv = NS_OK; if (mService->IsShuttingDown()) { *aOutRv = NS_ERROR_ILLEGAL_DURING_SHUTDOWN; return true; } RefPtr gmp(mService->GetById(aPluginId)); if (!gmp) { *aOutRv = NS_ERROR_FAILURE; return true; } if (!gmp->EnsureProcessLoaded(aOutProcessId)) { return false; } *aOutDisplayName = gmp->GetDisplayName(); return aAlreadyBridgedTo.Contains(*aOutProcessId) || gmp->Bridge(this); } bool GMPServiceParent::RecvGetGMPNodeId(const nsString& aOrigin, const nsString& aTopLevelOrigin, const nsString& aGMPName, const bool& aInPrivateBrowsing, nsCString* aID) { nsresult rv = mService->GetNodeId(aOrigin, aTopLevelOrigin, aGMPName, aInPrivateBrowsing, *aID); return NS_SUCCEEDED(rv); } class DeleteGMPServiceParent : public mozilla::Runnable { public: explicit DeleteGMPServiceParent(GMPServiceParent* aToDelete) : mToDelete(aToDelete) { } NS_IMETHOD Run() override { return NS_OK; } private: nsAutoPtr mToDelete; }; void GMPServiceParent::CloseTransport(Monitor* aSyncMonitor, bool* aCompleted) { MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); MonitorAutoLock lock(*aSyncMonitor); // This deletes the transport. SetTransport(nullptr); *aCompleted = true; lock.NotifyAll(); } void GMPServiceParent::ActorDestroy(ActorDestroyReason aWhy) { Monitor monitor("DeleteGMPServiceParent"); bool completed = false; // Make sure the IPC channel is closed before destroying mToDelete. MonitorAutoLock lock(monitor); RefPtr task = NewNonOwningRunnableMethod(this, &GMPServiceParent::CloseTransport, &monitor, &completed); XRE_GetIOMessageLoop()->PostTask(Move(task.forget())); while (!completed) { lock.Wait(); } NS_DispatchToCurrentThread(new DeleteGMPServiceParent(this)); } class OpenPGMPServiceParent : public mozilla::Runnable { public: OpenPGMPServiceParent(GMPServiceParent* aGMPServiceParent, mozilla::ipc::Transport* aTransport, base::ProcessId aOtherPid, bool* aResult) : mGMPServiceParent(aGMPServiceParent), mTransport(aTransport), mOtherPid(aOtherPid), mResult(aResult) { } NS_IMETHOD Run() override { *mResult = mGMPServiceParent->Open(mTransport, mOtherPid, XRE_GetIOMessageLoop(), ipc::ParentSide); return NS_OK; } private: GMPServiceParent* mGMPServiceParent; mozilla::ipc::Transport* mTransport; base::ProcessId mOtherPid; bool* mResult; }; /* static */ PGMPServiceParent* GMPServiceParent::Create(Transport* aTransport, ProcessId aOtherPid) { RefPtr gmp = GeckoMediaPluginServiceParent::GetSingleton(); if (gmp->mShuttingDown) { // Shutdown is initiated. There is no point creating a new actor. return nullptr; } nsCOMPtr gmpThread; nsresult rv = gmp->GetThread(getter_AddRefs(gmpThread)); NS_ENSURE_SUCCESS(rv, nullptr); nsAutoPtr serviceParent(new GMPServiceParent(gmp)); bool ok; rv = gmpThread->Dispatch(new OpenPGMPServiceParent(serviceParent, aTransport, aOtherPid, &ok), NS_DISPATCH_SYNC); if (NS_FAILED(rv) || !ok) { return nullptr; } return serviceParent.forget(); } } // namespace gmp } // namespace mozilla