daemonize.c
c03cc017
 /*
53c7e0f1
  * Copyright (C) 2001-2003 FhG Fokus
c03cc017
  *
83e91df1
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
c03cc017
  *
83e91df1
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
c03cc017
  */
6a0f4382
 
1d0661db
 /*!
  * \file
88693a29
  * \brief Kamailio core :: Daemon init
1d0661db
  * \ingroup core
  * Module: \ref core
  */
 
c03cc017
 
9a428799
 
c03cc017
 #include <sys/types.h>
8d0543b9
 
5a03489e
 #define _XOPEN_SOURCE   /*!< needed on linux for the  getpgid prototype,  but
8d0543b9
                            openbsd 3.2 won't include common types (uint a.s.o)
                            if defined before including sys/types.h */
5a03489e
 #define _XOPEN_SOURCE_EXTENDED /*!< same as \ref _XOPEN_SOURCE */
 #define __USE_XOPEN_EXTENDED /*!< same as \ref _XOPEN_SOURCE, overrides features.h */
 #define __EXTENSIONS__ /*!< needed on solaris: if XOPEN_SOURCE is defined
8d0543b9
                           struct timeval defintion from <sys/time.h> won't
                           be included => workarround define _EXTENSIONS_
                            -andrei */
c03cc017
 #include <signal.h>
 #include <syslog.h>
 #include <errno.h>
 #include <string.h>
 #include <stdio.h>
 #include <stdlib.h>
c5e38877
 #include <sys/time.h>    
385c63eb
 #include <sys/resource.h> /* setrlimit */
88d3fa6d
 #include <unistd.h>
d32f7ac4
 #include <pwd.h>
 #include <grp.h>
88d3fa6d
 
f4136d77
 #ifdef __OS_linux
 #include <sys/prctl.h>
 #endif
 
71a0a583
 #ifdef HAVE_SCHED_SETSCHEDULER
 #include <sched.h>
 #endif
 
 #ifdef _POSIX_MEMLOCK
 #define HAVE_MLOCKALL
 #include <sys/mman.h>
 #endif
 
c03cc017
 #include "daemonize.h"
 #include "globals.h"
 #include "dprint.h"
bc404f2b
 #include "signals.h"
0d09b88d
 #include "cfg/cfg.h"
c03cc017
 
 
 #define MAX_FD 32 /* maximum number of inherited open file descriptors,
 		    (normally it shouldn't  be bigger  than 3) */
 
83e91df1
 /** temporary pipe FDs for sending exit status back to the ancestor process.
  * This pipe is used to send the desired exit status to the initial process,
  * that waits for it in the foreground. This way late errors preventing
  * startup (e.g. during modules child inits or TCP late init) can still be
  * reported back.
  */
 static int daemon_status_fd[2];
 
 
 
 /** init daemon status reporting.
  * Must be called before any other daemon_status function has a chance to
  * run.
  */
 void daemon_status_init()
 {
 	daemon_status_fd[0] = -1;
 	daemon_status_fd[1] = -1;
 }
 
 
 
 /** pre-daemonize init for daemon status reporting.
  * Must be called before forking.
  * Typically the parent process will call daemon_status_wait() while
  * one of the children will call daemon_status_send() at some point.
  *
  * @return 0 on success, -1 on error (and sets errno).
  */
abb01fb4
 int daemon_status_pre_daemonize(void)
83e91df1
 {
 	int ret;
 	
 retry:
 	ret = pipe(daemon_status_fd);
 	if (ret < 0 && errno == EINTR)
 		goto retry;
 	return ret;
 }
 
 
 
 /** wait for an exit status to be send by daemon_status_send().
  * @param status - filled with the sent status (a char).
  * @return  0 on success, -1 on error (e.g. process died before sending
  *          status, not intialized a.s.o.).
  * Side-effects: it will close the write side of the pipe
  *  (must not be used from the same process as the daemon_status_send()).
  * Note: if init is not complete (only init, but no pre-daemonize)
  * it will return success always and status 0.
  */
 int daemon_status_wait(char* status)
 {
 	int ret;
 	
 	/* close the output side of the pipe */
 	if (daemon_status_fd[1] != -1) {
 		close(daemon_status_fd[1]);
 		daemon_status_fd[1] = -1;
 	}
 	if (daemon_status_fd[0] == -1) {
 		*status = 0;
 		return -1;
 	}
 retry:
 	ret = read(daemon_status_fd[0], status, 1);
 	if (ret < 0 && errno == EINTR)
 		goto retry;
 	return (ret ==1 ) ? 0 : -1;
 }
 
 
 
 /** send 'status' to a waiting process running daemon_status_wait().
  * @param status - status byte
  * @return 0 on success, -1 on error.
  * Note: if init is not complete (only init, but no pre-daemonize)
  * it will return success always.
  */
 int daemon_status_send(char status)
 {
 	int ret;
 
 	if (daemon_status_fd[1] == -1)
 		return 0;
 retry:
 	ret = write(daemon_status_fd[1], &status, 1);
 	if (ret < 0 && errno == EINTR)
 		goto retry;
 	return (ret ==1 ) ? 0 : -1;
 }
 
 
 
 /** cleanup functions for new processes.
  * Should be called after fork(), for each new process that _does_ _not_
  * use  daemon_status_send() or daemon_status_wait().
  */
 void daemon_status_on_fork_cleanup()
 {
 	if (daemon_status_fd[0] != -1) {
 		close(daemon_status_fd[0]);
 		daemon_status_fd[0] = -1;
 	}
 	if (daemon_status_fd[1] != -1) {
 		close(daemon_status_fd[1]);
 		daemon_status_fd[1] = -1;
 	}
 }
 
 
 
 /** cleanup functions for processes that don't intead to wait.
  * Should be called after fork(), for each new process that doesn't
  * use daemon_status_wait().
  */
 void daemon_status_no_wait()
 {
 	if (daemon_status_fd[0] != -1) {
 		close(daemon_status_fd[0]);
 		daemon_status_fd[0] = -1;
 	}
 }
 
 
f4136d77
 /**
  * enable dumpable flag for core dumping after setuid() & friends
  * @return 0 when no critical error occured, -1 on such error
  */
 int enable_dumpable(void)
 {
 #ifdef __OS_linux
 	struct rlimit lim;
 	/* re-enable core dumping on linux after setuid() & friends */
 	if(disable_core_dump==0) {
 		LM_DBG("trying enable core dumping...\n");
 		if(prctl(PR_GET_DUMPABLE, 0, 0, 0, 0)<=0) {
 			LM_DBG("core dumping is disabled now...\n");
 			if(prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)<0) {
 				LM_WARN("cannot re-enable core dumping!\n");
 			} else {
 				LM_DBG("core dumping has just been enabled...\n");
 				if (getrlimit(RLIMIT_CORE, &lim)<0){
ccdf1c61
 					LM_CRIT( "cannot get the maximum core size: %s\n",
f4136d77
 							strerror(errno));
 					return -1;
 				} else {
ccdf1c61
 					LM_DBG("current core file limit: %lu (max: %lu)\n",
f4136d77
 						(unsigned long)lim.rlim_cur, (unsigned long)lim.rlim_max);
 				}
 			}
 		} else {
 			LM_DBG("core dumping is enabled now (%d)...\n",
 					prctl(PR_GET_DUMPABLE, 0, 0, 0, 0));
 		}
 	}
 #endif
 	return 0;
 }
83e91df1
 
 /** daemon init.
  *@param name - daemon name used for logging (used when opening syslog).
  *@param status_wait  - if 1 the original process will wait until it gets
  *                  an exit code send using daemon_status_send().
  *@return 0 in the child process (in case of daemonize mode),
  *        -1 on error.
  * The original process that called daemonize() will be terminated if
  * dont_daemonize == 0. The exit code depends on status_wait. If status_wait
  * is non-zero, the original process will wait for a status code, that
  * must be sent with daemon_status_send() (daemon_status_send() must be
  * called or the original process will remain waiting until all the children
  * close()). If status_wait is 0, the original process will exit immediately
  * with exit(0).
  * Global variables/config params used:
  * dont_daemonize
  * chroot_dir
  * working_dir
  * pid_file - if set the pid will be written here (ascii).
  * pgid_file - if set, the pgid will be written here (ascii).
  * log_stderr - if not set syslog will be opened (openlog(name,...))
  * 
  *
  * Side-effects:
  *  sets own_pgid after becoming session leader (own process group).
 */
 int daemonize(char*  name,  int status_wait)
c03cc017
 {
 	FILE *pid_stream;
 	pid_t pid;
 	int r, p;
83e91df1
 	char pipe_status;
c8737586
 	uid_t pid_uid;
 	gid_t pid_gid;
 
 	if(uid) pid_uid = uid;
 	else pid_uid = -1;
 
 	if(gid) pid_gid = gid;
 	else pid_gid = -1;
c03cc017
 
 	p=-1;
bb7ed2d1
 	/* flush std file descriptors to avoid flushes after fork
 	 *  (same message appearing multiple times)
 	 *  and switch to unbuffered
 	 */
 	setbuf(stdout, 0);
 	setbuf(stderr, 0);
c03cc017
 	if (chroot_dir&&(chroot(chroot_dir)<0)){
ccdf1c61
 		LM_CRIT("Cannot chroot to %s: %s\n", chroot_dir, strerror(errno));
c03cc017
 		goto error;
 	}
 	
 	if (chdir(working_dir)<0){
ccdf1c61
 		LM_CRIT("cannot chdir to %s: %s\n", working_dir, strerror(errno));
c03cc017
 		goto error;
 	}
 
bc404f2b
 	if (!dont_daemonize) {
83e91df1
 		if (status_wait) {
 			if (daemon_status_pre_daemonize() < 0)
 				goto error;
 		}
bc404f2b
 		/* fork to become!= group leader*/
 		if ((pid=fork())<0){
ccdf1c61
 			LM_CRIT("Cannot fork:%s\n", strerror(errno));
bc404f2b
 			goto error;
83e91df1
 		}else if (pid!=0){
 			if (status_wait) {
 				if (daemon_status_wait(&pipe_status) == 0)
 					exit((int)pipe_status);
 				else{
ccdf1c61
 					LM_ERR("Main process exited before writing to pipe\n");
83e91df1
 					exit(-1);
 				}
 			}
 			exit(0);
bc404f2b
 		}
83e91df1
 		if (status_wait)
 			daemon_status_no_wait(); /* clean unused read fd */
bc404f2b
 		/* become session leader to drop the ctrl. terminal */
 		if (setsid()<0){
ccdf1c61
 			LM_WARN("setsid failed: %s\n",strerror(errno));
bc404f2b
 		}else{
 			own_pgid=1;/* we have our own process group */
 		}
 		/* fork again to drop group  leadership */
 		if ((pid=fork())<0){
ccdf1c61
 			LM_CRIT("Cannot  fork:%s\n", strerror(errno));
bc404f2b
 			goto error;
 		}else if (pid!=0){
 			/*parent process => exit */
 			exit(0);
 		}
c03cc017
 	}
f4136d77
 
 	if(enable_dumpable()<0)
 		goto error;
 
c03cc017
 	/* added by noh: create a pid file for the main process */
 	if (pid_file!=0){
 		
 		if ((pid_stream=fopen(pid_file, "r"))!=NULL){
5dbe4f22
 			if (fscanf(pid_stream, "%d", &p) < 0) {
 				LM_WARN("could not parse pid file %s\n", pid_file);
 			}
c03cc017
 			fclose(pid_stream);
 			if (p==-1){
ccdf1c61
 				LM_CRIT("pid file %s exists, but doesn't contain a valid"
c03cc017
 					" pid number\n", pid_file);
 				goto error;
 			}
 			if (kill((pid_t)p, 0)==0 || errno==EPERM){
ccdf1c61
 				LM_CRIT("running process found in the pid file %s\n",
c03cc017
 					pid_file);
 				goto error;
 			}else{
ccdf1c61
 				LM_WARN("pid file contains old pid, replacing pid\n");
c03cc017
 			}
 		}
 		pid=getpid();
 		if ((pid_stream=fopen(pid_file, "w"))==NULL){
ccdf1c61
 			LM_WARN("unable to create pid file %s: %s\n", 
c03cc017
 				pid_file, strerror(errno));
 			goto error;
 		}else{
 			fprintf(pid_stream, "%i\n", (int)pid);
 			fclose(pid_stream);
c8737586
 			if(chown(pid_file, pid_uid, pid_gid)<0) {
 				LM_ERR("failed to chwon PID file: %s\n", strerror(errno));
 				goto error;
 			}
c03cc017
 		}
 	}
9a428799
 
 	if (pgid_file!=0){
 		if ((pid_stream=fopen(pgid_file, "r"))!=NULL){
5dbe4f22
 			if (fscanf(pid_stream, "%d", &p) < 0) {
 				 LM_WARN("could not parse pgid file %s\n", pgid_file);
 			}
9a428799
 			fclose(pid_stream);
 			if (p==-1){
ccdf1c61
 				LM_CRIT("pgid file %s exists, but doesn't contain a valid"
9a428799
 				    " pgid number\n", pgid_file);
 				goto error;
 			}
 		}
4355dd50
 		if (own_pgid){
 			pid=getpgid(0);
 			if ((pid_stream=fopen(pgid_file, "w"))==NULL){
ccdf1c61
 				LM_WARN("unable to create pgid file %s: %s\n",
4355dd50
 					pgid_file, strerror(errno));
 				goto error;
 			}else{
 				fprintf(pid_stream, "%i\n", (int)pid);
 				fclose(pid_stream);
c8737586
 				if(chown(pid_file, pid_uid, pid_gid)<0) {
 					LM_ERR("failed to chwon PGID file: %s\n", strerror(errno));
 					goto error;
 				}
4355dd50
 			}
9a428799
 		}else{
ccdf1c61
 			LM_WARN("we don't have our own process so we won't save"
4355dd50
 					" our pgid\n");
 			unlink(pgid_file); /* just to be sure nobody will miss-use the old
 								  value*/
9a428799
 		}
 	}
c03cc017
 	
 	/* try to replace stdin, stdout & stderr with /dev/null */
 	if (freopen("/dev/null", "r", stdin)==0){
ccdf1c61
 		LM_ERR("unable to replace stdin with /dev/null: %s\n",
c03cc017
 				strerror(errno));
 		/* continue, leave it open */
 	};
 	if (freopen("/dev/null", "w", stdout)==0){
ccdf1c61
 		LM_ERR("unable to replace stdout with /dev/null: %s\n",
c03cc017
 				strerror(errno));
 		/* continue, leave it open */
 	};
 	/* close stderr only if log_stderr=0 */
 	if ((!log_stderr) &&(freopen("/dev/null", "w", stderr)==0)){
ccdf1c61
 		LM_ERR("unable to replace stderr with /dev/null: %s\n",
c03cc017
 				strerror(errno));
 		/* continue, leave it open */
 	};
 	
83e91df1
 	/* close all but the daemon_status_fd output as the main process
9167c186
 	  must still write into it to tell the parent to exit with 0 */
c03cc017
 	closelog();
 	for (r=3;r<MAX_FD; r++){
83e91df1
 		if(r !=  daemon_status_fd[1])
c03cc017
 			close(r);
 	}
 	
 	if (log_stderr==0)
0d09b88d
 		openlog(name, LOG_PID|LOG_CONS, cfg_get(core, core_cfg, log_facility));
c03cc017
 		/* LOG_CONS, LOG_PERRROR ? */
 
 	return  0;
 
 error:
 	return -1;
 }
 
 
 
 int do_suid()
 {
d32f7ac4
 	struct passwd *pw;
 	
71fd3ebd
 	if (gid){
241ac281
 		if(gid!=getgid()) {
 			if(setgid(gid)<0){
 				LM_CRIT("cannot change gid to %d: %s\n", gid, strerror(errno));
 				goto error;
 			}
71fd3ebd
 		}
c03cc017
 	}
 	
71fd3ebd
 	if(uid){
d32f7ac4
 		if (!(pw = getpwuid(uid))){
ccdf1c61
 			LM_CRIT("user lookup failed: %s\n", strerror(errno));
d32f7ac4
 			goto error;
 		}
241ac281
 		if(uid!=getuid()) {
df92a532
 			if(initgroups(pw->pw_name, pw->pw_gid)<0){
 				LM_CRIT("cannot set supplementary groups: %s\n", 
 							strerror(errno));
 				goto error;
 			}
241ac281
 			if(setuid(uid)<0){
 				LM_CRIT("cannot change uid to %d: %s\n", uid, strerror(errno));
 				goto error;
 			}
71fd3ebd
 		}
c03cc017
 	}
f4136d77
 
 	if(enable_dumpable()<0)
 		goto error;
 
c03cc017
 	return 0;
 error:
 	return -1;
 }
 
 
