]> git.ipfire.org Git - thirdparty/systemd.git/blame_incremental - src/shared/pretty-print.c
io.systemd.Unit.List fix context/runtime split (#38172)
[thirdparty/systemd.git] / src / shared / pretty-print.c
... / ...
CommitLineData
1/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3#include <math.h>
4#include <stdio.h>
5#include <sys/utsname.h>
6#include <unistd.h>
7
8#include "alloc-util.h"
9#include "chase.h"
10#include "color-util.h"
11#include "conf-files.h"
12#include "constants.h"
13#include "env-util.h"
14#include "errno-util.h"
15#include "fd-util.h"
16#include "fileio.h"
17#include "fs-util.h"
18#include "log.h"
19#include "path-util.h"
20#include "pretty-print.h"
21#include "string-util.h"
22#include "strv.h"
23#include "terminal-util.h"
24#include "utf8.h"
25
26void draw_cylon(char buffer[], size_t buflen, unsigned width, unsigned pos) {
27 char *p = buffer;
28
29 assert(buflen >= CYLON_BUFFER_EXTRA + width + 1);
30 assert(pos <= width+1); /* 0 or width+1 mean that the center light is behind the corner */
31
32 if (pos > 1) {
33 if (pos > 2)
34 p = mempset(p, ' ', pos-2);
35 if (log_get_show_color())
36 p = stpcpy(p, ANSI_RED);
37 *p++ = '*';
38 }
39
40 if (pos > 0 && pos <= width) {
41 if (log_get_show_color())
42 p = stpcpy(p, ANSI_HIGHLIGHT_RED);
43 *p++ = '*';
44 }
45
46 if (log_get_show_color())
47 p = stpcpy(p, ANSI_NORMAL);
48
49 if (pos < width) {
50 if (log_get_show_color())
51 p = stpcpy(p, ANSI_RED);
52 *p++ = '*';
53 if (pos < width-1)
54 p = mempset(p, ' ', width-1-pos);
55 if (log_get_show_color())
56 p = stpcpy(p, ANSI_NORMAL);
57 }
58
59 *p = '\0';
60}
61
62bool urlify_enabled(void) {
63#if ENABLE_URLIFY
64 static int cached_urlify_enabled = -1;
65
66 if (cached_urlify_enabled < 0) {
67 int val;
68
69 val = getenv_bool("SYSTEMD_URLIFY");
70 if (val >= 0)
71 cached_urlify_enabled = val;
72 else
73 cached_urlify_enabled = colors_enabled();
74 }
75
76 return cached_urlify_enabled;
77#else
78 return 0;
79#endif
80}
81
82static bool url_suitable_for_osc8(const char *url) {
83 assert(url);
84
85 /* Not all URLs are safe for inclusion in OSC 8 due to charset and length restrictions. Let's detect
86 * which ones those are */
87
88 /* If the URL is longer than 2K let's not try to do OSC 8. As per recommendation in
89 * https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#length-limits */
90 if (strlen(url) > 2000)
91 return false;
92
93 /* OSC sequences may only contain chars from the 32..126 range, as per ECMA-48 */
94 for (const char *c = url; *c; c++)
95 if (!osc_char_is_valid(*c))
96 return false;
97
98 return true;
99}
100
101int terminal_urlify(const char *url, const char *text, char **ret) {
102 char *n;
103
104 assert(url);
105
106 /* Takes a URL and a pretty string and formats it as clickable link for the terminal. See
107 * https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda for details. */
108
109 if (isempty(text))
110 text = url;
111
112 if (urlify_enabled() && url_suitable_for_osc8(url))
113 n = strjoin(ANSI_OSC "8;;", url, ANSI_ST,
114 text,
115 ANSI_OSC "8;;" ANSI_ST);
116 else
117 n = strdup(text);
118 if (!n)
119 return -ENOMEM;
120
121 *ret = n;
122 return 0;
123}
124
125int file_url_from_path(const char *path, char **ret) {
126 _cleanup_free_ char *absolute = NULL;
127 struct utsname u;
128 char *url = NULL;
129 int r;
130
131 if (uname(&u) < 0)
132 return -errno;
133
134 if (!path_is_absolute(path)) {
135 r = path_make_absolute_cwd(path, &absolute);
136 if (r < 0)
137 return r;
138
139 path = absolute;
140 }
141
142 /* As suggested by https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda, let's include the local
143 * hostname here. Note that we don't use gethostname_malloc() or gethostname_strict() since we are interested
144 * in the raw string the kernel has set, whatever it may be, under the assumption that terminals are not overly
145 * careful with validating the strings either. */
146
147 url = strjoin("file://", u.nodename, path);
148 if (!url)
149 return -ENOMEM;
150
151 *ret = url;
152 return 0;
153}
154
155int terminal_urlify_path(const char *path, const char *text, char **ret) {
156 _cleanup_free_ char *url = NULL;
157 int r;
158
159 assert(path);
160
161 /* Much like terminal_urlify() above, but takes a file system path as input
162 * and turns it into a proper file:// URL first. */
163
164 if (isempty(path))
165 return -EINVAL;
166
167 if (isempty(text))
168 text = path;
169
170 if (!urlify_enabled())
171 return strdup_to(ret, text);
172
173 r = file_url_from_path(path, &url);
174 if (r < 0)
175 return r;
176
177 return terminal_urlify(url, text, ret);
178}
179
180int terminal_urlify_man(const char *page, const char *section, char **ret) {
181 const char *url, *text;
182
183 url = strjoina("man:", page, "(", section, ")");
184 text = strjoina(page, "(", section, ") man page");
185
186 return terminal_urlify(url, text, ret);
187}
188
189static int cat_file(const ConfFile *c, bool *newline, CatFlags flags) {
190 _cleanup_fclose_ FILE *f = NULL;
191 _cleanup_free_ char *urlified = NULL, *section = NULL, *old_section = NULL;
192 int r;
193
194 assert(c);
195 assert(c->original_path);
196 assert(c->resolved_path);
197 assert(c->fd >= 0);
198
199 if (newline) {
200 if (*newline)
201 putc('\n', stdout);
202 *newline = true;
203 }
204
205 bool resolved = !path_equal(c->original_path, c->resolved_path);
206
207 r = terminal_urlify_path(c->resolved_path, NULL, &urlified);
208 if (r < 0)
209 return log_error_errno(r, "Failed to urlify path \"%s\": %m", c->resolved_path);
210
211 printf("%s# %s%s%s%s\n",
212 ansi_highlight_blue(),
213 resolved ? c->original_path : "",
214 resolved ? " -> " : "",
215 urlified,
216 ansi_normal());
217
218 f = fopen(FORMAT_PROC_FD_PATH(c->fd), "re");
219 if (!f)
220 return log_error_errno(errno, "Failed to open \"%s\": %m", c->resolved_path);
221
222 for (bool continued = false;;) {
223 _cleanup_free_ char *line = NULL;
224
225 r = read_line(f, LONG_LINE_MAX, &line);
226 if (r < 0)
227 return log_error_errno(r, "Failed to read \"%s\": %m", c->resolved_path);
228 if (r == 0)
229 break;
230
231 const char *l = skip_leading_chars(line, WHITESPACE);
232
233 /* comment */
234 if (*l != '\0' && strchr(COMMENTS, *l)) {
235 if (!FLAGS_SET(flags, CAT_TLDR))
236 printf("%s%s%s\n", ansi_highlight_grey(), line, ansi_normal());
237 continue;
238 }
239
240 /* empty line */
241 if (FLAGS_SET(flags, CAT_TLDR) && (isempty(l) || streq(l, "\\")))
242 continue;
243
244 /* section */
245 if (FLAGS_SET(flags, CAT_FORMAT_HAS_SECTIONS) && *l == '[' && !continued) {
246 if (FLAGS_SET(flags, CAT_TLDR))
247 /* On TLDR, let's not print it yet. */
248 free_and_replace(section, line);
249 else
250 printf("%s%s%s\n", ansi_highlight_cyan(), line, ansi_normal());
251 continue;
252 }
253
254 /* normal line */
255
256 /* Before we print the line, print the last section header. */
257 if (FLAGS_SET(flags, CAT_TLDR) && section) {
258 /* Do not print redundant section headers */
259 if (!streq_ptr(section, old_section))
260 printf("%s%s%s\n", ansi_highlight_cyan(), section, ansi_normal());
261
262 free_and_replace(old_section, section);
263 }
264
265 /* Check if the line ends with a backslash. */
266 bool escaped = false;
267 char *e;
268 for (e = line; *e != '\0'; e++) {
269 if (escaped)
270 escaped = false;
271 else if (*e == '\\')
272 escaped = true;
273 }
274
275 /* Highlight the trailing backslash. */
276 if (escaped) {
277 assert(e > line);
278 *(e-1) = '\0';
279
280 if (!strextend(&line, ansi_highlight_red(), "\\", ansi_normal()))
281 return log_oom();
282 }
283
284 /* Highlight the left side (directive) of a Foo=bar assignment */
285 if (FLAGS_SET(flags, CAT_FORMAT_HAS_SECTIONS) && !continued) {
286 const char *p = strchr(line, '=');
287 if (p) {
288 _cleanup_free_ char *directive = NULL;
289
290 directive = strndup(line, p - line);
291 if (!directive)
292 return log_oom();
293
294 printf("%s%s=%s%s\n", ansi_highlight_green(), directive, ansi_normal(), p + 1);
295 continued = escaped;
296 continue;
297 }
298 }
299
300 /* Otherwise, print the line as is. */
301 printf("%s\n", line);
302 continued = escaped;
303 }
304
305 return 0;
306}
307
308int cat_files_full(const ConfFile *file, ConfFile * const *dropins, size_t n_dropins, CatFlags flags) {
309 bool newline = false;
310 int ret = 0;
311
312 assert(dropins || n_dropins == 0);
313
314 if (file)
315 ret = cat_file(file, &newline, flags);
316
317 FOREACH_ARRAY(i, dropins, n_dropins)
318 RET_GATHER(ret, cat_file(*i, &newline, flags));
319
320 return ret;
321}
322
323static int cat_file_by_path(const char *p, bool *newline, CatFlags flags) {
324 _cleanup_(conf_file_freep) ConfFile *c = NULL;
325 int r;
326
327 assert(p);
328
329 r = conf_file_new(p, /* root = */ NULL, CHASE_MUST_BE_REGULAR, &c);
330 if (r < 0)
331 return log_error_errno(r, "Failed to chase '%s': %m", p);
332
333 return cat_file(c, newline, flags);
334}
335
336int cat_files(const char *file, char **dropins, CatFlags flags) {
337 bool newline = false;
338 int ret = 0;
339
340 if (file)
341 ret = cat_file_by_path(file, &newline, flags);
342
343 STRV_FOREACH(path, dropins)
344 RET_GATHER(ret, cat_file_by_path(*path, &newline, flags));
345
346 return ret;
347}
348
349void print_separator(void) {
350
351 /* Outputs a separator line that resolves to whitespace when copied from the terminal. We do that by outputting
352 * one line filled with spaces with ANSI underline set, followed by a second (empty) line. */
353
354 if (underline_enabled()) {
355 size_t c = columns();
356
357 flockfile(stdout);
358 fputs_unlocked(ansi_grey_underline(), stdout);
359
360 for (size_t i = 0; i < c; i++)
361 fputc_unlocked(' ', stdout);
362
363 fputs_unlocked(ansi_normal(), stdout);
364 fputs_unlocked("\n\n", stdout);
365 funlockfile(stdout);
366 } else
367 fputs("\n\n", stdout);
368}
369
370static int guess_type(const char **name, char ***ret_prefixes, bool *ret_is_collection, const char **ret_extension) {
371 /* Try to figure out if name is like tmpfiles.d/ or systemd/system-presets/,
372 * i.e. a collection of directories without a main config file.
373 * Incidentally, all those formats don't use sections. So we return a single
374 * is_collection boolean, which also means that the format doesn't use sections.
375 */
376
377 _cleanup_free_ char *n = NULL;
378 bool run = false, coll = false;
379 const char *ext = ".conf";
380 /* This is static so that the array doesn't get deallocated when we exit the function */
381 static const char* const std_prefixes[] = { CONF_PATHS(""), NULL };
382 static const char* const run_prefixes[] = { "/run/", NULL };
383
384 if (path_equal(*name, "environment.d"))
385 /* Special case: we need to include /etc/environment in the search path, even
386 * though the whole concept is called environment.d. */
387 *name = "environment";
388
389 n = strdup(*name);
390 if (!n)
391 return log_oom();
392
393 delete_trailing_chars(n, "/");
394
395 /* We assume systemd-style config files support the /usr-/run-/etc split and dropins. */
396
397 if (endswith(n, ".d"))
398 coll = true;
399
400 if (path_equal(n, "udev/hwdb.d"))
401 ext = ".hwdb";
402 else if (path_equal(n, "udev/rules.d"))
403 ext = ".rules";
404 else if (path_equal(n, "kernel/install.d"))
405 ext = ".install";
406 else if (path_equal(n, "systemd/ntp-units.d")) {
407 coll = true;
408 ext = ".list";
409 } else if (path_equal(n, "systemd/relabel-extra.d")) {
410 coll = run = true;
411 ext = ".relabel";
412 } else if (PATH_IN_SET(n, "systemd/system-preset", "systemd/user-preset", "systemd/initrd-preset")) {
413 coll = true;
414 ext = ".preset";
415 }
416
417 *ret_prefixes = (char**) (run ? run_prefixes : std_prefixes);
418 *ret_is_collection = coll;
419 *ret_extension = ext;
420 return 0;
421}
422
423int conf_files_cat(const char *root, const char *name, CatFlags flags) {
424 _cleanup_strv_free_ char **dirs = NULL;
425 char **prefixes = NULL; /* explicit initialization to appease gcc */
426 bool is_collection;
427 const char *extension;
428 int r;
429
430 r = guess_type(&name, &prefixes, &is_collection, &extension);
431 if (r < 0)
432 return r;
433 assert(prefixes);
434 assert(extension);
435
436 STRV_FOREACH(prefix, prefixes) {
437 assert(endswith(*prefix, "/"));
438 r = strv_extendf(&dirs, "%s%s%s", *prefix, name,
439 is_collection ? "" : ".d");
440 if (r < 0)
441 return log_error_errno(r, "Failed to build directory list: %m");
442 }
443
444 if (DEBUG_LOGGING) {
445 log_debug("Looking for configuration in:");
446 if (!is_collection)
447 STRV_FOREACH(prefix, prefixes)
448 log_debug(" %s%s%s", strempty(root), *prefix, name);
449
450 STRV_FOREACH(t, dirs)
451 log_debug(" %s%s/*%s", strempty(root), *t, extension);
452 }
453
454 /* First locate the main config file, if any */
455 _cleanup_(conf_file_freep) ConfFile *c = NULL;
456 if (!is_collection) {
457 STRV_FOREACH(prefix, prefixes) {
458 _cleanup_free_ char *p = path_join(*prefix, name);
459 if (!p)
460 return log_oom();
461
462 if (conf_file_new(p, root, CHASE_MUST_BE_REGULAR, &c) >= 0)
463 break;
464 }
465
466 if (!c)
467 printf("%s# Main configuration file %s not found%s\n",
468 ansi_highlight_magenta(),
469 name,
470 ansi_normal());
471 }
472
473 /* Then locate the drop-ins, if any */
474 ConfFile **dropins = NULL;
475 size_t n_dropins = 0;
476 CLEANUP_ARRAY(dropins, n_dropins, conf_file_free_many);
477 r = conf_files_list_strv_full(extension, root, CONF_FILES_REGULAR | CONF_FILES_FILTER_MASKED, (const char* const*) dirs, &dropins, &n_dropins);
478 if (r < 0)
479 return log_error_errno(r, "Failed to query file list: %m");
480
481 /* Show */
482 if (is_collection)
483 flags |= CAT_FORMAT_HAS_SECTIONS;
484
485 return cat_files_full(c, dropins, n_dropins, flags);
486}
487
488int terminal_tint_color(double hue, char **ret) {
489 double red, green, blue;
490 int r;
491
492 assert(ret);
493
494 r = get_default_background_color(&red, &green, &blue);
495 if (r < 0)
496 return log_debug_errno(r, "Unable to get terminal background color: %m");
497
498 double s, v;
499 rgb_to_hsv(red, green, blue, /* ret_h= */ NULL, &s, &v);
500
501 if (v > 50) /* If the background is bright, then pull down saturation */
502 s = 25;
503 else /* otherwise pump it up */
504 s = 75;
505
506 v = MAX(20, v); /* Make sure we don't hide the color in black */
507
508 uint8_t r8, g8, b8;
509 hsv_to_rgb(hue, s, v, &r8, &g8, &b8);
510
511 if (asprintf(ret, "48;2;%u;%u;%u", r8, g8, b8) < 0)
512 return -ENOMEM;
513
514 return 0;
515}
516
517bool shall_tint_background(void) {
518 static int cache = -1;
519
520 if (cache >= 0)
521 return cache;
522
523 cache = getenv_bool("SYSTEMD_TINT_BACKGROUND");
524 if (cache == -ENXIO)
525 return (cache = true);
526 if (cache < 0)
527 log_debug_errno(cache, "Failed to parse $SYSTEMD_TINT_BACKGROUND, leaving background tinting enabled: %m");
528
529 return cache != 0;
530}
531
532void draw_progress_bar_unbuffered(const char *prefix, double percentage) {
533 fputc('\r', stderr);
534 if (prefix) {
535 fputs(prefix, stderr);
536 fputc(' ', stderr);
537 }
538
539 if (!terminal_is_dumb()) {
540 /* Generate the Windows Terminal progress indication OSC sequence here. Most Linux terminals currently
541 * ignore this. But let's hope this changes one day. For details about this OSC sequence, see:
542 *
543 * https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
544 * https://github.com/microsoft/terminal/pull/8055
545 */
546 fprintf(stderr, ANSI_OSC "9;4;1;%u" ANSI_ST, (unsigned) ceil(percentage));
547
548 size_t cols = columns();
549 size_t prefix_width = utf8_console_width(prefix) + 1 /* space */;
550 size_t length = cols > prefix_width + 6 ? cols - prefix_width - 6 : 0;
551
552 if (length > 5 && percentage >= 0.0 && percentage <= 100.0) {
553 size_t p = (size_t) (length * percentage / 100.0);
554 bool separator_done = false;
555
556 fputs(ansi_highlight_green(), stderr);
557
558 for (size_t i = 0; i < length; i++) {
559
560 if (i <= p) {
561 if (get_color_mode() == COLOR_24BIT) {
562 uint8_t r8, g8, b8;
563 double z = i == 0 ? 0 : (((double) i / p) * 100);
564 hsv_to_rgb(145 /* green */, z, 33 + z*2/3, &r8, &g8, &b8);
565 fprintf(stderr, "\x1B[38;2;%u;%u;%um", r8, g8, b8);
566 }
567
568 fputs(glyph(GLYPH_HORIZONTAL_FAT), stderr);
569 } else if (i+1 < length && !separator_done) {
570 fputs(ansi_normal(), stderr);
571 fputc(' ', stderr);
572 separator_done = true;
573 fputs(ansi_grey(), stderr);
574 } else
575 fputs(glyph(GLYPH_HORIZONTAL_DOTTED), stderr);
576 }
577
578 fputs(ansi_normal(), stderr);
579 fputc(' ', stderr);
580 }
581 }
582
583 fprintf(stderr,
584 "%s%3.0f%%%s",
585 ansi_highlight(),
586 percentage,
587 ansi_normal());
588
589 if (!terminal_is_dumb())
590 fputs(ANSI_ERASE_TO_END_OF_LINE, stderr);
591
592 fputc('\r', stderr);
593}
594
595void clear_progress_bar_unbuffered(const char *prefix) {
596 fputc('\r', stderr);
597
598 if (terminal_is_dumb())
599 fputs(strrepa(" ",
600 prefix ? utf8_console_width(prefix) + 5 : /* %3.0f%% (4 chars) + space */
601 LESS_BY(columns(), 1U)),
602 stderr);
603 else
604 /* Undo Windows Terminal progress indication again. */
605 fputs(ANSI_OSC "9;4;0;" ANSI_ST
606 ANSI_ERASE_TO_END_OF_LINE, stderr);
607
608 fputc('\r', stderr);
609}
610
611void draw_progress_bar(const char *prefix, double percentage) {
612 /* We are going output a bunch of small strings that shall appear as a single line to STDERR which is
613 * unbuffered by default. Let's temporarily turn on full buffering, so that this is passed to the tty
614 * as a single buffer, to make things more efficient. */
615 WITH_BUFFERED_STDERR;
616 draw_progress_bar_unbuffered(prefix, percentage);
617}
618
619int draw_progress_barf(double percentage, const char *prefixf, ...) {
620 _cleanup_free_ char *s = NULL;
621 va_list ap;
622 int r;
623
624 va_start(ap, prefixf);
625 r = vasprintf(&s, prefixf, ap);
626 va_end(ap);
627
628 if (r < 0)
629 return -ENOMEM;
630
631 draw_progress_bar(s, percentage);
632 return 0;
633}
634
635void clear_progress_bar(const char *prefix) {
636 WITH_BUFFERED_STDERR;
637 clear_progress_bar_unbuffered(prefix);
638}