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 		auto result = execute(args, parsed.environment, parsed.config, parsed.maxOutput, parsed.workDir);
116 		output = result.output.stripRight();
117 		return result.status;
118 	})(args);
119 	return output;
120 }
121 
122 /// std.process helper.
123 /// Run a command, feed it the given input, and collect its output.
124 /// Throw if it exited with non-zero status. Return output.
125 T[] pipe(T, Params...)(string[] args, in T[] input, Params params)
126 {
127 	auto parsed = ProcessParams(params);
128 	T[] output;
129 	invoke!({
130 		auto pipes = pipeProcess(args, Redirect.stdin | Redirect.stdout,
131 			parsed.environment, parsed.config, parsed.workDir);
132 		auto f = pipes.stdin;
133 		auto writer = writeFileAsync(f, input);
134 		scope(exit) writer.join();
135 		output = cast(T[])readFile(pipes.stdout);
136 		return pipes.pid.wait();
137 	})(args);
138 	return output;
139 }
140 
141 // ************************************************************************
142 
143 ubyte[] iconv(in void[] data, string inputEncoding, string outputEncoding)
144 {
145 	auto args = ["timeout", "30", "iconv", "-f", inputEncoding, "-t", outputEncoding];
146 	auto result = pipe(args, data);
147 	return cast(ubyte[])result;
148 }
149 
150 string iconv(in void[] data, string inputEncoding)
151 {
152 	import std.utf;
153 	auto result = cast(string)iconv(data, inputEncoding, "UTF-8");
154 	validate(result);
155 	return result;
156 }
157 
158 version (HAVE_UNIX)
159 unittest
160 {
161 	assert(iconv("Hello"w, "UTF-16LE") == "Hello");
162 }
163 
164 string sha1sum(in void[] data)
165 {
166 	auto output = cast(string)pipe(["sha1sum", "-b", "-"], data);
167 	return output[0..40];
168 }
169 
170 version (HAVE_UNIX)
171 unittest
172 {
173 	assert(sha1sum("") == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
174 	assert(sha1sum("a  b\nc\r\nd") == "667c71ffe2ac8a4fe500e3b96435417e4c5ec13b");
175 }
176 
177 // ************************************************************************
178 
179 import ae.utils.path;
180 deprecated alias NULL_FILE = nullFileName;
181 
182 // ************************************************************************
183 
184 /// Reverse of std.process.environment.toAA
185 void setEnvironment(string[string] env)
186 {
187 	foreach (k, v; env)
188 		if (k.length)
189 			environment[k] = v;
190 	foreach (k, v; environment.toAA())
191 		if (k.length && k !in env)
192 			environment.remove(k);
193 }
194 
195 import ae.utils.array;
196 import ae.utils.regex;
197 import std.regex;
198 
199 string expandEnvVars(alias RE)(string s, string[string] env = std.process.environment.toAA)
200 {
201 	string result;
202 	size_t last = 0;
203 	foreach (c; s.matchAll(RE))
204 	{
205 		result ~= s[last..s.sliceIndex(c[1])];
206 		auto var = c[2];
207 		string value = env.get(var, c[1]);
208 		result ~= value;
209 		last = s.sliceIndex(c[1]) + c[1].length;
210 	}
211 	result ~= s[last..$];
212 	return result;
213 }
214 
215 alias expandWindowsEnvVars = expandEnvVars!(re!`(%(.*?)%)`);
216 
217 unittest
218 {
219 	std.process.environment[`FOOTEST`] = `bar`;
220 	assert("a%FOOTEST%b".expandWindowsEnvVars() == "abarb");
221 }
222 
223 // ************************************************************************
224 
225 int waitTimeout(Pid pid, Duration time)
226 {
227 	bool ok = false;
228 	auto t = new Thread({
229 		Thread.sleep(time);
230 		if (!ok)
231 			try
232 				pid.kill();
233 			catch (Exception) {} // Ignore race condition
234 	}).start();
235 	auto result = pid.wait();
236 	ok = true;
237 	return result;
238 }