385c63eb
 
5a03489e
 /*! \brief try to increase the open file limit */
385c63eb
 int increase_open_fds(int target)
 {
 	struct rlimit lim;
 	struct rlimit orig;
 	
 	if (getrlimit(RLIMIT_NOFILE, &lim)<0){
ccdf1c61
 		LM_CRIT("cannot get the maximum number of file descriptors: %s\n",
385c63eb
 				strerror(errno));
 		goto error;
 	}
 	orig=lim;
2571b99c
 	LM_DBG("current open file limits: %lu/%lu\n",
385c63eb
 			(unsigned long)lim.rlim_cur, (unsigned long)lim.rlim_max);
 	if ((lim.rlim_cur==RLIM_INFINITY) || (target<=lim.rlim_cur))
 		/* nothing to do */
 		goto done;
 	else if ((lim.rlim_max==RLIM_INFINITY) || (target<=lim.rlim_max)){
 		lim.rlim_cur=target; /* increase soft limit to target */
 	}else{
 		/* more than the hard limit */
ccdf1c61
 		LM_INFO("trying to increase the open file limit"
385c63eb
 				" past the hard limit (%ld -> %d)\n", 
 				(unsigned long)lim.rlim_max, target);
 		lim.rlim_max=target;
 		lim.rlim_cur=target;
 	}
2571b99c
 	LM_DBG("increasing open file limits to: %lu/%lu\n",
385c63eb
 			(unsigned long)lim.rlim_cur, (unsigned long)lim.rlim_max);
 	if (setrlimit(RLIMIT_NOFILE, &lim)<0){
ccdf1c61
 		LM_CRIT("cannot increase the open file limit to"
385c63eb
 				" %lu/%lu: %s\n",
 				(unsigned long)lim.rlim_cur, (unsigned long)lim.rlim_max,
 				strerror(errno));
 		if (orig.rlim_max>orig.rlim_cur){
 			/* try to increase to previous maximum, better than not increasing
 		 	* at all */
 			lim.rlim_max=orig.rlim_max;
 			lim.rlim_cur=orig.rlim_max;
 			if (setrlimit(RLIMIT_NOFILE, &lim)==0){
ccdf1c61
 				LM_CRIT(" maximum number of file descriptors increased to"
385c63eb
 						" %u\n",(unsigned)orig.rlim_max);
 			}
 		}
 		goto error;
 	}
 done:
 	return 0;
 error:
 	return -1;
 }
 
 
 
