diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a562216a..a80f6db06 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: - name: Install Dependencies run: | apt update - apt install -y meson libadwaita-1-dev libfwupd-dev libgranite-7-dev libgtk-4-dev libgtop2-dev libgudev-1.0-dev libudisks2-dev libswitchboard-3-dev libappstream-dev libpackagekit-glib2-dev libpolkit-gobject-1-dev libsoup-3.0-dev valac + apt install -y meson libadwaita-1-dev libfwupd-dev libgranite-7-dev libgtk-4-dev libgtop2-dev libgudev-1.0-dev libudisks2-dev libswitchboard-3-dev libappstream-dev libpackagekit-glib2-dev libpolkit-gobject-1-dev libsoup-3.0-dev libsystemd-dev valac - name: Build env: DESTDIR: out 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/LogDialog/LogCell.vala b/src/LogDialog/LogCell.vala new file mode 100644 index 000000000..8e7a2106d --- /dev/null +++ b/src/LogDialog/LogCell.vala @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class About.LogCell : Granite.Bin { + public enum CellType { + ORIGIN, + MESSAGE + } + + public CellType cell_type { get; construct; } + + private Gtk.Label label; + + public LogCell (CellType cell_type) { + Object (cell_type: cell_type); + } + + construct { + label = new Gtk.Label (null) { + ellipsize = END, + single_line_mode = true, + halign = START, + }; + + if (cell_type == ORIGIN) { + label.max_width_chars = 10; + label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + } + + child = label; + } + + public void bind (SystemdLogEntry entry) { + switch (cell_type) { + case ORIGIN: + label.label = entry.origin; + break; + case MESSAGE: + label.label = entry.message; + break; + } + } +} diff --git a/src/LogDialog/LogDetailsView.vala b/src/LogDialog/LogDetailsView.vala new file mode 100644 index 000000000..5e8391b6b --- /dev/null +++ b/src/LogDialog/LogDetailsView.vala @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class About.LogDetailsView : Adw.NavigationPage { + public SystemdLogEntry entry { + set { + origin.label = value.origin; + timestamp.label = value.dt.format ("%s %s".printf ( + Granite.DateTime.get_default_date_format (), + Granite.DateTime.get_default_time_format (false, true) + )); + message.label = value.message; + title = value.origin; + } + } + + private Gtk.Label origin; + private Gtk.Label timestamp; + private Gtk.Label message; + + construct { + var origin_label = new Gtk.Label (_("Sender:")) { + halign = END + }; + origin_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + + origin = new Gtk.Label (null) { + halign = START, + wrap = true, + wrap_mode = WORD_CHAR + }; + + var timestamp_label = new Gtk.Label (_("Timestamp:")) { + halign = END + }; + timestamp_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + + timestamp = new Gtk.Label (null) { + halign = START, + wrap = true, + wrap_mode = WORD_CHAR + }; + + var message_label = new Gtk.Label (_("Message:")) { + halign = END + }; + message_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + + message = new Gtk.Label (null) { + halign = START, + wrap = true, + wrap_mode = WORD_CHAR + }; + + var grid = new Gtk.Grid () { + column_spacing = 3, + row_spacing = 6, + margin_start = 6, + margin_end = 6, + margin_top = 6, + margin_bottom = 6, + halign = CENTER + }; + grid.attach (origin_label, 0, 0); + grid.attach (origin, 1, 0); + grid.attach (timestamp_label, 0, 1); + grid.attach (timestamp, 1, 1); + grid.attach (message_label, 0, 2); + grid.attach (message, 1, 2); + + var scrolled = new Gtk.ScrolledWindow () { + child = grid + }; + + var toolbar_view = new Adw.ToolbarView () { + content = scrolled, + top_bar_style = RAISED + }; + toolbar_view.add_top_bar (new Adw.HeaderBar ()); + + child = toolbar_view; + } +} diff --git a/src/LogDialog/LogDialog.vala b/src/LogDialog/LogDialog.vala new file mode 100644 index 000000000..2edcb6d9a --- /dev/null +++ b/src/LogDialog/LogDialog.vala @@ -0,0 +1,147 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class About.LogDialog : Granite.Dialog { + private SystemdLogModel model; + private LogDetailsView details_view; + private Adw.NavigationView navigation_view; + + construct { + title = _("System Logs"); + modal = true; + default_height = 500; + default_width = 500; + + var title_label = new Gtk.Label ( + _("System Logs") + ) { + hexpand = true, + xalign = 0 + }; + title_label.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); + + var refresh_button = new Gtk.Button.from_icon_name ("view-refresh-symbolic") { + tooltip_text = _("Load new entries") + }; + + var top_box = new Gtk.Box (HORIZONTAL, 6); + top_box.append (title_label); + top_box.append (refresh_button); + + var search_entry = new Gtk.SearchEntry (); + + model = new SystemdLogModel (); + + var selection_model = new Gtk.NoSelection (model); + + var header_factory = new Gtk.SignalListItemFactory (); + header_factory.setup.connect (setup_header); + header_factory.bind.connect (bind_header); + + var origin_factory = new Gtk.SignalListItemFactory (); + origin_factory.setup.connect (setup_origin); + origin_factory.bind.connect (bind); + + var origin_column = new Gtk.ColumnViewColumn (_("Sender"), origin_factory); + + var message_factory = new Gtk.SignalListItemFactory (); + message_factory.setup.connect (setup_message); + message_factory.bind.connect (bind); + + var message_column = new Gtk.ColumnViewColumn (_("Message"), message_factory) { + expand = true + }; + + var column_view = new Gtk.ColumnView (selection_model) { + header_factory = header_factory, + single_click_activate = true, + }; + column_view.append_column (origin_column); + column_view.append_column (message_column); + + var scrolled = new Gtk.ScrolledWindow () { + child = column_view, + hscrollbar_policy = NEVER, + max_content_height = 400, + propagate_natural_height = true + }; + + var list_page = new Adw.NavigationPage (scrolled, "list"); + + details_view = new LogDetailsView (); + + navigation_view = new Adw.NavigationView (); + navigation_view.add (list_page); + + var frame = new Gtk.Frame (null) { + child = navigation_view, + hexpand = true, + vexpand = true + }; + + var box = new Gtk.Box (VERTICAL, 12); + box.append (top_box); + box.append (search_entry); + box.append (frame); + + get_content_area ().append (box); + + add_button (_("Close"), Gtk.ResponseType.CLOSE); + + refresh_button.clicked.connect (model.refresh); + search_entry.search_changed.connect (on_search_changed); + column_view.activate.connect (on_activate); + scrolled.edge_reached.connect (on_edge_reached); + + response.connect (() => close ()); + } + + private void setup_header (Object obj) { + var item = (Gtk.ListHeader) obj; + item.child = new Gtk.Label (null) { + halign = START, + use_markup = true + }; + } + + private void bind_header (Object obj) { + var item = (Gtk.ListHeader) obj; + var entry = (SystemdLogEntry) item.item; + var label = (Gtk.Label) item.child; + label.label = "%s".printf (entry.relative_time); + } + + private void setup_origin (Object obj) { + var item = (Gtk.ListItem) obj; + item.child = new LogCell (ORIGIN); + } + + private void setup_message (Object obj) { + var item = (Gtk.ListItem) obj; + item.child = new LogCell (MESSAGE); + } + + private void bind (Object obj) { + var item = (Gtk.ListItem) obj; + var entry = (SystemdLogEntry) item.item; + var cell = (LogCell) item.child; + cell.bind (entry); + } + + private void on_search_changed (Gtk.SearchEntry entry) { + model.search (entry.text); + } + + private void on_activate (uint pos) { + details_view.entry = (SystemdLogEntry) model.get_item (pos); + navigation_view.push (details_view); + } + + private void on_edge_reached (Gtk.PositionType pos) { + if (pos == BOTTOM) { + model.load_chunk (); + } + } +} diff --git a/src/LogDialog/SystemdLogEntry.vala b/src/LogDialog/SystemdLogEntry.vala new file mode 100644 index 000000000..8f47cad72 --- /dev/null +++ b/src/LogDialog/SystemdLogEntry.vala @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + + public class About.SystemdLogEntry : GLib.Object { + public string origin { get; construct; } + public string message { get; construct; } + public DateTime dt { get; construct; } + public string relative_time { get; construct; } + + public uint section_start { get; set; } + + public SystemdLogEntry (string origin, string message, DateTime time) { + Object ( + origin: origin, message: message, dt: time, + relative_time: format_time (time) + ); + } + + public bool matches (string term) { + return origin.contains (term) || message.contains (term); + } + + private static string format_time (DateTime time) { + var diff = SystemdLogModel.get_stable_now ().difference (time); + if (diff < TimeSpan.SECOND) { + return _("Now"); + } else if (diff < TimeSpan.MINUTE) { + var seconds = diff / TimeSpan.SECOND; + return dngettext (GETTEXT_PACKAGE, "%ds ago", "%ds ago", (ulong) seconds).printf ((int) seconds); + } else if (diff < TimeSpan.HOUR) { + var minutes = diff / TimeSpan.MINUTE; + var seconds = (diff - minutes * TimeSpan.MINUTE) / TimeSpan.SECOND; + + if (seconds == 0) { + return dngettext (GETTEXT_PACKAGE, "%dm ago", "%dm ago", (ulong) minutes).printf ((int) minutes); + } + + // I think the plural form is according to the last one?? + return dngettext (GETTEXT_PACKAGE, "%dm %ds ago", "%dm %ds ago", (ulong) seconds).printf ((int) minutes, (int) seconds); + } + + return time.format (Granite.DateTime.get_default_time_format ()); + } +} diff --git a/src/LogDialog/SystemdLogModel.vala b/src/LogDialog/SystemdLogModel.vala new file mode 100644 index 000000000..d652ecf9f --- /dev/null +++ b/src/LogDialog/SystemdLogModel.vala @@ -0,0 +1,240 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionModel { + private static DateTime? now; + + public static DateTime get_stable_now () { + return now; + } + + private const int CHUNK_SIZE = 200; + private const int64 CHUNK_TIME = 1000; // 1 millisecond + + private Systemd.Journal journal; + private Systemd.Id128 current_boot_id; + private uint64 current_tail_time = 0; + private string current_search_term = ""; + + // These fields are for the listmodel and section model implementation + // and only used for loading and in the implementations + private Gee.ArrayList entries; + private bool eof = false; + private bool loading = false; + private int current_section_start = 0; + private DateTime? current_section_time; + private HashTable section_end_for_start = new HashTable (null, null); + + construct { + entries = new Gee.ArrayList (); + + int res = Systemd.Journal.open_namespace (out journal, null, LOCAL_ONLY); + if (res != 0) { + critical ("%s", strerror (-res)); + return; + } + + res = Systemd.Id128.boot (out current_boot_id); + if (res != 0) { + critical ("%s", strerror (-res)); + return; + } + + init (); + } + + private void reset () { + if (!entries.is_empty) { + var removed = entries.size; + entries.clear (); + items_changed (0, removed, 0); + } + + eof = false; + loading = false; // Cancels ongoing load + current_section_start = 0; + current_section_time = null; + section_end_for_start.remove_all (); + + journal.flush_matches (); + } + + private void init () { + now = new DateTime.now_utc (); + + //TODO: Add exact matches, allow to filter by boot + journal.add_match ("_BOOT_ID=%s".printf (current_boot_id.str).data); + journal.add_conjunction (); + + if (current_tail_time == 0) { + journal.seek_tail (); + journal.previous (); + int res = journal.get_realtime_usec (out current_tail_time); + if (res != 0) { + critical ("Failed to get tail realtime: %s", strerror (-res)); + return; + } + } else { + journal.seek_realtime_usec (current_tail_time); + journal.previous (); + } + + load_chunk (); + } + + public void load_chunk () { + if (eof || loading) { + return; + } + + loading = true; + + var start_items = get_n_items (); + + Idle.add (() => { + if (!loading) { // We were cancelled + return Source.REMOVE; + } + + load_timed (); + loading = !eof && get_n_items () - start_items < CHUNK_SIZE; + return loading ? Source.CONTINUE : Source.REMOVE; + }); + } + + private void load_timed () { + if (eof) { + return; + } + + var start_n_items = entries.size; + var start_time = get_monotonic_time (); + + while (get_monotonic_time () - start_time < CHUNK_TIME) { + if (!load_next_entry ()) { + eof = true; + break; + } + } + + items_changed (start_n_items, 0, entries.size - start_n_items); + } + + private bool load_next_entry () { + int res = journal.previous (); + if (res == 0) { + return false; + } + + if (res < 0) { + critical ("Failed to go to next aka previous entry: %s", strerror (-res)); + return false; + } + + unowned uint8[] data; + unowned uint8[] comm_data; + res = journal.get_data ("MESSAGE", out data); + if (res != 0) { + critical ("Failed to get message: %s", strerror (-res)); + return true; // Don't eof just skip it + } + + res = journal.get_data ("_COMM", out comm_data); + if (res != 0) { + comm_data = "_COMM=kernel".data; + } + + var origin = ((string) comm_data).offset ("_COMM=".length); + var message = ((string) data).offset ("MESSAGE=".length); + + uint64 time; + res = journal.get_realtime_usec (out time); + if (res != 0) { + critical ("Failed to get time: %s", strerror (-res)); + time = 0; + } + + var dt = new DateTime.from_unix_utc ((int64) (time / TimeSpan.SECOND)); + + var entry = new SystemdLogEntry (origin, message, dt); + + // Filter if we're searching. We drop them and don't add them and use a filter model + // because when searching for e.g. a non existent term this would fill up memory *quick* + if (current_search_term.strip () != "" && !entry.matches (current_search_term)) { + return true; + } + + // Update sections (group entries that have a timestamp from the same second) + if (!update_current_range (dt)) { + section_end_for_start[current_section_start] = entries.size; + current_section_start = entries.size; + } + entry.section_start = current_section_start; + + entries.add (entry); + + return true; + } + + private bool update_current_range (DateTime dt) { + if (current_section_time == null) { + current_section_time = dt; + return true; + } + + if (current_section_time.difference (dt) <= TimeSpan.SECOND) { + return true; + } else { + current_section_time = dt; + return false; + } + } + + public void search (string term) { + reset (); + //TODO: tokenize etc. + current_search_term = term; + init (); + } + + public void refresh () { + reset (); + current_tail_time = 0; + init (); + } + + public Object? get_item (uint position) { + if (position >= entries.size) { + return null; + } else { + return entries[(int) position]; + } + } + + public Type get_item_type () { + return typeof (SystemdLogEntry); + } + + public uint get_n_items () { + return entries.size; + } + + public void get_section (uint for_position, out uint section_start, out uint section_end) { + if (for_position >= entries.size) { + // Documentation mandates this + section_start = entries.size; + section_end = uint.MAX; + return; + } + + section_start = entries[(int) for_position].section_start; + + if (section_start in section_end_for_start) { + section_end = section_end_for_start[section_start]; + } else { + section_end = entries.size; + } + } +} diff --git a/src/Plug.vala b/src/Plug.vala index 346f3fba2..ea16f2b23 100644 --- a/src/Plug.vala +++ b/src/Plug.vala @@ -52,6 +52,7 @@ public class About.Plug : Switchboard.Plug { public override Gtk.Widget get_widget () { if (toolbarview == 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/Views/OperatingSystemView.vala b/src/Views/OperatingSystemView.vala index 6a9f62b36..b427ed039 100644 --- a/src/Views/OperatingSystemView.vala +++ b/src/Views/OperatingSystemView.vala @@ -174,6 +174,13 @@ public class About.OperatingSystemView : Gtk.Box { kernel_version_label.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); kernel_version_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + var log_button = new Gtk.Button.from_icon_name ("system-logs-symbolic") { + tooltip_text = _("System logs"), + halign = END, + valign = BASELINE_CENTER, + hexpand = false + }; + packages = new Gtk.StringList (null); updates_image = new Gtk.Image () { @@ -350,11 +357,12 @@ 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 (sponsor_list, 1, 4); - software_grid.attach (links_list, 1, 5); + software_grid.attach (kernel_version_label, 1, 2, 2); + software_grid.attach (updates_list, 1, 3, 2); + software_grid.attach (sponsor_list, 1, 4, 2); + software_grid.attach (links_list, 1, 5, 2); var clamp = new Adw.Clamp () { child = software_grid, @@ -396,6 +404,12 @@ public class About.OperatingSystemView : Gtk.Box { launch_uri (((SponsorUsRow) row).uri); }); + log_button.clicked.connect (() => { + new LogDialog () { + transient_for = (Gtk.Window) get_root () + }.present (); + }); + links_list.row_activated.connect ((row) => { launch_uri (((LinkRow) row).uri); }); diff --git a/src/meson.build b/src/meson.build index 57bd2a4a8..ebc457450 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,11 @@ plug_files = files( 'DBus' / 'Drivers.vala', 'Interfaces/FirmwareClient.vala', 'Interfaces/LoginManager.vala', + 'LogDialog' / 'LogCell.vala', + 'LogDialog' / 'LogDetailsView.vala', + 'LogDialog' / 'LogDialog.vala', + 'LogDialog' / 'SystemdLogEntry.vala', + 'LogDialog' / 'SystemdLogModel.vala', 'Utils/ARMPartDecoder.vala', 'Views' / 'DriversView.vala', 'Views/FirmwareReleaseView.vala', @@ -12,7 +17,7 @@ plug_files = files( 'Views/OperatingSystemView.vala', 'Widgets/FirmwareUpdateRow.vala', 'Widgets' / 'DriverRow.vala', - 'Widgets' / 'UpdateDetailsDialog.vala' + 'Widgets' / 'UpdateDetailsDialog.vala', ) switchboard_dep = dependency('switchboard-3') @@ -42,6 +47,7 @@ shared_module( dependency('libadwaita-1'), dependency('libgtop-2.0'), dependency('libsoup-3.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; + } + } +}