All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.css.plugin.server.CssAnalyzerBridgeServer Maven / Gradle / Ivy

The newest version!
/*
 * SonarCSS
 * Copyright (C) 2018-2021 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * 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  02110-1301, USA.
 */
package org.sonar.css.plugin.server;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.time.Duration;
import javax.annotation.Nullable;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.sonar.api.Startable;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.notifications.AnalysisWarnings;
import org.sonar.api.scanner.ScannerSide;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.log.Profiler;
import org.sonar.css.plugin.server.bundle.Bundle;
import org.sonarsource.api.sonarlint.SonarLintSide;
import org.sonarsource.nodejs.NodeCommand;
import org.sonarsource.nodejs.NodeCommandBuilder;
import org.sonarsource.nodejs.NodeCommandException;

import static org.sonar.css.plugin.CssRuleSensor.hasCssFiles;
import static org.sonar.css.plugin.CssRuleSensor.throwFailFast;
import static org.sonarsource.api.sonarlint.SonarLintSide.MULTIPLE_ANALYSES;

@ScannerSide
@SonarLintSide(lifespan = MULTIPLE_ANALYSES)
public class CssAnalyzerBridgeServer implements Startable {

  private static final Logger LOG = Loggers.get(CssAnalyzerBridgeServer.class);
  private static final Profiler PROFILER = Profiler.createIfDebug(LOG);

  private static final int DEFAULT_TIMEOUT_SECONDS = 60;
  // internal property to set "--max-old-space-size" for Node process running this server
  private static final String MAX_OLD_SPACE_SIZE_PROPERTY = "sonar.css.node.maxspace";
  private static final Gson GSON = new Gson();

  private final OkHttpClient client;
  private final NodeCommandBuilder nodeCommandBuilder;
  final int timeoutSeconds;
  private final Bundle bundle;
  private final AnalysisWarnings analysisWarnings;
  private final String hostAddress;
  private int port;
  private NodeCommand nodeCommand;
  private final NodeDeprecationWarning deprecationWarning;
  private boolean failedToStart;

  // Used by pico container for dependency injection
  @SuppressWarnings("unused")
  public CssAnalyzerBridgeServer(Bundle bundle, @Nullable AnalysisWarnings analysisWarnings, NodeDeprecationWarning deprecationWarning) {
    this(NodeCommand.builder(), DEFAULT_TIMEOUT_SECONDS, bundle, analysisWarnings, deprecationWarning);
  }

  protected CssAnalyzerBridgeServer(NodeCommandBuilder nodeCommandBuilder, int timeoutSeconds, Bundle bundle,
                                    @Nullable AnalysisWarnings analysisWarnings, NodeDeprecationWarning deprecationWarning) {
    this.nodeCommandBuilder = nodeCommandBuilder;
    this.timeoutSeconds = timeoutSeconds;
    this.bundle = bundle;
    this.analysisWarnings = analysisWarnings;
    this.client = new OkHttpClient.Builder()
      .callTimeout(Duration.ofSeconds(timeoutSeconds))
      .readTimeout(Duration.ofSeconds(timeoutSeconds))
      .build();
    this.hostAddress = InetAddress.getLoopbackAddress().getHostAddress();
    this.deprecationWarning = deprecationWarning;
  }

  public void deploy(File deployLocation) {
    bundle.deploy(deployLocation.toPath());
  }

  public void startServer(SensorContext context) throws IOException {
    PROFILER.startDebug("Starting server");
    port = NetUtils.findOpenPort();

    File scriptFile = new File(bundle.startServerScript());
    if (!scriptFile.exists()) {
      throw new NodeCommandException("Node.js script to start css-bundle server doesn't exist: " + scriptFile.getAbsolutePath());
    }

    initNodeCommand(context, scriptFile);

    LOG.debug("Starting Node.js process to start css-bundle server at port " + port);
    nodeCommand.start();

    if (!waitServerToStart(timeoutSeconds * 1000)) {
      throw new NodeCommandException("Failed to start server (" + timeoutSeconds + "s timeout)");
    }
    PROFILER.stopDebug();
    deprecationWarning.logNodeDeprecation(nodeCommand.getActualNodeVersion());
  }

