fix(android): a network call that never returns no longer kills the interface

Reported symptom, with the local server stopped: Sync leaves the app inert -
neither Open vault nor Sync responds, and nothing is displayed. Log and
Server settings still work, because they are local to the page.

The log proves where it stops: the GET line is there, the "HTTP ... in ... ms"
line that immediately follows the call is not. So the request never returned,
SetBusy(False) never ran, and the two actions stayed disabled - a disabled
button fires no click, hence the silence. THTTPClient is given 15 s timeouts
and the Android request does inherit them, so this should not happen; rather
than keep chasing why it did, the situation is now impossible.

A deadline timer, armed while a request is in flight and longer than the HTTP
timeouts so the precise network error still wins when it works. On expiry the
interface comes back and says so. The thread is abandoned, not cancelled: it
dies when its socket fails, and if it somehow succeeds its result still shows.

The page's bridge queue had the same shape of bug: a throw while removing the
iframe left "sending" true forever, and every later command was dropped in
silence. The removal is now in a try/finally.

Also fixed while in there: SetBusy hardcoded "Sync" as the button label, so a
vault-less device lost "Download vault" on every busy transition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-17 13:11:21 +01:00
parent 0bcf9b95a2
commit c215a5755d
2 changed files with 55 additions and 5 deletions
+11 -2
View File
@@ -285,8 +285,15 @@ function drain() {
f.src = 'cmd://' + queue.shift();
document.body.appendChild(f);
setTimeout(function () {
f.parentNode.removeChild(f);
// LE try/finally EST L'INTERET DE CE BLOC. Sans lui, une exception au
// retrait de l'iframe laisse "sending" a vrai pour toujours : plus une
// seule commande ne repart, et l'interface devient muette sans rien
// signaler. Le meme piege existait cote Delphi sur l'indicateur d'occupation.
try {
if (f.parentNode) f.parentNode.removeChild(f);
} finally {
drain();
}
}, 60);
}
@@ -496,7 +503,9 @@ var PM = {
setActions: function (canOpen, syncLabel, busy) {
var o = document.getElementById('btnOpen'), s = document.getElementById('btnSync');
o.hidden = !canOpen;
s.innerHTML = '&#8635;&nbsp; ' + syncLabel;
// textContent et non innerHTML : l'intitule vient de Delphi, et une valeur
// interpretee comme du balisage n'a pas de raison de l'etre.
s.textContent = '\u21BB ' + syncLabel;
o.disabled = busy; s.disabled = busy;
o.style.opacity = busy ? .4 : 1;
s.style.opacity = busy ? .4 : 1;
+43 -2
View File
@@ -119,6 +119,14 @@ type
FClipTimer: TTimer;
FCopied: string;
/// ECHEANCE SUR UNE OPERATION RESEAU. Sans elle, un appel qui ne rend
/// jamais la main laisse l'interface morte et muette : les deux actions
/// restent desactivees, aucun message n'apparait, et rien n'y ramene -
/// c'est arrive avec le serveur local arrete. Les delais de THTTPClient
/// sont censes couvrir ce cas ; celui-ci couvre le jour ou ils ne le font
/// pas, quelle qu'en soit la raison.
FDeadline: TTimer;
// --- verrouillage sur inactivite
FIdleTimer: TTimer;
FLastActivity: TDateTime;
@@ -161,6 +169,7 @@ type
procedure PushConfig;
procedure OpenEntry(AVaultIndex: Integer);
procedure OnClipTimer(Sender: TObject);
procedure OnDeadline(Sender: TObject);
procedure OnHardwareBack(Sender: TObject; var Key: Word;
var KeyChar: WideChar; Shift: TShiftState);
@@ -228,6 +237,14 @@ begin
FClipTimer.Interval := CLIPBOARD_CLEAR_SECONDS * 1000;
FClipTimer.OnTimer := OnClipTimer;
// Plus long que les delais de THTTPClient (15 s), pour que le message precis
// du client reseau arrive le premier quand il fonctionne. Celui-ci n'est que
// le filet.
FDeadline := TTimer.Create(Self);
FDeadline.Enabled := False;
FDeadline.Interval := 25000;
FDeadline.OnTimer := OnDeadline;
FTotpTimer := TTimer.Create(Self);
FTotpTimer.Enabled := False;
FTotpTimer.Interval := 1000;
@@ -1022,11 +1039,35 @@ begin
end;
procedure TForm8.SetBusy(ABusy: Boolean);
var
LCanOpen: Boolean;
LLabel: string;
begin
FBusy := ABusy;
WebCall(Format('PM.setActions(%s, "Sync", %s)',
[LowerCase(BoolToStr(HasVault and not FVaultUnreadable, True)),
FDeadline.Enabled := False; // reposee a zero, pas prolongee
FDeadline.Enabled := ABusy;
LCanOpen := HasVault and not FVaultUnreadable;
// L'intitule suit l'etat du cache et non un mot fixe : sans coffre local,
// l'action n'est pas une synchronisation mais un premier telechargement, et
// le reprendre en dur ici effacait cette distinction a chaque passage.
if LCanOpen then LLabel := 'Sync' else LLabel := 'Download vault';
WebCall(Format('PM.setActions(%s, %s, %s)',
[LowerCase(BoolToStr(LCanOpen, True)), JsArg(LLabel),
LowerCase(BoolToStr(ABusy, True))]));
end;
/// L'operation reseau n'a pas rendu la main dans le delai. On rend l'interface
/// a l'utilisateur et on le dit. Le thread, lui, est ABANDONNE : il mourra
/// quand sa socket finira par echouer, et s'il aboutit malgre tout, son
/// resultat s'affichera - il n'y a rien a annuler.
procedure TForm8.OnDeadline(Sender: TObject);
begin
FDeadline.Enabled := False;
if not FBusy then Exit;
Log(' no reply after ' + (FDeadline.Interval div 1000).ToString +
' s - request abandoned');
Status('No reply from the server');
SetBusy(False);
end;
end.