Fix Stop Translation button
This commit is contained in:
parent
85ad7c5914
commit
ab7ec9eb23
1 changed files with 308 additions and 33 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue