util-linux.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Rick van Rein <rick@openfortress.nl>
To: util-linux@vger.kernel.org
Subject: PATCH: script: Introduced a streaming output command
Date: Thu, 06 Dec 2018 12:43:02 +0100	[thread overview]
Message-ID: <5C090B46.5090200@openfortress.nl> (raw)

Besides output to a file, this patch introduces commands for
interactive streaming, such as over a network connection.  A
default "scriptstream" command may be locally installed with
all the bells and whistles that make sense locally.

Signed-off-by: Rick van Rein <rick@openfortress.nl>
---
diff --git a/AUTHORS b/AUTHORS
index d7dbbd6d5..84399e833 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -543,6 +543,7 @@ CONTRIBUTORS:
       Richard W.M. Jones <rjones@redhat.com>
       Richard Yann <yann.richard@uhb.fr>
       Richard Yao <ryao@gentoo.org>
+      Rick van Rein <rick@openfortress.nl>
       Rickard Faith <faith@cs.unc.edu>
       Rick Sladkey <jrs@world.std.com>
       Riku Voipio <riku.voipio@iki.fi>
diff --git a/term-utils/script.1 b/term-utils/script.1
index 0fb4c4fd7..2eb740d89 100644
--- a/term-utils/script.1
+++ b/term-utils/script.1
@@ -31,13 +31,18 @@
 .\"
 .\"	@(#)script.1	6.5 (Berkeley) 7/27/91
 .\"
-.TH SCRIPT "1" "June 2014" "util-linux" "User Commands"
+.TH SCRIPT "1" "December 2018" "util-linux" "User Commands"
 .SH NAME
 script \- make typescript of terminal session
 .SH SYNOPSIS
 .B script
 [options]
 .RI [ file ]
+.PP
+.B script
+[options]
+.RI [ cmd
+.RI [ args... ]]
 .SH DESCRIPTION
 .B script
 makes a typescript of everything displayed on your terminal.  It is
useful for
@@ -45,14 +50,31 @@ students who need a hardcopy record of an
interactive session as proof of an
 assignment, as the typescript file can be printed out later with
 .BR lpr (1).
 .PP
-If the argument
-.I file
-is given,
 .B script
-saves the dialogue in this
-.IR file .
-If no filename is given, the dialogue is saved in the file
+in its second form can stream its output to a command.  This is useful
to link
+to a variety of forms of sharing, including interactively through a command
+like
+.BR nc (1).
+.PP
+The first form is assumed when one or no
+.I file
+argument is supplied but no
+.I -s
+option.
+In this case, the dialogue is saved in
+.I file
+or, if this is not supplied, in the default output file
 .BR typescript .
+.PP
+The second form is assumed in the other cases,
+and the dialogue is then streamed to the standard input of
+.I cmd [args...]
+or, if this is not supplied, to the standard input of
+the default streaming command
+.BR scriptstream .
+Note that the default streaming command is not installed along with
+.BR script ,
+as it is intended to facilitate locally meaningful streaming options.
 .SH OPTIONS
 Below, the \fIsize\fR argument may be followed by the multiplicative
 suffixes KiB (=1024), MiB (=1024*1024), and so on for GiB, TiB, PiB,
EiB, ZiB and YiB
@@ -65,6 +87,7 @@ Append the output to
 or to
 .BR typescript ,
 retaining the prior contents.
+This is ignored when streaming to a command.
 .TP
 \fB\-c\fR, \fB\-\-command\fR \fIcommand\fR
 Run the
@@ -82,6 +105,7 @@ the child process is always stored in type script
file too.
 Flush output after each write.  This is nice for telecooperation: one
person
 does `mkfifo foo; script -f foo', and another can supervise real-time
what is
 being done using `cat foo'.
+This behaviour is implied for output to a streaming command.
 .TP
 \fB\-\-force\fR
 Allow the default output destination, i.e. the typescript file, to be a
hard
@@ -99,6 +123,20 @@ Due to buffering, the resulting output file might be
larger than the specified v
 \fB\-q\fR, \fB\-\-quiet\fR
 Be quiet (do not write start and done messages to standard output).
 .TP
