1818
1919import datetime as dt
2020import json
21+ import os
2122import re
23+ import sys
24+ import threading
25+ import time
2226from pathlib import Path
2327
2428from . import store
@@ -837,12 +841,20 @@ def __init__(
837841 genuine_path : Path | str | None = None ,
838842 request_delay_ms : int = 900 ,
839843 result_timeout_ms : int = 20000 ,
844+ stall_seconds : int = 120 ,
840845 ) -> None :
841846 self .cdp_endpoint = cdp_endpoint
842847 self .store_dir = store_dir
843848 self .genuine_path = genuine_path
844849 self .request_delay_ms = max (0 , int (request_delay_ms ))
845850 self .result_timeout_ms = max (1000 , int (result_timeout_ms ))
851+ # Canlı adım gözcüsü (watchdog): hiçbir adım stall_seconds'ı aşarsa net tanı ile GÜVENLE sonlandır
852+ # (önceki koşumdan kalan TAKILI görüntüleyici sekmesi page.content()'i sonsuza dek bloklayabilir).
853+ self .stall_seconds = max (15 , int (stall_seconds ))
854+ self ._hb_step = "başlatılıyor"
855+ self ._hb_ts = time .monotonic ()
856+ self ._tab_count = - 1
857+ self ._live_active = False
846858
847859 def diagnose_form (self ) -> dict : # pragma: no cover - canlı tarayıcı gerektirir
848860 """READ-ONLY tanı: oturuma bağlanıp 'Geçmiş İlanlar' formunun GERÇEK kontrol yapısını döndürür.
@@ -1012,12 +1024,24 @@ def run(
10121024 collector = BrowserCollector (cdp_endpoint = self .cdp_endpoint )
10131025 acquired_total = 0
10141026 with sync_playwright () as pw :
1027+ self ._live_active = True
1028+ watchdog_stop = self ._start_watchdog ()
1029+ self ._beat ("CDP oturumuna bağlanılıyor" )
10151030 browser = pw .chromium .connect_over_cdp (self .cdp_endpoint )
10161031 if not browser .contexts :
1032+ self ._live_active = False
1033+ watchdog_stop .set ()
10171034 raise RuntimeError ("no_usable_browser_context: CDP oturumunda kullanılabilir bağlam yok." )
10181035 context = browser .contexts [0 ]
1036+ try :
1037+ self ._tab_count = len (context .pages )
1038+ except Exception :
1039+ self ._tab_count = - 1
1040+ self ._beat (f"'Geçmiş İlanlar' sekmesi aranıyor ({ self ._tab_count } açık sekme)" )
10191041 page = self ._find_gecmis_page (context )
10201042 if page is None :
1043+ self ._live_active = False
1044+ watchdog_stop .set ()
10211045 raise RuntimeError (
10221046 "no_gecmis_ilanlar_page: 'Geçmiş İlanlar' sekmesi bulunamadı. "
10231047 "Önce UYAP e-Satış → İhaleler → Geçmiş İlanlar sayfasını elle açın."
@@ -1048,6 +1072,8 @@ def _acquire(file_id, institution):
10481072 if stop == "MAX_RECORDS" :
10491073 summary ["stopped_reason" ] = "max_records"
10501074 break
1075+ self ._live_active = False
1076+ watchdog_stop .set ()
10511077
10521078 for c in store .load_candidates (self .store_dir ):
10531079 dec = (c .get ("audit" ) or {}).get ("decision" )
@@ -1061,6 +1087,7 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
10611087 start_ui = format_uyap_ui_date (w ["start" ])
10621088 end_ui = format_uyap_ui_date (w ["end" ])
10631089 self ._print (f"[UYAP BULK] pencere { w ['start' ]} →{ w ['end' ]} · { CATEGORY_TASINMAZ } · { province } " )
1090+ self ._beat (f"pencere { w ['start' ]} →{ w ['end' ]} : form dolduruluyor (kategori/il/tarih)" )
10641091
10651092 self ._dismiss_notices (page )
10661093 cat_ok = self ._select_category_tasinmaz (page )
@@ -1077,6 +1104,7 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
10771104 save_bulk_state (state , self .store_dir )
10781105 self ._print (" ARA (arama) kontrolü bulunamadı — pencere atlandı. `--diagnose` çıktısındaki aksiyon adaylarını paylaşın." )
10791106 return None
1107+ self ._beat ("ARA sonrası sonuç durumu bekleniyor" )
10801108 result_html = self ._wait_result_state (page )
10811109
10821110 exp = detect_session_expiration (result_html , page .url )
@@ -1117,6 +1145,7 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
11171145 processed_ids : set = set ()
11181146 pages_failed : list = []
11191147 for pnum in pages_remaining (rec , valid_pages ):
1148+ self ._beat (f"sayfa { pnum } yükleniyor" )
11201149 if not self ._goto_page (page , pnum ):
11211150 pages_failed .append (pnum )
11221151 self ._print (f" sayfa { pnum } yüklenemedi/kart kümesi değişmedi — atlanıyor (pencere COMPLETE değil, tekrar denenir)." )
@@ -1148,6 +1177,7 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
11481177 continue # hedefli edinim: yalnız istenen KAYIT NO işlenir
11491178 if max_records and summary ["records_processed" ] >= max_records :
11501179 return "MAX_RECORDS"
1180+ self ._beat (f"KAYIT NO { card .get ('kayit_no' )} : belge ediniliyor (native)" )
11511181 res = process_sold_auction (
11521182 card , acquire_documents = acquire , store_dir = self .store_dir ,
11531183 genuine_path = self .genuine_path , discovery_only = discovery_only ,
@@ -1190,8 +1220,21 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
11901220
11911221 # -- Canlı DOM yardımcıları (pragma) — gerçek gözlenen UYAP DOM'una uyarlanır ---------- #
11921222 def _find_gecmis_page (self , context ): # pragma: no cover - canlı DOM
1193- best = None
1223+ # ÖNCE URL'e göre ayıkla (ucuz, ASILMAZ): önceki koşumdan kalan TAKILI görüntüleyici/indirme
1224+ # sekmeleri (blob:/data:/file:/about:blank) ölü renderer'a sahip olabilir; page.content() ZAMAN
1225+ # AŞIMSIZ bloklar → o sekmelerin content()'ine HİÇ dokunma (bir saatlik asılmanın kök nedeni).
1226+ _skip_prefix = ("blob:" , "data:" , "file:" , "chrome:" , "about:" , "devtools:" , "view-source:" )
1227+ app_pages = []
11941228 for p in context .pages :
1229+ try :
1230+ url = _fold (p .url or "" )
1231+ except Exception :
1232+ continue
1233+ if not url or any (url .startswith (s ) for s in _skip_prefix ):
1234+ continue
1235+ app_pages .append (p )
1236+ best = None
1237+ for p in app_pages :
11951238 try :
11961239 fold = _fold (p .content ())
11971240 except Exception :
@@ -1200,7 +1243,7 @@ def _find_gecmis_page(self, context): # pragma: no cover - canlı DOM
12001243 return p
12011244 if best is None and ("ilan" in fold or "ihale" in fold ):
12021245 best = p
1203- return best or (context . pages [0 ] if context . pages else None )
1246+ return best or (app_pages [0 ] if app_pages else None )
12041247
12051248 def _dismiss_notices (self , page ): # pragma: no cover - canlı DOM
12061249 """Bilgilendirme/duyuru pop-up'larını + açık kalmış Bootstrap modallarını KAPATIR.
@@ -1534,7 +1577,40 @@ def _report_auction(self, res: dict) -> None: # pragma: no cover - canlı çık
15341577 + (f" · denetim={ dec } " if dec else "" ))
15351578
15361579 def _print (self , msg : str ) -> None : # pragma: no cover - canlı çıktı
1537- print (msg )
1580+ print (msg , flush = True )
1581+
1582+ def _beat (self , step : str ) -> None : # pragma: no cover - canlı çıktı/heartbeat
1583+ """Canlı adım nabzı: gözcü zamanlayıcısını sıfırlar + ilerlemeyi stderr'e (flush) yazar."""
1584+ self ._hb_step = step
1585+ self ._hb_ts = time .monotonic ()
1586+ print (f"[UYAP BULK] · { step } " , file = sys .stderr , flush = True )
1587+
1588+ def _start_watchdog (self ): # pragma: no cover - zamanlayıcı iş parçacığı
1589+ """Arka plan gözcüsü: canlı bir adım stall_seconds'ı aşarsa net tanı ile SERT sonlandır.
1590+
1591+ Takılı bir Playwright sync çağrısı (ör. ölü renderer'lı sekmede page.content()) ana iş parçacığını
1592+ kesintisiz bloklar; tek güvenli kurtarma os._exit'tir. Kontrol noktaları zaten sayfa-sayfa kaydedilir.
1593+ """
1594+ stop = threading .Event ()
1595+
1596+ def _mon ():
1597+ while not stop .wait (5.0 ):
1598+ if not self ._live_active :
1599+ continue
1600+ age = time .monotonic () - self ._hb_ts
1601+ if age > self .stall_seconds :
1602+ sys .stderr .write (
1603+ f"\n [UYAP BULK] ZAMAN AŞIMI: '{ self ._hb_step } ' adımı { int (age )} sn ilerlemedi "
1604+ f"(eşik { self .stall_seconds } sn; --stall-timeout ile ayarlanır). Açık sekme≈{ self ._tab_count } . "
1605+ f"Olası neden: önceki koşumdan kalan TAKILI görüntüleyici/indirme sekmesi 'page.content()'i "
1606+ f"blokluyor. ÇÖZÜM: Chrome'da YALNIZ 'Geçmiş İlanlar' sekmesini bırakıp diğer sekmeleri "
1607+ f"kapatın, sonra komutu tekrar çalıştırın. Kontrol noktaları kaydedildi; güvenle sonlandırılıyor.\n "
1608+ )
1609+ sys .stderr .flush ()
1610+ os ._exit (4 )
1611+
1612+ threading .Thread (target = _mon , name = "uyap-bulk-watchdog" , daemon = True ).start ()
1613+ return stop
15381614
15391615 def _print_summary (self , s : dict ) -> None : # pragma: no cover - canlı çıktı
15401616 self ._print ("\n [UYAP BULK] ÖZET" )
0 commit comments