mirror of
https://github.com/xroche/httrack.git
synced 2026-08-14 11:42:27 +03:00
Compare commits
12 Commits
zz-pidreus
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
935ea5a89b | ||
|
|
3a0cf262d6 | ||
|
|
47a41fb39e | ||
|
|
fabbf80ea5 | ||
|
|
af0e36e326 | ||
|
|
2dc30ab490 | ||
|
|
d681929d1a | ||
|
|
1483289eec | ||
|
|
1751c4b593 | ||
|
|
bad28b629c | ||
|
|
6584fded15 | ||
|
|
d8f0cec17d |
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -18,18 +18,29 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build (${{ matrix.arch }}, ${{ matrix.cc }})
|
||||
name: build (${{ matrix.arch }}, ${{ matrix.cc }}${{ matrix.label }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# cflags is spelled out everywhere: an exported empty CFLAGS reads as
|
||||
# "set" to configure, which then drops its own -g -O2.
|
||||
include:
|
||||
- { arch: x86-64, runner: ubuntu-24.04, cc: gcc }
|
||||
- { arch: x86-64, runner: ubuntu-24.04, cc: clang }
|
||||
- { arch: arm64, runner: ubuntu-24.04-arm, cc: gcc }
|
||||
- { arch: arm64, runner: ubuntu-24.04-arm, cc: clang }
|
||||
- { arch: x86-64, runner: ubuntu-24.04, cc: gcc, cflags: -g -O2 }
|
||||
- { arch: x86-64, runner: ubuntu-24.04, cc: clang, cflags: -g -O2 }
|
||||
- { arch: arm64, runner: ubuntu-24.04-arm, cc: gcc, cflags: -g -O2 }
|
||||
- { arch: arm64, runner: ubuntu-24.04-arm, cc: clang, cflags: -g -O2 }
|
||||
# Ubuntu builds httrack this way; no other leg here does.
|
||||
- {
|
||||
arch: x86-64,
|
||||
runner: ubuntu-24.04,
|
||||
cc: gcc,
|
||||
label: " -O3 -flto",
|
||||
cflags: -g -O3 -flto=auto -ffat-lto-objects,
|
||||
}
|
||||
env:
|
||||
CC: ${{ matrix.cc }}
|
||||
CFLAGS: ${{ matrix.cflags }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
|
||||
5
.github/workflows/windows-build.yml
vendored
5
.github/workflows/windows-build.yml
vendored
@@ -270,8 +270,9 @@ jobs:
|
||||
# Through the environment, never argv, which the process list exposes.
|
||||
WATCHDOG_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WATCHDOG_REPO: ${{ github.repository }}
|
||||
# github.sha here is the PR's merge commit, so statuses posted against it stay out of the PR's checks UI.
|
||||
WATCHDOG_SHA: ${{ github.sha }}
|
||||
# The PR head, not github.sha: a merge commit is garbage-collected, and
|
||||
# these statuses are the only trace a lost runner leaves (#1228).
|
||||
WATCHDOG_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
WATCHDOG_CONTEXT: windows-suite (${{ matrix.platform }}, ${{ matrix.configuration }})
|
||||
WATCHDOG_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
|
||||
@@ -30,6 +30,11 @@ the operational checklist: toolchain, invariants, and how to ship a change.
|
||||
check`, or `PATH="<bld>/src:$PATH"` for a manual run.
|
||||
- Give new `.test` scripts `set -e`: the older ones predate the rule, so several
|
||||
`local-crawl.sh` calls with no `set -e` report PASS on any non-last failure.
|
||||
- Each test runs under a 600s wall-clock guard that reports a wedge as 124. A test
|
||||
whose own work outlasts it raises the budget with a `# TEST_TIMEOUT_AT_LEAST: N`
|
||||
line, at column 0 within its first 40 lines, and paces itself with
|
||||
`skip_if_out_of_budget` so a host too slow to finish skips instead. The value only
|
||||
ever raises the budget: nothing can disarm the guard.
|
||||
- Run teardown with errexit off: `trap 'set +e; cleanup' EXIT`. Under `set -e` a
|
||||
failing cleanup command becomes the test's exit status (#773). Keep the other
|
||||
signals on their own `trap` line, or errexit stays off for the rest of the run.
|
||||
|
||||
10
debian/changelog
vendored
10
debian/changelog
vendored
@@ -1,3 +1,13 @@
|
||||
httrack (3.49.21-2) unstable; urgency=medium
|
||||
|
||||
* Fix the FTBFS on hppa: three suite tests measured the build host rather
|
||||
than the property they cover, and failed on the qemu-user buildd purely
|
||||
for being slower there. Patched from upstream, which now skips a step a
|
||||
host is too slow to finish instead of failing the build
|
||||
(skip-emulated-host-test-failures.patch).
|
||||
|
||||
-- Xavier Roche <xavier@debian.org> Thu, 13 Aug 2026 07:26:38 +0200
|
||||
|
||||
httrack (3.49.21-1) unstable; urgency=medium
|
||||
|
||||
* New upstream release: a site answering to several hostnames can now be
|
||||
|
||||
5
debian/control
vendored
5
debian/control
vendored
@@ -29,7 +29,10 @@ Description: Copy websites to your computer (Offline browser)
|
||||
Package: webhttrack
|
||||
Architecture: any
|
||||
Multi-Arch: foreign
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}, webhttrack-common, sensible-utils, chromium | firefox-esr | www-browser
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}, webhttrack-common, sensible-utils
|
||||
# Recommends, not Depends: the autoremoval gatherer follows only a disjunction's
|
||||
# first alternative, which ties the httrack source to whichever browser leads it.
|
||||
Recommends: firefox-esr | chromium | www-browser
|
||||
Replaces: webhttrack-common (<< 3.43.9-2)
|
||||
Breaks: webhttrack-common (<< 3.43.9-2)
|
||||
Suggests: httrack, httrack-doc
|
||||
|
||||
@@ -1,26 +1,85 @@
|
||||
// Function aimed to ping the webhttrack server regularly to keep it alive
|
||||
// If the browser window is closed, the server will eventually shutdown
|
||||
function ping_server() {
|
||||
var iframe = document.getElementById('pingiframe');
|
||||
if (iframe && iframe.src) {
|
||||
iframe.src = iframe.src;
|
||||
setTimeout(ping_server, 30000);
|
||||
}
|
||||
// Tell the server this window is alive, so an abandoned server stops instead of
|
||||
// outliving the session. The period is the one htsweb.c sizes its timeout from.
|
||||
var PING_PERIOD = 5000;
|
||||
|
||||
// Identifies this window for as long as it is open. The server counts windows,
|
||||
// so closing one of two must not read as the session ending.
|
||||
var PING_WINDOW =
|
||||
String(Math.random()).replace(/[^0-9]/g, "") + String(new Date().getTime());
|
||||
|
||||
function ping_url(extra) {
|
||||
// Unique, or a cached response would never reach the server again.
|
||||
return "/ping?w=" + PING_WINDOW + "&t=" + new Date().getTime() +
|
||||
(extra ? "&" + extra : "");
|
||||
}
|
||||
|
||||
// Create an invisible iframe to hold the server ping result
|
||||
// Only modern browsers will support that, but old browsers are compatible
|
||||
// with the legacy "wait for browser PID" mode
|
||||
if (document && document.createElement && document.body
|
||||
&& document.body.appendChild && document.getElementById) {
|
||||
var iframe = document.createElement('iframe');
|
||||
if (iframe) {
|
||||
iframe.id = 'pingiframe';
|
||||
iframe.style.display = "none";
|
||||
iframe.style.visibility = "hidden";
|
||||
iframe.width = iframe.height = 0;
|
||||
iframe.src = "/ping";
|
||||
document.body.appendChild(iframe);
|
||||
ping_server();
|
||||
}
|
||||
// An iframe is the fallback only: reassigning its src can push a history entry,
|
||||
// which would turn the Back button into a walk through past heartbeats.
|
||||
function ping_send(url) {
|
||||
if (window.fetch) {
|
||||
fetch(url, {cache : "no-store"});
|
||||
return true;
|
||||
}
|
||||
var iframe = document.getElementById('pingiframe');
|
||||
if (!iframe) {
|
||||
return false;
|
||||
}
|
||||
iframe.src = url;
|
||||
return true;
|
||||
}
|
||||
|
||||
function ping_server() {
|
||||
if (ping_send(ping_url())) {
|
||||
setTimeout(ping_server, PING_PERIOD);
|
||||
}
|
||||
}
|
||||
|
||||
// The session id this page carries, empty on the few pages that hold no form.
|
||||
function ping_sid() {
|
||||
var f = document.getElementsByName('sid');
|
||||
return f && f.length ? f[0].value : "";
|
||||
}
|
||||
|
||||
// Closing the window is the common case, and waiting out the timeout for it
|
||||
// would hold the server open long after the user considers it gone. The server
|
||||
// takes this only from a request holding the session id, so it goes as a POST;
|
||||
// a page without one falls back to the timeout.
|
||||
function ping_leaving() {
|
||||
var sid = ping_sid();
|
||||
if (!sid) {
|
||||
return;
|
||||
}
|
||||
var url = ping_url("e=bye");
|
||||
var body = "sid=" + encodeURIComponent(sid);
|
||||
var type = "application/x-www-form-urlencoded";
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon(url, new Blob([ body ], {type : type}));
|
||||
} else if (window.XMLHttpRequest) {
|
||||
// Synchronous: the page is going away, and an async send dies with it.
|
||||
var x = new XMLHttpRequest();
|
||||
x.open("POST", url, false);
|
||||
x.setRequestHeader("Content-Type", type);
|
||||
x.send(body);
|
||||
}
|
||||
}
|
||||
|
||||
// Old browsers reach none of this and stay on the legacy "wait for the launcher
|
||||
// to die" mode.
|
||||
if (document && document.createElement && document.body &&
|
||||
document.body.appendChild && document.getElementById) {
|
||||
if (!window.fetch) {
|
||||
var iframe = document.createElement('iframe');
|
||||
if (iframe) {
|
||||
iframe.id = 'pingiframe';
|
||||
iframe.style.display = "none";
|
||||
iframe.style.visibility = "hidden";
|
||||
iframe.width = iframe.height = 0;
|
||||
document.body.appendChild(iframe);
|
||||
}
|
||||
}
|
||||
ping_server();
|
||||
// pagehide, not unload: Safari's back/forward cache never fires unload.
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('pagehide', ping_leaving, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,13 @@ typedef const char *(*t_hts_htmlcheck_query3)(t_hts_callbackarg *carg,
|
||||
httrackp *opt,
|
||||
const char *question);
|
||||
|
||||
/* query3 answers HTS_WIZARD_SCOPE_INCLUDE+k and HTS_WIZARD_SCOPE_EXCLUDE+k take
|
||||
or drop the k-th host scope hts_wizard_host_scope() enumerates. The stride
|
||||
outruns any hostname's label count, so it cannot collide with the plain
|
||||
single-digit answers. */
|
||||
#define HTS_WIZARD_SCOPE_INCLUDE 1000
|
||||
#define HTS_WIZARD_SCOPE_EXCLUDE 2000
|
||||
|
||||
/* Per-tick progress hook: 'back' is the transfer slot array of 'back_max'
|
||||
entries, back_index the active one; lien_tot/lien_ntot and stats report
|
||||
queue size and running totals, stat_time the elapsed time. */
|
||||
|
||||
@@ -221,9 +221,11 @@ Please visit our Website: http://www.httrack.com
|
||||
|
||||
/* Copyright (C) 1998 Xavier Roche and other contributors */
|
||||
#define HTTRACK_AFF_AUTHORS "[XR&CO'2014]"
|
||||
/* Named fields (hts_footer_format); a "%s" anywhere would switch the template
|
||||
back to the legacy positional model, a user's own additions included. */
|
||||
#define HTS_DEFAULT_FOOTER \
|
||||
"<!-- Mirrored from %s%s by HTTrack Website Copier/" HTTRACK_AFF_VERSION \
|
||||
" " HTTRACK_AFF_AUTHORS ", %s -->"
|
||||
"<!-- Mirrored from {url} by HTTrack Website Copier/" HTTRACK_AFF_VERSION \
|
||||
" " HTTRACK_AFF_AUTHORS ", {date} -->"
|
||||
/* Honest crawler User-Agent; no fake OS/browser to go stale. */
|
||||
#define HTS_DEFAULT_USER_AGENT \
|
||||
"Mozilla/5.0 (compatible; HTTrack/" HTTRACK_AFF_VERSION \
|
||||
|
||||
13
src/htslib.c
13
src/htslib.c
@@ -4136,6 +4136,19 @@ hts_boolean hts_is_control_free(const char *str) {
|
||||
return hts_is_control_free_sized(str, strlen(str));
|
||||
}
|
||||
|
||||
hts_boolean hts_host_is_ipv4(const char *host, size_t len) {
|
||||
size_t i;
|
||||
int dots = 0;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
if (host[i] == '.')
|
||||
dots++;
|
||||
else if (host[i] < '0' || host[i] > '9')
|
||||
return HTS_FALSE;
|
||||
}
|
||||
return dots > 0 ? HTS_TRUE : HTS_FALSE;
|
||||
}
|
||||
|
||||
hts_boolean hts_proxy_is_socks(const char *name) {
|
||||
if (name == NULL)
|
||||
return HTS_FALSE;
|
||||
|
||||
@@ -255,6 +255,10 @@ hts_boolean hts_is_control_free_sized(const char *str, size_t len);
|
||||
/* Same over a NUL-terminated string. */
|
||||
hts_boolean hts_is_control_free(const char *str);
|
||||
|
||||
/* TRUE if host[0..len) is an IPv4 literal: digits and dots, at least one dot.
|
||||
Such a host has no domain structure to reverse or to widen into. */
|
||||
hts_boolean hts_host_is_ipv4(const char *host, size_t len);
|
||||
|
||||
/* TRUE if this -P proxy name (which keeps its scheme) is a SOCKS5 proxy. */
|
||||
hts_boolean hts_proxy_is_socks(const char *name);
|
||||
|
||||
|
||||
@@ -874,14 +874,14 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
|
||||
tempo[0] = '\0';
|
||||
strcatbuff(tempo, eol);
|
||||
// hts_footer_format returns <0 on overflow, leaving tempo
|
||||
// unterminated; emitting it would abort in strcatbuff
|
||||
// below.
|
||||
if (hts_footer_format(tempo + strlen(tempo),
|
||||
sizeof(tempo) - strlen(tempo),
|
||||
StringBuff(opt->footer), fields,
|
||||
sizeof(fields) / sizeof(fields[0])) >=
|
||||
0) {
|
||||
// Overflow (<0) leaves tempo unterminated, and the closing
|
||||
// eol is reserved below because strcatbuff aborts rather
|
||||
// than clips: either one would kill the crawl.
|
||||
if (hts_footer_format(
|
||||
tempo + strlen(tempo),
|
||||
sizeof(tempo) - strlen(tempo) - strlen(eol),
|
||||
StringBuff(opt->footer), fields,
|
||||
sizeof(fields) / sizeof(fields[0])) >= 0) {
|
||||
strcatbuff(tempo, eol);
|
||||
HT_ADD(tempo);
|
||||
}
|
||||
|
||||
@@ -4112,25 +4112,27 @@ static int st_hashkey_bounds(httrackp *opt, int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Prints the filter answer <n> emits for (adr, fil) [up]; with no arguments,
|
||||
asserts every answer against its expected pattern (#1119). */
|
||||
/* Prints the filter answer <n> emits for (adr, fil) [up] in [slot]; with no
|
||||
arguments, asserts every answer against its expected pattern (#1119). */
|
||||
static int st_wizardfilter(httrackp *opt, int argc, char **argv) {
|
||||
char pattern[HTS_URLMAXSIZE * 2];
|
||||
htsbuff f = htsbuff_array(pattern);
|
||||
|
||||
(void) opt;
|
||||
if (argc >= 3) {
|
||||
hts_wizard_answer_filter(&f, atoi(argv[0]), argv[1], argv[2],
|
||||
argc >= 4 && atoi(argv[3]) != 0 ? HTS_TRUE
|
||||
: HTS_FALSE);
|
||||
hts_wizard_answer_filter(
|
||||
&f, argc >= 5 ? atoi(argv[4]) : 0, atoi(argv[0]), argv[1], argv[2],
|
||||
argc >= 4 && atoi(argv[3]) != 0 ? HTS_TRUE : HTS_FALSE);
|
||||
printf("%s\n", pattern);
|
||||
return 0;
|
||||
}
|
||||
#define EMITS(n, adr, fil, up, expect) \
|
||||
#define EMITS_SLOT(slot, n, adr, fil, up, expect) \
|
||||
do { \
|
||||
hts_wizard_answer_filter(&f, (n), (adr), (fil), (up)); \
|
||||
hts_wizard_answer_filter(&f, (slot), (n), (adr), (fil), (up)); \
|
||||
assertf(strcmp(pattern, (expect)) == 0); \
|
||||
} while (0)
|
||||
#define EMITS(n, adr, fil, up, expect) \
|
||||
EMITS_SLOT(0, (n), (adr), (fil), (up), (expect))
|
||||
|
||||
/* the host-wide answers: 2 forbids, 5 (allowed to go up) and 6 authorize */
|
||||
EMITS(2, "foo.com", "/index.html", HTS_FALSE, "-foo.com/*");
|
||||
@@ -4171,11 +4173,252 @@ static int st_wizardfilter(httrackp *opt, int argc, char **argv) {
|
||||
EMITS(3, "foo.com", "/x", HTS_FALSE, "");
|
||||
EMITS(4, "foo.com", "/x", HTS_FALSE, "");
|
||||
EMITS(50, "foo.com", "/x", HTS_FALSE, "");
|
||||
/* only slot 0 is ever filled outside the host-scope answers */
|
||||
EMITS_SLOT(1, 2, "foo.com", "/x", HTS_FALSE, "");
|
||||
EMITS_SLOT(1, 6, "foo.com", "/x", HTS_FALSE, "");
|
||||
|
||||
/* the host-scope answers (#1117): both slots, the starred one missing the
|
||||
apex is why the second exists */
|
||||
#define SCOPE_IN HTS_WIZARD_SCOPE_INCLUDE
|
||||
#define SCOPE_EX HTS_WIZARD_SCOPE_EXCLUDE
|
||||
EMITS_SLOT(0, SCOPE_IN, "www.example.co.uk", "/x", HTS_FALSE,
|
||||
"+*.www.example.co.uk/*");
|
||||
EMITS_SLOT(1, SCOPE_IN, "www.example.co.uk", "/x", HTS_FALSE,
|
||||
"+www.example.co.uk/*");
|
||||
EMITS_SLOT(0, SCOPE_IN + 1, "www.example.co.uk", "/x", HTS_FALSE,
|
||||
"+*.example.co.uk/*");
|
||||
EMITS_SLOT(1, SCOPE_IN + 1, "www.example.co.uk", "/x", HTS_FALSE,
|
||||
"+example.co.uk/*");
|
||||
EMITS_SLOT(0, SCOPE_EX + 1, "www.example.co.uk", "/x", HTS_FALSE,
|
||||
"-*.example.co.uk/*");
|
||||
EMITS_SLOT(1, SCOPE_EX + 1, "www.example.co.uk", "/x", HTS_FALSE,
|
||||
"-example.co.uk/*");
|
||||
/* the port rides along, the credentials do not */
|
||||
EMITS_SLOT(0, SCOPE_IN + 1, "www.foo.com:8080", "/x", HTS_FALSE,
|
||||
"+*.foo.com:8080/*");
|
||||
EMITS_SLOT(1, SCOPE_IN, "user:pass@www.foo.com", "/x", HTS_FALSE,
|
||||
"+www.foo.com/*");
|
||||
/* past the last scope, and slot 2, emit nothing */
|
||||
EMITS_SLOT(0, SCOPE_IN + 2, "www.foo.com", "/x", HTS_FALSE, "");
|
||||
EMITS_SLOT(2, SCOPE_IN, "www.foo.com", "/x", HTS_FALSE, "");
|
||||
|
||||
/* what the pair must and must not catch */
|
||||
EMITS_SLOT(0, SCOPE_IN + 1, "www.example.co.uk", "/x", HTS_FALSE,
|
||||
"+*.example.co.uk/*");
|
||||
assertf(strjoker("a.b.example.co.uk/x", pattern + 1, NULL, NULL) != NULL);
|
||||
assertf(strjoker("example.co.uk/x", pattern + 1, NULL, NULL) == NULL);
|
||||
assertf(strjoker("notexample.co.uk/x", pattern + 1, NULL, NULL) == NULL);
|
||||
assertf(strjoker("example.co.uk.evil.com/x", pattern + 1, NULL, NULL) ==
|
||||
NULL);
|
||||
EMITS_SLOT(1, SCOPE_IN + 1, "www.example.co.uk", "/x", HTS_FALSE,
|
||||
"+example.co.uk/*");
|
||||
assertf(strjoker("example.co.uk/x", pattern + 1, NULL, NULL) != NULL);
|
||||
assertf(strjoker("notexample.co.uk/x", pattern + 1, NULL, NULL) == NULL);
|
||||
#undef SCOPE_IN
|
||||
#undef SCOPE_EX
|
||||
#undef EMITS_SLOT
|
||||
#undef EMITS
|
||||
printf("wizardfilter self-test OK\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Prints the domain scopes offered for <question>; with no argument, asserts
|
||||
the enumeration (#1117). */
|
||||
static int st_wizardscope(httrackp *opt, int argc, char **argv) {
|
||||
char scope[HTS_URLMAXSIZE];
|
||||
int k;
|
||||
|
||||
(void) opt;
|
||||
if (argc >= 1) {
|
||||
for (k = 0; hts_wizard_host_scope(argv[0], k, scope, sizeof(scope)); k++)
|
||||
printf("%d %s\n", k, scope);
|
||||
return 0;
|
||||
}
|
||||
#define SCOPE(question, k, expect) \
|
||||
do { \
|
||||
assertf(hts_wizard_host_scope((question), (k), scope, sizeof(scope))); \
|
||||
assertf(strcmp(scope, (expect)) == 0); \
|
||||
} while (0)
|
||||
/* poisoned first: comparing against '\0' cannot see a clear that never ran */
|
||||
#define NOSCOPE_SIZED(question, k, size) \
|
||||
do { \
|
||||
memset(scope, 'X', sizeof(scope)); \
|
||||
assertf(!hts_wizard_host_scope((question), (k), scope, (size))); \
|
||||
assertf(scope[0] == '\0'); \
|
||||
} while (0)
|
||||
#define NOSCOPE(question, k) NOSCOPE_SIZED((question), (k), sizeof(scope))
|
||||
|
||||
/* k widens by one label at a time, starting at the host itself */
|
||||
SCOPE("download.example.co.uk/x", 0, "download.example.co.uk");
|
||||
SCOPE("download.example.co.uk/x", 1, "example.co.uk");
|
||||
SCOPE("download.example.co.uk/x", 2, "co.uk");
|
||||
NOSCOPE("download.example.co.uk/x", 3); /* "uk" is a bare TLD */
|
||||
SCOPE("example.com", 0, "example.com"); /* an adr with no fil works */
|
||||
NOSCOPE("example.com", 1);
|
||||
NOSCOPE("localhost/x", 0); /* nothing to widen into */
|
||||
NOSCOPE("download.example.co.uk/x", -1);
|
||||
|
||||
/* protocol and credentials are stripped, the port kept */
|
||||
SCOPE("ftp://user:pass@www.foo.com/x", 1, "foo.com");
|
||||
SCOPE("www.foo.com:8080/x", 0, "www.foo.com:8080");
|
||||
SCOPE("www.foo.com:8080/x", 1, "foo.com:8080");
|
||||
NOSCOPE("www.foo.com:8080/x", 2);
|
||||
/* a path that carries dots or a colon must not be read as host labels */
|
||||
SCOPE("foo.com/a.b.c/d:e", 0, "foo.com");
|
||||
NOSCOPE("foo.com/a.b.c/d:e", 1);
|
||||
|
||||
/* the shared predicate: a dotless run of digits is a hostname, not an IP */
|
||||
assertf(hts_host_is_ipv4("1.2.3.4", 7));
|
||||
assertf(!hts_host_is_ipv4("12345", 5));
|
||||
assertf(!hts_host_is_ipv4("foo.com", 7));
|
||||
|
||||
/* an IP literal splits on dots without being a domain */
|
||||
NOSCOPE("192.168.1.1/x", 0);
|
||||
NOSCOPE("192.168.1.1:8080/x", 0);
|
||||
NOSCOPE("[3ffe:b80:1234::1]/x", 0);
|
||||
/* the dots inside this one reach the label walk unless brackets are refused
|
||||
*/
|
||||
NOSCOPE("[::ffff:1.2.3.4]/x", 0);
|
||||
|
||||
/* the root label of a fully-qualified host is not a label */
|
||||
SCOPE("www.foo.com./x", 0, "www.foo.com.");
|
||||
SCOPE("www.foo.com./x", 1, "foo.com.");
|
||||
NOSCOPE("www.foo.com./x", 2); /* "com." is still a bare TLD */
|
||||
NOSCOPE(".", 0);
|
||||
|
||||
/* the destination must fit the scope and its terminator, and never truncate
|
||||
*/
|
||||
{
|
||||
const char *q = "www.example.com/x";
|
||||
const size_t need = strlen("www.example.com");
|
||||
|
||||
memset(scope, 'X', sizeof(scope));
|
||||
assertf(hts_wizard_host_scope(q, 0, scope, need + 1));
|
||||
assertf(strcmp(scope, "www.example.com") == 0);
|
||||
NOSCOPE_SIZED(q, 0, need);
|
||||
NOSCOPE_SIZED(q, 0, 4);
|
||||
NOSCOPE_SIZED(q, 0, 1);
|
||||
}
|
||||
#undef SCOPE
|
||||
#undef NOSCOPE
|
||||
#undef NOSCOPE_SIZED
|
||||
printf("wizardscope self-test OK\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* #1117: which host-scope range an answer falls in. */
|
||||
static int st_wizardscopeanswer(httrackp *opt, int argc, char **argv) {
|
||||
(void) opt;
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
#define ANSWER(n, expect) assertf(hts_wizard_scope_answer(n) == (expect))
|
||||
/* the plain answers, and the boundary just below the first range */
|
||||
ANSWER(-999, HTS_DEFAULT);
|
||||
ANSWER(-1, HTS_DEFAULT);
|
||||
ANSWER(0, HTS_DEFAULT);
|
||||
ANSWER(7, HTS_DEFAULT);
|
||||
ANSWER(50, HTS_DEFAULT);
|
||||
ANSWER(HTS_WIZARD_SCOPE_INCLUDE - 1, HTS_DEFAULT);
|
||||
/* include runs up to the exclude base, and exclude has no upper end */
|
||||
ANSWER(HTS_WIZARD_SCOPE_INCLUDE, HTS_FALSE);
|
||||
ANSWER(HTS_WIZARD_SCOPE_EXCLUDE - 1, HTS_FALSE);
|
||||
ANSWER(HTS_WIZARD_SCOPE_EXCLUDE, HTS_TRUE);
|
||||
ANSWER(INT_MAX, HTS_TRUE);
|
||||
#undef ANSWER
|
||||
printf("wizardscopeanswer self-test OK\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Poison: comparing the recursion cap against 0 would not see a stray write of
|
||||
the level the crawl uses. */
|
||||
#define PRIO_UNSET 42
|
||||
|
||||
/* Prints what answer `n` does to the crawl; with no arguments, asserts every
|
||||
answer, on both an allowed and an already refused link. */
|
||||
static int st_wizardverdict(httrackp *opt, int argc, char **argv) {
|
||||
const hts_wizard asked = opt->wizard;
|
||||
FILE *const projectlog = opt->log;
|
||||
char line[HTS_URLMAXSIZE];
|
||||
FILE *log;
|
||||
int url, depth;
|
||||
|
||||
url = 0;
|
||||
depth = PRIO_UNSET;
|
||||
opt->wizard = HTS_WIZARD_ASK;
|
||||
if (argc >= 1) {
|
||||
hts_wizard_apply_verdict(opt, atoi(argv[0]), "foo.com", "/a/b.html", &url,
|
||||
&depth);
|
||||
printf("forbidden=%d stop=%d prio=%d\n", url,
|
||||
opt->wizard == HTS_WIZARD_AUTO, depth);
|
||||
opt->wizard = asked;
|
||||
return 0;
|
||||
}
|
||||
opt->log = NULL; /* the battery walks the answers that warn */
|
||||
/* answer `n` over a link the crawl had left at `in`: the verdict it must leave,
|
||||
whether it stops the questions, and the recursion cap it must set. */
|
||||
#define APPLIES(n, in, forbidden, stop, prio) \
|
||||
do { \
|
||||
url = (in); \
|
||||
depth = PRIO_UNSET; \
|
||||
opt->wizard = HTS_WIZARD_ASK; \
|
||||
hts_wizard_apply_verdict(opt, (n), "foo.com", "/a/b.html", &url, &depth); \
|
||||
assertf(url == (forbidden)); \
|
||||
assertf(opt->wizard == ((stop) ? HTS_WIZARD_AUTO : HTS_WIZARD_ASK)); \
|
||||
assertf(depth == (prio)); \
|
||||
} while (0)
|
||||
/* '*' refuses and stops the questions */
|
||||
APPLIES(-1, 0, 1, 1, PRIO_UNSET);
|
||||
APPLIES(-1, 1, 1, 1, PRIO_UNSET);
|
||||
/* the refusing answers, 3 included although it emits no filter yet */
|
||||
APPLIES(0, 0, 1, 0, PRIO_UNSET);
|
||||
APPLIES(1, 0, 1, 0, PRIO_UNSET);
|
||||
APPLIES(2, 0, 1, 0, PRIO_UNSET);
|
||||
APPLIES(3, 0, 1, 0, PRIO_UNSET);
|
||||
/* 4 caps the recursion and decides the link neither way */
|
||||
APPLIES(4, 0, 0, 0, 1);
|
||||
APPLIES(4, 1, 1, 0, 1);
|
||||
/* an accepting answer never clears a refusal the crawl already computed */
|
||||
APPLIES(5, 1, 1, 0, PRIO_UNSET);
|
||||
APPLIES(6, 1, 1, 0, PRIO_UNSET);
|
||||
APPLIES(7, 1, 1, 0, PRIO_UNSET);
|
||||
APPLIES(50, 1, 1, 0, PRIO_UNSET);
|
||||
APPLIES(-999, 1, 1, 0, PRIO_UNSET);
|
||||
APPLIES(6, 0, 0, 0, PRIO_UNSET);
|
||||
/* both ends of each scope range: include allows, exclude forbids */
|
||||
APPLIES(HTS_WIZARD_SCOPE_INCLUDE, 0, 0, 0, PRIO_UNSET);
|
||||
APPLIES(HTS_WIZARD_SCOPE_EXCLUDE - 1, 0, 0, 0, PRIO_UNSET);
|
||||
APPLIES(HTS_WIZARD_SCOPE_EXCLUDE, 0, 1, 0, PRIO_UNSET);
|
||||
APPLIES(INT_MAX, 0, 1, 0, PRIO_UNSET);
|
||||
/* an answer in no range is not taken for an accept, nor for a refusal */
|
||||
APPLIES(8, 0, 0, 0, PRIO_UNSET);
|
||||
APPLIES(8, 1, 1, 0, PRIO_UNSET);
|
||||
APPLIES(999, 0, 0, 0, PRIO_UNSET);
|
||||
APPLIES(-2, 0, 0, 0, PRIO_UNSET);
|
||||
APPLIES(-1000, 0, 0, 0, PRIO_UNSET);
|
||||
APPLIES(INT_MIN, 0, 0, 0, PRIO_UNSET);
|
||||
APPLIES(HTS_WIZARD_SCOPE_INCLUDE - 1, 0, 0, 0, PRIO_UNSET);
|
||||
#undef APPLIES
|
||||
|
||||
/* the warning is all an unknown answer does, so a known one must be silent */
|
||||
log = tmpfile();
|
||||
assertf(log != NULL);
|
||||
opt->log = log;
|
||||
hts_wizard_apply_verdict(opt, 6, "foo.com", "/a/b.html", &url, &depth);
|
||||
hts_wizard_apply_verdict(opt, 8, "foo.com", "/a/b.html", &url, &depth);
|
||||
rewind(log);
|
||||
assertf(fgets(line, (int) sizeof(line), log) != NULL);
|
||||
assertf(strstr(line, "unknown answer 8") != NULL);
|
||||
assertf(fgets(line, (int) sizeof(line), log) == NULL);
|
||||
fclose(log);
|
||||
|
||||
opt->log = projectlog;
|
||||
opt->wizard = asked;
|
||||
printf("wizardverdict self-test OK\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
#undef PRIO_UNSET
|
||||
|
||||
/* #159: hts_redirect_same_savefile decides whether a redirect is a same-file
|
||||
* alias. */
|
||||
static int st_redirect_samefile(httrackp *opt, int argc, char **argv) {
|
||||
@@ -9687,8 +9930,14 @@ static const struct selftest_entry {
|
||||
st_hashkey_bounds},
|
||||
{"redirect-samefile", "", "same-file redirect detection self-test (#159)",
|
||||
st_redirect_samefile},
|
||||
{"wizardfilter", "[<answer> <adr> <fil> [up]]",
|
||||
{"wizardfilter", "[<answer> <adr> <fil> [up [slot]]]",
|
||||
"filter emitted by a wizard answer", st_wizardfilter},
|
||||
{"wizardscope", "[<question>]",
|
||||
"domain scopes the wizard can offer for a host", st_wizardscope},
|
||||
{"wizardscopeanswer", "", "host-scope range of a wizard answer",
|
||||
st_wizardscopeanswer},
|
||||
{"wizardverdict", "[<answer>]", "what a wizard answer applies",
|
||||
st_wizardverdict},
|
||||
{"mime", "<filename>", "MIME type for a filename", st_mime},
|
||||
{"charset", "<charset> <hex:..|string>",
|
||||
"convert a string to UTF-8 from a charset", st_charset},
|
||||
|
||||
@@ -93,9 +93,16 @@ int commandReturnSet = 0;
|
||||
|
||||
httrackp *global_opt = NULL;
|
||||
|
||||
static void (*pingFun)(void*) = NULL;
|
||||
static void (*pingFun)(void *, smallserver_client_event, const char *) = NULL;
|
||||
static void* pingFunArg = NULL;
|
||||
|
||||
/* Report a client liveness event, if anybody is listening. */
|
||||
static void client_event(smallserver_client_event ev, const char *window) {
|
||||
if (pingFun != NULL) {
|
||||
pingFun(pingFunArg, ev, window);
|
||||
}
|
||||
}
|
||||
|
||||
/* Extern */
|
||||
extern void webhttrack_main(char *cmd);
|
||||
extern void webhttrack_lock(void);
|
||||
@@ -385,6 +392,43 @@ static void copy_header_value(char *dst, size_t size, const char *value) {
|
||||
strlncatbuff(dst, value, size, size - 1);
|
||||
}
|
||||
|
||||
/** Copy query parameter "name"'s alphanumeric value into dst; true when a
|
||||
non-empty one fit, and dst is left empty otherwise. Query-string counterpart
|
||||
to the POST-body checker below. */
|
||||
static hts_boolean query_alnum_value(char *dst, size_t size, const char *query,
|
||||
const char *name) {
|
||||
const size_t namelen = strlen(name);
|
||||
const char *s = query;
|
||||
|
||||
dst[0] = '\0';
|
||||
while (*s != '\0') {
|
||||
const char *const amp = strchr(s, '&');
|
||||
|
||||
if (strncmp(s, name, namelen) == 0 && s[namelen] == '=') {
|
||||
const char *v = s + namelen + 1;
|
||||
size_t n = 0;
|
||||
|
||||
while (*v != '\0' && *v != '&' && n + 1 < size &&
|
||||
isalnum((unsigned char) *v)) {
|
||||
dst[n++] = *v++;
|
||||
}
|
||||
dst[n] = '\0';
|
||||
/* Truncated, or not alphanumeric to its end, is no value at all: it must
|
||||
not reach a caller that trusted the return. */
|
||||
if (n > 0 && (*v == '\0' || *v == '&')) {
|
||||
return HTS_TRUE;
|
||||
}
|
||||
dst[0] = '\0';
|
||||
return HTS_FALSE;
|
||||
}
|
||||
if (amp == NULL) {
|
||||
break;
|
||||
}
|
||||
s = amp + 1;
|
||||
}
|
||||
return HTS_FALSE;
|
||||
}
|
||||
|
||||
/** Does the urlencoded request body present the expected session id?
|
||||
True only if at least one "sid" field is present and every occurrence
|
||||
matches, so it holds whichever one a later last-write-wins parse keeps.
|
||||
@@ -686,8 +730,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
};
|
||||
initStrElt initStr[] = {
|
||||
{"user", HTS_DEFAULT_USER_AGENT},
|
||||
{"footer", "<!-- Mirrored from %s%s by HTTrack Website Copier/3.x "
|
||||
"[XR&CO'2014], %s -->"},
|
||||
{"footer", HTS_DEFAULT_FOOTER},
|
||||
{"url2",
|
||||
"+*.png +*.gif +*.jpg +*.jpeg +*.css +*.js -ad.doubleclick.net/*"},
|
||||
{NULL, NULL}};
|
||||
@@ -723,6 +766,8 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
LLint length = 0;
|
||||
const char *error_redirect = NULL;
|
||||
hts_boolean denied = HTS_FALSE;
|
||||
/* The request proved it holds the session id. */
|
||||
hts_boolean authed = HTS_FALSE;
|
||||
char origin[256];
|
||||
char host[256];
|
||||
|
||||
@@ -753,9 +798,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
while((soc_c = (T_SOC) accept(soc, NULL, NULL)) == INVALID_SOCKET) ;
|
||||
|
||||
/* Ping */
|
||||
if (pingFun != NULL) {
|
||||
pingFun(pingFunArg);
|
||||
}
|
||||
client_event(SMALLSERVER_CLIENT_REQUEST, NULL);
|
||||
|
||||
/* Lock */
|
||||
webhttrack_lock();
|
||||
@@ -865,6 +908,8 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
buffer[0] = '\0';
|
||||
meth = 0;
|
||||
denied = HTS_TRUE;
|
||||
} else {
|
||||
authed = HTS_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1158,6 +1203,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
if (url && *++url == '/' && (pos = strchr(url, ' ')) && !(*pos = '\0')) {
|
||||
char fsfile[1024];
|
||||
const char *file;
|
||||
const char *query = "";
|
||||
FILE *fp;
|
||||
char *qpos;
|
||||
|
||||
@@ -1166,6 +1212,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
if (error_redirect == NULL) {
|
||||
if ((qpos = strchr(url, '?'))) {
|
||||
*qpos = '\0';
|
||||
query = qpos + 1;
|
||||
}
|
||||
if (strcmp(url, "/") == 0) {
|
||||
file = "/server/index.html";
|
||||
@@ -1790,13 +1837,32 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
} else if (strcmp(file, "/ping") == 0 ||
|
||||
strncmp(file, "/ping?", 6) == 0) {
|
||||
} else if (strcmp(file, "/ping") == 0) {
|
||||
/* A cached heartbeat would never reach us again, and silence is
|
||||
what the watchdog reads as a dead window. */
|
||||
char error_hdr[] =
|
||||
"HTTP/1.0 200 Pong\r\n" "Server: httrack small server\r\n"
|
||||
"Content-type: text/html\r\n";
|
||||
"HTTP/1.0 200 Pong\r\n"
|
||||
"Server: httrack small server\r\n"
|
||||
"Content-type: text/html\r\n"
|
||||
"Cache-Control: no-cache, must-revalidate, private\r\n"
|
||||
"Pragma: no-cache\r\n";
|
||||
|
||||
char window[SMALLSERVER_WINDOW_ID_MAX + 1];
|
||||
|
||||
StringCat(headers, error_hdr);
|
||||
if (query_alnum_value(window, sizeof(window), query, "w")) {
|
||||
char verb[SMALLSERVER_WINDOW_ID_MAX + 1];
|
||||
|
||||
/* Ending a session is a command, so it carries the session id
|
||||
like every other one. A heartbeat can only extend a life, and
|
||||
any local peer or visited page can send one of those. */
|
||||
client_event(
|
||||
authed && query_alnum_value(verb, sizeof(verb), query, "e") &&
|
||||
strcmp(verb, "bye") == 0
|
||||
? SMALLSERVER_CLIENT_LEAVING
|
||||
: SMALLSERVER_CLIENT_PING,
|
||||
window);
|
||||
}
|
||||
} else {
|
||||
char error_hdr[] =
|
||||
"HTTP/1.0 404 Not Found\r\n" "Server: httrack small server\r\n"
|
||||
@@ -1873,6 +1939,10 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Only the UI asking to quit is a clean stop; losing the socket or the buffer
|
||||
is what the caller reports as a failure. */
|
||||
retour = willexit;
|
||||
|
||||
StringFree(headers);
|
||||
StringFree(output);
|
||||
StringFree(tmpbuff);
|
||||
@@ -1918,7 +1988,9 @@ int htslang_uninit(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void smallserver_setpinghandler(void (*fun)(void*), void*arg) {
|
||||
void smallserver_setpinghandler(void (*fun)(void *, smallserver_client_event,
|
||||
const char *),
|
||||
void *arg) {
|
||||
pingFun = fun;
|
||||
pingFunArg = arg;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,20 @@ extern httrackp *global_opt;
|
||||
#define min(a,b) ((a)>(b)?(b):(a))
|
||||
#define max(a,b) ((a)>(b)?(a):(b))
|
||||
|
||||
extern void smallserver_setpinghandler(void (*fun)(void*), void*arg);
|
||||
/* What the UI just told us about itself, reported to the ping handler. */
|
||||
typedef enum {
|
||||
SMALLSERVER_CLIENT_REQUEST, /* any request; claims no window */
|
||||
SMALLSERVER_CLIENT_PING, /* one window's heartbeat */
|
||||
SMALLSERVER_CLIENT_LEAVING /* that window is closing */
|
||||
} smallserver_client_event;
|
||||
|
||||
/* Longest window id a page may claim; a longer one is ignored. */
|
||||
#define SMALLSERVER_WINDOW_ID_MAX 32
|
||||
|
||||
/* fun() receives the id of the window the event came from, NULL when no window
|
||||
claimed it. */
|
||||
extern void smallserver_setpinghandler(
|
||||
void (*fun)(void *, smallserver_client_event, const char *), void *arg);
|
||||
extern int smallserver_setkey(const char *key, const char *value);
|
||||
extern int smallserver_setkeyint(const char *key, LLint value);
|
||||
extern int smallserver_setkeyarr(const char *key, int id, const char *key2, const char *value);
|
||||
|
||||
@@ -557,19 +557,6 @@ static char *path_basename_dup(const char *path) {
|
||||
return strdupt(b);
|
||||
}
|
||||
|
||||
/* A host made only of digits and dots is an IPv4 literal (never reversed). */
|
||||
static int surt_host_is_ip(const char *h, size_t n) {
|
||||
size_t i;
|
||||
int dots = 0;
|
||||
for (i = 0; i < n; i++) {
|
||||
if (h[i] == '.')
|
||||
dots++;
|
||||
else if (h[i] < '0' || h[i] > '9')
|
||||
return 0;
|
||||
}
|
||||
return dots > 0;
|
||||
}
|
||||
|
||||
/* SURT-canonicalize url into out (no newline): scheme and userinfo dropped,
|
||||
host lowercased with a leading www[digits] label stripped and the scheme
|
||||
default port removed, labels reversed and comma-joined then ')', path+query
|
||||
@@ -637,7 +624,7 @@ static int surt_canon(const char *url, wbuf *out) {
|
||||
hostbuf[hlen] = '\0';
|
||||
|
||||
if (!is_ipv6)
|
||||
is_ip = surt_host_is_ip(hostbuf, hlen);
|
||||
is_ip = hts_host_is_ipv4(hostbuf, hlen);
|
||||
|
||||
if (!is_ipv6 && !is_ip && hlen >= 4 && hostbuf[0] == 'w' &&
|
||||
hostbuf[1] == 'w' && hostbuf[2] == 'w') {
|
||||
|
||||
174
src/htsweb.c
174
src/htsweb.c
@@ -105,49 +105,133 @@ static void htsweb_sig_brpipe(int code) {
|
||||
/* Threads that never return; no wait may count on them draining. */
|
||||
static int nonjoinable_threads = 0;
|
||||
|
||||
/* Server/client ping handling */
|
||||
/* Session lifetime: each window pings under its own id and drops it when it
|
||||
closes, so an abandoned server stops instead of outliving the session and
|
||||
holding its payload open (a mounted disk image, on macOS). Windows are
|
||||
counted, not timed: closing one of several must not end the session. */
|
||||
#define PING_PERIOD 5
|
||||
/* Silence tolerated from one window. Generous: a hidden tab has its timers
|
||||
throttled to as little as one wake-up a minute. */
|
||||
static int pingTimeout = 120;
|
||||
/* Once the last window leaves, only a page navigation can bring one back, and
|
||||
that takes a fraction of a second over the loopback. */
|
||||
#define LEAVE_GRACE max(2, min(5, pingTimeout / 4))
|
||||
/* Windows tracked at once. A full table refuses newcomers rather than evicting:
|
||||
dropping a live window is what would let a flood of ids end the session. */
|
||||
#define MAX_WINDOWS 16
|
||||
|
||||
static htsmutex pingMutex = HTSMUTEX_INIT;
|
||||
static unsigned int pingId = 0;
|
||||
static unsigned int getPingId(void) {
|
||||
unsigned int id;
|
||||
hts_mutexlock(&pingMutex);
|
||||
id = pingId;
|
||||
hts_mutexrelease(&pingMutex);
|
||||
return id;
|
||||
/* Seconds the watchdog has been awake, not wall-clock: time(NULL) jumps across
|
||||
a laptop suspend, and a suspended machine must not age a session. */
|
||||
static int ticks = 0;
|
||||
|
||||
static struct {
|
||||
char id[SMALLSERVER_WINDOW_ID_MAX + 1];
|
||||
int last_seen;
|
||||
} windows[MAX_WINDOWS];
|
||||
|
||||
static int windowCount = 0;
|
||||
static int emptySince = 0; /* tick the last window left at */
|
||||
static hts_boolean anyWindow = HTS_FALSE; /* a window has claimed an id */
|
||||
static int lastSeen = 0; /* tick of the last request of any kind */
|
||||
static hts_boolean anyRequest = HTS_FALSE; /* something has connected */
|
||||
|
||||
/* Drop windows[i], moving the last entry into its slot: a caller removing while
|
||||
it iterates must walk backwards. Caller holds pingMutex. */
|
||||
static void window_forget(int i) {
|
||||
windows[i] = windows[--windowCount];
|
||||
if (windowCount == 0) {
|
||||
emptySince = ticks;
|
||||
}
|
||||
}
|
||||
static void ping(void) {
|
||||
|
||||
static void pingHandler(void *arg, smallserver_client_event ev,
|
||||
const char *window) {
|
||||
int i = 0;
|
||||
|
||||
(void) arg;
|
||||
hts_mutexlock(&pingMutex);
|
||||
pingId++;
|
||||
lastSeen = ticks;
|
||||
anyRequest = HTS_TRUE;
|
||||
/* A bare request names no window: any local peer can open a connection, but
|
||||
none may cancel a real window's departure. */
|
||||
if (window != NULL) {
|
||||
while (i < windowCount && strcmp(windows[i].id, window) != 0) {
|
||||
i++;
|
||||
}
|
||||
if (ev == SMALLSERVER_CLIENT_LEAVING) {
|
||||
if (i < windowCount) {
|
||||
window_forget(i);
|
||||
}
|
||||
} else if (i < windowCount) {
|
||||
windows[i].last_seen = ticks;
|
||||
} else if (windowCount < MAX_WINDOWS) {
|
||||
windows[windowCount].id[0] = '\0';
|
||||
strlncatbuff(windows[windowCount].id, window,
|
||||
sizeof(windows[windowCount].id),
|
||||
sizeof(windows[windowCount].id) - 1);
|
||||
windows[windowCount++].last_seen = ticks;
|
||||
anyWindow = HTS_TRUE;
|
||||
}
|
||||
}
|
||||
hts_mutexrelease(&pingMutex);
|
||||
}
|
||||
|
||||
static void client_ping(void *pP) {
|
||||
#ifndef _WIN32
|
||||
/* Timeout to 120s ; normally client pings every 30 second */
|
||||
static int timeout = 120;
|
||||
/* Wait for parent to die (legacy browser mode). */
|
||||
const pid_t ppid = (pid_t) (uintptr_t) pP;
|
||||
while (!kill(ppid, 0)) {
|
||||
sleep(1);
|
||||
}
|
||||
/* Parent (webhttrack script) is dead: is client pinging ? */
|
||||
for(;;) {
|
||||
unsigned int id = getPingId();
|
||||
sleep(timeout);
|
||||
if (getPingId() == id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Die! */
|
||||
fprintf(stderr,
|
||||
"Parent process %d died, and client did not ping for %ds: exiting!\n",
|
||||
(int) ppid, timeout);
|
||||
exit(EXIT_FAILURE);
|
||||
/* True unless the launcher we were started from is known to be gone. */
|
||||
static hts_boolean parent_is_alive(uintptr_t ppid) {
|
||||
#ifdef _WIN32
|
||||
(void) ppid;
|
||||
return HTS_TRUE; /* no cheap probe; the heartbeat carries this */
|
||||
#else
|
||||
/* kill(0) would signal our own process group, never a parent. */
|
||||
return ppid == 0 || kill((pid_t) ppid, 0) == 0 ? HTS_TRUE : HTS_FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void pingHandler(void*arg) {
|
||||
ping();
|
||||
static void client_ping(void *pP) {
|
||||
/* uintptr_t, not pid_t: MSVC has no such type, and this signature is not
|
||||
inside a POSIX guard. */
|
||||
const uintptr_t ppid = (uintptr_t) pP;
|
||||
const char *why = NULL;
|
||||
|
||||
while (why == NULL) {
|
||||
int i;
|
||||
|
||||
Sleep(1000);
|
||||
/* A mirror in flight outranks every rule below: it may have hours of
|
||||
crawling behind it, and the user can always come back to its page. */
|
||||
if (commandRunning) {
|
||||
continue;
|
||||
}
|
||||
hts_mutexlock(&pingMutex);
|
||||
ticks++;
|
||||
/* A window that stops pinging without a goodbye crashed with its browser.
|
||||
*/
|
||||
for (i = windowCount; i-- > 0;) {
|
||||
if (ticks - windows[i].last_seen >= pingTimeout) {
|
||||
window_forget(i);
|
||||
}
|
||||
}
|
||||
if (anyWindow && windowCount == 0 && ticks - emptySince >= LEAVE_GRACE) {
|
||||
why = "the interface was closed";
|
||||
} else if (!anyWindow &&
|
||||
ticks - lastSeen >= pingTimeout
|
||||
/* No window ever pinged: a browser too old for it, or none
|
||||
opened. Fall back to the launcher dying with that browser,
|
||||
rather than to silence, which a reader also produces. */
|
||||
&& (!anyRequest || !parent_is_alive(ppid))) {
|
||||
why = "the interface went silent";
|
||||
}
|
||||
hts_mutexrelease(&pingMutex);
|
||||
/* Re-read after the decision: a mirror may have started while it was made,
|
||||
and exiting now would lose it. */
|
||||
if (commandRunning) {
|
||||
why = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "Exiting: %s\n", why);
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
@@ -184,6 +268,7 @@ int main(int argc, char *argv[]) {
|
||||
fprintf(stderr, "** Warning: use the webhttrack frontend if available\n");
|
||||
fprintf(stderr,
|
||||
"usage: %s [--port <port>] [--bind <address>] [--ppid parent-pid] "
|
||||
"[--ping-timeout <seconds>] "
|
||||
"<path-to-html-root-dir> [key value [key value]..]\n",
|
||||
argv[0]);
|
||||
fprintf(stderr, "example: %s /usr/share/httrack/\n", argv[0]);
|
||||
@@ -288,6 +373,17 @@ int main(int argc, char *argv[]) {
|
||||
fprintf(stderr, "couldn't set the parent PID to %s\n", argv[i + 1]);
|
||||
return -1;
|
||||
}
|
||||
} else if (strcmp(argv[i], "--ping-timeout") == 0 && i + 1 < argc) {
|
||||
/* Bounded, not just parsed: %d wrapping a huge value into a plausible one
|
||||
is what #614 cost on --port, two cases above. */
|
||||
char *end = NULL;
|
||||
const long v = strtol(argv[i + 1], &end, 10);
|
||||
|
||||
if (end == argv[i + 1] || *end != '\0' || v < 1 || v > 86400) {
|
||||
fprintf(stderr, "couldn't set the ping timeout to %s\n", argv[i + 1]);
|
||||
return -1;
|
||||
}
|
||||
pingTimeout = (int) v;
|
||||
} else if (i + 1 < argc) {
|
||||
smallserver_setkey(argv[i], argv[i + 1]);
|
||||
} else {
|
||||
@@ -304,9 +400,7 @@ int main(int argc, char *argv[]) {
|
||||
/* pinger */
|
||||
if (parentPid > 0) {
|
||||
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);
|
||||
}
|
||||
@@ -389,7 +483,13 @@ static void back_launch_cmd(void *pP) {
|
||||
void webhttrack_main(char *cmd) {
|
||||
commandRunning = 1;
|
||||
DEBUG(fprintf(stderr, "commandRunning=1\n"));
|
||||
hts_newthread(back_launch_cmd, (void *) strdup(cmd));
|
||||
if (hts_newthread(back_launch_cmd, (void *) strdup(cmd)) != 0) {
|
||||
/* Nothing else clears the flag, and while it is set the watchdog holds the
|
||||
server open for a mirror that never started. */
|
||||
commandRunning = 0;
|
||||
commandEnd = 1;
|
||||
commandReturn = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void webhttrack_lock(void) {
|
||||
|
||||
192
src/htswizard.c
192
src/htswizard.c
@@ -181,7 +181,84 @@ static void wizard_cat_path(htsbuff *f, const char *sign, const char *adr,
|
||||
htsbuff_catn(f, fil, len);
|
||||
}
|
||||
|
||||
void hts_wizard_answer_filter(htsbuff *f, int n, const char *adr,
|
||||
HTSEXT_API hts_boolean hts_wizard_host_scope(const char *question, int k,
|
||||
char *dst, size_t dstsize) {
|
||||
const char *host, *port, *slash, *end, *scope;
|
||||
size_t len;
|
||||
|
||||
if (dst == NULL || dstsize == 0)
|
||||
return HTS_FALSE;
|
||||
dst[0] = '\0';
|
||||
if (question == NULL || k < 0)
|
||||
return HTS_FALSE;
|
||||
|
||||
host = jump_identification_const(question);
|
||||
port = jump_toport_const(question);
|
||||
slash = strchr(host, '/');
|
||||
end = host + strlen(host);
|
||||
scope = host;
|
||||
if (slash != NULL && slash < end)
|
||||
end = slash;
|
||||
/* the port belongs to the filter, so keep it and only bound the label walk */
|
||||
if (port != NULL && port < end)
|
||||
slash = port;
|
||||
else
|
||||
slash = end;
|
||||
/* a fully-qualified "foo.com." ends on the root label, which is not one */
|
||||
if (slash > host && slash[-1] == '.')
|
||||
slash--;
|
||||
if (slash == host || *host == '[') /* no host, or an IPv6 literal */
|
||||
return HTS_FALSE;
|
||||
if (hts_host_is_ipv4(host, (size_t) (slash - host)))
|
||||
return HTS_FALSE;
|
||||
|
||||
/* widen by dropping one leading label per step, and never offer a bare TLD */
|
||||
for (; k > 0; k--) {
|
||||
const char *dot = memchr(scope, '.', (size_t) (slash - scope));
|
||||
|
||||
if (dot == NULL)
|
||||
return HTS_FALSE;
|
||||
scope = dot + 1;
|
||||
}
|
||||
if (memchr(scope, '.', (size_t) (slash - scope)) == NULL)
|
||||
return HTS_FALSE;
|
||||
|
||||
len = (size_t) (end - scope);
|
||||
if (len >= dstsize)
|
||||
return HTS_FALSE;
|
||||
memcpy(dst, scope, len);
|
||||
dst[len] = '\0';
|
||||
return HTS_TRUE;
|
||||
}
|
||||
|
||||
hts_tristate hts_wizard_scope_answer(int n) {
|
||||
if (n >= HTS_WIZARD_SCOPE_EXCLUDE)
|
||||
return HTS_TRUE;
|
||||
if (n >= HTS_WIZARD_SCOPE_INCLUDE)
|
||||
return HTS_FALSE;
|
||||
return HTS_DEFAULT;
|
||||
}
|
||||
|
||||
/* The subdomain form of the scope in slot 0, its apex in slot 1: the starred
|
||||
one does not match the apex, so a whole-domain answer needs both. */
|
||||
static void wizard_cat_scope(htsbuff *f, const char *sign, const char *adr,
|
||||
int n, int slot) {
|
||||
char scope[HTS_URLMAXSIZE];
|
||||
const int k =
|
||||
n - (hts_wizard_scope_answer(n) == HTS_TRUE ? HTS_WIZARD_SCOPE_EXCLUDE
|
||||
: HTS_WIZARD_SCOPE_INCLUDE);
|
||||
|
||||
if (slot >= HTS_WIZARD_MAX_FILTERS ||
|
||||
!hts_wizard_host_scope(adr, k, scope, sizeof(scope)))
|
||||
return;
|
||||
htsbuff_cpy(f, sign);
|
||||
if (slot == 0)
|
||||
htsbuff_cat(f, "*.");
|
||||
htsbuff_cat(f, scope);
|
||||
htsbuff_cat(f, "/*");
|
||||
}
|
||||
|
||||
void hts_wizard_answer_filter(htsbuff *f, int slot, int n, const char *adr,
|
||||
const char *fil, hts_boolean seeker_up) {
|
||||
size_t dir = hts_lastcharoffset(fil);
|
||||
|
||||
@@ -189,6 +266,13 @@ void hts_wizard_answer_filter(htsbuff *f, int n, const char *adr,
|
||||
dir--;
|
||||
|
||||
htsbuff_cpy(f, "");
|
||||
if (hts_wizard_scope_answer(n) != HTS_DEFAULT) {
|
||||
wizard_cat_scope(f, hts_wizard_scope_answer(n) == HTS_TRUE ? "-" : "+", adr,
|
||||
n, slot);
|
||||
return;
|
||||
}
|
||||
if (slot != 0) /* every other answer emits a single filter */
|
||||
return;
|
||||
switch (n) {
|
||||
case 0: /* this link only */
|
||||
wizard_cat_path(f, "-", adr, fil, (size_t) -1);
|
||||
@@ -238,6 +322,46 @@ void hts_wizard_answer_filter(htsbuff *f, int n, const char *adr,
|
||||
}
|
||||
}
|
||||
|
||||
void hts_wizard_apply_verdict(httrackp *opt, int n, const char *adr,
|
||||
const char *fil, int *forbidden_url,
|
||||
int *set_prio_to) {
|
||||
switch (n) {
|
||||
case -1: /* skip this link and every question after it */
|
||||
*forbidden_url = 1;
|
||||
opt->wizard = HTS_WIZARD_AUTO;
|
||||
break;
|
||||
|
||||
case 0: /* this link */
|
||||
case 1: /* this directory and below */
|
||||
case 2: /* the whole host */
|
||||
case 3: /* the parent directory, which emits no filter yet */
|
||||
*forbidden_url = 1;
|
||||
break;
|
||||
|
||||
case 4: /* wizard filters both allow and forbid, so an isolated link taken
|
||||
with no depth limit would mirror the whole site */
|
||||
*set_prio_to = 0 + 1; /* recursion level 0 */
|
||||
break;
|
||||
|
||||
case 5: /* this directory and below, or the whole host */
|
||||
case 6: /* the whole host */
|
||||
case 7: /* this directory, files only */
|
||||
case 50: /* nothing to do */
|
||||
case -999: /* the "!" answer, and anything the front end could not parse */
|
||||
break;
|
||||
|
||||
default: /* a scope answer forbids like 2 or allows like 6 */
|
||||
if (hts_wizard_scope_answer(n) == HTS_TRUE)
|
||||
*forbidden_url = 1;
|
||||
else if (hts_wizard_scope_answer(n) == HTS_DEFAULT)
|
||||
hts_log_print(opt, LOG_WARNING,
|
||||
"(wizard) unknown answer %d at %s%s, keeping the computed "
|
||||
"verdict",
|
||||
n, adr, fil);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static int hts_acceptlink_(httrackp * opt, int ptr,
|
||||
const char *adr, const char *fil, const char *tag,
|
||||
const char *attribute, int *set_prio_to,
|
||||
@@ -765,8 +889,9 @@ static int hts_acceptlink_(httrackp * opt, int ptr,
|
||||
n = force_mirror;
|
||||
}
|
||||
|
||||
/* sanity check - reallocate filters HERE */
|
||||
if ((*_FILTERS_PTR) + 1 >= opt->maxfilter) {
|
||||
/* sanity check - reallocate filters HERE (a host-scope answer emits two)
|
||||
*/
|
||||
if ((*_FILTERS_PTR) + 2 >= opt->maxfilter) {
|
||||
opt->maxfilter += HTS_FILTERSINC;
|
||||
if (filters_init(&_FILTERS, opt->maxfilter, HTS_FILTERSINC) == 0) {
|
||||
printf("PANIC! : Too many filters : >%d [%d]\n", (*_FILTERS_PTR),
|
||||
@@ -780,64 +905,21 @@ static int hts_acceptlink_(httrackp * opt, int ptr,
|
||||
}
|
||||
}
|
||||
// here we have enough room for a new filter if necessary
|
||||
switch (n) {
|
||||
case -1: // sauter tout le reste
|
||||
forbidden_url = 1;
|
||||
opt->wizard = HTS_WIZARD_AUTO; // sauter tout le reste
|
||||
break;
|
||||
case 0: // forbid the same link: adr/fil
|
||||
case 1: // forbid the whole directory and subdirs: adr/path/*
|
||||
case 2: // the whole address: adr/*
|
||||
forbidden_url = 1;
|
||||
break;
|
||||
|
||||
case 3: // ** A FAIRE
|
||||
forbidden_url = 1;
|
||||
/*
|
||||
{
|
||||
int i=strlen(adr)-1;
|
||||
while((adr[i]!='/') && (i>0)) i--;
|
||||
if (i>0) {
|
||||
hts_wizard_apply_verdict(opt, n, adr, fil, &forbidden_url, set_prio_to);
|
||||
|
||||
}
|
||||
|
||||
} */
|
||||
|
||||
break;
|
||||
//
|
||||
case 4: // same link
|
||||
// PAS BESOIN!!
|
||||
/*HT_INSERT_FILTERS0; // insérer en 0
|
||||
strcpybuff(_FILTERS[0],"+");
|
||||
strcatbuff(_FILTERS[0],adr);
|
||||
if (*fil!='/') strcatbuff(_FILTERS[0],"/");
|
||||
strcatbuff(_FILTERS[0],fil); */
|
||||
|
||||
// étant donné le renversement wizard/primary filter (les primary autorisent up/down ET interdisent)
|
||||
// il faut éviter d'un lien isolé effectue un miroir total..
|
||||
|
||||
*set_prio_to = 0 + 1; // niveau de récursion=0 (pas de miroir)
|
||||
|
||||
break;
|
||||
|
||||
case 5: // allow the whole directory and its children, or the domain
|
||||
case 6: // same domain
|
||||
case 7: // allow this directory
|
||||
break;
|
||||
|
||||
case 50: // nothing to do
|
||||
break;
|
||||
} // switch
|
||||
|
||||
/* the pattern half of the answer: a new answer needs both switches */
|
||||
/* the pattern half of the answer */
|
||||
{
|
||||
char BIGSTK pattern[HTS_FILTER_SLOT_SIZE];
|
||||
htsbuff f = htsbuff_array(pattern);
|
||||
int slot;
|
||||
|
||||
hts_wizard_answer_filter(
|
||||
&f, n, adr, fil,
|
||||
(opt->seeker & HTS_SEEKER_UP) != 0 ? HTS_TRUE : HTS_FALSE);
|
||||
if (f.len != 0) {
|
||||
for (slot = 0; slot < HTS_WIZARD_MAX_FILTERS; slot++) {
|
||||
hts_wizard_answer_filter(
|
||||
&f, slot, n, adr, fil,
|
||||
(opt->seeker & HTS_SEEKER_UP) != 0 ? HTS_TRUE : HTS_FALSE);
|
||||
if (f.len == 0)
|
||||
break;
|
||||
HT_INSERT_FILTERS0; // insert at slot 0
|
||||
strlcpybuff(_FILTERS[0], pattern, HTS_FILTER_SLOT_SIZE);
|
||||
}
|
||||
|
||||
@@ -57,12 +57,30 @@ hts_boolean hts_robots_forbids(httrackp *opt, const char *adr, const char *fil,
|
||||
hts_boolean filters_decided,
|
||||
hts_boolean filters_refused);
|
||||
|
||||
/* Builds into `f` the filter answer `n` adds for the link (adr,fil), and leaves
|
||||
`f` empty when the answer adds none. `seeker_up` is the HTS_SEEKER_UP bit of
|
||||
opt->seeker, read by answer 5. */
|
||||
void hts_wizard_answer_filter(htsbuff *f, int n, const char *adr,
|
||||
/* Most filters one wizard answer can add. Slots must stay contiguous: the
|
||||
caller stops at the first empty one. */
|
||||
#define HTS_WIZARD_MAX_FILTERS 2
|
||||
|
||||
/* Builds into `f` the `slot`-th filter answer `n` adds for the link (adr,fil),
|
||||
and leaves `f` empty past the last one. Only the host-scope answers emit a
|
||||
second, because their starred form misses the apex. `seeker_up` is the
|
||||
HTS_SEEKER_UP bit of opt->seeker, read by answer 5. */
|
||||
void hts_wizard_answer_filter(htsbuff *f, int slot, int n, const char *adr,
|
||||
const char *fil, hts_boolean seeker_up);
|
||||
|
||||
/* Which host-scope range answer `n` falls in: HTS_TRUE excludes the scope,
|
||||
HTS_FALSE includes it, HTS_DEFAULT for any answer outside both ranges. */
|
||||
hts_tristate hts_wizard_scope_answer(int n);
|
||||
|
||||
/* Applies the verdict half of answer `n` for the link (adr,fil): refuses it,
|
||||
stops the questions, or bans recursion from it. Each effect is one-way, so an
|
||||
answer deciding none of them leaves *forbidden_url, *set_prio_to and
|
||||
opt->wizard alone; the filter half is hts_wizard_answer_filter(), and a new
|
||||
answer needs both. */
|
||||
void hts_wizard_apply_verdict(httrackp *opt, int n, const char *adr,
|
||||
const char *fil, int *forbidden_url,
|
||||
int *set_prio_to);
|
||||
|
||||
/* A (tag, attribute) pair naming a reference kind. */
|
||||
#ifndef HTS_DEF_DEFSTRUCT_htspair_t
|
||||
#define HTS_DEF_DEFSTRUCT_htspair_t
|
||||
|
||||
@@ -452,6 +452,23 @@ HTSEXT_API char *jump_identification(char *);
|
||||
|
||||
HTSEXT_API const char *jump_identification_const(const char *);
|
||||
|
||||
/** Write into dst the k-th domain scope the wizard can offer for the string
|
||||
query3 was handed (an "adr" or an "adr[/fil]"). Scopes widen as k grows: for
|
||||
download.example.co.uk/x, "download.example.co.uk", then "example.co.uk",
|
||||
then "co.uk". A front end enumerates its menu by looping until this returns
|
||||
HTS_FALSE, and answers HTS_WIZARD_SCOPE_INCLUDE+k or
|
||||
HTS_WIZARD_SCOPE_EXCLUDE+k.
|
||||
|
||||
A bare TLD is never offered, and an IP literal has no scopes, so an empty
|
||||
menu is normal. Protocol and credentials are stripped and the port is kept,
|
||||
so a caller must not split the host itself.
|
||||
|
||||
dst is emptied on every failure, and HTS_URLMAXSIZE bytes always suffice. A
|
||||
shorter dst also returns HTS_FALSE, which the loop cannot tell from the end
|
||||
of the list. */
|
||||
HTSEXT_API hts_boolean hts_wizard_host_scope(const char *question, int k,
|
||||
char *dst, size_t dstsize);
|
||||
|
||||
/** Like jump_identification() and also strip a leading "www." host prefix,
|
||||
returning a pointer into the input to the normalized host. */
|
||||
HTSEXT_API char *jump_normalized(char *);
|
||||
|
||||
@@ -712,6 +712,20 @@ static const char *__cdecl htsshow_query3(t_hts_callbackarg * carg,
|
||||
"5 Mirror this link (useful)\n"
|
||||
"6 Mirror all links located on the same domain as this link\n" "\n",
|
||||
question);
|
||||
/* the domain scopes are host-dependent, so the engine enumerates them */
|
||||
{
|
||||
char scope[HTS_URLMAXSIZE];
|
||||
int k;
|
||||
|
||||
for (k = 0; hts_wizard_host_scope(question, k, scope, sizeof(scope)); k++)
|
||||
printf("%d Mirror %s and every host below it\n",
|
||||
HTS_WIZARD_SCOPE_INCLUDE + k, scope);
|
||||
for (k = 0; hts_wizard_host_scope(question, k, scope, sizeof(scope)); k++)
|
||||
printf("%d Ignore %s and every host below it\n",
|
||||
HTS_WIZARD_SCOPE_EXCLUDE + k, scope);
|
||||
if (k != 0)
|
||||
printf("\n");
|
||||
}
|
||||
do {
|
||||
printf(">> ");
|
||||
io_flush;
|
||||
|
||||
@@ -59,6 +59,18 @@ function launch_browser {
|
||||
log "Browser (or helper) exited"
|
||||
}
|
||||
|
||||
# Wait until the server stops (the interface closed, so this process has nothing
|
||||
# left to represent) or the browser exits, whichever comes first. An old browser
|
||||
# only ever signals by exiting.
|
||||
function wait_for_session {
|
||||
local browserpid=$1 sessionpid=$2
|
||||
while test -n "${sessionpid}${browserpid}"; do
|
||||
test -n "${sessionpid}" && ! kill -0 "${sessionpid}" 2>/dev/null && break
|
||||
test -n "${browserpid}" && ! kill -0 "${browserpid}" 2>/dev/null && break
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# First ensure that we can launch the server
|
||||
BINPATH=
|
||||
for i in "${SRCHPATH[@]}"; do
|
||||
@@ -147,8 +159,14 @@ function cleanup {
|
||||
# Cleanup in case of emergency
|
||||
trap "cleanup now; exit" HUP INT QUIT PIPE TERM
|
||||
|
||||
# Got SRVURL, launch browser
|
||||
launch_browser "${BROWSEREXE}" "${SRVURL}"
|
||||
# Got SRVURL, launch browser. Backgrounded so the wait below can watch the
|
||||
# server too, rather than only the browser.
|
||||
launch_browser "${BROWSEREXE}" "${SRVURL}" &
|
||||
BROWSERPID=$!
|
||||
# Deliberately not SRVPID, which cleanup would then kill, taking a running mirror
|
||||
# with it.
|
||||
SESSIONPID=$(grep -E PID= "${TMPSRVFILE}" | cut -f2- -d=)
|
||||
wait_for_session "${BROWSERPID}" "${SESSIONPID}"
|
||||
|
||||
# That's all, folks!
|
||||
trap "" HUP INT QUIT PIPE TERM
|
||||
|
||||
104
tests/01_engine-footer-boundary.test
Normal file
104
tests/01_engine-footer-boundary.test
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A -%F footer whose expansion exactly filled the on-page buffer aborted the
|
||||
# crawl (SIGABRT): the emitter appends its closing newline with strcatbuff,
|
||||
# which aborts rather than clips. #670 covered the overflow, not the exact fit.
|
||||
|
||||
set -eu
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "$(dirname "$0")/testlib.sh"
|
||||
|
||||
# The emitter's buffer, 1024 + 2 * HTS_URLMAXSIZE. The sweep window is derived
|
||||
# from it; a wrong value makes the crossing check below fail loudly.
|
||||
bufsize=3072
|
||||
# A {path} of this length divides the buffer with room left for the literal
|
||||
# padding that tunes the last few bytes, and keeps the mirror under MAX_PATH.
|
||||
pathlen=120
|
||||
mark=ZFOOTERZ
|
||||
|
||||
dir=$(mktemp -d)
|
||||
cleanup_push rm -rf "$dir"
|
||||
mir="$dir/mir"
|
||||
page=
|
||||
body=
|
||||
|
||||
make_page() { # make_page SEGLEN, leaving an LF page at $page
|
||||
local seg
|
||||
seg=$(printf 'a%.0s' $(seq 1 "$1"))
|
||||
mkdir -p "$dir/$seg"
|
||||
page="$dir/$seg/index.html"
|
||||
write_page '<html><body>hi</body></html>'
|
||||
}
|
||||
|
||||
# The emitter follows the page's own line ending, so the CR here is what picks
|
||||
# "\r\n" and moves the boundary two bytes.
|
||||
write_page() { printf '%s' "$1" >"$page"; }
|
||||
|
||||
crawl() { # crawl FOOTER [LABEL], leaving the mirrored page in $body
|
||||
rm -rf "$mir"
|
||||
httrack "file://$page" -O "$mir" -%F "$1" -q -s0 -%v0 >/dev/null 2>&1 ||
|
||||
fail "crawl died (exit $?) on ${2:-a ${#1}-char footer}"
|
||||
# Under file/, not $mir: the makeindex top index carries no -%F footer.
|
||||
local found
|
||||
found=$(find "$mir/file" -name index.html 2>/dev/null || true)
|
||||
test -n "$found" || fail "page not mirrored; the footer path never ran"
|
||||
body=$(cat "$found")
|
||||
}
|
||||
|
||||
# Which line the footer lands on follows the page's own newlines, so find it by
|
||||
# its marker rather than by position.
|
||||
footer_line() { firstline "$(tr -d '\r' <<<"$body" | grep -F "$mark" || true)"; }
|
||||
|
||||
path_expansion() { # the length {path} expands to
|
||||
local line
|
||||
crawl "${mark}{path}"
|
||||
line=$(footer_line)
|
||||
test -n "$line" || fail "no footer emitted while measuring {path}"
|
||||
printf '%s\n' "$((${#line} - ${#mark}))"
|
||||
}
|
||||
|
||||
# -%F caps at 253 chars, so reaching a ~3 KB expansion takes repeats of the
|
||||
# longest field a file:// crawl offers. Size the directory to make {path}
|
||||
# exactly pathlen, rather than inherit whatever length mktemp handed out.
|
||||
make_page 8
|
||||
fixed=$(($(path_expansion) - 8))
|
||||
# Loudly, not a skip: the Windows leg compares the skip set exactly, so a skip
|
||||
# reds it there anyway and quietly drops the coverage everywhere else.
|
||||
test "$fixed" -lt "$pathlen" ||
|
||||
fail "temp path is ${fixed} chars, leaving no room for a ${pathlen}-char one"
|
||||
make_page $((pathlen - fixed))
|
||||
got=$(path_expansion)
|
||||
test "$got" -eq "$pathlen" || fail "{path} is ${got} chars, wanted ${pathlen}"
|
||||
|
||||
reps=$(((bufsize - 72) / pathlen))
|
||||
|
||||
sweep() { # sweep LABEL: walk the expansion across the point where footers drop
|
||||
local total pad footer line emitted=0 dropped=0
|
||||
for total in $(seq $((bufsize - 6)) "$bufsize"); do
|
||||
pad=$((total - reps * pathlen))
|
||||
test "$pad" -gt ${#mark} || fail "padding is down to ${pad} chars"
|
||||
footer="${mark}$(printf 'A%.0s' $(seq 1 $((pad - ${#mark}))))"
|
||||
footer="${footer}$(printf '{path}%.0s' $(seq 1 "$reps"))"
|
||||
test ${#footer} -lt 254 || fail "footer template is ${#footer} chars"
|
||||
crawl "$footer" "a $1 page at expansion ${total}"
|
||||
if grep -q "$mark" <<<"$body"; then
|
||||
line=$(footer_line)
|
||||
# Whole or not at all: a clipped footer runs into the page below it.
|
||||
test "${#line}" -eq "$total" ||
|
||||
fail "$1: ${total}-char footer emitted as ${#line} chars"
|
||||
emitted=$((emitted + 1))
|
||||
else
|
||||
dropped=$((dropped + 1))
|
||||
fi
|
||||
done
|
||||
# Both outcomes prove the window straddles the drop point, where the abort
|
||||
# was. All-emitted or all-dropped means the window missed it.
|
||||
test "$emitted" -gt 0 || fail "$1: no length in the sweep emitted a footer"
|
||||
test "$dropped" -gt 0 || fail "$1: no length in the sweep was dropped"
|
||||
}
|
||||
|
||||
sweep LF
|
||||
# CRLF costs two bytes at each end, so the boundary sits two lengths lower: a
|
||||
# fix reserving one byte rather than strlen(eol) still aborts here.
|
||||
write_page $'<html>\r\n<body>hi</body></html>'
|
||||
sweep CRLF
|
||||
@@ -36,11 +36,43 @@ echo "marker: the wedged test started"
|
||||
sleep 300
|
||||
EOF
|
||||
|
||||
start=$SECONDS
|
||||
# Time the guard to its DUMP announcement, not to the driver's exit: the dump that
|
||||
# follows runs for minutes on an emulated host, and that is not what is under test.
|
||||
rc=0
|
||||
HTTRACK_PROGRESS_LOG="$tmp/progress" HTTRACK_TEST_TIMEOUT=5 \
|
||||
bash "$driver" "$tmp/90_wedged.test" >"$out" 2>&1 || rc=$?
|
||||
elapsed=$((SECONDS - start))
|
||||
fired=
|
||||
marked=
|
||||
total=
|
||||
run_wedged() { # run_wedged <budget> [VAR=VAL...]
|
||||
local budget=$1 start=$SECONDS pid
|
||||
shift
|
||||
: >"$tmp/progress"
|
||||
rc=0
|
||||
fired=
|
||||
marked=
|
||||
env "$@" HTTRACK_PROGRESS_LOG="$tmp/progress" HTTRACK_TEST_TIMEOUT="$budget" \
|
||||
bash "$driver" "$tmp/90_wedged.test" >"$out" 2>&1 &
|
||||
pid=$!
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
# Read with the shell: a fork per poll would blur the latency being measured.
|
||||
if read -r line <"$tmp/progress" 2>/dev/null && test "$line" = "DUMP 90_wedged.test"; then
|
||||
fired=$((SECONDS - start))
|
||||
marked=1
|
||||
break
|
||||
fi
|
||||
poll_wait 0.1 || sleep 1
|
||||
done
|
||||
wait "$pid" || rc=$?
|
||||
total=$((SECONDS - start))
|
||||
# Missed between two polls: the whole run then bounds the latency from above.
|
||||
test -n "$fired" || fired=$total
|
||||
}
|
||||
|
||||
# Retried once, because a dump short enough to fall between two polls is a race, not a
|
||||
# regression; announcing after the dump loses both attempts.
|
||||
run_wedged 5
|
||||
test -n "$marked" || run_wedged 5
|
||||
test -n "$marked" ||
|
||||
fail "the announcement was never seen while the guard ran, so its latency is unknown"
|
||||
|
||||
# Announced when the dump starts, which runs for minutes: a suite watchdog
|
||||
# reading that log would take the silence for a wedge and kill the step.
|
||||
@@ -49,8 +81,8 @@ grep -qx 'DUMP 90_wedged.test' "$tmp/progress" ||
|
||||
|
||||
test "$rc" -eq 124 || fail "wedged test reported $rc, want 124"
|
||||
# Never before the budget, or a slow-but-healthy test would be killed too.
|
||||
test "$elapsed" -ge 5 || fail "the guard fired early (${elapsed}s of a 5s budget)"
|
||||
test "$elapsed" -lt 30 || fail "the guard fired late (${elapsed}s)"
|
||||
test "$fired" -ge 5 || fail "the guard fired early (${fired}s of a 5s budget)"
|
||||
test "$fired" -lt 30 || fail "the guard fired late (${fired}s)"
|
||||
grep -q 'marker: the wedged test started' "$out" || fail "the test's own output was lost"
|
||||
# The header, not a bare name: the process list quotes the test's path too, so a
|
||||
# wrapper that named nothing would still match that.
|
||||
@@ -58,6 +90,20 @@ grep -q '^===== TIMEOUT: 90_wedged.test exceeded' "$out" ||
|
||||
fail "the diagnostics do not name the test"
|
||||
grep -q "own process tree" "$out" || fail "no process list in the diagnostics"
|
||||
|
||||
# Announced BEFORE the dump, not merely at some point during it: the watchdog reading
|
||||
# that log takes the silence of a dump for a wedge. Only a slow dump tells the two
|
||||
# orderings apart, and the dump's own ps is what this makes slow.
|
||||
if ! is_windows; then
|
||||
printf '#!/bin/sh\nsleep 3\nexec %s "$@"\n' "$(command -v ps)" >"$shim/ps"
|
||||
chmod +x "$shim/ps"
|
||||
run_wedged 5 "PATH=$shim:$PATH"
|
||||
rm -f "$shim/ps" # before the starve shim shares this directory
|
||||
test "$rc" -eq 124 || fail "the guard reported $rc under a slow dump, want 124"
|
||||
test -n "$marked" || fail "no announcement under a slow dump"
|
||||
test "$((total - fired))" -ge 2 ||
|
||||
fail "announced with the dump (${fired}s of ${total}s), not before it"
|
||||
fi
|
||||
|
||||
# The budget is read, not hard-coded: well under it, the same shape survives.
|
||||
printf 'sleep 3\necho "slow but healthy"\n' >"$tmp/92_slow.test"
|
||||
rc=0
|
||||
@@ -78,15 +124,11 @@ sleep 1
|
||||
# the loop it stretches is the same one poll_wait's fd tick drives.
|
||||
(
|
||||
starve_sleep "$shim" 4 || fail "could not install the slow sleep"
|
||||
start=$SECONDS
|
||||
rc=0
|
||||
HTTRACK_POLL_SLEEP=1 HTTRACK_TEST_TIMEOUT=1 \
|
||||
bash "$driver" "$tmp/90_wedged.test" >"$out" 2>&1 || rc=$?
|
||||
elapsed=$((SECONDS - start))
|
||||
run_wedged 1 HTTRACK_POLL_SLEEP=1
|
||||
test "$rc" -eq 124 || fail "starved guard reported $rc, want 124"
|
||||
# Generous: the diagnostics dump runs inside this window too.
|
||||
test "$elapsed" -lt 25 ||
|
||||
fail "budget counted polls, not seconds: ${elapsed}s for a 1s budget"
|
||||
# 10 stretched polls would be 40s; a handful of them is the whole margin here.
|
||||
test "$fired" -lt 25 ||
|
||||
fail "budget counted polls, not seconds: ${fired}s for a 1s budget"
|
||||
)
|
||||
|
||||
# --- exit status and output of a healthy test pass straight through ----------
|
||||
@@ -127,6 +169,84 @@ saw_budget 45 "an explicit budget"
|
||||
HTTRACK_TEST_TIMEOUT=0 bash "$driver" "$tmp/95_budget.test" >"$out" 2>&1
|
||||
saw_budget 0 "a disabled guard"
|
||||
|
||||
# --- a test may raise the budget, never lower it ----------------------------
|
||||
# 269's header sweep is n^2 compiles, real work that outlasts the wedge budget on an
|
||||
# emulated host; anything else asking would be disarming the guard.
|
||||
raiser() { # raiser <asked for>
|
||||
# shellcheck disable=SC2016 # the fixture has to read the variable, not us
|
||||
printf '# TEST_TIMEOUT_AT_LEAST: %s\necho "budget=${HTTRACK_TEST_TIMEOUT-unset}"\n' \
|
||||
"$1" >"$tmp/97_raise.test"
|
||||
}
|
||||
# 0900 must not read as octal, and a value past intmax must not reach `test`, which
|
||||
# errors on it rather than comparing and would leave the guard unarmed.
|
||||
for want in 900:900 5:600 garbage:600 0900:900 99999999999999999999:600 ' 900':600; do
|
||||
raiser "${want%%:*}"
|
||||
HTTRACK_TEST_TIMEOUT=600 bash "$driver" "$tmp/97_raise.test" >"$out" 2>&1
|
||||
saw_budget "${want##*:}" "a test asking for '${want%%:*}'"
|
||||
done
|
||||
raiser 900
|
||||
HTTRACK_TEST_TIMEOUT=0 bash "$driver" "$tmp/97_raise.test" >"$out" 2>&1
|
||||
saw_budget 0 "a raise under a disabled guard"
|
||||
# Read from the header only, or a test's own data would be one: 151 writes this line.
|
||||
window() { # window <lines of padding> <budget wanted>
|
||||
{
|
||||
i=0
|
||||
while test "$i" -lt "$1"; do
|
||||
i=$((i + 1))
|
||||
echo "# pad $i"
|
||||
done
|
||||
cat "$tmp/97_raise.test"
|
||||
} >"$tmp/97_deep.test"
|
||||
HTTRACK_TEST_TIMEOUT=600 bash "$driver" "$tmp/97_deep.test" >"$out" 2>&1
|
||||
saw_budget "$2" "a raise on line $(($1 + 1))"
|
||||
}
|
||||
raiser 900
|
||||
window 39 900 # the last line the header reaches
|
||||
window 40 600
|
||||
|
||||
# Enforced, not merely exported: the number the guard uses is the one it kills on.
|
||||
printf '# TEST_TIMEOUT_AT_LEAST: 900\nsleep 4\necho "outlived the default"\n' \
|
||||
>"$tmp/97_raise.test"
|
||||
rc=0
|
||||
HTTRACK_TEST_TIMEOUT=2 bash "$driver" "$tmp/97_raise.test" >"$out" 2>&1 || rc=$?
|
||||
test "$rc" -eq 0 || fail "a 4s test that raised the budget to 900 reported $rc"
|
||||
grep -q 'outlived the default' "$out" || fail "the raised budget killed the test anyway"
|
||||
printf '# TEST_TIMEOUT_AT_LEAST: 1\nsleep 3\necho "not shrunk"\n' >"$tmp/97_raise.test"
|
||||
rc=0
|
||||
HTTRACK_TEST_TIMEOUT=600 bash "$driver" "$tmp/97_raise.test" >"$out" 2>&1 || rc=$?
|
||||
test "$rc" -eq 0 || fail "a 3s test asking for a 1s budget reported $rc"
|
||||
grep -q 'not shrunk' "$out" || fail "the header shrank the budget and killed the test"
|
||||
|
||||
# --- one budget parser, and what is left of it ------------------------------
|
||||
secs() { # secs <value in the environment> <seconds it must read as>
|
||||
local got
|
||||
got=$(HTTRACK_TEST_TIMEOUT=$1 bash -c '. "$1"; budget_secs' _ "${testdir}/testlib.sh" 2>&1)
|
||||
test "$got" = "$2" || fail "budget_secs read '$1' as '$got', want $2"
|
||||
}
|
||||
secs 45 45
|
||||
secs 0900 900 # decimal, or $((...)) and test disagree on the same string
|
||||
secs garbage 600
|
||||
secs 99999999999999999999 600 # past intmax, where test errors instead of comparing
|
||||
secs 0 0
|
||||
|
||||
# budget_left hands a child what is left of it, and keeps 0 meaning "no guard".
|
||||
# shellcheck disable=SC2016 # the fixture has to call the helper, not us
|
||||
printf '. "%s"\nsleep 2\necho "left=$(budget_left) at=$SECONDS"\n' "${testdir}/testlib.sh" \
|
||||
>"$tmp/98_left.test"
|
||||
# Exact, against the clock the child itself read: a tolerance would pass a wrong epoch.
|
||||
HTTRACK_TEST_TIMEOUT=60 bash "$driver" "$tmp/98_left.test" >"$out" 2>&1
|
||||
got=$(sed -n 's/^left=\([0-9][0-9]*\) .*/\1/p' "$out")
|
||||
at=$(sed -n 's/^left=[0-9][0-9]* at=\([0-9][0-9]*\)$/\1/p' "$out")
|
||||
case "$got$at" in '' | *[!0-9]*) fail "a 60s budget printed '$(cat "$out")'" ;; esac
|
||||
test "$got" -eq "$((60 - at))" ||
|
||||
fail "a 60s budget left $got with $at gone, want $((60 - at))"
|
||||
HTTRACK_TEST_TIMEOUT=0 bash "$driver" "$tmp/98_left.test" >"$out" 2>&1
|
||||
grep -q '^left=0 ' "$out" || fail "a disabled guard left '$(cat "$out")', want 0"
|
||||
# Never 0 on an exhausted budget: a child would read that as the guard being off.
|
||||
HTTRACK_TEST_TIMEOUT=1 bash -c '. "$1"; sleep 2; echo "left=$(budget_left)"' \
|
||||
_ "${testdir}/testlib.sh" >"$out" 2>&1
|
||||
grep -qx 'left=1' "$out" || fail "an exhausted budget left '$(cat "$out")', want 1"
|
||||
|
||||
# --- a test too slow to finish skips instead of being killed ----------------
|
||||
# hppa spends ~150s on one configure run, and 124 takes the build down where 77
|
||||
# does not.
|
||||
|
||||
@@ -8,6 +8,8 @@ set -euo pipefail
|
||||
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "$(dirname "$0")/testlib.sh"
|
||||
# shellcheck source=tests/proclib.sh
|
||||
. "$(dirname "$0")/proclib.sh"
|
||||
|
||||
sh=${BASH_SHELL:-}
|
||||
test -n "$sh" || {
|
||||
@@ -51,11 +53,21 @@ chmod 755 "$tmp/fakebin/bash"
|
||||
mkfifo "$tmp/fifo"
|
||||
chmod 755 "$tmp/fifo"
|
||||
|
||||
# Sampled rather than polled per second: the size read is a fork, and an emulated
|
||||
# host pays for it. SILENCE clears the slowest single configure probe there.
|
||||
SAMPLE=5
|
||||
SILENCE=${HTTRACK_CONFIGURE_SILENCE:-120}
|
||||
RESERVE=15 # what killing the run and skipping still needs of the budget
|
||||
|
||||
n=0
|
||||
cases=16 # reject/accept calls below; pinned again once they have all run
|
||||
status=0
|
||||
log=
|
||||
rundir=
|
||||
took=0
|
||||
# What run() launches, so the checks below can hand it a child that hangs or one that
|
||||
# only crawls; nothing else may override it.
|
||||
configure_cmd=(bash "$tmp/src/configure" --disable-https)
|
||||
run() { # run <label> <env argument>...
|
||||
local label=$1 began=$SECONDS
|
||||
shift
|
||||
@@ -65,23 +77,56 @@ run() { # run <label> <env argument>...
|
||||
status=0
|
||||
# Capped: configure executes the candidate, and a hang wedges "make check" with no output
|
||||
# at all. Polled, not a backgrounded "sleep" watchdog, which outlives the run it guards.
|
||||
(cd "$rundir" && env "$@" bash "$tmp/src/configure" --disable-https) \
|
||||
local had_m=
|
||||
case "$-" in *m*) had_m=1 ;; esac
|
||||
# Own process group, so the kills below reach what configure spawned: bash 3.2 keeps
|
||||
# the subshell it runs in, and killing that alone leaves the child running (macOS).
|
||||
set -m
|
||||
(cd "$rundir" && env "$@" "${configure_cmd[@]}") \
|
||||
>"$rundir/log" 2>&1 &
|
||||
local pid=$! waited=0
|
||||
while test "$waited" -lt 300 && kill -0 "$pid" 2>/dev/null; do
|
||||
local pid=$! waited=0 quiet=0 size=0 now left
|
||||
test -n "$had_m" || set +m
|
||||
# A hang is silence, not slowness: configure writes a line per probe,
|
||||
# but hppa's emulated run can take longer overall than a runner's whole budget (#1146).
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
test "$((waited % SAMPLE))" -eq 0 || continue
|
||||
now=$(wc -c <"$rundir/log")
|
||||
if test "$now" -gt "$size"; then
|
||||
size=$now
|
||||
quiet=0
|
||||
else
|
||||
quiet=$((quiet + SAMPLE))
|
||||
fi
|
||||
test "$quiet" -lt "$SILENCE" || {
|
||||
kill_tree "$pid"
|
||||
echo "configure wrote nothing for ${quiet}s of ${waited}s for $label" >&2
|
||||
tail -5 "$rundir/log" >&2
|
||||
exit 1
|
||||
}
|
||||
# Still writing but out of time: skip, where the harness would kill the whole test
|
||||
# and take the build down with it. Only while writing, and only with a full silence
|
||||
# window still affordable, or a hang would reach this before the check above fires
|
||||
# and a wedge would report a skip. 0 is the guard off.
|
||||
left=$(budget_left)
|
||||
if test "$quiet" -eq 0 && test "$left" -ne 0 &&
|
||||
test "$left" -le "$((RESERVE + SILENCE))"; then
|
||||
kill_tree "$pid"
|
||||
echo "$label was still configuring ${waited}s in and the budget is out; skipping" >&2
|
||||
exit 77
|
||||
fi
|
||||
done
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
kill -9 "$pid" 2>/dev/null
|
||||
echo "configure did not return within ${waited}s for $label" >&2
|
||||
tail -5 "$rundir/log" >&2
|
||||
exit 1
|
||||
fi
|
||||
wait "$pid" || status=$?
|
||||
log=$(cat "$rundir/log")
|
||||
took=$((SECONDS - began))
|
||||
echo "run $n ($label): exit $status"
|
||||
skip_if_out_of_budget "$((cases - n))" "$((SECONDS - began))"
|
||||
}
|
||||
|
||||
# Pace here rather than in run(), which returns with the answer still unjudged: a
|
||||
# skip between the two would bury a configure that answered wrongly.
|
||||
paced() {
|
||||
skip_if_out_of_budget "$((cases - n))" "$took"
|
||||
}
|
||||
|
||||
reject() { # reject <label> <expected message> <env argument>...
|
||||
@@ -97,6 +142,7 @@ reject() { # reject <label> <expected message> <env argument>...
|
||||
tail -5 <<<"$log" >&2
|
||||
exit 1
|
||||
}
|
||||
paced
|
||||
}
|
||||
|
||||
# accept <label> <expected $(BASH_SHELL), "" for any> <expected message, "" for none> <env argument>...
|
||||
@@ -125,8 +171,69 @@ accept() {
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
paced
|
||||
}
|
||||
|
||||
# --- what run() does to a child that hangs, and to one that is merely slow -------
|
||||
# Driven through configure_cmd, since the real configure can do neither on demand.
|
||||
probe() { # probe <run number> <seconds of budget left> <command>...
|
||||
local want_n=$1 left=$2 rc=0
|
||||
shift 2
|
||||
(
|
||||
# shellcheck disable=SC2030 # the isolation is the point: the real count is next door
|
||||
n=$want_n SAMPLE=1 SILENCE=2
|
||||
# shellcheck disable=SC2030,SC2031 # likewise: the budget here is the probe's own
|
||||
export HTTRACK_TEST_TIMEOUT=$((SECONDS + left))
|
||||
configure_cmd=("$@")
|
||||
run probe
|
||||
) >"$tmp/probe.log" 2>&1 || rc=$?
|
||||
echo "$rc"
|
||||
}
|
||||
# A wedge must fail even with the budget gone, or #922 comes back as a skip.
|
||||
rc=$(probe 90 6 sleep 999)
|
||||
test "$rc" -eq 1 || fail "a silent configure with 6s of budget reported $rc, want 1"
|
||||
grep -q 'wrote nothing' "$tmp/probe.log" || fail "the hang was not named: $(cat "$tmp/probe.log")"
|
||||
# Slow but talking is the emulated buildd, and a skip there beats the harness kill.
|
||||
rc=$(probe 91 6 bash -c 'while :; do echo tick; sleep 1; done')
|
||||
test "$rc" -eq 77 || fail "a slow but writing configure with 6s of budget reported $rc, want 77"
|
||||
# The kill has to reach what configure spawned. bash 3.2 keeps the subshell around the
|
||||
# child, so killing that alone leaves a live configure behind: it outlives "make check"
|
||||
# and holds the CI step open to its own timeout, with the suite reporting no failure.
|
||||
rc=$(probe 92 6 bash -c 'sleep 987 & wait')
|
||||
test "$rc" -eq 1 || fail "a silent configure with a child of its own reported $rc, want 1"
|
||||
sleep 1
|
||||
! ps_snapshot | grep -q '[s]leep 987' || fail "the killed run left its child running"
|
||||
# The pacer must not fire before the case is judged: run() returns with the verdict
|
||||
# still unread, and a skip there would bury a configure that answered wrongly. Through
|
||||
# the real run(), since a stub cannot see a pacer left inside the one it replaced.
|
||||
verdict() { # verdict <accept|reject> <run number> <status the child exits with>
|
||||
local rc=0
|
||||
(
|
||||
# shellcheck disable=SC2030,SC2031 # the isolation is the point: the real run is next door
|
||||
# Cases still to come, or the pacer this is looking for would decline to fire.
|
||||
n=$2 cases=$(($2 + 5)) SAMPLE=1
|
||||
# Spent by the time the run ends, so a pacer anywhere after it would fire.
|
||||
# shellcheck disable=SC2030,SC2031 # likewise: the budget here is the probe's own
|
||||
export HTTRACK_TEST_TIMEOUT=$((SECONDS + 4))
|
||||
configure_cmd=(bash -c "sleep 3; exit $3")
|
||||
# Their arities differ, and an extra argument would reach run() as an env
|
||||
# assignment: the child would then fail to exec and answer the wrong question.
|
||||
case "$1" in
|
||||
accept) accept probe-verdict '' '' ;;
|
||||
*) reject probe-verdict '' ;;
|
||||
esac
|
||||
) >/dev/null 2>&1 || rc=$?
|
||||
test "$rc" -eq 1 || fail "$1 of a wrong answer with the budget spent reported $rc, want 1"
|
||||
}
|
||||
verdict accept 80 1 # configure rejected what it must accept
|
||||
verdict reject 81 0 # configure accepted what it must reject
|
||||
# The probes ran in subshells, so the real cases below start from a clean count.
|
||||
cases=16
|
||||
n=0
|
||||
status=0
|
||||
log=
|
||||
took=0
|
||||
|
||||
# The four that configure to completion run first. A reject stops at the
|
||||
# BASH_SHELL check and costs a fraction of one, and the pacer projects the step it
|
||||
# just timed: behind the cheap ones it read far too low and 151 met the harness
|
||||
|
||||
@@ -188,20 +188,29 @@ rc=0
|
||||
kill_pid() { echo "DIRECT $1" >>"$rec"; }
|
||||
# shellcheck disable=SC2317
|
||||
kill_tree() {
|
||||
echo "TREE $1" >>"$rec"
|
||||
echo "TREE $*" >>"$rec"
|
||||
exit 9
|
||||
}
|
||||
# The suite's own pid has no /proc entry here, so the capture answers what a
|
||||
# POSIX box answers: empty, and the kill goes on unguarded.
|
||||
# shellcheck disable=SC2317
|
||||
win_capture() {
|
||||
echo "CAPTURE $1" >>"$rec"
|
||||
WIN_PID=4242 WIN_IMAGE=bash.exe
|
||||
}
|
||||
hb_depth=$BASH_SUBSHELL
|
||||
ci_suite_heartbeat 960 360 "$progress" 900 4242 >"$tmp/hedge" 2>&1
|
||||
) || rc=$?
|
||||
test "$rc" -eq 9 || fail "the tree kill never fired: watchdog returned $rc"
|
||||
test "$(sed -n 1p "$rec")" = "DIRECT 777" ||
|
||||
fail "the reporter was not killed ahead of the suite: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n 2p "$rec")" = "DIRECT 4242" ||
|
||||
test "$(sed -n 2p "$rec")" = "CAPTURE 4242" ||
|
||||
fail "the winpid was not read before the target was signalled: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n 3p "$rec")" = "DIRECT 4242" ||
|
||||
fail "the target was not signalled directly ahead of the tree walk: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n 3p "$rec")" = "TREE 4242" ||
|
||||
fail "the tree was not killed after the direct signal: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n '$=' "$rec")" -eq 3 || fail "extra kills: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n 4p "$rec")" = "TREE 4242 4242 bash.exe" ||
|
||||
fail "the tree kill did not carry what was captured: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n '$=' "$rec")" -eq 4 || fail "extra kills: $(tr '\n' '/' <"$rec")"
|
||||
|
||||
test ! -e "$tmp/forked" || fail "the clock was read through a subshell, a fork a starved box cannot spare"
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@ cleanup_push rm -rf "$tmp"
|
||||
"$nm" --defined-only "$lib" >"$tmp/all.raw" 2>/dev/null ||
|
||||
skip "$nm cannot read the symbol table of $lib"
|
||||
awk 'NF >= 3 { print $3 }' "$tmp/exported.raw" | sort -u >"$tmp/exported"
|
||||
awk 'NF >= 3 { print $3 }' "$tmp/all.raw" | sort -u >"$tmp/all"
|
||||
# LTO and the optimizer's clones rename a local to "name.lto_priv.0", which
|
||||
# stops matching the header identifier and empties the candidate list.
|
||||
clone_alt='lto_priv|constprop|isra|part|cold|llvm'
|
||||
strip_clones="s/(\\.($clone_alt)(\\.[0-9]+)*)+\$//"
|
||||
awk 'NF >= 3 { print $3 }' "$tmp/all.raw" | sed -E "$strip_clones" | sort -u >"$tmp/all"
|
||||
comm -23 "$tmp/all" "$tmp/exported" >"$tmp/hidden"
|
||||
n_exported=$(wc -l <"$tmp/exported")
|
||||
n_hidden=$(wc -l <"$tmp/hidden")
|
||||
@@ -89,7 +93,26 @@ env MAKEFLAGS= MFLAGS= "$make" -C "$abs_top_builddir/src" install-DevIncludesDAT
|
||||
# A synthetic leak the loop must report: every real candidate is a static defined
|
||||
# in the header, which links from the probe's own copy and so proves nothing.
|
||||
canary=zzz-canary.h
|
||||
printf 'extern void %s(void);\n' "$neg" >"$tmp/include/httrack/$canary"
|
||||
canaries=("$neg")
|
||||
|
||||
# One name per clone suffix, taken from the raw table: a strip that drops a
|
||||
# suffix fails here instead of quietly shedding the candidates carrying it.
|
||||
# Listed again on purpose; sharing $clone_alt would let one edit disarm both.
|
||||
awk 'NF >= 3 && $3 !~ /\./ { print $3 }' "$tmp/all.raw" | sort -u >"$tmp/plain"
|
||||
for suffix in lto_priv constprop isra part cold llvm; do
|
||||
awk -v s="$suffix" 'NF >= 3 && $3 ~ "\\." s "([.$]|$)" { print $3 }' "$tmp/all.raw" |
|
||||
sed -E 's/(\.[A-Za-z_][A-Za-z0-9_]*(\.[0-9]+)*)+$//' | sort -u >"$tmp/cloned"
|
||||
sym=$(comm -23 <(comm -23 "$tmp/cloned" "$tmp/plain") "$tmp/exported" |
|
||||
awk '/^[A-Za-z_][A-Za-z0-9_]*$/ { print; exit }')
|
||||
if [ -n "$sym" ]; then
|
||||
canaries+=("$sym")
|
||||
fi
|
||||
done
|
||||
mapfile -t canaries < <(printf '%s\n' "${canaries[@]}" | sort -u)
|
||||
|
||||
for sym in "${canaries[@]}"; do
|
||||
printf 'extern void %s(void);\n' "$sym"
|
||||
done >"$tmp/include/httrack/$canary"
|
||||
|
||||
headers=("$tmp/include/httrack"/*.h)
|
||||
[ "${#headers[@]}" -ge 10 ] || fail "only ${#headers[@]} headers installed, the list cannot be right"
|
||||
@@ -113,7 +136,7 @@ ours {
|
||||
EOF
|
||||
|
||||
probed=0
|
||||
leaks=""
|
||||
leaks=()
|
||||
for h in "${headers[@]}"; do
|
||||
b=$(basename "$h")
|
||||
# config.h first, as a consumer must: it is what turns HTS_USEOPENSSL on, and
|
||||
@@ -136,16 +159,26 @@ for h in "${headers[@]}"; do
|
||||
"$tmp/probe.c" 2>/dev/null || continue
|
||||
probed=$((probed + 1))
|
||||
"${cc_argv[@]}" -w -o "$tmp/probe" "$tmp/probe.o" "$lib" 2>/dev/null ||
|
||||
leaks="$leaks $b:$sym"
|
||||
leaks+=("$b:$sym")
|
||||
done < <(comm -12 "$tmp/hidden" "$tmp/ids")
|
||||
done
|
||||
|
||||
echo "linked $probed reachable symbol(s) from ${#headers[@]} installed headers" \
|
||||
"($n_hidden hidden, $n_exported exported)"
|
||||
[ "$probed" -ge 5 ] || fail "only $probed symbols reached the link probe, the candidate list is broken"
|
||||
[ "${leaks#* "$canary":"$neg"}" != "$leaks" ] ||
|
||||
fail "the synthetic $canary:$neg leak went unreported, the candidate list is broken"
|
||||
leaks=${leaks/ "$canary":"$neg"/}
|
||||
[ -z "$leaks" ] || fail "installed headers declare symbols $lib does not export:$leaks"
|
||||
for sym in "${canaries[@]}"; do
|
||||
case " ${leaks[*]} " in
|
||||
*" $canary:$sym "*) ;;
|
||||
*) fail "the synthetic $canary:$sym leak went unreported, the candidate list is broken" ;;
|
||||
esac
|
||||
done
|
||||
real=()
|
||||
for leak in "${leaks[@]}"; do
|
||||
case $leak in
|
||||
"$canary":*) ;;
|
||||
*) real+=("$leak") ;;
|
||||
esac
|
||||
done
|
||||
[ "${#real[@]}" -eq 0 ] || fail "installed headers declare symbols $lib does not export: ${real[*]}"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -214,8 +214,9 @@ def perms_of(wf, job):
|
||||
WANT_ENV = {
|
||||
"WATCHDOG_TOKEN": "${{ secrets.GITHUB_TOKEN }}",
|
||||
"WATCHDOG_REPO": "${{ github.repository }}",
|
||||
# The merge commit, which no PR checks UI reads.
|
||||
"WATCHDOG_SHA": "${{ github.sha }}",
|
||||
# The PR head: statuses on the merge commit are GC'd, and they are the only
|
||||
# trace a lost runner leaves (#1228).
|
||||
"WATCHDOG_SHA": "${{ github.event.pull_request.head.sha || github.sha }}",
|
||||
}
|
||||
|
||||
def audit(wf):
|
||||
@@ -264,7 +265,7 @@ def mutate(wf, kind):
|
||||
elif kind == "token":
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_TOKEN"] = "${{ secrets.WATCHDOG_PAT }}"
|
||||
elif kind == "sha":
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_SHA"] = "${{ github.event.pull_request.head.sha }}"
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_SHA"] = "${{ github.sha }}"
|
||||
elif kind == "context":
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_CONTEXT"] = "windows-suite"
|
||||
elif kind == "url":
|
||||
@@ -555,6 +556,12 @@ backoff_leg() {
|
||||
echo "$calls calls and $lines log lines against an API rejecting every one: nothing backs off"
|
||||
return 1
|
||||
fi
|
||||
# Every attempt here is refused, so each one after the first must say how many
|
||||
# went missing: x= is what separates a stopped box from a network that healed.
|
||||
grep -q ' x=[1-9]' "$posts" || {
|
||||
echo "no posted status counted the failures before it: $(cat "$posts")"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
fullstdout_leg() {
|
||||
@@ -609,9 +616,18 @@ for psrun in "${psruns[@]}"; do
|
||||
# shellcheck disable=SC2016
|
||||
mutate tail-always-ok 's/\$r = @{ Ok = \$false;/$r = @{ Ok = $true;/' 'reads as one that was read'
|
||||
mutate counters-empty "s/return (\$f -join ' ')/return ''/" 'counters are not key=value'
|
||||
# shellcheck disable=SC2016
|
||||
mutate lag-not-measured "s/'l={0}' -f \$LagMs/'l={0}' -f 0/" 'loop lag is not what the caller measured'
|
||||
# shellcheck disable=SC2016
|
||||
mutate failed-not-reported "s/'x={0}' -f \$Failed/'x={0}' -f 0/" 'failed-post count is not what the caller'
|
||||
# shellcheck disable=SC2016
|
||||
mutate tcp-total-not-delta 's/(\$Cur\[0\] - \$Prev\[0\])/($Cur[0])/' 'total was reported where the delta'
|
||||
# shellcheck disable=SC2016
|
||||
mutate lag-not-a-peak 's/if (\$lag -gt \$Peak) { return \$lag }/if ($false) { return $lag }/' \
|
||||
'overshot by 200ms'
|
||||
# The production cadence: no leg below runs without a schedule of its own.
|
||||
# shellcheck disable=SC2016
|
||||
mutate default-cadence 's/\[int\]\$IntervalSeconds = 30/[int]$IntervalSeconds = 3000/' \
|
||||
mutate default-cadence 's/\[int\]\$IntervalSeconds = 15/[int]$IntervalSeconds = 1500/' \
|
||||
'default status cadence'
|
||||
# shellcheck disable=SC2016
|
||||
mutate default-poll 's/\[int\]\$PollSeconds = 5/[int]$PollSeconds = 50/' 'default poll'
|
||||
@@ -714,6 +730,10 @@ for psrun in "${psruns[@]}"; do
|
||||
throttle 'a landed post throttles the next'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg backoff-never-skips 's/if (\$skip -gt 0)/if ($false)/' backoff 'nothing backs off'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg failures-not-counted \
|
||||
's/if (\$ok) { \$failed = 0; \$lagMax = 0 } else { \$failed++ }/$failed = 0/' \
|
||||
backoff 'counted the failures before it'
|
||||
if test "$devfull" -eq 1; then
|
||||
mutate_leg log-write-fatal 's/try { \(Write-Host .*\) } catch { }/\1/' \
|
||||
fullstdout 'took the loop with it'
|
||||
|
||||
@@ -72,6 +72,45 @@ kill_tree 99
|
||||
test "$(cat "$tmp/killed")" = '/F /T /PID 4242' || fail "kill_tree with a winpid ran: $(cat "$tmp/killed")"
|
||||
win_pid() { :; }
|
||||
|
||||
# The image read while the target was alive: a freed winpid can already be a
|
||||
# stranger's, and /T would take its children too (#1228).
|
||||
: >"$tmp/killed"
|
||||
kill_tree 99 4242 PROXYTRACK.EXE
|
||||
test "$(cat "$tmp/killed")" = '/F /T /PID 4242' ||
|
||||
fail "a verified tree kill did not run: $(cat "$tmp/killed")"
|
||||
: >"$tmp/killed"
|
||||
out=$(kill_tree 99 4242 python.exe)
|
||||
test ! -s "$tmp/killed" || fail "pid 4242 was killed as a python.exe: $(cat "$tmp/killed")"
|
||||
grep -q '::warning::pid 4242 no longer runs python.exe' <<<"$out" ||
|
||||
fail "the skipped kill was not reported: $out"
|
||||
# Gone from the table entirely, which is what a freed winpid usually looks like.
|
||||
: >"$tmp/killed"
|
||||
kill_tree 99 4343 proxytrack.exe >/dev/null
|
||||
test ! -s "$tmp/killed" || fail "a pid tasklist does not list was killed: $(cat "$tmp/killed")"
|
||||
# A stranger's pid leaves the caller with no target, so the serial runner's last
|
||||
# resort still applies: skipping it too would leave the engines running.
|
||||
: >"$tmp/killed"
|
||||
HTTRACK_EXCLUSIVE_HOST=1 kill_tree 99 4242 python.exe >/dev/null
|
||||
got=$(sort "$tmp/killed")
|
||||
test "$got" = "$want" || fail "an unverified pid skipped the last-resort sweep: $got"
|
||||
|
||||
# Graded on the order, since only a winpid read while the target lived names it.
|
||||
: >"$tmp/killed"
|
||||
: >"$tmp/order"
|
||||
kill() { echo "kill $*" >>"$tmp/order"; }
|
||||
win_capture() {
|
||||
echo "capture $*" >>"$tmp/order"
|
||||
WIN_PID=4242 WIN_IMAGE=proxytrack.exe
|
||||
}
|
||||
# The pid is fictional, and reap_bounded polls it through the kill stub above.
|
||||
reap_bounded() { :; }
|
||||
stop_server 99
|
||||
got=$(tr '\n' ' ' <"$tmp/order")
|
||||
test "$got" = 'capture 99 kill 99 ' || fail "stop_server did not capture before signalling: $got"
|
||||
test "$(cat "$tmp/killed")" = '/F /T /PID 4242' ||
|
||||
fail "stop_server did not tree-kill what it captured: $(cat "$tmp/killed")"
|
||||
unset -f kill win_capture reap_bounded
|
||||
|
||||
: >"$tmp/killed"
|
||||
out=$(reap_leftover_processes 99_probe.test)
|
||||
grep -q '99_probe.test left processes behind' <<<"$out" || fail "the leak was not attributed: $out"
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
# a break needing three headers, or a macro the consumer defined first, is out
|
||||
# of reach here. The sweep is shared with the MSVC job, which has no automake to
|
||||
# install with and so stages the same list out of DevIncludes_DATA (#1153).
|
||||
#
|
||||
# n^2 compiles is real work, not a wedge: emulated, it needs more than the suite's
|
||||
# default budget, and the sweep paces itself against whatever is left of this one.
|
||||
# TEST_TIMEOUT_AT_LEAST: 900
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -69,7 +73,10 @@ done
|
||||
|
||||
sweep_argv=(--headers-dir "$tmp/include/httrack" --cc "${CC:-cc}" --cxx "$cxx")
|
||||
[ "${#cpp_argv[@]}" -eq 0 ] || sweep_argv+=(-- "${cpp_argv[@]}")
|
||||
bash "$testdir/install-headers-sweep.sh" "${sweep_argv[@]}" ||
|
||||
fail "installed headers do not survive every include order"
|
||||
rc=0
|
||||
bash "$testdir/install-headers-sweep.sh" --budget "$(budget_left)" "${sweep_argv[@]}" || rc=$?
|
||||
# 77 is the sweep giving up on a host too slow to finish it, not a broken header.
|
||||
[ "$rc" -ne 77 ] || exit 77
|
||||
[ "$rc" -eq 0 ] || fail "installed headers do not survive every include order"
|
||||
|
||||
exit 0
|
||||
|
||||
243
tests/289_webhttrack-lifetime.test
Normal file
243
tests/289_webhttrack-lifetime.test
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# An abandoned htsserver has to stop on its own: it holds its payload open, and
|
||||
# on macOS that payload is a disk image the user then cannot eject.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=tests/webhttracklib.sh
|
||||
. "$(dirname "$0")/webhttracklib.sh"
|
||||
|
||||
htsserver_require
|
||||
|
||||
# Short enough to sit in the suite, long enough that a loaded parallel run
|
||||
# cannot starve a ping past it. The server derives the leave grace from it.
|
||||
timeout=10
|
||||
grace=2
|
||||
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/webhttrack_life.XXXXXX") || fail "no tmpdir"
|
||||
pinger=
|
||||
csrv=
|
||||
sleeper=
|
||||
cleanup() {
|
||||
htsserver_cleanup
|
||||
for p in "${pinger}" "${csrv}" "${sleeper}"; do
|
||||
test -z "${p}" || kill -9 "${p}" 2>/dev/null || true
|
||||
done
|
||||
wait "${pinger}" "${csrv}" "${sleeper}" 2>/dev/null || true
|
||||
rm -rf "${work}"
|
||||
}
|
||||
cleanup_push cleanup
|
||||
|
||||
export HOME="${work}"
|
||||
|
||||
# Each case owns a server, so save its handle: the library keeps only the last.
|
||||
# $1, when given, is the pid to name as the launcher instead of this shell.
|
||||
start_case() {
|
||||
htsserver_start --home "${work}" -- \
|
||||
--ppid "${1:-$$}" --ping-timeout "${timeout}"
|
||||
test -n "${HTS_PID}" || skip "this platform announces no server pid"
|
||||
}
|
||||
|
||||
get() { "${HTS_PYTHON}" "${testdir}/httpclient.py" --port "$1" --path "$2"; }
|
||||
ping() { get "$1" "/ping?w=$2&t=${RANDOM}${3:+&$3}" >/dev/null; }
|
||||
# The session id every UI page carries, which the server demands of a farewell.
|
||||
page_sid() {
|
||||
firstline "$(get "$1" /server/index.html |
|
||||
sed -n 's/.*name="sid" value="\([0-9a-f]*\)".*/\1/p')"
|
||||
}
|
||||
# A farewell as the page sends one: a POST holding that id.
|
||||
bye() {
|
||||
"${HTS_PYTHON}" "${testdir}/httpclient.py" --port "$1" \
|
||||
--path "/ping?w=$2&t=${RANDOM}&e=bye" --field "sid=$3" >/dev/null
|
||||
}
|
||||
|
||||
# Wait up to $2 seconds for $1 to go away.
|
||||
died_within() {
|
||||
local pid=$1 limit=$2 start=$SECONDS
|
||||
while kill -0 "${pid}" 2>/dev/null; do
|
||||
test "$((SECONDS - start))" -lt "${limit}" || return 1
|
||||
poll_wait 0.2
|
||||
done
|
||||
}
|
||||
|
||||
printf '[an abandoned WebHTTrack server stops on its own] ..\t'
|
||||
|
||||
# The page and the server agree on one wire format, and only the page half runs
|
||||
# in a browser: a client that stopped naming its window, or spelled the farewell
|
||||
# differently, would leave every case below testing the server against itself.
|
||||
js="${HTS_DISTDIR}/html/server/ping.js"
|
||||
grep -qF '"/ping?w=" + PING_WINDOW' "${js}" || fail "ping.js stopped naming its window"
|
||||
grep -qF 'ping_url("e=bye")' "${js}" || fail "ping.js stopped saying goodbye"
|
||||
grep -qF '"sid=" + encodeURIComponent(sid)' "${js}" ||
|
||||
fail "ping.js stopped signing its goodbye, which the server then ignores"
|
||||
|
||||
# 1. The heartbeat must never be answered from a cache: a reply the browser
|
||||
# reuses is one the server never sees, and silence is what kills it.
|
||||
start_case
|
||||
bye_pid=${HTS_PID}
|
||||
bye_port=${HTS_PORT}
|
||||
sid=$(page_sid "${bye_port}")
|
||||
test "${#sid}" -eq 32 || fail "no session id on the wizard's first page"
|
||||
reply=$(get "${bye_port}" '/ping?w=w1')
|
||||
grep -q '^HTTP/1\.0 200 ' <<<"${reply}" || fail "no pong: $(head -1 <<<"${reply}")"
|
||||
grep -qi '^Cache-Control:.*no-cache' <<<"${reply}" ||
|
||||
fail "the heartbeat is cacheable: ${reply}"
|
||||
|
||||
# 2. Closing one of two windows must not end a session the other is still using.
|
||||
# A hidden tab has its timers throttled to as little as one wake-up a minute, so
|
||||
# this cannot rest on the survivor answering inside the grace.
|
||||
ping "${bye_port}" w2
|
||||
bye "${bye_port}" w1 "${sid}"
|
||||
sleep $((grace + 2))
|
||||
kill -0 "${bye_pid}" 2>/dev/null ||
|
||||
fail "closing one window ended a session another window still had open"
|
||||
|
||||
# 3. An unsigned farewell is not one: /ping is a GET, so it clears neither the
|
||||
# session-id nor the Origin gate, and any local process or visited page can send
|
||||
# it. Only a heartbeat may go unproven, and a heartbeat only extends a life.
|
||||
ping "${bye_port}" w2 e=bye
|
||||
sleep $((grace + 2))
|
||||
kill -0 "${bye_pid}" 2>/dev/null ||
|
||||
fail "an unauthenticated goodbye ended the session"
|
||||
|
||||
# 4. A flood of window ids must not push a real window out of the table: with
|
||||
# the last real one evicted, saying goodbye to the flood would end the session.
|
||||
# One more than the table holds, so the refusal itself is exercised.
|
||||
for i in $(seq 0 16); do
|
||||
ping "${bye_port}" "f${i}"
|
||||
done
|
||||
for i in $(seq 0 16); do
|
||||
bye "${bye_port}" "f${i}" "${sid}"
|
||||
done
|
||||
sleep $((grace + 2))
|
||||
kill -0 "${bye_pid}" 2>/dev/null ||
|
||||
fail "a flood of window ids evicted the real window and ended the session"
|
||||
|
||||
# 5. The last window leaving takes the server with it, well inside the idle
|
||||
# timeout that would otherwise apply.
|
||||
bye "${bye_port}" w2 "${sid}"
|
||||
bye_at=${SECONDS}
|
||||
died_within "${bye_pid}" $((timeout - 1)) ||
|
||||
fail "the server outlived its last window by $((SECONDS - bye_at))s"
|
||||
test "$((SECONDS - bye_at))" -le $((grace + 3)) ||
|
||||
fail "the last window took $((SECONDS - bye_at))s, past the ${grace}s grace"
|
||||
|
||||
# The four cases below share one wait, so they cost one timeout, not four.
|
||||
|
||||
# 6. A window that stops pinging without a goodbye has crashed with its browser.
|
||||
start_case
|
||||
lost_pid=${HTS_PID}
|
||||
ping "${HTS_PORT}" w1
|
||||
lost_at=${SECONDS}
|
||||
|
||||
# 7. A window that keeps pinging keeps its server, past that same deadline.
|
||||
start_case
|
||||
live_pid=${HTS_PID}
|
||||
live_port=${HTS_PORT}
|
||||
live_at=${SECONDS}
|
||||
pingfail="${work}/pingfail"
|
||||
(
|
||||
while :; do
|
||||
ping "${live_port}" w1 || echo failed >>"${pingfail}"
|
||||
poll_wait 0.5
|
||||
done
|
||||
) &
|
||||
pinger=$!
|
||||
|
||||
# 8. A client that never pings at all is a browser too old for the heartbeat, or
|
||||
# one with scripting off, and may be a user reading the page: only the
|
||||
# launcher's death may end that session, and nothing here kills this shell.
|
||||
start_case
|
||||
quiet_pid=${HTS_PID}
|
||||
quiet_at=${SECONDS}
|
||||
get "${HTS_PORT}" /server/index.html >/dev/null
|
||||
|
||||
# 9. ..and when that launcher does die, the same session ends: it is the only
|
||||
# signal such a browser produces. A process of our own stands in for it.
|
||||
sleep 600 &
|
||||
sleeper=$!
|
||||
start_case "${sleeper}"
|
||||
legacy_pid=${HTS_PID}
|
||||
get "${HTS_PORT}" /server/index.html >/dev/null
|
||||
legacy_at=${SECONDS}
|
||||
kill "${sleeper}" 2>/dev/null || true
|
||||
wait "${sleeper}" 2>/dev/null || true # absorb bash's async "Terminated" notice
|
||||
sleeper=
|
||||
|
||||
# The crashed window must outlive its setup, or the floor asserted below could
|
||||
# be met by the time these four servers took to start.
|
||||
kill -0 "${lost_pid}" 2>/dev/null ||
|
||||
fail "the idle timeout fired during setup, $((SECONDS - lost_at))s in"
|
||||
|
||||
died_within "${lost_pid}" $((timeout * 3)) ||
|
||||
fail "a server whose window stopped pinging survived $((SECONDS - lost_at))s"
|
||||
test "$((SECONDS - lost_at))" -ge $((timeout - 2)) ||
|
||||
fail "the idle timeout fired after $((SECONDS - lost_at))s, under ${timeout}s"
|
||||
died_within "${legacy_pid}" $((timeout * 3)) ||
|
||||
fail "a server outlived its launcher by $((SECONDS - legacy_at))s"
|
||||
|
||||
# Both survivors started after the crashed one, so its death is too early to
|
||||
# judge them by. Two timeouts, not one: a window whose refresh stopped working
|
||||
# would still be inside its first.
|
||||
while test "$((SECONDS - live_at))" -le $((timeout * 2)) ||
|
||||
test "$((SECONDS - quiet_at))" -le $((timeout * 2)); do
|
||||
poll_wait 0.5
|
||||
done
|
||||
# Aliveness first: a server that dies also fails the pings aimed at it, and the
|
||||
# probe's own failure would then be the only thing reported.
|
||||
kill -0 "${live_pid}" 2>/dev/null || fail "a pinged server was killed anyway"
|
||||
kill -0 "${quiet_pid}" 2>/dev/null ||
|
||||
fail "a server whose client never pings was killed under a live launcher"
|
||||
! test -f "${pingfail}" || fail "the probe's own pings failed; nothing was proven"
|
||||
kill "${pinger}" 2>/dev/null || true
|
||||
pinger=
|
||||
|
||||
# 10. Closing the window while a mirror runs must not take the mirror down: it
|
||||
# may have hours of crawling behind it. The veto has to lift when the crawl
|
||||
# ends, or the server it saved becomes immortal.
|
||||
start_case
|
||||
crawl_pid=${HTS_PID}
|
||||
crawl_port=${HTS_PORT}
|
||||
clog="${work}/content.log"
|
||||
"${HTS_PYTHON}" "${testdir}/local-server.py" --root "${work}" >"${clog}" 2>&1 &
|
||||
csrv=$!
|
||||
cport=$(discover_server_port "${clog}" "${csrv}") || fail "no content server"
|
||||
|
||||
sid=$(page_sid "${crawl_port}")
|
||||
test "${#sid}" -eq 32 || fail "no session id to start a mirror with"
|
||||
# Its index links one page that sleeps 5s, so the crawl outlasts the grace and
|
||||
# still ends inside the test.
|
||||
"${HTS_PYTHON}" "${testdir}/httpclient.py" --port "${crawl_port}" \
|
||||
--path /step4.html --field "sid=${sid}" --field "path=${work}" \
|
||||
--field projname=crawl --field winprofile=x --field command_do=start \
|
||||
--field "command=httrack --quiet --robots=0 http://127.0.0.1:${cport}/abortpurge/index.html -O ${work}/crawl" \
|
||||
>/dev/null
|
||||
|
||||
ping "${crawl_port}" w1
|
||||
bye "${crawl_port}" w1 "${sid}"
|
||||
sleep $((grace + 2))
|
||||
kill -0 "${crawl_pid}" 2>/dev/null ||
|
||||
fail "the crawling server quit, taking its mirror with it"
|
||||
# A crawl that never started would leave the veto unexercised, and the survival
|
||||
# above would prove nothing.
|
||||
test -d "${work}/crawl/hts-cache" || fail "no mirror ever started: $(cat "${clog}")"
|
||||
|
||||
died_within "${crawl_pid}" $((timeout * 3)) ||
|
||||
fail "the server never left once its mirror had finished"
|
||||
|
||||
# 11. The wizard's own Quit button is the other way out, and the only one that
|
||||
# leaves through smallserver(), which has to report the server it did create.
|
||||
start_case
|
||||
quit_pid=${HTS_PID}
|
||||
sid=$(page_sid "${HTS_PORT}")
|
||||
test "${#sid}" -eq 32 || fail "no session id to quit with"
|
||||
"${HTS_PYTHON}" "${testdir}/httpclient.py" --port "${HTS_PORT}" \
|
||||
--path /server/exit.html --field "sid=${sid}" --field command=quit >/dev/null
|
||||
died_within "${quit_pid}" "${grace}" || fail "Quit did not stop the server"
|
||||
! grep -q 'Unable to create the server' "${HTS_LOG}" ||
|
||||
fail "a clean quit reported a server it could not create: $(cat "${HTS_LOG}")"
|
||||
|
||||
htsserver_stop
|
||||
htsserver_assert_reaped
|
||||
echo OK
|
||||
16
tests/290_local-footer-default.test
Normal file
16
tests/290_local-footer-default.test
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The default footer carries named fields, so a mirrored page must show the
|
||||
# crawled URL and date, not a literal "{url}" (what the legacy positional model
|
||||
# emits for a template it does not understand).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
bash "$top_srcdir/tests/local-crawl.sh" \
|
||||
--files 5 --errors 0 \
|
||||
--file-matches 'simple/basic.html' \
|
||||
"<!-- Mirrored from http://127\.0\.0\.1:[0-9]+/simple/basic\.html by HTTrack Website Copier/[^ ]+ \[XR&CO'[0-9]{4}\], [A-Z][a-z]{2}, [0-9]{2} [A-Z][a-z]{2} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} GMT -->" \
|
||||
--file-not-matches 'simple/basic.html' '\{(url|date)\}' \
|
||||
httrack 'BASEURL/simple/basic.html'
|
||||
39
tests/291_webhttrack-footer-default.test
Normal file
39
tests/291_webhttrack-footer-default.test
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# WebHTTrack prefills the engine's default footer, and a new project must get
|
||||
# the named fields (see HTS_DEFAULT_FOOTER on why a stray "%s" is fatal).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=tests/webhttracklib.sh
|
||||
. "$(dirname "$0")/webhttracklib.sh"
|
||||
|
||||
htsserver_require
|
||||
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/webhttrack_footer.XXXXXX") || fail "no tmpdir"
|
||||
cleanup() {
|
||||
htsserver_cleanup
|
||||
rm -rf "${work}"
|
||||
}
|
||||
cleanup_push cleanup
|
||||
|
||||
printf '[a fresh profile prefills the named-field footer] ..\t'
|
||||
|
||||
# An isolated HOME: a stored ~/.httrack.ini would answer with its own footer.
|
||||
mkdir -p "${work}/websites"
|
||||
htsserver_start --home "${work}"
|
||||
reply=$(htsserver_get /server/option6.html)
|
||||
grep -q '^HTTP/1.[01] 200' <<<"${reply}" || fail "option6.html: ${reply}"
|
||||
|
||||
value=$(firstline "$(sed -n 's/.*name="footer" value="\([^"]*\)".*/\1/p' <<<"${reply}")")
|
||||
test -n "${value}" || fail "option6.html serves no footer field: ${reply}"
|
||||
case ${value} in
|
||||
*'%s'*) fail "the prefilled footer is still positional: ${value}" ;;
|
||||
esac
|
||||
grep -q '{url}.*{date}' <<<"${value}" || fail "prefilled footer: ${value}"
|
||||
|
||||
htsserver_stop
|
||||
# A leaked server wedges the parallel harness behind a green log.
|
||||
htsserver_assert_reaped
|
||||
|
||||
echo OK
|
||||
50
tests/292_engine-wizard-scope.test
Normal file
50
tests/292_engine-wizard-scope.test
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "$(dirname "$0")/testlib.sh"
|
||||
|
||||
# #1117: the self-test asserts the scopes and the patterns; this feeds the pair
|
||||
# a scope answer emits back through the matcher that authorizes a link.
|
||||
|
||||
# The front end enumerates by looping until the engine stops answering. Strip
|
||||
# the CRs: MSYS hands the engine a text-mode stdout and $(...) eats only the
|
||||
# trailing newline, so an assertion spanning several lines keeps the interior
|
||||
# ones. Every other assert_selftest here compares a single line and cannot see
|
||||
# this.
|
||||
want="0 download.example.co.uk
|
||||
1 example.co.uk
|
||||
2 co.uk"
|
||||
got=$(httrack -O /dev/null -#test=wizardscope download.example.co.uk/x | tr -d '\r')
|
||||
test "$got" = "$want" || fail "wizardscope enumeration: got [$got]"
|
||||
|
||||
# widening stops before a bare TLD, and an IP literal offers nothing at all
|
||||
assert_selftest "0 example.com" wizardscope example.com/x
|
||||
assert_selftest "" wizardscope 192.168.1.1/x
|
||||
|
||||
# answer 1000+1 is "example.co.uk and every host below it": two filters, since
|
||||
# the starred one misses the apex
|
||||
sub=$(httrack -O /dev/null -#test=wizardfilter 1001 www.example.co.uk /x)
|
||||
apex=$(httrack -O /dev/null -#test=wizardfilter 1001 www.example.co.uk /x 0 1)
|
||||
test "$sub" = "+*.example.co.uk/*" || fail "subdomain filter: got [$sub]"
|
||||
test "$apex" = "+example.co.uk/*" || fail "apex filter: got [$apex]"
|
||||
|
||||
for host in www.example.co.uk a.b.example.co.uk; do
|
||||
assert_selftest "$host/x does match ${sub#+}" filter "${sub#+}" "$host/x"
|
||||
done
|
||||
assert_selftest "example.co.uk/x does NOT match ${sub#+}" filter "${sub#+}" example.co.uk/x
|
||||
assert_selftest "example.co.uk/x does match ${apex#+}" filter "${apex#+}" example.co.uk/x
|
||||
|
||||
# neither half may leak past the domain boundary
|
||||
for bad in notexample.co.uk/x example.co.uk.evil.com/x; do
|
||||
assert_selftest "$bad does NOT match ${sub#+}" filter "${sub#+}" "$bad"
|
||||
assert_selftest "$bad does NOT match ${apex#+}" filter "${apex#+}" "$bad"
|
||||
done
|
||||
|
||||
# the exclude range emits the same pair negated
|
||||
assert_selftest "-*.example.co.uk/*" wizardfilter 2001 www.example.co.uk /x
|
||||
|
||||
assert_selftest "wizardscope self-test OK" wizardscope
|
||||
assert_selftest "wizardfilter self-test OK" wizardfilter
|
||||
15
tests/293_engine-wizard-verdict.test
Normal file
15
tests/293_engine-wizard-verdict.test
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "$(dirname "$0")/testlib.sh"
|
||||
|
||||
# The verdict half of a wizard answer, driven without the interactive callback.
|
||||
|
||||
assert_selftest "forbidden=1 stop=1 prio=42" wizardverdict -1
|
||||
assert_selftest "forbidden=0 stop=0 prio=42" wizardverdict 6
|
||||
|
||||
# every answer, both scope ranges, and a link the crawl had already refused
|
||||
assert_selftest "wizardverdict self-test OK" wizardverdict
|
||||
@@ -39,15 +39,8 @@ test "$((SECONDS - start))" -lt 15 || fail "watchdog fired late"
|
||||
rc=0
|
||||
if is_windows; then
|
||||
# Existence by exact Windows PID, not a global ping.exe count: the timing
|
||||
# sub-test above leaves a still-dying ping that a count would race. Plain
|
||||
# tasklist, no switches (the workflow's MSYS2_ARG_CONV_EXCL='*' mangles a
|
||||
# //FI filter arg into a silent no-match); $2 is the PID, and $1 must be
|
||||
# ping.exe too, since Windows hands a freed PID straight back out. Folded
|
||||
# case, as the tasklist matchers in proclib.sh already are.
|
||||
alive() {
|
||||
tasklist 2>/dev/null |
|
||||
awk -v p="$1" 'tolower($1) == "ping.exe" && $2 == p {f = 1} END {exit !f}'
|
||||
}
|
||||
# sub-test above leaves a still-dying ping that a count would race.
|
||||
alive() { win_pid_runs "$1" ping.exe; }
|
||||
# alive() is a conjunction now, and one that never matches would call every
|
||||
# survivor reaped. Prove it fires on a live ping, reached the same way.
|
||||
ping -n 20 127.0.0.1 >/dev/null 2>&1 &
|
||||
@@ -55,6 +48,12 @@ if is_windows; then
|
||||
cw=$(cat "/proc/$cpid/winpid" 2>/dev/null)
|
||||
test -n "$cw" || fail "could not read a live ping's Windows PID"
|
||||
alive "$cw" || fail "alive() cannot see a running ping.exe (pid $cw)"
|
||||
# What kill_tree checks before firing (#1228), on a live process: /proc must
|
||||
# name the image tasklist answers with, or the check passes nothing on.
|
||||
win_capture "$cpid"
|
||||
test "$WIN_PID" = "$cw" || fail "win_capture read winpid '$WIN_PID', /proc says $cw"
|
||||
win_pid_runs "$cw" "$WIN_IMAGE" || fail "tasklist does not call pid $cw a '$WIN_IMAGE'"
|
||||
! win_pid_runs "$cw" no-such-image.exe || fail "win_pid_runs accepts any image at all"
|
||||
kill_tree "$cpid" "$cw"
|
||||
wait "$cpid" 2>/dev/null || true
|
||||
# The grandchild ping records its own Windows PID: non-empty proves it ran
|
||||
|
||||
@@ -73,14 +73,18 @@ ci_start_native_watchdog() {
|
||||
# End the step, announcing $2 first: the kill runs no EXIT trap, so an unexplained
|
||||
# death is all the log would otherwise hold.
|
||||
ci_heartbeat_kill() {
|
||||
local main=$1
|
||||
local main=$1 winpid winimage
|
||||
ci_annotate error "suite watchdog" "$2"
|
||||
# Ahead of the kill, which runs no EXIT trap: an orphan would outlive the
|
||||
# step and overwrite its last status with a frozen tail.
|
||||
test -z "${watchdog:-}" || kill_pid "$watchdog"
|
||||
# Read before the two kills below, which would leave the winpid naming
|
||||
# whoever Windows hands the number to next (#1228).
|
||||
win_capture "$main"
|
||||
winpid=$WIN_PID winimage=$WIN_IMAGE
|
||||
# Direct first: kill_tree may reap this watchdog before its own root (#953).
|
||||
kill_pid "$main"
|
||||
kill_tree "$main"
|
||||
kill_tree "$main" "$winpid" "$winimage"
|
||||
}
|
||||
|
||||
ci_suite_heartbeat() {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
# channel that outlives a dead runner. Every status carries the same state.
|
||||
param(
|
||||
[string]$ProgressLog = '',
|
||||
[int]$IntervalSeconds = 30,
|
||||
# 15s, not 30: a lost runner dies inside a single status period (#1228).
|
||||
[int]$IntervalSeconds = 15,
|
||||
[int]$PollSeconds = 5,
|
||||
# Cannot outlive the step, whatever the caller forgets to kill.
|
||||
[int]$MaxSeconds = 2700,
|
||||
@@ -48,19 +49,40 @@ function Format-WatchdogStatus {
|
||||
$q = '?'
|
||||
if ($Static -ge 0) { $q = [string]$Static }
|
||||
$t = ($InFlight -replace '\s+', ' ').Trim()
|
||||
if ($t.Length -gt 46) { $t = $t.Substring(0, 46) }
|
||||
if ($t.Length -gt 30) { $t = $t.Substring(0, 30) }
|
||||
$s = 't={0}s q={1}s {2} | {3}' -f $Elapsed, $q, $t, $Counters
|
||||
if ($s.Length -gt 140) { $s = $s.Substring(0, 140) }
|
||||
return $s
|
||||
}
|
||||
|
||||
# The worse of the running peak and how much longer the last iteration took than
|
||||
# the poll it asked for. Peak, not last: a status covers several iterations.
|
||||
function Get-MaxLag {
|
||||
param([int]$Peak, [double]$Elapsed, [double]$Since, [int]$Poll)
|
||||
$lag = [int]($Elapsed - $Since - $Poll * 1000)
|
||||
if ($lag -gt $Peak) { return $lag }
|
||||
return $Peak
|
||||
}
|
||||
|
||||
# The TCP counters are cumulative since boot, so only the change over a status
|
||||
# period says what the suite did; $Prev is $null on the first sample.
|
||||
function Get-TcpDelta {
|
||||
param($Prev, $Cur)
|
||||
if ($null -eq $Prev) { return 'n=? f=?' }
|
||||
return 'n={0} f={1}' -f ($Cur[0] - $Prev[0]), ($Cur[1] - $Prev[1])
|
||||
}
|
||||
|
||||
# --- probes ------------------------------------------------------------------
|
||||
|
||||
$script:LastTcp = $null
|
||||
|
||||
# One try/catch per counter: a probe that fails costs its own field, not the loop.
|
||||
# In-process only. A CIM query is richer, but its connect to a wedged WMI service
|
||||
# is unbounded, and would hang the one reporter still standing.
|
||||
function Get-WatchdogCounters {
|
||||
param([int]$LagMs = 0, [int]$Failed = 0)
|
||||
$f = New-Object System.Collections.ArrayList
|
||||
$ps = @()
|
||||
try {
|
||||
$ps = @(Get-Process)
|
||||
[void]$f.Add('p={0}' -f $ps.Count)
|
||||
@@ -68,8 +90,31 @@ function Get-WatchdogCounters {
|
||||
} catch { [void]$f.Add('p=? h=?') }
|
||||
try {
|
||||
$drive = New-Object System.IO.DriveInfo($env:SystemDrive + '\')
|
||||
[void]$f.Add('d={0}' -f [int]($drive.AvailableFreeSpace / 1MB))
|
||||
[void]$f.Add('d={0}' -f [int]($drive.AvailableFreeSpace / 1GB))
|
||||
} catch { [void]$f.Add('d=?') }
|
||||
# The ramp detector: starvation is what makes a poll overshoot.
|
||||
[void]$f.Add('l={0}' -f $LagMs)
|
||||
# Box-stop against network-break: the status that lands after an outage says
|
||||
# how many it swallowed, and a box that stopped never lands one.
|
||||
[void]$f.Add('x={0}' -f $Failed)
|
||||
try {
|
||||
# One GetTcpStatisticsEx; GetActiveTcpConnections() would allocate per socket.
|
||||
$t = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetTcpIPv4Statistics()
|
||||
$cur = @($t.ConnectionsInitiated, ($t.FailedConnectionAttempts + $t.ResetConnections))
|
||||
[void]$f.Add((Get-TcpDelta $script:LastTcp $cur))
|
||||
$script:LastTcp = $cur
|
||||
[void]$f.Add('e={0}' -f $t.CurrentConnections)
|
||||
} catch { [void]$f.Add('n=? f=? e=?') }
|
||||
try {
|
||||
if ($ps.Count -lt 1) { throw 'no process list' }
|
||||
[void]$f.Add('m={0}' -f [int]((($ps | Measure-Object -Property WorkingSet64 -Sum).Sum) / 1MB))
|
||||
[void]$f.Add('c={0}' -f [int]((($ps | Measure-Object -Property PagedMemorySize64 -Sum).Sum) / 1MB))
|
||||
# Its own field: the agent is what stops reporting, and the box total hides it.
|
||||
$agent = @($ps | Where-Object { $_.Name -eq 'Runner.Worker' })
|
||||
$ws = 0
|
||||
if ($agent.Count -gt 0) { $ws = [int]((($agent | Measure-Object -Property WorkingSet64 -Sum).Sum) / 1MB) }
|
||||
[void]$f.Add('a={0}' -f $ws)
|
||||
} catch { [void]$f.Add('m=? c=? a=?') }
|
||||
return ($f -join ' ')
|
||||
}
|
||||
|
||||
@@ -156,14 +201,15 @@ function Invoke-WatchdogSelfTest {
|
||||
Assert-That ($ko[0] -eq 8 -and $ko[1] -eq 8) 'a repeat rejection does not widen the gap'
|
||||
|
||||
$long = '43_local-update-truncate-with-a-very-long-name-indeed.test'
|
||||
$line = Format-WatchdogStatus 812 41 $long 'p=118 h=41230 d=13210'
|
||||
# The widest real counter line, so a status that fits here fits on the runner.
|
||||
$line = Format-WatchdogStatus 2700 2700 $long 'p=201 h=54598 d=85 l=120 x=0 n=412 f=0 e=180 m=3100 c=4200 a=210'
|
||||
Assert-That ($line.Length -le 140) ('status description is {0} characters' -f $line.Length)
|
||||
Assert-That ($line -like 't=812s q=41s 43_local-update-truncate*') ('status leads with the wrong fields: {0}' -f $line)
|
||||
Assert-That ($line -like '*d=13210') 'the counters did not survive a long test name'
|
||||
Assert-That ($line -like 't=2700s q=2700s 43_local-update-truncate*') ('status leads with the wrong fields: {0}' -f $line)
|
||||
Assert-That ($line -like '*a=210') 'the counters did not survive a long test name'
|
||||
# -match, not -like: '?' is a wildcard there, so q=0s would satisfy it too.
|
||||
Assert-That ((Format-WatchdogStatus 8 -1 'x' 'y') -match '^t=8s q=\?s x \| y$') 'an unknown staticness reads as a number'
|
||||
$clip = Format-WatchdogStatus 1 2 ('x' * 80) 'c'
|
||||
Assert-That ($clip -match '^t=1s q=2s x{46} \| c$') ('the in-flight name was not clipped to 46: {0}' -f $clip)
|
||||
Assert-That ($clip -match '^t=1s q=2s x{30} \| c$') ('the in-flight name was not clipped to 30: {0}' -f $clip)
|
||||
$wide = Format-WatchdogStatus 1 2 ('x' * 300) ('y' * 300)
|
||||
Assert-That ($wide.Length -le 140) ('an oversized status was not clipped: {0}' -f $wide.Length)
|
||||
# Cut from the tail: the head carries the fields a wedge is read for.
|
||||
@@ -181,15 +227,23 @@ function Invoke-WatchdogSelfTest {
|
||||
|
||||
# Space-separated key=value: the counters share the 140-char description with
|
||||
# the fields a wedge is read for, and '?' from a failed probe is a value.
|
||||
$c = Get-WatchdogCounters
|
||||
$c = Get-WatchdogCounters 120 3
|
||||
Assert-That ($c -match '^[a-z]+=\S+( [a-z]+=\S+)*$') ('the counters are not key=value pairs: {0}' -f $c)
|
||||
foreach ($k in 'p', 'h', 'd') {
|
||||
foreach ($k in 'p', 'h', 'd', 'l', 'x', 'n', 'f', 'e', 'm', 'c', 'a') {
|
||||
Assert-That ($c -match ('(^| ){0}=' -f $k)) ('the counters dropped {0}=: {1}' -f $k, $c)
|
||||
}
|
||||
Assert-That ($c.Length -le 60) ('the counters take {0} of the 140 characters' -f $c.Length)
|
||||
Assert-That ($c -match '(^| )l=120( |$)') ('the loop lag is not what the caller measured: {0}' -f $c)
|
||||
Assert-That ($c -match '(^| )x=3( |$)') ('the failed-post count is not what the caller passed: {0}' -f $c)
|
||||
Assert-That ((Get-MaxLag 0 6200 1000 5) -eq 200) 'a poll that overshot by 200ms was not measured'
|
||||
Assert-That ((Get-MaxLag 500 6200 1000 5) -eq 500) 'a smaller lag replaced the peak'
|
||||
Assert-That ((Get-MaxLag 0 5900 1000 5) -eq 0) 'a poll that returned early reported a lag'
|
||||
Assert-That ((Get-TcpDelta $null @(70, 9)) -eq 'n=? f=?') 'a first sample with no predecessor reported a delta'
|
||||
Assert-That ((Get-TcpDelta @(64, 7) @(70, 9)) -eq 'n=6 f=2') 'a total was reported where the delta was asked for'
|
||||
# 140 less the 16 of t=/q= and the 33 a clipped test name and its separator take.
|
||||
Assert-That ($c.Length -le 91) ('the counters take {0} of the 140 characters' -f $c.Length)
|
||||
|
||||
# Nothing else reads these: every other leg passes its own schedule.
|
||||
Assert-That ($IntervalSeconds -eq 30) ('the default status cadence is {0}s' -f $IntervalSeconds)
|
||||
Assert-That ($IntervalSeconds -eq 15) ('the default status cadence is {0}s' -f $IntervalSeconds)
|
||||
Assert-That ($PollSeconds -eq 5) ('the default poll is {0}s' -f $PollSeconds)
|
||||
|
||||
Assert-That (-not (Send-WatchdogStatus 'self-test')) 'the self-test can reach the API'
|
||||
@@ -216,6 +270,9 @@ $movedAt = 0
|
||||
$postedAt = -$IntervalSeconds
|
||||
$backoff = 0
|
||||
$skip = 0
|
||||
$lagMax = 0
|
||||
$failed = 0
|
||||
$tickAt = $sw.Elapsed.TotalMilliseconds
|
||||
|
||||
# Guarded like the rest; the launcher waits for this exact line.
|
||||
try { Write-Host 'watchdog ready' } catch { }
|
||||
@@ -223,7 +280,12 @@ Write-WatchdogLog ('watching {0} every {1}s' -f $ProgressLog, $IntervalSeconds)
|
||||
|
||||
while ($sw.Elapsed.TotalSeconds -lt $MaxSeconds) {
|
||||
# Measured, never accumulated: starvation is what makes a sleep overshoot.
|
||||
$now = [int]$sw.Elapsed.TotalSeconds
|
||||
$ms = $sw.Elapsed.TotalMilliseconds
|
||||
$now = [int]($ms / 1000)
|
||||
# Measured at the top, so the lag covers the probes and the post as well as
|
||||
# the sleep: starvation stretches all three.
|
||||
$lagMax = Get-MaxLag $lagMax $ms $tickAt $PollSeconds
|
||||
$tickAt = $ms
|
||||
try {
|
||||
$tail = Get-ProgressTail -Path $ProgressLog
|
||||
if ($tail.Ok -and $tail.Signature -ne $lastSig) {
|
||||
@@ -234,14 +296,18 @@ while ($sw.Elapsed.TotalSeconds -lt $MaxSeconds) {
|
||||
$postedAt = $now
|
||||
$static = -1
|
||||
if ($tail.Ok) { $static = $now - $movedAt }
|
||||
$desc = Format-WatchdogStatus $now $static $tail.Line (Get-WatchdogCounters)
|
||||
$desc = Format-WatchdogStatus $now $static $tail.Line (Get-WatchdogCounters $lagMax $failed)
|
||||
# Logged whatever the backoff decides: it throttles the API, not the
|
||||
# artifact, which is all a run whose token cannot post will leave.
|
||||
Write-WatchdogLog $desc
|
||||
if ($skip -gt 0) {
|
||||
$skip--
|
||||
} else {
|
||||
$next = Get-NextThrottle (Send-WatchdogStatus $desc) $backoff
|
||||
$ok = Send-WatchdogStatus $desc
|
||||
# Cleared together, and only by a status that landed: a peak reached
|
||||
# while nothing was getting through is what the next one has to carry.
|
||||
if ($ok) { $failed = 0; $lagMax = 0 } else { $failed++ }
|
||||
$next = Get-NextThrottle $ok $backoff
|
||||
$skip = $next[0]
|
||||
$backoff = $next[1]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "usage: ${0##*/} {--srcdir DIR [--builddir DIR] | --headers-dir DIR}" \
|
||||
"[--backend cl|cc] [--cc CMD] [--cxx CMD] [--self-test] [-- CPPFLAGS...]" >&2
|
||||
"[--backend cl|cc] [--cc CMD] [--cxx CMD] [--budget SECONDS] [--self-test]" \
|
||||
"[-- CPPFLAGS...]" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -26,6 +27,7 @@ cc_cmd=""
|
||||
cxx_cmd=""
|
||||
cxx_set=0
|
||||
selftest=0
|
||||
budget=
|
||||
extra=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
@@ -33,6 +35,10 @@ while [ $# -gt 0 ]; do
|
||||
selftest=1
|
||||
shift
|
||||
;;
|
||||
--budget)
|
||||
budget=${2-}
|
||||
shift 2 || usage
|
||||
;;
|
||||
--srcdir)
|
||||
srcdir=${2-}
|
||||
shift 2 || usage
|
||||
@@ -237,15 +243,45 @@ fi
|
||||
|
||||
began=$SECONDS
|
||||
bad=0
|
||||
# Sliced only for a caller that gave a budget: one call per batch cannot be given up on,
|
||||
# and an emulated compiler needs more time for it than the harness allows a test (#1146).
|
||||
# Unsliced elsewhere, so the Windows job keeps paying one compiler spawn per batch.
|
||||
if [ -n "$budget" ] && [ "$budget" -gt 0 ]; then
|
||||
export HTTRACK_TEST_TIMEOUT=$budget
|
||||
slices=8
|
||||
else
|
||||
slices=1
|
||||
fi
|
||||
slice=$(((${#units[@]} + slices - 1) / slices))
|
||||
# From the slice size, not from $slices: they differ whenever the units do not divide
|
||||
# evenly, and a step count that outlives the loop leaves the pacer projecting forever.
|
||||
per=$(((${#units[@]} + slice - 1) / slice))
|
||||
left=$((${#langs[@]} * ${#modes[@]} * per))
|
||||
swept=0
|
||||
for lang in "${langs[@]}"; do
|
||||
for mode in "${modes[@]}"; do
|
||||
compile "$lang" "$mode" "${units[@]}" || {
|
||||
head -40 "$sweep_log" >&2
|
||||
echo "the headers do not compile as $lang standalone and pairwise ($mode)" >&2
|
||||
bad=1
|
||||
}
|
||||
i=0
|
||||
while [ "$i" -lt "${#units[@]}" ]; do
|
||||
step=$SECONDS
|
||||
chunk=("${units[@]:i:slice}")
|
||||
swept=$((swept + ${#chunk[@]}))
|
||||
compile "$lang" "$mode" "${chunk[@]}" || {
|
||||
head -40 "$sweep_log" >&2
|
||||
echo "the headers do not compile as $lang standalone and pairwise ($mode)" >&2
|
||||
bad=1
|
||||
}
|
||||
i=$((i + slice))
|
||||
left=$((left - 1))
|
||||
# Only while nothing has failed: a skip past a real break would bury it.
|
||||
[ "$bad" -ne 0 ] || [ -z "$budget" ] ||
|
||||
skip_if_out_of_budget "$left" "$((SECONDS - step))"
|
||||
done
|
||||
done
|
||||
done
|
||||
# What reached the compiler, not what was generated: a slice loop that steps past a unit
|
||||
# would otherwise report the full set and pass.
|
||||
want=$((${#langs[@]} * ${#modes[@]} * ${#units[@]}))
|
||||
[ "$swept" -eq "$want" ] || fail "compiled $swept units of $want, the slicing lost some"
|
||||
echo "swept $n headers standalone and pairwise x ${#modes[@]} bytecode modes x ${langs[*]}" \
|
||||
"= $((${#modes[@]} * ${#langs[@]} * ${#units[@]})) units in $((SECONDS - began))s with $backend"
|
||||
[ "$bad" -eq 0 ] || exit 1
|
||||
|
||||
@@ -22,19 +22,39 @@ testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# (CRAWL_DEADLINE, 180s a pass) -- budget below that and a slow-but-legitimate
|
||||
# run would be killed. The slowest healthy test measures 39s. A non-numeric or
|
||||
# absurd value falls back; 0 disables the guard, for use under a debugger.
|
||||
budget=${HTTRACK_TEST_TIMEOUT:-600}
|
||||
case "$budget" in
|
||||
'' | *[!0-9]*) budget=600 ;;
|
||||
esac
|
||||
budget=$(budget_secs)
|
||||
|
||||
# The test script is the last argument; automake passes no others today.
|
||||
for path in "$@"; do :; done
|
||||
name=$(basename "$path")
|
||||
|
||||
# A test whose work legitimately outlasts the wedge budget says so in its header
|
||||
# (269 sweeps n^2 compiles and paces itself inside it). The name carries the rule the
|
||||
# reader cannot see: it raises the budget, so no test can disarm the guard. Read with
|
||||
# the shell to keep it off the per-test fork bill, and bounded, since bash's `test`
|
||||
# errors rather than compares past intmax and would leave the guard unarmed.
|
||||
if test "$budget" -gt 0 && test -r "$path"; then
|
||||
read_lines=0
|
||||
while test "$read_lines" -lt 40 && IFS= read -r line; do
|
||||
read_lines=$((read_lines + 1))
|
||||
case "$line" in
|
||||
'# TEST_TIMEOUT_AT_LEAST: '*)
|
||||
want=${line#'# TEST_TIMEOUT_AT_LEAST: '}
|
||||
case "$want" in
|
||||
'' | *[!0-9]* | ???????*) ;;
|
||||
*) test "$((10#$want))" -le "$budget" || budget=$((10#$want)) ;;
|
||||
esac
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done <"$path"
|
||||
fi
|
||||
|
||||
# Exported so a test can pace itself against the same number (skip_if_out_of_budget)
|
||||
# instead of being killed halfway.
|
||||
export HTTRACK_TEST_TIMEOUT="$budget"
|
||||
test "$budget" -gt 0 || exec "$BASH" "$@"
|
||||
|
||||
# The test script is the last argument; automake passes no others today.
|
||||
for name in "$@"; do :; done
|
||||
name=$(basename "$name")
|
||||
|
||||
# Give the test its own TMPDIR, so the hang dump can salvage exactly this test's
|
||||
# crawl logs instead of racing (and deleting) a sibling's under "make check -j".
|
||||
tmproot=${TMPDIR:-/tmp}
|
||||
|
||||
@@ -286,8 +286,12 @@ poll_wait() {
|
||||
# trap, where a survivor would turn a passing test into a harness timeout.
|
||||
stop_server() {
|
||||
test -n "${1:-}" || return 0
|
||||
local winpid winimage
|
||||
# Before the signal: a winpid read after it can already name a stranger.
|
||||
win_capture "$1"
|
||||
winpid=$WIN_PID winimage=$WIN_IMAGE
|
||||
kill "$1" 2>/dev/null || true
|
||||
if is_windows; then kill_tree "$1"; fi
|
||||
if is_windows; then kill_tree "$1" "$winpid" "$winimage"; fi
|
||||
reap_bounded "$1" || true
|
||||
return 0
|
||||
}
|
||||
@@ -454,6 +458,29 @@ win_pid() {
|
||||
fi
|
||||
}
|
||||
|
||||
# WIN_PID and WIN_IMAGE for MSYS pid $1, read while it is alive: /proc keeps the
|
||||
# entry once the process is gone and Windows reissues the number at once, so a
|
||||
# later read can name a stranger (#1228). Not for a job just backgrounded: until
|
||||
# its exec lands, tens of milliseconds later, both still name the forking shell.
|
||||
# Assigned rather than echoed, a command substitution being a fork (#795).
|
||||
win_capture() { # win_capture <pid>
|
||||
WIN_PID='' WIN_IMAGE=''
|
||||
is_windows || return 0
|
||||
# Unguarded reads: a missing file leaves the empty value set above, and read
|
||||
# reports EOF on an unterminated line having already assigned it.
|
||||
{ read -r WIN_PID <"/proc/$1/winpid"; } 2>/dev/null || true
|
||||
{ read -r WIN_IMAGE <"/proc/$1/winexename"; } 2>/dev/null || true
|
||||
WIN_IMAGE=${WIN_IMAGE##*[\\/]}
|
||||
return 0
|
||||
}
|
||||
|
||||
# Whether Windows PID $1 runs image $2. Both columns at once, since either alone
|
||||
# answers for a recycled PID, and case-folded as the proclib.sh matchers are.
|
||||
win_pid_runs() { # win_pid_runs <winpid> <image>
|
||||
tasklist 2>/dev/null |
|
||||
awk -v p="$1" -v i="$2" 'tolower($1) == tolower(i) && $2 == p { f = 1 } END { exit !f }'
|
||||
}
|
||||
|
||||
# Signal one process, never its descendants: a caller inside the target's own
|
||||
# tree cannot rely on kill_tree, whose taskkill is then a grandchild of it (#953).
|
||||
kill_pid() {
|
||||
@@ -477,11 +504,17 @@ kill_pid() {
|
||||
# so args pass verbatim and a //T would reach taskkill unfolded and be rejected.
|
||||
# $2 is that Windows PID when the caller read it while the job was certainly
|
||||
# alive: /proc/<pid>/winpid is already gone for a job that has just died, and
|
||||
# without it the only route left is the host-wide sweep below.
|
||||
# without it the only route left is the host-wide sweep below. $3 is the image it
|
||||
# ran then: a number that no longer runs it was reissued while we were not
|
||||
# looking, and naming a stranger is as good as naming nobody (#1228).
|
||||
kill_tree() {
|
||||
local pid=$1 winpid=${2:-}
|
||||
local pid=$1 winpid=${2:-} image=${3:-}
|
||||
if is_windows; then
|
||||
test -n "$winpid" || winpid=$(win_pid "$pid")
|
||||
if test -n "$winpid" && test -n "$image" && ! win_pid_runs "$winpid" "$image"; then
|
||||
printf '::warning::pid %s no longer runs %s, not killing it\n' "$winpid" "$image"
|
||||
winpid=
|
||||
fi
|
||||
if test -n "$winpid"; then
|
||||
taskkill /F /T /PID "$winpid" >/dev/null 2>&1 || true
|
||||
# Last resort, so it is opt-in: it kills every engine and every python on
|
||||
@@ -539,16 +572,39 @@ EOF
|
||||
# one step is slower than its neighbours. It asks an ordering of the callers
|
||||
# instead, expensive steps first, so no step left can outrun the reserve the one
|
||||
# before it set (#1146).
|
||||
skip_if_out_of_budget() { # skip_if_out_of_budget <steps left> <seconds the last took>
|
||||
local budget=${HTTRACK_TEST_TIMEOUT:-600} need=$(($2 + $2 / 2))
|
||||
# The budget test-timeout.sh enforces, in seconds, 0 being the guard off. The one
|
||||
# parser: a value bash arithmetic or test would choke on falls back to the default,
|
||||
# and a leading zero would otherwise read as octal in one place and decimal in the next.
|
||||
budget_secs() {
|
||||
local budget=${HTTRACK_TEST_TIMEOUT:-600}
|
||||
case "$budget" in '' | *[!0-9]* | ???????*) budget=600 ;; esac
|
||||
echo "$((10#$budget))"
|
||||
}
|
||||
|
||||
case "$budget" in '' | *[!0-9]*) budget=600 ;; esac
|
||||
skip_if_out_of_budget() { # skip_if_out_of_budget <steps left> <seconds the last took>
|
||||
local budget need=$(($2 + $2 / 2))
|
||||
|
||||
budget=$(budget_secs)
|
||||
test "$1" -gt 0 && test "$budget" -gt 0 || return 0
|
||||
test "$((SECONDS + need))" -ge "$budget" || return 0
|
||||
echo "$1 steps left, the last took ${2}s and the budget is ${budget}s; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
|
||||
# Seconds left of the budget, for a child pacing itself against it (269 hands it to
|
||||
# the sweep). Never below 1 unless the guard is off, when it stays 0.
|
||||
budget_left() {
|
||||
local budget left
|
||||
budget=$(budget_secs)
|
||||
test "$budget" -gt 0 || {
|
||||
echo 0
|
||||
return 0
|
||||
}
|
||||
left=$((budget - SECONDS))
|
||||
test "$left" -ge 1 || left=1
|
||||
echo "$left"
|
||||
}
|
||||
|
||||
# Collect a killed job, giving up after REAP_GRACE seconds. kill_tree can fail to
|
||||
# reap a native Windows descendant -- the very case these watchdogs exist for --
|
||||
# and a bare `wait` then blocks the watchdog itself forever, so the timeout it was
|
||||
|
||||
@@ -48,7 +48,12 @@ cd /bld
|
||||
bash "${GITHUB_WORKSPACE:-/src}/configure"
|
||||
make -j"$(nproc)"
|
||||
# The buildd's own invocation, so a failure here is the one it would report.
|
||||
make check -j"$(nproc)"
|
||||
rc=0
|
||||
make check -j"$(nproc)" || rc=$?
|
||||
# Always, not only where automake prints it: this leg exists to say what an emulated
|
||||
# host does. A paced-out skip must not read as coverage with no reason given.
|
||||
cat tests/test-suite.log || true
|
||||
test "$rc" -eq 0 || exit "$rc"
|
||||
|
||||
# make check exits 0 for an all-SKIP run, and this leg skips a lot by design, so
|
||||
# a container that quietly lost a dependency would report a green covering
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
# -o, --outdir DIR output directory (default: <repo>/dist)
|
||||
# --orig FILE reuse this upstream orig tarball instead of
|
||||
# regenerating it (required for a Debian revision
|
||||
# >= 2, whose orig is frozen in the archive)
|
||||
# >= 2, whose orig is frozen in the archive, and
|
||||
# whenever debian/patches carries a patch)
|
||||
# -s, --source-only build only the source package
|
||||
# -u, --unsigned do not sign anything (implies no release sigs)
|
||||
# --no-release-artifacts skip the orig tarball .asc/.md5/.sha1
|
||||
@@ -39,7 +40,9 @@
|
||||
#
|
||||
# The Debian revision in debian/changelog decides the orig: revision 1 builds a
|
||||
# fresh upstream tarball; revision >= 2 must reuse the orig frozen at revision 1
|
||||
# (the .dsc references it by checksum), so pass it with --orig.
|
||||
# (the .dsc references it by checksum), so pass it with --orig. debian/patches
|
||||
# needs the same tarball for a different reason: a patch backported from upstream
|
||||
# no longer applies to a tree that has the fix, which is what HEAD would give.
|
||||
#
|
||||
# SOURCE_DATE_EPOCH is honored for reproducible output.
|
||||
|
||||
@@ -127,6 +130,10 @@ main() {
|
||||
if [[ $unsigned -eq 0 ]]; then
|
||||
need gpg
|
||||
[[ -n $key ]] || die "no signing key (pass --key or set DEBSIGN_KEYID, or use --unsigned)"
|
||||
# Here rather than at debsign, which runs once the tarball is built: a key id gpg
|
||||
# cannot resolve to a secret key would otherwise cost the whole build first.
|
||||
gpg --list-secret-keys -- "$key" >/dev/null 2>&1 ||
|
||||
die "gpg has no secret key for '$key' (an 0x-prefixed full fingerprint is unambiguous)"
|
||||
fi
|
||||
|
||||
local repo
|
||||
@@ -168,6 +175,13 @@ main() {
|
||||
die "Debian revision $rev needs --orig FILE (the orig is frozen from revision 1)"
|
||||
fi
|
||||
|
||||
# A quilt patch is written against the orig it is applied to. Once the fix is
|
||||
# upstream, HEAD already carries it, so a regenerated orig makes the patch fail
|
||||
# or, worse, apply with fuzz. Unsigned too: this one breaks the build, not policy.
|
||||
if [[ -z $orig_in && -s $export_dir/debian/patches/series ]]; then
|
||||
die "debian/patches is not empty, so --orig FILE is required: the orig built from HEAD already carries the patches"
|
||||
fi
|
||||
|
||||
if [[ -n $orig_in ]]; then
|
||||
info "reusing upstream tarball $orig_in"
|
||||
cp -- "$orig_in" "$scratch/$orig"
|
||||
|
||||
Reference in New Issue
Block a user