diff options
Diffstat (limited to 'sonar-runner-api/src/main/java/org/sonar')
20 files changed, 866 insertions, 902 deletions
diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/Command.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/Command.java deleted file mode 100644 index b427c14..0000000 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/Command.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * SonarQube 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; - -import java.io.File; -import java.util.*; - -class Command { - private final String executable; - private final List<String> arguments; - private final Map<String, String> env; - private final File directory; - - private Command(Builder builder) { - this.executable = builder.executable; - this.arguments = Collections.unmodifiableList(builder.arguments); - this.env = Collections.unmodifiableMap(builder.env); - this.directory = builder.directory; - } - - File directory() { - return directory; - } - - String executable() { - return executable; - } - - List<String> arguments() { - return arguments; - } - - /** - * Environment variables that are propagated during command execution. - * - * @return a non-null and immutable map of variables - */ - Map<String, String> envVariables() { - return env; - } - - String[] toStrings() { - String[] strings = new String[1 + arguments.size()]; - strings[0] = executable; - for (int index = 0; index < arguments.size(); index++) { - strings[index + 1] = arguments.get(index); - } - return strings; - } - - @Override - public String toString() { - return Utils.join(toStrings(), " "); - } - - static Builder builder() { - return new Builder(); - } - - static class Builder { - private String executable; - private final List<String> arguments = new ArrayList<String>(); - private final Map<String, String> env = new HashMap<String, String>(); - private File directory; - - private Builder() { - } - - Builder setExecutable(String s) { - this.executable = s; - return this; - } - - Builder addArguments(String... args) { - return addArguments(Arrays.asList(args)); - } - - Builder addArguments(List<String> args) { - for (String arg : args) { - if (arg != null && !"".equals(arg.trim())) { - arguments.add(arg); - } - } - return this; - } - - Builder setEnvVariable(String key, String value) { - env.put(key, value); - return this; - } - - Builder addEnvVariables(Map<String, String> map) { - env.putAll(map); - return this; - } - - Builder setDirectory(File d) { - this.directory = d; - return this; - } - - Command build() { - if (executable == null) { - throw new IllegalArgumentException("Command executable is not defined"); - } - return new Command(this); - } - } -} 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 deleted file mode 100644 index 27140b3..0000000 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * SonarQube 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; - -import org.sonar.runner.impl.Logs; - -import javax.annotation.Nullable; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -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. - * For example it does not allow to run asynchronously or to automatically quote command-line arguments. - * - * @since 2.7 - */ -class CommandExecutor { - - private static final CommandExecutor INSTANCE = new CommandExecutor(); - - private CommandExecutor() { - } - - static CommandExecutor create() { - // stateless object, so a single singleton can be shared - return INSTANCE; - } - - int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds, @Nullable ProcessMonitor processMonitor) { - ExecutorService executorService = null; - Process process = null; - StreamGobbler outputGobbler = null; - StreamGobbler errorGobbler = null; - try { - ProcessBuilder builder = new ProcessBuilder(command.toStrings()); - builder.directory(command.directory()); - builder.environment().putAll(command.envVariables()); - process = builder.start(); - - outputGobbler = new StreamGobbler(process.getInputStream(), stdOut); - errorGobbler = new StreamGobbler(process.getErrorStream(), stdErr); - outputGobbler.start(); - errorGobbler.start(); - - executorService = Executors.newSingleThreadExecutor(); - 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"); - verifyGobbler(command, errorGobbler, "stdErr"); - return exitCode; - - } catch (TimeoutException te) { - if (process != null) { - process.destroy(); - } - throw new CommandException("Timeout exceeded: " + timeoutMilliseconds + " ms", command, te); - - } catch (CommandException e) { - throw e; - - } catch (Exception e) { - throw new CommandException("Fail to execute command", command, e); - - } finally { - waitUntilFinish(outputGobbler); - waitUntilFinish(errorGobbler); - closeStreams(process); - if (executorService != null) { - executorService.shutdown(); - } - } - } - - private static 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>() { - @Override - public Integer call() throws InterruptedException { - return finalProcess.waitFor(); - } - }); - } - - private static void verifyGobbler(Command command, StreamGobbler gobbler, String type) { - if (gobbler.getException() != null) { - throw new CommandException("Error inside " + type + " stream", command, gobbler.getException()); - } - } - - private static void closeStreams(Process process) { - if (process != null) { - Utils.closeQuietly(process.getInputStream()); - Utils.closeQuietly(process.getInputStream()); - Utils.closeQuietly(process.getOutputStream()); - Utils.closeQuietly(process.getErrorStream()); - } - } - - private static void waitUntilFinish(StreamGobbler thread) { - if (thread != null) { - try { - thread.join(); - } catch (InterruptedException e) { - Logs.error("InterruptedException while waiting finish of " + thread.toString(), e); - } - } - } - - private static class StreamGobbler extends Thread { - private final InputStream is; - private final StreamConsumer consumer; - private volatile Exception exception; - - StreamGobbler(InputStream is, StreamConsumer consumer) { - super("ProcessStreamGobbler"); - this.is = is; - this.consumer = consumer; - } - - @Override - public void run() { - try (InputStreamReader isr = new InputStreamReader(is); - BufferedReader br = new BufferedReader(isr)) { - String line; - while ((line = br.readLine()) != null) { - consumeLine(line); - } - } catch (IOException ioe) { - exception = ioe; - - } - } - - private void consumeLine(String line) { - if (exception == null) { - try { - consumer.consumeLine(line); - } catch (Exception e) { - exception = e; - } - } - } - - public Exception getException() { - return exception; - } - } -} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/Dirs.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/Dirs.java index 2ab4d6c..c592237 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/Dirs.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/Dirs.java @@ -19,17 +19,19 @@ */ package org.sonar.runner.api; -import org.sonar.runner.impl.Logs; - import java.io.File; import java.util.Properties; +import org.sonar.home.cache.Logger; class Dirs { - private Dirs() { + private final Logger logger; + + Dirs(Logger logger) { + this.logger = logger; } - static void init(Properties p) { + void init(Properties p) { boolean onProject = Utils.taskRequiresProject(p); if (onProject) { initProjectDirs(p); @@ -38,7 +40,7 @@ class Dirs { } } - private static void initProjectDirs(Properties p) { + private void initProjectDirs(Properties p) { String path = p.getProperty(ScanProperties.PROJECT_BASEDIR, "."); File projectDir = new File(path); if (!projectDir.isDirectory()) { @@ -59,7 +61,7 @@ class Dirs { } Utils.deleteQuietly(workDir); p.setProperty(RunnerProperties.WORK_DIR, workDir.getAbsolutePath()); - Logs.info("Work directory: " + workDir.getAbsolutePath()); + logger.info("Work directory: " + workDir.getAbsolutePath()); } /** diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/EmbeddedRunner.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/EmbeddedRunner.java index 4883ea0..d48ca77 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/EmbeddedRunner.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/EmbeddedRunner.java @@ -19,110 +19,202 @@ */ package org.sonar.runner.api; -import org.sonar.home.log.LogListener; - -import org.sonar.runner.impl.Logs; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.Properties; +import javax.annotation.Nullable; +import org.sonar.home.cache.Logger; import org.sonar.runner.batch.IsolatedLauncher; +import org.sonar.runner.impl.InternalProperties; import org.sonar.runner.impl.IsolatedLauncherFactory; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Properties; - /** - * Implementation of {@link Runner} that is executed in the same JVM. The application can inject - * some extensions into Sonar IoC container (see {@link #addExtensions(Object...)}. It can be - * used for example in the Maven Sonar plugin to register Maven components like MavenProject - * or MavenPluginExecutor. + * Entry point to run SonarQube analysis programmatically. * @since 2.2 */ -public class EmbeddedRunner extends Runner<EmbeddedRunner> { +public class EmbeddedRunner { private final IsolatedLauncherFactory launcherFactory; private IsolatedLauncher launcher; - private final List<Object> extensions = new ArrayList<Object>(); - private static final String MASK_RULES_PROP = "sonarRunner.maskRules"; + private final LogOutput logOutput; + private final Properties globalProperties = new Properties(); + private final Logger logger; - EmbeddedRunner(IsolatedLauncherFactory bl) { + EmbeddedRunner(IsolatedLauncherFactory bl, Logger logger, LogOutput logOutput) { + this.logger = logger; this.launcherFactory = bl; + this.logOutput = logOutput; } - /** - * Create a new instance. - */ - public static EmbeddedRunner create() { - return new EmbeddedRunner(new IsolatedLauncherFactory()); + public static EmbeddedRunner create(final LogOutput logOutput) { + Logger logger = new Logger() { + + @Override + public void warn(String msg) { + logOutput.log(msg, LogOutput.Level.WARN); + } + + @Override + public void info(String msg) { + logOutput.log(msg, LogOutput.Level.INFO); + } + + @Override + public void error(String msg, Throwable t) { + StringWriter errors = new StringWriter(); + t.printStackTrace(new PrintWriter(errors)); + logOutput.log(msg + "\n" + errors.toString(), LogOutput.Level.ERROR); + } + + @Override + public void error(String msg) { + logOutput.log(msg, LogOutput.Level.ERROR); + } + + @Override + public void debug(String msg) { + logOutput.log(msg, LogOutput.Level.DEBUG); + } + }; + return new EmbeddedRunner(new IsolatedLauncherFactory(logger), logger, logOutput); + } + + public Properties globalProperties() { + Properties clone = new Properties(); + clone.putAll(globalProperties); + return clone; } - public static EmbeddedRunner create(LogListener logListener) { - Logs.setListener(logListener); - return new EmbeddedRunner(new IsolatedLauncherFactory()); + /** + * Declare Sonar properties, for example sonar.projectKey=>foo. + * + * @see #setProperty(String, String) + */ + public EmbeddedRunner addGlobalProperties(Properties p) { + globalProperties.putAll(p); + return this; } /** - * Sonar is executed in an almost fully isolated classloader (mask everything by default). This method allows to unmask some classes based on - * a prefix of their fully qualified name. It is related to the extensions provided by {@link #addExtensions(Object...)}. - * Complex mask/unmask rules can be defined by chaining several ordered calls to {@link #unmask(String)} and {@link #mask(String)}. - * Registered mask/unmask rules are considered in their registration order and as soon as a matching prefix is found - * the class is masked/unmasked accordingly. - * If no matching prefix can be found then by default class is masked. + * Declare a SonarQube property. + * + * @see RunnerProperties + * @see ScanProperties */ - public EmbeddedRunner unmask(String fqcnPrefix) { - return addMaskRule("UNMASK", fqcnPrefix); + public EmbeddedRunner setGlobalProperty(String key, String value) { + globalProperties.setProperty(key, value); + return this; + } + + public String globalProperty(String key, @Nullable String defaultValue) { + return globalProperties.getProperty(key, defaultValue); } /** - * @see EmbeddedRunner#unmask(String) + * User-agent used in the HTTP requests to the SonarQube server */ - public EmbeddedRunner mask(String fqcnPrefix) { - return addMaskRule("MASK", fqcnPrefix); + public EmbeddedRunner setApp(String app, String version) { + setGlobalProperty(InternalProperties.RUNNER_APP, app); + setGlobalProperty(InternalProperties.RUNNER_APP_VERSION, version); + return this; + } + + public String app() { + return globalProperty(InternalProperties.RUNNER_APP, null); + } + + public String appVersion() { + return globalProperty(InternalProperties.RUNNER_APP_VERSION, null); } - private EmbeddedRunner addMaskRule(String type, String fqcnPrefix) { - String existingRules = globalProperty(MASK_RULES_PROP, ""); - if (!"".equals(existingRules)) { - existingRules += ","; + public void runAnalysis(Properties analysisProperties) { + Properties copy = new Properties(); + copy.putAll(analysisProperties); + initAnalysisProperties(copy); + + String dumpToFile = copy.getProperty(InternalProperties.RUNNER_DUMP_TO_FILE); + if (dumpToFile != null) { + File dumpFile = new File(dumpToFile); + Utils.writeProperties(dumpFile, copy); + logger.info("Simulation mode. Configuration written to " + dumpFile.getAbsolutePath()); + } else { + doExecute(copy); } - existingRules += type + "|" + fqcnPrefix; - return setGlobalProperty(MASK_RULES_PROP, existingRules); + } + + public void start() { + initGlobalDefaultValues(); + doStart(); + } + + public void stop() { + doStop(); } /** - * @deprecated since 2.3 use {@link #unmask(String)} + * @deprecated since 2.5 use {@link #start()}, {@link #runAnalysis(Properties)} and then {@link #stop()} */ @Deprecated - public EmbeddedRunner setUnmaskedPackages(String... packages) { - for (String packagePrefix : packages) { - unmask(packagePrefix + "."); - } - return this; + public final void execute() { + start(); + runAnalysis(new Properties()); + stop(); } - public EmbeddedRunner addExtensions(Object... objects) { - extensions.addAll(Arrays.asList(objects)); - return this; + private void initGlobalDefaultValues() { + setGlobalDefaultValue(RunnerProperties.HOST_URL, "http://localhost:9000"); + setGlobalDefaultValue(InternalProperties.RUNNER_APP, "SonarQubeRunner"); + setGlobalDefaultValue(InternalProperties.RUNNER_APP_VERSION, RunnerVersion.version()); } - List<Object> extensions() { - return extensions; + private void initAnalysisProperties(Properties p) { + initSourceEncoding(p); + new Dirs(logger).init(p); + } + + void initSourceEncoding(Properties p) { + boolean onProject = Utils.taskRequiresProject(p); + if (onProject) { + String sourceEncoding = p.getProperty(ScanProperties.PROJECT_SOURCE_ENCODING, ""); + boolean platformDependent = false; + if ("".equals(sourceEncoding)) { + sourceEncoding = Charset.defaultCharset().name(); + platformDependent = true; + p.setProperty(ScanProperties.PROJECT_SOURCE_ENCODING, sourceEncoding); + } + logger.info("Default locale: \"" + Locale.getDefault() + "\", source code encoding: \"" + sourceEncoding + "\"" + + (platformDependent ? " (analysis is platform dependent)" : "")); + } + } + + private void setGlobalDefaultValue(String key, String value) { + if (!globalProperties.containsKey(key)) { + setGlobalProperty(key, value); + } } - @Override protected void doStart() { launcher = launcherFactory.createLauncher(globalProperties()); if (Utils.isAtLeast52(launcher.getVersion())) { - launcher.start(globalProperties(), extensions, Logs.getListener()); + launcher.start(globalProperties(), new org.sonar.runner.batch.LogOutput() { + + @Override + public void log(String formattedMessage, Level level) { + logOutput.log(formattedMessage, LogOutput.Level.valueOf(level.name())); + } + + }); } } - @Override protected void doStop() { if (Utils.isAtLeast52(launcher.getVersion())) { launcher.stop(); } } - @Override protected void doExecute(Properties analysisProperties) { if (Utils.isAtLeast52(launcher.getVersion())) { launcher.execute(analysisProperties); @@ -130,7 +222,7 @@ public class EmbeddedRunner extends Runner<EmbeddedRunner> { Properties prop = new Properties(); prop.putAll(globalProperties()); prop.putAll(analysisProperties); - launcher.executeOldVersion(prop, extensions); + launcher.executeOldVersion(prop); } } } 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 deleted file mode 100644 index 2a3e2b5..0000000 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * SonarQube 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; - -import org.sonar.runner.impl.Logs; -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; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -/** - * Runner executed in a new JVM. - * - * @since 2.2 - */ -public class ForkedRunner extends Runner<ForkedRunner> { - - private static final int ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; - - private final Map<String, String> jvmEnvVariables = new HashMap<String, String>(); - private final List<String> jvmArguments = new ArrayList<String>(); - private String javaExecutable; - private StreamConsumer stdOut = null; - private StreamConsumer stdErr = null; - private final JarExtractor jarExtractor; - private final CommandExecutor commandExecutor; - - private ProcessMonitor processMonitor; - - ForkedRunner(JarExtractor jarExtractor, CommandExecutor commandExecutor, @Nullable ProcessMonitor processMonitor) { - this.jarExtractor = jarExtractor; - this.commandExecutor = commandExecutor; - this.processMonitor = processMonitor; - } - - ForkedRunner(JarExtractor jarExtractor, CommandExecutor commandExecutor) { - this(jarExtractor, commandExecutor, null); - } - - /** - * Create new instance. Never return null. - */ - public static ForkedRunner create() { - 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) - */ - public ForkedRunner setJavaExecutable(@Nullable String s) { - this.javaExecutable = s; - return this; - } - - public List<String> jvmArguments() { - return new ArrayList<String>(jvmArguments); - } - - /** - * See {@link #addJvmArguments(java.util.List)} - */ - public ForkedRunner addJvmArguments(String... s) { - return addJvmArguments(Arrays.asList(s)); - } - - /** - * JVM arguments, for example "-Xmx512m" - */ - public ForkedRunner addJvmArguments(List<String> args) { - jvmArguments.addAll(args); - return this; - } - - /** - * Set a JVM environment variable. By default no variables are set. - */ - public ForkedRunner setJvmEnvVariable(String key, String value) { - jvmEnvVariables.put(key, value); - return this; - } - - /** - * Add some JVM environment variables. By default no variables are set. - */ - public ForkedRunner addJvmEnvVariables(Map<String, String> map) { - jvmEnvVariables.putAll(map); - return this; - } - - /** - * @deprecated Since 2.5. Use {@link ForkedRunner#setLogListener} instead. - * Subscribe to the standard output from the forked process. By default, logs messages are sent with INFO level to the log listener set. - */ - public ForkedRunner setStdOut(@Nullable StreamConsumer stream) { - this.stdOut = stream; - return this; - } - - /** - * @deprecated Since 2.5. Use {@link ForkedRunner#setLogListener}instead. - * Subscribe to the error output from the forked process. By default, logs messages are sent with ERROR level to the log listener set. - */ - public ForkedRunner setStdErr(@Nullable StreamConsumer stream) { - this.stdErr = stream; - return this; - } - - @Override - protected void doExecute(Properties props) { - // merge both global and analysis-specific properties because it will be used both to start and to execute. - Properties p = new Properties(); - - p.putAll(globalProperties()); - p.putAll(props); - - ForkCommand forkCommand = createCommand(p); - try { - fork(forkCommand); - } finally { - deleteTempFiles(forkCommand); - } - } - - @Override - protected void doStop() { - // nothing to do - } - - @Override - protected void doStart() { - // nothing to do - } - - ForkCommand createCommand(Properties p) { - File propertiesFile = writeProperties(p); - File jarFile = jarExtractor.extractToTemp("sonar-runner-impl"); - if (javaExecutable == null) { - 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(); - return new ForkCommand(command, jarFile, propertiesFile); - } - - File writeProperties(Properties p) { - try { - File file = File.createTempFile("sonar-project", ".properties"); - try (OutputStream output = new FileOutputStream(file)) { - p.store(output, "Generated by sonar-runner"); - return file; - } - } catch (Exception e) { - throw new IllegalStateException("Fail to export sonar-runner properties", e); - } - } - - private static void deleteTempFiles(ForkCommand forkCommand) { - Utils.deleteQuietly(forkCommand.jarFile); - Utils.deleteQuietly(forkCommand.propertiesFile); - } - - private void fork(ForkCommand forkCommand) { - if (stdOut == null) { - stdOut = new OutConsumer(); - } - if (stdErr == null) { - stdErr = new ErrConsumer(); - } - - int status = commandExecutor.execute(forkCommand.command, stdOut, stdErr, ONE_DAY_IN_MILLISECONDS, processMonitor); - if (status != 0) { - if (processMonitor != null && processMonitor.stop()) { - stdOut.consumeLine(String.format("SonarQube Runner was stopped [status=%s]", status)); - } else { - throw new IllegalStateException("Error status [command: " + forkCommand.command + "]: " + status); - } - } - } - - private static class OutConsumer implements StreamConsumer { - @Override - public void consumeLine(String line) { - Logs.info(line); - } - } - private static class ErrConsumer implements StreamConsumer { - @Override - public void consumeLine(String line) { - Logs.error(line); - } - } - - static class ForkCommand { - Command command; - File jarFile; - File propertiesFile; - - private ForkCommand(Command command, File jarFile, File propertiesFile) { - this.command = command; - this.jarFile = jarFile; - this.propertiesFile = propertiesFile; - } - } -} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/LogListener.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/LogOutput.java index bfcfdc4..4dfe94e 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/LogListener.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/LogOutput.java @@ -19,8 +19,9 @@ */ package org.sonar.runner.api; -public interface LogListener { - void log(String msg, Level level); +public interface LogOutput { + + void log(String formattedMessage, Level level); enum Level { ERROR, WARN, INFO, DEBUG, TRACE; diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/Runner.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/Runner.java deleted file mode 100644 index 1bd0515..0000000 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/Runner.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * SonarQube 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; - -import org.sonar.runner.impl.Logs; -import org.sonar.runner.impl.InternalProperties; - -import javax.annotation.Nullable; - -import java.io.File; -import java.util.Properties; - -/** - * @since 2.2 - */ -public abstract class Runner<T extends Runner<?>> { - private final Properties globalProperties = new Properties(); - - protected Runner() { - } - - public Properties globalProperties() { - Properties clone = new Properties(); - clone.putAll(globalProperties); - return clone; - } - - /** - * Set a log stream. All log events will be redirected to the listener. - * By default, all logs are sent to stdout, except for logs of ERROR level, which are sent to stderr. - * If null is given, the default is behavior is set. - */ - public T setLogListener(LogListener stream) { - Logs.setListener(new LogListenerAdapter(stream)); - return (T) this; - } - - /** - * Declare Sonar properties, for example sonar.projectKey=>foo. - * - * @see #setProperty(String, String) - */ - public T addGlobalProperties(Properties p) { - globalProperties.putAll(p); - return (T) this; - } - - /** - * Declare a Sonar property. - * - * @see RunnerProperties - * @see ScanProperties - */ - public T setGlobalProperty(String key, String value) { - globalProperties.setProperty(key, value); - return (T) this; - } - - public String globalProperty(String key, @Nullable String defaultValue) { - return globalProperties.getProperty(key, defaultValue); - } - - /** - * User-agent used in the HTTP requests to the Sonar server - */ - public T setApp(String app, String version) { - setGlobalProperty(InternalProperties.RUNNER_APP, app); - setGlobalProperty(InternalProperties.RUNNER_APP_VERSION, version); - return (T) this; - } - - public String app() { - return globalProperty(InternalProperties.RUNNER_APP, null); - } - - public String appVersion() { - return globalProperty(InternalProperties.RUNNER_APP_VERSION, null); - } - - public void runAnalysis(Properties analysisProperties) { - Properties copy = new Properties(); - copy.putAll(analysisProperties); - initAnalysisProperties(copy); - - String dumpToFile = copy.getProperty(InternalProperties.RUNNER_DUMP_TO_FILE); - if (dumpToFile != null) { - File dumpFile = new File(dumpToFile); - Utils.writeProperties(dumpFile, copy); - Logs.info("Simulation mode. Configuration written to " + dumpFile.getAbsolutePath()); - } else { - doExecute(copy); - } - } - - public void start() { - initGlobalDefaultValues(); - doStart(); - } - - public void stop() { - doStop(); - } - - /** - * @deprecated since 2.5 use {@link #start()}, {@link #runAnalysis(Properties)} and then {@link #stop()} - */ - @Deprecated - public final void execute() { - start(); - runAnalysis(new Properties()); - stop(); - } - - protected abstract void doStart(); - - protected abstract void doStop(); - - protected abstract void doExecute(Properties analysisProperties); - - private void initGlobalDefaultValues() { - setGlobalDefaultValue(RunnerProperties.HOST_URL, "http://localhost:9000"); - setGlobalDefaultValue(InternalProperties.RUNNER_APP, "SonarQubeRunner"); - setGlobalDefaultValue(InternalProperties.RUNNER_APP_VERSION, RunnerVersion.version()); - } - - private static void initAnalysisProperties(Properties p) { - SourceEncoding.init(p); - Dirs.init(p); - } - - private void setGlobalDefaultValue(String key, String value) { - if (!globalProperties.containsKey(key)) { - setGlobalProperty(key, value); - } - } - - private class LogListenerAdapter implements org.sonar.home.log.LogListener { - private LogListener adaptedListener; - - LogListenerAdapter(LogListener listener) { - this.adaptedListener = listener; - } - - @Override - public void log(String msg, org.sonar.home.log.LogListener.Level level) { - this.adaptedListener.log(msg, translate(level)); - } - private LogListener.Level translate(org.sonar.home.log.LogListener.Level level) { - switch(level) { - case ERROR: - return LogListener.Level.ERROR; - case WARN: - return LogListener.Level.WARN; - case INFO: - return LogListener.Level.INFO; - case DEBUG: - default: - return LogListener.Level.DEBUG; - case TRACE: - return LogListener.Level.TRACE; - } - } - } -} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/SourceEncoding.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/SourceEncoding.java deleted file mode 100644 index d21f9f3..0000000 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/SourceEncoding.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarQube 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; - -import org.sonar.runner.impl.Logs; - -import java.nio.charset.Charset; -import java.util.Locale; -import java.util.Properties; - -class SourceEncoding { - private SourceEncoding() { - - } - - static void init(Properties p) { - boolean onProject = Utils.taskRequiresProject(p); - if (onProject) { - String sourceEncoding = p.getProperty(ScanProperties.PROJECT_SOURCE_ENCODING, ""); - boolean platformDependent = false; - if ("".equals(sourceEncoding)) { - sourceEncoding = Charset.defaultCharset().name(); - platformDependent = true; - p.setProperty(ScanProperties.PROJECT_SOURCE_ENCODING, sourceEncoding); - } - Logs.info("Default locale: \"" + Locale.getDefault() + "\", source code encoding: \"" + sourceEncoding + "\"" - + (platformDependent ? " (analysis is platform dependent)" : "")); - } - } -} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandException.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/StdOutLogOutput.java index 6c371a5..016e614 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandException.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/StdOutLogOutput.java @@ -19,12 +19,10 @@ */ package org.sonar.runner.api; -import javax.annotation.Nullable; +public class StdOutLogOutput implements LogOutput { -class CommandException extends RuntimeException { - - CommandException(String message, Command command, @Nullable Throwable throwable) { - super(message + " [command: " + command + "]", throwable); + @Override + public void log(String formattedMessage, org.sonar.runner.api.LogOutput.Level level) { + System.out.println(level.name() + ": " + formattedMessage); } - } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/InternalProperties.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/InternalProperties.java new file mode 100644 index 0000000..48c3319 --- /dev/null +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/InternalProperties.java @@ -0,0 +1,26 @@ +/* + * SonarQube 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.impl; + +public interface InternalProperties { + String RUNNER_APP = "sonarRunner.app"; + String RUNNER_APP_VERSION = "sonarRunner.appVersion"; + String RUNNER_DUMP_TO_FILE = "sonarRunner.dumpToFile"; +} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedClassloader.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedClassloader.java new file mode 100644 index 0000000..d5c52b3 --- /dev/null +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedClassloader.java @@ -0,0 +1,111 @@ +/* + * SonarQube 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.impl; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; +import java.util.List; + +/** + * Special {@link java.net.URLClassLoader} to execute batch, which restricts loading from parent. + */ +class IsolatedClassloader extends URLClassLoader { + + /** + * The parent classloader is used only for loading classes and resources in unmasked packages + */ + IsolatedClassloader(ClassLoader parent) { + super(new URL[0], parent); + } + + void addFiles(List<File> files) { + try { + for (File file : files) { + addURL(file.toURI().toURL()); + } + } catch (MalformedURLException e) { + throw new IllegalStateException("Fail to create classloader", e); + } + } + + /** + * Same behavior as in {@link java.net.URLClassLoader#loadClass(String, boolean)}, except loading from parent. + */ + @Override + protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + // First, check if the class has already been loaded + Class<?> c = findLoadedClass(name); + if (c == null) { + try { + // Load from parent + if (getParent() != null && fromSonarBatchPackage(name)) { + c = getParent().loadClass(name); + } else { + + // Load from system + + // I don't know for other vendors, but for Oracle JVM : + // - ClassLoader.getSystemClassLoader() is sun.misc.Launcher$AppClassLoader. It contains app classpath. + // - ClassLoader.getSystemClassLoader().getParent() is sun.misc.Launcher$ExtClassLoader. It contains core JVM + ClassLoader systemClassLoader = getSystemClassLoader(); + if (systemClassLoader.getParent() != null) { + systemClassLoader = systemClassLoader.getParent(); + } + c = systemClassLoader.loadClass(name); + } + } catch (ClassNotFoundException e) { + // If still not found, then invoke findClass in order + // to find the class. + c = findClass(name); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + + private boolean fromSonarBatchPackage(String name) { + return name.startsWith("org.sonar.runner.batch"); + } + + /** + * Unlike {@link java.net.URLClassLoader#getResource(String)} don't return resource from parent. + * See http://jira.codehaus.org/browse/SONAR-2276 + */ + @Override + public URL getResource(String name) { + return findResource(name); + } + + /** + * Unlike {@link java.net.URLClassLoader#getResources(String)} don't return resources from parent. + * See http://jira.codehaus.org/browse/SONAR-2276 + */ + @Override + public Enumeration<URL> getResources(String name) throws IOException { + return findResources(name); + } + +} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java new file mode 100644 index 0000000..601776d --- /dev/null +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java @@ -0,0 +1,94 @@ +/* + * SonarQube 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.impl; + +import java.io.File; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.List; +import java.util.Properties; +import org.sonar.home.cache.Logger; +import org.sonar.home.cache.PersistentCache; +import org.sonar.home.cache.PersistentCacheBuilder; +import org.sonar.runner.batch.IsolatedLauncher; + +public class IsolatedLauncherFactory { + static final String ISOLATED_LAUNCHER_IMPL = "org.sonar.runner.batch.BatchIsolatedLauncher"; + private final TempCleaning tempCleaning; + private final String launcherImplClassName; + private final Logger logger; + + /** + * For unit tests + */ + IsolatedLauncherFactory(String isolatedLauncherClassName, TempCleaning tempCleaning, Logger logger) { + this.tempCleaning = tempCleaning; + this.launcherImplClassName = isolatedLauncherClassName; + this.logger = logger; + } + + public IsolatedLauncherFactory(Logger logger) { + this(ISOLATED_LAUNCHER_IMPL, new TempCleaning(logger), logger); + } + + private PersistentCache getCache(Properties props) { + PersistentCacheBuilder builder = new PersistentCacheBuilder(logger); + + if (!"true".equals(props.getProperty("sonar.enableHttpCache"))) { + builder.forceUpdate(true); + } + + return builder.build(); + } + + private ClassLoader createClassLoader(List<File> jarFiles) { + IsolatedClassloader classloader = new IsolatedClassloader(getClass().getClassLoader()); + classloader.addFiles(jarFiles); + + return classloader; + } + + public IsolatedLauncher createLauncher(Properties props) { + ServerConnection serverConnection = ServerConnection.create(props, getCache(props), logger); + JarDownloader jarDownloader = new JarDownloader(serverConnection, logger); + + return createLauncher(jarDownloader); + } + + IsolatedLauncher createLauncher(final JarDownloader jarDownloader) { + return AccessController.doPrivileged(new PrivilegedAction<IsolatedLauncher>() { + @Override + public IsolatedLauncher run() { + try { + List<File> jarFiles = jarDownloader.download(); + logger.debug("Create isolated classloader..."); + ClassLoader cl = createClassLoader(jarFiles); + IsolatedLauncher objProxy = IsolatedLauncherProxy.create(cl, IsolatedLauncher.class, launcherImplClassName, logger); + tempCleaning.clean(); + + return objProxy; + } catch (Exception e) { + // Catch all other exceptions, which relates to reflection + throw new RunnerException("Unable to execute SonarQube", e); + } + } + }); + } +} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherProxy.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherProxy.java new file mode 100644 index 0000000..411d731 --- /dev/null +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherProxy.java @@ -0,0 +1,85 @@ +/* + * SonarQube 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.impl; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.UndeclaredThrowableException; +import org.sonar.home.cache.Logger; + +public class IsolatedLauncherProxy implements InvocationHandler { + private final Object proxied; + private final ClassLoader cl; + private final Logger logger; + + private IsolatedLauncherProxy(ClassLoader cl, Object proxied, Logger logger) { + this.cl = cl; + this.proxied = proxied; + this.logger = logger; + } + + public static <T> T create(ClassLoader cl, Class<T> interfaceClass, String proxiedClassName, Logger logger) throws ReflectiveOperationException { + Object proxied = createProxiedObject(cl, proxiedClassName); + // interfaceClass needs to be loaded with a parent ClassLoader (common to both ClassLoaders) + // In addition, Proxy.newProxyInstance checks if the target ClassLoader sees the same class as the one given + Class<?> loadedInterfaceClass = cl.loadClass(interfaceClass.getName()); + return (T) create(cl, proxied, loadedInterfaceClass, logger); + } + + public static <T> T create(ClassLoader cl, Object proxied, Class<T> interfaceClass, Logger logger) { + Class<?>[] c = {interfaceClass}; + return (T) Proxy.newProxyInstance(cl, c, new IsolatedLauncherProxy(cl, proxied, logger)); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + ClassLoader initialContextClassLoader = Thread.currentThread().getContextClassLoader(); + + try { + Thread.currentThread().setContextClassLoader(cl); + logger.debug("Execution " + method.getName()); + return method.invoke(proxied, args); + } catch (UndeclaredThrowableException | InvocationTargetException e) { + throw unwrapException(e); + } finally { + Thread.currentThread().setContextClassLoader(initialContextClassLoader); + } + } + + private static Throwable unwrapException(Throwable e) { + Throwable cause = e; + + while (cause.getCause() != null) { + if (cause instanceof UndeclaredThrowableException || cause instanceof InvocationTargetException) { + cause = cause.getCause(); + } else { + break; + } + } + return cause; + } + + private static Object createProxiedObject(ClassLoader cl, String proxiedClassName) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + Class<?> proxiedClass = cl.loadClass(proxiedClassName); + return proxiedClass.newInstance(); + } +} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/Os.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/JarDownloader.java index 168f722..62fc157 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/Os.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/JarDownloader.java @@ -17,24 +17,22 @@ * 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; +package org.sonar.runner.impl; import java.io.File; +import java.util.List; +import org.sonar.home.cache.Logger; -class Os { - boolean isWindows() { - return System.getProperty("os.name").contains("Windows"); - } +class JarDownloader { + private final ServerConnection serverConnection; + private final Logger logger; - File thisJavaHome() { - return new File(System.getProperty("java.home")); + JarDownloader(ServerConnection conn, Logger logger) { + this.serverConnection = conn; + this.logger = logger; } - /** - * Path to the java executable used by this VM - */ - File thisJavaExe() { - File bin = new File(thisJavaHome(), "bin"); - return new File(bin, isWindows() ? "java.exe" : "java"); + List<File> download() { + return new Jars(serverConnection, new JarExtractor(), logger).download(); } } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/PrintStreamConsumer.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/JarExtractor.java index bab55a7..c4e3fa7 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/PrintStreamConsumer.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/JarExtractor.java @@ -17,25 +17,25 @@ * 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; +package org.sonar.runner.impl; -import java.io.PrintStream; +import org.apache.commons.io.FileUtils; -/** - * Implementation of StreamConsumer that prints lines to {@link java.io.PrintStream}. Generally used - * to forward to {@link System.out} or {@link System.err}. - * - * @since 2.2 - */ -public class PrintStreamConsumer implements StreamConsumer { - final PrintStream output; +import java.io.File; +import java.net.URL; - public PrintStreamConsumer(PrintStream output) { - this.output = output; - } +public class JarExtractor { - @Override - public void consumeLine(String line) { - output.println(line); + public File extractToTemp(String filenameWithoutSuffix) { + String filename = filenameWithoutSuffix + ".jar"; + URL url = getClass().getResource("/" + filename); + try { + File copy = File.createTempFile(filenameWithoutSuffix, ".jar"); + FileUtils.copyURLToFile(url, copy); + return copy; + } catch (Exception e) { + throw new IllegalStateException("Fail to extract " + filename, e); + } } } + diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/Jars.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/Jars.java new file mode 100644 index 0000000..d181f23 --- /dev/null +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/Jars.java @@ -0,0 +1,99 @@ +/* + * SonarQube 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.impl; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.sonar.home.cache.FileCache; +import org.sonar.home.cache.FileCacheBuilder; +import org.sonar.home.cache.Logger; + +class Jars { + private static final String BOOTSTRAP_INDEX_PATH = "/batch_bootstrap/index"; + static final String BATCH_PATH = "/batch/"; + + private final FileCache fileCache; + private final ServerConnection connection; + private final JarExtractor jarExtractor; + private final Logger logger; + + Jars(ServerConnection conn, JarExtractor jarExtractor, Logger logger) { + this.logger = logger; + this.fileCache = new FileCacheBuilder(logger).build(); + this.connection = conn; + this.jarExtractor = jarExtractor; + } + + /** + * For unit tests + */ + Jars(FileCache fileCache, ServerConnection conn, JarExtractor jarExtractor, Logger logger) { + this.logger = logger; + this.fileCache = fileCache; + this.connection = conn; + this.jarExtractor = jarExtractor; + } + + List<File> download() { + List<File> files = new ArrayList<File>(); + logger.debug("Extract sonar-runner-batch in temp..."); + files.add(jarExtractor.extractToTemp("sonar-runner-batch")); + files.addAll(dowloadFiles()); + return files; + } + + private List<File> dowloadFiles() { + try { + List<File> files = new ArrayList<File>(); + logger.debug("Get bootstrap index..."); + String libs = connection.downloadStringCache(BOOTSTRAP_INDEX_PATH); + logger.debug("Get bootstrap completed"); + String[] lines = libs.split("[\r\n]+"); + BatchFileDownloader batchFileDownloader = new BatchFileDownloader(connection); + for (String line : lines) { + line = line.trim(); + if (!"".equals(line)) { + String[] libAndHash = line.split("\\|"); + String filename = libAndHash[0]; + String hash = libAndHash.length > 0 ? libAndHash[1] : ""; + files.add(fileCache.get(filename, hash, batchFileDownloader)); + } + } + return files; + } catch (Exception e) { + throw new IllegalStateException("Fail to download libraries from server", e); + } + } + + static class BatchFileDownloader implements FileCache.Downloader { + private final ServerConnection connection; + + BatchFileDownloader(ServerConnection conn) { + this.connection = conn; + } + + @Override + public void download(String filename, File toFile) throws IOException { + connection.download(BATCH_PATH + filename, toFile); + } + } +} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/RunnerException.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/RunnerException.java new file mode 100644 index 0000000..9d60947 --- /dev/null +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/RunnerException.java @@ -0,0 +1,28 @@ +/* + * SonarQube 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.impl; + +public class RunnerException extends RuntimeException { + + public RunnerException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/ServerConnection.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/ServerConnection.java new file mode 100644 index 0000000..291ee30 --- /dev/null +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/ServerConnection.java @@ -0,0 +1,165 @@ +/* + * SonarQube 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.impl; + +import com.github.kevinsawicki.http.HttpRequest; +import java.io.File; +import java.io.IOException; +import java.net.ConnectException; +import java.net.URL; +import java.net.UnknownHostException; +import java.text.MessageFormat; +import java.util.Properties; +import java.util.concurrent.Callable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.io.FileUtils; +import org.sonar.home.cache.Logger; +import org.sonar.home.cache.PersistentCache; + +class ServerConnection { + + private static final String SONAR_SERVER_CAN_NOT_BE_REACHED = "Sonar server ''{0}'' can not be reached"; + private static final String STATUS_RETURNED_BY_URL_IS_INVALID = "Status returned by url : ''{0}'' is invalid : {1}"; + static final int CONNECT_TIMEOUT_MILLISECONDS = 30000; + static final int READ_TIMEOUT_MILLISECONDS = 60000; + private static final Pattern CHARSET_PATTERN = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)"); + + private final String serverUrl; + private final String userAgent; + + private final PersistentCache wsCache; + private final boolean isModePreview; + private final Logger logger; + + private ServerConnection(String serverUrl, String app, String appVersion, boolean preview, PersistentCache cache, Logger logger) { + this.logger = logger; + this.serverUrl = removeEndSlash(serverUrl); + this.userAgent = app + "/" + appVersion; + this.wsCache = cache; + this.isModePreview = preview; + } + + private static String removeEndSlash(String url) { + if (url == null) { + return null; + } + return url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + } + + static ServerConnection create(Properties properties, PersistentCache cache, Logger logger) { + String serverUrl = properties.getProperty("sonar.host.url"); + String app = properties.getProperty(InternalProperties.RUNNER_APP); + String appVersion = properties.getProperty(InternalProperties.RUNNER_APP_VERSION); + String analysisMode = properties.getProperty("sonar.analysis.mode"); + boolean preview = "preview".equalsIgnoreCase(analysisMode); + + return new ServerConnection(serverUrl, app, appVersion, preview, cache, logger); + } + + private class StringDownloader implements Callable<String> { + private String url; + + StringDownloader(String url) { + this.url = url; + } + + @Override + public String call() throws Exception { + HttpRequest httpRequest = null; + try { + httpRequest = newHttpRequest(new URL(url)); + String charset = getCharsetFromContentType(httpRequest.contentType()); + if (charset == null || "".equals(charset)) { + charset = "UTF-8"; + } + if (!httpRequest.ok()) { + throw new IOException(MessageFormat.format(STATUS_RETURNED_BY_URL_IS_INVALID, url, httpRequest.code())); + } + return httpRequest.body(charset); + } finally { + if (httpRequest != null) { + httpRequest.disconnect(); + } + } + } + } + + void download(String path, File toFile) { + String fullUrl = serverUrl + path; + try { + logger.debug("Download " + fullUrl + " to " + toFile.getAbsolutePath()); + HttpRequest httpRequest = newHttpRequest(new URL(fullUrl)); + if (!httpRequest.ok()) { + throw new IOException(MessageFormat.format(STATUS_RETURNED_BY_URL_IS_INVALID, fullUrl, httpRequest.code())); + } + httpRequest.receive(toFile); + + } catch (Exception e) { + if (e.getCause() instanceof ConnectException || e.getCause() instanceof UnknownHostException) { + logger.error(MessageFormat.format(SONAR_SERVER_CAN_NOT_BE_REACHED, serverUrl)); + } + FileUtils.deleteQuietly(toFile); + throw new IllegalStateException("Fail to download: " + fullUrl, e); + } + } + + String downloadStringCache(String path) throws Exception { + String fullUrl = serverUrl + path; + try { + if (isModePreview) { + return wsCache.getString(serverUrl, new StringDownloader(fullUrl)); + } else { + return new StringDownloader(fullUrl).call(); + } + } catch (HttpRequest.HttpRequestException e) { + if (e.getCause() instanceof ConnectException || e.getCause() instanceof UnknownHostException) { + logger.error(MessageFormat.format(SONAR_SERVER_CAN_NOT_BE_REACHED, serverUrl)); + } + throw e; + } + } + + private HttpRequest newHttpRequest(URL url) { + HttpRequest request = HttpRequest.get(url); + request.trustAllCerts().trustAllHosts(); + request.acceptGzipEncoding().uncompress(true); + request.connectTimeout(CONNECT_TIMEOUT_MILLISECONDS).readTimeout(READ_TIMEOUT_MILLISECONDS); + request.userAgent(userAgent); + return request; + } + + /** + * Parse out a charset from a content type header. + * + * @param contentType e.g. "text/html; charset=EUC-JP" + * @return "EUC-JP", or null if not found. Charset is trimmed and upper-cased. + */ + static String getCharsetFromContentType(String contentType) { + if (contentType == null) { + return null; + } + Matcher m = CHARSET_PATTERN.matcher(contentType); + if (m.find()) { + return m.group(1).trim().toUpperCase(); + } + return null; + } +} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/TempCleaning.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/TempCleaning.java new file mode 100644 index 0000000..045e36d --- /dev/null +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/TempCleaning.java @@ -0,0 +1,65 @@ +/* + * SonarQube 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.impl; + +import java.io.File; +import java.util.Collection; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.AgeFileFilter; +import org.apache.commons.io.filefilter.AndFileFilter; +import org.apache.commons.io.filefilter.PrefixFileFilter; +import org.sonar.home.cache.Logger; + +/** + * The file sonar-runner-batch.jar is locked by the classloader on Windows and can't be dropped at the end of the execution. + * See {@link IsolatedLauncherFactory} + */ +class TempCleaning { + static final int ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; + + final File tempDir; + + private final Logger logger; + + TempCleaning(Logger logger) { + this(new File(System.getProperty("java.io.tmpdir")), logger); + } + + /** + * For unit tests + */ + TempCleaning(File tempDir, Logger logger) { + this.logger = logger; + this.tempDir = tempDir; + } + + void clean() { + logger.debug("Start temp cleaning..."); + long cutoff = System.currentTimeMillis() - ONE_DAY_IN_MILLISECONDS; + Collection<File> files = FileUtils.listFiles(tempDir, new AndFileFilter( + new PrefixFileFilter("sonar-runner-batch"), + new AgeFileFilter(cutoff)), null); + + for (File file : files) { + FileUtils.deleteQuietly(file); + } + logger.debug("Temp cleaning done"); + } +} diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/StreamConsumer.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/package-info.java index a1d0d7c..d7ac772 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/StreamConsumer.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/package-info.java @@ -17,11 +17,6 @@ * 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; +@javax.annotation.ParametersAreNonnullByDefault +package org.sonar.runner.impl; -/** - * @since 2.2 - */ -public interface StreamConsumer { - void consumeLine(String line); -} |