From afc87ad1e5d22040304d5dc9608362927936dfec Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Fri, 21 Sep 2018 19:58:28 -0400 Subject: [PATCH] Add a history command The history command pulls the transaction log entries out of the journal, and presents them nicely. We use the sd-journal api for this, so we need to link against libsystemd now, but we make the dependency optional. If libsystemd is not available, the history command will simply print an error. --- app/Makefile.am.inc | 5 +- app/flatpak-builtins-history.c | 488 +++++++++++++++++++++++++++++++++ app/flatpak-builtins.h | 1 + app/flatpak-main.c | 1 + configure.ac | 5 + doc/Makefile.am | 7 +- doc/flatpak-docs.xml.in | 1 + doc/flatpak-history.xml | 322 ++++++++++++++++++++++ doc/flatpak.xml | 7 + 9 files changed, 832 insertions(+), 5 deletions(-) create mode 100644 app/flatpak-builtins-history.c create mode 100644 doc/flatpak-history.xml diff --git a/app/Makefile.am.inc b/app/Makefile.am.inc index 0a25a6e4f..d1d6b2210 100644 --- a/app/Makefile.am.inc +++ b/app/Makefile.am.inc @@ -62,6 +62,7 @@ flatpak_SOURCES = \ app/flatpak-builtins-repair.c \ app/flatpak-builtins-create-usb.c \ app/flatpak-builtins-kill.c \ + app/flatpak-builtins-history.c \ app/flatpak-table-printer.c \ app/flatpak-table-printer.h \ app/flatpak-complete.c \ @@ -82,9 +83,9 @@ app/parse-datetime.c: app/parse-datetime.y Makefile BUILT_SOURCES += $(flatpak_dbus_built_sources) CLEANFILES += app/parse-datetime.c $(flatpak_dbus_built_sources) -flatpak_LDADD = $(AM_LDADD) $(BASE_LIBS) $(OSTREE_LIBS) $(SOUP_LIBS) $(JSON_LIBS) $(APPSTREAM_GLIB_LIBS) \ +flatpak_LDADD = $(AM_LDADD) $(BASE_LIBS) $(OSTREE_LIBS) $(SOUP_LIBS) $(JSON_LIBS) $(APPSTREAM_GLIB_LIBS) $(SYSTEMD_LIBS) \ libglnx.la libflatpak-common.la -flatpak_CFLAGS = $(AM_CFLAGS) $(BASE_CFLAGS) $(OSTREE_CFLAGS) $(SOUP_CFLAGS) $(JSON_CFLAGS) $(APPSTREAM_GLIB_CFLAGS) \ +flatpak_CFLAGS = $(AM_CFLAGS) $(BASE_CFLAGS) $(OSTREE_CFLAGS) $(SOUP_CFLAGS) $(JSON_CFLAGS) $(APPSTREAM_GLIB_CFLAGS) $(SYSTEMD_CFLAGS) \ -DFLATPAK_COMPILATION \ -I$(srcdir)/app \ -I$(builddir)/app \ diff --git a/app/flatpak-builtins-history.c b/app/flatpak-builtins-history.c new file mode 100644 index 000000000..e7146cab2 --- /dev/null +++ b/app/flatpak-builtins-history.c @@ -0,0 +1,488 @@ +/* + * Copyright © 2018 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "libglnx/libglnx.h" + +#ifdef HAVE_LIBSYSTEMD +#include +#endif + +#include "flatpak-builtins.h" +#include "flatpak-builtins-utils.h" +#include "flatpak-utils-private.h" +#include "flatpak-table-printer.h" + +static char *opt_since; +static char *opt_until; +static gboolean opt_reverse; +static const char **opt_cols; + +static GOptionEntry options[] = { + { "since", 0, 0, G_OPTION_ARG_STRING, &opt_since, N_("Only show changes after TIME"), N_("TIME") }, + { "until", 0, 0, G_OPTION_ARG_STRING, &opt_until, N_("Only show changes before TIME"), N_("TIME") }, + { "reverse", 0, 0, G_OPTION_ARG_NONE, &opt_reverse, N_("Show newest entries first"), NULL }, + { "columns", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_cols, N_("What information to show"), N_("FIELD,…") }, + { NULL } +}; + +static Column all_columns[] = { + { "time", N_("Time"), N_("Show when the change happend"), 1, 1 }, + { "change", N_("Change"), N_("Show the kind of change"), 1, 1 }, + { "ref", N_("Ref"), N_("Show the ref"), 0, 0 }, + { "application", N_("Application"), N_("Show the application/runtime ID"), 1, 1 }, + { "arch", N_("Architecture"), N_("Show the architecture"), 1, 0 }, + { "branch", N_("Branch"), N_("Show the branch"), 1, 1 }, + { "installation", N_("Installation"), N_("Show the affected installation"), 1, 1 }, + { "remote", N_("Remote"), N_("Show the remote"), 1, 1 }, + { "commit", N_("Commit"), N_("Show the current commit"), 1, 0 }, + { "old-commit", N_("Old Commit"), N_("Show the previous commit"), 1, 0 }, + { "url", N_("URL"), N_("Show the remote URL"), 1, 0 }, + { "user", N_("User"), N_("Show the user doing the change"), 1, 0 }, + { "tool", N_("Tool"), N_("Show the tool that was used"), 1, 0 }, + { "version", N_("Version"), N_("Show the Flatpak version"), 1, 0 }, + { NULL } +}; + +#ifdef HAVE_LIBSYSTEMD + +static char * +get_field (sd_journal *j, + const char *name, + GError **error) +{ + const char *data; + gsize len; + int r; + + if ((r = sd_journal_get_data (j, name, (const void **)&data, &len)) < 0) + { + if (r != -ENOENT) + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Failed to get journal data (%s): %s"), + name, strerror (-r)); + + return NULL; + } + + return g_strndup (data + strlen (name) + 1, len - (strlen (name) + 1)); +} + +static GDateTime * +get_time (sd_journal *j, + GError **error) +{ + g_autofree char *value = NULL; + GError *local_error = NULL; + gint64 t; + + value = get_field (j, "_SOURCE_REALTIME_TIMESTAMP", &local_error); + + if (local_error) + { + g_propagate_error (error, local_error); + return NULL; + } + + t = g_ascii_strtoll (value, NULL, 10) / 1000000; + return g_date_time_new_from_unix_local (t); +} + +static gboolean +print_history (GPtrArray *dirs, + Column *columns, + GDateTime *since, + GDateTime *until, + gboolean reverse, + GCancellable *cancellable, + GError **error) +{ + FlatpakTablePrinter *printer; + sd_journal *j; + int r; + int i; + int k; + int ret; + + if (columns[0].name == NULL) + return TRUE; + + printer = flatpak_table_printer_new (); + for (i = 0; columns[i].name; i++) + flatpak_table_printer_set_column_title (printer, i, _(columns[i].title)); + + if ((r = sd_journal_open (&j, 0)) < 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Failed to open journal: %s"), strerror (-r)); + return FALSE; + } + + if ((r = sd_journal_add_match (j, "MESSAGE_ID=" FLATPAK_MESSAGE_ID, 0)) < 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Failed to add match to journal: %s"), strerror (-r)); + return FALSE; + } + + if (reverse) + ret = sd_journal_seek_tail (j); + else + ret = sd_journal_seek_head (j); + if (ret == 0) + while ((reverse && sd_journal_previous (j) > 0) || + (!reverse && sd_journal_next (j) > 0)) + { + /* determine whether to skip this entry */ + + if (dirs) + { + gboolean include = FALSE; + g_autofree char *installation = get_field (j, "INSTALLATION", NULL); + + if (installation && installation[0] == '/') + include = TRUE; /* pull to a temp repo */ + + for (i = 0; i < dirs->len && !include; i++) + { + g_autofree char *name = flatpak_dir_get_name (dirs->pdata[i]); + if (g_strcmp0 (name, installation) == 0) + include = TRUE; + } + if (!include) + continue; + } + + if (since || until) + { + g_autoptr(GDateTime) time = get_time (j, NULL); + + if (since && time && g_date_time_difference (since, time) >= 0) + continue; + + if (until && time && g_date_time_difference (until, time) <= 0) + continue; + } + + for (k = 0; columns[k].name; k++) + { + if (strcmp (columns[k].name, "time") == 0) + { + g_autoptr(GDateTime) time = NULL; + g_autofree char *s = NULL; + + time = get_time (j, error); + if (*error) + return FALSE; + + s = g_date_time_format (time, "%b %e %T"); + flatpak_table_printer_add_column (printer, s); + } + else if (strcmp (columns[k].name, "change") == 0) + { + g_autofree char *op = get_field (j, "OPERATION", error); + if (*error) + return FALSE; + flatpak_table_printer_add_column (printer, op); + } + else if (strcmp (columns[k].name, "ref") == 0 || + strcmp (columns[k].name, "application") == 0 || + strcmp (columns[k].name, "arch") == 0 || + strcmp (columns[k].name, "branch") == 0) + { + g_autofree char *ref = get_field (j, "REF", error); + if (*error) + return FALSE; + if (strcmp (columns[k].name, "ref") == 0) + flatpak_table_printer_add_column (printer, ref); + else + { + g_auto(GStrv) pref = flatpak_decompose_ref (ref, NULL); + if (strcmp (columns[k].name, "application") == 0) + flatpak_table_printer_add_column (printer, pref ? pref[1] : ""); + else if (strcmp (columns[k].name, "arch") == 0) + flatpak_table_printer_add_column (printer, pref ? pref[2] : ""); + else + flatpak_table_printer_add_column (printer, pref ? pref[3] : ""); + } + } + else if (strcmp (columns[k].name, "installation") == 0) + { + g_autofree char *installation = get_field (j, "INSTALLATION", error); + if (*error) + return FALSE; + flatpak_table_printer_add_column (printer, installation); + } + else if (strcmp (columns[k].name, "remote") == 0) + { + g_autofree char *remote = get_field (j, "REMOTE", error); + if (*error) + return FALSE; + flatpak_table_printer_add_column (printer, remote); + } + else if (strcmp (columns[k].name, "commit") == 0) + { + g_autofree char *commit = get_field (j, "COMMIT", error); + if (*error) + return FALSE; + flatpak_table_printer_add_column_len (printer, commit, 12); + } + else if (strcmp (columns[k].name, "old-commit") == 0) + { + g_autofree char *old_commit = get_field (j, "OLD_COMMIT", error); + if (*error) + return FALSE; + flatpak_table_printer_add_column_len (printer, old_commit, 12); + } + else if (strcmp (columns[k].name, "url") == 0) + { + g_autofree char *url = get_field (j, "URL", error); + if (*error) + return FALSE; + flatpak_table_printer_add_column (printer, url); + } + else if (strcmp (columns[k].name, "user") == 0) + { + g_autofree char *id = get_field (j, "_UID", error); + g_autofree char *oid = NULL; + int uid; + struct passwd *pwd; + + if (*error) + return FALSE; + + uid = g_ascii_strtoll (id, NULL, 10); + pwd = getpwuid (uid); + if (pwd) + { + g_free (id); + id = g_strdup (pwd->pw_name); + } + + oid = get_field (j, "OBJECT_UID", NULL); + if (oid) + { + /* flatpak-system-helper acting on behalf of sb else */ + g_autofree char *str = NULL; + uid = g_ascii_strtoll (oid, NULL, 10); + pwd = getpwuid (uid); + str = g_strdup_printf ("%s (%s)", id, pwd ? pwd->pw_name : oid); + flatpak_table_printer_add_column (printer, str); + } + else + flatpak_table_printer_add_column (printer, id); + } + else if (strcmp (columns[k].name, "tool") == 0) + { + g_autofree char *exe = get_field (j, "_EXE", error); + g_autofree char *oexe = NULL; + g_autofree char *tool = NULL; + if (*error) + return FALSE; + tool = g_path_get_basename (exe); + oexe = get_field (j, "OBJECT_EXE", NULL); + if (oexe) + { + /* flatpak-system-helper acting on behalf of sb else */ + g_autofree char *otool = NULL; + g_autofree char *str = NULL; + + otool = g_path_get_basename (oexe); + str = g_strdup_printf ("%s (%s)", tool, otool); + flatpak_table_printer_add_column (printer, str); + } + else + flatpak_table_printer_add_column (printer, tool); + } + else if (strcmp (columns[k].name, "version") == 0) + { + g_autofree char *version = get_field (j, "FLATPAK_VERSION", error); + if (*error) + return FALSE; + flatpak_table_printer_add_column (printer, version); + } + } + + flatpak_table_printer_finish_row (printer); + } + + flatpak_table_printer_print (printer); + flatpak_table_printer_free (printer); + + sd_journal_close (j); + + return TRUE; +} + +#else + +static gboolean +print_history (GPtrArray *dirs, + Column *columns, + GDateTime *since, + GDateTime *until, + gboolean reverse, + GCancellable *cancellable, + GError **error) +{ + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "history not available without libsystemd"); + return FALSE; +} + +#endif + +static GDateTime * +parse_time (const char *opt_since) +{ + g_autoptr (GDateTime) now = NULL; + g_auto(GStrv) parts = NULL; + int i; + int days = 0; + int hours = 0; + int minutes = 0; + int seconds = 0; + const char *fmts[] = { + "%H:%M", + "%H:%M:%S", + "%Y-%m-%d", + "%Y-%m-%d %H:%M:%S" + }; + + now = g_date_time_new_now_local (); + + for (i = 0; i < G_N_ELEMENTS(fmts); i++) + { + const char *rest; + struct tm tm; + + tm.tm_year = g_date_time_get_year (now); + tm.tm_mon = g_date_time_get_month (now); + tm.tm_mday = g_date_time_get_day_of_month (now); + tm.tm_hour = 0; + tm.tm_min = 0; + tm.tm_sec = 0; + + rest = strptime (opt_since, fmts[i], &tm); + if (rest && *rest == '\0') + return g_date_time_new_local (tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); + } + + parts = g_strsplit (opt_since, " ", -1); + + for (i = 0; parts[i]; i++) + { + gint64 n; + char *end; + + n = g_ascii_strtoll (parts[i], &end, 10); + if (g_strcmp0 (end, "d") == 0 || + g_strcmp0 (end, "day") == 0 || + g_strcmp0 (end, "days") == 0) + days = (int) n; + else if (g_strcmp0 (end, "h") == 0 || + g_strcmp0 (end, "hour") == 0 || + g_strcmp0 (end, "hours") == 0) + hours = (int) n; + else if (g_strcmp0 (end, "m") == 0 || + g_strcmp0 (end, "minute") == 0 || + g_strcmp0 (end, "minutes") == 0) + minutes = (int) n; + else if (g_strcmp0 (end, "s") == 0 || + g_strcmp0 (end, "second") == 0 || + g_strcmp0 (end, "seconds") == 0) + seconds = (int) n; + else + return NULL; + } + + return g_date_time_add_full (now, 0, 0, -days, -hours, -minutes, -seconds); +} + +gboolean +flatpak_builtin_history (int argc, char **argv, GCancellable *cancellable, GError **error) +{ + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GPtrArray) dirs = NULL; + g_autoptr(GDateTime) since = NULL; + g_autoptr(GDateTime) until = NULL; + g_autofree char *col_help = NULL; + g_autofree Column *columns = NULL; + + context = g_option_context_new (_(" - Show history")); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + col_help = column_help (all_columns); + g_option_context_set_description (context, col_help); + + if (!flatpak_option_context_parse (context, options, &argc, &argv, + FLATPAK_BUILTIN_FLAG_ALL_DIRS | FLATPAK_BUILTIN_FLAG_OPTIONAL_REPO, + &dirs, cancellable, error)) + return FALSE; + + if (argc > 1) + return usage_error (context, _("Too many arguments"), error); + + if (opt_since) + { + since = parse_time (opt_since); + if (!since) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("Failed to parse the --since option")); + return FALSE; + } + } + + if (opt_until) + { + until = parse_time (opt_until); + if (!until) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("Failed to parse the --until option")); + return FALSE; + } + } + columns = handle_column_args (all_columns, FALSE, opt_cols, error); + if (columns == NULL) + return FALSE; + + if (!print_history (dirs, columns, since, until, opt_reverse, cancellable, error)) + return FALSE; + + return TRUE; +} + +gboolean +flatpak_complete_history (FlatpakCompletion *completion) +{ + flatpak_complete_options (completion, global_entries); + flatpak_complete_options (completion, options); + return TRUE; +} diff --git a/app/flatpak-builtins.h b/app/flatpak-builtins.h index 7c81474f4..e264dcaf0 100644 --- a/app/flatpak-builtins.h +++ b/app/flatpak-builtins.h @@ -99,6 +99,7 @@ BUILTINPROTO (search) BUILTINPROTO (repair) BUILTINPROTO (create_usb) BUILTINPROTO (kill) +BUILTINPROTO (history) #undef BUILTINPROTO diff --git a/app/flatpak-main.c b/app/flatpak-main.c index 0419d20f3..3dcfa8e42 100644 --- a/app/flatpak-main.c +++ b/app/flatpak-main.c @@ -68,6 +68,7 @@ static FlatpakCommand commands[] = { { "remove", NULL, flatpak_builtin_uninstall, flatpak_complete_uninstall, TRUE }, { "list", N_("List installed apps and/or runtimes"), flatpak_builtin_list, flatpak_complete_list }, { "info", N_("Show info for installed app or runtime"), flatpak_builtin_info, flatpak_complete_info }, + { "history", N_("Show history"), flatpak_builtin_history, flatpak_complete_history }, { "config", N_("Configure flatpak"), flatpak_builtin_config, flatpak_complete_config }, { "repair", N_("Repair flatpak installation"), flatpak_builtin_repair, flatpak_complete_repair }, { "create-usb", N_("Put apps and/or runtimes onto removable media"), flatpak_builtin_create_usb, flatpak_complete_create_usb }, diff --git a/configure.ac b/configure.ac index 671287c73..78c98edc6 100644 --- a/configure.ac +++ b/configure.ac @@ -203,6 +203,10 @@ POLKIT_GOBJECT_REQUIRED=0.98 PKG_CHECK_MODULES(BASE, [glib-2.0 >= $GLIB_REQS gio-2.0 gio-unix-2.0 libarchive >= 2.8.0 libxml-2.0 >= 2.4 ]) PKG_CHECK_MODULES(SOUP, [libsoup-2.4]) +PKG_CHECK_MODULES(SYSTEMD, [libsystemd], [have_libsystemd=yes], [have_libsystemd=no]) +if test $have_libsystemd = yes; then + AC_DEFINE(HAVE_LIBSYSTEMD, 1, [Define if libsystemd is available]) +fi save_LIBS=$LIBS LIBS=$BASE_LIBS @@ -484,4 +488,5 @@ echo " Use sandboxed triggers: $enable_sandboxed_triggers" echo " Use seccomp: $enable_seccomp" echo " Privileged group: $PRIVILEGED_GROUP" echo " Privilege mode: $with_priv_mode" +echo " Use libsystemd: $have_libsystemd" echo "" diff --git a/doc/Makefile.am b/doc/Makefile.am index 2326399bc..474422d97 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -55,9 +55,10 @@ man1 = \ flatpak-build-commit-from.1 \ flatpak-repo.1 \ flatpak-search.1 \ - flatpak-create-usb.1 \ - flatpak-repair.1 \ - flatpak-kill.1 \ + flatpak-create-usb.1 \ + flatpak-repair.1 \ + flatpak-kill.1 \ + flatpak-history.1 \ $(NULL) man5 = \ diff --git a/doc/flatpak-docs.xml.in b/doc/flatpak-docs.xml.in index a24ca1da4..2114938a5 100644 --- a/doc/flatpak-docs.xml.in +++ b/doc/flatpak-docs.xml.in @@ -40,6 +40,7 @@ + diff --git a/doc/flatpak-history.xml b/doc/flatpak-history.xml new file mode 100644 index 000000000..8c0549d16 --- /dev/null +++ b/doc/flatpak-history.xml @@ -0,0 +1,322 @@ + + + + + + + flatpak history + flatpak + + + + Developer + Matthias + Clasen + mclasen@redhat.com + + + + + + flatpak history + 1 + + + + flatpak-history + Show history + + + + + flatpak history + OPTION + + + + + Description + + + Shows changes to the flatpak installations on the system. This includes + installs, updates and removals of applications and runtimes. + + + By default, both per-user and system-wide installations are shown. Use the + --user, --installation or --system options to change this. + + + The information for the history command is taken from the systemd journal, + and can also be accessed using e.g. the journalctl command. + + + + + + Options + + The following options are understood: + + + + + + + + Show help options and exit. + + + + + + + + Show changes to the user installation. + + + + + + + + Show changes to the default system-wide installation. + + + + + + + + Show changes to the installation specified by NAME + among those defined in /etc/flatpak/installations.d/. + Using --installation=default is equivalent to using + --system. + + + + + + + + Only show changes that are newer than the time specified by + TIME. + + TIME can be either an absolute time + in a format like YYYY-MM-DD HH:MM:SS, or a relative time like + "2h", "7days", "4days 2hours". + + + + + + + + Only show changes that are older than the time specified by + TIME. + + + + + + + + Show newest entries first. + + + + + + + + + Print debug information during command processing. + + + + + + + + Print OSTree debug information during command processing. + + + + + + + + Show the available values for the option. + + + + + + + + Specify what information to show about each ref. You can + list multiple fields, or use this option multiple times. + + + + + + + + Fields + + The following fields are understood by the option: + + + + time + + + Show when the change happened + + + + + change + + + Show the kind of change + + + + + ref + + + Show the ref + + + + + application + + + Show the application/runtime ID + + + + + arch + + + Show the architecture + + + + + branch + + + Show the branch + + + + + installation + + + Show the affected installation. + + This will be either the ID of a Flatpak installation, + or the path to a temporary OSTree repository. + + + + + remote + + + Show the remote that is used. + + This will be either the name of a configured remote, + or the path to a temporary OSTree repository. + + + + + old-commit + + + Show the previous commit. For pulls, this is the previous HEAD of the branch. + For deploys, it is the previously active commit + + + + + commit + + + Show the current commit. For pulls, this is the HEAD of the branch. + For deploys, it is the active commit + + + + + url + + + Show the remote url + + + + + user + + + Show the user doing the change. + + If this is the system helper operating as root, + also show which user triggered the change. + + + + + tool + + + Show the tool that was used. + + If this is the system helper, also show + which tool was used to triggered the change. + + + + + all + + + Show all columns + + + + + help + + + Show the list of available columns + + + + + + Note that field names can be abbreviated to a unique prefix. + + + + + + See also + + + flatpak1, + journalctl1 + + + + + diff --git a/doc/flatpak.xml b/doc/flatpak.xml index f69f577af..3703f8bb5 100644 --- a/doc/flatpak.xml +++ b/doc/flatpak.xml @@ -190,6 +190,13 @@ Show information for an installed application or runtime. + + flatpak-history1 + + + Show history. + + flatpak-config1