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

almond.kernel.install.Install.scala Maven / Gradle / Ivy

package almond.kernel.install

import java.io.{ByteArrayOutputStream, File, IOException, InputStream}
import java.net.{URI, URL}
import java.nio.file.{Files, Path, Paths, StandardCopyOption}
import java.util.jar.{Attributes, Manifest}
import java.util.zip.ZipFile

import almond.kernel.util.JupyterPath
import almond.protocol.KernelSpec

import scala.annotation.tailrec
import scala.jdk.CollectionConverters._

object Install {

  import com.github.plokhotnyuk.jsoniter_scala.core._

  private def deleteRecursively(f: Path): Unit = {
    if (Files.isDirectory(f))
      Files.list(f)
        .iterator()
        .asScala
        .foreach(deleteRecursively)

    Files.deleteIfExists(f)
  }

  private def readFully(is: => InputStream): Array[Byte] = {

    var is0: InputStream = null

    try {
      is0 = is

      val buffer = new ByteArrayOutputStream
      val data   = Array.ofDim[Byte](16384)

      var nRead = 0
      while ({
        nRead = is0.read(data, 0, data.length)
        nRead != -1
      })
        buffer.write(data, 0, nRead)

      buffer.flush()
      buffer.toByteArray
    }
    finally
      if (is0 != null)
        is0.close()
  }

  /** Installs a Jupyter kernel.
    */
  def installIn(
    kernelId: String,
    spec: KernelSpec,
    jupyterPath: Either[Path, JupyterPath],
    logo64PngOpt: Option[URL] = None,
    force: Boolean = false,
    copyLauncher: Boolean = false
  ): Path = {

    // About the logo, don't know if other formats / sizes are allowed.

    // Ensure we can read that one upfront
    val logoContentOpt = logo64PngOpt.map(url => readFully(url.openStream()))

    val jupyterDir = jupyterPath match {
      case Left(p) => p
      case Right(j) =>
        j.paths.headOption.getOrElse(
          throw new InstallException.JupyterDirectoryTypeNotFound(j.name)
        )
    }

    val dir = jupyterDir.resolve(kernelId)

    if (Files.exists(dir))
      if (force)
        // not deleting dir itself - on Windows, sometimes running into java.nio.file.AccessDeniedException in
        // createDirectories below else
        Files.list(dir)
          .iterator()
          .asScala
          .foreach(deleteRecursively)
      else
        throw new InstallException.InstallDirAlreadyExists(dir)

    Files.createDirectories(dir)

    if (!Files.exists(dir))
      throw new Exception(s"Could not create $dir")

    val spec0 =
      if (copyLauncher)
        launcherPos(spec.argv) match {
          case None =>
            throw new Exception(
              s"Can't copy launcher, launcher argument in command ${spec.argv.mkString(" ")} cannot be found"
            )
          case Some((launcher0, pos)) =>
            val dest   = dir.resolve("launcher.jar")
            val source = Paths.get(launcher0)
            if (!Files.exists(source))
              throw new Exception(s"Launcher $launcher0 in kernel spec command not found")
            if (!Files.isRegularFile(source))
              throw new Exception(
                s"Launcher $launcher0 in kernel spec command is not a regular file"
              )
            try Files.copy(
                Paths.get(launcher0),
                dest,
                StandardCopyOption.REPLACE_EXISTING,
                StandardCopyOption.COPY_ATTRIBUTES
              )
            catch {
              case _: IOException =>
                Files.copy(Paths.get(launcher0), dest, StandardCopyOption.REPLACE_EXISTING)
            }
            spec.copy(
              argv = spec.argv.updated(pos, spec.argv(pos).replace(launcher0, dest.toString))
            )
        }
      else
        spec

    val kernelJsonContent = writeToArray(spec0, WriterConfig.withIndentionStep(2))

    Files.write(dir.resolve("kernel.json"), kernelJsonContent)
    for (b <- logoContentOpt)
      Files.write(dir.resolve("logo-64x64.png"), b)

    dir
  }

  private def mainClass(jar: String): String = {
    val zf  = new ZipFile(new File(jar))
    val ent = zf.getEntry("META-INF/MANIFEST.MF")
    if (ent == null)
      sys.error(s"No manifest found in $jar")
    val mf   = new Manifest(zf.getInputStream(ent))
    val attr = mf.getMainAttributes()
    Option(attr.getValue(Attributes.Name.MAIN_CLASS)).getOrElse {
      sys.error(s"No main class found in $jar")
    }
  }

  /** Gets the command that launched the current application if possible.
    *
    * Works if the coursier launcher is involved.
    *
    * @param filterOutArgs:
    *   arguments to filter-out
    */
  def currentAppCommand(
    extraStartupClassPath: Seq[String],
    filterOutArgs: Set[String] = Set.empty
  ): Option[List[String]] =
    for {
      mainJar <- sys.props.get("coursier.mainJar")
      mainJar0 =
        if (mainJar.startsWith("/"))
          // Paths.get throws if given paths like "C:\\foo" on Windows, without
          // this URI stuff
          Paths.get(new URI("file://" + mainJar)).toString
        else
          mainJar // that case shouldn't happen
      mainArgs = Iterator.from(0)
        .map(i => s"coursier.main.arg-$i")
        .map(sys.props.get)
        .takeWhile(_.nonEmpty)
        .collect { case Some(arg) if !filterOutArgs(arg) => arg }
        .toList
      cp = (extraStartupClassPath :+ mainJar0).mkString(File.pathSeparator)
    } yield "java" :: "-cp" :: cp :: mainClass(mainJar0) :: mainArgs

