11package os
22
3- import java .util .concurrent .{ArrayBlockingQueue , Semaphore , TimeUnit }
43import collection .JavaConverters ._
5- import scala .annotation .tailrec
64import java .lang .ProcessBuilder .Redirect
75import os .SubProcess .InputStream
86import java .io .IOException
97import java .util .concurrent .LinkedBlockingQueue
108import ProcessOps ._
11- import scala .util .Try
129
1310object call {
1411
@@ -28,7 +25,8 @@ object call {
2825 timeout : Long = - 1 ,
2926 check : Boolean = true ,
3027 propagateEnv : Boolean = true ,
31- timeoutGracePeriod : Long = 100
28+ shutdownGracePeriod : Long = 100 ,
29+ destroyOnExit : Boolean = true
3230 ): CommandResult = {
3331 os.proc(cmd).call(
3432 cwd = cwd,
@@ -40,7 +38,40 @@ object call {
4038 timeout = timeout,
4139 check = check,
4240 propagateEnv = propagateEnv,
43- timeoutGracePeriod = timeoutGracePeriod
41+ shutdownGracePeriod = shutdownGracePeriod,
42+ destroyOnExit = destroyOnExit
43+ )
44+ }
45+
46+ // Bincompat Forwarder
47+ def apply (
48+ cmd : Shellable ,
49+ env : Map [String , String ],
50+ // Make sure `cwd` only comes after `env`, so `os.call("foo", path)` is a compile error
51+ // since the correct syntax is `os.call(("foo", path))`
52+ cwd : Path ,
53+ stdin : ProcessInput ,
54+ stdout : ProcessOutput ,
55+ stderr : ProcessOutput ,
56+ mergeErrIntoOut : Boolean ,
57+ timeout : Long ,
58+ check : Boolean ,
59+ propagateEnv : Boolean ,
60+ timeoutGracePeriod : Long
61+ ): CommandResult = {
62+ call(
63+ cmd = cmd,
64+ cwd = cwd,
65+ env = env,
66+ stdin = stdin,
67+ stdout = stdout,
68+ stderr = stderr,
69+ mergeErrIntoOut = mergeErrIntoOut,
70+ timeout = timeout,
71+ check = check,
72+ propagateEnv = propagateEnv,
73+ shutdownGracePeriod = timeoutGracePeriod,
74+ destroyOnExit = false
4475 )
4576 }
4677}
@@ -59,7 +90,9 @@ object spawn {
5990 stdout : ProcessOutput = Pipe ,
6091 stderr : ProcessOutput = os.Inherit ,
6192 mergeErrIntoOut : Boolean = false ,
62- propagateEnv : Boolean = true
93+ propagateEnv : Boolean = true ,
94+ shutdownGracePeriod : Long = 100 ,
95+ destroyOnExit : Boolean = true
6396 ): SubProcess = {
6497 os.proc(cmd).spawn(
6598 cwd = cwd,
@@ -68,7 +101,36 @@ object spawn {
68101 stdout = stdout,
69102 stderr = stderr,
70103 mergeErrIntoOut = mergeErrIntoOut,
71- propagateEnv = propagateEnv
104+ propagateEnv = propagateEnv,
105+ shutdownGracePeriod = shutdownGracePeriod,
106+ destroyOnExit = destroyOnExit
107+ )
108+ }
109+
110+ // Bincompat Forwarder
111+ def apply (
112+ cmd : Shellable ,
113+ // Make sure `cwd` only comes after `env`, so `os.spawn("foo", path)` is a compile error
114+ // since the correct syntax is `os.spawn(("foo", path))`
115+ env : Map [String , String ],
116+ cwd : Path ,
117+ stdin : ProcessInput ,
118+ stdout : ProcessOutput ,
119+ stderr : ProcessOutput ,
120+ mergeErrIntoOut : Boolean ,
121+ propagateEnv : Boolean
122+ ): SubProcess = {
123+ spawn(
124+ cmd = cmd,
125+ cwd = cwd,
126+ env = env,
127+ stdin = stdin,
128+ stdout = stdout,
129+ stderr = stderr,
130+ mergeErrIntoOut = mergeErrIntoOut,
131+ propagateEnv = propagateEnv,
132+ shutdownGracePeriod = 100 ,
133+ destroyOnExit = false
72134 )
73135 }
74136}
@@ -119,7 +181,7 @@ case class proc(command: Shellable*) {
119181 * fails with a non-zero exit code
120182 * @param propagateEnv disable this to avoid passing in this parent process's
121183 * environment variables to the subprocess
122- * @param timeoutGracePeriod if the timeout is enabled, how long in milliseconds for the
184+ * @param shutdownGracePeriod if the timeout is enabled, how long in milliseconds for the
123185 * subprocess to gracefully terminate before attempting to
124186 * forcibly kill it
125187 * (-1 for no kill, 0 for always kill immediately)
@@ -138,7 +200,8 @@ case class proc(command: Shellable*) {
138200 check : Boolean = true ,
139201 propagateEnv : Boolean = true ,
140202 // this cannot be next to `timeout` as this will introduce a bin-compat break (default arguments are numbered in the bytecode)
141- timeoutGracePeriod : Long = 100
203+ shutdownGracePeriod : Long = 100 ,
204+ destroyOnExit : Boolean = true
142205 ): CommandResult = {
143206
144207 val chunks = new java.util.concurrent.ConcurrentLinkedQueue [Either [geny.Bytes , geny.Bytes ]]
@@ -159,7 +222,7 @@ case class proc(command: Shellable*) {
159222 propagateEnv
160223 )
161224
162- sub.join(timeout, timeoutGracePeriod )
225+ sub.join(timeout, shutdownGracePeriod )
163226
164227 val chunksSeq = chunks.iterator.asScala.toIndexedSeq
165228 val res = CommandResult (commandChunks, sub.exitCode(), chunksSeq)
@@ -188,7 +251,33 @@ case class proc(command: Shellable*) {
188251 timeout,
189252 check,
190253 propagateEnv,
191- timeoutGracePeriod = 100
254+ shutdownGracePeriod = 100
255+ )
256+
257+ // Bincompat Forwarder
258+ private [os] def call (
259+ cwd : Path ,
260+ env : Map [String , String ],
261+ stdin : ProcessInput ,
262+ stdout : ProcessOutput ,
263+ stderr : ProcessOutput ,
264+ mergeErrIntoOut : Boolean ,
265+ timeout : Long ,
266+ check : Boolean ,
267+ propagateEnv : Boolean ,
268+ timeoutGracePeriod : Long
269+ ): CommandResult = call(
270+ cwd,
271+ env,
272+ stdin,
273+ stdout,
274+ stderr,
275+ mergeErrIntoOut,
276+ timeout,
277+ check,
278+ propagateEnv,
279+ timeoutGracePeriod,
280+ destroyOnExit = false
192281 )
193282
194283 /**
@@ -208,7 +297,9 @@ case class proc(command: Shellable*) {
208297 stdout : ProcessOutput = Pipe ,
209298 stderr : ProcessOutput = os.Inherit ,
210299 mergeErrIntoOut : Boolean = false ,
211- propagateEnv : Boolean = true
300+ propagateEnv : Boolean = true ,
301+ shutdownGracePeriod : Long = 100 ,
302+ destroyOnExit : Boolean = true
212303 ): SubProcess = {
213304
214305 val cmdChunks = commandChunks
@@ -230,19 +321,62 @@ case class proc(command: Shellable*) {
230321 propagateEnv
231322 )
232323
324+ lazy val shutdownHookThread =
325+ if (! destroyOnExit) None
326+ else Some (new Thread (" subprocess-shutdown-hook" ) {
327+ override def run (): Unit = proc.destroy(shutdownGracePeriod)
328+ })
329+
330+ lazy val shutdownHookMonitorThread = shutdownHookThread.map(t =>
331+ new Thread (" subprocess-shutdown-hook-monitor" ) {
332+ override def run (): Unit = {
333+ while (proc.wrapped.isAlive) Thread .sleep(1 )
334+ try Runtime .getRuntime().removeShutdownHook(t)
335+ catch { case e : Throwable => /* do nothing*/ }
336+ }
337+ }
338+ )
339+
340+ shutdownHookThread.foreach(Runtime .getRuntime().addShutdownHook)
341+
233342 lazy val proc : SubProcess = new SubProcess (
234343 builder.start(),
235344 resolvedStdin.processInput(proc.stdin).map(new Thread (_, commandStr + " stdin thread" )),
236345 resolvedStdout.processOutput(proc.stdout).map(new Thread (_, commandStr + " stdout thread" )),
237- resolvedStderr.processOutput(proc.stderr).map(new Thread (_, commandStr + " stderr thread" ))
346+ resolvedStderr.processOutput(proc.stderr).map(new Thread (_, commandStr + " stderr thread" )),
347+ shutdownGracePeriod = shutdownGracePeriod,
348+ shutdownHookMonitorThread = shutdownHookMonitorThread
238349 )
239350
351+ shutdownHookMonitorThread.foreach(_.start())
352+
240353 proc.inputPumperThread.foreach(_.start())
241354 proc.outputPumperThread.foreach(_.start())
242355 proc.errorPumperThread.foreach(_.start())
243356 proc
244357 }
245358
359+ // Bincompat Forwarder
360+ def spawn (
361+ cwd : Path ,
362+ env : Map [String , String ],
363+ stdin : ProcessInput ,
364+ stdout : ProcessOutput ,
365+ stderr : ProcessOutput ,
366+ mergeErrIntoOut : Boolean ,
367+ propagateEnv : Boolean
368+ ): SubProcess = spawn(
369+ cwd = cwd,
370+ env = env,
371+ stdin = stdin,
372+ stdout = stdout,
373+ stderr = stderr,
374+ mergeErrIntoOut = mergeErrIntoOut,
375+ propagateEnv = propagateEnv,
376+ shutdownGracePeriod = 100 ,
377+ destroyOnExit = false
378+ )
379+
246380 /**
247381 * Pipes the output of this process into the input of the [[next ]] process. Returns a
248382 * [[ProcGroup ]] containing both processes, which you can then either execute or
@@ -295,7 +429,7 @@ case class ProcGroup private[os] (commands: Seq[proc]) {
295429 * will be caught and handled by killing the writing process. This behaviour
296430 * is consistent with handlers of SIGPIPE signals in most programs
297431 * supporting interruptable piping. Disabled by default on Windows.
298- * @param timeoutGracePeriod if the timeout is enabled, how long in milliseconds for the
432+ * @param shutdownGracePeriod if the timeout is enabled, how long in milliseconds for the
299433 * subprocess to gracefully terminate before attempting to
300434 * forcibly kill it
301435 * (-1 for no kill, 0 for always kill immediately)
@@ -316,7 +450,7 @@ case class ProcGroup private[os] (commands: Seq[proc]) {
316450 pipefail : Boolean = true ,
317451 handleBrokenPipe : Boolean = ! isWindows,
318452 // this cannot be next to `timeout` as this will introduce a bin-compat break (default arguments are numbered in the bytecode)
319- timeoutGracePeriod : Long = 100
453+ shutdownGracePeriod : Long = 100
320454 ): CommandResult = {
321455 val chunks = new java.util.concurrent.ConcurrentLinkedQueue [Either [geny.Bytes , geny.Bytes ]]
322456
@@ -337,7 +471,7 @@ case class ProcGroup private[os] (commands: Seq[proc]) {
337471 pipefail
338472 )
339473
340- sub.join(timeout, timeoutGracePeriod )
474+ sub.join(timeout, shutdownGracePeriod )
341475
342476 val chunksSeq = chunks.iterator.asScala.toIndexedSeq
343477 val res =
@@ -370,7 +504,7 @@ case class ProcGroup private[os] (commands: Seq[proc]) {
370504 propagateEnv,
371505 pipefail,
372506 handleBrokenPipe,
373- timeoutGracePeriod = 100
507+ shutdownGracePeriod = 100
374508 )
375509
376510 /**
0 commit comments