diff --git a/winrm/__init__.py b/winrm/__init__.py index c3ee260..414e9f4 100644 --- a/winrm/__init__.py +++ b/winrm/__init__.py @@ -16,6 +16,53 @@ FEATURE_PROXY_SUPPORT = True +def clean_error_msg(msg): + """converts a Powershell CLIXML message to a more human readable string + """ + # TODO prepare unit test, beautify code + # if the msg does not start with this, return it as is + if msg.startswith(b"#< CLIXML\r\n"): + # for proper xml, we need to remove the CLIXML part + # (the first line) + msg_xml = msg[11:] + try: + # remove the namespaces from the xml for easier processing + msg_xml = _strip_namespace(msg_xml) + root = ET.fromstring(msg_xml) + # the S node is the error message, find all S nodes + nodes = root.findall("./S") + new_msg = "" + for s in nodes: + # append error msg string to result, also + # the hex chars represent CRLF so we replace with newline + new_msg += s.text.replace("_x000D__x000A_", "\n") + except Exception as e: + # if any of the above fails, the msg was not true xml + # print a warning and return the original string + warnings.warn( + "There was a problem converting the Powershell error " + "message: %s" % (e)) + else: + # if new_msg was populated, that's our error message + # otherwise the original error message will be used + if len(new_msg): + # remove leading and trailing whitespace while we are here + return new_msg.strip().encode('utf-8') + + # either failed to decode CLIXML or there was nothing to decode + # just return the original message + return msg + + +def _strip_namespace(xml): + """strips any namespaces from an xml string""" + p = re.compile(b"xmlns=*[\"\"][^\"\"]*[\"\"]") + allmatches = p.finditer(xml) + for match in allmatches: + xml = xml.replace(match.group(), b"") + return xml + + class Response(object): """Response from a remote command execution""" def __init__(self, args): @@ -54,54 +101,9 @@ def run_ps(self, script): if len(rs.std_err): # if there was an error message, clean it it up and make it human # readable - rs.std_err = self._clean_error_msg(rs.std_err) + rs.std_err = clean_error_msg(rs.std_err) return rs - def _clean_error_msg(self, msg): - """converts a Powershell CLIXML message to a more human readable string - """ - # TODO prepare unit test, beautify code - # if the msg does not start with this, return it as is - if msg.startswith(b"#< CLIXML\r\n"): - # for proper xml, we need to remove the CLIXML part - # (the first line) - msg_xml = msg[11:] - try: - # remove the namespaces from the xml for easier processing - msg_xml = self._strip_namespace(msg_xml) - root = ET.fromstring(msg_xml) - # the S node is the error message, find all S nodes - nodes = root.findall("./S") - new_msg = "" - for s in nodes: - # append error msg string to result, also - # the hex chars represent CRLF so we replace with newline - new_msg += s.text.replace("_x000D__x000A_", "\n") - except Exception as e: - # if any of the above fails, the msg was not true xml - # print a warning and return the original string - warnings.warn( - "There was a problem converting the Powershell error " - "message: %s" % (e)) - else: - # if new_msg was populated, that's our error message - # otherwise the original error message will be used - if len(new_msg): - # remove leading and trailing whitespace while we are here - return new_msg.strip().encode('utf-8') - - # either failed to decode CLIXML or there was nothing to decode - # just return the original message - return msg - - def _strip_namespace(self, xml): - """strips any namespaces from an xml string""" - p = re.compile(b"xmlns=*[\"\"][^\"\"]*[\"\"]") - allmatches = p.finditer(xml) - for match in allmatches: - xml = xml.replace(match.group(), b"") - return xml - @staticmethod def _build_url(target, transport): match = re.match( diff --git a/winrm/tests/test_session.py b/winrm/tests/test_session.py index 875d493..95ad009 100644 --- a/winrm/tests/test_session.py +++ b/winrm/tests/test_session.py @@ -1,6 +1,7 @@ import pytest from winrm import Session +from winrm import clean_error_msg def test_run_cmd(protocol_fake): @@ -62,7 +63,7 @@ def test_decode_clixml_error(): s = Session('windows-host.example.com', auth=('john.smith', 'secret')) msg = b'#< CLIXML\r\nSystem.Management.Automation.PSCustomObjectSystem.Object1Preparing modules for first use.0-1-1Completed-1 1Preparing modules for first use.0-1-1Completed-1 fake : The term \'fake\' is not recognized as the name of a cmdlet, function, script file, or operable program. Check _x000D__x000A_the spelling of the name, or if a path was included, verify that the path is correct and try again._x000D__x000A_At line:1 char:1_x000D__x000A_+ fake cmdlet_x000D__x000A_+ ~~~~_x000D__x000A_ + CategoryInfo : ObjectNotFound: (fake:String) [], CommandNotFoundException_x000D__x000A_ + FullyQualifiedErrorId : CommandNotFoundException_x000D__x000A_ _x000D__x000A_' expected = b"fake : The term 'fake' is not recognized as the name of a cmdlet, function, script file, or operable program. Check \nthe spelling of the name, or if a path was included, verify that the path is correct and try again.\nAt line:1 char:1\n+ fake cmdlet\n+ ~~~~\n + CategoryInfo : ObjectNotFound: (fake:String) [], CommandNotFoundException\n + FullyQualifiedErrorId : CommandNotFoundException" - actual = s._clean_error_msg(msg) + actual = clean_error_msg(msg) assert actual == expected @@ -70,7 +71,7 @@ def test_decode_clixml_no_clixml(): s = Session('windows-host.example.com', auth=('john.smith', 'secret')) msg = b"stderr line" expected = b"stderr line" - actual = s._clean_error_msg(msg) + actual = clean_error_msg(msg) assert actual == expected @@ -78,7 +79,7 @@ def test_decode_clixml_no_errors(): s = Session('windows-host.example.com', auth=('john.smith', 'secret')) msg = b'#< CLIXML\r\nSystem.Management.Automation.PSCustomObjectSystem.Object1Preparing modules for first use.0-1-1Completed-1 1Preparing modules for first use.0-1-1Completed-1 ' expected = msg - actual = s._clean_error_msg(msg) + actual = clean_error_msg(msg) assert actual == expected @@ -87,6 +88,6 @@ def test_decode_clixml_invalid_xml(): msg = b'#< CLIXML\r\ndasf' with pytest.warns(UserWarning, match="There was a problem converting the Powershell error message"): - actual = s._clean_error_msg(msg) + actual = clean_error_msg(msg) assert actual == msg