  /** Gets the command that launched the current application if possible.
    *
    * Works if the current app is launched via a single JAR, specifying a main class in its
    * manifest.
    *
    * @param filterOutArgs:
    *   arguments to filter-out
    */
  def fatJarCurrentAppCommand(filterOutArgs: Set[String] = Set.empty): Option[List[String]] =
    for {
      mainJar <- Option(getClass.getProtectionDomain.getCodeSource)
        .flatMap(s => Option(s.getLocation))
        .flatMap(u => scala.util.Try(Paths.get(u.toURI).toFile.getAbsolutePath).toOption)
      command <- sys.props.get("sun.java.command")
      mainArgs = command
        .split("\\s+")
        .drop(1) // drop launcher path (should be mainJar)
        .filter(!filterOutArgs(_))
        .toList
    } yield "java" :: "-jar" :: mainJar :: mainArgs

  def launcherPos(command: List[String]): Option[(String, Int)] =
    command match {
      case h :: t if h == "java" || h.endsWith("/java") =>
        @tailrec
        def helper(l: List[(String, Int)]): Option[(String, Int)] =
          l match {
            case Nil =>
              None
            case (h, _) :: t =>
              if (h == "-cp")
                t.headOption.map { case (arg, n) =>
                  // assuming the launcher is last here…
                  (arg.split(File.pathSeparator).last, n + 1)
                }
              else if (h == "-jar")
                t.headOption.map { case (arg, n) => (arg, n + 1) }
              else if (h.startsWith("-"))
                helper(t)
              else
                None
          }

        helper(t.zipWithIndex)
      case _ =>
        None
    }

  def defaultConnectionFileArgs: Seq[String] =
    Seq("--connection-file", "{connection_file}")

  def install(
    defaultId: String,
    defaultDisplayName: String,
    language: String,
    options: Options,
    extraStartupClassPath: Seq[String] = Nil,
    defaultLogoOpt: Option[URL] = None,
    connectionFileArgs: Seq[String] = defaultConnectionFileArgs,
    interruptMode: Option[String] = None
  ): Path =
    install(
      defaultId,
      defaultDisplayName,
      language,
      options,
      defaultLogoOpt,
      connectionFileArgs,
      interruptMode,
      Map.empty,
      extraStartupClassPath
    )

  def install(
    defaultId: String,
    defaultDisplayName: String,
    language: String,
    options: Options,
    defaultLogoOpt: Option[URL],
    connectionFileArgs: Seq[String],
    interruptMode: Option[String],
    env: Map[String, String],
    extraStartupClassPath: Seq[String]
  ): Path = {

    val path =
      options.jupyterPath match {
        case Some(p) => Left(Paths.get(p))
        case None =>
          Right(
            if (options.global)
              JupyterPath.System
            else
              JupyterPath.User
          )
      }

    val cmd =
      options.command.map(_.trim).filter(_.nonEmpty) match {
        case Some(command) =>
          command
            .split("\\s+")
            .toSeq
        case None =>
          if (options.arg.isEmpty)
            Install.currentAppCommand(
              extraStartupClassPath,
              Set("--install", "--force", "--global").flatMap(s =>
                Seq(s, s"$s=true")
              )
            ).getOrElse {
              throw new InstallException.CannotGetKernelCommand
            }
          else
            options.arg
      }

    val logoOpt =
      options.logo match {
        case None =>
          defaultLogoOpt
        case Some("") =>
          None
        case Some(f) =>
          Some(Paths.get(f).toUri.toURL)
      }

    Install.installIn(
      options.id.getOrElse(defaultId),
      KernelSpec(
        argv = (cmd ++ connectionFileArgs).toList,
        display_name = options.displayName.getOrElse(defaultDisplayName),
        language = language,
        interrupt_mode = interruptMode,
        env = env
      ),
      path,
      logo64PngOpt = logoOpt,
      force = options.force,
      copyLauncher = options.copyLauncher0
    )
  }

  def installOrError(
    defaultId: String,
    defaultDisplayName: String,
    language: String,
    options: Options,
    extraStartupClassPath: Seq[String],
    defaultLogoOpt: Option[URL] = None,
    connectionFileArgs: Seq[String] = defaultConnectionFileArgs,
    interruptMode: Option[String] = None
  ): Either[InstallException, Path] =
    installOrError(
      defaultId,
      defaultDisplayName,
      language,
      options,
      defaultLogoOpt,
      connectionFileArgs,
      interruptMode,
      Map.empty,
      extraStartupClassPath
    )

  def installOrError(
    defaultId: String,
    defaultDisplayName: String,
    language: String,
    options: Options,
    defaultLogoOpt: Option[URL],
    connectionFileArgs: Seq[String],
    interruptMode: Option[String],
    env: Map[String, String],
    extraStartupClassPath: Seq[String]
  ): Either[InstallException, Path] =
    try {
      val dir = install(
        defaultId,
        defaultDisplayName,
        language,
        options,
        defaultLogoOpt,
        connectionFileArgs,
        interruptMode,
        env,
        extraStartupClassPath
      )
      Right(dir)
    }
    catch {
      case e: InstallException =>
        Left(e)
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy