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.
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;
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");
}
}
+ 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>() {
import org.sonar.runner.impl.JarExtractor;
import javax.annotation.Nullable;
-
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
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>();
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);
}
/**
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)
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);
}
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);
}
}
--- /dev/null
+/*
+ * 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();
+}
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();
}
};
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();
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
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() {
.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();
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;
}
}
+ @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 {
@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> {
@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");
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);
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");
+ }
}