5a03489e
 /*! \brief enable core dumps */
f4136d77
 int set_core_dump(int enable, long unsigned int size)
385c63eb
 {
 	struct rlimit lim;
 	struct rlimit newlim;
 	
 	if (enable){
 		if (getrlimit(RLIMIT_CORE, &lim)<0){
ccdf1c61
 			LM_CRIT("cannot get the maximum core size: %s\n",
385c63eb
 					strerror(errno));
 			goto error;
 		}
 		if (lim.rlim_cur<size){
 			/* first try max limits */
 			newlim.rlim_max=RLIM_INFINITY;
 			newlim.rlim_cur=newlim.rlim_max;
 			if (setrlimit(RLIMIT_CORE, &newlim)==0) goto done;
 			/* now try with size */
 			if (lim.rlim_max<size){
 				newlim.rlim_max=size;
 			}
 			newlim.rlim_cur=newlim.rlim_max;
 			if (setrlimit(RLIMIT_CORE, &newlim)==0) goto done;
 			/* if this failed too, try rlim_max, better than nothing */
 			newlim.rlim_max=lim.rlim_max;
 			newlim.rlim_cur=newlim.rlim_max;
 			if (setrlimit(RLIMIT_CORE, &newlim)<0){
ccdf1c61
 				LM_CRIT("could increase core limits at all: %s\n",
385c63eb
 						strerror (errno));
 			}else{
ccdf1c61
 				LM_CRIT("core limits increased only to %lu\n",
385c63eb
 						(unsigned long)lim.rlim_max);
 			}
 			goto error; /* it's an error we haven't got the size we wanted*/
f4136d77
 		}else{
 			newlim.rlim_cur=lim.rlim_cur;
 			newlim.rlim_max=lim.rlim_max;
 			goto done; /*nothing to do */
385c63eb
 		}
 	}else{
 		/* disable */
 		newlim.rlim_cur=0;
 		newlim.rlim_max=0;
 		if (setrlimit(RLIMIT_CORE, &newlim)<0){
ccdf1c61
 			LM_CRIT("failed to disable core dumps: %s\n",
385c63eb
 					strerror(errno));
 			goto error;
 		}
 	}
 done:
2571b99c
 	LM_DBG("core dump limits set to %lu\n", (unsigned long)newlim.rlim_cur);
385c63eb
 	return 0;
 error:
 	return -1;
 }
