Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

pseudo-terminals



Intentionally no `PATCH:' in the subject...

I tried to build a module that allows one to start commands, talking
to them via a pty. This is how far I've come.

The zsh/zpty module has one builtin `zpty' that can be used to start
such commands and option to write to them and read from them. Process
handling is still very simple, i.e. it can really only start external
commands -- I didn't like those extra zsh's when using an eval-like
mechanism and signal handling (when trying to kill such a command)
somehow didn't work. Maybe I just didn't try hard enough...

I also added a hook to the shell core: `exit' is called when the shell 
is about to exit, so modules can make sure they are called before
that.

And finally, this changes the nslookup function to use zpty. And
history. But, as you know...

About the implementation: it works for me on Linux, Digital Unix and
Solaris, I don't have access to other systems. This means that there
would be a lot of work to do to make it work on other systems because
pty handling is very different... sigh. This is also the main reason
why I don't know if this is worth including.

There are also some other questions -- I've left comments starting
with /**** in zpty.c in those places.

Bye
 Sven

diff -ru ../z.old/Doc/Makefile.in ./Doc/Makefile.in
--- ../z.old/Doc/Makefile.in	Thu Jan 20 10:47:52 2000
+++ ./Doc/Makefile.in	Thu Jan 20 15:56:59 2000
@@ -60,7 +60,7 @@
 Zsh/mod_deltochar.yo Zsh/mod_example.yo Zsh/mod_files.yo \
 Zsh/mod_mapfile.yo Zsh/mod_mathfunc.yo Zsh/mod_parameter.yo Zsh/mod_sched.yo \
 Zsh/mod_stat.yo Zsh/mod_zftp.yo Zsh/mod_zle.yo Zsh/mod_zleparameter.yo \
-Zsh/mod_zutil.yo Zsh/mod_zprof.yo
+Zsh/mod_zutil.yo Zsh/mod_zprof.yo Zsh/mod_zpty.yo
 
 YODLSRC = zmacros.yo zman.yo ztexi.yo Zsh/arith.yo Zsh/builtins.yo \
 Zsh/compat.yo Zsh/compctl.yo Zsh/compsys.yo Zsh/compwid.yo Zsh/cond.yo \
diff -ru ../z.old/Doc/Zsh/mod_zpty.yo Doc/Zsh/mod_zpty.yo
--- ../z.old/Doc/Zsh/mod_zpty.yo	Thu Jan 20 16:07:39 2000
+++ Doc/Zsh/mod_zpty.yo	Thu Jan 20 15:55:39 2000
@@ -0,0 +1,40 @@
+COMMENT(!MOD!zsh/zpty
+A builtin for starting a command in a pseudo-terminal.
+!MOD!)
+The tt(zsh/zpty) module offers one builtin:
+
+startitem()
+findex(zpty)
+xitem(tt(zpty) [ tt(-e) ] [ tt(-b) ] var(name) var(command) [ var(args ...) ])
+xitem(tt(zpty) tt(-d) [ var(names) ... ])
+xitem(tt(zpty) tt(-w) [ tt(-n) ] var(name) var(strings ...))
+xitem(tt(zpty) tt(-r) var(name) [ var(param) [ var(pattern) ] ])
+item(tt(zpty) [ tt(-L) ])(
+In the first form, the var(command) is started with the var(args) as
+arguments. After this, the var(name) can be used in further calls to
+tt(zpty) to refer to this command. With the tt(-e) option given, the
+pseudo-terminal will be set up so that input characters are echoed and 
+with the tt(-b) option given, input and output from and to the
+pseudo-terminal will be blocking.
+
+The second form with the tt(-d) option is used to delete commands
+started before by giving their var(names). If no var(names) are given, 
+all commands are deleted. Deleting a command makes the HUP signal be
+sent to the process started for it.
+
+The tt(-w) option can be used to sent the command var(name) the given
+var(strings) as input (separated by spaces). If the tt(-n) option is
+not given, a newline will be sent after the last var(string).
+
+The tt(-r) option can be used to read the output of the command
+var(name). Without a var(param) argument, the string read will be
+printed to standard output. With a var(param) argument, the string
+read will be put in the parameter named var(param). If the
+var(pattern) is also given, output will be read until the whole string 
+read matches the var(pattern).
+
+The last form without any arguments is used to list the commands
+currently defined. If the tt(-L) option is given, this is done in the
+form of calls to the tt(zpty) builtin.
+)
+enditem()
diff -ru ../z.old/Functions/Misc/nslookup Functions/Misc/nslookup
--- ../z.old/Functions/Misc/nslookup	Thu Jan 20 10:48:33 2000
+++ Functions/Misc/nslookup	Thu Jan 20 13:27:35 2000
@@ -1,34 +1,27 @@
 # Simple wrapper function for `nslookup'. With completion if you are using
 # the function based completion system.
 
