Skip to content

Every finished crawl marked itself interrupted - #158

Merged
xroche merged 3 commits into
masterfrom
fix/lockfix
Aug 23, 2026
Merged

Every finished crawl marked itself interrupted#158
xroche merged 3 commits into
masterfrom
fix/lockfix

Conversation

@xroche

@xroche xroche commented Aug 23, 2026

Copy link
Copy Markdown
Owner

The finished pane calls stopMirror(true) on the way in, to be sure a crawl that is still running gets stopped. The engine keeps its option block alive after hts_main2 returns, so that late request is always answered, and the answer went straight into hts-cache/interrupted.lock. Every crawl therefore ended by stamping itself interrupted, and every project the user had ever completed reopened defaulting to "Continue an interrupted download".

The verdict now belongs to the run rather than to the stop request: runInternal writes the marker once, before the finished pane opens, and a stop that arrives after that is ignored. A crawl that drained its queue counts as finished however many links failed on the way. A user Stop, a nonzero engine code, and a size or time cap all count as cut short; the caps needed a new HTTrackLib.wasStopped(), because the engine applies them by stopping itself and still returns 0. Markers already on disk are left alone: nothing tells a stale one from a real interruption, so clearing them all would cost resumable state to anyone who genuinely did stop a crawl, and each one now corrects itself the next time that project is crawled to the end.

xroche and others added 3 commits August 23, 2026 11:47
The finished pane stops the runner on the way in, and the engine answers a stop
long after hts_main2 has returned, so interrupted.lock was written after every
crawl and every completed project reopened on "Continue an interrupted
download".

Decide the marker from the run instead: runInternal writes it once, before the
finished pane opens, and a stop that arrives afterwards is ignored. Errors do
not count as unfinished work. A user stop and a nonzero engine code do, and so
do the size and time caps, which the engine applies by stopping itself while
still returning 0, so HTTrackLib.wasStopped() reports them.

Markers already on disk are left in place; nothing tells a stale one from a
real interruption, and each corrects itself on the next completed crawl.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Review caught a regression this PR introduced. wasStopped() read state.stop,
which only a cap or a user Stop sets, and hts_main2 returns 0 for nearly every
other abort. A mirror killed by a full disk, by a full link table, by an
aborting callback or rolled back for want of a connection therefore looked
finished, and the marker was deleted rather than merely not written. Under
master it was always written, so those users would have lost a resume they
used to have and re-downloaded the site.

It now also asks hts_is_exiting(), which is exit_xh and is what the engine sets
in all four cases.

The unit tests could not see this: they drive the predicate with hand-supplied
pairs, so they re-assert what I believed the engine does. The new one reads the
JNI source and fails if wasStopped stops consulting either flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

# Conflicts:
#	app/src/test/java/com/httrack/android/TestSources.java
@xroche
xroche merged commit da0c3a5 into master Aug 23, 2026
6 checks passed
@xroche
xroche deleted the fix/lockfix branch August 23, 2026 14:31
xroche added a commit that referenced this pull request Aug 24, 2026
Review of the first commit found two defects in it.

The abort branch sat ahead of the interrupted one, but exit_xh is not a
sign that the engine gave up on its own: a stop early enough that nothing
was saved is rolled back for want of data (htscore.c:2088, on a guard that
never consults state.stop), and a forced stop refuses the loop callback
(htscore.c:951/1072). Both of the commonest ways a user ends a crawl were
about to be announced as aborts nobody asked for, undoing #158.

The reason string could never arrive. _hts_errmsg has one writer,
HTS_PANIC_PRINTF, whose call sites all sit in htscoremain.c ahead of the
mirror and return -1, so main() returning 0 and a non-empty hts_errmsg()
are disjoint. Every abort would have read "mirror aborted", and the
escaping around it guarded text that cannot exist. So abortCode() hands
back exit_xh itself and the three causes carry three messages, the full
link table is dropped from the claim (htsparse.c returns -1 without
touching exit_xh), and no engine string crosses JNI.

Choosing the outcome moves to MirrorOutcome, which imports no android.*
and is table-tested; both defects above fail it under mutation. between()
moves to TestSources, where two tests had already inlined it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
xroche added a commit that referenced this pull request Aug 24, 2026
The review found the assertion I wrote to replace #158's guard was
strictly weaker than the one it replaced. Rewriting the production line
was fair, since stop != NONE is equivalent to the old expression, but
contains("engine.wasStopped()") is satisfied by the ternary anywhere in
runInternal, so leavesPendingWork(stop == Stop.USER, code) brought #158
back with both assertions green. The argument is now taken with balanced
parentheses instead of up to the first comma, and it must read the run's
verdict without narrowing it to one kind of stop.

Changing false to Stop.ENGINE in two existing rows also left no row
pairing Stop.NONE with a live abort code, so an abort that never set the
stop flag could read as a success. Those rows are back alongside the new
ones.

Three more the same pass found: transposing the ternary's branches was
invisible, so each branch is pinned to its source; the pane's first
argument is pinned by position rather than by a four-character substring
a comment could satisfy; and the new Stopped! wording was pinned to
nothing.

STOPPED_AT_LIMIT moves after the aborts, because declaration order read
as a precedence and of() ranks it last. Stop.ENGINE's comment claimed the
cap, which is only what of() decides later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant