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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ Consequences:
`WinRMClient.Builder.arraySeparator(String)` changes the separator. The `WqlRow` Javadoc now
says what the code does: a WMI `NULL` is an empty string, `null` means "no such property".

- **A WQL row is no longer dropped when none of its selected properties came back** (#201).
WinRM leaves an empty or `NULL` array out of the response, so `SELECT IPSecPermitTCPPorts FROM
Win32_NetworkAdapterConfiguration` returned no rows at all, where WMI returns one per adapter.
Each instance is now a row, on which `string()` returns `null` for the missing property.
`WqlResult.columns()` now also reads the property names of every row, not just the first one:
with `SELECT * FROM Win32_NetworkAdapterConfiguration`, a first adapter without IP configuration
hid `IPAddress`, `DefaultIPGateway`, ... from the columns of all the rows.

- **Streaming terminals now report protocol failures as `WinRMClientException`** (#188).
`WqlRequest.stream()`, `CommandRequest.start()`, `RemoteFile.openStream()`/`openReader()`,
`RemoteDirectoryListing.stream()` and the closing of a `RemoteProcess` let raw
Expand Down
22 changes: 11 additions & 11 deletions src/main/java/org/metricshub/winrm/WmiHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/

import java.util.Arrays;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
Expand Down Expand Up @@ -150,7 +151,7 @@ public static List<String> extractPropertiesFromResult(final List<Map<String, Ob
* Note: The exact case cannot be retrieved if result is empty, in which case all
* names are reported in lower case
*
* @param resultRows The result whose first row will be parsed
* @param resultRows The result whose rows will be parsed
* @param wqlQuery The WQL query that was used (so we make sure to return the properties in the same order)
* @return a list of property names
*/
Expand All @@ -165,23 +166,22 @@ public static List<String> extractPropertiesFromResult(
return wqlQuery.getSelectedProperties();
}

// Extract the actual property names
final String[] resultPropertyArray = resultRows.get(0).keySet().toArray(new String[0]);
// Extract the actual property names from every row (lower case -> actual case): WinRM
// leaves out an empty or NULL array, so a row may lack properties that others have
final Map<String, String> resultProperties = new LinkedHashMap<>();
resultRows.forEach(row -> row.keySet().forEach(name -> resultProperties.putIfAbsent(name.toLowerCase(), name)));

// First case: we don't have any specified properties in the WQL Query, so we just
// return the properties from the result set in alphabetical order
if (wqlQuery.getSelectedProperties().isEmpty()) {
Arrays.sort(resultPropertyArray, String.CASE_INSENSITIVE_ORDER);
return Arrays.asList(resultPropertyArray);
final List<String> properties = new ArrayList<>(resultProperties.values());
properties.sort(String.CASE_INSENSITIVE_ORDER);
return properties;
}

// Create a new list based on queryPropertyArray (with its order), but with the values
// from resultPropertyArray
// from resultProperties
final List<String> queryProperties = wqlQuery.getSelectedProperties();
final Map<String, String> resultProperties = Arrays
.asList(resultPropertyArray)
.stream()
.collect(Collectors.toMap(String::toLowerCase, property -> property));
return queryProperties
.stream()
.map(property -> resultProperties.getOrDefault(property.toLowerCase(), property))
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/metricshub/winrm/WqlRow.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
/**
* One row of a WQL query result: an immutable, ordered view of the instance properties.
* Property lookup is case-insensitive, matching WMI semantics.
* <p>
* A WMI array property is a single string, its elements joined with the client's array
* separator ({@code a|b}, no trailing separator). WinRM sends no type information: a
* one-element array reads like a scalar ({@code a}), and an empty or {@code NULL} array is left
* out of the response, so the row has no such property.
*/
public final class WqlRow {

Expand Down
10 changes: 5 additions & 5 deletions src/main/java/org/metricshub/winrm/light/WsmanClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -1437,11 +1437,11 @@ private static void collectRows(
value.append(arraySeparator).append(prop.getTextContent());
}
}
if (!values.isEmpty()) {
final Map<String, String> row = new LinkedHashMap<>();
values.forEach((name, value) -> row.put(name, value.toString()));
rows.add(row);
}
// An instance with no property element is still a row: WinRM leaves out an empty (or
// NULL) array, so selecting only such a property yields empty instances
final Map<String, String> row = new LinkedHashMap<>();
values.forEach((name, value) -> row.put(name, value.toString()));
rows.add(row);
Comment thread
bertysentry marked this conversation as resolved.
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/site/markdown/wql.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ A WMI **array** property (`IPAddress` in `Win32_NetworkAdapterConfiguration`, `C
`Win32_DiskDrive`, ...) comes back as a single string, its elements joined with `|`:
`"192.0.2.10|fe80::1"`. The builder's `arraySeparator(String)` changes the separator.

There is no trailing separator, and WinRM sends no type information: a one-element array reads
like a scalar (`"fr-FR"` for `MUILanguages`), and an empty or `NULL` array is left out of the
response, so `string()` returns `null` for it (unlike a scalar `NULL`, an empty string). The
instance is still a row, even when that array is the only selected property.

### Column order and case

These rules apply to `WqlResult.columns()`; each row's `asMap()` keeps the server's order, and
Expand Down
27 changes: 27 additions & 0 deletions src/test/java/org/metricshub/winrm/WmiHelperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,40 @@
* limitations under the License.
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;

class WmiHelperTest {

@Test
void extractPropertiesFromResultReadsTheNamesOfEveryRow() {
// WinRM leaves out an empty or NULL array: the first row may lack properties others have
final List<Map<String, Object>> rows = List.of(
Map.of("Index", "0"),
Map.of("Index", "1", "IPAddress", "192.0.2.10")
);
assertEquals(
List.of("Index", "IPAddress"),
WmiHelper.extractPropertiesFromResult(rows, "SELECT index, ipaddress FROM Win32_NetworkAdapterConfiguration")
);
assertEquals(
List.of("Index", "IPAddress"),
WmiHelper.extractPropertiesFromResult(rows, "SELECT * FROM Win32_NetworkAdapterConfiguration")
);
assertEquals(
List.of("IPAddress"),
WmiHelper.extractPropertiesFromResult(
List.of(Map.of(), Map.of("IPAddress", "192.0.2.10")),
"SELECT ipaddress FROM Win32_NetworkAdapterConfiguration"
)
);
}

@Test
void isValidWqlAcceptsSingleLineQueries() {
assertTrue(WmiHelper.isValidWql("SELECT * FROM Win32_Service"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ void itemsCollectedFromBothNamespaceVariants() throws Exception {

@Test
void arrayPropertyElementsAreJoinedWithTheSeparator() throws Exception {
// WS-Management sends a WMI array as sibling elements sharing one name; a WMI NULL is xsi:nil.
// WS-Management sends a WMI array as sibling elements sharing one name; a scalar NULL is xsi:nil
// (an empty or NULL array is left out).
final Document doc = parse(
"<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\">" +
"<s:Body><wsen:PullResponse xmlns:wsen=\"" +
Expand All @@ -136,7 +137,7 @@ void arrayPropertyElementsAreJoinedWithTheSeparator() throws Exception {
"<p:IPAddress>192.0.2.10</p:IPAddress>" +
"<p:Caption>eth0</p:Caption>" +
"<p:IPAddress>fe80::1</p:IPAddress>" +
"<p:DefaultIPGateway xsi:nil=\"true\"/>" +
"<p:DNSDomain xsi:nil=\"true\"/>" +
"</p:Cfg>" +
"</wsman:Items>" +
"</wsen:PullResponse></s:Body></s:Envelope>"
Expand All @@ -146,7 +147,30 @@ void arrayPropertyElementsAreJoinedWithTheSeparator() throws Exception {
assertEquals(1, rows.size());
assertEquals("192.0.2.10, fe80::1", rows.get(0).get("IPAddress"));
assertEquals("eth0", rows.get(0).get("Caption"));
assertEquals("", rows.get(0).get("DefaultIPGateway"));
assertEquals("", rows.get(0).get("DNSDomain"));
}

@Test
void instanceWithNoPropertyElementIsStillARow() throws Exception {
// WinRM leaves out an empty array: selecting only such a property yields an empty fragment
final Document doc = parse(
"<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\">" +
"<s:Body><wsen:PullResponse xmlns:wsen=\"" +
WSEN +
"\">" +
"<wsman:Items xmlns:wsman=\"" +
WSMAN +
"\">" +
"<wsman:XmlFragment/>" +
"<wsman:XmlFragment><IPSecPermitTCPPorts>80</IPSecPermitTCPPorts></wsman:XmlFragment>" +
"</wsman:Items>" +
"</wsen:PullResponse></s:Body></s:Envelope>"
);
final List<Map<String, String>> rows = new ArrayList<>();
WsmanClient.collectItems(doc, rows, "|");
assertEquals(2, rows.size());
assertEquals(Map.of(), rows.get(0));
assertEquals("80", rows.get(1).get("IPSecPermitTCPPorts"));
}

@Test
Expand Down
Loading