Fix #362: auto-detect race, VM shell path, UI hints

- Defer resolve in autoDetectLXCContainers (pct/qm list) so stdout is complete
- Pass containerType when opening shell; use qm terminal for VMs, pct enter for LXC
- Add UI hint for VM shell (serial console, Ctrl+O, serial port requirement)
- Rename auto-detect to Containers & VMs and update help text

Fixes #362
This commit is contained in:
Michel Rögl-Brunner
2026-01-29 14:12:49 +01:00
parent a34566651a
commit b5c6beafff
3 changed files with 38 additions and 23 deletions

View File

@@ -312,7 +312,7 @@ class ScriptExecutionHandler {
} else if (isUpdate && containerId) { } else if (isUpdate && containerId) {
await this.startUpdateExecution(ws, containerId, executionId, mode, server, backupStorage); await this.startUpdateExecution(ws, containerId, executionId, mode, server, backupStorage);
} else if (isShell && containerId) { } else if (isShell && containerId) {
await this.startShellExecution(ws, containerId, executionId, mode, server); await this.startShellExecution(ws, containerId, executionId, mode, server, containerType);
} else { } else {
await this.startScriptExecution(ws, scriptPath, executionId, mode, server, envVars); await this.startScriptExecution(ws, scriptPath, executionId, mode, server, envVars);
} }
@@ -1474,21 +1474,21 @@ class ScriptExecutionHandler {
* @param {string} executionId * @param {string} executionId
* @param {string} mode * @param {string} mode
* @param {ServerInfo|null} server * @param {ServerInfo|null} server
* @param {'lxc'|'vm'} [containerType='lxc']
*/ */
async startShellExecution(ws, containerId, executionId, mode = 'local', server = null) { async startShellExecution(ws, containerId, executionId, mode = 'local', server = null, containerType = 'lxc') {
try { try {
const typeLabel = containerType === 'vm' ? 'VM' : 'container';
// Send start message
this.sendMessage(ws, { this.sendMessage(ws, {
type: 'start', type: 'start',
data: `Starting shell session for container ${containerId}...`, data: `Starting shell session for ${typeLabel} ${containerId}...`,
timestamp: Date.now() timestamp: Date.now()
}); });
if (mode === 'ssh' && server) { if (mode === 'ssh' && server) {
await this.startSSHShellExecution(ws, containerId, executionId, server); await this.startSSHShellExecution(ws, containerId, executionId, server, containerType);
} else { } else {
await this.startLocalShellExecution(ws, containerId, executionId); await this.startLocalShellExecution(ws, containerId, executionId, containerType);
} }
} catch (error) { } catch (error) {
@@ -1505,12 +1505,12 @@ class ScriptExecutionHandler {
* @param {ExtendedWebSocket} ws * @param {ExtendedWebSocket} ws
* @param {string} containerId * @param {string} containerId
* @param {string} executionId * @param {string} executionId
* @param {'lxc'|'vm'} [containerType='lxc']
*/ */
async startLocalShellExecution(ws, containerId, executionId) { async startLocalShellExecution(ws, containerId, executionId, containerType = 'lxc') {
const { spawn } = await import('node-pty'); const { spawn } = await import('node-pty');
const shellCommand = containerType === 'vm' ? `qm terminal ${containerId}` : `pct enter ${containerId}`;
// Create a shell process that will run pct enter const childProcess = spawn('bash', ['-c', shellCommand], {
const childProcess = spawn('bash', ['-c', `pct enter ${containerId}`], {
name: 'xterm-color', name: 'xterm-color',
cols: 80, cols: 80,
rows: 24, rows: 24,
@@ -1553,14 +1553,15 @@ class ScriptExecutionHandler {
* @param {string} containerId * @param {string} containerId
* @param {string} executionId * @param {string} executionId
* @param {ServerInfo} server * @param {ServerInfo} server
* @param {'lxc'|'vm'} [containerType='lxc']
*/ */
async startSSHShellExecution(ws, containerId, executionId, server) { async startSSHShellExecution(ws, containerId, executionId, server, containerType = 'lxc') {
const sshService = getSSHExecutionService(); const sshService = getSSHExecutionService();
const shellCommand = containerType === 'vm' ? `qm terminal ${containerId}` : `pct enter ${containerId}`;
try { try {
const execution = await sshService.executeCommand( const execution = await sshService.executeCommand(
server, server,
`pct enter ${containerId}`, shellCommand,
/** @param {string} data */ /** @param {string} data */
(data) => { (data) => {
this.sendMessage(ws, { this.sendMessage(ws, {

View File

@@ -80,6 +80,7 @@ export function InstalledScriptsTab() {
id: number; id: number;
containerId: string; containerId: string;
server?: any; server?: any;
containerType?: 'lxc' | 'vm';
} | null>(null); } | null>(null);
const [showBackupPrompt, setShowBackupPrompt] = useState(false); const [showBackupPrompt, setShowBackupPrompt] = useState(false);
const [showStorageSelection, setShowStorageSelection] = useState(false); const [showStorageSelection, setShowStorageSelection] = useState(false);
@@ -1167,6 +1168,7 @@ export function InstalledScriptsTab() {
id: script.id, id: script.id,
containerId: script.container_id, containerId: script.container_id,
server: server, server: server,
containerType: script.is_vm ? 'vm' : 'lxc',
}); });
}; };
@@ -1452,6 +1454,13 @@ export function InstalledScriptsTab() {
{/* Shell Terminal */} {/* Shell Terminal */}
{openingShell && ( {openingShell && (
<div className="mb-8" data-terminal="shell"> <div className="mb-8" data-terminal="shell">
{openingShell.containerType === 'vm' && (
<p className="text-muted-foreground mb-2 text-sm">
VM shell uses the Proxmox serial console. The VM must have a
serial port configured (e.g. <code className="bg-muted rounded px-1">qm set {openingShell.containerId} -serial0 socket</code>).
Detach with <kbd className="bg-muted rounded px-1">Ctrl+O</kbd>.
</p>
)}
<Terminal <Terminal
scriptPath={`shell-${openingShell.containerId}`} scriptPath={`shell-${openingShell.containerId}`}
onClose={handleCloseShellTerminal} onClose={handleCloseShellTerminal}
@@ -1459,6 +1468,7 @@ export function InstalledScriptsTab() {
server={openingShell.server} server={openingShell.server}
isShell={true} isShell={true}
containerId={openingShell.containerId} containerId={openingShell.containerId}
containerType={openingShell.containerType}
/> />
</div> </div>
)} )}
@@ -1538,7 +1548,7 @@ export function InstalledScriptsTab() {
> >
{showAutoDetectForm {showAutoDetectForm
? "Cancel Auto-Detect" ? "Cancel Auto-Detect"
: '🔍 Auto-Detect LXC Containers (Must contain a tag with "community-script")'} : '🔍 Auto-Detect Containers & VMs (tag: community-script)'}
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
@@ -1764,12 +1774,11 @@ export function InstalledScriptsTab() {
</div> </div>
)} )}
{/* Auto-Detect LXC Containers Form */} {/* Auto-Detect Containers & VMs Form */}
{showAutoDetectForm && ( {showAutoDetectForm && (
<div className="bg-card border-border mb-6 rounded-lg border p-4 shadow-sm sm:p-6"> <div className="bg-card border-border mb-6 rounded-lg border p-4 shadow-sm sm:p-6">
<h3 className="text-foreground mb-4 text-lg font-semibold sm:mb-6"> <h3 className="text-foreground mb-4 text-lg font-semibold sm:mb-6">
Auto-Detect LXC Containers (Must contain a tag with Auto-Detect Containers &amp; VMs (tag: community-script)
&quot;community-script&quot;)
</h3> </h3>
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<div className="bg-muted/30 border-muted rounded-lg border p-4"> <div className="bg-muted/30 border-muted rounded-lg border p-4">
@@ -1795,12 +1804,12 @@ export function InstalledScriptsTab() {
<p>This feature will:</p> <p>This feature will:</p>
<ul className="mt-1 list-inside list-disc space-y-1"> <ul className="mt-1 list-inside list-disc space-y-1">
<li>Connect to the selected server via SSH</li> <li>Connect to the selected server via SSH</li>
<li>Scan all LXC config files in /etc/pve/lxc/</li> <li>Scan LXC configs in /etc/pve/lxc/ and VM configs in /etc/pve/qemu-server/</li>
<li> <li>
Find containers with &quot;community-script&quot; in Find containers and VMs with &quot;community-script&quot; in
their tags their tags
</li> </li>
<li>Extract the container ID and hostname</li> <li>Extract the container/VM ID and hostname or name</li>
<li>Add them as installed script entries</li> <li>Add them as installed script entries</li>
</ul> </ul>
</div> </div>
@@ -2302,6 +2311,11 @@ export function InstalledScriptsTab() {
"stopped" "stopped"
} }
className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20"
title={
script.is_vm
? "VM serial console (requires serial port; detach with Ctrl+O)"
: undefined
}
> >
Shell Shell
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -1060,7 +1060,7 @@ export const installedScriptsRouter = createTRPCRouter({
reject(new Error(`pct list failed: ${error}`)); reject(new Error(`pct list failed: ${error}`));
}, },
(_exitCode: number) => { (_exitCode: number) => {
resolve(); setImmediate(() => resolve());
} }
); );
}); });
@@ -1079,7 +1079,7 @@ export const installedScriptsRouter = createTRPCRouter({
reject(new Error(`qm list failed: ${error}`)); reject(new Error(`qm list failed: ${error}`));
}, },
(_exitCode: number) => { (_exitCode: number) => {
resolve(); setImmediate(() => resolve());
} }
); );
}); });