[PATCH setup 2/2] List and offer to kill processes preventing a file from being written
Jon TURNEY
jon.turney@dronecode.org.uk
Wed Feb 6 20:03:00 GMT 2013
- 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
- Offer to kill processes which have files open, trying /usr/bin/kill with SIGTERM,
then SIGKILL, then TerminateProcess
v2:
- Use TerminateProcess directly, rather than using kill -f, so we will work even
if kill doesn't
- Fix formatting: spaces before parentheses for functions and macros
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.
---
Makefile.am | 4 +-
install.cc | 152 ++++++++++++++++++++++++----------
processlist.cc | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
processlist.h | 41 +++++++++
res.rc | 20 +++++
resource.h | 4 +
6 files changed, 424 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..a838aff 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..e3363f9
--- /dev/null
+++ b/processlist.cc
@@ -0,0 +1,250 @@
+/*
+ * 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)
+{
+ if (force >= 2)
+ {
+ // use TerminateProcess() to request force-killing of the process
+ HANDLE hProcess = OpenProcess (PROCESS_TERMINATE, FALSE, processID);
+
+ if (hProcess)
+ {
+ if (TerminateProcess(hProcess, (UINT)-1))
+ {
+ log (LOG_BABBLE) << "TerminateProcess succeeded on pid " << processID << endLog;
+ }
+ else
+ {
+ log (LOG_BABBLE) << "TerminateProcess failed " << GetLastError () << " for pid " << processID << endLog;
+ }
+ CloseHandle (hProcess);
+ }
+
+ return;
+ }
+
+ std::string signame;
+
+ switch (force)
+ {
+ case 0:
+ signame = "-TERM";
+ break;
+
+ default:
+ case 1:
+ signame = "-KILL";
+ break;
+ }
+
+ std::string kill_cmd = backslash (cygpath ("/bin/kill.exe")) + " " + signame + " " + 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..ef734a3
--- /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
More information about the Cygwin-apps
mailing list