From 67e1437e55df6789d0883cb8846d12071de75c63 Mon Sep 17 00:00:00 2001
From: Jason Zaugg <jzaugg@gmail.com>
Date: Mon, 2 Oct 2017 10:06:55 +1000
Subject: [PATCH] Move compilation daemon portfile under `~/.scalac/`

Store the compilation daemon's administrativia (port file, redirection)
under `~/.scalac/`, instead of the less standard
`/tmp/scala-devel/${USER:shared}/scalac-compile-server-port`.

On creation, remove group- and other-permissions from these
private files, ditto for the repl's history file.

On Java 6 on Windows, opt in to compilation daemon using `-nc:false`.

Cherry picked from b64ad85, aa133c9, 2ceb09c
---
 .../scala/tools/nsc/CompileServer.scala       |  22 ++--
 .../scala/tools/nsc/CompileSocket.scala       |  68 ++++++-----
 .../tools/nsc/GenericRunnerSettings.scala     |   5 +-
 src/compiler/scala/tools/nsc/Properties.scala |   5 +
 .../scala/tools/nsc/ScriptRunner.scala        |  20 +++-
 .../session/FileBackedHistory.scala           |  32 +++++-
 .../tools/nsc/util/ScalaClassLoader.scala     |  27 ++---
 .../internal/util/OwnerOnlyChmod.scala        | 107 ++++++++++++++++++
 8 files changed, 221 insertions(+), 65 deletions(-)
 create mode 100644 src/reflect/scala/reflect/internal/util/OwnerOnlyChmod.scala

