OOZIE-3405 SSH action shows empty error Message and Error code (matijhs via asalamon74)
diff --git a/core/src/main/java/org/apache/oozie/ErrorCode.java b/core/src/main/java/org/apache/oozie/ErrorCode.java
index 6b0ce47..8e98535 100644
--- a/core/src/main/java/org/apache/oozie/ErrorCode.java
+++ b/core/src/main/java/org/apache/oozie/ErrorCode.java
@@ -228,6 +228,8 @@
 
     E1102(XLog.STD, "Invalid operation [{0}] for bulk command"),
 
+    E1111(XLog.STD, "Script failed on remote host with [{0}]"),
+
     E1201(XLog.STD, "State [{0}] is invalid for job [{1}]."),
 
     E1301(XLog.STD, "Could not read the bundle job definition, [{0}]"),
diff --git a/core/src/main/java/org/apache/oozie/action/ssh/SshActionExecutor.java b/core/src/main/java/org/apache/oozie/action/ssh/SshActionExecutor.java
index 6956cba..fbc94f1 100644
--- a/core/src/main/java/org/apache/oozie/action/ssh/SshActionExecutor.java
+++ b/core/src/main/java/org/apache/oozie/action/ssh/SshActionExecutor.java
@@ -30,6 +30,7 @@
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.apache.hadoop.util.StringUtils;
 
+import org.apache.oozie.ErrorCode;
 import org.apache.oozie.client.WorkflowAction;
 import org.apache.oozie.client.OozieClient;
 import org.apache.oozie.client.WorkflowAction.Status;
@@ -144,21 +145,7 @@
                 String dataCommand = SSH_COMMAND_BASE + action.getTrackerUri() + " cat " + outFile;
                 LOG.debug("Ssh command [{0}]", dataCommand);
                 try {
-                    final Process process = Runtime.getRuntime().exec(dataCommand.split("\\s"));
-                    final BufferDrainer bufferDrainer = new BufferDrainer(process, maxLen);
-                    bufferDrainer.drainBuffers();
-                    final StringBuffer outBuffer = bufferDrainer.getInputBuffer();
-                    final StringBuffer errBuffer = bufferDrainer.getErrorBuffer();
-                    boolean overflow = false;
-                    LOG.trace("outBuffer={0}", outBuffer);
-                    LOG.trace("errBuffer={0}", errBuffer);
-                    if (outBuffer.length() > maxLen) {
-                        overflow = true;
-                    }
-                    if (overflow) {
-                        throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR,
-                                                          "ERR_OUTPUT_EXCEED_MAX_LEN", "unknown error");
-                    }
+                    final StringBuffer outBuffer = getActionOutputMessage(dataCommand);
                     context.setExecutionData(status.toString(), PropertiesUtils.stringToProperties(outBuffer.toString()));
                     LOG.trace("Execution data set. status={0}, properties={1}", status,
                             PropertiesUtils.stringToProperties(outBuffer.toString()));
@@ -177,6 +164,9 @@
             if (status == Status.ERROR) {
                 LOG.warn("Execution data set to null in ERROR");
                 context.setExecutionData(status.toString(), null);
+                String actionErrorMsg = getActionErrorMessage(context, action);
+                LOG.warn("{0}: Script failed on remote host with [{1}]", ErrorCode.E1111, actionErrorMsg);
+                context.setErrorInfo(ErrorCode.E1111.toString(), actionErrorMsg);
             }
             else {
                 LOG.warn("Execution data not set");
@@ -186,6 +176,40 @@
         LOG.trace("check() end for action={0}", action);
     }
 
