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