/* 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 "Hal.h" #include #include #include #include #include "nsIObserverService.h" #include "nsIDiskSpaceWatcher.h" #include "mozilla/ModuleUtils.h" #include "nsAutoPtr.h" #include "nsThreadUtils.h" #include "base/message_loop.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "nsXULAppAPI.h" #include "fanotify.h" #include "DiskSpaceWatcher.h" using namespace mozilla; namespace mozilla { namespace hal_impl { class GonkDiskSpaceWatcher; } } using namespace mozilla::hal_impl; template<> struct RunnableMethodTraits { static void RetainCallee(GonkDiskSpaceWatcher* obj) { } static void ReleaseCallee(GonkDiskSpaceWatcher* obj) { } }; namespace mozilla { namespace hal_impl { // NOTE: this should be unnecessary once we no longer support ICS. #ifndef __NR_fanotify_init #if defined(__ARM_EABI__) #define __NR_fanotify_init 367 #define __NR_fanotify_mark 368 #elif defined(__i386__) #define __NR_fanotify_init 338 #define __NR_fanotify_mark 339 #else #error "Unhandled architecture" #endif #endif // fanotify_init and fanotify_mark functions are syscalls. // The user space bits are not part of bionic so we add them here // as well as fanotify.h int fanotify_init (unsigned int flags, unsigned int event_f_flags) { return syscall(__NR_fanotify_init, flags, event_f_flags); } // Add, remove, or modify an fanotify mark on a filesystem object. int fanotify_mark (int fanotify_fd, unsigned int flags, uint64_t mask, int dfd, const char *pathname) { // On 32 bits platforms we have to convert the 64 bits mask into // two 32 bits ints. if (sizeof(void *) == 4) { union { uint64_t _64; uint32_t _32[2]; } _mask; _mask._64 = mask; return syscall(__NR_fanotify_mark, fanotify_fd, flags, _mask._32[0], _mask._32[1], dfd, pathname); } return syscall(__NR_fanotify_mark, fanotify_fd, flags, mask, dfd, pathname); } class GonkDiskSpaceWatcher final : public MessageLoopForIO::Watcher { public: GonkDiskSpaceWatcher(); ~GonkDiskSpaceWatcher() {}; virtual void OnFileCanReadWithoutBlocking(int aFd); // We should never write to the fanotify fd. virtual void OnFileCanWriteWithoutBlocking(int aFd) { MOZ_CRASH("Must not write to fanotify fd"); } void DoStart(); void DoStop(); private: void NotifyUpdate(); uint64_t mLowThreshold; uint64_t mHighThreshold; TimeDuration mTimeout; TimeStamp mLastTimestamp; uint64_t mLastFreeSpace; uint32_t mSizeDelta; bool mIsDiskFull; uint64_t mFreeSpace; int mFd; MessageLoopForIO::FileDescriptorWatcher mReadWatcher; }; static GonkDiskSpaceWatcher* gHalDiskSpaceWatcher = nullptr; #define WATCHER_PREF_LOW "disk_space_watcher.low_threshold" #define WATCHER_PREF_HIGH "disk_space_watcher.high_threshold" #define WATCHER_PREF_TIMEOUT "disk_space_watcher.timeout" #define WATCHER_PREF_SIZE_DELTA "disk_space_watcher.size_delta" static const char kWatchedPath[] = "/data"; // Helper class to dispatch calls to xpcom on the main thread. class DiskSpaceNotifier : public nsRunnable { public: DiskSpaceNotifier(const bool aIsDiskFull, const uint64_t aFreeSpace) : mIsDiskFull(aIsDiskFull), mFreeSpace(aFreeSpace) {} NS_IMETHOD Run() { MOZ_ASSERT(NS_IsMainThread()); DiskSpaceWatcher::UpdateState(mIsDiskFull, mFreeSpace); return NS_OK; } private: bool mIsDiskFull; uint64_t mFreeSpace; }; // Helper runnable to delete the watcher on the main thread. class DiskSpaceCleaner : public nsRunnable { public: NS_IMETHOD Run() { MOZ_ASSERT(NS_IsMainThread()); if (gHalDiskSpaceWatcher) { delete gHalDiskSpaceWatcher; gHalDiskSpaceWatcher = nullptr; } return NS_OK; } }; GonkDiskSpaceWatcher::GonkDiskSpaceWatcher() : mLastFreeSpace(UINT64_MAX), mIsDiskFull(false), mFreeSpace(UINT64_MAX), mFd(-1) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(gHalDiskSpaceWatcher == nullptr); // Default values: 5MB for low threshold, 10MB for high threshold, and // a timeout of 5 seconds. mLowThreshold = Preferences::GetInt(WATCHER_PREF_LOW, 5) * 1024 * 1024; mHighThreshold = Preferences::GetInt(WATCHER_PREF_HIGH, 10) * 1024 * 1024; mTimeout = TimeDuration::FromSeconds(Preferences::GetInt(WATCHER_PREF_TIMEOUT, 5)); mSizeDelta = Preferences::GetInt(WATCHER_PREF_SIZE_DELTA, 1) * 1024 * 1024; } void GonkDiskSpaceWatcher::DoStart() { NS_ASSERTION(XRE_GetIOMessageLoop() == MessageLoopForIO::current(), "Not on the correct message loop"); mFd = fanotify_init(FAN_CLASS_NOTIF, FAN_CLOEXEC | O_LARGEFILE); if (mFd == -1) { if (errno == ENOSYS) { // Don't change these printf_stderr since we need these logs even // in opt builds. printf_stderr("Warning: No fanotify support in this device's kernel.\n"); #if ANDROID_VERSION >= 19 MOZ_CRASH("Fanotify support must be enabled in the kernel."); #endif } else { printf_stderr("Error calling fanotify_init()"); } return; } if (fanotify_mark(mFd, FAN_MARK_ADD | FAN_MARK_MOUNT, FAN_CLOSE, 0, kWatchedPath) < 0) { NS_WARNING("Error calling fanotify_mark"); close(mFd); mFd = -1; return; } if (!MessageLoopForIO::current()->WatchFileDescriptor( mFd, /* persistent = */ true, MessageLoopForIO::WATCH_READ, &mReadWatcher, gHalDiskSpaceWatcher)) { NS_WARNING("Unable to watch fanotify fd."); close(mFd); mFd = -1; } } void GonkDiskSpaceWatcher::DoStop() { NS_ASSERTION(XRE_GetIOMessageLoop() == MessageLoopForIO::current(), "Not on the correct message loop"); if (mFd != -1) { mReadWatcher.StopWatchingFileDescriptor(); fanotify_mark(mFd, FAN_MARK_FLUSH, 0, 0, kWatchedPath); close(mFd); mFd = -1; } // Dispatch the cleanup to the main thread. nsCOMPtr runnable = new DiskSpaceCleaner(); NS_DispatchToMainThread(runnable); } // We are called off the main thread, so we proxy first to the main thread // before calling the xpcom object. void GonkDiskSpaceWatcher::NotifyUpdate() { mLastTimestamp = TimeStamp::Now(); mLastFreeSpace = mFreeSpace; nsCOMPtr runnable = new DiskSpaceNotifier(mIsDiskFull, mFreeSpace); NS_DispatchToMainThread(runnable); } void GonkDiskSpaceWatcher::OnFileCanReadWithoutBlocking(int aFd) { struct fanotify_event_metadata* fem = nullptr; char buf[4096]; struct statfs sfs; int32_t len, rc; do { len = read(aFd, buf, sizeof(buf)); } while(len == -1 && errno == EINTR); // Bail out if the file is busy. if (len < 0 && errno == ETXTBSY) { return; } // We should get an exact multiple of fanotify_event_metadata if (len <= 0 || (len % FAN_EVENT_METADATA_LEN != 0)) { MOZ_CRASH("About to crash: fanotify_event_metadata read error."); } fem = reinterpret_cast(buf); while (FAN_EVENT_OK(fem, len)) { rc = fstatfs(fem->fd, &sfs); if (rc < 0) { NS_WARNING("Unable to stat fan_notify fd"); } else { bool firstRun = mFreeSpace == UINT64_MAX; mFreeSpace = sfs.f_bavail * sfs.f_bsize; // We change from full <-> free depending on the free space and the // low and high thresholds. // Once we are in 'full' mode we send updates for all size changes with // a minimum of time between messages or when we cross a size change // threshold. if (firstRun) { mIsDiskFull = mFreeSpace <= mLowThreshold; // Always notify the current state at first run. NotifyUpdate(); } else if (!mIsDiskFull && (mFreeSpace <= mLowThreshold)) { mIsDiskFull = true; NotifyUpdate(); } else if (mIsDiskFull && (mFreeSpace > mHighThreshold)) { mIsDiskFull = false; NotifyUpdate(); } else if (mIsDiskFull) { if (mTimeout < TimeStamp::Now() - mLastTimestamp || mSizeDelta < llabs(mFreeSpace - mLastFreeSpace)) { NotifyUpdate(); } } } close(fem->fd); fem = FAN_EVENT_NEXT(fem, len); } } void StartDiskSpaceWatcher() { MOZ_ASSERT(NS_IsMainThread()); // Bail out if called several times. if (gHalDiskSpaceWatcher != nullptr) { return; } gHalDiskSpaceWatcher = new GonkDiskSpaceWatcher(); XRE_GetIOMessageLoop()->PostTask( FROM_HERE, NewRunnableMethod(gHalDiskSpaceWatcher, &GonkDiskSpaceWatcher::DoStart)); } void StopDiskSpaceWatcher() { MOZ_ASSERT(NS_IsMainThread()); if (!gHalDiskSpaceWatcher) { return; } XRE_GetIOMessageLoop()->PostTask( FROM_HERE, NewRunnableMethod(gHalDiskSpaceWatcher, &GonkDiskSpaceWatcher::DoStop)); } } // namespace hal_impl } // namespace mozilla