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 /// btdu entry point 20 module btdu.main; 21 22 import core.runtime : Runtime; 23 import core.time; 24 25 import std.conv : to; 26 import std.exception; 27 import std.parallelism : totalCPUs; 28 import std.path; 29 import std.random; 30 import std.socket; 31 import std.stdio; 32 import std.string; 33 34 import ae.sys.file : getPathMountInfo; 35 import ae.utils.funopt; 36 import ae.utils.main; 37 import ae.utils.time.parsedur; 38 39 import btdu.browser; 40 import btdu.common; 41 import btdu.paths; 42 import btdu.sample; 43 import btdu.subproc; 44 import btdu.state; 45 46 @(`Sampling disk usage profiler for btrfs.`) 47 void program( 48 Parameter!(string, "Path to the root of the filesystem to analyze") path, 49 Option!(uint, "Number of sampling subprocesses\n (default is number of logical CPUs for this system)", "N", 'j') procs = 0, 50 Option!(Seed, "Random seed used to choose samples") seed = 0, 51 Switch!hiddenOption subprocess = false, 52 Option!(string, hiddenOption) benchmark = null, 53 Switch!("Expert mode: collect and show additional metrics.\nUses more memory.") expert = false, 54 Switch!hiddenOption man = false, 55 ) 56 { 57 if (man) 58 { 59 stdout.write(generateManPage!program( 60 "btdu", 61 ".B btdu 62 is a sampling disk usage profiler for btrfs. 63 64 For a detailed description, please see the full documentation: 65 66 .I https://github.com/CyberShadow/btdu#readme", 67 null, 68 `.SH BUGS 69 Please report defects and enhancement requests to the GitHub issue tracker: 70 71 .I https://github.com/CyberShadow/btdu/issues 72 73 .SH AUTHORS 74 75 \fBbtdu\fR is written by Vladimir Panteleev <btdu@c\fRy.m\fRd> and contributors: 76 77 .I https://github.com/CyberShadow/btdu/graphs/contributors 78 `, 79 )); 80 return; 81 } 82 83 rndGen = Random(seed); 84 fsPath = path.buildNormalizedPath; 85 86 if (subprocess) 87 return subprocessMain(path); 88 89 checkBtrfs(fsPath); 90 91 if (procs == 0) 92 procs = totalCPUs; 93 94 bool headless; 95 Duration benchmarkTime; 96 ulong benchmarkSamples; 97 if (benchmark) 98 { 99 headless = true; 100 if (isNumeric(benchmark[])) 101 benchmarkSamples = benchmark.to!ulong; 102 else 103 benchmarkTime = parseDuration(benchmark); 104 } 105 106 subprocesses = new Subprocess[procs]; 107 foreach (ref subproc; subprocesses) 108 subproc.start(); 109 110 Socket stdinSocket; 111 if (!headless) 112 { 113 stdinSocket = new Socket(cast(socket_t)stdin.fileno, AddressFamily.UNSPEC); 114 stdinSocket.blocking = false; 115 } 116 117 .expert = expert; 118 119 Browser browser; 120 if (!headless) 121 { 122 browser.start(); 123 browser.update(); 124 } 125 126 auto startTime = MonoTime.currTime(); 127 enum refreshInterval = 500.msecs; 128 auto nextRefresh = startTime; 129 130 auto readSet = new SocketSet; 131 auto exceptSet = new SocketSet; 132 133 // Main event loop 134 while (true) 135 { 136 readSet.reset(); 137 exceptSet.reset(); 138 if (stdinSocket) 139 { 140 readSet.add(stdinSocket); 141 exceptSet.add(stdinSocket); 142 } 143 if (!paused) 144 foreach (ref subproc; subprocesses) 145 readSet.add(subproc.socket); 146 147 Socket.select(readSet, null, exceptSet); 148 auto now = MonoTime.currTime(); 149 150 if (stdinSocket && browser.handleInput()) 151 { 152 do {} while (browser.handleInput()); // Process all input 153 if (browser.done) 154 break; 155 browser.update(); 156 nextRefresh = now + refreshInterval; 157 } 158 if (!paused) 159 foreach (ref subproc; subprocesses) 160 if (readSet.isSet(subproc.socket)) 161 subproc.handleInput(); 162 if (!headless && now > nextRefresh) 163 { 164 browser.update(); 165 nextRefresh = now + refreshInterval; 166 } 167 if (benchmarkTime && now > startTime + benchmarkTime) 168 break; 169 if (benchmarkSamples && browserRoot.data[SampleType.represented].samples >= benchmarkSamples) 170 break; 171 } 172 173 if (benchmarkTime) 174 writeln(browserRoot.data[SampleType.represented].samples); 175 if (benchmarkSamples) 176 writeln(MonoTime.currTime() - startTime); 177 } 178 179 void checkBtrfs(string fsPath) 180 { 181 import core.sys.posix.fcntl : open, O_RDONLY; 182 import std.string : toStringz; 183 import btrfs : isBTRFS, isSubvolume, getSubvolumeID; 184 import btrfs.c.kernel_shared.ctree : BTRFS_FS_TREE_OBJECTID; 185 186 int fd = open(fsPath.toStringz, O_RDONLY); 187 errnoEnforce(fd >= 0, "open"); 188 189 enforce(fd.isBTRFS, 190 fsPath ~ " is not a btrfs filesystem"); 191 192 enforce(fd.isSubvolume, { 193 auto rootPath = getPathMountInfo(fsPath).file; 194 if (!rootPath) 195 rootPath = "/"; 196 return format( 197 "%s is not the root of a btrfs subvolume - " ~ 198 "please specify the path to the subvolume root" ~ 199 "\n" ~ 200 "E.g.: %s", 201 fsPath, 202 [Runtime.args[0], rootPath].escapeShellCommand, 203 ); 204 }()); 205 206 enforce(fd.getSubvolumeID() == BTRFS_FS_TREE_OBJECTID, { 207 auto device = getPathMountInfo(fsPath).spec; 208 if (!device) 209 device = "/dev/sda1"; // placeholder 210 auto tmpName = "/tmp/" ~ device.baseName; 211 return format( 212 "%s is not the root btrfs subvolume - " ~ 213 "please specify the path to a mountpoint mounted with subvol=/ or subvolid=5" ~ 214 "\n" ~ 215 "E.g.: %s && %s && %s", 216 fsPath, 217 ["mkdir", tmpName].escapeShellCommand, 218 ["mount", "-o", "subvol=/", device, tmpName].escapeShellCommand, 219 [Runtime.args[0], tmpName].escapeShellCommand, 220 ); 221 }()); 222 } 223 224 private string escapeShellCommand(string[] args) 225 { 226 import std.process : escapeShellFileName; 227 import std.algorithm.searching : all; 228 import ae.utils.array : isOneOf; 229 230 foreach (ref arg; args) 231 if (!arg.representation.all!(c => c.isOneOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_/.=:@%"))) 232 arg = arg.escapeShellFileName; 233 return args.join(" "); 234 } 235 236 void usageFun(string usage) 237 { 238 stderr.writeln("btdu v" ~ btduVersion); 239 stderr.writeln(usage); 240 } 241 242 mixin main!(funopt!(program, FunOptConfig.init, usageFun));