1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2025-12-21 21:20:44 -08:00

Allow creating processes where only one of stdin or stdout is a PTY

* src/lisp.h (emacs_spawn):
* src/callproc.c (emacs_spawn): Add PTY_IN and PTY_OUT arguments to
specify which streams should be set up as a PTY.
(call_process): Adjust call to 'emacs_spawn'.

* src/process.h (Lisp_Process): Replace 'pty_flag' with 'pty_in' and
'pty_out'.

* src/process.c (is_pty_from_symbol): New function.
(make-process): Allow :connection-type to be a cons cell, and allow
using a stderr process with a PTY for stdin/stdout.
(create_process): Handle creating a process where only one of stdin or
stdout is a PTY.

* lisp/eshell/esh-proc.el (eshell-needs-pipe, eshell-needs-pipe-p):
Remove.
(eshell-gather-process-output): Use 'make-process' and set
':connection-type' as needed by the value of 'eshell-in-pipeline-p'.

* lisp/net/tramp.el (tramp-handle-make-process):
* lisp/net/tramp-adb.el (tramp-adb-handle-make-process):
* lisp/net/tramp-sh.el (tramp-sh-handle-make-process): Don't signal an
error when ':connection-type' is a cons cell.

* test/src/process-tests.el
(process-test-sentinel-wait-function-working-p): Allow passing PROC
in, and rework into...
(process-test-wait-for-sentinel): ... this.
(process-test-sentinel-accept-process-output)
(process-test-sentinel-sit-for, process-test-quoted-batfile)
(process-test-stderr-filter): Use 'process-test-wait-for-sentinel'.
(make/process/test-connection-type): New function.
(make-process/connection-type/pty, make-process/connection-type/pty-2)
(make-process/connection-type/pipe)
(make-process/connection-type/pipe-2)
(make-process/connection-type/in-pty)
(make-process/connection-type/out-pty)
(make-process/connection-type/pty-with-stderr-buffer)
(make-process/connection-type/out-pty-with-stderr-buffer): New tests.

* test/lisp/eshell/esh-proc-tests.el (esh-proc-test--detect-pty-cmd):
New variable.
(esh-proc-test/pipeline-connection-type/no-pipeline)
(esh-proc-test/pipeline-connection-type/first)
(esh-proc-test/pipeline-connection-type/middle)
(esh-proc-test/pipeline-connection-type/last): New tests.

* doc/lispref/processes.texi (Asynchronous Processes): Document new
':connection-type' behavior.
(Output from Processes): Remove caveat about ':stderr' forcing
'make-process' to use pipes.

