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