fix: deliver Invoke-PveVmGuestExec -Args to the guest as argv

ExecuteGuestCommand JSON-serialized the args array into the agent/exec
'input-data' field — which is the process's STDIN, not its arguments. So
guest commands ran with no argv: cmd.exe started interactively and the
JSON blob ['/c','echo',...] arrived at its prompt.

PVE's agent/exec 'command' parameter is itself an array (element 0 = the
executable, the rest = argv) sent as repeated form keys. The low-level
client couldn't express repeated keys (Dictionary<string,string> only),
so:

- Add PostAsync(string, IEnumerable<KeyValuePair<string,string>>) to
  IPveHttpClient/PveHttpClient; BuildFormContent now emits one key=value
  field per pair, so a key may repeat.
- ExecuteGuestCommand builds command = [exe] + args as repeated 'command'
  fields and no longer touches input-data.

Tests: form-encoder repeated-key + per-value encoding cases; VmService
tests asserting the command array, order, and absence of input-data; an
integration regression guard that echoes an arg and checks it round-trips
as stdout.

Tracked as F091. Closes #68.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-05-22 14:32:04 -05:00
parent 5266accfae
commit bc71ed4a12
7 changed files with 199 additions and 10 deletions
@@ -79,6 +79,46 @@ namespace PSProxmoxVE.Core.Tests.Client
Assert.Contains("ide2=nas:iso/win.iso,media%3Dcdrom", handler.LastBody);
}
[Fact]
public async Task PostAsync_RepeatedKeys_EmitOneFieldPerValue()
{
var (client, handler) = NewCapturingClient();
using (client)
{
// PVE array params (e.g. guest-exec command) are sent as repeated keys.
var data = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("command", "cmd.exe"),
new KeyValuePair<string, string>("command", "/c"),
new KeyValuePair<string, string>("command", "echo"),
new KeyValuePair<string, string>("command", "WLMARK42"),
};
await client.PostAsync("nodes/pve/qemu/100/agent/exec", data);
}
Assert.Equal("command=cmd.exe&command=/c&command=echo&command=WLMARK42", handler.LastBody);
}
[Fact]
public async Task PostAsync_RepeatedKeys_EncodeEachValueIndependently()
{
var (client, handler) = NewCapturingClient();
using (client)
{
var data = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("command", "powershell.exe"),
new KeyValuePair<string, string>("command", "-Command"),
new KeyValuePair<string, string>("command", "echo a=b;c"),
};
await client.PostAsync("nodes/pve/qemu/100/agent/exec", data);
}
// The '=' and ';' inside an arg must be encoded so they don't split the body,
// while spaces follow the form convention ('+').
Assert.Equal("command=powershell.exe&command=-Command&command=echo+a%3Db%3Bc", handler.LastBody);
}
private sealed class CapturingHandler : HttpMessageHandler
{
public string LastBody { get; private set; } = string.Empty;