1 /*
2  * Copyright (C) 2020, 2021  Vladimir Panteleev <btdu@cy.md>
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public
6  * License v2 as published by the Free Software Foundation.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public
14  * License along with this program; if not, write to the
15  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
16  * Boston, MA 021110-1307, USA.
17  */
18 
19 /// Subprocess management
20 module btdu.subproc;
21 
22 import core.sys.posix.signal;
23 import core.sys.posix.unistd;
24 
25 import std.algorithm.iteration;
26 import std.algorithm.mutation;
27 import std.algorithm.searching;
28 import std.conv;
29 import std.exception;
30 import std.file;
31 import std.process;
32 import std.random;
33 import std.socket;
34 import std.stdio;
35 import std..string;
36 
37 import ae.utils.array;
38 
39 import btrfs.c.kernel_shared.ctree;
40 
41 import btdu.common;
42 import btdu.paths;
43 import btdu.proto;
44 import btdu.state;
45 
46 /// Represents one managed subprocess
47 struct Subprocess
48 {
49 	Pipe pipe;
50 	Socket socket;
51 	Pid pid;
52 
53 	void start()
54 	{
55 		pipe = .pipe();
56 		socket = new Socket(cast(socket_t)pipe.readEnd.fileno.dup, AddressFamily.UNSPEC);
57 		socket.blocking = false;
58 
59 		pid = spawnProcess(
60 			[
61 				thisExePath,
62 				"--subprocess",
63 				"--seed", rndGen.uniform!Seed.text,
64 				"--",
65 				fsPath,
66 			],
67 			stdin,
68 			pipe.writeEnd,
69 		);
70 	}
71 
72 	void pause(bool doPause)
73 	{
74 		pid.kill(doPause ? SIGSTOP : SIGCONT);
75 	}
76 
77 	/// Receive buffer
78 	private ubyte[] buf;
79 	/// Section of buffer containing received and unparsed data
80 	private size_t bufStart, bufEnd;
81 
82 	/// Called when select() identifies that the process wrote something.
83 	void handleInput()
84 	{
85 		while (true)
86 		{
87 			auto data = buf[bufStart .. bufEnd];
88 			auto bytesNeeded = parse(data, this);
89 			bufStart = bufEnd - data.length;
90 			if (bufStart == bufEnd)
91 				bufStart = bufEnd = 0;
92 			if (buf.length < bufEnd + bytesNeeded)
93 			{
94 				// Moving remaining data to the start of the buffer
95 				// may allow us to avoid an allocation.
96 				if (bufStart > 0)
97 				{
98 					copy(buf[bufStart .. bufEnd], buf[0 .. bufEnd - bufStart]);
99 					bufEnd -= bufStart;
100 					bufStart -= bufStart;
101 				}
102 				if (buf.length < bufEnd + bytesNeeded)
103 				{
104 					buf.length = bufEnd + bytesNeeded;
105 					buf.length = buf.capacity;
106 				}
107 			}
108 			auto received = read(pipe.readEnd.fileno, buf.ptr + bufEnd, buf.length - bufEnd);
109 			enforce(received != 0, "Unexpected subprocess termination");
110 			if (received == Socket.ERROR)
111 			{
112 				errnoEnforce(wouldHaveBlocked, "Subprocess read error");
113 				return;
114 			}
115 			bufEnd += received;
116 		}
117 	}
118 
119 	void handleMessage(StartMessage m)
120 	{
121 		if (!totalSize)
122 			totalSize = m.totalSize;
123 	}
124 
125 	void handleMessage(NewRootMessage m)
126 	{
127 		globalRoots.require(m.rootID, {
128 			if (m.parentRootID || m.name.length)
129 				return new GlobalPath(
130 					*(m.parentRootID in globalRoots).enforce("Unknown parent root"),
131 					subPathRoot.appendPath(m.name),
132 				);
133 			else
134 			if (m.rootID == BTRFS_FS_TREE_OBJECTID)
135 				return new GlobalPath(null, &subPathRoot);
136 			else
137 			if (m.rootID == BTRFS_ROOT_TREE_OBJECTID)
138 				return new GlobalPath(null, subPathRoot.appendName("\0ROOT_TREE"));
139 			else
140 				return new GlobalPath(null, subPathRoot.appendName(format!"\0TREE_%d"(m.rootID)));
141 		}());
142 	}
143 
144 	private struct Result
145 	{
146 		ulong logicalOffset;
147 		BrowserPath* browserPath;
148 		GlobalPath* inodeRoot;
149 		bool haveInode, havePath;
150 		bool ignoringOffset;
151 	}
152 	private Result result;
153 	private FastAppender!GlobalPath allPaths;
154 
155 	void handleMessage(ResultStartMessage m)
156 	{
157 		result.logicalOffset = m.logicalOffset;
158 		result.browserPath = &browserRoot;
159 		static immutable flagNames = [
160 			"DATA",
161 			"SYSTEM",
162 			"METADATA",
163 			"RAID0",
164 			"RAID1",
165 			"DUP",
166 			"RAID10",
167 			"RAID5",
168 			"RAID6",
169 			"RAID1C3",
170 			"RAID1C4",
171 		].amap!(s => "\0" ~ s);
172 		if ((m.chunkFlags & BTRFS_BLOCK_GROUP_PROFILE_MASK) == 0)
173 			result.browserPath = result.browserPath.appendName("\0SINGLE");
174 		foreach_reverse (b; 0 .. flagNames.length)
175 			if (m.chunkFlags & (1UL << b))
176 				result.browserPath = result.browserPath.appendName(flagNames[b]);
177 		if ((m.chunkFlags & BTRFS_BLOCK_GROUP_DATA) == 0)
178 			result.haveInode = true; // Sampler won't even try
179 	}
180 
181 	void handleMessage(ResultInodeStartMessage m)
182 	{
183 		result.haveInode = true;
184 		result.havePath = false;
185 		result.inodeRoot = *(m.rootID in globalRoots).enforce("Unknown inode root");
186 		result.ignoringOffset = m.ignoringOffset; // Will be the same for all inodes
187 	}
188 
189 	void handleMessage(ResultInodeErrorMessage m)
190 	{
191 		allPaths ~= GlobalPath(result.inodeRoot, subPathRoot.appendError(m.error));
192 	}
193 
194 	void handleMessage(ResultMessage m)
195 	{
196 		result.havePath = true;
197 		allPaths ~= GlobalPath(result.inodeRoot, subPathRoot.appendPath(m.path));
198 	}
199 
200 	void handleMessage(ResultInodeEndMessage m)
201 	{
202 		cast(void) m; // empty message
203 		if (!result.havePath)
204 			allPaths ~= GlobalPath(result.inodeRoot, subPathRoot.appendPath("\0NO_PATH"));
205 	}
206 
207 	void handleMessage(ResultErrorMessage m)
208 	{
209 		allPaths ~= GlobalPath(null, subPathRoot.appendError(m.error));
210 		result.haveInode = true;
211 	}
212 
213 	void handleMessage(ResultEndMessage m)
214 	{
215 		if (result.ignoringOffset)
216 			result.browserPath = result.browserPath.appendName("\0UNREACHABLE");
217 		if (!result.haveInode)
218 			result.browserPath = result.browserPath.appendName("\0NO_INODE");
219 		auto representativeBrowserPath = result.browserPath;
220 		if (allPaths.get().length)
221 		{
222 			auto representativePath = allPaths.get().fold!((a, b) {
223 				// Prefer paths with resolved roots
224 				auto aResolved = a.isResolved();
225 				auto bResolved = b.isResolved();
226 				if (aResolved != bResolved)
227 					return aResolved ? a : b;
228 				// Shortest path always wins
229 				auto aLength = a.length;
230 				auto bLength = b.length;
231 				if (aLength != bLength)
232 					return aLength < bLength ? a : b;
233 				// If the length is the same, pick the lexicographically smallest one
234 				return a < b ? a : b;
235 			})();
236 			representativeBrowserPath = result.browserPath.appendPath(&representativePath);
237 		}
238 		representativeBrowserPath.addSample(SampleType.represented, result.logicalOffset, m.duration);
239 
240 		if (allPaths.get().length)
241 		{
242 			foreach (ref path; allPaths.get())
243 				(*representativeBrowserPath.seenAs.getOrAdd(path, 0UL))++;
244 
245 			if (expert)
246 			{
247 				auto distributedShare = 1.0 / allPaths.get().length;
248 
249 				static FastAppender!(BrowserPath*) browserPaths;
250 				browserPaths.clear();
251 				foreach (ref path; allPaths.get())
252 				{
253 					auto browserPath = result.browserPath.appendPath(&path);
254 					browserPaths.put(browserPath);
255 
256 					browserPath.addSample(SampleType.shared_, result.logicalOffset, m.duration);
257 					browserPath.addDistributedSample(distributedShare);
258 				}
259 
260 				auto exclusiveBrowserPath = BrowserPath.commonPrefix(browserPaths.get());
261 				exclusiveBrowserPath.addSample(SampleType.exclusive, result.logicalOffset, m.duration);
262 			}
263 		}
264 		else
265 		{
266 			if (expert)
267 			{
268 				representativeBrowserPath.addSample(SampleType.shared_, result.logicalOffset, m.duration);
269 				representativeBrowserPath.addSample(SampleType.exclusive, result.logicalOffset, m.duration);
270 				representativeBrowserPath.addDistributedSample(1);
271 			}
272 		}
273 
274 		result = Result.init;
275 		allPaths.clear();
276 	}
277 
278 	void handleMessage(FatalErrorMessage m)
279 	{
280 		throw new Exception("Subprocess encountered a fatal error:\n" ~ cast(string)m.msg);
281 	}
282 }
283 
284 private bool isResolved(ref GlobalPath p)
285 {
286 	return !p.range
287 		.map!(g => g.range)
288 		.joiner
289 		.canFind!(n => n.startsWith("\0TREE_"));
290 }
291 
292 private SubPath* appendError(ref SubPath path, ref btdu.proto.Error error)
293 {
294 	auto result = &path;
295 	result = result.appendName("\0ERROR");
296 	result = result.appendName(error.msg);
297 	if (error.errno | error.path.length)
298 	{
299 		result = result.appendName(getErrno(error.errno).name);
300 		if (error.path.length)
301 		{
302 			auto errorPath = error.path;
303 			if (!errorPath.skipOver("/"))
304 				debug assert(false, "Non-absolute path: " ~ errorPath);
305 			result = result.appendPath(errorPath);
306 		}
307 	}
308 	return result;
309 }