-setopt localoptions completealiases
+setopt localoptions localtraps completealiases
 
-local char line compcontext=nslookup pid
+local tmp line compcontext=nslookup curcontext=':nslookup' pmpt
 
-trap 'print -p exit;return' INT
+trap 'return 130' INT
+trap 'zpty -d nslookup' EXIT
 
-coproc command nslookup
-pid=$!
+zstyle -s ':nslookup' prompt pmpt || pmpt='> '
 
-while read -pk 1 char; do
-  line="$line$char"
-  [[ "$line" = *'
-> ' ]] && break
-done
-print -nr - "$line"
+zpty nslookup nslookup
+
+zpty -r nslookup line '*> '
+print -nr "$line"
+
+while line=''; vared -p "$pmpt" line; do
+  [[ "$line" = exit ]] && break
+
+  zpty -w nslookup "$line"
 
-line=''
-while vared -p '> ' line; do
-  print -p "$line"
-  line=''
-  while read -pk 1 char; do
-    line="$line$char"
-    [[ "$line" = *'
-> ' ]] && break
-  done
-  print -nr - "$line"
-  line=''
+  zpty -r nslookup line '*> '
+  print -nr "$line"
 done
 
-print -p exit
-wait $pid
+zpty -w nslookup 'exit'
diff -ru ../z.old/Src/Modules/.distfiles Src/Modules/.distfiles
--- ../z.old/Src/Modules/.distfiles	Thu Jan 20 16:25:15 2000
+++ Src/Modules/.distfiles	Thu Jan 20 16:25:19 2000
@@ -10,4 +10,5 @@
     stat.mdd stat.c
     zftp.mdd zftp.c
     zutil.mdd zutil.c
+    zpty.mdd zpty.c
 '
diff -ru ../z.old/Src/Modules/zpty.c Src/Modules/zpty.c
--- ../z.old/Src/Modules/zpty.c	Thu Jan 20 16:07:39 2000
+++ Src/Modules/zpty.c	Thu Jan 20 16:48:10 2000
@@ -0,0 +1,564 @@
+/*
+ * zpty.c - sub-processes with pseudo terminals
+ *
+ * This file is part of zsh, the Z shell.
+ *
+ * Copyright (c) 2000 Sven Wischnowsky
+ * All rights reserved.
+ *
+ * Permission is hereby granted, without written agreement and without
+ * license or royalty fees, to use, copy, modify, and distribute this
+ * software and to distribute modified versions of this software for any
+ * purpose, provided that the above copyright notice and the following
+ * two paragraphs appear in all copies of this software.
+ *
+ * In no event shall Sven Wischnowsky or the Zsh Development Group be liable
+ * to any party for direct, indirect, special, incidental, or consequential
+ * damages arising out of the use of this software and its documentation,
+ * even if Sven Wischnowsky and the Zsh Development Group have been advised of
+ * the possibility of such damage.
+ *
+ * Sven Wischnowsky and the Zsh Development Group specifically disclaim any
+ * warranties, including, but not limited to, the implied warranties of
+ * merchantability and fitness for a particular purpose.  The software
+ * provided hereunder is on an "as is" basis, and Sven Wischnowsky and the
+ * Zsh Development Group have no obligation to provide maintenance,
+ * support, updates, enhancements, or modifications.
+ *
+ */
+
+#include "zpty.mdh"
+#include "zpty.pro"
+
+typedef struct ptycmd *Ptycmd;
+
+struct ptycmd {
+    Ptycmd next;
+    char *name;
+    char **args;
+    int fd;
+    int pid;
+    int echo;
+    int block;
+    int fin;
+};
+
+static Ptycmd ptycmds;
+
+static int
+ptynonblock(int fd)
+{
+#ifdef O_NDELAY
+# ifdef O_NONBLOCK
+#  define NONBLOCK (O_NDELAY|O_NONBLOCK)
+# else /* !O_NONBLOCK */
+#  define NONBLOCK O_NDELAY
+# endif /* !O_NONBLOCK */
+#else /* !O_NDELAY */
+# ifdef O_NONBLOCK
+#  define NONBLOCK O_NONBLOCK
+# else /* !O_NONBLOCK */
+#  define NONBLOCK 0
+# endif /* !O_NONBLOCK */
+#endif /* !O_NDELAY */
+
+#if NONBLOCK
+    long mode;
+
+    mode = fcntl(fd, F_GETFL, 0);
+    if (mode != -1 && !(mode & NONBLOCK) &&
+	!fcntl(fd, F_SETFL, mode | NONBLOCK))
+	return 1;
+
+#endif /* NONBLOCK */
+    return 0;
+
+#undef NONBLOCK
+}
+
+/**/
+static int
+ptygettyinfo(int fd, struct ttyinfo *ti)
+{
+    if (fd != -1) {
+#ifdef HAVE_TERMIOS_H
+# ifdef HAVE_TCGETATTR
+	if (tcgetattr(fd, &ti->tio) == -1)
+# else
+	if (ioctl(fd, TCGETS, &ti->tio) == -1)
+# endif
+	    return 1;
+#else
+# ifdef HAVE_TERMIO_H
+	ioctl(fd, TCGETA, &ti->tio);
+# else
+	ioctl(fd, TIOCGETP, &ti->sgttyb);
+	ioctl(fd, TIOCLGET, &ti->lmodes);
+	ioctl(fd, TIOCGETC, &ti->tchars);
+	ioctl(fd, TIOCGLTC, &ti->ltchars);
+# endif
+#endif
+	return 0;
+    }
+    return 1;
+}
+
+/**/
+static void
+ptysettyinfo(int fd, struct ttyinfo *ti)
+{
+    if (fd != -1) {
+#ifdef HAVE_TERMIOS_H
+# ifdef HAVE_TCGETATTR
+#  ifndef TCSADRAIN
+#   define TCSADRAIN 1	/* XXX Princeton's include files are screwed up */
+#  endif
+	tcsetattr(fd, TCSADRAIN, &ti->tio);
+    /* if (tcsetattr(SHTTY, TCSADRAIN, &ti->tio) == -1) */
+# else
+	ioctl(fd, TCSETS, &ti->tio);
+    /* if (ioctl(SHTTY, TCSETS, &ti->tio) == -1) */
+# endif
+	/*	zerr("settyinfo: %e",NULL,errno)*/ ;
+#else
+# ifdef HAVE_TERMIO_H
+	ioctl(fd, TCSETA, &ti->tio);
+# else
+	ioctl(fd, TIOCSETN, &ti->sgttyb);
+	ioctl(fd, TIOCLSET, &ti->lmodes);
+	ioctl(fd, TIOCSETC, &ti->tchars);
+	ioctl(fd, TIOCSLTC, &ti->ltchars);
+# endif
+#endif
+    }
+}
+
+static Ptycmd
+getptycmd(char *name)
+{
+    Ptycmd p;
+
+    for (p = ptycmds; p; p = p->next)
+	if (!strcmp(p->name, name))
+	    return p;
+
+    return NULL;
+}
+
+/**** maybe we should use configure here */
+/**** and we certainly need more/better #if tests */
+
+#ifdef __osf__
+
+static int
+get_pty(int *master, int *slave)
+{
+    return openpty(master, slave, NULL, NULL, NULL);
+}
+
+#else /* ! __osf__ */
+
+#if __SVR4
+
+#include <sys/stropts.h>
+
+static int
+get_pty(int *master, int *slave)
+{
+    int mfd, sfd;
+    char *name;
+
+    if ((mfd = open("/dev/ptmx", O_RDWR)) < 0)
+	return 1;
+
+    if (!(name = ptsname(mfd)) || grantpt(mfd) || unlockpt(mfd)) {
+	close(mfd);
+	return 1;
+    }
+    if ((sfd = open(name, O_RDWR)) < 0) {
+	close(mfd);
+	return 1;
+    }
+    if (ioctl(sfd, I_PUSH, "ptem") ||
+	ioctl(sfd, I_PUSH, "ldterm") ||
+	ioctl(sfd, I_PUSH, "ttcompat")) {
+	close(mfd);
+	close(sfd);
+	return 1;
+    }
+    *master = mfd;
+    *slave = sfd;
+
+    return 0;
+}
+
+#else /* ! __SVR4 */
+
+static int
+get_pty(int *master, int *slave)
+{
+
+#ifdef __linux
+    static char char1[] = "abcdefghijklmnopqrstuvwxyz";
+    static char char2[] = "0123456789abcdef";
+#else /* ! __linux */
+    static char char1[] = "pq";
+    static char char2[] = "0123456789abcdef";
+#endif /* __linux */
+
+    char name[11], *p1, *p2;
+    int mfd, sfd;
+
+    strcpy(name, "/dev/ptyxx");
+
+    for (p1 = char1; *p1; p1++) {
+	name[8] = *p1;
+	for (p2 = char2; *p2; p2++) {
+	    name[9] = *p2;
+	    if ((mfd = open(name, O_RDWR)) >= 0) {
+		name[5] = 't';
+		if ((sfd = open(name, O_RDWR)) >= 0) {
+		    *master = mfd;
+		    *slave = sfd;
+
+		    return 0;
+		}
+		name[5] = 'p';
+		close(mfd);
+	    }
+	}
+    }
+    return 1;
+}
+
+#endif /* __SVR4 */
+#endif /* __osf__ */
+
+static int
+newptycmd(char *nam, char *pname, char **args, int echo, int block)
+{
+    Ptycmd p;
+    int master, slave, pid;
+    char *cmd;
+
+    if (!(cmd = findcmd(*args, 1))) {
+	zerrnam(nam, "unknown command: %s", *args, 0);
+	return 1;
+    }
+    if (get_pty(&master, &slave)) {
+	zerrnam(nam, "can't open pseudo terminal", NULL, 0);
+	return 1;
+    }
+    if ((pid = fork()) == -1) {
+	close(master);
+	close(slave);
+	zerrnam(nam, "couldn't create pty command: %s", pname, 0);
+	return 1;
+    } else if (!pid) {
+	if (!echo) {
+	    struct ttyinfo info;
+
+	    if (!ptygettyinfo(slave, &info)) {
+#ifdef HAVE_TERMIOS_H
+		info.tio.c_lflag &= ~ECHO;
+#else
+#ifdef HAVE_TERMIO_H
+		info.tio.c_lflag &= ~ECHO;
+#else
+		info.tio.lmodes &= ~ECHO; /**** dunno if this is right */
+#endif
+#endif
+		ptysettyinfo(slave, &info);
+	    }
+	}
+
+#ifdef TIOCGWINSZ
+	if (interact) {
+	    struct ttyinfo info;
+
+	    if (ioctl(slave, TIOCGWINSZ, (char *) &info.winsize) == 0) {
+		info.winsize.ws_row = lines;
+		info.winsize.ws_col = columns;
+		ioctl(slave, TIOCSWINSZ, (char *) &info.winsize);
+	    }
+	}
+#endif /* TIOCGWINSZ */
+
+	signal_default(SIGTERM);
+	signal_default(SIGINT);
+	signal_default(SIGQUIT);
+
+	close(0);
+	close(1);
+	close(2);
+
+	dup2(slave, 0);
+	dup2(slave, 1);
+	dup2(slave, 2);
+
+	close(slave);
+
+	execve(cmd, args, environ);
+	exit(0);
+    }
+    master = movefd(master);
+    close(slave);
+
+    p = (Ptycmd) zalloc(sizeof(*p));
+
+    p->name = ztrdup(pname);
+    PERMALLOC {
+	p->args = arrdup(args);
+    } LASTALLOC;
+    p->fd = master;
+    p->pid = pid;
+    p->echo = echo;
+    p->block = block;
+    p->fin = 0;
+
+    p->next = ptycmds;
+    ptycmds = p;
+
+    if (!block)
+	ptynonblock(master);
+
+    return 0;
+}
+
+static void
+deleteptycmd(Ptycmd cmd)
+{
+    Ptycmd p, q;
+
+    for (q = NULL, p = ptycmds; p != cmd; q = p, p = p->next);
+
+    if (p != cmd)
+	return;
+
+    if (q)
+	q->next = p->next;
+    else
+	ptycmds = p->next;
+
+    zsfree(p->name);
+    freearray(p->args);
+
+    kill(p->pid, SIGHUP);
+
+    zclose(cmd->fd);
+
+    zfree(p, sizeof(*p));
+}
+
+static void
+deleteallptycmds(void)
+{
+    Ptycmd p, n;
+
+    for (p = ptycmds; p; p = n) {
+	n = p->next;
+	deleteptycmd(p);
+    }
+}
+
+/**** a better process handling would be nice */
+
+static void
+checkptycmd(Ptycmd cmd)
+{
+    if (kill(cmd->pid, 0) < 0) {
+	cmd->fin = 1;
+	zclose(cmd->fd);
+    }
+}
+
+static int
+ptyread(char *nam, Ptycmd cmd, char **args)
+{
+    int blen = 256, used = 0, ret;
+    char *buf = (char *) zhalloc(blen + 1);
+    Patprog prog = NULL;
+
+    if (*args && args[1]) {
+	char *p;
+
+	if (args[2]) {
+	    zerrnam(nam, "too many arguments", NULL, 0);
+	    return 1;
+	}
+	p = dupstring(args[1]);
+	tokenize(p);
+	remnulargs(p);
+	if (!(prog = patcompile(p, PAT_STATIC, NULL))) {
+	    zerrnam(nam, "bad pattern: %s", args[1], 0);
+	    return 1;
+	}
+    }
+    do {
+	while ((ret = read(cmd->fd, buf + used, 1)) == 1) {
+	    if (++used == blen) {
+		buf = hrealloc(buf, blen, blen << 1);
+		blen <<= 1;
+	    }
+	}
+	buf[used] = '\0';
+    } while (prog && !pattry(prog, buf));
+
+    if (*args)
+	setsparam(*args, ztrdup(buf));
+    else
+	printf("%s", buf);
+
+    return !used;
+}
+
+static int
+ptywrite(Ptycmd cmd, char **args, int nonl)
+{
+    if (*args) {
+	char sp = ' ';
+
+	while (*args) {
+	    write(cmd->fd, *args, strlen(*args));
+
+	    if (*++args)
+		write(cmd->fd, &sp, 1);
+	}
+	if (!nonl) {
+	    sp = '\n';
+	    write(cmd->fd, &sp, 1);
+	}
+    } else {
+	int n;
+	char buf[BUFSIZ];
+
+	while ((n = read(0, buf, BUFSIZ)) > 0)
+	    write(cmd->fd, buf, n);
+    }
+    return 0;
+}
+
+/**/
+static int
+bin_zpty(char *nam, char **args, char *ops, int func)
+{
+    if ((ops['r'] && ops['w']) ||
+	((ops['r'] || ops['w']) && (ops['d'] || ops['e'] ||
+				    ops['b'] || ops['L'])) ||
+	(ops['n'] && (ops['b'] || ops['e'] || ops['r'] ||
+		      ops['d'] || ops['L'])) ||
+	(ops['d'] && (ops['b'] || ops['e'] || ops['L'])) ||
+	(ops['L'] && (ops['b'] || ops['e']))) {
+	zerrnam(nam, "illegal option combination", NULL, 0);
+	return 1;
+    }
+    if (ops['r'] || ops['w']) {
+	Ptycmd p;
+
+	if (!*args) {
+	    zerrnam(nam, "missing pty command name", NULL, 0);
+	    return 1;
+	} else if (!(p = getptycmd(*args))) {
+	    zerrnam(nam, "no such pty command: %s", *args, 0);
+	    return 1;
+	}
+	checkptycmd(p);
+	if (p->fin)
+	    return 1;
+	return (ops['r'] ?
+		ptyread(nam, p, args + 1) :
+		ptywrite(p, args + 1, ops['n']));
+    } else if (ops['d']) {
+	Ptycmd p;
+	int ret = 0;
+
+	if (*args) {
+	    while (*args)
+		if ((p = getptycmd(*args++)))
+		    deleteptycmd(p);
+		else {
+		    zwarnnam(nam, "no such pty command: %s", args[-1], 0);
+		    ret = 1;
+		}
+	} else
+	    deleteallptycmds();
+
+	return ret;
+    } else if (*args) {
+	if (!args[1]) {
+	    zerrnam(nam, "missing command", NULL, 0);
+	    return 1;
+	}
+	if (getptycmd(*args)) {
+	    zerrnam(nam, "pty command name already used: %s", *args, 0);
+	    return 1;
+	}
+	return newptycmd(nam, *args, args + 1, ops['e'], ops['b']);
+    } else {
+	Ptycmd p;
+	char **a;
+
+	for (p = ptycmds; p; p = p->next) {
+	    checkptycmd(p);
+	    if (ops['L'])
+		printf("%s %s%s%s ", nam, (p->echo ? "-e " : ""),
+		       (p->block ? "-b " : ""), p->name);
+	    else if (p->fin)
+		printf("(finished) %s: ", p->name);
+	    else
+		printf("(%d) %s: ", p->pid, p->name);
+	    for (a = p->args; *a; ) {
+		quotedzputs(*a++, stdout);
+		if (*a)
+		    putchar(' ');
+	    }
+	    putchar('\n');
+	}
+	return 0;
+    }
+}
+
+static int
+ptyhook(Hookdef d, void *dummy)
+{
+    deleteallptycmds();
+    return 0;
+}
+
+static struct builtin bintab[] = {
+    BUILTIN("zpty", 0, bin_zpty, 0, -1, 0, "ebdrwLn", NULL),
+};
+
+/**/
+int
+setup_(Module m)
+{
+    return 0;
+}
+
+/**/
+int
+boot_(Module m)
+{
+    ptycmds = NULL;
+
+    addhookfunc("exit", ptyhook);
+    return !addbuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
+}
+
+/**/
+int
+cleanup_(Module m)
+{
+    deletehookfunc("exit", ptyhook);
+    deleteallptycmds();
+    deletebuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
+    return 0;
+}
+
+/**/
+int
+finish_(Module m)
+{
+    return 0;
+}
diff -ru ../z.old/Src/Modules/zpty.mdd Src/Modules/zpty.mdd
--- ../z.old/Src/Modules/zpty.mdd	Thu Jan 20 16:07:39 2000
+++ Src/Modules/zpty.mdd	Thu Jan 20 11:05:18 2000
@@ -0,0 +1,5 @@
+name=zsh/zpty
+
+autobins="zpty"
+
+objects="zpty.o"
diff -ru ../z.old/Src/builtin.c Src/builtin.c
--- ../z.old/Src/builtin.c	Thu Jan 20 10:47:33 2000
+++ Src/builtin.c	Thu Jan 20 11:05:19 2000
@@ -3219,6 +3219,7 @@
 	}
 	if (sigtrapped[SIGEXIT])
 	    dotrap(SIGEXIT);
+	runhookdef(EXITHOOK, NULL);
 	if (mypid != getpid())
 	    _exit(val);
 	else
diff -ru ../z.old/Src/init.c Src/init.c
--- ../z.old/Src/init.c	Thu Jan 20 10:47:34 2000
+++ Src/init.c	Thu Jan 20 11:05:19 2000
@@ -89,6 +89,11 @@
 mod_export int alloc_stackp;
 #endif
 
+/**/
+mod_export struct hookdef zshhooks[] = {
+    HOOKDEF("exit", NULL, HOOKF_ALL),
+};
+
 /* keep executing lists until EOF found */
 
 /**/
@@ -559,6 +564,8 @@
     int fpathlen = 0;
 # endif
 #endif
+
+    addhookdefs(argzero, zshhooks, sizeof(zshhooks)/sizeof(*zshhooks));
 
     init_eprog();
 
diff -ru ../z.old/Src/zsh.h Src/zsh.h
--- ../z.old/Src/zsh.h	Thu Jan 20 10:47:38 2000
+++ Src/zsh.h	Thu Jan 20 11:05:19 2000
@@ -1641,3 +1641,9 @@
 /***************************************/
 
 #define mod_export
+
+/***************************************/
+/* Hooks in core.                      */
+/***************************************/
+
+#define EXITHOOK (zshhooks + 0)

--
Sven Wischnowsky                         wischnow@xxxxxxxxxxxxxxxxxxxxxxx



Messages sorted by: Reverse Date, Date, Thread, Author