From 4c29b57fee845660c00b126941137ec3f091105a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Corentin=20No=C3=ABl?= Date: Wed, 2 Oct 2024 13:07:21 +0200 Subject: [PATCH] Add System Logs view Allows to graphically access the system logs. --- data/gresource.xml | 4 + data/system-logs-symbolic.svg | 41 ++++++++ src/Plug.vala | 1 + src/Utils/SystemdLogModel.vala | 152 +++++++++++++++++++++++++++++ src/Views/OperatingSystemView.vala | 21 +++- src/Widgets/LogsDialog.vala | 68 +++++++++++++ src/meson.build | 5 +- vapi/libsystemd.vapi | 128 ++++++++++++++++++++++++ 8 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 data/system-logs-symbolic.svg create mode 100644 src/Utils/SystemdLogModel.vala create mode 100644 src/Widgets/LogsDialog.vala create mode 100644 vapi/libsystemd.vapi diff --git a/data/gresource.xml b/data/gresource.xml index ddaa79c17..e1c556eb8 100644 --- a/data/gresource.xml +++ b/data/gresource.xml @@ -3,4 +3,8 @@ OperatingSystemView.css + + + system-logs-symbolic.svg + diff --git a/data/system-logs-symbolic.svg b/data/system-logs-symbolic.svg new file mode 100644 index 000000000..3cff47aea --- /dev/null +++ b/data/system-logs-symbolic.svg @@ -0,0 +1,41 @@ + + + + + + + diff --git a/src/Plug.vala b/src/Plug.vala index 98cb7a084..8b148a480 100644 --- a/src/Plug.vala +++ b/src/Plug.vala @@ -51,6 +51,7 @@ public class About.Plug : Switchboard.Plug { public override Gtk.Widget get_widget () { if (main_grid == null) { + Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()).add_resource_path ("/io/elementary/settings/system/icons"); operating_system_view = new OperatingSystemView (); var hardware_view = new HardwareView (); diff --git a/src/Utils/SystemdLogModel.vala b/src/Utils/SystemdLogModel.vala new file mode 100644 index 000000000..5cdf46762 --- /dev/null +++ b/src/Utils/SystemdLogModel.vala @@ -0,0 +1,152 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + */ + +public class About.SystemdLogRow : GLib.Object { + public string origin { get; construct; } + public string message { get; construct; } + + public SystemdLogRow (string origin, string message) { + Object(origin: origin, message: message); + } +} + +public class About.SystemdLogModel : GLib.Object, GLib.ListModel { + private GLib.HashTable cached_rows; + private uint current_line = uint.MAX; + private uint num_entries = 0; + private Systemd.Journal journal; + + construct { + cached_rows = new GLib.HashTable(GLib.direct_hash, GLib.direct_equal); + create_journal (); + load_data.begin (); + } + + private void create_journal () { + int res = Systemd.Journal.open_namespace (out journal, null, LOCAL_ONLY); + if (res != 0) { + critical ("%s", strerror(-res)); + return; + } + + Systemd.Id128 boot_id; + res = Systemd.Id128.boot (out boot_id); + if (res != 0) { + critical ("%s", strerror(-res)); + return; + } + + journal.add_match ("_BOOT_ID=%s".printf(boot_id.str).data); + journal.add_conjunction (); + } + + private uint load_next_entries () { + if (current_line != num_entries) { + int res = journal.seek_head (); + current_line = 0; + if (res != 0) { + critical ("%s", strerror(-res)); + return 0; + } + + if (num_entries > 0) { + res = journal.next_skip (num_entries); + current_line += num_entries; + if (res < 0) { + critical ("%s", strerror(-res)); + return 0; + } + } + } + + int res = journal.next (); + if (res < 0) { + critical ("%s", strerror(-res)); + return 0; + } + + current_line += res; + num_entries += res; + return res; + } + + private async void load_data () { + GLib.Idle.add(() => { + uint added = 0, old_num_entries = 0; + lock (journal) { + old_num_entries = num_entries; + added = load_next_entries (); + } + if (added > 0) { + items_changed (old_num_entries, 0, added); + return Source.CONTINUE; + } else { + return Source.REMOVE; + } + }, GLib.Priority.LOW); + } + + public Object? get_item (uint position) { + About.SystemdLogRow? row = null; + lock (journal) { + row = cached_rows.get (position); + if (row != null) { + return row; + } + + int res = journal.seek_head (); + current_line = 0; + if (res != 0) { + critical ("%s", strerror(-res)); + return null; + } + + res = journal.next (); + if (res < 0) { + critical ("%s", strerror(-res)); + return null; + } + + res = journal.next_skip (position); + current_line += position; + if (res < 0) { + critical ("%s", strerror(-res)); + return null; + } + + unowned uint8[] data; + unowned uint8[] comm_data; + res = journal.get_data ("MESSAGE", out data); + if (res != 0) { + critical ("%s", strerror(-res)); + return null; + } + + res = journal.get_data ("_COMM", out comm_data); + if (res != 0) { + //critical ("%s %s", strerror(-res), (string) data); + comm_data = "_COMM=kernel".data; + } + + row = new About.SystemdLogRow (((string)comm_data).offset ("_COMM=".length), ((string)data).offset("MESSAGE=".length)); + cached_rows.set (position, row); + row.weak_ref((obj) => { + cached_rows.foreach_remove ((key, val) => { + return val == obj; + }); + }); + } + + return (owned)row; + } + + public Type get_item_type () { + return typeof(About.SystemdLogRow); + } + + public uint get_n_items () { + return num_entries; + } +} diff --git a/src/Views/OperatingSystemView.vala b/src/Views/OperatingSystemView.vala index b99d3ddf9..c2ff31fe5 100644 --- a/src/Views/OperatingSystemView.vala +++ b/src/Views/OperatingSystemView.vala @@ -300,6 +300,13 @@ public class About.OperatingSystemView : Gtk.Box { hexpand = true }; + var log_button = new Gtk.Button.from_icon_name ("system-logs-symbolic") { + tooltip_text = _("System logs"), + halign = END, + valign = BASELINE_CENTER, + hexpand = false + }; + var button_grid = new Gtk.Box (HORIZONTAL, 6); button_grid.append (settings_restore_button); button_grid.append (bug_button); @@ -312,10 +319,11 @@ public class About.OperatingSystemView : Gtk.Box { }; software_grid.attach (logo_overlay, 0, 0, 1, 4); software_grid.attach (title, 1, 0); + software_grid.attach (log_button, 2, 0); - software_grid.attach (kernel_version_label, 1, 2); - software_grid.attach (updates_list, 1, 3); - software_grid.attach (links_list, 1, 4); + software_grid.attach (kernel_version_label, 1, 2, 2); + software_grid.attach (updates_list, 1, 3, 2); + software_grid.attach (links_list, 1, 4, 2); var clamp = new Adw.Clamp () { child = software_grid @@ -353,6 +361,13 @@ public class About.OperatingSystemView : Gtk.Box { } }); + log_button.clicked.connect (() => { + var logs_dialog = new LogsDialog () { + transient_for = (Gtk.Window) get_root () + }; + logs_dialog.present (); + }); + links_list.row_activated.connect ((row) => { launch_uri (((LinkRow) row).uri); }); diff --git a/src/Widgets/LogsDialog.vala b/src/Widgets/LogsDialog.vala new file mode 100644 index 000000000..df6231b73 --- /dev/null +++ b/src/Widgets/LogsDialog.vala @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + */ + +public class About.LogsDialog : Granite.Dialog { + + public LogsDialog () { + } + + construct { + title = _("System Logs"); + modal = true; + + var title_label = new Gtk.Label ( + _("System Logs") + ) { + halign = START + }; + title_label.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); + + var log_listbox = new Gtk.ListBox () { + vexpand = true, + selection_mode = NONE + }; + var model = new About.SystemdLogModel (); + log_listbox.bind_model (model, (obj) => { + unowned var row = (About.SystemdLogRow) obj; + + var origin_label = new Gtk.Label (row.origin); + origin_label.add_css_class (Granite.STYLE_CLASS_H4_LABEL); + var message_label = new Gtk.Label (row.message) { + wrap = true, + hexpand = true, + halign = START, + }; + + var box = new Gtk.Box (HORIZONTAL, 6); + box.append (origin_label); + box.append (message_label); + + return box; + }); + + var scrolled = new Gtk.ScrolledWindow () { + child = log_listbox, + hscrollbar_policy = NEVER, + max_content_height = 400, + propagate_natural_height = true + }; + + var frame = new Gtk.Frame (null) { + child = scrolled + }; + + var box = new Gtk.Box (VERTICAL, 12); + box.append (title_label); + box.append (frame); + + get_content_area ().append (box); + + add_button (_("Close"), Gtk.ResponseType.CLOSE); + + response.connect (() => { + close (); + }); + } +} diff --git a/src/meson.build b/src/meson.build index 949033662..70cc660eb 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,6 +5,7 @@ plug_files = files( 'Interfaces/FirmwareClient.vala', 'Interfaces/LoginManager.vala', 'Utils/ARMPartDecoder.vala', + 'Utils' / 'SystemdLogModel.vala', 'Views' / 'DriversView.vala', 'Views/FirmwareReleaseView.vala', 'Views/FirmwareView.vala', @@ -12,7 +13,8 @@ plug_files = files( 'Views/OperatingSystemView.vala', 'Widgets/FirmwareUpdateRow.vala', 'Widgets' / 'DriverRow.vala', - 'Widgets' / 'UpdateDetailsDialog.vala' + 'Widgets' / 'UpdateDetailsDialog.vala', + 'Widgets' / 'LogsDialog.vala', ) switchboard_dep = dependency('switchboard-3') @@ -41,6 +43,7 @@ shared_module( dependency('gtk4'), dependency('libadwaita-1'), dependency('libgtop-2.0'), + dependency('libsystemd'), dependency('packagekit-glib2'), dependency('gudev-1.0'), dependency('udisks2'), diff --git a/vapi/libsystemd.vapi b/vapi/libsystemd.vapi new file mode 100644 index 000000000..35e2fabfe --- /dev/null +++ b/vapi/libsystemd.vapi @@ -0,0 +1,128 @@ +[CCode (lower_case_cprefix = "sd_")] +namespace Systemd { + [Compact, CCode (cname = "sd_journal", cheader_filename = "systemd/sd-journal.h", free_function = "sd_journal_close")] + public class Journal { + [CCode (cname = "int", cprefix = "LOG_", lower_case_cprefix = "sd_journal_", cheader_filename = "systemd/sd-journal.h,syslog.h", has_type_id = false)] + public enum Priority { + EMERG, + ALERT, + CRIT, + ERR, + WARNING, + NOTICE, + INFO, + DEBUG; + + [PrintfFormat] + public int print (string format, ...); + public int printv (string format, va_list ap); + + [CCode (instance_pos = 1.5)] + public int stream_fd (string identifier, bool level_prefix); + [CCode (cname = "_vala_sd_journal_stream")] + public GLib.FileStream? stream (string identifier, bool level_prefix) { + int fd = this.stream_fd (identifier, level_prefix); + return (fd < 0) ? null : GLib.FileStream.fdopen (fd, "w"); + } + } + + public static int send (string format, ...); + public static int sendv (Posix.iovector[] iov); + public static int perror (string message); + + [Flags, CCode (cname = "int", cprefix = "SD_JOURNAL_", has_type_id = false)] + public enum OpenFlags { + LOCAL_ONLY, + RUNTIME_ONLY, + SYSTEM, + CURRENT_USER, + OS_ROOT, + ALL_NAMESPACES, + INCLUDE_DEFAULT_NAMESPACE, + TAKE_DIRECTORY_FD, + ASSUME_IMMUTABLE + } + public static int open (out Systemd.Journal ret, Systemd.Journal.OpenFlags flags); + public static int open_namespace (out Systemd.Journal ret, string? name_space, Systemd.Journal.OpenFlags flags); + public static int open_directory (out Systemd.Journal ret, string path, Systemd.Journal.OpenFlags flags); + public static int open_files (out Systemd.Journal ret, [CCode (array_length = false, array_null_terminated = true)] string[] paths, Systemd.Journal.OpenFlags flags); + + public int previous (); + public int next (); + + public int previous_skip (uint64 skip); + public int next_skip (uint64 skip); + + public int get_realtime_usec (out uint64 ret); + public int get_monotonic_usec (out uint64 ret, out Systemd.Id128 ret_boot_id); + + public int set_data_threshold (size_t sz); + public int get_data_threshold (out size_t sz); + + public int get_data (string field, [CCode (type = "const void**", array_length_type = "size_t")] out unowned uint8[] data); + public int enumerate_data ([CCode (type = "const void**", array_length_type = "size_t")] out unowned uint8[] data); + public void restart_data (); + + public int add_match ([CCode (array_length_type = "size_t")] uint8[] data); + public int add_disjunction (); + public int add_conjunction (); + public void flush_matches (); + + public int seek_head (); + public int seek_tail (); + public int seek_monotonic_usec (Systemd.Id128 boot_id, uint64 usec); + public int seek_realtime_usec (uint64 usec); + public int seek_cursor (string cursor); + + public int get_cursor (out unowned string cursor); + public int test_cursor (string cursor); + + public int get_cutoff_realtime_usec (out uint64 from, out uint64 to); + public int get_cutoff_monotonic_usec (Systemd.Id128 boot_id, out uint64 from, out uint64 to); + + public int get_usage (out uint64 bytes); + + public int query_unique (string field); + public int enumerate_unique ([CCode (type = "const void**", array_length_type = "size_t")] out unowned uint8[] data); + public void restart_unique (); + + public int get_fd (); + public int get_events (); + public int get_timeout (out uint64 timeout_usec); + public int process (); + public int wait (uint64 timeout_usec); + public int reliable_fd (); + + public int get_catalog (out unowned string text); + public int get_catalog_for_message_id (Systemd.Id128 id, out unowned string ret); + } + + [SimpleType, CCode (cname = "sd_id128_t", lower_case_cprefix = "sd_id128_", cheader_filename = "systemd/sd-id128.h", default_value = "SD_ID128_NULL")] + public struct Id128 { + public uint8 bytes[16]; + public uint64 qwords[2]; + + [CCode (cname = "SD_ID128_NULL")] + public const Systemd.Id128 NULL; + + [CCode (cname = "sd_id128_randomize")] + public static int random (out Systemd.Id128 ret); + [CCode (cname = "sd_id128_get_machine")] + public static int machine (out Systemd.Id128 ret); + [CCode (cname = "sd_id128_get_boot")] + public static int boot (out Systemd.Id128 ret); + public static int from_string (string s, out Systemd.Id128 ret); + + [CCode (cname = "_vala_sd_id128_to_string")] + public string to_string () { + return this.str; + } + + public static bool equal (Systemd.Id128 a, Systemd.Id128 b); + + public unowned string str { + [CCode (cname = "SD_ID128_TO_STRING")] + get; + } + } +}