1 /*
2 
3 Taken from dlangide:
4 https://github.com/buggins/dlangide/blob/master/src/dlangide/builders/extprocess.d
5 
6 Copyright: 2018 Mark Fisher
7 
8 License:
9 Permission is hereby granted, free of charge, to any person obtaining a copy of
10 this software and associated documentation files (the "Software"), to deal in
11 the Software without restriction, including without limitation the rights to
12 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
13 of the Software, and to permit persons to whom the Software is furnished to do
14 so, subject to the following conditions:
15 
16 The above copyright notice and this permission notice shall be included in all
17 copies or substantial portions of the Software.
18 
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 SOFTWARE.
26 */
27 /**
28  * Load and execute external programs buffering stdio.
29  **/
30 module dxx.sys.spawn;
31 
32 private import std.process;
33 private import std.stdio;
34 private import std.utf;
35 private import std.stdio;
36 private import core.thread;
37 private import core.sync.mutex;
38 
39 private import dxx.util;
40 
41 mixin __Text;
42 
43 /// interface to forward process output to
44 interface TextWriter {
45     /// log lines
46     void writeText(dstring text);
47 }
48 
49 /// interface to read text
50 interface TextReader {
51     /// log lines
52     dstring readText();
53 }
54 
55 /// protected text storage box to read and write text from different threads
56 class ProtectedTextStorage : TextReader, TextWriter {
57 
58     private Mutex _mutex;
59     private shared bool _closed;
60     private dchar[] _buffer;
61 
62     this() {
63         _mutex = new Mutex();
64     }
65 
66     @property bool closed() { return _closed; }
67 
68     void close() {
69         if (_closed)
70             return;
71         _closed = true;
72         _buffer = null;
73     }
74 
75     /// log lines
76     override void writeText(dstring text) {
77         if (!_closed) {
78             // if not closed
79             _mutex.lock();
80             scope(exit) _mutex.unlock();
81             // append text
82             _buffer ~= text;
83         }
84     }
85 
86     /// log lines
87     override dstring readText() {
88         if (!_closed) {
89             // if not closed
90             _mutex.lock();
91             scope(exit) _mutex.unlock();
92             if (!_buffer.length)
93                 return null;
94             dstring res = _buffer.dup;
95             _buffer = null;
96             return res;
97         } else {
98             // reading from closed
99             return null;
100         }
101     }
102 }
103 
104 enum ExternalProcessState : uint {
105     /// not initialized
106     None,
107     /// running
108     Running,
109     /// stop is requested
110     Stopping,
111     /// stopped
112     Stopped,
113     /// error occured, e.g. cannot run process
114     Error
115 }
116 
117 /// base class for text reading from std.stdio.File in background thread
118 class BackgroundReaderBase : Thread {
119     private std.stdio.File _file;
120     private shared bool _finished;
121     private ubyte[1] _byteBuffer;
122     private ubyte[] _bytes;
123     dchar[] _textbuffer;
124     private int _len;
125     private bool _utfError;
126 
127     this(std.stdio.File f) {
128         super(&run);
129         assert(f.isOpen());
130         _file = f;
131         _len = 0;
132         _finished = false;
133     }
134 
135     @property bool finished() {
136         return _finished;
137     }
138 
139     ubyte prevchar;
140     void addByte(ubyte data) {
141         if (_bytes.length < _len + 1)
142             _bytes.length = _bytes.length ? _bytes.length * 2 : 1024;
143         bool eolchar = (data == '\r' || data == '\n');
144         bool preveol = (prevchar == '\r' || prevchar == '\n');
145         _bytes[_len++] = data;
146         if (data == '\n')
147             flush();
148         //if ((eolchar && !preveol) || (!eolchar && preveol) || data == '\n') {
149         //    //Log.d("Flushing for prevChar=", prevchar, " newChar=", data);
150         //    flush();
151         //}
152         prevchar = data;
153     }
154     void flush() {
155         if (!_len)
156             return;
157         if (_textbuffer.length < _len)
158             _textbuffer.length = _len + 256;
159         size_t count = 0;
160         for(size_t i = 0; i < _len;) {
161             dchar ch = 0;
162             if (_utfError) {
163                 ch = _bytes[i++];
164             } else {
165                 try {
166                     ch = decode(cast(string)_bytes, i);
167                 } catch (UTFException e) {
168                     _utfError = true;
169                     ch = _bytes[i++];
170                     MsgLog.error(MsgText!(DXXConfig.messages.MSG_ERR_NONUNICODE_PROC_OUTPUT));
171                 }
172             }
173             _textbuffer[count++] = ch;
174         }
175         _len = 0;
176 
177         if (!count)
178             return;
179 
180         // fix line endings - must be '\n'
181         count = convertLineEndings(_textbuffer[0..count]);
182 
183         // data is ready to send
184         if (count)
185             sendResult(_textbuffer[0..count].dup);
186     }
187     /// inplace convert line endings to unix format (\n)
188     size_t convertLineEndings(dchar[] text) {
189         size_t src = 0;
190         size_t dst = 0;
191         for(;src < text.length;) {
192             dchar ch = text[src++];
193             dchar nextch = src < text.length ? text[src] : 0;
194             if (ch == '\n') {
195                 if (nextch == '\r')
196                     src++;
197                 text[dst++] = '\n';
198             } else if (ch == '\r') {
199                 if (nextch == '\n')
200                     src++;
201                 text[dst++] = '\n';
202             } else {
203                 text[dst++] = ch;
204             }
205         }
206         return dst;
207     }
208     protected void sendResult(dstring text) {
209         // override to deal with ready data
210     }
211 
212     protected void handleFinish() {
213         // override to do something when thread is finishing
214     }
215 
216     private void run() {
217         //Log.d("BackgroundReaderBase run() enter");
218         // read file by bytes
219         try {
220             version (Windows) {
221                 import core.sys.windows.windows;
222                 // separate version for windows as workaround for hanging rawRead
223                 HANDLE h = _file.windowsHandle;
224                 DWORD bytesRead = 0;
225                 DWORD err;
226                 for (;;) {
227                     BOOL res = ReadFile(h, _byteBuffer.ptr, 1, &bytesRead, null);
228                     if (res) {
229                         if (bytesRead == 1)
230                             addByte(_byteBuffer[0]);
231                     } else {
232                         err = GetLastError();
233                         if (err == ERROR_MORE_DATA) {
234                             if (bytesRead == 1)
235                                 addByte(_byteBuffer[0]);
236                             continue;
237                         }
238                         //if (err == ERROR_BROKEN_PIPE || err = ERROR_INVALID_HANDLE)
239                         break;
240                     }
241                 }
242             } else {
243                 for (;;) {
244                     //Log.d("BackgroundReaderBase run() reading file");
245                     if (_file.eof)
246                         break;
247                     ubyte[] r = _file.rawRead(_byteBuffer);
248                     if (!r.length)
249                         break;
250                     //Log.d("BackgroundReaderBase run() read byte: ", r[0]);
251                     addByte(r[0]);
252                 }
253             }
254             _file.close();
255             flush();
256             //Log.d("BackgroundReaderBase run() closing file");
257             //Log.d("BackgroundReaderBase run() file closed");
258         } catch (Exception e) {
259             //Log.e("Exception occured while reading stream: ", e);
260         }
261         handleFinish();
262         _finished = true;
263         //Log.d("BackgroundReaderBase run() exit");
264     }
265 
266     void waitForFinish() {
267         static if (false) {
268             while (isRunning && !_finished)
269                 Thread.sleep( dur!("msecs")( 10 ) );
270         } else {
271             join(false);
272         }
273     }
274 
275 }
276 
277 /// reader which sends output text to TextWriter (warning: call will be made from background thread)
278 class BackgroundReader : BackgroundReaderBase {
279     protected TextWriter _destination;
280     this(std.stdio.File f, TextWriter destination) {
281         super(f);
282         assert(destination);
283         _destination = destination;
284     }
285     override protected void sendResult(dstring text) {
286         // override to deal with ready data
287         _destination.writeText(text);
288     }
289     override protected void handleFinish() {
290         // remove link to destination to help GC
291         _destination = null;
292     }
293 }
294 
295 /// runs external process, catches output, allows to stop
296 class ExternalProcess {
297 
298     protected char[][] _args;
299     protected char[] _workDir;
300     protected char[] _program;
301     protected string[string] _env;
302     protected TextWriter _stdout;
303     protected TextWriter _stderr;
304     protected BackgroundReader _stdoutReader;
305     protected BackgroundReader _stderrReader;
306     protected ProcessPipes _pipes;
307     protected ExternalProcessState _state;
308 
309     protected int _result;
310 
311     @property ExternalProcessState state() { return _state; }
312     /// returns process result for stopped process
313     @property int result() { return _result; }
314 
315     this() {
316     }
317 
318     ExternalProcessState run(string program, string[]args, string dir, TextWriter stdoutTarget, TextWriter stderrTarget = null) {
319         char[][] arguments;
320         foreach(a; args)
321             arguments ~= a.dup;
322         return run(program.dup, arguments, dir.dup, stdoutTarget, stderrTarget);
323     }
324     ExternalProcessState run(char[] program, char[][]args, char[] dir, TextWriter stdoutTarget, TextWriter stderrTarget = null) {
325         MsgLog.trace(MsgText!(DXXConfig.messages.MSG_PROC_RUN)(program,args));
326         _state = ExternalProcessState.None;
327         _program = findExecutablePath(cast(string)program).dup;
328         if (!_program) {
329             _state = ExternalProcessState.Error;
330             MsgLog.error(MsgText!(DXXConfig.messages.MSG_ERR_PROC_NOT_FOUND)(program));
331             return _state;
332         }
333         _args = args;
334         _workDir = dir;
335         _stdout = stdoutTarget;
336         _stderr = stderrTarget;
337         _result = 0;
338         assert(_stdout);
339         Redirect redirect;
340         char[][] params;
341         params ~= _program;
342         params ~= _args;
343         if (!_stderr)
344             redirect = Redirect.stdout | Redirect.stderrToStdout | Redirect.stdin;
345         else
346             redirect = Redirect.all;
347 //        sharedLog.info("Trying to run program ", _program, " with args ", _args);
348 //        MsgLog.trace(MsgText!(DXXConfig.messages.MSG_PROC_RUN)(program,args));
349         try {
350             _pipes = pipeProcess(params, redirect, _env, std.process.Config.suppressConsole, _workDir);
351             _state = ExternalProcessState.Running;
352             // start readers
353             _stdoutReader = new BackgroundReader(_pipes.stdout, _stdout);
354             _stdoutReader.start();
355             if (_stderr) {
356                 _stderrReader = new BackgroundReader(_pipes.stderr, _stderr);
357                 _stderrReader.start();
358             }
359         } catch (ProcessException e) {
360             MsgLog.error(MsgText!(DXXConfig.messages.MSG_ERR_PROC_RUN)(program,e));
361         } catch (std.stdio.StdioException e) {
362             MsgLog.error(MsgText!(DXXConfig.messages.MSG_ERR_PROC_REDIR)(program,e));
363         } catch (Throwable e) {
364             //sharedLog.error("Exception while trying to run program ", _program, " ", e);
365             MsgLog.error(MsgText!(DXXConfig.messages.MSG_ERR_PROC_UNKNOWN)(program,e));
366         }
367         return _state;
368     }
369 
370     protected void waitForReadingCompletion() {
371         try {
372             if (_stdoutReader && !_stdoutReader.finished) {
373                 _pipes.stdout.detach();
374                 //Log.d("waitForReadingCompletion - waiting for stdout");
375                 _stdoutReader.waitForFinish();
376                 //Log.d("waitForReadingCompletion - joined stdout");
377             }
378             _stdoutReader = null;
379         } catch (Exception e) {
380             MsgLog.error(MsgText!(DXXConfig.messages.MSG_ERR_PROC_WAITING_STDOUT)(_program,e));
381         }
382         try {
383             if (_stderrReader && !_stderrReader.finished) {
384                 _pipes.stderr.detach();
385                 //Log.d("waitForReadingCompletion - waiting for stderr");
386                 _stderrReader.waitForFinish();
387                 _stderrReader = null;
388                 //Log.d("waitForReadingCompletion - joined stderr");
389             }
390         } catch (Exception e) {
391             MsgLog.error(MsgText!(DXXConfig.messages.MSG_ERR_PROC_WAITING_STDERR)(_program,e));
392         }
393         //Log.d("waitForReadingCompletion - done");
394     }
395 
396     /// polls all available output from process streams
397     ExternalProcessState poll() {
398         //Log.d("ExternalProcess.poll state = ", _state);
399         bool res = true;
400         if (_state == ExternalProcessState.Error || _state == ExternalProcessState.None || _state == ExternalProcessState.Stopped)
401             return _state;
402         // check for process finishing
403         try {
404             auto pstate = std.process.tryWait(_pipes.pid);
405             if (pstate.terminated) {
406                 _state = ExternalProcessState.Stopped;
407                 _result = pstate.status;
408                 waitForReadingCompletion();
409             }
410         } catch (Exception e) {
411             MsgLog.error(MsgText!(DXXConfig.messages.MSG_ERR_PROC_WAITING)(_program));
412             _state = ExternalProcessState.Error;
413         }
414         return _state;
415     }
416 
417     /// waits until termination
418     ExternalProcessState wait() {
419         MsgLog.info(MsgText!(DXXConfig.messages.MSG_PROC_WAITING));
420         if (_state == ExternalProcessState.Error || _state == ExternalProcessState.None || _state == ExternalProcessState.Stopped)
421             return _state;
422         try {
423             _result = std.process.wait(_pipes.pid);
424             _state = ExternalProcessState.Stopped;
425             MsgLog.trace(MsgText!(DXXConfig.messages.MSG_PROC_READWAITING));
426             waitForReadingCompletion();
427         } catch (Exception e) {
428             MsgLog.error(MsgText!(DXXConfig.messages.MSG_ERR_PROC_UNKNOWN));
429             _state = ExternalProcessState.Error;
430         }
431         return _state;
432     }
433 
434     /// request process stop
435     ExternalProcessState kill() {
436         MsgLog.info(MsgText!(DXXConfig.messages.MSG_PROC_KILL));
437 
438         if (_state == ExternalProcessState.Error || _state == ExternalProcessState.None || _state == ExternalProcessState.Stopped)
439             return _state;
440         if (_state == ExternalProcessState.Running) {
441             std.process.kill(_pipes.pid);
442             _state = ExternalProcessState.Stopping;
443         }
444         return _state;
445     }
446 
447     bool write(string data) {
448         if (_state == ExternalProcessState.Error || _state == ExternalProcessState.None || _state == ExternalProcessState.Stopped) {
449             return false;
450         } else {
451             //Log.d("writing ", data.length, " characters to stdin");
452             _pipes.stdin.write("", data);
453             _pipes.stdin.flush();
454             //_pipes.stdin.close();
455             return true;
456         }
457     }
458 }
459 private import std.algorithm;
460 private import std.process;
461 private import std.path;
462 private import std.file;
463 private import std.utf;
464 
465 /// for executable name w/o path, find absolute path to executable
466 string findExecutablePath(string executableName) {
467     import std..string : split;
468     version (Windows) {
469         if (!executableName.endsWith(".exe"))
470             executableName = executableName ~ ".exe";
471     }
472     if(exists(executableName) && isFile(executableName)) return executableName;
473     string currentExeDir = dirName(thisExePath());
474     string inCurrentExeDir = absolutePath(buildNormalizedPath(currentExeDir, executableName));
475     if (exists(inCurrentExeDir) && isFile(inCurrentExeDir))
476         return inCurrentExeDir; // found in current directory
477     string pathVariable = environment.get("PATH");
478     if (!pathVariable)
479         return null;
480     string[] paths = pathVariable.split(pathSeparator);
481     foreach(path; paths) {
482         string pathname = absolutePath(buildNormalizedPath(path, executableName));
483         if (exists(pathname) && isFile(pathname))
484             return pathname;
485     }
486     return null;
487 }