From c0ab38f43aa1a0eddbcead603227ee1497023512 Mon Sep 17 00:00:00 2001 From: alpar Date: Mon, 24 Jun 2013 23:04:50 +0200 Subject: [PATCH] SONARPLUGINS-3012 Add possibility to stop the forked runner process --- .../org/sonar/runner/api/CommandExecutor.java | 35 +++++++++-- .../org/sonar/runner/api/ForkedRunner.java | 35 ++++++++--- .../org/sonar/runner/api/ProcessMonitor.java | 33 ++++++++++ .../sonar/runner/api/CommandExecutorTest.java | 36 +++++++++-- .../sonar/runner/api/ForkedRunnerTest.java | 61 +++++++++++++------ 5 files changed, 164 insertions(+), 36 deletions(-) create mode 100644 sonar-runner-api/src/main/java/org/sonar/runner/api/ProcessMonitor.java diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java index ad56103..801c485 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java @@ -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 ft = executeProcess(executorService, process); - int exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS); + final Future 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 executeProcess(ExecutorService executorService, Process process) { final Process finalProcess = process; return executorService.submit(new Callable() { diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java index 1a0e009..7a35f17 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java @@ -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 { private static final int ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; + private static final int TERMINATED_STATUS = 143; private final Map jvmEnvVariables = new HashMap(); private final List jvmArguments = new ArrayList(); @@ -51,9 +51,16 @@ public class ForkedRunner extends Runner { 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 { 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 { 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 { 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 index 0000000..99bfe76 --- /dev/null +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/ProcessMonitor.java @@ -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(); +} diff --git a/sonar-runner-api/src/test/java/org/sonar/runner/api/CommandExecutorTest.java b/sonar-runner-api/src/test/java/org/sonar/runner/api/CommandExecutorTest.java index ecc6999..e360795 100644 --- a/sonar-runner-api/src/test/java/org/sonar/runner/api/CommandExecutorTest.java +++ b/sonar-runner-api/src/test/java/org/sonar/runner/api/CommandExecutorTest.java @@ -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 { diff --git a/sonar-runner-api/src/test/java/org/sonar/runner/api/ForkedRunnerTest.java b/sonar-runner-api/src/test/java/org/sonar/runner/api/ForkedRunnerTest.java index f8784e3..de613e6 100644 --- a/sonar-runner-api/src/test/java/org/sonar/runner/api/ForkedRunnerTest.java +++ b/sonar-runner-api/src/test/java/org/sonar/runner/api/ForkedRunnerTest.java @@ -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 { @@ -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"); + } } -- 2.39.5