michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=8 sts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "nsDumpUtils.h" michael@0: #include "nsDirectoryServiceDefs.h" michael@0: #include "nsDirectoryServiceUtils.h" michael@0: #include "prenv.h" michael@0: #include michael@0: #include "mozilla/Services.h" michael@0: #include "nsIObserverService.h" michael@0: #include "mozilla/ClearOnShutdown.h" michael@0: michael@0: #ifdef XP_UNIX // { michael@0: #include "mozilla/Preferences.h" michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: michael@0: using namespace mozilla; michael@0: michael@0: /* michael@0: * The following code supports triggering a registered callback upon michael@0: * receiving a specific signal. michael@0: * michael@0: * Take about:memory for example, we register michael@0: * 1. doGCCCDump for doMemoryReport michael@0: * 2. doMemoryReport for sDumpAboutMemorySignum(SIGRTMIN) michael@0: * and sDumpAboutMemoryAfterMMUSignum(SIGRTMIN+1). michael@0: * michael@0: * When we receive one of these signals, we write the signal number to a pipe. michael@0: * The IO thread then notices that the pipe has been written to, and kicks off michael@0: * the appropriate task on the main thread. michael@0: * michael@0: * This scheme is similar to using signalfd(), except it's portable and it michael@0: * doesn't require the use of sigprocmask, which is problematic because it michael@0: * masks signals received by child processes. michael@0: * michael@0: * In theory, we could use Chromium's MessageLoopForIO::CatchSignal() for this. michael@0: * But that uses libevent, which does not handle the realtime signals (bug michael@0: * 794074). michael@0: */ michael@0: michael@0: // This is the write-end of a pipe that we use to notice when a michael@0: // specific signal occurs. michael@0: static Atomic sDumpPipeWriteFd(-1); michael@0: michael@0: const char* const FifoWatcher::kPrefName = michael@0: "memory_info_dumper.watch_fifo.enabled"; michael@0: michael@0: static void michael@0: DumpSignalHandler(int aSignum) michael@0: { michael@0: // This is a signal handler, so everything in here needs to be michael@0: // async-signal-safe. Be careful! michael@0: michael@0: if (sDumpPipeWriteFd != -1) { michael@0: uint8_t signum = static_cast(aSignum); michael@0: write(sDumpPipeWriteFd, &signum, sizeof(signum)); michael@0: } michael@0: } michael@0: michael@0: NS_IMPL_ISUPPORTS(FdWatcher, nsIObserver); michael@0: michael@0: void FdWatcher::Init() michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: nsCOMPtr os = services::GetObserverService(); michael@0: os->AddObserver(this, "xpcom-shutdown", /* ownsWeak = */ false); michael@0: michael@0: XRE_GetIOMessageLoop()->PostTask( michael@0: FROM_HERE, michael@0: NewRunnableMethod(this, &FdWatcher::StartWatching)); michael@0: } michael@0: michael@0: // Implementations may call this function multiple times if they ensure that michael@0: // it's safe to call OpenFd() multiple times and they call StopWatching() michael@0: // first. michael@0: void FdWatcher::StartWatching() michael@0: { michael@0: MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); michael@0: MOZ_ASSERT(mFd == -1); michael@0: michael@0: mFd = OpenFd(); michael@0: if (mFd == -1) { michael@0: LOG("FdWatcher: OpenFd failed."); michael@0: return; michael@0: } michael@0: michael@0: MessageLoopForIO::current()->WatchFileDescriptor( michael@0: mFd, /* persistent = */ true, michael@0: MessageLoopForIO::WATCH_READ, michael@0: &mReadWatcher, this); michael@0: } michael@0: michael@0: // Since implementations can call StartWatching() multiple times, they can of michael@0: // course call StopWatching() multiple times. michael@0: void FdWatcher::StopWatching() michael@0: { michael@0: MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); michael@0: michael@0: mReadWatcher.StopWatchingFileDescriptor(); michael@0: if (mFd != -1) { michael@0: close(mFd); michael@0: mFd = -1; michael@0: } michael@0: } michael@0: michael@0: StaticRefPtr SignalPipeWatcher::sSingleton; michael@0: michael@0: /* static */ SignalPipeWatcher* michael@0: SignalPipeWatcher::GetSingleton() michael@0: { michael@0: if (!sSingleton) { michael@0: sSingleton = new SignalPipeWatcher(); michael@0: sSingleton->Init(); michael@0: ClearOnShutdown(&sSingleton); michael@0: } michael@0: return sSingleton; michael@0: } michael@0: michael@0: void michael@0: SignalPipeWatcher::RegisterCallback(uint8_t aSignal, michael@0: PipeCallback aCallback) michael@0: { michael@0: MutexAutoLock lock(mSignalInfoLock); michael@0: michael@0: for (SignalInfoArray::index_type i = 0; i < mSignalInfo.Length(); i++) michael@0: { michael@0: if (mSignalInfo[i].mSignal == aSignal) { michael@0: LOG("Register Signal(%d) callback failed! (DUPLICATE)", aSignal); michael@0: return; michael@0: } michael@0: } michael@0: SignalInfo signalInfo = { aSignal, aCallback }; michael@0: mSignalInfo.AppendElement(signalInfo); michael@0: RegisterSignalHandler(signalInfo.mSignal); michael@0: } michael@0: michael@0: void michael@0: SignalPipeWatcher::RegisterSignalHandler(uint8_t aSignal) michael@0: { michael@0: struct sigaction action; michael@0: memset(&action, 0, sizeof(action)); michael@0: sigemptyset(&action.sa_mask); michael@0: action.sa_handler = DumpSignalHandler; michael@0: michael@0: if (aSignal) { michael@0: if (sigaction(aSignal, &action, nullptr)) { michael@0: LOG("SignalPipeWatcher failed to register sig %d.", aSignal); michael@0: } michael@0: } else { michael@0: MutexAutoLock lock(mSignalInfoLock); michael@0: for (SignalInfoArray::index_type i = 0; i < mSignalInfo.Length(); i++) { michael@0: if (sigaction(mSignalInfo[i].mSignal, &action, nullptr)) { michael@0: LOG("SignalPipeWatcher failed to register signal(%d) " michael@0: "dump signal handler.", mSignalInfo[i].mSignal); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: SignalPipeWatcher::~SignalPipeWatcher() michael@0: { michael@0: if (sDumpPipeWriteFd != -1) { michael@0: StopWatching(); michael@0: } michael@0: } michael@0: michael@0: int SignalPipeWatcher::OpenFd() michael@0: { michael@0: MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); michael@0: michael@0: // Create a pipe. When we receive a signal in our signal handler, we'll michael@0: // write the signum to the write-end of this pipe. michael@0: int pipeFds[2]; michael@0: if (pipe(pipeFds)) { michael@0: LOG("SignalPipeWatcher failed to create pipe."); michael@0: return -1; michael@0: } michael@0: michael@0: // Close this pipe on calls to exec(). michael@0: fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC); michael@0: fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC); michael@0: michael@0: int readFd = pipeFds[0]; michael@0: sDumpPipeWriteFd = pipeFds[1]; michael@0: michael@0: RegisterSignalHandler(); michael@0: return readFd; michael@0: } michael@0: michael@0: void SignalPipeWatcher::StopWatching() michael@0: { michael@0: MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); michael@0: michael@0: // Close sDumpPipeWriteFd /after/ setting the fd to -1. michael@0: // Otherwise we have the (admittedly far-fetched) race where we michael@0: // michael@0: // 1) close sDumpPipeWriteFd michael@0: // 2) open a new fd with the same number as sDumpPipeWriteFd michael@0: // had. michael@0: // 3) receive a signal, then write to the fd. michael@0: int pipeWriteFd = sDumpPipeWriteFd.exchange(-1); michael@0: close(pipeWriteFd); michael@0: michael@0: FdWatcher::StopWatching(); michael@0: } michael@0: michael@0: void SignalPipeWatcher::OnFileCanReadWithoutBlocking(int aFd) michael@0: { michael@0: MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); michael@0: michael@0: uint8_t signum; michael@0: ssize_t numReceived = read(aFd, &signum, sizeof(signum)); michael@0: if (numReceived != sizeof(signum)) { michael@0: LOG("Error reading from buffer in " michael@0: "SignalPipeWatcher::OnFileCanReadWithoutBlocking."); michael@0: return; michael@0: } michael@0: michael@0: { michael@0: MutexAutoLock lock(mSignalInfoLock); michael@0: for (SignalInfoArray::index_type i = 0; i < mSignalInfo.Length(); i++) { michael@0: if(signum == mSignalInfo[i].mSignal) { michael@0: mSignalInfo[i].mCallback(signum); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: LOG("SignalPipeWatcher got unexpected signum."); michael@0: } michael@0: michael@0: StaticRefPtr FifoWatcher::sSingleton; michael@0: michael@0: /* static */ FifoWatcher* michael@0: FifoWatcher::GetSingleton() michael@0: { michael@0: if (!sSingleton) { michael@0: nsAutoCString dirPath; michael@0: Preferences::GetCString( michael@0: "memory_info_dumper.watch_fifo.directory", &dirPath); michael@0: sSingleton = new FifoWatcher(dirPath); michael@0: sSingleton->Init(); michael@0: ClearOnShutdown(&sSingleton); michael@0: } michael@0: return sSingleton; michael@0: } michael@0: michael@0: /* static */ bool michael@0: FifoWatcher::MaybeCreate() michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: if (XRE_GetProcessType() != GeckoProcessType_Default) { michael@0: // We want this to be main-process only, since two processes can't listen michael@0: // to the same fifo. michael@0: return false; michael@0: } michael@0: michael@0: if (!Preferences::GetBool(kPrefName, false)) { michael@0: LOG("Fifo watcher disabled via pref."); michael@0: return false; michael@0: } michael@0: michael@0: // The FifoWatcher is held alive by the observer service. michael@0: if (!sSingleton) { michael@0: GetSingleton(); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: void michael@0: FifoWatcher::RegisterCallback(const nsCString& aCommand, FifoCallback aCallback) michael@0: { michael@0: MutexAutoLock lock(mFifoInfoLock); michael@0: michael@0: for (FifoInfoArray::index_type i = 0; i < mFifoInfo.Length(); i++) michael@0: { michael@0: if (mFifoInfo[i].mCommand.Equals(aCommand)) { michael@0: LOG("Register command(%s) callback failed! (DUPLICATE)", aCommand.get()); michael@0: return; michael@0: } michael@0: } michael@0: FifoInfo aFifoInfo = { aCommand, aCallback }; michael@0: mFifoInfo.AppendElement(aFifoInfo); michael@0: } michael@0: michael@0: FifoWatcher::~FifoWatcher() michael@0: { michael@0: } michael@0: michael@0: int FifoWatcher::OpenFd() michael@0: { michael@0: // If the memory_info_dumper.directory pref is specified, put the fifo michael@0: // there. Otherwise, put it into the system's tmp directory. michael@0: michael@0: nsCOMPtr file; michael@0: michael@0: nsresult rv; michael@0: if (mDirPath.Length() > 0) { michael@0: rv = XRE_GetFileFromPath(mDirPath.get(), getter_AddRefs(file)); michael@0: if (NS_FAILED(rv)) { michael@0: LOG("FifoWatcher failed to open file \"%s\"", mDirPath.get()); michael@0: return -1; michael@0: } michael@0: } else { michael@0: rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(file)); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) michael@0: return -1; michael@0: } michael@0: michael@0: rv = file->AppendNative(NS_LITERAL_CSTRING("debug_info_trigger")); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) michael@0: return -1; michael@0: michael@0: nsAutoCString path; michael@0: rv = file->GetNativePath(path); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) michael@0: return -1; michael@0: michael@0: // unlink might fail because the file doesn't exist, or for other reasons. michael@0: // But we don't care it fails; any problems will be detected later, when we michael@0: // try to mkfifo or open the file. michael@0: if (unlink(path.get())) { michael@0: LOG("FifoWatcher::OpenFifo unlink failed; errno=%d. " michael@0: "Continuing despite error.", errno); michael@0: } michael@0: michael@0: if (mkfifo(path.get(), 0766)) { michael@0: LOG("FifoWatcher::OpenFifo mkfifo failed; errno=%d", errno); michael@0: return -1; michael@0: } michael@0: michael@0: #ifdef ANDROID michael@0: // Android runs with a umask, so we need to chmod our fifo to make it michael@0: // world-writable. michael@0: chmod(path.get(), 0666); michael@0: #endif michael@0: michael@0: int fd; michael@0: do { michael@0: // The fifo will block until someone else has written to it. In michael@0: // particular, open() will block until someone else has opened it for michael@0: // writing! We want open() to succeed and read() to block, so we open michael@0: // with NONBLOCK and then fcntl that away. michael@0: fd = open(path.get(), O_RDONLY | O_NONBLOCK); michael@0: } while (fd == -1 && errno == EINTR); michael@0: michael@0: if (fd == -1) { michael@0: LOG("FifoWatcher::OpenFifo open failed; errno=%d", errno); michael@0: return -1; michael@0: } michael@0: michael@0: // Make fd blocking now that we've opened it. michael@0: if (fcntl(fd, F_SETFL, 0)) { michael@0: close(fd); michael@0: return -1; michael@0: } michael@0: michael@0: return fd; michael@0: } michael@0: michael@0: void FifoWatcher::OnFileCanReadWithoutBlocking(int aFd) michael@0: { michael@0: MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); michael@0: michael@0: char buf[1024]; michael@0: int nread; michael@0: do { michael@0: // sizeof(buf) - 1 to leave space for the null-terminator. michael@0: nread = read(aFd, buf, sizeof(buf)); michael@0: } while(nread == -1 && errno == EINTR); michael@0: michael@0: if (nread == -1) { michael@0: // We want to avoid getting into a situation where michael@0: // OnFileCanReadWithoutBlocking is called in an infinite loop, so when michael@0: // something goes wrong, stop watching the fifo altogether. michael@0: LOG("FifoWatcher hit an error (%d) and is quitting.", errno); michael@0: StopWatching(); michael@0: return; michael@0: } michael@0: michael@0: if (nread == 0) { michael@0: // If we get EOF, that means that the other side closed the fifo. We need michael@0: // to close and re-open the fifo; if we don't, michael@0: // OnFileCanWriteWithoutBlocking will be called in an infinite loop. michael@0: michael@0: LOG("FifoWatcher closing and re-opening fifo."); michael@0: StopWatching(); michael@0: StartWatching(); michael@0: return; michael@0: } michael@0: michael@0: nsAutoCString inputStr; michael@0: inputStr.Append(buf, nread); michael@0: michael@0: // Trimming whitespace is important because if you do michael@0: // |echo "foo" >> debug_info_trigger|, michael@0: // it'll actually write "foo\n" to the fifo. michael@0: inputStr.Trim("\b\t\r\n"); michael@0: michael@0: { michael@0: MutexAutoLock lock(mFifoInfoLock); michael@0: michael@0: for (FifoInfoArray::index_type i = 0; i < mFifoInfo.Length(); i++) { michael@0: const nsCString commandStr = mFifoInfo[i].mCommand; michael@0: if(inputStr == commandStr.get()) { michael@0: mFifoInfo[i].mCallback(inputStr); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: LOG("Got unexpected value from fifo; ignoring it."); michael@0: } michael@0: michael@0: #endif // XP_UNIX } michael@0: michael@0: // In Android case, this function will open a file named aFilename under michael@0: // /data/local/tmp/"aFoldername". michael@0: // Otherwise, it will open a file named aFilename under "NS_OS_TEMP_DIR". michael@0: /* static */ nsresult michael@0: nsDumpUtils::OpenTempFile(const nsACString& aFilename, nsIFile** aFile, michael@0: const nsACString& aFoldername) michael@0: { michael@0: #ifdef ANDROID michael@0: // For Android, first try the downloads directory which is world-readable michael@0: // rather than the temp directory which is not. michael@0: if (!*aFile) { michael@0: char *env = PR_GetEnv("DOWNLOADS_DIRECTORY"); michael@0: if (env) { michael@0: NS_NewNativeLocalFile(nsCString(env), /* followLinks = */ true, aFile); michael@0: } michael@0: } michael@0: #endif michael@0: nsresult rv; michael@0: if (!*aFile) { michael@0: rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, aFile); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) michael@0: return rv; michael@0: } michael@0: michael@0: #ifdef ANDROID michael@0: // /data/local/tmp is a true tmp directory; anyone can create a file there, michael@0: // but only the user which created the file can remove it. We want non-root michael@0: // users to be able to remove these files, so we write them into a michael@0: // subdirectory of the temp directory and chmod 777 that directory. michael@0: if (aFoldername != EmptyCString()) { michael@0: rv = (*aFile)->AppendNative(aFoldername); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) michael@0: return rv; michael@0: michael@0: // It's OK if this fails; that probably just means that the directory already michael@0: // exists. michael@0: (*aFile)->Create(nsIFile::DIRECTORY_TYPE, 0777); michael@0: michael@0: nsAutoCString dirPath; michael@0: rv = (*aFile)->GetNativePath(dirPath); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) michael@0: return rv; michael@0: michael@0: while (chmod(dirPath.get(), 0777) == -1 && errno == EINTR) {} michael@0: } michael@0: #endif michael@0: michael@0: nsCOMPtr file(*aFile); michael@0: michael@0: rv = file->AppendNative(aFilename); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) michael@0: return rv; michael@0: michael@0: rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) michael@0: return rv; michael@0: michael@0: #ifdef ANDROID michael@0: // Make this file world-read/writable; the permissions passed to the michael@0: // CreateUnique call above are not sufficient on Android, which runs with a michael@0: // umask. michael@0: nsAutoCString path; michael@0: rv = file->GetNativePath(path); michael@0: if (NS_WARN_IF(NS_FAILED(rv))) michael@0: return rv; michael@0: michael@0: while (chmod(path.get(), 0666) == -1 && errno == EINTR) {} michael@0: #endif michael@0: michael@0: return NS_OK; michael@0: }