fix(launcher): address code review findings in stats sync

- merge-catalog: stop dropping anilist_id on every new anime insert
  (bun:sqlite .get() returns null, so the old !== undefined guard always
  fired); the guard was dead anyway since a colliding id is caught earlier
- sync-command: throw instead of fail() inside runHostSync so the finally
  block runs and temp dirs holding snapshot data are cleaned up on failure
- ssh: shell-quote the user-supplied --remote-cmd in the probe, and reject
  option-like (-prefixed) SSH hosts that ssh/scp would parse as flags
- sync-db: close the remote handle if opening the local DB throws
- sync-shared: treat EPERM from process.kill(pid, 0) as alive, not dead
- cli-parser: trim the sync host before the mode-exclusivity check
- tests: cover anilist_id preservation, anilist-based anime matching, and
  the SSH host/shell-quote guards
This commit is contained in:
2026-07-08 23:51:46 -07:00
parent be31f96f02
commit 58d54311d8
8 changed files with 131 additions and 21 deletions
+14 -4
View File
@@ -9,7 +9,13 @@ import {
formatMergeSummary,
mergeSnapshotIntoDb,
} from '../sync/sync-db.js';
import { resolveRemoteSubminerCommand, runScp, runSsh, shellQuote } from '../sync/ssh.js';
import {
assertSafeSshHost,
resolveRemoteSubminerCommand,
runScp,
runSsh,
shellQuote,
} from '../sync/ssh.js';
import { resolvePathMaybe } from '../util.js';
import type { LauncherCommandContext } from './context.js';
@@ -66,6 +72,7 @@ function cleanupRemote(host: string, remoteTmpDir: string): void {
function runHostSync(context: LauncherCommandContext, dbPath: string): void {
const { args } = context;
const host = args.syncHost;
assertSafeSshHost(host);
ensureTrackerQuiescent(context, dbPath);
@@ -75,10 +82,13 @@ function runHostSync(context: LauncherCommandContext, dbPath: string): void {
const localTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-sync-'));
let remoteTmpDir = '';
try {
// Signal failures by throwing (not fail(), which exits synchronously and
// would skip the finally cleanup, leaking temp dirs holding snapshot data).
// main().catch() reports the message the same way fail() would.
const mktemp = runSsh(host, 'mktemp -d /tmp/subminer-sync.XXXXXX');
remoteTmpDir = mktemp.stdout.trim();
if (mktemp.status !== 0 || !remoteTmpDir.startsWith('/tmp/')) {
fail(`Could not create a temporary directory on ${host}.`);
throw new Error(`Could not create a temporary directory on ${host}.`);
}
const forceFlag = args.syncForce ? ' --force' : '';
@@ -94,7 +104,7 @@ function runHostSync(context: LauncherCommandContext, dbPath: string): void {
`${remoteCmd} sync --snapshot ${shellQuote(remoteSnapshot)}${forceFlag}`,
);
if (snapshotRun.status !== 0) {
fail(`Remote snapshot failed on ${host}.`);
throw new Error(`Remote snapshot failed on ${host}.`);
}
const pulledSnapshot = path.join(localTmpDir, 'remote.sqlite');
@@ -113,7 +123,7 @@ function runHostSync(context: LauncherCommandContext, dbPath: string): void {
);
process.stdout.write(mergeRun.stdout);
if (mergeRun.status !== 0) {
fail(
throw new Error(
`Remote merge failed on ${host}. The local database was updated; re-run "subminer sync ${host}" once the remote issue is fixed.`,
);
}