Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions src/app/_components/InstalledScriptsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function InstalledScriptsTab() {
} | null>(null);
const [controllingScriptId, setControllingScriptId] = useState<number | null>(null);
const scriptsRef = useRef<InstalledScript[]>([]);
const statusCheckTimeoutRef = useRef<NodeJS.Timeout | null>(null);

// Error modal state
const [errorModal, setErrorModal] = useState<{
Expand Down Expand Up @@ -312,17 +313,34 @@ export function InstalledScriptsTab() {

// Function to fetch container statuses - simplified to just check all servers
const fetchContainerStatuses = useCallback(() => {
const currentScripts = scriptsRef.current;
console.log('fetchContainerStatuses called, isPending:', containerStatusMutation.isPending);

// Get unique server IDs from scripts
const serverIds = [...new Set(currentScripts
.filter(script => script.server_id)
.map(script => script.server_id!))];

if (serverIds.length > 0) {
containerStatusMutation.mutate({ serverIds });
// Prevent multiple simultaneous status checks
if (containerStatusMutation.isPending) {
console.log('Status check already pending, skipping');
return;
}
}, [containerStatusMutation]);

// Clear any existing timeout
if (statusCheckTimeoutRef.current) {
clearTimeout(statusCheckTimeoutRef.current);
}

// Debounce status checks by 500ms
statusCheckTimeoutRef.current = setTimeout(() => {
const currentScripts = scriptsRef.current;

// Get unique server IDs from scripts
const serverIds = [...new Set(currentScripts
.filter(script => script.server_id)
.map(script => script.server_id!))];

console.log('Executing status check for server IDs:', serverIds);
if (serverIds.length > 0) {
containerStatusMutation.mutate({ serverIds });
}
}, 500);
}, []); // Remove containerStatusMutation from dependencies to prevent loops

// Run cleanup when component mounts and scripts are loaded (only once)
useEffect(() => {
Expand All @@ -335,9 +353,19 @@ export function InstalledScriptsTab() {

useEffect(() => {
if (scripts.length > 0) {
console.log('Status check triggered - scripts length:', scripts.length);
fetchContainerStatuses();
}
}, [scripts.length, fetchContainerStatuses]);
}, [scripts.length]); // Remove fetchContainerStatuses from dependencies

// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (statusCheckTimeoutRef.current) {
clearTimeout(statusCheckTimeoutRef.current);
}
};
}, []);

const scriptsWithStatus = scripts.map(script => ({
...script,
Expand Down
26 changes: 21 additions & 5 deletions src/app/_components/SSHKeyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,25 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
);

if (keyLine) {
const keyType = keyLine.includes('RSA') ? 'RSA' :
keyLine.includes('ED25519') ? 'ED25519' :
keyLine.includes('ECDSA') ? 'ECDSA' : 'Unknown';
let keyType = 'Unknown';

// Check for traditional PEM format keys
if (keyLine.includes('RSA')) {
keyType = 'RSA';
} else if (keyLine.includes('ED25519')) {
keyType = 'ED25519';
} else if (keyLine.includes('ECDSA')) {
keyType = 'ECDSA';
} else if (keyLine.includes('OPENSSH PRIVATE KEY')) {
// For OpenSSH format keys, try to detect type from the key content
// Look for common patterns in the base64 content
const base64Content = keyContent.replace(/-----BEGIN.*?-----/, '').replace(/-----END.*?-----/, '').replace(/\s/g, '');

// This is a heuristic - OpenSSH ED25519 keys typically start with specific patterns
// We'll default to "OpenSSH" for now since we can't reliably detect the type
keyType = 'OpenSSH';
}

return `${keyType} key (${keyContent.length} characters)`;
}

Expand Down Expand Up @@ -142,7 +158,7 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
<input
ref={fileInputRef}
type="file"
accept=".pem,.key,.id_rsa,.id_ed25519,.id_ecdsa"
accept=".pem,.key,.id_rsa,.id_ed25519,.id_ecdsa,ed25519,id_rsa,id_ed25519,id_ecdsa,*"
onChange={handleFileSelect}
className="hidden"
disabled={disabled}
Expand All @@ -153,7 +169,7 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
Drag and drop your SSH private key here, or click to browse
</p>
<p className="text-xs text-muted-foreground">
Supported formats: RSA, ED25519, ECDSA (.pem, .key, .id_rsa, etc.)
Supported formats: RSA, ED25519, ECDSA (.pem, .key, .id_rsa, ed25519, etc.)
</p>
</div>
</div>
Expand Down
40 changes: 24 additions & 16 deletions src/server/api/routers/installedScripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,23 +551,31 @@ export const installedScriptsRouter = createTRPCRouter({
const listCommand = 'pct list';
let listOutput = '';

await new Promise<void>((resolve, reject) => {
void sshExecutionService.executeCommand(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
server as any,
listCommand,
(data: string) => {
listOutput += data;
},
(error: string) => {
console.error(`pct list error on server ${(server as any).name}:`, error);
reject(new Error(error));
},
(_exitCode: number) => {
resolve();
}
);
// Add timeout to prevent hanging connections
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('SSH command timeout after 30 seconds')), 30000);
});

await Promise.race([
new Promise<void>((resolve, reject) => {
void sshExecutionService.executeCommand(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
server as any,
listCommand,
(data: string) => {
listOutput += data;
},
(error: string) => {
console.error(`pct list error on server ${(server as any).name}:`, error);
reject(new Error(error));
},
(_exitCode: number) => {
resolve();
}
);
}),
timeoutPromise
]);

// Parse pct list output
const lines = listOutput.split('\n').filter(line => line.trim());
Expand Down
14 changes: 8 additions & 6 deletions src/server/ssh-execution-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ class SSHExecutionService {
const tempDir = mkdtempSync(join(tmpdir(), 'ssh-key-'));
const tempKeyPath = join(tempDir, 'private_key');

writeFileSync(tempKeyPath, ssh_key);
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
const normalizedKey = ssh_key.trimEnd() + '\n';
writeFileSync(tempKeyPath, normalizedKey);
chmodSync(tempKeyPath, 0o600); // Set proper permissions

return tempKeyPath;
Expand Down Expand Up @@ -295,7 +297,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
unlinkSync(tempDir);
rmdirSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}
Expand All @@ -314,7 +316,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
unlinkSync(tempDir);
rmdirSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}
Expand All @@ -328,7 +330,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
unlinkSync(tempDir);
rmdirSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}
Expand Down Expand Up @@ -383,7 +385,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
unlinkSync(tempDir);
rmdirSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}
Expand Down Expand Up @@ -414,7 +416,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
unlinkSync(tempDir);
rmdirSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}
Expand Down
4 changes: 3 additions & 1 deletion src/server/ssh-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,9 @@ expect {
tempKeyPath = join(tempDir, 'private_key');

// Write the private key to temporary file
writeFileSync(tempKeyPath, ssh_key);
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
const normalizedKey = ssh_key.trimEnd() + '\n';
writeFileSync(tempKeyPath, normalizedKey);
chmodSync(tempKeyPath, 0o600); // Set proper permissions

// Build SSH command
Expand Down