mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Work on an atexit implementation that survives a crash
This commit is contained in:
parent
32888f5f5d
commit
e3f6c88d2c
@ -308,6 +308,8 @@ def find_tests(which_tests=None, exclude_tests=None):
|
|||||||
a(find_tests())
|
a(find_tests())
|
||||||
from calibre.utils.copy_files_test import find_tests
|
from calibre.utils.copy_files_test import find_tests
|
||||||
a(find_tests())
|
a(find_tests())
|
||||||
|
from calibre.utils.safe_atexit import find_tests
|
||||||
|
a(find_tests())
|
||||||
if iswindows:
|
if iswindows:
|
||||||
from calibre.utils.windows.wintest import find_tests
|
from calibre.utils.windows.wintest import find_tests
|
||||||
a(find_tests())
|
a(find_tests())
|
||||||
|
134
src/calibre/utils/safe_atexit.py
Normal file
134
src/calibre/utils/safe_atexit.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
# Atexit that works even if the process crashes
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from contextlib import suppress
|
||||||
|
from functools import wraps
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
from calibre.constants import iswindows
|
||||||
|
from calibre.utils.filenames import make_long_path_useable
|
||||||
|
from calibre.utils.ipc.simple_worker import start_pipe_worker
|
||||||
|
|
||||||
|
lock = Lock()
|
||||||
|
worker = None
|
||||||
|
RMTREE_ACTION = 'rmtree'
|
||||||
|
|
||||||
|
|
||||||
|
def thread_safe(f):
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*a, **kw):
|
||||||
|
with lock:
|
||||||
|
return f(*a, **kw)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@thread_safe
|
||||||
|
def remove_folder(path: str) -> None:
|
||||||
|
_send_command(RMTREE_ACTION, os.path.abspath(path))
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_worker():
|
||||||
|
global worker
|
||||||
|
if worker is None:
|
||||||
|
worker = start_pipe_worker('from calibre.utils.safe_atexit import main; main()', stdout=None)
|
||||||
|
def close_worker():
|
||||||
|
worker.stdin.close()
|
||||||
|
worker.wait(10)
|
||||||
|
atexit.register(close_worker)
|
||||||
|
return worker
|
||||||
|
|
||||||
|
|
||||||
|
def _send_command(action: str, payload: str) -> None:
|
||||||
|
worker = ensure_worker()
|
||||||
|
worker.stdin.write(json.dumps({'action': action, 'payload': payload}).encode('utf-8'))
|
||||||
|
worker.stdin.write(os.linesep.encode())
|
||||||
|
worker.stdin.flush()
|
||||||
|
|
||||||
|
|
||||||
|
if iswindows:
|
||||||
|
def remove_dir(x):
|
||||||
|
x = make_long_path_useable(x)
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
for i in range(10):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(x)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
import os # noqa
|
||||||
|
if os.path.exists(x):
|
||||||
|
# In case some other program has one of the files open.
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
with suppress(Exception):
|
||||||
|
shutil.rmtree(x, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
def remove_dir(x):
|
||||||
|
import shutil
|
||||||
|
with suppress(Exception):
|
||||||
|
shutil.rmtree(x, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for line in sys.stdin.buffer:
|
||||||
|
if line:
|
||||||
|
try:
|
||||||
|
cmd = json.loads(line)
|
||||||
|
if cmd['action'] == RMTREE_ACTION:
|
||||||
|
atexit.register(remove_dir, cmd['payload'])
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
def main_for_test(do_forced_exit=False):
|
||||||
|
tf = 'test-folder'
|
||||||
|
os.mkdir(tf)
|
||||||
|
open(os.path.join(tf, 'test-file'), 'w').close()
|
||||||
|
remove_folder(tf)
|
||||||
|
if do_forced_exit:
|
||||||
|
os._exit(os.EX_OK)
|
||||||
|
else:
|
||||||
|
sys.stdin.read()
|
||||||
|
|
||||||
|
|
||||||
|
def find_tests():
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestSafeAtexit(unittest.TestCase):
|
||||||
|
|
||||||
|
def wait_for_empty(self, tdir, timeout=10):
|
||||||
|
st = time.monotonic()
|
||||||
|
while time.monotonic() - st < timeout:
|
||||||
|
q = os.listdir(tdir)
|
||||||
|
if not q:
|
||||||
|
break
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.assertFalse(q)
|
||||||
|
|
||||||
|
def test_safe_atexit(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tdir:
|
||||||
|
self.assertFalse(os.listdir(tdir))
|
||||||
|
p = start_pipe_worker('from calibre.utils.safe_atexit import main_for_test; main_for_test()', cwd=tdir)
|
||||||
|
p.stdin.close()
|
||||||
|
p.wait(10)
|
||||||
|
self.wait_for_empty(tdir)
|
||||||
|
p = start_pipe_worker('from calibre.utils.safe_atexit import main_for_test; main_for_test()', cwd=tdir)
|
||||||
|
p.kill()
|
||||||
|
p.wait(10)
|
||||||
|
self.wait_for_empty(tdir)
|
||||||
|
p = start_pipe_worker('from calibre.utils.safe_atexit import main_for_test; main_for_test(True)', cwd=tdir)
|
||||||
|
p.wait(10)
|
||||||
|
self.wait_for_empty(tdir)
|
||||||
|
|
||||||
|
return unittest.defaultTestLoader.loadTestsFromTestCase(TestSafeAtexit)
|
Loading…
x
Reference in New Issue
Block a user