ge-tool/backend/services/nas_sharing_worker.py

375 lines
15 KiB
Python
Raw Normal View History

2025-12-10 06:41:43 +00:00
"""
NAS Sharing Worker - Thread-based queue processor
REFACTORED: Dùng nas_sharing_auth + nas_sharing_api modules
"""
import os
import time
import threading
import queue
import uuid
from typing import Dict, Optional
from functools import wraps
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import NoSuchWindowException, WebDriverException
from webdriver_manager.chrome import ChromeDriverManager
from .nas_sharing_api import (
SharingSessionManager,
perform_login,
extract_sharing_id,
get_initial_path,
get_file_list,
)
def handle_window_closed(func):
"""Decorator to handle browser crash/close errors"""
@wraps(func)
def wrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except Exception as e:
error_msg = str(e).lower()
is_driver_error = (
'window already closed' in error_msg or
'web view not found' in error_msg or
'max retries exceeded' in error_msg or
'connection refused' in error_msg or
isinstance(e, (NoSuchWindowException, WebDriverException))
)
if is_driver_error:
print(f"[SharingWorker] ⚠️ Driver error: {str(e)[:100]}")
print(f"[SharingWorker] 🔄 Resetting driver...")
try:
if self.driver:
self.driver.quit()
except:
pass
self.driver = None
return func(self, *args, **kwargs)
else:
raise
return wrapper
class SharingLinkWorker:
"""
Worker processes sharing link requests from queue
Single browser instance with session persistence
"""
def __init__(self):
self.driver: Optional[webdriver.Chrome] = None
self.session_manager: Optional[SharingSessionManager] = None
self.request_queue = queue.Queue()
self.results = {}
self.is_running = False
self.worker_thread = None
# Thread safety: Lock to prevent concurrent driver access
self.driver_lock = threading.RLock()
# OTP handling with modal shown tracking
self.otp_pending = False
self.otp_code: Optional[str] = None
self.otp_modal_shown = False
self.otp_submitted = False # Track OTP submission success
def start(self):
"""Start worker thread"""
if self.is_running:
return
self.is_running = True
self.worker_thread = threading.Thread(target=self._worker_loop, daemon=True)
self.worker_thread.start()
print("[SharingWorker] Started")
def stop(self):
"""Stop worker and cleanup"""
self.is_running = False
if self.driver:
try:
# Close all windows first
if len(self.driver.window_handles) > 0:
self.driver.quit()
else:
# Force kill if no windows
self.driver.service.process.terminate()
except Exception as e:
print(f"[SharingWorker] Warning during cleanup: {e}")
finally:
self.driver = None
print("[SharingWorker] Stopped")
def submit_request(self, url: str) -> str:
"""Submit sharing link for processing"""
request_id = str(uuid.uuid4())
self.request_queue.put({
'id': request_id,
'url': url,
'timestamp': time.time()
})
self.results[request_id] = {
'status': 'pending',
'message': 'Đang xử lý sharing link...'
}
return request_id
def get_result(self, request_id: str) -> Optional[Dict]:
"""Get processing result"""
return self.results.get(request_id)
def _worker_loop(self):
"""Main worker loop"""
print("[SharingWorker] Worker loop started")
while self.is_running:
try:
try:
request = self.request_queue.get(timeout=1)
except queue.Empty:
continue
request_id = request['id']
url = request['url']
print(f"[SharingWorker] Processing: {url}")
result = self._process_sharing_link(url)
self.results[request_id] = result
print(f"[SharingWorker] Completed: {result['status']}")
except Exception as e:
print(f"[SharingWorker] Error: {e}")
import traceback
traceback.print_exc()
def _ensure_driver_ready(self):
"""Setup Chrome driver if not exists - Thread-safe"""
with self.driver_lock:
if self.driver:
try:
_ = self.driver.current_url
print("[SharingWorker] ✅ Reusing existing driver")
return
except:
print("[SharingWorker] ⚠️ Driver dead, creating new...")
try:
self.driver.quit()
except:
pass
self.driver = None
# ========== TẤT CẢ CODE TẠO DRIVER TRONG LOCK ĐỂ TRÁNH RACE CONDITION ==========
print("[SharingWorker] 🚀 Creating new Chrome driver...")
chrome_options = Options()
# Chrome profile from environment
profile_path_env = os.getenv("NAS_CHROME_PROFILE_PATH")
if not profile_path_env:
raise ValueError("NAS_CHROME_PROFILE_PATH must be set in .env.local")
# Resolve absolute path
current_file = os.path.abspath(__file__)
backend_dir = os.path.dirname(os.path.dirname(current_file))
workspace_root = os.path.dirname(backend_dir)
profile_path = os.path.join(workspace_root, profile_path_env)
os.makedirs(profile_path, exist_ok=True)
# Chrome options (fix crash issues)
chrome_options.add_argument(f'user-data-dir={profile_path}')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--start-maximized')
chrome_options.add_argument('--ignore-certificate-errors')
# Additional stability options (prevent crashes)
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
chrome_options.add_argument('--remote-debugging-port=0') # Let Chrome choose port
# Disable extensions to avoid conflicts
chrome_options.add_argument('--disable-extensions')
# Prevent "Chrome is being controlled by automated test software" banner
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)
service = Service(ChromeDriverManager().install())
try:
self.driver = webdriver.Chrome(service=service, options=chrome_options)
except Exception as e:
print(f"[SharingWorker] ❌ Failed to create Chrome driver: {e}")
print(f"[SharingWorker] Profile path: {profile_path}")
print(f"[SharingWorker] Chrome options: {chrome_options.arguments}")
# Try to kill any zombie Chrome processes
import subprocess
try:
subprocess.run(['taskkill', '/F', '/IM', 'chrome.exe'],
capture_output=True, timeout=5)
subprocess.run(['taskkill', '/F', '/IM', 'chromedriver.exe'],
capture_output=True, timeout=5)
print(f"[SharingWorker] Killed zombie Chrome processes, retrying...")
time.sleep(2)
# Retry once after killing zombies
self.driver = webdriver.Chrome(service=service, options=chrome_options)
except Exception as retry_error:
print(f"[SharingWorker] ❌ Retry also failed: {retry_error}")
raise RuntimeError(
f"Cannot create Chrome driver. "
f"Try: 1) Close all Chrome windows, 2) Delete chrome_profile_nas folder, 3) Restart"
) from e
# Create session manager
self.session_manager = SharingSessionManager(self.driver)
print(f"[SharingWorker] ✅ Driver created, profile: {profile_path}")
@handle_window_closed
def _perform_login(self) -> bool:
"""
Perform DSM login using nas_sharing_auth module
OTP via modal pattern
"""
if not self.driver or not self.session_manager:
raise RuntimeError("Driver not initialized")
# Type safety assertions
assert self.driver is not None
assert self.session_manager is not None
# OTP callback for nas_sharing_auth.perform_login()
def otp_callback() -> Optional[str]:
"""Wait for OTP from frontend modal"""
# Set pending flag
if not self.otp_pending:
self.otp_pending = True
self.otp_modal_shown = False
self.otp_submitted = False
# Wait for OTP (max 5 minutes)
for i in range(300):
if self.otp_code:
code = self.otp_code
# DON'T reset flags yet - wait for login completion
self.otp_code = None
return code
time.sleep(1)
if i % 10 == 0:
print(f"[SharingWorker] ⏳ Waiting for OTP... ({300-i}s)")
# Timeout
self.otp_pending = False
self.otp_modal_shown = False
self.otp_submitted = False
return None
# Call perform_login() from nas_sharing_api
success = perform_login(
driver=self.driver,
otp_callback=otp_callback
)
if success:
print("[SharingWorker] ✅ Login successful!")
# QUAN TRỌNG: Mark OTP submitted TRƯỚC KHI reset flags
if self.otp_pending:
self.otp_submitted = True
print("[SharingWorker] ✅ OTP đã được xác nhận thành công")
# Đợi để Chrome profile lưu cookies (quan trọng!)
print("[SharingWorker] ⏳ Đợi 5s để lưu cookies vào Chrome profile...")
time.sleep(5)
# Reset OTP flags sau khi đã đợi
self.otp_pending = False
self.otp_modal_shown = False
print("[SharingWorker] ✅ Cookies đã được lưu vào Chrome profile")
return True
# Login failed - reset flags
self.otp_pending = False
self.otp_modal_shown = False
self.otp_submitted = False
return False
@handle_window_closed
def _process_sharing_link(self, url: str) -> Dict:
"""
Process sharing link - navigate extract file list
"""
from .nas_sharing_api.selenium_operations import extract_sharing_id, get_initial_path, get_file_list
try:
sharing_id = extract_sharing_id(url)
if not sharing_id:
raise Exception("Cannot extract sharing_id from URL")
print(f"[SharingWorker] Sharing ID: {sharing_id}")
with self.driver_lock:
self._ensure_driver_ready()
assert self.driver is not None
# Clear cache để tránh conflict ExtJS
try:
self.driver.execute_cdp_cmd('Network.clearBrowserCache', {})
except Exception:
pass
self.driver.get(url)
try:
initial_path = get_initial_path(self.driver)
except RuntimeError as e:
if "NEEDS_LOGIN" in str(e):
print("[SharingWorker] Login required")
if not self._perform_login():
raise Exception("Login failed")
# XÓA CACHE sau khi login xong để tránh conflict ExtJS
print("[SharingWorker] Xóa cache trước khi truy cập lại sharing link...")
try:
self.driver.execute_cdp_cmd('Network.clearBrowserCache', {})
except Exception as clear_error:
print(f"[SharingWorker] ⚠️ Không thể xóa cache: {clear_error}")
self.driver.get(url)
initial_path = get_initial_path(self.driver)
else:
raise
print(f"[SharingWorker] 📋 Lấy danh sách ROOT folder: {initial_path}")
files = get_file_list(self.driver, sharing_id, initial_path)
return {
'status': 'success',
'sharing_id': sharing_id,
'path': initial_path,
'files': files,
'total_files': len(files),
'message': f'Found {len(files)} files'
}
except Exception as e:
print(f"[SharingWorker] Error: {e}")
import traceback
traceback.print_exc()
return {
'status': 'error',
'message': str(e)
}