summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteffen Nurpmeso <steffen@sdaoden.eu>2019-01-22 22:11:33 +0100
committerJuergen Daubert <jue@jue.li>2019-03-23 07:16:12 +0100
commita8fc6bed6a45cc2d6370ecef74b7b8f6890a8d49 (patch)
tree8a7f37650b841b75f950db326801e6f9f8a0432d
parent1419c791d05ccecabdc04c4be7443ecb016cce5c (diff)
downloadstart-stop-daemon-a8fc6bed6a45cc2d6370ecef74b7b8f6890a8d49.tar.gz
start-stop-daemon-a8fc6bed6a45cc2d6370ecef74b7b8f6890a8d49.tar.xz
Sync with dpkg 1.19.3
-rw-r--r--start-stop-daemon.845
-rw-r--r--start-stop-daemon.c368
2 files changed, 359 insertions, 54 deletions
diff --git a/start-stop-daemon.8 b/start-stop-daemon.8
index de2d35c..4723596 100644
--- a/start-stop-daemon.8
+++ b/start-stop-daemon.8
@@ -5,7 +5,7 @@
.\" Copyright © 2000-2001 Wichert Akkerman <wakkerma@debian.org>
.\" Copyright © 2002-2003 Adam Heath <doogie@debian.org>
.\" Copyright © 2004 Scott James Remnant <keybuk@debian.org>
-.\" Copyright © 2008-2015 Guillem Jover <guillem@debian.org>
+.\" Copyright © 2008-2016, 2018 Guillem Jover <guillem@debian.org>
.\"
.\" This is free software; you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
@@ -20,7 +20,7 @@
.\" You should have received a copy of the GNU General Public License
.\" along with this program. If not, see <https://www.gnu.org/licenses/>.
.
-.TH start\-stop\-daemon 8 "2017-07-04" "Debian Project" "dpkg suite"
+.TH start\-stop\-daemon 8 "%RELEASE_DATE%" "%VERSION%" "dpkg suite"
.nh
.SH NAME
start\-stop\-daemon \- start and stop system daemon programs
@@ -116,9 +116,17 @@ Check for a process with the specified parent pid \fIppid\fP
The \fIppid\fP must be a number greater than 0.
.TP
.BR \-p ", " \-\-pidfile " \fIpid-file\fP"
-Check whether a process has created the file \fIpid-file\fP. Note: using this
-matching option alone might cause unintended processes to be acted on, if the
-old process terminated without being able to remove the \fIpid-file\fP.
+Check whether a process has created the file \fIpid-file\fP.
+.IP
+Note: using this matching option alone might cause unintended processes to
+be acted on, if the old process terminated without being able to remove the
+\fIpid-file\fP.
+.IP
+\fBWarning:\fP Using this match option alone with a daemon that writes the
+pidfile as an unprivileged user is a security risk, because if the daemon
+gets compromised the contents of the pidfile cannot be trusted, and then
+a privileged runner (such as an init script executed as root) would end up
+acting on any system process.
.TP
.BR \-x ", " \-\-exec " \fIexecutable\fP"
Check for processes that are instances of this \fIexecutable\fP. The
@@ -258,6 +266,33 @@ reason. This is a last resort, and is only meant for programs that either
make no sense forking on their own, or where it's not feasible to add the
code for them to do this themselves.
.TP
+.BR \-\-notify\-await
+Wait for the background process to send a readiness notification before
+considering the service started (since version 1.19.3).
+This implements parts of the systemd readiness procotol, as specified
+in the \fBsd_notify\fP(3) man page.
+The following variables are supported:
+.RS
+.TP
+.B READY=1
+The program is ready to give service, so we can exit safely.
+.TP
+.BI EXTEND_TIMEOUT_USEC= number
+The program requests to extend the timeout by \fInumber\fP microseconds.
+This will reset the current timeout to the specified value.
+.TP
+.BI ERRNO= number
+The program is exiting with an error.
+Do the same and print the user-friendly string for the \fBerrno\fP value.
+.RE
+.
+.TP
+.BI \-\-notify\-timeout timeout
+Set a timeout for the \fB\-\-notify\-await\fP option (since version 1.19.3).
+When the timeout is reached, \fBstart\-stop\-daemon\fP will exit with an
+error code, and no readiness notification will be awaited.
+The default is \fB60\fP seconds.
+.TP
.BR \-C ", " \-\-no\-close
Do not close any file descriptor when forcing the daemon into the background
(since version 1.16.5).
diff --git a/start-stop-daemon.c b/start-stop-daemon.c
index e972042..e7e1cdc 100644
--- a/start-stop-daemon.c
+++ b/start-stop-daemon.c
@@ -79,6 +79,8 @@
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/un.h>
#include <errno.h>
#include <limits.h>
@@ -183,6 +185,16 @@ enum action_code {
ACTION_STATUS,
};
+enum match_code {
+ MATCH_NONE = 0,
+ MATCH_PID = 1 << 0,
+ MATCH_PPID = 1 << 1,
+ MATCH_PIDFILE = 1 << 2,
+ MATCH_EXEC = 1 << 3,
+ MATCH_NAME = 1 << 4,
+ MATCH_USER = 1 << 5,
+};
+
/* Time conversion constants. */
enum {
NANOSEC_IN_SEC = 1000000000L,
@@ -194,11 +206,16 @@ enum {
static const long MIN_POLL_INTERVAL = 20L * NANOSEC_IN_MILLISEC;
static enum action_code action;
+static enum match_code match_mode;
static bool testmode = false;
static int quietmode = 0;
static int exitnodo = 1;
static bool background = false;
static bool close_io = true;
+static bool notify_await = false;
+static int notify_timeout = 60;
+static char *notify_sockdir;
+static char *notify_socket;
static bool mpidfile = false;
static bool rpidfile = false;
static int signal_nr = SIGTERM;
@@ -270,6 +287,32 @@ static struct schedule_item *schedule = NULL;
static void DPKG_ATTR_PRINTF(1)
+debug(const char *format, ...)
+{
+ va_list arglist;
+
+ if (quietmode >= 0)
+ return;
+
+ va_start(arglist, format);
+ vprintf(format, arglist);
+ va_end(arglist);
+}
+
+static void DPKG_ATTR_PRINTF(1)
+info(const char *format, ...)
+{
+ va_list arglist;
+
+ if (quietmode > 0)
+ return;
+
+ va_start(arglist, format);
+ vprintf(format, arglist);
+ va_end(arglist);
+}
+
+static void DPKG_ATTR_PRINTF(1)
warning(const char *format, ...)
{
va_list arglist;
@@ -399,6 +442,26 @@ newpath(const char *dirname, const char *filename)
return path;
}
+static int
+parse_unsigned(const char *string, int base, int *value_r)
+{
+ long value;
+ char *endptr;
+
+ if (!string[0])
+ return -1;
+
+ errno = 0;
+ value = strtol(string, &endptr, base);
+ if (string == endptr || *endptr != '\0' || errno != 0)
+ return -1;
+ if (value < 0 || value > INT_MAX)
+ return -1;
+
+ *value_r = value;
+ return 0;
+}
+
static long
get_open_fd_max(void)
{
@@ -469,6 +532,187 @@ wait_for_child(pid_t pid)
}
static void
+cleanup_socket_dir(void)
+{
+ unlink(notify_socket);
+ rmdir(notify_sockdir);
+}
+
+static char *
+setup_socket_name(const char *suffix)
+{
+ const char *basedir;
+
+ if (getuid() == 0 && access("/run", F_OK) == 0) {
+ basedir = "/run";
+ } else {
+ basedir = getenv("TMPDIR");
+ if (basedir == NULL)
+ basedir = P_tmpdir;
+ }
+
+ if (asprintf(&notify_sockdir, "%s/%s.XXXXXX", basedir, suffix) < 0)
+ fatal("cannot allocate socket directory name");
+
+ if (mkdtemp(notify_sockdir) == NULL)
+ fatal("cannot create socket directory %s", notify_sockdir);
+
+ atexit(cleanup_socket_dir);
+
+ if (chown(notify_sockdir, runas_uid, runas_gid))
+ fatal("cannot change socket directory ownership");
+
+ if (asprintf(&notify_socket, "%s/notify", notify_sockdir) < 0)
+ fatal("cannot allocate socket name");
+
+ setenv("NOTIFY_SOCKET", notify_socket, 1);
+
+ return notify_socket;
+}
+
+static int
+create_notify_socket(void)
+{
+ const char *sockname;
+ struct sockaddr_un su;
+ int fd, rc, flags;
+ static const int enable = 1;
+
+ /* Create notification socket. */
+ fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK, 0);
+ if (fd < 0)
+ fatal("cannot create notification socket");
+
+ /* We could set SOCK_CLOEXEC instead, but then we would need to
+ * check whether the socket call failed, try and then do this anyway,
+ * when we have no threading problems to worry about. */
+ flags = fcntl(fd, F_GETFD);
+ if (flags < 0)
+ fatal("cannot read fd flags for notification socket");
+ if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0)
+ fatal("cannot set close-on-exec flag for notification socket");
+
+ sockname = setup_socket_name(".s-s-d-notify");
+
+ /* Bind to a socket in a temporary directory, selected based on
+ * the platform. */
+ memset(&su, 0, sizeof(su));
+ su.sun_family = AF_UNIX;
+ strncpy(su.sun_path, sockname, sizeof(su.sun_path) - 1);
+
+ rc = bind(fd, &su, sizeof(su));
+ if (rc < 0)
+ fatal("cannot bind to notification socket");
+
+ rc = chmod(su.sun_path, 0660);
+ if (rc < 0)
+ fatal("cannot change notification socket permissions");
+
+ rc = chown(su.sun_path, runas_uid, runas_gid);
+ if (rc < 0)
+ fatal("cannot change notification socket ownership");
+
+ /* XXX: Verify we are talking to an expected child? Although it is not
+ * clear whether this is feasible given the knowledge we have got. */
+ setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &enable, sizeof(enable));
+
+ return fd;
+}
+
+static void
+wait_for_notify(int fd)
+{
+ struct timespec startat, now, elapsed, timeout, timeout_orig;
+ fd_set fdrs;
+ int rc;
+
+ timeout.tv_sec = notify_timeout;
+ timeout.tv_nsec = 0;
+ timeout_orig = timeout;
+
+ timespec_gettime(&startat);
+
+ while (timeout.tv_sec >= 0 && timeout.tv_nsec >= 0) {
+ FD_ZERO(&fdrs);
+ FD_SET(fd, &fdrs);
+
+ /* Wait for input. */
+ debug("Waiting for notifications... (timeout %lusec %lunsec)\n",
+ timeout.tv_sec, timeout.tv_nsec);
+ rc = pselect(fd + 1, &fdrs, NULL, NULL, &timeout, NULL);
+
+ /* Catch non-restartable errors, that is, not signals nor
+ * kernel out of resources. */
+ if (rc < 0 && (errno != EINTR && errno != EAGAIN))
+ fatal("cannot monitor notification socket for activity");
+
+ /* Timed-out. */
+ if (rc == 0)
+ fatal("timed out waiting for a notification");
+
+ /* Update the timeout, as should not rely on pselect() having
+ * done that for us, which is an unportable assumption. */
+ timespec_gettime(&now);
+ timespec_sub(&now, &startat, &elapsed);
+ timespec_sub(&timeout_orig, &elapsed, &timeout);
+
+ /* Restartable error, a signal or kernel out of resources. */
+ if (rc < 0)
+ continue;
+
+ /* Parse it and check for a supported notification message,
+ * once we get a READY=1, we exit. */
+ for (;;) {
+ ssize_t nrecv;
+ char buf[4096];
+ char *line, *line_next;
+
+ nrecv = recv(fd, buf, sizeof(buf), 0);
+ if (nrecv < 0 && (errno != EINTR && errno != EAGAIN))
+ fatal("cannot receive notification packet");
+ if (nrecv < 0)
+ break;
+
+ buf[nrecv] = '\0';
+
+ for (line = buf; *line; line = line_next) {
+ line_next = strchrnul(line, '\n');
+ if (*line_next == '\n')
+ *line_next++ = '\0';
+
+ debug("Child sent some notification...\n");
+ if (strncmp(line, "EXTEND_TIMEOUT_USEC=", 20) == 0) {
+ int extend_usec = 0;
+
+ if (parse_unsigned(line + 20, 10, &extend_usec) != 0)
+ fatal("cannot parse extended timeout notification %s", line);
+
+ /* Reset the current timeout. */
+ timeout.tv_sec = extend_usec / 1000L;
+ timeout.tv_nsec = (extend_usec % 1000L) *
+ NANOSEC_IN_MILLISEC;
+ timeout_orig = timeout;
+
+ timespec_gettime(&startat);
+ } else if (strncmp(line, "ERRNO=", 6) == 0) {
+ int suberrno = 0;
+
+ if (parse_unsigned(line + 6, 10, &suberrno) != 0)
+ fatal("cannot parse errno notification %s", line);
+ errno = suberrno;
+ fatal("program failed to initialize");
+ } else if (strcmp(line, "READY=1") == 0) {
+ debug("-> Notification => ready for service.\n");
+ return;
+ } else {
+ debug("-> Notification line '%s' received\n", line);
+ }
+ }
+ }
+ }
+}
+
+static void
write_pidfile(const char *filename, pid_t pid)
{
FILE *fp;
@@ -499,12 +743,12 @@ remove_pidfile(const char *filename)
static void
daemonize(void)
{
+ int notify_fd = -1;
pid_t pid;
sigset_t mask;
sigset_t oldmask;
- if (quietmode < 0)
- printf("Detaching to start %s...", startas);
+ debug("Detaching to start %s...\n", startas);
/* Block SIGCHLD to allow waiting for the child process while it is
* performing actions, such as creating a pidfile. */
@@ -513,6 +757,9 @@ daemonize(void)
if (sigprocmask(SIG_BLOCK, &mask, &oldmask) == -1)
fatal("cannot block SIGCHLD");
+ if (notify_await)
+ notify_fd = create_notify_socket();
+
pid = fork();
if (pid < 0)
fatal("unable to do first fork");
@@ -522,6 +769,15 @@ daemonize(void)
* not suffer from race conditions on return. */
wait_for_child(pid);
+ if (notify_await) {
+ /* Wait for a readiness notification from the second
+ * child, so that we can safely exit when the service
+ * is up. */
+ wait_for_notify(notify_fd);
+ close(notify_fd);
+ cleanup_socket_dir();
+ }
+
_exit(0);
}
@@ -535,7 +791,7 @@ daemonize(void)
else if (pid) { /* Second parent. */
/* Set a default umask for dumb programs, which might get
* overridden by the --umask option later on, so that we get
- * a defined umask when creating the pidfille. */
+ * a defined umask when creating the pidfile. */
umask(022);
if (mpidfile && pidfile != NULL)
@@ -548,8 +804,7 @@ daemonize(void)
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1)
fatal("cannot restore signal mask");
- if (quietmode < 0)
- printf("done.\n");
+ debug("Detaching complete...\n");
}
static void
@@ -620,6 +875,8 @@ usage(void)
" scheduler (default prio is 4)\n"
" -k, --umask <mask> change the umask to <mask> before starting\n"
" -b, --background force the process to detach\n"
+" --notify-await wait for a readiness notification\n"
+" --notify-timeout <int> timeout after <int> seconds of notify wait\n"
" -C, --no-close do not close any file descriptor\n"
" -m, --make-pidfile create the pidfile before starting\n"
" --remove-pidfile delete the pidfile after stopping\n"
@@ -710,26 +967,6 @@ static const struct sigpair siglist[] = {
};
static int
-parse_unsigned(const char *string, int base, int *value_r)
-{
- long value;
- char *endptr;
-
- if (!string[0])
- return -1;
-
- errno = 0;
- value = strtol(string, &endptr, base);
- if (string == endptr || *endptr != '\0' || errno != 0)
- return -1;
- if (value < 0 || value > INT_MAX)
- return -1;
-
- *value_r = value;
- return 0;
-}
-
-static int
parse_pid(const char *pid_str, int *pid_num)
{
if (parse_unsigned(pid_str, 10, pid_num) != 0)
@@ -930,15 +1167,15 @@ parse_schedule(const char *schedule_str)
} else {
count = 0;
repeatat = -1;
- while (schedule_str != NULL) {
- slash = strchr(schedule_str, '/');
- str_len = slash ? (size_t)(slash - schedule_str) : strlen(schedule_str);
+ while (*schedule_str) {
+ slash = strchrnul(schedule_str, '/');
+ str_len = (size_t)(slash - schedule_str);
if (str_len >= sizeof(item_buf))
badusage("invalid schedule item: far too long"
" (you must delimit items with slashes)");
memcpy(item_buf, schedule_str, str_len);
item_buf[str_len] = '\0';
- schedule_str = slash ? slash + 1 : NULL;
+ schedule_str = *slash ? slash + 1 : slash;
parse_schedule_item(item_buf, &schedule[count]);
if (schedule[count].type == sched_forever) {
@@ -979,6 +1216,8 @@ set_action(enum action_code new_action)
#define OPT_PID 500
#define OPT_PPID 501
#define OPT_RM_PIDFILE 502
+#define OPT_NOTIFY_AWAIT 503
+#define OPT_NOTIFY_TIMEOUT 504
static void
parse_options(int argc, char * const *argv)
@@ -1009,6 +1248,8 @@ parse_options(int argc, char * const *argv)
{ "iosched", 1, NULL, 'I'},
{ "umask", 1, NULL, 'k'},
{ "background", 0, NULL, 'b'},
+ { "notify-await", 0, NULL, OPT_NOTIFY_AWAIT},
+ { "notify-timeout", 1, NULL, OPT_NOTIFY_TIMEOUT},
{ "no-close", 0, NULL, 'C'},
{ "make-pidfile", 0, NULL, 'm'},
{ "remove-pidfile", 0, NULL, OPT_RM_PIDFILE},
@@ -1023,6 +1264,7 @@ parse_options(int argc, char * const *argv)
const char *schedule_str = NULL;
const char *proc_schedule_str = NULL;
const char *io_schedule_str = NULL;
+ const char *notify_timeout_str = NULL;
size_t changeuser_len;
int c;
@@ -1052,18 +1294,22 @@ parse_options(int argc, char * const *argv)
startas = optarg;
break;
case 'n': /* --name <process-name> */
+ match_mode |= MATCH_NAME;
cmdname = optarg;
break;
case 'o': /* --oknodo */
exitnodo = 0;
break;
case OPT_PID: /* --pid <pid> */
+ match_mode |= MATCH_PID;
pid_str = optarg;
break;
case OPT_PPID: /* --ppid <ppid> */
+ match_mode |= MATCH_PPID;
ppid_str = optarg;
break;
case 'p': /* --pidfile <pid-file> */
+ match_mode |= MATCH_PIDFILE;
pidfile = optarg;
break;
case 'q': /* --quiet */
@@ -1076,12 +1322,14 @@ parse_options(int argc, char * const *argv)
testmode = true;
break;
case 'u': /* --user <username>|<uid> */
+ match_mode |= MATCH_USER;
userspec = optarg;
break;
case 'v': /* --verbose */
quietmode = -1;
break;
case 'x': /* --exec <executable> */
+ match_mode |= MATCH_EXEC;
execname = optarg;
break;
case 'c': /* --chuid <username>|<uid> */
@@ -1116,6 +1364,12 @@ parse_options(int argc, char * const *argv)
case 'b': /* --background */
background = true;
break;
+ case OPT_NOTIFY_AWAIT:
+ notify_await = true;
+ break;
+ case OPT_NOTIFY_TIMEOUT:
+ notify_timeout_str = optarg;
+ break;
case 'C': /* --no-close */
close_io = false;
break;
@@ -1168,11 +1422,16 @@ parse_options(int argc, char * const *argv)
badusage("umask value must be a positive number");
}
+ if (notify_timeout_str != NULL)
+ if (parse_unsigned(notify_timeout_str, 10, &notify_timeout) != 0)
+ badusage("invalid notify timeout value");
+
if (action == ACTION_NONE)
badusage("need one of --start or --stop or --status");
- if (!execname && !pid_str && !ppid_str && !pidfile && !userspec &&
- !cmdname)
+ if (match_mode == MATCH_NONE ||
+ (!execname && !cmdname && !userspec &&
+ !pid_str && !ppid_str && !pidfile))
badusage("need at least one of --exec, --pid, --ppid, --pidfile, --user or --name");
#ifdef PROCESS_NAME_SIZE
@@ -1194,7 +1453,7 @@ parse_options(int argc, char * const *argv)
badusage("--remove-pidfile requires --pidfile");
if (pid_str && pidfile)
- badusage("need either --pid of --pidfile, not both");
+ badusage("need either --pid or --pidfile, not both");
if (background && action != ACTION_START)
badusage("--background is only relevant with --start");
@@ -1994,6 +2253,24 @@ do_pidfile(const char *name)
if (f) {
enum status_code pid_status;
+ /* If we are only matching on the pidfile, and it is owned by
+ * a non-root user, then this is a security risk, and the
+ * contents cannot be trusted, because the daemon might have
+ * been compromised. */
+ if (match_mode == MATCH_PIDFILE) {
+ struct stat st;
+ int fd = fileno(f);
+
+ if (fstat(fd, &st) < 0)
+ fatal("cannot stat pidfile %s", name);
+
+ if ((st.st_uid != getuid() && st.st_uid != 0) ||
+ (st.st_gid != getgid() && st.st_gid != 0))
+ fatal("matching only on non-root pidfile %s is insecure", name);
+ if (st.st_mode & 0002)
+ fatal("matching only on world-writable pidfile %s is insecure", name);
+ }
+
if (fscanf(f, "%d", &pid) == 1)
pid_status = pid_check(pid);
else
@@ -2037,7 +2314,7 @@ do_procinit(void)
prog_status = pid_status;
}
closedir(procdir);
- if (!foundany)
+ if (foundany == 0)
fatal("nothing in /proc - not mounted?");
return prog_status;
@@ -2218,8 +2495,7 @@ do_start(int argc, char **argv)
do_findprocs();
if (found) {
- if (quietmode <= 0)
- printf("%s already running.\n", execname ? execname : "process");
+ info("%s already running.\n", execname ? execname : "process");
return exitnodo;
}
if (testmode && quietmode <= 0) {
@@ -2247,8 +2523,7 @@ do_start(int argc, char **argv)
}
if (testmode)
return 0;
- if (quietmode < 0)
- printf("Starting %s...\n", startas);
+ debug("Starting %s...\n", startas);
*--argv = startas;
if (background)
/* Ok, we need to detach this process. */
@@ -2334,9 +2609,7 @@ do_stop(int sig_num, int *n_killed, int *n_notkilled)
for (p = found; p; p = p->next) {
if (testmode) {
- if (quietmode <= 0)
- printf("Would send signal %d to %d.\n",
- sig_num, p->pid);
+ info("Would send signal %d to %d.\n", sig_num, p->pid);
(*n_killed)++;
} else if (kill(p->pid, sig_num) == 0) {
pid_list_push(&killed, p->pid);
@@ -2456,8 +2729,7 @@ finish_stop_schedule(bool anykilled)
if (anykilled)
return 0;
- if (quietmode <= 0)
- printf("No %s found running; none killed.\n", what_stop);
+ info("No %s found running; none killed.\n", what_stop);
return exitnodo;
}
@@ -2470,8 +2742,7 @@ run_stop_schedule(void)
if (testmode) {
if (schedule != NULL) {
- if (quietmode <= 0)
- printf("Ignoring --retry in test mode\n");
+ info("Ignoring --retry in test mode\n");
schedule = NULL;
}
}
@@ -2497,8 +2768,8 @@ run_stop_schedule(void)
if (schedule == NULL) {
do_stop(signal_nr, &n_killed, &n_notkilled);
do_stop_summary(0);
- if (n_notkilled > 0 && quietmode <= 0)
- printf("%d pids were not killed\n", n_notkilled);
+ if (n_notkilled > 0)
+ info("%d pids were not killed\n", n_notkilled);
if (n_killed)
anykilled = true;
return finish_stop_schedule(anykilled);
@@ -2532,9 +2803,8 @@ run_stop_schedule(void)
}
}
- if (quietmode <= 0)
- printf("Program %s, %d process(es), refused to die.\n",
- what_stop, n_killed);
+ info("Program %s, %d process(es), refused to die.\n",
+ what_stop, n_killed);
return 2;
}

Generated by cgit