Bug 876782 - Automounter shouldn't unmount so agressively. r=qdot
authorDave Hylands <dhylands@mozilla.com>
Tue, 16 Jul 2013 13:14:09 -0700
changeset 138760 d556762ff01220e66990f16c91005d0dab2019a5
parent 138719 fd10ead17acea3be28ab8b485c499b0ccd142fbb
child 138761 1350632b8fe91faa0b5d24ceeca993d754c3c3b1
push id24968
push useremorley@mozilla.com
push dateWed, 17 Jul 2013 14:42:00 +0000
treeherderautoland@5a86438c4093 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersqdot
bugs876782
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 876782 - Automounter shouldn't unmount so agressively. r=qdot
dom/system/gonk/AutoMounter.cpp
dom/system/gonk/OpenFileFinder.cpp
dom/system/gonk/OpenFileFinder.h
dom/system/gonk/moz.build
--- a/dom/system/gonk/AutoMounter.cpp
+++ b/dom/system/gonk/AutoMounter.cpp
@@ -23,16 +23,17 @@
 #include "mozilla/FileUtils.h"
 #include "mozilla/Hal.h"
 #include "mozilla/StaticPtr.h"
 #include "nsAutoPtr.h"
 #include "nsMemory.h"
 #include "nsString.h"
 #include "nsThreadUtils.h"
 #include "nsXULAppAPI.h"
