do_command.c revision 50479
12311Sjkh/* Copyright 1988,1990,1993,1994 by Paul Vixie 22311Sjkh * All rights reserved 32311Sjkh * 42311Sjkh * Distribute freely, except: don't remove my name from the source or 52311Sjkh * documentation (don't take credit for my work), mark your changes (don't 62311Sjkh * get me blamed for your possible bugs), don't alter or remove this 72311Sjkh * notice. May be sold if buildable source is provided to buyer. No 82311Sjkh * warrantee of any kind, express or implied, is included with this 92311Sjkh * software; use at your own risk, responsibility for damages (if any) to 102311Sjkh * anyone resulting from the use of this software rests entirely with the 112311Sjkh * user. 122311Sjkh * 132311Sjkh * Send bug reports, bug fixes, enhancements, requests, flames, etc., and 142311Sjkh * I'll try to keep a version up to date. I can be reached as follows: 152311Sjkh * Paul Vixie <paul@vix.com> uunet!decwrl!vixie!paul 162311Sjkh */ 172311Sjkh 182311Sjkh#if !defined(lint) && !defined(LINT) 1929452Scharnierstatic const char rcsid[] = 2050479Speter "$FreeBSD: head/usr.sbin/cron/cron/do_command.c 50479 1999-08-28 01:35:59Z peter $"; 212311Sjkh#endif 222311Sjkh 232311Sjkh 242311Sjkh#include "cron.h" 252311Sjkh#include <sys/signal.h> 262311Sjkh#if defined(sequent) 272311Sjkh# include <sys/universe.h> 282311Sjkh#endif 292311Sjkh#if defined(SYSLOG) 302311Sjkh# include <syslog.h> 312311Sjkh#endif 3221895Sdavidn#if defined(LOGIN_CAP) 3321895Sdavidn# include <login_cap.h> 3421895Sdavidn#endif 352311Sjkh 362311Sjkh 372311Sjkhstatic void child_process __P((entry *, user *)), 382311Sjkh do_univ __P((user *)); 392311Sjkh 402311Sjkh 412311Sjkhvoid 422311Sjkhdo_command(e, u) 432311Sjkh entry *e; 442311Sjkh user *u; 452311Sjkh{ 462311Sjkh Debug(DPROC, ("[%d] do_command(%s, (%s,%d,%d))\n", 472311Sjkh getpid(), e->cmd, u->name, e->uid, e->gid)) 482311Sjkh 492311Sjkh /* fork to become asynchronous -- parent process is done immediately, 502311Sjkh * and continues to run the normal cron code, which means return to 512311Sjkh * tick(). the child and grandchild don't leave this function, alive. 522311Sjkh * 532311Sjkh * vfork() is unsuitable, since we have much to do, and the parent 542311Sjkh * needs to be able to run off and fork other processes. 552311Sjkh */ 562311Sjkh switch (fork()) { 572311Sjkh case -1: 582311Sjkh log_it("CRON",getpid(),"error","can't fork"); 592311Sjkh break; 602311Sjkh case 0: 612311Sjkh /* child process */ 622311Sjkh acquire_daemonlock(1); 632311Sjkh child_process(e, u); 642311Sjkh Debug(DPROC, ("[%d] child process done, exiting\n", getpid())) 652311Sjkh _exit(OK_EXIT); 662311Sjkh break; 672311Sjkh default: 682311Sjkh /* parent process */ 692311Sjkh break; 702311Sjkh } 712311Sjkh Debug(DPROC, ("[%d] main process returning to work\n", getpid())) 722311Sjkh} 732311Sjkh 742311Sjkh 752311Sjkhstatic void 762311Sjkhchild_process(e, u) 772311Sjkh entry *e; 782311Sjkh user *u; 792311Sjkh{ 802311Sjkh int stdin_pipe[2], stdout_pipe[2]; 812311Sjkh register char *input_data; 822311Sjkh char *usernm, *mailto; 832311Sjkh int children = 0; 8421895Sdavidn# if defined(LOGIN_CAP) 8523884Speter struct passwd *pwd; 8630895Sache login_cap_t *lc; 8721895Sdavidn# endif 882311Sjkh 892311Sjkh Debug(DPROC, ("[%d] child_process('%s')\n", getpid(), e->cmd)) 902311Sjkh 912311Sjkh /* mark ourselves as different to PS command watchers by upshifting 922311Sjkh * our program name. This has no effect on some kernels. 932311Sjkh */ 942311Sjkh /*local*/{ 952311Sjkh register char *pch; 962311Sjkh 972311Sjkh for (pch = ProgramName; *pch; pch++) 982311Sjkh *pch = MkUpper(*pch); 992311Sjkh } 1002311Sjkh 1012311Sjkh /* discover some useful and important environment settings 1022311Sjkh */ 1032311Sjkh usernm = env_get("LOGNAME", e->envp); 1042311Sjkh mailto = env_get("MAILTO", e->envp); 1057809Sache 1062311Sjkh#ifdef USE_SIGCHLD 1072311Sjkh /* our parent is watching for our death by catching SIGCHLD. we 1082311Sjkh * do not care to watch for our children's deaths this way -- we 1092311Sjkh * use wait() explictly. so we have to disable the signal (which 1102311Sjkh * was inherited from the parent). 1112311Sjkh */ 1122311Sjkh (void) signal(SIGCHLD, SIG_IGN); 1132311Sjkh#else 1142311Sjkh /* on system-V systems, we are ignoring SIGCLD. we have to stop 1152311Sjkh * ignoring it now or the wait() in cron_pclose() won't work. 1162311Sjkh * because of this, we have to wait() for our children here, as well. 1172311Sjkh */ 1182311Sjkh (void) signal(SIGCLD, SIG_DFL); 1192311Sjkh#endif /*BSD*/ 1202311Sjkh 1212311Sjkh /* create some pipes to talk to our future child 1222311Sjkh */ 1232311Sjkh pipe(stdin_pipe); /* child's stdin */ 1242311Sjkh pipe(stdout_pipe); /* child's stdout */ 1258857Srgrimes 1262311Sjkh /* since we are a forked process, we can diddle the command string 1272311Sjkh * we were passed -- nobody else is going to use it again, right? 1282311Sjkh * 1292311Sjkh * if a % is present in the command, previous characters are the 1302311Sjkh * command, and subsequent characters are the additional input to 1312311Sjkh * the command. Subsequent %'s will be transformed into newlines, 1322311Sjkh * but that happens later. 13310660Sjoerg * 13410660Sjoerg * If there are escaped %'s, remove the escape character. 1352311Sjkh */ 1362311Sjkh /*local*/{ 1372311Sjkh register int escaped = FALSE; 1382311Sjkh register int ch; 13910660Sjoerg register char *p; 1402311Sjkh 14129452Scharnier for (input_data = p = e->cmd; (ch = *input_data); 14210660Sjoerg input_data++, p++) { 14310660Sjoerg if (p != input_data) 14410660Sjoerg *p = ch; 1452311Sjkh if (escaped) { 14610660Sjoerg if (ch == '%' || ch == '\\') 14710660Sjoerg *--p = ch; 1482311Sjkh escaped = FALSE; 1492311Sjkh continue; 1502311Sjkh } 1512311Sjkh if (ch == '\\') { 1522311Sjkh escaped = TRUE; 1532311Sjkh continue; 1542311Sjkh } 1552311Sjkh if (ch == '%') { 1562311Sjkh *input_data++ = '\0'; 1572311Sjkh break; 1582311Sjkh } 1592311Sjkh } 16010660Sjoerg *p = '\0'; 1612311Sjkh } 1622311Sjkh 1632311Sjkh /* fork again, this time so we can exec the user's command. 1642311Sjkh */ 1652311Sjkh switch (vfork()) { 1662311Sjkh case -1: 1672311Sjkh log_it("CRON",getpid(),"error","can't vfork"); 1682311Sjkh exit(ERROR_EXIT); 1692311Sjkh /*NOTREACHED*/ 1702311Sjkh case 0: 1712311Sjkh Debug(DPROC, ("[%d] grandchild process Vfork()'ed\n", 1722311Sjkh getpid())) 1732311Sjkh 1742311Sjkh /* write a log message. we've waited this long to do it 1752311Sjkh * because it was not until now that we knew the PID that 1762311Sjkh * the actual user command shell was going to get and the 1772311Sjkh * PID is part of the log message. 1782311Sjkh */ 1792311Sjkh /*local*/{ 1802311Sjkh char *x = mkprints((u_char *)e->cmd, strlen(e->cmd)); 1812311Sjkh 1822311Sjkh log_it(usernm, getpid(), "CMD", x); 1832311Sjkh free(x); 1842311Sjkh } 1852311Sjkh 1862311Sjkh /* that's the last thing we'll log. close the log files. 1872311Sjkh */ 1882311Sjkh#ifdef SYSLOG 1892311Sjkh closelog(); 1902311Sjkh#endif 1912311Sjkh 1922311Sjkh /* get new pgrp, void tty, etc. 1932311Sjkh */ 1942311Sjkh (void) setsid(); 1952311Sjkh 1962311Sjkh /* close the pipe ends that we won't use. this doesn't affect 1972311Sjkh * the parent, who has to read and write them; it keeps the 1982311Sjkh * kernel from recording us as a potential client TWICE -- 1992311Sjkh * which would keep it from sending SIGPIPE in otherwise 2002311Sjkh * appropriate circumstances. 2012311Sjkh */ 2022311Sjkh close(stdin_pipe[WRITE_PIPE]); 2032311Sjkh close(stdout_pipe[READ_PIPE]); 2042311Sjkh 2052311Sjkh /* grandchild process. make std{in,out} be the ends of 2062311Sjkh * pipes opened by our daddy; make stderr go to stdout. 2072311Sjkh */ 2082311Sjkh close(STDIN); dup2(stdin_pipe[READ_PIPE], STDIN); 2092311Sjkh close(STDOUT); dup2(stdout_pipe[WRITE_PIPE], STDOUT); 2102311Sjkh close(STDERR); dup2(STDOUT, STDERR); 2112311Sjkh 2122311Sjkh /* close the pipes we just dup'ed. The resources will remain. 2132311Sjkh */ 2142311Sjkh close(stdin_pipe[READ_PIPE]); 2152311Sjkh close(stdout_pipe[WRITE_PIPE]); 2162311Sjkh 2172311Sjkh /* set our login universe. Do this in the grandchild 2182311Sjkh * so that the child can invoke /usr/lib/sendmail 2192311Sjkh * without surprises. 2202311Sjkh */ 2212311Sjkh do_univ(u); 2222311Sjkh 22321895Sdavidn# if defined(LOGIN_CAP) 22421895Sdavidn /* Set user's entire context, but skip the environment 22521895Sdavidn * as cron provides a separate interface for this 22621895Sdavidn */ 22730895Sache if ((pwd = getpwnam(usernm)) == NULL) 22830895Sache pwd = getpwuid(e->uid); 22930895Sache lc = NULL; 23030895Sache if (pwd != NULL) { 23130895Sache pwd->pw_gid = e->gid; 23230895Sache if (e->class != NULL) 23330895Sache lc = login_getclass(e->class); 23430895Sache } 23523886Speter if (pwd && 23630895Sache setusercontext(lc, pwd, e->uid, 23723886Speter LOGIN_SETALL & ~(LOGIN_SETPATH|LOGIN_SETENV)) == 0) 23823886Speter (void) endpwent(); 23923886Speter else { 24023884Speter /* fall back to the old method */ 24123886Speter (void) endpwent(); 24223884Speter# endif 24323884Speter /* set our directory, uid and gid. Set gid first, 24423884Speter * since once we set uid, we've lost root privledges. 24523884Speter */ 24623884Speter setgid(e->gid); 2472311Sjkh# if defined(BSD) 24830895Sache initgroups(usernm, e->gid); 2492311Sjkh# endif 25023884Speter setlogin(usernm); 25123884Speter setuid(e->uid); /* we aren't root after this..*/ 25223884Speter#if defined(LOGIN_CAP) 25323884Speter } 25421895Sdavidn#endif 25520573Spst chdir(env_get("HOME", e->envp)); 2562311Sjkh 2572311Sjkh /* exec the command. 2582311Sjkh */ 2592311Sjkh { 2602311Sjkh char *shell = env_get("SHELL", e->envp); 2612311Sjkh 2622311Sjkh# if DEBUGGING 2632311Sjkh if (DebugFlags & DTEST) { 2642311Sjkh fprintf(stderr, 2652311Sjkh "debug DTEST is on, not exec'ing command.\n"); 2662311Sjkh fprintf(stderr, 2672311Sjkh "\tcmd='%s' shell='%s'\n", e->cmd, shell); 2682311Sjkh _exit(OK_EXIT); 2692311Sjkh } 2702311Sjkh# endif /*DEBUGGING*/ 2712311Sjkh execle(shell, shell, "-c", e->cmd, (char *)0, e->envp); 27229452Scharnier warn("execl: couldn't exec `%s'", shell); 2732311Sjkh _exit(ERROR_EXIT); 2742311Sjkh } 2752311Sjkh break; 2762311Sjkh default: 2772311Sjkh /* parent process */ 2782311Sjkh break; 2792311Sjkh } 2802311Sjkh 2812311Sjkh children++; 2822311Sjkh 2832311Sjkh /* middle process, child of original cron, parent of process running 2842311Sjkh * the user's command. 2852311Sjkh */ 2862311Sjkh 2872311Sjkh Debug(DPROC, ("[%d] child continues, closing pipes\n", getpid())) 2882311Sjkh 2892311Sjkh /* close the ends of the pipe that will only be referenced in the 2902311Sjkh * grandchild process... 2912311Sjkh */ 2922311Sjkh close(stdin_pipe[READ_PIPE]); 2932311Sjkh close(stdout_pipe[WRITE_PIPE]); 2942311Sjkh 2952311Sjkh /* 2962311Sjkh * write, to the pipe connected to child's stdin, any input specified 2972311Sjkh * after a % in the crontab entry. while we copy, convert any 2982311Sjkh * additional %'s to newlines. when done, if some characters were 2992311Sjkh * written and the last one wasn't a newline, write a newline. 3002311Sjkh * 3012311Sjkh * Note that if the input data won't fit into one pipe buffer (2K 3022311Sjkh * or 4K on most BSD systems), and the child doesn't read its stdin, 3032311Sjkh * we would block here. thus we must fork again. 3042311Sjkh */ 3052311Sjkh 3062311Sjkh if (*input_data && fork() == 0) { 3072311Sjkh register FILE *out = fdopen(stdin_pipe[WRITE_PIPE], "w"); 3082311Sjkh register int need_newline = FALSE; 3092311Sjkh register int escaped = FALSE; 3102311Sjkh register int ch; 3112311Sjkh 3122311Sjkh Debug(DPROC, ("[%d] child2 sending data to grandchild\n", getpid())) 3132311Sjkh 3142311Sjkh /* close the pipe we don't use, since we inherited it and 3152311Sjkh * are part of its reference count now. 3162311Sjkh */ 3172311Sjkh close(stdout_pipe[READ_PIPE]); 3182311Sjkh 3192311Sjkh /* translation: 3202311Sjkh * \% -> % 3212311Sjkh * % -> \n 3222311Sjkh * \x -> \x for all x != % 3232311Sjkh */ 32429452Scharnier while ((ch = *input_data++)) { 3252311Sjkh if (escaped) { 3262311Sjkh if (ch != '%') 3272311Sjkh putc('\\', out); 3282311Sjkh } else { 3292311Sjkh if (ch == '%') 3302311Sjkh ch = '\n'; 3312311Sjkh } 3322311Sjkh 3332311Sjkh if (!(escaped = (ch == '\\'))) { 3342311Sjkh putc(ch, out); 3352311Sjkh need_newline = (ch != '\n'); 3362311Sjkh } 3372311Sjkh } 3382311Sjkh if (escaped) 3392311Sjkh putc('\\', out); 3402311Sjkh if (need_newline) 3412311Sjkh putc('\n', out); 3422311Sjkh 3432311Sjkh /* close the pipe, causing an EOF condition. fclose causes 3442311Sjkh * stdin_pipe[WRITE_PIPE] to be closed, too. 3452311Sjkh */ 3462311Sjkh fclose(out); 3472311Sjkh 3482311Sjkh Debug(DPROC, ("[%d] child2 done sending to grandchild\n", getpid())) 3492311Sjkh exit(0); 3502311Sjkh } 3512311Sjkh 3522311Sjkh /* close the pipe to the grandkiddie's stdin, since its wicked uncle 3532311Sjkh * ernie back there has it open and will close it when he's done. 3542311Sjkh */ 3552311Sjkh close(stdin_pipe[WRITE_PIPE]); 3562311Sjkh 3572311Sjkh children++; 3582311Sjkh 3592311Sjkh /* 3602311Sjkh * read output from the grandchild. it's stderr has been redirected to 3612311Sjkh * it's stdout, which has been redirected to our pipe. if there is any 3622311Sjkh * output, we'll be mailing it to the user whose crontab this is... 3632311Sjkh * when the grandchild exits, we'll get EOF. 3642311Sjkh */ 3652311Sjkh 3662311Sjkh Debug(DPROC, ("[%d] child reading output from grandchild\n", getpid())) 3672311Sjkh 3682311Sjkh /*local*/{ 3692311Sjkh register FILE *in = fdopen(stdout_pipe[READ_PIPE], "r"); 3702311Sjkh register int ch = getc(in); 3712311Sjkh 3722311Sjkh if (ch != EOF) { 3732311Sjkh register FILE *mail; 3742311Sjkh register int bytes = 1; 3752311Sjkh int status = 0; 3762311Sjkh 3772311Sjkh Debug(DPROC|DEXT, 3782311Sjkh ("[%d] got data (%x:%c) from grandchild\n", 3792311Sjkh getpid(), ch, ch)) 3802311Sjkh 3812311Sjkh /* get name of recipient. this is MAILTO if set to a 3822311Sjkh * valid local username; USER otherwise. 3832311Sjkh */ 3842311Sjkh if (mailto) { 3852311Sjkh /* MAILTO was present in the environment 3862311Sjkh */ 3872311Sjkh if (!*mailto) { 3882311Sjkh /* ... but it's empty. set to NULL 3892311Sjkh */ 3902311Sjkh mailto = NULL; 3912311Sjkh } 3922311Sjkh } else { 3932311Sjkh /* MAILTO not present, set to USER. 3942311Sjkh */ 3952311Sjkh mailto = usernm; 3962311Sjkh } 3978857Srgrimes 3982311Sjkh /* if we are supposed to be mailing, MAILTO will 3992311Sjkh * be non-NULL. only in this case should we set 4002311Sjkh * up the mail command and subjects and stuff... 4012311Sjkh */ 4022311Sjkh 4032311Sjkh if (mailto) { 4042311Sjkh register char **env; 4052311Sjkh auto char mailcmd[MAX_COMMAND]; 4062311Sjkh auto char hostname[MAXHOSTNAMELEN]; 4072311Sjkh 4082311Sjkh (void) gethostname(hostname, MAXHOSTNAMELEN); 40920573Spst (void) snprintf(mailcmd, sizeof(mailcmd), 41020573Spst MAILARGS, MAILCMD); 4112311Sjkh if (!(mail = cron_popen(mailcmd, "w"))) { 41229452Scharnier warn("%s", MAILCMD); 4132311Sjkh (void) _exit(ERROR_EXIT); 4142311Sjkh } 4152311Sjkh fprintf(mail, "From: root (Cron Daemon)\n"); 4162311Sjkh fprintf(mail, "To: %s\n", mailto); 4172311Sjkh fprintf(mail, "Subject: Cron <%s@%s> %s\n", 4182311Sjkh usernm, first_word(hostname, "."), 4192311Sjkh e->cmd); 4202311Sjkh# if defined(MAIL_DATE) 4212311Sjkh fprintf(mail, "Date: %s\n", 4222311Sjkh arpadate(&TargetTime)); 4232311Sjkh# endif /* MAIL_DATE */ 4242311Sjkh for (env = e->envp; *env; env++) 4252311Sjkh fprintf(mail, "X-Cron-Env: <%s>\n", 4262311Sjkh *env); 4272311Sjkh fprintf(mail, "\n"); 4282311Sjkh 4292311Sjkh /* this was the first char from the pipe 4302311Sjkh */ 4312311Sjkh putc(ch, mail); 4322311Sjkh } 4332311Sjkh 4342311Sjkh /* we have to read the input pipe no matter whether 4352311Sjkh * we mail or not, but obviously we only write to 4362311Sjkh * mail pipe if we ARE mailing. 4372311Sjkh */ 4382311Sjkh 4392311Sjkh while (EOF != (ch = getc(in))) { 4402311Sjkh bytes++; 4412311Sjkh if (mailto) 4422311Sjkh putc(ch, mail); 4432311Sjkh } 4442311Sjkh 4452311Sjkh /* only close pipe if we opened it -- i.e., we're 4462311Sjkh * mailing... 4472311Sjkh */ 4482311Sjkh 4492311Sjkh if (mailto) { 4502311Sjkh Debug(DPROC, ("[%d] closing pipe to mail\n", 4512311Sjkh getpid())) 4522311Sjkh /* Note: the pclose will probably see 4532311Sjkh * the termination of the grandchild 4542311Sjkh * in addition to the mail process, since 4552311Sjkh * it (the grandchild) is likely to exit 4562311Sjkh * after closing its stdout. 4572311Sjkh */ 4582311Sjkh status = cron_pclose(mail); 4592311Sjkh } 4602311Sjkh 4612311Sjkh /* if there was output and we could not mail it, 4622311Sjkh * log the facts so the poor user can figure out 4632311Sjkh * what's going on. 4642311Sjkh */ 4652311Sjkh if (mailto && status) { 4662311Sjkh char buf[MAX_TEMPSTR]; 4672311Sjkh 46820573Spst snprintf(buf, sizeof(buf), 4692311Sjkh "mailed %d byte%s of output but got status 0x%04x\n", 4702311Sjkh bytes, (bytes==1)?"":"s", 4712311Sjkh status); 4722311Sjkh log_it(usernm, getpid(), "MAIL", buf); 4732311Sjkh } 4742311Sjkh 4752311Sjkh } /*if data from grandchild*/ 4762311Sjkh 4772311Sjkh Debug(DPROC, ("[%d] got EOF from grandchild\n", getpid())) 4782311Sjkh 4792311Sjkh fclose(in); /* also closes stdout_pipe[READ_PIPE] */ 4802311Sjkh } 4812311Sjkh 4822311Sjkh /* wait for children to die. 4832311Sjkh */ 4842311Sjkh for (; children > 0; children--) 4852311Sjkh { 4862311Sjkh WAIT_T waiter; 4872311Sjkh PID_T pid; 4882311Sjkh 4892311Sjkh Debug(DPROC, ("[%d] waiting for grandchild #%d to finish\n", 4902311Sjkh getpid(), children)) 4912311Sjkh pid = wait(&waiter); 4922311Sjkh if (pid < OK) { 4932311Sjkh Debug(DPROC, ("[%d] no more grandchildren--mail written?\n", 4942311Sjkh getpid())) 4952311Sjkh break; 4962311Sjkh } 4972311Sjkh Debug(DPROC, ("[%d] grandchild #%d finished, status=%04x", 4982311Sjkh getpid(), pid, WEXITSTATUS(waiter))) 4992311Sjkh if (WIFSIGNALED(waiter) && WCOREDUMP(waiter)) 5002311Sjkh Debug(DPROC, (", dumped core")) 5012311Sjkh Debug(DPROC, ("\n")) 5022311Sjkh } 5032311Sjkh} 5042311Sjkh 5052311Sjkh 5062311Sjkhstatic void 5072311Sjkhdo_univ(u) 5082311Sjkh user *u; 5092311Sjkh{ 5102311Sjkh#if defined(sequent) 5112311Sjkh/* Dynix (Sequent) hack to put the user associated with 5122311Sjkh * the passed user structure into the ATT universe if 5132311Sjkh * necessary. We have to dig the gecos info out of 5142311Sjkh * the user's password entry to see if the magic 5152311Sjkh * "universe(att)" string is present. 5162311Sjkh */ 5172311Sjkh 5182311Sjkh struct passwd *p; 5192311Sjkh char *s; 5202311Sjkh int i; 5212311Sjkh 5222311Sjkh p = getpwuid(u->uid); 5232311Sjkh (void) endpwent(); 5242311Sjkh 5252311Sjkh if (p == NULL) 5262311Sjkh return; 5272311Sjkh 5282311Sjkh s = p->pw_gecos; 5292311Sjkh 5302311Sjkh for (i = 0; i < 4; i++) 5312311Sjkh { 5322311Sjkh if ((s = strchr(s, ',')) == NULL) 5332311Sjkh return; 5342311Sjkh s++; 5352311Sjkh } 5362311Sjkh if (strcmp(s, "universe(att)")) 5372311Sjkh return; 5382311Sjkh 5392311Sjkh (void) universe(U_ATT); 5402311Sjkh#endif 5412311Sjkh} 542