dsh-pve-vm 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/lib/config.js +14 -2
- package/lib/session.js +19 -0
- package/lib/ssh.js +19 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,43 @@
|
|
|
3
3
|
All notable changes to this plugin are documented here. The format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow [SemVer](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.1.1] — 2026-10-13
|
|
7
|
+
|
|
8
|
+
Linux guest support. Everything below was found by running the plugin against a real RedOS 8
|
|
9
|
+
guest with a MATE desktop, not by reading the code.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
* **Guest OS detection rejected almost every Linux distribution.** The guest agent reports `id`
|
|
14
|
+
from `/etc/os-release`, and only IDs containing `linux` were accepted — RedOS reports `redos`,
|
|
15
|
+
Debian `debian`, Ubuntu `ubuntu`. All of them landed in `other` and every `vm_*` tool answered
|
|
16
|
+
*running a shell command is not supported for guest OS «other»*, so the whole guest-agent half
|
|
17
|
+
was dead on Linux. Windows was unaffected (`mswindows`), which is why it went unnoticed.
|
|
18
|
+
Windows is now matched first, Linux by `id`/`name`/`pretty-name`, and a non-Windows guest that
|
|
19
|
+
answered `get-osinfo` with a non-empty `kernel-release` counts as Linux.
|
|
20
|
+
* **`vm_type_text` typed nothing on a GDM desktop.** The built-in typing script used
|
|
21
|
+
`$home/.Xauthority`, which does not exist when GDM owns the session — RedOS and RHEL keep the
|
|
22
|
+
file at `/run/user/<uid>/gdm/Xauthority`. `xdotool` then talked to the X server with no
|
|
23
|
+
authorization at all, failed with *No protocol specified*, and the text never appeared. The
|
|
24
|
+
script now tries `~/.Xauthority`, then the GDM path, then any `/run/user/<uid>/*/Xauthority`.
|
|
25
|
+
* **`vm_type_text` could not submit text.** `text=$(… | base64 -d)` — command substitution strips
|
|
26
|
+
trailing newlines, so text ending in `"\n"` was typed without pressing Enter and the shell
|
|
27
|
+
prompt never came back.
|
|
28
|
+
* **Console errors hid the actual cause.** When the node's `sshd` does not accept `AcceptEnv`,
|
|
29
|
+
`LC_PVE_TICKET` never arrives and `qm vncproxy` exits with a precise
|
|
30
|
+
*LC_PVE_TICKET not set, VNC proxy without password is forbidden* — but the RFB reader saw EOF
|
|
31
|
+
before ssh had finished exiting, so only *the VNC stream was closed by the node* surfaced. The
|
|
32
|
+
ssh stderr is now reported (after a short grace period), both during the handshake and when a
|
|
33
|
+
live session dies mid-way.
|
|
34
|
+
|
|
35
|
+
### Known limitations
|
|
36
|
+
|
|
37
|
+
* Proxmox applies a **default 5-second timeout to QMP commands** (`PVE/QMPClient.pm`), and
|
|
38
|
+
`guest-exec` waits for the guest agent to acknowledge. While a previous long command is still
|
|
39
|
+
running inside the guest that acknowledgement arrives too late and the call fails with
|
|
40
|
+
*qmp command 'guest-exec' failed - got timeout*. On an idle guest 10 out of 10 calls complete
|
|
41
|
+
in about a second. `vm_exec` is currently synchronous and does not retry.
|
|
42
|
+
|
|
6
43
|
## [0.1.0] — 2026-10-13
|
|
7
44
|
|
|
8
45
|
Initial public release.
|
package/lib/config.js
CHANGED
|
@@ -12,7 +12,11 @@ import z from '@deepseek-ai/schemastery';
|
|
|
12
12
|
/** Text input script inside a Linux guest (used by `vm_type_text`). */
|
|
13
13
|
export const DEFAULT_GUEST_TYPE_COMMAND = [
|
|
14
14
|
'set -e',
|
|
15
|
-
|
|
15
|
+
// `$(...)` strips trailing newlines, and without them the text cannot be
|
|
16
|
+
// "submitted" with Enter: `vm_type_text` with a trailing \"\\n\" silently typed
|
|
17
|
+
// the line and left the prompt waiting.
|
|
18
|
+
'text=$(printf %s "$1" | base64 -d; printf X)',
|
|
19
|
+
'text=${text%X}',
|
|
16
20
|
'uid=""; home=""; disp=""; xauth=""',
|
|
17
21
|
'for s in $(loginctl list-sessions --no-legend 2>/dev/null | awk \'{print $1}\'); do',
|
|
18
22
|
' [ "$(loginctl show-session "$s" -p Active --value 2>/dev/null)" = "yes" ] || continue',
|
|
@@ -22,7 +26,15 @@ export const DEFAULT_GUEST_TYPE_COMMAND = [
|
|
|
22
26
|
' [ -n "$disp" ] && break',
|
|
23
27
|
'done',
|
|
24
28
|
'[ -n "$disp" ] || disp=":0"',
|
|
25
|
-
'[ -n "$
|
|
29
|
+
'[ -n "$uid" ] || uid=$(id -u)',
|
|
30
|
+
// XAUTHORITY. GDM (RedOS 8, RHEL) keeps it in /run/user/<uid>/gdm/Xauthority,
|
|
31
|
+
// and ~/.Xauthority may not exist at all — xdotool then talked to X with no
|
|
32
|
+
// authorization whatsoever, got "No protocol specified" and typed nothing
|
|
33
|
+
// (found during acceptance on 2026-10-13, RedOS 8 + MATE + GDM).
|
|
34
|
+
'if [ -n "$home" ] && [ -f "$home/.Xauthority" ]; then xauth="$home/.Xauthority"',
|
|
35
|
+
'elif [ -f "/run/user/$uid/gdm/Xauthority" ]; then xauth="/run/user/$uid/gdm/Xauthority"',
|
|
36
|
+
'else xauth=$(ls -1 /run/user/$uid/*/Xauthority /run/user/$uid/xauth* 2>/dev/null | head -1)',
|
|
37
|
+
'fi',
|
|
26
38
|
'run() { if [ -n "$xauth" ]; then XAUTHORITY="$xauth" DISPLAY="$disp" "$@"; else DISPLAY="$disp" "$@"; fi; }',
|
|
27
39
|
'if command -v xdotool >/dev/null 2>&1; then run xdotool type --delay 12 --clearmodifiers -- "$text"; exit 0; fi',
|
|
28
40
|
'if command -v wtype >/dev/null 2>&1; then',
|
package/lib/session.js
CHANGED
|
@@ -252,6 +252,19 @@ export class VmController {
|
|
|
252
252
|
} catch (error) {
|
|
253
253
|
session.alive = false;
|
|
254
254
|
this.#drop(vmid);
|
|
255
|
+
// RFB sees EOF before ssh has finished exiting, and the most valuable
|
|
256
|
+
// explanation — the node's stderr — is lost. Give ssh a short grace
|
|
257
|
+
// period: without it "the VNC stream was closed by the node" cannot tell
|
|
258
|
+
// an sshd without AcceptEnv (`LC_PVE_TICKET not set, VNC proxy without
|
|
259
|
+
// password is forbidden`) from a powered-off VM or a busy QMP socket.
|
|
260
|
+
// Verified by a negative test on 2026-10-13 (sshd on :2222, no AcceptEnv).
|
|
261
|
+
const detail = await Promise.race([
|
|
262
|
+
handle.done.then(() => stderr.trim()).catch(() => ''),
|
|
263
|
+
new Promise((resolve) => setTimeout(() => resolve(''), 1500)),
|
|
264
|
+
]);
|
|
265
|
+
if (detail.length > 0 && error?.code === 'EOF') {
|
|
266
|
+
throw new VmError(`failed to open the console of VM ${vmid}: ${detail}`, 'CONSOLE_FAILED');
|
|
267
|
+
}
|
|
255
268
|
throw error;
|
|
256
269
|
}
|
|
257
270
|
this.sessions.set(vmid, session);
|
|
@@ -281,6 +294,12 @@ export class VmController {
|
|
|
281
294
|
if (error?.code === 'EOF' || error?.code === 'TIMEOUT') {
|
|
282
295
|
session.alive = false;
|
|
283
296
|
this.#drop(id);
|
|
297
|
+
// A stream that died mid-way is explained by ssh's stderr: for
|
|
298
|
+
// instance `qm vncproxy` exiting on its own 15-minute idle timeout.
|
|
299
|
+
const detail = session.stderrText?.() ?? '';
|
|
300
|
+
if (detail.length > 0) {
|
|
301
|
+
throw new VmError(`${error.message}; ssh: ${detail}`, error.code);
|
|
302
|
+
}
|
|
284
303
|
}
|
|
285
304
|
throw error;
|
|
286
305
|
}
|
package/lib/ssh.js
CHANGED
|
@@ -72,13 +72,30 @@ export function guestPingCommand(vmid) {
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Detect the guest OS from the agent's response.
|
|
75
|
+
*
|
|
76
|
+
* The guest agent reports `id` from `/etc/os-release` (`ID=`), and every distro
|
|
77
|
+
* uses its own name there: `redos`, `debian`, `ubuntu`, `altlinux`, `astra-linux`…
|
|
78
|
+
* So "id == linux" is not a usable Linux test — it filtered out almost every real
|
|
79
|
+
* distribution (found during acceptance on 2026-10-13: RedOS 8 reports `redos`,
|
|
80
|
+
* Debian reports `debian`; both landed in `other` and every `vm_*` tool refused
|
|
81
|
+
* to work). Windows must be checked first: its agent also reports
|
|
82
|
+
* `kernel-release` (the NT build number), so "has kernel-release => Linux" is
|
|
83
|
+
* wrong on its own — the order of the checks matters.
|
|
84
|
+
*
|
|
75
85
|
* @param {unknown} osinfo the parsed `get-osinfo` JSON.
|
|
76
86
|
* @returns {'windows'|'linux'|'other'}
|
|
77
87
|
*/
|
|
78
88
|
export function guestOsFamily(osinfo) {
|
|
79
89
|
const id = String(osinfo?.id ?? '').toLowerCase();
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
const name = String(osinfo?.name ?? '').toLowerCase();
|
|
91
|
+
const pretty = String(osinfo?.['pretty-name'] ?? '').toLowerCase();
|
|
92
|
+
if (id === 'mswindows' || id.includes('windows') || name.includes('windows')) return 'windows';
|
|
93
|
+
if (id.includes('linux') || name.includes('linux') || pretty.includes('linux')) return 'linux';
|
|
94
|
+
// Whatever is left is a non-Windows guest that answered get-osinfo. Only
|
|
95
|
+
// Windows and Linux are supported, so it is Linux; `kernel-release` is a
|
|
96
|
+
// sanity check on the reply (an empty agent response never reaches here).
|
|
97
|
+
const kernel = String(osinfo?.['kernel-release'] ?? '');
|
|
98
|
+
if (kernel.length > 0) return 'linux';
|
|
82
99
|
return 'other';
|
|
83
100
|
}
|
|
84
101
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-pve-vm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Proxmox VE VM control for DeepSeek Harness: see the screen and click in a VM console over VNC, plus commands, files and typed text through the QEMU guest agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|