Compare commits

..

2 Commits

Author SHA1 Message Date
Xavier Roche
9a1913ff55 htsserver never returns from main(), it blocks on its own exit wait
background_threads counted two unrelated things: the pinger, which only ever
leaves through exit(), and each mirror thread, which does return. main() then
waited on background_threads - 1, so with no pinger it asked for a negative
number of outstanding threads and the counter cannot go there, and with one it
waited on the pinger itself. Either way htsserver hung instead of returning.

Count only what never returns, and let each wait name what it must leave
running: the pinger for main(), the pinger plus itself for back_launch_cmd().
Waiting on background_threads alone would have swapped the hang for an early
return, letting main() tear down while a mirror was still crawling.

Closes #753

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
2026-07-27 08:31:41 +02:00
Xavier Roche
1172eeca43 htsserver never returns from main(), it blocks on its own exit wait
htsthread_wait_n(background_threads - 1) subtracts one more than the count of
threads that must not be joined. Without --ppid that count is zero, so the wait
asks for a negative number of outstanding threads and the counter never gets
there; with --ppid it waits on the pinger, which by design never returns.

Wait for background_threads instead, which is what the sibling call inside
back_launch_cmd() already passes.

Closes #753

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
2026-07-27 08:11:22 +02:00
10 changed files with 62 additions and 51 deletions

View File

