Page MenuHomeFreeBSD

D51839.id160149.diff
No OneTemporary

D51839.id160149.diff

diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -1197,6 +1197,8 @@
..
rs
..
+ script
+ ..
sdiff
..
sed
diff --git a/usr.bin/script/Makefile b/usr.bin/script/Makefile
--- a/usr.bin/script/Makefile
+++ b/usr.bin/script/Makefile
@@ -1,5 +1,10 @@
+.include <src.opts.mk>
+
PROG= script
LIBADD= util
+HAS_TESTS=
+SUBDIR.${MK_TESTS}+= tests
+
.include <bsd.prog.mk>
diff --git a/usr.bin/script/script.c b/usr.bin/script/script.c
--- a/usr.bin/script/script.c
+++ b/usr.bin/script/script.c
@@ -47,6 +47,7 @@
#include <libutil.h>
#include <paths.h>
#include <signal.h>
+#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -62,8 +63,15 @@
uint32_t scr_direction; /* 'i', 'o', etc (also indicates endianness) */
};
+enum elm_type {
+ ET_DATA,
+ ET_EOF,
+ ET_CLOSE,
+};
+
struct buf_elm {
TAILQ_ENTRY(buf_elm) link;
+ enum elm_type etype;
size_t rpos;
size_t len;
char ibuf[];
@@ -74,7 +82,7 @@
static int child;
static const char *fname;
static char *fmfname;
-static int fflg, qflg, ttyflg;
+static int fflg, kflg, qflg, ttyflg;
static int usesleep, rawout, showexit;
static TAILQ_HEAD(, buf_elm) obuf_list = TAILQ_HEAD_INITIALIZER(obuf_list);
static volatile sig_atomic_t doresize;
@@ -97,6 +105,66 @@
static void usage(void) __dead2;
static void resizeit(int);
+/*
+ * Writes the enqueued buf_elm out to the pty. Returns true if the element has
+ * been completely consumed, false if it has not.
+ */
+static bool
+write_input(struct buf_elm *be, struct termios *stt)
+{
+ ssize_t cc;
+
+ switch (be->etype) {
+ case ET_DATA:
+ cc = write(master, be->ibuf + be->rpos, be->len);
+ if (cc == -1) {
+ if (errno == EWOULDBLOCK ||
+ errno == EINTR)
+ break;
+ warn("write master");
+ done(1);
+ } else if (cc == 0)
+ return (false); /* retry later ? */
+
+ if (kflg && tcgetattr(master, stt) >= 0 &&
+ ((stt->c_lflag & ECHO) == 0))
+ (void)fwrite(be->ibuf + be->rpos, 1, cc, fscript);
+
+ be->len -= cc;
+ if (be->len != 0) {
+ be->rpos += cc;
+ return (false);
+ }
+
+ break;
+ case ET_EOF:
+ (void)write(master, &stt->c_cc[VEOF], 1);
+ break;
+ case ET_CLOSE:
+ (void)close(master);
+ master = -1;
+ break;
+ }
+
+ return (true);
+}
+
+static void
+enqueue_input(enum elm_type etype, const char *buf, size_t bufsz)
+{
+ struct buf_elm *be;
+
+ assert(etype != ET_DATA || buf != NULL);
+ be = malloc(sizeof(*be) + bufsz);
+ if (be == NULL)
+ err(1, "malloc");
+ be->etype = etype;
+ be->rpos = 0;
+ be->len = bufsz;
+ memcpy(be->ibuf, buf, bufsz);
+ TAILQ_INSERT_TAIL(&obuf_list, be, link);
+}
+
int
main(int argc, char *argv[])
{
@@ -110,7 +178,7 @@
fd_set rfd, wfd;
struct buf_elm *be;
ssize_t cc;
- int aflg, Fflg, kflg, pflg, wflg, ch, k, n, fcm;
+ int aflg, Fflg, pflg, wflg, ch, k, n, fcm;
int flushtime, readstdin;
int fm_fd, fm_log;
@@ -295,11 +363,16 @@
start = tvec = time(0);
readstdin = 1;
- for (;;) {
+
+ /*
+ * Keep looping until we end up closing the pty or we have run out of
+ * obuf_list and cannot possibly read any more.
+ */
+ while (master >= 0 && (!TAILQ_EMPTY(&obuf_list) || readstdin >= 0)) {
FD_ZERO(&rfd);
FD_ZERO(&wfd);
FD_SET(master, &rfd);
- if (readstdin)
+ if (readstdin > 0)
FD_SET(STDIN_FILENO, &rfd);
if (!TAILQ_EMPTY(&obuf_list))
FD_SET(master, &wfd);
@@ -327,51 +400,42 @@
if (n > 0 && FD_ISSET(STDIN_FILENO, &rfd)) {
cc = read(STDIN_FILENO, ibuf, BUFSIZ);
- if (cc < 0)
- break;
+ if (cc < 0) {
+ /*
+ * readstdin == 0 signals that we have nothing
+ * to read, but we may try to read stdin after
+ * some timeout if it's a tty -- negative values
+ * of readstdin will avoid it.
+ */
+ readstdin = -1;
+ enqueue_input(ET_CLOSE, NULL, 0);
+ }
if (cc == 0) {
if (tcgetattr(master, &stt) == 0 &&
(stt.c_lflag & ICANON) != 0) {
- (void)write(master, &stt.c_cc[VEOF], 1);
+ enqueue_input(ET_EOF, NULL, 0);
}
readstdin = 0;
}
if (cc > 0) {
if (rawout)
record(fscript, ibuf, cc, 'i');
- be = malloc(sizeof(*be) + cc);
- be->rpos = 0;
- be->len = cc;
- memcpy(be->ibuf, ibuf, cc);
- TAILQ_INSERT_TAIL(&obuf_list, be, link);
+ enqueue_input(ET_DATA, ibuf, cc);
}
+
+ n--;
}
if (n > 0 && FD_ISSET(master, &wfd)) {
while ((be = TAILQ_FIRST(&obuf_list)) != NULL) {
- cc = write(master, be->ibuf + be->rpos,
- be->len);
- if (cc == -1) {
- if (errno == EWOULDBLOCK ||
- errno == EINTR)
- break;
- warn("write master");
- done(1);
- }
- if (cc == 0)
- break; /* retry later ? */
- if (kflg && tcgetattr(master, &stt) >= 0 &&
- ((stt.c_lflag & ECHO) == 0)) {
- (void)fwrite(be->ibuf + be->rpos,
- 1, cc, fscript);
- }
- be->len -= cc;
- if (be->len == 0) {
- TAILQ_REMOVE(&obuf_list, be, link);
- free(be);
- } else {
- be->rpos += cc;
- }
+ if (!write_input(be, &stt))
+ break;
+
+ /* Item is consumed. */
+ TAILQ_REMOVE(&obuf_list, be, link);
+ free(be);
}
+
+ n--;
}
if (n > 0 && FD_ISSET(master, &rfd)) {
cc = read(master, obuf, sizeof(obuf));
@@ -382,6 +446,8 @@
record(fscript, obuf, cc, 'o');
else
(void)fwrite(obuf, 1, cc, fscript);
+
+ n--;
}
tvec = time(0);
if (tvec - start >= flushtime) {
@@ -391,6 +457,10 @@
if (Fflg)
fflush(fscript);
}
+
+ if (master < 0)
+ assert(TAILQ_EMPTY(&obuf_list));
+
finish();
done(0);
}
@@ -470,7 +540,8 @@
}
}
(void)fclose(fscript);
- (void)close(master);
+ if (master >= 0)
+ (void)close(master);
exit(eno);
}
diff --git a/usr.bin/script/tests/Makefile b/usr.bin/script/tests/Makefile
new file mode 100644
--- /dev/null
+++ b/usr.bin/script/tests/Makefile
@@ -0,0 +1,6 @@
+PACKAGE= tests
+
+ATF_TESTS_SH+= script_test
+${PACKAGE}FILES+= echo_loop.sh
+
+.include <bsd.test.mk>
diff --git a/usr.bin/script/tests/echo_loop.sh b/usr.bin/script/tests/echo_loop.sh
new file mode 100755
--- /dev/null
+++ b/usr.bin/script/tests/echo_loop.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+#
+# Copyright (C) 2025 Kyle Evans <kevans@FreeBSD.org>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+# porch(1) will race script(1) putting the tty into raw mode and flushing it at
+# the same time, and we lose our input almost every time. Providing a sentinel
+# that naturally occurs *after* the tty is configured to avoid that is the main
+# point of this script.
+echo "Proceed"
+while read line; do
+ echo "$line"
+done
+echo "Finished"
diff --git a/usr.bin/script/tests/script_test.sh b/usr.bin/script/tests/script_test.sh
new file mode 100644
--- /dev/null
+++ b/usr.bin/script/tests/script_test.sh
@@ -0,0 +1,119 @@
+#
+# Copyright (c) 2025 Kyle Evans <kevans@FreeBSD.org>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+echo_loop="$(atf_get_srcdir)/echo_loop.sh"
+
+atf_test_case interactive_basic
+interactive_basic_head()
+{
+ atf_set "descr" "Test basic interactive use"
+ atf_set "require.progs" "porch"
+}
+interactive_basic_body()
+{
+ cat <<EOF > basic.porch
+match "Proceed"
+write "sample\r"
+match "sample"
+write "^D"
+eof()
+EOF
+
+ atf_check porch -f basic.porch script echo.script sh "$echo_loop"
+ atf_check grep -q "Script started" echo.script
+ atf_check grep -q "Command:" echo.script
+ atf_check grep -q "sample" echo.script
+ atf_check grep -q "exit status" echo.script
+ atf_check grep -q "Script done" echo.script
+ atf_check -o match:"1" -x 'grep -o "sample" echo.script | uniq -c'
+
+ # Check -k for input recording while we're here.
+ atf_check porch -f basic.porch script -k echo.script sh "$echo_loop"
+ atf_check -o match:"2" -x 'grep -o "sample" echo.script | uniq -c'
+}
+
+atf_test_case noninteractive_cmd
+noninteractive_cmd_head()
+{
+ atf_set "descr" "Test non-interactive usage of the command form"
+}
+noninteractive_cmd_body()
+{
+ echo "sample" > input
+ atf_check -o not-empty script cat.script cat < input
+
+ atf_check grep -q "Script started" cat.script
+ atf_check grep -q "Command:" cat.script
+ atf_check grep -q "sample" cat.script
+ atf_check grep -q "exit status" cat.script
+ atf_check grep -q "Script done" cat.script
+
+ # We should see the sample string twice: once for echo, once from cat.
+ # One will have VEOF following it, the other not (thus grep -o).
+ atf_check -o match:"2" -x 'grep -o "sample" cat.script | uniq -c'
+}
+
+atf_test_case noninteractive_cmd_append
+noninteractive_cmd_append_head()
+{
+ atf_set "descr" "Test append (-a) using the noninteractive command form"
+}
+noninteractive_cmd_append_body()
+{
+ echo "sample" > input
+ atf_check -o not-empty script cat.script cat < input
+ atf_check -o not-empty script -a cat.script cat < input
+
+ atf_check -o match:"4" -x 'grep -o "sample" cat.script | uniq -c'
+}
+
+atf_test_case noninteractive_cmd_error
+noninteractive_cmd_error_head()
+{
+ atf_set "descr" \
+ "Test non-interactive usage of the command form encountering an error"
+}
+noninteractive_cmd_error_body()
+{
+ # Check both that the error is propagated, and that we can observe it
+ # in the script file.
+ atf_check -o not-empty -s exit:9 script _.script /bin/sh -c 'exit 9'
+ atf_check grep -q 'exit status: 9' _.script
+}
+
+atf_test_case quiet
+quiet_head()
+{
+ atf_set "descr" "Test the quiet form of output"
+}
+quiet_body()
+{
+ echo "sample" > input
+
+ # Largely lifted from the noninteractive_cmd test; just the framing is
+ # stripped off.
+ atf_check -o not-empty script -q cat.script cat < input
+
+ atf_check -s not-exit:0 grep -q "Script started" cat.script
+ atf_check -s not-exit:0 grep -q "Command:" cat.script
+ atf_check grep -q "sample" cat.script
+ atf_check -s not-exit:0 grep -q "exit status" cat.script
+ atf_check -s not-exit:0 grep -q "Script done" cat.script
+
+ # We should see the sample string twice: once for echo, once from cat.
+ # One will have VEOF following it, the other not (thus grep -o).
+ atf_check -o match:"2" -x 'grep -o "sample" cat.script | uniq -c'
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case interactive_basic
+ atf_add_test_case noninteractive_cmd
+ atf_add_test_case noninteractive_cmd_append
+ atf_add_test_case noninteractive_cmd_error
+ atf_add_test_case quiet
+}
+

File Metadata

Mime Type
text/plain
Expires
Sun, Jul 19, 7:45 AM (8 h, 14 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
35226591
Default Alt Text
D51839.id160149.diff (10 KB)

Event Timeline