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 idunknown
push userunknown
push dateunknown
reviewersqdot
bugs876782
milestone25.0a1
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',
     ]