diff --git a/src/compiler/scala/tools/nsc/CompileServer.scala b/src/compiler/scala/tools/nsc/CompileServer.scala
index 6352d75686a..c454ba8b62b 100644
--- a/src/compiler/scala/tools/nsc/CompileServer.scala
+++ b/src/compiler/scala/tools/nsc/CompileServer.scala
@@ -183,14 +183,15 @@ object CompileServer {
     execute(() => (), args)
 
   /**
-   * Used for internal testing. The callback is called upon
-   * server start, notifying the caller that the server is
-   * ready to run. WARNING: the callback runs in the
-   * server's thread, blocking the server from doing any work
-   * until the callback is finished. Callbacks should be kept
-   * simple and clients should not try to interact with the
-   * server while the callback is processing.
-   */
+    * The server's main loop.
+    *
+    * `startupCallback` is used for internal testing; it's called upon server start,
+    * notifying the caller that the server is ready to run.
+    *
+    * WARNING: the callback runs in the server's thread, blocking the server from doing any work
+    * until the callback is finished. Callbacks should be kept simple and clients should not try to
+    * interact with the server while the callback is processing.
+    */
   def execute(startupCallback : () => Unit, args: Array[String]) {
     val debug = args contains "-v"
     var port = 0
@@ -198,14 +199,13 @@ object CompileServer {
     val i = args.indexOf("-p")
     if (i >= 0 && args.length > i + 1) {
     	scala.util.control.Exception.ignoring(classOf[NumberFormatException]) {
-    		port = args(i + 1).toInt
+		port = args(i + 1).toInt
     	}
     }
 
     // Create instance rather than extend to pass a port parameter.
     val server = new StandardCompileServer(port)
-    val redirectDir = (server.compileSocket.tmpDir / "output-redirects").createDirectory()
-
+    val redirectDir = server.compileSocket.mkDaemonDir("fsc_redirects")
     if (debug) {
       server.echo("Starting CompileServer on port " + server.port)
       server.echo("Redirect dir is " + redirectDir)
diff --git a/src/compiler/scala/tools/nsc/CompileSocket.scala b/src/compiler/scala/tools/nsc/CompileSocket.scala
index f5039b8303f..b73d251e9cc 100644
--- a/src/compiler/scala/tools/nsc/CompileSocket.scala
+++ b/src/compiler/scala/tools/nsc/CompileSocket.scala
@@ -5,13 +5,17 @@
 
 package scala.tools.nsc
 
-import java.io.FileNotFoundException
+import java.math.BigInteger
 import java.security.SecureRandom
 
+import scala.io.Codec
+import scala.reflect.internal.util.OwnerOnlyChmod
 import scala.reflect.internal.util.StringOps.splitWhere
 import scala.sys.process._
-import scala.tools.nsc.io.{File, Path, Socket}
+import scala.tools.nsc.Properties.scalacDir
+import scala.tools.nsc.io.{File, Socket}
 import scala.tools.util.CompileOutputCommon
+import scala.util.control.NonFatal
 
 trait HasCompileSocket {
   def compileSocket: CompileSocket
@@ -46,14 +50,10 @@ trait HasCompileSocket {
 class CompileSocket extends CompileOutputCommon {
   protected lazy val compileClient: StandardCompileClient = CompileClient
   def verbose = compileClient.verbose
-
+  def verbose_=(v: Boolean) = compileClient.verbose = v
   /* Fixes the port where to start the server, 0 yields some free port */
   var fixPort = 0
 
-  /** The prefix of the port identification file, which is followed
-   *  by the port number.
-   */
-  protected lazy val dirName = "scalac-compile-server-port"
   protected def cmdName = Properties.scalaCmd
 
   /** The vm part of the command to start a new scala compile server */
@@ -69,20 +69,8 @@ class CompileSocket extends CompileOutputCommon {
   protected val serverClass     = "scala.tools.nsc.CompileServer"
   protected def serverClassArgs = (if (verbose) List("-v") else Nil) ::: (if (fixPort > 0) List("-p", fixPort.toString) else Nil)
 
-  /** A temporary directory to use */
-  val tmpDir = {
-    val udir  = Option(Properties.userName) getOrElse "shared"
-    val f     = (Path(Properties.tmpDir) / ("scala-devel" + udir)).createDirectory()
-
-    if (f.isDirectory && f.canWrite) {
-      info("[Temp directory: " + f + "]")
-      f
-    }
-    else fatal("Could not find a directory for temporary files")
-  }
-
   /* A directory holding port identification files */
-  val portsDir = (tmpDir / dirName).createDirectory()
+  private lazy val portsDir = mkDaemonDir("fsc_port")
 
   /** The command which starts the compile server, given vm arguments.
     *
@@ -104,7 +92,7 @@ class CompileSocket extends CompileOutputCommon {
   }
 
   /** The port identification file */
-  def portFile(port: Int) = portsDir / File(port.toString)
+  def portFile(port: Int): File = portsDir / File(port.toString)
 
   /** Poll for a server port number; return -1 if none exists yet */
   private def pollPort(): Int = if (fixPort > 0) {
@@ -138,19 +126,19 @@ class CompileSocket extends CompileOutputCommon {
     }
     info("[Port number: " + port + "]")
     if (port < 0)
-      fatal("Could not connect to compilation daemon after " + attempts + " attempts.")
+      fatal(s"Could not connect to compilation daemon after $attempts attempts. To run without it, use `-nocompdaemon` or `-nc`.")
     port
   }
 
   /** Set the port number to which a scala compile server is connected */
-  def setPort(port: Int) {
-    val file    = portFile(port)
-    val secret  = new SecureRandom().nextInt.toString
-
-    try file writeAll secret catch {
-      case e @ (_: FileNotFoundException | _: SecurityException) =>
-        fatal("Cannot create file: %s".format(file.path))
-    }
+  def setPort(port: Int): Unit = {
+    val file = portFile(port)
+    // 128 bits of delicious randomness, suitable for printing with println over a socket,
+    // and storage in a file -- see getPassword
+    val secretDigits = new BigInteger(128, new SecureRandom()).toString.getBytes("UTF-8")
+
+    try OwnerOnlyChmod().chmodAndWrite(file.jfile, secretDigits)
+    catch chmodFailHandler(s"Cannot create file: ${file}")
   }
 
   /** Delete the port number to which a scala compile server was connected */
@@ -208,7 +196,7 @@ class CompileSocket extends CompileOutputCommon {
 
   def getPassword(port: Int): String = {
     val ff  = portFile(port)
-    val f   = ff.bufferedReader()
+    val f   = ff.bufferedReader(Codec.UTF8)
 
     // allow some time for the server to start up
     def check = {
@@ -223,6 +211,24 @@ class CompileSocket extends CompileOutputCommon {
     f.close()
     result
   }
+
+  private def chmodFailHandler(msg: String): PartialFunction[Throwable, Unit] = {
+    case NonFatal(e) =>
+      if (verbose) e.printStackTrace()
+      fatal(msg)
+  }
+
+  def mkDaemonDir(name: String) = {
+    val dir = (scalacDir / name).createDirectory()
+
+    if (dir.isDirectory && dir.canWrite) info(s"[Temp directory: $dir]")
+    else fatal(s"Could not create compilation daemon directory $dir")
+
+    try OwnerOnlyChmod().chmod(dir.jfile)
+    catch chmodFailHandler(s"Failed to change permissions on $dir. The compilation daemon requires a secure directory; use -nc to disable the daemon.")
+    dir
+  }
+
 }
 
 
diff --git a/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala b/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala
index 9c2db11a56e..edfc095c7f7 100644
--- a/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala
+++ b/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala
@@ -38,8 +38,11 @@ class GenericRunnerSettings(error: String => Unit) extends Settings(error) {
 
   val nc = BooleanSetting(
       "-nc",
-      "do not use the fsc compilation daemon") withAbbreviation "-nocompdaemon"
+      "do not use the fsc compilation daemon") withAbbreviation "-nocompdaemon" withPostSetHook((x: BooleanSetting) => {_useCompDaemon = !x.value })
 
   @deprecated("Use `nc` instead", "2.9.0") def nocompdaemon = nc
   @deprecated("Use `save` instead", "2.9.0") def savecompiled = save
+
+  private[this] var _useCompDaemon = true
+  def useCompDaemon: Boolean = _useCompDaemon
 }
diff --git a/src/compiler/scala/tools/nsc/Properties.scala b/src/compiler/scala/tools/nsc/Properties.scala
index 55fd1967164..8b314ba0b82 100644
--- a/src/compiler/scala/tools/nsc/Properties.scala
+++ b/src/compiler/scala/tools/nsc/Properties.scala
@@ -5,6 +5,8 @@
 
 package scala.tools.nsc
 
+import scala.tools.nsc.io.Path
+
 /** Loads `compiler.properties` from the jar archive file.
  */
 object Properties extends scala.util.PropertiesTrait {
@@ -22,4 +24,7 @@ object Properties extends scala.util.PropertiesTrait {
   // derived values
   def isEmacsShell         = propOrEmpty("env.emacs") != ""
   def fileEndings          = fileEndingString.split("""\|""").toList
+
+  // Where we keep fsc's state (ports/redirection)
+  lazy val scalacDir = (Path(Properties.userHome) / ".scalac").createDirectory(force = false)
 }
diff --git a/src/compiler/scala/tools/nsc/ScriptRunner.scala b/src/compiler/scala/tools/nsc/ScriptRunner.scala
index 107c4b3df3d..9af0079ffd6 100644
--- a/src/compiler/scala/tools/nsc/ScriptRunner.scala
+++ b/src/compiler/scala/tools/nsc/ScriptRunner.scala
@@ -77,7 +77,10 @@ class ScriptRunner extends HasCompileSocket {
     val coreCompArgs     = compSettings flatMap (_.unparse)
     val compArgs         = coreCompArgs ++ List("-Xscript", scriptMain(settings), scriptFile)
 
-    CompileSocket getOrCreateSocket "" match {
+    // TODO: untangle this mess of top-level objects with their own little view of the mutable world of settings
+    compileSocket.verbose = settings.verbose.value
+
+    compileSocket getOrCreateSocket "" match {
       case Some(sock) => compileOnServer(sock, compArgs)
       case _          => false
     }
@@ -109,14 +112,23 @@ class ScriptRunner extends HasCompileSocket {
 
       settings.outdir.value = compiledPath.path
 
-      if (settings.nc.value) {
-        /** Setting settings.script.value informs the compiler this is not a
-         *  self contained compilation unit.
+      // can't reliably lock down permissions on the portfile in this environment => disable by default.
+      // not the cleanest to do this here, but I don't see where else to decide this and emit the warning below
+      val cantLockdown = !settings.nc.isSetByUser && scala.util.Properties.isWin && !scala.util.Properties.isJavaAtLeast("7")
+
+      if (cantLockdown) settings.nc.value = true
+
+      if (!settings.useCompDaemon) {
+        /* Setting settings.script.value informs the compiler this is not a
+         * self contained compilation unit.
          */
         settings.script.value = mainClass
         val reporter = new ConsoleReporter(settings)
         val compiler = newGlobal(settings, reporter)
 
+        if (cantLockdown)
+          reporter.echo("[info] The compilation daemon is disabled by default on this platform. To force its usage, use `-nocompdaemon:false`.")
+
         new compiler.Run compile List(scriptFile)
         if (reporter.hasErrors) None else Some(compiledPath)
       }
diff --git a/src/compiler/scala/tools/nsc/interpreter/session/FileBackedHistory.scala b/src/compiler/scala/tools/nsc/interpreter/session/FileBackedHistory.scala
index dddfb1b8f64..5467c0a61ef 100644
--- a/src/compiler/scala/tools/nsc/interpreter/session/FileBackedHistory.scala
+++ b/src/compiler/scala/tools/nsc/interpreter/session/FileBackedHistory.scala
@@ -7,14 +7,37 @@ package scala.tools.nsc
 package interpreter
 package session
 
-import scala.tools.nsc.io._
-import FileBackedHistory._
+import scala.reflect.internal.util.OwnerOnlyChmod
+import scala.reflect.io.{File, Path}
+import scala.tools.nsc.Properties.{propOrNone, userHome}
+import scala.util.control.NonFatal
 
 /** TODO: file locking.
  */
 trait FileBackedHistory extends JLineHistory with JPersistentHistory {
   def maxSize: Int = 2500
-  protected lazy val historyFile: File = defaultFile
+
+  // For a history file in the standard location, always try to restrict permission,
+  // creating an empty file if none exists.
+  // For a user-specified location, only lock down permissions if we're the ones
+  // creating it, otherwise responsibility for permissions is up to the caller.
+  protected lazy val historyFile: File = File {
+    propOrNone("scala.shell.histfile").map(Path.apply) match {
+      case Some(p) => if (!p.exists) secure(p) else p
+      case None => secure(Path(userHome) / FileBackedHistory.defaultFileName)
+    }
+  }
+
+  private def secure(p: Path): Path = {
+    try OwnerOnlyChmod().chmodOrCreateEmpty(p.jfile)
+    catch { case NonFatal(e) =>
+      if (interpreter.isReplDebug) e.printStackTrace()
+      interpreter.replinfo(s"Warning: history file ${p}'s permissions could not be restricted to owner-only.")
+    }
+
+    p
+  }
+
   private var isPersistent = true
 
   locally {
@@ -79,6 +102,5 @@ object FileBackedHistory {
   //   val ContinuationNL: String = Array('\003', '\n').mkString
   import Properties.userHome
 
-  def defaultFileName = ".scala_history"
-  def defaultFile: File = File(Path(userHome) / defaultFileName)
+  final val defaultFileName = ".scala_history"
 }
diff --git a/src/compiler/scala/tools/nsc/util/ScalaClassLoader.scala b/src/compiler/scala/tools/nsc/util/ScalaClassLoader.scala
index 1f6fa68f572..0673fa1f758 100644
--- a/src/compiler/scala/tools/nsc/util/ScalaClassLoader.scala
+++ b/src/compiler/scala/tools/nsc/util/ScalaClassLoader.scala
@@ -3,19 +3,18 @@
  * @author  Paul Phillips
  */
 
-package scala.tools.nsc
-package util
-
-import java.lang.{ ClassLoader => JClassLoader }
-import java.lang.reflect.{ Constructor, Modifier, Method }
-import java.io.{ File => JFile }
-import java.net.{ URLClassLoader => JURLClassLoader }
-import java.net.URL
-import scala.reflect.runtime.ReflectionUtils.unwrapHandler
-import ScalaClassLoader._
-import scala.util.control.Exception.{ catching }
+package scala.tools.nsc.util
+
+import java.io.{File => JFile}
+import java.lang.reflect.{Constructor, Modifier}
+import java.lang.{ClassLoader => JClassLoader}
+import java.net.{URL, URLClassLoader => JURLClassLoader}
+
 import scala.language.implicitConversions
-import scala.reflect.{ ClassTag, classTag }
+import scala.reflect.runtime.ReflectionUtils.unwrapHandler
+import scala.reflect.{ClassTag, classTag}
+import scala.tools.nsc.io.Streamable
+import scala.util.control.Exception.catching
 
 trait HasClassPath {
   def classPathURLs: Seq[URL]
@@ -25,6 +24,8 @@ trait HasClassPath {
  *  of java reflection.
  */
 trait ScalaClassLoader extends JClassLoader {
+  import ScalaClassLoader._
+
   /** Executing an action with this classloader as context classloader */
   def asContext[T](action: => T): T = {
     val saved = contextLoader
@@ -52,7 +53,7 @@ trait ScalaClassLoader extends JClassLoader {
   /** The actual bytes for a class file, or an empty array if it can't be found. */
   def classBytes(className: String): Array[Byte] = classAsStream(className) match {
     case null   => Array()
-    case stream => io.Streamable.bytes(stream)
+    case stream => Streamable.bytes(stream)
   }
 
   /** An InputStream representing the given class name, or null if not found. */
diff --git a/src/reflect/scala/reflect/internal/util/OwnerOnlyChmod.scala b/src/reflect/scala/reflect/internal/util/OwnerOnlyChmod.scala
new file mode 100644
index 00000000000..c0da65db387
--- /dev/null
+++ b/src/reflect/scala/reflect/internal/util/OwnerOnlyChmod.scala
@@ -0,0 +1,107 @@
+/* NSC -- new Scala compiler
+ * Copyright 2017 LAMP/EPFL
+ * @author  Martin Odersky
+ */
+package scala.reflect.internal.util
+
+import java.io.{File, FileOutputStream, IOException}
+
+
+trait OwnerOnlyChmod {
+  /** Remove group/other permissions for `file`, it if exists */
+  def chmod(file: java.io.File): Unit
+
+  /** Delete `file` if it exists, recreate it with no group/other permissions, and write `contents` */
+  final def chmodAndWrite(file: File, contents: Array[Byte]): Unit = {
+    file.delete()
+    val fos = new FileOutputStream(file)
+    fos.close()
+    chmod(file)
+    val fos2 = new FileOutputStream(file)
+    try {
+      fos2.write(contents)
+    } finally {
+      fos2.close()
+    }
+  }
+
+  // TODO: use appropriate NIO call instead of two-step exists?/create!
+  final def chmodOrCreateEmpty(file: File): Unit =
+    if (!file.exists()) chmodAndWrite(file, Array[Byte]()) else chmod(file)
+
+}
+
+object OwnerOnlyChmod {
+  def apply(): OwnerOnlyChmod = {
+    if (!util.Properties.isWin) Java6UnixChmod
+    else if (util.Properties.isJavaAtLeast("7")) new NioAclChmodReflective
+    else NoOpOwnerOnlyChmod
+  }
+}
+
+object NoOpOwnerOnlyChmod extends OwnerOnlyChmod {
+  override def chmod(file: File): Unit = ()
+}
+
+
+/** Adjust permissions with `File.{setReadable, setWritable}` */
+object Java6UnixChmod extends OwnerOnlyChmod {
+
+  def chmod(file: File): Unit = if (file.exists()) {
+    def clearAndSetOwnerOnly(f: (Boolean, Boolean) => Boolean): Unit = {
+      def fail() = throw new IOException("Unable to modify permissions of " + file)
+      // attribute = false, ownerOnly = false
+      if (!f(false, false)) fail()
+      // attribute = true, ownerOnly = true
+      if (!f(true, true)) fail()
+    }
+    if (file.isDirectory) {
+      clearAndSetOwnerOnly(file.setExecutable)
+    }
+    clearAndSetOwnerOnly(file.setReadable)
+    clearAndSetOwnerOnly(file.setWritable)
+  }
+}
+
+
+object NioAclChmodReflective {
+  val file_toPath = classOf[java.io.File].getMethod("toPath")
+  val files = Class.forName("java.nio.file.Files")
+  val path_class = Class.forName("java.nio.file.Path")
+  val getFileAttributeView = files.getMethod("getFileAttributeView", path_class, classOf[Class[_]], Class.forName("[Ljava.nio.file.LinkOption;"))
+  val linkOptionEmptyArray = java.lang.reflect.Array.newInstance(Class.forName("java.nio.file.LinkOption"), 0)
+  val aclFileAttributeView_class = Class.forName("java.nio.file.attribute.AclFileAttributeView")
+  val aclEntry_class = Class.forName("java.nio.file.attribute.AclEntry")
+  val aclEntryBuilder_class = Class.forName("java.nio.file.attribute.AclEntry$Builder")
+  val newBuilder = aclEntry_class.getMethod("newBuilder")
+  val aclEntryBuilder_build = aclEntryBuilder_class.getMethod("build")
+  val userPrinciple_class = Class.forName("java.nio.file.attribute.UserPrincipal")
+  val setPrincipal = aclEntryBuilder_class.getMethod("setPrincipal", userPrinciple_class)
+  val setPermissions = aclEntryBuilder_class.getMethod("setPermissions", Class.forName("[Ljava.nio.file.attribute.AclEntryPermission;"))
+  val aclEntryType_class = Class.forName("java.nio.file.attribute.AclEntryType")
+  val setType = aclEntryBuilder_class.getMethod("setType", aclEntryType_class)
+  val aclEntryPermission_class = Class.forName("java.nio.file.attribute.AclEntryPermission")
+  val aclEntryPermissionValues = aclEntryPermission_class.getDeclaredMethod("values")
+  val aclEntryType_ALLOW = aclEntryType_class.getDeclaredField("ALLOW")
+}
+
+/** Reflective version of `NioAclChmod` */
+final class NioAclChmodReflective extends OwnerOnlyChmod {
+  import NioAclChmodReflective._
+  def chmod(file: java.io.File): Unit = {
+    val path = file_toPath.invoke(file)
+    val view = getFileAttributeView.invoke(null, path, aclFileAttributeView_class, linkOptionEmptyArray)
+    val setAcl = aclFileAttributeView_class.getMethod("setAcl", classOf[java.util.List[_]])
+    val getOwner = aclFileAttributeView_class.getMethod("getOwner")
+    val owner = getOwner.invoke(view)
+    setAcl.invoke(view, acls(owner))
+  }
+
+  private def acls(owner: Object) = {
+    val builder = newBuilder.invoke(null)
+    setPrincipal.invoke(builder, owner)
+    setPermissions.invoke(builder, aclEntryPermissionValues.invoke(null))
+    setType.invoke(builder, aclEntryType_ALLOW.get(null))
+    java.util.Collections.singletonList(aclEntryBuilder_build.invoke(builder))
+  }
+}
