This is the mail archive of the cygwin-apps mailing list for the Cygwin project.


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]
Other format: [Raw text]

[PATCH setup 2/2] List and offer to kill processes preventing a file from being written


- Enumerate processes preventing a file from being written
- Replace the MessageBox reporting an in-use file with a DialogBox reporting the
in-use file and the processes which are using that file.
- Use /usr/bin/kill to kill processes which have files open, trying SIGTERM,
then SIGKILL, then TerminateProcess

2013-02-01  Jon TURNEY  <jon.turney@dronecode.org.uk>

	* install.cc ( _custom_MessageBox): Remove custom message box.
	(FileInuseDlgProc): Add file-in-use dialog box.
	(installOne): Use processlist to list processes using a file, and
	offer to kill them with the file-in-use dialog.
	* res.rc (IDD_FILE_INUSE) : New dialog.
	* resource.h (IDD_FILE_INUSE, IDC_FILE_INUSE_EDIT)
	(IDC_FILE_INUSE_MSG, IDC_FILE_INUSE_HELP): Define corresponding
	resource ID numbers.
	* processlist.h: New file.
	* processlist.cc: New file.
	* Makefile.am (setup_LDADD): Add -lpsapi.
	(setup_SOURCES): Add new files.

Signed-off-by: Jon TURNEY <jon.turney@dronecode.org.uk>
---
 Makefile.am    |    4 +-
 install.cc     |  152 +++++++++++++++++++++++++-----------
 processlist.cc |  237 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 processlist.h  |   41 ++++++++++
 res.rc         |   20 +++++
 resource.h     |    4 +
 6 files changed, 411 insertions(+), 47 deletions(-)
 create mode 100644 processlist.cc
 create mode 100644 processlist.h

diff --git a/Makefile.am b/Makefile.am
index 0f1498b..ddd19ed 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -105,7 +105,7 @@ inilint_SOURCES = \
 
 setup_LDADD = \
 	libgetopt++/libgetopt++.la -lgcrypt -lgpg-error \
-	-lshlwapi -lcomctl32 -lole32 -lwsock32 -lnetapi32 -luuid -llzma -lbz2 -lz 
+	-lshlwapi -lcomctl32 -lole32 -lwsock32 -lnetapi32 -lpsapi -luuid -llzma -lbz2 -lz
 setup_LDFLAGS = -mwindows -Wc,-static -static-libtool-libs
 setup_SOURCES = \
 	AntiVirus.cc \
@@ -230,6 +230,8 @@ setup_SOURCES = \
 	postinstallresults.h \
 	prereq.cc \
 	prereq.h \
+	processlist.cc \
+	processlist.h \
 	proppage.cc \
 	proppage.h \
 	propsheet.cc \
diff --git a/install.cc b/install.cc
index 9d39f33..61333b7 100644
--- a/install.cc
+++ b/install.cc
@@ -63,6 +63,7 @@ static const char *cvsid = "\n%%% $Id: install.cc,v 2.101 2011/07/25 14:36:24 jt
 
 #include "threebar.h"
 #include "Exception.h"
+#include "processlist.h"
 
 using namespace std;
 
@@ -192,39 +193,61 @@ Installer::replaceOnRebootSucceeded (const std::string& fn, bool &rebootneeded)
   rebootneeded = true;
 }
 
-#define MB_RETRYCONTINUE 7
-#if !defined(IDCONTINUE)
-#define IDCONTINUE IDCANCEL
-#endif
+typedef struct
+{
+  const char *msg;
+  const char *processlist;
+  int iteration;
+} FileInuseDlgData;
 
