]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2274 API: add utility class to execute command-lines
authorsimonbrandhof <simon.brandhof@gmail.com>
Sun, 13 Mar 2011 18:18:32 +0000 (19:18 +0100)
committersimonbrandhof <simon.brandhof@gmail.com>
Sun, 13 Mar 2011 18:18:32 +0000 (19:18 +0100)
sonar-plugin-api/src/main/java/org/sonar/api/utils/command/Command.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandException.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandExecutorTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/scripts/echo.bat [new file with mode: 0755]
sonar-plugin-api/src/test/scripts/echo.sh [new file with mode: 0755]
sonar-plugin-api/src/test/scripts/forever.bat [new file with mode: 0755]
sonar-plugin-api/src/test/scripts/forever.sh [new file with mode: 0755]

diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/Command.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/Command.java
new file mode 100644 (file)
index 0000000..74d6a75
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.utils.command;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @since 2.7
+ */
+public final class Command {
+
+  private String executable;
+  private List<String> arguments = Lists.newArrayList();
+
+  private Command(String executable) {
+    this.executable = executable;
+  }
+
+  public String getExecutable() {
+    return executable;
+  }
+
+  public List<String> getArguments() {
+    return Collections.unmodifiableList(arguments);
+  }
+
+  public Command addArgument(String arg) {
+    arguments.add(arg);
+    return this;
+  }
+
+  public Command addArguments(List<String> args) {
+    arguments.addAll(args);
+    return this;
+  }
+
+  public Command addArguments(String[] args) {
+    arguments.addAll(Arrays.asList(args));
+    return this;
+  }
+
+  String[] toStrings() {
+    List<String> command = Lists.newArrayList();
+    command.add(executable);
+    command.addAll(arguments);
+    return command.toArray(new String[command.size()]);
+  }
+
+  public String toCommandLine() {
+    return Joiner.on(" ").join(toStrings());
+  }
+
+  @Override
+  public String toString() {
+    return toCommandLine();
+  }
+
+  /**
+   * Create a command line without any arguments
+   * @param executable
+   */
+  public static Command create(String executable) {
+    if (StringUtils.isBlank(executable)) {
+      throw new IllegalArgumentException("Command executable can not be blank");
+    }
+    return new Command(executable);
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandException.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandException.java
new file mode 100644 (file)
index 0000000..5f39b6a
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.utils.command;
+
+public final class CommandException extends RuntimeException {
+  private Command command;
+
+  public CommandException(Command command, String message, Throwable throwable) {
+    super(message + " [command: " + command + "]", throwable);
+    this.command = command;
+  }
+
+  public CommandException(Command command, Throwable throwable) {
+    super(throwable);
+    this.command = command;
+  }
+
+  public Command getCommand() {
+    return command;
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java
new file mode 100644 (file)
index 0000000..dee0314
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.utils.command;
+
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.*;
+
+/**
+ * Synchronously execute a native command line. It's much more limited than the Apache Commons Exec library.
+ * For example it does not allow to get process output, to run asynchronously or to automatically quote
+ * command-line arguments.
+ *
+ * @since 2.7
+ */
+public final class CommandExecutor {
+
+  private static final CommandExecutor INSTANCE = new CommandExecutor();
+
+  private CommandExecutor() {
+  }
+
+  public static CommandExecutor create() {
+    // stateless object, so a single singleton can be shared
+    return INSTANCE;
+  }
+
+  public int execute(Command command, long timeoutMilliseconds) {
+    ExecutorService executorService = null;
+    Process process = null;
+    try {
+      LoggerFactory.getLogger(getClass()).debug("Executing command: " + command);
+      ProcessBuilder builder = new ProcessBuilder(command.toStrings());
+      process = builder.start();
+
+      // consume and display the error and output streams
+      StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream());
+      StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream());
+      outputGobbler.start();
+      errorGobbler.start();
+
+      final Process finalProcess = process;
+      Callable<Integer> call = new Callable<Integer>() {
+        public Integer call() throws Exception {
+          finalProcess.waitFor();
+          return finalProcess.exitValue();
+        }
+      };
+
+      executorService = Executors.newSingleThreadExecutor();
+      Future<Integer> ft = executorService.submit(call);
+      return ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS);
+
+    } catch (TimeoutException te) {
+      if (process != null) {
+        process.destroy();
+      }
+      throw new CommandException(command, "Timeout exceeded: " + timeoutMilliseconds + " ms", te);
+
+    } catch (Exception e) {
+      throw new CommandException(command, e);
+
+    } finally {
+      if (executorService != null) {
+        executorService.shutdown();
+      }
+    }
+  }
+
+  private static class StreamGobbler extends Thread {
+    InputStream is;
+
+    StreamGobbler(InputStream is) {
+      this.is = is;
+    }
+
+    public void run() {
+      Logger logger = LoggerFactory.getLogger(CommandExecutor.class);
+      InputStreamReader isr = new InputStreamReader(is);
+      BufferedReader br = new BufferedReader(isr);
+      try {
+        String line;
+        while ((line = br.readLine()) != null) {
+          logger.info(line);
+        }
+      } catch (IOException ioe) {
+        logger.error("Error while reading Obeo analyzer output", ioe);
+
+      } finally {
+        IOUtils.closeQuietly(br);
+        IOUtils.closeQuietly(isr);
+      }
+    }
+  }
+}
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandExecutorTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandExecutorTest.java
new file mode 100644 (file)
index 0000000..b0f7bb7
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.utils.command;
+
+import org.apache.commons.lang.SystemUtils;
+import org.junit.Test;
+
+import java.io.File;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.number.OrderingComparisons.greaterThanOrEqualTo;
+import static org.hamcrest.number.OrderingComparisons.lessThan;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+public class CommandExecutorTest {
+
+  @Test
+  public void shouldEchoArguments() {
+    String executable = getScript("echo");
+    int exitCode = CommandExecutor.create().execute(Command.create(executable), 1000L);
+    assertThat(exitCode, is(0));
+  }
+
+  @Test
+  public void shouldStopWithTimeout() {
+    String executable = getScript("forever");
+    long start = System.currentTimeMillis();
+    try {
+      CommandExecutor.create().execute(Command.create(executable), 100L);
+      fail();
+    } catch (CommandException e) {
+      long duration = System.currentTimeMillis()-start;
+      assertThat(e.getMessage(), duration, greaterThanOrEqualTo(100L));
+      assertThat(e.getMessage(), duration, lessThan(1000L));
+    }
+  }
+
+  @Test(expected = CommandException.class)
+  public void shouldFailIfScriptNotFound() {
+    CommandExecutor.create().execute(Command.create("notfound"), 1000L);
+  }
+
+  private String getScript(String name) {
+    String filename;
+    if (SystemUtils.IS_OS_WINDOWS) {
+      filename = name + ".bat";
+    } else {
+      filename = name + ".sh";
+    }
+    return new File("src/test/scripts/" + filename).getPath();
+  }
+}
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandTest.java
new file mode 100644 (file)
index 0000000..b32d894
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.utils.command;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public class CommandTest {
+
+  @Test(expected = IllegalArgumentException.class)
+  public void shouldFailWhenBlankExecutable() throws Exception {
+    Command.create("  ");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void shouldFailWhenNullExecutable() throws Exception {
+    Command.create(null);
+  }
+
+  @Test
+  public void shouldCreateCommand() throws Exception {
+    Command command = Command.create("java");
+    command.addArgument("-Xmx512m");
+    command.addArgument("-Dfoo=bar");
+    assertThat(command.getExecutable(), is("java"));
+    assertThat(command.getArguments().size(), is(2));
+    assertThat(command.toCommandLine(), is("java -Xmx512m -Dfoo=bar"));
+  }
+}
diff --git a/sonar-plugin-api/src/test/scripts/echo.bat b/sonar-plugin-api/src/test/scripts/echo.bat
new file mode 100755 (executable)
index 0000000..4c84e2d
--- /dev/null
@@ -0,0 +1,2 @@
+@ECHO OFF
+@ECHO "Parameter: " + %1
diff --git a/sonar-plugin-api/src/test/scripts/echo.sh b/sonar-plugin-api/src/test/scripts/echo.sh
new file mode 100755 (executable)
index 0000000..eebd2bd
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "Parameter: " + $1
\ No newline at end of file
diff --git a/sonar-plugin-api/src/test/scripts/forever.bat b/sonar-plugin-api/src/test/scripts/forever.bat
new file mode 100755 (executable)
index 0000000..0ecf4e9
--- /dev/null
@@ -0,0 +1,5 @@
+@ECHO OFF
+
+:LOOP
+  @ping 127.0.0.1 -n 2 -w 1000 > nul
+GOTO LOOP
diff --git a/sonar-plugin-api/src/test/scripts/forever.sh b/sonar-plugin-api/src/test/scripts/forever.sh
new file mode 100755 (executable)
index 0000000..c7f0081
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/sh
+while test "notempty"
+do
+  sleep 1
+done
\ No newline at end of file