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 import ae.utils.array;
207 import ae.utils.regex;
208 import std.regex;
209 
210 string expandEnvVars(alias RE)(string s, string[string] env = std.process.environment.toAA)
211 {
212 	string result;
213 	size_t last = 0;
214 	foreach (c; s.matchAll(RE))
215 	{
216 		result ~= s[last..s.sliceIndex(c[1])];
217 		auto var = c[2];
218 		string value = env.get(var, c[1]);
219 		result ~= value;
220 		last = s.sliceIndex(c[1]) + c[1].length;
221 	}
222 	result ~= s[last..$];
223 	return result;
224 }
225 
226 alias expandWindowsEnvVars = expandEnvVars!(re!`(%(.*?)%)`);
227 
228 unittest
229 {
230 	std.process.environment[`FOOTEST`] = `bar`;
231 	assert("a%FOOTEST%b".expandWindowsEnvVars() == "abarb");
232 }
233 
234 // ************************************************************************
235 
236 int waitTimeout(Pid pid, Duration time)
237 {
238 	bool ok = false;
239 	auto t = new Thread({
240 		Thread.sleep(time);
241 		if (!ok)
242 			try
243 				pid.kill();
244 			catch (Exception) {} // Ignore race condition
245 	}).start();
246 	auto result = pid.wait();
247 	ok = true;
248 	return result;
249 }
250 
251 /// Wait for process to exit asynchronously.
252 /// Call callback when it exits.
253 /// WARNING: the callback will be invoked in another thread!
254 void waitAsync(Pid pid, void delegate(int) callback = null)
255 {
256 	auto t = new Thread({
257 		auto result = pid.wait();
258 		if (callback)
259 			callback(result);
260 	}).start();
261 }