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 }