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

org.sonar.plugins.javascript.bridge.EmbeddedNode Maven / Gradle / Ivy

/*
 * SonarQube JavaScript Plugin
 * Copyright (C) 2011-2024 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.plugins.javascript.bridge;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
import static org.sonar.plugins.javascript.bridge.EmbeddedNode.Platform.UNSUPPORTED;
import static org.sonarsource.api.sonarlint.SonarLintSide.INSTANCE;

import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.scanner.ScannerSide;
import org.sonar.plugins.javascript.nodejs.NodeVersion;
import org.sonar.plugins.javascript.nodejs.ProcessWrapper;
import org.sonarsource.api.sonarlint.SonarLintSide;
import org.tukaani.xz.XZInputStream;

/**
 * Class handling the extraction of the embedded Node.JS runtime
 */
@ScannerSide
@SonarLintSide(lifespan = INSTANCE)
public class EmbeddedNode {

  public static final String VERSION_FILENAME = "version.txt";
  private static final String DEPLOY_LOCATION = Path.of("js", "node-runtime").toString();
  private static final long EXTRACTION_LOCK_WAIT_TIME_MILLIS = 10000;
  private static final Logger LOG = LoggerFactory.getLogger(EmbeddedNode.class);
  private final Path deployLocation;
  private final Platform platform;
  private final Environment env;
  private final ProcessWrapper processWrapper;
  private boolean isAvailable;

  enum Platform {
    WIN_X64,
    LINUX_ARM64,
    LINUX_X64,
    DARWIN_ARM64,
    DARWIN_X64,
    UNSUPPORTED;

    private String pathInJar() {
      switch (this) {
        case WIN_X64:
          return "/win-x64/";
        case LINUX_ARM64:
          return "/linux-arm64/";
        case LINUX_X64:
          return "/linux-x64/";
        case DARWIN_ARM64:
          return "/darwin-arm64/";
        case DARWIN_X64:
          return "/darwin-x64/";
        default:
          return "";
      }
    }

    /**
     * @return the path of the node compressed node runtime in the JAR
     */
    String archivePathInJar() {
      return pathInJar() + binary() + ".xz";
    }

    /**
     * @return the path of the file storing the version of the node runtime in the JAR
     */
    String versionPathInJar() {
      return pathInJar() + VERSION_FILENAME;
    }

    /**
     * @return the correct binary name depending on the platform: `node` or `node.exe`
     */
    String binary() {
      if (this == WIN_X64) {
        return "node.exe";
      } else {
        return "node";
      }
    }

    /**
     * @return The platform where this code is running
     */
    static Platform detect(Environment env) {
      var osName = env.getOsName();
      var lowerCaseOsName = osName.toLowerCase(Locale.ROOT);
      if (osName.contains("Windows") && isX64(env)) {
        return WIN_X64;
      } else if (lowerCaseOsName.contains("linux") && isARM64(env) ) {
        return LINUX_ARM64;
      } else if (lowerCaseOsName.contains("linux") && isX64(env) && !env.isAlpine()) {
        // alpine linux is using musl libc, which is not compatible with linux-x64
        return LINUX_X64;
      } else if (lowerCaseOsName.contains("mac os") && isARM64(env)) {
        return DARWIN_ARM64;
      } else if (lowerCaseOsName.contains("mac os") && isX64(env)) {
        return DARWIN_X64;
      }
      return UNSUPPORTED;
    }

    private static boolean isX64(Environment env) {
      return env.getOsArch().contains("amd64");
    }

    private static boolean isARM64(Environment env) {
      return env.getOsArch().contains("aarch64");
    }
  }

  public EmbeddedNode(ProcessWrapper processWrapper, Environment env) {
    this.platform = Platform.detect(env);
    this.deployLocation = runtimeCachePathFrom(env.getSonarUserHome());
    this.env = env;
    this.processWrapper = processWrapper;
  }

  /**
   * @return a path to `DEPLOY_LOCATION` from the given `baseDir`
   */
  private static Path runtimeCachePathFrom(Path baseDir) {
    return baseDir.resolve(DEPLOY_LOCATION);
  }

  public boolean isAvailable() {
    return platform != UNSUPPORTED && isAvailable;
  }

