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.

This commit is contained in:
Kovid Goyal 2014-02-25 22:45:41 +05:30
parent c8ddf2858f
commit f5301dbfef
5 changed files with 87 additions and 131 deletions

View File

@ -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')

View File

@ -1,28 +0,0 @@
/*
* recycle.c
* Copyright (C) 2013 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "Windows.h"
#include "Shellapi.h"
/* #include <wchar.h> */
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);
}

View File

@ -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):

View File

@ -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')

View File

@ -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 <kovid@kovidgoyal.net>'
__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]