Skip to content

Commit

Permalink
Merge pull request #6120 from adriaanm/dotfiles-212
Browse files Browse the repository at this point in the history
Move compilation daemon portfile under `~/.scalac/`
  • Loading branch information
adriaanm committed Oct 9, 2017
2 parents 4daca47 + cd54e2b commit 67fcf5c
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 56 deletions.
29 changes: 15 additions & 14 deletions src/compiler/scala/tools/nsc/CompileServer.scala
Expand Up @@ -6,11 +6,12 @@
package scala.tools.nsc

import java.io.PrintStream
import io.Directory
import scala.tools.nsc.reporters.{Reporter, ConsoleReporter}

import scala.reflect.internal.util.FakePos
import scala.tools.nsc.io.Directory
import scala.tools.nsc.reporters.{ConsoleReporter, Reporter}
import scala.tools.nsc.settings.FscSettings
import scala.tools.util.SocketServer
import settings.FscSettings

/**
* The server part of the fsc offline compiler. It awaits compilation
Expand All @@ -33,7 +34,7 @@ class StandardCompileServer(fixPort: Int = 0) extends SocketServer(fixPort) {
val MaxCharge = 0.8

private val runtime = Runtime.getRuntime()
import runtime.{ totalMemory, freeMemory, maxMemory }
import runtime.{freeMemory, maxMemory, totalMemory}

/** Create a new compiler instance */
def newGlobal(settings: Settings, reporter: Reporter) =
Expand Down Expand Up @@ -178,14 +179,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
Expand All @@ -199,8 +201,7 @@ object CompileServer {

// 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)
Expand Down
71 changes: 39 additions & 32 deletions src/compiler/scala/tools/nsc/CompileSocket.scala
Expand Up @@ -5,12 +5,17 @@

package scala.tools.nsc

import java.io.FileNotFoundException
import java.math.BigInteger
import java.security.SecureRandom
import io.{ File, Path, Socket }
import scala.tools.util.CompileOutputCommon

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.Properties.scalacDir
import scala.tools.nsc.io.{File, Socket}
import scala.tools.util.CompileOutputCommon
import scala.util.control.NonFatal

trait HasCompileSocket {
def compileSocket: CompileSocket
Expand Down Expand Up @@ -46,14 +51,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 */
Expand All @@ -69,20 +70,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.
*
Expand All @@ -104,7 +93,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) {
Expand Down Expand Up @@ -138,19 +127,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.chmodFileAndWrite(file.jfile.toPath, secretDigits)
catch chmodFailHandler(s"Cannot create file: ${file}")
}

/** Delete the port number to which a scala compile server was connected */
Expand Down Expand Up @@ -208,7 +197,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 = {
Expand All @@ -223,6 +212,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.toPath)
catch chmodFailHandler(s"Failed to change permissions on $dir. The compilation daemon requires a secure directory; use -nc to disable the daemon.")
dir
}

}


Expand Down
6 changes: 5 additions & 1 deletion src/compiler/scala/tools/nsc/GenericRunnerSettings.scala
Expand Up @@ -45,5 +45,9 @@ 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 })


