-
Notifications
You must be signed in to change notification settings - Fork 0
/
DaemonModule.scala
200 lines (182 loc) · 6.75 KB
/
DaemonModule.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package com.mchange.milldaemon
import mill._, define._, scalalib._
import mill.api.{Ctx,Result}
import mill.define.Command
import mill.util.Jvm
import mainargs.arg
import scala.util.control.NonFatal
trait DaemonModule extends JavaModule {
val EnvMillDaemonPidFile = "MILL_DAEMON_PID_FILE"
def runDaemonOut : os.ProcessOutput = os.InheritRaw
def runDaemonErr : os.ProcessOutput = os.InheritRaw
def runDaemonPidFile : Option[os.Path] = None
// modified from mill.util.Jvm.runSubprocessWithBackgroundOutputs
/**
* Runs a JVM subprocess as a freely forked daemon with the given configuration
* @param mainClass The main class to run
* @param classPath The classpath
* @param JvmArgs Arguments given to the forked JVM
* @param envArgs Environment variables used when starting the forked JVM
* @param workingDir The working directory to be used by the forked JVM
* @param daemonOutputs A tuple (stdout,stderr) for the spawned process
* @param useCpPassingJar When `false`, the `-cp` parameter is used to pass the classpath
* to the forked JVM.
* When `true`, a temporary empty JAR is created
* which contains a `Class-Path` manifest entry containing the actual classpath.
* This might help with long classpaths on OS'es (like Windows)
* which only supports limited command-line length
*/
def runDaemonSubprocess(
mainClass: String,
classPath: Agg[os.Path],
jvmArgs: Seq[String] = Seq.empty,
envArgs: Map[String, String] = Map.empty,
mainArgs: Seq[String] = Seq.empty,
workingDir: os.Path = null,
daemonOutputs: Tuple2[os.ProcessOutput, os.ProcessOutput],
useCpPassingJar: Boolean = false
)(implicit ctx: Ctx): os.SubProcess = {
val cp =
if (useCpPassingJar && !classPath.iterator.isEmpty) {
val passingJar = os.temp(prefix = "run-", suffix = ".jar", deleteOnExit = false)
ctx.log.debug(
s"Creating classpath passing jar '${passingJar}' with Class-Path: ${classPath.iterator.map(
_.toNIO.toUri().toURL().toExternalForm()
).mkString(" ")}"
)
Jvm.createClasspathPassingJar(passingJar, classPath)
Agg(passingJar)
} else {
classPath
}
val javaExe = Jvm.javaExe
val args =
Vector(javaExe) ++
jvmArgs ++
Vector("-cp", cp.iterator.mkString(java.io.File.pathSeparator), mainClass) ++
mainArgs
ctx.log.debug(s"Run daemon subprocess with args: ${args.map(a => s"'${a}'").mkString(" ")}")
// from 0.12.0+, we can't use mill utility JVM.spawnSubprocessWithBackgroundOutputs(...)
// because the newer version of os-lib it uses defaults to destroying the subprocess on parent
// process exit.
os.proc(args).spawn(
cwd = workingDir,
env = envArgs,
stdin = "",
stdout = daemonOutputs._1,
stderr = daemonOutputs._2,
destroyOnExit = false
)
}
lazy val ProcessPidMethod : java.lang.reflect.Method =
try {
classOf[Process].getMethod("pid")
}
catch {
case nsme : NoSuchMethodException =>
throw new Exception("Pre Java 9 JVMs do not support discovery of process PIDs.", nsme)
}
private def pid( subProcess : os.SubProcess ) : Long = {
val jproc = subProcess.wrapped
ProcessPidMethod.invoke( jproc ).asInstanceOf[Long]
}
private def canWrite( ctx : Ctx, path : os.Path ) : Boolean = {
val f = path.toIO
if (os.exists(path)) f.canWrite()
else {
try {
f.createNewFile()
}
catch {
case NonFatal(t) =>
ctx.log.debug(s"Exception while testing file creation: $t")
false
}
finally {
try f.delete()
catch {
case NonFatal(t) =>
ctx.log.debug(s"Exception while cleaning up (deleting) file creation test: $t")
}
}
}
}
protected def doRunDaemon(
runClasspath: Seq[PathRef],
forkArgs: Seq[String],
forkEnv: Map[String, String],
finalMainClass: String,
forkWorkingDir: os.Path,
runUseArgsFile: Boolean,
daemonOutputs: Tuple2[os.ProcessOutput, os.ProcessOutput],
pidFile: Option[os.Path]
)(args: String*): Ctx => Result[os.SubProcess] = ctx => {
def spawnIt( extraEnv : Seq[(String,String)] = Nil ) =
runDaemonSubprocess(
finalMainClass,
runClasspath.map(_.path),
forkArgs,
forkEnv ++ extraEnv,
args,
workingDir = forkWorkingDir,
daemonOutputs,
useCpPassingJar = runUseArgsFile
)(ctx)
try {
pidFile match {
case Some( path ) if os.exists( path ) =>
Result.Failure(s"A file already exists at PID file location ${path}. Please ensure no daemon is currently running, then delete this file.")
case Some( path ) if !canWrite(ctx,path) =>
Result.Failure(s"Insufficient permission: Cannot write PID file to location ${path}.")
case Some( path ) =>
val subProcess = spawnIt( Seq( EnvMillDaemonPidFile -> path.toString() ) )
os.write( path, data = pid(subProcess).toString() + System.lineSeparator() )
Result.Success(subProcess)
case None =>
Result.Success(spawnIt())
}
}
catch {
case NonFatal(t) =>
Result.Failure("Failed to spawn daemon subprocess: " + t.printStackTrace())
}
}
/**
* Runs this module's code in the background as a freestanding daemon process.
* The process will run indefinitely, until it exits or it is terminated externally.
* It can survive termination of the parent mill process.
*/
def runDaemon(args: String*): Command[Unit] = T.command {
val ctx = implicitly[Ctx]
val rsubp =
doRunDaemon(
runClasspath = runClasspath(),
forkArgs = forkArgs(),
forkEnv = forkEnv(),
finalMainClass = finalMainClass(),
forkWorkingDir = forkWorkingDir(),
runUseArgsFile = runUseArgsFile(),
daemonOutputs = ( runDaemonOut, runDaemonErr ),
pidFile = runDaemonPidFile
)(args: _*)(ctx)
rsubp.map( _ => () )
}
/**
* Same as `runDaemon`, but lets you specify a main class to run
*/
def runMainDaemon(@arg(positional = true) mainClass: String, args: String*): Command[Unit] = T.command {
val ctx = implicitly[Ctx]
val rsubp =
doRunDaemon(
runClasspath = runClasspath(),
forkArgs = forkArgs(),
forkEnv = forkEnv(),
finalMainClass = mainClass,
forkWorkingDir = forkWorkingDir(),
runUseArgsFile = runUseArgsFile(),
daemonOutputs = ( runDaemonOut, runDaemonErr ),
pidFile = runDaemonPidFile
)(args: _*)(ctx)
rsubp.map( _ => () )
}
}