summaryrefslogtreecommitdiffstats
path: root/toolkit/components/processtools
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/processtools')
-rw-r--r--toolkit/components/processtools/Cargo.toml14
-rw-r--r--toolkit/components/processtools/ProcInfo.h239
-rw-r--r--toolkit/components/processtools/ProcInfo.mm164
-rw-r--r--toolkit/components/processtools/ProcInfo_linux.cpp298
-rw-r--r--toolkit/components/processtools/ProcInfo_win.cpp193
-rw-r--r--toolkit/components/processtools/ProcessToolsService.cpp33
-rw-r--r--toolkit/components/processtools/ProcessToolsService.h8
-rw-r--r--toolkit/components/processtools/components.conf10
-rw-r--r--toolkit/components/processtools/moz.build46
-rw-r--r--toolkit/components/processtools/nsIProcessToolsService.idl40
-rw-r--r--toolkit/components/processtools/src/lib.rs107
-rw-r--r--toolkit/components/processtools/tests/browser/browser.ini9
-rw-r--r--toolkit/components/processtools/tests/browser/browser_test_procinfo.js173
-rw-r--r--toolkit/components/processtools/tests/browser/dummy.html20
-rw-r--r--toolkit/components/processtools/tests/xpcshell/test_process_kill.js57
-rw-r--r--toolkit/components/processtools/tests/xpcshell/xpcshell.ini6
16 files changed, 1417 insertions, 0 deletions
diff --git a/toolkit/components/processtools/Cargo.toml b/toolkit/components/processtools/Cargo.toml
new file mode 100644
index 0000000000..90e1077125
--- /dev/null
+++ b/toolkit/components/processtools/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "processtools"
+version = "0.1.0"
+authors = ["David Teller <dteller@mozilla.com>"]
+
+[dependencies]
+nserror = { path = "../../../xpcom/rust/nserror" }
+xpcom = { path = "../../../xpcom/rust/xpcom" }
+
+[target.'cfg(windows)'.dependencies]
+winapi = "0.3.7"
+
+[target.'cfg(unix)'.dependencies]
+libc = "0.2"
diff --git a/toolkit/components/processtools/ProcInfo.h b/toolkit/components/processtools/ProcInfo.h
new file mode 100644
index 0000000000..8f0565086c
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo.h
@@ -0,0 +1,239 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#ifndef __mozilla_ProcInfo_h
+#define __mozilla_ProcInfo_h
+
+#include <base/process.h>
+#include <stdint.h>
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/ChromeUtilsBinding.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/HashTable.h"
+#include "mozilla/MozPromise.h"
+
+namespace mozilla {
+
+namespace ipc {
+class GeckoChildProcessHost;
+}
+
+// Process types. When updating this enum, please make sure to update
+// WebIDLProcType, ChromeUtils::RequestProcInfo and ProcTypeToWebIDL to
+// mirror the changes.
+enum class ProcType {
+ // These must match the ones in ContentParent.h, and E10SUtils.jsm
+ Web,
+ WebIsolated,
+ File,
+ Extension,
+ PrivilegedAbout,
+ PrivilegedMozilla,
+ WebLargeAllocation,
+ WebCOOPCOEP,
+ // the rest matches GeckoProcessTypes.h
+ Browser, // Default is named Browser here
+ Plugin,
+ IPDLUnitTest,
+ GMPlugin,
+ GPU,
+ VR,
+ RDD,
+ Socket,
+ RemoteSandboxBroker,
+#ifdef MOZ_ENABLE_FORKSERVER
+ ForkServer,
+#endif
+ Preallocated,
+ // Unknown type of process
+ Unknown,
+ Max = Unknown,
+};
+
+struct ThreadInfo {
+ // Thread Id.
+ base::ProcessId tid = 0;
+ // Thread name, if any.
+ nsString name;
+ // User time in ns.
+ uint64_t cpuUser = 0;
+ // System time in ns.
+ uint64_t cpuKernel = 0;
+};
+
+// Info on a DOM window.
+struct WindowInfo {
+ explicit WindowInfo()
+ : outerWindowId(0),
+ documentURI(nullptr),
+ documentTitle(u""_ns),
+ isProcessRoot(false),
+ isInProcess(false) {}
+ WindowInfo(uint64_t aOuterWindowId, nsIURI* aDocumentURI,
+ nsAString&& aDocumentTitle, bool aIsProcessRoot, bool aIsInProcess)
+ : outerWindowId(aOuterWindowId),
+ documentURI(aDocumentURI),
+ documentTitle(std::move(aDocumentTitle)),
+ isProcessRoot(aIsProcessRoot),
+ isInProcess(aIsInProcess) {}
+
+ // Internal window id.
+ const uint64_t outerWindowId;
+
+ // URI of the document.
+ const nsCOMPtr<nsIURI> documentURI;
+
+ // Title of the document.
+ const nsString documentTitle;
+
+ // True if this is the toplevel window of the process.
+ // Note that this may be an iframe from another process.
+ const bool isProcessRoot;
+
+ const bool isInProcess;
+};
+
+struct ProcInfo {
+ // Process Id
+ base::ProcessId pid = 0;
+ // Child Id as defined by Firefox when a child process is created.
+ dom::ContentParentId childId;
+ // Process type
+ ProcType type;
+ // Origin, if any
+ nsCString origin;
+ // Process filename (without the path name).
+ nsString filename;
+ // RSS in bytes.
+ int64_t residentSetSize = 0;
+ // Unshared resident size in bytes.
+ int64_t residentUniqueSize = 0;
+ // User time in ns.
+ uint64_t cpuUser = 0;
+ // System time in ns.
+ uint64_t cpuKernel = 0;
+ // Threads owned by this process.
+ CopyableTArray<ThreadInfo> threads;
+ // DOM windows represented by this process.
+ CopyableTArray<WindowInfo> windows;
+};
+
+typedef MozPromise<mozilla::HashMap<base::ProcessId, ProcInfo>, nsresult, true>
+ ProcInfoPromise;
+
+/**
+ * Data we need to request process info (e.g. CPU usage, memory usage)
+ * from the operating system and populate the resulting `ProcInfo`.
+ *
+ * Note that this structure contains a mix of:
+ * - low-level handles that we need to request low-level process info
+ * (`aChildTask` on macOS, `aPid` on other platforms); and
+ * - high-level data that we already acquired while looking for
+ * `aPid`/`aChildTask` and that we will need further down the road.
+ */
+struct ProcInfoRequest {
+ ProcInfoRequest(base::ProcessId aPid, ProcType aProcessType,
+ const nsACString& aOrigin, nsTArray<WindowInfo>&& aWindowInfo,
+ uint32_t aChildId = 0
+#ifdef XP_MACOSX
+ ,
+ mach_port_t aChildTask = 0
+#endif // XP_MACOSX
+ )
+ : pid(aPid),
+ processType(aProcessType),
+ origin(aOrigin),
+ windowInfo(std::move(aWindowInfo)),
+ childId(aChildId)
+#ifdef XP_MACOSX
+ ,
+ childTask(aChildTask)
+#endif // XP_MACOSX
+ {
+ }
+ const base::ProcessId pid;
+ const ProcType processType;
+ const nsCString origin;
+ const nsTArray<WindowInfo> windowInfo;
+ // If the process is a child, its child id, otherwise `0`.
+ const int32_t childId;
+#ifdef XP_MACOSX
+ const mach_port_t childTask;
+#endif // XP_MACOSX
+};
+
+/**
+ * Batch a request for low-level information on Gecko processes.
+ *
+ * # Request
+ *
+ * Argument `aRequests` is a list of processes, along with high-level data
+ * we have already obtained on them and that we need to populate the
+ * resulting array of `ProcInfo`.
+ *
+ * # Result
+ *
+ * This call succeeds (possibly with missing data, see below) unless we
+ * cannot allocate memory.
+ *
+ * # Performance
+ *
+ * - This call is always executed on a background thread.
+ * - This call does NOT wake up children processes.
+ * - This function is sometimes observably slow to resolve, in particular
+ * under Windows.
+ *
+ * # Error-handling and race conditions
+ *
+ * Requesting low-level information on a process and its threads is inherently
+ * subject to race conditions. Typically, if a process or a thread is killed
+ * while we're preparing to fetch information, we can easily end up with
+ * system/lib calls that return failures.
+ *
+ * For this reason, this API assumes that errors when placing a system/lib call
+ * are likely and normal. When some information cannot be obtained, the API will
+ * simply skip over said information.
+ *
+ * Note that due to different choices by OSes, the exact information we skip may
+ * vary across platforms. For instance, under Unix, failing to access the
+ * threads of a process will cause us to skip all data on the process, while
+ * under Windows, process information will be returned without thread
+ * information.
+ */
+RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests);
+
+/**
+ * Utility function: copy data from a `ProcInfo` and into either a
+ * `ParentProcInfoDictionary` or a `ChildProcInfoDictionary`.
+ */
+template <typename T>
+nsresult CopySysProcInfoToDOM(const ProcInfo& source, T* dest) {
+ // Copy system info.
+ dest->mPid = source.pid;
+ dest->mFilename.Assign(source.filename);
+ dest->mResidentSetSize = source.residentSetSize;
+ dest->mResidentUniqueSize = source.residentUniqueSize;
+ dest->mCpuUser = source.cpuUser;
+ dest->mCpuKernel = source.cpuKernel;
+
+ // Copy thread info.
+ mozilla::dom::Sequence<mozilla::dom::ThreadInfoDictionary> threads;
+ for (const ThreadInfo& entry : source.threads) {
+ mozilla::dom::ThreadInfoDictionary* thread =
+ threads.AppendElement(fallible);
+ if (NS_WARN_IF(!thread)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ thread->mCpuUser = entry.cpuUser;
+ thread->mCpuKernel = entry.cpuKernel;
+ thread->mTid = entry.tid;
+ thread->mName.Assign(entry.name);
+ }
+ dest->mThreads = std::move(threads);
+ return NS_OK;
+}
+
+} // namespace mozilla
+#endif // ProcInfo_h
diff --git a/toolkit/components/processtools/ProcInfo.mm b/toolkit/components/processtools/ProcInfo.mm
new file mode 100644
index 0000000000..077692d9e4
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo.mm
@@ -0,0 +1,164 @@
+/* -*- 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/ProcInfo.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+
+#include "nsMemoryReporterManager.h"
+#include "nsNetCID.h"
+
+#include <cstdio>
+#include <cstring>
+#include <unistd.h>
+
+#include <libproc.h>
+#include <sys/sysctl.h>
+#include <mach/mach.h>
+
+namespace mozilla {
+
+RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests) {
+ auto holder = MakeUnique<MozPromiseHolder<ProcInfoPromise>>();
+ RefPtr<ProcInfoPromise> promise = holder->Ensure(__func__);
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get stream transport service");
+ holder->Reject(rv, __func__);
+ return promise;
+ }
+
+ RefPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__, [holder = std::move(holder), requests = std::move(aRequests)]() {
+ HashMap<base::ProcessId, ProcInfo> gathered;
+ if (!gathered.reserve(requests.Length())) {
+ holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+ for (const auto& request : requests) {
+ ProcInfo info;
+ info.pid = request.pid;
+ info.childId = request.childId;
+ info.type = request.processType;
+ info.origin = std::move(request.origin);
+ info.windows = std::move(request.windowInfo);
+ struct proc_bsdinfo proc;
+ if ((unsigned long)proc_pidinfo(request.pid, PROC_PIDTBSDINFO, 0, &proc,
+ PROC_PIDTBSDINFO_SIZE) < PROC_PIDTBSDINFO_SIZE) {
+ // Can't read data for this proc.
+ // Probably either a sandboxing issue or a race condition, e.g.
+ // the process has been just been killed. Regardless, skip process.
+ continue;
+ }
+
+ struct proc_taskinfo pti;
+ if ((unsigned long)proc_pidinfo(request.pid, PROC_PIDTASKINFO, 0, &pti,
+ PROC_PIDTASKINFO_SIZE) < PROC_PIDTASKINFO_SIZE) {
+ continue;
+ }
+
+ // copying all the info to the ProcInfo struct
+ info.filename.AssignASCII(proc.pbi_name);
+ info.residentSetSize = pti.pti_resident_size;
+ info.cpuUser = pti.pti_total_user;
+ info.cpuKernel = pti.pti_total_system;
+
+ mach_port_t selectedTask;
+ // If we did not get a task from a child process, we use mach_task_self()
+ if (request.childTask == MACH_PORT_NULL) {
+ selectedTask = mach_task_self();
+ } else {
+ selectedTask = request.childTask;
+ }
+ // Computing the resident unique size is somewhat tricky,
+ // so we use about:memory's implementation. This implementation
+ // uses the `task` so, in theory, should be no additional
+ // race condition. However, in case of error, the result is `0`.
+ info.residentUniqueSize = nsMemoryReporterManager::ResidentUnique(selectedTask);
+
+ // Now getting threads info
+
+ // task_threads() gives us a snapshot of the process threads
+ // but those threads can go away. All the code below makes
+ // the assumption that thread_info() calls may fail, and
+ // these errors will be ignored.
+ thread_act_port_array_t threadList;
+ mach_msg_type_number_t threadCount;
+ kern_return_t kret = task_threads(selectedTask, &threadList, &threadCount);
+ if (kret != KERN_SUCCESS) {
+ // For some reason, we have no data on the threads for this process.
+ // Most likely reason is that we have just lost a race condition and
+ // the process is dead.
+ // Let's stop here and ignore the entire process.
+ continue;
+ }
+
+ // Deallocate the thread list.
+ // Note that this deallocation is entirely undocumented, so the following code is based
+ // on guesswork and random examples found on the web.
+ auto guardThreadCount = MakeScopeExit([&] {
+ if (threadList == nullptr) {
+ return;
+ }
+ // Free each thread to avoid leaks.
+ for (mach_msg_type_number_t i = 0; i < threadCount; i++) {
+ mach_port_deallocate(mach_task_self(), threadList[i]);
+ }
+ vm_deallocate(mach_task_self(), /* address */ (vm_address_t)threadList,
+ /* size */ sizeof(thread_t) * threadCount);
+ });
+
+ mach_msg_type_number_t count;
+
+ for (mach_msg_type_number_t i = 0; i < threadCount; i++) {
+ // Basic thread info.
+ thread_extended_info_data_t threadInfoData;
+ count = THREAD_EXTENDED_INFO_COUNT;
+ kret = thread_info(threadList[i], THREAD_EXTENDED_INFO, (thread_info_t)&threadInfoData,
+ &count);
+ if (kret != KERN_SUCCESS) {
+ continue;
+ }
+
+ // Getting the thread id.
+ thread_identifier_info identifierInfo;
+ count = THREAD_IDENTIFIER_INFO_COUNT;
+ kret = thread_info(threadList[i], THREAD_IDENTIFIER_INFO,
+ (thread_info_t)&identifierInfo, &count);
+ if (kret != KERN_SUCCESS) {
+ continue;
+ }
+
+ // The two system calls were successful, let's add that thread
+ ThreadInfo* thread = info.threads.AppendElement(fallible);
+ if (!thread) {
+ holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+ thread->cpuUser = threadInfoData.pth_user_time;
+ thread->cpuKernel = threadInfoData.pth_system_time;
+ thread->name.AssignASCII(threadInfoData.pth_name);
+ thread->tid = identifierInfo.thread_id;
+ }
+
+ if (!gathered.put(request.pid, std::move(info))) {
+ holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+ }
+ // ... and we're done!
+ holder->Resolve(std::move(gathered), __func__);
+ });
+
+ rv = target->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the LoadDataRunnable.");
+ }
+ return promise;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/processtools/ProcInfo_linux.cpp b/toolkit/components/processtools/ProcInfo_linux.cpp
new file mode 100644
index 0000000000..4483147445
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo_linux.cpp
@@ -0,0 +1,298 @@
+/* -*- 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/ProcInfo.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "nsLocalFile.h"
+#include "nsMemoryReporterManager.h"
+#include "nsNetCID.h"
+#include "nsWhitespaceTokenizer.h"
+
+#include <cstdio>
+#include <cstring>
+#include <unistd.h>
+#include <dirent.h>
+
+#define NANOPERSEC 1000000000.
+
+namespace mozilla {
+
+// StatReader can parse and tokenize a POSIX stat file.
+// see http://man7.org/linux/man-pages/man5/proc.5.html
+//
+// Its usage is quite simple:
+//
+// StatReader reader(pid);
+// ProcInfo info;
+// rv = reader.ParseProc(info);
+// if (NS_FAILED(rv)) {
+// // the reading of the file or its parsing failed.
+// }
+//
+class StatReader {
+ public:
+ explicit StatReader(const base::ProcessId aPid)
+ : mPid(aPid), mMaxIndex(53), mTicksPerSec(sysconf(_SC_CLK_TCK)) {
+ mFilepath.AppendPrintf("/proc/%u/stat", mPid);
+ }
+
+ nsresult ParseProc(ProcInfo& aInfo) {
+ nsAutoString fileContent;
+ nsresult rv = ReadFile(fileContent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // We first extract the filename
+ int32_t startPos = fileContent.RFindChar('(');
+ if (startPos == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ int32_t endPos = fileContent.RFindChar(')');
+ if (endPos == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ int32_t len = endPos - (startPos + 1);
+ aInfo.filename.Assign(Substring(fileContent, startPos + 1, len));
+
+ // now we can use the tokenizer for the rest of the file
+ nsWhitespaceTokenizer tokenizer(Substring(fileContent, endPos + 2));
+ int32_t index = 2; // starting at third field
+ while (tokenizer.hasMoreTokens() && index < mMaxIndex) {
+ const nsAString& token = tokenizer.nextToken();
+ rv = UseToken(index, token, aInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+ index++;
+ }
+ return NS_OK;
+ }
+
+ protected:
+ // Called for each token found in the stat file.
+ nsresult UseToken(int32_t aIndex, const nsAString& aToken, ProcInfo& aInfo) {
+ // We're using a subset of what stat has to offer for now.
+ nsresult rv = NS_OK;
+ // see the proc documentation for fields index references.
+ switch (aIndex) {
+ case 13:
+ // Amount of time that this process has been scheduled
+ // in user mode, measured in clock ticks
+ aInfo.cpuUser = GetCPUTime(aToken, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case 14:
+ // Amount of time that this process has been scheduled
+ // in kernel mode, measured in clock ticks
+ aInfo.cpuKernel = GetCPUTime(aToken, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case 23:
+ // Resident Set Size: number of pages the process has
+ // in real memory.
+ uint64_t pageCount = Get64Value(aToken, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint64_t pageSize = sysconf(_SC_PAGESIZE);
+ aInfo.residentSetSize = pageCount * pageSize;
+ break;
+ }
+ return rv;
+ }
+
+ // Converts a token into a int64_t
+ uint64_t Get64Value(const nsAString& aToken, nsresult* aRv) {
+ // We can't use aToken.ToInteger64() since it returns a signed 64.
+ // and that can result into an overflow.
+ nsresult rv = NS_OK;
+ uint64_t out = 0;
+ if (sscanf(NS_ConvertUTF16toUTF8(aToken).get(), "%" PRIu64, &out) == 0) {
+ rv = NS_ERROR_FAILURE;
+ }
+ *aRv = rv;
+ return out;
+ }
+
+ // Converts a token into CPU time in nanoseconds.
+ uint64_t GetCPUTime(const nsAString& aToken, nsresult* aRv) {
+ nsresult rv;
+ uint64_t value = Get64Value(aToken, &rv);
+ *aRv = rv;
+ if (NS_FAILED(rv)) {
+ return 0;
+ }
+ if (value) {
+ value = (value * NANOPERSEC) / mTicksPerSec;
+ }
+ return value;
+ }
+
+ base::ProcessId mPid;
+ int32_t mMaxIndex;
+ nsCString mFilepath;
+ ProcInfo mProcInfo;
+
+ private:
+ // Reads the stat file and puts its content in a nsString.
+ nsresult ReadFile(nsAutoString& aFileContent) {
+ RefPtr<nsLocalFile> file = new nsLocalFile(mFilepath);
+ bool exists;
+ nsresult rv = file->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) {
+ return NS_ERROR_FAILURE;
+ }
+ // /proc is a virtual file system and all files are
+ // of size 0, so GetFileSize() and related functions will
+ // return 0 - so the way to read the file is to fill a buffer
+ // of an arbitrary big size and look for the end of line char.
+ FILE* fstat;
+ if (NS_FAILED(file->OpenANSIFileDesc("r", &fstat)) || !fstat) {
+ return NS_ERROR_FAILURE;
+ }
+ char buffer[2048];
+ char* end;
+ char* start = fgets(buffer, 2048, fstat);
+ fclose(fstat);
+ if (start == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+ // let's find the end
+ end = strchr(buffer, '\n');
+ if (!end) {
+ return NS_ERROR_FAILURE;
+ }
+ aFileContent.AssignASCII(buffer, size_t(end - start));
+ return NS_OK;
+ }
+
+ int64_t mTicksPerSec;
+};
+
+// Threads have the same stat file. The only difference is its path
+// and we're getting less info in the ThreadInfo structure.
+class ThreadInfoReader final : public StatReader {
+ public:
+ ThreadInfoReader(const base::ProcessId aPid, const base::ProcessId aTid)
+ : StatReader(aPid), mTid(aTid) {
+ // Adding the thread path
+ mFilepath.Truncate();
+ mFilepath.AppendPrintf("/proc/%u/task/%u/stat", aPid, mTid);
+ mMaxIndex = 17;
+ }
+
+ nsresult ParseThread(ThreadInfo& aInfo) {
+ ProcInfo info;
+ nsresult rv = StatReader::ParseProc(info);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aInfo.tid = mTid;
+ // Copying over the data we got from StatReader::ParseProc()
+ aInfo.cpuKernel = info.cpuKernel;
+ aInfo.cpuUser = info.cpuUser;
+ aInfo.name.Assign(info.filename);
+ return NS_OK;
+ }
+
+ private:
+ base::ProcessId mTid;
+};
+
+RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests) {
+ auto holder = MakeUnique<MozPromiseHolder<ProcInfoPromise>>();
+ RefPtr<ProcInfoPromise> promise = holder->Ensure(__func__);
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get stream transport service");
+ holder->Reject(rv, __func__);
+ return promise;
+ }
+
+ RefPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__,
+ [holder = std::move(holder), requests = std::move(aRequests)]() {
+ HashMap<base::ProcessId, ProcInfo> gathered;
+ if (!gathered.reserve(requests.Length())) {
+ holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+ for (const auto& request : requests) {
+ // opening the stat file and reading its content
+ StatReader reader(request.pid);
+ ProcInfo info;
+ nsresult rv = reader.ParseProc(info);
+ if (NS_FAILED(rv)) {
+ // Can't read data for this proc.
+ // Probably either a sandboxing issue or a race condition, e.g.
+ // the process has been just been killed. Regardless, skip process.
+ continue;
+ }
+ // Computing the resident unique size is somewhat tricky,
+ // so we use about:memory's implementation. This implementation
+ // reopens `/proc/[pid]`, so there is the risk of an additional
+ // race condition. In that case, the result is `0`.
+ info.residentUniqueSize =
+ nsMemoryReporterManager::ResidentUnique(request.pid);
+
+ // Extra info
+ info.pid = request.pid;
+ info.childId = request.childId;
+ info.type = request.processType;
+ info.origin = request.origin;
+ info.windows = std::move(request.windowInfo);
+
+ // Let's look at the threads
+ nsCString taskPath;
+ taskPath.AppendPrintf("/proc/%u/task", request.pid);
+ DIR* dirHandle = opendir(taskPath.get());
+ if (!dirHandle) {
+ // For some reason, we have no data on the threads for this process.
+ // Most likely reason is that we have just lost a race condition and
+ // the process is dead.
+ // Let's stop here and ignore the entire process.
+ continue;
+ }
+ auto cleanup = mozilla::MakeScopeExit([&] { closedir(dirHandle); });
+
+ // If we can't read some thread info, we ignore that thread.
+ dirent* entry;
+ while ((entry = readdir(dirHandle)) != nullptr) {
+ if (entry->d_name[0] == '.') {
+ continue;
+ }
+ // Threads have a stat file, like processes.
+ nsAutoCString entryName(entry->d_name);
+ int32_t tid = entryName.ToInteger(&rv);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ ThreadInfoReader reader(request.pid, tid);
+ ThreadInfo threadInfo;
+ rv = reader.ParseThread(threadInfo);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ info.threads.AppendElement(threadInfo);
+ }
+
+ if (!gathered.put(request.pid, std::move(info))) {
+ holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+ }
+
+ // ... and we're done!
+ holder->Resolve(std::move(gathered), __func__);
+ });
+
+ rv = target->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the LoadDataRunnable.");
+ }
+ return promise;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/processtools/ProcInfo_win.cpp b/toolkit/components/processtools/ProcInfo_win.cpp
new file mode 100644
index 0000000000..c73b15237e
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo_win.cpp
@@ -0,0 +1,193 @@
+/* -*- 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/ProcInfo.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "nsMemoryReporterManager.h"
+#include "nsNetCID.h"
+#include "nsWindowsHelpers.h"
+#include <windows.h>
+#include <psapi.h>
+#include <tlhelp32.h>
+
+typedef HRESULT(WINAPI* GETTHREADDESCRIPTION)(HANDLE hThread,
+ PWSTR* threadDescription);
+
+namespace mozilla {
+
+uint64_t ToNanoSeconds(const FILETIME& aFileTime) {
+ // FILETIME values are 100-nanoseconds units, converting
+ ULARGE_INTEGER usec = {{aFileTime.dwLowDateTime, aFileTime.dwHighDateTime}};
+ return usec.QuadPart * 100;
+}
+
+RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests) {
+ auto holder = MakeUnique<MozPromiseHolder<ProcInfoPromise>>();
+ RefPtr<ProcInfoPromise> promise = holder->Ensure(__func__);
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get stream transport service");
+ holder->Reject(rv, __func__);
+ return promise;
+ }
+
+ RefPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__,
+ [holder = std::move(holder), requests = std::move(aRequests)]() -> void {
+ HashMap<base::ProcessId, ProcInfo> gathered;
+ if (!gathered.reserve(requests.Length())) {
+ holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+
+ // ---- Copying data on processes (minus threads).
+
+ for (const auto& request : requests) {
+ nsAutoHandle handle(OpenProcess(
+ PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, request.pid));
+
+ if (!handle) {
+ // Ignore process, it may have died.
+ continue;
+ }
+
+ wchar_t filename[MAX_PATH];
+ if (GetProcessImageFileNameW(handle.get(), filename, MAX_PATH) == 0) {
+ // Ignore process, it may have died.
+ continue;
+ }
+ FILETIME createTime, exitTime, kernelTime, userTime;
+ if (!GetProcessTimes(handle.get(), &createTime, &exitTime,
+ &kernelTime, &userTime)) {
+ // Ignore process, it may have died.
+ continue;
+ }
+ PROCESS_MEMORY_COUNTERS memoryCounters;
+ if (!GetProcessMemoryInfo(handle.get(),
+ (PPROCESS_MEMORY_COUNTERS)&memoryCounters,
+ sizeof(memoryCounters))) {
+ // Ignore process, it may have died.
+ continue;
+ }
+
+ // Assumption: values of `pid` are distinct between processes,
+ // regardless of any race condition we might have stumbled upon. Even
+ // if it somehow could happen, in the worst case scenario, we might
+ // end up overwriting one process info and we might end up with too
+ // many threads attached to a process, as the data is not crucial, we
+ // do not need to defend against that (unlikely) scenario.
+ ProcInfo info;
+ info.pid = request.pid;
+ info.childId = request.childId;
+ info.type = request.processType;
+ info.origin = request.origin;
+ info.windows = std::move(request.windowInfo);
+ info.filename.Assign(filename);
+ info.cpuKernel = ToNanoSeconds(kernelTime);
+ info.cpuUser = ToNanoSeconds(userTime);
+ info.residentSetSize = memoryCounters.WorkingSetSize;
+
+ // Computing the resident unique size is somewhat tricky,
+ // so we use about:memory's implementation. This implementation
+ // uses the `HANDLE` so, in theory, should be no additional
+ // race condition. However, in case of error, the result is `0`.
+ info.residentUniqueSize =
+ nsMemoryReporterManager::ResidentUnique(handle.get());
+
+ if (!gathered.put(request.pid, std::move(info))) {
+ holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+ }
+
+ // ---- Add thread data to already-copied processes.
+
+ // First, we need to capture a snapshot of all the threads on this
+ // system.
+ nsAutoHandle hThreadSnap(CreateToolhelp32Snapshot(
+ /* dwFlags */ TH32CS_SNAPTHREAD, /* ignored */ 0));
+ if (!hThreadSnap) {
+ holder->Reject(NS_ERROR_UNEXPECTED, __func__);
+ return;
+ }
+
+ // `GetThreadDescription` is available as of Windows 10.
+ // We attempt to import it dynamically, knowing that it
+ // may be `nullptr`.
+ auto getThreadDescription =
+ reinterpret_cast<GETTHREADDESCRIPTION>(::GetProcAddress(
+ ::GetModuleHandleW(L"Kernel32.dll"), "GetThreadDescription"));
+
+ THREADENTRY32 te32;
+ te32.dwSize = sizeof(THREADENTRY32);
+
+ // Now, walk through the threads.
+ for (auto success = Thread32First(hThreadSnap.get(), &te32); success;
+ success = Thread32Next(hThreadSnap.get(), &te32)) {
+ auto processLookup = gathered.lookup(te32.th32OwnerProcessID);
+ if (!processLookup) {
+ // Not one of the processes we're interested in.
+ continue;
+ }
+ ThreadInfo* threadInfo =
+ processLookup->value().threads.AppendElement(fallible);
+ if (!threadInfo) {
+ holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+
+ nsAutoHandle hThread(
+ OpenThread(/* dwDesiredAccess = */ THREAD_QUERY_INFORMATION,
+ /* bInheritHandle = */ FALSE,
+ /* dwThreadId = */ te32.th32ThreadID));
+ if (!hThread) {
+ // Cannot open thread. Not sure why, but let's erase this thread
+ // and attempt to find data on other threads.
+ processLookup->value().threads.RemoveLastElement();
+ continue;
+ }
+
+ threadInfo->tid = te32.th32ThreadID;
+
+ // Attempt to get thread times.
+ // If we fail, continue without this piece of information.
+ FILETIME createTime, exitTime, kernelTime, userTime;
+ if (GetThreadTimes(hThread.get(), &createTime, &exitTime, &kernelTime,
+ &userTime)) {
+ threadInfo->cpuKernel = ToNanoSeconds(kernelTime);
+ threadInfo->cpuUser = ToNanoSeconds(userTime);
+ }
+
+ // Attempt to get thread name.
+ // If we fail, continue without this piece of information.
+ if (getThreadDescription) {
+ PWSTR threadName = nullptr;
+ if (getThreadDescription(hThread.get(), &threadName) &&
+ threadName) {
+ threadInfo->name = threadName;
+ }
+ if (threadName) {
+ LocalFree(threadName);
+ }
+ }
+ }
+
+ // ----- We're ready to return.
+ holder->Resolve(std::move(gathered), __func__);
+ });
+
+ rv = target->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the LoadDataRunnable.");
+ }
+
+ return promise;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/processtools/ProcessToolsService.cpp b/toolkit/components/processtools/ProcessToolsService.cpp
new file mode 100644
index 0000000000..3855996fdf
--- /dev/null
+++ b/toolkit/components/processtools/ProcessToolsService.cpp
@@ -0,0 +1,33 @@
+/* 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/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+#include "nsCOMPtr.h"
+#include "ProcessToolsService.h"
+
+// This anonymous namespace prevents outside C++ code from improperly accessing
+// these implementation details.
+namespace {
+extern "C" {
+// Implemented in Rust.
+void new_process_tools_service(nsIProcessToolsService** result);
+}
+
+static mozilla::StaticRefPtr<nsIProcessToolsService> sProcessToolsService;
+} // namespace
+
+already_AddRefed<nsIProcessToolsService> GetProcessToolsService() {
+ nsCOMPtr<nsIProcessToolsService> processToolsService;
+
+ if (sProcessToolsService) {
+ processToolsService = sProcessToolsService;
+ } else {
+ new_process_tools_service(getter_AddRefs(processToolsService));
+ sProcessToolsService = processToolsService;
+ mozilla::ClearOnShutdown(&sProcessToolsService);
+ }
+
+ return processToolsService.forget();
+}
diff --git a/toolkit/components/processtools/ProcessToolsService.h b/toolkit/components/processtools/ProcessToolsService.h
new file mode 100644
index 0000000000..932fd5d4eb
--- /dev/null
+++ b/toolkit/components/processtools/ProcessToolsService.h
@@ -0,0 +1,8 @@
+/* -*- 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 "nsIProcessToolsService.h"
+
+already_AddRefed<nsIProcessToolsService> GetProcessToolsService();
diff --git a/toolkit/components/processtools/components.conf b/toolkit/components/processtools/components.conf
new file mode 100644
index 0000000000..6e7aede0dc
--- /dev/null
+++ b/toolkit/components/processtools/components.conf
@@ -0,0 +1,10 @@
+Classes = [
+ {
+ 'cid': '{79A13656-A472-4713-B0E1-AB39A15CF790}',
+ 'contract_ids': ["@mozilla.org/processtools-service;1"],
+ 'type': 'nsIProcessToolsService',
+ 'constructor': 'GetProcessToolsService',
+ 'singleton': True,
+ 'headers': ['ProcessToolsService.h'],
+ }
+]
diff --git a/toolkit/components/processtools/moz.build b/toolkit/components/processtools/moz.build
new file mode 100644
index 0000000000..c69f65e5b8
--- /dev/null
+++ b/toolkit/components/processtools/moz.build
@@ -0,0 +1,46 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Content Processes")
+
+XPIDL_MODULE = "toolkit_processtools"
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+XPIDL_SOURCES += [
+ "nsIProcessToolsService.idl",
+]
+
+EXPORTS.mozilla += [
+ "ProcInfo.h",
+]
+
+EXPORTS += [
+ "ProcessToolsService.h",
+]
+
+UNIFIED_SOURCES += [
+ "ProcessToolsService.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
+
+# Platform-specific implementations of `ProcInfo`.
+toolkit = CONFIG["MOZ_WIDGET_TOOLKIT"]
+if toolkit == "gtk" or toolkit == "android":
+ UNIFIED_SOURCES += ["ProcInfo_linux.cpp"]
+elif toolkit == "windows":
+ UNIFIED_SOURCES += ["ProcInfo_win.cpp"]
+elif toolkit == "cocoa":
+ UNIFIED_SOURCES += ["ProcInfo.mm"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/toolkit/components/processtools/nsIProcessToolsService.idl b/toolkit/components/processtools/nsIProcessToolsService.idl
new file mode 100644
index 0000000000..e257b424c4
--- /dev/null
+++ b/toolkit/components/processtools/nsIProcessToolsService.idl
@@ -0,0 +1,40 @@
+/* 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 "nsISupports.idl"
+
+[scriptable, uuid(1341f571-ebed-4305-b264-4d8fc3b6b11c)]
+interface nsIProcessToolsService: nsISupports {
+ /**
+ * Kill a process running on this system.
+ *
+ * Does not cause a crash report to be generated and sent.
+ *
+ * # Note
+ *
+ * `pid` is the unique-to-the-system process identifier, as
+ * obtained with attribute `pid` of this service.
+ *
+ * Under Un*ix, that's what you obtain with `getpid()`, etc.
+ * Under Windows, that's what you obtain with `GetCurrentProcessId()`,
+ * NOT the same thing as the process `HANDLE`.
+ *
+ * # Failure
+ *
+ * Under Windows, if two processes race to `kill()` a third process,
+ * or two threads race to `kill()` a process there is a (small) window
+ * during which this can cause a crash in the losing process.
+ *
+ * # Caveats
+ *
+ * Under Windows, process killing is asynchronous. Therefore, this
+ * function can return before process `pid` is actually dead.
+ */
+ void kill(in unsigned long long pid);
+
+ /**
+ * The pid for the current process.
+ */
+ readonly attribute unsigned long long pid;
+};
diff --git a/toolkit/components/processtools/src/lib.rs b/toolkit/components/processtools/src/lib.rs
new file mode 100644
index 0000000000..c1357354ed
--- /dev/null
+++ b/toolkit/components/processtools/src/lib.rs
@@ -0,0 +1,107 @@
+/* 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/. */
+
+#[cfg(not(target_os = "windows"))]
+extern crate libc;
+#[cfg(target_os = "windows")]
+extern crate winapi;
+
+extern crate nserror;
+extern crate xpcom;
+
+use std::convert::TryInto;
+
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use xpcom::{interfaces::nsIProcessToolsService, xpcom, xpcom_method, RefPtr};
+
+// Separate this `use` to avoid build-breaking warnings.
+#[cfg(target_os = "windows")]
+use nserror::NS_ERROR_NOT_AVAILABLE;
+
+#[no_mangle]
+pub unsafe extern "C" fn new_process_tools_service(result: *mut *const nsIProcessToolsService) {
+ let service: RefPtr<ProcessToolsService> = ProcessToolsService::new();
+ RefPtr::new(service.coerce::<nsIProcessToolsService>()).forget(&mut *result);
+}
+
+// Implementation note:
+//
+// We're following the strategy employed by the `kvstore`.
+// See https://searchfox.org/mozilla-central/rev/a87a1c3b543475276e6d57a7a80cb02f3e42b6ed/toolkit/components/kvstore/src/lib.rs#78
+
+#[derive(xpcom)]
+#[refcnt = "atomic"]
+#[xpimplements(nsIProcessToolsService)]
+pub struct InitProcessToolsService {}
+
+impl ProcessToolsService {
+ pub fn new() -> RefPtr<ProcessToolsService> {
+ ProcessToolsService::allocate(InitProcessToolsService {})
+ }
+
+ // Method `kill`.
+
+ xpcom_method!(
+ kill => Kill(id: u64)
+ );
+
+ #[cfg(target_os = "windows")]
+ pub fn kill(&self, pid: u64) -> Result<(), nsresult> {
+ let handle = unsafe {
+ winapi::um::processthreadsapi::OpenProcess(
+ /* dwDesiredAccess */
+ winapi::um::winnt::PROCESS_TERMINATE | winapi::um::winnt::SYNCHRONIZE,
+ /* bInheritHandle */ 0,
+ /* dwProcessId */ pid.try_into().unwrap(),
+ )
+ };
+ if handle.is_null() {
+ // Could not open process.
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+
+ let result = unsafe {
+ winapi::um::processthreadsapi::TerminateProcess(
+ /* hProcess */ handle, /* uExitCode */ 0,
+ )
+ };
+
+ // Close handle regardless of success.
+ let _ = unsafe { winapi::um::handleapi::CloseHandle(handle) };
+
+ if result == 0 {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Ok(())
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ pub fn kill(&self, pid: u64) -> Result<(), nsresult> {
+ let pid = pid.try_into().or(Err(NS_ERROR_FAILURE))?;
+ let result = unsafe { libc::kill(pid, libc::SIGKILL) };
+ if result == 0 {
+ Ok(())
+ } else {
+ Err(NS_ERROR_FAILURE)
+ }
+ }
+
+ // Attribute `pid`
+
+ xpcom_method!(
+ get_pid => GetPid() -> u64
+ );
+
+ #[cfg(not(target_os = "windows"))]
+ pub fn get_pid(&self) -> Result<u64, nsresult> {
+ let pid = unsafe { libc::getpid() } as u64;
+ Ok(pid)
+ }
+
+ #[cfg(target_os = "windows")]
+ pub fn get_pid(&self) -> Result<u64, nsresult> {
+ let pid = unsafe { winapi::um::processthreadsapi::GetCurrentProcessId() } as u64;
+ Ok(pid)
+ }
+}
diff --git a/toolkit/components/processtools/tests/browser/browser.ini b/toolkit/components/processtools/tests/browser/browser.ini
new file mode 100644
index 0000000000..77570e5b31
--- /dev/null
+++ b/toolkit/components/processtools/tests/browser/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+prefs=
+ media.rdd-process.enabled=true
+
+support-files =
+ dummy.html
+
+[browser_test_procinfo.js]
+skip-if = (ccov && os == "linux") # https://bugzilla.mozilla.org/show_bug.cgi?id=1608080
diff --git a/toolkit/components/processtools/tests/browser/browser_test_procinfo.js b/toolkit/components/processtools/tests/browser/browser_test_procinfo.js
new file mode 100644
index 0000000000..87ceadf8fa
--- /dev/null
+++ b/toolkit/components/processtools/tests/browser/browser_test_procinfo.js
@@ -0,0 +1,173 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const DUMMY_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+ ) + "/dummy.html";
+
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const MAC = AppConstants.platform == "macosx";
+const HAS_THREAD_NAMES =
+ AppConstants.platform != "win" ||
+ AppConstants.isPlatformAndVersionAtLeast("win", 10);
+const isFissionEnabled = SpecialPowers.useRemoteSubframes;
+
+const SAMPLE_SIZE = 10;
+
+add_task(async function test_proc_info() {
+ console.log("YORIC", "Test starts");
+ // Open a few `about:home` tabs, they'll end up in `privilegedabout`.
+ let tabsAboutHome = [];
+ for (let i = 0; i < 5; ++i) {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:home");
+ tabsAboutHome.push(tab);
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ }
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: DUMMY_URL },
+ async function(browser) {
+ let cpuThreads = 0;
+ let cpuUser = 0;
+
+ // We test `SAMPLE_SIZE` times to increase a tad the chance of encountering race conditions.
+ for (let z = 0; z < SAMPLE_SIZE; z++) {
+ let parentProc = await ChromeUtils.requestProcInfo();
+ cpuUser += parentProc.cpuUser;
+
+ Assert.equal(
+ parentProc.type,
+ "browser",
+ "Parent proc type should be browser"
+ );
+
+ for (var x = 0; x < parentProc.threads.length; x++) {
+ cpuThreads += parentProc.threads[x].cpuUser;
+ }
+
+ // Under Windows, thread names appeared with Windows 10.
+ if (HAS_THREAD_NAMES) {
+ Assert.ok(
+ parentProc.threads.some(thread => thread.name),
+ "At least one of the threads of the parent process is named"
+ );
+ }
+
+ Assert.ok(
+ parentProc.residentUniqueSize > 0,
+ "Resident-unique-size was set"
+ );
+ Assert.ok(
+ parentProc.residentUniqueSize <= parentProc.residentSetSize,
+ `Resident-unique-size should be bounded by resident-set-size ${parentProc.residentUniqueSize} <= ${parentProc.residentSetSize}`
+ );
+
+ // While it's very unlikely that the parent will disappear while we're running
+ // tests, some children can easily vanish. So we go twice through the list of
+ // children. Once to test stuff that all process data respects the invariants
+ // that don't care whether we have a race condition and once to test that at
+ // least one well-known process that should not be able to vanish during
+ // the test respects all the invariants.
+ for (var i = 0; i < parentProc.children.length; i++) {
+ let childProc = parentProc.children[i];
+ Assert.notEqual(
+ childProc.type,
+ "browser",
+ "Child proc type should not be browser"
+ );
+
+ // We set the `childID` for child processes that have a `ContentParent`/`ContentChild`
+ // actor hierarchy.
+ if (childProc.type.startsWith("web")) {
+ Assert.notEqual(
+ childProc.childID,
+ 0,
+ "Child proc should have been set"
+ );
+ }
+ Assert.notEqual(
+ childProc.type,
+ "unknown",
+ "Child proc type should be known"
+ );
+ if (childProc.type == "webIsolated") {
+ Assert.notEqual(
+ childProc.origin || "",
+ "",
+ "Child process should have an origin"
+ );
+ }
+
+ for (var y = 0; y < childProc.threads.length; y++) {
+ cpuThreads += childProc.threads[y].cpuUser;
+ }
+ cpuUser += childProc.cpuUser;
+ }
+
+ // We only check other properties on the `privilegedabout` subprocess, which
+ // as of this writing is always active and available.
+ var hasPrivilegedAbout = false;
+ var numberOfAboutTabs = 0;
+ for (i = 0; i < parentProc.children.length; i++) {
+ let childProc = parentProc.children[i];
+ if (childProc.type != "privilegedabout") {
+ continue;
+ }
+ hasPrivilegedAbout = true;
+ Assert.ok(
+ childProc.residentUniqueSize > 0,
+ "Resident-unique-size was set"
+ );
+ Assert.ok(
+ childProc.residentUniqueSize <= childProc.residentSetSize,
+ `Resident-unique-size should be bounded by resident-set-size ${childProc.residentUniqueSize} <= ${childProc.residentSetSize}`
+ );
+
+ for (var win of childProc.windows) {
+ if (win.documentURI.spec != "about:home") {
+ // We're only interested in about:home for this test.
+ continue;
+ }
+ numberOfAboutTabs++;
+ Assert.ok(
+ win.outerWindowId > 0,
+ `ContentParentID should be > 0 ${win.outerWindowId}`
+ );
+ if (win.documentTitle) {
+ // Unfortunately, we sometimes reach this point before the document is fully loaded, so
+ // `win.documentTitle` may still be empty.
+ Assert.equal(win.documentTitle, "New Tab");
+ }
+ }
+ Assert.ok(
+ numberOfAboutTabs >= tabsAboutHome.length,
+ "We have found at least as many about:home tabs as we opened"
+ );
+
+ // Once we have verified the privileged about process, bailout.
+ break;
+ }
+
+ Assert.ok(
+ hasPrivilegedAbout,
+ "We have found the privileged about process"
+ );
+ }
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=1529023
+ if (!MAC) {
+ Assert.greater(cpuThreads, 0, "Got some cpu time in the threads");
+ }
+ Assert.greater(cpuUser, 0, "Got some cpu time");
+
+ for (let tab of tabsAboutHome) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+ );
+});
diff --git a/toolkit/components/processtools/tests/browser/dummy.html b/toolkit/components/processtools/tests/browser/dummy.html
new file mode 100644
index 0000000000..e69dad24d4
--- /dev/null
+++ b/toolkit/components/processtools/tests/browser/dummy.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Dummy test page</p>
+<div id="holder" class="">Holder</div>
+<script>
+ let text = "";
+ for (let i = 0; i < 1000; i++) {
+ text += "more";
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("holder").innerHTML = text;
+ }
+ document.getElementById("holder").classList.add("loaded");
+</script>
+</body>
+</html>
diff --git a/toolkit/components/processtools/tests/xpcshell/test_process_kill.js b/toolkit/components/processtools/tests/xpcshell/test_process_kill.js
new file mode 100644
index 0000000000..e8dce6d364
--- /dev/null
+++ b/toolkit/components/processtools/tests/xpcshell/test_process_kill.js
@@ -0,0 +1,57 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+const { Subprocess } = ChromeUtils.import(
+ "resource://gre/modules/Subprocess.jsm"
+);
+
+const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+);
+
+const env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+);
+
+let PYTHON;
+
+// Find Python.
+add_task(async function setup() {
+ PYTHON = await Subprocess.pathSearch(env.get("PYTHON"));
+});
+
+// Ensure that killing a process... kills the process.
+add_task(async function test_subprocess_kill() {
+ // We launch Python, as it's a long-running process and it exists
+ // on all desktop platforms on which we run tests.
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: [],
+ });
+
+ let isTerminated = false;
+
+ proc.wait().then(() => {
+ isTerminated = true;
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+ Assert.ok(
+ !isTerminated,
+ "We haven't killed the process yet, it should still be running."
+ );
+
+ // Time to kill the process.
+ ProcessTools.kill(proc.pid);
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+ Assert.ok(
+ isTerminated,
+ "We have killed the process already, it shouldn't be running anymore."
+ );
+});
diff --git a/toolkit/components/processtools/tests/xpcshell/xpcshell.ini b/toolkit/components/processtools/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..972282c114
--- /dev/null
+++ b/toolkit/components/processtools/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+firefox-appdir = browser
+skip-if = os == 'android'
+subprocess = true
+
+[test_process_kill.js]