Skip to content

Commit 6bd402a

Browse files
Merge pull request #279 from community-scripts/fix/262
Fix stale LXC entries and improve orphaned script cleanup
2 parents 9fc61bb + bd3ca74 commit 6bd402a

2 files changed

Lines changed: 121 additions & 44 deletions

File tree

src/app/_components/InstalledScriptsTab.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,18 @@ export function InstalledScriptsTab() {
935935
>
936936
{showAutoDetectForm ? 'Cancel Auto-Detect' : '🔍 Auto-Detect LXC Containers (Must contain a tag with "community-script")'}
937937
</Button>
938+
<Button
939+
onClick={() => {
940+
cleanupRunRef.current = false; // Allow cleanup to run again
941+
void cleanupMutation.mutate();
942+
}}
943+
disabled={cleanupMutation.isPending}
944+
variant="outline"
945+
size="default"
946+
className="border-warning/30 text-warning hover:bg-warning/10"
947+
>
948+
{cleanupMutation.isPending ? '🧹 Cleaning up...' : '🧹 Cleanup Orphaned Scripts'}
949+
</Button>
938950
<Button
939951
onClick={() => {
940952
// Trigger status check by calling the mutation directly

src/server/api/routers/installedScripts.ts

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -887,77 +887,142 @@ export const installedScriptsRouter = createTRPCRouter({
887887
);
888888

889889

890+
// Group scripts by server to batch check containers
891+
const scriptsByServer = new Map<number, any[]>();
890892
for (const script of scriptsToCheck) {
893+
const scriptData = script as any;
894+
if (!scriptData.server_id) continue;
895+
896+
if (!scriptsByServer.has(scriptData.server_id)) {
897+
scriptsByServer.set(scriptData.server_id, []);
898+
}
899+
scriptsByServer.get(scriptData.server_id)!.push(scriptData);
900+
}
901+
902+
// Process each server
903+
for (const [serverId, serverScripts] of scriptsByServer.entries()) {
891904
try {
892-
const scriptData = script as any;
893-
const server = allServers.find((s: any) => s.id === scriptData.server_id);
905+
const server = allServers.find((s: any) => s.id === serverId);
894906
if (!server) {
895-
await db.deleteInstalledScript(Number(scriptData.id));
896-
deletedScripts.push(String(scriptData.script_name));
907+
// Server doesn't exist, delete all scripts for this server
908+
for (const scriptData of serverScripts) {
909+
await db.deleteInstalledScript(Number(scriptData.id));
910+
deletedScripts.push(String(scriptData.script_name));
911+
}
897912
continue;
898913
}
899914

900-
901915
// Test SSH connection
902-
903916
const connectionTest = await sshService.testSSHConnection(server as Server);
904917
if (!(connectionTest as any).success) {
918+
console.warn(`cleanupOrphanedScripts: SSH connection failed for server ${String((server as any).name)}, skipping ${serverScripts.length} scripts`);
905919
continue;
906920
}
907921

908-
// Check if the container config file still exists
909-
const checkCommand = `test -f "/etc/pve/lxc/${scriptData.container_id}.conf" && echo "exists" || echo "not_found"`;
922+
// Get all existing containers from pct list (more reliable than checking config files)
923+
const listCommand = 'pct list';
924+
let listOutput = '';
910925

911-
// Await full command completion to avoid early false negatives
912-
const containerExists = await new Promise<boolean>((resolve) => {
913-
let combinedOutput = '';
914-
let resolved = false;
915-
916-
const finish = () => {
917-
if (resolved) return;
918-
resolved = true;
919-
const out = combinedOutput.trim();
920-
if (out.includes('exists')) {
921-
resolve(true);
922-
} else if (out.includes('not_found')) {
923-
resolve(false);
924-
} else {
925-
// Unknown output; treat as not found but log for diagnostics
926-
console.warn(`cleanupOrphanedScripts: unexpected output for ${String(scriptData.script_name)} (${String(scriptData.container_id)}): ${out}`);
927-
resolve(false);
928-
}
929-
};
930-
931-
// Add a guard timeout so we don't hang indefinitely
932-
const timer = setTimeout(() => {
933-
console.warn(`cleanupOrphanedScripts: timeout while checking ${String(scriptData.script_name)} on server ${String((server as any).name)}`);
934-
finish();
935-
}, 15000);
926+
const existingContainerIds = await new Promise<Set<string>>((resolve, reject) => {
927+
const timeout = setTimeout(() => {
928+
console.warn(`cleanupOrphanedScripts: timeout while getting container list from server ${String((server as any).name)}`);
929+
resolve(new Set()); // Treat timeout as no containers found
930+
}, 20000);
936931

937932
void sshExecutionService.executeCommand(
938933
server as Server,
939-
checkCommand,
934+
listCommand,
940935
(data: string) => {
941-
combinedOutput += data;
936+
listOutput += data;
942937
},
943938
(error: string) => {
944-
combinedOutput += error;
939+
console.error(`cleanupOrphanedScripts: error getting container list from server ${String((server as any).name)}:`, error);
940+
clearTimeout(timeout);
941+
resolve(new Set()); // Treat error as no containers found
945942
},
946943
(_exitCode: number) => {
947-
clearTimeout(timer);
948-
finish();
944+
clearTimeout(timeout);
945+
946+
// Parse pct list output to extract container IDs
947+
const containerIds = new Set<string>();
948+
const lines = listOutput.split('\n').filter(line => line.trim());
949+
950+
for (const line of lines) {
951+
// pct list format: CTID Status Name
952+
// Skip header line if present
953+
if (line.includes('CTID') || line.includes('VMID')) continue;
954+
955+
const parts = line.trim().split(/\s+/);
956+
if (parts.length > 0) {
957+
const containerId = parts[0]?.trim();
958+
if (containerId && /^\d{3,4}$/.test(containerId)) {
959+
containerIds.add(containerId);
960+
}
961+
}
962+
}
963+
964+
resolve(containerIds);
949965
}
950966
);
951967
});
952968

953-
if (!containerExists) {
954-
await db.deleteInstalledScript(Number(scriptData.id));
955-
deletedScripts.push(String(scriptData.script_name));
956-
} else {
957-
}
969+
// Check each script against the list of existing containers
970+
for (const scriptData of serverScripts) {
971+
try {
972+
const containerId = String(scriptData.container_id).trim();
973+
974+
// Check if container exists in pct list
975+
if (!existingContainerIds.has(containerId)) {
976+
// Also verify config file doesn't exist as a double-check
977+
const checkCommand = `test -f "/etc/pve/lxc/${containerId}.conf" && echo "exists" || echo "not_found"`;
978+
979+
const configExists = await new Promise<boolean>((resolve) => {
980+
let combinedOutput = '';
981+
let resolved = false;
982+
983+
const finish = () => {
984+
if (resolved) return;
985+
resolved = true;
986+
const out = combinedOutput.trim();
987+
resolve(out.includes('exists'));
988+
};
989+
990+
const timer = setTimeout(() => {
991+
finish();
992+
}, 10000);
958993

994+
void sshExecutionService.executeCommand(
995+
server as Server,
996+
checkCommand,
997+
(data: string) => {
998+
combinedOutput += data;
999+
},
1000+
(_error: string) => {
1001+
// Ignore errors, just check output
1002+
},
1003+
(_exitCode: number) => {
1004+
clearTimeout(timer);
1005+
finish();
1006+
}
1007+
);
1008+
});
1009+
1010+
// If container is not in pct list AND config file doesn't exist, it's orphaned
1011+
if (!configExists) {
1012+
console.log(`cleanupOrphanedScripts: Removing orphaned script ${String(scriptData.script_name)} (container ${containerId}) from server ${String((server as any).name)}`);
1013+
await db.deleteInstalledScript(Number(scriptData.id));
1014+
deletedScripts.push(String(scriptData.script_name));
1015+
} else {
1016+
// Config exists but not in pct list - might be in a transitional state, log but don't delete
1017+
console.warn(`cleanupOrphanedScripts: Container ${containerId} (${String(scriptData.script_name)}) config exists but not in pct list - may be in transitional state`);
1018+
}
1019+
}
1020+
} catch (error) {
1021+
console.error(`cleanupOrphanedScripts: Error checking script ${String((scriptData as any).script_name)}:`, error);
1022+
}
1023+
}
9591024
} catch (error) {
960-
console.error(`Error checking script ${(script as any).script_name}:`, error);
1025+
console.error(`cleanupOrphanedScripts: Error processing server ${serverId}:`, error);
9611026
}
9621027
}
9631028

0 commit comments

Comments
 (0)