Fix: close all file descriptors when executed as daemon
[lttng-tools.git] / src / bin / lttng-sessiond / main.c
index 9e2b21b2dd417d7fc217f75de2293ff932f8b43c..5cfd57971f7423fe21f69c1d58b2cfafca99a05b 100644 (file)
@@ -2,22 +2,21 @@
  * Copyright (C) 2011 - David Goulet <david.goulet@polymtl.ca>
  *                      Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
  *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation; only version 2 of the License.
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version 2 only,
+ * as published by the Free Software Foundation.
  *
- * This program 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 General Public License for
- * more details.
+ * This program 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 General Public License for more details.
  *
- * You should have received a copy of the GNU General Public License along with
- * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
- * Place - Suite 330, Boston, MA  02111-1307, USA.
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
 
 #define _GNU_SOURCE
-#include <fcntl.h>
 #include <getopt.h>
 #include <grp.h>
 #include <limits.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <sys/wait.h>
-#include <urcu/futex.h>
+#include <urcu/uatomic.h>
 #include <unistd.h>
 #include <config.h>
 
 #include <common/common.h>
 #include <common/compat/poll.h>
+#include <common/compat/socket.h>
 #include <common/defaults.h>
 #include <common/kernel-consumer/kernel-consumer.h>
 #include <common/ust-consumer/ust-consumer.h>
+#include <common/futex.h>
 
 #include "lttng-sessiond.h"
 #include "channel.h"
 #include "context.h"
 #include "event.h"
-#include "futex.h"
 #include "kernel.h"
 #include "modprobe.h"
 #include "shm.h"
 #include "ust-ctl.h"
 #include "utils.h"
+#include "fd-limit.h"
 
 #define CONSUMERD_FILE "lttng-consumerd"
 
@@ -81,14 +82,10 @@ const char default_tracing_group[] = DEFAULT_TRACING_GROUP;
 const char default_ust_sock_dir[] = DEFAULT_UST_SOCK_DIR;
 const char default_global_apps_pipe[] = DEFAULT_GLOBAL_APPS_PIPE;
 
-/* Variables */
-int opt_verbose;    /* Not static for lttngerr.h */
-int opt_verbose_consumer;    /* Not static for lttngerr.h */
-int opt_quiet;      /* Not static for lttngerr.h */
-
 const char *progname;
 const char *opt_tracing_group;
 static int opt_sig_parent;
+static int opt_verbose_consumer;
 static int opt_daemon;
 static int opt_no_kernel;
 static int is_root;                    /* Set to 1 if the daemon is running as root */
@@ -182,6 +179,40 @@ static const char *consumerd64_bin = CONFIG_CONSUMERD64_BIN;
 static const char *consumerd32_libdir = CONFIG_CONSUMERD32_LIBDIR;
 static const char *consumerd64_libdir = CONFIG_CONSUMERD64_LIBDIR;
 
