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