[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: CVS problem with ssh
From: |
Paul Eggert |
Subject: |
Re: CVS problem with ssh |
Date: |
Sat, 16 Jul 2005 18:58:05 -0700 |
User-agent: |
Gnus/5.1007 (Gnus v5.10.7) Emacs/21.4 (gnu/linux) |
It is possible to fix the problem without creating an intervening "cat
-u"-like process as was done in the patch I just sent in. This can be
done by creating a pipe, redirecting ssh's stderr into this pipe, and
having cvs read from the pipe and write to stderr. I sketched out a
solution to this problem, enclosed below, but I haven't had time to
make it real (it doesn't work as written).
I'm sending this incomplete patch only because perhaps someone else
might want to run with it.
diff -pubr cvs-1.12.12/src/buffer.c cvs-1.12.12-nonblock-1/src/buffer.c
--- cvs-1.12.12/src/buffer.c 2005-03-17 11:42:23.000000000 -0800
+++ cvs-1.12.12-nonblock-1/src/buffer.c 2005-07-16 12:18:30.000000000 -0700
@@ -1717,6 +1717,8 @@ struct fd_buffer
{
/* The file descriptor. */
int fd;
+ /* The error file descriptor, or -1 if this does not apply. */
+ int errorfd;
/* Nonzero if the file descriptor is in blocking mode. */
int blocking;
/* The child process id when fd is a pipe. */
@@ -1733,18 +1735,21 @@ static int fd_buffer_get_fd (void *);
static int fd_buffer_shutdown (struct buffer *);
/* Initialize a buffer built on a file descriptor. FD is the file
- descriptor. INPUT is nonzero if this is for input, zero if this is
- for output. MEMORY is the function to call when a memory error
- occurs. */
+ descriptor. ERRORFD is an error file descriptor; normally it is
+ -1, representing an unused value, but can be nonnegative if FD is
+ an input FD; ERRORFD represents stderr from the same process.
+ INPUT is nonzero if FD is for input, zero if for output. MEMORY is
+ the function to call when a memory error occurs. */
struct buffer *
-fd_buffer_initialize (int fd, pid_t child_pid, cvsroot_t *root, bool input,
- void (*memory) (struct buffer *))
+fd_buffer_initialize (int fd, int errorfd, pid_t child_pid, cvsroot_t *root,
+ bool input, void (*memory) (struct buffer *))
{
struct fd_buffer *n;
n = xmalloc (sizeof *n);
n->fd = fd;
+ n->errorfd = errorfd;
n->child_pid = child_pid;
n->root = root;
fd_buffer_block (n, true);
@@ -1758,10 +1763,34 @@ fd_buffer_initialize (int fd, pid_t chil
}
+/* If ERRORFD is a file descriptor, copy it to standard error.
+ Return true if successful, false if EOF reached. */
+
+static bool
+copy_error_fd (int errorfd)
+{
+ if (0 <= errorfd)
+ for (;;)
+ {
+ char errbuf [1 << 13];
+ ssize_t r = read (errorfd, errbuf, sizeof errbuf);
+ if (0 < r)
+ {
+ fflush (stdout);
+ fwrite (errbuf, 1, r, stderr);
+ }
+ else if (r == 0)
+ return false;
+ else if (r < 0 && errno != EINTR)
+ break;
+ }
+
+ return true;
+}
/* The buffer input function for a buffer built on a file descriptor.
*
- * In non-blocing mode, this function will read as many bytes as it can in a
+ * In non-blocking mode, this function will read as many bytes as it can in a
* single try, up to SIZE bytes, and return.
*
* In blocking mode with NEED > 0, this function will read as many bytes as it
@@ -1801,6 +1830,11 @@ fd_buffer_input (void *closure, char *da
{
struct fd_buffer *fb = closure;
int nbytes;
+ int numfds;
+ int errorfd = fb->errorfd;
+ int maxfd = MAX (fb->fd, errorfd);
+ fd_set readfds;
+ char errbuf[1 << 13];
assert (need <= size);
@@ -1809,28 +1843,39 @@ fd_buffer_input (void *closure, char *da
if (fb->blocking)
{
int status;
- fd_set readfds;
/* Set non-block. */
status = fd_buffer_block (fb, false);
if (status != 0) return status;
- FD_ZERO (&readfds);
- FD_SET (fb->fd, &readfds);
do
{
- int numfds;
-
do {
/* This used to select on exceptions too, but as far
as I know there was never any reason to do that and
SCO doesn't let you select on exceptions on pipes. */
- numfds = select (fb->fd + 1, &readfds, NULL, NULL, NULL);
+ FD_ZERO (&readfds);
+ FD_SET (fb->fd, &readfds);
+ if (0 <= errorfd)
+ FD_SET (errorfd, &readfds);
+ numfds = select (maxfd + 1, &readfds, NULL, NULL, NULL);
if (numfds < 0 && errno != EINTR)
{
status = errno;
goto block_done;
}
+ if (0 < numfds && 0 <= errorfd && FD_ISSET (errorfd, &readfds))
+ {
+ ssize_t r = read (errorfd, errbuf, sizeof errbuf);
+ if (r == 0)
+ fb->errorfd = errorfd = -1;
+ else if (0 < r)
+ {
+ fflush (stdout);
+ fwrite (errbuf, 1, r, stderr);
+ }
+ continue;
+ }
} while (numfds < 0);
nbytes = read (fb->fd, data + *got, size - *got);
@@ -1883,6 +1928,10 @@ block_done:
}
/* The above will always return. Handle non-blocking read. */
+
+ if (! copy_error_fd (errorfd))
+ fb->errorfd = -1;
+
nbytes = read (fb->fd, data, size);
if (nbytes > 0)
@@ -1954,11 +2003,12 @@ fd_buffer_output (void *closure, const c
static int
fd_buffer_flush (void *closure)
{
- /* We don't need to do anything here. Our fd doesn't have its own buffer
+ struct fd_buffer *fb = closure;
+ copy_error_fd (fb->errorfd);
+
+ /* We don't need to sync here. Our fd doesn't have its own buffer
* and syncing won't do anything but slow us down.
*
- * struct fd_buffer *fb = closure;
- *
* if (fsync (fb->fd) < 0 && errno != EROFS && errno != EINVAL)
* return errno;
*/
@@ -1970,15 +2020,13 @@ fd_buffer_flush (void *closure)
static struct stat devnull;
static int devnull_set = -1;
-/* The buffer block function for a buffer built on a file descriptor. */
-static int
-fd_buffer_block (void *closure, bool block)
+static void
+block_fd (int fd, bool block)
{
- struct fd_buffer *fb = closure;
# if defined (F_GETFL) && defined (O_NONBLOCK) && defined (F_SETFL)
int flags;
- flags = fcntl (fb->fd, F_GETFL, 0);
+ flags = fcntl (fd, F_GETFL, 0);
if (flags < 0)
return errno;
@@ -1987,7 +2035,7 @@ fd_buffer_block (void *closure, bool blo
else
flags |= O_NONBLOCK;
- if (fcntl (fb->fd, F_SETFL, flags) < 0)
+ if (fcntl (fd, F_SETFL, flags) < 0)
{
/*
* BSD returns ENODEV when we try to set block/nonblock on /dev/null.
@@ -2002,7 +2050,7 @@ fd_buffer_block (void *closure, bool blo
if (devnull_set >= 0)
/* Equivalent to /dev/null ? */
- isdevnull = (fstat (fb->fd, &sb) >= 0
+ isdevnull = (fstat (fd, &sb) >= 0
&& sb.st_dev == devnull.st_dev
&& sb.st_ino == devnull.st_ino
&& sb.st_mode == devnull.st_mode
@@ -2020,6 +2068,16 @@ fd_buffer_block (void *closure, bool blo
}
}
# endif /* F_GETFL && O_NONBLOCK && F_SETFL */
+}
+
+/* The buffer block function for a buffer built on a file descriptor. */
+static int
+fd_buffer_block (void *closure, bool block)
+{
+ struct fd_buffer *fb = closure;
+ block_fd (fb->fd, block);
+ if (0 <= fb->errorfd)
+ block_fd (fb->errorfd, block);
fb->blocking = block;
diff -pubr cvs-1.12.12/src/buffer.h cvs-1.12.12-nonblock-1/src/buffer.h
--- cvs-1.12.12/src/buffer.h 2005-03-15 09:45:09.000000000 -0800
+++ cvs-1.12.12-nonblock-1/src/buffer.h 2005-07-15 22:12:51.000000000 -0700
@@ -162,8 +162,8 @@ int buf_count_mem (struct buffer *);
#endif /* SERVER_FLOWCONTROL */
struct buffer *
-fd_buffer_initialize (int fd, pid_t child_pid, cvsroot_t *root, bool input,
- void (*memory) (struct buffer *));
+fd_buffer_initialize (int fd, int errorfd, pid_t child_pid, cvsroot_t *root,
+ bool input, void (*memory) (struct buffer *));
/* EWOULDBLOCK is not defined by POSIX, but some BSD systems will
return it, rather than EAGAIN, for nonblocking writes. */
diff -pubr cvs-1.12.12/src/client.c cvs-1.12.12-nonblock-1/src/client.c
--- cvs-1.12.12/src/client.c 2005-04-14 07:13:25.000000000 -0700
+++ cvs-1.12.12-nonblock-1/src/client.c 2005-07-15 22:12:20.000000000 -0700
@@ -2718,9 +2718,6 @@ handle_wrapper_rcs_option (char *args, s
static void
handle_m (char *args, size_t len)
{
- fd_set wfds;
- int s;
-
/* In the case where stdout and stderr point to the same place,
fflushing stderr will make output happen in the correct order.
Often stderr will be line-buffered and this won't be needed,
@@ -2728,12 +2725,6 @@ handle_m (char *args, size_t len)
based on being confused between default buffering between
stdout and stderr. But I'm not sure). */
fflush (stderr);
- FD_ZERO (&wfds);
- FD_SET (STDOUT_FILENO, &wfds);
- errno = 0;
- s = select (STDOUT_FILENO+1, NULL, &wfds, NULL, NULL);
- if (s < 1 && errno != 0)
- perror ("cannot write to stdout");
fwrite (args, len, sizeof (*args), stdout);
putc ('\n', stdout);
}
@@ -2779,24 +2770,9 @@ handle_mbinary (char *args, size_t len)
static void
handle_e (char *args, size_t len)
{
- fd_set wfds;
- int s;
-
/* In the case where stdout and stderr point to the same place,
fflushing stdout will make output happen in the correct order. */
fflush (stdout);
- FD_ZERO (&wfds);
- FD_SET (STDERR_FILENO, &wfds);
- errno = 0;
- s = select (STDERR_FILENO+1, NULL, &wfds, NULL, NULL);
- /*
- * If stderr has problems, then adding a call to
- * perror ("cannot write to stderr")
- * will not work. So, try to write a message on stdout and
- * terminate cvs.
- */
- if (s < 1 && errno != 0)
- fperrmsg (stdout, 1, errno, "cannot write to stderr");
fwrite (args, len, sizeof (*args), stderr);
putc ('\n', stderr);
}
@@ -3369,14 +3345,15 @@ get_proxy_port_number (const cvsroot_t *
void
-make_bufs_from_fds(int tofd, int fromfd, int child_pid, cvsroot_t *root,
+make_bufs_from_fds(int tofd, int fromfd, int errorfd,
+ int child_pid, cvsroot_t *root,
struct buffer **to_server_p,
struct buffer **from_server_p, int is_sock)
{
# ifdef NO_SOCKET_TO_FD
if (is_sock)
{
- assert (tofd == fromfd);
+ assert (tofd == fromfd && errorfd < 0);
*to_server_p = socket_buffer_initialize (tofd, 0, NULL);
*from_server_p = socket_buffer_initialize (tofd, 1, NULL);
}
@@ -3386,6 +3363,8 @@ make_bufs_from_fds(int tofd, int fromfd,
/* todo: some OS's don't need these calls... */
close_on_exec (tofd);
close_on_exec (fromfd);
+ if (0 <= errorfd)
+ close_on_exec (errorfd);
/* SCO 3 and AIX have a nasty bug in the I/O libraries which precludes
fdopening the same file descriptor twice, so dup it if it is the
@@ -3403,9 +3382,9 @@ make_bufs_from_fds(int tofd, int fromfd,
* child_pid in there. In theory, it should be stored in both
* buffers with a ref count...
*/
- *to_server_p = fd_buffer_initialize (tofd, 0, root, false, NULL);
- *from_server_p = fd_buffer_initialize (fromfd, child_pid, root,
- true, NULL);
+ *to_server_p = fd_buffer_initialize (tofd, -1, 0, root, false, NULL);
+ *from_server_p = fd_buffer_initialize (fromfd, errorfd, child_pid,
+ root, true, NULL);
}
}
#endif /* defined (AUTH_CLIENT_SUPPORT) || defined (SERVER_SUPPORT) || defined
(HAVE_KERBEROS) || defined(HAVE_GSSAPI) */
@@ -3472,7 +3451,7 @@ connect_to_pserver (cvsroot_t *root, str
root->proxy_hostname ? proxy_port_number : port_number,
SOCK_STRERROR (SOCK_ERRNO));
- make_bufs_from_fds (sock, sock, 0, root, &to_server, &from_server, 1);
+ make_bufs_from_fds (sock, sock, -1, 0, root, &to_server, &from_server, 1);
/* if we have proxy then connect to the proxy first */
if (root->proxy_hostname)
@@ -3758,11 +3737,11 @@ connect_to_forked_server (cvsroot_t *roo
TRACE (TRACE_FUNCTION, "Forking server: %s %s",
command[0] ? command[0] : "(null)", command[1]);
- child_pid = piped_child (command, &tofd, &fromfd);
+ child_pid = piped_child (command, &tofd, &fromfd, NULL);
if (child_pid < 0)
error (1, 0, "could not fork server process");
- make_bufs_from_fds (tofd, fromfd, child_pid, root, to_server_p,
+ make_bufs_from_fds (tofd, fromfd, -1, child_pid, root, to_server_p,
from_server_p, 0);
}
#endif /* CLIENT_SUPPORT || SERVER_SUPPORT */
@@ -3850,10 +3829,10 @@ open_connection_to_server (cvsroot_t *ro
root->hostname,
root->directory);
# ifdef START_SERVER_RETURNS_SOCKET
- make_bufs_from_fds (tofd, fromfd, 0, root, to_server_p,
+ make_bufs_from_fds (tofd, fromfd, -1, 0, root, to_server_p,
from_server_p, 1);
# else /* ! START_SERVER_RETURNS_SOCKET */
- make_bufs_from_fds (tofd, fromfd, 0, root, to_server_p,
+ make_bufs_from_fds (tofd, fromfd, -1, 0, root, to_server_p,
from_server_p, 0);
# endif /* START_SERVER_RETURNS_SOCKET */
}
diff -pubr cvs-1.12.12/src/client.h cvs-1.12.12-nonblock-1/src/client.h
--- cvs-1.12.12/src/client.h 2004-10-14 09:44:45.000000000 -0700
+++ cvs-1.12.12-nonblock-1/src/client.h 2005-07-15 23:45:41.000000000 -0700
@@ -9,7 +9,7 @@ extern int file_gzip_level;
struct buffer;
-void make_bufs_from_fds (int, int, int, cvsroot_t *,
+void make_bufs_from_fds (int, int, int, int, cvsroot_t *,
struct buffer **, struct buffer **, int);
diff -pubr cvs-1.12.12/src/cvs.h cvs-1.12.12-nonblock-1/src/cvs.h
--- cvs-1.12.12/src/cvs.h 2005-04-14 07:14:55.000000000 -0700
+++ cvs-1.12.12-nonblock-1/src/cvs.h 2005-07-15 22:07:02.000000000 -0700
@@ -666,7 +666,7 @@ int run_piped (int *, int *);
/* other similar-minded stuff from run.c. */
FILE *run_popen (const char *, const char *);
-int piped_child (char *const *, int *, int *);
+int piped_child (char *const *, int *, int *, int *);
void close_on_exec (int);
pid_t waitpid (pid_t, int *, int);
diff -pubr cvs-1.12.12/src/kerberos4-client.c
cvs-1.12.12-nonblock-1/src/kerberos4-client.c
--- cvs-1.12.12/src/kerberos4-client.c 2005-04-14 07:13:26.000000000 -0700
+++ cvs-1.12.12-nonblock-1/src/kerberos4-client.c 2005-07-15
23:45:45.000000000 -0700
@@ -95,7 +95,7 @@ start_kerberos4_server (cvsroot_t *root,
free (hname);
/* Give caller the values it wants. */
- make_bufs_from_fds (s, s, 0, to_server_p, from_server_p, 1);
+ make_bufs_from_fds (s, s, -1, 0, to_server_p, from_server_p, 1);
}
void
@@ -107,4 +107,3 @@ initialize_kerberos4_encryption_buffers(
*from_server_p = krb_encrypt_buffer_initialize (*from_server_p, 1,
sched, kblock, NULL);
}
-
diff -pubr cvs-1.12.12/src/rsh-client.c cvs-1.12.12-nonblock-1/src/rsh-client.c
--- cvs-1.12.12/src/rsh-client.c 2005-03-15 09:45:10.000000000 -0800
+++ cvs-1.12.12-nonblock-1/src/rsh-client.c 2005-07-15 23:45:44.000000000
-0700
@@ -118,7 +118,7 @@ start_rsh_server (cvsroot_t *root, struc
error (1, errno, "cannot start server via rsh");
/* Give caller the file descriptors in a form it can deal with. */
- make_bufs_from_fds (pipes[0], pipes[1], child_pid, to_server_p,
+ make_bufs_from_fds (pipes[0], pipes[1], -1, child_pid, to_server_p,
from_server_p, 0);
}
@@ -136,7 +136,7 @@ start_rsh_server (cvsroot_t *root, struc
char *cvs_server = (root->cvs_server != NULL
? root->cvs_server : getenv ("CVS_SERVER"));
char *command;
- int tofd, fromfd;
+ int tofd, fromfd, errorfd;
int child_pid;
if (!cvs_rsh)
@@ -184,14 +184,14 @@ start_rsh_server (cvsroot_t *root, struc
fprintf (stderr, "%s ", argv[i]);
putc ('\n', stderr);
}
- child_pid = piped_child (argv, &tofd, &fromfd);
+ child_pid = piped_child (argv, &tofd, &fromfd, &errorfd);
if (child_pid < 0)
error (1, errno, "cannot start server via rsh");
}
free (command);
- make_bufs_from_fds (tofd, fromfd, child_pid, root, to_server_p,
+ make_bufs_from_fds (tofd, fromfd, errorfd, child_pid, root, to_server_p,
from_server_p, 0);
}
diff -pubr cvs-1.12.12/src/run.c cvs-1.12.12-nonblock-1/src/run.c
--- cvs-1.12.12/src/run.c 2005-04-14 07:13:26.000000000 -0700
+++ cvs-1.12.12-nonblock-1/src/run.c 2005-07-16 11:29:34.000000000 -0700
@@ -1,4 +1,4 @@
-/* run.c --- routines for executing subprocesses.
+!/* run.c --- routines for executing subprocesses.
This file is part of GNU CVS.
@@ -430,18 +430,112 @@ run_popen (const char *cmd, const char *
}
+/* Work around an OpenSSH problem: it can put its standard file
+ descriptors into nonblocking mode, which will mess us up if we
+ share file descriptions with it. The simplest workaround is
+ to create an intervening process between OpenSSH and the
+ actual stderr. */
+
+static void
+work_around_openssh_glitch (void)
+{
+ pid_t pid;
+ int stderr_pipe[2];
+ struct stat sb;
+
+ /* Do nothing unless stderr is a file that is affected by
+ nonblocking mode. */
+ if (! (fstat (STDERR_FILENO, &sb) == 0
+ && (S_ISFIFO (sb.st_mode) || S_ISSOCK (sb.st_mode)
+ || S_ISCHR (sb.st_mode) || S_ISBLK (sb.st_mode))))
+ return;
+
+ if (pipe (stderr_pipe) < 0)
+ error (1, errno, "cannot create pipe");
+ pid = fork ();
+ if (pid < 0)
+ error (1, errno, "cannot fork");
+ if (pid != 0)
+ {
+ /* Still in child of original process. Act like "cat -u". */
+ char buf[1 << 13];
+ ssize_t inbytes;
+ pid_t w;
+ int status;
+
+ if (close (stderr_pipe[1]) < 0)
+ error (1, errno, "cannot close pipe");
+
+ while ((inbytes = read (stderr_pipe[0], buf, sizeof buf)) != 0)
+ {
+ size_t outbytes = 0;
+
+ if (inbytes < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ error (1, errno, "reading from pipe");
+ }
+
+ do
+ {
+ ssize_t w = write (STDERR_FILENO,
+ buf + outbytes, inbytes - outbytes);
+ if (w < 0)
+ {
+ if (errno = EINTR)
+ w = 0;
+ if (w < 0)
+ _exit (1);
+ }
+ outbytes += w;
+ }
+ while (inbytes != outbytes);
+ }
+
+ /* Done processing output from grandchild. Propagate
+ its exit status back to the parent. */
+ while ((w = waitpid (pid, &status, 0)) == -1 && errno == EINTR)
+ continue;
+ if (w < 0)
+ error (1, errno, "waiting for child");
+ if (! WIFEXITED (status))
+ {
+ if (WIFSIGNALED (status))
+ raise (WTERMSIG (status));
+ error (1, errno, "child did not exit cleanly");
+ }
+ _exit (WEXITSTATUS (status));
+ }
+
+ /* Grandchild of original process. */
+ if (close (stderr_pipe[0]) < 0)
+ error (1, errno, "cannot close pipe");
+
+ if (stderr_pipe[1] != STDERR_FILENO)
+ {
+ if (dup2 (stderr_pipe[1], STDERR_FILENO) < 0)
+ error (1, errno, "cannot dup2 pipe");
+ if (close (stderr_pipe[1]) < 0)
+ error (1, errno, "cannot close pipe");
+ }
+}
+
int
-piped_child (char *const *command, int *tofdp, int *fromfdp)
+piped_child (char *const *command, int *tofdp, int *fromfdp, int *errorfdp)
{
int pid;
int to_child_pipe[2];
int from_child_pipe[2];
+ int error_child_pipe[2];
if (pipe (to_child_pipe) < 0)
error (1, errno, "cannot create pipe");
if (pipe (from_child_pipe) < 0)
error (1, errno, "cannot create pipe");
+ if (errorfdp && pipe (error_child_pipe) < 0)
+ error (1, errno, "cannot create pipe");
#ifdef USE_SETMODE_BINARY
setmode (to_child_pipe[0], O_BINARY);
@@ -467,6 +561,13 @@ piped_child (char *const *command, int *
error (1, errno, "cannot close pipe");
if (dup2 (from_child_pipe[1], STDOUT_FILENO) < 0)
error (1, errno, "cannot dup2 pipe");
+ if (errorfdp)
+ {
+ if (close (error_child_pipe[0]) < 0)
+ error (1, errno, "cannot close pipe");
+ if (dup2 (error_child_pipe[1], STDERR_FILENO) < 0)
+ error (1, errno, "cannot dup2 pipe");
+ }
/* Okay to cast out const below - execvp don't return anyhow. */
execvp ((char *)command[0], (char **)command);
@@ -476,9 +577,16 @@ piped_child (char *const *command, int *
error (1, errno, "cannot close pipe");
if (close (from_child_pipe[1]) < 0)
error (1, errno, "cannot close pipe");
+ if (errorfdp && close (error_child_pipe[1] < 0))
+ error (1, errno, "cannot close pipe");
*tofdp = to_child_pipe[1];
*fromfdp = from_child_pipe[0];
+ if (errorfdp)
+ {
+ *errorfdp = error_child_pipe[0];
+ set_nonblock_fd (*errorfdp);
+ }
return pid;
}
@@ -488,7 +596,7 @@ int
run_piped (int *tofdp, int *fromfdp)
{
run_add_arg (NULL);
- return piped_child (run_argv, tofdp, fromfdp);
+ return piped_child (run_argv, tofdp, fromfdp, NULL);
}
- Re: CVS problem with ssh, (continued)
- Re: CVS problem with ssh, Paul Eggert, 2005/07/14
- Re: CVS problem with ssh, Richard M. Stallman, 2005/07/15
- Re: CVS problem with ssh, Larry Jones, 2005/07/15
- Re: CVS problem with ssh, Richard M. Stallman, 2005/07/15
- Re: CVS problem with ssh, Larry Jones, 2005/07/15
- Re: CVS problem with ssh, Richard M. Stallman, 2005/07/16
- Re: CVS problem with ssh, Derek Price, 2005/07/15
- Message not available
- Message not available
- Re: CVS problem with ssh, Paul Eggert, 2005/07/16
- Re: CVS problem with ssh, Alexander Taler, 2005/07/19
Re: CVS problem with ssh, Richard M. Stallman, 2005/07/13
Re: CVS problem with ssh,
Paul Eggert <=