feat: enhance client probing mechanism and improve resolver scan logging

This commit is contained in:
Sarto
2026-04-03 23:10:58 +03:30
parent 3746dbdf1b
commit 3ad823ed76
5 changed files with 92 additions and 44 deletions
@@ -25,7 +25,6 @@ class MainActivity : ComponentActivity() {
private lateinit var webView: WebView
private lateinit var txtStatus: TextView
private val handler = Handler(Looper.getMainLooper())
private var probeAttempt = 0
private val notificationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
@@ -41,7 +40,6 @@ class MainActivity : ComponentActivity() {
requestNotificationPermission()
configureWebView()
startThefeedService()
probeAttempt = 0
waitForServerThenLoad()
}
@@ -84,7 +82,6 @@ class MainActivity : ComponentActivity() {
) {
// Server was reachable during probe but dropped connection — retry probe cycle
if (request?.isForMainFrame == true) {
probeAttempt = 0
setStatus("Reconnecting...")
handler.postDelayed({ waitForServerThenLoad() }, RETRY_DELAY_MS)
}
@@ -102,57 +99,48 @@ class MainActivity : ComponentActivity() {
}
/**
* Polls the server in a background thread until it responds with HTTP 200 (or any
* response — a TCP connection refused is what we're avoiding). Only then hands the
* URL to WebView, ensuring it never shows a browser error page on startup.
* Polls SharedPreferences for the port on every attempt, then probes the URL.
* This handles force-kill restarts where the service picks a new port:
* the loop follows the port change automatically instead of hammering a stale one.
*/
private fun waitForServerThenLoad() {
val port = getCurrentPort()
if (port <= 0) {
if (probeAttempt < MAX_PROBE_ATTEMPTS) {
probeAttempt++
setStatus("Waiting for service... ($probeAttempt/$MAX_PROBE_ATTEMPTS)")
handler.postDelayed({ waitForServerThenLoad() }, PROBE_INTERVAL_MS)
} else {
setStatus("Service unavailable. Restart the app.")
}
return
}
val url = "http://127.0.0.1:$port"
setStatus("Connecting...")
setStatus("Waiting for service...")
Thread {
var ready = false
repeat(MAX_PROBE_ATTEMPTS) { attempt ->
if (ready) return@repeat
var lastPort = -1
for (attempt in 1..MAX_PROBE_ATTEMPTS) {
val port = getCurrentPort()
if (port <= 0) {
handler.post { setStatus("Waiting for service... ($attempt/$MAX_PROBE_ATTEMPTS)") }
Thread.sleep(PROBE_INTERVAL_MS)
continue
}
if (port != lastPort) {
// Service restarted with a new port — reset and start fresh
lastPort = port
handler.post { setStatus("Connecting...") }
}
try {
val conn = URL(url).openConnection() as HttpURLConnection
val conn = URL("http://127.0.0.1:$port").openConnection() as HttpURLConnection
conn.connectTimeout = PROBE_TIMEOUT_MS.toInt()
conn.readTimeout = PROBE_TIMEOUT_MS.toInt()
conn.requestMethod = "GET"
val code = conn.responseCode
conn.disconnect()
if (code > 0) { // any HTTP response means the server is up
if (code > 0) {
ready = true
return@repeat
val url = "http://127.0.0.1:$port"
handler.post { setStatus(""); webView.loadUrl(url) }
return@Thread
}
} catch (_: Exception) {
// Connection refused or timeout — server not ready yet
// Connection refused not ready yet
}
handler.post { setStatus("Waiting for server... ($attempt/$MAX_PROBE_ATTEMPTS)") }
Thread.sleep(PROBE_INTERVAL_MS)
handler.post {
setStatus("Waiting for server... (${attempt + 1}/$MAX_PROBE_ATTEMPTS)")
}
}
handler.post {
if (ready) {
setStatus("")
webView.loadUrl(url)
} else {
setStatus("Could not connect. Restart the app.")
}
if (!ready) {
handler.post { setStatus("Could not connect. Restart the app.") }
}
}.start()
}
@@ -22,6 +22,7 @@ class ThefeedService : Service() {
super.onCreate()
createNotificationChannel()
startForeground(NOTIFICATION_ID, buildNotification("Starting local service..."))
savePort(-1) // Clear stale port from any previous (force-killed) session
startClientProcessAsync()
}
+12 -5
View File
@@ -74,11 +74,13 @@ func (rc *ResolverChecker) CheckNow() {
return
}
rc.log("Checking %d resolver(s)...", len(resolvers))
total := len(resolvers)
rc.log("RESOLVER_SCAN start %d", total)
var healthy []string
var mu sync.Mutex
var wg sync.WaitGroup
var done int
wg := &sync.WaitGroup{}
sem := make(chan struct{}, 10) // probe up to 10 resolvers concurrently
for _, r := range resolvers {
@@ -88,14 +90,17 @@ func (rc *ResolverChecker) CheckNow() {
sem <- struct{}{}
defer func() { <-sem }()
if rc.checkOne(r) {
mu.Lock()
ok := rc.checkOne(r)
mu.Lock()
if ok {
healthy = append(healthy, r)
mu.Unlock()
rc.log("Resolver OK: %s", r)
} else {
rc.log("Resolver failed: %s", r)
}
done++
rc.log("RESOLVER_SCAN progress %d/%d", done, total)
mu.Unlock()
}(r)
}
wg.Wait()
@@ -103,9 +108,11 @@ func (rc *ResolverChecker) CheckNow() {
rc.fetcher.SetActiveResolvers(healthy)
if len(healthy) == 0 {
rc.log("Resolver check done: 0/%d healthy", len(resolvers))
rc.log("RESOLVER_SCAN done 0/%d", total)
return
}
rc.log("Resolver check done: %d/%d healthy", len(healthy), len(resolvers))
rc.log("RESOLVER_SCAN done %d/%d", len(healthy), total)
}
// checkOne probes a single resolver by sending a metadata channel query
+52 -1
View File
@@ -217,6 +217,7 @@ html[dir=ltr] .active-badge{margin-left:0;margin-right:6px}
<span class="profile-btn-arrow">&#9660;</span>
</button>
<button class="icon-btn" onclick="openSettings()" title="Settings" data-i18n-title="settings">&#9881;</button>
<button class="icon-btn" onclick="jumpToLog()" title="LOG">&#128220;</button>
</div>
<input class="sidebar-search" id="channelSearch" type="text" data-i18n-ph="search" placeholder="Search..." oninput="filterChannels()">
</div>
@@ -840,6 +841,8 @@ function addLogLine(line){
var div=document.createElement('div');
var cls='inf';
if(typeof line==='string'){
// Handle structured resolver scan events — show progress bar, suppress from log
if(line.startsWith('RESOLVER_SCAN ')){updateResolverScanDisplay(line);return}
if(line.includes('Error:')||line.includes('error')||line.includes('Invalid passphrase'))cls='err';
else if(line.includes('Warning:'))cls='warn';
else if(line.includes('OK')||line.includes('success')||line.includes('done'))cls='ok';
@@ -850,6 +853,43 @@ function addLogLine(line){
while(el.children.length>200)el.removeChild(el.firstChild);
}
function updateResolverScanDisplay(line){
var panel=document.getElementById('progressPanel');
var item=document.getElementById('prog-resolvers');
// RESOLVER_SCAN start N
var startMatch=line.match(/^RESOLVER_SCAN start (\d+)/);
if(startMatch){
var total=parseInt(startMatch[1]);
if(!item){
item=document.createElement('div');item.id='prog-resolvers';item.className='progress-item';
item.innerHTML='<button class="progress-close" onclick="this.parentNode.remove()" title="Dismiss">&times;</button><div class="progress-label"></div><div class="progress-bar"><div class="progress-fill" style="width:0%"></div></div>';
panel.insertBefore(item,panel.firstChild);
}
item.dataset.total=total;
item.querySelector('.progress-label').textContent='Scanning resolvers 0/'+total;
item.querySelector('.progress-fill').style.width='0%';
return;
}
if(!item)return;
// RESOLVER_SCAN progress D/T
var progMatch=line.match(/^RESOLVER_SCAN progress (\d+)\/(\d+)/);
if(progMatch){
var done=parseInt(progMatch[1]),tot=parseInt(progMatch[2]);
var pct=Math.round((done/tot)*100);
item.querySelector('.progress-label').textContent='Scanning resolvers '+done+'/'+tot;
item.querySelector('.progress-fill').style.width=pct+'%';
return;
}
// RESOLVER_SCAN done K/T
var doneMatch=line.match(/^RESOLVER_SCAN done (\d+)\/(\d+)/);
if(doneMatch){
var healthy=parseInt(doneMatch[1]),total2=parseInt(doneMatch[2]);
item.querySelector('.progress-label').textContent='Resolvers ready: '+healthy+'/'+total2+' active';
item.querySelector('.progress-fill').style.width='100%';
setTimeout(function(){if(item.parentNode)item.parentNode.removeChild(item)},2000);
}
}
function updateProgressDisplay(line){
var match=line.match(/Channel\s+(\d+)/);if(!match)return;
var channelNum=parseInt(match[1]);
@@ -878,13 +918,24 @@ function toggleLog(){
p.classList.toggle('hidden',!logVisible);
ic.innerHTML=logVisible?'&#9660;':'&#9654;';
}
function openLog(){
if(logVisible)return;
logVisible=true;
document.getElementById('logPanel').classList.remove('hidden');
document.getElementById('logToggleIcon').innerHTML='&#9660;';
}
function jumpToLog(){
openChat();
openLog();
setTimeout(function(){document.getElementById('logPanel').scrollIntoView({behavior:'smooth',block:'end'})},300);
}
// ===== REFRESH =====
function showInitProgress(){
document.getElementById('progressPanel').innerHTML='';
var p=document.getElementById('progressPanel');
p.innerHTML='<div class="progress-item" id="prog-init"><button class="progress-close" onclick="this.parentNode.remove()" title="Dismiss">&times;</button><div class="progress-label">'+t('loading')+'</div><div class="progress-bar"><div class="progress-fill" style="width:30%;animation:prog-pulse 1.5s ease-in-out infinite"></div></div></div>';
if(window.innerWidth<=768) openChat();
if(window.innerWidth<=768){openChat();openLog();}
}
function startAutoRefresh(){if(autoRefreshTimer)return;autoRefreshTimer=setInterval(function(){if(selectedChannel>0)doRefresh(true)},600000)}
function updateNextFetchDisplay(){
+1
View File
@@ -564,6 +564,7 @@ func (s *Server) startCheckerThenRefresh() {
return
}
checker.StartAndNotify(ctx, func() {
time.Sleep(1 * time.Second)
s.refreshMetadataOnly()
})
}