+\fB\-s\fR, \fB\-\-stream\fR
+Do not output to a file, but to a streaming command.  This option is
+implied when more than a single
+.I file
+is given, as that can only be meaningfully interpreted as the form
+.I cmd args...
+but the option must be specified when streaming to the form
+.I cmd
+or when no command is specified at all.  In the latter case, the
+default command
+.B scriptstream
+is started.  This command is not installed by default, but may be
+created to obtain locally useful default behaviour.
+.TP
 \fB\-t\fR[\fIfile\fR], \fB\-\-timing\fR[=\fIfile\fR]
 Output timing data to standard error, or to
 .I file
@@ -162,6 +200,29 @@ You should also avoid use of script in command
pipes, as
 .B script
 can read more input than you would expect.
 .PP
+An example of a
+.B scriptstream
+command that would stream information to a certain port on a
+certain server could be
+.RS
+.RE
+.sp
+.na
+.RS
+.nf
+#!/bin/sh
+#
+# /usr/local/bin/scriptstream -- used for "script -s"
+#
+nc -q0 2001:db8::1234 5678 | sed 's/$/\\r/'
+.fi
+.RE
+.ad
+.PP
+Note how
+.IR nc (1)
+is said to terminate when reading EOF, and line endings of
+any output is mapped from LF to CRLF.
 .SH ENVIRONMENT
 The following environment variable is utilized by
 .BR script :
diff --git a/term-utils/script.c b/term-utils/script.c
index 7692f91e2..7a74ee3c6 100644
--- a/term-utils/script.c
+++ b/term-utils/script.c
@@ -39,6 +39,9 @@
  *
  * 2014-05-30 Csaba Kos <csaba.kos@gmail.com>
  * - fixed a rare deadlock after child termination
+ *
+ * 2018-12-05 Rick van Rein <rick@openfortress.nl>
+ * - added streaming command as an output option
  */

 #include <stdio.h>
@@ -97,12 +100,16 @@ UL_DEBUG_DEFINE_MASKNAMES(script) =
UL_DEBUG_EMPTY_MASKNAMES;
 #endif

 #define DEFAULT_TYPESCRIPT_FILENAME "typescript"
