1 /**
2  * Simple execution of shell commands,
3  * and wrappers for common utilities.
4  *
5  * License:
6  *   This Source Code Form is subject to the terms of
7  *   the Mozilla Public License, v. 2.0. If a copy of
8  *   the MPL was not distributed with this file, You
9  *   can obtain one at http://mozilla.org/MPL/2.0/.
10  *
11  * Authors:
12  *   Vladimir Panteleev <ae@cy.md>
13  */
14 
15 module ae.sys.cmd;
16 
17 import core.thread;
18 
19 import std.exception;
20 import std.process;
21 import std.stdio;
22 import std..string;
23 
24 import ae.sys.file;
25 
26 /// Returns a very unique name for a temporary file.
27 string getTempFileName(string extension)
28 {
29 	import std.random;
30 	import std.file;
31 	import std.path : buildPath;
32 
33 	static int counter;
34 	return buildPath(tempDir(), format("run-%d-%d-%d-%d.%s",
35 		getpid(),
36 		getCurrentThreadID(),
37 		uniform!uint(),
38 		counter++,
39 		extension
40 	));
41 }
42 
43 /// Like `thisProcessID`, but for threads.
44 ulong getCurrentThreadID()
45 {
46 	version (Windows)
47 	{
48 		import core.sys.windows.windows;
49 		return GetCurrentThreadId();
50 	}
51 	else
52 	version (Posix)
53 	{
54 		import core.sys.posix.pthread;
55 		return cast(ulong)pthread_self();
56 	}
57 }
58 
59 // ************************************************************************
60 
61 struct ProcessParams
62 {
63 	const(string[string]) environment = null;
64 	std.process.Config config = std.process.Config.none;
65 	size_t maxOutput = size_t.max;
66 	string workDir = null;
67 
68 	this(Params...)(Params params)
69 	{
70 		foreach (arg; params)
71 		{
72 			static if (is(typeof(arg) == string))
73 				workDir = arg;
74 			else
75 			static if (is(typeof(arg) : const(string[string])))
76 				environment = arg;
77 			else
78 			static if (is(typeof(arg) == size_t))
79 				maxOutput = arg;
80 			else
81 			static if (is(typeof(arg) == std.process.Config))
82 				config = arg;
83 			else
84 				static assert(false, "Unknown type for process invocation parameter: " ~ typeof(arg).stringof);
85 		}
86 	}
87 }
88 
89 private void invoke(alias runner)(string[] args)
90 {
91 	//debug scope(failure) std.stdio.writeln("[CWD] ", getcwd());
92 	debug(CMD) std.stdio.stderr.writeln("invoke: ", args);
93 	auto status = runner();
94 	enforce(status == 0,
95 		"Command `%s` failed with status %d".format(escapeShellCommand(args), status));
96 }
97 
98 /// std.process helper.
99 /// Run a command, and throw if it exited with a non-zero status.
100 void run(Params...)(string[] args, Params params)
101 {
102 	auto parsed = ProcessParams(params);
103 	invoke!({ return spawnProcess(
104 				args, stdin, stdout, stderr,
105 				parsed.environment, parsed.config, parsed.workDir
106 			).wait(); })(args);
107 }
108 
109 /// std.process helper.
110 /// Run a command and collect its output.
111 /// Throw if it exited with a non-zero status.
112 string query(Params...)(string[] args, Params params)
113 {
114 	auto parsed = ProcessParams(params);
115 	string output;
116 	invoke!({
117 		// Don't use execute due to https://issues.dlang.org/show_bug.cgi?id=17844
118 		version (none)
119 		{
120 			auto result = execute(args, parsed.environment, parsed.config, parsed.maxOutput, parsed.workDir);
121 			output = result.output.stripRight();
122 			return result.status;
123 		}
124 		else
125 		{
126 			auto pipes = pipeProcess(args, Redirect.stdout,
127 				parsed.environment, parsed.config, parsed.workDir);
128 			output = cast(string)readFile(pipes.stdout);
129 			return pipes.pid.wait();
130 		}
131 	})(args);
132 	return output;
133 }
134 
135 /// std.process helper.
136 /// Run a command, feed it the given input, and collect its output.
137 /// Throw if it exited with non-zero status. Return output.
138 T[] pipe(T, Params...)(string[] args, in T[] input, Params params)
139 {
140 	auto parsed = ProcessParams(params);
141 	T[] output;
142 	invoke!({
143 		auto pipes = pipeProcess(args, Redirect.stdin | Redirect.stdout,
144 			parsed.environment, parsed.config, parsed.workDir);
145 		auto f = pipes.stdin;
146 		auto writer = writeFileAsync(f, input);
147 		scope(exit) writer.join();
148 		output = cast(T[])readFile(pipes.stdout);
149 		return pipes.pid.wait();
150 	})(args);
151 	return output;
152 }
153 
154 // ************************************************************************
155 
156 /// Wrapper for the `iconv` program.
157 ubyte[] iconv(in void[] data, string inputEncoding, string outputEncoding)
158 {
159 	auto args = ["timeout", "30", "iconv", "-f", inputEncoding, "-t", outputEncoding];
160 	auto result = pipe(args, data);
161 	return cast(ubyte[])result;
162 }
163 
164 /// ditto
165 string iconv(in void[] data, string inputEncoding)
166 {
167 	import std.utf;
168 	auto result = cast(string)iconv(data, inputEncoding, "UTF-8");
169 	validate(result);
170 	return result;
171 }
172 
173 version (HAVE_UNIX)
174 unittest
175 {
176 	assert(iconv("Hello"w, "UTF-16LE") == "Hello");
177 }
178 
179 /// Wrapper for the `sha1sum` program.
180 string sha1sum(in void[] data)
181 {
182 	auto output = cast(string)pipe(["sha1sum", "-b", "-"], data);
183 	return output[0..40];
184 }
185 
186 version (HAVE_UNIX)
187 unittest
188 {
189 	assert(sha1sum("") == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
190 	assert(sha1sum("a  b\nc\r\nd") == "667c71ffe2ac8a4fe500e3b96435417e4c5ec13b");
191 }
192 
193 // ************************************************************************
194 
195 import ae.utils.path;
196 deprecated alias NULL_FILE = nullFileName;
197 
198 // ************************************************************************
199 
200 /// Reverse of std.process.environment.toAA
201 void setEnvironment(string[string] env)
202 {
203 	foreach (k, v; env)
204 		if (k.length)
205 			environment[k] = v;
206 	foreach (k, v; environment.toAA())
207 		if (k.length && k !in env)
208 			environment.remove(k);
209 }
210 
211 /// Expand Windows-like variable placeholders (`"%VAR%"`) in the given string.
212 string expandWindowsEnvVars(alias getenv = environment.get)(string s)
213 {
214 	import std.array;
215 	auto buf = appender!string();
216 
217 	size_t lastPercent = 0;
218 	bool inPercent = false;
219 
220 	foreach (i, c; s)
221 		if (c == '%')
222 		{
223 			if (inPercent)
224 				buf.put(lastPercent == i ? "%" : getenv(s[lastPercent .. i]));
225 			else
226 				buf.put(s[lastPercent .. i]);
227 			inPercent = !inPercent;
228 			lastPercent = i + 1;
229 		}
230 	enforce(!inPercent, "Unterminated environment variable name");
231 	buf.put(s[lastPercent .. $]);
232 	return buf.data;
233 }
234 
235 unittest
236 {
237 	std.process.environment[`FOOTEST`] = `bar`;
238 	assert("a%FOOTEST%b".expandWindowsEnvVars() == "abarb");
239 }
240 
241 // ************************************************************************
242 
243 /// Like `std.process.wait`, but with a timeout.
244 /// If the timeout is exceeded, the program is killed.
245 int waitTimeout(Pid pid, Duration time)
246 {
247 	bool ok = false;
248 	auto t = new Thread({
249 		Thread.sleep(time);
250 		if (!ok)
251 			try
252 				pid.kill();
253 			catch (Exception) {} // Ignore race condition
254 	}).start();
255 	auto result = pid.wait();
256 	ok = true;
257 	return result;
258 }
259 
260 /// Wait for process to exit asynchronously.
261 /// Call callback when it exits.
262 /// WARNING: the callback will be invoked in another thread!
263 void waitAsync(Pid pid, void delegate(int) callback = null)
264 {
265 	auto t = new Thread({
266 		auto result = pid.wait();
267 		if (callback)
268 			callback(result);
269 	}).start();
270 }