]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Add tool to display emergency log message full-screen on boot failure.
authorOMOJOLA JOSHUA <omojolajoshua@gmail.com>
Mon, 19 Jun 2023 14:16:23 +0000 (15:16 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Thu, 3 Aug 2023 23:24:54 +0000 (00:24 +0100)
man/rules/meson.build
man/systemd-bsod.xml [new file with mode: 0644]
src/basic/terminal-util.c
src/basic/terminal-util.h
src/journal/bsod.c [new file with mode: 0644]
src/journal/meson.build
src/shared/qrcode-util.c
src/shared/qrcode-util.h

index a450fedb0902eb999e8f31e3a3ff16201fd5e8bc..f03cde876d74d34bb11122b7048a34f2523fd854 100644 (file)
@@ -902,6 +902,7 @@ manpages = [
   ''],
  ['systemd-boot-random-seed.service', '8', [], 'ENABLE_BOOTLOADER'],
  ['systemd-boot', '7', ['sd-boot'], 'ENABLE_BOOTLOADER'],
+ ['systemd-bsod', '8', [], 'HAVE_QRENCODE'],
  ['systemd-cat', '1', [], ''],
  ['systemd-cgls', '1', [], ''],
  ['systemd-cgtop', '1', [], ''],
diff --git a/man/systemd-bsod.xml b/man/systemd-bsod.xml
new file mode 100644 (file)
index 0000000..7a64f4f
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="systemd-bsod" conditional='HAVE_QRENCODE' xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <refentryinfo>
+    <title>systemd-bsod</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>systemd-bsod</refentrytitle>
+    <manvolnum>8</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>systemd-bsod</refname>
+    <refpurpose>Displays boot-time emergency log message full-screen.</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>systemd-bsod</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para><command>systemd-bsod</command> is used to display a blue screen which contains a message relating to
+        a boot failure, including a QR code which users can scan with their phones to get helpful information
+        about the cause of their boot failure.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>Options</title>
+
+    <para>The following options are understood:</para>
+
+    <variablelist>
+      <xi:include href="standard-options.xml" xpointer="help" />
+      <xi:include href="standard-options.xml" xpointer="version" />
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Exit status</title>
+
+    <para>On success (displaying the journal message successfully), 0 is returned, a non-zero failure code otherwise.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para>
+      <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+    </para>
+  </refsect1>
+
+</refentry>
index d1cfb161de3b0fcd5e4635f38ceb4ea4ad626863..2575a658b16065c41d809c8475a757ab152ec81b 100644 (file)
@@ -1535,3 +1535,18 @@ void get_log_colors(int priority, const char **on, const char **off, const char
                         *highlight = ansi_highlight_red();
         }
 }
+
+int set_terminal_cursor_position(int fd, unsigned int row, unsigned int column) {
+        int r;
+        char cursor_position[STRLEN("\x1B[") + DECIMAL_STR_MAX(int) * 2 + STRLEN(";H") + 1];
+
+        assert(fd >= 0);
+
+        xsprintf(cursor_position, "\x1B[%u;%uH", row, column);
+
+        r = loop_write(fd, cursor_position, SIZE_MAX, /* do_poll = */false);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to set cursor position, ignoring: %m");
+
+        return 0;
+}
index 945deb5c6615f6a72c0105f2abb8ce71b070bef7..2a7d48b95da8af1e03002591203ca7a656622269 100644 (file)
@@ -68,6 +68,9 @@
 #define ANSI_HIGHLIGHT_YELLOW_FALLBACK           "\x1B[0;1;33m"
 #define ANSI_HIGHLIGHT_YELLOW_FALLBACK_UNDERLINE "\x1B[0;1;4;33m"
 
+/* Background colors */
+#define ANSI_BACKGROUND_BLUE "\x1B[44m"
+
 /* Reset/clear ANSI styles */
 #define ANSI_NORMAL "\x1B[0m"
 
@@ -82,6 +85,7 @@
 
 int reset_terminal_fd(int fd, bool switch_to_text);
 int reset_terminal(const char *name);
+int set_terminal_cursor_position(int fd, unsigned int row, unsigned int column);
 
 int open_terminal(const char *name, int mode);
 
diff --git a/src/journal/bsod.c b/src/journal/bsod.c
new file mode 100644 (file)
index 0000000..7d106eb
--- /dev/null
@@ -0,0 +1,271 @@
+/* SPDX-License-Identifier: LPGL-2.1-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+#include <linux/vt.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
+
+#include "sd-id128.h"
+#include "sd-journal.h"
+
+#include "alloc-util.h"
+#include "build.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "io-util.h"
+#include "log.h"
+#include "logs-show.h"
+#include "main-func.h"
+#include "pretty-print.h"
+#include "qrcode-util.h"
+#include "sysctl-util.h"
+#include "terminal-util.h"
+
+static int help(void) {
+        _cleanup_free_ char *link = NULL;
+        int r;
+
+        r = terminal_urlify_man("systemd-bsod", "8", &link);
+        if (r < 0)
+                return log_oom();
+
+        printf("%s\n\n"
+               "%sFilter the journal to fetch the first message from the\n"
+               "current boot with an emergency log level and displays it\n"
+               "as a string and a QR code.\n\n%s"
+               "   -h --help            Show this help\n"
+               "      --version         Show package version\n"
+               "\nSee the %s for details.\n",
+               program_invocation_short_name,
+               ansi_highlight(),
+               ansi_normal(),
+               link);
+
+        return 0;
+}
+
+static int acquire_first_emergency_log_message(char **ret) {
+        _cleanup_(sd_journal_closep) sd_journal *j = NULL;
+        _cleanup_free_ char *message = NULL;
+        const void *d;
+        size_t l;
+        int r;
+
+        assert(ret);
+
+        r = sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY);
+        if (r < 0)
+                return log_error_errno(r, "Failed to open journal: %m");
+
+        r = add_match_this_boot(j, NULL);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to add boot ID filter: %m");
+
+        r = sd_journal_add_match(j, "_UID=0", 0);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to add User ID filter: %m");
+
+        assert_cc(0 == LOG_EMERG);
+        r = sd_journal_add_match(j, "PRIORITY=0", 0);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to add Emergency filter: %m");
+
+        r = sd_journal_seek_head(j);
+        if (r < 0)
+                return log_error_errno(r, "Failed to seek to start of jornal: %m");
+
+        r = sd_journal_next(j);
+        if (r < 0)
+                return log_error_errno(r, "Failed to read next journal entry: %m");
+        if (r == 0) {
+                log_debug("No emergency level entries in the journal");
+                *ret = NULL;
+                return 0;
+        }
+
+        r = sd_journal_get_data(j, "MESSAGE", &d, &l);
+        if (r < 0)
+                return log_error_errno(r, "Failed to read journal message: %m");
+
+        message = memdup_suffix0((const char*)d + STRLEN("MESSAGE="), l - STRLEN("MESSAGE="));
+        if (!message)
+                return log_oom();
+
+        *ret = TAKE_PTR(message);
+
+        return 0;
+}
+
+static int find_next_free_vt(int fd, int *ret_free_vt, int *ret_original_vt) {
+        struct vt_stat terminal_status;
+
+        assert(fd);
+        assert(ret_free_vt);
+        assert(ret_original_vt);
+
+        if (ioctl(fd, VT_GETSTATE, &terminal_status) < 0)
+                return -errno;
+
+        for (size_t i = 0; i < sizeof(terminal_status.v_state) * 8; i++)
+                if ((terminal_status.v_state & (1 << i)) == 0) {
+                        *ret_free_vt = i;
+                        *ret_original_vt = terminal_status.v_active;
+                        return 0;
+                }
+
+        return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "No free VT found: %m");
+}
+
+static int display_emergency_message_fullscreen(const char *message) {
+        int r, free_vt = 0, original_vt = 0;
+        unsigned qr_code_start_row = 1, qr_code_start_column = 1;
+        char tty[STRLEN("/dev/tty") + DECIMAL_STR_MAX(int) + 1];
+        _cleanup_close_ int fd = -EBADF;
+        _cleanup_fclose_ FILE *stream = NULL;
+        char read_character_buffer = '\0';
+        struct winsize w = {
+                .ws_col = 80,
+                .ws_row = 25,
+        };
+
+        assert(message);
+
+        fd = open_terminal("/dev/tty1", O_RDWR|O_NOCTTY|O_CLOEXEC);
+        if (fd < 0)
+                return log_error_errno(fd, "Failed to open tty1: %m");
+
+        r = find_next_free_vt(fd, &free_vt, &original_vt);
+        if (r < 0)
+                return log_error_errno(r, "Failed to find a free VT: %m");
+
+        xsprintf(tty, "/dev/tty%d", free_vt + 1);
+
+        r = open_terminal(tty, O_RDWR|O_NOCTTY|O_CLOEXEC);
+        if (r < 0)
+                return log_error_errno(fd, "Failed to open tty: %m");
+
+        close_and_replace(fd, r);
+
+        if (ioctl(fd, TIOCGWINSZ, &w) < 0)
+                log_warning_errno(errno, "Failed to fetch tty size, ignoring: %m");
+
+        if (ioctl(fd, VT_ACTIVATE, free_vt + 1) < 0)
+                return log_error_errno(errno, "Failed to activate tty: %m");
+
+        r = loop_write(fd, ANSI_BACKGROUND_BLUE ANSI_HOME_CLEAR, SIZE_MAX, /* do_poll = */ false);
+        if (r < 0)
+                log_warning_errno(r, "Failed to clear terminal, ignoring: %m");
+
+        r = set_terminal_cursor_position(fd, 2, 4);
+        if (r < 0)
+                log_warning_errno(r, "Failed to move terminal cursor position, ignoring: %m");
+
+        r = loop_write(fd, "The current boot has failed!", SIZE_MAX, /* do_poll = */false);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to write to terminal: %m");
+
+        qr_code_start_row = w.ws_row * 3U / 5U;
+        qr_code_start_column = w.ws_col * 3U / 4U;
+        r = set_terminal_cursor_position(fd, 4, 4);
+        if (r < 0)
+                log_warning_errno(r, "Failed to move terminal cursor position, ignoring: %m");
+
+        r = loop_write(fd, message, SIZE_MAX, /* do_poll = */false);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to write emergency message to terminal: %m");
+
+        r = fdopen_independent(fd, "r+", &stream);
+        if (r < 0)
+                return log_error_errno(errno, "Failed to open output file: %m");
+
+        r = print_qrcode_full(stream, "Scan the QR code", message, qr_code_start_row, qr_code_start_column, w.ws_col, w.ws_row);
+        if (r < 0)
+                log_warning_errno(r, "QR code could not be printed, ignoring: %m");
+
+        r = set_terminal_cursor_position(fd, w.ws_row - 1, w.ws_col * 2U / 5U);
+        if (r < 0)
+                log_warning_errno(r, "Failed to move terminal cursor position, ignoring: %m");
+
+        r = loop_write(fd, "Press any key to exit...", SIZE_MAX, /* do_poll = */false);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to write to terminal: %m");
+
+        r = read_one_char(stream, &read_character_buffer, USEC_INFINITY, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to read character: %m");
+
+        if (ioctl(fd, VT_ACTIVATE, original_vt) < 0)
+                return log_error_errno(errno, "Failed to switch back to original VT: %m");
+
+        return 0;
+}
+
+static int parse_argv(int argc, char * argv[]) {
+
+        enum {
+                ARG_VERSION = 0x100,
+        };
+
+        static const struct option options[] = {
+                { "help",    no_argument, NULL, 'h'         },
+                { "version", no_argument, NULL, ARG_VERSION },
+                {}
+        };
+
+        int c;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+
+                switch (c) {
+
+                case 'h':
+                        return help();
+
+                case ARG_VERSION:
+                        return version();
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached();
+                }
+        if (optind < argc)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "%s takes no argument.",
+                                       program_invocation_short_name);
+        return 1;
+}
+
+static int run(int argc, char *argv[]) {
+        int r;
+        _cleanup_free_ char *message = NULL;
+
+        log_open();
+        log_parse_environment();
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                return r;
+
+        r = acquire_first_emergency_log_message(&message);
+        if (r < 0)
+                return log_error_errno(r, "Failed to acquire first emergency log message: %m");
+
+        if (!message) {
+                log_debug("No emergency-level entries");
+                return 0;
+        }
+
+        r = display_emergency_message_fullscreen((const char*) message);
+        if (r < 0)
+                return log_error_errno(r, "Failed to display emergency message on terminal: %m");
+
+        return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
index 65adb2ccba48b255151d55027ce4cb629481acfd..3412e273e51a153f95acb8533e1515e92206f874 100644 (file)
@@ -95,6 +95,14 @@ executables += [
                         threads,
                 ],
         },
+        executable_template + {
+                'name' : 'systemd-bsod',
+                'conditions' : ['HAVE_QRENCODE'],
+                'public' : true,
+                'sources' : files('bsod.c'),
+                'link_with' : libshared,
+                'dependencies' : libqrencode,
+        },
         journal_test_template + {
                 'sources' : files('test-journal-append.c'),
                 'type' : 'manual',
index 6d2cf58fa31f23c59c9ff1c38abea2e39db10204..b0dd90acd1a7d9b5a7823ae9b394297b62a6ed90 100644 (file)
@@ -36,60 +36,138 @@ int dlopen_qrencode(void) {
         return r;
 }
 
-static void print_border(FILE *output, unsigned width) {
-        /* Four rows of border */
-        for (unsigned y = 0; y < 4; y += 2) {
-                fputs(ANSI_WHITE_ON_BLACK, output);
+static void print_border(FILE *output, unsigned width, unsigned row, unsigned column) {
+        assert(output);
+        assert(width);
 
-                for (unsigned x = 0; x < 4 + width + 4; x++)
-                        fputs(UNICODE_FULL_BLOCK, output);
+        if (row != UINT_MAX && column != UINT_MAX) {
+                int r, fd;
 
-                fputs(ANSI_NORMAL "\n", output);
+                fd = fileno(output);
+                if (fd < 0)
+                        return (void)log_debug_errno(errno, "Failed to get file descriptor from the file stream: %m");
+
+                r = set_terminal_cursor_position(fd, row, column);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to move terminal cursor position, ignoring: %m");
+
+                /* Four rows of border */
+                for (unsigned y = 0; y < 4; y += 2) {
+                        fputs(ANSI_WHITE_ON_BLACK, output);
+
+                        for (unsigned x = 0; x < 4 + width + 4; x++)
+                                fputs(UNICODE_FULL_BLOCK, output);
+
+                        fputs(ANSI_NORMAL "\n", output);
+                        r = set_terminal_cursor_position(fd, row + 1, column);
+                        if (r < 0)
+                                log_warning_errno(r, "Failed to move terminal cursor position, ignoring: %m");
+                }
+        } else {
+                /* Four rows of border */
+                for (unsigned y = 0; y < 4; y += 2) {
+                        fputs(ANSI_WHITE_ON_BLACK, output);
+
+                        for (unsigned x = 0; x < 4 + width + 4; x++)
+                                fputs(UNICODE_FULL_BLOCK, output);
+
+                        fputs(ANSI_NORMAL "\n", output);
+                }
         }
 }
 
-static void write_qrcode(FILE *output, QRcode *qr) {
+static void write_qrcode(FILE *output, QRcode *qr, unsigned int row, unsigned int column) {
         assert(qr);
 
         if (!output)
                 output = stdout;
 
-        print_border(output, qr->width);
+        print_border(output, qr->width, row, column);
+
+        if (row != UINT_MAX && column != UINT_MAX) {
+                /* After printing two rows of top border, we need to move the cursor down two rows before starting to print the actual QR code */
+                int r, fd, move_down = 2;
+                fd = fileno(output);
+                if (fd < 0)
+                        return (void)log_debug_errno(errno, "Failed to get file descriptor from the file stream: %m");
+
+                r = set_terminal_cursor_position(fd, row + move_down, column);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to move terminal cursor position, ignoring: %m");
+
+                for (unsigned y = 0; y < (unsigned) qr->width; y += 2) {
+                        const uint8_t *row1 = qr->data + qr->width * y;
+                        const uint8_t *row2 = row1 + qr->width;
+
+                        fputs(ANSI_WHITE_ON_BLACK, output);
+
+                        for (unsigned x = 0; x < 4; x++)
+                                fputs(UNICODE_FULL_BLOCK, output);
+
+                        for (unsigned x = 0; x < (unsigned) qr->width; x++) {
+                                bool a, b;
 
-        for (unsigned y = 0; y < (unsigned) qr->width; y += 2) {
-                const uint8_t *row1 = qr->data + qr->width * y;
-                const uint8_t *row2 = row1 + qr->width;
+                                a = row1[x] & 1;
+                                b = (y+1) < (unsigned) qr->width ? (row2[x] & 1) : false;
 
-                fputs(ANSI_WHITE_ON_BLACK, output);
-                for (unsigned x = 0; x < 4; x++)
-                        fputs(UNICODE_FULL_BLOCK, output);
+                                if (a && b)
+                                        fputc(' ', output);
+                                else if (a)
+                                        fputs(UNICODE_LOWER_HALF_BLOCK, output);
+                                else if (b)
+                                        fputs(UNICODE_UPPER_HALF_BLOCK, output);
+                                else
+                                        fputs(UNICODE_FULL_BLOCK, output);
+                        }
+
+                        for (unsigned x = 0; x < 4; x++)
+                                fputs(UNICODE_FULL_BLOCK, output);
+                        r = set_terminal_cursor_position(fd, row + move_down, column);
+                        if (r < 0)
+                                log_warning_errno(r, "Failed to move terminal cursor position, ignoring: %m");
+                        move_down += 1;
+                        fputs(ANSI_NORMAL "\n", output);
+                }
 
-                for (unsigned x = 0; x < (unsigned) qr->width; x++) {
-                        bool a, b;
+                print_border(output, qr->width, row + move_down, column);
+        } else {
 
-                        a = row1[x] & 1;
-                        b = (y+1) < (unsigned) qr->width ? (row2[x] & 1) : false;
+                for (unsigned y = 0; y < (unsigned) qr->width; y += 2) {
+                        const uint8_t *row1 = qr->data + qr->width * y;
+                        const uint8_t *row2 = row1 + qr->width;
 
-                        if (a && b)
-                                fputc(' ', output);
-                        else if (a)
-                                fputs(UNICODE_LOWER_HALF_BLOCK, output);
-                        else if (b)
-                                fputs(UNICODE_UPPER_HALF_BLOCK, output);
-                        else
+                        fputs(ANSI_WHITE_ON_BLACK, output);
+                        for (unsigned x = 0; x < 4; x++)
                                 fputs(UNICODE_FULL_BLOCK, output);
+
+                        for (unsigned x = 0; x < (unsigned) qr->width; x++) {
+                                bool a, b;
+
+                                a = row1[x] & 1;
+                                b = (y+1) < (unsigned) qr->width ? (row2[x] & 1) : false;
+
+                                if (a && b)
+                                        fputc(' ', output);
+                                else if (a)
+                                        fputs(UNICODE_LOWER_HALF_BLOCK, output);
+                                else if (b)
+                                        fputs(UNICODE_UPPER_HALF_BLOCK, output);
+                                else
+                                        fputs(UNICODE_FULL_BLOCK, output);
+                        }
+
+                        for (unsigned x = 0; x < 4; x++)
+                                fputs(UNICODE_FULL_BLOCK, output);
+                        fputs(ANSI_NORMAL "\n", output);
                 }
 
-                for (unsigned x = 0; x < 4; x++)
-                        fputs(UNICODE_FULL_BLOCK, output);
-                fputs(ANSI_NORMAL "\n", output);
+                print_border(output, qr->width, row, column);
         }
 
-        print_border(output, qr->width);
         fflush(output);
 }
 
-int print_qrcode(FILE *out, const char *header, const char *string) {
+int print_qrcode_full(FILE *out, const char *header, const char *string, unsigned row, unsigned column, unsigned tty_width, unsigned tty_height) {
         QRcode* qr;
         int r;
 
@@ -106,10 +184,34 @@ int print_qrcode(FILE *out, const char *header, const char *string) {
         if (!qr)
                 return -ENOMEM;
 
-        if (header)
-                fprintf(out, "\n%s:\n\n", header);
+        if (row != UINT_MAX && column != UINT_MAX) {
+                int fd;
+                unsigned qr_code_width, qr_code_height;
+                fd = fileno(out);
+                if (fd < 0)
+                        return log_debug_errno(errno, "Failed to get file descriptor from the file stream: %m");
+                qr_code_width = qr_code_height = qr->width + 8;
+
+                if (column + qr_code_width > tty_width)
+                        column = tty_width - qr_code_width;
+
+                /* Terminal characters are twice as high as they are wide so it's qr_code_height / 2,
+                * our QR code prints an extra new line, so we have -1 as well */
+                if (row + qr_code_height > tty_height)
+                        row = tty_height - (qr_code_height / 2 ) - 1;
+
+                if (header) {
+                        r = set_terminal_cursor_position(fd, row - 2, tty_width - qr_code_width - 2);
+                        if (r < 0)
+                                log_warning_errno(r, "Failed to move terminal cursor position, ignoring: %m");
+
+                        fprintf(out, "%s:\n\n", header);
+                }
+        } else
+                if (header)
+                        fprintf(out, "\n%s:\n\n", header);
 
-        write_qrcode(out, qr);
+        write_qrcode(out, qr, row, column);
 
         fputc('\n', out);
 
index b64ecce80a3e9590379a034af2458fc98488e6de..ee58294436bf80a91e094a2047d5efdfa60de66f 100644 (file)
@@ -3,12 +3,19 @@
 #pragma once
 #include <stdio.h>
 #include <errno.h>
+#include <limits.h>
 
 #if HAVE_QRENCODE
 int dlopen_qrencode(void);
 
-int print_qrcode(FILE *out, const char *header, const char *string);
+int print_qrcode_full(FILE *out, const char *header, const char *string, unsigned row, unsigned column, unsigned tty_width, unsigned tty_height);
+static inline int print_qrcode(FILE *out, const char *header, const char *string) {
+        return print_qrcode_full(out, header, string, UINT_MAX, UINT_MAX, UINT_MAX, UINT_MAX);
+}
 #else
+static inline int print_qrcode_full(FILE *out, const char *header, const char *string, unsigned row, unsigned column, unsigned tty_width, unsigned tty_height) {
+        return -EOPNOTSUPP;
+}
 static inline int print_qrcode(FILE *out, const char *header, const char *string) {
         return -EOPNOTSUPP;
 }