-static HHOOK hMsgBoxHook;
-LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam) {
-  HWND hWnd;
-  switch (nCode) {
-    case HCBT_ACTIVATE:
-      hWnd = (HWND)wParam;
-      if (GetDlgItem(hWnd, IDCANCEL) != NULL)
-         SetDlgItemText(hWnd, IDCANCEL, "Continue");
-      UnhookWindowsHookEx(hMsgBoxHook);
-  }
-  return CallNextHookEx(hMsgBoxHook, nCode, wParam, lParam);
-}
+static BOOL CALLBACK
+FileInuseDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+  switch (uMsg)
+    {
+    case WM_INITDIALOG:
+      {
+        FileInuseDlgData *dlg_data = (FileInuseDlgData *)lParam;
+
+        SetDlgItemText(hwndDlg, IDC_FILE_INUSE_MSG, dlg_data->msg);
+        SetDlgItemText(hwndDlg, IDC_FILE_INUSE_EDIT, dlg_data->processlist);
+
+        switch (dlg_data->iteration)
+          {
+          case 0:
+            break; // show the dialog the way it is in the resource
+
+          case 1:
+            SetDlgItemText(hwndDlg, IDRETRY, "&Kill Processes");
+            SetDlgItemText(hwndDlg, IDC_FILE_INUSE_HELP,
+                           "Select 'Kill' to kill Cygwin processes and retry, or "
+                           "select 'Continue' to go on anyway (you will need to reboot).");
+            break;
+
+          default:
+          case 2:
+            SetDlgItemText(hwndDlg, IDRETRY, "&Kill Processes");
+            SetDlgItemText(hwndDlg, IDC_FILE_INUSE_HELP,
+                           "Select 'Kill' to forcibly kill all processes and retry, or "
+                           "select 'Continue' to go on anyway (you will need to reboot).");
+          }
+      }
+      return TRUE; // automatically set focus, please
 
-int _custom_MessageBox(HWND hWnd, LPCTSTR szText, LPCTSTR szCaption, UINT uType) {
-  int retval;
-  bool retry_continue = (uType & MB_TYPEMASK) == MB_RETRYCONTINUE;
-  if (retry_continue) {
-    uType &= ~MB_TYPEMASK; uType |= MB_RETRYCANCEL;
-    // Install a window hook, so we can intercept the message-box
-    // creation, and customize it
-    // Only install for THIS thread!!!
-    hMsgBoxHook = SetWindowsHookEx(WH_CBT, CBTProc, NULL, GetCurrentThreadId());
-  }
-  retval = MessageBox(hWnd, szText, szCaption, uType);
-  // Intercept the return value for less confusing results
-  if (retry_continue && retval == IDCANCEL)
-    return IDCONTINUE;
-  return retval;
+    case WM_COMMAND:
+      if (HIWORD(wParam) == BN_CLICKED)
+        {
+          switch (LOWORD(wParam))
+            {
+            case IDRETRY:
+            case IDOK:
+              EndDialog(hwndDlg, LOWORD (wParam));
+              return TRUE;
+            }
+        }
+    }
+
+  return FALSE;
 }
 
 /* Helper function to create the registry value "AllowProtectedRenames",
@@ -316,9 +339,6 @@ Installer::extract_replace_on_reboot (archive *tarstream, const std::string& pre
   return false;
 }
 
-#undef MessageBox
-#define MessageBox _custom_MessageBox
-
 static char all_null[512];
 
 /* install one source at a given prefix. */
@@ -427,7 +447,7 @@ Installer::installOne (packagemeta &pkgm, const packageversion &ver,
     }
 
   bool error_in_this_package = false;
-  bool ignoreInUseErrors = unattended_mode;
+  bool ignoreInUseErrors = false;
   bool ignoreExtractErrors = unattended_mode;
 
   package_bytes = source.size;
