1 /** 2 Convenience wrappers for executing subprocesses. 3 4 Copyright: © 2019 Arne Ludwig <arne.ludwig@posteo.de> 5 License: Subject to the terms of the MIT license, as written in the 6 included LICENSE file. 7 Authors: Arne Ludwig <arne.ludwig@posteo.de> 8 */ 9 module dalicious.process; 10 11 12 import std.algorithm : 13 endsWith, 14 filter; 15 import std.array : array; 16 import std.process : 17 kill, 18 Redirect, 19 Config, 20 pipeProcess, 21 pipeShell, 22 ProcessPipes, 23 wait; 24 import std.range.primitives; 25 import std.traits : isSomeString; 26 import vibe.data.json : toJson = serializeToJson; 27 28 29 auto pipeLines(Range)(Range command, in string workdir = null) 30 if (isInputRange!Range && isSomeString!(ElementType!Range)) 31 { 32 auto sanitizedCommand = command.filter!"a != null".array; 33 34 return new LinesPipe!ProcessInfo(ProcessInfo(sanitizedCommand, workdir)); 35 } 36 37 auto pipeLines(in string shellCommand, in string workdir = null) 38 { 39 return new LinesPipe!ShellInfo(ShellInfo(shellCommand, workdir)); 40 } 41 42 unittest 43 { 44 import std.algorithm : equal; 45 import std.range : only, take; 46 47 auto cheers = pipeLines("yes 'Cheers!'"); 48 assert(cheers.take(5).equal([ 49 "Cheers!", 50 "Cheers!", 51 "Cheers!", 52 "Cheers!", 53 "Cheers!", 54 ])); 55 56 auto helloWorld = pipeLines(only("echo", "Hello World!")); 57 assert(helloWorld.equal(["Hello World!"])); 58 } 59 60 private struct ProcessInfo 61 { 62 const(string[]) command; 63 const(string) workdir; 64 } 65 66 private struct ShellInfo 67 { 68 const(string) command; 69 const(string) workdir; 70 } 71 72 static final class LinesPipe(CommandInfo) 73 { 74 static enum lineTerminator = "\n"; 75 76 private CommandInfo processInfo; 77 private ProcessPipes process; 78 private string currentLine; 79 80 this(CommandInfo processInfo) 81 { 82 this.processInfo = processInfo; 83 } 84 85 ~this() 86 { 87 if (!(process.pid is null)) 88 releaseProcess(); 89 } 90 91 void releaseProcess() 92 { 93 if (!process.stdout.isOpen) 94 return; 95 96 process.stdout.close(); 97 98 version (Posix) 99 { 100 import core.sys.posix.signal : SIGKILL; 101 102 process.pid.kill(SIGKILL); 103 } 104 else 105 { 106 static assert(0, "Only intended for use on POSIX compliant OS."); 107 } 108 109 process.pid.wait(); 110 } 111 112 private void ensureInitialized() 113 { 114 if (!(process.pid is null)) 115 return; 116 117 process = launchProcess(); 118 119 if (!empty) 120 popFront(); 121 } 122 123 static if (is(CommandInfo == ProcessInfo)) 124 ProcessPipes launchProcess() 125 { 126 return pipeProcess( 127 processInfo.command, 128 Redirect.stdout, 129 null, 130 Config.none, 131 processInfo.workdir, 132 ); 133 } 134 else static if (is(CommandInfo == ShellInfo)) 135 ProcessPipes launchProcess() 136 { 137 return pipeShell( 138 processInfo.command, 139 Redirect.stdout, 140 null, 141 Config.none, 142 processInfo.workdir, 143 ); 144 } 145 146 void popFront() 147 { 148 ensureInitialized(); 149 assert(!empty, "Attempting to popFront an empty LinesPipe"); 150 currentLine = process.stdout.readln(); 151 152 if (currentLine.empty) 153 { 154 currentLine = null; 155 releaseProcess(); 156 } 157 158 if (currentLine.endsWith(lineTerminator)) 159 currentLine = currentLine[0 .. $ - lineTerminator.length]; 160 } 161 162 @property string front() 163 { 164 ensureInitialized(); 165 assert(!empty, "Attempting to fetch the front of an empty LinesPipe"); 166 167 return currentLine; 168 } 169 170 @property bool empty() 171 { 172 ensureInitialized(); 173 174 if (!process.stdout.isOpen || process.stdout.eof) 175 { 176 releaseProcess(); 177 178 return true; 179 } 180 else 181 { 182 return false; 183 } 184 } 185 }