// Copyright (C) 2012 jOVAL.org. All rights reserved.
// This software is licensed under the AGPL 3.0 license available at http://www.joval.org/agpl_v3.txt
package jwsmv;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.security.auth.login.FailedLoginException;
import com.microsoft.wsman.fault.WSManFaultType;
import com.microsoft.wsman.shell.CommandLine;
import com.microsoft.wsman.shell.CommandResponse;
import com.microsoft.wsman.shell.CommandStateType;
import com.microsoft.wsman.shell.DesiredStreamType;
import com.microsoft.wsman.shell.Receive;
import com.microsoft.wsman.shell.ReceiveResponse;
import com.microsoft.wsman.shell.Signal;
import com.microsoft.wsman.shell.SignalResponse;
import com.microsoft.wsman.shell.Send;
import com.microsoft.wsman.shell.SendResponse;
import com.microsoft.wsman.shell.StreamType;
import org.dmtf.wsman.AttributableEmpty;
import org.dmtf.wsman.AttributablePositiveInteger;
import org.dmtf.wsman.OptionSet;
import org.dmtf.wsman.OptionType;
import org.dmtf.wsman.SelectorSetType;
import org.dmtf.wsman.SelectorType;
import org.w3c.soap.envelope.Fault;
import jwsmv.util.Xpress;
import jwsmv.wsman.FaultException;
import jwsmv.wsman.Port;
import jwsmv.wsman.operation.CommandOperation;
import jwsmv.wsman.operation.ReceiveOperation;
import jwsmv.wsman.operation.SendOperation;
import jwsmv.wsman.operation.SignalOperation;
/**
* Simple implementation of a WinRM Shell-based Process.
*
* @author David A. Solin
* @version %I% %G%
*/
public class ShellCommand extends Process implements Constants, Runnable {
static final long TIMEOUT_CODE = 2150858793L;
static final byte CTL_C = 0x03;
static final byte CTL_BREAK = 0x1C;
/**
* An enumeration of codes that can be issued to a running process using a signal.
*/
static enum SignalCode {
TERMINATE("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate"),
CTL_C("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/ctrl_c"),
CTL_BREAK("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/ctrl_break");
private String value;
private SignalCode(String value) {
this.value = value;
}
String value() {
return value;
}
}
/**
* An enumeration of the possible states of a running process, embedding their URI values.
*/
static enum State {
DONE("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done"),
PENDING("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Pending"),
ERROR("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Error"),
RUNNING("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running");
private String value;
private State(String value) {
this.value = value;
}
String value() {
return value;
}
static State fromValue(String s) throws IllegalArgumentException {
for (State state : values()) {
if (state.value().equals(s)) {
return state;
}
}
throw new IllegalArgumentException(s);
}
}
/**
* Get the ID of the command.
*/
public String getId() {
return id;
}
public String getCommand() {
StringBuffer sb = new StringBuffer(cmd);
if (args != null) {
for (String arg : args) {
sb.append(" ");
if (arg.indexOf(" ") == -1) {
sb.append(arg);
} else {
sb.append("\"").append(arg.replace("\"","\\\"")).append("\"");
}
}
}
return sb.toString();
}
/**
* Wait for at most millis milliseconds for the process to finish executing.
*/
public void waitFor(long millis) throws InterruptedException {
long endTime = System.currentTimeMillis() + millis;
while (isRunning() && System.currentTimeMillis() < endTime) {
Thread.sleep(25);
}
long maxWait = endTime - System.currentTimeMillis();
if (maxWait > 0) {
if (thread != null && thread.isAlive()) {
thread.join(maxWait);
}
}
}
/**
* Test to see if the process is running.
*/
public boolean isRunning() {
return state == State.RUNNING;
}
/**
* Test to see if there was an error managing the remote process execution.
*/
public boolean isError() {
return state == State.ERROR;
}
/**
* Get the error.
*/
public Exception getError() throws IllegalStateException {
if (state == State.ERROR) {
return error;
} else {
throw new IllegalStateException(state.toString());
}
}
/**
* Start the process running.
*/
public void start() throws JAXBException, IOException, FaultException {
CommandLine cl = Factories.SHELL.createCommandLine();
cl.setCommand(cmd);
if (args != null) {
for (String arg : args) {
cl.getArguments().add(arg);
}
}
CommandOperation commandOperation = new CommandOperation(cl);
commandOperation.addResourceURI(SHELL_URI);
commandOperation.addSelectorSet(selector);
//
// The client-side mode for standard input is console if TRUE and pipe if FALSE. This does not
// have an impact on the wire protocol. This option name MUST be used by the client of the Text-based
// Command Shell when starting the execution of a command using rsp:Command request to indicate that
// the client side of the standard input is console; the default implies pipe.
//
OptionType winrsStdin = Factories.WSMAN.createOptionType();
winrsStdin.setName("WINRS_CONSOLEMODE_STDIN");
winrsStdin.setValue("FALSE");
//
// If set to TRUE, this option requests that the server runs the command without using cmd.exe; if
// set to FALSE, the server is requested to use cmd.exe. By default the value is FALSE. This does
// not have any impact on the wire protocol.
//
OptionType winrsSkipCmd = Factories.WSMAN.createOptionType();
winrsSkipCmd.setName("WINRS_SKIP_CMD_SHELL");
winrsSkipCmd.setValue("FALSE");
OptionSet options = Factories.WSMAN.createOptionSet();
options.getOption().add(winrsStdin);
options.getOption().add(winrsSkipCmd);
commandOperation.addHeader(options);
try {
CommandResponse response = commandOperation.dispatch(port);
state = State.RUNNING;
disposed = false;
id = response.getCommandId();
stdoutPipe = new PipedOutputStream();
stdout = new PipedInputStream(stdoutPipe, 65536);
stderrPipe = new PipedOutputStream();
stderr = new PipedInputStream(stderrPipe, 65536);
thread = new Thread(group, this, "ShellCommand:" + id);
thread.start();
} catch (FailedLoginException e) {
throw new RuntimeException(e);
}
}
// Overrides of Process methods
@Override
public int waitFor() throws InterruptedException {
waitFor(3600000L); // 1hr max
if (isRunning()) {
destroy();
}
return exitValue();
}
@Override
public void destroy() {
finalize();
state = State.DONE;
}
@Override
public OutputStream getOutputStream() {
if (stdin == null) {
stdin = new CommandOutputStream(1024);
}
return stdin;
}
@Override
public InputStream getInputStream() {
return stdout;
}
@Override
public InputStream getErrorStream() {
return stderr;
}
@Override
public int exitValue() throws IllegalThreadStateException {
switch(state) {
case DONE:
return exitCode;
case ERROR:
IllegalThreadStateException ex = new IllegalThreadStateException(state.toString());
ex.initCause(getError());
throw ex;
default:
throw new IllegalThreadStateException(state.toString());
}
}
// Implement Runnable
/**
* Read stdout and stderr from the remote process.
*/
public void run() {
while(isRunning()) {
try {
DesiredStreamType desired = Factories.SHELL.createDesiredStreamType();
desired.setCommandId(id);
desired.getValue().add(Shell.STDOUT);
desired.getValue().add(Shell.STDERR);
Receive receive = Factories.SHELL.createReceive();
receive.setDesiredStream(desired);
ReceiveOperation receiveOperation = new ReceiveOperation(receive);
receiveOperation.addResourceURI(SHELL_URI);
receiveOperation.addSelectorSet(selector);
OptionType keepAlive = Factories.WSMAN.createOptionType();
keepAlive.setName("WSMAN_CMDSHELL_OPTION_KEEPALIVE");
keepAlive.setValue("TRUE");
OptionSet options = Factories.WSMAN.createOptionSet();
options.getOption().add(keepAlive);
receiveOperation.addHeader(options);
//
// Send error stream before output stream
//
ReceiveResponse response = receiveOperation.dispatch(port);
for (StreamType stream : response.getStream()) {
if (stream.isSetValue()) {
byte[] val = null;
if (compress) {
val = codec.decode(stream.getValue());
} else {
val = stream.getValue();
}
if (val.length > 0) {
if (Shell.STDERR.equals(stream.getName())) {
stderrPipe.write(val);
}
}
}
}
stderrPipe.flush();
for (StreamType stream : response.getStream()) {
if (stream.isSetValue()) {
byte[] val = null;
if (compress) {
val = codec.decode(stream.getValue());
} else {
val = stream.getValue();
}
if (val.length > 0) {
if (Shell.STDOUT.equals(stream.getName())) {
stdoutPipe.write(val);
}
}
}
}
stdoutPipe.flush();
if (response.isSetCommandState()) {
CommandStateType state = response.getCommandState();
ShellCommand.this.state = State.fromValue(state.getState());
if (state.isSetExitCode()) {
exitCode = state.getExitCode().intValue();
//
// Per section section 3.1.4.14, point 4 of MS-WSMV specification client MUST send
// signal message with Terminate code after receiving final response from server.
//
ShellCommand.this.finalize();
}
}
} catch (FaultException e) {
boolean retry = false;
Fault fault = e.getFault();
if (fault.isSetDetail()) {
for (Object obj : fault.getDetail().getAny()) {
if (obj instanceof JAXBElement) {
obj = ((JAXBElement)obj).getValue();
}
if (obj instanceof WSManFaultType) {
WSManFaultType wsFault = (WSManFaultType)obj;
if (wsFault.getCode() == TIMEOUT_CODE) {
retry = true;
}
}
}
}
if (!retry) {
error = e;
state = State.ERROR;
}
} catch (Exception e) {
error = e;
state = State.ERROR;
}
}
try {
stdoutPipe.close();
} catch (IOException e) {
}
try {
stderrPipe.close();
} catch (IOException e) {
}
}
// Internal
private Port port;
private boolean compress;
private Xpress codec;
private String id;
private SelectorSetType selector;
private State state;
private int exitCode;
private PipedOutputStream stdoutPipe, stderrPipe;
private InputStream stdout, stderr;
private OutputStream stdin;
private String cmd;
private String[] args;
private boolean disposed;
private Exception error;
private ThreadGroup group;
private Thread thread;
/**
* Create a command for the specified Shell.
*/
ShellCommand(Shell shell, String cmd, String[] args) {
selector = shell.getSelectorSet();
group = shell.group;
compress = shell.compress;
if (compress) {
codec = new Xpress();
}
port = shell.port;
this.cmd = cmd;
this.args = args;
stdin = null;
stderr = null;
stdout = null;
exitCode = -1;
state = State.PENDING;
disposed = false;
}
/**
* Send a signal to the process.
*/
SignalResponse signal(SignalCode code) throws FailedLoginException, JAXBException, FaultException, IOException {
Signal signal = Factories.SHELL.createSignal();
signal.setCommandId(id);
signal.setCode(code.value());
SignalOperation signalOperation = new SignalOperation(signal);
signalOperation.addResourceURI(SHELL_URI);
signalOperation.addSelectorSet(selector);
return signalOperation.dispatch(port);
}
/**
* Delete the ShellCommand on the target machine (idempotent).
*/
@Override
protected synchronized void finalize() {
if (state != State.PENDING && !disposed) {
try {
signal(SignalCode.TERMINATE);
try {
stdoutPipe.close();
} catch (IOException e) {
}
try {
stderrPipe.close();
} catch (IOException e) {
}
} catch (Exception e) {
} finally {
disposed = true;
}
}
}
/**
* An OutputStream implementation that is triggered by flush() to send data upstream to the process.
*/
class CommandOutputStream extends ByteArrayOutputStream {
int MAX_BUFFER_LEN = 250000;
CommandOutputStream(int size) {
super(size);
}
@Override
public synchronized void flush() throws IOException {
try {
byte[] data = toByteArray();
if (data.length == 1 && data[0] == CTL_C) {
signal(SignalCode.CTL_C);
} else if (data.length == 1 && data[0] == CTL_BREAK) {
signal(SignalCode.CTL_BREAK);
} else {
for (int offset=0; offset < data.length; offset+=MAX_BUFFER_LEN) {
int len = Math.min(MAX_BUFFER_LEN, data.length - offset);
byte[] buff = new byte[len];
System.arraycopy(data, offset, buff, 0, len);
StreamType stream = Factories.SHELL.createStreamType();
stream.setName(Shell.STDIN);
stream.setCommandId(id);
if (compress) {
stream.setValue(codec.encode(buff));
} else {
stream.setValue(buff);
}
Send send = Factories.SHELL.createSend();
send.getStream().add(stream);
SendOperation sendOperation = new SendOperation(send);
sendOperation.addResourceURI(SHELL_URI);
sendOperation.addSelectorSet(selector);
SendResponse response = sendOperation.dispatch(port);
if (response.isSetDesiredStream()) {
StreamType rs = response.getDesiredStream();
if (rs.getName().equals(Shell.STDIN) && rs.getEnd()) {
close();
break;
}
}
}
}
} catch (FailedLoginException e) {
throw new IOException(e);
} catch (JAXBException e) {
throw new IOException(e);
} catch (FaultException e) {
throw new IOException(e);
} finally {
reset();
}
}
}
}