transcode.py: improve interrupt handling

This commit is contained in:
2025-12-06 20:10:00 +01:00
parent 2a1aa37ae9
commit c9cfd8deb6
+66 -31
View File
@@ -13,6 +13,7 @@ import json
import logging import logging
import os import os
import re import re
import select
import signal import signal
import subprocess import subprocess
import sys import sys
@@ -310,9 +311,7 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
logging.debug(f"Final transcode target: {transcode_output_path}") logging.debug(f"Final transcode target: {transcode_output_path}")
# 1. Check existence logic (Skip if target exists, unless we are using a temp file for replacement) # 1. Check existence logic
# If replace_mode is True and extensions differ, we still want to respect -n (no overwrite)
# for the destination file if it already exists.
if transcode_output_path.exists(): if transcode_output_path.exists():
logging.info( logging.info(
leftalign(f"SKIP: Output file already exists: {transcode_output_path}") leftalign(f"SKIP: Output file already exists: {transcode_output_path}")
@@ -320,6 +319,10 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
return return
# 2. Check if file is ready (simple size stability check) # 2. Check if file is ready (simple size stability check)
if not input_path.exists():
logging.warning(f"File vanished during checks: {input_path}")
return
logging.info(leftalign("WAIT: Ensuring file is ready...")) logging.info(leftalign("WAIT: Ensuring file is ready..."))
try: try:
historical_size = -1 historical_size = -1
@@ -389,15 +392,22 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
regex_elapsed = re.compile(r"elapsed=([0-9:.]+)") regex_elapsed = re.compile(r"elapsed=([0-9:.]+)")
transcoding_duration = None transcoding_duration = None
# Write FFmpeg output with timestamps as it's generated # Helper to check if process is alive
def is_alive(p):
return p.poll() is None
# Non-blocking read loop using select
try: try:
while True: while is_alive(process):
try: # select.select() waits up to 0.5s for data to be ready.
output = process.stdout.readline() # pyright: ignore[reportOptionalMemberAccess] # This prevents the script from blocking indefinitely on .readline()
if output == "" and process.poll() is not None: # if the FFmpeg process hangs (e.g., D-state on network drive).
break reads = [process.stdout.fileno()]
ret = select.select(reads, [], [], 0.5)
if reads[0] in ret[0]:
output = process.stdout.readline()
if output: if output:
# capture the elapsed time for later use, only keep the last occurence
if regex_result := re.findall(regex_elapsed, output): if regex_result := re.findall(regex_elapsed, output):
transcoding_duration = regex_result[0] transcoding_duration = regex_result[0]
@@ -406,36 +416,56 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
) )
f_log.write(f"[{timestamp}] {output.rstrip()}\n") f_log.write(f"[{timestamp}] {output.rstrip()}\n")
f_log.flush() f_log.flush()
except KeyboardInterrupt: elif is_alive(process):
if not graceful_exit: # Output is empty but process is alive?
logging.info( # Could be buffering or momentary pause.
" > [GRACEFUL EXIT] Signal received. Finishing current file, then exiting script. Press Ctrl+C again to FORCE QUIT." time.sleep(0.1)
)
graceful_exit = True
# Continue loop, process is still running because of start_new_session=True
continue continue
else:
except KeyboardInterrupt:
if not graceful_exit:
logging.info(
" > [GRACEFUL EXIT] Signal received. Processing will finish, then script will exit."
)
logging.info(" > Press Ctrl+C again to FORCE QUIT immediately.")
graceful_exit = True
# Resume waiting for the process to finish naturally.
while is_alive(process):
try:
# We still need to drain the output pipe to prevent FFmpeg from blocking
reads = [process.stdout.fileno()]
ret = select.select(reads, [], [], 0.5)
if reads[0] in ret[0]:
output = process.stdout.readline()
if output:
timestamp = datetime.datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)
f_log.write(f"[{timestamp}] {output.rstrip()}\n")
except KeyboardInterrupt:
# User pressed Ctrl+C a second time inside the graceful wait loop
logging.warning( logging.warning(
" >> [FORCE QUIT] Signal received again. Terminating process..." " >> [FORCE QUIT] Signal received again. Killing process..."
) )
# Explicitly terminate the process group or process
process.terminate() process.terminate()
try: try:
process.wait(timeout=5) process.wait(timeout=5)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
process.kill() process.kill()
raise # Re-raise to trigger outer cleanup raise # Re-raise to trigger outer cleanup
else:
# Wait for process to complete and get return code # This block handles the case where the interrupt happens exactly
result = process.wait() # as we re-enter the loop or in a race condition
except Exception as e: logging.warning(" >> [FORCE QUIT] Killing process...")
# Ensure we clean up temporary files on error (e.g. if we forced kill inside loop) process.terminate()
if replace_mode and use_temp_file and transcode_output_path.exists():
try: try:
transcode_output_path.unlink() process.wait(timeout=5)
except Exception: except subprocess.TimeoutExpired:
pass process.kill()
raise e raise
# Wait for process to complete and get return code
result = process.wait()
if result == 0: if result == 0:
logging.info( logging.info(
@@ -526,7 +556,7 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
else: else:
logging.error( logging.error(
f"FFmpeg failed for {input_path}. See log at {ffmpeg_log_file}" f"FFmpeg failed for {input_path} with exit code {result}. See log at {ffmpeg_log_file}"
) )
if replace_mode and use_temp_file and transcode_output_path.exists(): if replace_mode and use_temp_file and transcode_output_path.exists():
try: try:
@@ -541,6 +571,11 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
except Exception as e: except Exception as e:
logging.exception(f"Unexpected error during transcoding of {input_path}: {e}") logging.exception(f"Unexpected error during transcoding of {input_path}: {e}")
if replace_mode and use_temp_file and transcode_output_path.exists():
try:
transcode_output_path.unlink()
except Exception:
pass
except KeyboardInterrupt: except KeyboardInterrupt:
# This catches the re-raised exception from the inner loop (2nd Ctrl+C / Force Quit) # This catches the re-raised exception from the inner loop (2nd Ctrl+C / Force Quit)
logging.info("Transcoding aborted by user.") logging.info("Transcoding aborted by user.")