+#define DEFAULT_TYPESCRIPT_COMMAND  "scriptstream"

 struct script_control {
 	char *shell;		/* shell to be executed */
 	char *command;		/* command to be executed */
 	char *fname;		/* output file path */
 	FILE *typescriptfp;	/* output file pointer */
+	int stream;		/* output to streaming command */
+	char **streamcmd;	/* streaming command */
+	char *minicmd [2];	/* storage for a mini streaming command */
 	char *tname;		/* timing file path */
 	FILE *timingfp;		/* timing file pointer */
 	uint64_t outsz;         /* current output file size */
@@ -112,7 +119,9 @@ struct script_control {
 	int slave;		/* pseudoterminal slave file descriptor */
 	int poll_timeout;	/* poll() timeout, used in end of execution */
 	pid_t child;		/* child pid */
+	pid_t streampid;	/* streaming process */
 	int childstatus;	/* child process exit value */
+	int streamstatus;	/* streaming process exit value */
 	struct termios attrs;	/* slave terminal runtime attributes */
 	struct winsize win;	/* terminal window size */
 #if !HAVE_LIBUTIL || !HAVE_PTY_H
@@ -161,7 +170,7 @@ static void __attribute__((__noreturn__)) usage(void)
 {
 	FILE *out = stdout;
 	fputs(USAGE_HEADER, out);
-	fprintf(out, _(" %s [options] [file]\n"), program_invocation_short_name);
+	fprintf(out, _(" %s [options] [file]\n %s [options] [cmd
[args...]]\n"), program_invocation_short_name,
program_invocation_short_name);

 	fputs(USAGE_SEPARATOR, out);
 	fputs(_("Make a typescript of a terminal session.\n"), out);
@@ -172,6 +181,7 @@ static void __attribute__((__noreturn__)) usage(void)
 		" -e, --return                  return exit code of the child process\n"
 		" -f, --flush                   run flush after each write\n"
 		"     --force                   use output file even when it is a link\n"
+		" -s, --stream                  do not output to file but to a
streaming command\n"
 		" -o, --output-limit <size>     terminate if output files exceed size\n"
 		" -q, --quiet                   be quiet\n"
 		" -t[<file>], --timing[=<file>] output timing data to stderr or to
FILE\n"
@@ -313,15 +323,27 @@ static void __attribute__((__noreturn__))
fail(struct script_control *ctl)

 static void wait_for_child(struct script_control *ctl, int wait)
 {
-	int status;
-	pid_t pid;
 	int options = wait ? 0 : WNOHANG;

 	DBG(MISC, ul_debug("waiting for child"));

-	while ((pid = wait3(&status, options, NULL)) > 0)
-		if (pid == ctl->child)
-			ctl->childstatus = status;
+	waitpid(ctl->child, &ctl->childstatus, options);
+}
+
+static void wait_for_stream_cmd(struct script_control *ctl, int wait)
+{
+	int options = wait ? 0 : WNOHANG;
+
+	if (ctl->stream && ctl->typescriptfp) {
+		DBG(MISC, ul_debug("closing stream subcommand"));
+		fclose (ctl->typescriptfp);
+		ctl->typescriptfp = NULL;
+		kill(ctl->streampid, SIGTERM);
+	}
+
+	DBG(MISC, ul_debug("waiting for stream subcommand"));
+
+	waitpid(ctl->streampid, &ctl->streamstatus, options);
 }

 static void write_output(struct script_control *ctl, char *obuf,
@@ -483,6 +505,7 @@ static void handle_signal(struct script_control
*ctl, int fd)
 		    || info.ssi_code == CLD_KILLED
 		    || info.ssi_code == CLD_DUMPED) {
 			wait_for_child(ctl, 0);
+			wait_for_stream_cmd(ctl, 0);
 			ctl->poll_timeout = 10;

 		/* In case of ssi_code is CLD_TRAPPED, CLD_STOPPED, or CLD_CONTINUED */
@@ -519,6 +542,7 @@ static void handle_signal(struct script_control
*ctl, int fd)
 static void do_io(struct script_control *ctl)
 {
 	int ret, eof = 0;
+	int cmdio[2];
 	time_t tvec = script_time((time_t *)NULL);
 	enum {
 		POLLFD_SIGNAL = 0,
@@ -533,9 +557,33 @@ static void do_io(struct script_control *ctl)
 	};


-	if ((ctl->typescriptfp =
-	     fopen(ctl->fname, ctl->append ? "a" UL_CLOEXECSTR : "w"
UL_CLOEXECSTR)) == NULL) {
-
+	if (ctl->stream) {
+		if (pipe(cmdio) == -1) {
+			warn(_("pipe failed"));
+			fail(ctl);
+			exit(1);
+		}
+		ctl->streampid = fork();
+		switch (ctl->streampid) {
+		case -1: /* error */
+			warn(_("fork failed"));
+			fail(ctl);
+			exit(1);
+		case 0: /* child */
+			close(cmdio[1]);
+			dup2(cmdio[0],0);
+			execvp(ctl->streamcmd[0], ctl->streamcmd);
+			fprintf(stderr,_("streaming command failed: %s\r\n"),strerror(errno));
+			exit(1);
+			break;
+		default: /* parent */
+			close(cmdio[0]);
+			ctl->typescriptfp = fdopen(cmdio[1],"w");
+		}
+	} else {
+		ctl->typescriptfp = fopen(ctl->fname, ctl->append ? "a" UL_CLOEXECSTR
: "w" UL_CLOEXECSTR);
+	}
+	if (ctl->typescriptfp == NULL) {
 		restore_tty(ctl, TCSANOW);
 		warn(_("cannot open %s"), ctl->fname);
 		fail(ctl);
@@ -622,6 +670,9 @@ static void do_io(struct script_control *ctl)
 		wait_for_child(ctl, 1);

 	done(ctl);
+
+	if (ctl->stream)
+		wait_for_stream_cmd(ctl, 1);
 }

 static void getslave(struct script_control *ctl)
@@ -775,6 +826,7 @@ int main(int argc, char **argv)
 		{"flush", no_argument, NULL, 'f'},
 		{"force", no_argument, NULL, FORCE_OPTION,},
 		{"output-limit", required_argument, NULL, 'o'},
+		{"stream", no_argument, NULL, 's'},
 		{"quiet", no_argument, NULL, 'q'},
 		{"timing", optional_argument, NULL, 't'},
 		{"version", no_argument, NULL, 'V'},
@@ -797,7 +849,7 @@ int main(int argc, char **argv)

 	script_init_debug();

-	while ((ch = getopt_long(argc, argv, "ac:efo:qt::Vh", longopts, NULL))
!= -1)
+	while ((ch = getopt_long(argc, argv, "ac:efo:sqt::Vh", longopts,
NULL)) != -1)
 		switch (ch) {
 		case 'a':
 			ctl.append = 1;
@@ -817,6 +869,10 @@ int main(int argc, char **argv)
 		case 'o':
 			ctl.maxsz = strtosize_or_err(optarg, _("failed to parse output limit
size"));
 			break;
+		case 's':
+			ctl.stream = 1;
+			ctl.flush = 1;
+			break;
 		case 'q':
 			ctl.quiet = 1;
 			break;
@@ -837,12 +893,32 @@ int main(int argc, char **argv)
 		}
 	argc -= optind;
 	argv += optind;
-
-	if (argc > 0)
-		ctl.fname = argv[0];
-	else {
-		ctl.fname = DEFAULT_TYPESCRIPT_FILENAME;
-		die_if_link(&ctl);
+	assert (argv [argc] == NULL);
+
+	/* streaming may be ordered with option --stream or with argc > 1 */
+	if (argc == 0) {
+		if (ctl.stream) {
+			ctl.minicmd[0] = DEFAULT_TYPESCRIPT_COMMAND;
+			ctl.minicmd[1] = NULL;
+			ctl.streamcmd = &ctl.minicmd[0];
+		} else {
+			/* No default implementation to make this pluggable */
+			ctl.fname = DEFAULT_TYPESCRIPT_FILENAME;
+			die_if_link(&ctl);
+		}
+	} else if (argc == 1) {
+		if (ctl.stream) {
+			ctl.minicmd[0] = argv[0];
+			ctl.minicmd[1] = NULL;
+			ctl.streamcmd = &ctl.minicmd[0];
+		} else {
+			ctl.fname = argv[0];
+		}
+	} else {
+		/* implicit streaming for more than 1 command argument */
+		ctl.streamcmd = argv;
+		ctl.stream = 1;
+		ctl.flush = 1;
 	}

 	ctl.shell = getenv("SHELL");
@@ -850,8 +926,13 @@ int main(int argc, char **argv)
 		ctl.shell = _PATH_BSHELL;

 	getmaster(&ctl);
-	if (!ctl.quiet)
-		printf(_("Script started, file is %s\n"), ctl.fname);
+	if (!ctl.quiet) {
+		if (ctl.stream) {
+			printf(_("Script started, streaming command is %s\n"),
ctl.streamcmd[0]);
+		} else {
+			printf(_("Script started, file is %s\n"), ctl.fname);
+		}
+	}
 	enable_rawmode_tty(&ctl);

 #ifdef HAVE_LIBUTEMPTER

             reply	other threads:[~2018-12-06 11:43 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-12-06 11:43 Rick van Rein [this message]
2018-12-10 11:46 ` PATCH: script: Introduced a streaming output command Karel Zak
2018-12-10 12:47   ` Rick van Rein
2018-12-10 14:47   ` Rick van Rein
2018-12-17  9:43     ` Karel Zak

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=5C090B46.5090200@openfortress.nl \
    --to=rick@openfortress.nl \
    --cc=util-linux@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).