From 8d4fab85509a8864cecb5781fd4bc8d407a5e964 Mon Sep 17 00:00:00 2001 From: simonbrandhof Date: Sun, 13 Mar 2011 19:18:32 +0100 Subject: [PATCH] SONAR-2274 API: add utility class to execute command-lines --- .../org/sonar/api/utils/command/Command.java | 91 ++++++++++++++ .../api/utils/command/CommandException.java | 38 ++++++ .../api/utils/command/CommandExecutor.java | 118 ++++++++++++++++++ .../utils/command/CommandExecutorTest.java | 70 +++++++++++ .../sonar/api/utils/command/CommandTest.java | 48 +++++++ sonar-plugin-api/src/test/scripts/echo.bat | 2 + sonar-plugin-api/src/test/scripts/echo.sh | 2 + sonar-plugin-api/src/test/scripts/forever.bat | 5 + sonar-plugin-api/src/test/scripts/forever.sh | 5 + 9 files changed, 379 insertions(+) create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/utils/command/Command.java create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandException.java create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java create mode 100644 sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandExecutorTest.java create mode 100644 sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandTest.java create mode 100755 sonar-plugin-api/src/test/scripts/echo.bat create mode 100755 sonar-plugin-api/src/test/scripts/echo.sh create mode 100755 sonar-plugin-api/src/test/scripts/forever.bat create mode 100755 sonar-plugin-api/src/test/scripts/forever.sh 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 index 00000000000..74d6a75de9e --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/Command.java @@ -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 arguments = Lists.newArrayList(); + + private Command(String executable) { + this.executable = executable; + } + + public String getExecutable() { + return executable; + } + + public List getArguments() { + return Collections.unmodifiableList(arguments); + } + + public Command addArgument(String arg) { + arguments.add(arg); + return this; + } + + public Command addArguments(List args) { + arguments.addAll(args); + return this; + } + + public Command addArguments(String[] args) { + arguments.addAll(Arrays.asList(args)); + return this; + } + + String[] toStrings() { + List 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 index 00000000000..5f39b6af5a3 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandException.java @@ -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 index 00000000000..dee03149aa2 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/command/CommandExecutor.java @@ -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 call = new Callable() { + public Integer call() throws Exception { + finalProcess.waitFor(); + return finalProcess.exitValue(); + } + }; + + executorService = Executors.newSingleThreadExecutor(); + Future 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 index 00000000000..b0f7bb7ef7b --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandExecutorTest.java @@ -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 index 00000000000..b32d8946440 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/utils/command/CommandTest.java @@ -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 index 00000000000..4c84e2dc8a9 --- /dev/null +++ b/sonar-plugin-api/src/test/scripts/echo.bat @@ -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 index 00000000000..eebd2bd2fe6 --- /dev/null +++ b/sonar-plugin-api/src/test/scripts/echo.sh @@ -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 index 00000000000..0ecf4e9215a --- /dev/null +++ b/sonar-plugin-api/src/test/scripts/forever.bat @@ -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 index 00000000000..c7f008117e2 --- /dev/null +++ b/sonar-plugin-api/src/test/scripts/forever.sh @@ -0,0 +1,5 @@ +#!/bin/sh +while test "notempty" +do + sleep 1 +done \ No newline at end of file -- 2.39.5