11package os
22
33import java .util .concurrent .{ArrayBlockingQueue , Semaphore , TimeUnit }
4-
4+ import collection . JavaConverters . _
55import scala .annotation .tailrec
6+ import java .lang .ProcessBuilder .Redirect
7+ import os .SubProcess .InputStream
8+ import java .io .IOException
9+ import java .util .concurrent .LinkedBlockingQueue
10+ import ProcessOps ._
11+ import scala .util .Try
612
713/**
814 * Convenience APIs around [[java.lang.Process ]] and [[java.lang.ProcessBuilder ]]:
@@ -21,6 +27,7 @@ import scala.annotation.tailrec
2127 * the standard stdin/stdout/stderr streams, using whatever protocol you
2228 * want
2329 */
30+
2431case class proc (command : Shellable * ) {
2532 def commandChunks : Seq [String ] = command.flatMap(_.value)
2633
@@ -79,7 +86,6 @@ case class proc(command: Shellable*) {
7986 mergeErrIntoOut,
8087 propagateEnv
8188 )
82- import collection .JavaConverters ._
8389
8490 sub.join(timeout)
8591
@@ -94,9 +100,6 @@ case class proc(command: Shellable*) {
94100 * and starts a subprocess, and returns it as a `java.lang.Process` for you to
95101 * interact with however you like.
96102 *
97- * To implement pipes, you can spawn a process, take it's stdout, and pass it
98- * as the stdin of a second spawned process.
99- *
100103 * Note that if you provide `ProcessOutput` callbacks to `stdout`/`stderr`,
101104 * the calls to those callbacks take place on newly spawned threads that
102105 * execute in parallel with the main thread. Thus make sure any data
@@ -111,28 +114,13 @@ case class proc(command: Shellable*) {
111114 mergeErrIntoOut : Boolean = false ,
112115 propagateEnv : Boolean = true
113116 ): SubProcess = {
114- val builder = new java.lang.ProcessBuilder ()
115-
116- val baseEnv =
117- if (propagateEnv) sys.env
118- else Map ()
119- for ((k, v) <- baseEnv ++ Option (env).getOrElse(Map ())) {
120- if (v != null ) builder.environment().put(k, v)
121- else builder.environment().remove(k)
122- }
123-
124- builder.directory(Option (cwd).getOrElse(os.pwd).toIO)
117+ val builder =
118+ buildProcess(commandChunks, cwd, env, stdin, stdout, stderr, mergeErrIntoOut, propagateEnv)
125119
126120 val cmdChunks = commandChunks
127121 val commandStr = cmdChunks.mkString(" " )
128122 lazy val proc : SubProcess = new SubProcess (
129- builder
130- .command(cmdChunks : _* )
131- .redirectInput(stdin.redirectFrom)
132- .redirectOutput(stdout.redirectTo)
133- .redirectError(stderr.redirectTo)
134- .redirectErrorStream(mergeErrIntoOut)
135- .start(),
123+ builder.start(),
136124 stdin.processInput(proc.stdin).map(new Thread (_, commandStr + " stdin thread" )),
137125 stdout.processOutput(proc.stdout).map(new Thread (_, commandStr + " stdout thread" )),
138126 stderr.processOutput(proc.stderr).map(new Thread (_, commandStr + " stderr thread" ))
@@ -143,4 +131,228 @@ case class proc(command: Shellable*) {
143131 proc.errorPumperThread.foreach(_.start())
144132 proc
145133 }
134+
135+ /**
136+ * Pipes the output of this process into the input of the [[next ]] process. Returns a
137+ * [[ProcGroup ]] containing both processes, which you can then either execute or
138+ * pipe further.
139+ */
140+ def pipeTo (next : proc): ProcGroup = ProcGroup (Seq (this , next))
141+ }
142+
143+ /**
144+ * A group of processes that are piped together, corresponding to e.g. `ls -l | grep .scala`.
145+ * You can create a `ProcGroup` by calling `.pipeTo` on a [[proc ]] multiple times.
146+ * Contains methods corresponding to the methods on [[proc ]], but defined for pipelines
147+ * of processes.
148+ */
149+ case class ProcGroup private [os] (commands : Seq [proc]) {
150+ assert(commands.size >= 2 )
151+
152+ private lazy val isWindows = sys.props(" os.name" ).toLowerCase().contains(" windows" )
153+
154+ /**
155+ * Invokes the given pipeline like a function, passing in input and returning a
156+ * [[CommandResult ]]. You can then call `result.exitCode` to see how it exited, or
157+ * `result.out.bytes` or `result.err.string` to access the aggregated stdout and
158+ * stderr of the subprocess in a number of convenient ways. If a non-zero exit code
159+ * is returned, this throws a [[os.SubprocessException ]] containing the
160+ * [[CommandResult ]], unless you pass in `check = false`.
161+ *
162+ * For each process in pipeline, the output will be forwarded to the input of the next
163+ * process. Input of the first process is set to provided [[stdin ]] The output of the last
164+ * process will be returned as the output of the pipeline. [[stderr ]] is set for all processes.
165+ *
166+ * `call` provides a number of parameters that let you configure how the pipeline
167+ * is run:
168+ *
169+ * @param cwd the working directory of the pipeline
170+ * @param env any additional environment variables you wish to set in the pipeline
171+ * @param stdin any data you wish to pass to the pipelines's standard input (to the first process)
172+ * @param stdout How the pipelines's output stream is configured (the last process stdout)
173+ * @param stderr How the process's error stream is configured (set for all processes)
174+ * @param mergeErrIntoOut merges the pipeline's stderr stream into it's stdout. Note that then the
175+ * stderr will be forwarded with stdout to subsequent processes in the pipeline.
176+ * @param timeout how long to wait in milliseconds for the pipeline to complete
177+ * @param check disable this to avoid throwing an exception if the pipeline
178+ * fails with a non-zero exit code
179+ * @param propagateEnv disable this to avoid passing in this parent process's
180+ * environment variables to the pipeline
181+ * @param pipefail if true, the pipeline's exitCode will be the exit code of the first
182+ * failing process. If no process fails, the exit code will be 0.
183+ * @param handleBrokenPipe if true, every [[java.io.IOException ]] when redirecting output of a process
184+ * will be caught and handled by killing the writing process. This behaviour
185+ * is consistent with handlers of SIGPIPE signals in most programs
186+ * supporting interruptable piping. Disabled by default on Windows.
187+ */
188+ def call (
189+ cwd : Path = null ,
190+ env : Map [String , String ] = null ,
191+ stdin : ProcessInput = Pipe ,
192+ stdout : ProcessOutput = Pipe ,
193+ stderr : ProcessOutput = os.Inherit ,
194+ mergeErrIntoOut : Boolean = false ,
195+ timeout : Long = - 1 ,
196+ check : Boolean = true ,
197+ propagateEnv : Boolean = true ,
198+ pipefail : Boolean = true ,
199+ handleBrokenPipe : Boolean = ! isWindows
200+ ): CommandResult = {
201+ val chunks = new java.util.concurrent.ConcurrentLinkedQueue [Either [geny.Bytes , geny.Bytes ]]
202+
203+ val sub = spawn(
204+ cwd,
205+ env,
206+ stdin,
207+ if (stdout ne os.Pipe ) stdout
208+ else os.ProcessOutput .ReadBytes ((buf, n) =>
209+ chunks.add(Left (new geny.Bytes (java.util.Arrays .copyOf(buf, n))))
210+ ),
211+ if (stderr ne os.Pipe ) stderr
212+ else os.ProcessOutput .ReadBytes ((buf, n) =>
213+ chunks.add(Right (new geny.Bytes (java.util.Arrays .copyOf(buf, n))))
214+ ),
215+ mergeErrIntoOut,
216+ propagateEnv,
217+ pipefail
218+ )
219+
220+ sub.join(timeout)
221+
222+ val chunksSeq = chunks.iterator.asScala.toIndexedSeq
223+ val res =
224+ CommandResult (commands.flatMap(_.commandChunks :+ " |" ).init, sub.exitCode(), chunksSeq)
225+ if (res.exitCode == 0 || ! check) res
226+ else throw SubprocessException (res)
227+ }
228+
229+ /**
230+ * The most flexible of the [[os.ProcGroup ]] calls. It sets-up a pipeline of processes,
231+ * and returns a [[ProcessPipeline ]] for you to interact with however you like.
232+ *
233+ * Note that if you provide `ProcessOutput` callbacks to `stdout`/`stderr`,
234+ * the calls to those callbacks take place on newly spawned threads that
235+ * execute in parallel with the main thread. Thus make sure any data
236+ * processing you do in those callbacks is thread safe!
237+ * @param cwd the working directory of the pipeline
238+ * @param env any additional environment variables you wish to set in the pipeline
239+ * @param stdin any data you wish to pass to the pipelines's standard input (to the first process)
240+ * @param stdout How the pipelines's output stream is configured (the last process stdout)
241+ * @param stderr How the process's error stream is configured (set for all processes)
242+ * @param mergeErrIntoOut merges the pipeline's stderr stream into it's stdout. Note that then the
243+ * stderr will be forwarded with stdout to subsequent processes in the pipeline.
244+ * @param propagateEnv disable this to avoid passing in this parent process's
245+ * environment variables to the pipeline
246+ * @param pipefail if true, the pipeline's exitCode will be the exit code of the first
247+ * failing process. If no process fails, the exit code will be 0.
248+ * @param handleBrokenPipe if true, every [[java.io.IOException ]] when redirecting output of a process
249+ * will be caught and handled by killing the writing process. This behaviour
250+ * is consistent with handlers of SIGPIPE signals in most programs
251+ * supporting interruptable piping. Disabled by default on Windows.
252+ */
253+ def spawn (
254+ cwd : Path = null ,
255+ env : Map [String , String ] = null ,
256+ stdin : ProcessInput = Pipe ,
257+ stdout : ProcessOutput = Pipe ,
258+ stderr : ProcessOutput = os.Inherit ,
259+ mergeErrIntoOut : Boolean = false ,
260+ propagateEnv : Boolean = true ,
261+ pipefail : Boolean = true ,
262+ handleBrokenPipe : Boolean = ! isWindows
263+ ): ProcessPipeline = {
264+ val brokenPipeQueue = new LinkedBlockingQueue [Int ]()
265+ val (_, procs) =
266+ commands.zipWithIndex.foldLeft((Option .empty[ProcessInput ], Seq .empty[SubProcess ])) {
267+ case ((None , _), (proc, _)) =>
268+ val spawned = proc.spawn(cwd, env, stdin, Pipe , stderr, mergeErrIntoOut, propagateEnv)
269+ (Some (spawned.stdout), Seq (spawned))
270+ case ((Some (input), acc), (proc, index)) if index == commands.length - 1 =>
271+ val spawned = proc.spawn(
272+ cwd,
273+ env,
274+ wrapWithBrokenPipeHandler(input, index - 1 , brokenPipeQueue),
275+ stdout,
276+ stderr,
277+ mergeErrIntoOut,
278+ propagateEnv
279+ )
280+ (None , acc :+ spawned)
281+ case ((Some (input), acc), (proc, index)) =>
282+ val spawned = proc.spawn(
283+ cwd,
284+ env,
285+ wrapWithBrokenPipeHandler(input, index - 1 , brokenPipeQueue),
286+ Pipe ,
287+ stderr,
288+ mergeErrIntoOut,
289+ propagateEnv
290+ )
291+ (Some (spawned.stdout), acc :+ spawned)
292+ }
293+ val pipeline =
294+ new ProcessPipeline (procs, pipefail, if (handleBrokenPipe) Some (brokenPipeQueue) else None )
295+ pipeline.brokenPipeHandler.foreach(_.start())
296+ pipeline
297+ }
298+
299+ private def wrapWithBrokenPipeHandler (
300+ wrapped : ProcessInput ,
301+ index : Int ,
302+ queue : LinkedBlockingQueue [Int ]
303+ ) =
304+ new ProcessInput {
305+ override def redirectFrom : Redirect = wrapped.redirectFrom
306+ override def processInput (stdin : => InputStream ): Option [Runnable ] =
307+ wrapped.processInput(stdin).map { runnable =>
308+ new Runnable {
309+ def run () = {
310+ try {
311+ runnable.run()
312+ } catch {
313+ case e : IOException =>
314+ println(s " Broken pipe in process $index" )
315+ queue.put(index)
316+ }
317+ }
318+ }
319+ }
320+ }
321+
322+ /**
323+ * Pipes the output of this pipeline into the input of the [[next ]] process.
324+ */
325+ def pipeTo (next : proc) = ProcGroup (commands :+ next)
326+ }
327+
328+ private [os] object ProcessOps {
329+ def buildProcess (
330+ command : Seq [String ],
331+ cwd : Path = null ,
332+ env : Map [String , String ] = null ,
333+ stdin : ProcessInput = Pipe ,
334+ stdout : ProcessOutput = Pipe ,
335+ stderr : ProcessOutput = os.Inherit ,
336+ mergeErrIntoOut : Boolean = false ,
337+ propagateEnv : Boolean = true
338+ ): ProcessBuilder = {
339+ val builder = new java.lang.ProcessBuilder ()
340+
341+ val baseEnv =
342+ if (propagateEnv) sys.env
343+ else Map ()
344+ for ((k, v) <- baseEnv ++ Option (env).getOrElse(Map ())) {
345+ if (v != null ) builder.environment().put(k, v)
346+ else builder.environment().remove(k)
347+ }
348+
349+ builder.directory(Option (cwd).getOrElse(os.pwd).toIO)
350+
351+ builder
352+ .command(command : _* )
353+ .redirectInput(stdin.redirectFrom)
354+ .redirectOutput(stdout.redirectTo)
355+ .redirectError(stderr.redirectTo)
356+ .redirectErrorStream(mergeErrIntoOut)
357+ }
146358}
0 commit comments