+    private StringBuffer getActionOutputMessage(String dataCommand) throws IOException, ActionExecutorException {
+        final Process process = Runtime.getRuntime().exec(dataCommand.split("\\s"));
+        boolean overflow = false;
+        final BufferDrainer bufferDrainer = new BufferDrainer(process, maxLen);
+        bufferDrainer.drainBuffers();
+        final StringBuffer outBuffer = bufferDrainer.getInputBuffer();
+        final StringBuffer errBuffer = bufferDrainer.getErrorBuffer();
+        LOG.debug("outBuffer={0}", outBuffer);
+        LOG.debug("errBuffer={0}", errBuffer);
+        if (outBuffer.length() > maxLen) {
+            overflow = true;
+        }
+        if (overflow) {
+            throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR,
+                    "ERR_OUTPUT_EXCEED_MAX_LEN", "unknown error");
+        }
+        return outBuffer;
+    }
+
+    private String getActionErrorMessage(Context context, WorkflowAction action) throws ActionExecutorException {
+        String outFile = getRemoteFileName(context, action, "error", false, true);
+        String errorMsgCmd = SSH_COMMAND_BASE + action.getTrackerUri() + " cat " + outFile;
+        LOG.debug("Get error message command: [{0}]", errorMsgCmd);
+        String errorMessage;
+        try {
+            final StringBuffer outBuffer = getActionOutputMessage(errorMsgCmd);
+            errorMessage = outBuffer.toString().replaceAll("\n", "");
+        } catch (Exception ex) {
+            throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, "ERR_UNKNOWN_ERROR",
+                    "unknown error", ex);
+        }
+        return errorMessage;
+    }
+
     /**
      * Kill ssh action.
      *
@@ -423,7 +447,6 @@
     protected String doExecute(String host, String dirLocation, String cmnd, String[] args, boolean ignoreOutput,
                                WorkflowAction action, String recoveryId, boolean preserveArgs)
                                throws IOException, InterruptedException {
-        XLog log = XLog.getLog(getClass());
         Runtime runtime = Runtime.getRuntime();
         String callbackPost = ignoreOutput ? "_" : ConfigurationService.get(HTTP_COMMAND_OPTIONS).replace(" ", "%%%");
         String preserveArgsS = preserveArgs ? "PRESERVE_ARGS" : "FLATTEN_ARGS";
@@ -431,8 +454,7 @@
         String callBackUrl = Services.get().get(CallbackService.class)
                 .createCallBackUrl(action.getId(), EXT_STATUS_VAR);
         String command = XLog.format("{0}{1} {2}ssh-base.sh {3} {4} \"{5}\" \"{6}\" {7} {8} ", SSH_COMMAND_BASE, host, dirLocation,
-                preserveArgsS, ConfigurationService.get(HTTP_COMMAND), callBackUrl, callbackPost, recoveryId, cmnd)
-                .toString();
+                preserveArgsS, ConfigurationService.get(HTTP_COMMAND), callBackUrl, callbackPost, recoveryId, cmnd);
         String[] commandArray = command.split("\\s");
         String[] finalCommand;
         if (args == null) {
diff --git a/core/src/main/resources/ssh-wrapper.sh b/core/src/main/resources/ssh-wrapper.sh
index e2e6f7f..4bd6b8c 100644
--- a/core/src/main/resources/ssh-wrapper.sh
+++ b/core/src/main/resources/ssh-wrapper.sh
@@ -33,6 +33,8 @@
 echo $mpid > $dir/$actionId.pid
 stdout="$dir/$mpid.$actionId.stdout"
 stderr="$dir/$mpid.$actionId.stderr"
+errorFile="$dir/$mpid.$actionId.error"
+exitCodeMsg="Exit code:"
 
 if [ $preserveArgs == "PRESERVE_ARGS" ]
 then
@@ -41,16 +43,18 @@
     if $cmnd "$@" >>${stdout} 2>>${stderr}; then
         export callbackUrl=`echo ${callbackUrl} | sed -e 's/#status/OK/'`
     else
+        ec=$?
         export callbackUrl=`echo ${callbackUrl} | sed -e 's/#status/ERROR/'`
-        touch $dir/$mpid.$actionId.error
+        echo $exitCodeMsg$ec > $errorFile
     fi
 else
     cmnd="${*}"
     if $cmnd >>${stdout} 2>>${stderr}; then
         export callbackUrl=`echo ${callbackUrl} | sed -e 's/#status/OK/'`
     else
+        ec=$?
         export callbackUrl=`echo ${callbackUrl} | sed -e 's/#status/ERROR/'`
-        touch $dir/$mpid.$actionId.error
+        echo $exitCodeMsg$ec > $errorFile
     fi
 fi
 sleep 1
diff --git a/core/src/test/java/org/apache/oozie/action/ssh/TestSshActionExecutor.java b/core/src/test/java/org/apache/oozie/action/ssh/TestSshActionExecutor.java
index d68aed0..a5e0fbe 100644
--- a/core/src/test/java/org/apache/oozie/action/ssh/TestSshActionExecutor.java
+++ b/core/src/test/java/org/apache/oozie/action/ssh/TestSshActionExecutor.java
@@ -32,6 +32,7 @@
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
+import org.apache.oozie.ErrorCode;
 import org.apache.oozie.WorkflowActionBean;
 import org.apache.oozie.WorkflowJobBean;
 import org.apache.oozie.action.ActionExecutor;
@@ -154,6 +155,7 @@
 
         @Override
         public void setErrorInfo(String str, String exMsg) {
+            action.setErrorInfo(str, exMsg);
         }
     }
 
@@ -603,6 +605,36 @@
         verify(contextMock).setEndData(WorkflowAction.Status.KILLED, "ERROR");
     }
 
+    /**
+     * test {@code SshActionExecutor.check()} where the remote executable returns an error code
+     */
+    public void testCaptureActionErrorStream() throws Exception {
+        WorkflowJobBean workflow = createBaseWorkflowJobBean();
+        final WorkflowActionBean action = new WorkflowActionBean();
+        action.setId("actionId");
+        action.setConf("<ssh xmlns='" + getActionXMLSchema() + "'>" +
+                "<host>localhost</host>" +
+                "<command>exit 123</command>" +
+                "<capture-output/>" +
+                "</ssh>");
+        action.setName("ssh");
+        final SshActionExecutor ssh = new SshActionExecutor();
+        final Context context = new Context(workflow, action);
+        ssh.start(context, action);
+
+        waitFor(30 * 1000, new Predicate() {
+            public boolean evaluate() throws Exception {
+                ssh.check(context, action);
+                return Status.DONE == action.getStatus();
+            }
+        });
+        ssh.end(context, action);
+        assertEquals("Action status is not ERROR", Status.ERROR, action.getStatus());
+        assertEquals("Error code is not what expected", ErrorCode.E1111.toString(), action.getErrorCode());
+        assertEquals("The remote executable exit code and action error message is not what expected",
+                "Exit code:123", action.getErrorMessage());
+    }
+
     @Override
     protected void tearDown() throws Exception {
         if (!isSshPresent()) {
diff --git a/release-log.txt b/release-log.txt
index 550debf..c5cbc5f 100644
--- a/release-log.txt
+++ b/release-log.txt
@@ -1,5 +1,6 @@
 -- Oozie 5.2.0 release (trunk - unreleased)
 
+OOZIE-3405 SSH action shows empty error Message and Error code (matijhs via asalamon74)
 OOZIE-3542 Handle better old Hdfs implementations in ECPolicyDisabler (zsombor dionusos via kmarton)
 OOZIE-3540 Use StringBuilder instead of StringBuffer if concurrent access is not required (zsombor via asalamon74)
 OOZIE-3539 amend Support http proxy/basic authentication in the command line client (zsombor via asalamon74)