feat(launcher): добавить ежедневный запуск проверок cc и ic
Docker / build (push) Failing after 24s

Флаг --daily ставит cron, который раз в сутки заново качает launcher со свежим токеном. Снять: --remove-daily.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-08-25 16:18:14 +07:00
co-authored by Cursor
parent 7480b9dc11
commit b03318800f
8 changed files with 303 additions and 20 deletions
+4 -2
View File
@@ -93,9 +93,10 @@ vps-tracker/
## Статус блокировок (censorcheck)
Ручная проверка с VPS: `curl -fsSL https://vt.shnt.top/cc | bash` (тот же контейнер, Traefik dual Host).
Раз в сутки: `curl -fsSL https://vt.shnt.top/cc | bash -s -- --daily` (cron, свежий HMAC-токен на каждый запуск).
- **Vendor:** `apps/api/scripts/censorcheck/censorcheck.sh` (pin SHA `12c5839`, MIT)
- **Launcher:** `GET /cc` минтит HMAC ingest-токен (TTL 20 мин); по `/etc/os-release` ставит `jq`/`dig`/`column` без prompt; прогресс-бар в stderr; `GET /cc/vendor` — скрипт (LF)
- **Launcher:** `GET /cc` минтит HMAC ingest-токен (TTL 20 мин); по `/etc/os-release` ставит `jq`/`dig`/`column` без prompt; прогресс-бар в stderr; `GET /cc/vendor` — скрипт (LF); `--daily` / `--remove-daily`
- **Ingest:** `POST /api/integrations/censorcheck/runs` (без portal JWT)
- **UI:** `/blocking` — текущие прогоны и история, группировка VPS / сервис
- Env: `CENSORCHECK_INGEST_SECRET`, `CENSORCHECK_PUBLIC_URL`, `VPS_LAUNCHER_DOMAIN`
@@ -103,9 +104,10 @@ vps-tracker/
## GeoIP (ipregion)
Ручная проверка с VPS: `curl -fsSL https://vt.shnt.top/ic | bash`.
Раз в сутки: `curl -fsSL https://vt.shnt.top/ic | bash -s -- --daily`.
- **Vendor:** `apps/api/scripts/ipregion/ipregion.sh` (pin SHA `7d1c25c`, MIT, [vernette/ipregion](https://github.com/vernette/ipregion))
- **Launcher:** `GET /ic` минтит HMAC ingest-токен (тот же `CENSORCHECK_INGEST_SECRET`); `GET /ic/vendor` — pinned скрипт (LF)
- **Launcher:** `GET /ic` минтит HMAC ingest-токен (тот же `CENSORCHECK_INGEST_SECRET`); `GET /ic/vendor` — pinned скрипт (LF); `--daily` / `--remove-daily`
- **Ingest:** `POST /api/integrations/ipregion/runs` (без portal JWT)
- **UI:** `/geo` — матрица ISO-стран VPS × сервисы (primary / custom / cdn)
+124 -1
View File
@@ -5,8 +5,10 @@ set -euo pipefail
VT_API_URL="${VT_API_URL:-__VT_API_URL__}"
VT_INGEST_TOKEN="${VT_INGEST_TOKEN:-__VT_INGEST_TOKEN__}"
LAUNCHER_VERSION="5"
LAUNCHER_VERSION="6"
VENDOR_SHA="12c5839"
DAILY_SLUG="vt-censorcheck"
DAILY_PATH="/cc"
OS_ID="unknown"
OS_LIKE=""
@@ -247,6 +249,102 @@ detect_hoster() {
return 1
}
show_launcher_help() {
cat <<EOF
Использование:
curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash
curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --daily
curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily
--daily выполнить проверку и поставить cron раз в сутки
--remove-daily убрать ежедневный cron
EOF
}
daily_run_cmd() {
local url="${VT_API_URL}${DAILY_PATH}"
local curl_bin bash_bin flock_bin
curl_bin="$(command -v curl)"
bash_bin="$(command -v bash)"
flock_bin="$(command -v flock || true)"
if [ -n "$flock_bin" ]; then
printf "%s -n /tmp/%s.lock -c '%s -fsSL --max-time 180 %s | %s'" \
"$flock_bin" "$DAILY_SLUG" "$curl_bin" "$url" "$bash_bin"
else
printf '%s -fsSL --max-time 180 %s | %s' "$curl_bin" "$url" "$bash_bin"
fi
}
daily_slot() {
local seed n
seed="$(hostname -f 2>/dev/null || hostname 2>/dev/null || cat /etc/hostname 2>/dev/null || echo vt)"
n="$(printf '%s' "$seed" | cksum | awk '{print $1}')"
DAILY_MINUTE=$((n % 60))
DAILY_HOUR=$((3 + (n / 60) % 4))
}
remove_daily() {
local prefix="" removed=0
if [ -e "/etc/cron.d/${DAILY_SLUG}" ]; then
if prefix="$(root_prefix)"; then
# shellcheck disable=SC2086
$prefix rm -f "/etc/cron.d/${DAILY_SLUG}"
removed=1
fi
fi
if command -v crontab >/dev/null 2>&1; then
local current
current="$(crontab -l 2>/dev/null || true)"
if printf '%s\n' "$current" | grep -qF "vps-tracker:${DAILY_SLUG}"; then
printf '%s\n' "$current" | grep -vF "vps-tracker:${DAILY_SLUG}" | crontab -
removed=1
fi
fi
if [ "$removed" -eq 1 ]; then
log "Ежедневная проверка снята (${DAILY_SLUG})"
else
log "Ежедневная проверка не была установлена"
fi
}
install_daily() {
daily_slot
local run line prefix=""
local marker="# vps-tracker:${DAILY_SLUG}"
run="$(daily_run_cmd)"
if [ -d /etc/cron.d ] && prefix="$(root_prefix)"; then
local tmp
tmp="$(mktemp)"
cat >"$tmp" <<EOF
# Managed by VPS Tracker launcher. ${marker}
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
${DAILY_MINUTE} ${DAILY_HOUR} * * * root ${run} >>/var/log/${DAILY_SLUG}.log 2>&1
EOF
# shellcheck disable=SC2086
$prefix cp "$tmp" "/etc/cron.d/${DAILY_SLUG}"
# shellcheck disable=SC2086
$prefix chmod 644 "/etc/cron.d/${DAILY_SLUG}"
rm -f "$tmp"
log "Ежедневная проверка: каждый день в ${DAILY_HOUR}:$(printf '%02d' "$DAILY_MINUTE") (/etc/cron.d/${DAILY_SLUG})"
log "Снять: curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily"
return 0
fi
if ! command -v crontab >/dev/null 2>&1; then
die "Нет cron (ни /etc/cron.d, ни crontab). Установите пакет cron и повторите --daily"
fi
line="${DAILY_MINUTE} ${DAILY_HOUR} * * * ${run} >>/tmp/${DAILY_SLUG}.log 2>&1 ${marker}"
{
(crontab -l 2>/dev/null || true) | grep -vF "vps-tracker:${DAILY_SLUG}" || true
printf '%s\n' "$line"
} | crontab -
log "Ежедневная проверка: каждый день в ${DAILY_HOUR}:$(printf '%02d' "$DAILY_MINUTE") (crontab)"
log "Снять: curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily"
}
write_vendor() {
local dest="$1"
if [ -n "${CENSORCHECK_VENDOR_B64:-}" ]; then
@@ -273,6 +371,22 @@ VT_API_URL="${VT_API_URL%/}"
[ -n "$VT_API_URL" ] || die "VT_API_URL пуст"
[ -n "$VT_INGEST_TOKEN" ] || die "VT_INGEST_TOKEN пуст"
DAILY_INSTALL=0
DAILY_REMOVE=0
for arg in "$@"; do
case "$arg" in
--daily|--install-daily) DAILY_INSTALL=1 ;;
--remove-daily|--uninstall-daily) DAILY_REMOVE=1 ;;
-h|--help) show_launcher_help; exit 0 ;;
*) die "Неизвестный аргумент: $arg. См. --help" ;;
esac
done
if [ "$DAILY_REMOVE" -eq 1 ]; then
remove_daily
exit 0
fi
TMPDIR="$(mktemp -d /tmp/vt-censorcheck.XXXXXX)"
cleanup() { rm -rf "$TMPDIR"; }
trap 'cleanup; exit 130' INT
@@ -336,6 +450,9 @@ set -e
if [ "$POST_EXIT" -ne 0 ]; then
cp "$PAYLOAD" "$FALLBACK"
log "API недоступен (curl exit ${POST_EXIT}). JSON сохранён: ${FALLBACK}" >&2
if [ "$DAILY_INSTALL" -eq 1 ]; then
install_daily
fi
exit 2
fi
@@ -345,9 +462,15 @@ if [ -z "$CHECK_ID" ]; then
cp "$PAYLOAD" "$FALLBACK"
log "Некорректный ответ API. JSON сохранён: ${FALLBACK}" >&2
log "$RESP" >&2
if [ "$DAILY_INSTALL" -eq 1 ]; then
install_daily
fi
exit 2
fi
log "Check ID: ${CHECK_ID}"
log "VPS: ${MATCHED}"
if [ "$DAILY_INSTALL" -eq 1 ]; then
install_daily
fi
exit 0
+124 -1
View File
@@ -5,8 +5,10 @@ set -euo pipefail
VT_API_URL="${VT_API_URL:-__VT_API_URL__}"
VT_INGEST_TOKEN="${VT_INGEST_TOKEN:-__VT_INGEST_TOKEN__}"
LAUNCHER_VERSION="1"
LAUNCHER_VERSION="2"
VENDOR_SHA="7d1c25c"
DAILY_SLUG="vt-ipregion"
DAILY_PATH="/ic"
OS_ID="unknown"
OS_LIKE=""
@@ -247,6 +249,102 @@ detect_hoster() {
return 1
}
show_launcher_help() {
cat <<EOF
Использование:
curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash
curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --daily
curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily
--daily выполнить проверку и поставить cron раз в сутки
--remove-daily убрать ежедневный cron
EOF
}
daily_run_cmd() {
local url="${VT_API_URL}${DAILY_PATH}"
local curl_bin bash_bin flock_bin
curl_bin="$(command -v curl)"
bash_bin="$(command -v bash)"
flock_bin="$(command -v flock || true)"
if [ -n "$flock_bin" ]; then
printf "%s -n /tmp/%s.lock -c '%s -fsSL --max-time 180 %s | %s'" \
"$flock_bin" "$DAILY_SLUG" "$curl_bin" "$url" "$bash_bin"
else
printf '%s -fsSL --max-time 180 %s | %s' "$curl_bin" "$url" "$bash_bin"
fi
}
daily_slot() {
local seed n
seed="$(hostname -f 2>/dev/null || hostname 2>/dev/null || cat /etc/hostname 2>/dev/null || echo vt)"
n="$(printf '%s' "$seed" | cksum | awk '{print $1}')"
DAILY_MINUTE=$((n % 60))
DAILY_HOUR=$((3 + (n / 60) % 4))
}
remove_daily() {
local prefix="" removed=0
if [ -e "/etc/cron.d/${DAILY_SLUG}" ]; then
if prefix="$(root_prefix)"; then
# shellcheck disable=SC2086
$prefix rm -f "/etc/cron.d/${DAILY_SLUG}"
removed=1
fi
fi
if command -v crontab >/dev/null 2>&1; then
local current
current="$(crontab -l 2>/dev/null || true)"
if printf '%s\n' "$current" | grep -qF "vps-tracker:${DAILY_SLUG}"; then
printf '%s\n' "$current" | grep -vF "vps-tracker:${DAILY_SLUG}" | crontab -
removed=1
fi
fi
if [ "$removed" -eq 1 ]; then
log "Ежедневная проверка снята (${DAILY_SLUG})"
else
log "Ежедневная проверка не была установлена"
fi
}
install_daily() {
daily_slot
local run line prefix=""
local marker="# vps-tracker:${DAILY_SLUG}"
run="$(daily_run_cmd)"
if [ -d /etc/cron.d ] && prefix="$(root_prefix)"; then
local tmp
tmp="$(mktemp)"
cat >"$tmp" <<EOF
# Managed by VPS Tracker launcher. ${marker}
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
${DAILY_MINUTE} ${DAILY_HOUR} * * * root ${run} >>/var/log/${DAILY_SLUG}.log 2>&1
EOF
# shellcheck disable=SC2086
$prefix cp "$tmp" "/etc/cron.d/${DAILY_SLUG}"
# shellcheck disable=SC2086
$prefix chmod 644 "/etc/cron.d/${DAILY_SLUG}"
rm -f "$tmp"
log "Ежедневная проверка: каждый день в ${DAILY_HOUR}:$(printf '%02d' "$DAILY_MINUTE") (/etc/cron.d/${DAILY_SLUG})"
log "Снять: curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily"
return 0
fi
if ! command -v crontab >/dev/null 2>&1; then
die "Нет cron (ни /etc/cron.d, ни crontab). Установите пакет cron и повторите --daily"
fi
line="${DAILY_MINUTE} ${DAILY_HOUR} * * * ${run} >>/tmp/${DAILY_SLUG}.log 2>&1 ${marker}"
{
(crontab -l 2>/dev/null || true) | grep -vF "vps-tracker:${DAILY_SLUG}" || true
printf '%s\n' "$line"
} | crontab -
log "Ежедневная проверка: каждый день в ${DAILY_HOUR}:$(printf '%02d' "$DAILY_MINUTE") (crontab)"
log "Снять: curl -fsSL ${VT_API_URL}${DAILY_PATH} | bash -s -- --remove-daily"
}
write_vendor() {
local dest="$1"
if [ -n "${IPREGION_VENDOR_B64:-}" ]; then
@@ -273,6 +371,22 @@ VT_API_URL="${VT_API_URL%/}"
[ -n "$VT_API_URL" ] || die "VT_API_URL пуст"
[ -n "$VT_INGEST_TOKEN" ] || die "VT_INGEST_TOKEN пуст"
DAILY_INSTALL=0
DAILY_REMOVE=0
for arg in "$@"; do
case "$arg" in
--daily|--install-daily) DAILY_INSTALL=1 ;;
--remove-daily|--uninstall-daily) DAILY_REMOVE=1 ;;
-h|--help) show_launcher_help; exit 0 ;;
*) die "Неизвестный аргумент: $arg. См. --help" ;;
esac
done
if [ "$DAILY_REMOVE" -eq 1 ]; then
remove_daily
exit 0
fi
TMPDIR="$(mktemp -d /tmp/vt-ipregion.XXXXXX)"
cleanup() { rm -rf "$TMPDIR"; }
trap 'cleanup; exit 130' INT
@@ -339,6 +453,9 @@ set -e
if [ "$POST_EXIT" -ne 0 ]; then
cp "$PAYLOAD" "$FALLBACK"
log "API недоступен (curl exit ${POST_EXIT}). JSON сохранён: ${FALLBACK}" >&2
if [ "$DAILY_INSTALL" -eq 1 ]; then
install_daily
fi
exit 2
fi
@@ -348,9 +465,15 @@ if [ -z "$CHECK_ID" ]; then
cp "$PAYLOAD" "$FALLBACK"
log "Некорректный ответ API. JSON сохранён: ${FALLBACK}" >&2
log "$RESP" >&2
if [ "$DAILY_INSTALL" -eq 1 ]; then
install_daily
fi
exit 2
fi
log "Check ID: ${CHECK_ID}"
log "VPS: ${MATCHED}"
if [ "$DAILY_INSTALL" -eq 1 ]; then
install_daily
fi
exit 0
+9
View File
@@ -33,6 +33,11 @@ describe('GET /cc launcher', () => {
expect(res.body).toContain('/etc/os-release')
expect(res.body).toContain('apt-get install -y -qq')
expect(res.body).toContain('Проверяю сайты')
expect(res.body).toContain('--daily')
expect(res.body).toContain('--remove-daily')
expect(res.body).toContain('DAILY_SLUG="vt-censorcheck"')
expect(res.body).toContain('vps-tracker:${DAILY_SLUG}')
expect(res.body).toContain('/etc/cron.d')
expect(res.body).not.toContain('2>/tmp/vt-censorcheck-err')
expect(res.body).not.toContain('\r')
expect(res.body).not.toContain('__VT_API_URL__')
@@ -80,6 +85,10 @@ describe('GET /ic launcher', () => {
expect(res.body).toContain('/api/integrations/ipregion/runs')
expect(res.body).toContain('/ic/vendor')
expect(res.body).toContain('7d1c25c')
expect(res.body).toContain('--daily')
expect(res.body).toContain('--remove-daily')
expect(res.body).toContain('DAILY_SLUG="vt-ipregion"')
expect(res.body).toContain('vps-tracker:${DAILY_SLUG}')
expect(res.body).not.toContain('\r')
expect(res.body).not.toContain('__VT_API_URL__')
expect(res.body).not.toContain('__VT_INGEST_TOKEN__')
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import {
BanIcon,
CalendarClockIcon,
CopyIcon,
GlobeIcon,
ServerIcon,
@@ -38,6 +39,7 @@ import { filterCensorcheckRuns, serviceMatrixRows } from './blocking-filters'
import {
CENSORCHECK_STATUS_LABELS,
LAUNCHER_CMD,
LAUNCHER_CMD_DAILY,
formatCheckedAt,
type CensorcheckRunDto,
} from './types'
@@ -143,14 +145,24 @@ export function BlockingPage() {
const blocked = filtered.reduce((sum, row) => sum + row.summary.blocked, 0)
const copyLauncher = (
<Button
type="button"
variant="outline"
onClick={() => void copyText(LAUNCHER_CMD, 'Команда скопирована')}
>
<CopyIcon data-icon="inline-start" />
Скопировать команду
</Button>
<>
<Button
type="button"
variant="outline"
onClick={() => void copyText(LAUNCHER_CMD, 'Команда скопирована')}
>
<CopyIcon data-icon="inline-start" />
Скопировать команду
</Button>
<Button
type="button"
variant="outline"
onClick={() => void copyText(LAUNCHER_CMD_DAILY, 'Команда для ежедневного запуска скопирована')}
>
<CalendarClockIcon data-icon="inline-start" />
Раз в день
</Button>
</>
)
return (
@@ -57,6 +57,7 @@ export const CENSORCHECK_STATUS_LABELS: Record<string, string> = {
}
export const LAUNCHER_CMD = 'curl -fsSL https://vt.shnt.top/cc | bash'
export const LAUNCHER_CMD_DAILY = 'curl -fsSL https://vt.shnt.top/cc | bash -s -- --daily'
export function formatVpsResources(vcpu: number, ramGb: number, diskGb: number): string {
return `${vcpu} vCPU / ${ramGb} GB / ${diskGb} GB`
+20 -8
View File
@@ -1,6 +1,7 @@
import { useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import {
CalendarClockIcon,
CopyIcon,
GlobeIcon,
MapPinIcon,
@@ -43,6 +44,7 @@ import {
import {
IPREGION_STATUS_LABELS,
LAUNCHER_CMD,
LAUNCHER_CMD_DAILY,
formatCheckedAt,
type IpregionRunDto,
} from './types'
@@ -148,14 +150,24 @@ export function GeoPage() {
const mismatches = filtered.filter(isGeoMismatch).length
const copyLauncher = (
<Button
type="button"
variant="outline"
onClick={() => void copyText(LAUNCHER_CMD, 'Команда скопирована')}
>
<CopyIcon data-icon="inline-start" />
Скопировать команду
</Button>
<>
<Button
type="button"
variant="outline"
onClick={() => void copyText(LAUNCHER_CMD, 'Команда скопирована')}
>
<CopyIcon data-icon="inline-start" />
Скопировать команду
</Button>
<Button
type="button"
variant="outline"
onClick={() => void copyText(LAUNCHER_CMD_DAILY, 'Команда для ежедневного запуска скопирована')}
>
<CalendarClockIcon data-icon="inline-start" />
Раз в день
</Button>
</>
)
return (
@@ -56,6 +56,7 @@ export const IPREGION_STATUS_LABELS: Record<string, string> = {
}
export const LAUNCHER_CMD = 'curl -fsSL https://vt.shnt.top/ic | bash'
export const LAUNCHER_CMD_DAILY = 'curl -fsSL https://vt.shnt.top/ic | bash -s -- --daily'
export function formatVpsResources(vcpu: number, ramGb: number, diskGb: number): string {
return `${vcpu} vCPU / ${ramGb} GB / ${diskGb} GB`