transcode.py: improve interrupt handling
This commit is contained in:
+66
-31
@@ -13,6 +13,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import signal
|
||||
import subprocess
|
||||
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}")
|
||||
|
||||
# 1. Check existence logic (Skip if target exists, unless we are using a temp file for replacement)
|
||||
# If replace_mode is True and extensions differ, we still want to respect -n (no overwrite)
|
||||
# for the destination file if it already exists.
|
||||
# 1. Check existence logic
|
||||
if transcode_output_path.exists():
|
||||
logging.info(
|
||||
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
|
||||
|
||||
# 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..."))
|
||||
try:
|
||||
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:.]+)")
|
||||
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:
|
||||
while True:
|
||||
try:
|
||||
output = process.stdout.readline() # pyright: ignore[reportOptionalMemberAccess]
|
||||
if output == "" and process.poll() is not None:
|
||||
break
|
||||
while is_alive(process):
|
||||
# select.select() waits up to 0.5s for data to be ready.
|
||||
# This prevents the script from blocking indefinitely on .readline()
|
||||
# if the FFmpeg process hangs (e.g., D-state on network drive).
|
||||
reads = [process.stdout.fileno()]
|
||||
ret = select.select(reads, [], [], 0.5)
|
||||
|
||||
if reads[0] in ret[0]:
|
||||
output = process.stdout.readline()
|
||||
if output:
|
||||
# capture the elapsed time for later use, only keep the last occurence
|
||||
if regex_result := re.findall(regex_elapsed, output):
|
||||
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.flush()
|
||||
except KeyboardInterrupt:
|
||||
if not graceful_exit:
|
||||
logging.info(
|
||||
" > [GRACEFUL EXIT] Signal received. Finishing current file, then exiting script. Press Ctrl+C again to FORCE QUIT."
|
||||
)
|
||||
graceful_exit = True
|
||||
# Continue loop, process is still running because of start_new_session=True
|
||||
elif is_alive(process):
|
||||
# Output is empty but process is alive?
|
||||
# Could be buffering or momentary pause.
|
||||
time.sleep(0.1)
|
||||
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(
|
||||
" >> [FORCE QUIT] Signal received again. Terminating process..."
|
||||
" >> [FORCE QUIT] Signal received again. Killing process..."
|
||||
)
|
||||
# Explicitly terminate the process group or process
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
raise # Re-raise to trigger outer cleanup
|
||||
|
||||
# Wait for process to complete and get return code
|
||||
result = process.wait()
|
||||
except Exception as e:
|
||||
# Ensure we clean up temporary files on error (e.g. if we forced kill inside loop)
|
||||
if replace_mode and use_temp_file and transcode_output_path.exists():
|
||||
else:
|
||||
# This block handles the case where the interrupt happens exactly
|
||||
# as we re-enter the loop or in a race condition
|
||||
logging.warning(" >> [FORCE QUIT] Killing process...")
|
||||
process.terminate()
|
||||
try:
|
||||
transcode_output_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
raise e
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
raise
|
||||
|
||||
# Wait for process to complete and get return code
|
||||
result = process.wait()
|
||||
|
||||
if result == 0:
|
||||
logging.info(
|
||||
@@ -526,7 +556,7 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
|
||||
|
||||
else:
|
||||
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():
|
||||
try:
|
||||
@@ -541,6 +571,11 @@ def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=Fal
|
||||
|
||||
except Exception as 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:
|
||||
# This catches the re-raised exception from the inner loop (2nd Ctrl+C / Force Quit)
|
||||
logging.info("Transcoding aborted by user.")
|
||||
|
||||
Reference in New Issue
Block a user