Fix Stop Translation button

This commit is contained in:
dazedanon 2025-07-18 16:51:56 -05:00
parent 85ad7c5914
commit ab7ec9eb23

View file

@ -10,8 +10,10 @@ import subprocess
import threading
import sys
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import ThreadPoolExecutor, as_completed, Future
import traceback
import signal
import multiprocessing
from colorama import Fore
from tqdm import tqdm
from dotenv import load_dotenv
@ -20,7 +22,7 @@ from PyQt5.QtWidgets import (
QTextEdit, QMessageBox, QListWidget, QListWidgetItem,
QSplitter, QFileDialog, QComboBox, QCheckBox, QProgressBar
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex, QProcess
from PyQt5.QtGui import QFont
@ -38,12 +40,48 @@ class TranslationWorker(QThread):
self.estimate_only = estimate_only
self.should_stop = False
self.mutex = QMutex() # For thread safety
self.executor = None # Store reference to executor for proper shutdown
self.running_processes = [] # Track running processes for termination
def stop(self):
"""Stop the translation process."""
self.mutex.lock()
try:
if self.should_stop:
# Already stopping, don't log again
return
self.should_stop = True
self.emit_log("🛑 Stopping translation worker and canceling pending tasks...")
# Shutdown the executor if it exists
if self.executor:
# For older Python versions compatibility, use shutdown(wait=False)
# and manually cancel futures
try:
# Try to use cancel_futures parameter (Python 3.9+)
self.executor.shutdown(wait=False, cancel_futures=True)
except TypeError:
# Fallback for older Python versions
self.executor.shutdown(wait=False)
# Terminate any running processes
if self.running_processes:
self.emit_log("🛑 Terminating running translation processes...")
for process in self.running_processes:
try:
if process.poll() is None: # Process is still running
process.terminate()
# Give it a moment to terminate gracefully
try:
process.wait(timeout=2)
except subprocess.TimeoutExpired:
# Force kill if it doesn't terminate
process.kill()
process.wait()
except Exception as e:
self.emit_log(f"⚠️ Warning: Could not terminate process: {e}")
self.running_processes.clear()
finally:
self.mutex.unlock()
@ -55,6 +93,194 @@ class TranslationWorker(QThread):
"""Thread-safe progress emission."""
self.progress_signal.emit(current, total, filename)
def run_module_in_process(self, filename, estimate_only):
"""Run a module handler in a separate process for better control."""
try:
# Create a separate Python process to run the module
# This allows us to terminate it if needed
script_content = f'''
import sys
import os
from pathlib import Path
import io
from contextlib import redirect_stdout, redirect_stderr
# Set UTF-8 encoding for stdout to handle Unicode characters
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
# Add project root to path
project_root = Path(r"{self.project_root}")
sys.path.insert(0, str(project_root))
try:
# Change to project directory
os.chdir(str(project_root))
# Import and run the handler
{self.get_import_statement()}
# Capture all output in a string buffer to avoid encoding issues
output_buffer = io.StringIO()
error_buffer = io.StringIO()
with redirect_stdout(output_buffer), redirect_stderr(error_buffer):
handler_result = {self.get_handler_call()}(r"{filename}", {estimate_only})
# Print only the result, avoiding any Unicode characters from tqdm
if handler_result:
# Clean the result of any problematic Unicode characters
clean_result = str(handler_result).encode('ascii', 'ignore').decode('ascii')
print(f"RESULT:{{clean_result}}")
else:
print("RESULT:Fail")
except Exception as e:
import traceback
error_msg = str(e).encode('ascii', 'ignore').decode('ascii')
print(f"ERROR:{{error_msg}}")
sys.exit(1)
'''
# Write script to temporary file
temp_script = self.project_root / "temp_translation_script.py"
with open(temp_script, 'w', encoding='utf-8') as f:
f.write(script_content)
# Run the script in a separate process
env = os.environ.copy()
env['PYTHONIOENCODING'] = 'utf-8' # Force UTF-8 encoding
process = subprocess.Popen(
[sys.executable, str(temp_script)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
errors='replace',
cwd=str(self.project_root),
env=env
)
# Track the process for potential termination
self.running_processes.append(process)
# Wait for completion
stdout, stderr = process.communicate()
# Remove from tracking
if process in self.running_processes:
self.running_processes.remove(process)
# Clean up temp file
if temp_script.exists():
temp_script.unlink()
# Check if process was terminated by stop signal
if self.should_stop:
return "Stopped"
# Parse result
if process.returncode == 0:
for line in stdout.strip().split('\n'):
if line.startswith('RESULT:'):
result_text = line[7:] # Remove 'RESULT:' prefix
# Clean up any Unicode issues in the result
try:
return result_text
except UnicodeError:
return result_text.encode('ascii', 'ignore').decode('ascii')
return "Success"
else:
error_msg = stderr.strip() if stderr.strip() else "Unknown error"
# Handle potential Unicode errors in error messages
try:
clean_error = error_msg.encode('ascii', 'ignore').decode('ascii')
except:
clean_error = "Unicode encoding error in process output"
self.emit_log(f"❌ Process error: {clean_error}")
return "Fail"
except Exception as e:
self.emit_log(f"❌ Failed to run module process: {str(e)}")
return "Fail"
def get_import_statement(self):
"""Get the import statement for the selected module."""
module_name = self.module_info[0]
if "RPG Maker MV/MZ" in module_name:
return "from modules.rpgmakermvmz import handleMVMZ"
elif "RPG Maker Ace" in module_name:
return "from modules.rpgmakerace import handleACE"
elif "CSV" in module_name:
return "from modules.csv import handleCSV"
elif "Tyrano" in module_name:
return "from modules.tyrano import handleTyrano"
elif "Kirikiri" in module_name:
return "from modules.kirikiri import handleKirikiri"
elif "JSON" in module_name:
return "from modules.json import handleJSON"
elif "Lune" in module_name:
return "from modules.lune import handleLune"
elif "NScript" in module_name:
return "from modules.nscript import handleOnscripter"
elif "Wolf RPG 2" in module_name:
return "from modules.wolf2 import handleWOLF2"
elif "Wolf RPG" in module_name:
return "from modules.wolf import handleWOLF"
elif "Regex" in module_name:
return "from modules.regex import handleRegex"
elif "Text" in module_name:
return "from modules.text import handleText"
elif "RenPy" in module_name:
return "from modules.renpy import handleRenpy"
elif "Unity" in module_name:
return "from modules.unity import handleUnity"
elif "Images" in module_name:
return "from modules.images import handleImages"
elif "Plugin" in module_name:
return "from modules.rpgmakerplugin import handlePlugin"
else:
return "# Unknown module"
def get_handler_call(self):
"""Get the handler function call for the selected module."""
module_name = self.module_info[0]
if "RPG Maker MV/MZ" in module_name:
return "handleMVMZ"
elif "RPG Maker Ace" in module_name:
return "handleACE"
elif "CSV" in module_name:
return "handleCSV"
elif "Tyrano" in module_name:
return "handleTyrano"
elif "Kirikiri" in module_name:
return "handleKirikiri"
elif "JSON" in module_name:
return "handleJSON"
elif "Lune" in module_name:
return "handleLune"
elif "NScript" in module_name:
return "handleOnscripter"
elif "Wolf RPG 2" in module_name:
return "handleWOLF2"
elif "Wolf RPG" in module_name:
return "handleWOLF"
elif "Regex" in module_name:
return "handleRegex"
elif "Text" in module_name:
return "handleText"
elif "RenPy" in module_name:
return "handleRenpy"
elif "Unity" in module_name:
return "handleUnity"
elif "Images" in module_name:
return "handleImages"
elif "Plugin" in module_name:
return "handlePlugin"
else:
return "lambda f, e: 'Unknown module'"
def run(self):
"""Run the translation process."""
try:
@ -113,38 +339,61 @@ class TranslationWorker(QThread):
os.chdir(str(self.project_root))
try:
with ThreadPoolExecutor(max_workers=threads) as executor:
# Submit all tasks first
future_to_filename = {
executor.submit(self.module_info[2], filename, self.estimate_only): filename
for filename in matching_files
}
completed_count = 0
total_files = len(matching_files)
for future in as_completed(future_to_filename):
if self.should_stop:
self.emit_log("🛑 Translation stopped by user")
break
filename = future_to_filename[future]
completed_count += 1
# Use a simpler approach with limited parallelism
# to have better control over stopping
max_workers = min(threads, 2) # Limit to 2 concurrent processes max
self.executor = ThreadPoolExecutor(max_workers=max_workers)
# Submit tasks to run modules in separate processes
future_to_filename = {
self.executor.submit(self.run_module_in_process, filename, self.estimate_only): filename
for filename in matching_files
}
completed_count = 0
total_files = len(matching_files)
for future in as_completed(future_to_filename):
if self.should_stop:
# Don't log here, the stop() method already logged
# Cancel remaining futures
for remaining_future in future_to_filename:
if not remaining_future.done():
remaining_future.cancel()
break
# Emit progress signal (less frequent updates)
self.emit_progress(completed_count, total_files, filename)
try:
result = future.result()
filename = future_to_filename[future]
completed_count += 1
# Emit progress signal (less frequent updates)
self.emit_progress(completed_count, total_files, filename)
try:
result = future.result()
if result and result != "Fail" and result != "Stopped":
total_cost = result
# Only log every file completion, not internal progress
self.emit_log(f"✅ Completed {filename} ({completed_count}/{total_files})")
except Exception as e:
tb_line = str(traceback.extract_tb(sys.exc_info()[2])[-1].lineno)
error_msg = f"❌ Error processing {filename}: {str(e)} | Line: {tb_line}"
self.emit_log(error_msg)
elif result == "Stopped":
# Don't log here, already handled
break
else:
self.emit_log(f"❌ Failed processing {filename}")
except Exception as e:
tb_line = str(traceback.extract_tb(sys.exc_info()[2])[-1].lineno)
error_msg = f"❌ Error processing {filename}: {str(e)} | Line: {tb_line}"
self.emit_log(error_msg)
finally:
# Properly shutdown the executor
if self.executor:
try:
# Try to use cancel_futures parameter (Python 3.9+)
self.executor.shutdown(wait=False, cancel_futures=True)
except TypeError:
# Fallback for older Python versions
self.executor.shutdown(wait=False)
self.executor = None
# Change back to original directory
os.chdir(old_cwd)
# Clean up temporary files
@ -152,6 +401,22 @@ class TranslationWorker(QThread):
if tmp_file.exists():
tmp_file.unlink()
# Clean up any remaining temporary scripts
temp_script = self.project_root / "temp_translation_script.py"
if temp_script.exists():
temp_script.unlink()
# Ensure all processes are terminated
if self.running_processes:
for process in self.running_processes:
try:
if process.poll() is None:
process.terminate()
process.wait(timeout=1)
except:
pass
self.running_processes.clear()
# Report results
if total_cost != "Fail" and not self.should_stop:
self.emit_log("")
@ -165,6 +430,10 @@ class TranslationWorker(QThread):
if not self.should_stop:
self.emit_log("❌ Translation failed!")
self.finished_signal.emit(False, "Translation failed")
else:
# Only log the final stop message here
self.emit_log("🛑 Translation stopped by user")
self.finished_signal.emit(False, "Translation stopped")
except Exception as e:
error_msg = f"❌ Unexpected error: {str(e)}"
@ -714,8 +983,12 @@ class TranslationTab(QWidget):
"""Stop the translation process."""
if hasattr(self, 'translation_worker') and self.translation_worker.isRunning():
self.translation_worker.stop()
self.translation_worker.wait(3000) # Wait up to 3 seconds
self.append_log("🛑 Translation process stopped by user.")
# Wait for the worker to stop gracefully
if not self.translation_worker.wait(5000): # Wait up to 5 seconds
self.append_log("⚠️ Worker didn't stop gracefully, forcing termination...")
self.translation_worker.terminate()
self.translation_worker.wait(2000) # Wait for termination
self.translate_button.setEnabled(True)
self.stop_button.setEnabled(False)
@ -741,5 +1014,7 @@ class TranslationTab(QWidget):
self.log_timer.stop()
if hasattr(self, 'translation_worker') and self.translation_worker.isRunning():
self.translation_worker.stop()
self.translation_worker.wait(3000)
if not self.translation_worker.wait(3000):
self.translation_worker.terminate()
self.translation_worker.wait(1000)
event.accept()