mirror of
https://github.com/xroche/httrack.git
synced 2026-08-07 00:07:01 +03:00
Compare commits
12 Commits
3.49.18
...
fix/uutils
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
581784d374 | ||
|
|
70f28202d1 | ||
|
|
dd8b4ccdee | ||
|
|
f2abfc36ef | ||
|
|
b2cd2a51d2 | ||
|
|
562cd1c867 | ||
|
|
a2a20ef684 | ||
|
|
bfe0a4a7f7 | ||
|
|
03c30e17ce | ||
|
|
1a45e0237b | ||
|
|
2a44dc1728 | ||
|
|
23e182281c |
68
.github/workflows/ci.yml
vendored
68
.github/workflows/ci.yml
vendored
@@ -41,7 +41,8 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
build-essential clang autoconf automake libtool autoconf-archive \
|
||||
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
|
||||
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev \
|
||||
appstream appstream-util
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
@@ -137,6 +138,66 @@ jobs:
|
||||
if: failure()
|
||||
run: cat tests/test-suite.log 2>/dev/null || true
|
||||
|
||||
# The Ubuntu buildds run a development release every runner above is well behind.
|
||||
# That gap is what shipped 3.49.18 broken on every Ubuntu architecture but
|
||||
# riscv64, when 25.10 made uutils the default coreutils (#1042).
|
||||
ubuntu-devel:
|
||||
name: build (Ubuntu devel, uutils coreutils)
|
||||
runs-on: ubuntu-24.04
|
||||
container: ubuntu:devel
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
# Before checkout: the container has no git, so submodules would be lost.
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool autoconf-archive \
|
||||
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev \
|
||||
git ca-certificates python3 procps
|
||||
|
||||
- name: Assert the coreutils dispatch on argv[0]
|
||||
# The property #1042 broke on, not the version banner, which uutils has
|
||||
# already reworded once. Without it the job quietly becomes a slower copy
|
||||
# of the one above.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ln -s "$(command -v sleep)" /tmp/not-a-coreutil
|
||||
if /tmp/not-a-coreutil 0; then
|
||||
echo "::error::coreutils here take any argv[0]; this image no longer guards uutils" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
set -euo pipefail
|
||||
autoreconf -fi
|
||||
./configure
|
||||
# A renamed dev package would disable a codec or TLS here, and the tests
|
||||
# that cover them would SKIP rather than fail.
|
||||
grep -q "define HTS_USEBROTLI 1" config.h
|
||||
grep -q "define HTS_USEZSTD 1" config.h
|
||||
grep -q "define HTS_USEOPENSSL 1" config.h
|
||||
|
||||
- name: Build
|
||||
run: make -j"$(nproc)"
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 25
|
||||
run: |
|
||||
set -euo pipefail
|
||||
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
|
||||
make check -j"$jobs"
|
||||
|
||||
- name: Print the test log on failure
|
||||
if: failure()
|
||||
run: cat tests/test-suite.log 2>/dev/null || true
|
||||
|
||||
# Portability: build and test on macOS (Darwin/clang) on a native runner --
|
||||
# no VM. The tree has no __APPLE__ branches, so Darwin exercises the
|
||||
# generic-Unix path on a second libc and kernel. brew's openssl@3 is keg-only,
|
||||
@@ -715,7 +776,8 @@ jobs:
|
||||
sudo apt-get update
|
||||
# noble ships shfmt 3.8.0 (universe), matching the pinned local dev
|
||||
# version; use it rather than fetching a release binary from github.com.
|
||||
sudo apt-get install -y --no-install-recommends shellcheck shfmt appstream
|
||||
sudo apt-get install -y --no-install-recommends shellcheck shfmt appstream \
|
||||
appstream-util
|
||||
shfmt --version
|
||||
appstreamcli --version
|
||||
|
||||
@@ -742,6 +804,8 @@ jobs:
|
||||
set -euo pipefail
|
||||
appstreamcli validate --no-net --explain \
|
||||
html/server/div/com.httrack.WebHTTrack.metainfo.xml
|
||||
appstream-util validate-relax --nonet \
|
||||
html/server/div/com.httrack.WebHTTrack.metainfo.xml
|
||||
|
||||
# Check clang-format on CHANGED LINES ONLY. The engine predates clang-format
|
||||
# (it was shaped by an old Visual Studio formatter) and does not round-trip,
|
||||
|
||||
14
.github/workflows/windows-build.yml
vendored
14
.github/workflows/windows-build.yml
vendored
@@ -14,6 +14,9 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# The suite watchdog reports as a commit status, the only channel that
|
||||
# outlives the runner it is reporting on (#795).
|
||||
statuses: write
|
||||
|
||||
# Cancel superseded runs on the same branch or PR.
|
||||
concurrency:
|
||||
@@ -37,6 +40,9 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive # coucal lives in src/coucal
|
||||
# Or the job token stays in .git/config, where every test the suite
|
||||
# runs can read it.
|
||||
persist-credentials: false
|
||||
|
||||
# Located through vswhere rather than microsoft/setup-msbuild: the repo
|
||||
# only allows GitHub-owned actions.
|
||||
@@ -171,6 +177,14 @@ jobs:
|
||||
shell: bash
|
||||
working-directory: tests
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
# 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 }}
|
||||
WATCHDOG_CONTEXT: windows-suite (${{ matrix.platform }}, ${{ matrix.configuration }})
|
||||
WATCHDOG_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
bash ./ci-windows-suite.sh \
|
||||
"$(cygpath -u "$GITHUB_WORKSPACE")/src/${{ matrix.platform }}/${{ matrix.configuration }}"
|
||||
|
||||
@@ -29,6 +29,8 @@ AC_CONFIG_SRCDIR(src/httrack.c)
|
||||
AC_CONFIG_MACRO_DIR([m4])
|
||||
AC_CONFIG_HEADERS(config.h)
|
||||
AM_INIT_AUTOMAKE([subdir-objects])
|
||||
# 3:11:0: revision-only bump. #1001 moved SOCaddr_inetntoa_ out of htsnet.h, adding
|
||||
# an export where callers had an inline; nothing changed or went away.
|
||||
# 3:10:0: revision-only bump; only the version macro moved, the engine is untouched.
|
||||
# 3:9:0: revision-only bump. #991 and #1005 each added an export
|
||||
# (hts_set_thread_hooks, escape_control_url); nothing changed or went away.
|
||||
@@ -38,7 +40,7 @@ AM_INIT_AUTOMAKE([subdir-objects])
|
||||
# moves nothing). Soname stays .so.3: HTTrackQt is the only consumer of the installed
|
||||
# headers, so a libhttrack4 rename isn't worth it.
|
||||
# (3:0:0 was the htsblk mime-buffer widening, the ABI break that moved .so.2 -> .so.3.)
|
||||
VERSION_INFO="3:10:0"
|
||||
VERSION_INFO="3:11:0"
|
||||
AM_MAINTAINER_MODE
|
||||
AC_USE_SYSTEM_EXTENSIONS
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ WebIcon64x64dir = $(datadir)/icons/hicolor/64x64/apps
|
||||
WebIcon128x128dir = $(datadir)/icons/hicolor/128x128/apps
|
||||
WebIcon256x256dir = $(datadir)/icons/hicolor/256x256/apps
|
||||
WebIconScalabledir = $(datadir)/icons/hicolor/scalable/apps
|
||||
VFolderEntrydir = $(prefix)/share/applications
|
||||
VFolderEntrydir = $(datadir)/applications
|
||||
MetaInfodir = $(datadir)/metainfo
|
||||
|
||||
# Wildcards are globbed against $(srcdir): a bare "*.html" is resolved against
|
||||
|
||||
@@ -155,6 +155,8 @@ rather than on <tt>url</tt> when a mirror is known to carry legacy-charset URLs.
|
||||
|
||||
<h3 id="example">Example</h3>
|
||||
|
||||
One captured run; <tt>generator</tt> and <tt>date</tt> will read differently in yours.
|
||||
|
||||
<pre>
|
||||
{
|
||||
"schema": 1,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 447 B After Width: | Height: | Size: 98 B |
@@ -26,7 +26,6 @@
|
||||
</ul>
|
||||
</description>
|
||||
<launchable type="desktop-id">WebHTTrack.desktop</launchable>
|
||||
<icon type="stock">httrack</icon>
|
||||
<categories>
|
||||
<category>Network</category>
|
||||
</categories>
|
||||
|
||||
@@ -1,37 +1,24 @@
|
||||
/* XPM */
|
||||
static char *httrack16x16[] = {
|
||||
/* columns rows colors chars-per-pixel */
|
||||
"16 16 15 1 ",
|
||||
" c #0D0D10",
|
||||
". c #191921",
|
||||
"X c #2A2A38",
|
||||
"o c #363647",
|
||||
"O c #424257",
|
||||
"+ c #53536E",
|
||||
"@ c #717197",
|
||||
"# c #7E7EA8",
|
||||
"$ c #8585B1",
|
||||
"% c #9898CB",
|
||||
"& c #9999CC",
|
||||
"* c #9A9ACE",
|
||||
"= c #9696C9",
|
||||
"- c #9C9CD0",
|
||||
"; c #9999CC",
|
||||
"16 16 2 1 ",
|
||||
" c #040404",
|
||||
". c #9999CC",
|
||||
/* pixels */
|
||||
"&&&&&&&&&%%&&&&&",
|
||||
"&&&&&&&&%%%%&&&&",
|
||||
"&&&&&&&&%%%%&&&&",
|
||||
"&%%&&&%%%%%%%%%&",
|
||||
"%+o$&%+o@ooooo+%",
|
||||
"%o $-%X @X. .O%",
|
||||
"%o @$$X $=@ o$=&",
|
||||
"%o . $-# o&&&",
|
||||
"%o oOo. $-# o%&&",
|
||||
"%o $-%X $-# o%&&",
|
||||
"%X $&%X $-# o%&&",
|
||||
"%+o$&%+o$&$o+%&&",
|
||||
"%%%&&&%%&&&%%&&&",
|
||||
"%%&&&&&&&&&&&&&&",
|
||||
"%&&&&&&&&&&&&&&&",
|
||||
"&&&&&&&&&&&&&&&&"
|
||||
"................",
|
||||
"................",
|
||||
"................",
|
||||
"................",
|
||||
". ... . .",
|
||||
". ... . .",
|
||||
". ... ... ...",
|
||||
". ... ...",
|
||||
". ... ...",
|
||||
". ... ... ...",
|
||||
". ... ... ...",
|
||||
". ... ... ...",
|
||||
"................",
|
||||
"................",
|
||||
"................",
|
||||
"................"
|
||||
};
|
||||
|
||||
@@ -29,8 +29,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
========================================================================
|
||||
MAKEFILE PROJECT : libtest Project Overview
|
||||
|
||||
@@ -43,7 +43,8 @@ htsserver_LDADD = $(THREADS_LIBS) $(SOCKET_LIBS) libhttrack.la
|
||||
proxytrack_LDADD = $(THREADS_LIBS) $(SOCKET_LIBS)
|
||||
|
||||
httrack_CFLAGS = $(AM_CFLAGS) $(CFLAGS_PIE)
|
||||
proxytrack_CFLAGS = $(AM_CFLAGS) $(CFLAGS_PIE) -DNO_MALLOCT -DZLIB_CONST -DHTS_INTHASH_USES_MD5
|
||||
# -DHTS_NO_LIBHTTRACK: see htsnet.h's HTSNET_API guard.
|
||||
proxytrack_CFLAGS = $(AM_CFLAGS) $(CFLAGS_PIE) -DNO_MALLOCT -DZLIB_CONST -DHTS_INTHASH_USES_MD5 -DHTS_NO_LIBHTTRACK
|
||||
htsserver_CFLAGS = $(AM_CFLAGS) $(CFLAGS_PIE) -DZLIB_CONST -DHTS_INTHASH_USES_MD5
|
||||
|
||||
# @RPATH_ORIGIN_LDFLAGS@ makes a copied tree find libhttrack next to it (#906),
|
||||
@@ -59,7 +60,7 @@ htsserver_SOURCES = htsserver.c htsserver.h htsweb.c htsweb.h htsstats.h \
|
||||
htsurlport.c htsurlport.h
|
||||
proxytrack_SOURCES = proxy/main.c \
|
||||
proxy/proxytrack.c proxy/store.c \
|
||||
htsurlport.c htsurlport.h \
|
||||
htsurlport.c htsurlport.h htsnet.c \
|
||||
coucal/coucal.c htsmd5.c md5.c \
|
||||
minizip/ioapi.c minizip/mztools.c minizip/unzip.c minizip/zip.c
|
||||
|
||||
@@ -80,7 +81,7 @@ libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
|
||||
htsname.c htsrobots.c htstools.c htswizard.c \
|
||||
htsalias.c htsthread.c htsindex.c htsbauth.c \
|
||||
htscrashtest.c \
|
||||
htsmd5.c htscodec.c htswarc.c htschanges.c htssinglefile.c htssitemap.c htsproxy.c htszlib.c htswrap.c htsconcat.c \
|
||||
htsmd5.c htsnet.c htscodec.c htswarc.c htschanges.c htssinglefile.c htssitemap.c htsproxy.c htszlib.c htswrap.c htsconcat.c \
|
||||
htsmodules.c htscharset.c punycode.c htsencoding.c htssniff.c \
|
||||
md5.c \
|
||||
minizip/ioapi.c minizip/mztools.c minizip/unzip.c minizip/zip.c \
|
||||
|
||||
148
src/htsftp.c
148
src/htsftp.c
@@ -122,42 +122,48 @@ void launch_ftp(FTPDownloadStruct * params) {
|
||||
return 0; \
|
||||
}
|
||||
|
||||
/* Bounded split of a hostile-URL "user[:pass]@" prefix (see htsftp.h). */
|
||||
void ftp_split_userpass(const char *src, const char *end, char *user,
|
||||
size_t user_size, char *pass, size_t pass_size) {
|
||||
size_t n = 0;
|
||||
/* Split a hostile-URL "user[:pass]@" prefix (see htsftp.h). */
|
||||
hts_boolean ftp_split_userpass(const char *src, const char *end, char *user,
|
||||
size_t user_size, char *pass, size_t pass_size) {
|
||||
size_t len = 0, user_len, pass_len;
|
||||
const char *colon;
|
||||
|
||||
assertf(user_size > 0 && pass_size > 0); /* the size-1 math underflows on 0 */
|
||||
assertf(end > src); /* end is one past the '@' */
|
||||
|
||||
while (src[n] != '\0' && src[n] != ':') {
|
||||
if (n < user_size - 1)
|
||||
user[n] = src[n];
|
||||
n++;
|
||||
}
|
||||
user[n < user_size ? n : user_size - 1] = '\0';
|
||||
pass[0] = '\0';
|
||||
if (src[n] == ':') { // password follows the colon
|
||||
const size_t base = n + 1;
|
||||
size_t k = 0;
|
||||
|
||||
while (&src[base + k + 1] < end && src[base + k] != '\0') {
|
||||
if (k < pass_size - 1)
|
||||
pass[k] = src[base + k];
|
||||
k++;
|
||||
}
|
||||
pass[k < pass_size ? k : pass_size - 1] = '\0';
|
||||
}
|
||||
user[0] = pass[0] = '\0'; // fail safe for a caller that ignores the result
|
||||
while (len < (size_t) (end - src) - 1 && src[len] != '\0')
|
||||
len++;
|
||||
colon = memchr(src, ':', len);
|
||||
user_len = colon != NULL ? (size_t) (colon - src) : len;
|
||||
pass_len = colon != NULL ? len - user_len - 1 : 0;
|
||||
/* a clipped name is another account's, so refuse rather than log in as it */
|
||||
if (user_len >= user_size || pass_len >= pass_size)
|
||||
return HTS_FALSE;
|
||||
memcpy(user, src, user_len);
|
||||
user[user_len] = '\0';
|
||||
if (pass_len != 0)
|
||||
memcpy(pass, colon + 1, pass_len);
|
||||
pass[pass_len] = '\0';
|
||||
return HTS_TRUE;
|
||||
}
|
||||
|
||||
/* Build "<verb> <path>" (see htsftp.h). */
|
||||
void ftp_command(char *line, size_t line_size, const char *verb,
|
||||
const char *path) {
|
||||
hts_boolean ftp_command(char *line, size_t line_size, const char *verb,
|
||||
const char *path) {
|
||||
int n;
|
||||
|
||||
/* A leading '-' would reach a server that shells out to ls as a flag. */
|
||||
if (path[0] == '-' || strchr(path, ' ') != NULL ||
|
||||
strchr(path, '\"') != NULL || strchr(path, '\'') != NULL)
|
||||
snprintf(line, line_size, "%s \"%s\"", verb, path);
|
||||
n = snprintf(line, line_size, "%s \"%s\"", verb, path);
|
||||
else
|
||||
snprintf(line, line_size, "%s %s", verb, path);
|
||||
n = snprintf(line, line_size, "%s %s", verb, path);
|
||||
if (n < 0 || (size_t) n >= line_size) {
|
||||
line[0] = '\0'; // fail safe for a caller that ignores the result
|
||||
return HTS_FALSE;
|
||||
}
|
||||
return HTS_TRUE;
|
||||
}
|
||||
|
||||
/* MDTM reply "213 YYYYMMDDHHMMSS[.frac]" (RFC 3659, UTC) into tm_time. */
|
||||
@@ -211,7 +217,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
httrackp *opt = pStruct->pOpt;
|
||||
char user[256] = "anonymous";
|
||||
char pass[256] = "user@";
|
||||
char line_retr[2048];
|
||||
char line_retr[FTP_LINE_SIZE];
|
||||
int port = 21;
|
||||
|
||||
#if FTP_PASV
|
||||
@@ -251,7 +257,11 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
while(*real_adr == '/')
|
||||
real_adr++; // sauter /
|
||||
if ((adr = jump_identification(real_adr)) != real_adr) { // user
|
||||
ftp_split_userpass(real_adr, adr, user, sizeof(user), pass, sizeof(pass));
|
||||
if (!ftp_split_userpass_buf(real_adr, adr, user, pass)) {
|
||||
strcpybuff(back->r.msg, "FTP user name or password too long");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
_HALT_FTP return 0;
|
||||
}
|
||||
}
|
||||
// Calculer RETR <nom>
|
||||
{
|
||||
@@ -262,13 +272,19 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
if (strnotempty(a)) {
|
||||
const size_t len_a =
|
||||
strlen(unescape_http(ftp_path, sizeof(ftp_path), a));
|
||||
hts_boolean fits;
|
||||
|
||||
if (len_a > 0 &&
|
||||
ftp_path[len_a - 1] == '/') { /* obviously a directory listing */
|
||||
transfer_list = 1;
|
||||
ftp_command(line_retr, sizeof(line_retr), "LIST -A", ftp_path);
|
||||
fits = ftp_command_line(line_retr, "LIST -A", ftp_path);
|
||||
} else {
|
||||
ftp_command(line_retr, sizeof(line_retr), "RETR", ftp_path);
|
||||
fits = ftp_command_line(line_retr, "RETR", ftp_path);
|
||||
}
|
||||
if (!fits) {
|
||||
strcpybuff(back->r.msg, "FTP path too long");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
_HALT_FTP return 0;
|
||||
}
|
||||
} else {
|
||||
transfer_list = 1;
|
||||
@@ -298,6 +314,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
SOCaddr server;
|
||||
char *a;
|
||||
char _adr[256];
|
||||
size_t adr_len;
|
||||
const char *error = "unknown error";
|
||||
|
||||
_adr[0] = '\0';
|
||||
@@ -315,9 +332,16 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
back->r.statuscode = STATUSCODE_INVALID; // permanent, unlike a DNS miss
|
||||
_HALT_FTP return 0;
|
||||
}
|
||||
strncatbuff(_adr, adr, (int) (a - adr));
|
||||
adr_len = (size_t) (a - adr);
|
||||
} else
|
||||
strcpybuff(_adr, adr);
|
||||
adr_len = strlen(adr);
|
||||
// no resolvable name is this long, and clipping would query another host
|
||||
if (adr_len >= sizeof(_adr)) {
|
||||
htsblk_failf(&back->r, "Host name too long");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
_HALT_FTP return 0;
|
||||
}
|
||||
strncatbuff(_adr, adr, (int) adr_len);
|
||||
|
||||
// récupérer adresse résolue
|
||||
strcpybuff(back->info, "host name");
|
||||
@@ -356,7 +380,10 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
_CHECK_HALT_FTP;
|
||||
|
||||
{
|
||||
char BIGSTK line[1024];
|
||||
char BIGSTK line[FTP_LINE_SIZE];
|
||||
|
||||
/* line_retr is copied here verbatim; a narrower line[] would clip it. */
|
||||
HTS_COMPILE_ASSERT(sizeof(line) == sizeof(line_retr));
|
||||
|
||||
// envoi du login
|
||||
|
||||
@@ -514,9 +541,8 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
#endif
|
||||
// SIZE
|
||||
if (back->r.statuscode != -1) {
|
||||
if (!transfer_list) {
|
||||
ftp_command(line, sizeof(line), "SIZE", ftp_path);
|
||||
|
||||
// a clipped probe would size and date a different file
|
||||
if (!transfer_list && ftp_command_line(line, "SIZE", ftp_path)) {
|
||||
// SIZE?
|
||||
strcpybuff(back->info, "size");
|
||||
send_line(soc_ctl, line);
|
||||
@@ -537,22 +563,24 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
}
|
||||
|
||||
// MDTM?
|
||||
ftp_command(line, sizeof(line), "MDTM", ftp_path);
|
||||
strcpybuff(back->info, "mdtm");
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
_CHECK_HALT_FTP;
|
||||
if (ftp_parse_mdtm(line, &remote_tm)) {
|
||||
char date[256];
|
||||
if (ftp_command_line(line, "MDTM", ftp_path)) {
|
||||
strcpybuff(back->info, "mdtm");
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
_CHECK_HALT_FTP;
|
||||
if (ftp_parse_mdtm(line, &remote_tm)) {
|
||||
char date[256];
|
||||
|
||||
time_rfc822(date, &remote_tm);
|
||||
/* Stamp the mirror as the HTTP path does, so a later pass
|
||||
compares server-clock times instead of crossing clocks. */
|
||||
back->r.lastmodified[0] = '\0';
|
||||
strlncatbuff(back->r.lastmodified, date,
|
||||
sizeof(back->r.lastmodified),
|
||||
sizeof(back->r.lastmodified) - 1);
|
||||
remote_mtime = timegm(&remote_tm);
|
||||
time_rfc822(date, &remote_tm);
|
||||
/* Stamp the mirror as the HTTP path does, so a later pass
|
||||
compares server-clock times instead of crossing clocks.
|
||||
*/
|
||||
back->r.lastmodified[0] = '\0';
|
||||
strlncatbuff(back->r.lastmodified, date,
|
||||
sizeof(back->r.lastmodified),
|
||||
sizeof(back->r.lastmodified) - 1);
|
||||
remote_mtime = timegm(&remote_tm);
|
||||
}
|
||||
}
|
||||
|
||||
/* Only over a copy back_add() judged partial: on --update every
|
||||
@@ -570,7 +598,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
rest_understood = 1;
|
||||
} // else never mind
|
||||
}
|
||||
} // sinon tant pis
|
||||
} // sinon tant pis
|
||||
}
|
||||
}
|
||||
#if FTP_PASV
|
||||
@@ -613,7 +641,9 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
SOCaddr_initport(server, port_pasv);
|
||||
if (connect(soc_dat, &SOCaddr_sockaddr(server), SOCaddr_size(server)) == 0) {
|
||||
strcpybuff(back->info, "retr");
|
||||
strcpybuff(line, line_retr);
|
||||
// clip, never abort: this line is built from a crawled URL
|
||||
line[0] = '\0';
|
||||
strlncatbuff(line, line_retr, sizeof(line), sizeof(line) - 1);
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
_CHECK_HALT_FTP;
|
||||
@@ -660,7 +690,9 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
_CHECK_HALT_FTP;
|
||||
if (line[0] == '2') { // ok
|
||||
strcpybuff(back->info, "retr");
|
||||
strcpybuff(line, line_retr);
|
||||
// clip, never abort: this line is built from a crawled URL
|
||||
line[0] = '\0';
|
||||
strlncatbuff(line, line_retr, sizeof(line), sizeof(line) - 1);
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
_CHECK_HALT_FTP;
|
||||
@@ -922,7 +954,8 @@ FILE *dd = NULL;
|
||||
// routines de réception/émission
|
||||
// 0 = ERROR
|
||||
int send_line(T_SOC soc, const char *data) {
|
||||
char BIGSTK line[1024];
|
||||
char BIGSTK line[FTP_LINE_SIZE + 2]; // room for the CRLF of a maximal command
|
||||
int n;
|
||||
|
||||
// backstop: the driver fails earlier, but no injected byte reaches the wire
|
||||
if (!hts_is_control_free(data))
|
||||
@@ -942,7 +975,10 @@ int send_line(T_SOC soc, const char *data) {
|
||||
printf("---> %s", data);
|
||||
fflush(stdout);
|
||||
#endif
|
||||
snprintf(line, sizeof(line), "%s\x0d\x0a", data);
|
||||
// an unterminated command would blend into whatever the server reads next
|
||||
n = snprintf(line, sizeof(line), "%s\x0d\x0a", data);
|
||||
if (n < 0 || n >= (int) sizeof(line))
|
||||
return 0;
|
||||
if (check_socket_connect(soc) != 1) {
|
||||
#if FTP_DEBUG
|
||||
printf("!SOC WRITE ERROR\n");
|
||||
|
||||
33
src/htsftp.h
33
src/htsftp.h
@@ -59,6 +59,9 @@ struct FTPDownloadStruct {
|
||||
|
||||
/* Library internal definictions */
|
||||
#ifdef HTS_INTERNAL_BYTECODE
|
||||
/* Capacity of every FTP control-line buffer; send_line() adds the CRLF. */
|
||||
#define FTP_LINE_SIZE 1024
|
||||
|
||||
#if USE_BEGINTHREAD
|
||||
void launch_ftp(FTPDownloadStruct * params);
|
||||
void back_launch_ftp(void *pP);
|
||||
@@ -71,15 +74,29 @@ int run_launch_ftp(FTPDownloadStruct * params);
|
||||
int send_line(T_SOC soc, const char *data);
|
||||
int get_ftp_line(T_SOC soc, char *line, size_t line_size, int timeout);
|
||||
/* Split a "user[:pass]@" prefix (end = jump_identification result) into
|
||||
bounded, NUL-terminated user/pass buffers, truncating to fit.
|
||||
NUL-terminated user/pass buffers. Returns HTS_FALSE and empties both when a
|
||||
field does not fit, as a clipped one would name another account.
|
||||
Both sizes must be nonzero. */
|
||||
void ftp_split_userpass(const char *src, const char *end, char *user,
|
||||
size_t user_size, char *pass, size_t pass_size);
|
||||
/* Build "<verb> <path>" into line[line_size], truncating to fit. The path is
|
||||
quoted whenever a bare one would give the server a second token; it must
|
||||
already have been screened for control bytes. */
|
||||
void ftp_command(char *line, size_t line_size, const char *verb,
|
||||
const char *path);
|
||||
hts_boolean ftp_split_userpass(const char *src, const char *end, char *user,
|
||||
size_t user_size, char *pass, size_t pass_size);
|
||||
/* ftp_split_userpass() into the caller's fixed buffers; a buffer too wide for
|
||||
its "USER <user>" line would be clipped again when the command is built. */
|
||||
#define ftp_split_userpass_buf(src, end, user, pass) \
|
||||
(HTS_COMPILE_ASSERT(sizeof(user) + sizeof("USER ") - 1 <= FTP_LINE_SIZE && \
|
||||
sizeof(pass) + sizeof("PASS ") - 1 <= FTP_LINE_SIZE), \
|
||||
ftp_split_userpass((src), (end), (user), sizeof(user), (pass), \
|
||||
sizeof(pass)))
|
||||
/* Build "<verb> <path>" into line[line_size]. The path is quoted whenever a
|
||||
bare one would give the server a second token; it must already have been
|
||||
screened for control bytes. Returns HTS_FALSE and empties line when the
|
||||
command does not fit, as a clipped one would name a different file. */
|
||||
hts_boolean ftp_command(char *line, size_t line_size, const char *verb,
|
||||
const char *path);
|
||||
/* ftp_command() into a control line of the one capacity every FTP buffer has;
|
||||
anything narrower fails the build rather than refusing a path that fits. */
|
||||
#define ftp_command_line(line, verb, path) \
|
||||
(HTS_COMPILE_ASSERT(sizeof(line) == FTP_LINE_SIZE), \
|
||||
ftp_command((line), sizeof(line), (verb), (path)))
|
||||
T_SOC get_datasocket(char *to_send, size_t to_send_size);
|
||||
int stop_ftp(lien_back * back);
|
||||
char *linejmp(char *line);
|
||||
|
||||
@@ -273,6 +273,9 @@ typedef int hts_tristate;
|
||||
/* True when A is a non-NULL, non-empty string. */
|
||||
#define strnotempty(A) (((A) != NULL && (A)[0] != '\0'))
|
||||
|
||||
/* Compile-time check, usable as an expression. */
|
||||
#define HTS_COMPILE_ASSERT(cond) ((void) sizeof(char[(cond) ? 1 : -1]))
|
||||
|
||||
/* 'inline' where the dialect supports it (C++), nothing in plain C. */
|
||||
#ifdef __cplusplus
|
||||
#define HTS_INLINE inline
|
||||
|
||||
52
src/htsnet.c
Normal file
52
src/htsnet.c
Normal file
@@ -0,0 +1,52 @@
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
HTTrack Website Copier, Offline Browser for Windows and Unix
|
||||
Copyright (C) 2026 Xavier Roche and other contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Ethical use: we kindly ask that you NOT use this software to harvest email
|
||||
addresses or to collect any other private information about people. Doing so
|
||||
would dishonor our work and waste the many hours we have spent on it.
|
||||
|
||||
Please visit our Website: http://www.httrack.com
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* File: Out-of-line htsnet.h helpers, kept here so the installed */
|
||||
/* header needs nothing beyond strict ISO C */
|
||||
/* Author: Xavier Roche */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
#include "htsnet.h"
|
||||
|
||||
HTSNET_API void SOCaddr_inetntoa_(char *namebuf, size_t namebuflen,
|
||||
SOCaddr *const ss, const char *file,
|
||||
const int line) {
|
||||
assertf_(namebuf != NULL, file, line);
|
||||
assertf_(ss != NULL, file, line);
|
||||
|
||||
if (getnameinfo(&ss->m_addr.sa, sizeof(ss->m_addr), namebuf, namebuflen, NULL,
|
||||
0, NI_NUMERICHOST) == 0) {
|
||||
/* remove scope id(s) */
|
||||
char *const pos = strchr(namebuf, '%');
|
||||
if (pos != NULL) {
|
||||
*pos = '\0';
|
||||
}
|
||||
} else {
|
||||
namebuf[0] = '\0';
|
||||
}
|
||||
}
|
||||
31
src/htsnet.h
31
src/htsnet.h
@@ -273,25 +273,20 @@ static HTS_UNUSED socklen_t SOCaddr_copyaddr_(SOCaddr *const server,
|
||||
__LINE__); \
|
||||
} while (0)
|
||||
|
||||
/** Write the numeric (dotted/colon) host of ss into namebuf (capacity
|
||||
namebuflen), scope id stripped. On failure namebuf becomes "". */
|
||||
static HTS_UNUSED void SOCaddr_inetntoa_(char *namebuf, size_t namebuflen,
|
||||
SOCaddr *const ss, const char *file,
|
||||
const int line) {
|
||||
assertf_(namebuf != NULL, file, line);
|
||||
assertf_(ss != NULL, file, line);
|
||||
/* proxytrack compiles htsnet.c in rather than linking the library, and MSVC
|
||||
rejects a dllimport definition. */
|
||||
#ifdef HTS_NO_LIBHTTRACK
|
||||
#define HTSNET_API
|
||||
#else
|
||||
#define HTSNET_API HTSEXT_API
|
||||
#endif
|
||||
|
||||
if (getnameinfo(&ss->m_addr.sa, sizeof(ss->m_addr), namebuf, namebuflen, NULL,
|
||||
0, NI_NUMERICHOST) == 0) {
|
||||
/* remove scope id(s) */
|
||||
char *const pos = strchr(namebuf, '%');
|
||||
if (pos != NULL) {
|
||||
*pos = '\0';
|
||||
}
|
||||
} else {
|
||||
namebuf[0] = '\0';
|
||||
}
|
||||
}
|
||||
/** Write the numeric (dotted/colon) host of ss into namebuf (capacity
|
||||
namebuflen), scope id stripped. On failure namebuf becomes "". Out of line:
|
||||
getnameinfo() isn't declared to a strict-ISO translation unit (#1001). */
|
||||
HTSNET_API void SOCaddr_inetntoa_(char *namebuf, size_t namebuflen,
|
||||
SOCaddr *const ss, const char *file,
|
||||
const int line);
|
||||
|
||||
/** Numeric host of ss into namebuf (capacity namebuflen); "" on failure. */
|
||||
#define SOCaddr_inetntoa(namebuf, namebuflen, ss) \
|
||||
|
||||
@@ -4501,45 +4501,153 @@ static int st_ftpline(httrackp *opt, int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ftp_split_userpass: well-formed split, plus a hostile over-long userinfo
|
||||
that pre-fix overran user[256]/pass[256]. */
|
||||
/* ftp_split_userpass: the split itself, and userinfo refused rather than
|
||||
clipped into another account's name (#1032). */
|
||||
static int st_ftpuser(httrackp *opt, int argc, char **argv) {
|
||||
char user[256], pass[256];
|
||||
char in[1200];
|
||||
static const size_t caps[] = {16, 256}; /* asymmetric: a shared bound shows */
|
||||
char ubuf[256 + 32], pbuf[sizeof(ubuf)], poison[sizeof(ubuf)];
|
||||
char in[2 * 256 + 8];
|
||||
size_t c, over;
|
||||
|
||||
(void) opt;
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
memset(poison, '#', sizeof(poison));
|
||||
{
|
||||
const char ok[] = "bob:secret@host/f"; // '@' at index 10
|
||||
|
||||
ftp_split_userpass(ok, ok + 11, user, sizeof(user), pass, sizeof(pass));
|
||||
assertf(strcmp(user, "bob") == 0);
|
||||
assertf(strcmp(pass, "secret") == 0);
|
||||
assertf(ftp_split_userpass(ok, ok + 11, ubuf, sizeof(ubuf), pbuf,
|
||||
sizeof(pbuf)) == HTS_TRUE);
|
||||
assertf(strcmp(ubuf, "bob") == 0);
|
||||
assertf(strcmp(pbuf, "secret") == 0);
|
||||
}
|
||||
memset(in, 'u', 400);
|
||||
in[400] = ':';
|
||||
memset(in + 401, 'p', 400);
|
||||
in[801] = '@';
|
||||
in[802] = '\0';
|
||||
ftp_split_userpass(in, in + 802, user, sizeof(user), pass, sizeof(pass));
|
||||
assertf(strlen(user) == sizeof(user) - 1);
|
||||
assertf(strlen(pass) == sizeof(pass) - 1);
|
||||
{
|
||||
/* tight sizes + guard byte catch an off-by-one the 256 case can't */
|
||||
char ubuf[16], pbuf[16];
|
||||
const char ok[] = "bob@host/f"; // no password: the '@' still ends the user
|
||||
|
||||
memset(ubuf, 'Z', sizeof(ubuf));
|
||||
memset(pbuf, 'Z', sizeof(pbuf));
|
||||
ftp_split_userpass(in, in + 802, ubuf, 8, pbuf, 8);
|
||||
assertf(strcmp(ubuf, "uuuuuuu") == 0);
|
||||
assertf(strcmp(pbuf, "ppppppp") == 0);
|
||||
assertf(ubuf[8] == 'Z' && pbuf[8] == 'Z');
|
||||
assertf(ftp_split_userpass(ok, ok + 4, ubuf, sizeof(ubuf), pbuf,
|
||||
sizeof(pbuf)) == HTS_TRUE);
|
||||
assertf(strcmp(ubuf, "bob") == 0);
|
||||
assertf(pbuf[0] == '\0');
|
||||
}
|
||||
{
|
||||
const char ok[] = "u@relay:pw@gw/f"; // only the last '@' ends the userinfo
|
||||
|
||||
assertf(ftp_split_userpass(ok, ok + 11, ubuf, sizeof(ubuf), pbuf,
|
||||
sizeof(pbuf)) == HTS_TRUE);
|
||||
assertf(strcmp(ubuf, "u@relay") == 0);
|
||||
assertf(strcmp(pbuf, "pw") == 0);
|
||||
}
|
||||
for (c = 0; c < sizeof(caps) / sizeof(caps[0]); c++) {
|
||||
const size_t ucap = caps[c], pcap = caps[1 - c];
|
||||
|
||||
/* overshoot the user, the pass, then a bare name bounded only by '@' */
|
||||
for (over = 0; over <= 2; over++) {
|
||||
const size_t cap = over == 1 ? pcap : ucap;
|
||||
size_t len;
|
||||
|
||||
for (len = cap - 2; len <= cap + 1; len++) {
|
||||
const size_t user_len = over == 1 ? 1 : len;
|
||||
const size_t pass_len = over == 0 ? 1 : (over == 1 ? len : 0);
|
||||
const size_t total = user_len + pass_len + (over == 2 ? 1 : 2);
|
||||
const hts_boolean fits = len < cap ? HTS_TRUE : HTS_FALSE;
|
||||
|
||||
memset(in, 'u', user_len);
|
||||
if (over != 2) {
|
||||
in[user_len] = ':';
|
||||
memset(in + user_len + 1, 'p', pass_len);
|
||||
}
|
||||
in[total - 1] = '@';
|
||||
in[total] = '\0';
|
||||
memcpy(ubuf, poison, sizeof(ubuf)); /* a zero canary would hide a NUL */
|
||||
memcpy(pbuf, poison, sizeof(pbuf));
|
||||
assertf(ftp_split_userpass(in, in + total, ubuf, ucap, pbuf, pcap) ==
|
||||
fits);
|
||||
if (fits) {
|
||||
assertf(strlen(ubuf) == user_len && strlen(pbuf) == pass_len);
|
||||
assertf(ubuf[user_len - 1] == 'u');
|
||||
assertf(pass_len == 0 || pbuf[pass_len - 1] == 'p');
|
||||
} else {
|
||||
assertf(ubuf[0] == '\0' && pbuf[0] == '\0'); /* fail safe */
|
||||
}
|
||||
/* the whole tail: one canary byte misses a write just past it */
|
||||
assertf(memcmp(ubuf + ucap, poison, sizeof(ubuf) - ucap) == 0);
|
||||
assertf(memcmp(pbuf + pcap, poison, sizeof(pbuf) - pcap) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
printf("ftp-userpass self-test OK\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Both quoting forms at two capacities: the quoted form is two bytes wider
|
||||
(#1019). */
|
||||
static int st_ftpcmdlen(httrackp *opt, int argc, char **argv) {
|
||||
static const size_t caps[] = {32, FTP_LINE_SIZE};
|
||||
char BIGSTK buf[FTP_LINE_SIZE + 32];
|
||||
char BIGSTK poison[FTP_LINE_SIZE + 32];
|
||||
char BIGSTK path[FTP_LINE_SIZE + 2];
|
||||
char BIGSTK wire[FTP_LINE_SIZE * 2];
|
||||
size_t c, got = 0;
|
||||
T_SOC sv[2];
|
||||
|
||||
(void) opt;
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
memset(poison, '#', sizeof(poison));
|
||||
for (c = 0; c < sizeof(caps) / sizeof(caps[0]); c++) {
|
||||
const size_t cap = caps[c];
|
||||
int quoted;
|
||||
|
||||
for (quoted = 0; quoted <= 1; quoted++) {
|
||||
const size_t verb = 5 + 2 * (size_t) quoted; /* "RETR " plus quotes */
|
||||
const size_t fit = cap - 1 - verb; /* longest path still fitting */
|
||||
size_t len;
|
||||
|
||||
for (len = fit - 1; len <= fit + 1; len++) {
|
||||
memset(path, 'p', len);
|
||||
path[len] = '\0';
|
||||
if (quoted)
|
||||
path[0] = ' '; /* any of these forces the quoted form */
|
||||
memcpy(buf, poison, sizeof(buf)); /* a zero canary would hide a NUL */
|
||||
if (len > fit) {
|
||||
assertf(ftp_command(buf, cap, "RETR", path) == HTS_FALSE);
|
||||
assertf(buf[0] == '\0'); /* fail-safe for an ignored result */
|
||||
} else {
|
||||
assertf(ftp_command(buf, cap, "RETR", path) == HTS_TRUE);
|
||||
assertf(strlen(buf) == verb + len);
|
||||
assertf(strncmp(buf, "RETR ", 5) == 0);
|
||||
assertf(buf[verb + len - 1] == (quoted ? '\"' : 'p'));
|
||||
}
|
||||
/* the whole tail: one canary byte misses a write just past it */
|
||||
assertf(memcmp(buf + cap, poison, sizeof(buf) - cap) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* send_line() adds the CRLF and drops, rather than truncates, an over-length
|
||||
line. */
|
||||
memset(path, 'q', FTP_LINE_SIZE);
|
||||
path[FTP_LINE_SIZE] = '\0';
|
||||
assertf(st_socketpair(sv) == 0);
|
||||
assertf(send_line(sv[0], path) == 0); /* one byte too long: never sent */
|
||||
path[FTP_LINE_SIZE - 1] = '\0';
|
||||
assertf(send_line(sv[0], path) != 0);
|
||||
deletesoc(sv[0]);
|
||||
for (;;) {
|
||||
const int n = (int) recv(sv[1], wire + got, (int) (sizeof(wire) - got), 0);
|
||||
|
||||
if (n <= 0)
|
||||
break;
|
||||
got += (size_t) n;
|
||||
}
|
||||
deletesoc(sv[1]);
|
||||
assertf(got == FTP_LINE_SIZE + 1); /* the maximal command alone */
|
||||
assertf(memcmp(wire, path, FTP_LINE_SIZE - 1) == 0);
|
||||
assertf(memcmp(wire + FTP_LINE_SIZE - 1, "\r\n", 2) == 0);
|
||||
printf("ftp-cmdlen self-test OK (%d bytes sent)\n", (int) got);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* send_line() must drop a command line carrying a control byte (#1010). */
|
||||
static int st_ftpctrl(httrackp *opt, int argc, char **argv) {
|
||||
/* Verb and URL path as run_launch_ftp() hands them over, then the line the
|
||||
@@ -8120,6 +8228,8 @@ static const struct selftest_entry {
|
||||
{"ftp-userpass", "", "ftp_split_userpass bounds URL userinfo", st_ftpuser},
|
||||
{"ftp-ctrlchars", "", "send_line rejects a control byte in an FTP command",
|
||||
st_ftpctrl},
|
||||
{"ftp-cmdlen", "",
|
||||
"an FTP command too long for its control line is refused", st_ftpcmdlen},
|
||||
{"warc", "<dir>", "WARC/1.1 writer: framing, digests, revisit dedup",
|
||||
st_warc},
|
||||
{"warc-trunc", "<dir>", "WARC-Truncated on a cap-truncated body",
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
<ClCompile Include="htscmdline.c" />
|
||||
<ClCompile Include="htsurlport.c" />
|
||||
<ClCompile Include="htsmd5.c" />
|
||||
<ClCompile Include="htsnet.c" />
|
||||
<ClCompile Include="htsmodules.c" />
|
||||
<ClCompile Include="htsname.c" />
|
||||
<ClCompile Include="htsparse.c" />
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<ClCompile>
|
||||
<!-- Matches proxytrack_CFLAGS in Makefile.am. Standalone: it does not link
|
||||
libhttrack, it only borrows headers (hts_effective_mime is a macro). -->
|
||||
<PreprocessorDefinitions>WIN32;_CONSOLE;_MBCS;NO_MALLOCT;ZLIB_CONST;HTS_INTHASH_USES_MD5;ZLIB_DLL;WINVER=0x0601;_WIN32_WINNT=0x0601;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>WIN32;_CONSOLE;_MBCS;NO_MALLOCT;HTS_NO_LIBHTTRACK;ZLIB_CONST;HTS_INTHASH_USES_MD5;ZLIB_DLL;WINVER=0x0601;_WIN32_WINNT=0x0601;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory);$(MSBuildThisFileDirectory)coucal;$(MSBuildThisFileDirectory)proxy;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
@@ -100,6 +100,7 @@
|
||||
<ClCompile Include="proxy\proxytrack.c" />
|
||||
<ClCompile Include="proxy\store.c" />
|
||||
<ClCompile Include="htsurlport.c" />
|
||||
<ClCompile Include="htsnet.c" />
|
||||
<ClCompile Include="coucal\coucal.c" />
|
||||
<ClCompile Include="htsmd5.c" />
|
||||
<ClCompile Include="md5.c" />
|
||||
|
||||
@@ -1,8 +1,43 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Over-long FTP userinfo must fail the link, never log in under a clipped name
|
||||
# (#1032).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ftp_split_userpass bounds an over-long user:pass@ from a hostile ftp:// URL.
|
||||
out=$(httrack -O /dev/null -#test=ftp-userpass run)
|
||||
grep -q "ftp-userpass self-test OK" <<<"$out"
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftpuser.XXXXXX") || exit 1
|
||||
trap 'set +e; rm -rf "${tmpdir}"' EXIT
|
||||
trap 'exit 1' HUP INT QUIT TERM
|
||||
|
||||
out=$(httrack -O "${tmpdir}/st" "-#test=ftp-userpass" run 2>&1) ||
|
||||
fail "self-test exited non-zero: ${out}"
|
||||
grep -q "ftp-userpass self-test OK" <<<"${out}" || fail "unexpected output: ${out}"
|
||||
|
||||
# The reachable half: a URL carries the userinfo straight into user[256]/pass[256].
|
||||
rep() { awk -v n="$1" 'BEGIN { while (i++ < n) printf "a" }'; }
|
||||
|
||||
probe() {
|
||||
rm -rf "${tmpdir}/mir"
|
||||
httrack "ftp://$1@127.0.0.1:1/f.txt" -O "${tmpdir}/mir" -q >/dev/null 2>&1 ||
|
||||
fail "an FTP URL with ${#1}-byte userinfo crashed the engine"
|
||||
cat "${tmpdir}/mir/hts-log.txt"
|
||||
}
|
||||
|
||||
# The bare name has no ':' to bound it, only the '@', and is the worse branch.
|
||||
for u in "$(rep 256):pw" "u:$(rep 256)" "$(rep 256)"; do
|
||||
log=$(probe "${u}")
|
||||
grep -q "FTP user name or password too long" <<<"${log}" ||
|
||||
fail "over-long userinfo was not refused: ${log}"
|
||||
done
|
||||
|
||||
# 255 still fits, and a plain login is the outsider a widened gate would refuse.
|
||||
for u in "$(rep 255):pw" "u:$(rep 255)" "$(rep 255)" "bob:secret"; do
|
||||
log=$(probe "${u}")
|
||||
grep -q "Unable to connect to the server" <<<"${log}" ||
|
||||
fail "userinfo that fits did not reach the connect: ${log}"
|
||||
done
|
||||
|
||||
@@ -52,19 +52,7 @@ serverlog="${tmpdir}/server.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" \
|
||||
--mode-file "$(nativepath "$mode")" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "ftp server exited early: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
host="127.0.0.1_${port}"
|
||||
urls=()
|
||||
for name in keep empty stay; do
|
||||
|
||||
@@ -23,8 +23,11 @@ fail() {
|
||||
}
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_sutmo.XXXXXX") || exit 1
|
||||
trap 'set +e; rm -rf "$tmp"' EXIT
|
||||
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
|
||||
# A colon-free root for the PATH shim below: MSYS hands out a drive-letter TMPDIR
|
||||
# and a PATH entry carrying a colon is read as two, which leaves it unreachable.
|
||||
shim=$(mktemp -d /tmp/httrack_sutmoshim.XXXXXX 2>/dev/null) || shim="$tmp/starve"
|
||||
trap 'set +e; rm -rf "$tmp" "$shim"' EXIT
|
||||
trap 'rm -rf "$tmp" "$shim"' HUP INT QUIT PIPE TERM
|
||||
out="$tmp/out"
|
||||
|
||||
# --- a hanging test is killed, named, and reported 124 ----------------------
|
||||
@@ -71,11 +74,14 @@ sleep 1
|
||||
# --- the budget is wall clock, not a count of poll iterations ----------------
|
||||
# Starve the poll and a counted budget stretches with it: 10 polls of a 0.1s tick
|
||||
# become 40s, so the guard fires long after the step it was meant to beat (#795).
|
||||
# HTTRACK_POLL_SLEEP forces the forked tick, the only one a shimmed sleep reaches;
|
||||
# the loop it stretches is the same one poll_wait's fd tick drives.
|
||||
(
|
||||
starve_sleep "$tmp/starve" 4 || fail "could not install the slow sleep"
|
||||
starve_sleep "$shim" 4 || fail "could not install the slow sleep"
|
||||
start=$SECONDS
|
||||
rc=0
|
||||
HTTRACK_TEST_TIMEOUT=1 bash "$driver" "$tmp/90_wedged.test" >"$out" 2>&1 || rc=$?
|
||||
HTTRACK_POLL_SLEEP=1 HTTRACK_TEST_TIMEOUT=1 \
|
||||
bash "$driver" "$tmp/90_wedged.test" >"$out" 2>&1 || rc=$?
|
||||
elapsed=$((SECONDS - start))
|
||||
test "$rc" -eq 124 || fail "starved guard reported $rc, want 124"
|
||||
# Generous: the diagnostics dump runs inside this window too.
|
||||
@@ -145,23 +151,44 @@ if ! is_windows && test -r /proc/self/stat; then
|
||||
mkdir -p "$tmp/nops"
|
||||
printf '#!/bin/sh\nexit 127\n' >"$tmp/nops/ps"
|
||||
chmod +x "$tmp/nops/ps"
|
||||
# Both lists match on the basename alone, so any long-lived binary will do.
|
||||
ln -sf "$(command -v sleep)" "$tmp/httrack"
|
||||
# A shell copy, not a renamed sleep: uutils refuses an argv[0] it does not own
|
||||
# (#1042). Either works, since both lists match on the basename alone.
|
||||
cp "${BASH:-$(command -v sh)}" "$tmp/httrack" || fail "no shell to copy"
|
||||
"$tmp/httrack" -c : || fail "cannot exec out of $tmp (noexec?)"
|
||||
# Read-write, so the open does not block and the timeout can bound it: the
|
||||
# fakes then fork nothing for the SIGABRT to orphan, and a test killed
|
||||
# outright leaks no process named httrack onto the runner.
|
||||
mkfifo "$tmp/hold"
|
||||
hold="read -t 60 _ <>'$tmp/hold'"
|
||||
# Started from a subshell so ppid and pgid differ: swapping those two columns
|
||||
# is invisible whenever the test shell happens to lead its own group.
|
||||
(
|
||||
"$tmp/httrack" 60 &
|
||||
"$tmp/httrack" -c "$hold" &
|
||||
echo $! >"$tmp/fake.pid"
|
||||
)
|
||||
# Its own group, to prove the filter keeps an outsider out.
|
||||
(
|
||||
set -m
|
||||
"$tmp/httrack" 60 &
|
||||
"$tmp/httrack" -c "$hold" &
|
||||
echo $! >"$tmp/outside.pid"
|
||||
)
|
||||
fake=$(cat "$tmp/fake.pid")
|
||||
outside=$(cat "$tmp/outside.pid")
|
||||
trap 'set +e; kill "$fake" "$outside" 2>/dev/null; rm -rf "$tmp"' EXIT
|
||||
# /proc holds the pid from the fork on, so a fake that never exec'd would fail
|
||||
# some later assertion instead of this one.
|
||||
running() {
|
||||
local i=0 argv0
|
||||
while test "$i" -lt 20; do
|
||||
{ read -r -d '' argv0 <"/proc/$1/cmdline"; } 2>/dev/null &&
|
||||
test "${argv0##*/}" = httrack && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
running "$fake" || fail "the fake engine never started"
|
||||
running "$outside" || fail "the outsider never started"
|
||||
# ppid and pgid of $1: the oracle the listing is checked against.
|
||||
procfields() { awk '{ sub(/.*\) /, ""); print $2, $3 }' "/proc/$1/stat"; }
|
||||
procstate() { awk '{ sub(/.*\) /, ""); print $1 }' "/proc/$1/stat" 2>/dev/null || echo gone; }
|
||||
@@ -186,16 +213,37 @@ if ! is_windows && test -r /proc/self/stat; then
|
||||
grep -qx "$fake" <<<"$engines" || fail "no-ps engine list lost pid $fake"
|
||||
! grep -qx "$outside" <<<"$engines" ||
|
||||
fail "no-ps engine list reached outside the group (pid $outside)"
|
||||
# A platform with no stack mechanism must say so and leave the engine
|
||||
# alone. Shimmed, because no CI leg is one: the Hurd branch below runs
|
||||
# only on the buildds that failed for want of it.
|
||||
# shellcheck disable=SC2016 # the shim reads them, not us
|
||||
printf '#!/bin/sh\ncase "$1" in -s) echo GNU;; *) exec %s "$@";; esac\n' \
|
||||
"$(command -v uname)" >"$tmp/nops/uname"
|
||||
chmod +x "$tmp/nops/uname"
|
||||
grep -q "no stack mechanism known for GNU" <<<"$(request_engine_backtraces "$pgid")" ||
|
||||
fail "a platform with no stack mechanism reported nothing"
|
||||
case "$(procstate "$fake")" in
|
||||
Z | gone) fail "the no-mechanism branch signalled pid $fake" ;;
|
||||
esac
|
||||
rm -f "$tmp/nops/uname"
|
||||
|
||||
# The chain, not just its input: this is what reported nothing on Fedora.
|
||||
stacks=$(request_engine_backtraces "$pgid")
|
||||
! grep -q 'no engine process left' <<<"$stacks" ||
|
||||
fail "no-ps stack request found no engine to signal: $stacks"
|
||||
# Gone, or a zombie: the fake is reparented, and a container's pid 1 does
|
||||
# not always reap. kill -0 alone succeeds on a zombie and proves nothing.
|
||||
case "$(procstate "$fake")" in
|
||||
Z | gone) ;;
|
||||
*) fail "the SIGABRT never reached pid $fake" ;;
|
||||
esac
|
||||
# Only Linux signals; elsewhere (Hurd's uname reports "GNU") the chain
|
||||
# owes the reader its no-mechanism report instead of silence.
|
||||
if test "$(uname -s)" = Linux; then
|
||||
# Gone, or a zombie: the fake is reparented, and a container's pid 1
|
||||
# does not always reap. kill -0 succeeds on a zombie, proving nothing.
|
||||
case "$(procstate "$fake")" in
|
||||
Z | gone) ;;
|
||||
*) fail "the SIGABRT never reached pid $fake" ;;
|
||||
esac
|
||||
else
|
||||
grep -q 'no stack mechanism known' <<<"$stacks" ||
|
||||
fail "no stack mechanism was reported on $(uname -s): $stacks"
|
||||
fi
|
||||
# With neither source the dump must say so, not print an empty section.
|
||||
# shellcheck disable=SC2317 # reached through ps_snapshot
|
||||
proc_snapshot() { return 1; }
|
||||
@@ -205,6 +253,34 @@ if ! is_windows && test -r /proc/self/stat; then
|
||||
kill "$fake" "$outside" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# --- an emulated buildd hides the engine behind its binfmt interpreter -------
|
||||
# Synthetic rows: no runner here has qemu-user, and the column shift is the point.
|
||||
if ! is_windows; then
|
||||
(
|
||||
# shellcheck disable=SC2317 # reached through the two matchers below
|
||||
ps_snapshot() {
|
||||
cat <<'EOF'
|
||||
PID PPID PGID ELAPSED S COMMAND
|
||||
11 1 40 12 S /usr/libexec/qemu-binfmt/hppa-binfmt-P /bld/src/httrack -q http://h/
|
||||
12 1 40 12 S /usr/bin/qemu-hppa-static /usr/bin/python3 /t/local-server.py 8080
|
||||
13 1 40 12 S /bld/src/httrack -q http://h/
|
||||
14 1 40 12 S /usr/bin/strace /bld/src/httrack -q http://h/
|
||||
15 1 40 12 S /usr/bin/qemu-img convert /srv/local-server.py out.raw
|
||||
16 1 40 12 S /usr/bin/qemu-nbd /mnt/httrack
|
||||
17 1 40 12 S /opt/my-custom-binfmt /mnt/httrack
|
||||
EOF
|
||||
}
|
||||
# Rows 14 to 17 are the controls: shifting past anything but an emulator
|
||||
# would take a wrapper, or a disk image, for the engine and kill it.
|
||||
engines=$(list_engine_pids 40)
|
||||
test "$engines" = $'11\n13' ||
|
||||
fail "emulated engine list is '$engines', want pids 11 and 13"
|
||||
named=$(list_stray_processes 0 named | awk 'NR > 1 { print $1 }')
|
||||
test "$named" = $'11\n12\n13' ||
|
||||
fail "emulated stray list is '$named', want pids 11, 12 and 13"
|
||||
)
|
||||
fi
|
||||
|
||||
# --- a wedged crawl yields a symbolized engine stack ------------------------
|
||||
# The whole point of the dump: name the frame the engine is stuck in. Windows has
|
||||
# neither half (MSYS signals do not reach a native httrack.exe, and that build has
|
||||
|
||||
@@ -52,19 +52,7 @@ done
|
||||
serverlog="${tmpdir}/server.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "ftp server exited early: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
host="127.0.0.1_${port}"
|
||||
|
||||
urls=()
|
||||
|
||||
@@ -64,19 +64,7 @@ serverlog="${tmpdir}/server.out"
|
||||
--mode-file "$(nativepath "$mode")" --log "$(nativepath "$cmds")" \
|
||||
>"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "ftp server exited early: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
host="127.0.0.1_${port}"
|
||||
url="ftp://127.0.0.1:${port}/a.bin"
|
||||
mirror="${out}/${host}/a.bin"
|
||||
|
||||
@@ -79,19 +79,7 @@ serverlog="${tmpdir}/server.out"
|
||||
--mode-file "$(nativepath "$mode")" --log "$(nativepath "$cmds")" \
|
||||
>"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "ftp server exited early: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
host="127.0.0.1_${port}"
|
||||
url="ftp://127.0.0.1:${port}/a.bin"
|
||||
mirror="${out}/${host}/a.bin"
|
||||
|
||||
@@ -126,8 +126,13 @@ wait_drained() { # log
|
||||
|
||||
quietport=$(freeport)
|
||||
: >"$dir/quiet.log" # the drainer creates it asynchronously
|
||||
env -u HTS_LOG "$python" -c "$pty_exec" "$dir/quiet.log" \
|
||||
proxytrack "127.0.0.1:$quietport" "127.0.0.1:$(freeport)" "$dir/in.arc" &
|
||||
# Subshell, not env(1): uutils' env forks rather than execs, so stop_server would
|
||||
# kill the wrapper and leave proxytrack holding the pty open (#1042).
|
||||
(
|
||||
unset HTS_LOG
|
||||
exec "$python" -c "$pty_exec" "$dir/quiet.log" \
|
||||
proxytrack "127.0.0.1:$quietport" "127.0.0.1:$(freeport)" "$dir/in.arc"
|
||||
) &
|
||||
quietpid=$!
|
||||
wait_listen "$dir/quiet.log" "$quietpid"
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Before the driver is sourced: with these set, anything it starts posts a commit
|
||||
# status on whatever the surrounding shell was last looking at.
|
||||
unset WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_URL
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/ci-windows-suite.sh
|
||||
. "${testdir}/ci-windows-suite.sh"
|
||||
@@ -138,7 +142,8 @@ test "$killed" -le 10080 || fail "killed at $killed, want 9960 within a tick"
|
||||
|
||||
# taskkill is a grandchild of its own target, so a leaves-first /T would reap the
|
||||
# watchdog before the root (#953): kill_tree here never returns, and the stubs
|
||||
# record the order, which the code under test cannot write to.
|
||||
# record the order, which the code under test cannot write to. The reporter goes
|
||||
# first: the kill runs no EXIT trap, and the tree walk reaches this heartbeat.
|
||||
rec="$tmp/killed"
|
||||
: >"$rec"
|
||||
vnow=9000
|
||||
@@ -146,6 +151,7 @@ ticks=0
|
||||
printf 'RUN 96_wedged.test at 0s\n' >"$progress"
|
||||
rc=0
|
||||
(
|
||||
watchdog=777
|
||||
kill_pid() { echo "DIRECT $1" >>"$rec"; }
|
||||
kill_tree() {
|
||||
echo "TREE $1" >>"$rec"
|
||||
@@ -154,10 +160,12 @@ rc=0
|
||||
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 4242" ||
|
||||
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" ||
|
||||
fail "the target was not signalled directly ahead of the tree walk: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n 2p "$rec")" = "TREE 4242" ||
|
||||
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 2 || fail "extra kills: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n '$=' "$rec")" -eq 3 || fail "extra kills: $(tr '\n' '/' <"$rec")"
|
||||
|
||||
echo "heartbeat OK"
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Before the driver is sourced: with these set, anything it starts posts a commit
|
||||
# status on whatever the surrounding shell was last looking at.
|
||||
unset WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_URL
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
driver="$testdir/ci-windows-suite.sh"
|
||||
|
||||
@@ -39,7 +43,8 @@ grep -q usage <<<"$out" || fail "argless run said: $out"
|
||||
# sibling's.
|
||||
run=$tmp/run
|
||||
mkdir -p "$run"
|
||||
cp "$driver" "$testdir/testlib.sh" "$testdir/test-timeout.sh" "$run/"
|
||||
cp "$driver" "$testdir/testlib.sh" "$testdir/test-timeout.sh" \
|
||||
"$testdir/ci-windows-watchdog.ps1" "$run/"
|
||||
chmod u+w "$run"/*.sh # distcheck's srcdir is read-only, and cp carries that over
|
||||
cat >>"$run/testlib.sh" <<'EOF'
|
||||
reap_leftover_processes() { return 0; }
|
||||
@@ -48,26 +53,69 @@ EOF
|
||||
bin=$tmp/bin
|
||||
mkdir -p "$bin"
|
||||
printf '#!/bin/sh\nexit 0\n' >"$bin/httrack"
|
||||
# shellcheck disable=SC2016 # $2 is the stub's own argument
|
||||
printf '#!/bin/sh\necho "$2"\n' >"$bin/cygpath"
|
||||
# The first child the driver forks: the token has to be gone before it runs.
|
||||
# shellcheck disable=SC2016 # $2 and the token are the stub's own expansions
|
||||
printf '#!/bin/sh\necho "TOKEN=${WATCHDOG_TOKEN:-}" >>%s\necho "$2"\n' "$tmp/cygpathtoken" \
|
||||
>"$bin/cygpath"
|
||||
chmod +x "$bin/httrack" "$bin/cygpath"
|
||||
test -x "$bin/httrack" || {
|
||||
# The driver puts this bindir first on PATH, so this is the interpreter it finds.
|
||||
# Unconditional, because a real PowerShell here runs the real watchdog, and only
|
||||
# the scrub above stands between that and api.github.com.
|
||||
wdargv=$tmp/wdargv
|
||||
wdmark=$tmp/wdmark
|
||||
: >"$wdargv"
|
||||
cat >"$bin/pwsh" <<EOF
|
||||
#!/bin/sh
|
||||
{ echo "TOKEN=\${WATCHDOG_TOKEN:-}"; printf '%s\n' "\$@"; } >>"$wdargv"
|
||||
echo "watchdog ready"
|
||||
# Ticks rather than sleeps: a pid can be reused, a running reporter cannot.
|
||||
n=0
|
||||
while :; do
|
||||
n=\$((n + 1))
|
||||
echo "\$n" >"$wdmark.part"
|
||||
mv "$wdmark.part" "$wdmark"
|
||||
sleep 0.2
|
||||
done
|
||||
EOF
|
||||
chmod +x "$bin/pwsh"
|
||||
# Ahead of any command -v, which asks access(X_OK) and so reads false here.
|
||||
test -x "$bin/pwsh" || {
|
||||
echo "SKIP: ${TMPDIR:-/tmp} is noexec, the stub bindir cannot be run"
|
||||
exit 77
|
||||
}
|
||||
test "$(PATH="$bin:$PATH" command -v pwsh)" = "$bin/pwsh" ||
|
||||
fail "the stub does not shadow PowerShell: $(PATH="$bin:$PATH" command -v pwsh)"
|
||||
|
||||
# The stub advances $wdmark every 0.2s, so a marker that moves says it still runs.
|
||||
# Sampled three times, or a loaded box starving the stub would read as a kill.
|
||||
still_ticking() {
|
||||
local a b
|
||||
a=$(cat "$wdmark" 2>/dev/null || true)
|
||||
for _ in 1 2 3; do
|
||||
sleep 1
|
||||
b=$(cat "$wdmark" 2>/dev/null || true)
|
||||
test -n "$b" && test "$a" != "$b" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# One per glob the driver enumerates: an empty category is counted as a failing
|
||||
# test named after the unexpanded pattern, which would drown the accounting.
|
||||
for t in 00_runnable 10_engine-pass 13_zlib-pass 14_local-pass 15_watchdog-pass \
|
||||
for t in 00_runnable 13_zlib-pass 14_local-pass 15_watchdog-pass \
|
||||
16_crawl_proxy_https 17_crawl-log-salvage; do
|
||||
printf '#!/bin/sh\nexit 0\n' >"$run/$t.test"
|
||||
done
|
||||
# One test reports back what it inherited: the token the step is handed can post
|
||||
# commit statuses, and a forged one has already reached a commit under test.
|
||||
# shellcheck disable=SC2016 # the stub expands it, not this shell
|
||||
printf '#!/bin/sh\necho "TOKEN=${WATCHDOG_TOKEN:-}" >%s\nexit 0\n' "$tmp/childtoken" \
|
||||
>"$run/10_engine-pass.test"
|
||||
printf '#!/bin/sh\nexit 77\n' >"$run/11_engine-skip.test"
|
||||
printf '#!/bin/sh\nexit 3\n' >"$run/12_engine-fail.test"
|
||||
|
||||
cd "$run"
|
||||
rc=0
|
||||
out=$(RUNNER_TEMP="$tmp" GITHUB_STEP_SUMMARY="$tmp/summary" \
|
||||
out=$(RUNNER_TEMP="$tmp" GITHUB_STEP_SUMMARY="$tmp/summary" WATCHDOG_TOKEN=s3cr3t \
|
||||
bash ./ci-windows-suite.sh "$bin" 2>&1) || rc=$?
|
||||
# The skip set and the pass floor are pinned to the real suite, so a stub run
|
||||
# ends on the floor; what is under test is the tally that reaches it.
|
||||
@@ -77,6 +125,20 @@ grep -q '^ran=9 pass=7 fail=1 skip=1$' <<<"$out" ||
|
||||
grep -q '::error::only 7 tests passed (1 skipped)' <<<"$out" ||
|
||||
fail "the pass floor did not report the count"
|
||||
grep -q 'FAIL 12_engine-fail.test (exit 3)' <<<"$out" || fail "the failing test was not named"
|
||||
|
||||
# This launches the watchdog the way the real driver does, not through a test
|
||||
# sourcing the helper, proving it starts and holds a token nothing else sees.
|
||||
grep -q '^watchdog ready$' watchdog.log || fail "no watchdog was launched: $(cat watchdog.log)"
|
||||
test -r "$tmp/childtoken" || fail "the test that reads its environment did not run"
|
||||
grep -qx 'TOKEN=' "$tmp/childtoken" ||
|
||||
fail "a test inherited the status token: $(cat "$tmp/childtoken")"
|
||||
grep -qx 'TOKEN=s3cr3t' "$wdargv" ||
|
||||
fail "the watchdog was launched without its token: $(cat "$wdargv")"
|
||||
test -s "$tmp/cygpathtoken" || fail "the cygpath stub never ran"
|
||||
grep -q 'TOKEN=s3cr3t' "$tmp/cygpathtoken" &&
|
||||
fail "the driver forked a child before scrubbing its token: $(cat "$tmp/cygpathtoken")"
|
||||
# An orphan runs out its own deadline, re-posting a frozen tail past the step (#795).
|
||||
! still_ticking || fail "the driver exited leaving its reporter running"
|
||||
# Positive control for the gate below: with every category matched it stays silent.
|
||||
grep -q 'matched no tests' <<<"$out" && fail "a full suite reported an empty category"
|
||||
|
||||
@@ -110,4 +172,34 @@ grep -q '::error::test category runnable matched no tests' <<<"$out" ||
|
||||
fail "the empty single-test category was not named: $out"
|
||||
grep -q 'FAIL 00_runnable' <<<"$out" && fail "the unexpanded name was run as a test: $out"
|
||||
|
||||
# The wedge branch has to end the reporter itself: its kill of the suite is a
|
||||
# TerminateProcess, which runs no EXIT trap. Forked the way the driver forks it,
|
||||
# since the branch reads $watchdog from the shell it was forked from.
|
||||
wedge_leg() (
|
||||
PATH="$bin:$PATH"
|
||||
export PATH
|
||||
prog=$run/wedge-progress.log
|
||||
printf 'RUN 99_wedged.test at 0s\n' >"$prog"
|
||||
ci_watchdog_pid=''
|
||||
ci_start_native_watchdog "$prog" || fail "the wedge leg launched no watchdog"
|
||||
watchdog=$ci_watchdog_pid
|
||||
still_ticking || fail "the wedge leg's reporter was not running to begin with"
|
||||
# Its own process group, so the tree kill below cannot reach this shell.
|
||||
set -m
|
||||
sleep 300 &
|
||||
victim=$!
|
||||
set +m
|
||||
ci_suite_heartbeat 0 1 "$prog" 0 "$victim" >"$tmp/wedge.out" 2>&1 &
|
||||
wait "$!"
|
||||
grep -q '::error title=suite watchdog::' "$tmp/wedge.out" ||
|
||||
fail "the wedge branch was never reached: $(cat "$tmp/wedge.out")"
|
||||
! still_ticking || fail "the wedge kill orphaned the reporter"
|
||||
)
|
||||
# Held back: bash announces every job it reaps from a signal, which reads as a
|
||||
# failure in a log that passed.
|
||||
wedge_leg >"$tmp/wedge.log" 2>&1 || {
|
||||
cat "$tmp/wedge.log" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "ci-windows driver OK"
|
||||
|
||||
@@ -88,32 +88,36 @@ env MAKEFLAGS= MFLAGS= "$make" -C "$abs_top_builddir/src" install-DevIncludesDAT
|
||||
}
|
||||
|
||||
headers=("$tmp/include/httrack"/*.h)
|
||||
[ "${#headers[@]}" -ge 10 ] || fail "only ${#headers[@]} headers installed, the list cannot be right"
|
||||
[ -f "$tmp/include/httrack/htssafe.h" ] || fail "htssafe.h was not installed"
|
||||
|
||||
# No libc hands getnameinfo() to a strict-ISO unit, so a consumer of the two
|
||||
# socket headers must ask for POSIX. Prefer 2001: 2008 also un-hides strnlen,
|
||||
# which would stop those two covering that half of #972.
|
||||
posix_argv=(-D_POSIX_C_SOURCE=200112L)
|
||||
printf '#include <httrack/htsnet.h>\n' >"$tmp/tu.c"
|
||||
if ! "${cc_argv[@]}" "${cpp_argv[@]}" "${posix_argv[@]}" "-std=${stds[0]}" \
|
||||
-fsyntax-only "$tmp/tu.c" 2>/dev/null; then
|
||||
fail "htsnet.h needs POSIX.1-2008, which un-hides strnlen and would leave that half of #972 uncovered"
|
||||
fi
|
||||
# Derived from DevIncludes_DATA so a new header needs no edit here. A header
|
||||
# dropped from that list moves both sides, hence the named checks below.
|
||||
declared=$(awk '/^DevIncludes_DATA[[:space:]]*=/ { inlist = 1 }
|
||||
inlist {
|
||||
last = ($0 !~ /\\$/)
|
||||
sub(/^[^=]*=/, "")
|
||||
gsub(/\\/, " ")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i ~ /\.h$/) n++
|
||||
if (last) exit
|
||||
}
|
||||
END { print n + 0 }' "${abs_top_srcdir:?}/src/Makefile.am")
|
||||
[ "$declared" -ge 10 ] || fail "read only $declared headers out of DevIncludes_DATA, the parse is wrong"
|
||||
[ "${#headers[@]}" -eq "$declared" ] ||
|
||||
fail "${#headers[@]} headers installed, DevIncludes_DATA declares $declared"
|
||||
for h in htssafe.h htsnet.h htsopt.h; do
|
||||
[ -f "$tmp/include/httrack/$h" ] || fail "$h was not installed"
|
||||
done
|
||||
|
||||
# No -D_POSIX_C_SOURCE anywhere below: every installed header, htsnet.h and
|
||||
# htsopt.h included, must compile against the strict-ISO libc alone (#1001).
|
||||
count=0
|
||||
bad=0
|
||||
for h in "${headers[@]}"; do
|
||||
b=$(basename "$h")
|
||||
argv=("${cpp_argv[@]}")
|
||||
case "$b" in
|
||||
htsnet.h | htsopt.h) argv+=("${posix_argv[@]}") ;;
|
||||
esac
|
||||
printf '#include <httrack/%s>\n' "$b" >"$tmp/tu.c"
|
||||
for std in "${stds[@]}"; do
|
||||
for mode in -UHTS_INTERNAL_BYTECODE -DHTS_INTERNAL_BYTECODE; do
|
||||
count=$((count + 1))
|
||||
if ! "${cc_argv[@]}" "${argv[@]}" "-std=$std" "$mode" -fsyntax-only "$tmp/tu.c" 2>"$tmp/cc.log"; then
|
||||
if ! "${cc_argv[@]}" "${cpp_argv[@]}" "-std=$std" "$mode" -fsyntax-only "$tmp/tu.c" 2>"$tmp/cc.log"; then
|
||||
echo "$b does not compile under -std=$std ($mode):" >&2
|
||||
head -5 "$tmp/cc.log" >&2
|
||||
bad=1
|
||||
@@ -122,6 +126,7 @@ for h in "${headers[@]}"; do
|
||||
done
|
||||
done
|
||||
echo "compiled ${#headers[@]} headers x ${#stds[@]} std x 2 bytecode modes = $count units" >&2
|
||||
[ "$count" -eq $((${#headers[@]} * ${#stds[@]} * 2)) ] || fail "the compile loop skipped units"
|
||||
[ "$bad" -eq 0 ] || fail "installed headers do not compile in strict-ISO mode"
|
||||
|
||||
# A macro body is only compiled where it expands, so the include-only loop above
|
||||
@@ -178,19 +183,23 @@ int main(void) {
|
||||
return 0;
|
||||
}
|
||||
EOF
|
||||
# Vacuous if the gate handed us libc's strnlen. Match that branch, not the
|
||||
# loop, or a mutated loop reads as a lost fallback instead. Needs no runnable
|
||||
# binary, so it stays outside the gate below.
|
||||
for std in "${stds[@]}"; do
|
||||
pp=$("${cc_argv[@]}" "${cpp_argv[@]}" "-std=$std" -E "$tmp/strnlen.c") ||
|
||||
fail "cannot preprocess the htssafe_strnlen_ probe under -std=$std"
|
||||
if grep -q 'return strnlen' <<<"$pp"; then
|
||||
fail "-std=$std left htssafe_strnlen_ on libc, the differential proves nothing"
|
||||
fi
|
||||
done
|
||||
|
||||
# A noexec TMPDIR or a cross-compiler would red the run for the wrong reason.
|
||||
printf 'int main(void) { return 0; }\n' >"$tmp/tu.c"
|
||||
if ! ("${cc_argv[@]}" "${cpp_argv[@]}" -o "$tmp/tu" "$tmp/tu.c" 2>/dev/null && "$tmp/tu"); then
|
||||
echo "cannot run a binary built in $tmp, skipping the htssafe_strnlen_ differential" >&2
|
||||
else
|
||||
for std in "${stds[@]}"; do
|
||||
# Vacuous if the gate handed us libc's strnlen. Match that branch, not
|
||||
# the loop, or a mutated loop reads as a lost fallback instead.
|
||||
pp=$("${cc_argv[@]}" "${cpp_argv[@]}" "-std=$std" -E "$tmp/strnlen.c") ||
|
||||
fail "cannot preprocess the htssafe_strnlen_ probe under -std=$std"
|
||||
if grep -q 'return strnlen' <<<"$pp"; then
|
||||
fail "-std=$std left htssafe_strnlen_ on libc, the check below proves nothing"
|
||||
fi
|
||||
"${cc_argv[@]}" "${cpp_argv[@]}" "-std=$std" -o "$tmp/strnlen" "$tmp/strnlen.c" 2>"$tmp/cc.log" || {
|
||||
head -5 "$tmp/cc.log" >&2
|
||||
fail "the htssafe_strnlen_ probe does not build under -std=$std"
|
||||
@@ -221,9 +230,6 @@ else
|
||||
for h in "${headers[@]}"; do
|
||||
b=$(basename "$h")
|
||||
argv=("${base_cpp_argv[@]}" -Wall -Wextra -Werror)
|
||||
case "$b" in
|
||||
htsnet.h | htsopt.h) argv+=("${posix_argv[@]}") ;;
|
||||
esac
|
||||
printf '#include <httrack/%s>\nint main() { return 0; }\n' "$b" >"$tmp/tu.cpp"
|
||||
for std in "${cxx_stds[@]}"; do
|
||||
for mode in -UHTS_INTERNAL_BYTECODE -DHTS_INTERNAL_BYTECODE; do
|
||||
|
||||
@@ -6,14 +6,34 @@
|
||||
set -euo pipefail
|
||||
|
||||
srcdir="${abs_top_srcdir:?not run under make check}"
|
||||
metainfo="${srcdir}/html/server/div/com.httrack.WebHTTrack.metainfo.xml"
|
||||
|
||||
command -v appstreamcli >/dev/null 2>&1 || {
|
||||
echo "appstreamcli not installed; skipping" >&2
|
||||
# --no-net/--nonet: validate the file, not the reachability of the screenshot
|
||||
# host. Fedora's build gates on appstream-util, stricter about icons and
|
||||
# metadata than appstreamcli.
|
||||
validate() {
|
||||
case $1 in
|
||||
appstreamcli) appstreamcli validate --no-net --explain "$2" ;;
|
||||
appstream-util) appstream-util validate-relax --nonet "$2" ;;
|
||||
*)
|
||||
echo "FAIL: no validator named $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
tools=()
|
||||
for tool in appstreamcli appstream-util; do
|
||||
command -v "${tool}" >/dev/null 2>&1 && tools+=("${tool}")
|
||||
done
|
||||
test ${#tools[@]} -gt 0 || {
|
||||
echo "no appstream validator installed; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
test -f "${metainfo}" || {
|
||||
echo "FAIL: ${metainfo} not found" >&2
|
||||
|
||||
# html/Makefile.am installs these by glob; a second one must not ship unchecked.
|
||||
metainfos=("${srcdir}"/html/server/div/*.metainfo.xml)
|
||||
test -f "${metainfos[0]}" || {
|
||||
echo "FAIL: no metainfo under ${srcdir}/html/server/div" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -23,21 +43,18 @@ work=$(mktemp -d "${TMPDIR:-/tmp}/appstream.XXXXXX") || {
|
||||
}
|
||||
trap 'set +e; rm -rf "${work}"' EXIT
|
||||
|
||||
# --no-net: validate the file, not the reachability of the screenshot host.
|
||||
validate() { appstreamcli validate --no-net --explain "$1"; }
|
||||
|
||||
validate "${metainfo}" || {
|
||||
echo "FAIL: metainfo does not validate" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Positive control: an appstreamcli that never fails would pass the above.
|
||||
grep -v '<id>com.httrack.WebHTTrack</id>' "${metainfo}" >"${work}/noid.xml"
|
||||
if validate "${work}/noid.xml" >"${work}/noid.log" 2>&1; then
|
||||
echo "FAIL: metainfo without <id> validated; the check proves nothing" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q component-id-missing "${work}/noid.log" ||
|
||||
echo "note: id-less copy failed for another reason: $(tail -3 "${work}/noid.log")" >&2
|
||||
|
||||
echo "appstream metainfo: valid"
|
||||
for metainfo in "${metainfos[@]}"; do
|
||||
# Positive control: a validator that never fails would pass the check below.
|
||||
sed '/<id>/d' "${metainfo}" >"${work}/noid.xml"
|
||||
for tool in "${tools[@]}"; do
|
||||
validate "${tool}" "${metainfo}" || {
|
||||
echo "FAIL: ${metainfo##*/} does not validate under ${tool}" >&2
|
||||
exit 1
|
||||
}
|
||||
if validate "${tool}" "${work}/noid.xml" >"${work}/noid.log" 2>&1; then
|
||||
echo "FAIL: ${tool} validated a metainfo with no <id>; it proves nothing" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "appstream metainfo: ${metainfo##*/} valid (${tools[*]})"
|
||||
done
|
||||
|
||||
42
tests/224_engine-ftp-cmdlen.test
Executable file
42
tests/224_engine-ftp-cmdlen.test
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A too-long FTP command must be refused, never truncated or aborted (#1019).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftpcmdlen.XXXXXX") || exit 1
|
||||
trap 'set +e; rm -rf "${tmpdir}"' EXIT
|
||||
trap 'exit 1' HUP INT QUIT TERM
|
||||
|
||||
out=$(httrack -O "${tmpdir}/st" "-#test=ftp-cmdlen" run 2>&1) ||
|
||||
fail "self-test exited non-zero: ${out}"
|
||||
grep -q "ftp-cmdlen self-test OK" <<<"${out}" || fail "unexpected output: ${out}"
|
||||
|
||||
# The reachable half: the copy into _adr[256] aborted on a long host.
|
||||
rep() { awk -v n="$1" 'BEGIN { while (i++ < n) printf "a" }'; }
|
||||
|
||||
probe_host() {
|
||||
rm -rf "${tmpdir}/host"
|
||||
httrack "ftp://$1/f.txt" -O "${tmpdir}/host" -q >/dev/null 2>&1 ||
|
||||
fail "a ${#1}-byte FTP host name crashed the engine"
|
||||
cat "${tmpdir}/host/hts-log.txt"
|
||||
}
|
||||
|
||||
for n in 256 257 300; do
|
||||
log=$(probe_host "$(rep "${n}")")
|
||||
grep -q "Host name too long" <<<"${log}" ||
|
||||
fail "a ${n}-byte host was not rejected: ${log}"
|
||||
done
|
||||
|
||||
# 255 still fits _adr[256]; 127.0.0.1:1 is the outsider a widened gate swallows.
|
||||
log=$(probe_host "$(rep 255)")
|
||||
grep -q "Unable to get server's address" <<<"${log}" ||
|
||||
fail "a 255-byte host did not reach the resolver: ${log}"
|
||||
log=$(probe_host "127.0.0.1:1")
|
||||
grep -q "Unable to connect to the server" <<<"${log}" ||
|
||||
fail "a normal host did not reach the connect: ${log}"
|
||||
99
tests/225_install-manifest.test
Executable file
99
tests/225_install-manifest.test
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Distro packages (Fedora %files, debian/*.files) name every installed file, so a
|
||||
# file appearing or vanishing here breaks their build and not ours; it broke
|
||||
# Fedora's twice in three weeks.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${abs_top_builddir:?not run under make check}"
|
||||
manifest="${abs_top_srcdir:?not run under make check}/tests/install-manifest.txt"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# macOS names the library .dylib, and Windows has no distro packaging to protect.
|
||||
test "$(uname -s)" = Linux || {
|
||||
echo "install layout is pinned on Linux only; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
test -r "${manifest}" || fail "no ${manifest}"
|
||||
|
||||
# The docs are dropped below, so a docdir holding datadir would drop the pinned
|
||||
# data files with them and leave nothing to compare.
|
||||
docdir=${CONFIGURED_DOCDIR:?not run under make check}
|
||||
case "${CONFIGURED_DATADIR:?not run under make check}" in
|
||||
"${docdir%/}" | "${docdir%/}"/*)
|
||||
echo "docdir contains datadir; skipping" >&2
|
||||
exit 77
|
||||
;;
|
||||
esac
|
||||
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/manifest.XXXXXX") || {
|
||||
echo "no tmpdir" >&2
|
||||
exit 1
|
||||
}
|
||||
trap 'set +e; rm -rf "${work}"' EXIT
|
||||
|
||||
stage=${work}/stage
|
||||
env -u MAKEFLAGS -u MAKELEVEL "${MAKE:-make}" -C "${abs_top_builddir}" \
|
||||
install DESTDIR="${stage}" >"${work}/install.log" 2>&1 || {
|
||||
cat "${work}/install.log" >&2
|
||||
fail "make install DESTDIR=${stage}"
|
||||
}
|
||||
|
||||
# Fold the configured directories back to tokens so the pin survives any --prefix
|
||||
# or --libdir. Longest path first, so a nested directory beats the one it sits in
|
||||
# whatever the layout puts where.
|
||||
: >"${work}/dirs"
|
||||
for var in DOCDIR HTMLDIR MANDIR INCLUDEDIR LIBDIR BINDIR DATADIR; do
|
||||
name=CONFIGURED_${var}
|
||||
path=${!name:-}
|
||||
test -n "${path}" || fail "${name} not set"
|
||||
path=${path%/}
|
||||
printf '%s\t%s\t%s\n' "${#path}" "${path}" "${var}" >>"${work}/dirs"
|
||||
done
|
||||
LC_ALL=C sort -rn "${work}/dirs" -o "${work}/dirs"
|
||||
|
||||
paths=() tokens=()
|
||||
while IFS=$'\t' read -r _ path var; do
|
||||
paths+=("${path}")
|
||||
tokens+=("${var}")
|
||||
done <"${work}/dirs"
|
||||
|
||||
# Matched literally: a directory holding a regex metacharacter would silently
|
||||
# fold nothing if this were a pattern.
|
||||
fold() {
|
||||
local line=$1 i
|
||||
for i in "${!paths[@]}"; do
|
||||
case ${line} in
|
||||
"${paths[i]}"/*)
|
||||
printf '@%s@/%s\n' "${tokens[i]}" "${line#"${paths[i]}"/}"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
done
|
||||
printf '%s\n' "${line}"
|
||||
}
|
||||
|
||||
# Dropped: the docs, since where they sit under docdir is a --htmldir choice each
|
||||
# distro makes its own way and no package names them one by one; the .a/.la
|
||||
# libtool bookkeeping no distro ships; and the library revision, which moves
|
||||
# every release. The soname digit stays: a bump renames Debian's package.
|
||||
(cd "${stage}" && find . \( -type f -o -type l \) | sed 's|^\.||') |
|
||||
while IFS= read -r line; do fold "${line}"; done |
|
||||
sed -e '/^@DOCDIR@\//d' -e '/^@HTMLDIR@\//d' \
|
||||
-e '/\.la$/d' -e '/\.a$/d' \
|
||||
-e 's|\(\.so\.[0-9][0-9]*\)\.[0-9][0-9]*\.[0-9][0-9]*$|\1.@revision@|' |
|
||||
LC_ALL=C sort >"${work}/actual"
|
||||
|
||||
# An install that staged nothing matches nothing; say that, not a 300-line diff.
|
||||
grep -qx '@BINDIR@/httrack' "${work}/actual" || fail "nothing staged under ${stage}"
|
||||
|
||||
LC_ALL=C sort "${manifest}" >"${work}/expected"
|
||||
diff -u "${work}/expected" "${work}/actual" ||
|
||||
fail "the installed file set changed; update tests/install-manifest.txt and say so in the release notes -- Fedora's %files and debian/*.files name these files"
|
||||
|
||||
echo "install manifest: $(wc -l <"${work}/actual") files"
|
||||
726
tests/226_watchdog-native.test
Normal file
726
tests/226_watchdog-native.test
Normal file
@@ -0,0 +1,726 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The off-box suite watchdog (#795): its pure decisions through -SelfTest, what
|
||||
# reaches the status API, the process APIs it must never name, and the driver
|
||||
# wiring no .ps1 can check for itself.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Before the driver is sourced: with these set, anything it starts posts a commit
|
||||
# status on whatever the surrounding shell was last looking at.
|
||||
unset WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_URL
|
||||
export WATCHDOG_CONTEXT='226_watchdog-native (never posted)'
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "$testdir/testlib.sh"
|
||||
|
||||
top=${abs_top_srcdir:-$(cd "$testdir/.." && pwd)}
|
||||
wdscript="$testdir/ci-windows-watchdog.ps1"
|
||||
driver="$testdir/ci-windows-suite.sh"
|
||||
workflow="$top/.github/workflows/windows-build.yml"
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_wdnat.XXXXXX")
|
||||
sinkpidfile="$tmp/sink.pid"
|
||||
: >"$sinkpidfile"
|
||||
toppid=$$
|
||||
trap 'set +e; stop_server "$(cat "$sinkpidfile" 2>/dev/null)"; rm -rf "$tmp"' EXIT
|
||||
trap 'exit 1' TERM
|
||||
# Signalled, since a bare exit only ends the subshell a leg runs in.
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
kill -TERM "$toppid" 2>/dev/null || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
test -r "$wdscript" || fail "no $wdscript"
|
||||
test -z "${WATCHDOG_TOKEN:-}${WATCHDOG_SHA:-}" || fail "a credential survived the scrub"
|
||||
|
||||
# The prose in the script names the very calls the audits forbid. A trailing
|
||||
# comment goes only on a quote-free line: cutting at any '#' would hide a call
|
||||
# sitting after an issue number inside a string.
|
||||
uncommented() { sed -e 's/^[[:space:]]*#.*$//' -e "/['\"]/! s/[[:space:]]#.*$//" "$1"; }
|
||||
|
||||
# Telemetry only, so every spawn and kill idiom is out, aliases and short forms
|
||||
# included: PowerShell needs neither the System. prefix nor a space after '&'.
|
||||
forbidden_calls() {
|
||||
uncommented "$1" | grep -nEi \
|
||||
'Start-Process|Start-Job|Start-ThreadJob|(Start|Stop)-Service|Stop-Process|Invoke-Expression|Invoke-Item|Invoke-Command|Invoke-(Cim|Wmi)Method|Get-WmiObject|Register-Scheduled(Job|Task)|WScript\.Shell|taskkill|cmd\.exe|wmic|schtasks|Diagnostics\.Process|\.Kill\(|(^|[^-a-zA-Z0-9_])(iex|saps|spps|spjb|sajb|spsv|sasv|start|kill)([^-a-zA-Z0-9_]|$)|(^|[^-a-zA-Z0-9_])&[^-a-zA-Z0-9_=&]'
|
||||
}
|
||||
if hits=$(forbidden_calls "$wdscript"); then
|
||||
fail "the watchdog names a process API: $hits"
|
||||
fi
|
||||
# One control per idiom: a denylist is worth only the entries that actually fire.
|
||||
# shellcheck disable=SC2016 # PowerShell source, not this shell's expansions
|
||||
for probe in 'Start-Process notepad.exe' 'Stop-Process -Id 4 -Force' \
|
||||
'[Diagnostics.Process]::Start("x")' '(Get-Process -Name httrack).Kill()' \
|
||||
'Invoke-Expression $cmd' '& "C:\\x.exe" -y' 'Start-Job { 1 }' 'taskkill /F /IM x.exe' \
|
||||
'iex $cmd' '$cmd | iex' '&"C:\\x.exe"' '&$exe' 'saps notepad.exe' 'start notepad.exe' \
|
||||
'spps -Name httrack' 'spjb { 1 }' 'sajb { 1 }' 'kill -Name httrack' \
|
||||
'Start-Service w32time' 'Stop-Service w32time' 'spsv w32time' 'sasv w32time' \
|
||||
'Get-WmiObject Win32_Process' \
|
||||
'Invoke-CimMethod -ClassName Win32_Process -MethodName Create' \
|
||||
'Invoke-WmiMethod -Class Win32_Process -Name Create' \
|
||||
'Invoke-Command -ScriptBlock { 1 }' 'Register-ScheduledJob -Name x -ScriptBlock { 1 }' \
|
||||
'Register-ScheduledTask -TaskName x -Action $a' \
|
||||
'(New-Object -ComObject WScript.Shell).Run("x")' \
|
||||
'Write-Host "#795"; Start-Process x'; do
|
||||
{
|
||||
cat "$wdscript"
|
||||
echo "$probe"
|
||||
} >"$tmp/probe.ps1"
|
||||
if ! forbidden_calls "$tmp/probe.ps1" >/dev/null; then
|
||||
fail "the process-API audit cannot see: $probe"
|
||||
fi
|
||||
done
|
||||
|
||||
# shellcheck disable=SC2016 # $env: is PowerShell's, not this shell's
|
||||
grep -q '\$env:WATCHDOG_TOKEN' "$wdscript" ||
|
||||
fail "the watchdog does not read its token from the environment"
|
||||
if sed -n '/^param(/,/^)/p' "$wdscript" | grep -qi 'token'; then
|
||||
fail "the watchdog takes its token as a parameter, where the process list exposes it"
|
||||
fi
|
||||
grep -q "ApiBase = 'https://api.github.com'" "$wdscript" ||
|
||||
fail "the watchdog's default endpoint is not the GitHub API"
|
||||
# One state, so nothing downstream can read a verdict out of a status that only
|
||||
# carries telemetry. The job's own conclusion is the verdict.
|
||||
test "$(grep -c "state = 'success'" "$wdscript")" -eq 1 ||
|
||||
fail "the watchdog posts something other than the one telemetry state"
|
||||
# The token stays in the driver's own shell: exported, every test it runs would
|
||||
# inherit a credential that can post statuses. 172 drives that end to end.
|
||||
grep -q '^unset WATCHDOG_TOKEN$' "$driver" ||
|
||||
fail "the driver leaves its token in the environment its tests inherit"
|
||||
# A background holder of the step's stdout keeps the step open past the suite (#949).
|
||||
grep -q '>>watchdog.log 2>&1' "$driver" ||
|
||||
fail "the watchdog's output is not redirected off the step's stdout"
|
||||
|
||||
# Sourcing the driver is enough to start an interpreter that can post, so every
|
||||
# test that does it has to scrub first.
|
||||
scrubs() {
|
||||
grep -q 'ci-windows-suite\.sh' "$1" || return 0
|
||||
grep -qx 'unset WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_URL' "$1"
|
||||
}
|
||||
for t in "$testdir"/*.test; do
|
||||
scrubs "$t" || fail "$(basename "$t") can reach the driver without scrubbing its credentials"
|
||||
done
|
||||
printf '#!/bin/sh\n. ./ci-windows-suite.sh\n' >"$tmp/unscrubbed.test"
|
||||
scrubs "$tmp/unscrubbed.test" && fail "the scrub audit passes a test that does not scrub"
|
||||
|
||||
# PATH holds MSYS-style entries only, and TMPDIR on Windows is a D:/... path
|
||||
# that would split it on the drive colon.
|
||||
posixtmp=$tmp
|
||||
if is_windows && command -v cygpath >/dev/null 2>&1; then
|
||||
posixtmp=$(cygpath -u "$tmp")
|
||||
fi
|
||||
bin="$posixtmp/bin"
|
||||
mkdir -p "$bin"
|
||||
# Renamed into place, so the poll below cannot read a half-written file.
|
||||
printf '#!/bin/sh\nprintf "%%s\\n" "$@" >%s/argv.part\nmv %s/argv.part %s/argv\necho "watchdog ready"\n' \
|
||||
"$posixtmp" "$posixtmp" "$posixtmp" >"$bin/argvstub"
|
||||
printf '#!/bin/sh\nexit 127\n' >"$bin/deadstub"
|
||||
chmod +x "$bin/argvstub" "$bin/deadstub"
|
||||
usestub() { cp "$bin/$1" "$bin/pwsh" && cp "$bin/$1" "$bin/powershell.exe"; }
|
||||
usestub argvstub
|
||||
|
||||
if test -x "$bin/pwsh"; then
|
||||
(
|
||||
cd "$posixtmp"
|
||||
# shellcheck disable=SC2030 # local to this subshell is the point
|
||||
PATH="$bin:$PATH"
|
||||
export PATH
|
||||
# An unshadowed pwsh would drive the real watchdog, and every assertion
|
||||
# below would still read as a plain launch failure.
|
||||
test "$(command -v pwsh)" = "$bin/pwsh" ||
|
||||
fail "the stub does not shadow PowerShell: $(command -v pwsh)"
|
||||
# shellcheck source=tests/ci-windows-suite.sh
|
||||
. "$driver"
|
||||
ci_watchdog_pid=''
|
||||
ci_start_native_watchdog "$posixtmp/progress.log" ||
|
||||
fail "the launch reported no watchdog with a PowerShell on PATH"
|
||||
test -n "$ci_watchdog_pid" || fail "the launch set no pid"
|
||||
# wait only reaps a child, which is what the teardown has to signal.
|
||||
wait "$ci_watchdog_pid" || fail "the watchdog is not a child of the driver"
|
||||
|
||||
# A pid comes from the fork, so a dead interpreter still yields one.
|
||||
usestub deadstub
|
||||
ci_watchdog_pid='dangling'
|
||||
rc=0
|
||||
ci_start_native_watchdog "$posixtmp/progress.log" || rc=$?
|
||||
test "$rc" -ne 0 || fail "an interpreter exiting 127 was reported as launched"
|
||||
test -z "$ci_watchdog_pid" || fail "a failed launch left pid $ci_watchdog_pid behind"
|
||||
)
|
||||
for _ in $(seq 1 100); do
|
||||
test -r "$posixtmp/argv" && break
|
||||
sleep 0.1
|
||||
done
|
||||
test -r "$posixtmp/argv" || fail "the watchdog was never launched"
|
||||
grep -qx -- '-SelfTest' "$posixtmp/argv" && fail "the production launch runs the self-test"
|
||||
# Bypass is load-bearing: Windows PowerShell refuses an unsigned .ps1 without it.
|
||||
for flag in -NoProfile -NonInteractive -ExecutionPolicy Bypass -File; do
|
||||
grep -qx -- "$flag" "$posixtmp/argv" ||
|
||||
fail "the launch dropped $flag: $(tr '\n' ' ' <"$posixtmp/argv")"
|
||||
done
|
||||
grep -qx -- '-ApiBase' "$posixtmp/argv" &&
|
||||
fail "the production launch redirects the status API away from GitHub"
|
||||
grep -qx -- '-NoPost' "$posixtmp/argv" &&
|
||||
fail "the production launch cannot post at all"
|
||||
# Native form, because a non-MSYS interpreter cannot resolve a /d/a/... path.
|
||||
grep -qx -- "$(nativepath "$wdscript")" "$posixtmp/argv" ||
|
||||
fail "the launch did not name the script: $(tr '\n' ' ' <"$posixtmp/argv")"
|
||||
grep -qx -- "$(nativepath "$posixtmp/progress.log")" "$posixtmp/argv" ||
|
||||
fail "the launch did not pass the progress log"
|
||||
else
|
||||
echo "note: $tmp is noexec, the driver wiring was not driven"
|
||||
fi
|
||||
|
||||
py=$(find_python || true)
|
||||
if ! test -r "$workflow"; then
|
||||
test -d "$top/.github" && fail "$workflow is gone but $top/.github is not"
|
||||
echo "note: no .github under $top (dist tarball), the workflow audits did not run"
|
||||
elif test -z "$py" || ! "$py" -c 'import yaml' 2>/dev/null; then
|
||||
echo "note: no python3 with PyYAML here, the workflow audits did not run"
|
||||
else
|
||||
# Parsed, not grepped: every key below has to be read from the block that
|
||||
# owns it, and an ordinary reformatting of the workflow must not move it.
|
||||
cat >"$tmp/wf.py" <<'PY'
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def steps_of(wf):
|
||||
return [s for j in (wf.get("jobs") or {}).values() for s in (j.get("steps") or [])]
|
||||
|
||||
|
||||
def checkouts(wf):
|
||||
return [s for s in steps_of(wf) if str(s.get("uses", "")).startswith("actions/checkout")]
|
||||
|
||||
|
||||
def suite_steps(wf):
|
||||
return [s for s in steps_of(wf) if "ci-windows-suite.sh" in str(s.get("run", ""))]
|
||||
|
||||
|
||||
def suite_jobs(wf):
|
||||
return [
|
||||
j
|
||||
for j in (wf.get("jobs") or {}).values()
|
||||
if any("ci-windows-suite.sh" in str(s.get("run", "")) for s in (j.get("steps") or []))
|
||||
]
|
||||
|
||||
|
||||
# A job-level block replaces the workflow's outright, so one naming only contents
|
||||
# silently drops statuses: write.
|
||||
def perms_of(wf, job):
|
||||
p = job.get("permissions", wf.get("permissions"))
|
||||
if p in ("read-all", "write-all"):
|
||||
w = "read" if p == "read-all" else "write"
|
||||
return {"contents": w, "statuses": w}
|
||||
return p if isinstance(p, dict) else {}
|
||||
|
||||
|
||||
WANT_ENV = {
|
||||
"WATCHDOG_TOKEN": "${{ secrets.GITHUB_TOKEN }}",
|
||||
"WATCHDOG_REPO": "${{ github.repository }}",
|
||||
# The merge commit, which no PR checks UI reads.
|
||||
"WATCHDOG_SHA": "${{ github.sha }}",
|
||||
}
|
||||
|
||||
|
||||
def audit(wf):
|
||||
bad = []
|
||||
if not checkouts(wf):
|
||||
bad.append("no actions/checkout step to audit")
|
||||
for s in checkouts(wf):
|
||||
if (s.get("with") or {}).get("persist-credentials") is not False:
|
||||
bad.append("a checkout persists the job token for the whole job")
|
||||
jobs, found = suite_jobs(wf), suite_steps(wf)
|
||||
if len(jobs) != 1 or len(found) != 1:
|
||||
bad.append("%d jobs and %d steps run the suite, cannot tell which is watched" % (len(jobs), len(found)))
|
||||
return bad
|
||||
perms = perms_of(wf, jobs[0])
|
||||
if perms.get("statuses") != "write":
|
||||
bad.append("the suite job cannot post a commit status: statuses is %r" % perms.get("statuses"))
|
||||
if perms.get("contents") != "read":
|
||||
bad.append("contents: read was dropped from the suite job")
|
||||
env = found[0].get("env") or {}
|
||||
for v, want in WANT_ENV.items():
|
||||
if env.get(v) != want:
|
||||
bad.append("%s is %r, want %r" % (v, env.get(v), want))
|
||||
# Both axes, or the matrix legs overwrite each other's status.
|
||||
for axis in ("matrix.platform", "matrix.configuration"):
|
||||
if axis not in str(env.get("WATCHDOG_CONTEXT") or ""):
|
||||
bad.append("WATCHDOG_CONTEXT does not name %s" % axis)
|
||||
if "github.run_id" not in str(env.get("WATCHDOG_URL") or ""):
|
||||
bad.append("WATCHDOG_URL does not link the run being watched")
|
||||
cap = found[0].get("timeout-minutes")
|
||||
if not isinstance(cap, int):
|
||||
bad.append("the suite step has no timeout-minutes of its own")
|
||||
else:
|
||||
print("timeout=%d" % cap)
|
||||
return bad
|
||||
|
||||
|
||||
def mutate(wf, kind):
|
||||
if kind == "perm":
|
||||
wf["permissions"]["statuses"] = "read"
|
||||
elif kind == "jobperm":
|
||||
suite_jobs(wf)[0]["permissions"] = {"contents": "read"}
|
||||
elif kind == "creds":
|
||||
for s in checkouts(wf):
|
||||
(s.get("with") or {}).pop("persist-credentials", None)
|
||||
elif kind == "env":
|
||||
suite_steps(wf)[0]["env"].pop("WATCHDOG_TOKEN")
|
||||
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 }}"
|
||||
elif kind == "context":
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_CONTEXT"] = "windows-suite"
|
||||
elif kind == "url":
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_URL"] = "${{ github.server_url }}"
|
||||
elif kind == "cap":
|
||||
suite_steps(wf)[0].pop("timeout-minutes")
|
||||
elif kind != "none":
|
||||
raise SystemExit("unknown mutation " + kind)
|
||||
return wf
|
||||
|
||||
|
||||
wf = yaml.safe_load(open(sys.argv[2]))
|
||||
if sys.argv[1] == "mutate":
|
||||
yaml.safe_dump(mutate(wf, sys.argv[3]), sys.stdout)
|
||||
else:
|
||||
for b in audit(wf):
|
||||
print("BAD " + b)
|
||||
PY
|
||||
said=$("$py" "$tmp/wf.py" audit "$workflow") || fail "the workflow audit did not run: $said"
|
||||
grep -q '^BAD ' <<<"$said" && fail "$(grep '^BAD ' <<<"$said" | sed 's/^BAD //')"
|
||||
cap=$(($(sed -n 's/^timeout=//p' <<<"$said") * 60))
|
||||
|
||||
# One control per key, or an audit that stopped looking at one would ride on
|
||||
# another still firing. "none" round-trips the file through the parser, which
|
||||
# rewrites quoting, order and flow style: no verdict may move.
|
||||
for m in none:'' perm:'cannot post' jobperm:'cannot post' creds:'persists the job token' \
|
||||
env:'WATCHDOG_TOKEN is' token:'WATCHDOG_TOKEN is' sha:'WATCHDOG_SHA is' \
|
||||
context:'does not name matrix.platform' url:'does not link the run' \
|
||||
cap:'no timeout-minutes of its own'; do
|
||||
"$py" "$tmp/wf.py" mutate "$workflow" "${m%%:*}" >"$tmp/wf.yml"
|
||||
said=$("$py" "$tmp/wf.py" audit "$tmp/wf.yml") ||
|
||||
fail "the workflow audit did not run on the ${m%%:*} control: $said"
|
||||
if test -z "${m#*:}"; then
|
||||
grep -q '^BAD ' <<<"$said" && fail "a reformatted workflow fails the audit: $said"
|
||||
else
|
||||
grep -q "^BAD .*${m#*:}" <<<"$said" ||
|
||||
fail "the ${m%%:*} control did not trip the audit: $said"
|
||||
fi
|
||||
done
|
||||
|
||||
# The heartbeat still owns the kill and must beat the step timeout.
|
||||
stuck=$(sed -n 's/^stuck=\([0-9]*\)$/\1/p' "$driver")
|
||||
deadline=$(sed -n 's/^suite_deadline=\([0-9]*\)$/\1/p' "$driver")
|
||||
test -n "$stuck" || fail "cannot read the heartbeat's static window"
|
||||
test -n "$deadline" || fail "cannot read the driver's suite deadline"
|
||||
# A test may start one second under the deadline and never report again.
|
||||
test $((deadline + stuck + 240)) -le "$cap" ||
|
||||
fail "a kill at ${deadline}+${stuck}s leaves under 240s before the ${cap}s step timeout"
|
||||
|
||||
# The watchdog's own stop is its MaxSeconds, which must land inside the step.
|
||||
# shellcheck disable=SC2016 # $MaxSeconds is PowerShell's
|
||||
maxsecs=$(sed -n 's/^[[:space:]]*\[int\]\$MaxSeconds = \([0-9]*\).*$/\1/p' "$wdscript")
|
||||
test -n "$maxsecs" || fail "cannot read the watchdog's own deadline"
|
||||
test "$maxsecs" -le "$cap" || fail "the watchdog runs ${maxsecs}s past the ${cap}s step timeout"
|
||||
# Bounded below too: the reporter has to outlive the heartbeat's own kill.
|
||||
test "$maxsecs" -ge $((deadline + stuck)) ||
|
||||
fail "the watchdog stops at ${maxsecs}s, before the kill at $((deadline + stuck))s"
|
||||
fi
|
||||
|
||||
psruns=()
|
||||
for c in pwsh powershell.exe powershell; do
|
||||
command -v "$c" >/dev/null 2>&1 || continue
|
||||
# powershell and powershell.exe are one interpreter under two names.
|
||||
seen=0
|
||||
for p in ${psruns[@]+"${psruns[@]}"}; do
|
||||
test "${p%.exe}" != "${c%.exe}" || seen=1
|
||||
done
|
||||
test "$seen" -eq 1 || psruns+=("$c")
|
||||
done
|
||||
if test "${#psruns[@]}" -eq 0; then
|
||||
echo "SKIP: no PowerShell here, the watchdog's own self-test cannot run"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
# Bounded: a mutant that loops forever would otherwise be caught only by the step
|
||||
# timeout this work exists to beat. -NoPost layers over the scrub above.
|
||||
run_wd() {
|
||||
local secs=$1
|
||||
shift
|
||||
# -NoPost last: everything after -File <script> belongs to the script.
|
||||
run_with_timeout "$secs" "$psrun" "${psargs[@]}" "$@" -NoPost 2>&1
|
||||
}
|
||||
|
||||
# The legs below grade what reached the API. Printing and posting are decoupled,
|
||||
# so counting what the loop wrote can see neither the request rate nor the backoff.
|
||||
sink="$tmp/sink.py"
|
||||
cat >"$sink" <<'PY'
|
||||
import http.server
|
||||
import socketserver
|
||||
import sys
|
||||
|
||||
codes = [int(c) for c in sys.argv[1].split(",")]
|
||||
rec = sys.argv[2]
|
||||
seen = []
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
with open(rec, "ab") as f:
|
||||
f.write(self.rfile.read(n) + b"\n")
|
||||
seen.append(1)
|
||||
self.send_response(codes[(len(seen) - 1) % len(codes)])
|
||||
self.send_header("Content-Length", "2")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"{}")
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class Sink(http.server.HTTPServer):
|
||||
# Skip stock server_bind()'s getfqdn(): it stalls 35s on macOS (#870).
|
||||
def server_bind(self):
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
self.server_name, self.server_port = self.server_address[:2]
|
||||
|
||||
|
||||
srv = Sink(("127.0.0.1", 0), Handler)
|
||||
# LF, since discover_server_port parses this line: Windows would emit \r\n.
|
||||
sys.stdout.reconfigure(newline="\n")
|
||||
print("PORT %d" % srv.server_port, flush=True)
|
||||
srv.serve_forever()
|
||||
PY
|
||||
|
||||
posts="$tmp/posts"
|
||||
sinklog="$tmp/sink.log"
|
||||
legout="$tmp/leg.out"
|
||||
port='' sinkpid=''
|
||||
sink_start() {
|
||||
: >"$posts"
|
||||
: >"$sinklog"
|
||||
# Its own log: a leg's command substitution would else wait on the pipe the
|
||||
# sink inherited for as long as the sink lives.
|
||||
"$py" "$sink" "$1" "$posts" >"$sinklog" 2>&1 &
|
||||
sinkpid=$!
|
||||
# Where the teardown can find it: sink_start runs in a subshell of its own.
|
||||
echo "$sinkpid" >"$sinkpidfile"
|
||||
port=$(discover_server_port "$sinklog" "$sinkpid") ||
|
||||
fail "the status sink never bound a port: $(cat "$sinklog")"
|
||||
}
|
||||
sink_stop() {
|
||||
stop_server "$sinkpid"
|
||||
: >"$sinkpidfile"
|
||||
sinkpid=''
|
||||
}
|
||||
# Ours, so a leg that lost its -ApiBase reaches GitHub with a credential it
|
||||
# refuses. Distinctive, since the legs grade the log on never carrying it.
|
||||
export WATCHDOG_TOKEN=n0tat0ken WATCHDOG_REPO=octo/nowhere WATCHDOG_SHA=0000000
|
||||
|
||||
# The log rides into the job's artifact, which nothing secret-masks.
|
||||
token_free() { ! grep -q "$WATCHDOG_TOKEN" "$1"; }
|
||||
|
||||
run_posting() {
|
||||
local secs=$1
|
||||
shift
|
||||
run_with_timeout "$secs" "$psrun" "${psargs[@]}" "$@" -ApiBase "http://127.0.0.1:$port"
|
||||
}
|
||||
# t and q of each posted status, in order and whatever key order the JSON took.
|
||||
posted_pairs() {
|
||||
sed -n 's/.*t=\([0-9]*\)s q=\([0-9?]*\)s.*/\1 \2/p' "$posts"
|
||||
}
|
||||
|
||||
# One property per leg, each returning its diagnosis so a mutant can be required
|
||||
# to trip that same check rather than merely to fail.
|
||||
cadence_leg() {
|
||||
local n
|
||||
sink_start 201
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
run_posting 60 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 100 -PollSeconds 1 -MaxSeconds 4 >"$legout" 2>&1 || {
|
||||
sink_stop
|
||||
echo "the loop exited nonzero: $(cat "$legout")"
|
||||
return 1
|
||||
}
|
||||
sink_stop
|
||||
n=$(grep -c . "$posts" || true)
|
||||
test "$n" -eq 1 || {
|
||||
echo "$n API calls at a 100s cadence over 4s, want 1"
|
||||
return 1
|
||||
}
|
||||
grep -q '36_local-bigcrawl.test' "$posts" || {
|
||||
echo "the posted status does not name the test in flight: $(cat "$posts")"
|
||||
return 1
|
||||
}
|
||||
case $(posted_pairs) in
|
||||
*[0-9]' '[0-9]*) ;;
|
||||
*)
|
||||
echo "a readable log posted no staticness: $(posted_pairs)"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Staticness has to follow the log through movement and then through silence,
|
||||
# which is why this leg is the long one.
|
||||
staticness_leg() {
|
||||
local moved dq dt legpid stop
|
||||
sink_start 201
|
||||
printf 'RUN 36_local-bigcrawl.test at 0s\n' >"$tmp/progress.log"
|
||||
run_posting 90 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 16 >"$legout" 2>&1 &
|
||||
legpid=$!
|
||||
# Started on the first status that lands rather than on this shell's clock:
|
||||
# an interpreter three seconds into its own startup would see silence only,
|
||||
# and the moving half would go untested.
|
||||
for _ in $(seq 1 300); do
|
||||
test -s "$posts" && break
|
||||
sleep 0.1
|
||||
done
|
||||
stop=$((SECONDS + 3))
|
||||
while test "$SECONDS" -lt "$stop"; do
|
||||
printf 'RUN 36_local-bigcrawl.test at %ss\n' "$SECONDS" >>"$tmp/progress.log"
|
||||
sleep 0.4
|
||||
done
|
||||
wait "$legpid" || {
|
||||
sink_stop
|
||||
echo "the loop exited nonzero: $(cat "$legout")"
|
||||
return 1
|
||||
}
|
||||
sink_stop
|
||||
moved=$(posted_pairs | awk '$2 ~ /^[0-9]+$/ && $1 >= 2 && $2 <= 1 { n++ } END { print n + 0 }')
|
||||
test "$moved" -ge 1 || {
|
||||
echo "no post read the log as moving while it was being written: $(posted_pairs | tr '\n' '/')"
|
||||
return 1
|
||||
}
|
||||
# Once the log stops, staticness is elapsed time minus a constant, so it has
|
||||
# to track it one for one. A bound on it would report every wedge as the cap.
|
||||
dq=$(posted_pairs | awk '$1 >= 6 && $2 ~ /^[0-9]+$/ { if (f == "") f = $2; l = $2 } END { print l - f }')
|
||||
dt=$(posted_pairs | awk '$1 >= 6 && $2 ~ /^[0-9]+$/ { if (f == "") f = $1; l = $1 } END { print l - f }')
|
||||
if test "${dq:-0}" -ne "${dt:-0}" || test "${dq:-0}" -lt 4; then
|
||||
echo "staticness moved ${dq}s over ${dt}s of silence: $(posted_pairs | tr '\n' '/')"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Every due tick logs its line before the throttle decides, so these two counts
|
||||
# are a ratio no slow box can move: one to one while nothing is being skipped.
|
||||
throttle_leg() {
|
||||
local calls lines
|
||||
sink_start 201
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
run_posting 60 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 8 >"$legout" 2>&1 || {
|
||||
sink_stop
|
||||
echo "the loop exited nonzero: $(cat "$legout")"
|
||||
return 1
|
||||
}
|
||||
sink_stop
|
||||
calls=$(grep -c . "$posts" || true)
|
||||
lines=$(grep -c 't=[0-9]*s q=' "$legout" || true)
|
||||
test "$lines" -ge 3 || {
|
||||
echo "$lines due ticks over 8s at a 1s interval: too few to judge a throttle"
|
||||
return 1
|
||||
}
|
||||
test $((calls * 4)) -ge $((lines * 3)) || {
|
||||
echo "$calls of $lines due ticks reached an accepting API: a landed post throttles the next"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
backoff_leg() {
|
||||
local calls lines
|
||||
sink_start 403
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
run_posting 60 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 8 >"$legout" 2>&1 || {
|
||||
sink_stop
|
||||
echo "the loop exited nonzero: $(cat "$legout")"
|
||||
return 1
|
||||
}
|
||||
sink_stop
|
||||
calls=$(grep -c . "$posts" || true)
|
||||
lines=$(grep -c 't=[0-9]*s q=' "$legout" || true)
|
||||
test "$calls" -ge 2 || {
|
||||
echo "a rejecting API was called $calls times: the loop stopped trying"
|
||||
return 1
|
||||
}
|
||||
# One diagnosis for both: which one trips first depends on box speed. The
|
||||
# backoff must thin the calls, not the log; a token-less run leaves only the log.
|
||||
if test "$calls" -gt 5 || test "$lines" -lt $((2 * calls)); then
|
||||
echo "$calls calls and $lines log lines against an API rejecting every one: nothing backs off"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
fullstdout_leg() {
|
||||
local n
|
||||
sink_start 201
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
run_posting 60 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 5 >/dev/full 2>&1 || true
|
||||
sink_stop
|
||||
n=$(grep -c . "$posts" || true)
|
||||
test "$n" -ge 2 || {
|
||||
echo "$n posts with a full stdout: a failed log write took the loop with it"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
first=1
|
||||
for psrun in "${psruns[@]}"; do
|
||||
psargs=(-NoProfile -NonInteractive)
|
||||
case $psrun in
|
||||
powershell*) psargs+=(-ExecutionPolicy Bypass) ;;
|
||||
esac
|
||||
|
||||
out=$(run_wd 120 -File "$(nativepath "$wdscript")" -SelfTest) ||
|
||||
fail "the $psrun self-test failed: $out"
|
||||
grep -q 'watchdog self-test OK' <<<"$out" || fail "the $psrun self-test said: $out"
|
||||
|
||||
# A mutant the script survives is a property nothing is checking. Graded on
|
||||
# the assertion named: one that merely fails to parse would read as caught.
|
||||
mutate() {
|
||||
local name=$1 expr=$2 want=$3 m="$tmp/$1.ps1" rc=0 said
|
||||
sed "$expr" "$wdscript" >"$m"
|
||||
cmp -s "$wdscript" "$m" && fail "mutant $name changed nothing, so it proves nothing"
|
||||
said=$(run_wd 120 -File "$(nativepath "$m")" -SelfTest) || rc=$?
|
||||
test "$rc" -ne 0 || fail "mutant $name passed the $psrun self-test: $said"
|
||||
grep -q "self-test FAIL: .*$want" <<<"$said" ||
|
||||
fail "mutant $name tripped no assertion of its own: $said"
|
||||
}
|
||||
# shellcheck disable=SC2016 # PowerShell variables, quoted for sed
|
||||
mutate cadence-off-by-one 's/-ge \$Interval/-gt \$Interval/' 'due exactly on the interval'
|
||||
# shellcheck disable=SC2016
|
||||
mutate backoff-uncapped 's/\[Math\]::Min(\$Current \* 2, 32)/$Current * 2/' 'backoff is not capped'
|
||||
# shellcheck disable=SC2016
|
||||
mutate throttle-no-reset 's/if (\$Ok) { return @(0, 0) }/if ($Ok) { return @(0, $Backoff) }/' \
|
||||
'landed post leaves'
|
||||
# shellcheck disable=SC2016
|
||||
mutate status-unclipped '/if (\$s.Length -gt 140)/d' 'was not clipped'
|
||||
# An oversized status is cut from the tail: the head is what a wedge is read for.
|
||||
# shellcheck disable=SC2016
|
||||
mutate status-clipped-head 's/\$s = \$s.Substring(0, 140)/$s = $s.Substring($s.Length - 140)/' \
|
||||
'dropped the leading fields'
|
||||
# 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'
|
||||
# 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/' \
|
||||
'default status cadence'
|
||||
# shellcheck disable=SC2016
|
||||
mutate default-poll 's/\[int\]\$PollSeconds = 5/[int]$PollSeconds = 50/' 'default poll'
|
||||
|
||||
# Timed and counted by this shell, never read out of what the watchdog
|
||||
# writes: the cadence and the deadline are both mine to set.
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
out=$(run_wd 60 -File "$(nativepath "$wdscript")" \
|
||||
-ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 100 -PollSeconds 1 -MaxSeconds 5) ||
|
||||
fail "the $psrun slow-cadence loop exited nonzero: $out"
|
||||
slow=$(grep -c 't=[0-9]*s q=' <<<"$out" || true)
|
||||
test "$slow" -eq 1 || fail "$slow status lines at a 100s cadence over 5s, want 1: $out"
|
||||
|
||||
began=$SECONDS
|
||||
out=$(run_wd 60 -File "$(nativepath "$wdscript")" \
|
||||
-ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 5) ||
|
||||
fail "the $psrun loop exited nonzero: $out"
|
||||
spent=$((SECONDS - began))
|
||||
test "$spent" -ge 4 || fail "a 5s watchdog returned after ${spent}s: MaxSeconds is not seconds"
|
||||
test "$spent" -le 20 || fail "a 5s watchdog ran ${spent}s: it does not stop on its own"
|
||||
fast=$(grep -c 't=[0-9]*s q=' <<<"$out" || true)
|
||||
# Against the slow leg, not a floor: a probe tick is not instant.
|
||||
test "$fast" -gt "$slow" || fail "$fast status lines at 1s against $slow at 100s: $out"
|
||||
grep -q '36_local-bigcrawl.test' <<<"$out" || fail "the status does not name the test in flight: $out"
|
||||
grep -q '95_local-sitemap' <<<"$out" && fail "the status named a test that was never in flight"
|
||||
# Trips if the scrub and -NoPost were both lost: only a real attempt logs this.
|
||||
grep -q 'status post failed' <<<"$out" && fail "a test leg reached the status API"
|
||||
|
||||
# An unreadable log must not read as a wedged one.
|
||||
out=$(run_wd 60 -File "$(nativepath "$wdscript")" \
|
||||
-ProgressLog "$(nativepath "$tmp/no-such.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 3) ||
|
||||
fail "the $psrun loop exited nonzero on a missing log: $out"
|
||||
grep -q 'q=?s' <<<"$out" || fail "a missing log reported a staticness: $out"
|
||||
|
||||
# The legs below read the sink, not the loop, and they exercise the script's
|
||||
# decisions rather than the interpreter's: one flavour is enough.
|
||||
test "$first" -eq 1 || continue
|
||||
first=0
|
||||
if test -z "$py"; then
|
||||
echo "note: no python3 here, the posting legs did not run"
|
||||
continue
|
||||
fi
|
||||
|
||||
# ENOSPC on stdout, a state the sick runner reaches. Measured, since the leg
|
||||
# rests on it: MSYS hands a native PowerShell a /dev/full it can write to.
|
||||
devfull=0
|
||||
# shellcheck disable=SC2016 # PowerShell source, not this shell's expansions
|
||||
if test -w /dev/full &&
|
||||
! run_with_timeout 60 "$psrun" "${psargs[@]}" \
|
||||
-Command '$ErrorActionPreference = "Stop"; Write-Host ("x" * 200000)' \
|
||||
>/dev/full 2>/dev/null; then
|
||||
devfull=1
|
||||
fi
|
||||
legs=(cadence throttle staticness backoff)
|
||||
test "$devfull" -eq 0 || legs+=(fullstdout)
|
||||
for leg in "${legs[@]}"; do
|
||||
: >"$legout"
|
||||
said=$("${leg}_leg" "$(nativepath "$wdscript")") ||
|
||||
fail "the $psrun watchdog fails the $leg leg: $said"
|
||||
token_free "$legout" || fail "the $leg leg logged the status token"
|
||||
done
|
||||
# Control: a catch block widened to the whole exception is how it would get out.
|
||||
leaky="$tmp/token-in-log.ps1"
|
||||
# shellcheck disable=SC2016 # PowerShell variables, quoted for sed
|
||||
sed '/status post failed/s/$_.Exception.Message/$script:Token/' "$wdscript" >"$leaky"
|
||||
cmp -s "$wdscript" "$leaky" && fail "the leaky mutant changed nothing, so it proves nothing"
|
||||
backoff_leg "$(nativepath "$leaky")" >/dev/null || true
|
||||
token_free "$legout" && fail "the token audit cannot see a logged token"
|
||||
|
||||
# Same shape as mutate() above, graded by a leg instead of the self-test, and
|
||||
# on the diagnosis rather than the exit status for the same reason.
|
||||
mutate_leg() {
|
||||
local name=$1 expr=$2 leg=$3 want=$4 m="$tmp/$1.ps1" said
|
||||
sed "$expr" "$wdscript" >"$m"
|
||||
cmp -s "$wdscript" "$m" && fail "mutant $name changed nothing, so it proves nothing"
|
||||
said=$("${leg}_leg" "$(nativepath "$m")") &&
|
||||
fail "mutant $name passed the $leg leg"
|
||||
grep -q "$want" <<<"$said" || fail "mutant $name tripped no check of its own: $said"
|
||||
}
|
||||
mutate_leg status-every-poll \
|
||||
"s/if ((Get-WatchdogAction \$now \$postedAt \$IntervalSeconds) -eq 'post')/if (\$true)/" \
|
||||
cadence 'API calls at a 100s cadence'
|
||||
# shellcheck disable=SC2016 # PowerShell variables, quoted for sed
|
||||
mutate_leg static-unknown 's/\$static = \$now - \$movedAt/$static = -1/' \
|
||||
cadence 'posted no staticness'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg static-elapsed 's/\$static = \$now - \$movedAt/$static = $now/' \
|
||||
staticness 'read the log as moving'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg static-capped 's/if (\$tail.Ok) { \$static = \$now - \$movedAt }/if ($tail.Ok) { $static = [Math]::Min($now - $movedAt, 5) }/' \
|
||||
staticness 'staticness moved'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg skip-when-none-owed 's/if (\$skip -gt 0)/if ($skip -ge 0)/' \
|
||||
throttle 'a landed post throttles the next'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg success-skips 's/if (\$Ok) { return @(0, 0) }/if ($Ok) { return @(1, 0) }/' \
|
||||
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'
|
||||
if test "$devfull" -eq 1; then
|
||||
mutate_leg log-write-fatal 's/try { \(Write-Host .*\) } catch { }/\1/' \
|
||||
fullstdout 'took the loop with it'
|
||||
fi
|
||||
done
|
||||
|
||||
echo "native watchdog OK (${psruns[*]})"
|
||||
151
tests/227_watchdog-poll.test
Normal file
151
tests/227_watchdog-poll.test
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# testlib's polling helpers: the deadline tick every watchdog turns on, and the
|
||||
# wait for a fixture server's port line. Both used to fork a process per tick,
|
||||
# which is what the whole harness is built out of (#795).
|
||||
|
||||
set -eu
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_poll.XXXXXX") || exit 1
|
||||
# A colon-free root for the PATH shim below: MSYS hands out a drive-letter TMPDIR
|
||||
# and a PATH entry carrying a colon is read as two, which left every count at zero.
|
||||
shim=$(mktemp -d /tmp/httrack_pollshim.XXXXXX 2>/dev/null) || shim="$tmp/count"
|
||||
trap 'set +e; rm -rf "$tmp" "$shim"' EXIT
|
||||
trap 'rm -rf "$tmp" "$shim"' HUP INT QUIT PIPE TERM
|
||||
|
||||
# --- the tick waits, and costs no process where the fd timer works -----------
|
||||
count_sleeps() { # count_sleeps <dir> <log>: a sleep shim recording each call
|
||||
mkdir -p "$1"
|
||||
printf '#!/bin/sh\necho tick >>"%s"\nexec %s "$@"\n' "$2" "$(command -v sleep)" >"$1/sleep"
|
||||
chmod +x "$1/sleep"
|
||||
PATH="$1:$PATH"
|
||||
export PATH
|
||||
# Proved, not assumed: an unreachable shim counts zero sleeps, which reads as
|
||||
# "nothing forked" and would pass the very assertions it silently disarmed.
|
||||
: >"$2"
|
||||
sleep 0
|
||||
test -s "$2" || fail "the sleep shim is not on PATH (entry '$1')"
|
||||
}
|
||||
ticks() { # ticks <label> <secs> <count> <floor>: polls, counting the sleeps forked
|
||||
local began=$SECONDS
|
||||
local i=0
|
||||
: >"$tmp/sleeps"
|
||||
while test "$i" -lt "$3"; do
|
||||
poll_wait "$2"
|
||||
i=$((i + 1))
|
||||
done
|
||||
# Floored at the whole wait asked for, so a tick running at half speed fails
|
||||
# too, not only one that returns at once and spins every deadline loop.
|
||||
test "$((SECONDS - began))" -ge "$4" || fail "$1: $3 polls of ${2}s took under ${4}s"
|
||||
}
|
||||
sleeps() { wc -l <"$tmp/sleeps" | tr -d ' '; }
|
||||
# Probed, not assumed: a host that cannot make a fifo -- and Windows, which opts
|
||||
# out -- is entitled to the forked tick, but one that can and took it anyway has
|
||||
# lost the whole point. The override counts as opting out too, or the escape hatch
|
||||
# for a host whose fd tick misbehaves would red the suite it exists to rescue.
|
||||
canfifo=
|
||||
if test -z "${HTTRACK_POLL_SLEEP:-}" && ! is_windows && mkfifo "$tmp/probe" 2>/dev/null; then
|
||||
canfifo=1
|
||||
fi
|
||||
(
|
||||
count_sleeps "$shim" "$tmp/sleeps"
|
||||
poll_wait 0.1 # settles POLL_STATE before the count
|
||||
echo "poll tick: $POLL_STATE"
|
||||
ticks "$POLL_STATE tick" 0.1 30 3
|
||||
test -z "$canfifo" || test "$POLL_STATE" = fifo ||
|
||||
fail "the tick fell back to sleep on a host with working fifos"
|
||||
if test "$POLL_STATE" = fifo; then
|
||||
if test "${BASH_VERSINFO[0]}" -ge 4; then
|
||||
test "$(sleeps)" -eq 0 || fail "the fd tick forked $(sleeps) sleeps"
|
||||
else
|
||||
# No fractional read -t below bash 4, so the documented fallback is the sleep asked for.
|
||||
test "$(sleeps)" -eq 30 || fail "bash ${BASH_VERSINFO[0]} forked $(sleeps) of 30 fractional ticks"
|
||||
fi
|
||||
# The whole-second tick every watchdog runs on must take the fd on every bash:
|
||||
# where a fraction cannot, it is the only win left. Sized for ~3s either way,
|
||||
# bash 4 shortening its own wait to 0.1s.
|
||||
if test "${BASH_VERSINFO[0]}" -ge 4; then n=30; else n=3; fi
|
||||
ticks "fd 1s tick" 1 "$n" 3
|
||||
test "$(sleeps)" -eq 0 || fail "the 1s fd tick forked $(sleeps) sleeps"
|
||||
fi
|
||||
# Forced back to sleep(1), which is the only tick a shimmed sleep can reach.
|
||||
# POLL_STATE is left as it settled: a run already on the fd must give it up
|
||||
# when a test starves the poll mid-run, so the override is read per tick.
|
||||
export HTTRACK_POLL_SLEEP=1
|
||||
ticks "forced sleep tick" 0.1 30 3
|
||||
test "$(sleeps)" -eq 30 || fail "HTTRACK_POLL_SLEEP did not fork a sleep per tick"
|
||||
# A timer fd that stops timing out must fall back, not spin. Whole seconds: a
|
||||
# fraction never reaches the fd below bash 4.
|
||||
unset HTTRACK_POLL_SLEEP # the fd is what is under test here, not the override
|
||||
POLL_STATE=fifo
|
||||
exec 9<&-
|
||||
: >"$tmp/sleeps"
|
||||
poll_wait 1 2>/dev/null
|
||||
test -s "$tmp/sleeps" || fail "a dead timer fd did not fall back to sleep"
|
||||
test "$POLL_STATE" = forked || fail "a dead timer fd was kept ($POLL_STATE)"
|
||||
)
|
||||
|
||||
# --- the port wait outlasts a slow announcement ------------------------------
|
||||
# A cold Python start lags seconds on a loaded runner, so falling through early
|
||||
# would fail every crawl test on exactly the hosts the wait exists for.
|
||||
log="$tmp/late.log"
|
||||
: >"$log"
|
||||
(
|
||||
sleep 2
|
||||
echo "warning: something merged from stderr"
|
||||
echo "PORT 40001"
|
||||
sleep 30
|
||||
) >>"$log" 2>&1 &
|
||||
writer=$!
|
||||
began=$SECONDS
|
||||
port=$(discover_server_port "$log" "$writer") || fail "the late port line was missed"
|
||||
test "$port" = 40001 || fail "read port '$port', want 40001"
|
||||
test "$((SECONDS - began))" -ge 2 || fail "the port arrived before it was written"
|
||||
kill "$writer" 2>/dev/null || true
|
||||
wait "$writer" 2>/dev/null || true
|
||||
|
||||
# --- a half-written line is not a port ---------------------------------------
|
||||
# The announcement is one write in practice, but reading it in two would hand the
|
||||
# crawl a truncated port number, which resolves to a different server or none.
|
||||
log="$tmp/split.log"
|
||||
printf 'PORT 40' >"$log"
|
||||
(
|
||||
sleep 2
|
||||
printf '002\n'
|
||||
sleep 30
|
||||
) >>"$log" 2>&1 &
|
||||
writer=$!
|
||||
port=$(discover_server_port "$log" "$writer") || fail "the completed port line was missed"
|
||||
test "$port" = 40002 || fail "read port '$port' from a split line, want 40002"
|
||||
kill "$writer" 2>/dev/null || true
|
||||
wait "$writer" 2>/dev/null || true
|
||||
|
||||
# --- a server that dies without announcing is reported, not waited out -------
|
||||
log="$tmp/dead.log"
|
||||
echo "Traceback: address already in use" >"$log"
|
||||
(exit 1) &
|
||||
writer=$!
|
||||
wait "$writer" 2>/dev/null || true
|
||||
began=$SECONDS
|
||||
rc=0
|
||||
err=$(discover_server_port "$log" "$writer" 2>&1 >/dev/null) || rc=$?
|
||||
test "$rc" -ne 0 || fail "a dead server yielded a port"
|
||||
test "$((SECONDS - began))" -lt 20 || fail "a dead server was waited out ($((SECONDS - began))s)"
|
||||
case "$err" in
|
||||
*"exited early"*) ;;
|
||||
*) fail "a dead server reported '$err'" ;;
|
||||
esac
|
||||
# 2, not 1: only the deadline is the announce race 72 and 105 skip on, so one
|
||||
# status for both had local-crawl.sh skipping a crashed server instead of failing.
|
||||
test "$rc" -eq 2 || fail "a dead server exited $rc, want 2"
|
||||
|
||||
echo "poll helpers OK"
|
||||
46
tests/228_icon-small-flat.test
Normal file
46
tests/228_icon-small-flat.test
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Issue #938: downscaling the vector master to 16 px antialiased every stem to
|
||||
# grey. The small raster is drawn on the pixel grid, so it must stay flat.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
srcdir="${abs_top_srcdir:?not run under make check}"
|
||||
div="${srcdir}/html/server/div"
|
||||
|
||||
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
colors() {
|
||||
"${python}" "${testdir}/png-colors.py" "${div}/$1/httrack.png" ||
|
||||
fail "$1: cannot read the icon"
|
||||
}
|
||||
|
||||
small=$(colors 16x16)
|
||||
n=$(wc -l <<<"${small}")
|
||||
test "${n}" -le 4 || fail "16x16 icon has ${n} colours, the stems are antialiased"
|
||||
|
||||
# Both flats present: an all-field tile and an all-ink one are flat too.
|
||||
for flat in '#9999cc' '#040404'; do
|
||||
grep -q " ${flat}\$" <<<"${small}" || fail "16x16 icon has no ${flat} pixel"
|
||||
done
|
||||
|
||||
# A hairline and a near-solid block both satisfy every count above.
|
||||
ink=$(sed -n "s/ #040404\$//p" <<<"${small}")
|
||||
if test "${ink}" -lt 26 || test "${ink}" -gt 128; then
|
||||
fail "16x16 icon inks ${ink} of 256 pixels"
|
||||
fi
|
||||
|
||||
# Control: a counter blind to antialiasing would pass every assertion above.
|
||||
big=$(wc -l <<<"$(colors 48x48)")
|
||||
test "${big}" -gt 16 || fail "48x48 icon has ${big} colours, the counter is blind"
|
||||
|
||||
echo "16x16 icon: ${n} colours, ${ink}/256 ink; 48x48: ${big} colours"
|
||||
94
tests/230_local-ftp-userpass.test
Executable file
94
tests/230_local-ftp-userpass.test
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The credentials an URL carries must reach the control channel whole: a clip
|
||||
# logged in as a different account (#1032). The engine test only ever probes a
|
||||
# dead port, so nothing there reads what the login actually sends.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
# Starves the x64 CI runner until the whole suite step is lost; Win32 is fine (#1038).
|
||||
if is_windows; then
|
||||
echo "windows: wedges the x64 runner (#1038), skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
|
||||
command -v httrack >/dev/null || {
|
||||
echo "could not find httrack" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
server=$(nativepath "${testdir}/ftp-server.py")
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftpuser.XXXXXX")
|
||||
serverpid=
|
||||
cleanup() {
|
||||
stop_server "$serverpid"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap 'set +e; cleanup' EXIT
|
||||
trap cleanup HUP INT QUIT PIPE TERM
|
||||
|
||||
root="${tmpdir}/root"
|
||||
out="${tmpdir}/crawl"
|
||||
cmds="${tmpdir}/cmds"
|
||||
mkdir -p "$root" "$out"
|
||||
printf 'body\n' >"${root}/f.txt"
|
||||
|
||||
serverlog="${tmpdir}/server.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" --require-pass \
|
||||
--log "$(nativepath "$cmds")" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
ok() { echo "OK: $*"; }
|
||||
rep() { awk -v n="$2" -v c="$1" 'BEGIN { while (i++ < n) printf "%s", c }'; }
|
||||
|
||||
crawl() {
|
||||
: >"$cmds"
|
||||
rm -rf "${out:?}"
|
||||
mkdir -p "$out"
|
||||
run_with_timeout 60 httrack "ftp://$1@127.0.0.1:${port}/f.txt" -O "$out" \
|
||||
--quiet --disable-security-limits --robots=0 --timeout=20 \
|
||||
--max-time=45 --retries=1 -c1 >"${tmpdir}/log" 2>&1
|
||||
}
|
||||
|
||||
# --- a plain login goes out unchanged ----------------------------------------
|
||||
crawl "bob:secret" || fail "the plain-login crawl never finished"
|
||||
sent=$(cat "$cmds")
|
||||
grep -qxF "USER bob" <<<"$sent" || fail "no USER bob on the wire: ${sent}"
|
||||
grep -qxF "PASS secret" <<<"$sent" || fail "no PASS secret on the wire: ${sent}"
|
||||
cmp -s "${out}/127.0.0.1_${port}/f.txt" "${root}/f.txt" ||
|
||||
fail "the login did not bring the body back; mirror holds $(find "$out" -type f)"
|
||||
ok "bob:secret reaches the server verbatim, and mirrors"
|
||||
|
||||
# --- the longest userinfo that fits, byte for byte ---------------------------
|
||||
# 255 of each: a 256th byte is refused, so this is what the wire must carry.
|
||||
user=$(rep a 255)
|
||||
pass=$(rep b 255)
|
||||
crawl "${user}:${pass}" || fail "the 255-byte login crawl never finished"
|
||||
sent=$(cat "$cmds")
|
||||
grep -qxF "USER ${user}" <<<"$sent" ||
|
||||
fail "the wire user is not the 255 bytes the URL named: $(grep -a '^USER' <<<"$sent")"
|
||||
grep -qxF "PASS ${pass}" <<<"$sent" ||
|
||||
fail "the wire pass is not the 255 bytes the URL named: $(grep -a '^PASS' <<<"$sent")"
|
||||
ok "a 255-byte user and password reach the server unclipped"
|
||||
|
||||
# --- over-long userinfo never opens a session --------------------------------
|
||||
# The bare name has no ':' to bound it, only the '@', and is the worse branch.
|
||||
for u in "$(rep a 256):pw" "$(rep a 256)"; do
|
||||
crawl "$u" || fail "the over-long crawl never finished"
|
||||
grep -aq "FTP user name or password too long" "${out}/hts-log.txt" ||
|
||||
fail "over-long userinfo was not refused: $(cat "${out}/hts-log.txt")"
|
||||
sent=$(cat "$cmds")
|
||||
test -z "$sent" || fail "the engine logged in under a clipped name: ${sent}"
|
||||
done
|
||||
ok "an over-long name is refused before anything reaches the control channel"
|
||||
@@ -12,7 +12,10 @@ testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_wd.XXXXXX")
|
||||
trap 'set +e; rm -rf "$tmp"' EXIT
|
||||
# A colon-free root for the PATH shim below: MSYS hands out a drive-letter TMPDIR
|
||||
# and a PATH entry carrying a colon is read as two, which leaves it unreachable.
|
||||
shim=$(mktemp -d /tmp/httrack_wdshim.XXXXXX 2>/dev/null) || shim="$tmp/starve"
|
||||
trap 'set +e; rm -rf "$tmp" "$shim"' EXIT
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
@@ -73,7 +76,10 @@ fi
|
||||
# Wall clock, not iteration count: under starvation a poll costs more than it asks,
|
||||
# so a counted deadline never fires before the CI step it was meant to beat.
|
||||
(
|
||||
starve_sleep "$tmp/starve" 4 || fail "could not install the slow sleep"
|
||||
# The forked tick is the only one a shimmed sleep can reach: against the fd
|
||||
# tick nothing here would be starved but the payload, and the case goes inert.
|
||||
export HTTRACK_POLL_SLEEP=1
|
||||
starve_sleep "$shim" 4 || fail "could not install the slow sleep"
|
||||
# Control: starvation must not make the watchdog kill work that is inside its
|
||||
# budget. The command outlives several polls, so a guard that fires on the
|
||||
# first one cannot pass this by exiting before it is ever looked at.
|
||||
|
||||
@@ -8,14 +8,15 @@ EXTRA_DIST = $(TESTS) renamefail.c threadattrfail.c nobacktrace.c altstackprobe.
|
||||
proxy-https-server.py socks5-server.py proxy-connect-server.py \
|
||||
proxytestlib.py tls-stall-server.py warc-validate.py wacz-validate.py \
|
||||
header-injection-server.py header-injection-check.py \
|
||||
pty-resize.py test-timeout.sh \
|
||||
pty-resize.py test-timeout.sh png-colors.py \
|
||||
local-crawl.sh local-server.py ftp-server.py testlib.sh \
|
||||
ci-windows-suite.sh \
|
||||
ci-windows-suite.sh ci-windows-watchdog.ps1 \
|
||||
server.crt server.key \
|
||||
server-root/simple/basic.html server-root/simple/link.html \
|
||||
server-root/stripquery/index.html server-root/stripquery/a.html \
|
||||
server-root/fraglink/index.html server-root/fraglink/target.html \
|
||||
fixtures/cache-golden/hts-cache/new.zip
|
||||
fixtures/cache-golden/hts-cache/new.zip \
|
||||
install-manifest.txt
|
||||
|
||||
TESTS_ENVIRONMENT =
|
||||
TESTS_ENVIRONMENT += PATH=$(top_builddir)/src$(PATH_SEPARATOR)$$PATH
|
||||
@@ -34,6 +35,10 @@ TESTS_ENVIRONMENT += abs_top_builddir=$(abs_top_builddir)
|
||||
TESTS_ENVIRONMENT += CONFIGURED_DATADIR=$(datadir)
|
||||
TESTS_ENVIRONMENT += CONFIGURED_LIBDIR=$(libdir)
|
||||
TESTS_ENVIRONMENT += CONFIGURED_BINDIR=$(bindir)
|
||||
# 225_install-manifest.test folds these back out of the staged paths.
|
||||
TESTS_ENVIRONMENT += CONFIGURED_MANDIR=$(mandir)
|
||||
TESTS_ENVIRONMENT += CONFIGURED_DOCDIR=$(docdir)
|
||||
TESTS_ENVIRONMENT += CONFIGURED_HTMLDIR=$(htmldir)
|
||||
TESTS_ENVIRONMENT += ORIGIN_RPATH=$(ORIGIN_RPATH)
|
||||
# 222_pkgconfig-consumer.test asserts the staged .pc against these, and both
|
||||
# sides of the gate deciding whether it hands a consumer an rpath.
|
||||
|
||||
@@ -29,6 +29,41 @@ ci_annotate() {
|
||||
# schedule the workflow really passes off a virtual clock instead of waiting it out.
|
||||
hb_now() { echo "$SECONDS"; }
|
||||
|
||||
# Start the off-box telemetry over the progress log $1, setting ci_watchdog_pid;
|
||||
# return 1 with no PowerShell available. Forks nothing and kills nothing, so it
|
||||
# reports where the heartbeat below cannot (#795).
|
||||
ci_start_native_watchdog() {
|
||||
local progress=$1 ps1 c exe='' waited=0
|
||||
ps1="$testdir/ci-windows-watchdog.ps1"
|
||||
test -r "$ps1" || return 1
|
||||
for c in pwsh powershell.exe; do
|
||||
if command -v "$c" >/dev/null 2>&1; then
|
||||
exe=$c
|
||||
break
|
||||
fi
|
||||
done
|
||||
test -n "$exe" || return 1
|
||||
# Never the step's stdout: a background holder of that pipe keeps the step
|
||||
# open past the suite (#949), and tests/*.log reaches the artifact anyway.
|
||||
: >watchdog.log
|
||||
WATCHDOG_TOKEN="${ci_watchdog_token:-}" \
|
||||
"$exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass \
|
||||
-File "$(nativepath "$ps1")" \
|
||||
-ProgressLog "$(nativepath "$progress")" \
|
||||
>>watchdog.log 2>&1 &
|
||||
ci_watchdog_pid=$!
|
||||
# $! comes from the fork, not the exec: wait for it to actually speak.
|
||||
while test "$waited" -lt 30; do
|
||||
grep -q '^watchdog ready$' watchdog.log && return 0
|
||||
kill -0 "$ci_watchdog_pid" 2>/dev/null || break
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
kill_pid "$ci_watchdog_pid"
|
||||
ci_watchdog_pid=''
|
||||
return 1
|
||||
}
|
||||
|
||||
ci_suite_heartbeat() {
|
||||
local quiet=$1 every=$2 progress=$3 stuck=$4 main=$5
|
||||
local tick=$2 begin now line said moved last=''
|
||||
@@ -48,6 +83,9 @@ ci_suite_heartbeat() {
|
||||
# outlive the step's own timeout before being caught.
|
||||
if test $((now - moved)) -ge "$stuck"; then
|
||||
ci_annotate error "suite watchdog" "killing the step: $((now - moved))s without progress, in flight: $last"
|
||||
# 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"
|
||||
# Direct first: kill_tree may reap this watchdog before its own root (#953).
|
||||
kill_pid "$main"
|
||||
kill_tree "$main"
|
||||
@@ -77,6 +115,11 @@ command -v httrack >/dev/null || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Out of the environment before the first child forks below: an inherited token
|
||||
# could post a commit status in the name of the run.
|
||||
ci_watchdog_token=${WATCHDOG_TOKEN:-}
|
||||
unset WATCHDOG_TOKEN
|
||||
|
||||
# httrack.exe is native, so MSYS rewrites any argument shaped like a
|
||||
# POSIX path, and a URL path is shaped exactly like one: "/a/b.html"
|
||||
# reached the engine as "C:/Program Files/Git/a/b.html". Switch that
|
||||
@@ -119,9 +162,16 @@ export HTTRACK_PROGRESS_LOG="$PWD/$progress"
|
||||
# its own terms keeps its log. Quiet past 16 min, clear of the 13 a healthy
|
||||
# run measures here, and a kill 900s after the last progress line clears
|
||||
# the longest legitimate gap, one $per_test.
|
||||
ci_suite_heartbeat 960 360 "$progress" 900 $$ &
|
||||
stuck=900
|
||||
# Orthogonal to that heartbeat rather than a spare of it: the heartbeat needs
|
||||
# 960s of quiet and 900s of static log, and every #795 death measured so far
|
||||
# lands inside the first 750s of the step.
|
||||
watchdog='' ci_watchdog_pid=''
|
||||
ci_start_native_watchdog "$PWD/$progress" && watchdog=$ci_watchdog_pid
|
||||
test -n "$watchdog" || echo "no off-box watchdog: no usable PowerShell"
|
||||
ci_suite_heartbeat 960 360 "$progress" "$stuck" $$ &
|
||||
heartbeat=$!
|
||||
trap 'kill "$heartbeat" 2>/dev/null || true' EXIT
|
||||
trap 'set +e; kill "$heartbeat" 2>/dev/null; test -z "$watchdog" || kill_pid "$watchdog"' EXIT
|
||||
|
||||
pass=0 fail=0 skip=0 failed="" skipped="" deadline=0
|
||||
# label:pattern, globbed rather than enumerated so a new NNN_engine-*.test or
|
||||
@@ -217,7 +267,8 @@ echo "ran=$((pass + fail + skip)) pass=$pass fail=$fail skip=$skip" |
|
||||
# datadir-ospath copies the unwrapped binary the automake build leaves in .libs,
|
||||
# and needs the loader variable libtool picked, neither of which this job has;
|
||||
# link-control-bytes names its fixtures with the raw control bytes the requests
|
||||
# decode back to, which NTFS refuses.
|
||||
# decode back to, which NTFS refuses;
|
||||
# ftp-userpass starves the x64 runner until the step is lost, Win32 passing (#1038).
|
||||
expected_skips="01_engine-footer-overflow.test
|
||||
100_local-purge-longpath.test
|
||||
158_local-link-control-bytes.test
|
||||
@@ -228,6 +279,7 @@ expected_skips="01_engine-footer-overflow.test
|
||||
152_engine-string-oom.test
|
||||
153_local-proxytrack-quiet.test
|
||||
215_engine-datadir-ospath.test
|
||||
230_local-ftp-userpass.test
|
||||
48_local-crange-memresume.test
|
||||
71_local-crange-repaircache.test
|
||||
79_local-proxytrack-webdav-mime.test
|
||||
|
||||
255
tests/ci-windows-watchdog.ps1
Normal file
255
tests/ci-windows-watchdog.ps1
Normal file
@@ -0,0 +1,255 @@
|
||||
# Off-box telemetry for the Windows suite step (#795): it spawns and kills
|
||||
# nothing, so a wedge cannot disarm it, and reports as a commit status, the only
|
||||
# channel that outlives a dead runner. Every status carries the same state.
|
||||
param(
|
||||
[string]$ProgressLog = '',
|
||||
[int]$IntervalSeconds = 30,
|
||||
[int]$PollSeconds = 5,
|
||||
# Cannot outlive the step, whatever the caller forgets to kill.
|
||||
[int]$MaxSeconds = 2700,
|
||||
# Seam for the suite's own test, which points it at a sink it can count.
|
||||
[string]$ApiBase = 'https://api.github.com',
|
||||
[switch]$NoPost,
|
||||
[switch]$SelfTest
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# Windows PowerShell renders a progress bar per web call otherwise.
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
# --- decisions, kept pure so -SelfTest can drive them by return value ---------
|
||||
|
||||
function Get-WatchdogAction {
|
||||
param([int]$Now, [int]$Posted, [int]$Interval)
|
||||
if (($Now - $Posted) -ge $Interval) { return 'post' }
|
||||
return 'wait'
|
||||
}
|
||||
|
||||
# Posts to skip after a rejected one: a fork PR's token is read-only for the run.
|
||||
function Get-NextBackoff {
|
||||
param([int]$Current)
|
||||
if ($Current -lt 1) { return 1 }
|
||||
return [Math]::Min($Current * 2, 32)
|
||||
}
|
||||
|
||||
# The (skip, backoff) pair after an attempted post, $Ok being whether it landed:
|
||||
# a rejection that later resolves has to leave nothing behind.
|
||||
function Get-NextThrottle {
|
||||
param([bool]$Ok, [int]$Backoff)
|
||||
if ($Ok) { return @(0, 0) }
|
||||
$b = Get-NextBackoff $Backoff
|
||||
return @($b, $b)
|
||||
}
|
||||
|
||||
# GitHub truncates a description at 140 chars, so the counters take the cut, not
|
||||
# the fields a wedge is read for. $Static below zero is unknown.
|
||||
function Format-WatchdogStatus {
|
||||
param([int]$Elapsed, [int]$Static, [string]$InFlight, [string]$Counters)
|
||||
$q = '?'
|
||||
if ($Static -ge 0) { $q = [string]$Static }
|
||||
$t = ($InFlight -replace '\s+', ' ').Trim()
|
||||
if ($t.Length -gt 46) { $t = $t.Substring(0, 46) }
|
||||
$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
|
||||
}
|
||||
|
||||
# --- probes ------------------------------------------------------------------
|
||||
|
||||
# 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 {
|
||||
$f = New-Object System.Collections.ArrayList
|
||||
try {
|
||||
$ps = @(Get-Process)
|
||||
[void]$f.Add('p={0}' -f $ps.Count)
|
||||
[void]$f.Add('h={0}' -f (($ps | Measure-Object -Property Handles -Sum).Sum))
|
||||
} 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))
|
||||
} catch { [void]$f.Add('d=?') }
|
||||
return ($f -join ' ')
|
||||
}
|
||||
|
||||
# Ok separates "nothing moved" from "could not read it", which would otherwise report
|
||||
# a wedge for an unreadable file. Share flags: the driver appends as we read.
|
||||
function Get-ProgressTail {
|
||||
param([string]$Path)
|
||||
$r = @{ Ok = $false; Signature = ''; Line = '' }
|
||||
if (-not $Path) { return $r }
|
||||
try {
|
||||
$share = [System.IO.FileShare]::ReadWrite -bor [System.IO.FileShare]::Delete
|
||||
$fs = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, $share)
|
||||
try {
|
||||
$sr = New-Object System.IO.StreamReader($fs)
|
||||
$text = $sr.ReadToEnd()
|
||||
} finally { $fs.Dispose() }
|
||||
$lines = @($text -split "`r?`n" | Where-Object { $_ -ne '' })
|
||||
if ($lines.Count -gt 0) { $r.Line = $lines[-1] }
|
||||
$r.Signature = '{0}|{1}' -f $text.Length, $r.Line
|
||||
$r.Ok = $true
|
||||
} catch { }
|
||||
return $r
|
||||
}
|
||||
|
||||
function Write-WatchdogLog {
|
||||
param([string]$Message)
|
||||
# A full disk is a state the sick runner reaches, and a log line lost to it
|
||||
# must not take the loop reporting off-box with it.
|
||||
try { Write-Host ('[watchdog {0:HH:mm:ss}] {1}' -f (Get-Date), $Message) } catch { }
|
||||
}
|
||||
|
||||
# --- reporting ---------------------------------------------------------------
|
||||
|
||||
$script:Repo = $env:WATCHDOG_REPO
|
||||
$script:Sha = $env:WATCHDOG_SHA
|
||||
$script:Token = $env:WATCHDOG_TOKEN
|
||||
$script:TargetUrl = $env:WATCHDOG_URL
|
||||
$script:Context = $env:WATCHDOG_CONTEXT
|
||||
if (-not $script:Context) { $script:Context = 'windows-suite-watchdog' }
|
||||
|
||||
# One state always, so nothing downstream can read a verdict out of telemetry:
|
||||
# GitHub records the job's conclusion already. -NoPost is the test's hard stop,
|
||||
# whatever the environment holds.
|
||||
function Send-WatchdogStatus {
|
||||
param([string]$Description)
|
||||
if ($NoPost -or $SelfTest) { return $false }
|
||||
if (-not $script:Token -or -not $script:Repo -or -not $script:Sha) { return $false }
|
||||
$body = @{ state = 'success'; context = $script:Context; description = $Description }
|
||||
if ($script:TargetUrl) { $body['target_url'] = $script:TargetUrl }
|
||||
try {
|
||||
Invoke-RestMethod -Method Post -TimeoutSec 20 `
|
||||
-Uri ('{0}/repos/{1}/statuses/{2}' -f $ApiBase.TrimEnd('/'), $script:Repo, $script:Sha) `
|
||||
-UserAgent 'httrack-windows-suite-watchdog' `
|
||||
-Headers @{
|
||||
Authorization = ('Bearer {0}' -f $script:Token)
|
||||
Accept = 'application/vnd.github+json'
|
||||
} -ContentType 'application/json' -Body ($body | ConvertTo-Json -Compress) | Out-Null
|
||||
return $true
|
||||
} catch {
|
||||
Write-WatchdogLog ('status post failed: {0}' -f $_.Exception.Message)
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# --- self-test ----------------------------------------------------------------
|
||||
|
||||
function Invoke-WatchdogSelfTest {
|
||||
$bad = New-Object System.Collections.ArrayList
|
||||
function Assert-That($cond, $what) { if (-not $cond) { [void]$bad.Add($what) } }
|
||||
|
||||
Assert-That ((Get-WatchdogAction 30 0 30) -eq 'post') 'a post due exactly on the interval was skipped'
|
||||
Assert-That ((Get-WatchdogAction 29 0 30) -eq 'wait') 'posted ahead of the interval'
|
||||
Assert-That ((Get-WatchdogAction 5000 4990 30) -eq 'wait') 'posted off cadence'
|
||||
|
||||
Assert-That ((Get-NextBackoff 0) -eq 1) 'the first rejection does not back off'
|
||||
Assert-That ((Get-NextBackoff 1) -eq 2) 'the backoff does not grow'
|
||||
Assert-That ((Get-NextBackoff 32) -eq 32) 'the backoff is not capped'
|
||||
|
||||
$ok = Get-NextThrottle $true 8
|
||||
Assert-That ($ok[0] -eq 0 -and $ok[1] -eq 0) 'a landed post leaves the run throttled'
|
||||
$ko = Get-NextThrottle $false 0
|
||||
Assert-That ($ko[0] -eq 1 -and $ko[1] -eq 1) 'a first rejection skips nothing'
|
||||
$ko = Get-NextThrottle $false 4
|
||||
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'
|
||||
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'
|
||||
# -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)
|
||||
$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.
|
||||
Assert-That ($wide -like 't=1s q=2s x*') ('clipping dropped the leading fields: {0}' -f $wide)
|
||||
|
||||
$gone = Get-ProgressTail -Path ('no-such-progress-log-{0}.tmp' -f $PID)
|
||||
Assert-That (-not $gone.Ok) 'an unreadable log reads as one that was read'
|
||||
$f = New-Object System.IO.FileInfo([System.IO.Path]::GetTempFileName())
|
||||
try {
|
||||
[System.IO.File]::WriteAllText($f.FullName, "first`nRUN 42_probe.test at 7s`n")
|
||||
$tail = Get-ProgressTail -Path $f.FullName
|
||||
Assert-That ($tail.Ok) 'a readable log reads as unreadable'
|
||||
Assert-That ($tail.Line -eq 'RUN 42_probe.test at 7s') ('the tail is not the last line: {0}' -f $tail.Line)
|
||||
} finally { [System.IO.File]::Delete($f.FullName) }
|
||||
|
||||
# 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
|
||||
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') {
|
||||
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)
|
||||
|
||||
# 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 ($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'
|
||||
|
||||
if ($bad.Count -gt 0) {
|
||||
foreach ($b in $bad) { Write-Host ('self-test FAIL: {0}' -f $b) }
|
||||
exit 1
|
||||
}
|
||||
Write-Host 'watchdog self-test OK'
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($SelfTest) { Invoke-WatchdogSelfTest }
|
||||
|
||||
# --- main loop ----------------------------------------------------------------
|
||||
|
||||
# Windows PowerShell still defaults below TLS 1.2, which api.github.com refuses.
|
||||
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { }
|
||||
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
$lastSig = ''
|
||||
$movedAt = 0
|
||||
# Negative, so the first tick posts: an early status is itself a datum.
|
||||
$postedAt = -$IntervalSeconds
|
||||
$backoff = 0
|
||||
$skip = 0
|
||||
|
||||
# Guarded like the rest; the launcher waits for this exact line.
|
||||
try { Write-Host 'watchdog ready' } catch { }
|
||||
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
|
||||
try {
|
||||
$tail = Get-ProgressTail -Path $ProgressLog
|
||||
if ($tail.Ok -and $tail.Signature -ne $lastSig) {
|
||||
$lastSig = $tail.Signature
|
||||
$movedAt = $now
|
||||
}
|
||||
if ((Get-WatchdogAction $now $postedAt $IntervalSeconds) -eq 'post') {
|
||||
$postedAt = $now
|
||||
$static = -1
|
||||
if ($tail.Ok) { $static = $now - $movedAt }
|
||||
$desc = Format-WatchdogStatus $now $static $tail.Line (Get-WatchdogCounters)
|
||||
# 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
|
||||
$skip = $next[0]
|
||||
$backoff = $next[1]
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-WatchdogLog ('tick failed: {0}' -f $_.Exception.Message)
|
||||
}
|
||||
Start-Sleep -Seconds $PollSeconds
|
||||
}
|
||||
|
||||
Write-WatchdogLog ('stopping after {0}s' -f [int]$sw.Elapsed.TotalSeconds)
|
||||
@@ -10,6 +10,8 @@ directory). Each line is "<path> <mode>...", path "*" matching everything:
|
||||
empty open the data connection and send nothing
|
||||
norest answer REST with 500, so the client re-fetches from scratch
|
||||
nomdtm answer MDTM with 500, like a server predating RFC 3659
|
||||
|
||||
--require-pass answers USER with 331, the only way the client's PASS is sent.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -25,12 +27,13 @@ def reply(conn, text):
|
||||
|
||||
|
||||
class Session(threading.Thread):
|
||||
def __init__(self, conn, root, mode_file, log):
|
||||
def __init__(self, conn, root, mode_file, log, require_pass=False):
|
||||
threading.Thread.__init__(self, daemon=True)
|
||||
self.conn = conn
|
||||
self.root = root
|
||||
self.mode_file = mode_file
|
||||
self.log = log
|
||||
self.require_pass = require_pass
|
||||
self.pasv = None
|
||||
self.rest = 0
|
||||
self.path = "/" # named by SIZE/RETR; REST carries no path
|
||||
@@ -132,7 +135,9 @@ class Session(threading.Thread):
|
||||
|
||||
def dispatch(self, verb, arg):
|
||||
conn = self.conn
|
||||
if verb in ("USER", "PASS", "TYPE", "NOOP"):
|
||||
if verb == "USER" and self.require_pass:
|
||||
reply(conn, "331 password required") # the client sends PASS only on a 3xx
|
||||
elif verb in ("USER", "PASS", "TYPE", "NOOP"):
|
||||
reply(conn, "200 ok")
|
||||
elif verb == "SYST":
|
||||
reply(conn, "215 UNIX Type: L8")
|
||||
@@ -209,6 +214,7 @@ def main():
|
||||
ap.add_argument("--root", required=True)
|
||||
ap.add_argument("--mode-file")
|
||||
ap.add_argument("--log")
|
||||
ap.add_argument("--require-pass", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
logfp = open(args.log, "a", encoding="utf-8") if args.log else None
|
||||
@@ -231,7 +237,7 @@ def main():
|
||||
root = os.path.abspath(args.root)
|
||||
while True:
|
||||
conn, _ = srv.accept()
|
||||
Session(conn, root, args.mode_file, log).start()
|
||||
Session(conn, root, args.mode_file, log, args.require_pass).start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
123
tests/install-manifest.txt
Normal file
123
tests/install-manifest.txt
Normal file
@@ -0,0 +1,123 @@
|
||||
@BINDIR@/htsserver
|
||||
@BINDIR@/httrack
|
||||
@BINDIR@/proxytrack
|
||||
@BINDIR@/webhttrack
|
||||
@DATADIR@/applications/WebHTTrack-Websites.desktop
|
||||
@DATADIR@/applications/WebHTTrack.desktop
|
||||
@DATADIR@/httrack/html
|
||||
@DATADIR@/httrack/lang.def
|
||||
@DATADIR@/httrack/lang.indexes
|
||||
@DATADIR@/httrack/lang/Bulgarian.txt
|
||||
@DATADIR@/httrack/lang/Castellano.txt
|
||||
@DATADIR@/httrack/lang/Cesky.txt
|
||||
@DATADIR@/httrack/lang/Chinese-BIG5.txt
|
||||
@DATADIR@/httrack/lang/Chinese-Simplified.txt
|
||||
@DATADIR@/httrack/lang/Croatian.txt
|
||||
@DATADIR@/httrack/lang/Dansk.txt
|
||||
@DATADIR@/httrack/lang/Deutsch.txt
|
||||
@DATADIR@/httrack/lang/Eesti.txt
|
||||
@DATADIR@/httrack/lang/English.txt
|
||||
@DATADIR@/httrack/lang/Finnish.txt
|
||||
@DATADIR@/httrack/lang/Francais.txt
|
||||
@DATADIR@/httrack/lang/Greek.txt
|
||||
@DATADIR@/httrack/lang/Italiano.txt
|
||||
@DATADIR@/httrack/lang/Japanese.txt
|
||||
@DATADIR@/httrack/lang/Macedonian.txt
|
||||
@DATADIR@/httrack/lang/Magyar.txt
|
||||
@DATADIR@/httrack/lang/Nederlands.txt
|
||||
@DATADIR@/httrack/lang/Norsk.txt
|
||||
@DATADIR@/httrack/lang/Polski.txt
|
||||
@DATADIR@/httrack/lang/Portugues-Brasil.txt
|
||||
@DATADIR@/httrack/lang/Portugues.txt
|
||||
@DATADIR@/httrack/lang/Romanian.txt
|
||||
@DATADIR@/httrack/lang/Russian.txt
|
||||
@DATADIR@/httrack/lang/Slovak.txt
|
||||
@DATADIR@/httrack/lang/Slovenian.txt
|
||||
@DATADIR@/httrack/lang/Svenska.txt
|
||||
@DATADIR@/httrack/lang/Turkish.txt
|
||||
@DATADIR@/httrack/lang/Ukrainian.txt
|
||||
@DATADIR@/httrack/lang/Uzbek.txt
|
||||
@DATADIR@/httrack/libtest/callbacks-example-baselinks.c
|
||||
@DATADIR@/httrack/libtest/callbacks-example-changecontent.c
|
||||
@DATADIR@/httrack/libtest/callbacks-example-contentfilter.c
|
||||
@DATADIR@/httrack/libtest/callbacks-example-displayheader.c
|
||||
@DATADIR@/httrack/libtest/callbacks-example-filename.c
|
||||
@DATADIR@/httrack/libtest/callbacks-example-filename2.c
|
||||
@DATADIR@/httrack/libtest/callbacks-example-filenameiisbug.c
|
||||
@DATADIR@/httrack/libtest/callbacks-example-listlinks.c
|
||||
@DATADIR@/httrack/libtest/callbacks-example-log.c
|
||||
@DATADIR@/httrack/libtest/callbacks-example-simple.c
|
||||
@DATADIR@/httrack/libtest/example-main.c
|
||||
@DATADIR@/httrack/libtest/example-main.h
|
||||
@DATADIR@/httrack/libtest/readme.txt
|
||||
@DATADIR@/httrack/templates/index-body.html
|
||||
@DATADIR@/httrack/templates/index-footer.html
|
||||
@DATADIR@/httrack/templates/index-header.html
|
||||
@DATADIR@/httrack/templates/topindex-body.html
|
||||
@DATADIR@/httrack/templates/topindex-bodycat.html
|
||||
@DATADIR@/httrack/templates/topindex-footer.html
|
||||
@DATADIR@/httrack/templates/topindex-header.html
|
||||
@DATADIR@/icons/hicolor/128x128/apps/httrack.png
|
||||
@DATADIR@/icons/hicolor/16x16/apps/httrack.png
|
||||
@DATADIR@/icons/hicolor/256x256/apps/httrack.png
|
||||
@DATADIR@/icons/hicolor/32x32/apps/httrack.png
|
||||
@DATADIR@/icons/hicolor/48x48/apps/httrack.png
|
||||
@DATADIR@/icons/hicolor/64x64/apps/httrack.png
|
||||
@DATADIR@/icons/hicolor/scalable/apps/httrack.svg
|
||||
@DATADIR@/metainfo/com.httrack.WebHTTrack.metainfo.xml
|
||||
@DATADIR@/pixmaps/httrack.xpm
|
||||
@DATADIR@/pixmaps/httrack16x16.xpm
|
||||
@DATADIR@/pixmaps/httrack32x32.xpm
|
||||
@DATADIR@/pixmaps/httrack48x48.xpm
|
||||
@INCLUDEDIR@/httrack/config.h
|
||||
@INCLUDEDIR@/httrack/htsarrays.h
|
||||
@INCLUDEDIR@/httrack/htsbasenet.h
|
||||
@INCLUDEDIR@/httrack/htsbauth.h
|
||||
@INCLUDEDIR@/httrack/htsconfig.h
|
||||
@INCLUDEDIR@/httrack/htsdefines.h
|
||||
@INCLUDEDIR@/httrack/htsglobal.h
|
||||
@INCLUDEDIR@/httrack/htsmodules.h
|
||||
@INCLUDEDIR@/httrack/htsnet.h
|
||||
@INCLUDEDIR@/httrack/htsopt.h
|
||||
@INCLUDEDIR@/httrack/htssafe.h
|
||||
@INCLUDEDIR@/httrack/htsstrings.h
|
||||
@INCLUDEDIR@/httrack/htswrap.h
|
||||
@INCLUDEDIR@/httrack/httrack-library.h
|
||||
@LIBDIR@/httrack/libbaselinks.so
|
||||
@LIBDIR@/httrack/libbaselinks.so.1
|
||||
@LIBDIR@/httrack/libbaselinks.so.1.@revision@
|
||||
@LIBDIR@/httrack/libchangecontent.so
|
||||
@LIBDIR@/httrack/libchangecontent.so.1
|
||||
@LIBDIR@/httrack/libchangecontent.so.1.@revision@
|
||||
@LIBDIR@/httrack/libcontentfilter.so
|
||||
@LIBDIR@/httrack/libcontentfilter.so.1
|
||||
@LIBDIR@/httrack/libcontentfilter.so.1.@revision@
|
||||
@LIBDIR@/httrack/libdisplayheader.so
|
||||
@LIBDIR@/httrack/libdisplayheader.so.1
|
||||
@LIBDIR@/httrack/libdisplayheader.so.1.@revision@
|
||||
@LIBDIR@/httrack/libfilename.so
|
||||
@LIBDIR@/httrack/libfilename.so.1
|
||||
@LIBDIR@/httrack/libfilename.so.1.@revision@
|
||||
@LIBDIR@/httrack/libfilename2.so
|
||||
@LIBDIR@/httrack/libfilename2.so.1
|
||||
@LIBDIR@/httrack/libfilename2.so.1.@revision@
|
||||
@LIBDIR@/httrack/libfilenameiisbug.so
|
||||
@LIBDIR@/httrack/libfilenameiisbug.so.1
|
||||
@LIBDIR@/httrack/libfilenameiisbug.so.1.@revision@
|
||||
@LIBDIR@/httrack/liblistlinks.so
|
||||
@LIBDIR@/httrack/liblistlinks.so.1
|
||||
@LIBDIR@/httrack/liblistlinks.so.1.@revision@
|
||||
@LIBDIR@/httrack/liblog.so
|
||||
@LIBDIR@/httrack/liblog.so.1
|
||||
@LIBDIR@/httrack/liblog.so.1.@revision@
|
||||
@LIBDIR@/httrack/libsimple.so
|
||||
@LIBDIR@/httrack/libsimple.so.1
|
||||
@LIBDIR@/httrack/libsimple.so.1.@revision@
|
||||
@LIBDIR@/libhttrack.so
|
||||
@LIBDIR@/libhttrack.so.3
|
||||
@LIBDIR@/libhttrack.so.3.@revision@
|
||||
@LIBDIR@/pkgconfig/libhttrack.pc
|
||||
@MANDIR@/man1/htsserver.1
|
||||
@MANDIR@/man1/httrack.1
|
||||
@MANDIR@/man1/proxytrack.1
|
||||
@MANDIR@/man1/webhttrack.1
|
||||
@@ -253,14 +253,12 @@ serverpid=$!
|
||||
|
||||
# Wait for the "PORT <n>" line (server prints it once bound). A cold Python
|
||||
# start under a parallel `make check -jN` can lag past a second on a loaded Windows runner.
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
# Match anywhere: a startup warning merged via 2>&1 could precede the PORT line.
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || die "server exited early: $(cat "$serverlog")"
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || die "could not discover server port: $(cat "$serverlog")"
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || {
|
||||
# A server that died is a hard failure; only the deadline is the announce race
|
||||
# 72 and 105 skip on, and they key on this exact wording.
|
||||
test "$?" -ne 2 || die "server exited early"
|
||||
die "could not discover server port"
|
||||
}
|
||||
debug "server listening on ${scheme}://127.0.0.1:${port}"
|
||||
|
||||
baseurl="${scheme}://127.0.0.1:${port}"
|
||||
|
||||
107
tests/png-colors.py
Executable file
107
tests/png-colors.py
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Print a PNG's distinct colours as "count #rrggbb", commonest first.
|
||||
|
||||
Usage: png-colors.py FILE
|
||||
|
||||
Stdlib only: the suite cannot assume Pillow or ImageMagick on a build host.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
CHANNELS = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}
|
||||
|
||||
|
||||
def chunks(blob):
|
||||
off = 8
|
||||
while off < len(blob):
|
||||
(n,) = struct.unpack(">I", blob[off : off + 4])
|
||||
yield blob[off + 4 : off + 8], blob[off + 8 : off + 8 + n]
|
||||
off += 12 + n
|
||||
|
||||
|
||||
def unfilter(raw, width, height, bpp, stride):
|
||||
"""Undo the per-scanline filters; returns the concatenated raw scanlines."""
|
||||
out, prev, off = bytearray(), bytearray(stride), 0
|
||||
for _ in range(height):
|
||||
ftype, line = raw[off], bytearray(raw[off + 1 : off + 1 + stride])
|
||||
off += 1 + stride
|
||||
for i in range(stride):
|
||||
a = line[i - bpp] if i >= bpp else 0
|
||||
b = prev[i]
|
||||
c = prev[i - bpp] if i >= bpp else 0
|
||||
if ftype == 1:
|
||||
line[i] = (line[i] + a) & 0xFF
|
||||
elif ftype == 2:
|
||||
line[i] = (line[i] + b) & 0xFF
|
||||
elif ftype == 3:
|
||||
line[i] = (line[i] + (a + b) // 2) & 0xFF
|
||||
elif ftype == 4:
|
||||
p = a + b - c
|
||||
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
|
||||
line[i] = (
|
||||
line[i] + (a if pa <= pb and pa <= pc else b if pb <= pc else c)
|
||||
) & 0xFF
|
||||
elif ftype != 0:
|
||||
raise SystemExit("unknown filter %d" % ftype)
|
||||
out += line
|
||||
prev = line
|
||||
return out
|
||||
|
||||
|
||||
def samples(row, depth, count):
|
||||
"""The first `count` samples of one unfiltered scanline."""
|
||||
if depth == 8:
|
||||
return list(row[:count])
|
||||
if depth == 16:
|
||||
return [row[i * 2] << 8 | row[i * 2 + 1] for i in range(count)]
|
||||
per, mask = 8 // depth, (1 << depth) - 1
|
||||
return [(row[i // per] >> (8 - depth * (i % per + 1))) & mask for i in range(count)]
|
||||
|
||||
|
||||
def colors(path):
|
||||
blob = open(path, "rb").read()
|
||||
if blob[:8] != b"\x89PNG\r\n\x1a\n":
|
||||
raise SystemExit("%s: not a PNG" % path)
|
||||
plte, idat = b"", b""
|
||||
for kind, data in chunks(blob):
|
||||
if kind == b"IHDR":
|
||||
width, height, depth, ctype, _, _, interlace = struct.unpack(
|
||||
">IIBBBBB", data
|
||||
)
|
||||
elif kind == b"PLTE":
|
||||
plte = data
|
||||
elif kind == b"IDAT":
|
||||
idat += data
|
||||
if interlace:
|
||||
raise SystemExit("%s: interlaced" % path)
|
||||
nchan = CHANNELS[ctype]
|
||||
stride = (width * nchan * depth + 7) // 8
|
||||
bpp = max(1, nchan * depth // 8)
|
||||
raw = unfilter(zlib.decompress(idat), width, height, bpp, stride)
|
||||
|
||||
tally = {}
|
||||
for y in range(height):
|
||||
row = raw[y * stride : (y + 1) * stride]
|
||||
vals = samples(row, depth, width * nchan)
|
||||
for x in range(width):
|
||||
px = tuple(vals[x * nchan : (x + 1) * nchan])
|
||||
if ctype == 3:
|
||||
px = tuple(plte[px[0] * 3 : px[0] * 3 + 3])
|
||||
elif ctype in (0, 4):
|
||||
px = (px[0], px[0], px[0])
|
||||
tally[px[:3]] = tally.get(px[:3], 0) + 1
|
||||
return tally
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit(__doc__)
|
||||
tally = colors(sys.argv[1])
|
||||
for px, n in sorted(tally.items(), key=lambda kv: (-kv[1], kv[0])):
|
||||
print("%d #%02x%02x%02x" % ((n,) + px))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -54,9 +54,8 @@ test -n "$had_m" || test -n "$windows" || set +m
|
||||
|
||||
# Poll, because bash cannot wait with a deadline and a watchdog subshell would
|
||||
# have to signal across process groups, which MSYS cannot do. The interval is the
|
||||
# latency this adds to every healthy test, so keep it small where fork is cheap;
|
||||
# under MSYS fork costs tens of milliseconds, and a second is what the Windows
|
||||
# suite already paid before this wrapper existed.
|
||||
# latency this adds to every healthy test; the ceiling only matters where poll_wait
|
||||
# has to fall back to a forked sleep, which under MSYS costs tens of milliseconds.
|
||||
if test -n "$windows"; then
|
||||
tick=1
|
||||
else
|
||||
@@ -80,6 +79,6 @@ while kill -0 "$pid" 2>/dev/null; do
|
||||
dump_crawl_logs
|
||||
exit 124
|
||||
fi
|
||||
sleep "$tick"
|
||||
poll_wait "$tick"
|
||||
done
|
||||
wait "$pid"
|
||||
|
||||
148
tests/testlib.sh
148
tests/testlib.sh
@@ -64,40 +64,117 @@ stage_install_exec() {
|
||||
stage_install_target install-exec "$1" "$2"
|
||||
}
|
||||
|
||||
IS_WINDOWS=
|
||||
is_windows() {
|
||||
case "$(uname -s)" in
|
||||
MINGW* | MSYS* | CYGWIN*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
if test -z "$IS_WINDOWS"; then
|
||||
case "$(uname -s)" in
|
||||
MINGW* | MSYS* | CYGWIN*) IS_WINDOWS=yes ;;
|
||||
*) IS_WINDOWS=no ;;
|
||||
esac
|
||||
fi
|
||||
test "$IS_WINDOWS" = yes
|
||||
}
|
||||
|
||||
# Open the timer fd poll_wait reads from: a fifo held open read-write, so there is
|
||||
# always a writer and a read blocks to its own timeout instead of seeing EOF. fd 9
|
||||
# is the harness's from here on; no test may hold it open across a poll.
|
||||
POLL_STATE=
|
||||
poll_open() {
|
||||
local f
|
||||
POLL_STATE=forked
|
||||
test -z "${HTTRACK_POLL_SLEEP:-}" || return 0
|
||||
# Not under MSYS: its fifos are emulated, and the leg whose stability #795 is
|
||||
# about is no place to discover how its select() behaves. That tick is a whole
|
||||
# second anyway, so there is little to win.
|
||||
is_windows && return 0
|
||||
# Unique: $$ is the same in every subshell, and the loser of a race on one name
|
||||
# opens a path that is gone.
|
||||
f=$(mktemp -u "${TMPDIR:-/tmp}/.httrack-poll.XXXXXX" 2>/dev/null) || return 0
|
||||
mkfifo "$f" 2>/dev/null || return 0
|
||||
# Braced: "exec 9<>x 2>/dev/null" alone would send this shell's stderr to
|
||||
# /dev/null for good, exec applying its redirections to the shell itself.
|
||||
# Still a fifo, or bash's <> created a regular file on a vanished path, which
|
||||
# reads EOF at once and spins every deadline loop.
|
||||
if { exec 9<>"$f"; } 2>/dev/null && test -p "$f"; then
|
||||
POLL_STATE=fifo
|
||||
else
|
||||
exec 9<&-
|
||||
fi
|
||||
rm -f "$f" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
# One poll tick of at most $1 seconds, forking nothing where bash can time out a
|
||||
# read on that fd (a `sleep` per tick was 22% of every process the suite created,
|
||||
# and costs milliseconds apiece under MSYS, #795). $1 is a ceiling: the fd tick
|
||||
# returns every 0.1s whatever it is asked, and callers re-check their own deadline.
|
||||
# HTTRACK_POLL_SLEEP=1 forces the forked tick back, re-read per tick so a test can
|
||||
# starve the poll mid-run.
|
||||
poll_wait() {
|
||||
local secs=$1 rc=0
|
||||
test -n "$POLL_STATE" || poll_open
|
||||
if test "$POLL_STATE" = fifo && test -z "${HTTRACK_POLL_SLEEP:-}"; then
|
||||
# bash 4.0 is where -t took a fraction; below it, hand a caller asking for
|
||||
# one the sleep it asked for rather than rounding its tick up to a second.
|
||||
if test "${BASH_VERSINFO[0]}" -ge 4; then
|
||||
secs=0.1
|
||||
else
|
||||
case "$secs" in *.*)
|
||||
sleep "$secs"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
read -t "$secs" -r -u 9 _ || rc=$?
|
||||
test "$rc" -le 128 || return 0 # >128 is the timeout we asked for
|
||||
# bash 4.0 is also where a timeout started reporting >128; 3.2 says 1, which is
|
||||
# what a closed fd says everywhere. Ask the fd itself rather than the status,
|
||||
# or macOS retires its timer on the first tick and forks for the whole run.
|
||||
# `true`, not `:`: a redirection error on a POSIX special builtin exits the
|
||||
# shell, which POSIXLY_CORRECT in the environment is enough to turn on.
|
||||
if { true >&9; } 2>/dev/null; then return 0; fi
|
||||
POLL_STATE=forked # not a timer any more, stop trusting it
|
||||
exec 9<&-
|
||||
fi
|
||||
sleep "$1"
|
||||
}
|
||||
|
||||
# On Windows MSYS can't signal a native python.exe, so kill_tree ends the whole
|
||||
# tree (a bare kill -9 leaves children). "|| true" throughout: callers run under
|
||||
# set -e and the reap makes wait return 143.
|
||||
# tree (a bare kill -9 leaves children). Bounded because this runs from an EXIT
|
||||
# trap, where a survivor would turn a passing test into a harness timeout.
|
||||
stop_server() {
|
||||
test -n "${1:-}" || return 0
|
||||
kill "$1" 2>/dev/null || true
|
||||
if is_windows; then kill_tree "$1"; fi
|
||||
wait "$1" 2>/dev/null || true
|
||||
reap_bounded "$1" || true
|
||||
return 0
|
||||
}
|
||||
|
||||
# Echo the port local-server.py announces on $1 (its log), $2 being its pid.
|
||||
# Waits 30s: a cold Python start under a parallel `make check -jN` lags well
|
||||
# past a second, and 5s was tight enough that macos-15 missed it on 15 tests.
|
||||
# Matches anywhere, since a warning merged via 2>&1 can precede the line.
|
||||
# Echo the port local-server.py announces on $1 (its log), $2 being its pid, or
|
||||
# return 2 if the server died and 1 on the deadline: only the deadline is a race a
|
||||
# caller may skip on. Matches anywhere, since a warning merged via 2>&1 can precede
|
||||
# the line. A full minute of wall clock: a cold Python start under a parallel
|
||||
# `make check -jN` lags well past a second, 5s had macos-15 missing it on 15 tests,
|
||||
# and the count-of-ticks loop this replaced self-extended under load instead.
|
||||
discover_server_port() {
|
||||
local log=$1 pid=$2 line _i
|
||||
for _i in $(seq 1 300); do
|
||||
if line=$(grep -m1 '^PORT ' "$log" 2>/dev/null); then
|
||||
printf '%s\n' "${line#PORT }"
|
||||
return 0
|
||||
local log=$1 pid=$2 line start=$SECONDS
|
||||
while :; do
|
||||
if test -r "$log"; then
|
||||
# Read in the shell: a grep per tick is a process per tick (#795).
|
||||
while read -r line; do
|
||||
case "$line" in 'PORT '*)
|
||||
printf '%s\n' "${line#PORT }"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done <"$log"
|
||||
fi
|
||||
kill -0 "$pid" 2>/dev/null || {
|
||||
echo "server exited early: $(cat "$log" 2>/dev/null)" >&2
|
||||
return 1
|
||||
return 2
|
||||
}
|
||||
sleep 0.1
|
||||
test "$((SECONDS - start))" -lt 60 || break
|
||||
poll_wait 0.1
|
||||
done
|
||||
echo "could not discover server port: $(cat "$log" 2>/dev/null)" >&2
|
||||
return 1
|
||||
@@ -176,6 +253,20 @@ kill_tree() {
|
||||
ENGINE_EXE_RE='^(lt-)?(httrack|proxytrack|htsserver|webhttrack)([.]exe)?$'
|
||||
FIXTURE_SERVER_RE='^(local-server|proxy-https-server|proxy-connect-server|socks5-server|tls-stall-server)[.]py$'
|
||||
|
||||
# awk prologue for the matchers below: under qemu-user the kernel reports the
|
||||
# binfmt interpreter as the command, so the name we match on lands one column
|
||||
# right (Debian's hppa buildd emulates).
|
||||
# shellcheck disable=SC2016 # awk fields, not shell expansions
|
||||
AWK_PROC_NAMES='
|
||||
function basen(s) { sub(/.*[\/\\]/, "", s); return s }
|
||||
function qemushift( n) {
|
||||
n = basen($6)
|
||||
# qemu-img and friends take an image, not a program, and this list is fed to
|
||||
# kill: shifting past them would read a disk path as the process name.
|
||||
if (n ~ /^qemu-(img|nbd|io|ga|edid|keymap)$/) return 0
|
||||
return n ~ /^qemu-[[:alnum:]_]+(-static)?$|^[[:alnum:]_]+-binfmt(-[[:upper:]]+)?$/ ? 1 : 0
|
||||
}'
|
||||
|
||||
# Every process as "PID PPID PGID ELAPSED S COMMAND", header first. A Fedora
|
||||
# build root has no procps, and an empty list reads as "nothing running" (#1021).
|
||||
ps_snapshot() {
|
||||
@@ -243,11 +334,13 @@ list_stray_processes() {
|
||||
# Fields 6 and 7 are the command and its first argument (the interpreter
|
||||
# and its script, for the Python fixtures).
|
||||
ps_snapshot |
|
||||
awk -v pg="$pgid" -v mode="$mode" -v eng="$ENGINE_EXE_RE" -v srv="$FIXTURE_SERVER_RE" '
|
||||
awk -v pg="$pgid" -v mode="$mode" -v eng="$ENGINE_EXE_RE" -v srv="$FIXTURE_SERVER_RE" \
|
||||
"$AWK_PROC_NAMES"'
|
||||
NR == 1 { print; next }
|
||||
{ ingroup = (pg > 0 && $3 == pg)
|
||||
c = $6; sub(/.*[\/\\]/, "", c)
|
||||
s = $7; sub(/.*[\/\\]/, "", s)
|
||||
q = qemushift()
|
||||
c = basen($(6 + q))
|
||||
s = basen($(7 + q))
|
||||
named = (c ~ eng || s ~ srv)
|
||||
if (mode == "group" ? ingroup : \
|
||||
mode == "named" ? named : (named && !ingroup)) print }' || true
|
||||
@@ -289,8 +382,8 @@ list_engine_pids() {
|
||||
local pgid=${1:-0}
|
||||
test "$pgid" -gt 0 2>/dev/null || return 0
|
||||
ps_snapshot |
|
||||
awk -v pg="$pgid" -v eng="$ENGINE_EXE_RE" \
|
||||
'NR > 1 && $3 == pg { c = $6; sub(/.*[\/\\]/, "", c); if (c ~ eng) print $1 }'
|
||||
awk -v pg="$pgid" -v eng="$ENGINE_EXE_RE" "$AWK_PROC_NAMES"'
|
||||
NR > 1 && $3 == pg { if (basen($(6 + qemushift())) ~ eng) print $1 }'
|
||||
}
|
||||
|
||||
# Ask the wedged test's engine processes for a stack. What is obtainable differs
|
||||
@@ -413,6 +506,9 @@ EOF
|
||||
chmod +x "$dir/sleep" || return 1
|
||||
PATH="$dir:$PATH"
|
||||
export PATH
|
||||
# Proved, not assumed: MSYS hands out a drive-letter TMPDIR and a PATH entry
|
||||
# carrying a colon is read as two, so an unreachable shim starves nothing.
|
||||
test "$(command -v sleep)" = "$dir/sleep" || return 1
|
||||
}
|
||||
|
||||
# Skip when the next of $1 remaining steps, at 1.5x the $2 seconds the last one
|
||||
@@ -439,7 +535,7 @@ reap_bounded() {
|
||||
local pid=$1 start=$SECONDS
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
test "$((SECONDS - start))" -le "$REAP_GRACE" || return 1
|
||||
sleep 1
|
||||
poll_wait 1
|
||||
done
|
||||
wait "$pid" 2>/dev/null || true
|
||||
return 0
|
||||
@@ -466,7 +562,7 @@ run_with_timeout() {
|
||||
reap_bounded "$pid" || true
|
||||
return 124
|
||||
fi
|
||||
sleep 1
|
||||
poll_wait 1
|
||||
done
|
||||
wait "$pid"
|
||||
}
|
||||
@@ -481,7 +577,7 @@ wait_bounded() {
|
||||
reap_bounded "$pid" || true
|
||||
return 124
|
||||
fi
|
||||
sleep 1
|
||||
poll_wait 1
|
||||
done
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
@@ -259,3 +259,9 @@ TESTS += 216_engine-ftp-ctrlchars.test
|
||||
TESTS += 221_local-ftp-ctrlchars.test
|
||||
TESTS += 218_crash-nopie-frames.test
|
||||
TESTS += 222_pkgconfig-consumer.test
|
||||
TESTS += 227_watchdog-poll.test
|
||||
TESTS += 228_icon-small-flat.test
|
||||
TESTS += 226_watchdog-native.test
|
||||
TESTS += 224_engine-ftp-cmdlen.test
|
||||
TESTS += 230_local-ftp-userpass.test
|
||||
TESTS += 225_install-manifest.test
|
||||
|
||||
Reference in New Issue
Block a user