com.vmware.o11n.plugin.powershell.remote.impl.winrm.ClientState.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.o11n.plugin.powershell.remote.impl.winrm.ClientState.java

Source

/* 
 * Copyright (c) 2011-2012 VMware, Inc.
 *  
 * This file is part of the vCO PowerShell Plug-in.
 *  
 * The vCO PowerShell Plug-in is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by the Free
 * Software Foundation version 3 and no later version.
 *  
 * The vCO PowerShell Plug-in is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3
 * for more details.
 *  
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
 * St, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package com.vmware.o11n.plugin.powershell.remote.impl.winrm;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.codec.binary.Base64;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vmware.o11n.plugin.powershell.remote.AuthenticationException;
import com.xebialabs.overthere.RuntimeIOException;
import com.xebialabs.overthere.cifs.winrm.HttpConnector;
import com.xebialabs.overthere.cifs.winrm.Namespaces;
import com.xebialabs.overthere.cifs.winrm.ResourceURI;
import com.xebialabs.overthere.cifs.winrm.ResponseExtractor;
import com.xebialabs.overthere.cifs.winrm.SoapAction;
import com.xebialabs.overthere.cifs.winrm.exception.WinRMAuthorizationException;
import com.xebialabs.overthere.cifs.winrm.exception.WinRMRuntimeIOException;

enum ClientState {
    Pending, Initialized, Done
}

public class WinRmPowerShellClient {

    private static Logger log = LoggerFactory.getLogger(WinRmPowerShellClient.class);
    private static final String CHARSET_UTF_8 = "UTF-8";

    private final URL targetURL;
    private final HttpConnector connector;

    private String timeout;
    private int envelopSize;
    private String locale;

    private String exitCode;
    private String shellId;
    private String commandId;

    private int chunk = 0;

    private ClientState state = ClientState.Pending;

    public WinRmPowerShellClient(HttpConnector connector, URL targetURL) {
        this.connector = connector;
        this.targetURL = targetURL;
    }

    synchronized public void open(String command) {
        shellId = openShell();
        if (shellId == null) {
            throw new WinRMRuntimeIOException("Unable to open winrm shell. ");
        }
        commandId = runCommand(command);
        if (commandId == null) {
            throw new WinRMRuntimeIOException("Unable to start winrm command. ");
        }

    }

    synchronized public void close() {
        log.debug("Closing WinRm client for shell {}", shellId);
        cleanUp();
        closeShell();
    }

    private void closeShell() {
        if (shellId == null)
            return;
        log.debug("closeShell shellId {}", shellId);
        final Document requestDocument = getRequestDocument(Action.WS_DELETE, ResourceURI.RESOURCE_URI_CMD, null,
                shellId, null);
        @SuppressWarnings("unused")
        Document responseDocument = sendMessage(requestDocument, null);
    }

    private void cleanUp() {
        if (commandId == null)
            return;
        log.debug("cleanUp shellId {} commandId {} ", shellId, commandId);
        final Element bodyContent = DocumentHelper.createElement(QName.get("Signal", Namespaces.NS_WIN_SHELL))
                .addAttribute("CommandId", commandId);
        bodyContent.addElement(QName.get("Code", Namespaces.NS_WIN_SHELL))
                .addText("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate");
        final Document requestDocument = getRequestDocument(Action.WS_SIGNAL, ResourceURI.RESOURCE_URI_CMD, null,
                shellId, bodyContent);
        @SuppressWarnings("unused")
        Document responseDocument = sendMessage(requestDocument, SoapAction.SIGNAL);

    }

    /**
     * Loops endlessly and provides output stream content to the OverthereProcessOutputHandler instance 
     * @param handler - instance of OverthereProcessOutputHandler that will collect the output
     */
    synchronized public void getCommandOutput(WinRmCapturingOverthereProcessOutputHandler handler) {
        log.debug("getCommandOutput shellId {} commandId {} ", shellId, commandId);
        final Element bodyContent = DocumentHelper.createElement(QName.get("Receive", Namespaces.NS_WIN_SHELL));
        bodyContent.addElement(QName.get("DesiredStream", Namespaces.NS_WIN_SHELL))
                .addAttribute("CommandId", commandId).addText("stdout stderr");
        final Document requestDocument = getRequestDocument(Action.WS_RECEIVE, ResourceURI.RESOURCE_URI_CMD, null,
                shellId, bodyContent);

        for (;;) {
            Document responseDocument = sendMessage(requestDocument, SoapAction.RECEIVE);

            //  If no output is available before the wsman:OperationTimeout expires, 
            //  the server MUST return a WSManFault with the Code attribute equal to "2150858793". 
            //  When the client receives this fault, it SHOULD issue another Receive request. 
            //  The client SHOULD continue to issue Receive messages as soon as the previous ReceiveResponse has been received.
            final List<?> faults = FaultExtractor.FAULT_CODE.getXPath().selectNodes(responseDocument);
            if (!faults.isEmpty()) {
                String faultCode = ((Attribute) faults.get(0)).getText();
                final List<?> faultMessages = FaultExtractor.FAULT_MESSAGE.getXPath().selectNodes(responseDocument);
                String faultMessage = ((Element) faultMessages.get(0)).getText();
                log.debug("fault code {} message", faultCode);
                if (faultCode.equalsIgnoreCase("2150858793")) {
                    continue;
                } else {
                    log.debug("fault code {}", faultCode);
                    throw new WinRMRuntimeIOException(faultMessage);
                }
            }

            String stdout = handleStream(responseDocument, ResponseExtractor.STDOUT);
            handler.handleOutputChunk(stdout);

            String stderr = handleStream(responseDocument, ResponseExtractor.STDERR);
            BufferedReader stderrReader = new BufferedReader(new StringReader(stderr));
            try {
                for (;;) {
                    String line = stderrReader.readLine();
                    if (line == null) {
                        break;
                    }
                    handler.handleErrorLine(line);
                }
            } catch (IOException exc) {
                throw new RuntimeIOException("Unexpected I/O exception while reading stderr", exc);
            }

            if (chunk == 0) {
                try {
                    exitCode = getFirstElement(responseDocument, ResponseExtractor.EXIT_CODE);
                    log.debug("exit code {}", exitCode);
                } catch (Exception e) {
                    log.debug("not found");
                }
            }
            chunk++;

            /* We may need to get additional output if the stream has not finished.
                                    The CommandState will change from Running to Done like so:
                                    @example
                
                                     from...
                                     <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
                                     to...
                                     <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
                                         <rsp:ExitCode>0</rsp:ExitCode>
                                     </rsp:CommandState>
                                 */
            final List<?> list = ResponseExtractor.STREAM_DONE.getXPath().selectNodes(responseDocument);
            if (!list.isEmpty()) {
                exitCode = getFirstElement(responseDocument, ResponseExtractor.EXIT_CODE);
                log.debug("exit code {}", exitCode);
                break;
            }

            if (handler.isDone() == true) {
                break;
            }
        }
        //logger.debug("all the command output has been fetched (chunk={})", chunk);
    }

    private String handleStream(Document responseDocument, ResponseExtractor stream) {
        StringBuffer buffer = new StringBuffer();
        @SuppressWarnings("unchecked")
        final List<Element> streams = (List<Element>) stream.getXPath().selectNodes(responseDocument);
        if (!streams.isEmpty()) {
            final Base64 base64 = new Base64();
            Iterator<Element> itStreams = streams.iterator();
            while (itStreams.hasNext()) {
                Element e = itStreams.next();
                //TODO check performance with http://www.iharder.net/current/java/base64/
                final byte[] decode = base64.decode(e.getText().getBytes());
                buffer.append(new String(decode));
            }
        }
        log.debug("handleStream {} buffer {}", stream, buffer);
        return buffer.toString();

    }

    private String runCommand(String command) {
        log.debug("runCommand shellId {} command {}", shellId, command);
        final Element bodyContent = DocumentHelper.createElement(QName.get("CommandLine", Namespaces.NS_WIN_SHELL));

        String encoded = command;
        if (!command.startsWith("\""))
            encoded = "\"" + encoded;
        if (!command.endsWith("\""))
            encoded = encoded + "\"";

        log.debug("Encoded command is {}", encoded);

        bodyContent.addElement(QName.get("Command", Namespaces.NS_WIN_SHELL)).addText(encoded);

        final Document requestDocument = getRequestDocument(Action.WS_COMMAND, ResourceURI.RESOURCE_URI_CMD,
                OptionSet.RUN_COMMAND, shellId, bodyContent);
        Document responseDocument = sendMessage(requestDocument, SoapAction.COMMAND_LINE);

        return getFirstElement(responseDocument, ResponseExtractor.COMMAND_ID);
    }

    synchronized public void sendCmdToInputStream(String command, boolean shouldEncode) {
        log.debug("runCommand shellId {} command {}", shellId, command);
        final Element bodyContent = DocumentHelper.createElement(QName.get("Send", Namespaces.NS_WIN_SHELL));
        String encoded = null;
        if (shouldEncode) {
            try {
                encoded = new String(Base64.encodeBase64(command.getBytes()), CHARSET_UTF_8);
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }

        String cmdToSent = shouldEncode ? encoded : command;
        log.debug("Encoded command is {}", cmdToSent);

        bodyContent.addElement(QName.get("Stream", Namespaces.NS_WIN_SHELL)).addText(cmdToSent)
                .addAttribute("Name", "stdin").addAttribute("CommandId", commandId);

        final Document requestDocument = getRequestDocument(Action.WS_SEND, ResourceURI.RESOURCE_URI_CMD,
                OptionSet.RUN_COMMAND, shellId, bodyContent);
        sendMessage(requestDocument, SoapAction.COMMAND_LINE);
    }

    private String getFirstElement(Document doc, ResponseExtractor extractor) {
        @SuppressWarnings("unchecked")
        final List<Element> nodes = (List<Element>) extractor.getXPath().selectNodes(doc);
        if (nodes.isEmpty())
            throw new RuntimeException("Cannot find " + extractor.getXPath() + " in " + toString(doc));

        final Element next = (Element) nodes.iterator().next();
        return next.getText();
    }

    private String openShell() {
        log.debug("openShell");

        final Element bodyContent = DocumentHelper.createElement(QName.get("Shell", Namespaces.NS_WIN_SHELL));
        bodyContent.addElement(QName.get("InputStreams", Namespaces.NS_WIN_SHELL)).addText("stdin");
        bodyContent.addElement(QName.get("OutputStreams", Namespaces.NS_WIN_SHELL)).addText("stdout stderr");

        final Document requestDocument = getRequestDocument(Action.WS_ACTION, ResourceURI.RESOURCE_URI_CMD,
                OptionSet.OPEN_SHELL, null, bodyContent);
        Document responseDocument = sendMessage(requestDocument, SoapAction.SHELL);

        return getFirstElement(responseDocument, ResponseExtractor.SHELL_ID);

    }

    private Document sendMessage(Document requestDocument, SoapAction soapAction) {
        try {
            return connector.sendMessage(requestDocument, soapAction);
        } catch (WinRMAuthorizationException e) {
            throw new AuthenticationException(e.getMessage(), e);
        } catch (WinRMRuntimeIOException e) {
            if (e.getCause() instanceof AuthenticationException) {
                log.error(e.getMessage());
                throw (AuthenticationException) e.getCause();
            }
            throw e;
        }

    }

    private Document getRequestDocument(Action action, ResourceURI resourceURI, OptionSet optionSet, String shelId,
            Element bodyContent) {
        Document doc = DocumentHelper.createDocument();
        final Element envelope = doc.addElement(QName.get("Envelope", Namespaces.NS_SOAP_ENV));
        envelope.add(getHeader(action, resourceURI, optionSet, shelId));

        final Element body = envelope.addElement(QName.get("Body", Namespaces.NS_SOAP_ENV));

        if (bodyContent != null)
            body.add(bodyContent);

        return doc;
    }

    private Element getHeader(Action action, ResourceURI resourceURI, OptionSet optionSet, String shellId) {
        final Element header = DocumentHelper.createElement(QName.get("Header", Namespaces.NS_SOAP_ENV));
        header.addElement(QName.get("To", Namespaces.NS_ADDRESSING)).addText(targetURL.toString());
        final Element replyTo = header.addElement(QName.get("ReplyTo", Namespaces.NS_ADDRESSING));
        replyTo.addElement(QName.get("Address", Namespaces.NS_ADDRESSING)).addAttribute("mustUnderstand", "true")
                .addText("http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous");
        header.addElement(QName.get("MaxEnvelopeSize", Namespaces.NS_WSMAN_DMTF))
                .addAttribute("mustUnderstand", "true").addText("" + envelopSize);
        header.addElement(QName.get("MessageID", Namespaces.NS_ADDRESSING)).addText(getUUID());
        header.addElement(QName.get("Locale", Namespaces.NS_WSMAN_DMTF)).addAttribute("mustUnderstand", "false")
                .addAttribute("xml:lang", locale);
        header.addElement(QName.get("DataLocale", Namespaces.NS_WSMAN_MSFT)).addAttribute("mustUnderstand", "false")
                .addAttribute("xml:lang", locale);
        header.addElement(QName.get("OperationTimeout", Namespaces.NS_WSMAN_DMTF)).addText(timeout);
        header.add(action.getElement());
        if (shellId != null) {
            header.addElement(QName.get("SelectorSet", Namespaces.NS_WSMAN_DMTF))
                    .addElement(QName.get("Selector", Namespaces.NS_WSMAN_DMTF)).addAttribute("Name", "ShellId")
                    .addText(shellId);
        }
        header.add(resourceURI.getElement());
        if (optionSet != null) {
            header.add(optionSet.getElement());
        }

        return header;
    }

    private String toString(Document doc) {
        StringWriter stringWriter = new StringWriter();
        XMLWriter xmlWriter = new XMLWriter(stringWriter, OutputFormat.createPrettyPrint());
        try {
            xmlWriter.write(doc);
            xmlWriter.close();
        } catch (IOException e) {
            throw new WinRMRuntimeIOException("error ", e);
        }
        return stringWriter.toString();
    }

    private String getUUID() {
        return "uuid:" + java.util.UUID.randomUUID().toString().toUpperCase();
    }

    public long getExitCode() {
        return Long.parseLong(exitCode);
    }

    public String getTimeout() {
        return timeout;
    }

    public void setTimeout(String timeout) {
        this.timeout = timeout;
    }

    public int getEnvelopSize() {
        return envelopSize;
    }

    public void setEnvelopSize(int envelopSize) {
        this.envelopSize = envelopSize;
    }

    public String getLocale() {
        return locale;
    }

    public void setLocale(String locale) {
        this.locale = locale;
    }

    public URL getTargetURL() {
        return targetURL;
    }

    synchronized public void setState(ClientState state) {
        this.state = state;
    }

    synchronized public ClientState getState() {
        return state;
    }

}