1 /**
2 Copyright: Copyright (c) 2019, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 */
10 module process;
11 
12 import core.sys.posix.unistd : pid_t;
13 import core.thread : Thread;
14 import core.time : dur, Duration;
15 import logger = std.experimental.logger;
16 import std.algorithm : filter, count, joiner, map;
17 import std.array : appender, empty, array;
18 import std.exception : collectException;
19 import std.stdio : File, fileno, writeln;
20 static import std.process;
21 
22 public import process.channel;
23 
24 version (unittest) {
25     import unit_threaded.assertions;
26     import std.file : remove;
27 }
28 
29 /// Automatically terminate the process when it goes out of scope.
30 auto scopeKill(T)(T p) {
31     return ScopeKill!T(p);
32 }
33 
34 struct ScopeKill(T) {
35     T process;
36     alias process this;
37 
38     ~this() {
39         process.dispose();
40     }
41 }
42 
43 /** Async process that do not block on read from stdin/stderr.
44  */
45 struct PipeProcess {
46     import std.algorithm : among;
47 
48     private {
49         enum State {
50             running,
51             terminated,
52             exitCode
53         }
54 
55         std.process.ProcessPipes process;
56         Pipe pipe_;
57         FileReadChannel stderr_;
58         int status_;
59         State st;
60     }
61 
62     this(std.process.ProcessPipes process) @safe {
63         this.process = process;
64         this.pipe_ = Pipe(this.process.stdout, this.process.stdin);
65         this.stderr_ = FileReadChannel(this.process.stderr);
66     }
67 
68     /// Returns: The raw OS handle for the process ID.
69     RawPid osHandle() nothrow @safe {
70         return process.pid.osHandle.RawPid;
71     }
72 
73     /// Access to stdin and stdout.
74     ref Pipe pipe() return scope nothrow @safe {
75         return pipe_;
76     }
77 
78     /// Access stderr.
79     ref FileReadChannel stderr() return scope nothrow @safe {
80         return stderr_;
81     }
82 
83     /// Kill and cleanup the process.
84     void dispose() @safe {
85         final switch (st) {
86         case State.running:
87             this.kill;
88             this.wait;
89             .destroy(process);
90             break;
91         case State.terminated:
92             this.wait;
93             .destroy(process);
94             break;
95         case State.exitCode:
96             break;
97         }
98 
99         st = State.exitCode;
100     }
101 
102     /// Kill the process.
103     void kill() nothrow @trusted {
104         import core.sys.posix.signal : SIGKILL;
105 
106         final switch (st) {
107         case State.running:
108             break;
109         case State.terminated:
110             return;
111         case State.exitCode:
112             return;
113         }
114 
115         try {
116             std.process.kill(process.pid, SIGKILL);
117         } catch (Exception e) {
118         }
119 
120         st = State.terminated;
121     }
122 
123     /// Blocking wait for the process to terminated.
124     /// Returns: the exit status.
125     int wait() @safe {
126         final switch (st) {
127         case State.running:
128             status_ = std.process.wait(process.pid);
129             break;
130         case State.terminated:
131             status_ = std.process.wait(process.pid);
132             break;
133         case State.exitCode:
134             break;
135         }
136 
137         st = State.exitCode;
138 
139         return status_;
140     }
141 
142     /// Non-blocking wait for the process termination.
143     /// Returns: `true` if the process has terminated.
144     bool tryWait() @safe {
145         final switch (st) {
146         case State.running:
147             auto s = std.process.tryWait(process.pid);
148             if (s.terminated) {
149                 st = State.exitCode;
150                 status_ = s.status;
151             }
152             break;
153         case State.terminated:
154             status_ = std.process.wait(process.pid);
155             st = State.exitCode;
156             break;
157         case State.exitCode:
158             break;
159         }
160 
161         return st.among(State.terminated, State.exitCode) != 0;
162     }
163 
164     /// Returns: The exit status of the process.
165     int status() @safe {
166         if (st != State.exitCode) {
167             throw new Exception(
168                     "Process has not terminated and wait/tryWait been called to collect the exit status");
169         }
170         return status_;
171     }
172 
173     /// Returns: If the process has terminated.
174     bool terminated() @safe {
175         return st.among(State.terminated, State.exitCode) != 0;
176     }
177 }
178 
179 PipeProcess pipeProcess(scope const(char[])[] args,
180         std.process.Redirect redirect = std.process.Redirect.all,
181         const string[string] env = null, std.process.Config config = std.process.Config.none,
182         scope const(char)[] workDir = null) @safe {
183     return PipeProcess(std.process.pipeProcess(args, redirect, env, config, workDir));
184 }
185 
186 PipeProcess pipeShell(scope const(char)[] command,
187         std.process.Redirect redirect = std.process.Redirect.all,
188         const string[string] env = null, std.process.Config config = std.process.Config.none,
189         scope const(char)[] workDir = null, string shellPath = std.process.nativeShell) @safe {
190     return PipeProcess(std.process.pipeShell(command, redirect, env, config, workDir, shellPath));
191 }
192 
193 /** Moves the process to a separate process group and on exit kill it and all
194  * its children.
195  */
196 struct Sandbox(ProcessT) {
197     private {
198         ProcessT p;
199     }
200 
201     this(ProcessT p) @safe {
202         import core.sys.posix.unistd : setpgid;
203 
204         this.p = p;
205         setpgid(p.osHandle, 0);
206     }
207 
208     RawPid osHandle() nothrow @safe {
209         return p.osHandle;
210     }
211 
212     ref Pipe pipe() nothrow @safe {
213         return p.pipe;
214     }
215 
216     ref FileReadChannel stderr() nothrow @safe {
217         return p.stderr;
218     }
219 
220     void dispose() @safe {
221         // this also reaps the children thus cleaning up zombies
222         this.kill;
223         p.dispose;
224     }
225 
226     void kill() nothrow @safe {
227         static import core.sys.posix.signal;
228         import core.sys.posix.sys.wait : waitpid, WNOHANG;
229 
230         static RawPid[] update(RawPid[] pids) @trusted {
231             auto app = appender!(RawPid[])();
232 
233             foreach (p; pids) {
234                 try {
235                     app.put(getDeepChildren(p));
236                 } catch (Exception e) {
237                 }
238             }
239 
240             return app.data;
241         }
242 
243         static void killChildren(RawPid[] children) @trusted {
244             foreach (const c; children) {
245                 core.sys.posix.signal.kill(c, core.sys.posix.signal.SIGKILL);
246             }
247         }
248 
249         p.kill;
250         auto children = update([p.osHandle]);
251         auto reapChildren = appender!(RawPid[])();
252         // if there ever are processes that are spawned with root permissions
253         // or something happens that they can't be killed by "this" process
254         // tree. Thus limit the iterations to a reasonable number
255         for (int i = 0; !children.empty && i < 5; ++i) {
256             reapChildren.put(children);
257             killChildren(children);
258             children = update(children);
259         }
260 
261         foreach (c; reapChildren.data) {
262             () @trusted { waitpid(c, null, WNOHANG); }();
263         }
264     }
265 
266     int wait() @safe {
267         return p.wait;
268     }
269 
270     bool tryWait() @safe {
271         return p.tryWait;
272     }
273 
274     int status() @safe {
275         return p.status;
276     }
277 
278     bool terminated() @safe {
279         return p.terminated;
280     }
281 }
282 
283 auto sandbox(T)(T p) @safe {
284     return Sandbox!T(p);
285 }
286 
287 @("shall terminate a group of processes")
288 unittest {
289     import std.algorithm : count;
290     import std.datetime.stopwatch : StopWatch, AutoStart;
291 
292     immutable scriptName = makeScript(`#!/bin/bash
293 sleep 10m &
294 sleep 10m &
295 sleep 10m
296 `);
297     scope (exit)
298         remove(scriptName);
299 
300     auto p = pipeProcess([scriptName]).sandbox.scopeKill;
301     for (int i = 0; getDeepChildren(p.osHandle).count < 3; ++i) {
302         Thread.sleep(50.dur!"msecs");
303     }
304     const preChildren = getDeepChildren(p.osHandle).count;
305     p.kill;
306     Thread.sleep(500.dur!"msecs"); // wait for the OS to kill the children
307     const postChildren = getDeepChildren(p.osHandle).count;
308 
309     p.wait.shouldEqual(-9);
310     p.terminated.shouldBeTrue;
311     preChildren.shouldEqual(3);
312     postChildren.shouldEqual(0);
313 }
314 
315 /** dispose the process after the timeout.
316  */
317 struct Timeout(ProcessT) {
318     import std.algorithm : among;
319     import std.datetime : Clock, Duration;
320     import core.thread;
321     import std.typecons : RefCounted, refCounted;
322 
323     private {
324         enum Msg {
325             none,
326             stop,
327             status,
328         }
329 
330         enum Reply {
331             none,
332             running,
333             normalDeath,
334             killedByTimeout,
335         }
336 
337         static struct Payload {
338             ProcessT p;
339             Background background;
340             Reply backgroundReply;
341         }
342 
343         RefCounted!Payload rc;
344     }
345 
346     this(ProcessT p, Duration timeout) @trusted {
347         import std.algorithm : move;
348 
349         rc = refCounted(Payload(move(p)));
350         rc.background = new Background(&rc.p, timeout);
351         rc.background.isDaemon = true;
352         rc.background.start;
353     }
354 
355     private static class Background : Thread {
356         import core.sync.condition : Condition;
357         import core.sync.mutex : Mutex;
358 
359         Duration timeout;
360         ProcessT* p;
361         Mutex mtx;
362         Msg[] msg;
363         Reply reply_;
364 
365         this(ProcessT* p, Duration timeout) {
366             this.p = p;
367             this.timeout = timeout;
368             this.mtx = new Mutex();
369 
370             super(&run);
371         }
372 
373         void run() {
374             checkProcess(p.osHandle, this.timeout, this);
375         }
376 
377         void put(Msg msg) @trusted {
378             this.mtx.lock_nothrow();
379             scope (exit)
380                 this.mtx.unlock_nothrow();
381             this.msg ~= msg;
382         }
383 
384         Msg popMsg() @trusted nothrow {
385             this.mtx.lock_nothrow();
386             scope (exit)
387                 this.mtx.unlock_nothrow();
388             if (msg.empty)
389                 return Msg.none;
390             auto rval = msg[$ - 1];
391             msg = msg[0 .. $ - 1];
392             return rval;
393         }
394 
395         void setReply(Reply reply_) @trusted {
396             {
397                 this.mtx.lock_nothrow();
398                 scope (exit)
399                     this.mtx.unlock_nothrow();
400                 this.reply_ = reply_;
401             }
402         }
403 
404         Reply reply() @trusted nothrow {
405             this.mtx.lock_nothrow();
406             scope (exit)
407                 this.mtx.unlock_nothrow();
408             return reply_;
409         }
410 
411         void kill() @trusted nothrow {
412             this.mtx.lock_nothrow();
413             scope (exit)
414                 this.mtx.unlock_nothrow();
415             p.kill;
416         }
417     }
418 
419     private static void checkProcess(RawPid p, Duration timeout, Background bg) {
420         import core.sys.posix.signal : SIGKILL;
421         import std.algorithm : max, min;
422         import std.variant : Variant;
423         static import core.sys.posix.signal;
424 
425         const stopAt = Clock.currTime + timeout;
426         // the purpose is to poll the process often "enough" that if it
427         // terminates early `Process` detects it fast enough. 1000 is chosen
428         // because it "feels good". the purpose
429         auto sleepInterval = min(500, max(20, timeout.total!"msecs" / 1000)).dur!"msecs";
430 
431         bool forceStop;
432         bool running = true;
433         while (running && Clock.currTime < stopAt) {
434             const msg = bg.popMsg;
435 
436             final switch (msg) {
437             case Msg.none:
438                 Thread.sleep(sleepInterval);
439                 break;
440             case Msg.stop:
441                 forceStop = true;
442                 running = false;
443                 break;
444             case Msg.status:
445                 bg.setReply(Reply.running);
446                 break;
447             }
448 
449             if (core.sys.posix.signal.kill(p, 0) == -1) {
450                 running = false;
451             }
452         }
453 
454         // may be children alive thus must ensure that the whole process tree
455         // is killed if this is a sandbox with a timeout.
456         bg.kill;
457 
458         if (!forceStop && Clock.currTime >= stopAt) {
459             bg.setReply(Reply.killedByTimeout);
460         } else {
461             bg.setReply(Reply.normalDeath);
462         }
463     }
464 
465     RawPid osHandle() nothrow @trusted {
466         return rc.p.osHandle;
467     }
468 
469     ref Pipe pipe() nothrow @trusted {
470         return rc.p.pipe;
471     }
472 
473     ref FileReadChannel stderr() nothrow @trusted {
474         return rc.p.stderr;
475     }
476 
477     void dispose() @trusted {
478         if (rc.backgroundReply.among(Reply.none, Reply.running)) {
479             rc.background.put(Msg.stop);
480             rc.background.join;
481             rc.backgroundReply = rc.background.reply;
482         }
483         rc.p.dispose;
484     }
485 
486     void kill() nothrow @trusted {
487         rc.background.kill;
488     }
489 
490     int wait() @trusted {
491         while (!this.tryWait) {
492             Thread.sleep(20.dur!"msecs");
493         }
494         return rc.p.wait;
495     }
496 
497     bool tryWait() @trusted {
498         return rc.p.tryWait;
499     }
500 
501     int status() @trusted {
502         return rc.p.status;
503     }
504 
505     bool terminated() @trusted {
506         return rc.p.terminated;
507     }
508 
509     bool timeoutTriggered() @trusted {
510         if (rc.backgroundReply.among(Reply.none, Reply.running)) {
511             rc.background.put(Msg.status);
512             rc.backgroundReply = rc.background.reply;
513         }
514         return rc.backgroundReply == Reply.killedByTimeout;
515     }
516 }
517 
518 auto timeout(T)(T p, Duration timeout_) @trusted {
519     return Timeout!T(p, timeout_);
520 }
521 
522 /// Returns when the process has pending data.
523 void waitForPendingData(ProcessT)(Process p) {
524     while (!p.pipe.hasPendingData || !p.stderr.hasPendingData) {
525         Thread.sleep(20.dur!"msecs");
526     }
527 }
528 
529 @("shall kill the process after the timeout")
530 unittest {
531     import std.datetime.stopwatch : StopWatch, AutoStart;
532 
533     auto p = pipeProcess(["sleep", "1m"]).timeout(100.dur!"msecs").scopeKill;
534     auto sw = StopWatch(AutoStart.yes);
535     p.wait;
536     sw.stop;
537 
538     sw.peek.shouldBeGreaterThan(100.dur!"msecs");
539     sw.peek.shouldBeSmallerThan(500.dur!"msecs");
540     p.wait.shouldEqual(-9);
541     p.terminated.shouldBeTrue;
542     p.status.shouldEqual(-9);
543     p.timeoutTriggered.shouldBeTrue;
544 }
545 
546 struct RawPid {
547     pid_t value;
548     alias value this;
549 }
550 
551 RawPid[] getShallowChildren(const int parentPid) {
552     import std.algorithm : filter, splitter;
553     import std.conv : to;
554     import std.file : exists;
555     import std.path : buildPath;
556 
557     const pidPath = buildPath("/proc", parentPid.to!string);
558     if (!exists(pidPath)) {
559         return null;
560     }
561 
562     auto children = appender!(RawPid[])();
563     foreach (const p; File(buildPath(pidPath, "task", parentPid.to!string, "children")).readln.splitter(" ")
564             .filter!(a => !a.empty)) {
565         try {
566             children.put(p.to!pid_t.RawPid);
567         } catch (Exception e) {
568             logger.trace(e.msg).collectException;
569         }
570     }
571 
572     return children.data;
573 }
574 
575 /// Returns: a list of all processes with the leafs being at the back.
576 RawPid[] getDeepChildren(const int parentPid) {
577     import std.container : DList;
578 
579     auto children = DList!(RawPid)();
580 
581     children.insert(getShallowChildren(parentPid));
582     auto res = appender!(RawPid[])();
583 
584     while (!children.empty) {
585         const p = children.front;
586         res.put(p);
587         children.insertBack(getShallowChildren(p));
588         children.removeFront;
589     }
590 
591     return res.data;
592 }
593 
594 struct DrainElement {
595     enum Type {
596         stdout,
597         stderr,
598     }
599 
600     Type type;
601     const(ubyte)[] data;
602 
603     /// Returns: iterates the data as an input range.
604     auto byUTF8() @safe pure nothrow const @nogc {
605         static import std.utf;
606 
607         return std.utf.byUTF!(const(char))(cast(const(char)[]) data);
608     }
609 
610     bool empty() @safe pure nothrow const @nogc {
611         return data.length == 0;
612     }
613 }
614 
615 /** A range that drains a process stdout/stderr until it terminates.
616  *
617  * There may be `DrainElement` that are empty.
618  */
619 struct DrainRange(ProcessT) {
620     enum State {
621         start,
622         draining,
623         lastStdout,
624         lastStderr,
625         lastElement,
626         empty,
627     }
628 
629     private {
630         Duration timeout;
631         ProcessT p;
632         DrainElement front_;
633         State st;
634         ubyte[] buf;
635         ubyte[] bufRead;
636     }
637 
638     this(ProcessT p, Duration timeout) {
639         this.p = p;
640         this.buf = new ubyte[4096];
641         this.timeout = timeout;
642     }
643 
644     DrainElement front() @safe pure nothrow const @nogc {
645         assert(!empty, "Can't get front of an empty range");
646         return front_;
647     }
648 
649     void popFront() @safe {
650         assert(!empty, "Can't pop front of an empty range");
651 
652         bool isAnyPipeOpen() {
653             return (p.pipe.hasData || p.stderr.hasData) && !p.terminated;
654         }
655 
656         void readData() @safe {
657             if (p.stderr.hasData && p.stderr.hasPendingData) {
658                 front_ = DrainElement(DrainElement.Type.stderr);
659                 bufRead = p.stderr.read(buf);
660             } else if (p.pipe.hasData && p.pipe.hasPendingData) {
661                 front_ = DrainElement(DrainElement.Type.stdout);
662                 bufRead = p.pipe.read(buf);
663             }
664         }
665 
666         void waitUntilData() @safe {
667             // may livelock if the process never terminates and never writes to
668             // the terminal. waitTime ensure that it sooner or later is
669             // interrupted. It lets e.g the timeout handling to kill the
670             // process.
671             const s = 20.dur!"msecs";
672             Duration waitTime;
673             while (waitTime < timeout) {
674                 import core.thread : Thread;
675                 import core.time : dur;
676 
677                 readData();
678                 if (front_.data.empty) {
679                     () @trusted { Thread.sleep(s); }();
680                     waitTime += s;
681                 }
682 
683                 if (!(bufRead.empty && isAnyPipeOpen)) {
684                     front_.data = bufRead.dup;
685                     break;
686                 }
687             }
688         }
689 
690         front_ = DrainElement.init;
691         bufRead = null;
692 
693         final switch (st) {
694         case State.start:
695             st = State.draining;
696             waitUntilData;
697             break;
698         case State.draining:
699             if (isAnyPipeOpen) {
700                 waitUntilData();
701             } else {
702                 st = State.lastStdout;
703             }
704             break;
705         case State.lastStdout:
706             if (p.pipe.hasData && p.pipe.hasPendingData) {
707                 front_ = DrainElement(DrainElement.Type.stdout);
708                 bufRead = p.pipe.read(buf);
709             }
710 
711             front_.data = bufRead.dup;
712             if (!p.pipe.hasData || p.terminated) {
713                 st = State.lastStderr;
714             }
715             break;
716         case State.lastStderr:
717             if (p.stderr.hasData && p.stderr.hasPendingData) {
718                 front_ = DrainElement(DrainElement.Type.stderr);
719                 bufRead = p.stderr.read(buf);
720             }
721 
722             front_.data = bufRead.dup;
723             if (!p.stderr.hasData || p.terminated) {
724                 st = State.lastElement;
725             }
726             break;
727         case State.lastElement:
728             st = State.empty;
729             break;
730         case State.empty:
731             break;
732         }
733     }
734 
735     bool empty() @safe pure nothrow const @nogc {
736         return st == State.empty;
737     }
738 }
739 
740 /// Drain a process pipe until empty.
741 auto drain(T)(T p, Duration timeout) {
742     return DrainRange!T(p, timeout);
743 }
744 
745 /// Read the data from a ReadChannel by line.
746 struct DrainByLineCopyRange(ProcessT) {
747     private {
748         ProcessT process;
749         DrainRange!ProcessT range;
750         const(ubyte)[] buf;
751         const(char)[] line;
752     }
753 
754     this(ProcessT p, Duration timeout) @safe {
755         process = p;
756         range = p.drain(timeout);
757     }
758 
759     string front() @trusted pure nothrow const @nogc {
760         import std.exception : assumeUnique;
761 
762         assert(!empty, "Can't get front of an empty range");
763         return line.assumeUnique;
764     }
765 
766     void popFront() @safe {
767         assert(!empty, "Can't pop front of an empty range");
768         import std.algorithm : countUntil;
769         import std.array : array;
770         static import std.utf;
771 
772         void fillBuf() {
773             if (!range.empty) {
774                 range.popFront;
775             }
776             if (!range.empty) {
777                 buf ~= range.front.data;
778             }
779         }
780 
781         size_t idx;
782         do {
783             fillBuf();
784             idx = buf.countUntil('\n');
785         }
786         while (!range.empty && idx == -1);
787 
788         const(ubyte)[] tmp;
789         if (buf.empty) {
790             // do nothing
791         } else if (idx == -1) {
792             tmp = buf;
793             buf = null;
794         } else {
795             idx = () {
796                 if (idx < buf.length) {
797                     return idx + 1;
798                 }
799                 return idx;
800             }();
801             tmp = buf[0 .. idx];
802             buf = buf[idx .. $];
803         }
804 
805         if (!tmp.empty && tmp[$ - 1] == '\n') {
806             tmp = tmp[0 .. $ - 1];
807         }
808 
809         line = std.utf.byUTF!(const(char))(cast(const(char)[]) tmp).array;
810     }
811 
812     bool empty() @safe pure nothrow const @nogc {
813         return range.empty && buf.empty && line.empty;
814     }
815 }
816 
817 @("shall drain the process output by line")
818 unittest {
819     import std.algorithm : filter, count, joiner, map;
820     import std.array : array;
821 
822     auto p = pipeProcess(["dd", "if=/dev/zero", "bs=10", "count=3"]).scopeKill;
823     auto res = p.process.drainByLineCopy(1.dur!"minutes").filter!"!a.empty".array;
824 
825     res.length.shouldEqual(4);
826     res.joiner.count.shouldBeGreaterThan(30);
827     p.wait.shouldEqual(0);
828     p.terminated.shouldBeTrue;
829 }
830 
831 auto drainByLineCopy(T)(T p, Duration timeout) @safe {
832     return DrainByLineCopyRange!T(p, timeout);
833 }
834 
835 /// Drain the process output until it is done executing.
836 auto drainToNull(T)(T p, Duration timeout) @safe {
837     foreach (l; p.drain(timeout)) {
838     }
839     return p;
840 }
841 
842 /// Drain the output from the process into an output range.
843 auto drain(ProcessT, T)(ProcessT p, ref T range, Duration timeout) {
844     foreach (l; p.drain(timeout)) {
845         range.put(l);
846     }
847     return p;
848 }
849 
850 @("shall drain the output of a process while it is running with a separation of stdout and stderr")
851 unittest {
852     auto p = pipeProcess(["dd", "if=/dev/urandom", "bs=10", "count=3"]).scopeKill;
853     auto res = p.process.drain(1.dur!"minutes").array;
854 
855     // this is just a sanity check. It has to be kind a high because there is
856     // some wiggleroom allowed
857     res.count.shouldBeSmallerThan(50);
858 
859     res.filter!(a => a.type == DrainElement.Type.stdout)
860         .map!(a => a.data)
861         .joiner
862         .count
863         .shouldEqual(30);
864     res.filter!(a => a.type == DrainElement.Type.stderr).count.shouldBeGreaterThan(0);
865     p.wait.shouldEqual(0);
866     p.terminated.shouldBeTrue;
867 }
868 
869 @("shall kill the process tree when the timeout is reached")
870 unittest {
871     immutable script = makeScript(`#!/bin/bash
872 sleep 10m
873 `);
874     scope (exit)
875         remove(script);
876 
877     auto p = pipeProcess([script]).sandbox.timeout(1.dur!"seconds").scopeKill;
878     do {
879         Thread.sleep(50.dur!"msecs");
880     }
881     while (getDeepChildren(p.osHandle).count < 1);
882     const preChildren = getDeepChildren(p.osHandle).count;
883     const res = p.process.drain(1.dur!"minutes").array;
884     const postChildren = getDeepChildren(p.osHandle).count;
885 
886     p.wait.shouldEqual(-9);
887     p.terminated.shouldBeTrue;
888     preChildren.shouldEqual(1);
889     postChildren.shouldEqual(0);
890 }
891 
892 string makeScript(string script, string file = __FILE__, uint line = __LINE__) {
893     import core.sys.posix.sys.stat;
894     import std.file : getAttributes, setAttributes, thisExePath;
895     import std.stdio : File;
896     import std.path : baseName;
897     import std.conv : to;
898 
899     immutable fname = thisExePath ~ "_" ~ file.baseName ~ line.to!string ~ ".sh";
900 
901     File(fname, "w").writeln(script);
902     setAttributes(fname, getAttributes(fname) | S_IXUSR | S_IXGRP | S_IXOTH);
903     return fname;
904 }