+#include "OpenFileFinder.h"
 #include "Volume.h"
 #include "VolumeManager.h"
 
 using namespace mozilla::hal;
 
 /**************************************************************************
 *
 * The following "switch" files are available for monitoring usb
@@ -66,18 +67,19 @@ using namespace mozilla::hal;
 
 #define ICS_SYS_USB_FUNCTIONS "/sys/devices/virtual/android_usb/android0/functions"
 #define ICS_SYS_UMS_DIRECTORY "/sys/devices/virtual/android_usb/android0/f_mass_storage"
 #define ICS_SYS_USB_STATE     "/sys/devices/virtual/android_usb/android0/state"
 
 #define USE_DEBUG 0
 
 #undef LOG
-#define LOG(args...)  __android_log_print(ANDROID_LOG_INFO, "AutoMounter" , ## args)
-#define ERR(args...)  __android_log_print(ANDROID_LOG_ERROR, "AutoMounter" , ## args)
+#define LOG(args...)  __android_log_print(ANDROID_LOG_INFO,  "AutoMounter", ## args)
+#define LOGW(args...) __android_log_print(ANDROID_LOG_WARN,  "AutoMounter", ## args)
+#define ERR(args...)  __android_log_print(ANDROID_LOG_ERROR, "AutoMounter", ## args)
 
 #if USE_DEBUG
 #define DBG(args...)  __android_log_print(ANDROID_LOG_DEBUG, "AutoMounter" , ## args)
 #else
 #define DBG(args...)
 #endif
 
 namespace mozilla {
@@ -397,50 +399,81 @@ AutoMounter::UpdateState()
 
     if (tryToShare && vol->IsSharingEnabled()) {
       // We're going to try to unmount and share the volumes
       switch (volState) {
         case nsIVolume::STATE_MOUNTED: {
           if (vol->IsMountLocked()) {
             // The volume is currently locked, so leave it in the mounted
             // state.
-            DBG("UpdateState: Mounted volume %s is locked, leaving",
-                vol->NameStr());
+            LOGW("UpdateState: Mounted volume %s is locked, not sharing",
+                 vol->NameStr());
             break;
           }
+
+          // Check to see if there are any open files on the volume and
+          // don't initiate the unmount while there are open files.
+          OpenFileFinder::Info fileInfo;
+          OpenFileFinder fileFinder(vol->MountPoint());
+          if (fileFinder.First(&fileInfo)) {
+            LOGW("The following files are open under '%s'",
+                 vol->MountPoint().get());
+            do {
+              LOGW("  PID: %d file: '%s' app: '%s' comm: '%s' exe: '%s'\n",
+                   fileInfo.mPid,
+                   fileInfo.mFileName.get(),
+                   fileInfo.mAppName.get(),
+                   fileInfo.mComm.get(),
+                   fileInfo.mExe.get());
+            } while (fileFinder.Next(&fileInfo));
+            LOGW("UpdateState: Mounted volume %s has open files, not sharing",
+                 vol->NameStr());
+
+            // Check again in 5 seconds to see if the files are closed. Since
+            // we're trying to share the volume, this implies that we're
+            // plugged into the PC via USB and this in turn implies that the
+            // battery is charging, so we don't need to be too concerned about
+            // wasting battery here.
+            MessageLoopForIO::current()->
+              PostDelayedTask(FROM_HERE,
+                              NewRunnableMethod(this, &AutoMounter::UpdateState),
+                              5000);
+            break;
+          }
+
           // Volume is mounted, we need to unmount before
           // we can share.
-          DBG("UpdateState: Unmounting %s", vol->NameStr());
+          LOG("UpdateState: Unmounting %s", vol->NameStr());
           vol->StartUnmount(mResponseCallback);
           return; // UpdateState will be called again when the Unmount command completes
         }
         case nsIVolume::STATE_IDLE: {
           // Volume is unmounted. We can go ahead and share.
-          DBG("UpdateState: Sharing %s", vol->NameStr());
+          LOG("UpdateState: Sharing %s", vol->NameStr());
           vol->StartShare(mResponseCallback);
           return; // UpdateState will be called again when the Share command completes
         }
         default: {
           // Not in a state that we can do anything about.
           break;
         }
       }
     } else {
       // We're going to try and unshare and remount the volumes
       switch (volState) {
         case nsIVolume::STATE_SHARED: {
           // Volume is shared. We can go ahead and unshare.
-          DBG("UpdateState: Unsharing %s", vol->NameStr());
+          LOG("UpdateState: Unsharing %s", vol->NameStr());
           vol->StartUnshare(mResponseCallback);
           return; // UpdateState will be called again when the Unshare command completes
         }
         case nsIVolume::STATE_IDLE: {
           // Volume is unmounted, try to mount.
 
-          DBG("UpdateState: Mounting %s", vol->NameStr());
+          LOG("UpdateState: Mounting %s", vol->NameStr());
           vol->StartMount(mResponseCallback);
           return; // UpdateState will be called again when Mount command completes
         }
         default: {
           // Not in a state that we can do anything about.
           break;
         }
       }
new file mode 100644
--- /dev/null
+++ b/dom/system/gonk/OpenFileFinder.cpp
@@ -0,0 +1,195 @@
+/* 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 "OpenFileFinder.h"
+
+#include "mozilla/FileUtils.h"
+#include "nsPrintfCString.h"
+
+#include <sys/stat.h>
+#include <errno.h>
+
+namespace mozilla {
+namespace system {
+
+OpenFileFinder::OpenFileFinder(const nsACString& aPath)
+  : mPath(aPath),
+    mProcDir(nullptr),
+    mFdDir(nullptr),
+    mPid(0)
+{
+}
+
+OpenFileFinder::~OpenFileFinder()
+{
+  Close();
+}
+
+bool
+OpenFileFinder::First(OpenFileFinder::Info* aInfo)
+{
+  Close();
+
+  mProcDir = opendir("/proc");
+  if (!mProcDir) {
+    return false;
+  }
+  mState = NEXT_PID;
+  return Next(aInfo);
+}
+
+bool
+OpenFileFinder::Next(OpenFileFinder::Info* aInfo)
+{
+  // NOTE: This function calls readdir and readlink, neither of which should
+  //       block since we're using the proc filesystem, which is a purely
+  // kernel in-memory filesystem and doesn't depend on external driver
+  // behaviour.
+  while (mState != DONE) {
+    switch (mState) {
+      case NEXT_PID: {
+        struct dirent *pidEntry;
+        pidEntry = readdir(mProcDir);
+        if (!pidEntry) {
+          mState = DONE;
+          break;
+        }
+        char *endPtr;
+        mPid = strtol(pidEntry->d_name, &endPtr, 10);
+        if (mPid == 0 || *endPtr != '\0') {
+          // Not a +ve number - ignore
+          continue;
+        }
+        // We've found a /proc/PID directory. Scan open file descriptors.
+        if (mFdDir) {
+          closedir(mFdDir);
+        }
+        nsPrintfCString fdDirPath("/proc/%d/fd", mPid);
+        mFdDir = opendir(fdDirPath.get());
+        if (!mFdDir) {
+          continue;
+        }
+        mState = CHECK_FDS;
+      }
+      // Fall through
+      case CHECK_FDS: {
+        struct dirent *fdEntry;
+        while((fdEntry = readdir(mFdDir))) {
+          if (!strcmp(fdEntry->d_name, ".") ||
+              !strcmp(fdEntry->d_name, "..")) {
+            continue;
+          }
+          nsPrintfCString fdSymLink("/proc/%d/fd/%s", mPid, fdEntry->d_name);
+          nsCString resolvedPath;
+          if (ReadSymLink(fdSymLink, resolvedPath) && PathMatches(resolvedPath)) {
+            // We found an open file contained within the directory tree passed
+            // into the constructor.
+            FillInfo(aInfo, resolvedPath);
+            return true;
+          }
+        }
+        // We've checked all of the files for this pid, move onto the next one.
+        mState = NEXT_PID;
+        continue;
+      }
+      case DONE:
+      default:
+        mState = DONE;  // covers the default case
+        break;
+    }
+  }
+  return false;
+}
+
+void
+OpenFileFinder::Close()
+{
+  if (mFdDir) {
+    closedir(mFdDir);
+  }
+  if (mProcDir) {
+    closedir(mProcDir);
+  }
+}
+
+void
+OpenFileFinder::FillInfo(OpenFileFinder::Info* aInfo, const nsACString& aPath)
+{
+  aInfo->mFileName = aPath;
+  aInfo->mPid = mPid;
+  nsPrintfCString exePath("/proc/%d/exe", mPid);
+  ReadSymLink(exePath, aInfo->mExe);
+  aInfo->mComm.Truncate();
+  aInfo->mAppName.Truncate();
+  nsPrintfCString statPath("/proc/%d/stat", mPid);
+  nsCString statString;
+  statString.SetLength(200);
+  char *stat = statString.BeginWriting();
+  if (!stat) {
+    return;
+  }
+  ReadSysFile(statPath.get(), stat, statString.Length());
+  // The stat line includes the comm field, surrounded by parenthesis.
+  // However, the contents of the comm field itself is arbitrary and
+  // and can include ')', so we search for the rightmost ) as being
+  // the end of the comm field.
+  char *closeParen = strrchr(stat, ')');
+  if (!closeParen) {
+    return;
+  }
+  char *openParen = strchr(stat, '(');
+  if (!openParen) {
+    return;
+  }
+  if (openParen >= closeParen) {
+    return;
+  }
+  nsDependentCSubstring comm(&openParen[1], closeParen - openParen - 1);
+  aInfo->mComm = comm;
+  // There is a single character field after the comm and then
+  // the parent pid (the field we're interested in).
+  // ) X ppid
+  // 01234
+  int ppid = atoi(&closeParen[4]);
+  // We assume that we're running in the parent process
+  if (ppid != getpid()) {
+    return;
+  }
+  // This looks like a content process. The comm field will be the
+  // app name.
+  aInfo->mAppName = aInfo->mComm;
+}
+
+bool
+OpenFileFinder::ReadSymLink(const nsACString& aSymLink, nsACString& aOutPath)
+{
+  aOutPath.Truncate();
+  const char *symLink = aSymLink.BeginReading();
+
+  // Verify that we actually have a symlink.
+  struct stat st;
+  if (lstat(symLink, &st)) {
+      return false;
+  }
+  if ((st.st_mode & S_IFMT) != S_IFLNK) {
+      return false;
+  }
+
+  // Contrary to the documentation st.st_size doesn't seem to be a reliable
+  // indication of the length when reading from /proc, so we use a fixed
+  // size buffer instead.
+
+  char resolvedSymLink[PATH_MAX];
+  ssize_t pathLength = readlink(symLink, resolvedSymLink,
+                                sizeof(resolvedSymLink) - 1);
+  if (pathLength <= 0) {
+      return false;
+  }
+  resolvedSymLink[pathLength] = '\0';
+  aOutPath.Assign(resolvedSymLink);
+  return true;
+}
+
+} // system
+} // mozilla
new file mode 100644
--- /dev/null
+++ b/dom/system/gonk/OpenFileFinder.h
@@ -0,0 +1,60 @@
+/* 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_system_openfilefinder_h__
+#define mozilla_system_openfilefinder_h__
+
+#include "nsString.h"
+
+#include <dirent.h>
+
+namespace mozilla {
+namespace system {
+
+class OpenFileFinder
+{
+public:
+  enum State
+  {
+    NEXT_PID,
+    CHECK_FDS,
+    DONE
+  };
+  class Info
+  {
+  public:
+    nsCString mFileName;  // name of the the open file
+    nsCString mAppName;   // App which has the file open (if it's a b2g app)
+    pid_t     mPid;       // pid of the process which has the file open
+    nsCString mComm;      // comm associated with pid
+    nsCString mExe;       // executable name associated with pid
+  };
+
+  OpenFileFinder(const nsACString& aPath);
+  ~OpenFileFinder();
+
+  bool First(Info* aInfo);  // Return the first open file
+  bool Next(Info* aInfo);   // Return the next open file
+  void Close();
+
+private:
+
+  void FillInfo(Info *aInfo, const nsACString& aPath);
+  bool ReadSymLink(const nsACString& aSymLink, nsACString& aOutPath);
+  bool PathMatches(const nsACString& aPath)
+  {
+    return Substring(aPath, 0, mPath.Length()).Equals(mPath);
+  }
+
+  State     mState;   // Keeps track of what we're doing.
+  nsCString mPath;    // Only report files contained within this directory tree
+  DIR*      mProcDir; // Used for scanning /proc
+  DIR*      mFdDir;   // Used for scanning /proc/PID/fd
+  int       mPid;     // PID currently being processed
+};
+
+} // system
+} // mozilla
+
+#endif  // mozilla_system_nsvolume_h__
--- a/dom/system/gonk/moz.build
+++ b/dom/system/gonk/moz.build
@@ -47,16 +47,17 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk
         'AutoMounter.cpp',
         'AutoMounterSetting.cpp',
         'GonkGPSGeolocationProvider.cpp',
         'AudioChannelManager.cpp',
         'nsVolume.cpp',
         'nsVolumeMountLock.cpp',
         'nsVolumeService.cpp',
         'nsVolumeStat.cpp',
+        'OpenFileFinder.cpp',
         'TimeZoneSettingObserver.cpp',
         'Volume.cpp',
         'VolumeCommand.cpp',
         'VolumeManager.cpp',
         'VolumeServiceIOThread.cpp',
         'VolumeServiceTest.cpp',
     ]