+/*
+ * Consumer daemon state which is changed when spawning it, killing it or in
+ * case of a fatal error.
+ */
+enum consumerd_state {
+       CONSUMER_STARTED = 1,
+       CONSUMER_STOPPED = 2,
+       CONSUMER_ERROR   = 3,
+};
+
+/*
+ * This consumer daemon state is used to validate if a client command will be
+ * able to reach the consumer. If not, the client is informed. For instance,
+ * doing a "lttng start" when the consumer state is set to ERROR will return an
+ * error to the client.
+ *
+ * The following example shows a possible race condition of this scheme:
+ *
+ * consumer thread error happens
+ *                                    client cmd arrives
+ *                                    client cmd checks state -> still OK
+ * consumer thread exit, sets error
+ *                                    client cmd try to talk to consumer
+ *                                    ...
+ *
+ * However, since the consumer is a different daemon, we have no way of making
+ * sure the command will reach it safely even with this state flag. This is why
+ * we consider that up to the state validation during command processing, the
+ * command is safe. After that, we can not guarantee the correctness of the
+ * client request vis-a-vis the consumer.
+ */
+static enum consumerd_state ust_consumerd_state;
+static enum consumerd_state kernel_consumerd_state;
+
 static
 void setup_consumerd_path(void)
 {
@@ -301,14 +332,22 @@ static gid_t allowed_group(void)
  */
 static int init_thread_quit_pipe(void)
 {
-       int ret;
+       int ret, i;
 
-       ret = pipe2(thread_quit_pipe, O_CLOEXEC);
+       ret = pipe(thread_quit_pipe);
        if (ret < 0) {
                PERROR("thread quit pipe");
                goto error;
        }
 
+       for (i = 0; i < 2; i++) {
+               ret = fcntl(thread_quit_pipe[i], F_SETFD, FD_CLOEXEC);
+               if (ret < 0) {
+                       PERROR("fcntl");
+                       goto error;
+               }
+       }
+
 error:
        return ret;
 }
@@ -445,7 +484,6 @@ static void cleanup(void)
                        if (ret) {
                                PERROR("close");
                        }
-                       
                }
        }
        for (i = 0; i < 2; i++) {
@@ -591,7 +629,7 @@ static int send_kconsumer_session_streams(struct consumer_data *consumer_data,
                lkm.u.channel.channel_key = session->metadata->fd;
                lkm.u.channel.max_sb_size = session->metadata->conf->attr.subbuf_size;
                lkm.u.channel.mmap_len = 0;     /* for kernel */
-               DBG("Sending metadata channel %d to consumer", lkm.u.stream.stream_key);
+               DBG("Sending metadata channel %d to consumer", lkm.u.channel.channel_key);
                ret = lttcomm_send_unix_sock(sock, &lkm, sizeof(lkm));
                if (ret < 0) {
                        PERROR("send consumer channel");
@@ -1085,6 +1123,17 @@ restart_poll:
        ERR("consumer return code : %s", lttcomm_get_readable_code(-code));
 
 error:
+       /* Immediately set the consumerd state to stopped */
+       if (consumer_data->type == LTTNG_CONSUMER_KERNEL) {
+               uatomic_set(&kernel_consumerd_state, CONSUMER_ERROR);
+       } else if (consumer_data->type == LTTNG_CONSUMER64_UST ||
+                       consumer_data->type == LTTNG_CONSUMER32_UST) {
+               uatomic_set(&ust_consumerd_state, CONSUMER_ERROR);
+       } else {
+               /* Code flow error... */
+               assert(0);
+       }
+
        if (consumer_data->err_sock >= 0) {
                ret = close(consumer_data->err_sock);
                if (ret) {
@@ -1420,6 +1469,17 @@ static void *thread_registration_apps(void *data)
                                         * Using message-based transmissions to ensure we don't
                                         * have to deal with partially received messages.
                                         */
+                                       ret = lttng_fd_get(LTTNG_FD_APPS, 1);
+                                       if (ret < 0) {
+                                               ERR("Exhausted file descriptors allowed for applications.");
+                                               free(ust_cmd);
+                                               ret = close(sock);
+                                               if (ret) {
+                                                       PERROR("close");
+                                               }
+                                               sock = -1;
+                                               continue;
+                                       }
                                        ret = lttcomm_recv_unix_sock(sock, &ust_cmd->reg_msg,
                                                        sizeof(struct ust_register_msg));
                                        if (ret < 0 || ret < sizeof(struct ust_register_msg)) {
@@ -1433,6 +1493,7 @@ static void *thread_registration_apps(void *data)
                                                if (ret) {
                                                        PERROR("close");
                                                }
+                                               lttng_fd_put(LTTNG_FD_APPS, 1);
                                                sock = -1;
                                                continue;
                                        }
@@ -1478,6 +1539,7 @@ error:
                if (ret) {
                        PERROR("close");
                }
+               lttng_fd_put(LTTNG_FD_APPS, 1);
        }
        unlink(apps_unix_sock_path);
 
@@ -2828,6 +2890,36 @@ error:
        return -ret;
 }
 
+/*
+ * Command LTTNG_LIST_TRACEPOINT_FIELDS processed by the client thread.
+ */
+static ssize_t cmd_list_tracepoint_fields(int domain,
+                       struct lttng_event_field **fields)
+{
+       int ret;
+       ssize_t nb_fields = 0;
+
+       switch (domain) {
+       case LTTNG_DOMAIN_UST:
+               nb_fields = ust_app_list_event_fields(fields);
+               if (nb_fields < 0) {
+                       ret = LTTCOMM_UST_LIST_FAIL;
+                       goto error;
+               }
+               break;
+       case LTTNG_DOMAIN_KERNEL:
+       default:        /* fall-through */
+               ret = LTTCOMM_UND;
+               goto error;
+       }
+
+       return nb_fields;
+
+error:
+       /* Return negative value to differentiate return code */
+       return -ret;
+}
+
 /*
  * Command LTTNG_START_TRACE processed by the client thread.
  */
@@ -2842,7 +2934,8 @@ static int cmd_start_trace(struct ltt_session *session)
        usess = session->ust_session;
 
        if (session->enabled) {
-               ret = LTTCOMM_UST_START_FAIL;
+               /* Already started. */
+               ret = LTTCOMM_TRACE_ALREADY_STARTED;
                goto error;
        }
 
@@ -2934,7 +3027,7 @@ static int cmd_stop_trace(struct ltt_session *session)
        usess = session->ust_session;
 
        if (!session->enabled) {
-               ret = LTTCOMM_UST_STOP_FAIL;
+               ret = LTTCOMM_TRACE_ALREADY_STOPPED;
                goto error;
        }
 
@@ -2985,11 +3078,12 @@ error:
 /*
  * Command LTTNG_CREATE_SESSION processed by the client thread.
  */
-static int cmd_create_session(char *name, char *path, struct ucred *creds)
+static int cmd_create_session(char *name, char *path, lttng_sock_cred *creds)
 {
        int ret;
 
-       ret = session_create(name, path, creds->uid, creds->gid);
+       ret = session_create(name, path, LTTNG_SOCK_GET_UID_CRED(creds),
+                       LTTNG_SOCK_GET_GID_CRED(creds));
        if (ret != LTTCOMM_OK) {
                goto error;
        }
@@ -3274,6 +3368,7 @@ static int process_client_msg(struct command_ctx *cmd_ctx)
        switch(cmd_ctx->lsm->cmd_type) {
        case LTTNG_LIST_SESSIONS:
        case LTTNG_LIST_TRACEPOINTS:
+       case LTTNG_LIST_TRACEPOINT_FIELDS:
        case LTTNG_LIST_DOMAINS:
        case LTTNG_LIST_CHANNELS:
        case LTTNG_LIST_EVENTS:
@@ -3293,13 +3388,18 @@ static int process_client_msg(struct command_ctx *cmd_ctx)
        case LTTNG_CALIBRATE:
        case LTTNG_LIST_SESSIONS:
        case LTTNG_LIST_TRACEPOINTS:
+       case LTTNG_LIST_TRACEPOINT_FIELDS:
                need_tracing_session = 0;
                break;
        default:
                DBG("Getting session %s by name", cmd_ctx->lsm->session.name);
+               /*
+                * We keep the session list lock across _all_ commands
+                * for now, because the per-session lock does not
+                * handle teardown properly.
+                */
                session_lock_list();
                cmd_ctx->session = session_find_by_name(cmd_ctx->lsm->session.name);
-               session_unlock_list();
                if (cmd_ctx->session == NULL) {
                        if (cmd_ctx->lsm->session.name != NULL) {
                                ret = LTTCOMM_SESS_NOT_FOUND;
@@ -3337,6 +3437,12 @@ static int process_client_msg(struct command_ctx *cmd_ctx)
                        }
                }
 
+               /* Consumer is in an ERROR state. Report back to client */
+               if (uatomic_read(&kernel_consumerd_state) == CONSUMER_ERROR) {
+                       ret = LTTCOMM_NO_KERNCONSUMERD;
+                       goto error;
+               }
+
                /* Need a session for kernel command */
                if (need_tracing_session) {
                        if (cmd_ctx->session->kernel_session == NULL) {
@@ -3357,13 +3463,21 @@ static int process_client_msg(struct command_ctx *cmd_ctx)
                                        ret = LTTCOMM_KERN_CONSUMER_FAIL;
                                        goto error;
                                }
+                               uatomic_set(&kernel_consumerd_state, CONSUMER_STARTED);
                        } else {
                                pthread_mutex_unlock(&kconsumer_data.pid_mutex);
                        }
                }
+
                break;
        case LTTNG_DOMAIN_UST:
        {
+               /* Consumer is in an ERROR state. Report back to client */
+               if (uatomic_read(&ust_consumerd_state) == CONSUMER_ERROR) {
+                       ret = LTTCOMM_NO_USTCONSUMERD;
+                       goto error;
+               }
+
                if (need_tracing_session) {
                        if (cmd_ctx->session->ust_session == NULL) {
                                ret = create_ust_session(cmd_ctx->session,
@@ -3387,6 +3501,7 @@ static int process_client_msg(struct command_ctx *cmd_ctx)
                                }
 
                                ust_consumerd64_fd = ustconsumer64_data.cmd_sock;
+                               uatomic_set(&ust_consumerd_state, CONSUMER_STARTED);
                        } else {
                                pthread_mutex_unlock(&ustconsumer64_data.pid_mutex);
                        }
@@ -3401,7 +3516,9 @@ static int process_client_msg(struct command_ctx *cmd_ctx)
                                        ust_consumerd32_fd = -EINVAL;
                                        goto error;
                                }
+
                                ust_consumerd32_fd = ustconsumer32_data.cmd_sock;
+                               uatomic_set(&ust_consumerd_state, CONSUMER_STARTED);
                        } else {
                                pthread_mutex_unlock(&ustconsumer32_data.pid_mutex);
                        }
@@ -3413,13 +3530,33 @@ static int process_client_msg(struct command_ctx *cmd_ctx)
        }
 skip_domain:
 
+       /* Validate consumer daemon state when start/stop trace command */
+       if (cmd_ctx->lsm->cmd_type == LTTNG_START_TRACE ||
+                       cmd_ctx->lsm->cmd_type == LTTNG_STOP_TRACE) {
+               switch (cmd_ctx->lsm->domain.type) {
+               case LTTNG_DOMAIN_UST:
+                       if (uatomic_read(&ust_consumerd_state) != CONSUMER_STARTED) {
+                               ret = LTTCOMM_NO_USTCONSUMERD;
+                               goto error;
+                       }
+                       break;
+               case LTTNG_DOMAIN_KERNEL:
+                       if (uatomic_read(&kernel_consumerd_state) != CONSUMER_STARTED) {
+                               ret = LTTCOMM_NO_KERNCONSUMERD;
+                               goto error;
+                       }
+                       break;
+               }
+       }
+
        /*
         * Check that the UID or GID match that of the tracing session.
         * The root user can interact with all sessions.
         */
        if (need_tracing_session) {
                if (!session_access_ok(cmd_ctx->session,
-                               cmd_ctx->creds.uid, cmd_ctx->creds.gid)) {
+                               LTTNG_SOCK_GET_UID_CRED(&cmd_ctx->creds),
+                               LTTNG_SOCK_GET_GID_CRED(&cmd_ctx->creds))) {
                        ret = LTTCOMM_EPERM;
                        goto error;
                }
@@ -3508,6 +3645,37 @@ skip_domain:
                ret = LTTCOMM_OK;
                break;
        }
+       case LTTNG_LIST_TRACEPOINT_FIELDS:
+       {
+               struct lttng_event_field *fields;
+               ssize_t nb_fields;
+
+               nb_fields = cmd_list_tracepoint_fields(cmd_ctx->lsm->domain.type, &fields);
+               if (nb_fields < 0) {
+                       ret = -nb_fields;
+                       goto error;
+               }
+
+               /*
+                * Setup lttng message with payload size set to the event list size in
+                * bytes and then copy list into the llm payload.
+                */
+               ret = setup_lttng_msg(cmd_ctx, sizeof(struct lttng_event_field) * nb_fields);
+               if (ret < 0) {
+                       free(fields);
+                       goto setup_error;
+               }
+
+               /* Copy event list into message payload */
+               memcpy(cmd_ctx->llm->payload, fields,
+                               sizeof(struct lttng_event_field) * nb_fields);
+
+               free(fields);
+
+               ret = LTTCOMM_OK;
+               break;
+       }
+
        case LTTNG_START_TRACE:
        {
                ret = cmd_start_trace(cmd_ctx->session);
@@ -3528,6 +3696,11 @@ skip_domain:
        {
                ret = cmd_destroy_session(cmd_ctx->session,
                                cmd_ctx->lsm->session.name);
+               /*
+                * Set session to NULL so we do not unlock it after
+                * free.
+                */
+               cmd_ctx->session = NULL;
                break;
        }
        case LTTNG_LIST_DOMAINS:
@@ -3557,7 +3730,7 @@ skip_domain:
        }
        case LTTNG_LIST_CHANNELS:
        {
-               size_t nb_chan;
+               int nb_chan;
                struct lttng_channel *channels;
 
                nb_chan = cmd_list_channels(cmd_ctx->lsm->domain.type,
@@ -3612,7 +3785,9 @@ skip_domain:
                unsigned int nr_sessions;
 
                session_lock_list();
-               nr_sessions = lttng_sessions_count(cmd_ctx->creds.uid, cmd_ctx->creds.gid);
+               nr_sessions = lttng_sessions_count(
+                               LTTNG_SOCK_GET_UID_CRED(&cmd_ctx->creds),
+                               LTTNG_SOCK_GET_GID_CRED(&cmd_ctx->creds));
 
                ret = setup_lttng_msg(cmd_ctx, sizeof(struct lttng_session) * nr_sessions);
                if (ret < 0) {
@@ -3622,7 +3797,8 @@ skip_domain:
 
                /* Filled the session array */
                list_lttng_sessions((struct lttng_session *)(cmd_ctx->llm->payload),
-                       cmd_ctx->creds.uid, cmd_ctx->creds.gid);
+                       LTTNG_SOCK_GET_UID_CRED(&cmd_ctx->creds),
+                       LTTNG_SOCK_GET_GID_CRED(&cmd_ctx->creds));
 
                session_unlock_list();
 
@@ -3659,6 +3835,9 @@ setup_error:
        if (cmd_ctx->session) {
                session_unlock(cmd_ctx->session);
        }
+       if (need_tracing_session) {
+               session_unlock_list();
+       }
 init_setup_error:
        return ret;
 }
@@ -3788,7 +3967,7 @@ static void *thread_manage_clients(void *data)
                                PERROR("close");
                        }
                        sock = -1;
-                       free(cmd_ctx);
+                       clean_command_ctx(&cmd_ctx);
                        continue;
                }
 
@@ -3975,11 +4154,11 @@ static int parse_args(int argc, char **argv)
                        opt_no_kernel = 1;
                        break;
                case 'q':
-                       opt_quiet = 1;
+                       lttng_opt_quiet = 1;
                        break;
                case 'v':
                        /* Verbose level can increase using multiple -v */
-                       opt_verbose += 1;
+                       lttng_opt_verbose += 1;
                        break;
                case 'Z':
                        opt_verbose_consumer += 1;
@@ -4081,13 +4260,15 @@ static int set_permissions(char *rundir)
        int ret;
        gid_t gid;
 
-       gid = allowed_group();
-       if (gid < 0) {
+       ret = allowed_group();
+       if (ret < 0) {
                WARN("No tracing group detected");
                ret = 0;
                goto end;
        }
 
+       gid = ret;
+
        /* Set lttng run dir */
        ret = chown(rundir, 0, gid);
        if (ret < 0) {
@@ -4142,7 +4323,24 @@ end:
  */
 static int create_kernel_poll_pipe(void)
 {
-       return pipe2(kernel_poll_pipe, O_CLOEXEC);
+       int ret, i;
+
+       ret = pipe(kernel_poll_pipe);
+       if (ret < 0) {
+               PERROR("kernel poll pipe");
+               goto error;
+       }
+
+       for (i = 0; i < 2; i++) {
+               ret = fcntl(kernel_poll_pipe[i], F_SETFD, FD_CLOEXEC);
+               if (ret < 0) {
+                       PERROR("fcntl kernel_poll_pipe");
+                       goto error;
+               }
+       }
+
+error:
+       return ret;
 }
 
 /*
@@ -4151,7 +4349,24 @@ static int create_kernel_poll_pipe(void)
  */
 static int create_apps_cmd_pipe(void)
 {
-       return pipe2(apps_cmd_pipe, O_CLOEXEC);
+       int ret, i;
+
+       ret = pipe(apps_cmd_pipe);
+       if (ret < 0) {
+               PERROR("apps cmd pipe");
+               goto error;
+       }
+
+       for (i = 0; i < 2; i++) {
+               ret = fcntl(apps_cmd_pipe[i], F_SETFD, FD_CLOEXEC);
+               if (ret < 0) {
+                       PERROR("fcntl apps_cmd_pipe");
+                       goto error;
+               }
+       }
+
+error:
+       return ret;
 }
 
 /*
@@ -4208,10 +4423,11 @@ static int set_consumer_sockets(struct consumer_data *consumer_data,
        ret = mkdir(path, S_IRWXU);
        if (ret < 0) {
                if (errno != EEXIST) {
+                       PERROR("mkdir");
                        ERR("Failed to create %s", path);
                        goto error;
                }
-               ret = 0;
+               ret = -1;
        }
 
        /* Create the kconsumerd error unix socket */
@@ -4331,11 +4547,6 @@ int main(int argc, char **argv)
 
        rcu_register_thread();
 
-       /* Create thread quit pipe */
-       if ((ret = init_thread_quit_pipe()) < 0) {
-               goto error;
-       }
-
        setup_consumerd_path();
 
        /* Parse arguments */
@@ -4346,11 +4557,31 @@ int main(int argc, char **argv)
 
        /* Daemonize */
        if (opt_daemon) {
+               int i;
+
+               /*
+                * fork
+                * child: setsid, close FD 0, 1, 2, chdir /
+                * parent: exit (if fork is successful)
+                */
                ret = daemon(0, 0);
                if (ret < 0) {
                        PERROR("daemon");
                        goto error;
                }
+               /*
+                * We are in the child. Make sure all other file
+                * descriptors are closed, in case we are called with
+                * more opened file descriptors than the standard ones.
+                */
+               for (i = 3; i < sysconf(_SC_OPEN_MAX); i++) {
+                       (void) close(i);
+               }
+       }
+
+       /* Create thread quit pipe */
+       if ((ret = init_thread_quit_pipe()) < 0) {
+               goto error;
        }
 
        /* Check if daemon is UID = 0 */
@@ -4433,6 +4664,10 @@ int main(int argc, char **argv)
                }
        }
 
+       /* Set consumer initial state */
+       kernel_consumerd_state = CONSUMER_STOPPED;
+       ust_consumerd_state = CONSUMER_STOPPED;
+
        DBG("Client socket path %s", client_unix_sock_path);
        DBG("Application socket path %s", apps_unix_sock_path);
        DBG("LTTng run directory path: %s", rundir);
@@ -4471,6 +4706,12 @@ int main(int argc, char **argv)
                goto error;
        }
 
+       /*
+        * Init UST app hash table. Alloc hash table before this point since
+        * cleanup() can get called after that point.
+        */
+       ust_app_ht_alloc();
+
        /* After this point, we can safely call cleanup() with "goto exit" */
 
        /*
@@ -4493,6 +4734,8 @@ int main(int argc, char **argv)
                /* Set ulimit for open files */
                set_ulimit();
        }
+       /* init lttng_fd tracking must be done after set_ulimit. */
+       lttng_fd_init();
 
        ret = set_consumer_sockets(&ustconsumer64_data, rundir);
        if (ret < 0) {
@@ -4536,9 +4779,6 @@ int main(int argc, char **argv)
        /* Init UST command queue. */
        cds_wfq_init(&ust_cmd_queue.queue);
 
-       /* Init UST app hash table */
-       ust_app_ht_alloc();
-
        /*
         * Get session list pointer. This pointer MUST NOT be free(). This list is
         * statically declared in session.c
This page took 0.031896 seconds and 4 git commands to generate.