  /**
   * Extracts the node runtime from the JAR to the given `deployLocation`.
   * Skips the operation if the platform is unsupported, already extracted or missing from the JAR (legacy).
   *
   * @throws IOException
   */
  public void deploy() throws IOException {
    LOG.info(
      "Detected os: {} arch: {} alpine: {}. Platform: {}",
      env.getOsName(),
      env.getOsArch(),
      env.isAlpine(),
      platform
    );
    if (platform == UNSUPPORTED) {
      LOG.debug(
        "Your platform is not supported for embedded Node.js. Falling back to host Node.js."
      );
      return;
    }
    try {
      var is = getClass().getResourceAsStream(platform.archivePathInJar());
      if (is == null) {
        LOG.debug("Embedded node not found for platform {}", platform.archivePathInJar());
        return;
      }

      var targetRuntime = deployLocation.resolve(platform.binary());
      var targetDirectory = targetRuntime.getParent();
      var targetVersion = targetDirectory.resolve(VERSION_FILENAME);
      // we assume that since the archive exists, the version file must as well
      var versionIs = getClass().getResourceAsStream(platform.versionPathInJar());

      if (Files.exists(targetVersion) && !isDifferent(versionIs, targetVersion)) {
        LOG.debug("Skipping node deploy. Deployed node has latest version.");
      } else {
        extractWithLocking(is, versionIs, targetRuntime, targetDirectory);
      }
      // we try to run 'node -v' to test that node is working
      var detected = NodeVersion.getVersion(processWrapper, binary().toString());
      LOG.debug("Deployed node version {}", detected);
      isAvailable = true;
    } catch (Exception e) {
      LOG.warn("""
        Embedded Node.js failed to deploy in {}.
        You can change the location by setting the option `sonar.userHome` or the environment variable `SONAR_USER_HOME`.
        Otherwise, it will default to {}.
        Will fallback to host Node.js.""",
        Environment.defaultSonarUserHome(),
        deployLocation,
        e
      );
    }
  }

  private static boolean isDifferent(InputStream newVersionIs, Path currentVersionPath)
    throws IOException {
    var newVersionString = new String(newVersionIs.readAllBytes(), StandardCharsets.UTF_8);
    var currentVersionString = Files.readString(currentVersionPath);
    LOG.debug(
      "Currently installed Node.js version: {}. Available version in analyzer: {}", currentVersionString, newVersionString
    );
    return !newVersionString.equals(currentVersionString);
  }

  /**
   * Creates the `targetDirectory` and extracts the `source` to `targetRuntime`
   * Writes the version from `versionIs` to `targetDirectory`/VERSION_FILENAME
   *
   * @param source
   * @param versionIs
   * @param targetRuntime
   * @param targetDirectory
   * @throws IOException
   */
  private void extractWithLocking(
    InputStream source,
    InputStream versionIs,
    Path targetRuntime,
    Path targetDirectory
  ) throws IOException {
    var targetLockFile = targetDirectory.resolve("lockfile");
    Files.createDirectories(targetDirectory);
    try (
      var fos = new FileOutputStream(targetLockFile.toString());
      var channel = fos.getChannel()
    ) {
      var lock = channel.tryLock();
      if (lock != null) {
        try {
          LOG.debug("Lock acquired for extraction");
          extract(source, targetRuntime);
          Files.copy(versionIs, deployLocation.resolve(VERSION_FILENAME), REPLACE_EXISTING);
        } finally {
          lock.release();
        }
      } else {
        try {
          LOG.debug(
            "Lock taken, waiting " +
            EXTRACTION_LOCK_WAIT_TIME_MILLIS +
            "ms for other process to extract node runtime."
          );
          Thread.sleep(EXTRACTION_LOCK_WAIT_TIME_MILLIS);
        } catch (InterruptedException e) {
          LOG.warn("Interrupted while waiting for another process to extract the node runtime.");
          Thread.currentThread().interrupt();
        }
      }
    }
  }

  /**
   * Expects an InputStream to a xz-compressed file ending in `.xz` like `node.xz` and
   * extracts it into the given target Path.
   * 

* Skips extraction if target file already exists. * * @param source Path for the file to extract * @throws IOException */ private void extract(InputStream source, Path target) throws IOException { try ( var stream = new BufferedInputStream(source); var archive = new XZInputStream(stream); var os = Files.newOutputStream(target) ) { LOG.debug("Extracting embedded node to {}", target); int nextBytes; byte[] buf = new byte[8 * 1024 * 1024]; while ((nextBytes = archive.read(buf)) > -1) { os.write(buf, 0, nextBytes); } if (platform != Platform.WIN_X64) { Files.setPosixFilePermissions(target, Set.of(OWNER_EXECUTE, OWNER_READ, OWNER_WRITE)); } } } /** * @return the path to the binary once it has been decompressed */ public Path binary() { return deployLocation.resolve(platform.binary()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy