1 /*
2 * Copyright 2019-2026 the Pacemaker project contributors
3 *
4 * The version control history for this file may have further details.
5 *
6 * This source code is licensed under the GNU Lesser General Public License
7 * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
8 */
9
10 #include <crm_internal.h>
11
12 #include <stdarg.h>
13 #include <stdbool.h>
14 #include <stdint.h>
15 #include <stdlib.h>
16
17 #include <crm/crm.h>
18 #include <crm/common/output.h>
19 #include <crm/stonith-ng.h>
20 #include <crm/fencing/internal.h> // stonith__history_description()
21 #include <crm/pengine/internal.h>
22 #include <glib.h>
23 #include <pacemaker-internal.h>
24
25 #include "crm_mon.h"
26
27 #if PCMK__ENABLE_CURSES
28
29 typedef struct {
30 unsigned int len;
31 char *singular_noun;
32 char *plural_noun;
33 } curses_list_data_t;
34
35 typedef struct {
36 GQueue *parent_q;
37 } private_data_t;
38
39 static void
40 free_list_data(gpointer data) {
41 curses_list_data_t *list_data = data;
42
43 free(list_data->singular_noun);
44 free(list_data->plural_noun);
45 free(list_data);
46 }
47
48 static void
49 curses_free_priv(pcmk__output_t *out) {
50 private_data_t *priv = NULL;
51
52 if (out == NULL || out->priv == NULL) {
53 return;
54 }
55
56 priv = out->priv;
57
58 g_queue_free_full(priv->parent_q, free_list_data);
59 g_clear_pointer(&out->priv, free);
60 }
61
62 static bool
63 curses_init(pcmk__output_t *out) {
64 private_data_t *priv = NULL;
65
66 pcmk__assert(out != NULL);
67
68 /* If curses_init was previously called on this output struct, just return. */
69 if (out->priv != NULL) {
70 return true;
71 } else {
72 out->priv = calloc(1, sizeof(private_data_t));
73 if (out->priv == NULL) {
74 return false;
75 }
76
77 priv = out->priv;
78 }
79
80 priv->parent_q = g_queue_new();
81
82 initscr();
83 cbreak();
84 noecho();
85
86 return true;
87 }
88
89 static void
90 curses_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) {
91 pcmk__assert(out != NULL);
92
93 echo();
94 nocbreak();
95 endwin();
96 }
97
98 static void
99 curses_reset(pcmk__output_t *out) {
100 pcmk__assert(out != NULL);
101
102 curses_free_priv(out);
103 curses_init(out);
104 }
105
106 static void
107 curses_subprocess_output(pcmk__output_t *out, int exit_status,
108 const char *proc_stdout, const char *proc_stderr) {
109 pcmk__assert(out != NULL);
110
111 if (proc_stdout != NULL) {
112 printw("%s\n", proc_stdout);
113 }
114
115 if (proc_stderr != NULL) {
116 printw("%s\n", proc_stderr);
117 }
118
119 clrtoeol();
120 refresh();
121 }
122
123 /* curses_version is defined in curses.h, so we can't use that name here.
124 * This function is empty because we create a text object instead of a console
125 * object if version is requested, so this is never called.
126 */
127 static void
128 curses_ver(pcmk__output_t *out)
129 {
130 pcmk__assert(out != NULL);
131 }
132
133 G_GNUC_PRINTF(2, 3)
134 static void
135 curses_error(pcmk__output_t *out, const char *format, ...) {
136 va_list ap;
137
138 pcmk__assert(out != NULL);
139
140 /* Informational output does not get indented, to separate it from other
141 * potentially indented list output.
142 */
143 va_start(ap, format);
144 vw_printw(stdscr, format, ap);
145 va_end(ap);
146
147 /* Add a newline. */
148 addch('\n');
149
150 clrtoeol();
151 refresh();
152 sleep(2);
153 }
154
155 G_GNUC_PRINTF(2, 3)
156 static int
157 curses_info(pcmk__output_t *out, const char *format, ...) {
158 va_list ap;
159
160 pcmk__assert(out != NULL);
161
162 if (out->is_quiet(out)) {
163 return pcmk_rc_no_output;
164 }
165
166 /* Informational output does not get indented, to separate it from other
167 * potentially indented list output.
168 */
169 va_start(ap, format);
170 vw_printw(stdscr, format, ap);
171 va_end(ap);
172
173 /* Add a newline. */
174 addch('\n');
175
176 clrtoeol();
177 refresh();
178 return pcmk_rc_ok;
179 }
180
181 static void
182 curses_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
183 pcmk__assert(out != NULL);
184 curses_indented_printf(out, "%s", buf);
185 }
186
187 G_GNUC_PRINTF(4, 5)
188 static void
189 curses_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun,
190 const char *format, ...) {
191 private_data_t *priv = NULL;
192 curses_list_data_t *new_list = NULL;
193 va_list ap;
194
195 pcmk__assert((out != NULL) && (out->priv != NULL));
196 priv = out->priv;
197
198 /* Empty formats can be used to create a new level of indentation, but without
199 * displaying some sort of list header. In that case we need to not do any of
200 * this stuff. vw_printw will act weird if told to print a NULL.
201 */
202 if (format != NULL) {
203 va_start(ap, format);
204
205 curses_indented_vprintf(out, format, ap);
206 printw(":\n");
207
208 va_end(ap);
209 }
210
211 new_list = pcmk__assert_alloc(1, sizeof(curses_list_data_t));
212 new_list->len = 0;
213 new_list->singular_noun = pcmk__str_copy(singular_noun);
214 new_list->plural_noun = pcmk__str_copy(plural_noun);
215
216 g_queue_push_tail(priv->parent_q, new_list);
217 }
218
219 G_GNUC_PRINTF(3, 4)
220 static void
221 curses_list_item(pcmk__output_t *out, const char *id, const char *format, ...) {
222 va_list ap;
223
224 pcmk__assert(out != NULL);
225
226 va_start(ap, format);
227
228 if (id != NULL) {
229 curses_indented_printf(out, "%s: ", id);
230 vw_printw(stdscr, format, ap);
231 } else {
232 curses_indented_vprintf(out, format, ap);
233 }
234
235 addch('\n');
236 va_end(ap);
237
238 out->increment_list(out);
239 }
240
241 static void
242 curses_increment_list(pcmk__output_t *out) {
243 private_data_t *priv = NULL;
244 gpointer tail;
245
246 pcmk__assert((out != NULL) && (out->priv != NULL));
247 priv = out->priv;
248
249 tail = g_queue_peek_tail(priv->parent_q);
250 pcmk__assert(tail != NULL);
251 ((curses_list_data_t *) tail)->len++;
252 }
253
254 static void
255 curses_end_list(pcmk__output_t *out) {
256 private_data_t *priv = NULL;
257 curses_list_data_t *node = NULL;
258
259 pcmk__assert((out != NULL) && (out->priv != NULL));
260 priv = out->priv;
261
262 node = g_queue_pop_tail(priv->parent_q);
263
264 if (node->singular_noun != NULL && node->plural_noun != NULL) {
265 if (node->len == 1) {
266 curses_indented_printf(out, "%d %s found\n", node->len, node->singular_noun);
267 } else {
268 curses_indented_printf(out, "%d %s found\n", node->len, node->plural_noun);
269 }
270 }
271
272 free_list_data(node);
273 }
274
275 static bool
276 curses_is_quiet(pcmk__output_t *out) {
277 pcmk__assert(out != NULL);
278 return out->quiet;
279 }
280
281 static void
282 curses_spacer(pcmk__output_t *out) {
283 pcmk__assert(out != NULL);
284 addch('\n');
285 }
286
287 static void
288 curses_progress(pcmk__output_t *out, bool end) {
289 pcmk__assert(out != NULL);
290
291 if (end) {
292 printw(".\n");
293 } else {
294 addch('.');
295 }
296 }
297
298 static void
299 curses_prompt(const char *prompt, bool do_echo, char **dest)
300 {
301 int rc = OK;
302
|
(1) Event path: |
Condition "prompt != NULL", taking true branch. |
|
(2) Event path: |
Condition "dest != NULL", taking true branch. |
303 pcmk__assert((prompt != NULL) && (dest != NULL));
304
305 /* This is backwards from the text version of this function on purpose. We
306 * disable echo by default in curses_init, so we need to enable it here if
307 * asked for.
308 */
|
(3) Event path: |
Condition "do_echo", taking true branch. |
309 if (do_echo) {
310 rc = echo();
311 }
312
|
(4) Event path: |
Condition "rc == 0", taking true branch. |
313 if (rc == OK) {
314 printw("%s: ", prompt);
315
|
(5) Event path: |
Condition "*dest != NULL", taking true branch. |
316 if (*dest != NULL) {
317 free(*dest);
318 }
319
320 *dest = pcmk__assert_alloc(1024, sizeof(char));
321 /* On older systems, scanw is defined as taking a char * for its first argument,
322 * while newer systems rightly want a const char *. Accomodate both here due
323 * to building with -Werror.
324 */
325 rc = scanw((NCURSES_CONST char *) "%1023s", *dest);
326 addch('\n');
327 }
328
|
(6) Event path: |
Condition "rc < 1", taking true branch. |
329 if (rc < 1) {
|
CID (unavailable; MK=1e4efbd9b21f4160c81993a5fc5bf089) (#1 of 1): Inconsistent C union access (INCONSISTENT_UNION_ACCESS): |
|
(7) Event assign_union_field: |
The union field "in" of "_pp" is written. |
|
(8) Event inconsistent_union_field_access: |
In "_pp.out", the union field used: "out" is inconsistent with the field most recently stored: "in". |
330 g_clear_pointer(dest, free);
331 }
332
333 if (do_echo) {
334 noecho();
335 }
336 }
337
338 pcmk__output_t *
339 crm_mon_mk_curses_output(char **argv) {
340 pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
341
342 if (retval == NULL) {
343 return NULL;
344 }
345
346 retval->fmt_name = "console";
347 retval->request = pcmk__quote_cmdline(argv);
348
349 retval->init = curses_init;
350 retval->free_priv = curses_free_priv;
351 retval->finish = curses_finish;
352 retval->reset = curses_reset;
353
354 retval->register_message = pcmk__register_message;
355 retval->message = pcmk__call_message;
356
357 retval->subprocess_output = curses_subprocess_output;
358 retval->version = curses_ver;
359 retval->err = curses_error;
360 retval->info = curses_info;
361 retval->transient = curses_info;
362 retval->output_xml = curses_output_xml;
363
364 retval->begin_list = curses_begin_list;
365 retval->list_item = curses_list_item;
366 retval->increment_list = curses_increment_list;
367 retval->end_list = curses_end_list;
368
369 retval->is_quiet = curses_is_quiet;
370 retval->spacer = curses_spacer;
371 retval->progress = curses_progress;
372 retval->prompt = curses_prompt;
373
374 return retval;
375 }
376
377 G_GNUC_PRINTF(2, 0)
378 void
379 curses_formatted_vprintf(pcmk__output_t *out, const char *format, va_list args) {
380 vw_printw(stdscr, format, args);
381
382 clrtoeol();
383 refresh();
384 }
385
386 G_GNUC_PRINTF(2, 3)
387 void
388 curses_formatted_printf(pcmk__output_t *out, const char *format, ...) {
389 va_list ap;
390
391 va_start(ap, format);
392 curses_formatted_vprintf(out, format, ap);
393 va_end(ap);
394 }
395
396 G_GNUC_PRINTF(2, 0)
397 void
398 curses_indented_vprintf(pcmk__output_t *out, const char *format, va_list args) {
399 int level = 0;
400 private_data_t *priv = NULL;
401
402 pcmk__assert((out != NULL) && (out->priv != NULL));
403
404 priv = out->priv;
405
406 level = g_queue_get_length(priv->parent_q);
407
408 for (int i = 0; i < level; i++) {
409 printw(" ");
410 }
411
412 if (level > 0) {
413 printw("* ");
414 }
415
416 curses_formatted_vprintf(out, format, args);
417 }
418
419 G_GNUC_PRINTF(2, 3)
420 void
421 curses_indented_printf(pcmk__output_t *out, const char *format, ...) {
422 va_list ap;
423
424 va_start(ap, format);
425 curses_indented_vprintf(out, format, ap);
426 va_end(ap);
427 }
428
429 PCMK__OUTPUT_ARGS("maint-mode", "uint64_t")
430 static int
431 cluster_maint_mode_console(pcmk__output_t *out, va_list args) {
432 uint64_t flags = va_arg(args, uint64_t);
433
434 if (pcmk__is_set(flags, pcmk__sched_in_maintenance)) {
435 curses_formatted_printf(out, "\n *** Resource management is DISABLED ***\n");
436 curses_formatted_printf(out, " The cluster will not attempt to start, stop or recover services\n");
437 return pcmk_rc_ok;
438 } else if (pcmk__is_set(flags, pcmk__sched_stop_all)) {
439 curses_formatted_printf(out, "\n *** Resource management is DISABLED ***\n");
440 curses_formatted_printf(out, " The cluster will keep all resources stopped\n");
441 return pcmk_rc_ok;
442 } else {
443 return pcmk_rc_no_output;
444 }
445 }
446
447 PCMK__OUTPUT_ARGS("cluster-status", "pcmk_scheduler_t *",
448 "enum pcmk_pacemakerd_state", "crm_exit_t",
449 "stonith_history_t *", "enum pcmk__fence_history", "uint32_t",
450 "uint32_t", "const char *", "GList *", "GList *")
451 static int
452 cluster_status_console(pcmk__output_t *out, va_list args) {
453 int rc = pcmk_rc_no_output;
454
455 clear();
456 rc = pcmk__cluster_status_text(out, args);
457 refresh();
458 return rc;
459 }
460
461 PCMK__OUTPUT_ARGS("stonith-event", "stonith_history_t *", "bool", "bool",
462 "const char *", "uint32_t")
463 static int
464 stonith_event_console(pcmk__output_t *out, va_list args)
465 {
466 stonith_history_t *event = va_arg(args, stonith_history_t *);
467 bool full_history = va_arg(args, int);
468 bool completed_only G_GNUC_UNUSED = va_arg(args, int);
469 const char *succeeded = va_arg(args, const char *);
470 uint32_t show_opts = va_arg(args, uint32_t);
471
472 gchar *desc = stonith__history_description(event, full_history, succeeded,
473 show_opts);
474
475
476 curses_indented_printf(out, "%s\n", desc);
477 g_free(desc);
478 return pcmk_rc_ok;
479 }
480
481 static pcmk__message_entry_t fmt_functions[] = {
482 { "cluster-status", "console", cluster_status_console },
483 { "maint-mode", "console", cluster_maint_mode_console },
484 { "stonith-event", "console", stonith_event_console },
485
486 { NULL, NULL, NULL }
487 };
488
489 #endif
490
491 void
492 crm_mon_register_messages(pcmk__output_t *out) {
493 #if PCMK__ENABLE_CURSES
494 pcmk__register_messages(out, fmt_functions);
495 #endif
496 }
497