// ************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: http://www.gnu.org/licenses/gpl.html * // * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * // ************************************************************************** #include "file_op.h" #include #include #include #define WIN32_LEAN_AND_MEAN #include #include #include #include #include #include #pragma comment(lib, "Rstrtmgr.lib") #define STRICT_TYPED_ITEMIDS //better type safety for IDLists #include #include #include //shell constants such as FO_* values using namespace zen; namespace { struct Win32Error { Win32Error(DWORD errorCode) : errorCode_(errorCode) {} DWORD errorCode_; }; std::vector getLockingProcesses(const wchar_t* filename); //throw Win32Error class RecyclerProgressCallback : public IFileOperationProgressSink { //Sample implementation: %ProgramFiles%\Microsoft SDKs\Windows\v7.1\Samples\winui\shell\appplatform\FileOperationProgressSink ~RecyclerProgressCallback() {} //private: do not allow stack usage "thanks" to IUnknown lifetime management! public: RecyclerProgressCallback(fileop::RecyclerCallback callback, void* sink) : cancellationRequested(false), callback_(callback), sink_(sink), refCount(1) {} //IUnknown: reference implementation according to: http://msdn.microsoft.com/en-us/library/office/cc839627.aspx virtual ULONG STDMETHODCALLTYPE AddRef() { return ::InterlockedIncrement(&refCount); } virtual ULONG STDMETHODCALLTYPE Release() { ULONG newRefCount = ::InterlockedDecrement(&refCount); if (newRefCount == 0) //race condition caveat: do NOT check refCount, which might have changed already! delete this; return newRefCount; } virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void __RPC_FAR* __RPC_FAR* ppvObject) { if (!ppvObject) return E_INVALIDARG; if (riid == IID_IUnknown || riid == IID_IFileOperationProgressSink) { *ppvObject = this; AddRef(); return S_OK; } *ppvObject = NULL; return E_NOINTERFACE; } //IFileOperationProgressSink virtual HRESULT STDMETHODCALLTYPE StartOperations() { return S_OK; } virtual HRESULT STDMETHODCALLTYPE FinishOperations(HRESULT hrResult) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PreRenameItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt_string LPCWSTR pszNewName) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PostRenameItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_string LPCWSTR pszNewName, HRESULT hrRename, __RPC__in_opt IShellItem* psiNewlyCreated) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PreMoveItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PostMoveItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName, HRESULT hrMove, __RPC__in_opt IShellItem* psiNewlyCreated) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PreCopyItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PostCopyItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName, HRESULT hrCopy, __RPC__in_opt IShellItem* psiNewlyCreated) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PreNewItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PostNewItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiDestinationFolder, __RPC__in_opt_string LPCWSTR pszNewName, __RPC__in_opt_string LPCWSTR pszTemplateName, DWORD dwFileAttributes, HRESULT hrNew, __RPC__in_opt IShellItem* psiNewItem) { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PreDeleteItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem) { if (psiItem) { LPWSTR itemPath = nullptr; HRESULT hr = psiItem->GetDisplayName(SIGDN_FILESYSPATH, &itemPath); if (FAILED(hr)) return hr; ZEN_ON_SCOPE_EXIT(::CoTaskMemFree(itemPath)); currentItem = itemPath; } //"Returns S_OK if successful, or an error value otherwise. In the case of an error value, the delete operation //and all subsequent operations pending from the call to IFileOperation are canceled." return cancellationRequested ? HRESULT_FROM_WIN32(ERROR_CANCELLED) : S_OK; } virtual HRESULT STDMETHODCALLTYPE PostDeleteItem(DWORD dwFlags, __RPC__in_opt IShellItem* psiItem, HRESULT hrDelete, __RPC__in_opt IShellItem* psiNewlyCreated) { if (FAILED(hrDelete)) lastError = make_unique>(currentItem, hrDelete); currentItem.clear(); //"Returns S_OK if successful, or an error value otherwise. In the case of an error value, //all subsequent operations pending from the call to IFileOperation are canceled." return cancellationRequested ? HRESULT_FROM_WIN32(ERROR_CANCELLED) : S_OK; } virtual HRESULT STDMETHODCALLTYPE UpdateProgress(UINT iWorkTotal, UINT iWorkSoFar) { if (callback_) try { if (!callback_(currentItem.c_str(), sink_)) //should not throw! cancellationRequested = true; } catch (...) { return E_UNEXPECTED; } //"If this method succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code." //-> this probably means, we cannot rely on returning a custom error code here and have IFileOperation::PerformOperations() fail with same //=> defer cancellation to PreDeleteItem()/PostDeleteItem() return S_OK; } virtual HRESULT STDMETHODCALLTYPE ResetTimer() { return S_OK; } virtual HRESULT STDMETHODCALLTYPE PauseTimer() { return S_OK; } virtual HRESULT STDMETHODCALLTYPE ResumeTimer() { return S_OK; } //call after IFileOperation::PerformOperations() const std::pair* getLastError() const { return lastError.get(); } //(file path, error code) private: std::wstring currentItem; bool cancellationRequested; std::unique_ptr> lastError; //file_op user callback fileop::RecyclerCallback callback_; void* sink_; //support IUnknown LONG refCount; }; void moveToRecycleBin(const wchar_t* fileNames[], //throw ComError size_t fileCount, fileop::RecyclerCallback callback, void* sink) { ComPtr fileOp; ZEN_CHECK_COM(::CoCreateInstance(CLSID_FileOperation, //throw ComError nullptr, CLSCTX_ALL, IID_PPV_ARGS(fileOp.init()))); // Set the operation flags. Turn off all UI from being shown to the user during the // operation. This includes error, confirmation and progress dialogs. ZEN_CHECK_COM(fileOp->SetOperationFlags(FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT | //no progress dialog box FOF_NOERRORUI | FOFX_EARLYFAILURE | //without FOFX_EARLYFAILURE, IFileOperationProgressSink::PostDeleteItem() will always report success, even if deletion failed!!? WTF!? //PerformOperations() will still succeed but set the uselessly generic GetAnyOperationsAborted() instead :((( //=> always set FOFX_EARLYFAILURE since we prefer good error messages over "doing as much as possible" //luckily for FreeFileSync we don't expect failures on individual files anyway: FreeFileSync moves files to be //deleted to a temporary folder first, so there is no reason why a second move (the recycling itself) should fail FOF_NO_CONNECTED_ELEMENTS)); //use FOFX_RECYCLEONDELETE when Windows 8 is available!? ComPtr opProgress; *opProgress.init() = new (std::nothrow) RecyclerProgressCallback(callback, sink); if (!opProgress) throw ComError(L"Error creating RecyclerProgressCallback.", E_OUTOFMEMORY); DWORD callbackID = 0; ZEN_CHECK_COM(fileOp->Advise(opProgress.get(), &callbackID)); ZEN_ON_SCOPE_EXIT(fileOp->Unadvise(callbackID)); //RecyclerProgressCallback might outlive current scope, so cut access to "callback, sink" int operationCount = 0; for (size_t i = 0; i < fileCount; ++i) { //SHCreateItemFromParsingName() physically checks file existence => callback if (callback) { bool continueExecution = false; try { continueExecution = callback(fileNames[i], sink); //should not throw! } catch (...) { throw ComError(L"Unexpected exception in callback.", E_UNEXPECTED); } if (!continueExecution) throw ComError(L"Operation cancelled.", HRESULT_FROM_WIN32(ERROR_CANCELLED)); } //create file/folder item object ComPtr psiFile; HRESULT hr = ::SHCreateItemFromParsingName(fileNames[i], nullptr, IID_PPV_ARGS(psiFile.init())); if (FAILED(hr)) { if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || //file not existing anymore hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)) continue; throw ComError(std::wstring(L"Error calling \"SHCreateItemFromParsingName\" for file:\n") + L"\'" + fileNames[i] + L"\'.", hr); } ZEN_CHECK_COM(fileOp->DeleteItem(psiFile.get(), nullptr)); ++operationCount; } if (operationCount == 0) //calling PerformOperations() without anything to do would yielt E_UNEXPECTED return; //perform planned operations try { ZEN_CHECK_COM(fileOp->PerformOperations()); } catch (const ComError&) { //first let's check if we have more detailed error information available if (const std::pair* lastError = opProgress->getLastError()) { try //create an even better error message if we detect a locking issue: { std::vector processes = getLockingProcesses(lastError->first.c_str()); //throw Win32Error if (!processes.empty()) { std::wstring msg = L"The file \'" + lastError->first + L"\' is locked by another process:"; std::for_each(processes.begin(), processes.end(), [&](const std::wstring& proc) { msg += L'\n'; msg += proc; }); throw ComError(msg); //message is already descriptive enough, no need to add the HRESULT code } } catch (const Win32Error&) {} throw ComError(std::wstring(L"Error during \"PerformOperations\" for file:\n") + L"\'" + lastError->first + L"\'.", lastError->second); } throw; } //if FOF_NOERRORUI without FOFX_EARLYFAILURE is set, PerformOperations() can return with success despite errors, but sets the following "aborted" flag instead BOOL pfAnyOperationsAborted = FALSE; ZEN_CHECK_COM(fileOp->GetAnyOperationsAborted(&pfAnyOperationsAborted)); if (pfAnyOperationsAborted == TRUE) throw ComError(L"Operation did not complete successfully."); } void copyFile(const wchar_t* sourceFile, //throw ComError const wchar_t* targetFile) { ComPtr fileOp; ZEN_CHECK_COM(::CoCreateInstance(CLSID_FileOperation, //throw ComError nullptr, CLSCTX_ALL, IID_PPV_ARGS(fileOp.init()))); // Set the operation flags. Turn off all UI // from being shown to the user during the // operation. This includes error, confirmation // and progress dialogs. ZEN_CHECK_COM(fileOp->SetOperationFlags(FOF_NOCONFIRMATION | //throw ComError FOF_SILENT | FOFX_EARLYFAILURE | FOF_NOERRORUI)); //create source object ComPtr psiSourceFile; { HRESULT hr = ::SHCreateItemFromParsingName(sourceFile, nullptr, IID_PPV_ARGS(psiSourceFile.init())); if (FAILED(hr)) throw ComError(std::wstring(L"Error calling \"SHCreateItemFromParsingName\" for file:\n") + L"\'" + sourceFile + L"\'.", hr); } const size_t pos = std::wstring(targetFile).find_last_of(L'\\'); if (pos == std::wstring::npos) throw ComError(L"Target filename does not contain a path separator."); const std::wstring targetFolder(targetFile, pos); const std::wstring targetFileNameShort = targetFile + pos + 1; //create target folder object ComPtr psiTargetFolder; { HRESULT hr = ::SHCreateItemFromParsingName(targetFolder.c_str(), nullptr, IID_PPV_ARGS(psiTargetFolder.init())); if (FAILED(hr)) throw ComError(std::wstring(L"Error calling \"SHCreateItemFromParsingName\" for folder:\n") + L"\'" + targetFolder + L"\'.", hr); } //schedule file copy operation ZEN_CHECK_COM(fileOp->CopyItem(psiSourceFile.get(), psiTargetFolder.get(), targetFileNameShort.c_str(), nullptr)); //perform actual operations ZEN_CHECK_COM(fileOp->PerformOperations()); //check if errors occured: if FOFX_EARLYFAILURE is not used, PerformOperations() can return with success despite errors! BOOL pfAnyOperationsAborted = FALSE; ZEN_CHECK_COM(fileOp->GetAnyOperationsAborted(&pfAnyOperationsAborted)); if (pfAnyOperationsAborted == TRUE) throw ComError(L"Operation did not complete successfully."); } void getFolderClsid(const wchar_t* dirname, CLSID& pathCLSID) //throw ComError { ComPtr desktopFolder; ZEN_CHECK_COM(::SHGetDesktopFolder(desktopFolder.init())); //throw ComError PIDLIST_RELATIVE pidlFolder = nullptr; ZEN_CHECK_COM(desktopFolder->ParseDisplayName(nullptr, // [in] HWND hwnd, nullptr, // [in] IBindCtx *pbc, const_cast(dirname), // [in] LPWSTR pszDisplayName, nullptr, // [out] ULONG *pchEaten, &pidlFolder, // [out] PIDLIST_RELATIVE* ppidl, nullptr)); // [in, out] ULONG *pdwAttributes ZEN_ON_SCOPE_EXIT(::ILFree(pidlFolder)); //older version: ::CoTaskMemFree ComPtr persistFolder; ZEN_CHECK_COM(desktopFolder->BindToObject(pidlFolder, // [in] PCUIDLIST_RELATIVE pidl, nullptr, // [in] IBindCtx *pbc, IID_PPV_ARGS(persistFolder.init()))); //throw ComError ZEN_CHECK_COM(persistFolder->GetClassID(&pathCLSID)); //throw ComError } std::vector getLockingProcesses(const wchar_t* filename) //throw Win32Error { wchar_t sessionKey[CCH_RM_SESSION_KEY + 1] = {}; //fixes two bugs: http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx DWORD sessionHandle = 0; DWORD rv1 = ::RmStartSession(&sessionHandle, //__out DWORD *pSessionHandle, 0, //__reserved DWORD dwSessionFlags, sessionKey); //__out WCHAR strSessionKey[ ] if (rv1 != ERROR_SUCCESS) throw Win32Error(rv1); ZEN_ON_SCOPE_EXIT(::RmEndSession(sessionHandle)); DWORD rv2 = ::RmRegisterResources(sessionHandle, //__in DWORD dwSessionHandle, 1, //__in UINT nFiles, &filename, //__in_opt LPCWSTR rgsFilenames[ ], 0, //__in UINT nApplications, nullptr, //__in_opt RM_UNIQUE_PROCESS rgApplications[ ], 0, //__in UINT nServices, nullptr); //__in_opt LPCWSTR rgsServiceNames[ ] if (rv2 != ERROR_SUCCESS) throw Win32Error(rv2); UINT procInfoSize = 0; UINT procInfoSizeNeeded = 0; DWORD rebootReasons = 0; ::RmGetList(sessionHandle, &procInfoSizeNeeded, &procInfoSize, nullptr, &rebootReasons); //get procInfoSizeNeeded //fails with "access denied" for C:\pagefile.sys! if (procInfoSizeNeeded == 0) return std::vector(); procInfoSize = procInfoSizeNeeded; std::vector procInfo(procInfoSize); DWORD rv3 = ::RmGetList(sessionHandle, //__in DWORD dwSessionHandle, &procInfoSizeNeeded, //__out UINT *pnProcInfoNeeded, &procInfoSize, //__inout UINT *pnProcInfo, &procInfo[0], //__inout_opt RM_PROCESS_INFO rgAffectedApps[ ], &rebootReasons); //__out LPDWORD lpdwRebootReasons if (rv3 != ERROR_SUCCESS) throw Win32Error(rv3); procInfo.resize(procInfoSize); std::vector output; for (auto iter = procInfo.begin(); iter != procInfo.end(); ++iter) { std::wstring processName = iter->strAppName; //try to get process path HANDLE hProcess = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, //__in DWORD dwDesiredAccess, false, //__in BOOL bInheritHandle, iter->Process.dwProcessId); //__in DWORD dwProcessId if (hProcess) { ZEN_ON_SCOPE_EXIT(::CloseHandle(hProcess)); FILETIME creationTime = {}; FILETIME exitTime = {}; FILETIME kernelTime = {}; FILETIME userTime = {}; if (::GetProcessTimes(hProcess, //__in HANDLE hProcess, &creationTime, //__out LPFILETIME lpCreationTime, &exitTime, //__out LPFILETIME lpExitTime, &kernelTime, //__out LPFILETIME lpKernelTime, &userTime)) //__out LPFILETIME lpUserTime if (::CompareFileTime(&iter->Process.ProcessStartTime, &creationTime) == 0) { DWORD bufferSize = MAX_PATH; std::vector buffer(bufferSize); if (::QueryFullProcessImageName(hProcess, //__in HANDLE hProcess, 0, //__in DWORD dwFlags, &buffer[0], //__out LPTSTR lpExeName, &bufferSize)) //__inout PDWORD lpdwSize if (bufferSize < buffer.size()) processName += std::wstring(L" - ") + L"\'" + &buffer[0] + L"\'"; } } output.push_back(processName); } return output; } boost::thread_specific_ptr lastErrorMessage; //use "thread_local" in C++11 } bool fileop::moveToRecycleBin(const wchar_t* fileNames[], size_t fileCount, RecyclerCallback callback, void* sink) { try { ::moveToRecycleBin(fileNames, fileCount, callback, sink); //throw ComError return true; } catch (const ComError& e) { lastErrorMessage.reset(new std::wstring(e.toString())); return false; } } bool fileop::copyFile(const wchar_t* sourceFile, const wchar_t* targetFile) { try { ::copyFile(sourceFile, targetFile); //throw ComError return true; } catch (const ComError& e) { lastErrorMessage.reset(new std::wstring(e.toString())); return false; } } bool fileop::checkRecycler(const wchar_t* dirname, bool& isRecycler) { try { CLSID clsid = {}; getFolderClsid(dirname, clsid); //throw ComError isRecycler = ::IsEqualCLSID(clsid, CLSID_RecycleBin) == TRUE; //silence perf warning return true; } catch (const ComError& e) { lastErrorMessage.reset(new std::wstring(e.toString())); return false; } } const wchar_t* fileop::getLastError() { return !lastErrorMessage.get() ? L"" : lastErrorMessage->c_str(); } bool fileop::getLockingProcesses(const wchar_t* filename, const wchar_t*& procList) { try { std::vector processes = ::getLockingProcesses(filename); //throw Win32Error std::wstring buffer; std::for_each(processes.begin(), processes.end(), [&](const std::wstring& proc) { buffer += proc; buffer += L'\n'; }); if (!processes.empty()) buffer.resize(buffer.size() - 1); //remove last line break auto tmp = new wchar_t [buffer.size() + 1]; //bad_alloc ? ::wmemcpy(tmp, buffer.c_str(), buffer.size() + 1); //include 0-termination procList = tmp; //ownership passed return true; } catch (const Win32Error& e) { lastErrorMessage.reset(new std::wstring(formatWin32Msg(e.errorCode_))); return false; } } void fileop::freeString(const wchar_t* str) { delete [] str; }