@@ -585,6 +585,14 @@ static int create_back_tmpfile(httrackp *opt, lien_back *const back,
return 0;
}
/* Move src onto dst; RENAME does not clobber an existing target on Windows. */
static hts_boolean replace_file(const char *src, const char *dst) {
if (RENAME(src, dst) == 0)
return HTS_TRUE;
(void) UNLINK(dst);
return RENAME(src, dst) == 0 ? HTS_TRUE : HTS_FALSE;
}
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
existing file moved the good copy to back->tmpfile before truncating url_sav.
commit keeps the new file and drops the backup; else restore it so an aborted
@@ -602,7 +610,7 @@ static void back_finalize_backup(httrackp *opt, lien_back *const back,
}
/* On failure keep the backup: an orphaned temp beats losing the good copy.
*/
if (!hts_rename_over(back->tmpfile, back->url_sav))
if (!replace_file(back->tmpfile, back->url_sav))
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"could not restore %s; previous copy kept as %s",
back->url_sav, back->tmpfile);
@@ -733,7 +741,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
"Read error when decompressing");
}
UNLINK(unpacked);
} else if (hts_rename_over(unpacked, back[p].url_sav)) {
} else if (replace_file(unpacked, back[p].url_sav)) {
/* The temp bypassed filecreate(), which is what chmods. */
#ifndef _WIN32
chmod(back[p].url_sav, HTS_ACCESS_FILE);

View File

@@ -1108,8 +1108,13 @@ hts_boolean singlefile_rewrite_file(httrackp *opt, const char *root,
(void) chmod(fconv(catbuff, sizeof(catbuff), StringBuff(tmp)),
HTS_ACCESS_FILE);
#endif
if (ok)
ok = hts_rename_over(StringBuff(tmp), page_path);
if (ok) {
/* RENAME does not clobber an existing target on Windows. */
if (RENAME(StringBuff(tmp), page_path) != 0) {
(void) UNLINK(page_path);
ok = RENAME(StringBuff(tmp), page_path) == 0 ? HTS_TRUE : HTS_FALSE;
}
}
if (!ok) {
hts_log_print(opt, LOG_ERROR, "single-file: could not rewrite %s",
page_path);

View File

@@ -1443,15 +1443,3 @@ HTSEXT_API hts_boolean hts_findissystem(find_handle find) {
}
return 0;
}
hts_boolean hts_rename_over(const char *src, const char *dst) {
char csrc[CATBUFF_SIZE], cdst[CATBUFF_SIZE];
fconv(csrc, sizeof(csrc), src);
fconv(cdst, sizeof(cdst), dst);
if (RENAME(csrc, cdst) == 0)
return HTS_TRUE;
/* RENAME does not clobber an existing target on Windows. */
(void) UNLINK(cdst);
return RENAME(csrc, cdst) == 0 ? HTS_TRUE : HTS_FALSE;
}

View File

@@ -137,10 +137,6 @@ HTSEXT_API hts_boolean hts_findisdir(find_handle find);
HTSEXT_API hts_boolean hts_findisfile(find_handle find);
HTSEXT_API hts_boolean hts_findissystem(find_handle find);
/* Move src onto dst, replacing an existing dst; HTS_TRUE on success. Both
paths are fconv()'d. */
hts_boolean hts_rename_over(const char *src, const char *dst);
#endif
#endif

View File

@@ -964,6 +964,16 @@ static zipFile wacz_zip_open(const char *path) {
return zipOpen2_64(path, 0 /*create*/, NULL, &ff);
}
/* Move src onto dst (UTF-8/Windows-safe); RENAME won't clobber on Windows, so
fall back to unlink+rename. Returns 0 on success. */
static int wacz_rename_over(const char *src, const char *dst) {
char cs[CATBUFF_SIZE], cd[CATBUFF_SIZE];
if (RENAME(fconv(cs, sizeof(cs), src), fconv(cd, sizeof(cd), dst)) == 0)
return 0;
(void) UNLINK(fconv(cd, sizeof(cd), dst));
return RENAME(fconv(cs, sizeof(cs), src), fconv(cd, sizeof(cd), dst));
}
/* Package the segment(s) + .cdx + a generated pages.jsonl into <base>.wacz at
crawl end (the archive file(s) and .cdx are already closed on disk). */
static void warc_wacz_package(warc_writer *w) {
@@ -1082,7 +1092,7 @@ static void warc_wacz_package(warc_writer *w) {
hts_log_print(w->opt, LOG_WARNING,
"WACZ: packaging failed, kept existing %s untouched",
waczpath);
} else if (!hts_rename_over(tmppath, waczpath)) {
} else if (wacz_rename_over(tmppath, waczpath) != 0) {
(void) UNLINK(fconv(catbuff, sizeof(catbuff), tmppath));
hts_log_print(w->opt, LOG_WARNING | LOG_ERRNO,
"WACZ: could not finalize %s", waczpath);

View File

@@ -101,8 +101,8 @@ static void htsweb_sig_brpipe(int code) {
/* ignore */
}
/* Number of background threads */
static int background_threads = 0;
/* Threads that never return; no wait may count on them draining. */
static int nonjoinable_threads = 0;
/* Server/client ping handling */
static htsmutex pingMutex = HTSMUTEX_INIT;
@@ -299,15 +299,19 @@ int main(int argc, char *argv[]) {
/* pinger */
if (parentPid > 0) {
hts_newthread(client_ping, (void *) (uintptr_t) parentPid);
background_threads++; /* Do not wait for this thread! */
if (hts_newthread(client_ping, (void *) (uintptr_t) parentPid) == 0) {
#ifndef _WIN32
nonjoinable_threads++; /* client_ping() only ever leaves through exit() */
#endif
}
smallserver_setpinghandler(pingHandler, NULL);
}
/* launch */
ret = help_server(argv[1], defaultPort, bindAddr);
htsthread_wait_n(background_threads - 1);
/* Drain everything a mirror may still have in flight, the pinger aside. */
htsthread_wait_n(nonjoinable_threads);
hts_uninit();
#ifdef _WIN32
@@ -382,7 +386,6 @@ void webhttrack_main(char *cmd) {
commandRunning = 1;
DEBUG(fprintf(stderr, "commandRunning=1\n"));
hts_newthread(back_launch_cmd, (void *) strdup(cmd));
background_threads++; /* Do not wait for this thread! */
}
void webhttrack_lock(void) {
@@ -423,8 +426,8 @@ static int webhttrack_runmain(httrackp * opt, int argc, char **argv) {
/* Rock'in! */
ret = hts_main2(argc, argv, opt);
/* Wait for pending threads to finish */
htsthread_wait_n(background_threads);
/* Wait for pending threads to finish; the pinger and this thread stay. */
htsthread_wait_n(nonjoinable_threads + 1);
return ret;
}

View File

@@ -18,7 +18,6 @@ set -euo pipefail
bash "$top_srcdir/tests/local-crawl.sh" \
--rerun-args '--update -M400000' \
--log-found 'More than 400000 bytes have been transferred.. giving up' \
--log-not-found 'could not restore' \
--found bigtrunc/slow.bin \
--file-min-bytes bigtrunc/slow.bin 655360 \
--file-min-bytes bigtrunc/fast.bin 655360 \

View File

@@ -79,6 +79,29 @@ done
# 65535 is a valid port the old "< 65535" bound refused
web_accepted 65535
# --- htsserver returns from main() instead of blocking at exit -------------
# The exit wait must exclude exactly the threads that never return (#753).
# $tmp has no lang.def, so the server fails right after announcing and main()
# reaches the wait on its own; rc, not EXITED, carries the verdict.
web_exits() {
local rc=0
: >"$tmp/exit.log"
run_with_timeout 30 htsserver "$tmp" "$@" >"$tmp/exit.log" 2>&1 || rc=$?
grep -q "^EXITED" "$tmp/exit.log" ||
! echo "FAIL: #753: htsserver ${*:-(no options)} never finished serving" || exit 1
# exactly 1, not merely "not 124": a crash or an assertf abort also escapes
# the wait, and would otherwise read as a pass
test "$rc" -eq 1 ||
! echo "FAIL: #753: htsserver ${*:-(no options)} exited $rc, want 1" || exit 1
}
# no pinger: the excluded count went negative
web_exits
# a pinger, which never returns and so must stay excluded
web_exits --ppid $$
# --- proxytrack <proxy-addr:port> <ICP-addr:port> --------------------------
# A bad argument falls through to the usage screen; it had no range check at
# all, so 65616 quietly listened on port 80. A valid one binds and blocks.

View File

@@ -5,16 +5,11 @@
# real gate: STORE-mode entries, the fixed layout, recomputed sha256 digests and
# the datapackage-digest chain (plus py-wacz/pywb when importable). The crawl
# skips cleanly on a build without OpenSSL (no conformant SHA-256 -> no package).
#
# The cacheless second pass repackages over the .wacz the first one left: the
# clobber, not the happy rename, is what breaks when a platform refuses to
# rename onto an existing file (#726).
set -eu
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --wacz-validate \
--rerun-args '-C0' --log-not-found 'could not finalize' \
--found 'mini304/index.html' --found 'mini304/page.html' \
httrack 'BASEURL/mini304/index.html' --warc-file warc-out --wacz

View File

@@ -57,8 +57,6 @@ rerun_dead=
tmpdir=
serverpid=
crawlpid=
wacz_poisoned=
wacz_poison="stale-wacz-that-a-second-pass-must-replace"
function warning {
echo "** $*" >&2
@@ -274,14 +272,6 @@ if test -n "$warc_validate"; then
test -z "$w1" || cp "$w1" "${tmpdir}/warc-pass1.gz"
fi
# Poison the first-pass .wacz when a second pass follows: repackaging moves the
# new archive over it, so the marker must be gone afterwards (#726). Poisoning
# beats comparing the two packages, which can come out byte-identical.
if test -n "$wacz_validate" && test -n "${rerun}${rerun_args}"; then
wacz_poisoned=$(find "$mirrorroot" -maxdepth 2 -name '*.wacz' 2>/dev/null | sort | tail -n1)
test -z "$wacz_poisoned" || echo "$wacz_poison" >"$wacz_poisoned"
fi
# --- optional second pass: re-mirror into the same dir (cache/update path) ----
if test -n "$rerun"; then
info "re-running httrack (update pass)"
@@ -426,12 +416,6 @@ if test -n "$wacz_validate"; then
fi
die "no .wacz file produced under $mirrorroot"
fi
if test -n "$wacz_poisoned"; then
info "checking the second pass replaced the .wacz"
grep -q "$wacz_poison" "$wacz_poisoned" 2>/dev/null &&
die "stale .wacz kept: $wacz_poisoned"
result "OK"
fi
validator=$(nativepath "${testdir}/wacz-validate.py")
info "validating WACZ package"
"$python" "$validator" "$(nativepath "$wacz")" >&2 || die "WACZ validation failed"