[Initng-svn] r2977 - initng/plugins/daemon
svn at initng.thinktux.net
svn at initng.thinktux.net
Thu Feb 9 19:26:29 CET 2006
Author: jimmy
Date: Thu Feb 9 19:26:28 2006
New Revision: 2977
Modified:
initng/plugins/daemon/initng_daemon.c
Log:
Lots of work to initng_daemon.c
Modified: initng/plugins/daemon/initng_daemon.c
==============================================================================
--- initng/plugins/daemon/initng_daemon.c (original)
+++ initng/plugins/daemon/initng_daemon.c Thu Feb 9 19:26:28 2006
@@ -34,6 +34,12 @@
#include <sys/reboot.h> /* reboot() RB_DISABLE_CAD */
#include <assert.h>
#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/types.h>
+#include <dirent.h>
#include "../../src/initng_handler.h"
@@ -45,6 +51,8 @@
#include "../../src/initng_static_states.h"
#include "../../src/initng_static_service_types.h"
#include "../../src/initng_depend.h"
+#include "../../src/initng_env_variable.h"
+#include "../../src/initng_static_process_types.h"
#include "../../src/initng_execute.h"
#include "initng_daemon.h"
@@ -56,6 +64,9 @@
*/
static void kill_daemon(active_db_h * service);
+static void clear_pidfile(active_db_h * s);
+static pid_t get_pidof(active_db_h * s);
+static pid_t get_pidfile(active_db_h * s, int warn);
/*
@@ -77,6 +88,7 @@
static void handle_DAEMON_WAITING_FOR_STOP_DEP(active_db_h * daemon);
static void handle_DAEMON_START_DEPS_MET(active_db_h * daemon);
static void handle_DAEMON_STOP_DEPS_MET(active_db_h * daemon);
+static void handle_DAEMON_WAIT_FOR_PID_FILE(active_db_h *daemon);
/*
* ############################################################################
@@ -101,6 +113,43 @@
ptype_h T_DAEMON = { "daemon", &handle_killed_daemon };
ptype_h T_KILL = { "kill", NULL };
+/*
+ * ############################################################################
+ * # DAEMON VARIABLES #
+ * ############################################################################
+ */
+
+/*
+ * timeout waiting for a pidfile to be created
+ * after the daemon returns happily.
+ */
+#define PID_TIMEOUT 90
+
+/* Rate limit on missing pidfile warnings */
+#define PID_WARN_RATE 2
+
+/* The name of the process, initng should probe */
+s_entry PIDOF = { "pid_of", STRING, &TYPE_DAEMON,
+ "When daemon exits, initng will look for a process with this name, and set daemon pid no to that pid."
+};
+
+/* The filename/path of a pidfile that initng can fetch pid no */
+s_entry PIDFILE = { "pid_file", STRINGS, &TYPE_DAEMON,
+ "When daemon exits, initng will get pid of daemon from this file."
+};
+
+/* Determines when initng looks for a pidfile */
+s_entry FORKS = { "forks", SET, &TYPE_DAEMON,
+ "Does the daemon fork?"
+};
+
+/*
+ * this is an internal option, that is set after the
+ * service died once, and the pid is updated.
+ */
+s_entry INTERNAL_GOT_PID = { NULL, SET, &TYPE_DAEMON, NULL, NULL };
+s_entry INTERNAL_PIDFILE_TIMEOUT = { NULL, INT, &TYPE_DAEMON, NULL, NULL };
+s_entry INTERNAL_PID_WARN_TIME = { NULL, INT, &TYPE_DAEMON, NULL, NULL };
/*
* ############################################################################
@@ -151,9 +200,14 @@
a_state_h DAEMON_STOPPED = { "DAEMON_STOPPED", IS_DOWN, NULL };
/*
- * This is the state, when the Start code is actually running.
+ * This is the state, when the launch is actually run.
+ */
+a_state_h DAEMON_LAUNCH = { "DAEMON_LAUNCH", IS_STARTING, NULL };
+
+/*
+ * In this state, the fork has return, and initng is waiting for a pidfile to appeare.
*/
-a_state_h DAEMON_RUN = { "DAEMON_RUN", IS_STARTING, NULL };
+a_state_h DAEMON_WAIT_FOR_PID_FILE = { "DAEMON_WAIT_FOR_PID_FILE", IS_STARTING, &handle_DAEMON_WAIT_FOR_PID_FILE };
/*
* Generally FAILING states, if something goes wrong, some of these are set.
@@ -211,10 +265,22 @@
return (FALSE);
}
+ /* Add a new servicetype */
initng_service_types_add(&TYPE_DAEMON);
+ /* Add 2 new processtype */
initng_process_db_ptype_add(&T_DAEMON);
+ initng_process_db_ptype_add(&T_KILL);
+ /* Add some new variables */
+ initng_service_data_types_add(&PIDFILE);
+ initng_service_data_types_add(&PIDOF);
+ initng_service_data_types_add(&FORKS);
+ initng_service_data_types_add(&INTERNAL_GOT_PID);
+ initng_service_data_types_add(&INTERNAL_PIDFILE_TIMEOUT);
+ initng_service_data_types_add(&INTERNAL_PID_WARN_TIME);
+
+ /* Add some new service-states */
initng_active_state_add(&DAEMON_START_MARKED);
initng_active_state_add(&DAEMON_STOP_MARKED);
initng_active_state_add(&DAEMON_RUNNING);
@@ -223,13 +289,14 @@
initng_active_state_add(&DAEMON_START_DEPS_MET);
initng_active_state_add(&DAEMON_STOP_DEPS_MET);
initng_active_state_add(&DAEMON_STOPPED);
- initng_active_state_add(&DAEMON_RUN);
+ initng_active_state_add(&DAEMON_LAUNCH);
+ initng_active_state_add(&DAEMON_WAIT_FOR_PID_FILE);
initng_active_state_add(&DAEMON_START_DEPS_FAILED);
initng_active_state_add(&DAEMON_STOP_DEPS_FAILED);
initng_active_state_add(&DAEMON_FAIL_STARTING);
initng_active_state_add(&DAEMON_FAIL_STOPPING);
-
+ /* return happily */
return (TRUE);
}
@@ -237,10 +304,7 @@
{
D_("module_unload();\n");
- initng_service_types_del(&TYPE_DAEMON);
-
- initng_process_db_ptype_del(&T_DAEMON);
-
+ /* Remove all added states */
initng_active_state_del(&DAEMON_START_MARKED);
initng_active_state_del(&DAEMON_STOP_MARKED);
initng_active_state_del(&DAEMON_RUNNING);
@@ -249,12 +313,28 @@
initng_active_state_del(&DAEMON_START_DEPS_MET);
initng_active_state_del(&DAEMON_STOP_DEPS_MET);
initng_active_state_del(&DAEMON_STOPPED);
- initng_active_state_del(&DAEMON_RUN);
+ initng_active_state_del(&DAEMON_LAUNCH);
+ initng_active_state_del(&DAEMON_WAIT_FOR_PID_FILE);
initng_active_state_del(&DAEMON_START_DEPS_FAILED);
initng_active_state_del(&DAEMON_STOP_DEPS_FAILED);
initng_active_state_del(&DAEMON_FAIL_STARTING);
initng_active_state_del(&DAEMON_FAIL_STOPPING);
+ /* Delete all added variables */
+ initng_service_data_types_del(&PIDFILE);
+ initng_service_data_types_del(&PIDOF);
+ initng_service_data_types_del(&FORKS);
+ initng_service_data_types_del(&INTERNAL_GOT_PID);
+ initng_service_data_types_del(&INTERNAL_PIDFILE_TIMEOUT);
+ initng_service_data_types_del(&INTERNAL_PID_WARN_TIME);
+
+ /* Delete the processstypes */
+ initng_process_db_ptype_del(&T_DAEMON);
+ initng_process_db_ptype_del(&T_KILL);
+
+ /* Last, delete the servicetype */
+ initng_service_types_del(&TYPE_DAEMON);
+
}
/*
@@ -386,7 +466,11 @@
static void handle_DAEMON_START_DEPS_MET(active_db_h * daemon)
{
- if (!initng_common_mark_service(daemon, &DAEMON_RUN))
+ /* clear all stale pidfiles if any */
+ clear_pidfile(daemon);
+
+ /* set the DAEMON_LAUNCH state */
+ if (!initng_common_mark_service(daemon, &DAEMON_LAUNCH))
return;
/* F I N A L L Y S T A R T */
@@ -404,8 +488,14 @@
return;
}
- /* We just set it to up, as soon as it is started */
- initng_common_mark_service(daemon, &DAEMON_RUNNING);
+ /* If this daemon never forks, set it to DAEMON_RUNNING directly */
+ if (!initng_active_db_is(&FORKS, daemon))
+ {
+ /* We just set it to up, as soon as it is started */
+ initng_common_mark_service(daemon, &DAEMON_RUNNING);
+ }
+
+ /* Else Let this service stay in state DAEMON_RUN */
}
static void handle_DAEMON_STOP_DEPS_MET(active_db_h * service)
@@ -467,58 +557,176 @@
-#ifdef THIS_IS_DISABLED
-static void handle_STOPPED(active_db_h * daemon_stopped)
+
+
+/*
+ * Is run on every interrupt for services DAEMON_WAIT_FOR_PID_FILE
+ */
+static void handle_DAEMON_WAIT_FOR_PID_FILE(active_db_h * s)
{
- active_db_h *current, *safe = NULL;
+ pid_t pid = -1;
+ process_h *p = NULL;
+ int timeout = 0, wt, warn = 0;
- /*
- * Make sure there is no daemons that needs this
- * that still think its running.
- */
- while_active_db_safe(current, safe)
+
+ timeout = initng_active_db_get_int(&INTERNAL_PIDFILE_TIMEOUT, s);
+ if (timeout > 0 && timeout < time(0))
{
- /* no idea to stop myself */
- if (current == daemon_stopped)
- continue;
+ F_("Service \"%s\" wait for pidfile timed out!\n", s->name);
+ initng_common_mark_service(s, &FAIL_STARTING);
+ return;
+ }
- /* DON'T stop runlevels OR virtuals */
- if (current->type == &TYPE_VIRTUAL)
- continue;
+ wt = initng_active_db_get_int(&INTERNAL_PID_WARN_TIME, s);
+ if (wt > time(0) || (wt + PID_WARN_RATE) < time(0))
+ {
+ warn = 1;
+ initng_active_db_set_int(&INTERNAL_PID_WARN_TIME, s, (int) time(0));
+ }
- /* check that current needs daemon_stopped */
- if (initng_depend(current, daemon_stopped) == FALSE)
- continue;
+ /* get the process to handle */
+ p = initng_process_db_get(&T_DAEMON, s);
+ if (!p)
+ {
+ F_("Did not find a daemon process on this service!\n");
+ initng_common_mark_service(s, &FAIL_STARTING);
+ return;
+ }
- /* don't stop a stopped daemon */
- if (IS_DOWN(current))
- continue;
+ /* check if string PIDOF or PIDFILE exits */
+ if (initng_active_db_is(&PIDOF, s))
+ {
+ D_("getting pid by PIDOF!\n");
+ /* get pid by process name */
+ pid = get_pidof(s);
+ D_("result : %d\n", pid);
+ }
- /* if stop this */
- D_("%s have to stop %s.\n", daemon_stopped->name, current->name);
- initng_handler_stop_daemon(current);
+ if (initng_active_db_is(&PIDFILE, s))
+ {
+ D_("getting pid by PIDFILE!\n");
+ pid = get_pidfile(s, warn);
+ D_("result : %d\n", pid);
}
- /* check if this daemon is restarting */
- if (initng_active_db_is(&RESTARTING, daemon_stopped))
+ if (pid < 2)
{
- initng_active_db_remove(&RESTARTING, daemon_stopped);
- initng_handler_start_daemon(daemon_stopped);
- D_("Service is restarting now!\n");
+ /* make sure this one is run in 1 second again */
+ initng_global_set_sleep(1);
return;
}
+ if (kill(pid, 0) < 0 && (errno == ESRCH))
+ {
+ F_("Got a non-existent pid %i for daemon \"%s\"\n", pid, s->name);
+ initng_common_mark_service(s, &FAIL_STARTING);
+ return;
+ }
- /* free daemon, and forget */
- initng_active_db_del(daemon_stopped);
- initng_active_db_free(daemon_stopped);
- D_("Service removed.\n");
+ /* set the new pid, to that we got from pidof or pidfile */
+ if (initng_active_db_is(&FORKS, s))
+ p->pid = pid;
+
+ /* FIXME: if forks=noret, we should send messages to the pid in the
+ pidfile (and track changes in it), but use the original pid for
+ death detection - needed for mysql */
+
+ /* set INTERNAL_GOT_PID so that this service may be set to done */
+ initng_active_db_set(&INTERNAL_GOT_PID, s);
+ initng_active_db_unset(&INTERNAL_PIDFILE_TIMEOUT, s);
+
+ /* mark this service running */
+ initng_common_mark_service(s, &RUNNING);
}
+/*
+ * ############################################################################
+ * # KILL HANDLER FUNCTIONS #
+ * ############################################################################
+ */
+
+static void handle_killed_daemon(active_db_h * daemon, process_h * process)
+{
+ assert(daemon);
+ assert(daemon->name);
+ assert(daemon->current_state);
+ assert(daemon->current_state->state_name);
+ assert(process);
+ D_("handle_killed_start(%s): initial status: \"%s\".\n",
+ daemon->name, daemon->current_state->state_name);
-#endif
+ /* Set the universal variable, that signalize that something happened */
+ initng_global_set_interrupt();
+
+ /*
+ * If the return code (for example "exit 1", in a bash script)
+ * from the program, is bigger than 0, this commonly signalize
+ * that something got wrong, print this as an error msg on screen
+ */
+ if (process->r_code > 0)
+ {
+ F_(" start %s, Returned with exit %i.\n", daemon->name,
+ process->r_code);
+ }
+
+
+ /*
+ * If the daemon is launching and not running
+ */
+ if (daemon->current_state == &DAEMON_LAUNCH)
+ {
+
+ /*
+ * If pidof or pidfile is set, try fetch the pid.
+ */
+ if (initng_active_db_is(&PIDOF, daemon) || initng_active_db_is(&PIDFILE, daemon))
+ {
+ /* Set the WAIT_FOR_PIDFILE state, This will start checking for pidfiles. */
+ initng_common_mark_service(daemon, &DAEMON_WAIT_FOR_PID_FILE);
+
+ /* set the timeout also */
+ initng_active_db_set_int(&INTERNAL_PIDFILE_TIMEOUT, daemon,
+ (int) time(0) + PID_TIMEOUT);
+ initng_active_db_set_int(&INTERNAL_PID_WARN_TIME, daemon, (int) time(0));
+
+ /* return away from here */
+ return;
+ }
+
+ W_("%s daemon forked, and exited, but initng have no way of getting the pid it got.\nInitng mark this daemon as running but wont notice if it dies.", daemon->name);
+ initng_common_mark_service(daemon, &DAEMON_RUNNING);
+ return;
+ }
+ /*
+ * Make sure r_code don't signal error (can be override by UP_ON_FAILURE.
+ */
+
+ /* TODO MAKE UP_ON_FAILURE more universal */
+ if (process->r_code && !initng_active_db_is(&UP_ON_FAILURE, daemon))
+ {
+
+ initng_common_mark_service(daemon, &DAEMON_FAIL_STARTING);
+ list_del(&process->list);
+ initng_process_db_free(process);
+ return;
+ }
+
+ /* OK! now daemon is STOPPED! */
+ initng_common_mark_service(daemon, &DAEMON_STOPPED);
+
+ /* free the process struct, to spare memory */
+ list_del(&process->list);
+ initng_process_db_free(process);
+}
+
+/*
+ * ############################################################################
+ * # LOCAL FUNCTIONS #
+ * ############################################################################
+ */
+
static void kill_daemon(active_db_h * service)
{
@@ -590,65 +798,276 @@
}
}
+/*
+ * pid_of(name)
+ * This function will walk /proc all numbers dirs, looking in the stat file for
+ * the process name, if it matches name, it will return the pid of it.
+ * If it not succeeds to find it, it will return(-1).
+ */
+static pid_t pid_of(const char *name)
+{
+ DIR *dir;
+ struct dirent *d;
+ FILE *fp;
+
+ /* maximum possible length for string "/proc/12232/stat" can be */
+#define BUFF_SIZE 512
+
+ D_("Will fetch pid of \"%s\"\n", name);
+
+ /* Open /proc or fail */
+ if (!(dir = opendir("/proc")))
+ return (-1);
+
+ /* Walk through the directory. */
+ while ((d = readdir(dir)) != NULL)
+ {
+ char buf[BUFF_SIZE + 1]; /* Will contain a fixed string like "/proc/12232/stat" */
+ char *s = NULL; /* Temporary pointer when parsing the content of the stat file */
+ int len = 0; /* An length counter */
+ pid_t pid = -1; /* Will contain the pid to return */
+
+ /* Make sure this dirname is a number == pid */
+ if ((pid = atoi(d->d_name)) <= 0)
+ continue;
+
+ /* Fix a string, that matches the full path of the stat file */
+ snprintf(buf, BUFF_SIZE, "/proc/%d/stat", pid);
+ D_("To open: %s\n", buf);
+
+ /* Read SID & statname from it or fail */
+ if (!(fp = fopen(buf, "r")))
+ {
+ W_("Could not open %s.\n", buf);
+ continue;
+ }
+
+ /* fetch the full stat file, or fail */
+ if (fgets(buf, BUFF_SIZE, fp) == NULL)
+ {
+ fclose(fp);
+ continue;
+ }
+
+ /* close stat file */
+ fclose(fp);
+
+ /* set the walk counter, to the start of the file content fetched */
+ s = buf;
+
+ /* skip all chars to the first space - first string contains the pid no */
+ while (*s && *s != ' ')
+ s++;
+
+ /* Make sure we have any data */
+ if (*s == '\0')
+ continue;
+
+ /* skip the space */
+ s++;
+
+ /* skip the '(' char */
+ if (*s != '(')
+ continue;
+ s++;
+
+ /* count the length */
+ while (s[len] && s[len] != ')')
+ len++;
+
+ /* compare the name in the '(' ')' chars with the process name we are looking for */
+ if (strncmp(s, name, len) == 0)
+ {
+ D_("Found %s with pid %d\n", name, pid);
+
+ /* make sure the dir (/proc) is closed. */
+ if(dir)
+ closedir(dir);
+
+ /* return happily with the pid */
+ return (pid);
+ }
+ }
+
+ /* close the dir (/proc) if still open */
+ if(dir) closedir(dir);
+
+ D_("Did not find a process with name \"%s\"\n", name);
+ return (-1);
+}
+
+
/*
- * ############################################################################
- * # KILL HANDLER FUNCTIONS #
- * ############################################################################
+ * Check if a pidfile exists, if it exists, update the
+ * pid in the active_db entry. and return TRUE
*/
+static pid_t pid_from_file(const char *name, int warn)
+{
+ int fd = 0;
+ int len = 0;
+ char buf[21];
-static void handle_killed_daemon(active_db_h * daemon, process_h * process)
+ assert(name);
+
+ /* open pid file */
+ fd = open(name, O_RDONLY);
+
+ /* If we cant open pidfile, this is bad */
+ if (fd == -1)
+ {
+ if (warn)
+ W_("Unable to open pidfile: %s, \"%s\", it might not be created yet.\n", name, strerror(errno));
+ return (-1);
+ }
+
+ /* Read data from buffer */
+ len = read(fd, buf, 20);
+ close(fd);
+ if (len < 1)
+ {
+ F_("Read 0 chars from %s, It's empty.\n", name);
+ return (-1);
+ }
+
+ /* remove last newline */
+ if (buf[len - 1] == '\n')
+ buf[len - 1] = 0;
+ else
+ buf[len] = 0;
+
+ /* Try to convert pid to int */
+ return (atoi(buf));
+}
+
+static pid_t get_pidof(active_db_h * s)
{
- assert(daemon);
- assert(daemon->name);
- assert(daemon->current_state);
- assert(daemon->current_state->state_name);
- assert(process);
+ pid_t pid;
+ const char *pidof;
+ char *pidof_fixed = NULL;
+
+ pidof = initng_active_db_get_string(&PIDOF, s);
+ if (!pidof)
+ return (-1);
+
+
+ pidof_fixed = fix_variables(pidof, s);
+ if (!pidof_fixed)
+ return (-1);
+
+ pid = pid_of(pidof_fixed);
+ free(pidof_fixed);
+ return (pid);
+}
- D_("handle_killed_start(%s): initial status: \"%s\".\n",
- daemon->name, daemon->current_state->state_name);
+/* this will get the pid of PIDFILE entry of service */
+static pid_t get_pidfile(active_db_h * s, int warn)
+{
+ pid_t pid;
+ const char *pidfile = NULL;
+ char *pidfile_fixed = NULL;
+
+ /* get the pidfile */
+ while ((pidfile = initng_active_db_get_next_string(&PIDFILE, s, pidfile)))
+ {
+ /* fix the variables in the string */
+ pidfile_fixed = fix_variables(pidfile, s);
+
+ /* check so we got the string */
+ if (!pidfile_fixed)
+ return (-1);
+
+ /* make sure the first char is a '/' so its a full path */
+ if(pidfile_fixed[0]!='/')
+ {
+ F_("The pidfile path %s is not a full path!\n", pidfile_fixed);
+ return(-1);
+ }
- /* Set the universal variable, that signalize that something happened */
- initng_global_set_interrupt();
+ /* get the pid from the file */
+ pid = pid_from_file(pidfile_fixed, warn);
+ free(pidfile_fixed);
+
+ /* return the pid */
+ if (pid > 1)
+ return (pid);
+ }
+ return (-1);
+}
- /*
- * If the return code (for example "exit 1", in a bash script)
- * from the program, is bigger than 0, this commonly signalize
- * that something got wrong, print this as an error msg on screen
- */
- if (process->r_code > 0)
- F_(" start %s, Returned with exit %i.\n", daemon->name,
- process->r_code);
+static void clear_pidfile(active_db_h * s)
+{
+ const char *pidfile = NULL;
- /*
- * If daemon is stopping, ignore this signal
- */
- if (daemon->current_state != &DAEMON_RUN)
+ /* clear this set variable. */
+ initng_active_db_unset(&INTERNAL_GOT_PID, s);
+
+
+ /* ok, search for pidfiles */
+ while ((pidfile =
+ initng_active_db_get_next_string(&PIDFILE, s, pidfile)))
{
- F_("Start exited!, and daemon is not marked starting!\n");
- return;
+ if (pidfile)
+ {
+ if (unlink(pidfile) != 0 && errno != ENOENT)
+ {
+ F_("Could not remove stale pidfile %s\n", pidfile);
+ return;
+ }
+ }
}
+}
+
+#ifdef THIS_IS_DISABLED
+static void handle_STOPPED(active_db_h * daemon_stopped)
+{
+ active_db_h *current, *safe = NULL;
/*
- * Make sure r_code don't signal error (can be override by UP_ON_FAILURE.
+ * Make sure there is no daemons that needs this
+ * that still think its running.
*/
-
- /* TODO MAKE UP_ON_FAILURE more universal */
- if (process->r_code && !initng_active_db_is(&UP_ON_FAILURE, daemon))
+ while_active_db_safe(current, safe)
{
+ /* no idea to stop myself */
+ if (current == daemon_stopped)
+ continue;
- initng_common_mark_service(daemon, &DAEMON_FAIL_STARTING);
- list_del(&process->list);
- initng_process_db_free(process);
+ /* DON'T stop runlevels OR virtuals */
+ if (current->type == &TYPE_VIRTUAL)
+ continue;
+
+ /* check that current needs daemon_stopped */
+ if (initng_depend(current, daemon_stopped) == FALSE)
+ continue;
+
+ /* don't stop a stopped daemon */
+ if (IS_DOWN(current))
+ continue;
+
+ /* if stop this */
+ D_("%s have to stop %s.\n", daemon_stopped->name, current->name);
+ initng_handler_stop_daemon(current);
+ }
+
+ /* check if this daemon is restarting */
+ if (initng_active_db_is(&RESTARTING, daemon_stopped))
+ {
+ initng_active_db_remove(&RESTARTING, daemon_stopped);
+ initng_handler_start_daemon(daemon_stopped);
+ D_("Service is restarting now!\n");
return;
}
- /* OK! now daemon is STOPPED! */
- initng_common_mark_service(daemon, &DAEMON_STOPPED);
- /* free the process struct, to spare memory */
- list_del(&process->list);
- initng_process_db_free(process);
+ /* free daemon, and forget */
+ initng_active_db_del(daemon_stopped);
+ initng_active_db_free(daemon_stopped);
+ D_("Service removed.\n");
}
+
+
+
+#endif
More information about the Initng-svn
mailing list