unbound-cli 1.8.0 → 1.9.0
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/package.json +1 -1
- package/src/commands/setup.js +45 -0
- package/test/setup-args.test.js +56 -1
package/package.json
CHANGED
package/src/commands/setup.js
CHANGED
|
@@ -448,6 +448,45 @@ function removeBinaryInstall({
|
|
|
448
448
|
return { removed, failed };
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
+
|
|
452
|
+
// Run the binary's own `unbound-hook clear` to deregister managed configs
|
|
453
|
+
// (strip our hooks from shared configs, remove our scripts + per-tool logs)
|
|
454
|
+
// BEFORE the install tree is deleted. Essential on binary-only machines that
|
|
455
|
+
// have no python3 to run the downloaded per-tool clears, and authoritative for
|
|
456
|
+
// binary installs generally — it reuses the same clear_setup() as the python
|
|
457
|
+
// clears. Fail-open: never throws; returns { ran, ok }. `binaryPath`/`runner`
|
|
458
|
+
// are overridable so tests stay hermetic.
|
|
459
|
+
function runBinaryClear({
|
|
460
|
+
binaryPath = path.join(BINARY_INSTALL_PATHS.optDir, 'current', 'unbound-hook', 'unbound-hook'),
|
|
461
|
+
runner = spawnSync,
|
|
462
|
+
} = {}) {
|
|
463
|
+
// lstatSync (not existsSync, which follows symlinks) so a present-but-broken
|
|
464
|
+
// `current/...` symlink is still detected — we attempt the clear and surface
|
|
465
|
+
// the failure instead of silently skipping it. A truly absent path (python-only
|
|
466
|
+
// install) throws → no-op.
|
|
467
|
+
let present = false;
|
|
468
|
+
try { fs.lstatSync(binaryPath); present = true; } catch (e) { present = false; }
|
|
469
|
+
if (!present) return { ran: false, ok: true };
|
|
470
|
+
try {
|
|
471
|
+
// timeout so a hung clear can never block nuke from removing the binary.
|
|
472
|
+
const r = runner(binaryPath, ['clear'], { stdio: 'pipe', timeout: 120000, killSignal: 'SIGKILL' });
|
|
473
|
+
if (r.error) {
|
|
474
|
+
const why = r.error.code === 'ETIMEDOUT' ? 'timed out (120s)' : r.error.message;
|
|
475
|
+
output.warn(`nuke: unbound-hook clear ${why}; continuing.`);
|
|
476
|
+
return { ran: true, ok: false };
|
|
477
|
+
}
|
|
478
|
+
if (r.status !== 0) {
|
|
479
|
+
const stderr = r.stderr ? r.stderr.toString().trim() : '';
|
|
480
|
+
output.warn(`nuke: unbound-hook clear exited ${r.status}: ${stderr || '(no stderr)'}`);
|
|
481
|
+
return { ran: true, ok: false };
|
|
482
|
+
}
|
|
483
|
+
return { ran: true, ok: true };
|
|
484
|
+
} catch (err) {
|
|
485
|
+
output.warn(`nuke: unbound-hook clear errored: ${err.message}`);
|
|
486
|
+
return { ran: true, ok: false };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
451
490
|
/**
|
|
452
491
|
* Returns true when the process has the privileges needed to touch system-level
|
|
453
492
|
* (MDM) configuration. On Windows, `net session` succeeds only when elevated, so
|
|
@@ -1047,8 +1086,13 @@ Examples:
|
|
|
1047
1086
|
// `_confirmProductionPaths` is the explicit "this is the real call site"
|
|
1048
1087
|
// ack — the function refuses production defaults without it as a guard
|
|
1049
1088
|
// against tests forgetting to override paths.
|
|
1089
|
+
// Deregister managed configs via the binary's own clear FIRST (works
|
|
1090
|
+
// without python3, which a binary-only machine lacks); reuses the same
|
|
1091
|
+
// clear_setup() as the python clears. Must precede the rm below.
|
|
1092
|
+
const binClear = runBinaryClear();
|
|
1050
1093
|
const { removed: binaryRemoved, failed: bf } = removeBinaryInstall({ _confirmProductionPaths: true });
|
|
1051
1094
|
binaryFailed = bf;
|
|
1095
|
+
if (binClear.ran && !binClear.ok) binaryFailed.push('unbound-hook clear');
|
|
1052
1096
|
if (binaryRemoved.length) {
|
|
1053
1097
|
output.info(`Removed binary install: ${binaryRemoved.join('; ')}.`);
|
|
1054
1098
|
}
|
|
@@ -1141,5 +1185,6 @@ module.exports = {
|
|
|
1141
1185
|
clearUnboundEnvsEverywhere,
|
|
1142
1186
|
NUKE_ENV_VARS,
|
|
1143
1187
|
removeBinaryInstall,
|
|
1188
|
+
runBinaryClear,
|
|
1144
1189
|
BINARY_INSTALL_PATHS,
|
|
1145
1190
|
};
|
package/test/setup-args.test.js
CHANGED
|
@@ -3,7 +3,7 @@ const assert = require('node:assert/strict');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { buildScriptArgs, scriptSupportsBackfill, resolveSetupAllTools, clearUnboundEnvsEverywhere, NUKE_ENV_VARS, removeBinaryInstall, BINARY_INSTALL_PATHS } = require('../src/commands/setup');
|
|
6
|
+
const { buildScriptArgs, scriptSupportsBackfill, resolveSetupAllTools, clearUnboundEnvsEverywhere, NUKE_ENV_VARS, removeBinaryInstall, runBinaryClear, BINARY_INSTALL_PATHS } = require('../src/commands/setup');
|
|
7
7
|
|
|
8
8
|
// shellEscape single-quotes every value, so a real key surfaces as
|
|
9
9
|
// --api-key '<key>' at the head of the argv tail.
|
|
@@ -396,3 +396,58 @@ test('removeBinaryInstall: refuses production-default paths without _confirmProd
|
|
|
396
396
|
test('BINARY_INSTALL_PATHS: is frozen', () => {
|
|
397
397
|
assert.ok(Object.isFrozen(BINARY_INSTALL_PATHS));
|
|
398
398
|
});
|
|
399
|
+
|
|
400
|
+
test('runBinaryClear: binary present — runs `<bin> clear` before removal, reports ok', () => {
|
|
401
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-binclear-'));
|
|
402
|
+
const bin = path.join(tmp, 'unbound-hook');
|
|
403
|
+
fs.writeFileSync(bin, '#');
|
|
404
|
+
const calls = [];
|
|
405
|
+
try {
|
|
406
|
+
const r = runBinaryClear({ binaryPath: bin, runner: (b, args, opts) => { calls.push([b, args, opts]); return { status: 0 }; } });
|
|
407
|
+
assert.deepEqual(r, { ran: true, ok: true });
|
|
408
|
+
assert.equal(calls.length, 1, 'binary clear should run exactly once');
|
|
409
|
+
assert.equal(calls[0][0], bin);
|
|
410
|
+
assert.deepEqual(calls[0][1], ['clear']);
|
|
411
|
+
assert.ok(calls[0][2].timeout > 0, 'clear must run with a timeout so a hang cannot block nuke');
|
|
412
|
+
} finally {
|
|
413
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('runBinaryClear: dangling binary symlink — still attempts (surfaces, not silently skipped)', () => {
|
|
418
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-binclear-'));
|
|
419
|
+
const link = path.join(tmp, 'unbound-hook');
|
|
420
|
+
fs.symlinkSync(path.join(tmp, 'missing-target'), link); // dangling: target absent
|
|
421
|
+
let called = false;
|
|
422
|
+
try {
|
|
423
|
+
const r = runBinaryClear({ binaryPath: link, runner: () => { called = true; return { error: { code: 'ENOENT', message: 'spawn ENOENT' } }; } });
|
|
424
|
+
assert.equal(called, true, 'a dangling symlink must still be attempted, not treated as absent');
|
|
425
|
+
assert.deepEqual(r, { ran: true, ok: false });
|
|
426
|
+
} finally {
|
|
427
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test('runBinaryClear: binary absent (python-only install) — no-op, runner never called', () => {
|
|
432
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-binclear-'));
|
|
433
|
+
let called = false;
|
|
434
|
+
try {
|
|
435
|
+
const r = runBinaryClear({ binaryPath: path.join(tmp, 'nope'), runner: () => { called = true; return { status: 0 }; } });
|
|
436
|
+
assert.deepEqual(r, { ran: false, ok: true });
|
|
437
|
+
assert.equal(called, false);
|
|
438
|
+
} finally {
|
|
439
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('runBinaryClear: clear exits non-zero — fail-open (ran:true, ok:false), never throws', () => {
|
|
444
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-binclear-'));
|
|
445
|
+
const bin = path.join(tmp, 'unbound-hook');
|
|
446
|
+
fs.writeFileSync(bin, '#');
|
|
447
|
+
try {
|
|
448
|
+
const r = runBinaryClear({ binaryPath: bin, runner: () => ({ status: 1, stderr: Buffer.from('boom') }) });
|
|
449
|
+
assert.deepEqual(r, { ran: true, ok: false });
|
|
450
|
+
} finally {
|
|
451
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
452
|
+
}
|
|
453
|
+
});
|