@rubytech/create-maxy-code 0.1.459 → 0.1.461
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/cron-heartbeat-registration.test.js +57 -28
- package/dist/__tests__/launchd-plist.test.js +26 -0
- package/dist/cron-registration.js +30 -8
- package/dist/index.js +50 -9
- package/dist/launchd-plist.js +1 -1
- package/dist/uninstall.js +9 -3
- package/package.json +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +116 -6
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +20 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +116 -48
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.js +1 -0
- package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +27 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/object-limits.js +27 -7
- package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +130 -7
- package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +30 -0
- package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +129 -56
- package/payload/platform/lib/storage-broker/src/index.ts +1 -0
- package/payload/platform/lib/storage-broker/src/object-limits.ts +26 -6
- package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -4
- package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +96 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +220 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +201 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +178 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/harness.test.ts +32 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +204 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +207 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
- package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
- package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +125 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +32 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/log.ts +12 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.mjs +193 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +55 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +85 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +44 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +119 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +89 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +70 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +41 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +151 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/index.html +42 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.css +113 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.js +155 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +73 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +21 -0
- package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +8 -0
- package/payload/platform/plugins/docs/references/outlook-guide.md +3 -3
- package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -0
- package/payload/platform/plugins/filesystem/PLUGIN.md +0 -1
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +14 -10
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +14 -10
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +3 -5
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/index.js +3 -2
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +6 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +42 -22
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +59 -23
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
- package/payload/platform/plugins/outlook/references/auth.md +1 -1
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +7 -3
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +1 -1
- package/payload/platform/plugins/storage-broker/PLUGIN.md +1 -1
- package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +111 -12
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js +54 -21
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +444 -0
- package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +18 -2
- package/payload/platform/scripts/__tests__/logs-rotate.test.sh +256 -0
- package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +79 -0
- package/payload/platform/scripts/logs-rotate.sh +204 -0
- package/payload/platform/scripts/resume-tunnel.sh +27 -0
- package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +41 -5
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.d.ts +1 -0
- package/payload/platform/services/claude-session-manager/dist/index.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +22 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts +2 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js +12 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +122 -9
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-census.js +214 -29
- package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +36 -24
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +13 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js +4 -2
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +150 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +333 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts +10 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js +55 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js.map +1 -0
- package/payload/server/{chunk-Q6W4U6HL.js → chunk-JXWFVE5X.js} +83 -39
- package/payload/server/server.js +268 -73
- package/payload/server/{src-3I2RYZFB.js → src-4F37OHLK.js} +11 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +0 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests for logs-rotate.sh (Task 1706). Fixture-driven: builds a throwaway logs
|
|
3
|
+
# dir per case, runs the real script against it, asserts on the resulting files.
|
|
4
|
+
# No stubs — gzip/find/wc/date are the real binaries, because the portability of
|
|
5
|
+
# exactly those calls across Linux and darwin is part of what this asserts.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
ROTATE="$SCRIPT_DIR/../logs-rotate.sh"
|
|
10
|
+
|
|
11
|
+
PASS=0
|
|
12
|
+
FAIL=0
|
|
13
|
+
|
|
14
|
+
run_case() {
|
|
15
|
+
local name="$1"; shift
|
|
16
|
+
echo ""
|
|
17
|
+
echo "=== CASE: $name ==="
|
|
18
|
+
if "$@"; then
|
|
19
|
+
PASS=$((PASS + 1))
|
|
20
|
+
echo "PASS: $name"
|
|
21
|
+
else
|
|
22
|
+
FAIL=$((FAIL + 1))
|
|
23
|
+
echo "FAIL: $name"
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Build a throwaway logs dir. Echoes its path.
|
|
28
|
+
mk_logs_dir() {
|
|
29
|
+
local d
|
|
30
|
+
d="$(mktemp -d "${TMPDIR:-/tmp}/logs-rotate-test.XXXXXX")"
|
|
31
|
+
echo "$d"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Write a file of exactly $2 bytes at $1. $2 must be a whole number of MiB.
|
|
35
|
+
# bs=1048576 rather than bs=1m/1M: the suffix spellings differ between BSD and
|
|
36
|
+
# GNU dd, and this suite runs on both.
|
|
37
|
+
mk_file_of_size() {
|
|
38
|
+
local path="$1" bytes="$2" mib
|
|
39
|
+
mib=$(( bytes / 1048576 ))
|
|
40
|
+
dd if=/dev/zero bs=1048576 count="$mib" 2>/dev/null | tr '\0' 'x' > "$path"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
size_of() { wc -c < "$1" 2>/dev/null | awk '{print $1+0}'; }
|
|
44
|
+
|
|
45
|
+
# Backdate a path by $2 days. `date -r` is BSD, `date -d @` is GNU.
|
|
46
|
+
backdate() {
|
|
47
|
+
local path="$1" days="$2" secs
|
|
48
|
+
secs=$(( $(date +%s) - days * 86400 ))
|
|
49
|
+
touch -t "$(date -u -r "$secs" +%Y%m%d%H%M 2>/dev/null || date -u -d "@$secs" +%Y%m%d%H%M)" "$path"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case_under_threshold_is_noop() {
|
|
53
|
+
local d; d="$(mk_logs_dir)"
|
|
54
|
+
# 63 MiB — just under the 64 MiB threshold, so this also pins the boundary.
|
|
55
|
+
mk_file_of_size "$d/server.log" $((63 * 1024 * 1024))
|
|
56
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
57
|
+
local rc=$?
|
|
58
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0, got $rc"; rm -rf "$d"; return 1; }
|
|
59
|
+
[ ! -e "$d/server.log.1.gz" ] || { echo " rotated a 63 MiB file"; rm -rf "$d"; return 1; }
|
|
60
|
+
[ "$(size_of "$d/server.log")" -eq $((63 * 1024 * 1024)) ] \
|
|
61
|
+
|| { echo " file was modified"; rm -rf "$d"; return 1; }
|
|
62
|
+
rm -rf "$d"; return 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case_over_threshold_rotates_and_truncates() {
|
|
66
|
+
local d; d="$(mk_logs_dir)"
|
|
67
|
+
mk_file_of_size "$d/server.log" $((65 * 1024 * 1024))
|
|
68
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
69
|
+
[ -f "$d/server.log.1.gz" ] || { echo " no .1.gz produced"; rm -rf "$d"; return 1; }
|
|
70
|
+
# The live file must still exist (readers hardcode the path) and be small:
|
|
71
|
+
# it holds only the marker line.
|
|
72
|
+
[ -f "$d/server.log" ] || { echo " live path missing after rotate"; rm -rf "$d"; return 1; }
|
|
73
|
+
local live; live="$(size_of "$d/server.log")"
|
|
74
|
+
[ "$live" -lt 4096 ] || { echo " live file not truncated: $live bytes"; rm -rf "$d"; return 1; }
|
|
75
|
+
rm -rf "$d"; return 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case_marker_line_present() {
|
|
79
|
+
local d; d="$(mk_logs_dir)"
|
|
80
|
+
mk_file_of_size "$d/server.log" $((65 * 1024 * 1024))
|
|
81
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
82
|
+
grep -q '\[logs-rotate\] rotated file=server.log prev-bytes=68157440 prev-file=server.log.1.gz keep=5' "$d/server.log" \
|
|
83
|
+
|| { echo " marker line absent or malformed:"; cat "$d/server.log"; rm -rf "$d"; return 1; }
|
|
84
|
+
# The marker must be datable from itself.
|
|
85
|
+
grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z \[logs-rotate\]' "$d/server.log" \
|
|
86
|
+
|| { echo " marker line not timestamped"; rm -rf "$d"; return 1; }
|
|
87
|
+
rm -rf "$d"; return 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case_generation_shift_drops_the_sixth() {
|
|
91
|
+
local d; d="$(mk_logs_dir)"
|
|
92
|
+
# Seed 5 existing generations, each identifiable by content.
|
|
93
|
+
local i
|
|
94
|
+
for i in 1 2 3 4 5; do echo "gen$i" | gzip -c > "$d/server.log.$i.gz"; done
|
|
95
|
+
mk_file_of_size "$d/server.log" $((65 * 1024 * 1024))
|
|
96
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
97
|
+
# .5.gz must now hold what .4.gz held; the old .5.gz is gone.
|
|
98
|
+
[ "$(gzip -cd "$d/server.log.5.gz")" = "gen4" ] || { echo " .5.gz is not the old .4.gz"; rm -rf "$d"; return 1; }
|
|
99
|
+
[ "$(gzip -cd "$d/server.log.2.gz")" = "gen1" ] || { echo " .2.gz is not the old .1.gz"; rm -rf "$d"; return 1; }
|
|
100
|
+
[ ! -e "$d/server.log.6.gz" ] || { echo " a 6th generation was created"; rm -rf "$d"; return 1; }
|
|
101
|
+
rm -rf "$d"; return 0
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case_lock_held_is_noop_exit_zero() {
|
|
105
|
+
local d; d="$(mk_logs_dir)"
|
|
106
|
+
mk_file_of_size "$d/server.log" $((65 * 1024 * 1024))
|
|
107
|
+
mkdir "$d/.rotate.lock" # fresh lock, not stale
|
|
108
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
109
|
+
local rc=$?
|
|
110
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0 when lock held, got $rc"; rm -rf "$d"; return 1; }
|
|
111
|
+
[ ! -e "$d/server.log.1.gz" ] || { echo " rotated despite held lock"; rm -rf "$d"; return 1; }
|
|
112
|
+
# A held lock must survive the tick that could not take it.
|
|
113
|
+
[ -d "$d/.rotate.lock" ] || { echo " another holder's lock was removed"; rm -rf "$d"; return 1; }
|
|
114
|
+
rm -rf "$d"; return 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case_stale_lock_is_reclaimed() {
|
|
118
|
+
local d; d="$(mk_logs_dir)"
|
|
119
|
+
mk_file_of_size "$d/server.log" $((65 * 1024 * 1024))
|
|
120
|
+
mkdir "$d/.rotate.lock"
|
|
121
|
+
backdate "$d/.rotate.lock" 1 # 1 day > STALE_LOCK_MINUTES=30
|
|
122
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
123
|
+
[ -f "$d/server.log.1.gz" ] || { echo " stale lock was not reclaimed"; rm -rf "$d"; return 1; }
|
|
124
|
+
rm -rf "$d"; return 0
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case_missing_logs_dir_is_noop_exit_zero() {
|
|
128
|
+
"$ROTATE" "/nonexistent/logs/dir/for/test" >/dev/null 2>&1
|
|
129
|
+
local rc=$?
|
|
130
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0 on missing dir, got $rc"; return 1; }
|
|
131
|
+
return 0
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case_lock_released_on_exit() {
|
|
135
|
+
local d; d="$(mk_logs_dir)"
|
|
136
|
+
mk_file_of_size "$d/server.log" $((1 * 1024 * 1024))
|
|
137
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
138
|
+
[ ! -e "$d/.rotate.lock" ] || { echo " lock not released"; rm -rf "$d"; return 1; }
|
|
139
|
+
rm -rf "$d"; return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case_purges_rc_child_older_than_seven_days() {
|
|
143
|
+
local d; d="$(mk_logs_dir)"
|
|
144
|
+
echo old > "$d/rc-child-aaaa.log"; backdate "$d/rc-child-aaaa.log" 9
|
|
145
|
+
echo new > "$d/rc-child-bbbb.log" # today
|
|
146
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
147
|
+
[ ! -e "$d/rc-child-aaaa.log" ] || { echo " 9-day-old rc-child survived"; rm -rf "$d"; return 1; }
|
|
148
|
+
[ -f "$d/rc-child-bbbb.log" ] || { echo " today's rc-child was purged"; rm -rf "$d"; return 1; }
|
|
149
|
+
rm -rf "$d"; return 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case_purge_does_not_touch_server_log() {
|
|
153
|
+
local d; d="$(mk_logs_dir)"
|
|
154
|
+
echo old > "$d/server.log"; backdate "$d/server.log" 90
|
|
155
|
+
echo x | gzip -c > "$d/server.log.1.gz"; backdate "$d/server.log.1.gz" 90
|
|
156
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
157
|
+
[ -f "$d/server.log" ] || { echo " server.log was purged by age"; rm -rf "$d"; return 1; }
|
|
158
|
+
[ -f "$d/server.log.1.gz" ] || { echo " a kept generation was purged by age"; rm -rf "$d"; return 1; }
|
|
159
|
+
rm -rf "$d"; return 0
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case_mcp_stderr_live_file_size_rotates() {
|
|
163
|
+
local d; d="$(mk_logs_dir)"
|
|
164
|
+
mk_file_of_size "$d/mcp-memory-stderr-2026-07-17.log" $((65 * 1024 * 1024))
|
|
165
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
166
|
+
[ -f "$d/mcp-memory-stderr-2026-07-17.log.1.gz" ] \
|
|
167
|
+
|| { echo " live mcp stderr log was not size-rotated"; rm -rf "$d"; return 1; }
|
|
168
|
+
rm -rf "$d"; return 0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# --- Code-review regressions (2026-07-17) -----------------------------------
|
|
172
|
+
|
|
173
|
+
# A failing gzip must not cost a generation. The shift used to run BEFORE the
|
|
174
|
+
# gzip, so every failure moved .4->.5 ... .1->.2 and wrote nothing to .1 —
|
|
175
|
+
# leaving a hole and dropping the oldest. The trigger is ENOSPC, i.e. exactly
|
|
176
|
+
# the condition rotation exists to prevent: 5 failing ticks destroyed 4 of the
|
|
177
|
+
# 5 retained generations while never truncating the live file.
|
|
178
|
+
case_gzip_failure_preserves_generations() {
|
|
179
|
+
local d; d="$(mk_logs_dir)"
|
|
180
|
+
local bin="$d/../bin-$$"; mkdir -p "$bin"
|
|
181
|
+
printf '#!/bin/sh\nexit 1\n' > "$bin/gzip"; chmod +x "$bin/gzip"
|
|
182
|
+
local i
|
|
183
|
+
for i in 1 2 3 4 5; do echo "gen$i" | gzip -c > "$d/server.log.$i.gz"; done
|
|
184
|
+
mk_file_of_size "$d/server.log" $((65 * 1024 * 1024))
|
|
185
|
+
|
|
186
|
+
PATH="$bin:$PATH" "$ROTATE" "$d" >/dev/null 2>&1
|
|
187
|
+
local rc=$?
|
|
188
|
+
|
|
189
|
+
local fail=0
|
|
190
|
+
for i in 1 2 3 4 5; do
|
|
191
|
+
if [ "$(gzip -cd "$d/server.log.$i.gz" 2>/dev/null)" != "gen$i" ]; then
|
|
192
|
+
echo " .$i.gz should still be gen$i, got: $(gzip -cd "$d/server.log.$i.gz" 2>/dev/null)"; fail=1
|
|
193
|
+
fi
|
|
194
|
+
done
|
|
195
|
+
# The live file must be untouched — a failed rotate never truncates.
|
|
196
|
+
[ "$(size_of "$d/server.log")" -eq $((65 * 1024 * 1024)) ] \
|
|
197
|
+
|| { echo " live file was truncated despite gzip failure"; fail=1; }
|
|
198
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0, got $rc"; fail=1; }
|
|
199
|
+
[ ! -e "$d/server.log.1.gz.tmp" ] || { echo " temp file left behind"; fail=1; }
|
|
200
|
+
rm -rf "$d" "$bin"; return $fail
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# The mcp stderr tee opens its file at process start and holds the stream for the
|
|
204
|
+
# process's whole life, writing ONLY when the server emits stderr — which a
|
|
205
|
+
# healthy server does not do for days. So a live file's mtime DOES go stale, and
|
|
206
|
+
# deleting it sends every later stderr write to an unlinked inode: invisible
|
|
207
|
+
# exactly when the fault you are chasing finally appears. Never purge these.
|
|
208
|
+
case_never_purges_mcp_stderr_by_age() {
|
|
209
|
+
local d; d="$(mk_logs_dir)"
|
|
210
|
+
echo quiet > "$d/mcp-memory-stderr-2026-01-01.log"
|
|
211
|
+
backdate "$d/mcp-memory-stderr-2026-01-01.log" 90
|
|
212
|
+
echo x | gzip -c > "$d/mcp-memory-stderr-2026-01-01.log.1.gz"
|
|
213
|
+
backdate "$d/mcp-memory-stderr-2026-01-01.log.1.gz" 90
|
|
214
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
215
|
+
[ -f "$d/mcp-memory-stderr-2026-01-01.log" ] \
|
|
216
|
+
|| { echo " a 90-day-stale mcp stderr log was purged — it may still be held open"; rm -rf "$d"; return 1; }
|
|
217
|
+
[ -f "$d/mcp-memory-stderr-2026-01-01.log.1.gz" ] \
|
|
218
|
+
|| { echo " an mcp generation was purged by age"; rm -rf "$d"; return 1; }
|
|
219
|
+
rm -rf "$d"; return 0
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# rc-child's 16 MiB cap does NOT hold: `written` is in-memory per manager
|
|
223
|
+
# process, so a manager restart resumes appending to the same path. Observed at
|
|
224
|
+
# 54 MB on sitedesk-code (3.25x the cap). A live long-lived session's file has a
|
|
225
|
+
# fresh mtime forever, so age-purge never reaches it — it needs size rotation.
|
|
226
|
+
case_rc_child_size_rotates() {
|
|
227
|
+
local d; d="$(mk_logs_dir)"
|
|
228
|
+
mk_file_of_size "$d/rc-child-live-session.log" $((65 * 1024 * 1024))
|
|
229
|
+
"$ROTATE" "$d" >/dev/null 2>&1
|
|
230
|
+
[ -f "$d/rc-child-live-session.log.1.gz" ] \
|
|
231
|
+
|| { echo " an oversized rc-child log was not size-rotated"; rm -rf "$d"; return 1; }
|
|
232
|
+
local live; live="$(size_of "$d/rc-child-live-session.log")"
|
|
233
|
+
[ "$live" -lt 4096 ] || { echo " rc-child live file not truncated: $live"; rm -rf "$d"; return 1; }
|
|
234
|
+
rm -rf "$d"; return 0
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
run_case "gzip failure preserves generations" case_gzip_failure_preserves_generations
|
|
238
|
+
run_case "never purges mcp stderr by age" case_never_purges_mcp_stderr_by_age
|
|
239
|
+
run_case "rc-child size-rotates" case_rc_child_size_rotates
|
|
240
|
+
|
|
241
|
+
run_case "under threshold is a no-op" case_under_threshold_is_noop
|
|
242
|
+
run_case "over threshold rotates and truncates" case_over_threshold_rotates_and_truncates
|
|
243
|
+
run_case "marker line present and timestamped" case_marker_line_present
|
|
244
|
+
run_case "generation shift drops the sixth" case_generation_shift_drops_the_sixth
|
|
245
|
+
run_case "lock held is a no-op, exit 0" case_lock_held_is_noop_exit_zero
|
|
246
|
+
run_case "stale lock is reclaimed" case_stale_lock_is_reclaimed
|
|
247
|
+
run_case "missing logs dir is a no-op, exit 0" case_missing_logs_dir_is_noop_exit_zero
|
|
248
|
+
run_case "lock released on exit" case_lock_released_on_exit
|
|
249
|
+
run_case "purges rc-child older than 7 days" case_purges_rc_child_older_than_seven_days
|
|
250
|
+
run_case "purge does not touch server.log" case_purge_does_not_touch_server_log
|
|
251
|
+
run_case "mcp stderr live file size-rotates" case_mcp_stderr_live_file_size_rotates
|
|
252
|
+
|
|
253
|
+
echo ""
|
|
254
|
+
echo "===================="
|
|
255
|
+
echo "PASS: $PASS FAIL: $FAIL"
|
|
256
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -619,6 +619,83 @@ case_refuse_on_invalid_state_json() {
|
|
|
619
619
|
return 0
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
# Task 1706 — the rotator's lock must be held across the offset-capture →
|
|
623
|
+
# verification-poll window. logs-rotate.sh truncates cloudflared.log in place
|
|
624
|
+
# when it exceeds 64 MiB; a rotation landing inside this window makes
|
|
625
|
+
# `tail -c "+$LOG_OFFSET"` read past EOF and report a tunnel failure that did
|
|
626
|
+
# not happen.
|
|
627
|
+
case_holds_rotate_lock_during_verify() {
|
|
628
|
+
local root
|
|
629
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
630
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
631
|
+
|
|
632
|
+
local log_dir="$root/home/.maxy-code/logs"
|
|
633
|
+
local marker="$root/lock-was-held"
|
|
634
|
+
|
|
635
|
+
# Poll for the lock's existence while the script runs. The verify gate is the
|
|
636
|
+
# window under test, so a single observation of the lock proves it was held
|
|
637
|
+
# across it.
|
|
638
|
+
( for _ in $(seq 1 60); do
|
|
639
|
+
[[ -d "$log_dir/.rotate.lock" ]] && { touch "$marker"; break; }
|
|
640
|
+
sleep 0.05
|
|
641
|
+
done ) &
|
|
642
|
+
local watcher=$!
|
|
643
|
+
|
|
644
|
+
HOME="$root/home" \
|
|
645
|
+
PATH="$root/bin:$PATH" \
|
|
646
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
647
|
+
bash "$RESUME" >/dev/null 2>&1
|
|
648
|
+
local rc=$?
|
|
649
|
+
wait "$watcher" 2>/dev/null
|
|
650
|
+
|
|
651
|
+
local held=0 leaked=0
|
|
652
|
+
[[ -e "$marker" ]] && held=1
|
|
653
|
+
[[ -d "$log_dir/.rotate.lock" ]] && leaked=1
|
|
654
|
+
cleanup "$root"
|
|
655
|
+
|
|
656
|
+
if [[ $rc -ne 0 ]]; then
|
|
657
|
+
echo " expected exit 0, got $rc"; return 1
|
|
658
|
+
fi
|
|
659
|
+
if [[ $held -ne 1 ]]; then
|
|
660
|
+
echo " rotate lock was never held during the verify window"; return 1
|
|
661
|
+
fi
|
|
662
|
+
if [[ $leaked -ne 0 ]]; then
|
|
663
|
+
echo " rotate lock not released after the verify window"; return 1
|
|
664
|
+
fi
|
|
665
|
+
return 0
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
# A lock already held (by a live rotator tick) must not stop the tunnel coming
|
|
669
|
+
# up. A false-alarm verify line is a far smaller failure than no tunnel.
|
|
670
|
+
case_busy_rotate_lock_does_not_block_spawn() {
|
|
671
|
+
local root
|
|
672
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
673
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
674
|
+
|
|
675
|
+
mkdir -p "$root/home/.maxy-code/logs/.rotate.lock"
|
|
676
|
+
|
|
677
|
+
HOME="$root/home" \
|
|
678
|
+
PATH="$root/bin:$PATH" \
|
|
679
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
680
|
+
bash "$RESUME" >/dev/null 2>&1
|
|
681
|
+
local rc=$?
|
|
682
|
+
|
|
683
|
+
local log_content
|
|
684
|
+
log_content=$(cat "$root/home/.maxy-code/logs/cloudflared.log" 2>/dev/null || echo "")
|
|
685
|
+
cleanup "$root"
|
|
686
|
+
|
|
687
|
+
if [[ $rc -ne 0 ]]; then
|
|
688
|
+
echo " expected exit 0, got $rc"; return 1
|
|
689
|
+
fi
|
|
690
|
+
if ! echo "$log_content" | grep -q "spawned service=cloudflared-maxy-code.service"; then
|
|
691
|
+
echo " a busy rotate lock blocked the tunnel spawn"; echo "$log_content"; return 1
|
|
692
|
+
fi
|
|
693
|
+
if ! echo "$log_content" | grep -q "rotate-lock busy"; then
|
|
694
|
+
echo " proceeding without the lock was not logged"; echo "$log_content"; return 1
|
|
695
|
+
fi
|
|
696
|
+
return 0
|
|
697
|
+
}
|
|
698
|
+
|
|
622
699
|
run_case "skip when service active" case_skip_when_scope_active
|
|
623
700
|
run_case "spawn succeeds and logs verified" case_spawn_succeeds
|
|
624
701
|
run_case "spawn with no connection logs ERROR exits 0" case_spawn_no_connection
|
|
@@ -635,6 +712,8 @@ run_case "reap: skip when active unit's MainPID is unresolved" case_reap_skip_wh
|
|
|
635
712
|
run_case "token tunnel spawns without --origincert" case_token_tunnel_spawns_without_origincert
|
|
636
713
|
run_case "configured tunnel with no run credential refuses loudly" case_refuse_when_no_run_credential
|
|
637
714
|
run_case "corrupt tunnel.state refuses loudly and exits 0" case_refuse_on_invalid_state_json
|
|
715
|
+
run_case "holds the rotate lock during verify" case_holds_rotate_lock_during_verify
|
|
716
|
+
run_case "busy rotate lock does not block spawn" case_busy_rotate_lock_does_not_block_spawn
|
|
638
717
|
|
|
639
718
|
echo ""
|
|
640
719
|
echo "Results: $PASS passed, $FAIL failed"
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Brand log rotation — Task 1706.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS AND NOT logrotate: server.log, edge.log and cloudflared.log
|
|
5
|
+
# are systemd stdout sinks (`StandardOutput=append:`, see
|
|
6
|
+
# packages/create-maxy-code/src/port-resolution.ts:233 and
|
|
7
|
+
# platform/scripts/resume-tunnel.sh:230). systemd opens the file O_APPEND ONCE
|
|
8
|
+
# at unit start and the process holds that fd for its whole life. logrotate's
|
|
9
|
+
# default rename-then-create would leave every service writing into the rotated
|
|
10
|
+
# inode forever — rotation would look like it worked while the live file stayed
|
|
11
|
+
# empty. So we gzip a COPY and truncate IN PLACE: because the fd is O_APPEND,
|
|
12
|
+
# each write seeks to end-of-file first, so writes resume at offset 0 with no
|
|
13
|
+
# service restart and no sparse hole. Verified against a real systemd unit on
|
|
14
|
+
# 2026-07-17 (laptop 192.168.88.10): NRestarts=0, file 71 -> 0 -> 80 bytes for
|
|
15
|
+
# 10 fresh lines. Never replace `: > "$F"` with a mv.
|
|
16
|
+
#
|
|
17
|
+
# Portability: this runs on Linux (cron) and darwin (launchd StartInterval), so
|
|
18
|
+
# no flock(1) (absent on darwin) and no `date +%N` (GNU-only). The lock is a
|
|
19
|
+
# mkdir, which is atomic on both. find's -mmin/-mtime/-delete and the timestamp
|
|
20
|
+
# format were confirmed identical on both platforms.
|
|
21
|
+
#
|
|
22
|
+
# Usage: logs-rotate.sh [LOGS_DIR]
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
ROTATE_SIZE_BYTES=$((64 * 1024 * 1024))
|
|
26
|
+
KEEP=5
|
|
27
|
+
STALE_LOCK_MINUTES=30
|
|
28
|
+
|
|
29
|
+
# Resolve the logs dir the same way logs-read.sh:52-58 does: convention is
|
|
30
|
+
# configDir = ".${installDir}".
|
|
31
|
+
if [ "$#" -ge 1 ] && [ -n "${1:-}" ]; then
|
|
32
|
+
LOGS_DIR="$1"
|
|
33
|
+
else
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
PLATFORM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
36
|
+
INSTALL_DIR="$(dirname "$PLATFORM_ROOT")"
|
|
37
|
+
LOGS_DIR="$HOME/.$(basename "$INSTALL_DIR")/logs"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Absent logs dir is not an error: the installer creates it, and a rotator tick
|
|
41
|
+
# that lands before the first install completes has nothing to do.
|
|
42
|
+
[ -d "$LOGS_DIR" ] || exit 0
|
|
43
|
+
|
|
44
|
+
LOCK="$LOGS_DIR/.rotate.lock"
|
|
45
|
+
|
|
46
|
+
stamp() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
47
|
+
|
|
48
|
+
# A mkdir lock is NOT released by the kernel when its holder dies, so a crashed
|
|
49
|
+
# rotator would wedge rotation permanently and silently — worse than the race
|
|
50
|
+
# the lock prevents. Reclaim on age: 30 min is two rotator ticks and 90x
|
|
51
|
+
# resume-tunnel.sh's 20s window, so no live holder is mistaken for a dead one.
|
|
52
|
+
#
|
|
53
|
+
# The reclaim moves the stale lock aside with `mv` before recreating it, rather
|
|
54
|
+
# than `rm -rf`-then-mkdir. `mv` is the atomic gate: if two ticks both see the
|
|
55
|
+
# same stale lock, only one can move that inode — the loser's `mv` fails because
|
|
56
|
+
# the source is already gone, and it backs off. rm-then-mkdir had no such gate:
|
|
57
|
+
# the loser would delete the winner's brand-new lock and both would rotate,
|
|
58
|
+
# shifting generations twice for one new .1.gz.
|
|
59
|
+
acquire_lock() {
|
|
60
|
+
if mkdir "$LOCK" 2>/dev/null; then return 0; fi
|
|
61
|
+
if [ -d "$LOCK" ] && [ -z "$(find "$LOCK" -maxdepth 0 -mmin -"$STALE_LOCK_MINUTES" 2>/dev/null)" ]; then
|
|
62
|
+
local stale="$LOCK.stale.$$"
|
|
63
|
+
mv "$LOCK" "$stale" 2>/dev/null || return 1 # lost the reclaim race
|
|
64
|
+
rm -rf "$stale" 2>/dev/null
|
|
65
|
+
echo "$(stamp) [logs-rotate] stale-lock-reclaimed lock=$LOCK age-min=>$STALE_LOCK_MINUTES"
|
|
66
|
+
mkdir "$LOCK" 2>/dev/null && return 0 # a third tick may have won; then back off
|
|
67
|
+
fi
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if ! acquire_lock; then
|
|
72
|
+
# Another rotator tick, or a resume-tunnel.sh verification window, is live.
|
|
73
|
+
# Exit 0: the next tick is 15 min away and the bound is not tight enough for
|
|
74
|
+
# a skipped tick to matter.
|
|
75
|
+
echo "$(stamp) [logs-rotate] skipped reason=lock-held lock=$LOCK"
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT
|
|
79
|
+
|
|
80
|
+
size_of() { wc -c < "$1" 2>/dev/null | awk '{print $1+0}'; }
|
|
81
|
+
|
|
82
|
+
# Size-rotate one file: gzip a copy to a temp, shift older generations, move the
|
|
83
|
+
# temp into .1.gz, truncate in place, then mark the truncation so the file is
|
|
84
|
+
# never mistaken for one that simply starts there.
|
|
85
|
+
#
|
|
86
|
+
# ORDER IS LOAD-BEARING: gzip FIRST, shift only once it succeeded. The shift used
|
|
87
|
+
# to run first, so a gzip failure moved .4->.5 ... .1->.2 and wrote nothing to
|
|
88
|
+
# .1 — dropping the oldest generation and leaving a hole, on every tick. The
|
|
89
|
+
# trigger is ENOSPC, i.e. precisely the condition rotation exists to prevent: 75
|
|
90
|
+
# minutes of a full disk destroyed 4 of the 5 retained generations while
|
|
91
|
+
# correctly refusing to truncate. Never hoist the shift above the gzip.
|
|
92
|
+
rotate_file() {
|
|
93
|
+
local f="$1" base size i prev
|
|
94
|
+
[ -f "$f" ] || return 0
|
|
95
|
+
size="$(size_of "$f")"
|
|
96
|
+
[ "$size" -gt "$ROTATE_SIZE_BYTES" ] || return 0
|
|
97
|
+
base="$(basename "$f")"
|
|
98
|
+
|
|
99
|
+
# gzip to a temp first, so a failure (or a tick killed mid-compress) costs
|
|
100
|
+
# nothing: no generation has moved and the live file is untouched.
|
|
101
|
+
#
|
|
102
|
+
# gzip's stderr is CAPTURED, not discarded, and relayed verbatim on failure —
|
|
103
|
+
# it is the only thing that distinguishes ENOSPC from EACCES from a missing
|
|
104
|
+
# binary. `2>&1 >file` sends stderr to the command substitution and stdout to
|
|
105
|
+
# the temp; the order matters.
|
|
106
|
+
#
|
|
107
|
+
# A live log grows while gzip reads it, so gzip emits "file size changed while
|
|
108
|
+
# zipping" on nearly every real rotation. That is a WARNING and gzip still
|
|
109
|
+
# exits 0 (verified on the laptop against a file being appended to during the
|
|
110
|
+
# read), so it takes the success path and the noise is never logged. Only a
|
|
111
|
+
# genuine non-zero exit reaches the branch below.
|
|
112
|
+
local gzerr
|
|
113
|
+
gzerr="$(gzip -c "$f" 2>&1 >"$f.1.gz.tmp")"
|
|
114
|
+
if [ $? -ne 0 ]; then
|
|
115
|
+
rm -f "$f.1.gz.tmp"
|
|
116
|
+
echo "$(stamp) [logs-rotate] ERROR file=$base reason=gzip-failed bytes=$size err=$(printf '%s' "$gzerr" | tr '\n' ' ') — not truncated, generations intact"
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Shift down from the oldest so nothing is overwritten before it moves. The
|
|
121
|
+
# KEEP-th generation is overwritten by the (KEEP-1)-th, which is how the
|
|
122
|
+
# oldest falls off.
|
|
123
|
+
i="$KEEP"
|
|
124
|
+
while [ "$i" -gt 1 ]; do
|
|
125
|
+
prev=$((i - 1))
|
|
126
|
+
[ -f "$f.$prev.gz" ] && mv -f "$f.$prev.gz" "$f.$i.gz"
|
|
127
|
+
i="$prev"
|
|
128
|
+
done
|
|
129
|
+
mv -f "$f.1.gz.tmp" "$f.1.gz"
|
|
130
|
+
|
|
131
|
+
# THE load-bearing line. Truncate in place; do not mv. See the header.
|
|
132
|
+
#
|
|
133
|
+
# Lines written between gzip reaching EOF and this truncate are LOST. gzip
|
|
134
|
+
# reads to the CURRENT end of file, so the window is the shift plus two mvs —
|
|
135
|
+
# milliseconds, not the seconds the gzip itself takes. Inherent to
|
|
136
|
+
# copy-truncate, which the O_APPEND fd leaves no way around.
|
|
137
|
+
#
|
|
138
|
+
# `size` was sampled BEFORE gzip ran, so on a live log the archive legitimately
|
|
139
|
+
# holds slightly MORE than prev-bytes reports. prev-bytes locates the seam
|
|
140
|
+
# approximately, not exactly. See .docs/brand-log-retention.md.
|
|
141
|
+
: > "$f"
|
|
142
|
+
echo "$(stamp) [logs-rotate] rotated file=$base prev-bytes=$size prev-file=$base.1.gz keep=$KEEP" >> "$f"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
PURGE_DAYS=7
|
|
146
|
+
|
|
147
|
+
for f in server.log edge.log cloudflared.log check-due-events.log; do
|
|
148
|
+
rotate_file "$LOGS_DIR/$f"
|
|
149
|
+
done
|
|
150
|
+
|
|
151
|
+
# Globbed size targets. nullglob so a brand with none of these yet does not try
|
|
152
|
+
# to rotate a literal '*' path.
|
|
153
|
+
#
|
|
154
|
+
# mcp-*-stderr-*.log: the tee holds a live write stream, so this is the ONLY
|
|
155
|
+
# bound it gets here — see the purge note below for why it is never age-purged.
|
|
156
|
+
#
|
|
157
|
+
# rc-child-*.log: its 16 MiB writer cap does NOT hold. `written`
|
|
158
|
+
# (rc-child-log.ts:33) is in-memory per manager process, so a manager restart
|
|
159
|
+
# resumes appending to the same path with a fresh counter. Observed at 54 MB on
|
|
160
|
+
# sitedesk-code, 3.25x the cap. A long-lived session's file also has a fresh
|
|
161
|
+
# mtime forever, so the age purge below never reaches it — without size rotation
|
|
162
|
+
# it is genuinely unbounded.
|
|
163
|
+
shopt -s nullglob
|
|
164
|
+
for f in "$LOGS_DIR"/mcp-*-stderr-*.log "$LOGS_DIR"/rc-child-*.log; do
|
|
165
|
+
rotate_file "$f"
|
|
166
|
+
done
|
|
167
|
+
shopt -u nullglob
|
|
168
|
+
|
|
169
|
+
# Age purge — rc-child only.
|
|
170
|
+
#
|
|
171
|
+
# rc-child-*.log is one file per session and the set grows without bound (100 on
|
|
172
|
+
# sitedesk-code). Purging it is SAFE because rc-child-log.ts:51 writes with
|
|
173
|
+
# appendFileSync: no fd is held, so a purged file is simply recreated on the
|
|
174
|
+
# next write.
|
|
175
|
+
#
|
|
176
|
+
# mcp-*-stderr-*.log is deliberately NOT purged, though an earlier draft did.
|
|
177
|
+
# The tee (mcp-stderr-tee/src/index.ts:88) opens a createWriteStream at process
|
|
178
|
+
# start and holds it for the process's whole life, writing only when the server
|
|
179
|
+
# emits stderr — which a healthy server does not do for days. So a LIVE file's
|
|
180
|
+
# mtime DOES go stale, and deleting it would send every later stderr write to an
|
|
181
|
+
# unlinked inode: invisible exactly when the fault you are chasing finally
|
|
182
|
+
# appears, and its disk unreclaimed until the process exits. Bounding these by
|
|
183
|
+
# age needs live-vs-dead discrimination, which needs the writer to stop pinning
|
|
184
|
+
# its date at boot — that is Task 1721. Size rotation above is safe regardless,
|
|
185
|
+
# because truncate-in-place is precisely what a held fd tolerates.
|
|
186
|
+
#
|
|
187
|
+
# The pattern is `.log*` so a purged session's rotated .gz generations go too.
|
|
188
|
+
# `find` matches each path by its own mtime — there is no sibling relationship —
|
|
189
|
+
# so a LIVE rc-child's generations also age out at 7 days. That is intended:
|
|
190
|
+
# retention for rc-child is min(KEEP, 7 days), and 7 days of a session's spawn
|
|
191
|
+
# output is the stated bound.
|
|
192
|
+
# One traversal, and the count is what was actually deleted rather than what a
|
|
193
|
+
# separate counting pass saw a moment earlier. `-print` before `-delete` prints
|
|
194
|
+
# each path as it goes.
|
|
195
|
+
purge_old() {
|
|
196
|
+
local pattern="$1" n
|
|
197
|
+
n="$(find "$LOGS_DIR" -maxdepth 1 -name "$pattern" -type f -mtime +"$PURGE_DAYS" -print -delete 2>/dev/null | wc -l | awk '{print $1+0}')"
|
|
198
|
+
[ "$n" -gt 0 ] || return 0
|
|
199
|
+
echo "$(stamp) [logs-rotate] purged pattern=$pattern files=$n older-than-days=$PURGE_DAYS"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
purge_old 'rc-child-*.log*'
|
|
203
|
+
|
|
204
|
+
exit 0
|
|
@@ -194,6 +194,24 @@ log "Resuming tunnel $TUNNEL_ID for $DOMAIN..."
|
|
|
194
194
|
# Capture log byte offset before spawning so the verification poll only
|
|
195
195
|
# matches lines written by THIS invocation, not lines from a prior run.
|
|
196
196
|
mkdir -p "$LOG_DIR"
|
|
197
|
+
|
|
198
|
+
# Task 1706 — hold the log rotator's lock across offset-capture → verify-poll.
|
|
199
|
+
# logs-rotate.sh truncates cloudflared.log in place when it exceeds 64 MiB; a
|
|
200
|
+
# rotation landing inside this window makes `tail -c "+$LOG_OFFSET"` read past
|
|
201
|
+
# EOF and report a tunnel failure that did not happen. Same mkdir primitive as
|
|
202
|
+
# the rotator (no flock(1) on darwin, and a lock is only a lock if both ends
|
|
203
|
+
# agree). Best-effort: if the lock cannot be taken we proceed anyway rather than
|
|
204
|
+
# refuse to bring the tunnel up — a false-alarm log line is a far smaller
|
|
205
|
+
# failure than a tunnel that never starts.
|
|
206
|
+
ROTATE_LOCK="$LOG_DIR/.rotate.lock"
|
|
207
|
+
ROTATE_LOCK_HELD=0
|
|
208
|
+
if mkdir "$ROTATE_LOCK" 2>/dev/null; then
|
|
209
|
+
ROTATE_LOCK_HELD=1
|
|
210
|
+
trap 'rmdir "$ROTATE_LOCK" 2>/dev/null || true' EXIT
|
|
211
|
+
else
|
|
212
|
+
log "rotate-lock busy — proceeding without it; a concurrent rotation may skew the verify gate"
|
|
213
|
+
fi
|
|
214
|
+
|
|
197
215
|
LOG_OFFSET=$(wc -c < "$LOG_FILE" 2>/dev/null | awk '{print $1+0}' || echo 0)
|
|
198
216
|
|
|
199
217
|
# Spawn into a SUPERVISED transient service under cloudflared.slice (Task 757).
|
|
@@ -264,6 +282,15 @@ else
|
|
|
264
282
|
log "ERROR: tunnel service started but no edge connection within ${VERIFY_DEADLINE}s — service=$SERVICE_UNIT"
|
|
265
283
|
fi
|
|
266
284
|
|
|
285
|
+
# Task 1706 — release the rotator's lock. The verify gate above is the only
|
|
286
|
+
# offset-sensitive window, so holding it past here would make a rotator tick
|
|
287
|
+
# skip for no reason.
|
|
288
|
+
if [ "$ROTATE_LOCK_HELD" -eq 1 ]; then
|
|
289
|
+
rmdir "$ROTATE_LOCK" 2>/dev/null || true
|
|
290
|
+
ROTATE_LOCK_HELD=0
|
|
291
|
+
trap - EXIT
|
|
292
|
+
fi
|
|
293
|
+
|
|
267
294
|
# Update state file: keep pid/startedAt for observability, add serviceUnit as control signal.
|
|
268
295
|
NEW_STATE=$(echo "$STATE" | jq \
|
|
269
296
|
--arg unit "$SERVICE_UNIT" \
|