1 /**
2 	Handles all the console output of the Dub package manager, by providing useful
3 	methods for handling colored text. The module also disables colors when stdout
4 	and stderr are not a TTY in order to avoid ASCII escape sequences in piped
5 	output. The module can auto-detect and configure itself in this regard by
6 	calling initLogging() at the beginning of the program. But, whether to color
7 	text or not can also be set manually with setLoggingColorsEnabled(bool).
8 
9 	The output for the log levels error, warn and info is formatted like this:
10 
11 	"			 <tag> <text>"
12 	 '----------'
13 	 fixed width
14 
15 	the "tag" part can be colored (most often will be) and always has a fixed
16 	width, which is defined as a constant at the beginning of this module.
17 
18 	The output for the log levels debug and diagnostic will be just the plain
19 	string.
20 
21 	There are some default tag string and color values for some logging levels:
22 	- warn: "Warning", yellow bold
23 	- error: "Error", red bold
24 
25 	Actually, for error and warn levels, the tag color is fixed to the ones listed
26 	above.
27 
28 	Also, the default tag string for the info level is "" (the empty string) and
29 	the default color is white (usually it's manually set when calling logInfo
30 	with the wanted tag string, but this allows to just logInfo("text") without
31 	having to worry about the tag if it's not needed).
32 
33 	Usage:
34 		After initializing the logging module with initLogging(), the functions
35 		logDebug(..), logDiagnostic(..), logInfo(..), logWarning(..) and logError(..)
36 		can be used to print log messages. Whether the messages are printed on stdout
37 		or stderr depends on the log level (warning and error go to stderr).
38 		The log(..) function can also be used. Check the signature and documentation
39 		of the functions for more information.
40 
41 		The minimum log level to print can be configured using setLogLevel(..),
42 		and whether to color outputted text or not can be set with
43 		setLoggingColorsEnabled(..)
44 
45 		The color(str, color) function can be used to color text within a log
46 		message, for instance like this:
47 
48 		logInfo("Tag", Color.green, "My %s message", "colored".color(Color.red))
49 
50 	Copyright: © 2018 Giacomo De Lazzari
51 	License: Subject to the terms of the MIT license, as written in the included LICENSE file.
52 	Authors: Giacomo De Lazzari
53 */
54 
55 module dub.internal.logging;
56 
57 import std.stdio;
58 import std.array;
59 import std.format;
60 import std.string;
61 
62 import dub.internal.colorize : fg, mode;
63 
64 /**
65 	An enum listing possible colors for terminal output, useful to set the color
66 	of a tag. Re-exported from d-colorize in dub.internal.colorize. See the enum
67 	definition there for a list of possible values.
68 */
69 public alias Color = fg;
70 
71 /**
72 	An enum listing possible text "modes" for terminal output, useful to set the
73 	text to bold, underline, blinking, etc...
74 	Re-exported from d-colorize in dub.internal.colorize. See the enum definition
75 	there for a list of possible values.
76 */
77 public alias Mode = mode;
78 
79 /// Defines the current width of logging tags for justifying in chars.
80 /// Can be manipulated through push and pop.
81 struct TagWidth {
82 	import core.atomic;
83 
84 	private shared int value = 12;
85 	private shared int index;
86 	private shared int[16] stack;
87 
88 	/// Gets the tag width in chars
89 	public int get() {
90 		return value;
91 	}
92 
93 	/// Changes the tag width for all following logging calls, until $(LREF pop) is called.
94 	public void push(int width) {
95 		int currentIndex = index;
96 		index.atomicOp!"+="(1);
97 		stack[currentIndex] = value;
98 		assert(index < stack.length, "too many TagWidth.push without pop");
99 		value = width;
100 	}
101 
102 	/// Reverts the last $(LREF push) call.
103 	public void pop() {
104 		assert(index > 0);
105 		value = stack[index.atomicOp!"-="(1)];
106 	}
107 }
108 
109 /// The global tag width instance used for logging.
110 public __gshared TagWidth tagWidth;
111 
112 /// Possible log levels supported
113 enum LogLevel {
114 	debug_,
115 	diagnostic,
116 	info,
117 	warn,
118 	error,
119 	none
120 }
121 
122 // The current minimum log level to be printed
123 private shared LogLevel _minLevel = LogLevel.info;
124 
125 /*
126 	Whether to print text with colors or not, defaults to true but will be set
127 	to false in initLogging() if stdout or stderr are not a TTY (which means the
128 	output is probably being piped and we don't want ASCII escape chars in it)
129 */
130 private shared bool _printColors = true;
131 
132 /// Ditto
133 public bool hasColors () @trusted nothrow @nogc { return _printColors; }
134 
135 // isatty() is used in initLogging() to detect whether or not we are on a TTY
136 extern (C) int isatty(int);
137 
138 /**
139 	This function must be called at the beginning for the program, before any
140 	logging occurs. It will detect whether or not stdout/stderr are a console/TTY
141 	and will consequently disable colored output if needed. Also, if a NO_COLOR
142 	environment variable is defined, colors are disabled (https://no-color.org/).
143 
144 	Forgetting to call the function will result in ASCII escape sequences in the
145 	piped output, probably an undesirable thing.
146 */
147 void initLogging()
148 {
149 	import std.process : environment;
150 	import core.stdc.stdio;
151 
152 	_printColors = environment.get("NO_COLOR") == "";
153 	version (Windows)
154 	{
155 		version (CRuntime_DigitalMars)
156 		{
157 			if (!isatty(core.stdc.stdio.stdout._file) ||
158 					!isatty(core.stdc.stdio.stderr._file))
159 				_printColors = false;
160 		}
161 		else version (CRuntime_Microsoft)
162 		{
163 			if (!isatty(fileno(core.stdc.stdio.stdout)) ||
164 					!isatty(fileno(core.stdc.stdio.stderr)))
165 				_printColors = false;
166 		}
167 		else
168 			_printColors = false;
169 	}
170 	else version (Posix)
171 	{
172 		import core.sys.posix.unistd;
173 
174 		if (!isatty(STDERR_FILENO) || !isatty(STDOUT_FILENO))
175 			_printColors = false;
176 	}
177 }
178 
179 /// Sets the minimum log level to be printed
180 void setLogLevel(LogLevel level) nothrow
181 {
182 	_minLevel = level;
183 }
184 
185 /// Gets the minimum log level to be printed
186 LogLevel getLogLevel()
187 {
188 	return _minLevel;
189 }
190 
191 /// Set whether to print colors or not
192 void setLoggingColorsEnabled(bool enabled)
193 {
194 	_printColors = enabled;
195 }
196 
197 /**
198 	Shorthand function to log a message with debug/diagnostic level, no tag string
199 	or tag color required (since there will be no tag).
200 
201 	Params:
202 		fmt = See http://dlang.org/phobos/std_format.html#format-string
203 		args = Arguments matching the format string
204 */
205 void logDebug(T...)(string fmt, lazy T args) nothrow
206 {
207 	log(LogLevel.debug_, false, "", Color.init, fmt, args);
208 }
209 
210 /// ditto
211 void logDiagnostic(T...)(string fmt, lazy T args) nothrow
212 {
213 	log(LogLevel.diagnostic, false, "", Color.init, fmt, args);
214 }
215 
216 /**
217 	Shorthand function to log a message with info level, with custom tag string
218 	and tag color.
219 
220 	Params:
221 		tag = The string the tag at the beginning of the line should contain
222 		tagColor = The color the tag string should have
223 		fmt = See http://dlang.org/phobos/std_format.html#format-string
224 		args = Arguments matching the format string
225 */
226 void logInfo(T...)(string tag, Color tagColor, string fmt, lazy T args) nothrow
227 {
228 	log(LogLevel.info, false, tag, tagColor, fmt, args);
229 }
230 
231 /**
232 	Shorthand function to log a message with info level, this version prints an
233 	empty tag automatically (which is different from not having a tag - in this
234 	case there will be an indentation of tagWidth chars on the left anyway).
235 
236 	Params:
237 		fmt = See http://dlang.org/phobos/std_format.html#format-string
238 		args = Arguments matching the format string
239 */
240 void logInfo(T...)(string fmt, lazy T args) nothrow if (!is(T[0] : Color))
241 {
242 	log(LogLevel.info, false, "", Color.init, fmt, args);
243 }
244 
245 /**
246 	Shorthand function to log a message with info level, this version doesn't
247 	print a tag at all, it effectively just prints the given string.
248 
249 	Params:
250 		fmt = See http://dlang.org/phobos/std_format.html#format-string
251 		args = Arguments matching the format string
252 */
253 void logInfoNoTag(T...)(string fmt, lazy T args) nothrow if (!is(T[0] : Color))
254 {
255 	log(LogLevel.info, true, "", Color.init, fmt, args);
256 }
257 
258 /**
259 	Shorthand function to log a message with warning level, with custom tag string.
260 	The tag color is fixed to yellow.
261 
262 	Params:
263 		tag = The string the tag at the beginning of the line should contain
264 		fmt = See http://dlang.org/phobos/std_format.html#format-string
265 		args = Arguments matching the format string
266 */
267 void logWarnTag(T...)(string tag, string fmt, lazy T args) nothrow
268 {
269 	log(LogLevel.warn, false, tag, Color.yellow, fmt, args);
270 }
271 
272 /**
273 	Shorthand function to log a message with warning level, using the default
274 	tag "Warning". The tag color is also fixed to yellow.
275 
276 	Params:
277 		fmt = See http://dlang.org/phobos/std_format.html#format-string
278 		args = Arguments matching the format string
279 */
280 void logWarn(T...)(string fmt, lazy T args) nothrow
281 {
282 	log(LogLevel.warn, false, "Warning", Color.yellow, fmt, args);
283 }
284 
285 /**
286 	Shorthand function to log a message with error level, with custom tag string.
287 	The tag color is fixed to red.
288 
289 	Params:
290 		tag = The string the tag at the beginning of the line should contain
291 		fmt = See http://dlang.org/phobos/std_format.html#format-string
292 		args = Arguments matching the format string
293 */
294 void logErrorTag(T...)(string tag, string fmt, lazy T args) nothrow
295 {
296 	log(LogLevel.error, false, tag, Color.red, fmt, args);
297 }
298 
299 /**
300 	Shorthand function to log a message with error level, using the default
301 	tag "Error". The tag color is also fixed to red.
302 
303 	Params:
304 		fmt = See http://dlang.org/phobos/std_format.html#format-string
305 		args = Arguments matching the format string
306 */
307 void logError(T...)(string fmt, lazy T args) nothrow
308 {
309 	log(LogLevel.error, false, "Error", Color.red, fmt, args);
310 }
311 
312 /**
313 	Log a message with the specified log level and with the specified tag string
314 	and color. If the log level is debug or diagnostic, the tag is not printed
315 	thus the tag string and tag color will be ignored. If the log level is error
316 	or warning, the tag will be in bold text. Also the tag can be disabled (for
317 	any log level) by passing true as the second argument.
318 
319 	Params:
320 		level = The log level for the logged message
321 		disableTag = Setting this to true disables the tag, no matter what
322 		tag = The string the tag at the beginning of the line should contain
323 		tagColor = The color the tag string should have
324 		fmt = See http://dlang.org/phobos/std_format.html#format-string
325 		args = Arguments matching the format string
326 */
327 void log(T...)(
328 	LogLevel level,
329 	bool disableTag,
330 	string tag,
331 	Color tagColor,
332 	string fmt,
333 	lazy T args
334 ) nothrow
335 {
336 	if (level < _minLevel)
337 		return;
338 
339 	auto hasTag = true;
340 	if (level <= LogLevel.diagnostic)
341 		hasTag = false;
342 	if (disableTag)
343 		hasTag = false;
344 
345 	auto boldTag = false;
346 	if (level >= LogLevel.warn)
347 		boldTag = true;
348 
349 	try
350 	{
351 		string result = format(fmt, args);
352 
353 		if (hasTag)
354 			result = tag.rightJustify(tagWidth.get, ' ').color(tagColor, boldTag ? Mode.bold : Mode.init) ~ " " ~ result;
355 
356 		import dub.internal.colorize : cwrite;
357 
358 		File output = (level <= LogLevel.info) ? stdout : stderr;
359 
360 		if (output.isOpen)
361 		{
362 			output.cwrite(result, "\n");
363 			output.flush();
364 		}
365 	}
366 	catch (Exception e)
367 	{
368 		debug assert(false, e.msg);
369 	}
370 }
371 
372 /**
373 	Colors the specified string with the specified color. The function is used to
374 	print colored text within a log message. The function also checks whether
375 	color output is enabled or disabled (when not outputting to a TTY) and, in the
376 	last case, just returns the plain string. This allows to use it like so:
377 
378 	logInfo("Tag", Color.green, "My %s log message", "colored".color(Color.red));
379 
380 	without worrying whether or not colored output is enabled or not.
381 
382 	Also a mode can be specified, such as bold/underline/etc...
383 
384 	Params:
385 		str = The string to color
386 		c = The color to apply
387 		m = An optional mode, such as bold/underline/etc...
388 */
389 string color(const string str, const Color c, const Mode m = Mode.init)
390 {
391 	import dub.internal.colorize;
392 
393 	if (_printColors)
394 		return dub.internal.colorize.color(str, c, bg.init, m);
395 	else
396 		return str;
397 }
398 
399 /**
400 	This function is the same as the above one, but just accepts a mode.
401 	It's useful, for instance, when outputting bold text without changing the
402 	color.
403 
404 	Params:
405 		str = The string to color
406 		m = The mode, such as bold/underline/etc...
407 */
408 string color(const string str, const Mode m = Mode.init)
409 {
410 	import dub.internal.colorize;
411 
412 	if (_printColors)
413 		return dub.internal.colorize.color(str, fg.init, bg.init, m);
414 	else
415 		return str;
416 }