From f5301dbfef22fd2b8e9074976a577afe1a846d50 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 25 Feb 2014 22:45:41 +0530 Subject: [PATCH] Windows: Use a separate worker process to move files to the recycle bin. Fixes the problem of the recycle bin icon not being updated when deleting books into an empty recycle bin. Also avoids the overhead of launching a new, short-lived worker process for every delete. --- setup/installer/windows/freeze.py | 17 --- setup/installer/windows/recycle.c | 28 ----- src/calibre/utils/ipc/simple_worker.py | 25 +++++ src/calibre/utils/ipc/worker.py | 3 + src/calibre/utils/recycle_bin.py | 145 ++++++++++--------------- 5 files changed, 87 insertions(+), 131 deletions(-) delete mode 100644 setup/installer/windows/recycle.c diff --git a/setup/installer/windows/freeze.py b/setup/installer/windows/freeze.py index b52b375217..08aa91fe10 100644 --- a/setup/installer/windows/freeze.py +++ b/setup/installer/windows/freeze.py @@ -42,7 +42,6 @@ DESCRIPTIONS = { 'calibre-server': 'Standalone calibre content server', 'calibre-parallel': 'calibre worker process', 'calibre-smtp' : 'Command line interface for sending books via email', - 'calibre-recycle' : 'Helper program for deleting to recycle bin', 'calibre-eject' : 'Helper program for ejecting connected reader devices', } @@ -85,7 +84,6 @@ class Win32Freeze(Command, WixMixIn): self.initbase() self.build_launchers() self.build_eject() - self.build_recycle() self.add_plugins() self.freeze() self.embed_manifests() @@ -546,21 +544,6 @@ class Win32Freeze(Command, WixMixIn): finally: os.chdir(cwd) - def build_recycle(self): - self.info('Building calibre-recycle.exe') - base = self.j(self.src_root, 'setup', 'installer', 'windows') - src = self.j(base, 'recycle.c') - obj = self.j(self.obj_dir, self.b(src)+'.obj') - cflags = '/c /EHsc /MD /W3 /Ox /nologo /D_UNICODE'.split() - if self.newer(obj, src): - cmd = [msvc.cc] + cflags + ['/Fo'+obj, '/Tc'+src] - self.run_builder(cmd, show_output=True) - exe = self.j(self.base, 'calibre-recycle.exe') - cmd = [msvc.linker] + ['/MACHINE:'+machine, - '/SUBSYSTEM:CONSOLE', '/RELEASE', - '/OUT:'+exe] + [self.embed_resources(exe), obj, 'Shell32.lib'] - self.run_builder(cmd) - def build_eject(self): self.info('Building calibre-eject.exe') base = self.j(self.src_root, 'setup', 'installer', 'windows') diff --git a/setup/installer/windows/recycle.c b/setup/installer/windows/recycle.c deleted file mode 100644 index 3e51bc07a1..0000000000 --- a/setup/installer/windows/recycle.c +++ /dev/null @@ -1,28 +0,0 @@ -/* - * recycle.c - * Copyright (C) 2013 Kovid Goyal - * - * Distributed under terms of the GPL3 license. - */ - -#include "Windows.h" -#include "Shellapi.h" -/* #include */ - -int wmain(int argc, wchar_t *argv[ ]) { - wchar_t buf[512] = {0}; - SHFILEOPSTRUCTW op = {0}; - if (argc != 2) return 1; - if (wcsnlen_s(argv[1], 512) > 510) return 1; - if (wcscpy_s(buf, 512, argv[1]) != 0) return 1; - - op.wFunc = FO_DELETE; - op.pFrom = buf; - op.pTo = NULL; - op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI | FOF_SILENT | FOF_RENAMEONCOLLISION; - - /* wprintf(L"%ls\n", buf); */ - return SHFileOperationW(&op); -} - - diff --git a/src/calibre/utils/ipc/simple_worker.py b/src/calibre/utils/ipc/simple_worker.py index f21e44c823..fa580d67b3 100644 --- a/src/calibre/utils/ipc/simple_worker.py +++ b/src/calibre/utils/ipc/simple_worker.py @@ -136,6 +136,31 @@ def create_worker(env, priority='normal', cwd=None, func='main'): w(cwd=cwd, priority=priority) return listener, w +def start_pipe_worker(command, env=None, priority='normal'): + import subprocess, atexit + from functools import partial + w = Worker(env or {}) + args = {'stdout':subprocess.PIPE, 'stdin':subprocess.PIPE, 'env':w.env} + if iswindows: + import win32process + priority = { + 'high' : win32process.HIGH_PRIORITY_CLASS, + 'normal' : win32process.NORMAL_PRIORITY_CLASS, + 'low' : win32process.IDLE_PRIORITY_CLASS}[priority] + args['creationflags'] = win32process.CREATE_NO_WINDOW|priority + else: + def renice(niceness): + try: + os.nice(niceness) + except: + pass + niceness = {'normal' : 0, 'low' : 10, 'high' : 20}[priority] + args['preexec_fn'] = partial(renice, niceness) + + p = subprocess.Popen([w.executable, '--pipe-worker', command], **args) + atexit.register(w.kill) + return p + def fork_job(mod_name, func_name, args=(), kwargs={}, timeout=300, # seconds cwd=None, priority='normal', env={}, no_output=False, heartbeat=None, abort=None, module_is_source_code=False): diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index 95ec1ccb1d..cc4a2a73fa 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -175,6 +175,9 @@ def main(): func = getattr(mod, func) func() return + if '--pipe-worker' in sys.argv: + exec (sys.argv[-1]) + return address = cPickle.loads(unhexlify(os.environ['CALIBRE_WORKER_ADDRESS'])) key = unhexlify(os.environ['CALIBRE_WORKER_KEY']) resultf = unhexlify(os.environ['CALIBRE_WORKER_RESULT']).decode('utf-8') diff --git a/src/calibre/utils/recycle_bin.py b/src/calibre/utils/recycle_bin.py index 9721b1e623..f253de345a 100644 --- a/src/calibre/utils/recycle_bin.py +++ b/src/calibre/utils/recycle_bin.py @@ -1,113 +1,86 @@ #!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import print_function __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, shutil, time +import os, shutil, time, sys -from calibre import isbytestring, force_unicode +from calibre import isbytestring from calibre.constants import (iswindows, isosx, plugins, filesystem_encoding, islinux) recycle = None if iswindows: - import ctypes, subprocess, sys - from ctypes import POINTER, Structure - from ctypes.wintypes import HANDLE, LPVOID, WORD, DWORD, BOOL, ULONG, LPCWSTR - RECYCLE = force_unicode(os.path.join(os.path.dirname(sys.executable), 'calibre-recycle.exe'), filesystem_encoding) - LPDWORD = POINTER(DWORD) - LPHANDLE = POINTER(HANDLE) - ULONG_PTR = POINTER(ULONG) - CREATE_NO_WINDOW = 0x08000000 - INFINITE = 0xFFFFFFFF - WAIT_FAILED = 0xFFFFFFFF + from calibre.utils.ipc import eintr_retry_call + from threading import Lock + recycler = None + rlock = Lock() + def start_recycler(): + global recycler + if recycler is None: + from calibre.utils.ipc.simple_worker import start_pipe_worker + recycler = start_pipe_worker('from calibre.utils.recycle_bin import recycler_main; recycler_main()') - class SECURITY_ATTRIBUTES(Structure): - _fields_ = [("nLength", DWORD), - ("lpSecurityDescriptor", LPVOID), - ("bInheritHandle", BOOL)] - LPSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES) + def recycle_path(path): + from win32com.shell import shell, shellcon + flags = (shellcon.FOF_ALLOWUNDO | shellcon.FOF_NOCONFIRMATION | shellcon.FOF_NOCONFIRMMKDIR | shellcon.FOF_NOERRORUI | + shellcon.FOF_SILENT | shellcon.FOF_RENAMEONCOLLISION) + retcode, aborted = shell.SHFileOperation((0, shellcon.FO_DELETE, path, None, flags, None, None)) + if retcode != 0 or aborted: + raise RuntimeError('Failed to delete: %r with error code: %d' % (path, retcode)) - class STARTUPINFO(Structure): - _fields_ = [("cb", DWORD), - ("lpReserved", LPCWSTR), - ("lpDesktop", LPCWSTR), - ("lpTitle", LPCWSTR), - ("dwX", DWORD), - ("dwY", DWORD), - ("dwXSize", DWORD), - ("dwYSize", DWORD), - ("dwXCountChars", DWORD), - ("dwYCountChars", DWORD), - ("dwFillAttribute", DWORD), - ("dwFlags", DWORD), - ("wShowWindow", WORD), - ("cbReserved2", WORD), - ("lpReserved2", LPVOID), - ("hStdInput", HANDLE), - ("hStdOutput", HANDLE), - ("hStdError", HANDLE)] - LPSTARTUPINFO = POINTER(STARTUPINFO) + def recycler_main(): + while True: + path = eintr_retry_call(sys.stdin.readline) + if not path: + break + try: + path = path.decode('utf-8').rstrip() + except (ValueError, TypeError): + break + try: + recycle_path(path) + except: + eintr_retry_call(print, b'KO', file=sys.stdout) + sys.stdout.flush() + import traceback + traceback.print_exc() # goes to stderr, which is the same as for parent process + else: + eintr_retry_call(print, b'OK', file=sys.stdout) + sys.stdout.flush() - class PROCESS_INFORMATION(Structure): - _fields_ = [("hProcess", HANDLE), - ("hThread", HANDLE), - ("dwProcessId", DWORD), - ("dwThreadId", DWORD)] - LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION) - - CreateProcess = ctypes.windll.kernel32.CreateProcessW - CreateProcess.argtypes = [LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES, - LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFO, - LPPROCESS_INFORMATION] - CreateProcess.restype = BOOL - - WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject - WaitForSingleObject.argtypes = [HANDLE, DWORD] - WaitForSingleObject.restype = DWORD - - GetExitCodeProcess = ctypes.windll.kernel32.GetExitCodeProcess - GetExitCodeProcess.argtypes = [HANDLE, LPDWORD] - GetExitCodeProcess.restype = BOOL - - CloseHandle = ctypes.windll.kernel32.CloseHandle - CloseHandle.argtypes = [HANDLE] - CloseHandle.restype = BOOL + def delegate_recycle(path): + if '\n' in path: + raise ValueError('Cannot recycle paths that have newlines in them (%r)' % path) + with rlock: + start_recycler() + eintr_retry_call(print, path.encode('utf-8'), file=recycler.stdin) + recycler.stdin.flush() + # Theoretically this could be made non-blocking using a + # thread+queue, however the original implementation was blocking, + # so I am leaving it as blocking. + result = eintr_retry_call(recycler.stdout.readline) + if result.rstrip() != b'OK': + raise RuntimeError('recycler failed to recycle: %r' % path) def recycle(path): # We have to run the delete to recycle bin in a separate process as the # morons who wrote SHFileOperation designed it to spin the event loop # even when no UI is created. And there is no other way to send files - # to the recycle bin on windows. Le Sigh. We dont use subprocess since - # there is no way to pass unicode arguments with subprocess in 2.7 and - # the twit that maintains subprocess believes that this is not an - # bug but a request for a new feature. + # to the recycle bin on windows. Le Sigh. So we do it in a worker + # process. Unfortunately, if the worker process exits immediately after + # deleting to recycle bin, winblows does not update the recycle bin + # icon. Le Double Sigh. So we use a long lived worker process, that is + # started on first recycle, and sticks around to handle subsequent + # recycles. if isinstance(path, bytes): path = path.decode(filesystem_encoding) - si = STARTUPINFO() - si.cb = ctypes.sizeof(si) - pi = PROCESS_INFORMATION() - exit_code = DWORD() - cmd = subprocess.list2cmdline([RECYCLE, path]) - dwCreationFlags = CREATE_NO_WINDOW - if not CreateProcess(None, cmd, None, None, False, dwCreationFlags, - None, None, ctypes.byref(si), ctypes.byref(pi)): - raise ctypes.WinError() - try: - if WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED: - raise ctypes.WinError() - if not GetExitCodeProcess(pi.hProcess, ctypes.byref(exit_code)): - raise ctypes.WinError() - - finally: - CloseHandle(pi.hThread) - CloseHandle(pi.hProcess) - exit_code = exit_code.value - if exit_code != 0: - raise ctypes.WinError(exit_code) + path = os.path.abspath(path) # Windows does not like recycling relative paths + return delegate_recycle(path) elif isosx: u = plugins['usbobserver'][0]