This is an automated email from the ASF dual-hosted git repository.

SbloodyS pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new 756f2f248b [Fix-18201][TaskPlugin] Fix RemoteShell task 
NullPointerException and… (#18210)
756f2f248b is described below

commit 756f2f248b2297d5adec8f3c6eceffa092b9ef52
Author: leocook <[email protected]>
AuthorDate: Mon May 4 17:07:27 2026 +0800

    [Fix-18201][TaskPlugin] Fix RemoteShell task NullPointerException and… 
(#18210)
---
 .../plugin/task/remoteshell/RemoteExecutor.java    | 15 ++++---
 .../task/remoteshell/RemoteExecutorTest.java       | 48 +++++++++++-----------
 2 files changed, 34 insertions(+), 29 deletions(-)

diff --git 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java
 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java
index 7271e4524f..705b878fe4 100644
--- 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java
+++ 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java
@@ -35,9 +35,9 @@ import org.apache.sshd.sftp.client.SftpClientFactory;
 import org.apache.sshd.sftp.client.fs.SftpFileSystem;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -235,23 +235,28 @@ public class RemoteExecutor implements AutoCloseable {
     private int runRemoteAndProcessLines(String command, Consumer<String> 
lineConsumer) throws IOException {
         try (
                 ChannelExec channel = getSession().createExecChannel(command);
+                ByteArrayOutputStream out = new ByteArrayOutputStream();
                 ByteArrayOutputStream err = new ByteArrayOutputStream()) {
-            InputStream out = channel.getInvertedOut();
+            channel.setOut(out);
             channel.setErr(err);
             channel.open();
+            channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0);
             int readLines = 0;
-            try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(out, StandardCharsets.UTF_8))) {
+            try (
+                    BufferedReader reader = new BufferedReader(
+                            new InputStreamReader(new 
ByteArrayInputStream(out.toByteArray()),
+                                    StandardCharsets.UTF_8))) {
                 String line;
                 while ((line = reader.readLine()) != null) {
                     readLines++;
                     lineConsumer.accept(line);
                 }
             }
-            channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0);
             Integer exitStatus = channel.getExitStatus();
             if (exitStatus == null || exitStatus != 0) {
                 throw new TaskException(
-                        "Remote shell task error, exitStatus: " + exitStatus + 
" error message: " + err);
+                        "Remote shell task error, exitStatus: " + exitStatus + 
" error message: "
+                                + new String(err.toByteArray(), 
StandardCharsets.UTF_8));
             }
             return readLines;
         }
diff --git 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java
 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java
index 81ae92f67c..cb7352d348 100644
--- 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java
+++ 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java
@@ -17,8 +17,10 @@
 
 package org.apache.dolphinscheduler.plugin.task.remoteshell;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
@@ -32,16 +34,13 @@ import 
org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHDataSourcePara
 import 
org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHDataSourceProcessor;
 import org.apache.dolphinscheduler.plugin.task.api.TaskException;
 
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.io.input.NullInputStream;
 import org.apache.commons.lang3.SystemUtils;
 import org.apache.sshd.client.channel.ChannelExec;
 import org.apache.sshd.client.channel.ClientChannelEvent;
 import org.apache.sshd.client.session.ClientSession;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.EnumSet;
 
@@ -82,6 +81,15 @@ public class RemoteExecutorTest {
         sshConnectionUtilsMockedStatic.close();
     }
 