  boolean waitServerToStart(int timeoutMs) {
    int sleepStep = 100;
    long start = System.currentTimeMillis();
    try {
      Thread.sleep(sleepStep);
      while (!isAlive()) {
        if (System.currentTimeMillis() - start > timeoutMs) {
          return false;
        }
        Thread.sleep(sleepStep);
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    return true;
  }

  private void initNodeCommand(SensorContext context, File scriptFile) throws IOException {
    nodeCommandBuilder
      .outputConsumer(message -> {
        if (message.startsWith("DEBUG")) {
          LOG.debug(message.substring(5).trim());
        } else if (message.startsWith("WARN")) {
          LOG.warn(message.substring(4).trim());
        } else {
          LOG.info(message);
        }
      })
      .minNodeVersion(10)
      .configuration(context.config())
      .script(scriptFile.getAbsolutePath())
      .pathResolver(bundle)
      .scriptArgs(String.valueOf(port), hostAddress);

    context.config()
      .getInt(MAX_OLD_SPACE_SIZE_PROPERTY)
      .ifPresent(nodeCommandBuilder::maxOldSpaceSize);

    nodeCommand = nodeCommandBuilder.build();
  }

  /**
   * @return true when server is up and running normally, false otherwise
   */
  public boolean startServerLazily(SensorContext context) throws IOException {
    // required for SonarLint context to avoid restarting already failed server
    if (failedToStart) {
      LOG.debug("Skipping start of css-bundle server due to the failure during first analysis");
      LOG.debug("Skipping execution of CSS rules due to the problems with css-bundle server");
      return false;
    }

    try {
      if (isAlive()) {
        LOG.debug("css-bundle server is up, no need to start.");
        return true;
      }
      deploy(context.fileSystem().workDir());
      startServer(context);
      return true;
    } catch (NodeCommandException e) {
      failedToStart = true;
      processNodeCommandException(e, context);
      return false;
    }
  }

  // happens for example when NodeJS is not available, or version is too old
  private void processNodeCommandException(NodeCommandException e, SensorContext context) {
    String message = "CSS rules were not executed. " + e.getMessage();
    if (hasCssFiles(context)) {
      LOG.error(message, e);
      reportAnalysisWarning(message);
    } else {
      // error logs are often blocking (esp. in Azure), so we log at warning level if there is no CSS files in the project
      LOG.warn(message);
    }
    throwFailFast(context, e);
  }

  public Issue[] analyze(Request request) throws IOException {
    String json = GSON.toJson(request);
    return parseResponse(request(json));
  }

  private String request(String json) throws IOException {
    okhttp3.Request request = new okhttp3.Request.Builder()
      .url(url("analyze"))
      .post(RequestBody.create(MediaType.get("application/json"), json))
      .build();

    try (Response response = client.newCall(request).execute()) {
      // in this case response.body() is never null (according to docs)
      return response.body().string();
    }
  }

  private static Issue[] parseResponse(String result) {
    try {
      return GSON.fromJson(result, Issue[].class);
    } catch (JsonSyntaxException e) {
      String msg = "Failed to parse response: \n-----\n" + result + "\n-----\n";
      LOG.debug(msg);
      throw new IllegalStateException("Failed to parse response (check DEBUG logs for the response content)", e);
    }
  }

  public boolean isAlive() {
    if (nodeCommand == null) {
      return false;
    }
    okhttp3.Request request = new okhttp3.Request.Builder()
      .url(url("status"))
      .get()
      .build();

    try (Response response = client.newCall(request).execute()) {
      String body = response.body().string();
      // in this case response.body() is never null (according to docs)
      return "OK!".equals(body);
    } catch (IOException e) {
      return false;
    }
  }

  public String getCommandInfo() {
    if (nodeCommand == null) {
      return "Node.js command to start css-bundle server was not built yet.";
    } else {
      return "Node.js command to start css-bundle was: " + nodeCommand.toString();
    }
  }

  @Override
  public void start() {
    // Server is started lazily by the sensor
  }

  @Override
  public void stop() {
    clean();
  }

  void clean() {
    if (nodeCommand != null) {
      callClose();
      nodeCommand.waitFor();
      nodeCommand = null;
    }
  }

  private void callClose() {
    okhttp3.Request request = new okhttp3.Request.Builder()
      .url(url("close"))
      .post(RequestBody.create(MediaType.get("application/json"), ""))
      .build();
    try (Response response = client.newCall(request).execute()) {
      // nothing to do here
    } catch (IOException e) {
      LOG.warn("Failed to close stylelint-bridge server", e);
    }
  }

  private HttpUrl url(String endpoint) {
    HttpUrl.Builder builder = new HttpUrl.Builder();
    return builder
      .scheme("http")
      .host(hostAddress)
      .port(port)
      .addPathSegment(endpoint)
      .build();
  }

  // for testing purposes
  public void setPort(int port) {
    this.port = port;
  }

  private void reportAnalysisWarning(String message) {
    if (analysisWarnings != null) {
      analysisWarnings.addUnique(message);
    }
  }

  public static class Request {
    public final String filePath;
    /**
     * The fileContent is sent only in the SonarLint context or when the encoding
     * of the file is not utf-8. Otherwise, for performance reason, it's more efficient to
     * not have the fileContent and let the server getting it using filePath.
     */
    @Nullable
    public final String fileContent;
    public final String configFile;

    public Request(String filePath, @Nullable String fileContent, String configFile) {
      this.filePath = filePath;
      this.fileContent = fileContent;
      this.configFile = configFile;
    }
  }

  public static class Issue {
    public final Integer line;
    public final String rule;
    public final String text;

    public Issue(Integer line, String rule, String text) {
      this.line = line;
      this.rule = rule;
      this.text = text;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy