- 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.tur...@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