private[this] var _useCompDaemon = true
def useCompDaemon: Boolean = _useCompDaemon
}
5 changes: 5 additions & 0 deletions src/compiler/scala/tools/nsc/Properties.scala
Expand Up @@ -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 {
Expand All @@ -28,4 +30,7 @@ object Properties extends scala.util.PropertiesTrait {

// derived values
def isEmacsShell = propOrEmpty("env.emacs") != ""

// Where we keep fsc's state (ports/redirection)
lazy val scalacDir = (Path(Properties.userHome) / ".scalac").createDirectory(force = false)
}
7 changes: 5 additions & 2 deletions src/compiler/scala/tools/nsc/ScriptRunner.scala
Expand Up @@ -65,7 +65,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
}
Expand Down Expand Up @@ -97,7 +100,7 @@ class ScriptRunner extends HasCompileSocket {

settings.outdir.value = compiledPath.path

if (settings.nc) {
if (!settings.useCompDaemon) {
/* Setting settings.script.value informs the compiler this is not a
* self contained compilation unit.
*/
Expand Down
59 changes: 59 additions & 0 deletions src/reflect/scala/reflect/internal/util/OwnerOnlyChmod.scala
@@ -0,0 +1,59 @@
/* NSC -- new Scala compiler
* Copyright 2017 LAMP/EPFL
* @author Martin Odersky
*/
package scala.reflect.internal.util

import java.nio.ByteBuffer
import java.nio.file.StandardOpenOption.{CREATE, TRUNCATE_EXISTING, WRITE}
import java.nio.file.attribute.PosixFilePermission.{OWNER_EXECUTE, OWNER_READ, OWNER_WRITE}
import java.nio.file.attribute.PosixFilePermissions.asFileAttribute
import java.nio.file.attribute._
import java.nio.file.{Files, Path}
import java.util.EnumSet


object OwnerOnlyChmod {
private def canPosix(path: Path) =
Files.getFileStore(path).supportsFileAttributeView(classOf[PosixFileAttributeView])

private val posixDir = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE)
private val posixFile = EnumSet.of(OWNER_READ, OWNER_WRITE)
private def fileAttributes(path: Path) =
if (canPosix(path)) Array(asFileAttribute(posixFile)) else Array.empty[FileAttribute[_]]

/** Remove group/other permissions for `file`, it if exists, and if the runtime environment supports modifying permissions. */
def chmod(path: Path): Unit = {
if (canPosix(path)) Files.setPosixFilePermissions(path, if (Files.isDirectory(path)) posixDir else posixFile)
else {
// if getting this view fails, we fail
val view = Files.getFileAttributeView(path, classOf[AclFileAttributeView])
if (view == null) throw new UnsupportedOperationException(s"Cannot get file attribute view for $path")

val acls = {
val builder = AclEntry.newBuilder
builder.setPrincipal(view.getOwner)
builder.setPermissions(AclEntryPermission.values(): _*)
builder.setType(AclEntryType.ALLOW)
val entry = builder.build
java.util.Collections.singletonList(entry)
}

view.setAcl(acls)
}
}

def chmodFileOrCreateEmpty(path: Path): Unit = {
// Create new file if none existed, with appropriate permissions via the fileAttributes attributes (if supported).
Files.newByteChannel(path, EnumSet.of(WRITE, CREATE), fileAttributes(path): _*).close()
// Change (if needed -- either because the file already existed, or the FS needs a separate call to set the ACL)
chmod(path)
}

def chmodFileAndWrite(path: Path, contents: Array[Byte]): Unit = {
val sbc = Files.newByteChannel(path, EnumSet.of(WRITE, TRUNCATE_EXISTING), fileAttributes(path): _*)
try sbc.write(ByteBuffer.wrap(contents)) finally sbc.close()
chmod(path) // for acl-based FS
}
}

Expand Up @@ -8,15 +8,37 @@ package scala.tools.nsc.interpreter.jline
import _root_.jline.console.history.PersistentHistory

import scala.tools.nsc.interpreter
import scala.reflect.io.{ File, Path }
import scala.tools.nsc.Properties.{ propOrNone, userHome }
import scala.reflect.io.{File, Path}
import scala.tools.nsc.Properties.{propOrNone, userHome}
import scala.reflect.internal.util.OwnerOnlyChmod
import scala.util.control.NonFatal

/** TODO: file locking.
*/
trait FileBackedHistory extends JLineHistory with PersistentHistory {
def maxSize: Int

protected lazy val historyFile: File = FileBackedHistory.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.chmodFileOrCreateEmpty(p.jfile.toPath)
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 {
Expand Down Expand Up @@ -86,8 +108,4 @@ object FileBackedHistory {
// val ContinuationNL: String = Array('\003', '\n').mkString

final val defaultFileName = ".scala_history"

def defaultFile: File = File(
propOrNone("scala.shell.histfile") map (Path.apply) getOrElse (Path(userHome) / defaultFileName)
)
}

0 comments on commit 67fcf5c

Please sign in to comment.