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 }