@@ -448,7 +468,7 @@ Installer::installOne (packagemeta &pkgm, const packageversion &ver,
       if (Script::isAScript (fn))
         pkgm.desired.addScript (Script (canonicalfn));
 
-      bool firstIteration = true;
+      int iteration = 0;
       int extract_error = 0;
       while ((extract_error = archive::extract_file (tarstream, prefixURL, prefixPath)) != 0)
         {
@@ -458,21 +478,61 @@ Installer::installOne (packagemeta &pkgm, const packageversion &ver,
               {
                 if (!ignoreInUseErrors)
                   {
-                    char msg[fn.size() + 300];
-                    sprintf (msg,
-                             "%snable to extract /%s -- the file is in use.\r\n"
-                             "Please stop %s Cygwin processes and select \"Retry\", or\r\n"
-                             "select \"Continue\" to go on anyway (you will need to reboot).\r\n",
-                             firstIteration?"U":"Still u", fn.c_str(), firstIteration?"all":"ALL");
+                    // convert the file name to long UNC form
+                    std::string s = backslash(cygpath("/" + fn));
+                    WCHAR sname[s.size () + 7];
+                    mklongpath(sname, s.c_str (), s.size () + 7);
+
+                    // find any process which has that file loaded into it
+                    // (note that this doesn't find when the file is un-writeable because the process has
+                    // that file opened exclusively)
+                    ProcessList processes = Process::listProcessesWithModuleLoaded(sname);
+
+                    std::string plm;
+                    for (ProcessList::iterator i = processes.begin(); i != processes.end(); i++)
+                      {
+                        if (i != processes.begin()) plm += "\r\n";
 
-                    switch (MessageBox (owner, msg, "In-use files detected",
-                                        MB_RETRYCONTINUE | MB_ICONWARNING | MB_TASKMODAL))
+                        std::string processName = i->getName();
+                        log (LOG_BABBLE) << processName << endLog;
+                        plm += processName;
+                      }
+
+                    INT_PTR rc = (iteration < 3) ? IDRETRY : IDOK;
+                    if (unattended_mode == attended)
+                      {
+                        FileInuseDlgData dlg_data;
+                        dlg_data.msg = ("Unable to extract /" + fn).c_str();
+                        dlg_data.processlist = plm.c_str();
+                        dlg_data.iteration = iteration;
+
+                        rc = DialogBoxParam(hinstance, MAKEINTRESOURCE(IDD_FILE_INUSE), owner, FileInuseDlgProc, (LPARAM)&dlg_data);
+                      }
+
+                    switch (rc)
                       {
                       case IDRETRY:
+                        // try to stop all the processes
+                        for (ProcessList::iterator i = processes.begin(); i != processes.end(); i++)
+                          {
+                            i->kill(iteration);
+                          }
+
+                        // wait up to 15 seconds for processes to stop
+                        for (unsigned int i = 0; i < 15; i++)
+                          {
+                            processes = Process::listProcessesWithModuleLoaded(sname);
+                            if (processes.size() == 0)
+                              break;
+
+                            Sleep(1000);
+                          }
+
                         // retry
-                        firstIteration = false;
+                        iteration++;
                         continue;
-                      case IDCONTINUE:
+                      case IDOK:
+                        // ignore this in-use error, and any subsequent in-use errors for other files in the same package
                         ignoreInUseErrors = true;
                         break;
                       default:
diff --git a/processlist.cc b/processlist.cc
new file mode 100644
index 0000000..2e925e3
--- /dev/null
+++ b/processlist.cc
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2013 Jon TURNEY
+ *
+ *     This program is free software; you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation; either version 2 of the License, or
+ *     (at your option) any later version.
+ *
+ *     A copy of the GNU General Public License can be found at
+ *     http://www.gnu.org/
+ *
+ */
+
+#include <windows.h>
+#define PSAPI_VERSION 1
+#include <psapi.h>
+#include <stdio.h>
+
+#include "processlist.h"
+#include <String++.h>
+#include "LogSingleton.h"
+#include "script.h"
+#include "mount.h"
+#include "filemanip.h"
+
+// ---------------------------------------------------------------------------
+// implements class Process
+//
+// access to a Windows process
+// ---------------------------------------------------------------------------
+
+Process::Process(DWORD pid) : processID(pid)
+{
+}
+
+std::string
+Process::getName(void)
+{
+  HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processID);
+  char modName[MAX_PATH];
+  GetModuleFileNameExA(hProcess, NULL, modName, sizeof(modName));
+  CloseHandle(hProcess);
+
+  std::string s = modName;
+  s += " (pid " + stringify(processID) + ")";
+  return s;
+}
+
+DWORD
+Process::getProcessID(void)
+{
+  return processID;
+}
+
+void
+Process::kill(int force)
+{
+  std::string kill_cmd = backslash(cygpath("/bin/kill.exe")) + " ";
+
+  switch (force)
+    {
+      case 0:
+        kill_cmd += "-TERM";
+        break;
+
+      case 1:
+        kill_cmd += "-KILL";
+        break;
+
+      default:
+      case 2:
+        // -f means 'use TerminateProcess() if the kill(2) function fails'
+        // really we just want to TerminateProcess() here
+        kill_cmd += "-KILL -f";
+        break;
+     }
+
+  kill_cmd +=  " " + stringify(processID);
+  ::run(kill_cmd.c_str());
+}
+
+//
+// test if a module is loaded into a process
+//
+bool
+Process::isModuleLoadedInProcess(const WCHAR *moduleName)
+{
+  BOOL match = FALSE;
+
+  // Get process handle
+  HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION |
+                                 PROCESS_VM_READ,
+                                 FALSE, processID );
+
+  if (NULL == hProcess)
+    return FALSE;
+
+  static unsigned int bytesAllocated = 0;
+  static HMODULE *hMods = 0;
+
+  // initial allocation
+  if (bytesAllocated == 0)
+    {
+      bytesAllocated = sizeof(HMODULE)*1024;
+      hMods = (HMODULE *)malloc(bytesAllocated);
+    }
+
+  while (1)
+    {
+      DWORD cbNeeded;
+
+      // Get a list of all the modules in this process.
+      if (!EnumProcessModules(hProcess, hMods, bytesAllocated, &cbNeeded))
+        {
+          // Don't log ERROR_PARTIAL_COPY as expected for System process and those of different bitness
+          if (GetLastError() != ERROR_PARTIAL_COPY)
+            {
+              log (LOG_BABBLE) << "EnumProcessModules failed " << GetLastError() << " for pid " << processID << endLog;
+            }
+
+          cbNeeded = 0;
+        }
+
+      // If we didn't get all modules, retry with a larger array
+      if (cbNeeded > bytesAllocated)
+        {
+          bytesAllocated = cbNeeded;
+          hMods = (HMODULE *)realloc(hMods, bytesAllocated);
+          continue;
+        }
+
+      // Search module list for the module we are looking for
+      for (int i = 0; i < (cbNeeded / sizeof(HMODULE)); i++ )
+        {
+          WCHAR szModName[MAX_PATH];
+
+          // Get the full path to the module's file.
+          if (GetModuleFileNameExW(hProcess, hMods[i], szModName, sizeof(szModName)/sizeof(WCHAR)))
+            {
+              WCHAR canonicalModName[MAX_PATH];
+
+              // Canonicalise returned module name to long UNC form
+              if (wcscmp(szModName, L"\\\\?\\") != 0)
+                {
+                  wcscpy(canonicalModName, L"\\\\?\\");
+                  wcscat(canonicalModName, szModName);
+                }
+              else
+                {
+                  wcscpy(canonicalModName, szModName);
+                }
+
+              // Does it match the name ?
+              if (wcscmp(moduleName, canonicalModName) == 0)
+                {
+                  match = TRUE;
+                  break;
+                }
+            }
+        }
+
+      break;
+    }
+
+    // Release the process handle
+    CloseHandle(hProcess);
+
+    return match;
+}
+
+//
+// get a list of currently running processes
+//
+ProcessList
+Process::snapshot(void)
+{
+  static DWORD *pProcessIDs = 0;
+  static unsigned int bytesAllocated = 0;
+  DWORD bytesReturned;
+
+  // initial allocation
+  if (bytesAllocated == 0)
+    {
+      bytesAllocated = sizeof(DWORD);
+      pProcessIDs = (DWORD *)malloc(bytesAllocated);
+    }
+
+  // fetch a snapshot of process list
+  while (1)
+    {
+      if (!EnumProcesses(pProcessIDs, bytesAllocated, &bytesReturned))
+        {
+          log (LOG_BABBLE) << "EnumProcesses failed " << GetLastError() << endLog;
+          bytesReturned = 0;
+        }
+
+      // If we didn't get all processes, retry with a larger array
+      if (bytesReturned == bytesAllocated)
+        {
+          bytesAllocated = bytesAllocated*2;
+          pProcessIDs = (DWORD *)realloc(pProcessIDs, bytesAllocated);
+          continue;
+        }
+
+      break;
+    }
+
+  // convert to ProcessList vector
+  unsigned int nProcesses = bytesReturned/sizeof(DWORD);
+  ProcessList v(nProcesses, 0);
+  for (unsigned int i = 0; i < nProcesses; i++)
+    {
+      v[i] = pProcessIDs[i];
+    }
+
+  return v;
+}
+
+//
+// list processes which have a given executable module loaded
+//
+ProcessList
+Process::listProcessesWithModuleLoaded(const WCHAR *moduleName)
+{
+  ProcessList v;
+  ProcessList pl = snapshot();
+
+  for (ProcessList::iterator i = pl.begin(); i != pl.end(); i++)
+    {
+      if (i->isModuleLoadedInProcess(moduleName))
+        {
+          v.push_back(*i);
+        }
+    }
+
+  return v;
+}
diff --git a/processlist.h b/processlist.h
new file mode 100644
index 0000000..db03a3d
--- /dev/null
+++ b/processlist.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2013 Jon TURNEY
+ *
+ *     This program is free software; you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation; either version 2 of the License, or
+ *     (at your option) any later version.
+ *
+ *     A copy of the GNU General Public License can be found at
+ *     http://www.gnu.org/
+ *
+ */
+
+#include <windows.h>
+#include <vector>
+#include <string>
+
+// ---------------------------------------------------------------------------
+// interface to class Process
+//
+// access to a Windows process
+// ---------------------------------------------------------------------------
+
+class Process;
+
+// utility type ProcessList, a vector of Process
+typedef std::vector<Process> ProcessList;
+
+class Process
+{
+public:
+  Process(DWORD pid);
+  std::string getName(void);
+  DWORD getProcessID(void);
+  void kill(int force);
+  bool isModuleLoadedInProcess(const WCHAR *moduleName);
+  static ProcessList listProcessesWithModuleLoaded(const WCHAR *moduleName);
+private:
+  DWORD processID;
+  static ProcessList snapshot(void);
+};
diff --git a/res.rc b/res.rc
index d6c0ff0..4bc8de1 100644
--- a/res.rc
+++ b/res.rc
@@ -440,6 +440,26 @@ BEGIN
 
 END
 
+IDD_FILE_INUSE DIALOG DISCARDABLE  0, 0, SETUP_SMALL_DIALOG_DIMS
+STYLE DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION
+CAPTION "In-use file detected"
+FONT 8, "MS Shell Dlg"
+BEGIN
+    ICON            IDI_WARNING,IDC_HEADICON,5,5
+    LTEXT           "Unable to extract %s",
+                    IDC_FILE_INUSE_MSG,27,5,183,8,SS_PATHELLIPSIS
+    LTEXT           "The file is in use by the following processes:",
+                    IDC_STATIC,27,14,183,8
+    EDITTEXT        IDC_FILE_INUSE_EDIT,27,23,183,28,WS_VSCROLL |
+                    ES_LEFT | ES_MULTILINE | ES_READONLY |
+                    ES_AUTOVSCROLL | NOT WS_TABSTOP
+    LTEXT           "Select 'Stop' to stop Cygwin processes and retry, or "
+                    "select 'Continue' to go on anyway (you will need to reboot).",
+                    IDC_FILE_INUSE_HELP,27,52,183,16,NOT WS_GROUP
+    DEFPUSHBUTTON   "&Stop Processes",IDRETRY,47,75,55,15
+    PUSHBUTTON      "&Continue",IDOK,113,75,55,15
+END
+
 /////////////////////////////////////////////////////////////////////////////
 //
 // Manifest
diff --git a/resource.h b/resource.h
index df68473..99a6f42 100644
--- a/resource.h
+++ b/resource.h
@@ -64,6 +64,7 @@
 #define IDD_PREREQ                        220
 #define IDD_DROPPED                       221
 #define IDD_POSTINSTALL                   222
+#define IDD_FILE_INUSE                    223
 
 // Bitmaps
 
@@ -173,3 +174,6 @@
 #define IDC_CHOOSE_CLEAR_SEARCH           587
 #define IDC_LOCAL_DIR_DESC                588
 #define IDC_POSTINSTALL_EDIT              589
+#define IDC_FILE_INUSE_EDIT               590
+#define IDC_FILE_INUSE_MSG                591
+#define IDC_FILE_INUSE_HELP               592
-- 
1.7.9


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]