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