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 }