]> source.dussan.org Git - sonar-scanner-cli.git/commitdiff
SONARPLUGINS-3012 Add possibility to stop the forked runner process
authoralpar <alpar.gal@gmail.com>
Mon, 24 Jun 2013 21:04:50 +0000 (23:04 +0200)
committerJulien HENRY <julien.henry@sonarsource.com>
Wed, 26 Jun 2013 15:31:54 +0000 (17:31 +0200)
sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java
sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java
sonar-runner-api/src/main/java/org/sonar/runner/api/ProcessMonitor.java [new file with mode: 0644]
sonar-runner-api/src/test/java/org/sonar/runner/api/CommandExecutorTest.java
sonar-runner-api/src/test/java/org/sonar/runner/api/ForkedRunnerTest.java

index ad56103f9f77471116eb94e9934b493328e2dc68..801c485f0e1328ed67b6f2a2bbdb6dfc19631968 100644 (file)
@@ -25,7 +25,12 @@ import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.util.concurrent.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Synchronously execute a native command line. It's much more limited than the Apache Commons Exec library.
@@ -45,7 +50,7 @@ class CommandExecutor {
     return INSTANCE;
   }
 
-  int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds) {
+  int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds, ProcessMonitor processMonitor) {
     ExecutorService executorService = null;
     Process process = null;
     StreamGobbler outputGobbler = null;
@@ -62,8 +67,12 @@ class CommandExecutor {
       errorGobbler.start();
 
       executorService = Executors.newSingleThreadExecutor();
-      Future<Integer> ft = executeProcess(executorService, process);
-      int exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS);
+      final Future<Integer> futureTask = executeProcess(executorService, process);
+      if (processMonitor != null) {
+        monitorProcess(processMonitor, executorService, process);
+      }
+
+      int exitCode = futureTask.get(timeoutMilliseconds, TimeUnit.MILLISECONDS);
       waitUntilFinish(outputGobbler);
       waitUntilFinish(errorGobbler);
       verifyGobbler(command, outputGobbler, "stdOut");
@@ -90,6 +99,24 @@ class CommandExecutor {
     }
   }
 