* etc/NEWS: Announce this change (bug#56025).
This commit is contained in:
Jim Porter 2022-07-17 20:25:00 -07:00
parent b70369c557
commit d7b89ea407
12 changed files with 288 additions and 160 deletions

View file

@ -705,12 +705,13 @@ coding system will apply. @xref{Default Coding Systems}.
Initialize the type of device used to communicate with the subprocess. Initialize the type of device used to communicate with the subprocess.
Possible values are @code{pty} to use a pty, @code{pipe} to use a Possible values are @code{pty} to use a pty, @code{pipe} to use a
pipe, or @code{nil} to use the default derived from the value of the pipe, or @code{nil} to use the default derived from the value of the
@code{process-connection-type} variable. This parameter and the value @code{process-connection-type} variable. If @var{type} is a cons cell
of @code{process-connection-type} are ignored if a non-@code{nil} @w{@code{(@var{input} . @var{output})}}, then @var{input} will be used
value is specified for the @code{:stderr} parameter; in that case, the for standard input and @var{output} for standard output (and standard
type will always be @code{pipe}. On systems where ptys are not error if @code{:stderr} is @code{nil}).
available (MS-Windows), this parameter is likewise ignored, and pipes
are used unconditionally. On systems where ptys are not available (MS-Windows), this parameter
is ignored, and pipes are used unconditionally.
@item :noquery @var{query-flag} @item :noquery @var{query-flag}
Initialize the process query flag to @var{query-flag}. Initialize the process query flag to @var{query-flag}.
@ -1530,20 +1531,11 @@ a buffer, which is called the associated buffer of the process
default filter discards the output. default filter discards the output.
If the subprocess writes to its standard error stream, by default If the subprocess writes to its standard error stream, by default
the error output is also passed to the process filter function. If the error output is also passed to the process filter function.
Emacs uses a pseudo-TTY (pty) for communication with the subprocess, Alternatively, you could use the @code{:stderr} parameter with a
then it is impossible to separate the standard output and standard
error streams of the subprocess, because a pseudo-TTY has only one
output channel. In that case, if you want to keep the output to those
streams separate, you should redirect one of them to a file---for
example, by using an appropriate shell command via
@code{start-process-shell-command} or a similar function.
Alternatively, you could use the @code{:stderr} parameter with a
non-@code{nil} value in a call to @code{make-process} non-@code{nil} value in a call to @code{make-process}
(@pxref{Asynchronous Processes, make-process}) to make the destination (@pxref{Asynchronous Processes, make-process}) to make the destination
of the error output separate from the standard output; in that case, of the error output separate from the standard output.
Emacs will use pipes for communicating with the subprocess.
When a subprocess terminates, Emacs reads any pending output, When a subprocess terminates, Emacs reads any pending output,
then stops reading output from that subprocess. Therefore, if the then stops reading output from that subprocess. Therefore, if the

View file

@ -2332,6 +2332,12 @@ they will still be escaped, so the '.foo' symbol is still printed as
and remapping parent of basic faces does not work reliably. and remapping parent of basic faces does not work reliably.
Instead of remapping 'mode-line', you have to remap 'mode-line-active'. Instead of remapping 'mode-line', you have to remap 'mode-line-active'.
+++
** 'make-process' has been extended to support ptys when ':stderr' is set.
Previously, setting ':stderr' to a non-nil value would force the
process's connection to use pipes. Now, Emacs will use a pty for
stdin and stdout if requested no matter the value of ':stderr'.
--- ---
** User option 'mail-source-ignore-errors' is now obsolete. ** User option 'mail-source-ignore-errors' is now obsolete.
The whole mechanism for prompting users to continue in case of The whole mechanism for prompting users to continue in case of
@ -3323,6 +3329,12 @@ translation.
This is useful when quoting shell arguments for a remote shell This is useful when quoting shell arguments for a remote shell
invocation. Such shells are POSIX conformant by default. invocation. Such shells are POSIX conformant by default.
+++
** 'make-process' can set connection type independently for input and output.
When calling 'make-process', communication via pty can be enabled
selectively for just input or output by passing a cons cell for
':connection-type', e.g. '(pipe . pty)'.
+++ +++
** 'signal-process' now consults the list 'signal-process-functions'. ** 'signal-process' now consults the list 'signal-process-functions'.
This is to determine which function has to be called in order to This is to determine which function has to be called in order to

View file

@ -250,30 +250,6 @@ The prompt will be set to PROMPT."
"A marker that tracks the beginning of output of the last subprocess. "A marker that tracks the beginning of output of the last subprocess.
Used only on systems which do not support async subprocesses.") Used only on systems which do not support async subprocesses.")
(defvar eshell-needs-pipe
'("bc"
;; xclip.el (in GNU ELPA) calls all of these with
;; `process-connection-type' set to nil.
"pbpaste" "putclip" "xclip" "xsel" "wl-copy")
"List of commands which need `process-connection-type' to be nil.
Currently only affects commands in pipelines, and not those at
the front. If an element contains a directory part it must match
the full name of a command, otherwise just the nondirectory part must match.")
(defun eshell-needs-pipe-p (command)
"Return non-nil if COMMAND needs `process-connection-type' to be nil.
See `eshell-needs-pipe'."
(and (bound-and-true-p eshell-in-pipeline-p)
(not (eq eshell-in-pipeline-p 'first))
;; FIXME should this return non-nil for anything that is
;; neither 'first nor 'last? See bug#1388 discussion.
(catch 'found
(dolist (exe eshell-needs-pipe)
(if (string-equal exe (if (string-search "/" exe)
command
(file-name-nondirectory command)))
(throw 'found t))))))
(defun eshell-gather-process-output (command args) (defun eshell-gather-process-output (command args)
"Gather the output from COMMAND + ARGS." "Gather the output from COMMAND + ARGS."
(require 'esh-var) (require 'esh-var)
@ -290,31 +266,36 @@ See `eshell-needs-pipe'."
(cond (cond
((fboundp 'make-process) ((fboundp 'make-process)
(setq proc (setq proc
(let ((process-connection-type (let ((command (file-local-name (expand-file-name command)))
(unless (eshell-needs-pipe-p command) (conn-type (pcase (bound-and-true-p eshell-in-pipeline-p)
process-connection-type)) ('first '(nil . pipe))
(command (file-local-name (expand-file-name command)))) ('last '(pipe . nil))
(apply #'start-file-process ('t 'pipe)
(file-name-nondirectory command) nil command args))) ('nil nil))))
(eshell-record-process-object proc) (make-process
(set-process-buffer proc (current-buffer)) :name (file-name-nondirectory command)
(set-process-filter proc (if (eshell-interactive-output-p) :buffer (current-buffer)
:command (cons command args)
:filter (if (eshell-interactive-output-p)
#'eshell-output-filter #'eshell-output-filter
#'eshell-insertion-filter)) #'eshell-insertion-filter)
(set-process-sentinel proc #'eshell-sentinel) :sentinel #'eshell-sentinel
:connection-type conn-type
:file-handler t)))
(eshell-record-process-object proc)
(run-hook-with-args 'eshell-exec-hook proc) (run-hook-with-args 'eshell-exec-hook proc)
(when (fboundp 'process-coding-system) (when (fboundp 'process-coding-system)
(let ((coding-systems (process-coding-system proc))) (let ((coding-systems (process-coding-system proc)))
(setq decoding (car coding-systems) (setq decoding (car coding-systems)
encoding (cdr coding-systems))) encoding (cdr coding-systems)))
;; If start-process decided to use some coding system for ;; If `make-process' decided to use some coding system for
;; decoding data sent from the process and the coding system ;; decoding data sent from the process and the coding system
;; doesn't specify EOL conversion, we had better convert CRLF ;; doesn't specify EOL conversion, we had better convert CRLF
;; to LF. ;; to LF.
(if (vectorp (coding-system-eol-type decoding)) (if (vectorp (coding-system-eol-type decoding))
(setq decoding (coding-system-change-eol-conversion decoding 'dos) (setq decoding (coding-system-change-eol-conversion decoding 'dos)
changed t)) changed t))
;; Even if start-process left the coding system for encoding ;; Even if `make-process' left the coding system for encoding
;; data sent from the process undecided, we had better use the ;; data sent from the process undecided, we had better use the
;; same one as what we use for decoding. But, we should ;; same one as what we use for decoding. But, we should
;; suppress EOL conversion. ;; suppress EOL conversion.

View file

@ -877,7 +877,10 @@ implementation will be used."
(signal 'wrong-type-argument (list #'symbolp coding))) (signal 'wrong-type-argument (list #'symbolp coding)))
(when (eq connection-type t) (when (eq connection-type t)
(setq connection-type 'pty)) (setq connection-type 'pty))
(unless (memq connection-type '(nil pipe pty)) (unless (or (and (consp connection-type)
(memq (car connection-type) '(nil pipe pty))
(memq (cdr connection-type) '(nil pipe pty)))
(memq connection-type '(nil pipe pty)))
(signal 'wrong-type-argument (list #'symbolp connection-type))) (signal 'wrong-type-argument (list #'symbolp connection-type)))
(unless (or (null filter) (eq filter t) (functionp filter)) (unless (or (null filter) (eq filter t) (functionp filter))
(signal 'wrong-type-argument (list #'functionp filter))) (signal 'wrong-type-argument (list #'functionp filter)))

View file

@ -2842,7 +2842,10 @@ implementation will be used."
(signal 'wrong-type-argument (list #'symbolp coding))) (signal 'wrong-type-argument (list #'symbolp coding)))
(when (eq connection-type t) (when (eq connection-type t)
(setq connection-type 'pty)) (setq connection-type 'pty))
(unless (memq connection-type '(nil pipe pty)) (unless (or (and (consp connection-type)
(memq (car connection-type) '(nil pipe pty))
(memq (cdr connection-type) '(nil pipe pty)))
(memq connection-type '(nil pipe pty)))
(signal 'wrong-type-argument (list #'symbolp connection-type))) (signal 'wrong-type-argument (list #'symbolp connection-type)))
(unless (or (null filter) (eq filter t) (functionp filter)) (unless (or (null filter) (eq filter t) (functionp filter))
(signal 'wrong-type-argument (list #'functionp filter))) (signal 'wrong-type-argument (list #'functionp filter)))

View file

@ -4708,7 +4708,10 @@ substitution. SPEC-LIST is a list of char/value pairs used for
(signal 'wrong-type-argument (list #'symbolp coding))) (signal 'wrong-type-argument (list #'symbolp coding)))
(when (eq connection-type t) (when (eq connection-type t)
(setq connection-type 'pty)) (setq connection-type 'pty))
(unless (memq connection-type '(nil pipe pty)) (unless (or (and (consp connection-type)
(memq (car connection-type) '(nil pipe pty))
(memq (cdr connection-type) '(nil pipe pty)))
(memq connection-type '(nil pipe pty)))
(signal 'wrong-type-argument (list #'symbolp connection-type))) (signal 'wrong-type-argument (list #'symbolp connection-type)))
(unless (or (null filter) (eq filter t) (functionp filter)) (unless (or (null filter) (eq filter t) (functionp filter))
(signal 'wrong-type-argument (list #'functionp filter))) (signal 'wrong-type-argument (list #'functionp filter)))

View file

@ -650,7 +650,7 @@ call_process (ptrdiff_t nargs, Lisp_Object *args, int filefd,
child_errno child_errno
= emacs_spawn (&pid, filefd, fd_output, fd_error, new_argv, env, = emacs_spawn (&pid, filefd, fd_output, fd_error, new_argv, env,
SSDATA (current_dir), NULL, &oldset); SSDATA (current_dir), NULL, false, false, &oldset);
eassert ((child_errno == 0) == (0 < pid)); eassert ((child_errno == 0) == (0 < pid));
if (pid > 0) if (pid > 0)
@ -1412,14 +1412,15 @@ emacs_posix_spawn_init_attributes (posix_spawnattr_t *attributes,
int int
emacs_spawn (pid_t *newpid, int std_in, int std_out, int std_err, emacs_spawn (pid_t *newpid, int std_in, int std_out, int std_err,
char **argv, char **envp, const char *cwd, char **argv, char **envp, const char *cwd,
const char *pty, const sigset_t *oldset) const char *pty_name, bool pty_in, bool pty_out,
const sigset_t *oldset)
{ {
#if USABLE_POSIX_SPAWN #if USABLE_POSIX_SPAWN
/* Prefer the simpler `posix_spawn' if available. `posix_spawn' /* Prefer the simpler `posix_spawn' if available. `posix_spawn'
doesn't yet support setting up pseudoterminals, so we fall back doesn't yet support setting up pseudoterminals, so we fall back
to `vfork' if we're supposed to use a pseudoterminal. */ to `vfork' if we're supposed to use a pseudoterminal. */
bool use_posix_spawn = pty == NULL; bool use_posix_spawn = pty_name == NULL;
posix_spawn_file_actions_t actions; posix_spawn_file_actions_t actions;
posix_spawnattr_t attributes; posix_spawnattr_t attributes;
@ -1473,7 +1474,9 @@ emacs_spawn (pid_t *newpid, int std_in, int std_out, int std_err,
/* vfork, and prevent local vars from being clobbered by the vfork. */ /* vfork, and prevent local vars from being clobbered by the vfork. */
pid_t *volatile newpid_volatile = newpid; pid_t *volatile newpid_volatile = newpid;
const char *volatile cwd_volatile = cwd; const char *volatile cwd_volatile = cwd;
const char *volatile pty_volatile = pty; const char *volatile ptyname_volatile = pty_name;
bool volatile ptyin_volatile = pty_in;
bool volatile ptyout_volatile = pty_out;
char **volatile argv_volatile = argv; char **volatile argv_volatile = argv;
int volatile stdin_volatile = std_in; int volatile stdin_volatile = std_in;
int volatile stdout_volatile = std_out; int volatile stdout_volatile = std_out;
@ -1495,7 +1498,9 @@ emacs_spawn (pid_t *newpid, int std_in, int std_out, int std_err,
newpid = newpid_volatile; newpid = newpid_volatile;
cwd = cwd_volatile; cwd = cwd_volatile;
pty = pty_volatile; pty_name = ptyname_volatile;
pty_in = ptyin_volatile;
pty_out = ptyout_volatile;
argv = argv_volatile; argv = argv_volatile;
std_in = stdin_volatile; std_in = stdin_volatile;
std_out = stdout_volatile; std_out = stdout_volatile;
@ -1506,13 +1511,12 @@ emacs_spawn (pid_t *newpid, int std_in, int std_out, int std_err,
if (pid == 0) if (pid == 0)
#endif /* not WINDOWSNT */ #endif /* not WINDOWSNT */
{ {
bool pty_flag = pty != NULL;
/* Make the pty be the controlling terminal of the process. */ /* Make the pty be the controlling terminal of the process. */
#ifdef HAVE_PTYS #ifdef HAVE_PTYS
dissociate_controlling_tty (); dissociate_controlling_tty ();
/* Make the pty's terminal the controlling terminal. */ /* Make the pty's terminal the controlling terminal. */
if (pty_flag && std_in >= 0) if (pty_in && std_in >= 0)
{ {
#ifdef TIOCSCTTY #ifdef TIOCSCTTY
/* We ignore the return value /* We ignore the return value
@ -1521,7 +1525,7 @@ emacs_spawn (pid_t *newpid, int std_in, int std_out, int std_err,
#endif #endif
} }
#if defined (LDISC1) #if defined (LDISC1)
if (pty_flag && std_in >= 0) if (pty_in && std_in >= 0)
{ {
struct termios t; struct termios t;
tcgetattr (std_in, &t); tcgetattr (std_in, &t);
@ -1531,7 +1535,7 @@ emacs_spawn (pid_t *newpid, int std_in, int std_out, int std_err,
} }
#else #else
#if defined (NTTYDISC) && defined (TIOCSETD) #if defined (NTTYDISC) && defined (TIOCSETD)
if (pty_flag && std_in >= 0) if (pty_in && std_in >= 0)
{ {
/* Use new line discipline. */ /* Use new line discipline. */
int ldisc = NTTYDISC; int ldisc = NTTYDISC;
@ -1548,18 +1552,21 @@ emacs_spawn (pid_t *newpid, int std_in, int std_out, int std_err,
both TIOCSCTTY is defined. */ both TIOCSCTTY is defined. */
/* Now close the pty (if we had it open) and reopen it. /* Now close the pty (if we had it open) and reopen it.
This makes the pty the controlling terminal of the subprocess. */ This makes the pty the controlling terminal of the subprocess. */
if (pty_flag) if (pty_name)
{ {
/* I wonder if emacs_close (emacs_open (pty, ...)) /* I wonder if emacs_close (emacs_open (pty, ...))
would work? */ would work? */
if (std_in >= 0) if (pty_in && std_in >= 0)
emacs_close (std_in); emacs_close (std_in);
std_out = std_in = emacs_open_noquit (pty, O_RDWR, 0); int ptyfd = emacs_open_noquit (pty_name, O_RDWR, 0);
if (pty_in)
std_in = ptyfd;
if (pty_out)
std_out = ptyfd;
if (std_in < 0) if (std_in < 0)
{ {
emacs_perror (pty); emacs_perror (pty_name);
_exit (EXIT_CANCELED); _exit (EXIT_CANCELED);
} }
@ -1599,7 +1606,7 @@ emacs_spawn (pid_t *newpid, int std_in, int std_out, int std_err,
/* Stop blocking SIGCHLD in the child. */ /* Stop blocking SIGCHLD in the child. */
unblock_child_signal (oldset); unblock_child_signal (oldset);
if (pty_flag) if (pty_out)
child_setup_tty (std_out); child_setup_tty (std_out);
#endif #endif

View file

@ -4943,7 +4943,8 @@ extern void setup_process_coding_systems (Lisp_Object);
#endif #endif
extern int emacs_spawn (pid_t *, int, int, int, char **, char **, extern int emacs_spawn (pid_t *, int, int, int, char **, char **,
const char *, const char *, const sigset_t *); const char *, const char *, bool, bool,
const sigset_t *);
extern char **make_environment_block (Lisp_Object) ATTRIBUTE_RETURNS_NONNULL; extern char **make_environment_block (Lisp_Object) ATTRIBUTE_RETURNS_NONNULL;
extern void init_callproc_1 (void); extern void init_callproc_1 (void);
extern void init_callproc (void); extern void init_callproc (void);

View file

@ -1316,6 +1316,19 @@ set_process_filter_masks (struct Lisp_Process *p)
add_process_read_fd (p->infd); add_process_read_fd (p->infd);
} }
static bool
is_pty_from_symbol (Lisp_Object symbol)
{
if (EQ (symbol, Qpty))
return true;
else if (EQ (symbol, Qpipe))
return false;
else if (NILP (symbol))
return !NILP (Vprocess_connection_type);
else
report_file_error ("Unknown connection type", symbol);
}
DEFUN ("set-process-filter", Fset_process_filter, Sset_process_filter, DEFUN ("set-process-filter", Fset_process_filter, Sset_process_filter,
2, 2, 0, 2, 2, 0,
doc: /* Give PROCESS the filter function FILTER; nil means default. doc: /* Give PROCESS the filter function FILTER; nil means default.
@ -1741,15 +1754,18 @@ signals to stop and continue a process.
:connection-type TYPE -- TYPE is control type of device used to :connection-type TYPE -- TYPE is control type of device used to
communicate with subprocesses. Values are `pipe' to use a pipe, `pty' communicate with subprocesses. Values are `pipe' to use a pipe, `pty'
to use a pty, or nil to use the default specified through to use a pty, or nil to use the default specified through
`process-connection-type'. `process-connection-type'. If TYPE is a cons (INPUT . OUTPUT), then
INPUT will be used for standard input and OUTPUT for standard output
(and standard error if `:stderr' is nil).
:filter FILTER -- Install FILTER as the process filter. :filter FILTER -- Install FILTER as the process filter.
:sentinel SENTINEL -- Install SENTINEL as the process sentinel. :sentinel SENTINEL -- Install SENTINEL as the process sentinel.
:stderr STDERR -- STDERR is either a buffer or a pipe process attached :stderr STDERR -- STDERR is either a buffer or a pipe process attached
to the standard error of subprocess. Specifying this implies to the standard error of subprocess. When specifying this, the
`:connection-type' is set to `pipe'. If STDERR is nil, standard error subprocess's standard error will always communicate via a pipe, no
matter the value of `:connection-type'. If STDERR is nil, standard error
is mixed with standard output and sent to BUFFER or FILTER. (Note is mixed with standard output and sent to BUFFER or FILTER. (Note
that specifying :stderr will create a new, separate (but associated) that specifying :stderr will create a new, separate (but associated)
process, with its own filter and sentinel. See process, with its own filter and sentinel. See
@ -1845,22 +1861,20 @@ usage: (make-process &rest ARGS) */)
CHECK_TYPE (NILP (tem), Qnull, tem); CHECK_TYPE (NILP (tem), Qnull, tem);
tem = plist_get (contact, QCconnection_type); tem = plist_get (contact, QCconnection_type);
if (EQ (tem, Qpty)) if (CONSP (tem))
XPROCESS (proc)->pty_flag = true; {
else if (EQ (tem, Qpipe)) XPROCESS (proc)->pty_in = is_pty_from_symbol (XCAR (tem));
XPROCESS (proc)->pty_flag = false; XPROCESS (proc)->pty_out = is_pty_from_symbol (XCDR (tem));
else if (NILP (tem)) }
XPROCESS (proc)->pty_flag = !NILP (Vprocess_connection_type);
else else
report_file_error ("Unknown connection type", tem); {
XPROCESS (proc)->pty_in = XPROCESS (proc)->pty_out =
is_pty_from_symbol (tem);
}
if (!NILP (stderrproc)) if (!NILP (stderrproc))
{
pset_stderrproc (XPROCESS (proc), stderrproc); pset_stderrproc (XPROCESS (proc), stderrproc);
XPROCESS (proc)->pty_flag = false;
}
#ifdef HAVE_GNUTLS #ifdef HAVE_GNUTLS
/* AKA GNUTLS_INITSTAGE(proc). */ /* AKA GNUTLS_INITSTAGE(proc). */
verify (GNUTLS_STAGE_EMPTY == 0); verify (GNUTLS_STAGE_EMPTY == 0);
@ -2099,55 +2113,70 @@ static void
create_process (Lisp_Object process, char **new_argv, Lisp_Object current_dir) create_process (Lisp_Object process, char **new_argv, Lisp_Object current_dir)
{ {
struct Lisp_Process *p = XPROCESS (process); struct Lisp_Process *p = XPROCESS (process);
int inchannel, outchannel; int inchannel = -1, outchannel = -1;
pid_t pid = -1; pid_t pid = -1;
int vfork_errno; int vfork_errno;
int forkin, forkout, forkerr = -1; int forkin, forkout, forkerr = -1;
bool pty_flag = 0; bool pty_in = false, pty_out = false;
char pty_name[PTY_NAME_SIZE]; char pty_name[PTY_NAME_SIZE];
Lisp_Object lisp_pty_name = Qnil; Lisp_Object lisp_pty_name = Qnil;
int ptychannel = -1, pty_tty = -1;
sigset_t oldset; sigset_t oldset;
/* Ensure that the SIGCHLD handler can notify /* Ensure that the SIGCHLD handler can notify
`wait_reading_process_output'. */ `wait_reading_process_output'. */
child_signal_init (); child_signal_init ();
inchannel = outchannel = -1; if (p->pty_in || p->pty_out)
ptychannel = allocate_pty (pty_name);
if (p->pty_flag) if (ptychannel >= 0)
outchannel = inchannel = allocate_pty (pty_name);
if (inchannel >= 0)
{ {
p->open_fd[READ_FROM_SUBPROCESS] = inchannel;
#if ! defined (USG) || defined (USG_SUBTTY_WORKS) #if ! defined (USG) || defined (USG_SUBTTY_WORKS)
/* On most USG systems it does not work to open the pty's tty here, /* On most USG systems it does not work to open the pty's tty here,
then close it and reopen it in the child. */ then close it and reopen it in the child. */
/* Don't let this terminal become our controlling terminal /* Don't let this terminal become our controlling terminal
(in case we don't have one). */ (in case we don't have one). */
forkout = forkin = emacs_open (pty_name, O_RDWR | O_NOCTTY, 0); pty_tty = emacs_open (pty_name, O_RDWR | O_NOCTTY, 0);
if (forkin < 0) if (pty_tty < 0)
report_file_error ("Opening pty", Qnil); report_file_error ("Opening pty", Qnil);
p->open_fd[SUBPROCESS_STDIN] = forkin;
#else
forkin = forkout = -1;
#endif /* not USG, or USG_SUBTTY_WORKS */ #endif /* not USG, or USG_SUBTTY_WORKS */
pty_flag = 1; pty_in = p->pty_in;
pty_out = p->pty_out;
lisp_pty_name = build_string (pty_name); lisp_pty_name = build_string (pty_name);
} }
/* Set up stdin for the child process. */
if (ptychannel >= 0 && p->pty_in)
{
p->open_fd[SUBPROCESS_STDIN] = forkin = pty_tty;
outchannel = ptychannel;
}
else else
{ {
if (emacs_pipe (p->open_fd + SUBPROCESS_STDIN) != 0 if (emacs_pipe (p->open_fd + SUBPROCESS_STDIN) != 0)
|| emacs_pipe (p->open_fd + READ_FROM_SUBPROCESS) != 0)
report_file_error ("Creating pipe", Qnil); report_file_error ("Creating pipe", Qnil);
forkin = p->open_fd[SUBPROCESS_STDIN]; forkin = p->open_fd[SUBPROCESS_STDIN];
outchannel = p->open_fd[WRITE_TO_SUBPROCESS]; outchannel = p->open_fd[WRITE_TO_SUBPROCESS];
}
/* Set up stdout for the child process. */
if (ptychannel >= 0 && p->pty_out)
{
forkout = pty_tty;
p->open_fd[READ_FROM_SUBPROCESS] = inchannel = ptychannel;
}
else
{
if (emacs_pipe (p->open_fd + READ_FROM_SUBPROCESS) != 0)
report_file_error ("Creating pipe", Qnil);
inchannel = p->open_fd[READ_FROM_SUBPROCESS]; inchannel = p->open_fd[READ_FROM_SUBPROCESS];
forkout = p->open_fd[SUBPROCESS_STDOUT]; forkout = p->open_fd[SUBPROCESS_STDOUT];
#if defined(GNU_LINUX) && defined(F_SETPIPE_SZ) #if defined(GNU_LINUX) && defined(F_SETPIPE_SZ)
fcntl (inchannel, F_SETPIPE_SZ, read_process_output_max); fcntl (inchannel, F_SETPIPE_SZ, read_process_output_max);
#endif #endif
}
if (!NILP (p->stderrproc)) if (!NILP (p->stderrproc))
{ {
@ -2159,7 +2188,6 @@ create_process (Lisp_Object process, char **new_argv, Lisp_Object current_dir)
close_process_fd (&pp->open_fd[WRITE_TO_SUBPROCESS]); close_process_fd (&pp->open_fd[WRITE_TO_SUBPROCESS]);
close_process_fd (&pp->open_fd[SUBPROCESS_STDIN]); close_process_fd (&pp->open_fd[SUBPROCESS_STDIN]);
} }
}
if (FD_SETSIZE <= inchannel || FD_SETSIZE <= outchannel) if (FD_SETSIZE <= inchannel || FD_SETSIZE <= outchannel)
report_file_errno ("Creating pipe", Qnil, EMFILE); report_file_errno ("Creating pipe", Qnil, EMFILE);
@ -2183,7 +2211,8 @@ create_process (Lisp_Object process, char **new_argv, Lisp_Object current_dir)
we just reopen the device (see emacs_get_tty_pgrp) as this is we just reopen the device (see emacs_get_tty_pgrp) as this is
more portable (see USG_SUBTTY_WORKS above). */ more portable (see USG_SUBTTY_WORKS above). */
p->pty_flag = pty_flag; p->pty_in = pty_in;
p->pty_out = pty_out;
pset_status (p, Qrun); pset_status (p, Qrun);
if (!EQ (p->command, Qt) if (!EQ (p->command, Qt)
@ -2199,13 +2228,15 @@ create_process (Lisp_Object process, char **new_argv, Lisp_Object current_dir)
block_input (); block_input ();
block_child_signal (&oldset); block_child_signal (&oldset);
pty_flag = p->pty_flag; pty_in = p->pty_in;
eassert (pty_flag == ! NILP (lisp_pty_name)); pty_out = p->pty_out;
eassert ((pty_in || pty_out) == ! NILP (lisp_pty_name));
vfork_errno vfork_errno
= emacs_spawn (&pid, forkin, forkout, forkerr, new_argv, env, = emacs_spawn (&pid, forkin, forkout, forkerr, new_argv, env,
SSDATA (current_dir), SSDATA (current_dir),
pty_flag ? SSDATA (lisp_pty_name) : NULL, &oldset); pty_in || pty_out ? SSDATA (lisp_pty_name) : NULL,
pty_in, pty_out, &oldset);
eassert ((vfork_errno == 0) == (0 < pid)); eassert ((vfork_errno == 0) == (0 < pid));
@ -2263,7 +2294,7 @@ create_pty (Lisp_Object process)
{ {
struct Lisp_Process *p = XPROCESS (process); struct Lisp_Process *p = XPROCESS (process);
char pty_name[PTY_NAME_SIZE]; char pty_name[PTY_NAME_SIZE];
int pty_fd = !p->pty_flag ? -1 : allocate_pty (pty_name); int pty_fd = !(p->pty_in || p->pty_out) ? -1 : allocate_pty (pty_name);
if (pty_fd >= 0) if (pty_fd >= 0)
{ {
@ -2301,7 +2332,7 @@ create_pty (Lisp_Object process)
we just reopen the device (see emacs_get_tty_pgrp) as this is we just reopen the device (see emacs_get_tty_pgrp) as this is
more portable (see USG_SUBTTY_WORKS above). */ more portable (see USG_SUBTTY_WORKS above). */
p->pty_flag = 1; p->pty_in = p->pty_out = true;
pset_status (p, Qrun); pset_status (p, Qrun);
setup_process_coding_systems (process); setup_process_coding_systems (process);
@ -2412,7 +2443,7 @@ usage: (make-pipe-process &rest ARGS) */)
p->kill_without_query = 1; p->kill_without_query = 1;
if (tem = plist_get (contact, QCstop), !NILP (tem)) if (tem = plist_get (contact, QCstop), !NILP (tem))
pset_command (p, Qt); pset_command (p, Qt);
eassert (! p->pty_flag); eassert (! p->pty_in && ! p->pty_out);
if (!EQ (p->command, Qt) if (!EQ (p->command, Qt)
&& !EQ (p->filter, Qt)) && !EQ (p->filter, Qt))
@ -3147,7 +3178,7 @@ usage: (make-serial-process &rest ARGS) */)
p->kill_without_query = 1; p->kill_without_query = 1;
if (tem = plist_get (contact, QCstop), !NILP (tem)) if (tem = plist_get (contact, QCstop), !NILP (tem))
pset_command (p, Qt); pset_command (p, Qt);
eassert (! p->pty_flag); eassert (! p->pty_in && ! p->pty_out);
if (!EQ (p->command, Qt) if (!EQ (p->command, Qt)
&& !EQ (p->filter, Qt)) && !EQ (p->filter, Qt))
@ -6808,7 +6839,7 @@ process_send_signal (Lisp_Object process, int signo, Lisp_Object current_group,
error ("Process %s is not active", error ("Process %s is not active",
SDATA (p->name)); SDATA (p->name));
if (!p->pty_flag) if (! p->pty_in)
current_group = Qnil; current_group = Qnil;
/* If we are using pgrps, get a pgrp number and make it negative. */ /* If we are using pgrps, get a pgrp number and make it negative. */
@ -7177,7 +7208,7 @@ process has been transmitted to the serial port. */)
send_process (proc, "", 0, Qnil); send_process (proc, "", 0, Qnil);
} }
if (XPROCESS (proc)->pty_flag) if (XPROCESS (proc)->pty_in)
send_process (proc, "\004", 1, Qnil); send_process (proc, "\004", 1, Qnil);
else if (EQ (XPROCESS (proc)->type, Qserial)) else if (EQ (XPROCESS (proc)->type, Qserial))
{ {

View file

@ -156,8 +156,9 @@ struct Lisp_Process
/* True means kill silently if Emacs is exited. /* True means kill silently if Emacs is exited.
This is the inverse of the `query-on-exit' flag. */ This is the inverse of the `query-on-exit' flag. */
bool_bf kill_without_query : 1; bool_bf kill_without_query : 1;
/* True if communicating through a pty. */ /* True if communicating through a pty for input or output. */
bool_bf pty_flag : 1; bool_bf pty_in : 1;
bool_bf pty_out : 1;
/* Flag to set coding-system of the process buffer from the /* Flag to set coding-system of the process buffer from the
coding_system used to decode process output. */ coding_system used to decode process output. */
bool_bf inherit_coding_system_flag : 1; bool_bf inherit_coding_system_flag : 1;

View file

@ -28,6 +28,15 @@
(file-name-directory (or load-file-name (file-name-directory (or load-file-name
default-directory)))) default-directory))))
(defvar esh-proc-test--detect-pty-cmd
(concat "sh -c '"
"if [ -t 0 ]; then echo stdin; fi; "
"if [ -t 1 ]; then echo stdout; fi; "
"if [ -t 2 ]; then echo stderr; fi"
"'"))
;;; Tests:
(ert-deftest esh-proc-test/sigpipe-exits-process () (ert-deftest esh-proc-test/sigpipe-exits-process ()
"Test that a SIGPIPE is properly sent to a process if a pipe closes" "Test that a SIGPIPE is properly sent to a process if a pipe closes"
(skip-unless (and (executable-find "sh") (skip-unless (and (executable-find "sh")
@ -44,6 +53,40 @@
(eshell-wait-for-subprocess t) (eshell-wait-for-subprocess t)
(should (eq (process-list) nil)))) (should (eq (process-list) nil))))
(ert-deftest esh-proc-test/pipeline-connection-type/no-pipeline ()
"Test that all streams are PTYs when a command is not in a pipeline."
(skip-unless (executable-find "sh"))
(should (equal (eshell-test-command-result esh-proc-test--detect-pty-cmd)
;; PTYs aren't supported on MS-Windows.
(unless (eq system-type 'windows-nt)
"stdin\nstdout\nstderr\n"))))
(ert-deftest esh-proc-test/pipeline-connection-type/first ()
"Test that only stdin is a PTY when a command starts a pipeline."
(skip-unless (and (executable-find "sh")
(executable-find "cat")))
(should (equal (eshell-test-command-result
(concat esh-proc-test--detect-pty-cmd " | cat"))
(unless (eq system-type 'windows-nt)
"stdin\n"))))
(ert-deftest esh-proc-test/pipeline-connection-type/middle ()
"Test that all streams are pipes when a command is in the middle of a
pipeline."
(skip-unless (and (executable-find "sh")
(executable-find "cat")))
(should (equal (eshell-test-command-result
(concat "echo | " esh-proc-test--detect-pty-cmd " | cat"))
nil)))
(ert-deftest esh-proc-test/pipeline-connection-type/last ()
"Test that only output streams are PTYs when a command ends a pipeline."
(skip-unless (executable-find "sh"))
(should (equal (eshell-test-command-result
(concat "echo | " esh-proc-test--detect-pty-cmd))
(unless (eq system-type 'windows-nt)
"stdout\nstderr\n"))))
(ert-deftest esh-proc-test/kill-pipeline () (ert-deftest esh-proc-test/kill-pipeline ()
"Test that killing a pipeline of processes only emits a single "Test that killing a pipeline of processes only emits a single
prompt. See bug#54136." prompt. See bug#54136."

View file

@ -38,10 +38,11 @@
;; Timeout in seconds; the test fails if the timeout is reached. ;; Timeout in seconds; the test fails if the timeout is reached.
(defvar process-test-sentinel-wait-timeout 2.0) (defvar process-test-sentinel-wait-timeout 2.0)
;; Start a process that exits immediately. Call WAIT-FUNCTION, (defun process-test-wait-for-sentinel (proc exit-status &optional wait-function)
;; possibly multiple times, to wait for the process to complete. "Set a sentinel on PROC and wait for it to be called with EXIT-STATUS.
(defun process-test-sentinel-wait-function-working-p (wait-function) Call WAIT-FUNCTION, possibly multiple times, to wait for the
(let ((proc (start-process "test" nil "bash" "-c" "exit 20")) process to complete."
(let ((wait-function (or wait-function #'accept-process-output))
(sentinel-called nil) (sentinel-called nil)
(start-time (float-time))) (start-time (float-time)))
(set-process-sentinel proc (lambda (_proc _msg) (set-process-sentinel proc (lambda (_proc _msg)
@ -50,21 +51,22 @@
(> (- (float-time) start-time) (> (- (float-time) start-time)
process-test-sentinel-wait-timeout))) process-test-sentinel-wait-timeout)))
(funcall wait-function)) (funcall wait-function))
(cl-assert (eq (process-status proc) 'exit)) (should sentinel-called)
(cl-assert (= (process-exit-status proc) 20)) (should (eq (process-status proc) 'exit))
sentinel-called)) (should (= (process-exit-status proc) exit-status))))
(ert-deftest process-test-sentinel-accept-process-output () (ert-deftest process-test-sentinel-accept-process-output ()
(skip-unless (executable-find "bash")) (skip-unless (executable-find "bash"))
(with-timeout (60 (ert-fail "Test timed out")) (with-timeout (60 (ert-fail "Test timed out"))
(should (process-test-sentinel-wait-function-working-p (let ((proc (start-process "test" nil "bash" "-c" "exit 20")))
#'accept-process-output)))) (should (process-test-wait-for-sentinel proc 20)))))
(ert-deftest process-test-sentinel-sit-for () (ert-deftest process-test-sentinel-sit-for ()
(skip-unless (executable-find "bash")) (skip-unless (executable-find "bash"))
(with-timeout (60 (ert-fail "Test timed out")) (with-timeout (60 (ert-fail "Test timed out"))
(should (let ((proc (start-process "test" nil "bash" "-c" "exit 20")))
(process-test-sentinel-wait-function-working-p (lambda () (sit-for 0.01 t)))))) (should (process-test-wait-for-sentinel
proc 20 (lambda () (sit-for 0.01 t)))))))
(when (eq system-type 'windows-nt) (when (eq system-type 'windows-nt)
(ert-deftest process-test-quoted-batfile () (ert-deftest process-test-quoted-batfile ()
@ -97,17 +99,8 @@
"echo hello stderr! >&2; " "echo hello stderr! >&2; "
"exit 20")) "exit 20"))
:buffer stdout-buffer :buffer stdout-buffer
:stderr stderr-buffer)) :stderr stderr-buffer)))
(sentinel-called nil) (process-test-wait-for-sentinel proc 20)
(start-time (float-time)))
(set-process-sentinel proc (lambda (_proc _msg)
(setq sentinel-called t)))
(while (not (or sentinel-called
(> (- (float-time) start-time)
process-test-sentinel-wait-timeout)))
(accept-process-output))
(cl-assert (eq (process-status proc) 'exit))
(cl-assert (= (process-exit-status proc) 20))
(should (with-current-buffer stdout-buffer (should (with-current-buffer stdout-buffer
(goto-char (point-min)) (goto-char (point-min))
(looking-at "hello stdout!"))) (looking-at "hello stdout!")))
@ -118,8 +111,7 @@
(ert-deftest process-test-stderr-filter () (ert-deftest process-test-stderr-filter ()
(skip-unless (executable-find "bash")) (skip-unless (executable-find "bash"))
(with-timeout (60 (ert-fail "Test timed out")) (with-timeout (60 (ert-fail "Test timed out"))
(let* ((sentinel-called nil) (let* ((stderr-sentinel-called nil)
(stderr-sentinel-called nil)
(stdout-output nil) (stdout-output nil)
(stderr-output nil) (stderr-output nil)
(stdout-buffer (generate-new-buffer "*stdout*")) (stdout-buffer (generate-new-buffer "*stdout*"))
@ -131,23 +123,14 @@
(concat "echo hello stdout!; " (concat "echo hello stdout!; "
"echo hello stderr! >&2; " "echo hello stderr! >&2; "
"exit 20")) "exit 20"))
:stderr stderr-proc)) :stderr stderr-proc)))
(start-time (float-time)))
(set-process-filter proc (lambda (_proc input) (set-process-filter proc (lambda (_proc input)
(push input stdout-output))) (push input stdout-output)))
(set-process-sentinel proc (lambda (_proc _msg)
(setq sentinel-called t)))
(set-process-filter stderr-proc (lambda (_proc input) (set-process-filter stderr-proc (lambda (_proc input)
(push input stderr-output))) (push input stderr-output)))
(set-process-sentinel stderr-proc (lambda (_proc _input) (set-process-sentinel stderr-proc (lambda (_proc _input)
(setq stderr-sentinel-called t))) (setq stderr-sentinel-called t)))
(while (not (or sentinel-called (process-test-wait-for-sentinel proc 20)
(> (- (float-time) start-time)
process-test-sentinel-wait-timeout)))
(accept-process-output))
(cl-assert (eq (process-status proc) 'exit))
(cl-assert (= (process-exit-status proc) 20))
(should sentinel-called)
(should (equal 1 (with-current-buffer stdout-buffer (should (equal 1 (with-current-buffer stdout-buffer
(point-max)))) (point-max))))
(should (equal "hello stdout!\n" (should (equal "hello stdout!\n"
@ -289,6 +272,74 @@
(error :got-error)))) (error :got-error))))
(should have-called-debugger)))) (should have-called-debugger))))
(defun make-process/test-connection-type (ttys &rest args)
"Make a process and check whether its standard streams match TTYS.
This calls `make-process', passing ARGS to adjust how the process
is created. TTYS should be a list of 3 boolean values,
indicating whether the subprocess's stdin, stdout, and stderr
should be a TTY, respectively."
(declare (indent 1))
(let* (;; MS-Windows doesn't support communicating via pty.
(ttys (if (eq system-type 'windows-nt) '(nil nil nil) ttys))
(expected-output (concat (and (nth 0 ttys) "stdin\n")
(and (nth 1 ttys) "stdout\n")
(and (nth 2 ttys) "stderr\n")))
(stdout-buffer (generate-new-buffer "*stdout*"))
(proc (apply
#'make-process
:name "test"
:command (list "sh" "-c"
(concat "if [ -t 0 ]; then echo stdin; fi; "
"if [ -t 1 ]; then echo stdout; fi; "
"if [ -t 2 ]; then echo stderr; fi"))
:buffer stdout-buffer
args)))
(process-test-wait-for-sentinel proc 0)
(should (equal (with-current-buffer stdout-buffer (buffer-string))
expected-output))))
(ert-deftest make-process/connection-type/pty ()
(skip-unless (executable-find "sh"))
(make-process/test-connection-type '(t t t)
:connection-type 'pty))
(ert-deftest make-process/connection-type/pty-2 ()
(skip-unless (executable-find "sh"))
(make-process/test-connection-type '(t t t)
:connection-type '(pty . pty)))
(ert-deftest make-process/connection-type/pipe ()
(skip-unless (executable-find "sh"))
(make-process/test-connection-type '(nil nil nil)
:connection-type 'pipe))
(ert-deftest make-process/connection-type/pipe-2 ()
(skip-unless (executable-find "sh"))
(make-process/test-connection-type '(nil nil nil)
:connection-type '(pipe . pipe)))
(ert-deftest make-process/connection-type/in-pty ()
(skip-unless (executable-find "sh"))
(make-process/test-connection-type '(t nil nil)
:connection-type '(pty . pipe)))
(ert-deftest make-process/connection-type/out-pty ()
(skip-unless (executable-find "sh"))
(make-process/test-connection-type '(nil t t)
:connection-type '(pipe . pty)))
(ert-deftest make-process/connection-type/pty-with-stderr-buffer ()
(skip-unless (executable-find "sh"))
(let ((stderr-buffer (generate-new-buffer "*stderr*")))
(make-process/test-connection-type '(t t nil)
:connection-type 'pty :stderr stderr-buffer)))
(ert-deftest make-process/connection-type/out-pty-with-stderr-buffer ()
(skip-unless (executable-find "sh"))
(let ((stderr-buffer (generate-new-buffer "*stderr*")))
(make-process/test-connection-type '(nil t nil)
:connection-type '(pipe . pty) :stderr stderr-buffer)))
(ert-deftest make-process/file-handler/found () (ert-deftest make-process/file-handler/found ()
"Check that the `:file-handler argument of `make-process "Check that the `:file-handler argument of `make-process
works as expected if a file name handler is found." works as expected if a file name handler is found."