From c0e71e13f834938f100d36919cfa6da9f7ce79e8 Mon Sep 17 00:00:00 2001 From: Will Toohey Date: Sun, 26 Apr 2026 00:26:31 +1000 Subject: [PATCH] (vibe coded) Fix KeyboardInterrupt handling in threadpool --- src/ifstools/ifs.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/ifstools/ifs.py b/src/ifstools/ifs.py index 29a07de..74643b4 100644 --- a/src/ifstools/ifs.py +++ b/src/ifstools/ifs.py @@ -181,7 +181,10 @@ class IFS: # extract the files in parallel — the LZ77 native extension and PIL's # PNG codec both release the GIL, so threads scale across cores. - with ThreadPoolExecutor() as ex: + # Manage the executor manually so KeyboardInterrupt cancels pending + # work instead of waiting for the whole queue to drain. + ex = ThreadPoolExecutor() + try: futures = {ex.submit(f.extract, path, **kwargs): f for f in to_extract} with tqdm(total=len(to_extract), disable=not progress) as bar: for fut in as_completed(futures): @@ -190,6 +193,8 @@ class IFS: if progress: tqdm.write(f.full_path) bar.update(1) + finally: + ex.shutdown(wait=False, cancel_futures=True) # nested IFS extraction is sequential: each child opens its own thread # pool so we'd otherwise oversubscribe. @@ -256,8 +261,11 @@ class IFS: # PNG decode (PIL) and LZ77 compress (Rust) both release the GIL, so # threads scale. The actual write loop is serial; this stages each - # file's packed bytes in memory. - with ThreadPoolExecutor() as ex: + # file's packed bytes in memory. Manage the executor manually so + # KeyboardInterrupt cancels pending work instead of waiting on the + # whole queue. + ex = ThreadPoolExecutor() + try: futures = {ex.submit(f.preload, **kwargs): f for f in to_compress} with tqdm(total=len(to_compress), desc='Compressing', disable=not progress) as bar: for fut in as_completed(futures): @@ -266,6 +274,8 @@ class IFS: if progress: tqdm.write(f.full_path) bar.update(1) + finally: + ex.shutdown(wait=False, cancel_futures=True) tqdm_progress = None if progress: