mirror of
https://github.com/xroche/httrack.git
synced 2026-08-06 15:56:26 +03:00
Compare commits
19 Commits
3.49.17
...
fix-795-ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6594980ad6 | ||
|
|
0414d2878f | ||
|
|
b7229b2afa | ||
|
|
79eca5d37c | ||
|
|
5510bda24c | ||
|
|
a8fead6720 | ||
|
|
0bba66ee98 | ||
|
|
2866c29b0f | ||
|
|
1cec3b06d3 | ||
|
|
8e3082a8ff | ||
|
|
a2a20ef684 | ||
|
|
bfe0a4a7f7 | ||
|
|
03c30e17ce | ||
|
|
1a45e0237b | ||
|
|
2a44dc1728 | ||
|
|
23e182281c | ||
|
|
3426ea35ea | ||
|
|
0274e016af | ||
|
|
22fcf9ffff |
8
.github/workflows/ci.yml
vendored
8
.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: |
|
||||
@@ -715,7 +716,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 +744,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,
|
||||
|
||||
116
.github/workflows/cross-arch.yml
vendored
Normal file
116
.github/workflows/cross-arch.yml
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
# Cross-compiles for the Debian architectures no GitHub runner exists for.
|
||||
# 3.49.17 failed on four of them at once and every failure was a compile or
|
||||
# assemble error that x86-64 and arm64 cannot produce, so the buildd was the
|
||||
# first thing to see the code.
|
||||
name: Cross-arch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
# Least privilege: the workflow only needs to read the repo.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Cancel superseded runs on the same branch or PR.
|
||||
concurrency:
|
||||
group: cross-arch-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cross:
|
||||
name: cross (${{ matrix.arch }})
|
||||
runs-on: ubuntu-24.04
|
||||
# sid, not stable: the point is to predict what the sid buildds will say.
|
||||
container: debian:sid
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# arch selects the hardening flags, triplet is the toolchain prefix.
|
||||
# The first four are 3.49.17's failures.
|
||||
- { arch: armhf, triplet: arm-linux-gnueabihf }
|
||||
- { arch: powerpc, triplet: powerpc-linux-gnu }
|
||||
- { arch: hppa, triplet: hppa-linux-gnu }
|
||||
- { arch: loong64, triplet: loongarch64-linux-gnu }
|
||||
- { arch: sh4, triplet: sh4-linux-gnu }
|
||||
- { arch: m68k, triplet: m68k-linux-gnu }
|
||||
- { arch: sparc64, triplet: sparc64-linux-gnu }
|
||||
- { arch: riscv64, triplet: riscv64-linux-gnu }
|
||||
- { arch: s390x, triplet: s390x-linux-gnu }
|
||||
env:
|
||||
DEB_HOST_ARCH: ${{ matrix.arch }}
|
||||
TRIPLET: ${{ matrix.triplet }}
|
||||
ZPREFIX: /tmp/zlib-${{ matrix.arch }}
|
||||
steps:
|
||||
- name: Install the toolchain
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# deb-src, so zlib below comes from the archive and not a pinned URL.
|
||||
# In place: a second stanza has to repeat Signed-By, and apt rejects
|
||||
# the same source carrying two spellings of the keyring.
|
||||
sed -i 's/^Types: deb$/Types: deb deb-src/' \
|
||||
/etc/apt/sources.list.d/debian.sources
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool autoconf-archive \
|
||||
dpkg-dev git ca-certificates \
|
||||
"gcc-$TRIPLET" "libc6-dev-$DEB_HOST_ARCH-cross"
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Cross-build zlib
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Mandatory, and no ports arch has a zlib1g-dev:<arch> to
|
||||
# multiarch-install. -fPIC because libhttrack.so links it.
|
||||
mkdir -p /tmp/zsrc && cd /tmp/zsrc
|
||||
# Unpatched: Debian's arch-specific patches expect its own rules to
|
||||
# build them (s390x's vector CRC pulls a header they add), and we only
|
||||
# need something to link -lz against.
|
||||
apt-get source --download-only zlib1g
|
||||
dpkg-source --skip-patches -x ./*.dsc src
|
||||
cd src
|
||||
CHOST="$TRIPLET" CC="$TRIPLET-gcc" CFLAGS="-O2 -fPIC" \
|
||||
./configure --prefix="$ZPREFIX" --static
|
||||
# configure detects s390x's vector CRC, whose sources live in the
|
||||
# contrib/ the .dfsg repack drops: drop the define and the object it
|
||||
# would build. Nothing here needs a fast CRC.
|
||||
sed -i -E 's/(-DHAVE_S390X_VX|crc32_vx\.l?o)//g' Makefile
|
||||
make -j"$(nproc)"
|
||||
make install
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
set -euo pipefail
|
||||
autoreconf -fi
|
||||
mkdir -p /tmp/bld && cd /tmp/bld
|
||||
# The buildd's own per-arch flags: -D_FILE_OFFSET_BITS=64 is the shim
|
||||
# hazard, and -fstack-clash-protection is not offered everywhere.
|
||||
eval "$(dpkg-buildflags --export=sh)"
|
||||
# A cross AC_TRY_RUN answers "cross", which would swap in the bundled
|
||||
# snprintf that no buildd ever builds. Every glibc passes these.
|
||||
# --disable-https: no ports arch has a cross libssl, so the TLS paths
|
||||
# are the one part of the tree this matrix does not compile.
|
||||
"$GITHUB_WORKSPACE/configure" --host="$TRIPLET" \
|
||||
--build="$(dpkg-architecture -qDEB_BUILD_GNU_TYPE)" --disable-https \
|
||||
ac_cv_have_working_snprintf=yes ac_cv_have_working_vsnprintf=yes \
|
||||
CPPFLAGS="${CPPFLAGS:-} -I$ZPREFIX/include" \
|
||||
LDFLAGS="${LDFLAGS:-} -L$ZPREFIX/lib"
|
||||
|
||||
- name: Build
|
||||
run: make -C /tmp/bld -j"$(nproc)"
|
||||
|
||||
- name: Build the test artifacts
|
||||
# Nothing runs here, but the shims are where three of the four 3.49.17
|
||||
# failures were: they only build under `check`. Asserted, because an
|
||||
# empty TESTS= would otherwise pass having built nothing.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
make -C /tmp/bld -j"$(nproc)" check TESTS=
|
||||
test -f /tmp/bld/tests/.libs/libaltstackprobe.so
|
||||
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 token, which can now post statuses, sits in .git/config for
|
||||
# the whole job, 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 }}
|
||||
# A pull_request's merge commit, so these 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 }}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
AC_PREREQ([2.71])
|
||||
|
||||
AC_INIT([httrack], [3.49.17], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
|
||||
AC_INIT([httrack], [3.49.18], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
|
||||
AC_COPYRIGHT([
|
||||
HTTrack Website Copier, Offline Browser for Windows and Unix
|
||||
Copyright (C) 1998-2015 Xavier Roche and other contributors
|
||||
@@ -29,6 +29,9 @@ 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.
|
||||
# 3:8:0: revision-only bump, no ABI change.
|
||||
@@ -37,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:9:0"
|
||||
VERSION_INFO="3:11:0"
|
||||
AM_MAINTAINER_MODE
|
||||
AC_USE_SYSTEM_EXTENSIONS
|
||||
|
||||
|
||||
12
debian/changelog
vendored
12
debian/changelog
vendored
@@ -1,3 +1,15 @@
|
||||
httrack (3.49.18-1) unstable; urgency=medium
|
||||
|
||||
* New upstream release, fixing the 3.49.17-1 build failures on armhf,
|
||||
powerpc, hppa and loong64: the test suite's mmap interposer did not
|
||||
compile on the first three, and the alternate-stack test insisted on a
|
||||
longer backtrace than loong64's unwinder produces. Nothing in the engine
|
||||
changed. Upstream now cross-builds the test shims for the ports
|
||||
architectures on every push, so the next such break surfaces before the
|
||||
buildds do.
|
||||
|
||||
-- Xavier Roche <xavier@debian.org> Wed, 05 Aug 2026 23:00:24 +0200
|
||||
|
||||
httrack (3.49.17-1) unstable; urgency=medium
|
||||
|
||||
* New upstream release: security fixes in the FTP and HTTP request paths and
|
||||
|
||||
@@ -4,6 +4,11 @@ HTTrack Website Copier release history:
|
||||
|
||||
This file lists all changes and fixes that have been made for HTTrack
|
||||
|
||||
3.49-18
|
||||
+ Fixed: the 3.49.17 package build failed on four architectures: the test suite's mmap interposer did not compile on armhf, powerpc or hppa, and the alternate-stack test rejected loong64's shorter backtrace (#1023)
|
||||
+ Fixed: the test suite failed on a host with no ps command, such as a Fedora build root; its hang diagnostics now read /proc directly (#1021)
|
||||
+ Changed: multiple internal test and CI improvements, including a cross-compile matrix covering the Debian ports architectures
|
||||
|
||||
3.49-17
|
||||
+ New: a pkg-config file, libhttrack.pc, ships with the development headers (#1018)
|
||||
+ Fixed: a crawled page could read the WebHTTrack session id and drive the control panel; the id is now unguessable and cross-origin commands are refused (#877)
|
||||
|
||||
@@ -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>
|
||||
@@ -51,6 +50,13 @@
|
||||
<content_rating type="oars-1.1"/>
|
||||
<!-- Newest first; tests/01_engine-version-macros.test enforces it. -->
|
||||
<releases>
|
||||
<release version="3.49.18" date="2026-08-05">
|
||||
<description>
|
||||
<ul>
|
||||
<li>The program itself is unchanged: this release only fixes building HTTrack from source on some Linux distributions and processors</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.49.17" date="2026-08-05">
|
||||
<description>
|
||||
<ul>
|
||||
|
||||
@@ -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 \
|
||||
|
||||
99
src/htsftp.c
99
src/htsftp.c
@@ -150,14 +150,21 @@ void ftp_split_userpass(const char *src, const char *end, char *user,
|
||||
}
|
||||
|
||||
/* 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 +218,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
|
||||
@@ -262,13 +269,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 +311,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 +329,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 +377,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 +538,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 +560,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 +595,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
rest_understood = 1;
|
||||
} // else never mind
|
||||
}
|
||||
} // sinon tant pis
|
||||
} // sinon tant pis
|
||||
}
|
||||
}
|
||||
#if FTP_PASV
|
||||
@@ -613,7 +638,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 +687,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 +951,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 +972,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");
|
||||
|
||||
19
src/htsftp.h
19
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);
|
||||
@@ -75,11 +78,17 @@ int get_ftp_line(T_SOC soc, char *line, size_t line_size, int timeout);
|
||||
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);
|
||||
/* 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);
|
||||
|
||||
@@ -43,8 +43,8 @@ Please visit our Website: http://www.httrack.com
|
||||
configure.ac, decoupled from these). VERSION is the display form, VERSIONID
|
||||
the dotted numeric form, AFF_VERSION the short form shown in footers,
|
||||
LIB_VERSION the data/cache format generation. */
|
||||
#define HTTRACK_VERSION "3.49-17"
|
||||
#define HTTRACK_VERSIONID "3.49.17"
|
||||
#define HTTRACK_VERSION "3.49-18"
|
||||
#define HTTRACK_VERSIONID "3.49.18"
|
||||
#define HTTRACK_AFF_VERSION "3.x"
|
||||
#define HTTRACK_LIB_VERSION "2.0"
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -4540,6 +4540,75 @@ static int st_ftpuser(httrackp *opt, int argc, char **argv) {
|
||||
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 +8189,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" />
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 3, 49, 17, 0
|
||||
PRODUCTVERSION 3, 49, 17, 0
|
||||
FILEVERSION 3, 49, 18, 0
|
||||
PRODUCTVERSION 3, 49, 18, 0
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
@@ -35,12 +35,12 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Xavier Roche"
|
||||
VALUE "FileDescription", VER_FILE_DESCRIPTION
|
||||
VALUE "FileVersion", "3.49.17"
|
||||
VALUE "FileVersion", "3.49.18"
|
||||
VALUE "InternalName", VER_ORIGINAL_FILENAME
|
||||
VALUE "LegalCopyright", "Copyright (C) 1998-2026 Xavier Roche and other contributors. GNU GPL v3 or later."
|
||||
VALUE "OriginalFilename", VER_ORIGINAL_FILENAME
|
||||
VALUE "ProductName", "HTTrack Website Copier"
|
||||
VALUE "ProductVersion", "3.49-17"
|
||||
VALUE "ProductVersion", "3.49-18"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -65,7 +65,7 @@ grep -q 'slow but healthy' "$out" || fail "healthy test output lost"
|
||||
|
||||
# The killed tree must really be gone, or the next test inherits its ports.
|
||||
sleep 1
|
||||
! grep -q "$tmp/90_wedged.test" <<<"$(ps -A -o args 2>/dev/null)" ||
|
||||
! grep -q "$tmp/90_wedged.test" <<<"$(ps_snapshot)" ||
|
||||
fail "the wedged test survived the guard"
|
||||
|
||||
# --- the budget is wall clock, not a count of poll iterations ----------------
|
||||
@@ -139,6 +139,121 @@ HTTRACK_TEST_TIMEOUT=600 bash "$driver" "$tmp/94_pacer.test" >"$out" 2>&1 || rc=
|
||||
test "$rc" -eq 0 || fail "a 60s step under a 600s budget reported $rc, want 0"
|
||||
grep -q 'the pacer let it through' "$out" || fail "the pacer skipped a test that fits"
|
||||
|
||||
# --- the process lists survive a host with no ps ----------------------------
|
||||
# Fedora's build root ships no procps, and the guard then named nothing (#1021).
|
||||
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"
|
||||
# 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 &
|
||||
echo $! >"$tmp/fake.pid"
|
||||
)
|
||||
# Its own group, to prove the filter keeps an outsider out.
|
||||
(
|
||||
set -m
|
||||
"$tmp/httrack" 60 &
|
||||
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
|
||||
# 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; }
|
||||
read -r fppid pgid _ <<<"$(procfields "$fake")"
|
||||
read -r _ opgid _ <<<"$(procfields "$outside")"
|
||||
test "$fppid" != "$pgid" || fail "the fake shares ppid and pgid ($pgid)"
|
||||
test "$opgid" != "$pgid" || fail "the outsider landed in the group under test"
|
||||
(
|
||||
PATH="$tmp/nops:$PATH"
|
||||
! ps -A >/dev/null 2>&1 || fail "the ps shim did not take"
|
||||
snap=$(list_stray_processes "$pgid" group)
|
||||
# Line 1 is the header every consumer drops; a row there would be lost.
|
||||
case "$(head -n1 <<<"$snap")" in
|
||||
[0-9]*) fail "the no-ps listing has no header: $snap" ;;
|
||||
esac
|
||||
! grep -qE "^$outside " <<<"$snap" ||
|
||||
fail "no-ps group list reported pid $outside, in another group"
|
||||
read -r _ rppid rpgid _ <<<"$(grep -E "^$fake " <<<"$snap")"
|
||||
test "$rppid $rpgid" = "$fppid $pgid" ||
|
||||
fail "no-ps row for $fake reads ppid/pgid '$rppid $rpgid', want '$fppid $pgid'"
|
||||
engines=$(list_engine_pids "$pgid")
|
||||
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"
|
||||
# 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; }
|
||||
grep -q 'no process list' <<<"$(list_stray_processes "$pgid" group)" ||
|
||||
fail "a host with no ps and no /proc produced no notice"
|
||||
)
|
||||
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
|
||||
|
||||
@@ -138,7 +138,9 @@ 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. A taskkill runs no
|
||||
# trap, so this path posts the verdict itself, after silencing the reporter that
|
||||
# would otherwise overwrite it.
|
||||
rec="$tmp/killed"
|
||||
: >"$rec"
|
||||
vnow=9000
|
||||
@@ -146,6 +148,8 @@ ticks=0
|
||||
printf 'RUN 96_wedged.test at 0s\n' >"$progress"
|
||||
rc=0
|
||||
(
|
||||
watchdog=777
|
||||
ci_post_final_status() { echo "POST $1" >>"$rec"; }
|
||||
kill_pid() { echo "DIRECT $1" >>"$rec"; }
|
||||
kill_tree() {
|
||||
echo "TREE $1" >>"$rec"
|
||||
@@ -154,10 +158,10 @@ 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" ||
|
||||
fail "the target was not signalled directly ahead of the tree walk: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n 2p "$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")"
|
||||
want='DIRECT 777
|
||||
POST 1
|
||||
DIRECT 4242
|
||||
TREE 4242'
|
||||
test "$(cat "$rec")" = "$want" || fail "the kill path did: $(tr '\n' '/' <"$rec")"
|
||||
|
||||
echo "heartbeat OK"
|
||||
|
||||
@@ -39,7 +39,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; }
|
||||
@@ -51,6 +52,23 @@ 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"
|
||||
chmod +x "$bin/httrack" "$bin/cygpath"
|
||||
# The driver puts this bindir on PATH, so a stub here is the interpreter it finds
|
||||
# where there is no real one; a real PowerShell drives the real script instead.
|
||||
wdargv=$tmp/wdargv
|
||||
: >"$wdargv"
|
||||
if ! command -v pwsh >/dev/null 2>&1 && ! command -v powershell.exe >/dev/null 2>&1; then
|
||||
cat >"$bin/pwsh" <<EOF
|
||||
#!/bin/sh
|
||||
{ echo "TOKEN=\${WATCHDOG_TOKEN:-}"; printf '%s\n' "\$@"; } >>"$wdargv"
|
||||
case " \$* " in
|
||||
*" -Post failure "*) echo "final status failure"; exit 0 ;;
|
||||
*" -Post "*) echo "final status other"; exit 0 ;;
|
||||
esac
|
||||
echo "watchdog ready"
|
||||
exec sleep 60
|
||||
EOF
|
||||
chmod +x "$bin/pwsh"
|
||||
fi
|
||||
test -x "$bin/httrack" || {
|
||||
echo "SKIP: ${TMPDIR:-/tmp} is noexec, the stub bindir cannot be run"
|
||||
exit 77
|
||||
@@ -58,16 +76,21 @@ test -x "$bin/httrack" || {
|
||||
|
||||
# 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 +100,18 @@ 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"
|
||||
|
||||
# The reporter as the real driver launches it, rather than through a test
|
||||
# sourcing the helper: launched, ended, and holding a token nothing else sees.
|
||||
grep -q '^watchdog ready$' watchdog.log || fail "no watchdog was launched: $(cat watchdog.log)"
|
||||
grep -q 'final status failure' watchdog.log ||
|
||||
fail "the driver's own verdict was never posted: $(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")"
|
||||
if test -s "$wdargv"; then
|
||||
grep -qx 'TOKEN=s3cr3t' "$wdargv" || fail "the watchdog was launched without its token"
|
||||
fi
|
||||
# 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"
|
||||
|
||||
|
||||
@@ -63,11 +63,24 @@ grep -q "^Caught signal 11$" <<<"$worker" || fail "-#c=threadstack: no 'Caught s
|
||||
# worker's, which is what had no alternate stack.
|
||||
worker_frames=$(frame_count "$worker")
|
||||
plain_frames=$(frame_count "$plain")
|
||||
test "$worker_frames" -ge 100 ||
|
||||
fail "-#c=threadstack: $worker_frames frames, expected a runaway recursion"
|
||||
test "$plain_frames" -lt 100 ||
|
||||
fail "-#c=segv: $plain_frames frames, the threshold no longer discriminates"
|
||||
|
||||
# Naming no frame at all is the unwinder failing outright, not a weak one, and
|
||||
# the floor below would take it for loong64. hts_print_backtrace() says which.
|
||||
if grep -q "No stack trace available" <<<"$worker"; then
|
||||
fail "-#c=threadstack: the report carries no stack trace at all"
|
||||
fi
|
||||
|
||||
# armhf and loong64 cannot unwind past the frame that faulted on the guard page
|
||||
# (180 skips on the same floor); the release trace below still judges the fix.
|
||||
if [ "$worker_frames" -ge 1 ] && [ "$worker_frames" -lt 6 ]; then
|
||||
echo "-#c=threadstack: $worker_frames frames, too few for this unwinder to" \
|
||||
"show the recursion" >&2
|
||||
elif [ "$worker_frames" -lt 100 ]; then
|
||||
fail "-#c=threadstack: $worker_frames frames, expected a runaway recursion"
|
||||
fi
|
||||
|
||||
# Everything above only proves the stack gets installed: the crashing worker
|
||||
# never returns, so the release hook never runs there. -#test=threadwait spawns
|
||||
# workers that do return, and the syscall trace says what each did with its
|
||||
|
||||
@@ -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"
|
||||
528
tests/226_watchdog-native.test
Normal file
528
tests/226_watchdog-native.test
Normal file
@@ -0,0 +1,528 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The off-box suite watchdog (#795): its pure decisions through -SelfTest, the
|
||||
# process APIs it must never name, and the driver wiring no .ps1 can check for
|
||||
# itself. It only reports, so there is no kill path here to exercise.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# First, before anything here can start an interpreter: a token reaching one
|
||||
# posts forged statuses onto the commit under test.
|
||||
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")
|
||||
trap 'set +e; stop_server "${sinkpid:-}"; rm -rf "$tmp"' EXIT
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
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-Service|Stop-Process|Invoke-Expression|Invoke-Item|Invoke-Command|Invoke-CimMethod|Get-WmiObject|Register-ScheduledJob|WScript\.Shell|taskkill|cmd\.exe|wmic|schtasks|Diagnostics\.Process|\.Kill\(|(^|[^-a-zA-Z0-9_])(iex|saps|spps|spjb|sajb|start)([^-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 }' 'Get-WmiObject Win32_Process' \
|
||||
'Invoke-CimMethod -ClassName Win32_Process -MethodName Create' \
|
||||
'Invoke-Command -ScriptBlock { 1 }' 'Register-ScheduledJob -Name x -ScriptBlock { 1 }' \
|
||||
'(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"
|
||||
# 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"
|
||||
|
||||
if test -r "$workflow"; then
|
||||
# The heartbeat still owns the kill and must beat the step timeout. Anchored,
|
||||
# so an ordinary workflow edit mentioning the key in prose is not a second one.
|
||||
n=$(grep -cE '^[[:space:]]*timeout-minutes:[[:space:]]*[0-9]+[[:space:]]*$' "$workflow")
|
||||
test "$n" -eq 1 || fail "$n timeout-minutes in the workflow, cannot tell which bounds the suite"
|
||||
cap=$(($(awk '/^[[:space:]]*timeout-minutes:/ { print $2; exit }' "$workflow") * 60))
|
||||
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"
|
||||
|
||||
# Indentation-agnostic: pinning it would block ever scoping these to the job.
|
||||
grep -Eq '^[[:space:]]+statuses:[[:space:]]*write$' "$workflow" ||
|
||||
fail "the workflow token cannot post a commit status"
|
||||
grep -Eq '^[[:space:]]+contents:[[:space:]]*read$' "$workflow" ||
|
||||
fail "contents: read was dropped from the workflow"
|
||||
# A token that may post statuses must not also be left in .git/config.
|
||||
grep -Eq '^[[:space:]]+persist-credentials:[[:space:]]*false$' "$workflow" ||
|
||||
fail "the checkout persists the job token for the whole job"
|
||||
for v in WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_CONTEXT; do
|
||||
grep -q "$v:" "$workflow" || fail "$v is not passed to the suite step"
|
||||
done
|
||||
elif test -d "$top/.github"; then
|
||||
fail "$workflow is gone but $top/.github is not"
|
||||
else
|
||||
echo "note: no .github under $top (dist tarball), the workflow audits did not run"
|
||||
fi
|
||||
|
||||
# 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_watchdog_exe=''
|
||||
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"
|
||||
grep -qx -- '-Post' "$posixtmp/argv" && fail "the production launch is the one-shot mode"
|
||||
# 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"
|
||||
# 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"
|
||||
|
||||
# Both outcomes: a pending status that always dangles cannot mean anything.
|
||||
usestub argvstub
|
||||
for probe in 0:success 3:failure; do
|
||||
rm -f "$posixtmp/argv"
|
||||
(
|
||||
cd "$posixtmp"
|
||||
# shellcheck source=tests/ci-windows-suite.sh
|
||||
. "$driver"
|
||||
# shellcheck disable=SC2034 # read by the driver's ci_post_final_status
|
||||
ci_watchdog_exe="$bin/pwsh"
|
||||
ci_post_final_status "${probe%%:*}"
|
||||
)
|
||||
test -r "$posixtmp/argv" || fail "no final status posted for rc ${probe%%:*}"
|
||||
grep -qx -- "${probe#*:}" "$posixtmp/argv" ||
|
||||
fail "rc ${probe%%:*} posted $(tr '\n' ' ' <"$posixtmp/argv"), want ${probe#*:}"
|
||||
done
|
||||
|
||||
# The runner cancels a superseded or overrunning step by signal, and bash
|
||||
# reaches its EXIT trap from one with $? reading 0.
|
||||
sigs=()
|
||||
for sig in TERM HUP INT; do
|
||||
# A shell that inherited a signal ignored can neither trap nor take it,
|
||||
# which is what a nohup'd "make check" hands us for HUP.
|
||||
if test -z "$(trap -p "$sig")"; then
|
||||
sigs+=("$sig")
|
||||
fi
|
||||
done
|
||||
test "${#sigs[@]}" -ge 1 || fail "every signal is ignored here, the teardown was not driven"
|
||||
for sig in "${sigs[@]}"; do
|
||||
rm -f "$posixtmp/argv"
|
||||
(
|
||||
cd "$posixtmp"
|
||||
# shellcheck source=tests/ci-windows-suite.sh
|
||||
. "$driver"
|
||||
sleep 30 &
|
||||
# shellcheck disable=SC2034 # all three are read by the driver's teardown
|
||||
watchdog=$! heartbeat='' ci_watchdog_exe="$bin/pwsh"
|
||||
ci_install_traps
|
||||
kill -"$sig" "$BASHPID"
|
||||
sleep 5
|
||||
) >/dev/null 2>&1 || true
|
||||
test -r "$posixtmp/argv" || fail "a $sig-killed suite posted nothing"
|
||||
grep -qx -- failure "$posixtmp/argv" ||
|
||||
fail "a $sig-killed suite posted $(tr '\n' ' ' <"$posixtmp/argv"), want failure"
|
||||
done
|
||||
else
|
||||
echo "note: $tmp is noexec, the driver wiring was not driven"
|
||||
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.
|
||||
py=$(find_python || true)
|
||||
sink="$tmp/sink.py"
|
||||
cat >"$sink" <<'PY'
|
||||
import http.server
|
||||
import sys
|
||||
|
||||
code, rec, portfile = int(sys.argv[1]), sys.argv[2], sys.argv[3]
|
||||
|
||||
|
||||
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")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Length", "2")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"{}")
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
srv = http.server.HTTPServer(("127.0.0.1", 0), Handler)
|
||||
with open(portfile, "w") as f:
|
||||
f.write(str(srv.server_port))
|
||||
srv.serve_forever()
|
||||
PY
|
||||
|
||||
posts="$tmp/posts"
|
||||
portfile="$tmp/port"
|
||||
legout="$tmp/leg.out"
|
||||
port='' sinkpid=''
|
||||
sink_start() {
|
||||
: >"$posts"
|
||||
rm -f "$portfile"
|
||||
"$py" "$sink" "$1" "$posts" "$portfile" &
|
||||
sinkpid=$!
|
||||
for _ in $(seq 1 100); do
|
||||
test -s "$portfile" && break
|
||||
sleep 0.1
|
||||
done
|
||||
test -s "$portfile" || fail "the status sink never bound a port"
|
||||
port=$(cat "$portfile")
|
||||
}
|
||||
sink_stop() {
|
||||
stop_server "$sinkpid"
|
||||
sinkpid=''
|
||||
}
|
||||
# Ours, so a leg that lost its -ApiBase reaches GitHub with a credential it
|
||||
# refuses rather than with whatever the step was handed.
|
||||
export WATCHDOG_TOKEN=dummy WATCHDOG_REPO=octo/nowhere WATCHDOG_SHA=0000000
|
||||
|
||||
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 it.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
staticness_leg() {
|
||||
local stop moved lastq legpid
|
||||
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 12 >"$legout" 2>&1 &
|
||||
legpid=$!
|
||||
# Six seconds of a log being written, then silence. Staticness has to follow
|
||||
# the log through both, and elapsed time follows neither.
|
||||
stop=$((SECONDS + 6))
|
||||
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
|
||||
}
|
||||
lastq=$(posted_pairs | awk 'END { print $2 }')
|
||||
case $lastq in
|
||||
'' | *[!0-9]*)
|
||||
echo "the last post carries no staticness: $(posted_pairs | tr '\n' '/')"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
test "$lastq" -ge 3 || {
|
||||
echo "staticness stalled at ${lastq}s over six seconds of silence: $(posted_pairs | tr '\n' '/')"
|
||||
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
|
||||
}
|
||||
test "$calls" -le 5 || {
|
||||
echo "$calls calls to an API that rejects every one: nothing backs off"
|
||||
return 1
|
||||
}
|
||||
test "$lines" -ge $((2 * calls)) || {
|
||||
echo "$lines status lines for $calls calls: the artifact throttles with the API"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
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 3 || {
|
||||
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.
|
||||
mutate() {
|
||||
local name=$1 expr=$2 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"
|
||||
}
|
||||
# shellcheck disable=SC2016 # PowerShell variables, quoted for sed
|
||||
mutate cadence-off-by-one 's/-ge \$Interval/-gt \$Interval/'
|
||||
# shellcheck disable=SC2016
|
||||
mutate backoff-uncapped 's/\[Math\]::Min(\$Current \* 2, 32)/$Current * 2/'
|
||||
# shellcheck disable=SC2016
|
||||
mutate status-unclipped '/if (\$s.Length -gt 140)/d'
|
||||
# shellcheck disable=SC2016
|
||||
mutate tail-always-ok 's/\$r = @{ Ok = \$false;/$r = @{ Ok = $true;/'
|
||||
|
||||
# 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
|
||||
|
||||
legs=(cadence staticness backoff)
|
||||
# ENOSPC on stdout, which is a state the sick runner reaches.
|
||||
test -w /dev/full && legs+=(fullstdout)
|
||||
for leg in "${legs[@]}"; do
|
||||
said=$("${leg}_leg" "$(nativepath "$wdscript")") ||
|
||||
fail "the $psrun watchdog fails the $leg leg: $said"
|
||||
done
|
||||
|
||||
# Same shape as mutate() above, graded by a leg instead of the self-test.
|
||||
mutate_leg() {
|
||||
local name=$1 expr=$2 leg=$3 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"
|
||||
case $said in
|
||||
*'sink never bound'*) fail "the status sink failed under mutant $name" ;;
|
||||
esac
|
||||
}
|
||||
mutate_leg status-every-poll \
|
||||
"s/if ((Get-WatchdogAction \$now \$postedAt \$IntervalSeconds) -eq 'post')/if (\$true)/" cadence
|
||||
# shellcheck disable=SC2016 # PowerShell variables, quoted for sed
|
||||
mutate_leg static-frozen 's/\$static = \$now - \$movedAt/$static = 0/' staticness
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg static-unknown 's/\$static = \$now - \$movedAt/$static = -1/' staticness
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg static-elapsed 's/\$static = \$now - \$movedAt/$static = $now/' staticness
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg backoff-never-skips 's/if (\$skip -gt 0)/if ($false)/' backoff
|
||||
if test -w /dev/full; then
|
||||
mutate_leg log-write-fatal 's/try { \(Write-Host .*\) } catch { }/\1/' fullstdout
|
||||
fi
|
||||
done
|
||||
|
||||
echo "native watchdog OK (${psruns[*]})"
|
||||
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"
|
||||
@@ -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.
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
/* The largefile redirect renames this file's mmap() to mmap64, colliding with
|
||||
its own mmap64(). _TIME_BITS rides on _FILE_OFFSET_BITS, so it goes too. */
|
||||
#undef _FILE_OFFSET_BITS
|
||||
#undef _TIME_BITS
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
@@ -187,6 +192,19 @@ static void *trace_mapped(void *sp) {
|
||||
return sp;
|
||||
}
|
||||
|
||||
/* The fallback when dlsym() failed, so it has to be right: 32-bit arches have
|
||||
no SYS_mmap and i386's wants an argument block, so prefer SYS_mmap2, whose
|
||||
offset is in 4096-byte units whatever the page size. */
|
||||
static void *raw_mmap(void *addr, size_t len, int prot, int flags, int fd,
|
||||
off_t off) {
|
||||
#ifdef SYS_mmap2
|
||||
return (void *) syscall(SYS_mmap2, addr, len, prot, flags, fd,
|
||||
(unsigned long) (off / 4096));
|
||||
#else
|
||||
return (void *) syscall(SYS_mmap, addr, len, prot, flags, fd, off);
|
||||
#endif
|
||||
}
|
||||
|
||||
SHIM_EXPORT void *mmap(void *addr, size_t len, int prot, int flags, int fd,
|
||||
off_t off);
|
||||
|
||||
@@ -194,10 +212,9 @@ SHIM_EXPORT void *mmap(void *addr, size_t len, int prot, int flags, int fd,
|
||||
between, so the thread's last mapping is the whole of the check. */
|
||||
SHIM_EXPORT void *mmap(void *addr, size_t len, int prot, int flags, int fd,
|
||||
off_t off) {
|
||||
return trace_mapped(
|
||||
real_mmap != NULL
|
||||
? real_mmap(addr, len, prot, flags, fd, off)
|
||||
: (void *) syscall(SYS_mmap, addr, len, prot, flags, fd, off));
|
||||
return trace_mapped(real_mmap != NULL
|
||||
? real_mmap(addr, len, prot, flags, fd, off)
|
||||
: raw_mmap(addr, len, prot, flags, fd, off));
|
||||
}
|
||||
|
||||
/* glibc only, and the name that matters: _FILE_OFFSET_BITS=64 redirects the
|
||||
|
||||
@@ -29,6 +29,81 @@ 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
|
||||
# and ci_watchdog_exe; 1 with no PowerShell. Forks nothing and kills nothing, so
|
||||
# it still reports where the heartbeat below cannot (#795).
|
||||
ci_start_native_watchdog() {
|
||||
local progress=$1 ps1 c waited=0
|
||||
ps1="$testdir/ci-windows-watchdog.ps1"
|
||||
test -r "$ps1" || return 1
|
||||
ci_watchdog_exe=''
|
||||
for c in pwsh powershell.exe; do
|
||||
if command -v "$c" >/dev/null 2>&1; then
|
||||
ci_watchdog_exe=$c
|
||||
break
|
||||
fi
|
||||
done
|
||||
test -n "$ci_watchdog_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:-}" \
|
||||
"$ci_watchdog_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
|
||||
}
|
||||
|
||||
# Post the suite's own verdict from exit status $1, described by $2: the watchdog
|
||||
# cannot see the driver exit, and a pending status that never resolves says nothing.
|
||||
ci_post_final_status() {
|
||||
local rc=$1 msg=${2:-"suite step ended rc=$1"} state=success
|
||||
test -n "${ci_watchdog_exe:-}" || return 0
|
||||
test "$rc" -eq 0 || state=failure
|
||||
WATCHDOG_TOKEN="${ci_watchdog_token:-}" \
|
||||
"$ci_watchdog_exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass \
|
||||
-File "$(nativepath "$testdir/ci-windows-watchdog.ps1")" \
|
||||
-Post "$state" -Message "$msg" \
|
||||
>>watchdog.log 2>&1 || true
|
||||
}
|
||||
|
||||
# End both watchers and post the suite's verdict from exit status $1.
|
||||
ci_stop_watchers() {
|
||||
local rc=$1 msg="suite step ended rc=$1"
|
||||
test -z "${ci_signal:-}" || msg="suite step killed by SIG$ci_signal"
|
||||
kill "${heartbeat:-}" 2>/dev/null
|
||||
# Silenced before the verdict, or its next tick overwrites it.
|
||||
test -z "${watchdog:-}" || kill_pid "$watchdog"
|
||||
# Posted even for a launch given up on: it may have posted a pending first.
|
||||
ci_post_final_status "$rc" "$msg"
|
||||
return 0
|
||||
}
|
||||
|
||||
ci_on_signal() {
|
||||
ci_signal=$1
|
||||
exit $((128 + $2))
|
||||
}
|
||||
|
||||
# A shell killed by a signal reaches its EXIT trap with $? = 0, so a cancelled or
|
||||
# timed-out step would report success: the verdict has to come from the handler.
|
||||
ci_install_traps() {
|
||||
trap 'ci_on_signal INT 2' INT
|
||||
trap 'ci_on_signal TERM 15' TERM
|
||||
trap 'ci_on_signal HUP 1' HUP
|
||||
trap 'ci_rc=$?; set +e; ci_stop_watchers "$ci_rc" || true' EXIT
|
||||
}
|
||||
|
||||
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 +123,10 @@ 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"
|
||||
# A taskkill runs no trap, so this is the only verdict the kill path
|
||||
# can leave; the reporter goes first or its next tick overwrites it.
|
||||
test -z "${watchdog:-}" || kill_pid "$watchdog"
|
||||
ci_post_final_status 1 "killed after $((now - moved))s without progress, in flight: $last"
|
||||
# Direct first: kill_tree may reap this watchdog before its own root (#953).
|
||||
kill_pid "$main"
|
||||
kill_tree "$main"
|
||||
@@ -119,9 +198,21 @@ 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_watchdog_exe='' ci_signal='' ci_rc=0
|
||||
# Held here, never exported: a token every test child inherits is how four forged
|
||||
# statuses once reached a commit under test.
|
||||
ci_watchdog_token=${WATCHDOG_TOKEN:-}
|
||||
unset WATCHDOG_TOKEN
|
||||
ci_start_native_watchdog "$PWD/$progress" && watchdog=$ci_watchdog_pid
|
||||
test -n "$watchdog" || echo "no off-box watchdog: no usable PowerShell"
|
||||
# Started after it, so the kill path can silence and outlive the reporter.
|
||||
ci_suite_heartbeat 960 360 "$progress" "$stuck" $$ &
|
||||
heartbeat=$!
|
||||
trap 'kill "$heartbeat" 2>/dev/null || true' EXIT
|
||||
ci_install_traps
|
||||
|
||||
pass=0 fail=0 skip=0 failed="" skipped="" deadline=0
|
||||
# label:pattern, globbed rather than enumerated so a new NNN_engine-*.test or
|
||||
|
||||
243
tests/ci-windows-watchdog.ps1
Normal file
243
tests/ci-windows-watchdog.ps1
Normal file
@@ -0,0 +1,243 @@
|
||||
# Off-box telemetry for the Windows suite step (#795). It spawns nothing and
|
||||
# kills nothing, so nothing a wedge takes away can disarm it, and it reports as a
|
||||
# commit status, the only channel that outlives a dead runner.
|
||||
param(
|
||||
[string]$ProgressLog = '',
|
||||
[int]$IntervalSeconds = 30,
|
||||
[int]$PollSeconds = 5,
|
||||
# Cannot outlive the step, whatever the caller forgets to kill.
|
||||
[int]$MaxSeconds = 2700,
|
||||
# One-shot mode: post this state and exit, so the driver reports its own end.
|
||||
[string]$Post = '',
|
||||
[string]$Message = '',
|
||||
# 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)
|
||||
}
|
||||
|
||||
# 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.
|
||||
function Get-WatchdogCounters {
|
||||
$f = New-Object System.Collections.ArrayList
|
||||
try {
|
||||
$os = Get-CimInstance -ClassName Win32_OperatingSystem -OperationTimeoutSec 5
|
||||
[void]$f.Add('m={0}' -f [int]($os.FreePhysicalMemory / 1KB))
|
||||
[void]$f.Add('v={0}' -f [int]($os.FreeVirtualMemory / 1KB))
|
||||
} catch { [void]$f.Add('m=? v=?') }
|
||||
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' }
|
||||
|
||||
# -NoPost is the test's hard stop: no request, whatever the environment holds.
|
||||
function Send-WatchdogStatus {
|
||||
param([string]$State, [string]$Description)
|
||||
if ($NoPost -or $SelfTest) { return $false }
|
||||
if (-not $script:Token -or -not $script:Repo -or -not $script:Sha) { return $false }
|
||||
$body = @{ state = $State; 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'
|
||||
|
||||
$long = '43_local-update-truncate-with-a-very-long-name-indeed.test'
|
||||
$line = Format-WatchdogStatus 812 41 $long 'm=9012 v=14022 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)
|
||||
|
||||
$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) }
|
||||
|
||||
Assert-That (-not (Send-WatchdogStatus 'success' '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 }
|
||||
|
||||
# --- one-shot: the driver's own verdict ---------------------------------------
|
||||
|
||||
# Windows PowerShell still defaults below TLS 1.2, which api.github.com refuses.
|
||||
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { }
|
||||
|
||||
if ($Post) {
|
||||
# One transient 5xx here is the difference between a resolved status and a
|
||||
# finished suite left reading `pending`.
|
||||
for ($try = 1; $try -le 3; $try++) {
|
||||
if (Send-WatchdogStatus $Post $Message) { break }
|
||||
if ($NoPost -or -not $script:Token) { break }
|
||||
Start-Sleep -Seconds (2 * $try)
|
||||
}
|
||||
Write-WatchdogLog ('final status {0}: {1}' -f $Post, $Message)
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- main loop ----------------------------------------------------------------
|
||||
|
||||
$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--
|
||||
} elseif (Send-WatchdogStatus 'pending' $desc) {
|
||||
$backoff = 0
|
||||
} else {
|
||||
$backoff = Get-NextBackoff $backoff
|
||||
$skip = $backoff
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-WatchdogLog ('tick failed: {0}' -f $_.Exception.Message)
|
||||
}
|
||||
Start-Sleep -Seconds $PollSeconds
|
||||
}
|
||||
|
||||
Write-WatchdogLog ('stopping after {0}s' -f [int]$sw.Elapsed.TotalSeconds)
|
||||
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
|
||||
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()
|
||||
@@ -176,6 +176,70 @@ 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() {
|
||||
local snap
|
||||
# POSIX keywords, so the ps route holds on macOS too; a ps that lists nothing
|
||||
# (hidepid, a locked-down container) is no better than an absent one.
|
||||
if snap=$(ps -A -o pid,ppid,pgid,etime,state,args 2>/dev/null |
|
||||
awk 'NR > 1 { rows++ } { print } END { exit rows ? 0 : 1 }'); then
|
||||
printf '%s\n' "$snap"
|
||||
return 0
|
||||
fi
|
||||
proc_snapshot && return 0
|
||||
# Every consumer drops line 1, so the notice rides in the header's place.
|
||||
printf 'no process list: this host has neither ps nor a readable /proc\n'
|
||||
return 1
|
||||
}
|
||||
|
||||
# The same six columns out of /proc, in bash alone; elapsed as plain seconds.
|
||||
proc_snapshot() {
|
||||
local d stat rest arg args comm hz uptime
|
||||
local -a f
|
||||
local ws # spelled out of line: bash 3.2 fails to parse $'..' inside ${v//p/r}
|
||||
ws=$' \t\n\v\f\r'
|
||||
test -r /proc/self/stat || return 1
|
||||
hz=$(getconf CLK_TCK 2>/dev/null) || hz=
|
||||
# A zero or non-numeric tick would abort the shell in the division below.
|
||||
case "$hz" in '' | 0 | *[!0-9]*) hz=100 ;; esac
|
||||
read -r uptime _ </proc/uptime 2>/dev/null || return 1
|
||||
printf 'PID PPID PGID ELAPSED S COMMAND\n'
|
||||
for d in /proc/[0-9]*; do
|
||||
# Braced: a failed open reports to the caller's stderr, not the redirect.
|
||||
{ read -r stat <"$d/stat"; } 2>/dev/null || continue
|
||||
# comm may hold spaces and parens; every field past it is numeric.
|
||||
rest=${stat##*') '}
|
||||
comm=${stat#*(}
|
||||
comm=${comm%)*}
|
||||
read -ra f <<<"$rest" || continue
|
||||
test "${#f[@]}" -ge 20 || continue # short read: the process is going away
|
||||
args=
|
||||
# Whitespace inside an argv would split the row or shift the columns the
|
||||
# consumers match on, which ps avoids by mapping those bytes away.
|
||||
{ while IFS= read -r -d '' arg; do
|
||||
args="${args:+$args }${arg//["$ws"]/ }"
|
||||
done <"$d/cmdline"; } 2>/dev/null || true
|
||||
printf '%s %s %s %s %s %s\n' "${d#/proc/}" "${f[1]}" "${f[2]}" \
|
||||
"$(((${uptime%%.*} * hz - f[19]) / hz))" \
|
||||
"${f[0]}" "${args:-[$comm]}"
|
||||
done
|
||||
}
|
||||
|
||||
# List processes a hung test may have left running, one per line. $1 is the test's
|
||||
# process group; $2 selects "group" (that group's members, whatever their name),
|
||||
# "others" (engine and fixture processes outside it, which under "make check -j"
|
||||
@@ -190,15 +254,16 @@ list_stray_processes() {
|
||||
test "$mode" != others || return 0
|
||||
tasklist 2>/dev/null | grep -Ei 'httrack|proxytrack|htsserver|python' || true
|
||||
else
|
||||
# `args` and `state` are POSIX ps keywords, so this holds on macOS too.
|
||||
# Fields 6 and 7 are the command and its first argument (the interpreter
|
||||
# and its script, for the Python fixtures).
|
||||
ps -A -o pid,ppid,pgid,etime,state,args 2>/dev/null |
|
||||
awk -v pg="$pgid" -v mode="$mode" -v eng="$ENGINE_EXE_RE" -v srv="$FIXTURE_SERVER_RE" '
|
||||
ps_snapshot |
|
||||
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
|
||||
@@ -239,9 +304,9 @@ reap_leftover_processes() {
|
||||
list_engine_pids() {
|
||||
local pgid=${1:-0}
|
||||
test "$pgid" -gt 0 2>/dev/null || return 0
|
||||
ps -A -o pid,pgid,args 2>/dev/null |
|
||||
awk -v pg="$pgid" -v eng="$ENGINE_EXE_RE" \
|
||||
'NR > 1 && $2 == pg { c = $3; sub(/.*[\/\\]/, "", c); if (c ~ eng) print $1 }'
|
||||
ps_snapshot |
|
||||
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
|
||||
|
||||
@@ -259,3 +259,7 @@ TESTS += 216_engine-ftp-ctrlchars.test
|
||||
TESTS += 221_local-ftp-ctrlchars.test
|
||||
TESTS += 218_crash-nopie-frames.test
|
||||
TESTS += 222_pkgconfig-consumer.test
|
||||
TESTS += 228_icon-small-flat.test
|
||||
TESTS += 226_watchdog-native.test
|
||||
TESTS += 224_engine-ftp-cmdlen.test
|
||||
TESTS += 225_install-manifest.test
|
||||
|
||||
Reference in New Issue
Block a user