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));