Fix stale LXC entries and improve orphaned script cleanup

- Improved cleanupOrphanedScripts to use pct list for more reliable container verification
- Added batch processing by server for better efficiency
- Added double-check with config file existence before deletion
- Added manual cleanup button in Installed Scripts tab for on-demand cleanup
- Improved error handling and logging throughout cleanup process
- Fixes issue where deleted containers (like Planka) were still showing in the UI
This commit is contained in:
Michel Roegl-Brunner
2025-11-07 12:47:19 +01:00
parent 9fc61bb416
commit bd3ca74175
2 changed files with 121 additions and 44 deletions

View File

@@ -935,6 +935,18 @@ export function InstalledScriptsTab() {
> >
{showAutoDetectForm ? 'Cancel Auto-Detect' : '🔍 Auto-Detect LXC Containers (Must contain a tag with "community-script")'} {showAutoDetectForm ? 'Cancel Auto-Detect' : '🔍 Auto-Detect LXC Containers (Must contain a tag with "community-script")'}
</Button> </Button>
<Button
onClick={() => {
cleanupRunRef.current = false; // Allow cleanup to run again
void cleanupMutation.mutate();
}}
disabled={cleanupMutation.isPending}
variant="outline"
size="default"
className="border-warning/30 text-warning hover:bg-warning/10"
>
{cleanupMutation.isPending ? '🧹 Cleaning up...' : '🧹 Cleanup Orphaned Scripts'}
</Button>
<Button <Button
onClick={() => { onClick={() => {
// Trigger status check by calling the mutation directly // Trigger status check by calling the mutation directly

View File

@@ -887,29 +887,96 @@ export const installedScriptsRouter = createTRPCRouter({
); );
// Group scripts by server to batch check containers
const scriptsByServer = new Map<number, any[]>();
for (const script of scriptsToCheck) { for (const script of scriptsToCheck) {
try {
const scriptData = script as any; const scriptData = script as any;
const server = allServers.find((s: any) => s.id === scriptData.server_id); if (!scriptData.server_id) continue;
if (!scriptsByServer.has(scriptData.server_id)) {
scriptsByServer.set(scriptData.server_id, []);
}
scriptsByServer.get(scriptData.server_id)!.push(scriptData);
}
// Process each server
for (const [serverId, serverScripts] of scriptsByServer.entries()) {
try {
const server = allServers.find((s: any) => s.id === serverId);
if (!server) { if (!server) {
// Server doesn't exist, delete all scripts for this server
for (const scriptData of serverScripts) {
await db.deleteInstalledScript(Number(scriptData.id)); await db.deleteInstalledScript(Number(scriptData.id));
deletedScripts.push(String(scriptData.script_name)); deletedScripts.push(String(scriptData.script_name));
}
continue; continue;
} }
// Test SSH connection // Test SSH connection
const connectionTest = await sshService.testSSHConnection(server as Server); const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) { if (!(connectionTest as any).success) {
console.warn(`cleanupOrphanedScripts: SSH connection failed for server ${String((server as any).name)}, skipping ${serverScripts.length} scripts`);
continue; continue;
} }
// Check if the container config file still exists // Get all existing containers from pct list (more reliable than checking config files)
const checkCommand = `test -f "/etc/pve/lxc/${scriptData.container_id}.conf" && echo "exists" || echo "not_found"`; const listCommand = 'pct list';
let listOutput = '';
// Await full command completion to avoid early false negatives const existingContainerIds = await new Promise<Set<string>>((resolve, reject) => {
const containerExists = await new Promise<boolean>((resolve) => { const timeout = setTimeout(() => {
console.warn(`cleanupOrphanedScripts: timeout while getting container list from server ${String((server as any).name)}`);
resolve(new Set()); // Treat timeout as no containers found
}, 20000);
void sshExecutionService.executeCommand(
server as Server,
listCommand,
(data: string) => {
listOutput += data;
},
(error: string) => {
console.error(`cleanupOrphanedScripts: error getting container list from server ${String((server as any).name)}:`, error);
clearTimeout(timeout);
resolve(new Set()); // Treat error as no containers found
},
(_exitCode: number) => {
clearTimeout(timeout);
// Parse pct list output to extract container IDs
const containerIds = new Set<string>();
const lines = listOutput.split('\n').filter(line => line.trim());
for (const line of lines) {
// pct list format: CTID Status Name
// Skip header line if present
if (line.includes('CTID') || line.includes('VMID')) continue;
const parts = line.trim().split(/\s+/);
if (parts.length > 0) {
const containerId = parts[0]?.trim();
if (containerId && /^\d{3,4}$/.test(containerId)) {
containerIds.add(containerId);
}
}
}
resolve(containerIds);
}
);
});
// Check each script against the list of existing containers
for (const scriptData of serverScripts) {
try {
const containerId = String(scriptData.container_id).trim();
// Check if container exists in pct list
if (!existingContainerIds.has(containerId)) {
// Also verify config file doesn't exist as a double-check
const checkCommand = `test -f "/etc/pve/lxc/${containerId}.conf" && echo "exists" || echo "not_found"`;
const configExists = await new Promise<boolean>((resolve) => {
let combinedOutput = ''; let combinedOutput = '';
let resolved = false; let resolved = false;
@@ -917,22 +984,12 @@ export const installedScriptsRouter = createTRPCRouter({
if (resolved) return; if (resolved) return;
resolved = true; resolved = true;
const out = combinedOutput.trim(); const out = combinedOutput.trim();
if (out.includes('exists')) { resolve(out.includes('exists'));
resolve(true);
} else if (out.includes('not_found')) {
resolve(false);
} else {
// Unknown output; treat as not found but log for diagnostics
console.warn(`cleanupOrphanedScripts: unexpected output for ${String(scriptData.script_name)} (${String(scriptData.container_id)}): ${out}`);
resolve(false);
}
}; };
// Add a guard timeout so we don't hang indefinitely
const timer = setTimeout(() => { const timer = setTimeout(() => {
console.warn(`cleanupOrphanedScripts: timeout while checking ${String(scriptData.script_name)} on server ${String((server as any).name)}`);
finish(); finish();
}, 15000); }, 10000);
void sshExecutionService.executeCommand( void sshExecutionService.executeCommand(
server as Server, server as Server,
@@ -940,8 +997,8 @@ export const installedScriptsRouter = createTRPCRouter({
(data: string) => { (data: string) => {
combinedOutput += data; combinedOutput += data;
}, },
(error: string) => { (_error: string) => {
combinedOutput += error; // Ignore errors, just check output
}, },
(_exitCode: number) => { (_exitCode: number) => {
clearTimeout(timer); clearTimeout(timer);
@@ -950,14 +1007,22 @@ export const installedScriptsRouter = createTRPCRouter({
); );
}); });
if (!containerExists) { // If container is not in pct list AND config file doesn't exist, it's orphaned
if (!configExists) {
console.log(`cleanupOrphanedScripts: Removing orphaned script ${String(scriptData.script_name)} (container ${containerId}) from server ${String((server as any).name)}`);
await db.deleteInstalledScript(Number(scriptData.id)); await db.deleteInstalledScript(Number(scriptData.id));
deletedScripts.push(String(scriptData.script_name)); deletedScripts.push(String(scriptData.script_name));
} else { } else {
// Config exists but not in pct list - might be in a transitional state, log but don't delete
console.warn(`cleanupOrphanedScripts: Container ${containerId} (${String(scriptData.script_name)}) config exists but not in pct list - may be in transitional state`);
}
} }
} catch (error) { } catch (error) {
console.error(`Error checking script ${(script as any).script_name}:`, error); console.error(`cleanupOrphanedScripts: Error checking script ${String((scriptData as any).script_name)}:`, error);
}
}
} catch (error) {
console.error(`cleanupOrphanedScripts: Error processing server ${serverId}:`, error);
} }
} }