765 lines
29 KiB
Python
765 lines
29 KiB
Python
from PYAS_Diagnostics import log_exception
|
||
from PYAS_Diagnostics import observe_future, exception_sequence
|
||
import os
|
||
import json
|
||
import time
|
||
import stat
|
||
import threading
|
||
from itertools import islice
|
||
import ctypes
|
||
import ctypes.wintypes
|
||
from PYAS_Tools import MEMORY_BASIC_INFORMATION
|
||
from PYAS_Cloud import CloudQueueMixin
|
||
|
||
|
||
class ScannerMixin(CloudQueueMixin):
|
||
def init_engine_thread(self):
|
||
try:
|
||
self.start_daemon_thread(self.backup_mbr)
|
||
self.start_daemon_thread(self.relock_file)
|
||
self.start_daemon_thread(self.init_whitelist)
|
||
self.start_daemon_thread(self.popup_intercept_thread)
|
||
|
||
def log_callback(x):
|
||
self.write_log("INFO", "Load Engine", source=os.path.basename(x))
|
||
|
||
self.heuristic.load_path(self.path_heuristic, callback=log_callback)
|
||
self.properties.load_path(self.path_properties, callback=log_callback)
|
||
self.write_log("INFO", "System", detail="Engine Initialization Complete")
|
||
|
||
if "-scan" in self.args_pyas:
|
||
try:
|
||
idx = self.args_pyas.index("-scan")
|
||
self.trigger_context_scan(self.args_pyas[idx + 1])
|
||
except Exception:
|
||
log_exception("PYAS_Scanner.ScannerMixin.init_engine_thread:31")
|
||
pass
|
||
|
||
with self.lock_config:
|
||
first_launch = self.pyas_config.get("first_launch", True)
|
||
driver_enabled = self.pyas_config.get("driver_switch", False)
|
||
context_enabled = self.pyas_config.get("context_switch", True)
|
||
autostart_enabled = self.pyas_config.get("autostart_switch", True)
|
||
|
||
self.register_context_menu(context_enabled)
|
||
|
||
with self.lock_update:
|
||
with self.lock_config:
|
||
autostart_enabled = self.pyas_config.get("autostart_switch", True)
|
||
|
||
if not self.manage_autostart(autostart_enabled) and autostart_enabled:
|
||
with self.lock_config:
|
||
self.pyas_config["autostart_switch"] = False
|
||
self.save_config()
|
||
|
||
self.ui_queue.put(
|
||
"(()=>{const ats=document.querySelector('[data-config-key=autostart_switch]');if(ats)ats.checked=false;})();"
|
||
)
|
||
|
||
if not first_launch:
|
||
with self.lock_config:
|
||
if self.pyas_config.get("process_switch"):
|
||
self.start_feature_thread(self.protect_proc_thread, "process_switch")
|
||
|
||
if self.pyas_config.get("document_switch"):
|
||
self.start_feature_thread(self.protect_file_thread, "document_switch")
|
||
|
||
if self.pyas_config.get("system_switch"):
|
||
self.start_feature_thread(self.protect_system_thread, "system_switch")
|
||
|
||
if self.pyas_config.get("network_switch"):
|
||
self.start_feature_thread(self.protect_net_thread, "network_switch")
|
||
|
||
if driver_enabled:
|
||
if self.install_system_driver() and self.start_driver_listener(wait_ready=True):
|
||
pass
|
||
else:
|
||
self.stop_system_driver()
|
||
|
||
with self.lock_config:
|
||
self.pyas_config["driver_switch"] = False
|
||
self.save_config()
|
||
|
||
self.write_log(
|
||
"WARN",
|
||
"System",
|
||
detail="Driver Protection Failed to Start",
|
||
success=False,
|
||
)
|
||
|
||
except Exception as e:
|
||
log_exception("PYAS_Scanner.ScannerMixin.init_engine_thread:72")
|
||
self.write_log("WARN", "init_engine_thread", detail=str(e), success=False)
|
||
|
||
def yield_files(self, targets):
|
||
if isinstance(targets, str):
|
||
if os.path.isdir(targets):
|
||
for root, dirs, files in os.walk(targets):
|
||
dirs[:] = [d for d in dirs if not self._is_reparse_point(os.path.join(root, d))]
|
||
|
||
for f in files:
|
||
yield self.norm_path(os.path.join(root, f)), False
|
||
|
||
elif os.path.isfile(targets):
|
||
yield self.norm_path(targets), True
|
||
|
||
elif isinstance(targets, (list, tuple, set)):
|
||
for t in targets:
|
||
yield from self.yield_files(t)
|
||
|
||
def _is_reparse_point(self, path):
|
||
try:
|
||
if os.path.islink(path):
|
||
return True
|
||
|
||
if hasattr(os.path, "isjunction") and os.path.isjunction(path):
|
||
return True
|
||
|
||
st = os.lstat(path)
|
||
return bool(getattr(st, "st_file_attributes", 0) & 0x400)
|
||
except OSError:
|
||
log_exception("PYAS_Scanner.ScannerMixin._is_reparse_point:101")
|
||
return False
|
||
|
||
def scan_engine(self, file_path):
|
||
with self.lock_config:
|
||
ext_switch = self.pyas_config.get("extension_switch", False)
|
||
sen_switch = self.pyas_config.get("sensitive_switch", False)
|
||
|
||
try:
|
||
pe_label, _ = self.properties.pe_scan(file_path, enhanced_mode=sen_switch)
|
||
|
||
if pe_label:
|
||
return pe_label
|
||
except Exception:
|
||
log_exception("PYAS_Scanner.ScannerMixin.scan_engine:115")
|
||
pass
|
||
|
||
try:
|
||
if ext_switch:
|
||
yara_label, _ = self.heuristic.yara_scan(file_path)
|
||
|
||
if yara_label:
|
||
return yara_label
|
||
except Exception:
|
||
log_exception("PYAS_Scanner.ScannerMixin.scan_engine:123")
|
||
pass
|
||
|
||
return False
|
||
|
||
def safe_scan_engine(self, file_path):
|
||
norm_path = self.norm_path(file_path)
|
||
|
||
if not norm_path:
|
||
return False
|
||
|
||
gate, error = self._acquire_file_scan_gate(norm_path)
|
||
|
||
if not gate:
|
||
self.write_log("WARN", "Scan Deferred", source=norm_path, code=error, success=False)
|
||
self._schedule_file_task("verify", norm_path, self._retry_verified_scan, 0.5)
|
||
return self.scan_engine(norm_path)
|
||
|
||
try:
|
||
file_hash = self.calc_file_hash(norm_path)
|
||
|
||
if not file_hash:
|
||
return self.scan_engine(norm_path)
|
||
|
||
with self.lock_config:
|
||
policy = (
|
||
bool(self.pyas_config.get("extension_switch", False)),
|
||
bool(self.pyas_config.get("sensitive_switch", False)),
|
||
)
|
||
|
||
cache_key = (file_hash, policy)
|
||
|
||
with self.lock_file_ops:
|
||
if cache_key in self.hash_cache:
|
||
return self.hash_cache[cache_key]
|
||
|
||
event = self.scan_events.get(cache_key)
|
||
needs_scan = event is None
|
||
|
||
if needs_scan:
|
||
event = threading.Event()
|
||
self.scan_events[cache_key] = event
|
||
|
||
if not needs_scan:
|
||
event.wait(timeout=60)
|
||
|
||
with self.lock_file_ops:
|
||
if cache_key in self.hash_cache:
|
||
return self.hash_cache[cache_key]
|
||
|
||
return self.scan_engine(norm_path)
|
||
|
||
try:
|
||
before_exceptions = exception_sequence()
|
||
result = self.scan_engine(norm_path)
|
||
scan_succeeded = exception_sequence() == before_exceptions
|
||
|
||
with self.lock_config:
|
||
current_policy = (
|
||
bool(self.pyas_config.get("extension_switch", False)),
|
||
bool(self.pyas_config.get("sensitive_switch", False)),
|
||
)
|
||
|
||
if scan_succeeded and current_policy == policy:
|
||
with self.lock_file_ops:
|
||
if len(self.hash_cache) >= 100000:
|
||
for key in list(islice(self.hash_cache, 10000)):
|
||
del self.hash_cache[key]
|
||
|
||
self.hash_cache[cache_key] = result
|
||
|
||
return result
|
||
finally:
|
||
with self.lock_file_ops:
|
||
self.scan_events.pop(cache_key, None)
|
||
event.set()
|
||
finally:
|
||
self.kernel32.CloseHandle(gate)
|
||
|
||
def _retry_verified_scan(self, file_path):
|
||
if self.closing or not os.path.isfile(file_path):
|
||
return
|
||
|
||
result = self.safe_scan_engine(file_path)
|
||
|
||
if result:
|
||
self.manage_named_list(
|
||
"quarantine", [file_path], action="add", lock_func=self.lock_file
|
||
)
|
||
self.write_log(
|
||
"BLOCK",
|
||
"Deferred Scan Block",
|
||
source=file_path,
|
||
file_hash=self.calc_file_hash(file_path),
|
||
)
|
||
|
||
def _scan_result_messages(self, count=None, scanned=None, elapsed=None):
|
||
if count is None or scanned is None or elapsed is None:
|
||
with self.lock_virus:
|
||
count = len(getattr(self, "virus_results", [])) if count is None else count
|
||
scanned = getattr(self, "scan_count", 0) if scanned is None else scanned
|
||
scan_start = getattr(self, "scan_start", time.time())
|
||
elapsed = int(time.time() - scan_start) if elapsed is None else elapsed
|
||
|
||
return {
|
||
"traditional_switch": f"發現 {count} 個病毒,掃描 {scanned} 個檔案,耗時 {elapsed} 秒",
|
||
"simplified_switch": f"发现 {count} 个病毒,扫描 {scanned} 个文件,耗时 {elapsed} 秒",
|
||
"english_switch": f"Found {count} viruses, scanned {scanned} files, time {elapsed}s",
|
||
"japanese_switch": f"{count} 個のウイルスを発見し、{scanned} 個のファイルをスキャンしました(所要時間 {elapsed} 秒)",
|
||
"korean_switch": f"바이러스 {count}개 발견, 파일 {scanned}개 검사, 소요 시간 {elapsed}초",
|
||
"french_switch": f"{count} virus trouvés, {scanned} fichiers analysés, temps {elapsed}s",
|
||
"spanish_switch": f"Se encontraron {count} virus, {scanned} archivos escaneados, tiempo {elapsed}s",
|
||
"hindi_switch": f"{count} वायरस मिले, {scanned} फ़ाइलें स्कैन की गईं, समय {elapsed}s",
|
||
"arabic_switch": f"تم العثور على {count} فيروسات، تم فحص {scanned} ملفات، الوقت {elapsed} ثانية",
|
||
"russian_switch": f"Найдено {count} вирусов, проверено {scanned} файлов, время {elapsed}с",
|
||
"slovenian_switch": f"Najdenih {count} virusov, skeniranih {scanned} datotek, čas {elapsed}s",
|
||
}
|
||
|
||
def _is_scan_cancel_requested(self):
|
||
with self.lock_virus:
|
||
return getattr(self, "scan_stop_requested", False)
|
||
|
||
def _finish_scan_cancelled(self, messages=None):
|
||
with self.lock_virus:
|
||
count = len(getattr(self, "virus_results", []))
|
||
scanned = getattr(self, "scan_count", 0)
|
||
scan_start = getattr(self, "scan_start", time.time())
|
||
elapsed = int(time.time() - scan_start)
|
||
self.scan_running = False
|
||
self.scan_preparing = False
|
||
self.scan_finished = True
|
||
self.scan_stop_requested = False
|
||
|
||
messages = messages or self._scan_result_messages(count, scanned, elapsed)
|
||
|
||
if self._window:
|
||
js_cmd = f"if(window.finishScan) window.finishScan({json.dumps(messages)}, {count});"
|
||
self.ui_queue.put(js_cmd)
|
||
|
||
return False
|
||
|
||
def _begin_scan_preparation(self):
|
||
with self.lock_virus:
|
||
if getattr(self, "scan_running", False) or getattr(self, "scan_preparing", False):
|
||
return False
|
||
|
||
self.scan_preparing = True
|
||
self.scan_stop_requested = False
|
||
self.scan_finished = False
|
||
self.virus_results = []
|
||
self.scan_count = 0
|
||
self.scan_start = time.time()
|
||
return True
|
||
|
||
def start_scan(self, targets, from_preparing=False):
|
||
with self.lock_virus:
|
||
if getattr(self, "scan_running", False) or (
|
||
getattr(self, "scan_preparing", False) and not from_preparing
|
||
):
|
||
return False
|
||
|
||
if getattr(self, "scan_stop_requested", False):
|
||
self.scan_running = False
|
||
self.scan_preparing = False
|
||
self.scan_finished = True
|
||
return False
|
||
|
||
self.scan_preparing = False
|
||
self.scan_running = True
|
||
self.scan_finished = False
|
||
self.scan_stop_requested = False
|
||
self.virus_results = []
|
||
self.scan_count = 0
|
||
self.scan_start = time.time()
|
||
|
||
try:
|
||
observe_future(
|
||
self.scan_pool.submit(self.scan_worker, targets), "ScannerMixin.scan_worker"
|
||
)
|
||
return True
|
||
except Exception:
|
||
log_exception("PYAS_Scanner.ScannerMixin.start_scan:253")
|
||
|
||
with self.lock_virus:
|
||
self.scan_running = False
|
||
self.scan_preparing = False
|
||
self.scan_finished = True
|
||
|
||
return False
|
||
|
||
def scan_worker(self, targets):
|
||
last_update = 0.0
|
||
|
||
try:
|
||
for file_path, is_explicit in self.yield_files(targets):
|
||
with self.lock_virus:
|
||
if not self.scan_running or getattr(self, "scan_stop_requested", False):
|
||
break
|
||
|
||
norm_path = self.norm_path(file_path)
|
||
|
||
if not norm_path:
|
||
continue
|
||
|
||
current_time = time.time()
|
||
|
||
if current_time - last_update >= 0.05:
|
||
if self._window:
|
||
js_cmd = f"if(window.updateScanProgress) window.updateScanProgress({json.dumps(norm_path.replace(os.sep, '/'))});"
|
||
self.ui_queue.put(js_cmd)
|
||
|
||
last_update = current_time
|
||
|
||
was_locked = False
|
||
|
||
try:
|
||
with self.lock_file_ops:
|
||
if norm_path in self.virus_lock:
|
||
self.lock_file(norm_path, False)
|
||
was_locked = True
|
||
|
||
if self.is_in_whitelist(norm_path):
|
||
continue
|
||
|
||
with self.lock_virus:
|
||
if not self.scan_running or getattr(self, "scan_stop_requested", False):
|
||
break
|
||
|
||
self.scan_count += 1
|
||
|
||
with self.lock_config:
|
||
ext_filter = self.pyas_config.get("suffix_switch", True)
|
||
suffix = self.pyas_config.get("suffix", [])
|
||
|
||
if (
|
||
not is_explicit
|
||
and ext_filter
|
||
and os.path.splitext(norm_path)[-1].lower() not in suffix
|
||
):
|
||
continue
|
||
|
||
result = self.safe_scan_engine(norm_path)
|
||
|
||
with self.lock_virus:
|
||
if not self.scan_running or getattr(self, "scan_stop_requested", False):
|
||
break
|
||
|
||
if result:
|
||
with self.lock_virus:
|
||
self.virus_results.append(norm_path)
|
||
|
||
if self._window:
|
||
js_cmd = f"if(window.addVirusResult) window.addVirusResult({json.dumps(result)}, {json.dumps(norm_path.replace(os.sep, '/'))});"
|
||
self.ui_queue.put(js_cmd)
|
||
|
||
self.write_log(
|
||
"SCAN",
|
||
"Virus Detected",
|
||
source=norm_path,
|
||
file_hash=self.calc_file_hash(norm_path),
|
||
)
|
||
|
||
self.cloud_check(norm_path)
|
||
except Exception as e:
|
||
log_exception("PYAS_Scanner.ScannerMixin.scan_worker:318")
|
||
self.write_log(
|
||
"WARN", "Scan Engine", source=norm_path, detail=str(e), success=False
|
||
)
|
||
|
||
finally:
|
||
if was_locked:
|
||
self.lock_file(norm_path, True)
|
||
|
||
finally:
|
||
with self.lock_virus:
|
||
cancelled = getattr(self, "scan_stop_requested", False)
|
||
self.scan_running = False
|
||
self.scan_preparing = False
|
||
self.scan_finished = True
|
||
self.scan_stop_requested = False
|
||
count, scanned, elapsed = (
|
||
len(self.virus_results),
|
||
self.scan_count,
|
||
int(time.time() - self.scan_start),
|
||
)
|
||
|
||
messages = self._scan_result_messages(count, scanned, elapsed)
|
||
log_detail = f"Found {count} viruses, scanned {scanned} files, time {elapsed}s"
|
||
|
||
if self._window:
|
||
js_cmd = (
|
||
f"if(window.finishScan) window.finishScan({json.dumps(messages)}, {count});"
|
||
)
|
||
self.ui_queue.put(js_cmd)
|
||
|
||
self.write_log("INFO", "Scan Completed", detail=log_detail)
|
||
|
||
def stop_scan(self):
|
||
with self.lock_virus:
|
||
active = getattr(self, "scan_running", False) or getattr(self, "scan_preparing", False)
|
||
|
||
if active:
|
||
self.scan_stop_requested = True
|
||
self.scan_running = False
|
||
|
||
return active
|
||
|
||
def trigger_scan(self, method):
|
||
targets = []
|
||
|
||
if not self._begin_scan_preparation():
|
||
return False
|
||
|
||
try:
|
||
if method == "smart":
|
||
for folder in ["Desktop", "Downloads", "AppData"]:
|
||
if self._is_scan_cancel_requested():
|
||
return self._finish_scan_cancelled()
|
||
|
||
fp = os.path.join(self.path_user, folder)
|
||
|
||
if os.path.exists(fp):
|
||
targets.append(fp)
|
||
|
||
if self._is_scan_cancel_requested():
|
||
return self._finish_scan_cancelled()
|
||
|
||
if os.path.exists(self.path_config):
|
||
targets.append(self.path_config)
|
||
|
||
buf = ctypes.create_unicode_buffer(1024)
|
||
max_address = 0x7FFFFFFFFFFF if ctypes.sizeof(ctypes.c_void_p) == 8 else 0x7FFFFFFF
|
||
system_dir = self.path_system.lower()
|
||
|
||
for proc in self.get_process_list():
|
||
if self._is_scan_cancel_requested():
|
||
return self._finish_scan_cancelled()
|
||
|
||
if proc["path"] and proc["path"] != "None":
|
||
targets.append(proc["path"])
|
||
|
||
pid = proc["pid"]
|
||
|
||
if pid <= 4:
|
||
continue
|
||
|
||
h_process = self.kernel32.OpenProcess(0x1000, False, pid)
|
||
|
||
if h_process:
|
||
try:
|
||
address = 0
|
||
mbi = MEMORY_BASIC_INFORMATION()
|
||
|
||
while (
|
||
address < max_address
|
||
and not self._is_scan_cancel_requested()
|
||
and self.kernel32.VirtualQueryEx(
|
||
h_process,
|
||
ctypes.c_void_p(address),
|
||
ctypes.byref(mbi),
|
||
ctypes.sizeof(mbi),
|
||
)
|
||
):
|
||
|
||
if mbi.State == 0x1000 and mbi.Type == 0x1000000:
|
||
if self.psapi.GetMappedFileNameW(
|
||
h_process, ctypes.c_void_p(address), buf, 1024
|
||
):
|
||
raw_path = buf.value
|
||
|
||
if raw_path.startswith("\\"):
|
||
raw_path = self.device_path_to_drive(raw_path)
|
||
|
||
file_path = self.norm_path(raw_path)
|
||
|
||
if file_path and not file_path.lower().startswith(
|
||
system_dir
|
||
):
|
||
targets.append(file_path)
|
||
|
||
if mbi.RegionSize == 0:
|
||
break
|
||
|
||
address += mbi.RegionSize
|
||
finally:
|
||
self.kernel32.CloseHandle(h_process)
|
||
|
||
if self._is_scan_cancel_requested():
|
||
return self._finish_scan_cancelled()
|
||
|
||
if not self.start_scan(list(set(targets)), from_preparing=True):
|
||
return self._finish_scan_cancelled()
|
||
|
||
return True
|
||
|
||
elif method == "file":
|
||
targets = self.select_files()
|
||
|
||
if self._is_scan_cancel_requested():
|
||
return self._finish_scan_cancelled()
|
||
|
||
if targets:
|
||
if not self.start_scan(targets, from_preparing=True):
|
||
return self._finish_scan_cancelled()
|
||
|
||
return True
|
||
|
||
return self._finish_scan_cancelled()
|
||
|
||
elif method == "path":
|
||
targets = self.select_folder()
|
||
|
||
if self._is_scan_cancel_requested():
|
||
return self._finish_scan_cancelled()
|
||
|
||
if targets:
|
||
if not self.start_scan(targets, from_preparing=True):
|
||
return self._finish_scan_cancelled()
|
||
|
||
return True
|
||
|
||
return self._finish_scan_cancelled()
|
||
|
||
elif method == "full":
|
||
targets = [f"{chr(d)}:/" for d in range(65, 91) if os.path.exists(f"{chr(d)}:/")]
|
||
|
||
if self._is_scan_cancel_requested() or not self.start_scan(
|
||
targets, from_preparing=True
|
||
):
|
||
return self._finish_scan_cancelled()
|
||
|
||
return True
|
||
|
||
return self._finish_scan_cancelled()
|
||
|
||
except Exception as e:
|
||
log_exception("PYAS_Scanner.ScannerMixin.trigger_scan:453")
|
||
self.write_log("WARN", "trigger_scan", detail=str(e), success=False)
|
||
return self._finish_scan_cancelled()
|
||
|
||
def _queue_virus_delete(self, file_path):
|
||
self._schedule_file_task("delete", file_path, self._retry_virus_delete, 0.5)
|
||
|
||
def _retry_virus_delete(self, file_path):
|
||
file_hash = self.calc_file_hash(file_path)
|
||
|
||
try:
|
||
with self.lock_file_ops:
|
||
if file_path in self.virus_lock:
|
||
self.lock_file(file_path, False)
|
||
|
||
target_key = os.path.normcase(self.norm_path(file_path, must_exist=False) or file_path)
|
||
|
||
for proc in self.get_process_list():
|
||
proc_path = proc.get("path")
|
||
|
||
if not proc_path or proc_path == "None":
|
||
continue
|
||
|
||
proc_key = os.path.normcase(
|
||
self.norm_path(proc_path, must_exist=False) or proc_path
|
||
)
|
||
|
||
if proc_key == target_key:
|
||
self.kill_process(proc["pid"], expected_path=file_path)
|
||
|
||
try:
|
||
os.chmod(file_path, stat.S_IWRITE)
|
||
except Exception:
|
||
log_exception("PYAS_Scanner.ScannerMixin._retry_virus_delete:480")
|
||
pass
|
||
|
||
os.remove(file_path)
|
||
|
||
except FileNotFoundError:
|
||
pass
|
||
except Exception as e:
|
||
log_exception("PYAS_Scanner.ScannerMixin._retry_virus_delete:486")
|
||
self.lock_file(file_path, True, quiet=True)
|
||
self._queue_virus_delete(file_path)
|
||
self.write_log("INFO", "Virus Delete Deferred", source=file_path, detail=str(e))
|
||
return
|
||
|
||
self.remove_list_items("quarantine", [file_path])
|
||
|
||
with self.lock_virus:
|
||
target_key = os.path.normcase(self.norm_path(file_path, must_exist=False) or file_path)
|
||
self.virus_results = [
|
||
result
|
||
for result in self.virus_results
|
||
if os.path.normcase(self.norm_path(result, must_exist=False) or result)
|
||
!= target_key
|
||
]
|
||
|
||
self.write_log(
|
||
"INFO",
|
||
"Virus Delete",
|
||
source=file_path,
|
||
file_hash=file_hash,
|
||
operate=True,
|
||
success=True,
|
||
)
|
||
|
||
def solve_scan(self, file_paths):
|
||
deleted_paths = []
|
||
proc_map = {}
|
||
|
||
for proc in self.get_process_list():
|
||
if proc["path"] and proc["path"] != "None":
|
||
|
||
norm_p = self.norm_path(proc["path"], must_exist=False)
|
||
|
||
if norm_p:
|
||
proc_map.setdefault(os.path.normcase(norm_p), []).append(proc["pid"])
|
||
|
||
last_update = 0.0
|
||
|
||
with self.lock_virus:
|
||
deleted_set = set()
|
||
|
||
for raw_path in file_paths:
|
||
|
||
path = self.norm_path(raw_path, must_exist=False)
|
||
|
||
if not path:
|
||
continue
|
||
|
||
current_time = time.time()
|
||
|
||
if current_time - last_update >= 0.05:
|
||
if self._window:
|
||
self._window.evaluate_js(
|
||
f"if(window.updateDeleteProgress) window.updateDeleteProgress({json.dumps(path.replace(os.sep, '/'))});"
|
||
)
|
||
|
||
last_update = current_time
|
||
|
||
try:
|
||
if path in self.virus_lock:
|
||
self.lock_file(path, False)
|
||
|
||
path_key = os.path.normcase(path)
|
||
|
||
if path_key in proc_map:
|
||
for pid in proc_map[path_key]:
|
||
self.kill_process(pid, expected_path=path)
|
||
|
||
try:
|
||
os.chmod(path, stat.S_IWRITE)
|
||
except Exception:
|
||
log_exception("PYAS_Scanner.ScannerMixin.solve_scan:538")
|
||
pass
|
||
|
||
file_hash = self.calc_file_hash(path)
|
||
|
||
try:
|
||
os.remove(path)
|
||
except Exception as e:
|
||
log_exception("PYAS_Scanner.ScannerMixin.solve_scan:544")
|
||
self.lock_file(path, True, quiet=True)
|
||
self._queue_virus_delete(path)
|
||
self.write_log("INFO", "Virus Delete Deferred", source=path, detail=str(e))
|
||
continue
|
||
|
||
self.remove_list_items("quarantine", [path])
|
||
deleted_paths.append(raw_path)
|
||
deleted_set.add(path)
|
||
self.write_log(
|
||
"INFO",
|
||
"Virus Delete",
|
||
source=path,
|
||
file_hash=file_hash,
|
||
operate=True,
|
||
success=True,
|
||
)
|
||
|
||
except Exception as e:
|
||
log_exception("PYAS_Scanner.ScannerMixin.solve_scan:555")
|
||
self.lock_file(path, True)
|
||
self.write_log(
|
||
"SCAN",
|
||
"Virus Delete",
|
||
source=path,
|
||
file_hash=self.calc_file_hash(path),
|
||
detail=str(e),
|
||
operate=True,
|
||
success=False,
|
||
)
|
||
|
||
if deleted_set:
|
||
self.virus_results = [p for p in self.virus_results if p not in deleted_set]
|
||
|
||
remaining = len(self.virus_results)
|
||
|
||
messages = {
|
||
"traditional_switch": f"剩餘 {remaining} 個病毒,已刪除 {len(deleted_paths)} 個檔案。",
|
||
"simplified_switch": f"剩余 {remaining} 个病毒,已删除 {len(deleted_paths)} 个文件。",
|
||
"english_switch": f"Remaining {remaining} viruses, deleted {len(deleted_paths)} files.",
|
||
"japanese_switch": f"残りのウイルス {remaining} 個、削除されたファイル {len(deleted_paths)} 個。",
|
||
"korean_switch": f"남은 바이러스 {remaining}개, 삭제된 파일 {len(deleted_paths)}개.",
|
||
"french_switch": f"Virus restants : {remaining}, fichiers supprimés : {len(deleted_paths)}.",
|
||
"spanish_switch": f"Virus restantes: {remaining}, archivos eliminados: {len(deleted_paths)}.",
|
||
"hindi_switch": f"शेष वायरस {remaining}, हटाए गए फ़ाइलें {len(deleted_paths)}.",
|
||
"arabic_switch": f"الفيروسات المتبقية {remaining}، تم حذف {len(deleted_paths)} ملفات.",
|
||
"russian_switch": f"Осталось вирусов: {remaining}, удалено файлов: {len(deleted_paths)}.",
|
||
"slovenian_switch": f"Preostalih virusov: {remaining}, izbrisanih datotek: {len(deleted_paths)}.",
|
||
}
|
||
|
||
if self._window:
|
||
self._window.evaluate_js(
|
||
f"if(window.finishScan) window.finishScan({json.dumps(messages)}, {remaining});"
|
||
)
|
||
|
||
return deleted_paths
|
||
|
||
def remove_virus_result(self, paths):
|
||
with self.lock_virus:
|
||
norm_paths = set(self.norm_path(p, must_exist=False) for p in paths)
|
||
self.virus_results = [p for p in self.virus_results if p not in norm_paths]
|
||
return len(self.virus_results)
|