/* Main.c */ #define _main_c_ 1 /* Read the "COPYING" file for licensing terms. */ #include "Sys.h" #if (LOCK_METHOD == 2) # ifdef HAVE_SYS_FILE_H # include # endif #endif #ifdef SYSLOG # include #endif #include #include #include #ifdef __FreeBSD__ #include #endif #include #include #include #include "Util.h" #include "Main.h" #include "Cmds.h" #include "Open.h" #include "Cmdline.h" #include "DateSize.h" #include "Bookmark.h" #include "Prefs.h" #include "FTP.h" #include "Glob.h" #include "Getopt.h" #include "Xfer.h" #include "Complete.h" #include "Tips.h" #include "Version.h" /* We need to know our fully qualified hostname. That way we can give * a complete email address as a password for anonymous logins. */ string gOurHostName; /* The user's full email address, which we use for the password on * anonymous logins. */ string gEmailAddress; /* This is what we'll actually use for the anonymous password. * We keep an unmodified copy of gEmailAddress, and let the user * change the variable below if they want to. */ string gAnonPassword; /* Since we create a bunch of files, instead of making them all * .dot files in the home directory, we have our own .dot directory * and keep all our stuff in there. */ string gOurDirectoryPath; /* We keep some basic information about the user. Most of the structure * is just a copy of the user's password file entry. Instead of keeping * just that pointer from getpwnam(), we make our own copy so we can * use getpwnam() again later. */ UserInfo gUserInfo; /* Boolean indicating whether input is coming from a live user or * a file. */ int gIsFromTTY; /* Boolean indicating whether output is going to the screen or to * a file. */ int gIsToTTY; /* Boolean indicating whether input is coming from a script file. * This is really just a copy of gIsFromTTY. */ int gDoingScript; /* A FILE pointer to the user's usage log, or NULL if we're not * logging anything. */ FILE *gLogFile = NULL; /* We can optionally log all the debugging information to a separate * log file. */ FILE *gTraceLogFile = NULL; /* Full pathname of the user's usage log file, in our .dot directory. */ longstring gLogFileName; /* Maximum size we will let the log grow to before making it smaller. * If this is zero, there is no limit. */ int gMaxLogSize = 10240; /* Boolean indicating whether the user wanted to keep a usage log. */ int gLogging = USERLOG; /* Full pathname of the user's pager. Maybe be just "more" if the user * doesn't have a PAGER environment variable, or didn't set it explicitly. */ longstring gPager; int gVisualMode; int gDefaultVisualMode = VISUAL; /* If another ncftp process is running, we don't want to overwrite the * the files in our directory. We will only read them to prevent the * user losing changes made in the first process. */ int gOtherSessionRunning = 0; /* We track the number of times the user has run the program. We may * use that information for something else later. */ int gTotalRuns = 0; /* Types of startup messages user wants us to print when we first run. */ int gStartupMsgs = (kStartupMsg | kTips); string gVersion; /* If this user variable is set, we will change directory to the last * local directory the user was in when the user exited. */ int gRememberLCWD = 0; /* If this user variable is set, we will always chdir to this value each * time we run the program. */ longstring gDownloadDir = ""; jmp_buf gMainJmp; /* We save previously entered commands between sessions, so the user can * use the history. */ LineList gCmdHistory; /* Name of the file we use to tell if another ncftp process is running. */ longstring gLockFileName = ""; /* Boolean telling whether the splash-screen and interactive startup stuff * has been done. */ int gStartup = 0; extern int gStdout, gRealStdout; extern char *getlogin(void); extern longstring gLocalCWD; extern time_t gMailBoxTime; extern int gWinInit, gOptErr, gTrace, gDebug; extern char *gSprintfBuf; extern jmp_buf gCmdLoopJmp; extern LineList gRedir; /* This looks up the user's password entry, trying to look by the username. * We have a couple of extra hacks in place to increase the probability * that we can get the username. */ static struct passwd *GetPwByName(void) { char *cp; struct passwd *pw; cp = getlogin(); if (cp == NULL) { cp = (char *) getenv("LOGNAME"); if (cp == NULL) cp = (char *) getenv("USER"); } pw = NULL; if (cp != NULL) pw = getpwnam(cp); return (pw); } /* GetPwByName */ void GetUserInfo(void) { register char *cp; struct passwd *pw; string str; struct stat st; pw = NULL; errno = 0; #ifdef USE_GETPWUID /* Try to use getpwuid(), but if we have to, fall back to getpwnam(). */ if ((pw = getpwuid(getuid())) == NULL) pw = GetPwByName(); /* Oh well, try getpwnam() then. */ #else /* Try to use getpwnam(), but if we have to, fall back to getpwuid(). */ if ((pw = GetPwByName()) == NULL) pw = getpwuid(getuid()); /* Try getpwnam() then. */ #endif if (pw != NULL) { gUserInfo.uid = pw->pw_uid; (void) STRNCPY(gUserInfo.userName, pw->pw_name); gUserInfo.shell = StrDup(pw->pw_shell); if ((cp = (char *) getenv("HOME")) != NULL) gUserInfo.home = StrDup(cp); else gUserInfo.home = StrDup(pw->pw_dir); } else { /* Couldn't get information about this user. Since this isn't * the end of the world as far as I'm concerned, make up * some stuff that might work good enough. */ Error(kDoPerror, "Could not get your passwd entry!"); sleep(1); if ((cp = (char *) getenv("LOGNAME")) == NULL) cp = "nobody"; (void) STRNCPY(gUserInfo.userName, cp); gUserInfo.shell = StrDup("/bin/sh"); if ((cp = (char *) getenv("HOME")) == NULL) cp = "."; gUserInfo.home = StrDup(cp); gUserInfo.uid = 999; } cp = (char *) getenv("MAIL"); if (cp == NULL) cp = (char *) getenv("mail"); if (cp != NULL) { /* We do have a Mail environment variable. */ (void) STRNCPY(str, cp); cp = str; /* Mail variable may be like MAIL=(28 /usr/mail/me /usr/mail/you), * so try to find the first mail path. */ while ((*cp != '/') && (*cp != 0)) cp++; gUserInfo.mail = StrDup(cp); if ((cp = strchr(gUserInfo.mail, ' ')) != NULL) *cp = '\0'; } else { /* Guess between /usr/mail and /usr/spool/mail as * possible directories. We'll just choose /usr/spool/mail * if we have to. */ (void) sprintf(str, "/usr/mail/%s", gUserInfo.userName); if (stat(str, &st) < 0) (void) sprintf(str, "/usr/spool/mail/%s", gUserInfo.userName); gUserInfo.mail = StrDup(str); } DebugMsg("GetOurHostName returned %d, %s.\n", GetOurHostName(gOurHostName, sizeof(gOurHostName)), gOurHostName ); STRNCPY(gEmailAddress, gUserInfo.userName); STRNCAT(gEmailAddress, "@"); STRNCAT(gEmailAddress, gOurHostName); STRNCPY(gAnonPassword, gEmailAddress); } /* GetUserInfo */ /* Setup the malloc library, if we're using one. */ static void InitMalloc(void) { #ifdef DEBUG # if (LIBMALLOC == FAST_MALLOC) FILE *malloclog; mallopt(M_DEBUG, 1); mallopt(M_LOG, 1); /* turn on/off malloc/realloc/free logging */ /* set the file to use for logging */ if ((malloclog = fopen("malloc.log", "w")) != NULL) mallopt(M_LOGFILE, fileno(malloclog)); # endif #endif #if (LIBMALLOC == DEBUG_MALLOC) /* Thanks to Conor Cahill for making something like this possible! */ union dbmalloptarg m; # if 1 m.i = 1; dbmallopt(MALLOC_CKCHAIN, &m); # endif # if 1 m.i = M_HANDLE_IGNORE; dbmallopt(MALLOC_WARN, &m); # endif # if 0 /* If 0, stderr. */ m.str = "malloc.log"; dbmallopt(MALLOC_ERRFILE, &m); # endif #endif } /* InitMalloc */ void OpenTraceLog(void) { string traceLogPath; string traceLog2Path; string traceLogTmpPath; string line; FILE *f1, *f2, *f3; int i, lim; time_t now; if (gOtherSessionRunning != 0) return; if (gTraceLogFile != NULL) return; /* Already open. */ if (gOurDirectoryPath[0] == '\0') return; /* Don't create in root directory. */ OurDirectoryPath(traceLogPath, sizeof(traceLogPath), kTraceLogName); if (access(traceLogPath, F_OK) == 0) { /* Need to prepend last session's log to the master debug log. */ OurDirectoryPath(traceLog2Path, sizeof(traceLog2Path), kTraceLog2Name); OurDirectoryPath(traceLogTmpPath, sizeof(traceLogTmpPath), kTraceLogTmpName); f1 = fopen(traceLogTmpPath, "w"); if (f1 == NULL) { Error(kDoPerror, "Can't open %s for writing.\n", traceLogTmpPath); return; } f2 = fopen(traceLogPath, "r"); if (f2 == NULL) { Error(kDoPerror, "Can't open %s for reading.\n", traceLogPath); return; } lim = kMaxTraceLogLines - 1; for (i=0; i= lim) fputs("...Remaining lines omitted...\n", f1); (void) fclose(f1); (void) UNLINK(traceLogPath); (void) UNLINK(traceLog2Path); (void) rename(traceLogTmpPath, traceLog2Path); } gTraceLogFile = fopen(traceLogPath, "w"); if (gTraceLogFile == NULL) { Error(kDoPerror, "Can't open %s for writing.\n", traceLogPath); } else { time(&now); fprintf( gTraceLogFile, "SESSION STARTED at %s%s\n\n", ctime(&now), "-----------------------------------------------" ); } } /* OpenTraceLog */ void OpenLogs(void) { /* First try open the user's private log, unless the user * has this option turned off. */ OurDirectoryPath(gLogFileName, sizeof(gLogFileName), kLogName); gLogFile = NULL; if (gOtherSessionRunning == 0) { if ((gLogging) && (gMaxLogSize > 0)) gLogFile = fopen(gLogFileName, "a"); if (gTrace == kTracingOn) OpenTraceLog(); } /* Open a port to the system log, if you're hellbent on knowing * everything users do with the program. */ #ifdef SYSLOG # ifdef LOG_LOCAL3 openlog ("NcFTP", LOG_PID, LOG_LOCAL3); # else openlog ("NcFTP", LOG_PID); # endif #endif /* SYSLOG */ } /* OpenLogs */ /* Create, if necessary, a directory in the user's home directory to * put our incredibly important stuff in. */ void InitOurDirectory(void) { struct stat st; char *cp; cp = getenv("NCFTPDIR"); if (cp != NULL) { STRNCPY(gOurDirectoryPath, cp); } else if (STREQ(gUserInfo.home, "/")) { /* Don't create it if you're root and your home directory * is the root directory. */ gOurDirectoryPath[0] = '\0'; return; } else { (void) Path(gOurDirectoryPath, sizeof(gOurDirectoryPath), gUserInfo.home, kOurDirectoryName ); } if (stat(gOurDirectoryPath, &st) < 0) { (void) mkdir(gOurDirectoryPath, 00755); } } /* InitOurDirectory */ /* There would be problems if we had multiple ncftp's going. They would * each try to update the files in ~/.ncftp. Whichever program exited * last would have its stuff written, and the others which may have made * changes, would lose the changes they made. * * This checks to see if another ncftp is running. To do that we try * to lock a special file. If we could make the lock, then we are the first * ncftp running and our changes will be written. Other ncftps that may * start will not be able to save their changes. * * No matter what locking method in use, whenever we Exit() we try to * remove the lock file if we had the lock. For the first two methods, * exiting releases the lock automatically. */ static void CheckForOtherSessions(void) { int fd; char pidbuf[64]; time_t now; #if (LOCK_METHOD == 1) struct flock l; int err; l.l_type = F_WRLCK; l.l_start = 0; l.l_whence = SEEK_SET; l.l_len = 1; OurDirectoryPath(gLockFileName, sizeof(gLockFileName), kLockFileName); if ((fd = open(gLockFileName, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR)) < 0) { Error(kDoPerror, "Could not open lock file %s.\n", gLockFileName); sleep(5); /* Try to save data anyway. */ gOtherSessionRunning = 0; } else { err = fcntl(fd, F_SETLK, &l); if (err == 0) { /* We now have a lock set on the first byte. */ gOtherSessionRunning = 0; time(&now); sprintf(pidbuf, "%5d %s", (int) getpid(), ctime(&now)); write(fd, pidbuf, strlen(pidbuf) + 1); } else { /* Could not lock; maybe another ncftp process has * already locked it. */ gOtherSessionRunning = 1; } } #endif /* (LOCK_METHOD == 1) */ #if (LOCK_METHOD == 2) /* BSD's flock */ int err; OurDirectoryPath(gLockFileName, sizeof(gLockFileName), kLockFileName); if ((fd = open(gLockFileName, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR)) < 0) { Error(kDoPerror, "Could not open lock file %s.\n", gLockFileName); sleep(5); /* Try to save data anyway. */ gOtherSessionRunning = 0; } else { err = flock(fd, LOCK_EX | LOCK_NB); if (err == 0) { /* We now have a lock set on the whole file. */ gOtherSessionRunning = 0; time(&now); sprintf(pidbuf, "%5d %s", (int) getpid(), ctime(&now)); write(fd, pidbuf, strlen(pidbuf) + 1); } else { /* Could not lock; maybe another ncftp process has * already locked it. */ gOtherSessionRunning = 1; } } #endif /* (LOCK_METHOD == 2) */ #if (LOCK_METHOD == 3) /* Cheezy lock */ /* For this locking mechanism, we consider it locked if another * process had already created the lockfile. Unfortunately we * aren't guaranteed to get the lock file removed if the program * crashes somewhere without explicitly removing the lockfile. */ OurDirectoryPath(gLockFileName, sizeof(gLockFileName), kLockFileName); if ((fd = open(gLockFileName, O_CREAT | O_EXCL | O_WRONLY, S_IWUSR)) < 0) { gOtherSessionRunning = 1; } else { gOtherSessionRunning = 0; /* Unlike the other two methods, we don't need to keep the file * open. */ close(fd); } #endif /* (LOCK_METHOD == 3) */ } /* CheckForOtherSessions */ void Init(void) { char *cp; gRealStdout = gStdout = 1; InitMalloc(); InitXferBuffer(); gSprintfBuf = (char *) malloc((size_t) 4096); if (gSprintfBuf == NULL) OutOfMemory(); #if (kAlpha > 0) sprintf(gVersion, "%s Alpha %d (%s)", kVersion, kAlpha, kVersionDate ); #else # if (kBeta > 0) sprintf(gVersion, "%s Beta %d (%s)", kVersion, kBeta, kVersionDate ); # else sprintf(gVersion, "%s (%s)", kVersion, kVersionDate ); # endif #endif srand((unsigned int) time(NULL)); GetUserInfo(); InitDefaultFTPPort(); if ((cp = (char *) getenv("PAGER")) != NULL) STRNCPY(gPager, cp); else { #ifdef MORE STRNCPY(gPager, MORE); #else gPager[0] = '\0'; #endif } gIsFromTTY = gDoingScript = isatty(0); gIsToTTY = isatty(1); (void) UserLoggedIn(); /* Init parent-death detection. */ /* Init the mailbox checking code. */ (void) time(&gMailBoxTime); /* Clear the redir buffer. */ InitLineList(&gRedir); (void) GetCWD(gLocalCWD, sizeof(gLocalCWD)); /* Compute our own private directory's name, and create it * if we have to. Stuff that depends on this will need * to follow (duh). */ (void) InitOurDirectory(); (void) CheckForOtherSessions(); ReadPrefs(); OpenLogs(); ReadBookmarkFile(); ReadMacroFile(); InitCommandAndMacroNameList(); ++gTotalRuns; } /* Init */ void CloseTraceLog(void) { time_t now; if (gTraceLogFile != NULL) { time(&now); fprintf( gTraceLogFile, "\nSESSION ENDED at %s", ctime(&now) ); (void) fclose(gTraceLogFile); gTraceLogFile = NULL; } } /* CloseTraceLog */ void CloseLogs(void) { FILE *new, *old; struct stat st; long fat; string str; longstring tmpLog; #ifdef SYSLOG /* Close system log first. */ closelog(); #endif /* Close the debugging log next. */ CloseTraceLog(); /* The rest is for the user's log. */ if (!gLogging) return; CloseFile(&gLogFile); if (gOurDirectoryPath[0] == '\0') return; /* Don't create in root directory. */ /* If the user wants to, s/he can specify the maximum size of the log file, * so it doesn't waste too much disk space. If the log is too fat, trim the * older lines (at the top) until we're under the limit. */ if ((gMaxLogSize < 0) || (stat(gLogFileName, &st) < 0) || ((old = fopen(gLogFileName, "r")) == NULL)) return; /* Never trim, or no log. */ if ((size_t)st.st_size < (size_t)gMaxLogSize) return; /* Log size not over limit yet. */ /* Want to make it so we're about 30% below capacity. * That way we won't trim the log each time we run the program. */ fat = st.st_size - gMaxLogSize + (long) (0.30 * gMaxLogSize); DebugMsg("%s was over %ld limit; trimming at least %ld...\n", gLogFileName, gMaxLogSize, fat ); while (fat > 0L) { if (fgets(str, (int) sizeof(str), old) == NULL) return; fat -= (long) strlen(str); } /* skip lines until a new site was opened */ while (1) { if (fgets(str, (int) sizeof(str), old) == NULL) { (void) fclose(old); (void) UNLINK(gLogFileName); return; /* Nothing left, start anew next time. */ } if (! isspace(*str)) break; } /* Copy the remaining lines in "old" to "new" */ OurDirectoryPath(tmpLog, sizeof(tmpLog), kTmpLogName); if ((new = fopen(tmpLog, "w")) == NULL) { (void) Error(kDoPerror, "Could not open %s.\n", tmpLog); return; } (void) fputs(str, new); while (fgets(str, (int) sizeof(str), old) != NULL) (void) fputs(str, new); (void) fclose(old); (void) fclose(new); if (UNLINK(gLogFileName) < 0) (void) Error(kDoPerror, "Could not delete %s.\n", gLogFileName); if (rename(tmpLog, gLogFileName) < 0) (void) Error(kDoPerror, "Could not rename %s to %s.\n", tmpLog, gLogFileName); } /* CloseLogs */ void SaveHistory(void) { string histFileName; FILE *fp; LinePtr lp; int i; if (gWinInit) { if (gOurDirectoryPath[0] == '\0') return; /* Don't create in root directory. */ lp = gCmdHistory.last; OurDirectoryPath(histFileName, sizeof(histFileName), kHistoryName); fp = fopen(histFileName, "w"); if ((fp != NULL) && (lp != NULL)) { for (i = 1 ; (lp->prev != NULL) && (i < kMaxHistorySaveLines); i++) lp = lp->prev; for ( ; lp != NULL; lp = lp->next) fprintf(fp, "%s\n", lp->line); fclose(fp); } } } /* SaveHistory */ void LoadHistory(void) { string histFileName; string str; FILE *fp; InitLineList(&gCmdHistory); OurDirectoryPath(histFileName, sizeof(histFileName), kHistoryName); fp = fopen(histFileName, "r"); if (fp != NULL) { while (FGets(str, sizeof(str), fp) != NULL) AddLine(&gCmdHistory, str); } } /* LoadHistory */ void StartupMsgs(void) { char curLocalCWD[512]; if ((gStartupMsgs & kStartupMsg) != 0) { #if (kAlpha > 0) || (kBeta > 0) if (gWinInit == 0) PrintF("NcFTP %s, by Mike Gleason.\n", gVersion); BoldPrintF( "This pre-release is for testing only. Please do not archive.\n\n"); BoldPrintF( "Check the CHANGELOG file for changes between betas.\n\n"); #else if (gWinInit == 0) PrintF("NcFTP %s, by Mike Gleason.\n", gVersion); #endif } if (gTotalRuns < 2) { PrintF("NcFTP is Copyright (C) 1992, Mike Gleason. All rights reserved.\n\n"); PrintF("This program is free software; you can redistribute it and/or modify\n"); PrintF("it under the terms of the GNU General Public License as published by\n"); PrintF("the Free Software Foundation; either version 2 of the License, or\n"); PrintF("(at your option) any later version.\n\n"); PrintF("This program is distributed in the hope that it will be useful,\n"); PrintF("but WITHOUT ANY WARRANTY; without even the implied warranty of\n"); PrintF("MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"); PrintF("GNU General Public License for more details.\n\n"); PrintF("You should have received a copy of the GNU General Public License\n"); PrintF("along with this program; if not, write to the Free Software\n"); PrintF("Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA\n\n"); } else if ((gStartupMsgs & kTips) != 0) { PrintRandomTip(); } if (gOtherSessionRunning) { BoldPrintF("Note: Another session is running. Changes made to the\n"); BoldPrintF(" prefs, bookmarks, and trace files will not be saved.\n\n"); } (void) GetCWD(curLocalCWD, sizeof(curLocalCWD)); if (gDownloadDir[0] != '\0') { /* If set, always try to cd there. */ ExpandTilde(gDownloadDir, sizeof(gDownloadDir)); (void) chdir(gDownloadDir); /* May not succeed. */ } else if (gRememberLCWD) { /* Try restoring the last local directory we were in. * gLocalCWD may be set when the prefs are read in. */ if (gLocalCWD[0] != '\0') (void) chdir(gLocalCWD); /* May not succeed. */ } /* Print a message if we changed the directory. */ (void) GetCWD(gLocalCWD, sizeof(gLocalCWD)); if (strcmp(curLocalCWD, gLocalCWD)) PrintF("Current local directory is %s.\n", gLocalCWD); } /* StartupMsg */ void Startup(void) { if (gStartup == 0) { gStartup = 1; InitWindows(); StartupMsgs(); (void) RunPrefixedMacro("start.", "ncftp"); InitReadline(); } } /* Startup */ /*ARGSUSED*/ static void SigIntMain(/* int sigNum */ void) { SIGNAL(SIGINT, SigIntMain); alarm(0); EPrintF("\n*Interrupt*\n"); longjmp(gMainJmp, 1); } /* SigIntMain */ void main(int argc, char **argv) { int opt, result; OpenOptions openopt; #ifdef __FreeBSD__ setlocale(LC_ALL, ""); #endif Init(); RunStartupScript(); GetoptReset(); gOptErr = 0; if (GetOpenOptions(argc, argv, &openopt, 1) == kUsageErr) goto usage; gOptErr = 1; GetoptReset(); gVisualMode = gDefaultVisualMode; /* Note that we getopt on three sets of options, one for open, * one for get, and one for the program. That means that these * commands' flags and the program must use mutually exclusive * sets of flags. */ while ((opt = Getopt(argc, argv, "aiup:rd:g:cmCfGRn:zDLVH")) >= 0) { if (strchr("aiup:rd:g:cmCfGRn:z", opt) == NULL) { switch (opt) { case 'D': gDebug = kDebuggingOn; gTrace = kTracingOn; break; case 'L': gVisualMode = 0; break; case 'V': gVisualMode = 1; break; case 'H': VersionCmd(); Exit(kExitNoErr); break; default: usage: EPrintF( "Usage: ncftp [options] [hostname[:path]]\n"); EPrintF("Program options:\n\ -D : Turn debug mode and trace mode on.\n\ -L : Don't use visual mode (use line mode).\n\ -V : Use visual mode.\n\ -H : Dump the version information.\n"); EPrintF("Command-line open options:\n\ -a : Open anonymously.\n\ -u : Open with username and password prompt.\n\ -p X : Use port number X when opening.\n\ -r : Redial until connected.\n\ -d X : Redial, delaying X seconds between tries.\n\ -g X : Give up after X redials without connection.\n"); EPrintF("Command-line retrieve options:\n\ -C : Force continuation (reget).\n\ -f : Force overwrite.\n\ -G : Don't use wildcard matching.\n\ -R : Recursive. Useful for fetching whole directories.\n\ -n X : Get selected files only if X days old or newer.\n"); Exit(kExitUsageErr); } } } result = kNoErr; if (openopt.hostname[0] != '\0') { if (setjmp(gMainJmp)) { result = kNoErr; } else { SIGNAL(SIGINT, SigIntMain); SIGNAL(SIGPIPE, SIG_IGN); if (setjmp(gCmdLoopJmp)) { /* May have lost connection during Open. */ result = kCmdErr; } else { result = Open(&openopt); } } } if (result == kNoErr) { Startup(); /* Init the interactive shell. */ CommandShell(); DoQuit(kExitNoErr); } DoQuit(kExitColonModeFail); /*NOTREACHED*/ Exit(kExitNoErr); } /* main */ /* eof Main.c */