Magellan Linux

Diff of /trunk/mkinitrd-magellan/busybox/debianutils/start_stop_daemon.c

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 532 by niro, Sat Sep 1 22:45:15 2007 UTC revision 816 by niro, Fri Apr 24 18:33:46 2009 UTC
# Line 8  Line 8 
8   * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.   * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
9   */   */
10    
11  #include "busybox.h"  /*
12  #include <getopt.h>  This is how it is supposed to work:
13    
14    start-stop-daemon [OPTIONS] [--start|--stop] [[--] arguments...]
15    
16    One (only) of these must be given:
17            -S,--start              Start
18            -K,--stop               Stop
19    
20    Search for matching processes.
21    If --stop is given, stop all matching processes (by sending a signal).
22    If --start is given, start a new process unless a matching process was found.
23    
24    Options controlling process matching
25    (if multiple conditions are specified, all must match):
26            -u,--user USERNAME|UID  Only consider this user's processes
27            -n,--name PROCESS_NAME  Look for processes by matching PROCESS_NAME
28                                    with comm field in /proc/$PID/stat.
29                                    Only basename is compared:
30                                    "ntpd" == "./ntpd" == "/path/to/ntpd".
31    [TODO: can PROCESS_NAME be a full pathname? Should we require full match then
32    with /proc/$PID/exe or argv[0] (comm can't be matched, it never contains path)]
33            -x,--exec EXECUTABLE    Look for processes that were started with this
34                                    command in /proc/$PID/cmdline.
35                                    Unlike -n, we match against the full path:
36                                    "ntpd" != "./ntpd" != "/path/to/ntpd"
37            -p,--pidfile PID_FILE   Look for processes with PID from this file
38    
39    Options which are valid for --start only:
40            -x,--exec EXECUTABLE    Program to run (1st arg of execvp). Mandatory.
41            -a,--startas NAME       argv[0] (defaults to EXECUTABLE)
42            -b,--background         Put process into background
43            -N,--nicelevel N        Add N to process' nice level
44            -c,--chuid USER[:[GRP]] Change to specified user [and group]
45            -m,--make-pidfile       Write PID to the pidfile
46                                    (both -m and -p must be given!)
47    
48    Options which are valid for --stop only:
49            -s,--signal SIG         Signal to send (default:TERM)
50            -t,--test               Exit with status 0 if process is found
51                                    (we don't actually start or stop daemons)
52    
53    Misc options:
54            -o,--oknodo             Exit with status 0 if nothing is done
55            -q,--quiet              Quiet
56            -v,--verbose            Verbose
57    */
58    
59  #include <sys/resource.h>  #include <sys/resource.h>
60    
61  static int signal_nr = 15;  /* Override ENABLE_FEATURE_PIDFILE */
62  static int user_id = -1;  #define WANT_PIDFILE 1
63  static int quiet;  #include "libbb.h"
 static char *userspec;  
 static char *chuid;  
 static char *cmdname;  
 static char *execname;  
 static char *pidfile;  
64    
65  struct pid_list {  struct pid_list {
66   struct pid_list *next;   struct pid_list *next;
67   pid_t pid;   pid_t pid;
68  };  };
69    
70  static struct pid_list *found;  enum {
71     CTX_STOP       = (1 <<  0),
72     CTX_START      = (1 <<  1),
73     OPT_BACKGROUND = (1 <<  2), // -b
74     OPT_QUIET      = (1 <<  3), // -q
75     OPT_TEST       = (1 <<  4), // -t
76     OPT_MAKEPID    = (1 <<  5), // -m
77     OPT_a          = (1 <<  6), // -a
78     OPT_n          = (1 <<  7), // -n
79     OPT_s          = (1 <<  8), // -s
80     OPT_u          = (1 <<  9), // -u
81     OPT_c          = (1 << 10), // -c
82     OPT_x          = (1 << 11), // -x
83     OPT_p          = (1 << 12), // -p
84     OPT_OKNODO     = (1 << 13) * ENABLE_FEATURE_START_STOP_DAEMON_FANCY, // -o
85     OPT_VERBOSE    = (1 << 14) * ENABLE_FEATURE_START_STOP_DAEMON_FANCY, // -v
86     OPT_NICELEVEL  = (1 << 15) * ENABLE_FEATURE_START_STOP_DAEMON_FANCY, // -N
87    };
88    #define QUIET (option_mask32 & OPT_QUIET)
89    #define TEST  (option_mask32 & OPT_TEST)
90    
91  static inline void push(pid_t pid)  struct globals {
92     struct pid_list *found;
93     char *userspec;
94     char *cmdname;
95     char *execname;
96     char *pidfile;
97     int user_id;
98     smallint signal_nr;
99    };
100    #define G (*(struct globals*)&bb_common_bufsiz1)
101    #define found             (G.found               )
102    #define userspec          (G.userspec            )
103    #define cmdname           (G.cmdname             )
104    #define execname          (G.execname            )
105    #define pidfile           (G.pidfile             )
106    #define user_id           (G.user_id             )
107    #define signal_nr         (G.signal_nr           )
108    #define INIT_G() do { \
109     user_id = -1; \
110     signal_nr = 15; \
111    } while (0)
112    
113    #ifdef OLDER_VERSION_OF_X
114    /* -x,--exec EXECUTABLE
115     * Look for processes with matching /proc/$PID/exe.
116     * Match is performed using device+inode.
117     */
118    static int pid_is_exec(pid_t pid)
119  {  {
120   struct pid_list *p;   struct stat st;
121     char buf[sizeof("/proc//exe") + sizeof(int)*3];
122    
123   p = xmalloc(sizeof(*p));   sprintf(buf, "/proc/%u/exe", (unsigned)pid);
124   p->next = found;   if (stat(buf, &st) < 0)
125   p->pid = pid;   return 0;
126   found = p;   if (st.st_dev == execstat.st_dev
127     && st.st_ino == execstat.st_ino)
128     return 1;
129     return 0;
130  }  }
131    #endif
132    
133  static int pid_is_exec(pid_t pid, const char *name)  static int pid_is_exec(pid_t pid)
134  {  {
135   char buf[sizeof("/proc//exe") + sizeof(int)*3];   ssize_t bytes;
136   char *execbuf;   char buf[PATH_MAX];
  int sz;  
  int equal;  
   
  sprintf(buf, "/proc/%d/exe", pid);  
  sz = strlen(name) + 1;  
  execbuf = xzalloc(sz);  
  readlink(buf, execbuf, sz);  
137    
138   /* if readlink fails, execbuf still contains "" */   sprintf(buf, "/proc/%u/cmdline", (unsigned)pid);
139   equal = !strcmp(execbuf, name);   bytes = open_read_close(buf, buf, sizeof(buf) - 1);
140   if (ENABLE_FEATURE_CLEAN_UP)   if (bytes > 0) {
141   free(execbuf);   buf[bytes] = '\0';
142   return equal;   return strcmp(buf, execname) == 0;
143     }
144     return 0;
145  }  }
146    
147  static int pid_is_user(int pid, int uid)  static int pid_is_name(pid_t pid)
148  {  {
149   struct stat sb;   /* /proc/PID/stat is "PID (comm_15_bytes_max) ..." */
150   char buf[sizeof("/proc/") + sizeof(int)*3];   char buf[32]; /* should be enough */
151     char *p, *pe;
152    
153   sprintf(buf, "/proc/%u", pid);   sprintf(buf, "/proc/%u/stat", (unsigned)pid);
154   if (stat(buf, &sb) != 0)   if (open_read_close(buf, buf, sizeof(buf) - 1) < 0)
155     return 0;
156     buf[sizeof(buf) - 1] = '\0'; /* paranoia */
157     p = strchr(buf, '(');
158     if (!p)
159     return 0;
160     pe = strrchr(++p, ')');
161     if (!pe)
162     return 0;
163     *pe = '\0';
164     /* we require comm to match and to not be truncated */
165     /* in Linux, if comm is 15 chars, it may be a truncated
166     * name, so we don't allow that to match */
167     if (strlen(p) >= COMM_LEN - 1) /* COMM_LEN is 16 */
168   return 0;   return 0;
169   return (sb.st_uid == uid);   return strcmp(p, cmdname) == 0;
170  }  }
171    
172  static int pid_is_cmd(pid_t pid, const char *name)  static int pid_is_user(int pid)
173  {  {
174   char fname[sizeof("/proc//stat") + sizeof(int)*3];   struct stat sb;
175   char *buf;   char buf[sizeof("/proc/") + sizeof(int)*3];
  int r = 0;  
   
  sprintf(fname, "/proc/%u/stat", pid);  
  buf = xmalloc_open_read_close(fname, NULL);  
  if (buf) {  
  char *p = strchr(buf, '(');  
  if (p) {  
  char *pe = strrchr(++p, ')');  
  if (pe) {  
  *pe = '\0';  
  r = !strcmp(p, name);  
  }  
  }  
  free(buf);  
  }  
  return r;  
 }  
176    
177     sprintf(buf, "/proc/%u", (unsigned)pid);
178     if (stat(buf, &sb) != 0)
179     return 0;
180     return (sb.st_uid == (uid_t)user_id);
181    }
182    
183  static void check(int pid)  static void check(int pid)
184  {  {
185   if (execname && !pid_is_exec(pid, execname)) {   struct pid_list *p;
186    
187     if (execname && !pid_is_exec(pid)) {
188   return;   return;
189   }   }
190   if (userspec && !pid_is_user(pid, user_id)) {   if (cmdname && !pid_is_name(pid)) {
191   return;   return;
192   }   }
193   if (cmdname && !pid_is_cmd(pid, cmdname)) {   if (userspec && !pid_is_user(pid)) {
194   return;   return;
195   }   }
196   push(pid);   p = xmalloc(sizeof(*p));
197     p->next = found;
198     p->pid = pid;
199     found = p;
200  }  }
201    
   
202  static void do_pidfile(void)  static void do_pidfile(void)
203  {  {
204   FILE *f;   FILE *f;
205   pid_t pid;   unsigned pid;
206    
207   f = fopen(pidfile, "r");   f = fopen_for_read(pidfile);
208   if (f) {   if (f) {
209   if (fscanf(f, "%u", &pid) == 1)   if (fscanf(f, "%u", &pid) == 1)
210   check(pid);   check(pid);
# Line 124  static void do_procinit(void) Line 217  static void do_procinit(void)
217  {  {
218   DIR *procdir;   DIR *procdir;
219   struct dirent *entry;   struct dirent *entry;
220   int foundany, pid;   int pid;
221    
222   if (pidfile) {   if (pidfile) {
223   do_pidfile();   do_pidfile();
# Line 133  static void do_procinit(void) Line 226  static void do_procinit(void)
226    
227   procdir = xopendir("/proc");   procdir = xopendir("/proc");
228    
229   foundany = 0;   pid = 0;
230   while ((entry = readdir(procdir)) != NULL) {   while (1) {
231     errno = 0; /* clear any previous error */
232     entry = readdir(procdir);
233    // TODO: this check is too generic, it's better
234    // to check for exact errno(s) which mean that we got stale entry
235     if (errno) /* Stale entry, process has died after opendir */
236     continue;
237     if (!entry) /* EOF, no more entries */
238     break;
239   pid = bb_strtou(entry->d_name, NULL, 10);   pid = bb_strtou(entry->d_name, NULL, 10);
240   if (errno)   if (errno) /* NaN */
241   continue;   continue;
  foundany++;  
242   check(pid);   check(pid);
243   }   }
244   closedir(procdir);   closedir(procdir);
245   if (!foundany)   if (!pid)
246   bb_error_msg_and_die ("nothing in /proc - not mounted?");   bb_error_msg_and_die("nothing in /proc - not mounted?");
247  }  }
248    
   
249  static int do_stop(void)  static int do_stop(void)
250  {  {
251   char *what;   char *what;
252   struct pid_list *p;   struct pid_list *p;
253   int killed = 0;   int killed = 0;
254    
255   do_procinit();   if (cmdname) {
256     if (ENABLE_FEATURE_CLEAN_UP) what = xstrdup(cmdname);
257   if (cmdname)   if (!ENABLE_FEATURE_CLEAN_UP) what = cmdname;
258   what = xstrdup(cmdname);   } else if (execname) {
259   else if (execname)   if (ENABLE_FEATURE_CLEAN_UP) what = xstrdup(execname);
260   what = xstrdup(execname);   if (!ENABLE_FEATURE_CLEAN_UP) what = execname;
261   else if (pidfile)   } else if (pidfile) {
262   what = xasprintf("process in pidfile '%s'", pidfile);   what = xasprintf("process in pidfile '%s'", pidfile);
263   else if (userspec)   } else if (userspec) {
264   what = xasprintf("process(es) owned by '%s'", userspec);   what = xasprintf("process(es) owned by '%s'", userspec);
265   else   } else {
266   bb_error_msg_and_die("internal error, please report");   bb_error_msg_and_die("internal error, please report");
267     }
268    
269   if (!found) {   if (!found) {
270   if (!quiet)   if (!QUIET)
271   printf("no %s found; none killed\n", what);   printf("no %s found; none killed\n", what);
272   if (ENABLE_FEATURE_CLEAN_UP)   killed = -1;
273   free(what);   goto ret;
  return -1;  
274   }   }
275   for (p = found; p; p = p->next) {   for (p = found; p; p = p->next) {
276   if (kill(p->pid, signal_nr) == 0) {   if (TEST || kill(p->pid, signal_nr) == 0) {
  p->pid = -p->pid;  
277   killed++;   killed++;
278   } else {   } else {
279   bb_perror_msg("warning: failed to kill %d", p->pid);   p->pid = 0;
280     bb_perror_msg("warning: killing process %u", (unsigned)p->pid);
281   }   }
282   }   }
283   if (!quiet && killed) {   if (!QUIET && killed) {
284   printf("stopped %s (pid", what);   printf("stopped %s (pid", what);
285   for (p = found; p; p = p->next)   for (p = found; p; p = p->next)
286   if(p->pid < 0)   if (p->pid)
287   printf(" %d", -p->pid);   printf(" %u", (unsigned)p->pid);
288   puts(")");   puts(")");
289   }   }
290     ret:
291   if (ENABLE_FEATURE_CLEAN_UP)   if (ENABLE_FEATURE_CLEAN_UP)
292   free(what);   free(what);
293   return killed;   return killed;
294  }  }
295    
296  #if ENABLE_FEATURE_START_STOP_DAEMON_LONG_OPTIONS  #if ENABLE_FEATURE_START_STOP_DAEMON_LONG_OPTIONS
297  static const struct option long_options[] = {  static const char start_stop_daemon_longopts[] ALIGN1 =
298   { "stop",               0,      NULL,   'K' },   "stop\0"         No_argument       "K"
299   { "start",              0,      NULL,   'S' },   "start\0"        No_argument       "S"
300   { "background",         0,      NULL,   'b' },   "background\0"   No_argument       "b"
301   { "quiet",              0,      NULL,   'q' },   "quiet\0"        No_argument       "q"
302   { "make-pidfile",       0,      NULL,   'm' },   "test\0"         No_argument       "t"
303     "make-pidfile\0" No_argument       "m"
304  #if ENABLE_FEATURE_START_STOP_DAEMON_FANCY  #if ENABLE_FEATURE_START_STOP_DAEMON_FANCY
305   { "oknodo",             0,      NULL,   'o' },   "oknodo\0"       No_argument       "o"
306   { "verbose",            0,      NULL,   'v' },   "verbose\0"      No_argument       "v"
307   { "nicelevel",          1,      NULL,   'N' },   "nicelevel\0"    Required_argument "N"
308  #endif  #endif
309   { "startas",            1,      NULL,   'a' },   "startas\0"      Required_argument "a"
310   { "name",               1,      NULL,   'n' },   "name\0"         Required_argument "n"
311   { "signal",             1,      NULL,   's' },   "signal\0"       Required_argument "s"
312   { "user",               1,      NULL,   'u' },   "user\0"         Required_argument "u"
313   { "chuid",              1,      NULL,   'c' },   "chuid\0"        Required_argument "c"
314   { "exec",               1,      NULL,   'x' },   "exec\0"         Required_argument "x"
315   { "pidfile",            1,      NULL,   'p' },   "pidfile\0"      Required_argument "p"
316  #if ENABLE_FEATURE_START_STOP_DAEMON_FANCY  #if ENABLE_FEATURE_START_STOP_DAEMON_FANCY
317   { "retry",              1,      NULL,   'R' },   "retry\0"        Required_argument "R"
318  #endif  #endif
319   { 0,                    0,      0,      0 }   ;
 };  
320  #endif  #endif
321    
322  enum {  int start_stop_daemon_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
323   CTX_STOP       = 0x1,  int start_stop_daemon_main(int argc UNUSED_PARAM, char **argv)
  CTX_START      = 0x2,  
  OPT_BACKGROUND = 0x4,  
  OPT_QUIET      = 0x8,  
  OPT_MAKEPID    = 0x10,  
  OPT_OKNODO     = 0x20 * ENABLE_FEATURE_START_STOP_DAEMON_FANCY,  
  OPT_VERBOSE    = 0x40 * ENABLE_FEATURE_START_STOP_DAEMON_FANCY,  
  OPT_NICELEVEL  = 0x80 * ENABLE_FEATURE_START_STOP_DAEMON_FANCY,  
 };  
   
 int start_stop_daemon_main(int argc, char **argv)  
324  {  {
325   unsigned opt;   unsigned opt;
326   char *signame = NULL;   char *signame;
327   char *startas = NULL;   char *startas;
328     char *chuid;
329    #ifdef OLDER_VERSION_OF_X
330     struct stat execstat;
331    #endif
332  #if ENABLE_FEATURE_START_STOP_DAEMON_FANCY  #if ENABLE_FEATURE_START_STOP_DAEMON_FANCY
333  // char *retry_arg = NULL;  // char *retry_arg = NULL;
334  // int retries = -1;  // int retries = -1;
335   char *opt_N;   char *opt_N;
336  #endif  #endif
337    
338     INIT_G();
339    
340  #if ENABLE_FEATURE_START_STOP_DAEMON_LONG_OPTIONS  #if ENABLE_FEATURE_START_STOP_DAEMON_LONG_OPTIONS
341   applet_long_options = long_options;   applet_long_options = start_stop_daemon_longopts;
342  #endif  #endif
343    
344   /* Check required one context option was given */   /* -K or -S is required; they are mutually exclusive */
345   opt_complementary = "K:S:?:K--S:S--K:m?p:K?xpun:S?xa";   /* -p is required if -m is given */
346   opt = getopt32(argc, argv, "KSbqm"   /* -xpun (at least one) is required if -K is given */
347  // USE_FEATURE_START_STOP_DAEMON_FANCY("ovN:R:")   /* -xa (at least one) is required if -S is given */
348   USE_FEATURE_START_STOP_DAEMON_FANCY("ovN:")   /* -q turns off -v */
349   "a:n:s:u:c:x:p:"   opt_complementary = "K:S:K--S:S--K:m?p:K?xpun:S?xa"
350     USE_FEATURE_START_STOP_DAEMON_FANCY("q-v");
351     opt = getopt32(argv, "KSbqtma:n:s:u:c:x:p:"
352     USE_FEATURE_START_STOP_DAEMON_FANCY("ovN:R:"),
353     &startas, &cmdname, &signame, &userspec, &chuid, &execname, &pidfile
354   USE_FEATURE_START_STOP_DAEMON_FANCY(,&opt_N)   USE_FEATURE_START_STOP_DAEMON_FANCY(,&opt_N)
355  // USE_FEATURE_START_STOP_DAEMON_FANCY(,&retry_arg)   /* We accept and ignore -R <param> / --retry <param> */
356   ,&startas, &cmdname, &signame, &userspec, &chuid, &execname, &pidfile);   USE_FEATURE_START_STOP_DAEMON_FANCY(,NULL)
357     );
  quiet = (opt & OPT_QUIET) && !(opt & OPT_VERBOSE);  
358    
359   if (signame) {   if (opt & OPT_s) {
360   signal_nr = get_signum(signame);   signal_nr = get_signum(signame);
361   if (signal_nr < 0) bb_show_usage();   if (signal_nr < 0) bb_show_usage();
362   }   }
363    
364   if (!startas)   if (!(opt & OPT_a))
365   startas = execname;   startas = execname;
366     if (!execname) /* in case -a is given and -x is not */
367     execname = startas;
368    
369  // USE_FEATURE_START_STOP_DAEMON_FANCY(  // USE_FEATURE_START_STOP_DAEMON_FANCY(
370  // if (retry_arg)  // if (retry_arg)
371  // retries = xatoi_u(retry_arg);  // retries = xatoi_u(retry_arg);
372  // )  // )
373   argc -= optind;   //argc -= optind;
374   argv += optind;   argv += optind;
375    
376   if (userspec) {   if (userspec) {
# Line 276  int start_stop_daemon_main(int argc, cha Line 378  int start_stop_daemon_main(int argc, cha
378   if (errno)   if (errno)
379   user_id = xuname2uid(userspec);   user_id = xuname2uid(userspec);
380   }   }
381     /* Both start and stop need to know current processes */
382     do_procinit();
383    
384   if (opt & CTX_STOP) {   if (opt & CTX_STOP) {
385   int i = do_stop();   int i = do_stop();
386   return (opt & OPT_OKNODO) ? 0 : (i<=0);   return (opt & OPT_OKNODO) ? 0 : (i <= 0);
387   }   }
388    
  do_procinit();  
   
389   if (found) {   if (found) {
390   if (!quiet)   if (!QUIET)
391   printf("%s already running\n%d\n", execname, found->pid);   printf("%s is already running\n%u\n", execname, (unsigned)found->pid);
392   return !(opt & OPT_OKNODO);   return !(opt & OPT_OKNODO);
393   }   }
394    
395    #ifdef OLDER_VERSION_OF_X
396     if (execname)
397     xstat(execname, &execstat);
398    #endif
399    
400   *--argv = startas;   *--argv = startas;
401   if (opt & OPT_BACKGROUND) {   if (opt & OPT_BACKGROUND) {
402   setsid();  #if BB_MMU
403   bb_daemonize();   bb_daemonize(DAEMON_DEVNULL_STDIO + DAEMON_CLOSE_EXTRA_FDS);
404     /* DAEMON_DEVNULL_STDIO is superfluous -
405     * it's always done by bb_daemonize() */
406    #else
407     pid_t pid = vfork();
408     if (pid < 0) /* error */
409     bb_perror_msg_and_die("vfork");
410     if (pid != 0) {
411     /* parent */
412     /* why _exit? the child may have changed the stack,
413     * so "return 0" may do bad things */
414     _exit(EXIT_SUCCESS);
415     }
416     /* Child */
417     setsid(); /* detach from controlling tty */
418     /* Redirect stdio to /dev/null, close extra FDs.
419     * We do not actually daemonize because of DAEMON_ONLY_SANITIZE */
420     bb_daemonize_or_rexec(DAEMON_DEVNULL_STDIO
421     + DAEMON_CLOSE_EXTRA_FDS
422     + DAEMON_ONLY_SANITIZE,
423     NULL /* argv, unused */ );
424    #endif
425   }   }
426   if (opt & OPT_MAKEPID) {   if (opt & OPT_MAKEPID) {
427   /* user wants _us_ to make the pidfile */   /* User wants _us_ to make the pidfile */
428   FILE *pidf = xfopen(pidfile, "w");   write_pidfile(pidfile);
   
  pid_t pidt = getpid();  
  fprintf(pidf, "%d\n", pidt);  
  fclose(pidf);  
429   }   }
430   if (chuid) {   if (opt & OPT_c) {
431   user_id = bb_strtou(chuid, NULL, 10);   struct bb_uidgid_t ugid = { -1, -1 };
432   if (errno)   parse_chown_usergroup_or_die(&ugid, chuid);
433   user_id = xuname2uid(chuid);   if (ugid.gid != (gid_t) -1) xsetgid(ugid.gid);
434   xsetuid(user_id);   if (ugid.uid != (uid_t) -1) xsetuid(ugid.uid);
435   }   }
436  #if ENABLE_FEATURE_START_STOP_DAEMON_FANCY  #if ENABLE_FEATURE_START_STOP_DAEMON_FANCY
437   if (opt & OPT_NICELEVEL) {   if (opt & OPT_NICELEVEL) {
# Line 317  int start_stop_daemon_main(int argc, cha Line 442  int start_stop_daemon_main(int argc, cha
442   }   }
443   }   }
444  #endif  #endif
445   execv(startas, argv);   execvp(startas, argv);
446   bb_perror_msg_and_die("cannot start %s", startas);   bb_perror_msg_and_die("cannot start %s", startas);
447  }  }

Legend:
Removed from v.532  
changed lines
  Added in v.816