diff options
Diffstat (limited to 'toolkit/components/processtools')
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] |