1: /*
2: * $Id: init.c,v 1.12 2025/04/15 14:39:06 snw Exp $
3: * FreeM initialization
4: *
5: *
6: * Author: Serena Willis <snw@coherent-logic.com>
7: * Copyright (C) 1998 MUG Deutschland
8: * Copyright (C) 2020, 2025 Coherent Logic Development LLC
9: *
10: *
11: * This file is part of FreeM.
12: *
13: * FreeM is free software: you can redistribute it and/or modify
14: * it under the terms of the GNU Affero Public License as published by
15: * the Free Software Foundation, either version 3 of the License, or
16: * (at your option) any later version.
17: *
18: * FreeM is distributed in the hope that it will be useful,
19: * but WITHOUT ANY WARRANTY; without even the implied warranty of
20: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21: * GNU Affero Public License for more details.
22: *
23: * You should have received a copy of the GNU Affero Public License
24: * along with FreeM. If not, see <https://www.gnu.org/licenses/>.
25: *
26: * $Log: init.c,v $
27: * Revision 1.12 2025/04/15 14:39:06 snw
28: * Further improvements to logging
29: *
30: * Revision 1.11 2025/04/13 04:22:43 snw
31: * Fix snprintf calls
32: *
33: * Revision 1.10 2025/04/10 01:24:38 snw
34: * Remove C++ style comments
35: *
36: * Revision 1.9 2025/04/03 16:58:34 snw
37: * Make error message for shm_init error during initialization more friendly
38: *
39: * Revision 1.8 2025/03/24 04:44:55 snw
40: * Don't call ttyname on OS/2
41: *
42: * Revision 1.7 2025/03/24 04:05:36 snw
43: * Replace crlf with frm_crlf to avoid symbol conflict with readline on OS/2
44: *
45: * Revision 1.6 2025/03/09 19:14:25 snw
46: * First phase of REUSE compliance and header reformat
47: *
48: *
49: * SPDX-FileCopyrightText: (C) 2025 Coherent Logic Development LLC
50: * SPDX-License-Identifier: AGPL-3.0-or-later
51: **/
52:
53: #include <stdio.h>
54: #include <stdlib.h>
55: #include <string.h>
56: #include <unistd.h>
57: #include <limits.h>
58: #include <sys/types.h>
59: #include <sys/stat.h>
60: #include <pwd.h>
61: #include <time.h>
62: #include <errno.h>
63: #include <sys/ioctl.h>
64:
65: #if !defined(__APPLE__) && !defined(__gnu_hurd__) && !defined(EMSCRIPTEN)
66: # if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__AMIGA)
67: # include <termios.h>
68: # if !defined(__AMIGA)
69: # define TCGETA TIOCGETA
70: # define TCSETA TIOCSETA
71: # endif
72: # define termio termios
73: # else
74: # if !defined(MSDOS)
75: # include <termio.h>
76: # endif
77: # endif
78: #else
79: # include <termios.h>
80: #endif
81:
82: #include "config.h"
83:
84: #if defined(HAVE_MWAPI_MOTIF)
85: # include <Xm/Xm.h>
86: #endif
87:
88: #include "mpsdef.h"
89: #include "transact.h"
90: #include "namespace.h"
91: #include "events.h"
92: #include "mdebug.h"
93: #include "shmmgr.h"
94: #include "locktab.h"
95: #include "jobtab.h"
96: #include "datatypes.h"
97: #include "objects.h"
98: #include "log.h"
99:
100: #ifdef HAVE_LIBREADLINE
101: # if defined(HAVE_READLINE_READLINE_H)
102: # include <readline/readline.h>
103: # elif defined(HAVE_READLINE_H)
104: # include <readline.h>
105: # else /* !defined(HAVE_READLINE_H) */
106: extern char *readline ();
107: # endif /* !defined(HAVE_READLINE_H) */
108: char *cmdline = NULL;
109: #else /* !defined(HAVE_READLINE_READLINE_H) */
110: /* no readline */
111: #endif /* HAVE_LIBREADLINE */
112:
113: #ifdef HAVE_READLINE_HISTORY
114: # if defined(HAVE_READLINE_HISTORY_H)
115: # include <readline/history.h>
116: # elif defined(HAVE_HISTORY_H)
117: # include <history.h>
118: # else /* !defined(HAVE_HISTORY_H) */
119: extern void add_history ();
120: extern int write_history ();
121: extern int read_history ();
122: # endif /* defined(HAVE_READLINE_HISTORY_H) */
123: /* no history */
124: #endif /* HAVE_READLINE_HISTORY */
125:
126: #if defined(HAVE_WIRINGPI_H)
127: # include <wiringPi.h>
128: #endif
129:
130: #if !defined(PATH_MAX) && defined(_SCO_DS)
131: # define PATH_MAX 4096
132: #endif
133:
134: #if !defined(PATH_MAX) && defined(__gnu_hurd__)
135: # define PATH_MAX 1024
136: #endif
137:
138: #if !defined(PATH_MAX) && defined(__sun__)
139: # include <limits.h>
140: #endif
141:
142: #if defined(__NetBSD__) || defined(__FreeBSD__) || defined(__OpenBSD__)
143: # include <sys/syslimits.h>
144: #endif
145:
146: #define SHMKEY 0x990120
147: #define SHMSIZ 1048576
148:
149: void init_process(void);
150: void init_devtable(void);
151: void init_signals(void);
152: void init_timezone(void);
153: void init_freem_path(void);
154:
155: #if defined(HAVE_LIBREADLINE)
156: void init_readline(void);
157: #endif
158:
159: void init_execution_context(void);
160: void init_io(void);
161: void init_random_number(void);
162: void init_ztrap(void);
163: void init_ssvn(void);
164: void init_terminal(void);
165: void init_estack(void);
166:
167: void init_mwapi(void);
168:
169: short init (char *namespace_name)
170: {
171: short retval;
172:
173: init_log ();
174: init_process ();
175: init_devtable ();
176: init_signals ();
177: init_freem_path ();
178: init_timezone ();
179:
180: #if defined(HAVE_LIBREADLINE)
181: init_readline ();
182: #endif
183:
184: init_execution_context ();
185:
186: if (run_daemon == FALSE) {
187: init_io ();
188: }
189:
190: init_random_number ();
191: init_ztrap ();
192:
193: retval = shm_init (shm_init_size);
194:
195: if (retval == SHMS_GET_ERR) {
196: if (errno != 13) {
197: fprintf (stderr, "init: error initializing shared memory [errno %d]\r\n", errno);
198: }
199: else {
200: fprintf (stderr, "init: error attaching to environment -- does your user belong to the group that owns environment '%s'?\r\n", shm_env);
201: }
202: if (run_daemon == FALSE) {
203: set_io (UNIX);
204: }
205: exit (1);
206: }
207:
208: symtab_init ();
209: tp_init ();
210:
211: set_namespace (namespace_name, FALSE);
212:
213: if (first_process) {
214: fprintf (stderr, "init: we are the first process in the environment (pid %d)\r\n", pid);
215: }
216:
217: if (first_process) fprintf (stderr, "init: initializing job table\r\n");
218: jobtab_init ();
219:
220: if (first_process) fprintf (stderr, "init: adding job to job table\r\n");
221: job_init (FALSE);
222:
223: if (first_process) fprintf (stderr, "init: initializing structured system variables\r\n");
224: init_ssvn ();
225:
226: if (first_process) fprintf (stderr, "init: initializing terminal\r\n");
227: init_terminal ();
228:
229: if (first_process) fprintf (stderr, "init: initializing asynchronous events\r\n");
230: evt_init ();
231:
232: if (first_process) fprintf (stderr, "init: initializing debugger\r\n");
233: dbg_init ();
234:
235: if (first_process) fprintf (stderr, "init: initializing error stack\r\n");
236: init_estack();
237:
238: etrap[0] = EOL;
239: ecode[0] = EOL;
240: estack = 0;
241:
242: init_mwapi();
243:
244: if (merr () == OK) {
245: return TRUE;
246: }
247:
248: return FALSE;
249: }
250:
251: void init_process (void)
252: {
253: pid = getpid (); /* get $J = process ID */
254: umask (0); /* protection bits mask to full rights */
255: snprintf (fp_conversion, sizeof (fp_conversion) - 1, "%%.%df\201", DBL_DIG);
256:
257: if (fp_mode) {
258: zprecise = DBL_DIG;
259: }
260: else {
261: zprecise = 100;
262: }
263: }
264:
265: void init_devtable (void)
266: {
267: register int i;
268: register int j;
269:
270: for (j = 0; j <= MAXDEV; j++) { /* init. translation tables */
271:
272: for (i = 0; i < 256; i++) {
273: G0I[j][i] = (char) i;
274: G0O[j][i] = (char) i;
275: G1I[j][i] = (char) i;
276: G1O[j][i] = (char) i;
277: }
278:
279: G0I[j][UNSIGN (EOL)] = NUL;
280: G0O[j][UNSIGN (EOL)] = NUL;
281: G1I[j][UNSIGN (EOL)] = NUL;
282: G1O[j][UNSIGN (EOL)] = NUL;
283: G0I[j][UNSIGN (DELIM)] = NUL;
284: G0O[j][UNSIGN (DELIM)] = NUL;
285: G1I[j][UNSIGN (DELIM)] = NUL;
286: G1O[j][UNSIGN (DELIM)] = NUL;
287: G0I[j][256] = EOL;
288: G0O[j][256] = EOL;
289: G1I[j][256] = EOL;
290: G1O[j][256] = EOL;
291:
292: }
293:
294: #ifdef SCO
295: #ifndef HACK_NOXLATE
296: G0I[HOME][245] = 64;
297: G0O[HOME][64] = 245; /* Paragraph */
298: G0I[HOME][142] = 91;
299: G0O[HOME][91] = 142; /* A umlaut */
300: G0I[HOME][153] = 92;
301: G0O[HOME][92] = 153; /* O umlaut */
302: G0I[HOME][154] = 93;
303: G0O[HOME][93] = 154; /* U umlaut */
304: G0I[HOME][132] = 123;
305: G0O[HOME][123] = 132; /* a umlaut */
306: G0I[HOME][148] = 124;
307: G0O[HOME][124] = 148; /* o umlaut */
308: G0I[HOME][129] = 125;
309: G0O[HOME][125] = 129; /* u umlaut */
310: G0I[HOME][225] = 126;
311: G0O[HOME][126] = 225; /* sharp s */
312: #endif/*HACK_NOXLATE*/
313:
314: /* DEC Special graphics */
315: G1I[HOME][254] = 96;
316: G1O[HOME][96] = 254; /* diamond */
317: G1I[HOME][176] = 97;
318: G1O[HOME][97] = 176; /* checker board */
319: G1I[HOME][241] = 99;
320: G1O[HOME][99] = 241; /* FF */
321: G1I[HOME][242] = 100;
322: G1O[HOME][100] = 242; /* CR */
323: G1I[HOME][243] = 101;
324: G1O[HOME][101] = 243; /* LF */
325: G1I[HOME][248] = 102;
326: G1O[HOME][102] = 248; /* degree sign */
327: G1I[HOME][241] = 103;
328: G1O[HOME][103] = 241; /* plus minus */
329: G1I[HOME][244] = 104;
330: G1O[HOME][104] = 244; /* NL */
331: G1I[HOME][251] = 105;
332: G1O[HOME][105] = 251; /* VT */
333: G1I[HOME][217] = 106;
334: G1O[HOME][106] = 217; /* lower right corner */
335: G1I[HOME][191] = 107;
336: G1O[HOME][107] = 191; /* upper right corner */
337: G1I[HOME][218] = 108;
338: G1O[HOME][108] = 218; /* upper left corner */
339: G1I[HOME][192] = 109;
340: G1O[HOME][109] = 192; /* lower left corner */
341: G1I[HOME][197] = 110;
342: G1O[HOME][110] = 197; /* cross */
343: G1I[HOME][200] = 111;
344: G1O[HOME][111] = 200; /* linescan 5 */
345: G1I[HOME][201] = 112;
346: G1O[HOME][112] = 201; /* linescan 4 */
347: G1I[HOME][196] = 113;
348: G1O[HOME][113] = 196; /* linescan 3 */
349: G1I[HOME][202] = 114;
350: G1O[HOME][114] = 202; /* linescan 2 */
351: G1I[HOME][203] = 115;
352: G1O[HOME][115] = 203; /* linescan 1 */
353: G1I[HOME][195] = 116;
354: G1O[HOME][116] = 195; /* left junction */
355: G1I[HOME][180] = 117;
356: G1O[HOME][117] = 180; /* right junction */
357: G1I[HOME][193] = 118;
358: G1O[HOME][118] = 193; /* lower junction */
359: G1I[HOME][194] = 119;
360: G1O[HOME][119] = 194; /* upper junction */
361: G1I[HOME][179] = 120;
362: G1O[HOME][120] = 179; /* vertival bar */
363: G1I[HOME][243] = 121;
364: G1O[HOME][121] = 243; /* lower equals */
365: G1I[HOME][242] = 122;
366: G1O[HOME][122] = 242; /* greater equals */
367: G1I[HOME][227] = 123;
368: G1O[HOME][123] = 227; /* pi */
369: G1I[HOME][246] = 124;
370: G1O[HOME][124] = 246; /* not equals */
371: G1I[HOME][128] = 125;
372: G1O[HOME][125] = 128; /* euro sign */
373: G1I[HOME][250] = 126;
374: G1O[HOME][126] = 250; /* centered dot */
375: #endif /* SCO */
376: }
377:
378: void init_signals (void)
379: {
380: sig_init ();
381: }
382:
383: void init_timezone (void)
384: {
385:
386: struct tm lt;
387: struct tm gt;
388:
389: unsigned long gmt;
390: unsigned long lmt;
391:
392: long clock;
393:
394: #ifdef __CYGWIN__
395:
396: tzset (); /* may be required in order */
397: /* to guarantee _timezone set */
398: #else
399:
400: clock = time (0L);
401: lt = *localtime (&clock);
402: gt = *gmtime (&clock);
403:
404: /* This is awkward but I think it is portable: steve_morris */
405: gmt = gt.tm_year * 365;
406: gmt = (gmt + gt.tm_yday) * 24;
407: gmt = (gmt + gt.tm_hour) * 60;
408: gmt = (gmt + gt.tm_min);
409:
410: lmt = lt.tm_year * 365;
411: lmt = (lmt + lt.tm_yday) * 24;
412: lmt = (lmt + lt.tm_hour) * 60;
413: lmt = (lmt + lt.tm_min);
414:
415: FreeM_timezone = (gmt - lmt) * 60;
416: tzoffset = -FreeM_timezone;
417:
418: #endif /* __CYGWIN__ */
419:
420:
421: }
422:
423: void init_freem_path (void)
424: {
425:
426: if((freem_path = malloc(PATH_MAX + 1)) == NULL) {
427: fprintf(stderr, "Can't allocate freem_path. Exiting.");
428:
429: exit(1);
430: }
431:
432: freem_path[0] = NUL;
433:
434: /* check where I'm being executed from */
435: #ifdef __linux__
436: readlink ("/proc/self/exe", freem_path, PATH_MAX);
437: #endif
438: #ifdef __FreeBSD__
439: readlink ("/proc/curproc/file", freem_path, PATH_MAX);
440: #endif
441: #ifdef __sun
442: readlink ("/proc/self/path/a.out", freem_path, PATH_MAX);
443: #endif
444:
445: if(freem_path[0] == NUL) {
446: /* we don't know where we came from */
447: }
448:
449: getcwd (curdir, PATHLEN);
450: stcnv_c2m (curdir);
451:
452: }
453:
454: #if defined(HAVE_LIBREADLINE)
455: void init_readline (void)
456: {
457: uid_t uid = geteuid ();
458: struct passwd *pw = getpwuid (uid);
459: char *pw_buf;
460:
461: pw_buf = (char *) calloc (strlen(pw->pw_dir) + 1, sizeof(char));
462: strcpy (pw_buf, pw->pw_dir);
463:
464: snprintf (history_file, sizeof (history_file) - 1, "%s/.freem_history", pw_buf);
465:
466: free (pw_buf);
467:
468: using_history ();
469: read_history (history_file);
470: }
471: #endif
472:
473: void init_execution_context (void)
474: {
475: register int i;
476:
477: obj_init ();
478:
479: merr_clear ();
480:
481: codptr = code;
482: code[0] = EOL; /* init code_pointer */
483: partition = calloc ((unsigned) (PSIZE + 2), 1);
484:
485: if (partition == NULL) exit (2); /* could not allocate stuff... */
486:
487: for (i = 0; i < MAXNO_OF_RBUF; i++) {
488: rbuf_flags[i].standard = standard;
489: }
490:
491: for (i = 0; i < NESTLEVLS; i++) {
492: extr_types[i] = DT_STRING;
493: }
494:
495: symlen = PSIZE;
496: s = &partition[PSIZE] - 256; /* pointer to symlen_offset */
497: argptr = partition; /* pointer to beg of tmp-storage */
498:
499: svntable = calloc ((unsigned) (UDFSVSIZ + 1), 1);
500: if (svntable == NULL) exit (2); /* could not allocate stuff... */
501:
502: svnlen = UDFSVSIZ; /* begin of udf_svn_table */
503: buff = calloc ((unsigned) NO_OF_RBUF * (unsigned) PSIZE0, 1); /* routine buffer pool */
504: if (buff == NULL) exit (2); /* could not allocate stuff... */
505:
506:
507: newstack = calloc ((unsigned) NSIZE, 1);
508: if (newstack == NULL) exit (2); /* could not allocate stuff... */
509:
510: #ifdef DEBUG_NEWPTR
511: printf("Allocating newptr stack...\r\n");
512: #endif
513:
514: newptr = newstack;
515: newlimit = newstack + NSIZE - 1024;
516:
517:
518: namstck = calloc ((unsigned) NESTLEVLS * 13, 1);
519: if (namstck == NULL) exit (2); /* could not allocate stuff... */
520:
521: *namstck = EOL;
522: *(namstck + 1) = EOL;
523: namptr = namstck; /* routine name stack pointer */
524: framstck = calloc ((unsigned) NESTLEVLS * 256, 1);
525: if (framstck == NULL) exit (2); /* could not allocate stuff... */
526:
527: *framstck = EOL;
528: *(framstck + 1) = EOL;
529: dofrmptr = framstck; /* DO_frame stack pointer */
530: cmdstack = calloc ((unsigned) NESTLEVLS * 256, 1);
531: if (cmdstack == NULL) exit (2); /* could not allocate stuff... */
532:
533: cmdptr = cmdstack; /* command stack */
534:
535: rouend = rouins = rouptr = buff;
536: roucur = buff + (NO_OF_RBUF * PSIZE0 + 1);
537: *rouptr = EOL;
538: *(rouptr + 1) = EOL;
539: *(rouptr + 2) = EOL;
540:
541: err_suppl[0] = EOL; /* empty out supplemental error info */
542: }
543:
544: void init_estack (void)
545: {
546: stcpy (merr_stack[0].PLACE, "xecline()\201");
547: }
548:
549: #if defined(HAVE_MWAPI_MOTIF)
550: void init_mwapi (void)
551: {
552: /*
553: if (getenv("DISPLAY") != NULL) {
554: gtk_init (0, NULL);
555: }
556: */
557: /* TODO: init Motif/libXt */
558: }
559: #else
560: void init_mwapi (void)
561: {
562: return;
563: }
564: #endif
565:
566: void init_io (void)
567: {
568: register int i;
569:
570: /* initialize screen */
571: setbuf (stdin, NULL); /* no input buffering */
572: glvnflag.all = 0L;
573: stcpy (buff, "\201");
574: writeHOME (buff);
575: sq_modes[0] = '+';
576: for (i = 0; i <= MAXDEV; ug_buf[i++][0] = EOL); /* init read-buffers */
577:
578: frm_crlf[HOME] = frm_filter;
579:
580: if (hardcopy) zbreakon = ENABLE; /* enable CTRL/B */
581:
582: set_io (MUMPS); /* set i/o parameters */
583:
584: #if !defined(__AMIGA) && !defined(__OS2__)
585: if (ttyname (HOME)) { /* for $IO of HOME */
586: strcpy (dev[HOME], ttyname (HOME));
587: dev[HOME][strlen (dev[HOME])] = EOL;
588: }
589: else {
590: dev[HOME][0] = EOL; /* ...we are in a pipe */
591: }
592: #else
593: #if defined(__AMIGA)
594: strcpy (dev[HOME], "CONSOLE:");
595: #else
596: #if defined(__OS2__)
597: strcpy (dev[HOME], "CON:");
598: #endif
599: #endif
600: #endif
601:
602: /* init function keys */
603: for (i = 0; i < 44; zfunkey[i++][0] = EOL);
604: }
605:
606: void init_random_number (void)
607: {
608:
609: srand (time (NULL));
610:
611: if ((nrandom = time (0L) * getpid ()) < 0) {
612: nrandom = (-nrandom);
613: }
614:
615: }
616:
617: void init_ztrap (void)
618: {
619:
620: if (frm_filter) {
621: ztrap[0][0] = EOL; /* no default ztrap for filters */
622: }
623: else if (startuprou[0] == '^') {
624: stcpy (ztrap[0], startuprou);
625: }
626: else {
627: stcpy (ztrap[0], "^%SYSINIT\201");
628: }
629:
630: /* $ZT to be xecuted on startup */
631:
632: stcpy (ztrap[NESTLEVLS + 1], ztrap[0]); /* DSM V.2 error trapping */
633:
634: }
635:
636: void init_ssvn(void)
637: {
638: ssvn_job_update ();
639: ssvn_display_update ();
640: ssvn_routine_update ();
641: ssvn_library_update ();
642: if (first_process) ssvn_system_update ();
643: }
644:
645: void init_terminal(void)
646: {
647: xpos[HOME] = 80;
648: ypos[HOME] = 24;
649: }
650:
651: void reset_terminal(void)
652: {
653: struct termio tpara;
654:
655: ioctl (0, TCGETA, &tpara);
656:
657: tpara.c_lflag |= (ECHO | ICANON); /* enable echo/no cbreak mode */
658: tpara.c_iflag |= ICRNL; /* cr-lf mapping */
659: tpara.c_oflag |= ONLCR; /* cr-lf mapping */
660: tpara.c_cc[VMIN] = EOT;
661: tpara.c_cc[VTIME] = -1;
662:
663: ioctl (0, TCSETA, &tpara);
664: }
665:
666: void cleanup (void)
667: {
668: char k_buf[256];
669: int ch;
670:
671: /* remove this job's entry from ^$JOB SSVN */
672: snprintf (k_buf, sizeof (k_buf) - 1, "^$JOB\202%d\201", pid);
673: symtab_shm (kill_sym, k_buf, " \201");
674:
675: reset_terminal ();
676:
677: if (tp_level > 0) {
678:
679: if (direct_mode == TRUE) {
680: fprintf (stderr, "UNCOMMITTED TRANSACTIONS EXIST:\n\n");
681: tp_tdump ();
682: set_io (UNIX);
683: fprintf (stderr, "\nWould you like to c)ommit or r)ollback the above transactions and their operations? ($TLEVEL = %d) ", tp_level);
684:
685: for (;;) {
686: ch = fgetc (stdin);
687:
688: if (ch == 'c' || ch == 'C') {
689: while (tp_level > 0) tp_tcommit ();
690:
691: fprintf (stderr, "\n\nTransactions have been committed.\n");
692:
693: break;
694: }
695: else if (ch == 'r' || ch == 'R') {
696: tp_trollback (tp_level);
697:
698: fprintf (stderr, "\n\nTransactions have been rolled back.\n");
699:
700: break;
701: }
702: else {
703: fprintf (stderr, "\n\nInvalid input '%c'. Must choose c)ommit or r)ollback.\n", ch);
704: }
705: }
706: }
707: else {
708: fprintf (stderr, "Uncommitted transactions exist. Rolling back.\n");
709: tp_trollback (tp_level);
710: }
711: }
712:
713: #if defined(HAVE_LIBREADLINE)
714: write_history (history_file);
715: #endif
716:
717: locktab_unlock_all ();
718: job_remove (pid);
719:
720: shm_exit ();
721:
722: if (run_daemon == TRUE) {
723:
724: if (pid_fd != -1) {
725: lockf (pid_fd, F_ULOCK, 0);
726: close (pid_fd);
727: }
728:
729: if (pid_file_path != NULL) {
730: unlink (pid_file_path);
731: }
732:
733: }
734:
735:
736:
737: free (buff); /* free previously allocated space */
738: free (svntable);
739: if (partition) free (partition);
740: if (apartition) free (apartition);
741:
742:
743: free (newstack);
744:
745:
746: if (v22size) free (v22ali);
747:
748: return;
749: } /* end of cleanup */
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>