dsh-autorestart 0.7.0 → 0.8.1
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/CHANGELOG.md +47 -0
- package/README.md +137 -6
- package/client.js +387 -0
- package/cordis.patch.yml +7 -0
- package/docs/README.zh.md +83 -5
- package/docs/platform-matrix.md +92 -0
- package/docs/rescue-drill.md +606 -0
- package/index.js +598 -3
- package/lib/diagnostics.js +8 -0
- package/lib/rescue-paths.js +253 -0
- package/lib/rescue.js +596 -0
- package/lib/scheduler-capability.js +300 -0
- package/lib/watchdog-arbiter.js +117 -0
- package/lib/watchdog-schedule.js +310 -0
- package/lib/watchdog-tier.js +182 -0
- package/lib/watchdog.js +479 -0
- package/locale/en.json +28 -1
- package/locale/zh.json +28 -1
- package/package.json +6 -1
- package/scripts/README.md +65 -0
- package/scripts/drill-rescue.ps1 +508 -0
- package/scripts/drill-rescue.sh +427 -0
- package/scripts/drill-watchdog.mjs +341 -0
- package/scripts/emergency-disable.sh +314 -0
- package/scripts/emergency-toggle.ps1 +136 -65
- package/scripts/rescue-strings.zh.json +168 -0
- package/scripts/restore-profile.cmd +44 -0
- package/scripts/restore-profile.ps1 +608 -0
- package/scripts/restore-profile.sh +593 -0
- package/scripts/run-tests.ps1 +1 -1
- package/scripts/watchdog.mjs +203 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The rescue drill (POSIX): break a profile on purpose, put it back, prove it.
|
|
3
|
+
#
|
|
4
|
+
# Three traps this file avoids (the whole story: scripts/README.md):
|
|
5
|
+
# 1. the wording lives in rescue-strings.zh.json and is NESTED - it is read
|
|
6
|
+
# by path with node, never by a flat grep;
|
|
7
|
+
# 2. the English fallbacks below must NOT contain parentheses: dash counts
|
|
8
|
+
# them even inside single quotes, and one stray ")" breaks the script;
|
|
9
|
+
# 3. the sed expressions stay single-quoted: the shell eats the backslashes
|
|
10
|
+
# in a double-quoted sed program and truncates it.
|
|
11
|
+
#
|
|
12
|
+
# The same assertions as drill-rescue.ps1, in a directory of its own under the
|
|
13
|
+
# temp directory. Nothing here touches the profile of a running DSH.
|
|
14
|
+
#
|
|
15
|
+
# The wording comes from rescue-strings.zh.json; the English fallbacks keep
|
|
16
|
+
# the script working without it.
|
|
17
|
+
set -eu
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
20
|
+
RESTORE="$SCRIPT_DIR/restore-profile.sh"
|
|
21
|
+
DISABLE="$SCRIPT_DIR/emergency-disable.sh"
|
|
22
|
+
STRINGS_FILE="$SCRIPT_DIR/rescue-strings.zh.json"
|
|
23
|
+
PACKAGE_NAME='dsh-autorestart'
|
|
24
|
+
|
|
25
|
+
DRILL_DIR="${TMPDIR:-/tmp}/dsh-rescue-drill-$$"
|
|
26
|
+
FAILED=0
|
|
27
|
+
|
|
28
|
+
usage() {
|
|
29
|
+
printf 'Usage: %s [drill-directory]\n' "$0"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
while [ $# -gt 0 ]; do
|
|
33
|
+
case "$1" in
|
|
34
|
+
--help|-h) usage; exit 0 ;;
|
|
35
|
+
*) DRILL_DIR="$1" ;;
|
|
36
|
+
esac
|
|
37
|
+
shift
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
NODE_BIN=''
|
|
41
|
+
if command -v node >/dev/null 2>&1; then
|
|
42
|
+
NODE_BIN=$(command -v node)
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# t <key> <english fallback>
|
|
46
|
+
#
|
|
47
|
+
# The key is a PATH into the strings file ("drill.passed"), because that file
|
|
48
|
+
# is nested. Node reads it (it is on every machine that runs DSH); without
|
|
49
|
+
# node every message falls back to English.
|
|
50
|
+
t() {
|
|
51
|
+
key="$1"
|
|
52
|
+
fallback="$2"
|
|
53
|
+
if [ -n "$NODE_BIN" ] && [ -f "$STRINGS_FILE" ]; then
|
|
54
|
+
value=$("$NODE_BIN" -e '
|
|
55
|
+
const fs = require("fs");
|
|
56
|
+
try {
|
|
57
|
+
const doc = JSON.parse(fs.readFileSync(process.argv[1], "utf8").replace(/^\uFEFF/, ""));
|
|
58
|
+
let value = doc;
|
|
59
|
+
for (const part of process.argv[2].split(".")) value = value == null ? undefined : value[part];
|
|
60
|
+
if (typeof value !== "string" || value === "") process.exit(1);
|
|
61
|
+
process.stdout.write(value);
|
|
62
|
+
} catch (error) { process.exit(1); }
|
|
63
|
+
' "$STRINGS_FILE" "$key" 2>/dev/null) || value=''
|
|
64
|
+
if [ -n "$value" ]; then
|
|
65
|
+
printf '%s' "$value"
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
printf '%s' "$fallback"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# fmt <text> <value> ...
|
|
73
|
+
#
|
|
74
|
+
# Node does the replacement when it is there: the Chinese wording is UTF-8 and
|
|
75
|
+
# a byte-wise sed would mangle it. Without node only the English fallbacks are
|
|
76
|
+
# used, and those are plain ASCII.
|
|
77
|
+
fmt() {
|
|
78
|
+
text="$1"
|
|
79
|
+
shift
|
|
80
|
+
if [ -n "$NODE_BIN" ]; then
|
|
81
|
+
if "$NODE_BIN" -e '
|
|
82
|
+
let text = process.argv[1];
|
|
83
|
+
process.argv.slice(2).forEach((value, index) => { text = text.split("{" + index + "}").join(value); });
|
|
84
|
+
process.stdout.write(text);
|
|
85
|
+
' "$text" "$@" 2>/dev/null; then
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
index=0
|
|
90
|
+
for value in "$@"; do
|
|
91
|
+
safe=$(printf '%s' "$value" | sed 's/[&|\\]/\\&/g')
|
|
92
|
+
text=$(printf '%s' "$text" | sed "s|{$index}|$safe|g")
|
|
93
|
+
index=$((index + 1))
|
|
94
|
+
done
|
|
95
|
+
printf '%s' "$text"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
say() {
|
|
99
|
+
printf '%s\n' "$1"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
assert_pass() {
|
|
103
|
+
say "$(fmt "$(t 'drill.assertPass' ' [pass] {0}')" "$1")"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
assert_fail() {
|
|
107
|
+
FAILED=$((FAILED + 1))
|
|
108
|
+
say "$(fmt "$(t 'drill.assertFail' ' [FAIL] {0} -- {1}')" "$1" "$2")"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# The keywords a refusal has to carry (the same file the Windows drill uses).
|
|
112
|
+
keyword_list() {
|
|
113
|
+
key="$1"
|
|
114
|
+
if [ -n "$NODE_BIN" ] && [ -f "$STRINGS_FILE" ]; then
|
|
115
|
+
"$NODE_BIN" -e '
|
|
116
|
+
const fs = require("fs");
|
|
117
|
+
try {
|
|
118
|
+
const doc = JSON.parse(fs.readFileSync(process.argv[1], "utf8").replace(/^\uFEFF/, ""));
|
|
119
|
+
let value = doc;
|
|
120
|
+
for (const part of process.argv[2].split(".")) value = value == null ? undefined : value[part];
|
|
121
|
+
if (!Array.isArray(value)) process.exit(1);
|
|
122
|
+
process.stdout.write(value.join("\n"));
|
|
123
|
+
} catch (error) { process.exit(1); }
|
|
124
|
+
' "$STRINGS_FILE" "$key" 2>/dev/null
|
|
125
|
+
return $?
|
|
126
|
+
fi
|
|
127
|
+
printf ''
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# refused <name> <output> <exit code> <keyword key>
|
|
131
|
+
refused() {
|
|
132
|
+
name="$1"
|
|
133
|
+
output="$2"
|
|
134
|
+
code="$3"
|
|
135
|
+
key="$4"
|
|
136
|
+
if [ "$code" -eq 0 ]; then
|
|
137
|
+
assert_fail "$name" "$(fmt "$(t 'drill.refusedNotRefused' ' [FAIL] {0} -- the exit code is 0, the script did not refuse')" "$name")"
|
|
138
|
+
return
|
|
139
|
+
fi
|
|
140
|
+
found=''
|
|
141
|
+
for word in $(keyword_list "$key"); do
|
|
142
|
+
[ -n "$word" ] || continue
|
|
143
|
+
if printf '%s' "$output" | grep -q "$word"; then
|
|
144
|
+
found="$word"
|
|
145
|
+
break
|
|
146
|
+
fi
|
|
147
|
+
done
|
|
148
|
+
if [ -z "$found" ]; then
|
|
149
|
+
assert_fail "$name" "$(fmt "$(t 'drill.refusedNoReason' ' [FAIL] {0} -- the exit code is {1}, but the output has no reason')" "$name" "$code")"
|
|
150
|
+
return
|
|
151
|
+
fi
|
|
152
|
+
detail=$(fmt "$(t 'drill.refusedOk' ' [pass] {0} -- exit code {1}, the output says why')" "$name" "$code")
|
|
153
|
+
assert_pass "$name ($detail)"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if [ ! -x "$RESTORE" ] && [ ! -f "$RESTORE" ]; then
|
|
157
|
+
say "$(fmt "$(t 'drill.noScript' 'the drill failed: {0} not found')" "$RESTORE")"
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
if [ -e "$DRILL_DIR" ]; then
|
|
161
|
+
say "$(fmt "$(t 'drill.dirOccupied' 'the drill failed: this drill directory is already there: {0}')" "$DRILL_DIR")"
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
say "$(fmt "$(t 'drill.dir' 'drill directory: {0}')" "$DRILL_DIR")"
|
|
166
|
+
mkdir -p -- "$DRILL_DIR"
|
|
167
|
+
|
|
168
|
+
PROFILE="$DRILL_DIR/profile"
|
|
169
|
+
STATE="$DRILL_DIR/state"
|
|
170
|
+
BACKUPS="$STATE/backups"
|
|
171
|
+
SNAPSHOT="$BACKUPS/20260101-000000"
|
|
172
|
+
mkdir -p -- "$PROFILE/node_modules/$PACKAGE_NAME" "$SNAPSHOT/config" "$SNAPSHOT/node_modules/$PACKAGE_NAME"
|
|
173
|
+
|
|
174
|
+
# --- the profile and the snapshot ---
|
|
175
|
+
cat > "$PROFILE/package.json" <<'JSON'
|
|
176
|
+
{
|
|
177
|
+
"name": "dsh-profile",
|
|
178
|
+
"dsh": {
|
|
179
|
+
"profile": {
|
|
180
|
+
"bundles": [
|
|
181
|
+
"dsh-autorestart"
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
JSON
|
|
187
|
+
cat > "$PROFILE/cordis.patch.yml" <<'YML'
|
|
188
|
+
# the profile configuration
|
|
189
|
+
YML
|
|
190
|
+
cat > "$PROFILE/node_modules/$PACKAGE_NAME/package.json" <<'JSON'
|
|
191
|
+
{
|
|
192
|
+
"name": "dsh-autorestart",
|
|
193
|
+
"version": "1.2.3"
|
|
194
|
+
}
|
|
195
|
+
JSON
|
|
196
|
+
cp "$PROFILE/package.json" "$SNAPSHOT/config/package.json"
|
|
197
|
+
cp "$PROFILE/cordis.patch.yml" "$SNAPSHOT/config/cordis.patch.yml"
|
|
198
|
+
cp "$PROFILE/node_modules/$PACKAGE_NAME/package.json" "$SNAPSHOT/node_modules/$PACKAGE_NAME/package.json"
|
|
199
|
+
cat > "$SNAPSHOT/manifest.json" <<'JSON'
|
|
200
|
+
{
|
|
201
|
+
"at": 1767225600000,
|
|
202
|
+
"atLocal": "2026-01-01T00:00:00",
|
|
203
|
+
"includePlugins": true
|
|
204
|
+
}
|
|
205
|
+
JSON
|
|
206
|
+
|
|
207
|
+
# A NEWER decoy without a manifest: it must be skipped.
|
|
208
|
+
DECOY="$BACKUPS/20260202-000000"
|
|
209
|
+
mkdir -p -- "$DECOY/config"
|
|
210
|
+
cp "$PROFILE/package.json" "$DECOY/config/package.json"
|
|
211
|
+
|
|
212
|
+
# --- break it on purpose ---
|
|
213
|
+
printf '{ this is not json' > "$PROFILE/package.json"
|
|
214
|
+
rm -rf -- "$PROFILE/node_modules/$PACKAGE_NAME"
|
|
215
|
+
|
|
216
|
+
say "$(t 'drill.broke' 'broken on purpose: package.json is invalid JSON, the plugin directory is gone.')"
|
|
217
|
+
|
|
218
|
+
# --- 1) the dry run changes nothing ---
|
|
219
|
+
set +e
|
|
220
|
+
DRY_OUT=$("$RESTORE" --dry-run --profile "$PROFILE" --state-dir "$STATE" 2>&1)
|
|
221
|
+
DRY_CODE=$?
|
|
222
|
+
set -e
|
|
223
|
+
DRY_MARK=$(keyword_list 'drill.keywords.dryRun' | head -n 1)
|
|
224
|
+
if [ "$DRY_CODE" -eq 0 ] && { [ -z "$DRY_MARK" ] || printf '%s' "$DRY_OUT" | grep -q "$DRY_MARK"; }; then
|
|
225
|
+
assert_pass "$(t 'drill.assertDryRunExit' 'the dry run exits with 0')"
|
|
226
|
+
else
|
|
227
|
+
assert_fail "$(t 'drill.assertDryRunExit' 'the dry run exits with 0')" "$DRY_OUT"
|
|
228
|
+
fi
|
|
229
|
+
if [ -z "$(find "$DRILL_DIR" -maxdepth 1 -name 'profile.broken-*' 2>/dev/null)" ]; then
|
|
230
|
+
assert_pass "$(t 'drill.assertDryRunNoBroken' 'the dry run leaves no broken directory')"
|
|
231
|
+
else
|
|
232
|
+
assert_fail "$(t 'drill.assertDryRunNoBroken' 'the dry run leaves no broken directory')" 'there is one'
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# --- 2) the real restore ---
|
|
236
|
+
set +e
|
|
237
|
+
RESTORE_OUT=$("$RESTORE" --profile "$PROFILE" --state-dir "$STATE" 2>&1)
|
|
238
|
+
RESTORE_CODE=$?
|
|
239
|
+
set -e
|
|
240
|
+
if [ "$RESTORE_CODE" -eq 0 ]; then
|
|
241
|
+
assert_pass "$(t 'drill.assertRestoreExit' 'the restore exits with 0')"
|
|
242
|
+
else
|
|
243
|
+
assert_fail "$(t 'drill.assertRestoreExit' 'the restore exits with 0')" "$RESTORE_OUT"
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# --- the assertions about the result ---
|
|
247
|
+
if printf '%s' "$RESTORE_OUT" | LC_ALL=C grep -q '[^ -~]'; then
|
|
248
|
+
assert_pass "$(t 'drill.assertChineseOutput' 'the output of the restore script carries Chinese (the wording comes from the JSON)')"
|
|
249
|
+
else
|
|
250
|
+
assert_fail "$(t 'drill.assertChineseOutput' 'the output of the restore script carries Chinese (the wording comes from the JSON)')" 'no non-ASCII byte in the output'
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
if [ -n "$NODE_BIN" ] && "$NODE_BIN" -e 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))' "$PROFILE/package.json" >/dev/null 2>&1; then
|
|
254
|
+
assert_pass "$(t 'drill.assertParses' 'after the restore package.json can be parsed')"
|
|
255
|
+
else
|
|
256
|
+
assert_fail "$(t 'drill.assertParses' 'after the restore package.json can be parsed')" 'it does not parse'
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
if grep -q "$PACKAGE_NAME" "$PROFILE/package.json" 2>/dev/null; then
|
|
260
|
+
assert_pass "$(t 'drill.assertUsedSnapshot' 'the snapshot with the manifest was used (the newer decoy was skipped)')"
|
|
261
|
+
else
|
|
262
|
+
assert_fail "$(t 'drill.assertUsedSnapshot' 'the snapshot with the manifest was used (the newer decoy was skipped)')" 'the plugin is not in the bundles'
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
if grep -q '"bundles"' "$PROFILE/package.json" 2>/dev/null; then
|
|
266
|
+
assert_pass "$(t 'drill.assertBundles' 'after the restore dsh.profile.bundles has content')"
|
|
267
|
+
else
|
|
268
|
+
assert_fail "$(t 'drill.assertBundles' 'after the restore dsh.profile.bundles has content')" 'there is no bundles'
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
VERSION=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PROFILE/node_modules/$PACKAGE_NAME/package.json" 2>/dev/null | head -n 1)
|
|
272
|
+
if [ -n "$VERSION" ]; then
|
|
273
|
+
assert_pass "$(t 'drill.assertVersionReadable' 'after the restore the version of the plugin can be read')"
|
|
274
|
+
else
|
|
275
|
+
assert_fail "$(t 'drill.assertVersionReadable' 'after the restore the version of the plugin can be read')" 'there is no version'
|
|
276
|
+
fi
|
|
277
|
+
if [ "$VERSION" = '1.2.3' ]; then
|
|
278
|
+
assert_pass "$(t 'drill.assertVersionSame' 'the version is the one of the snapshot')"
|
|
279
|
+
else
|
|
280
|
+
assert_fail "$(t 'drill.assertVersionSame' 'the version is the one of the snapshot')" "got '$VERSION'"
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
BROKEN=$(find "$DRILL_DIR" -maxdepth 1 -name 'profile.broken-*' 2>/dev/null | head -n 1)
|
|
284
|
+
if [ -n "$BROKEN" ]; then
|
|
285
|
+
assert_pass "$(t 'drill.assertBrokenExists' 'the broken evidence directory is there')"
|
|
286
|
+
if grep -q 'this is not json' "$BROKEN/package.json" 2>/dev/null; then
|
|
287
|
+
assert_pass "$(t 'drill.assertBrokenKeeps' 'the broken directory keeps the broken version')"
|
|
288
|
+
else
|
|
289
|
+
assert_fail "$(t 'drill.assertBrokenKeeps' 'the broken directory keeps the broken version')" 'it does not'
|
|
290
|
+
fi
|
|
291
|
+
else
|
|
292
|
+
assert_fail "$(t 'drill.assertBrokenExists' 'the broken evidence directory is there')" 'there is none'
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
# --- the refusals: the exit code AND the reason in the output ---
|
|
296
|
+
NOT_A_PROFILE="$DRILL_DIR/not-a-profile"
|
|
297
|
+
mkdir -p -- "$NOT_A_PROFILE"
|
|
298
|
+
printf 'hello' > "$NOT_A_PROFILE/readme.txt"
|
|
299
|
+
set +e
|
|
300
|
+
OUT=$("$RESTORE" --profile "$NOT_A_PROFILE" --state-dir "$STATE" 2>&1)
|
|
301
|
+
CODE=$?
|
|
302
|
+
set -e
|
|
303
|
+
refused "$(t 'drill.assertNotProfileRefused' 'a directory that is not a profile is refused')" "$OUT" "$CODE" 'drill.keywords.notProfile'
|
|
304
|
+
if [ "$(cat "$NOT_A_PROFILE/readme.txt")" = 'hello' ]; then
|
|
305
|
+
assert_pass "$(t 'drill.assertNotProfileUntouched' 'the refused directory was not touched')"
|
|
306
|
+
else
|
|
307
|
+
assert_fail "$(t 'drill.assertNotProfileUntouched' 'the refused directory was not touched')" 'it changed'
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
set +e
|
|
311
|
+
OUT=$("$RESTORE" --profile / --state-dir "$STATE" 2>&1)
|
|
312
|
+
CODE=$?
|
|
313
|
+
set -e
|
|
314
|
+
refused "$(t 'drill.assertDriveRootRefused' 'the filesystem root is refused')" "$OUT" "$CODE" 'drill.keywords.driveRoot'
|
|
315
|
+
|
|
316
|
+
SYSTEM_DIR=/etc
|
|
317
|
+
set +e
|
|
318
|
+
OUT=$("$RESTORE" --profile "$SYSTEM_DIR" --state-dir "$STATE" 2>&1)
|
|
319
|
+
CODE=$?
|
|
320
|
+
set -e
|
|
321
|
+
refused "$(fmt "$(t 'drill.assertSystemDirRefused' 'the system directory {0} is refused')" "$SYSTEM_DIR")" "$OUT" "$CODE" 'drill.keywords.systemDir'
|
|
322
|
+
if [ -d "$SYSTEM_DIR" ]; then
|
|
323
|
+
assert_pass "$(fmt "$(t 'drill.assertSystemDirIntact' 'the system directory {0} is untouched')" "$SYSTEM_DIR")"
|
|
324
|
+
else
|
|
325
|
+
assert_fail "$(fmt "$(t 'drill.assertSystemDirIntact' 'the system directory {0} is untouched')" "$SYSTEM_DIR")" 'it is gone'
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
MISSING="$DRILL_DIR/does-not-exist"
|
|
329
|
+
set +e
|
|
330
|
+
OUT=$("$RESTORE" --profile "$MISSING" --state-dir "$STATE" 2>&1)
|
|
331
|
+
CODE=$?
|
|
332
|
+
set -e
|
|
333
|
+
refused "$(t 'drill.assertMissingDirRefused' 'a target profile directory that does not exist is refused')" "$OUT" "$CODE" 'drill.keywords.missingDir'
|
|
334
|
+
if [ ! -e "$MISSING" ]; then
|
|
335
|
+
assert_pass "$(t 'drill.assertMissingDirNotCreated' 'the missing directory was not created')"
|
|
336
|
+
else
|
|
337
|
+
assert_fail "$(t 'drill.assertMissingDirNotCreated' 'the missing directory was not created')" 'it is there'
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
# --- "the profile says one thing, the disk another": the restore must fail ---
|
|
341
|
+
LYING_PROFILE="$DRILL_DIR/lying"
|
|
342
|
+
LYING_SNAPSHOT="$BACKUPS/20260303-000000"
|
|
343
|
+
mkdir -p -- "$LYING_PROFILE" "$LYING_SNAPSHOT/config"
|
|
344
|
+
cat > "$LYING_PROFILE/package.json" <<'JSON'
|
|
345
|
+
{
|
|
346
|
+
"name": "dsh-profile",
|
|
347
|
+
"dsh": {
|
|
348
|
+
"profile": {
|
|
349
|
+
"bundles": [
|
|
350
|
+
"dsh-autorestart"
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
JSON
|
|
356
|
+
cp "$LYING_PROFILE/package.json" "$LYING_SNAPSHOT/config/package.json"
|
|
357
|
+
cat > "$LYING_SNAPSHOT/manifest.json" <<'JSON'
|
|
358
|
+
{
|
|
359
|
+
"at": 1772668800000,
|
|
360
|
+
"atLocal": "2026-03-03T00:00:00",
|
|
361
|
+
"includePlugins": false
|
|
362
|
+
}
|
|
363
|
+
JSON
|
|
364
|
+
set +e
|
|
365
|
+
OUT=$("$RESTORE" --profile "$LYING_PROFILE" --snapshot "$LYING_SNAPSHOT" --state-dir "$STATE" 2>&1)
|
|
366
|
+
CODE=$?
|
|
367
|
+
set -e
|
|
368
|
+
if [ "$CODE" -ne 0 ]; then
|
|
369
|
+
assert_pass "$(t 'drill.assertLyingRefused' 'the profile declares the plugin but does not have it -> the restore must fail')"
|
|
370
|
+
else
|
|
371
|
+
assert_fail "$(t 'drill.assertLyingRefused' 'the profile declares the plugin but does not have it -> the restore must fail')" 'it exited with 0'
|
|
372
|
+
fi
|
|
373
|
+
LYING_BROKEN=$(find "$DRILL_DIR" -maxdepth 1 -name 'lying.broken-*' 2>/dev/null | head -n 1)
|
|
374
|
+
if [ -n "$LYING_BROKEN" ]; then
|
|
375
|
+
assert_pass "$(t 'drill.assertLyingBrokenHint' 'the failing restore says the broken directory is still there')"
|
|
376
|
+
else
|
|
377
|
+
assert_fail "$(t 'drill.assertLyingBrokenHint' 'the failing restore says the broken directory is still there')" 'there is none'
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# --- the scripts are pure ASCII ---
|
|
381
|
+
for script in "$RESTORE" "$DISABLE"; do
|
|
382
|
+
if [ ! -f "$script" ]; then
|
|
383
|
+
continue
|
|
384
|
+
fi
|
|
385
|
+
if LC_ALL=C grep -q '[^ -~]' "$script"; then
|
|
386
|
+
assert_fail "$(fmt 'the script {0} is pure ASCII' "$(basename "$script")")" 'it holds a non-ASCII byte'
|
|
387
|
+
else
|
|
388
|
+
assert_pass "$(fmt 'the script {0} is pure ASCII' "$(basename "$script")")"
|
|
389
|
+
fi
|
|
390
|
+
done
|
|
391
|
+
|
|
392
|
+
# --- the emergency switch ---
|
|
393
|
+
if [ -f "$DISABLE" ]; then
|
|
394
|
+
EM_OUT=$("$DISABLE" --package "$PROFILE/package.json" --action disable 2>&1) || true
|
|
395
|
+
if grep -q "$PACKAGE_NAME" "$PROFILE/package.json" 2>/dev/null; then
|
|
396
|
+
assert_fail 'the emergency switch takes the plugin out of the bundles' 'it is still there'
|
|
397
|
+
else
|
|
398
|
+
assert_pass 'the emergency switch takes the plugin out of the bundles'
|
|
399
|
+
fi
|
|
400
|
+
if ls "$PROFILE"/package.json.bak-emergency-* >/dev/null 2>&1; then
|
|
401
|
+
assert_pass 'the emergency switch leaves a backup behind'
|
|
402
|
+
else
|
|
403
|
+
assert_fail 'the emergency switch leaves a backup behind' 'there is none'
|
|
404
|
+
fi
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
# --- only the temp directory was touched ---
|
|
408
|
+
if [ -n "$(find "$DRILL_DIR" -maxdepth 1 -name 'profile.broken-*' 2>/dev/null)" ]; then
|
|
409
|
+
assert_pass "$(t 'drill.assertOnlyTemp' 'the drill only touched its own temporary directory')"
|
|
410
|
+
else
|
|
411
|
+
assert_fail "$(t 'drill.assertOnlyTemp' 'the drill only touched its own temporary directory')" 'nothing was touched'
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
# --- cleanup ---
|
|
415
|
+
rm -rf -- "$DRILL_DIR"
|
|
416
|
+
if [ -e "$DRILL_DIR" ]; then
|
|
417
|
+
assert_fail "$(t 'drill.notCleaned' 'the temporary directory was not cleaned up, please delete it yourself: {0}')" "$DRILL_DIR"
|
|
418
|
+
else
|
|
419
|
+
assert_pass "$(t 'drill.cleaned' 'the temporary directory is clean.')"
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
if [ "$FAILED" -eq 0 ]; then
|
|
423
|
+
say "$(t 'drill.passed' 'the drill passed: broke it on purpose -> restored -> checked -> cleaned up.')"
|
|
424
|
+
exit 0
|
|
425
|
+
fi
|
|
426
|
+
say "$(fmt "$(t 'drill.failed' 'the drill failed: {0} assertions did not pass.')" "$FAILED")"
|
|
427
|
+
exit 1
|