fix(fpagent): sweep scratch left by a SIGKILLed installer run
The EXIT trap cannot run on SIGKILL, so a killed run could leave its fpagent-install.* temp dir behind. Sweep those before any network work, scoped to our prefix and this user and only dirs older than a day so a concurrent install is never touched.
This commit is contained in:
@@ -51,10 +51,18 @@ for arg in "$@"; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
for cmd in curl tar sha256sum python3; do
|
for cmd in curl tar sha256sum python3 find; do
|
||||||
command -v "$cmd" >/dev/null 2>&1 || die "$cmd is required but not installed"
|
command -v "$cmd" >/dev/null 2>&1 || die "$cmd is required but not installed"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
tmpbase="${TMPDIR:-/tmp}"
|
||||||
|
# Sweep scratch left by an earlier run that was SIGKILLed: the EXIT trap cannot
|
||||||
|
# run then (that is the one signal it cannot catch). Scoped to our own prefix,
|
||||||
|
# this user, and dirs older than a day, so a concurrent install is never touched.
|
||||||
|
# Runs before any network work so cleanup is not skipped when the fetch fails.
|
||||||
|
find "$tmpbase" -maxdepth 1 -mindepth 1 -type d -uid "$(id -u)" \
|
||||||
|
-name 'fpagent-install.*' -mtime +0 -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
# Offline bundles ship the libfprint .deb closure for a specific Ubuntu
|
# Offline bundles ship the libfprint .deb closure for a specific Ubuntu
|
||||||
# release; prefer the one matching this machine, else fall back to the generic
|
# release; prefer the one matching this machine, else fall back to the generic
|
||||||
# online bundle (whose installer apt-get's the dependency).
|
# online bundle (whose installer apt-get's the dependency).
|
||||||
@@ -85,7 +93,7 @@ asset="${readout#*$'\t'}"
|
|||||||
say " release ${tag}"
|
say " release ${tag}"
|
||||||
say " bundle ${asset}"
|
say " bundle ${asset}"
|
||||||
|
|
||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d "$tmpbase/fpagent-install.XXXXXXXX")"
|
||||||
trap 'rm -rf "$tmp"' EXIT
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
get() { curl -fsSL "$1" -o "$2" || die "download failed: $1"; }
|
get() { curl -fsSL "$1" -o "$2" || die "download failed: $1"; }
|
||||||
|
|||||||
@@ -4,11 +4,21 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { createApp, FPAGENT_REPO } from '../server.js';
|
import { createApp, FPAGENT_REPO } from '../server.js';
|
||||||
import { startServer, stopServer, urlOf } from './helpers.js';
|
import { startServer, stopServer, urlOf } from './helpers.js';
|
||||||
|
|
||||||
|
const SCRIPT = path.join(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
'..',
|
||||||
|
'public',
|
||||||
|
'fpagent',
|
||||||
|
'install.sh'
|
||||||
|
);
|
||||||
|
|
||||||
const TOKEN = 'release-install-secret';
|
const TOKEN = 'release-install-secret';
|
||||||
const TAG = 'v9.9.9';
|
const TAG = 'v9.9.9';
|
||||||
const ARCHIVE = 'fpagent-deadbee-linux-x86_64.tar.gz';
|
const ARCHIVE = 'fpagent-deadbee-linux-x86_64.tar.gz';
|
||||||
@@ -150,12 +160,37 @@ test('install.sh is served publicly at /fpagent/install.sh', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('install.sh is valid bash', () => {
|
test('install.sh is valid bash', () => {
|
||||||
const script = path.join(
|
execFileSync('bash', ['-n', SCRIPT], { stdio: 'pipe' });
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
});
|
||||||
'..',
|
|
||||||
'public',
|
test('install.sh sweeps only its own stale scratch dirs', () => {
|
||||||
'fpagent',
|
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'fpagent-sweep-test-'));
|
||||||
'install.sh'
|
const stale = path.join(base, 'fpagent-install.oldoldold');
|
||||||
);
|
const fresh = path.join(base, 'fpagent-install.freshfresh');
|
||||||
execFileSync('bash', ['-n', script], { stdio: 'pipe' });
|
const unrelated = path.join(base, 'someone-else.oldold');
|
||||||
|
for (const dir of [stale, fresh, unrelated]) fs.mkdirSync(dir);
|
||||||
|
const old = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
|
||||||
|
fs.utimesSync(stale, old, old);
|
||||||
|
fs.utimesSync(unrelated, old, old);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// The base URL is unreachable, so the run fails after the sweep: enough to
|
||||||
|
// exercise the cleanup without downloading anything.
|
||||||
|
let failed = false;
|
||||||
|
try {
|
||||||
|
execFileSync('bash', [SCRIPT], {
|
||||||
|
stdio: 'pipe',
|
||||||
|
env: { ...process.env, TMPDIR: base, FPAGENT_BASE_URL: 'http://127.0.0.1:9' },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
assert.equal(failed, true, 'the installer should fail on the unreachable base');
|
||||||
|
|
||||||
|
assert.equal(fs.existsSync(stale), false, 'stale fpagent scratch should be removed');
|
||||||
|
assert.equal(fs.existsSync(fresh), true, 'fresh (possibly concurrent) scratch should survive');
|
||||||
|
assert.equal(fs.existsSync(unrelated), true, 'another prefix should never be touched');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user