+  private void monitorProcess(final ProcessMonitor processMonitor, final ExecutorService executor, final Process process) {
+    new Thread() {
+      @Override
+      public void run() {
+        while (!executor.isTerminated()) {
+          if (processMonitor.stop()) {
+            process.destroy();
+          }
+          try {
+            Thread.sleep(100);
+          } catch (InterruptedException e) {
+            // ignore
+          }
+        }
+      }
+    }.start();
+  }
+
   private Future<Integer> executeProcess(ExecutorService executorService, Process process) {
     final Process finalProcess = process;
     return executorService.submit(new Callable<Integer>() {
index 1a0e0095154713b6d67c664b2a01457c5c8269e0..7a35f17db62e6930aabe8865d8737340878779ad 100644 (file)
@@ -25,7 +25,6 @@ import org.sonar.runner.impl.BatchLauncherMain;
 import org.sonar.runner.impl.JarExtractor;
 
 import javax.annotation.Nullable;
-
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.OutputStream;
@@ -43,6 +42,7 @@ import java.util.Map;
 public class ForkedRunner extends Runner<ForkedRunner> {
 
   private static final int ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
+  private static final int TERMINATED_STATUS = 143;
 
   private final Map<String, String> jvmEnvVariables = new HashMap<String, String>();
   private final List<String> jvmArguments = new ArrayList<String>();
@@ -51,9 +51,16 @@ public class ForkedRunner extends Runner<ForkedRunner> {
   private final JarExtractor jarExtractor;
   private final CommandExecutor commandExecutor;
 
-  ForkedRunner(JarExtractor jarExtractor, CommandExecutor commandExecutor) {
+  private ProcessMonitor processMonitor;
+
+  ForkedRunner(JarExtractor jarExtractor, CommandExecutor commandExecutor, ProcessMonitor processMonitor) {
     this.jarExtractor = jarExtractor;
     this.commandExecutor = commandExecutor;
+    this.processMonitor = processMonitor;
+  }
+
+  ForkedRunner(JarExtractor jarExtractor, CommandExecutor commandExecutor) {
+    this(jarExtractor, commandExecutor, null);
   }
 
   /**
@@ -63,6 +70,13 @@ public class ForkedRunner extends Runner<ForkedRunner> {
     return new ForkedRunner(new JarExtractor(), CommandExecutor.create());
   }
 
+  /**
+   * Create new instance. Never return null.
+   */
+  public static ForkedRunner create(ProcessMonitor processMonitor) {
+    return new ForkedRunner(new JarExtractor(), CommandExecutor.create(), processMonitor);
+  }
+
   /**
    * Path to the java executable. The JVM of the client app is used by default
    * (see the system property java.home)
@@ -140,11 +154,11 @@ public class ForkedRunner extends Runner<ForkedRunner> {
       javaExecutable = new Os().thisJavaExe().getAbsolutePath();
     }
     Command command = Command.builder()
-        .setExecutable(javaExecutable)
-        .addEnvVariables(jvmEnvVariables)
-        .addArguments(jvmArguments)
-        .addArguments("-cp", jarFile.getAbsolutePath(), BatchLauncherMain.class.getName(), propertiesFile.getAbsolutePath())
-        .build();
+      .setExecutable(javaExecutable)
+      .addEnvVariables(jvmEnvVariables)
+      .addArguments(jvmArguments)
+      .addArguments("-cp", jarFile.getAbsolutePath(), BatchLauncherMain.class.getName(), propertiesFile.getAbsolutePath())
+      .build();
     return new ForkCommand(command, jarFile, propertiesFile);
   }
 
@@ -176,8 +190,11 @@ public class ForkedRunner extends Runner<ForkedRunner> {
     if (stdErr == null) {
       stdErr = new PrintStreamConsumer(System.err);
     }
-    int status = commandExecutor.execute(forkCommand.command, stdOut, stdErr, ONE_DAY_IN_MILLISECONDS);
-    if (status != 0) {
+    int status = commandExecutor.execute(forkCommand.command, stdOut, stdErr, ONE_DAY_IN_MILLISECONDS, processMonitor);
+
+    if (status == TERMINATED_STATUS) {
+       stdOut.consumeLine(String.format("Sonar runner terminated with exit code %d", status));
+    } else if (status != 0) {
       throw new IllegalStateException("Error status [command: " + forkCommand.command + "]: " + status);
     }
   }
diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/ProcessMonitor.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/ProcessMonitor.java
new file mode 100644 (file)
index 0000000..99bfe76
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sonar Runner - API
+ * Copyright (C) 2011 SonarSource
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.runner.api;
+
+/**
+ * To be used with {@link ForkedRunner}
+ * @since 2.3
+ */
+public interface ProcessMonitor {
+
+  /**
+   * {@link ForkedRunner} will poll this method periodically and if true is returned
+   * then forked SonarQube Runner process will be killed.
+   */
+  boolean stop();
+}
index ecc6999f9278eb46a75815c3c7853180757ac4ba..e3607952e21ed58c2ce871b8d814f1f6368b1046 100644 (file)
@@ -32,9 +32,14 @@ import java.io.IOException;
 
 import static org.fest.assertions.Assertions.assertThat;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
 
 public class CommandExecutorTest {
 
+  public static final ProcessMonitor ACTIVITY_CONTROLLER = mock(ProcessMonitor.class);
+
+  public static final int PROCESS_TERMINATED_CODE = 143;
+
   @Rule
   public TemporaryFolder tempFolder = new TemporaryFolder();
 
@@ -68,7 +73,7 @@ public class CommandExecutorTest {
       }
     };
     Command command = Command.builder().setExecutable(getScript("output")).setDirectory(workDir).build();
-    int exitCode = CommandExecutor.create().execute(command, stdOutConsumer, stdErrConsumer, 1000L);
+    int exitCode = CommandExecutor.create().execute(command, stdOutConsumer, stdErrConsumer, 1000L, ACTIVITY_CONTROLLER);
     assertThat(exitCode).isEqualTo(0);
 
     String stdOut = stdOutBuilder.toString();
@@ -84,7 +89,7 @@ public class CommandExecutorTest {
     Command command = Command.builder().setExecutable(getScript("output")).setDirectory(workDir).build();
     thrown.expect(CommandException.class);
     thrown.expectMessage("Error inside stdOut stream");
-    CommandExecutor.create().execute(command, BAD_CONSUMER, NOP_CONSUMER, 1000L);
+    CommandExecutor.create().execute(command, BAD_CONSUMER, NOP_CONSUMER, 1000L, ACTIVITY_CONTROLLER);
   }
 
   @Test
@@ -92,7 +97,7 @@ public class CommandExecutorTest {
     Command command = Command.builder().setExecutable(getScript("output")).setDirectory(workDir).build();
     thrown.expect(CommandException.class);
     thrown.expectMessage("Error inside stdErr stream");
-    CommandExecutor.create().execute(command, NOP_CONSUMER, BAD_CONSUMER, 1000L);
+    CommandExecutor.create().execute(command, NOP_CONSUMER, BAD_CONSUMER, 1000L, ACTIVITY_CONTROLLER);
   }
 
   private static final StreamConsumer NOP_CONSUMER = new StreamConsumer() {
@@ -115,7 +120,7 @@ public class CommandExecutorTest {
         .addArguments("1")
         .setEnvVariable("ENVVAR", "2")
         .build();
-    int exitCode = CommandExecutor.create().execute(command, stdout, stderr, 1000L);
+    int exitCode = CommandExecutor.create().execute(command, stdout, stderr, 1000L, ACTIVITY_CONTROLLER);
     assertThat(exitCode).isEqualTo(0);
     File logFile = new File(workDir, "echo.log");
     assertThat(logFile).exists();
@@ -131,7 +136,7 @@ public class CommandExecutorTest {
     long start = System.currentTimeMillis();
     try {
       Command command = Command.builder().setExecutable(executable).setDirectory(workDir).build();
-      CommandExecutor.create().execute(command, stdout, stderr, 300L);
+      CommandExecutor.create().execute(command, stdout, stderr, 300L, ACTIVITY_CONTROLLER);
       fail();
     } catch (CommandException e) {
       long duration = System.currentTimeMillis() - start;
@@ -141,11 +146,30 @@ public class CommandExecutorTest {
     }
   }
 
+  @Test
+  public void test_should_stop_if_requested() throws Exception {
+
+    String executable = getScript("forever");
+    Command command = Command.builder().setExecutable(executable).setDirectory(workDir).build();
+
+    final long start = System.currentTimeMillis();
+    int result = CommandExecutor.create().execute(command, stdout, stderr, 2000L, new ProcessMonitor() {
+      @Override
+      public boolean stop() {
+        // kill after 1 seconds
+        return System.currentTimeMillis() - start > 1000;
+      }
+    });
+
+    assertThat(System.currentTimeMillis() - start).isGreaterThan(1000);
+    assertThat(result).isEqualTo(PROCESS_TERMINATED_CODE);
+  }
+
   @Test
   public void should_fail_if_script_not_found() {
     thrown.expect(CommandException.class);
     Command command = Command.builder().setExecutable("notfound").setDirectory(workDir).build();
-    CommandExecutor.create().execute(command, stdout, stderr, 1000L);
+    CommandExecutor.create().execute(command, stdout, stderr, 1000L, ACTIVITY_CONTROLLER);
   }
 
   private static String getScript(String name) throws IOException {
index f8784e363d340ca52608003aebd4dc1294770241..de613e63bba8c47ce0d3c5b48def6c472b4ee0c3 100644 (file)
@@ -46,23 +46,34 @@ public class ForkedRunnerTest {
   @Rule
   public TemporaryFolder temp = new TemporaryFolder();
 
+  private ProcessMonitor processMonitor = mock(ProcessMonitor.class);
+
+  private final StreamConsumer out = mock(StreamConsumer.class);
+
+  private final StreamConsumer err = mock(StreamConsumer.class);
+
   @Test
   public void should_create_forked_runner() {
     ForkedRunner runner = ForkedRunner.create();
     assertThat(runner).isNotNull().isInstanceOf(ForkedRunner.class);
   }
 
+  @Test
+  public void should_create_forked_runner_with_activity_controller() {
+    ForkedRunner runner = ForkedRunner.create(processMonitor);
+    assertThat(runner).isNotNull().isInstanceOf(ForkedRunner.class);
+  }
+
   @Test
   public void should_print_to_standard_outputs_by_default() throws IOException {
-    JarExtractor jarExtractor = mock(JarExtractor.class);
-    final File jar = temp.newFile();
-    when(jarExtractor.extractToTemp("sonar-runner-impl")).thenReturn(jar);
+    JarExtractor jarExtractor = createMockExtractor();
 
     CommandExecutor commandExecutor = mock(CommandExecutor.class);
     ForkedRunner runner = new ForkedRunner(jarExtractor, commandExecutor);
     runner.execute();
 
-    verify(commandExecutor).execute(any(Command.class), argThat(new StdConsumerMatcher(System.out)), argThat(new StdConsumerMatcher(System.err)), anyLong());
+    verify(commandExecutor).execute(any(Command.class), argThat(new StdConsumerMatcher(System.out)), argThat(new StdConsumerMatcher(System.err)), anyLong(),
+        any(ProcessMonitor.class));
   }
 
   static class StdConsumerMatcher extends ArgumentMatcher<StreamConsumer> {
@@ -79,11 +90,9 @@ public class ForkedRunnerTest {
 
   @Test
   public void properties_should_be_written_in_temp_file() throws Exception {
-    JarExtractor jarExtractor = mock(JarExtractor.class);
-    final File jar = temp.newFile();
-    when(jarExtractor.extractToTemp("sonar-runner-impl")).thenReturn(jar);
+    JarExtractor jarExtractor = createMockExtractor();
 
-    ForkedRunner runner = new ForkedRunner(jarExtractor, mock(CommandExecutor.class));
+    ForkedRunner runner = new ForkedRunner(jarExtractor, mock(CommandExecutor.class), any(ProcessMonitor.class));
     runner.setProperty("sonar.dynamicAnalysis", "false");
     runner.setProperty("sonar.login", "admin");
     runner.addJvmArguments("-Xmx512m");
@@ -138,21 +147,16 @@ public class ForkedRunnerTest {
         assertThat(command.envVariables().get("SONAR_HOME")).isEqualTo("/path/to/sonar");
         return true;
       }
-    }), any(PrintStreamConsumer.class), any(PrintStreamConsumer.class), anyLong());
+    }), any(PrintStreamConsumer.class), any(PrintStreamConsumer.class), anyLong(), any(ProcessMonitor.class));
 
   }
 
   @Test
   public void test_failure_of_java_command() throws IOException {
-    JarExtractor jarExtractor = mock(JarExtractor.class);
-    final File jar = temp.newFile();
-    when(jarExtractor.extractToTemp("sonar-runner-impl")).thenReturn(jar);
-    StreamConsumer out = mock(StreamConsumer.class);
-    StreamConsumer err = mock(StreamConsumer.class);
-    CommandExecutor commandExecutor = mock(CommandExecutor.class);
-    when(commandExecutor.execute(any(Command.class), eq(out), eq(err), anyLong())).thenReturn(3);
+    JarExtractor jarExtractor = createMockExtractor();
+    CommandExecutor commandExecutor = createMockRunnerWithExecutionStatus(3);
 
-    ForkedRunner runner = new ForkedRunner(jarExtractor, commandExecutor);
+    ForkedRunner runner = new ForkedRunner(jarExtractor, commandExecutor, processMonitor);
     runner.setStdOut(out);
     runner.setStdErr(err);
 
@@ -163,4 +167,27 @@ public class ForkedRunnerTest {
       assertThat(e.getMessage()).matches("Error status \\[command: .*java.*\\]: 3");
     }
   }
+
+  private CommandExecutor createMockRunnerWithExecutionStatus(int executionStatus) {
+    CommandExecutor commandExecutor = mock(CommandExecutor.class);
+    when(commandExecutor.execute(any(Command.class), eq(out), eq(err), anyLong(), eq(processMonitor))).thenReturn(executionStatus);
+    return commandExecutor;
+  }
+
+  private JarExtractor createMockExtractor() throws IOException {
+    JarExtractor jarExtractor = mock(JarExtractor.class);
+    final File jar = temp.newFile();
+    when(jarExtractor.extractToTemp("sonar-runner-impl")).thenReturn(jar);
+    return jarExtractor;
+  }
+
+  @Test
+  public void test_runner_was_requested_to_stop() throws Exception {
+
+    ForkedRunner runner = new ForkedRunner(createMockExtractor(), createMockRunnerWithExecutionStatus(143), processMonitor);
+    runner.setStdOut(out);
+    runner.setStdErr(err);
+    runner.execute();
+    verify(out).consumeLine("Sonar runner terminated with exit code 143");
+  }
 }