71a0a583
 
 
 
5a03489e
 /*! \brief lock pages in memory (make the process not swapable) */
71a0a583
 int mem_lock_pages()
 {
 #ifdef HAVE_MLOCKALL
 	if (mlockall(MCL_CURRENT|MCL_FUTURE) !=0){
ccdf1c61
 		LM_WARN("failed to lock the memory pages (disable swap): %s [%d]\n",
71a0a583
 				strerror(errno), errno);
 		goto error;
 	}
 	return 0;
 error:
 	return -1;
 #else /* if MLOCKALL not defined return error */
ccdf1c61
 		LM_WARN("failed to lock the memory pages: no mlockall support\n");
71a0a583
 	return -1;
 #endif 
 }
 
 
5a03489e
 /*! \brief tries to set real time priority 
71a0a583
  * policy: 0 - SCHED_OTHER, 1 - SCHED_RR, 2 - SCHED_FIFO */
 int set_rt_prio(int prio, int policy)
 {
 #ifdef HAVE_SCHED_SETSCHEDULER
 	struct sched_param sch_p;
 	int min_prio, max_prio;
 	int sched_policy;
 	
 	switch(policy){
 		case 0:
 			sched_policy=SCHED_OTHER;
 			break;
 		case 1:
 			sched_policy=SCHED_RR;
 			break;
 		case 2:
 			sched_policy=SCHED_FIFO;
 			break;
 		default:
b908f660
 			LM_WARN("invalid scheduling policy,using SCHED_OTHER\n");
71a0a583
 			sched_policy=SCHED_OTHER;
 	}
 	memset(&sch_p, 0, sizeof(sch_p));
 	max_prio=sched_get_priority_max(policy);
 	min_prio=sched_get_priority_min(policy);
 	if (prio<min_prio){
ccdf1c61
 		LM_WARN("scheduling priority %d too small, using minimum value"
71a0a583
 					" (%d)\n", prio, min_prio);
 		prio=min_prio;
 	}else if (prio>max_prio){
ccdf1c61
 		LM_WARN("scheduling priority %d too big, using maximum value"
71a0a583
 					" (%d)\n", prio, max_prio);
 		prio=max_prio;
 	}
 	sch_p.sched_priority=prio;
 	if (sched_setscheduler(0, sched_policy, &sch_p) != 0){
ccdf1c61
 		LM_WARN("could not switch to real time priority: %s [%d]\n",
71a0a583
 					strerror(errno), errno);
 		return -1;
 	};
 	return 0;
 #else
ccdf1c61
 	LM_WARN("real time support not available\n");
71a0a583
 	return -1;
 #endif
 }