+    /** Helper: make channel write data to whichever OutputStream is passed to 
setOut(). */
+    private void mockChannelOutput(ChannelExec channel, String data) throws 
IOException {
+        doAnswer(invocation -> {
+            OutputStream out = invocation.getArgument(0);
+            out.write(data.getBytes(StandardCharsets.UTF_8));
+            return null;
+        }).when(channel).setOut(any(OutputStream.class));
+    }
+
     @Test
     void testRunRemote() throws IOException {
         RemoteExecutor remoteExecutor = spy(new 
RemoteExecutor(sshConnectionParam));
@@ -89,16 +97,13 @@ public class RemoteExecutorTest {
         when(clientSession.auth().verify().isSuccess()).thenReturn(true);
         
when(clientSession.createExecChannel(Mockito.anyString())).thenReturn(channel);
         when(channel.getExitStatus()).thenReturn(1);
-        when(channel.getInvertedOut()).thenReturn(new NullInputStream());
         Assertions.assertThrows(TaskException.class, () -> 
remoteExecutor.runRemote("ls -l"));
 
-        // Mock the streaming runRemote to simulate log output
         String output = "total 26392\n" +
                 "dr-xr-xr-x.   6 root root      3072 Aug 15  2023 boot\n" +
                 "drwxr-xr-x   18 root root      3120 Sep 23  2023 dev\n" +
                 "drwxr-xr-x.  91 root root      4096 Sep 23  2023 etc\n";
-        InputStream inputStream = IOUtils.toInputStream(output, 
StandardCharsets.UTF_8);
-        when(channel.getInvertedOut()).thenReturn(inputStream);
+        mockChannelOutput(channel, output);
         when(channel.getExitStatus()).thenReturn(0);
         String actualOut = Assertions.assertDoesNotThrow(() -> 
remoteExecutor.runRemote("ls -l"));
         Assertions.assertEquals(output, actualOut);
@@ -157,7 +162,6 @@ public class RemoteExecutorTest {
     void getAllRemotePidStr() throws IOException {
 
         RemoteExecutor remoteExecutor = spy(new 
RemoteExecutor(sshConnectionParam));
-        // Mock pstree output based on OS
         if (SystemUtils.IS_OS_MAC) {
             doReturn("-+= 9527 root\n \\-+= 9528 
root").when(remoteExecutor).runRemote(anyString());
         } else {
@@ -186,37 +190,32 @@ public class RemoteExecutorTest {
         String taskId = "1234";
         ChannelExec channel = Mockito.mock(ChannelExec.class, 
RETURNS_DEEP_STUBS);
 
-        // Mock getTaskPid to control the loop, return a valid pid 2 times, 
then return empty
         doReturn("9527")
                 .doReturn("9527")
                 .doReturn("").when(remoteExecutor).getTaskPid(taskId);
         when(clientSession.auth().verify().isSuccess()).thenReturn(true);
         when(clientSession.createExecChannel(anyString())).thenReturn(channel);
 
-        // Mock the streaming runRemote to simulate log output
         String logContent = "some log line 1\n"
                 + "echo \"${setValue(my_prop=my_value)}\"\n"
                 + "some log line 2\n";
-        InputStream inputStream = IOUtils.toInputStream(logContent, 
StandardCharsets.UTF_8);
-        when(channel.getInvertedOut()).thenReturn(inputStream);
+        mockChannelOutput(channel, logContent);
         when(channel.getExitStatus()).thenReturn(0);
 
         remoteExecutor.track(taskId);
 
-        // Verify that the output parameter was parsed and stored
         Assertions.assertEquals(1, 
remoteExecutor.getTaskOutputParams().size());
         Assertions.assertEquals("my_value", 
remoteExecutor.getTaskOutputParams().get("my_prop"));
     }
 
     @Test
     void testRunRemoteWithEmptyOutput() throws Exception {
-        // Test empty output scenario (readLines = 0)
         RemoteExecutor remoteExecutor = spy(new 
RemoteExecutor(sshConnectionParam));
         ChannelExec channel = Mockito.mock(ChannelExec.class, 
RETURNS_DEEP_STUBS);
 
         when(clientSession.auth().verify().isSuccess()).thenReturn(true);
         when(clientSession.createExecChannel(anyString())).thenReturn(channel);
-        when(channel.getInvertedOut()).thenReturn(new ByteArrayInputStream(new 
byte[0]));
+        // no mockChannelOutput → setOut() is a no-op → ByteArrayOutputStream 
stays empty
         when(channel.getExitStatus()).thenReturn(0);
         when(channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0))
                 .thenReturn(EnumSet.of(ClientChannelEvent.CLOSED));
@@ -227,39 +226,40 @@ public class RemoteExecutorTest {
 
     @Test
     void testRunRemoteWithNonZeroExitStatus() throws Exception {
-        // Test command failure scenario (exitStatus != 0)
         RemoteExecutor remoteExecutor = spy(new 
RemoteExecutor(sshConnectionParam));
         ChannelExec channel = Mockito.mock(ChannelExec.class, 
RETURNS_DEEP_STUBS);
 
         when(clientSession.auth().verify().isSuccess()).thenReturn(true);
         when(clientSession.createExecChannel(anyString())).thenReturn(channel);
-        when(channel.getInvertedOut()).thenReturn(IOUtils.toInputStream("error 
output", StandardCharsets.UTF_8));
+        mockChannelOutput(channel, "partial output before failure\n");
         when(channel.getExitStatus()).thenReturn(1);
         when(channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0))
                 .thenReturn(EnumSet.of(ClientChannelEvent.CLOSED));
 
-        Assertions.assertThrows(TaskException.class, () -> 
remoteExecutor.runRemote("failing_command"));
+        TaskException ex =
+                Assertions.assertThrows(TaskException.class, () -> 
remoteExecutor.runRemote("failing_command"));
+        Assertions.assertTrue(ex.getMessage().contains("exitStatus: 1"));
     }
 
     @Test
     void testRunRemoteWithNullExitStatus() throws Exception {
-        // Test null exitStatus scenario
         RemoteExecutor remoteExecutor = spy(new 
RemoteExecutor(sshConnectionParam));
         ChannelExec channel = Mockito.mock(ChannelExec.class, 
RETURNS_DEEP_STUBS);
 
         when(clientSession.auth().verify().isSuccess()).thenReturn(true);
         when(clientSession.createExecChannel(anyString())).thenReturn(channel);
-        when(channel.getInvertedOut()).thenReturn(IOUtils.toInputStream("some 
output", StandardCharsets.UTF_8));
+        mockChannelOutput(channel, "some output\n");
         when(channel.getExitStatus()).thenReturn(null);
         when(channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0))
                 .thenReturn(EnumSet.of(ClientChannelEvent.CLOSED));
 
-        Assertions.assertThrows(TaskException.class, () -> 
remoteExecutor.runRemote("command"));
+        TaskException ex =
+                Assertions.assertThrows(TaskException.class, () -> 
remoteExecutor.runRemote("command"));
+        Assertions.assertTrue(ex.getMessage().contains("exitStatus: null"));
     }
 
     @Test
     void testTrackWithEmptyLogOutput() throws Exception {
-        // Test track with empty log output (readLines = 0 scenario in track 
loop)
         RemoteExecutor remoteExecutor = spy(new 
RemoteExecutor(sshConnectionParam));
         String taskId = "1234";
         ChannelExec channel = Mockito.mock(ChannelExec.class, 
RETURNS_DEEP_STUBS);
@@ -267,7 +267,7 @@ public class RemoteExecutorTest {
         doReturn("9527").doReturn("").when(remoteExecutor).getTaskPid(taskId);
         when(clientSession.auth().verify().isSuccess()).thenReturn(true);
         when(clientSession.createExecChannel(anyString())).thenReturn(channel);
-        when(channel.getInvertedOut()).thenReturn(new ByteArrayInputStream(new 
byte[0]));
+        // no output → readLines=0 → sleep once, then getTaskPid returns "" → 
exit
         when(channel.getExitStatus()).thenReturn(0);
         when(channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0))
                 .thenReturn(EnumSet.of(ClientChannelEvent.CLOSED));

Reply via email to