Drift now covers all three destinations
This commit is contained in:
@@ -1,17 +1,22 @@
|
|||||||
import { readdir, readFile, rm, stat } from 'node:fs/promises';
|
import { readdir, readFile, rm, stat } from 'node:fs/promises';
|
||||||
import { join, posix, relative, sep } from 'node:path';
|
import { join, posix, relative, sep } from 'node:path';
|
||||||
import { destOf } from '../config.js';
|
import { destOfKind, type ArtifactKind } from '../config.js';
|
||||||
import { db } from '../db/index.js';
|
import { db } from '../db/index.js';
|
||||||
import { emit } from '../pipeline/bus.js';
|
import { emit } from '../pipeline/bus.js';
|
||||||
import { sha256Buffer } from './files.js';
|
import { sha256Buffer } from './files.js';
|
||||||
import { listServerScripts, removeScript } from './inventory.js';
|
import { listServerScripts, removeScript } from './inventory.js';
|
||||||
import { allServers, listServers } from './servers.js';
|
import { allServers, listServers } from './servers.js';
|
||||||
|
|
||||||
|
// Destinations drift scans, in a stable order so the UI shows scripts first.
|
||||||
|
// Baseline shares the config destination — one physical dir, two logical kinds.
|
||||||
|
const DRIFT_DESTS: ReadonlyArray<ArtifactKind> = ['script', 'config', 'plugin'];
|
||||||
|
|
||||||
export interface DriftEntry {
|
export interface DriftEntry {
|
||||||
server: string;
|
server: string;
|
||||||
relPath: string;
|
relPath: string;
|
||||||
size: number;
|
size: number;
|
||||||
sha256: string;
|
sha256: string;
|
||||||
|
kind: ArtifactKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPosix(p: string): string {
|
function toPosix(p: string): string {
|
||||||
@@ -38,28 +43,35 @@ async function walkFiles(root: string): Promise<string[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan one server's sp/ tree and return every file that isn't tracked in
|
* Scan every managed destination tree on a server (scripts, configs, plugins)
|
||||||
* server_scripts. This includes user-placed scripts and leftovers from
|
* and return every file that isn't tracked in server_scripts. Beta artifacts
|
||||||
* before Electric Cherry started tracking inventory. Beta artifacts
|
* (-beta.gsc, .OLD, -beta.dll, composed dedicated_sp.cfg) are tracked while
|
||||||
* (-beta.gsc, .OLD) are tracked while a beta is active so they don't
|
* a beta is active so they don't appear as drift.
|
||||||
* appear as drift.
|
*
|
||||||
|
* The inventory table (server_scripts) keys on (server, rel_path) with no
|
||||||
|
* destination discriminator. Rel_paths across destinations don't collide in
|
||||||
|
* practice (`.gsc` for scripts, `.cfg` for configs, `.dll` for plugins), so
|
||||||
|
* a shared tracked-set is safe.
|
||||||
*/
|
*/
|
||||||
export async function scanServerDrift(server: string): Promise<DriftEntry[]> {
|
export async function scanServerDrift(server: string): Promise<DriftEntry[]> {
|
||||||
const dest = destOf(server);
|
|
||||||
const onDisk = await walkFiles(dest);
|
|
||||||
if (onDisk.length === 0) return [];
|
|
||||||
const tracked = new Set(listServerScripts(server).map((r) => r.rel_path));
|
const tracked = new Set(listServerScripts(server).map((r) => r.rel_path));
|
||||||
|
|
||||||
const drift: DriftEntry[] = [];
|
const drift: DriftEntry[] = [];
|
||||||
for (const relPath of onDisk) {
|
|
||||||
if (tracked.has(relPath)) continue;
|
for (const kind of DRIFT_DESTS) {
|
||||||
const abs = join(dest, relPath);
|
const dest = destOfKind(server, kind);
|
||||||
try {
|
const onDisk = await walkFiles(dest);
|
||||||
const st = await stat(abs);
|
if (onDisk.length === 0) continue;
|
||||||
const buf = await readFile(abs);
|
|
||||||
drift.push({ server, relPath, size: st.size, sha256: sha256Buffer(buf) });
|
for (const relPath of onDisk) {
|
||||||
} catch {
|
if (tracked.has(relPath)) continue;
|
||||||
/* file disappeared between walk and stat — skip */
|
const abs = join(dest, relPath);
|
||||||
|
try {
|
||||||
|
const st = await stat(abs);
|
||||||
|
const buf = await readFile(abs);
|
||||||
|
drift.push({ server, relPath, size: st.size, sha256: sha256Buffer(buf), kind });
|
||||||
|
} catch {
|
||||||
|
/* file disappeared between walk and stat — skip */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return drift;
|
return drift;
|
||||||
@@ -157,8 +169,8 @@ export async function runDriftDeployment(input: RunDriftInput = {}): Promise<Run
|
|||||||
const affected: string[] = [];
|
const affected: string[] = [];
|
||||||
for (const s of perServer) {
|
for (const s of perServer) {
|
||||||
if (s.entries.length === 0) continue;
|
if (s.entries.length === 0) continue;
|
||||||
const dest = destOf(s.server);
|
|
||||||
for (const entry of s.entries) {
|
for (const entry of s.entries) {
|
||||||
|
const dest = destOfKind(s.server, entry.kind);
|
||||||
const abs = join(dest, entry.relPath);
|
const abs = join(dest, entry.relPath);
|
||||||
try {
|
try {
|
||||||
const content = await readFile(abs);
|
const content = await readFile(abs);
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { kick } from '$lib/server/pipeline/worker.js';
|
|||||||
// GET returns the current drift plan for the whole fleet without touching disk.
|
// GET returns the current drift plan for the whole fleet without touching disk.
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const entries = await scanFleetDrift();
|
const entries = await scanFleetDrift();
|
||||||
const byServer = new Map<string, Array<{ relPath: string; size: number; sha256: string }>>();
|
const byServer = new Map<string, Array<{ relPath: string; size: number; sha256: string; kind: string }>>();
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
const arr = byServer.get(e.server) ?? [];
|
const arr = byServer.get(e.server) ?? [];
|
||||||
arr.push({ relPath: e.relPath, size: e.size, sha256: e.sha256 });
|
arr.push({ relPath: e.relPath, size: e.size, sha256: e.sha256, kind: e.kind });
|
||||||
byServer.set(e.server, arr);
|
byServer.set(e.server, arr);
|
||||||
}
|
}
|
||||||
return json({
|
return json({
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mkdir, writeFile } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||||
|
|
||||||
|
let fixture: Fixture;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
fixture = await newFixture(['t5_kino_1']);
|
||||||
|
delete process.env.KOMODO_ADDRESS;
|
||||||
|
delete process.env.RCON_PASSWORD;
|
||||||
|
// Ensure the three per-kind destinations exist under the fixture tree.
|
||||||
|
await mkdir(join(fixture.t5, 't5_kino_1', 'runtime/plutonium/storage/t5/raw/scripts/sp'), { recursive: true });
|
||||||
|
await mkdir(join(fixture.t5, 't5_kino_1', 'configs'), { recursive: true });
|
||||||
|
await mkdir(join(fixture.t5, 't5_kino_1', 'runtime/plutonium/plugins'), { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await fixture.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('drift scanning', () => {
|
||||||
|
it('detects untracked files across script, config, and plugin destinations', async () => {
|
||||||
|
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||||
|
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
|
||||||
|
await seedServerRegistry();
|
||||||
|
|
||||||
|
// Drop one stray file in each managed destination.
|
||||||
|
const server = 't5_kino_1';
|
||||||
|
await writeFile(
|
||||||
|
join(fixture.t5, server, 'runtime/plutonium/storage/t5/raw/scripts/sp', 'stray.gsc'),
|
||||||
|
'main(){}\n'
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
join(fixture.t5, server, 'configs', 'stray.cfg'),
|
||||||
|
'set foo "bar"\n'
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
join(fixture.t5, server, 'runtime/plutonium/plugins', 'stray.dll'),
|
||||||
|
Buffer.from([0xde, 0xad])
|
||||||
|
);
|
||||||
|
|
||||||
|
const drift = await scanServerDrift(server);
|
||||||
|
const byKind = Object.fromEntries(drift.map((d) => [d.kind, d.relPath]));
|
||||||
|
expect(byKind.script).toBe('stray.gsc');
|
||||||
|
expect(byKind.config).toBe('stray.cfg');
|
||||||
|
expect(byKind.plugin).toBe('stray.dll');
|
||||||
|
expect(drift.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user