Browse Source

Merge branch 'tty'

Serj Kalichev 6 months ago
parent
commit
b34cc54f97

+ 71 - 15
bin/klish/interactive.c

@@ -44,6 +44,8 @@ static bool_t stdin_cb(faux_eloop_t *eloop, faux_eloop_type_e type,
 	void *associated_data, void *user_data);
 static bool_t sigwinch_cb(faux_eloop_t *eloop, faux_eloop_type_e type,
 	void *associated_data, void *user_data);
+static bool_t ctrl_c_cb(faux_eloop_t *eloop, faux_eloop_type_e type,
+	void *associated_data, void *user_data);
 
 static void reset_hotkey_table(ctx_t *ctx);
 static bool_t interactive_stdout_cb(ktp_session_t *ktp, const char *line, size_t len,
@@ -119,6 +121,14 @@ int klish_interactive_shell(ktp_session_t *ktp, struct options *opts)
 
 	eloop = ktp_session_eloop(ktp);
 
+	// Reassign signal handlers. Handlers are used to send SIGINT
+	// to non-interactive commands
+	faux_eloop_add_signal(eloop, SIGINT, ctrl_c_cb, &ctx);
+	faux_eloop_add_signal(eloop, SIGTERM, ctrl_c_cb, &ctx);
+	faux_eloop_add_signal(eloop, SIGQUIT, ctrl_c_cb, &ctx);
+	// To don't stop klish client on exit. SIGTSTP can be pended
+	faux_eloop_add_signal(eloop, SIGTSTP, ctrl_c_cb, &ctx);
+
 	// Notify server about terminal window size change
 	faux_eloop_add_signal(eloop, SIGWINCH, sigwinch_cb, &ctx);
 
@@ -255,6 +265,31 @@ bool_t cmd_ack_cb(ktp_session_t *ktp, const faux_msg_t *msg, void *udata)
 	ctx_t *ctx = (ctx_t *)udata;
 	int rc = -1;
 	faux_error_t *error = NULL;
+	bool_t it_was_pager = BOOL_FALSE;
+
+	// Wait for pager
+	if (ctx->pager_working != TRI_UNDEFINED) {
+		pclose(ctx->pager_pipe);
+		ctx->pager_working = TRI_UNDEFINED;
+		ctx->pager_pipe = NULL;
+		it_was_pager = BOOL_TRUE;
+	}
+
+	// Disable SIGINT caught for non-interactive commands.
+	// Do it after pager exit. Else it can restore wrong tty mode after
+	// ISIG disabling
+	tinyrl_disable_isig(ctx->tinyrl);
+
+	// Sometimes output stream from server doesn't contain final crlf so
+	// goto newline itself
+	if (ktp_session_last_stream(ktp) == STDERR_FILENO) {
+		if (ktp_session_stderr_need_newline(ktp))
+			fprintf(stderr, "\n");
+	} else {
+		// Pager adds newline itself
+		if (ktp_session_stdout_need_newline(ktp) && !it_was_pager)
+			tinyrl_crlf(ctx->tinyrl);
+	}
 
 	process_prompt_param(ctx->tinyrl, msg);
 	process_hotkey_param(ctx, msg);
@@ -270,13 +305,6 @@ bool_t cmd_ack_cb(ktp_session_t *ktp, const faux_msg_t *msg, void *udata)
 	}
 	faux_error_free(error);
 
-	// Wait for pager
-	if (ctx->pager_working != TRI_UNDEFINED) {
-		pclose(ctx->pager_pipe);
-		ctx->pager_working = TRI_UNDEFINED;
-		ctx->pager_pipe = NULL;
-	}
-
 	tinyrl_set_busy(ctx->tinyrl, BOOL_FALSE);
 	if (!ktp_session_done(ktp))
 		tinyrl_redisplay(ctx->tinyrl);
@@ -296,11 +324,16 @@ bool_t cmd_incompleted_ack_cb(ktp_session_t *ktp, const faux_msg_t *msg, void *u
 {
 	ctx_t *ctx = (ctx_t *)udata;
 
-	// Interactive command. So restore stdin handler.
-	if ((ktp_session_state(ktp) == KTP_SESSION_STATE_WAIT_FOR_CMD) &&
-		KTP_STATUS_IS_INTERACTIVE(ktp_session_cmd_features(ktp))) {
-		faux_eloop_add_fd(ktp_session_eloop(ktp), STDIN_FILENO, POLLIN,
-			stdin_cb, ctx);
+	if (ktp_session_state(ktp) == KTP_SESSION_STATE_WAIT_FOR_CMD) {
+		// Cmd need stdin so restore stdin handler
+		if (KTP_STATUS_IS_NEED_STDIN(ktp_session_cmd_features(ktp))) {
+			// Disable SIGINT signal (it is used for commands that
+			// don't need stdin. Commands with stdin can get ^C
+			// themself interactively.)
+			tinyrl_disable_isig(ctx->tinyrl);
+			faux_eloop_add_fd(ktp_session_eloop(ktp), STDIN_FILENO, POLLIN,
+				stdin_cb, ctx);
+		}
 	}
 
 	// Happy compiler
@@ -333,9 +366,9 @@ static bool_t stdin_cb(faux_eloop_t *eloop, faux_eloop_type_e type,
 		return rc;
 	}
 
-	// Interactive command
+	// Command needs stdin
 	if ((state == KTP_SESSION_STATE_WAIT_FOR_CMD) &&
-		KTP_STATUS_IS_INTERACTIVE(ktp_session_cmd_features(ctx->ktp))) {
+		KTP_STATUS_IS_NEED_STDIN(ktp_session_cmd_features(ctx->ktp))) {
 		int fd = fileno(tinyrl_istream(ctx->tinyrl));
 		char buf[1024] = {};
 		ssize_t bytes_readed = 0;
@@ -406,6 +439,26 @@ static bool_t sigwinch_cb(faux_eloop_t *eloop, faux_eloop_type_e type,
 }
 
 
+static bool_t ctrl_c_cb(faux_eloop_t *eloop, faux_eloop_type_e type,
+	void *associated_data, void *udata)
+{
+	ctx_t *ctx = (ctx_t *)udata;
+	char ctrl_c = KEY_ETX;
+
+	if (!ctx)
+		return BOOL_FALSE;
+
+	ktp_session_stdin(ctx->ktp, &ctrl_c, sizeof(ctrl_c));
+
+	// Happy compiler
+	eloop = eloop;
+	type = type;
+	associated_data = associated_data;
+
+	return BOOL_TRUE;
+}
+
+
 static bool_t tinyrl_key_enter(tinyrl_t *tinyrl, unsigned char key)
 {
 	const char *line = NULL;
@@ -426,6 +479,9 @@ static bool_t tinyrl_key_enter(tinyrl_t *tinyrl, unsigned char key)
 
 	tinyrl_reset_line(tinyrl);
 	tinyrl_set_busy(tinyrl, BOOL_TRUE);
+	// Suppose non-interactive command by default
+	// Caught SIGINT for non-interactive commands
+	tinyrl_enable_isig(tinyrl);
 
 	key = key; // Happy compiler
 
@@ -724,7 +780,7 @@ static bool_t interactive_stdout_cb(ktp_session_t *ktp, const char *line, size_t
 	if (
 		ctx->opts->pager_enabled && // Pager enabled within config file
 		(ctx->pager_working == TRI_UNDEFINED) && // Pager is not working
-		!(ktp_session_cmd_features(ktp) & KTP_STATUS_INTERACTIVE) // Non interactive command
+		!KTP_STATUS_IS_INTERACTIVE(ktp_session_cmd_features(ktp)) // Non interactive command
 		) {
 
 		ctx->pager_pipe = popen(ctx->opts->pager, "we");

+ 1 - 0
bin/klish/klish.c

@@ -11,6 +11,7 @@
 #include <getopt.h>
 #include <sys/socket.h>
 #include <sys/un.h>
+#include <syslog.h>
 #ifdef HAVE_LOCALE_H
 #include <locale.h>
 #endif

+ 1 - 1
bin/klish/private.h

@@ -7,7 +7,7 @@
 #endif
 
 #define DEFAULT_CFGFILE "/etc/klish/klish.conf"
-#define DEFAULT_PAGER "/usr/bin/less -I -F -e -X -K -d -r"
+#define DEFAULT_PAGER "/usr/bin/less -I -F -e -X -K -d -R"
 
 /** @brief Command line and config file options
  */

+ 14 - 0
klish-no-pager.conf

@@ -0,0 +1,14 @@
+# Template for config file /etc/klish/klish.conf. It's used by klish utility.
+
+# The klishd uses UNIX domain socket to receive connections. It will create an
+# filesystem entry to allow clients to find connection point. By default klish
+# client utility will connect to /tmp/klish-unix-socket.
+#UnixSocketPath=/tmp/klish-unix-socket
+
+# The klish can use external pager for non-interactive commands. By default it
+# will execute "/usr/bin/less -I -F -e -X -K -d -r" process as a pager.
+Pager="/usr/bin/less -I -F -e -X -K -d -r"
+
+# External pager is enabled by default. But user can explicitly enable or
+# disable it. Use "y" or "n" values.
+UsePager=n

+ 1 - 1
klish.conf

@@ -7,7 +7,7 @@
 
 # The klish can use external pager for non-interactive commands. By default it
 # will execute "/usr/bin/less -I -F -e -X -K -d -r" process as a pager.
-Pager="/usr/bin/less -I -F -e -X -K -d -r"
+Pager="/usr/bin/less -I -F -e -X -K -d -R"
 
 # External pager is enabled by default. But user can explicitly enable or
 # disable it. Use "y" or "n" values.

+ 26 - 5
klish.xsd

@@ -145,10 +145,13 @@
 *	action execution.
 *
 * [interrupt="true/false"] - The boolean field that specify that action can be
-*	be interrupted by Ctrl^C. Default is false. Ignored for non-interactive
-*	actions.
+*	be interrupted by Ctrl^C. Default is false.
 *
-* [interactive="true/false"] - Is action interactive.
+* [in="true/false/tty"] - Does ACTION need input. The "tty" means action can use
+*	terminal.
+*
+* [out="true/false/tty"] - How does ACTION use output. The "tty" means action
+*	generate output for terminal.
 *
 * [exec_on="fail/success/always/never"] - ACTION's execution depends on
 *	return code of previous elements of ACTION chain. If the
@@ -183,13 +186,22 @@
 		</xs:restriction>
 	</xs:simpleType>
 
+	<xs:simpleType name="action_io_t">
+		<xs:restriction base="xs:string">
+			<xs:enumeration value="false"/>
+			<xs:enumeration value="true"/>
+			<xs:enumeration value="tty"/>
+		</xs:restriction>
+	</xs:simpleType>
+
 	<xs:complexType name="action_t">
 		<xs:simpleContent>
 			<xs:extension base="xs:string">
 				<xs:attribute name="sym" type="xs:string" use="optional"/>
 				<xs:attribute name="lock" type="xs:string" use="optional"/>
 				<xs:attribute name="interrupt" type="xs:boolean" use="optional" default="false"/>
-				<xs:attribute name="interactive" type="xs:boolean" use="optional" default="false"/>
+				<xs:attribute name="in" type="action_io_t" use="optional" default="false"/>
+				<xs:attribute name="out" type="action_io_t" use="optional" default="true"/>
 				<xs:attribute name="exec_on" type="action_cond_t" use="optional" default="success"/>
 				<xs:attribute name="update_retcode" type="xs:boolean" use="optional" default="true"/>
 				<xs:attribute name="permanent" type="xs:boolean" use="optional" default="false"/>
@@ -270,6 +282,14 @@
 		</xs:restriction>
 	</xs:simpleType>
 
+	<xs:simpleType name="entry_filter_t">
+		<xs:restriction base="xs:string">
+			<xs:enumeration value="true"/>
+			<xs:enumeration value="false"/>
+			<xs:enumeration value="dual"/>
+		</xs:restriction>
+	</xs:simpleType>
+
 	<xs:group name="entry_group_t">
 		<xs:choice>
 			<xs:element ref="HOTKEY" minOccurs="0" maxOccurs="unbounded"/>
@@ -305,7 +325,7 @@
 		<xs:attribute name="value" type="xs:string" use="optional"/>
 		<xs:attribute name="restore" type="xs:boolean" use="optional" default="false"/>
 		<xs:attribute name="order" type="xs:boolean" use="optional" default="false"/>
-		<xs:attribute name="filter" type="xs:boolean" use="optional" default="false"/>
+		<xs:attribute name="filter" type="entry_filter_t" use="optional" default="false"/>
 	</xs:complexType>
 
 
@@ -439,6 +459,7 @@
 		<xs:attribute name="ref" type="xs:string" use="optional"/>
 		<xs:attribute name="value" type="xs:string" use="optional"/>
 		<xs:attribute name="restore" type="xs:boolean" use="optional" default="false"/>
+		<xs:attribute name="filter" type="entry_filter_t" use="optional" default="false"/>
 	</xs:complexType>
 
 </xs:schema>

+ 2 - 1
klish/iaction.h

@@ -13,7 +13,8 @@ typedef struct iaction_s {
 	char *sym;
 	char *lock;
 	char *interrupt;
-	char *interactive;
+	char *in;
+	char *out;
 	char *exec_on;
 	char *update_retcode;
 	char *permanent;

+ 54 - 7
klish/ischeme/iaction.c

@@ -2,6 +2,7 @@
 #include <stdio.h>
 #include <string.h>
 #include <assert.h>
+#include <syslog.h>
 
 #include <faux/str.h>
 #include <faux/conv.h>
@@ -14,6 +15,41 @@
 #define TAG "ACTION"
 
 
+static const char * const kaction_io_e_str[] = {
+	NULL,
+	"false",
+	"true",
+	"tty",
+};
+
+
+static const char *kaction_io_e_enum2str(kaction_io_e io)
+{
+	if ((KACTION_IO_NONE == io) || (io >= KACTION_IO_MAX))
+		return NULL;
+
+	return kaction_io_e_str[io];
+}
+
+
+static kaction_io_e kaction_io_e_str2enum(const char *str)
+{
+	kaction_io_e io = KACTION_IO_NONE;
+
+	if (!str)
+		return KACTION_IO_NONE;
+
+	for (io = (KACTION_IO_NONE + 1); io < KACTION_IO_MAX; io++) {
+		if (faux_str_casecmp(str, kaction_io_e_str[io]) == 0)
+			break;
+	}
+	if (io >= KACTION_IO_MAX)
+		return KACTION_IO_NONE;
+
+	return io;
+}
+
+
 bool_t iaction_parse(const iaction_t *info, kaction_t *action, faux_error_t *error)
 {
 	bool_t retcode = BOOL_TRUE;
@@ -49,12 +85,22 @@ bool_t iaction_parse(const iaction_t *info, kaction_t *action, faux_error_t *err
 		}
 	}
 
-	// Interactive
-	if (!faux_str_is_empty(info->interactive)) {
-		bool_t b = BOOL_FALSE;
-		if (!faux_conv_str2bool(info->interactive, &b) ||
-			!kaction_set_interactive(action, b)) {
-			faux_error_add(error, TAG": Illegal 'interactive' attribute");
+	// In
+	if (!faux_str_is_empty(info->in)) {
+		kaction_io_e io = KACTION_IO_NONE;
+		if (((io = kaction_io_e_str2enum(info->in)) == KACTION_IO_NONE) ||
+			!kaction_set_in(action, io)) {
+			faux_error_add(error, TAG": Illegal 'in' attribute");
+			retcode = BOOL_FALSE;
+		}
+	}
+
+	// Out
+	if (!faux_str_is_empty(info->out)) {
+		kaction_io_e io = KACTION_IO_NONE;
+		if (((io = kaction_io_e_str2enum(info->out)) == KACTION_IO_NONE) ||
+			!kaction_set_out(action, io)) {
+			faux_error_add(error, TAG": Illegal 'out' attribute");
 			retcode = BOOL_FALSE;
 		}
 	}
@@ -152,7 +198,8 @@ char *iaction_deploy(const kaction_t *kaction, int level)
 	attr2ctext(&str, "sym", kaction_sym_ref(kaction), level + 1);
 	attr2ctext(&str, "lock", kaction_lock(kaction), level + 1);
 	attr2ctext(&str, "interrupt", faux_conv_bool2str(kaction_interrupt(kaction)), level + 1);
-	attr2ctext(&str, "interactive", faux_conv_bool2str(kaction_interactive(kaction)), level + 1);
+	attr2ctext(&str, "in", kaction_io_e_enum2str(kaction_in(kaction)), level + 1);
+	attr2ctext(&str, "out", kaction_io_e_enum2str(kaction_out(kaction)), level + 1);
 	// Exec_on
 	switch (kaction_exec_on(kaction)) {
 	case KACTION_COND_FAIL:

+ 25 - 4
klish/ischeme/ientry.c

@@ -134,9 +134,14 @@ bool_t ientry_parse(const ientry_t *info, kentry_t *entry, faux_error_t *error)
 
 	// Filter
 	if (!faux_str_is_empty(info->filter)) {
-		bool_t b = BOOL_FALSE;
-		if (!faux_conv_str2bool(info->filter, &b) ||
-			!kentry_set_filter(entry, b)) {
+		kentry_filter_e filter = KENTRY_FILTER_NONE;
+		if (!faux_str_casecmp(info->filter, "false"))
+			filter = KENTRY_FILTER_FALSE;
+		else if (!faux_str_casecmp(info->filter, "true"))
+			filter = KENTRY_FILTER_TRUE;
+		else if (!faux_str_casecmp(info->filter, "dual"))
+			filter = KENTRY_FILTER_DUAL;
+		if ((KENTRY_FILTER_NONE == filter) || !kentry_set_filter(entry, filter)) {
 			faux_error_add(error, TAG": Illegal 'filter' attribute");
 			retcode = BOOL_FALSE;
 		}
@@ -299,6 +304,7 @@ char *ientry_deploy(const kentry_t *kentry, int level)
 	char *tmp = NULL;
 	char *mode = NULL;
 	char *purpose = NULL;
+	char *filter = NULL;
 	kentry_entrys_node_t *entrys_iter = NULL;
 	kentry_actions_node_t *actions_iter = NULL;
 	kentry_hotkeys_node_t *hotkeys_iter = NULL;
@@ -374,7 +380,22 @@ char *ientry_deploy(const kentry_t *kentry, int level)
 		attr2ctext(&str, "value", kentry_value(kentry), level + 1);
 		attr2ctext(&str, "restore", faux_conv_bool2str(kentry_restore(kentry)), level + 1);
 		attr2ctext(&str, "order", faux_conv_bool2str(kentry_order(kentry)), level + 1);
-		attr2ctext(&str, "filter", faux_conv_bool2str(kentry_filter(kentry)), level + 1);
+
+		// Filter
+		switch (kentry_filter(kentry)) {
+		case KENTRY_FILTER_FALSE:
+			filter = "false";
+			break;
+		case KENTRY_FILTER_TRUE:
+			filter = "true";
+			break;
+		case KENTRY_FILTER_DUAL:
+			filter = "dual";
+			break;
+		default:
+			filter = NULL;
+		}
+		attr2ctext(&str, "filter", filter, level + 1);
 
 		// ENTRY list
 		entrys_iter = kentry_entrys_iter(kentry);

+ 13 - 2
klish/kaction.h

@@ -21,6 +21,14 @@ typedef enum {
 	KACTION_COND_NEVER,
 } kaction_cond_e;
 
+typedef enum {
+	KACTION_IO_NONE,
+	KACTION_IO_FALSE,
+	KACTION_IO_TRUE,
+	KACTION_IO_TTY,
+	KACTION_IO_MAX,
+} kaction_io_e;
+
 
 C_DECL_BEGIN
 
@@ -36,8 +44,11 @@ bool_t kaction_set_lock(kaction_t *action, const char *lock);
 bool_t kaction_interrupt(const kaction_t *action);
 bool_t kaction_set_interrupt(kaction_t *action, bool_t interrupt);
 
-bool_t kaction_interactive(const kaction_t *action);
-bool_t kaction_set_interactive(kaction_t *action, bool_t interactive);
+kaction_io_e kaction_in(const kaction_t *action);
+bool_t kaction_set_in(kaction_t *action, kaction_io_e in);
+
+kaction_io_e kaction_out(const kaction_t *action);
+bool_t kaction_set_out(kaction_t *action, kaction_io_e out);
 
 kaction_cond_e kaction_exec_on(const kaction_t *action);
 bool_t kaction_set_exec_on(kaction_t *action, kaction_cond_e exec_on);

+ 0 - 4
klish/kcontext.h

@@ -74,10 +74,6 @@ FAUX_HIDDEN bool_t kcontext_set_session(kcontext_t *context, ksession_t *session
 bool_t kcontext_done(const kcontext_t *context);
 FAUX_HIDDEN bool_t kcontext_set_done(kcontext_t *context, bool_t done);
 
-// Pseudo Terminal Slave filename
-const char *kcontext_pts_fname(const kcontext_t *context);
-bool_t kcontext_set_pts_fname(kcontext_t *context, const char *pts_fn);
-
 // Wrappers
 kparg_t *kcontext_candidate_parg(const kcontext_t *context);
 const kentry_t *kcontext_candidate_entry(const kcontext_t *context);

+ 11 - 3
klish/kentry.h

@@ -36,6 +36,13 @@ typedef enum {
 	KENTRY_PURPOSE_MAX,
 } kentry_purpose_e;
 
+// Filter flag
+typedef enum {
+	KENTRY_FILTER_NONE, // Illegal
+	KENTRY_FILTER_FALSE, // Entry is not a filter
+	KENTRY_FILTER_TRUE, // Entry is a filter
+	KENTRY_FILTER_DUAL, // Entry can be filter or non-filter
+} kentry_filter_e;
 
 // Number of max occurs
 typedef enum {
@@ -89,8 +96,8 @@ bool_t kentry_set_restore(kentry_t *entry, bool_t restore);
 bool_t kentry_order(const kentry_t *entry);
 bool_t kentry_set_order(kentry_t *entry, bool_t order);
 // Filter
-bool_t kentry_filter(const kentry_t *entry);
-bool_t kentry_set_filter(kentry_t *entry, bool_t filter);
+kentry_filter_e kentry_filter(const kentry_t *entry);
+bool_t kentry_set_filter(kentry_t *entry, kentry_filter_e filter);
 // User data
 void *kentry_udata(const kentry_t *entry);
 bool_t kentry_set_udata(kentry_t *entry, void *data, kentry_udata_free_fn udata_free_fn);
@@ -110,7 +117,8 @@ bool_t kentry_add_actions(kentry_t *entry, kaction_t *action);
 ssize_t kentry_actions_len(const kentry_t *entry);
 kentry_actions_node_t *kentry_actions_iter(const kentry_t *entry);
 kaction_t *kentry_actions_each(kentry_actions_node_t **iter);
-bool_t kentry_interactive(const kentry_t *entry);
+kaction_io_e kentry_in(const kentry_t *entry);
+kaction_io_e kentry_out(const kentry_t *entry);
 
 // HOTKEYs
 faux_list_t *kentry_hotkeys(const kentry_t *entry);

+ 3 - 1
klish/kexec.h

@@ -9,6 +9,7 @@
 #include <faux/list.h>
 #include <faux/buf.h>
 #include <klish/kcontext.h>
+#include <klish/ksession.h>
 
 typedef struct kexec_s kexec_t;
 
@@ -17,7 +18,7 @@ typedef faux_list_node_t kexec_contexts_node_t;
 
 C_DECL_BEGIN
 
-kexec_t *kexec_new(void);
+kexec_t *kexec_new(ksession_t *session, kcontext_type_e type);
 void kexec_free(kexec_t *exec);
 
 // Dry-run
@@ -56,6 +57,7 @@ kcontext_t *kexec_contexts_each(kexec_contexts_node_t **iter);
 
 bool_t kexec_continue_command_execution(kexec_t *exec, pid_t pid, int wstatus);
 bool_t kexec_exec(kexec_t *exec);
+bool_t kexec_need_stdin(const kexec_t *exec);
 bool_t kexec_interactive(const kexec_t *exec);
 bool_t kexec_set_winsize(kexec_t *exec);
 

+ 9 - 5
klish/kscheme/kaction.c

@@ -18,7 +18,8 @@ struct kaction_s {
 	kplugin_t *plugin; // Source of symbol
 	char *lock; // Named lock
 	bool_t interrupt;
-	bool_t interactive;
+	kaction_io_e in;
+	kaction_io_e out;
 	kaction_cond_e exec_on;
 	bool_t update_retcode;
 	tri_t permanent;
@@ -41,9 +42,11 @@ KSET_STR(action, lock);
 KGET_BOOL(action, interrupt);
 KSET_BOOL(action, interrupt);
 
-// Interactive
-KGET_BOOL(action, interactive);
-KSET_BOOL(action, interactive);
+// In/Out
+KGET(action, kaction_io_e, in);
+KSET(action, kaction_io_e, in);
+KGET(action, kaction_io_e, out);
+KSET(action, kaction_io_e, out);
 
 // Exec_on
 KGET(action, kaction_cond_e, exec_on);
@@ -87,7 +90,8 @@ kaction_t *kaction_new(void)
 	action->sym_ref = NULL;
 	action->lock = NULL;
 	action->interrupt = BOOL_FALSE;
-	action->interactive = BOOL_FALSE;
+	action->in = KACTION_IO_FALSE;
+	action->out = KACTION_IO_TRUE;
 	action->exec_on = KACTION_COND_SUCCESS;
 	action->update_retcode = BOOL_TRUE;
 	action->script = NULL;

+ 34 - 9
klish/kscheme/kentry.c

@@ -27,7 +27,7 @@ struct kentry_s {
 	char *value; // Additional info
 	bool_t restore; // Should entry restore its depth while execution
 	bool_t order; // Is entry ordered
-	bool_t filter; // Is entry filter. Filter can't have inline actions.
+	kentry_filter_e filter; // Is entry filter. Filter can't have inline actions.
 	faux_list_t *entrys; // Nested ENTRYs
 	faux_list_t *actions; // Nested ACTIONs
 	faux_list_t *hotkeys; // Hotkeys
@@ -88,8 +88,8 @@ KGET_BOOL(entry, order);
 KSET_BOOL(entry, order);
 
 // Filter
-KGET_BOOL(entry, filter);
-KSET_BOOL(entry, filter);
+KGET(entry, kentry_filter_e, filter);
+KSET(entry, kentry_filter_e, filter);
 
 // Nested ENTRYs list
 KGET(entry, faux_list_t *, entrys);
@@ -143,7 +143,7 @@ kentry_t *kentry_new(const char *name)
 	entry->value = NULL;
 	entry->restore = BOOL_FALSE;
 	entry->order = BOOL_FALSE;
-	entry->filter = BOOL_FALSE;
+	entry->filter = KENTRY_FILTER_FALSE;
 	entry->udata = NULL;
 	entry->udata_free_fn = NULL;
 
@@ -319,18 +319,43 @@ bool_t kentry_set_udata(kentry_t *entry, void *data, kentry_udata_free_fn free_f
 }
 
 
-bool_t kentry_interactive(const kentry_t *entry)
+// Get integral value of "in" field of all ENTRY's actions
+// false < true < tty
+kaction_io_e kentry_in(const kentry_t *entry)
 {
 	kentry_actions_node_t *iter = NULL;
 	kaction_t *action = NULL;
+	kaction_io_e io = KACTION_IO_FALSE;
 
 	if (!entry)
-		return BOOL_FALSE;
+		return io;
+	iter = kentry_actions_iter(entry);
+	while ((action = kentry_actions_each(&iter))) {
+		kaction_io_e cur_io = kaction_in(action);
+		if (cur_io > io)
+			io = cur_io;
+	}
+
+	return io;
+}
+
+
+// Get integral value of "out" field of all ENTRY's actions
+// false < true < tty
+kaction_io_e kentry_out(const kentry_t *entry)
+{
+	kentry_actions_node_t *iter = NULL;
+	kaction_t *action = NULL;
+	kaction_io_e io = KACTION_IO_FALSE;
+
+	if (!entry)
+		return io;
 	iter = kentry_actions_iter(entry);
 	while ((action = kentry_actions_each(&iter))) {
-		if (kaction_interactive(action))
-			return BOOL_TRUE;
+		kaction_io_e cur_io = kaction_out(action);
+		if (cur_io > io)
+			io = cur_io;
 	}
 
-	return BOOL_FALSE;
+	return io;
 }

+ 3 - 2
klish/kscheme/kscheme.c

@@ -263,8 +263,9 @@ bool_t kscheme_prepare_action_list(kscheme_t *scheme, kentry_t *entry,
 		}
 		kaction_set_sym(action, sym);
 		kaction_set_plugin(action, plugin);
-		// Filter can't contain sync symbols.
-		if (kentry_filter(entry) && kaction_is_sync(action)) {
+		// Filter can't contain sync symbols
+		if ((kentry_filter(entry) != KENTRY_FILTER_FALSE) &&
+			kaction_is_sync(action)) {
 			faux_error_sprintf(error, "Filter \"%s\" can't contain "
 				"sync symbol \"%s\"",
 				kentry_name(entry), sym_ref);

+ 0 - 7
klish/ksession/kcontext.c

@@ -32,7 +32,6 @@ struct kcontext_s {
 	int stderr;
 	pid_t pid;
 	bool_t done; // If all actions are done
-	char *pts_fname; // Pseudo Terminal Slave file name
 };
 
 
@@ -93,10 +92,6 @@ FAUX_HIDDEN KSET(context, ksession_t *, session);
 KGET_BOOL(context, done);
 FAUX_HIDDEN KSET_BOOL(context, done);
 
-// PTS file name (Pseudo Terminal Slave)
-KSET_STR(context, pts_fname);
-KGET_STR(context, pts_fname);
-
 
 kcontext_t *kcontext_new(kcontext_type_e type)
 {
@@ -122,7 +117,6 @@ kcontext_t *kcontext_new(kcontext_type_e type)
 	context->pid = -1; // PID of currently executed ACTION
 	context->session = NULL; // Don't free
 	context->done = BOOL_FALSE;
-	context->pts_fname = NULL;
 
 	return context;
 }
@@ -141,7 +135,6 @@ void kcontext_free(kcontext_t *context)
 		close(context->stdout);
 	if (context->stderr != -1)
 		close(context->stderr);
-	faux_str_free(context->pts_fname);
 
 	faux_free(context);
 }

+ 168 - 106
klish/ksession/kexec.c

@@ -30,6 +30,8 @@
 void grabber(int fds[][2]);
 
 struct kexec_s {
+	kcontext_type_e type; // Common ACTIONs or service ACTIONs
+	ksession_t *session;
 	faux_list_t *contexts;
 	bool_t dry_run;
 	int stdin;
@@ -39,6 +41,8 @@ struct kexec_s {
 	faux_buf_t *bufout;
 	faux_buf_t *buferr;
 	kpath_t *saved_path;
+	char *pts_fname; // Pseudoterminal slave file name
+	int pts; // Pseudoterminal slave handler
 };
 
 // Dry-run
@@ -79,16 +83,28 @@ KNESTED_IS_EMPTY(exec, contexts);
 KNESTED_ITER(exec, contexts);
 KNESTED_EACH(exec, kcontext_t *, contexts);
 
+// Pseudoterminal
+FAUX_HIDDEN KGET(exec, int, pts);
+FAUX_HIDDEN KSET(exec, int, pts);
+FAUX_HIDDEN KSET_STR(exec, pts_fname);
+FAUX_HIDDEN KGET_STR(exec, pts_fname);
 
-kexec_t *kexec_new()
+
+kexec_t *kexec_new(ksession_t *session, kcontext_type_e type)
 {
 	kexec_t *exec = NULL;
 
+	assert(session);
+	if (!session)
+		return NULL;
+
 	exec = faux_zmalloc(sizeof(*exec));
 	assert(exec);
 	if (!exec)
 		return NULL;
 
+	exec->type = type;
+	exec->session = session;
 	exec->dry_run = BOOL_FALSE;
 	exec->saved_path = NULL;
 
@@ -106,6 +122,10 @@ kexec_t *kexec_new()
 	exec->bufout = faux_buf_new(0);
 	exec->buferr = faux_buf_new(0);
 
+	// Pseudoterminal
+	exec->pts = -1;
+	exec->pts_fname = NULL;
+
 	return exec;
 }
 
@@ -128,6 +148,8 @@ void kexec_free(kexec_t *exec)
 	faux_buf_free(exec->bufout);
 	faux_buf_free(exec->buferr);
 
+	faux_str_free(exec->pts_fname);
+
 	kpath_free(exec->saved_path);
 
 	free(exec);
@@ -233,39 +255,29 @@ bool_t kexec_add(kexec_t *exec, kcontext_t *context)
 
 bool_t kexec_set_winsize(kexec_t *exec)
 {
-	int fd = -1;
 	size_t width = 0;
 	size_t height = 0;
 	struct winsize ws = {};
 	int res = -1;
-	kcontext_t *context = NULL;
-	ksession_t *session = NULL;
 
 	if (!exec)
 		return BOOL_FALSE;
-	if (!kexec_interactive(exec))
+	if (exec->pts < 0)
 		return BOOL_FALSE;
-	fd = kexec_stdin(exec);
-	if (fd < 0)
+	if (!isatty(exec->pts))
 		return BOOL_FALSE;
-	if (!isatty(fd))
-		return BOOL_FALSE;
-	context = (kcontext_t *)faux_list_data(faux_list_head(exec->contexts));
-	if (!context)
-		return BOOL_FALSE;
-	session = kcontext_session(context);
-	if (!session)
+	if (!exec->session)
 		return BOOL_FALSE;
 
 	// Set pseudo terminal window size
-	width = ksession_term_width(session);
-	height = ksession_term_height(session);
+	width = ksession_term_width(exec->session);
+	height = ksession_term_height(exec->session);
 	if ((width == 0) || (height == 0))
 		return BOOL_FALSE;
 
 	ws.ws_col = (unsigned short)width;
 	ws.ws_row = (unsigned short)height;
-	res = ioctl(fd, TIOCSWINSZ, &ws);
+	res = ioctl(exec->pts, TIOCSWINSZ, &ws);
 	if (res < 0)
 		return BOOL_FALSE;
 
@@ -279,6 +291,16 @@ static bool_t kexec_prepare(kexec_t *exec)
 	faux_list_node_t *iter = NULL;
 	int global_stderr = -1;
 	int fflags = 0;
+	int r_end = -1;
+	int w_end = -1;
+	// Pseudoterminal related vars
+	bool_t isatty_stdin = BOOL_FALSE;
+	bool_t isatty_stdout = BOOL_FALSE;
+	bool_t isatty_stderr = BOOL_FALSE;
+	int pts = -1;
+	int ptm = -1;
+	char *pts_name = NULL;
+
 
 	assert(exec);
 	if (!exec)
@@ -287,15 +309,19 @@ static bool_t kexec_prepare(kexec_t *exec)
 	if (kexec_contexts_is_empty(exec))
 		return BOOL_FALSE;
 
-	// If command is interactive then prepare pseudoterminal. Note
-	// interactive commands can't have filters
-	if (kexec_interactive(exec)) {
-		int pts = -1;
-		int ptm = -1;
-		char *pts_name = NULL;
-		kcontext_t *context = (kcontext_t *)faux_list_data(
-			faux_list_head(exec->contexts));
-
+	// If user has a terminal somewhere (stdin, stdout, stderr) then prepare
+	// pseudoterminal. Service actions (internal actions like PTYPE checks)
+	// never get terminal
+	if (exec->type == KCONTEXT_TYPE_ACTION) {
+		isatty_stdin = ksession_isatty_stdin(exec->session);
+		// Only if last command in pipeline is interactive then stdout
+		// can be pts. Because client adds its own pager to pipeline in
+		// a case of non-interactive commands
+		if (kexec_interactive(exec))
+			isatty_stdout = ksession_isatty_stdout(exec->session);
+		isatty_stderr = ksession_isatty_stderr(exec->session);
+	}
+	if (isatty_stdin || isatty_stdout || isatty_stderr) {
 		ptm = open(PTMX_PATH, O_RDWR, O_NOCTTY);
 		if (ptm < 0)
 			return BOOL_FALSE;
@@ -306,58 +332,74 @@ static bool_t kexec_prepare(kexec_t *exec)
 		grantpt(ptm);
 		unlockpt(ptm);
 		pts_name = ptsname(ptm);
+		// In a case of pseudo-terminal the pts
+		// must be reopened later in the child after setsid(). So
+		// save filename of pts
+		kexec_set_pts_fname(exec, pts_name);
 		// Open client side (pts) of pseudo terminal. It's necessary for
 		// sync action execution. Additionally open descriptor makes
 		// action (from child) to don't send SIGHUP on terminal handler.
 		pts = open(pts_name, O_RDWR, O_NOCTTY);
 		if (pts < 0)
 			return BOOL_FALSE;
-
-		kexec_set_stdin(exec, ptm);
-		kexec_set_stdout(exec, ptm);
-		kexec_set_stderr(exec, ptm);
-		kcontext_set_stdin(context, pts);
-		kcontext_set_stdout(context, pts);
-		kcontext_set_stderr(context, pts);
-		// In a case of pseudo-terminal the pts
-		// must be reopened later in the child after setsid(). So just
-		// save filename of pts.
-		kcontext_set_pts_fname(context, pts_name);
+		kexec_set_pts(exec, pts);
 		// Set pseudo terminal window size
 		kexec_set_winsize(exec);
-
-		return BOOL_TRUE;
 	}
 
-	// Commands without pseudoterminal
 	// Create "global" stdin, stdout, stderr for the whole job execution.
 
 	// STDIN
-	if (pipe(pipefd) < 0)
-		return BOOL_FALSE;
-	kcontext_set_stdin(faux_list_data(faux_list_head(exec->contexts)),
-		pipefd[0]); // Read end
-	kexec_set_stdin(exec, pipefd[1]); // Write end
+	if (isatty_stdin) {
+		r_end = pts;
+		w_end = ptm;
+	} else {
+		if (pipe(pipefd) < 0)
+			return BOOL_FALSE;
+		r_end = pipefd[0];
+		w_end = pipefd[1];
+	}
+	kcontext_set_stdin(faux_list_data(
+		faux_list_head(exec->contexts)), r_end); // Read end
+	kexec_set_stdin(exec, w_end); // Write end
 
 	// STDOUT
-	if (pipe(pipefd) < 0)
-		return BOOL_FALSE;
-	// Read end of 'stdout' pipe must be non-blocked
-	fflags = fcntl(pipefd[0], F_GETFL);
-	fcntl(pipefd[0], F_SETFL, fflags | O_NONBLOCK);
-	kexec_set_stdout(exec, pipefd[0]); // Read end
-	kcontext_set_stdout(faux_list_data(faux_list_tail(exec->contexts)),
-		pipefd[1]); // Write end
+	if (isatty_stdout) {
+		r_end = ptm;
+		w_end = pts;
+	} else {
+		if (pipe(pipefd) < 0)
+			return BOOL_FALSE;
+		// Read end of 'stdout' pipe must be non-blocked
+		fflags = fcntl(pipefd[0], F_GETFL);
+		fcntl(pipefd[0], F_SETFL, fflags | O_NONBLOCK);
+		r_end = pipefd[0];
+		w_end = pipefd[1];
+	}
+	kexec_set_stdout(exec, r_end); // Read end
+	kcontext_set_stdout(
+		faux_list_data(faux_list_tail(exec->contexts)), w_end); // Write end
 
 	// STDERR
-	if (pipe(pipefd) < 0)
-		return BOOL_FALSE;
-	// Read end of 'stderr' pipe must be non-blocked
-	fflags = fcntl(pipefd[0], F_GETFL);
-	fcntl(pipefd[0], F_SETFL, fflags | O_NONBLOCK);
-	kexec_set_stderr(exec, pipefd[0]); // Read end
+	if (isatty_stderr) {
+		r_end = ptm;
+		w_end = pts;
+	} else {
+		if (pipe(pipefd) < 0)
+			return BOOL_FALSE;
+		// Read end of 'stderr' pipe must be non-blocked
+		fflags = fcntl(pipefd[0], F_GETFL);
+		fcntl(pipefd[0], F_SETFL, fflags | O_NONBLOCK);
+		r_end = pipefd[0];
+		w_end = pipefd[1];
+	}
+	kexec_set_stderr(exec, r_end); // Read end
 	// STDERR write end will be set to all list members as stderr
-	global_stderr = pipefd[1]; // Write end
+	global_stderr = w_end; // Write end
+
+	// Save current path
+	if (ksession_path(exec->session))
+		exec->saved_path = kpath_clone(ksession_path(exec->session));
 
 	// Iterate all context_t elements to fill all stdin, stdout, stderr
 	for (iter = faux_list_head(exec->contexts); iter;
@@ -365,16 +407,6 @@ static bool_t kexec_prepare(kexec_t *exec)
 		faux_list_node_t *next = faux_list_next_node(iter);
 		kcontext_t *context = (kcontext_t *)faux_list_data(iter);
 
-		// The first context is a context of main command. The other
-		// contexts are contexts of filters. So save current path from
-		// first context.
-		if (iter == faux_list_head(exec->contexts)) {
-			ksession_t *session = kcontext_session(context);
-			if (session && ksession_path(session))
-				exec->saved_path = kpath_clone(
-					ksession_path(session));
-		}
-
 		// Set the same STDERR to all contexts
 		kcontext_set_stderr(context, global_stderr);
 
@@ -398,8 +430,8 @@ static bool_t kexec_prepare(kexec_t *exec)
 // service process will be forked. It gets output and stores it to the
 // internal buffer. After sym function return grabber will write
 // buffered data back. So grabber will simulate async sym execution.
-static bool_t exec_action_sync(kcontext_t *context, const kaction_t *action,
-	pid_t *pid, int *retcode)
+static bool_t exec_action_sync(const kexec_t *exec, kcontext_t *context,
+	const kaction_t *action, pid_t *pid, int *retcode)
 {
 	ksym_fn fn = NULL;
 	int exitcode = 0;
@@ -489,6 +521,8 @@ static bool_t exec_action_sync(kcontext_t *context, const kaction_t *action,
 
 	_exit(0);
 
+	exec = exec; // Happy compiler
+
 	return BOOL_TRUE;
 }
 
@@ -498,15 +532,14 @@ static bool_t exec_action_sync(kcontext_t *context, const kaction_t *action,
 // The parent will save forked process's pid and immediately return
 // control to event loop which will get forked process stdout and
 // wait for process termination.
-static bool_t exec_action_async(kcontext_t *context, const kaction_t *action,
-	pid_t *pid)
+static bool_t exec_action_async(const kexec_t *exec, kcontext_t *context,
+	const kaction_t *action, pid_t *pid)
 {
 	ksym_fn fn = NULL;
 	int exitcode = 0;
 	pid_t child_pid = -1;
 	int i = 0;
 	int fdmax = 0;
-	const char *pts_fname = NULL;
 	sigset_t sigs = {};
 
 	fn = ksym_function(kaction_sym(action));
@@ -538,21 +571,26 @@ static bool_t exec_action_async(kcontext_t *context, const kaction_t *action,
 	sigemptyset(&sigs);
 	sigprocmask(SIG_SETMASK, &sigs, NULL);
 
-	if ((pts_fname = kcontext_pts_fname(context)) != NULL) {
+	// Reopen streams if the pseudoterminal is used.
+	// It's necessary to set session terminal
+	if (exec->pts_fname != NULL) {
 		int fd = -1;
 		setsid();
-		fd = open(pts_fname, O_RDWR, 0);
+		fd = open(exec->pts_fname, O_RDWR, 0);
 		if (fd < 0)
 			_exit(-1);
-		dup2(fd, STDIN_FILENO);
-		dup2(fd, STDOUT_FILENO);
-		dup2(fd, STDERR_FILENO);
-	} else {
-		dup2(kcontext_stdin(context), STDIN_FILENO);
-		dup2(kcontext_stdout(context), STDOUT_FILENO);
-		dup2(kcontext_stderr(context), STDERR_FILENO);
+		if (isatty(kcontext_stdin(context)))
+			kcontext_set_stdin(context, fd);
+		if (isatty(kcontext_stdout(context)))
+			kcontext_set_stdout(context, fd);
+		if (isatty(kcontext_stderr(context)))
+			kcontext_set_stderr(context, fd);
 	}
 
+	dup2(kcontext_stdin(context), STDIN_FILENO);
+	dup2(kcontext_stdout(context), STDOUT_FILENO);
+	dup2(kcontext_stderr(context), STDERR_FILENO);
+
 	// Close all inherited fds except stdin, stdout, stderr
 	fdmax = (int)sysconf(_SC_OPEN_MAX);
 	for (i = (STDERR_FILENO + 1); i < fdmax; i++)
@@ -572,8 +610,8 @@ static bool_t exec_action_async(kcontext_t *context, const kaction_t *action,
 }
 
 
-static bool_t exec_action(kcontext_t *context, const kaction_t *action,
-	pid_t *pid, int *retcode)
+static bool_t exec_action(const kexec_t *exec, kcontext_t *context,
+	const kaction_t *action, pid_t *pid, int *retcode)
 {
 	assert(context);
 	if (!context)
@@ -583,9 +621,9 @@ static bool_t exec_action(kcontext_t *context, const kaction_t *action,
 		return BOOL_FALSE;
 
 	if (kaction_is_sync(action))
-		return exec_action_sync(context, action, pid, retcode);
+		return exec_action_sync(exec, context, action, pid, retcode);
 
-	return exec_action_async(context, action, pid);
+	return exec_action_async(exec, context, action, pid);
 }
 
 
@@ -665,7 +703,7 @@ static bool_t exec_action_sequence(const kexec_t *exec, kcontext_t *context,
 			exitstatus = 0; // Exit status while dry-run is always 0
 		 } else { // Normal execution
 			is_sync = kaction_is_sync(action);
-			exec_action(context, action, &new_pid, &exitstatus);
+			exec_action(exec, context, action, &new_pid, &exitstatus);
 		}
 
 		// SYNC: Compute new value for retcode.
@@ -743,31 +781,55 @@ bool_t kexec_exec(kexec_t *exec)
 }
 
 
+// If some kexec's kentry has tty as "out" then consider kexec as interactive
 bool_t kexec_interactive(const kexec_t *exec)
 {
-	faux_list_node_t *node = NULL;
+	faux_list_node_t *iter = NULL;
 	kcontext_t *context = NULL;
-	const kentry_t *entry = NULL;
-	const ksession_t *session = NULL;
 
 	assert(exec);
 	if (!exec)
 		return BOOL_FALSE;
 
-	// Only the ACTION of first context can be interactive
-	node = faux_list_head(exec->contexts);
-	if (!node)
-		return BOOL_FALSE;
-	context = (kcontext_t *)faux_list_data(node);
-	if (!context)
-		return BOOL_FALSE;
-	// If client has no input tty then consider command as non-interactive
-	session = kcontext_session(context);
-	if (session && !ksession_isatty_stdin(session))
-		return BOOL_FALSE;
-	entry = kcontext_command(context);
-	if (!entry)
+	iter = kexec_contexts_iter(exec);
+	while ((context = kexec_contexts_each(&iter))) {
+		const kentry_t *entry = kcontext_command(context);
+		if (!entry)
+			return BOOL_FALSE;
+		if (kentry_out(entry) == KACTION_IO_TTY)
+			return BOOL_TRUE;
+	}
+
+	return BOOL_FALSE;
+}
+
+
+// If some kexec's kentry has tty as "in" then consider kexec as need_stdin.
+// The first kentry with "in=true" also does kexec need_stdin
+bool_t kexec_need_stdin(const kexec_t *exec)
+{
+	faux_list_node_t *iter = NULL;
+	size_t num = 0;
+	kcontext_t *context = NULL;
+
+	assert(exec);
+	if (!exec)
 		return BOOL_FALSE;
 
-	return kentry_interactive(entry);
+	iter = kexec_contexts_iter(exec);
+	while ((context = kexec_contexts_each(&iter))) {
+		const kentry_t *entry = kcontext_command(context);
+		if (!entry)
+			return BOOL_FALSE;
+		// Check first command within pipeline
+		if (num == 0) {
+			if (kentry_in(entry) == KACTION_IO_TRUE)
+				return BOOL_TRUE;
+		}
+		num++;
+		if (kentry_in(entry) == KACTION_IO_TTY)
+			return BOOL_TRUE;
+	}
+
+	return BOOL_FALSE;
 }

+ 14 - 22
klish/ksession/ksession_parse.c

@@ -107,6 +107,7 @@ static kpargv_status_e ksession_parse_arg(ksession_t *session,
 	} else if (!kentry_container(entry)) {
 		const char *current_arg = NULL;
 		kparg_t *parg = NULL;
+		kentry_filter_e filter_flag = kentry_filter(entry);
 
 		// When purpose is COMPLETION or HELP then fill completion list.
 		// Additionally if it's last continuable argument then lie to
@@ -116,7 +117,9 @@ static kpargv_status_e ksession_parse_arg(ksession_t *session,
 		// filters or non-filters
 		if (((KPURPOSE_COMPLETION == purpose) ||
 			(KPURPOSE_HELP == purpose)) &&
-			((is_filter == kentry_filter(entry)) ||
+			((filter_flag == KENTRY_FILTER_DUAL) ||
+			(is_filter && (filter_flag == KENTRY_FILTER_TRUE)) ||
+			(!is_filter && (filter_flag == KENTRY_FILTER_FALSE)) ||
 			(is_filter && kpargv_pargs_len(pargv)))) {
 			if (!*argv_iter) {
 				// That's time to add entry to completions list.
@@ -483,27 +486,19 @@ static bool_t ksession_check_line(const kpargv_t *pargv, faux_error_t *error,
 	// First component
 	if (is_first) {
 
-		// First component can't be filter
-		if (kentry_filter(cmd)) {
+		// First component can't be a filter
+		if (kentry_filter(cmd) == KENTRY_FILTER_TRUE) {
 			faux_error_sprintf(error, "The filter \"%s\" "
 				"can't be used without previous pipeline",
 				kentry_name(cmd));
 			return BOOL_FALSE;
 		}
 
-		// Interactive command can't have filters
-		if (kentry_interactive(cmd) && is_piped) {
-			faux_error_sprintf(error, "The interactive command \"%s\" "
-				"can't have filters",
-				kentry_name(cmd));
-			return BOOL_FALSE;
-		}
-
 	// Components after pipe "|"
 	} else {
 
 		// Only the first component can be non-filter
-		if (!kentry_filter(cmd)) {
+		if (kentry_filter(cmd) == KENTRY_FILTER_FALSE) {
 			faux_error_sprintf(error, "The non-filter command \"%s\" "
 				"can't be destination of pipe",
 				kentry_name(cmd));
@@ -518,16 +513,10 @@ static bool_t ksession_check_line(const kpargv_t *pargv, faux_error_t *error,
 			return BOOL_FALSE;
 		}
 
-		// Only the first component can have 'interactive=true' attribute
-		if (kentry_interactive(cmd)) {
-			faux_error_sprintf(error, "The filter \"%s\" "
-				"can't be interactive",
-				kentry_name(cmd));
-			return BOOL_FALSE;
-		}
-
 	}
 
+	is_piped = is_piped; // Happy compiler
+
 	return BOOL_TRUE;
 }
 
@@ -608,7 +597,7 @@ kexec_t *ksession_parse_for_exec(ksession_t *session, const char *raw_line,
 	is_piped = (faux_list_len(split) > 1);
 
 	// Create exec list
-	exec = kexec_new();
+	exec = kexec_new(session, KCONTEXT_TYPE_ACTION);
 	assert(exec);
 	if (!exec) {
 		faux_list_free(split);
@@ -660,11 +649,14 @@ kexec_t *ksession_parse_for_local_exec(ksession_t *session,
 	kpargv_status_e pstatus = KPARSE_NONE;
 	const char *line = NULL; // TODO: Must be 'line' field of ENTRY
 
+	assert(session);
+	if (!session)
+		return NULL;
 	assert(entry);
 	if (!entry)
 		return NULL;
 
-	exec = kexec_new();
+	exec = kexec_new(session, KCONTEXT_TYPE_SERVICE_ACTION);
 	assert(exec);
 
 	argv = faux_argv_new();

+ 7 - 5
klish/ktp.h

@@ -48,20 +48,22 @@ typedef enum {
 	KTP_STATUS_NONE =		(uint32_t)0x00000000,
 	KTP_STATUS_ERROR =		(uint32_t)0x00000001,
 	KTP_STATUS_INCOMPLETED =	(uint32_t)0x00000002,
-	KTP_STATUS_INTERACTIVE =	(uint32_t)0x00000100,
-	KTP_STATUS_TTY_STDIN =		(uint32_t)0x00000200,
-	KTP_STATUS_TTY_STDOUT =		(uint32_t)0x00000400,
-	KTP_STATUS_TTY_STDERR =		(uint32_t)0x00000800,
+	KTP_STATUS_TTY_STDIN =		(uint32_t)0x00000100, // Client's stdin is tty
+	KTP_STATUS_TTY_STDOUT =		(uint32_t)0x00000200, // Client's stdout is tty
+	KTP_STATUS_TTY_STDERR =		(uint32_t)0x00000400, // Client's stderr is tty
+	KTP_STATUS_NEED_STDIN =		(uint32_t)0x00001000, // Server's cmd need stdin
+	KTP_STATUS_INTERACTIVE =	(uint32_t)0x00002000, // Server's stdout is for tty
 	KTP_STATUS_DRY_RUN =		(uint32_t)0x00010000,
 	KTP_STATUS_EXIT =		(uint32_t)0x80000000,
 } ktp_status_e;
 
 #define KTP_STATUS_IS_ERROR(status) (status & KTP_STATUS_ERROR)
 #define KTP_STATUS_IS_INCOMPLETED(status) (status & KTP_STATUS_INCOMPLETED)
-#define KTP_STATUS_IS_INTERACTIVE(status) (status & KTP_STATUS_INTERACTIVE)
 #define KTP_STATUS_IS_TTY_STDIN(status) (status & KTP_STATUS_TTY_STDIN)
 #define KTP_STATUS_IS_TTY_STDOUT(status) (status & KTP_STATUS_TTY_STDOUT)
 #define KTP_STATUS_IS_TTY_STDERR(status) (status & KTP_STATUS_TTY_STDERR)
+#define KTP_STATUS_IS_NEED_STDIN(status) (status & KTP_STATUS_NEED_STDIN)
+#define KTP_STATUS_IS_INTERACTIVE(status) (status & KTP_STATUS_INTERACTIVE)
 #define KTP_STATUS_IS_DRY_RUN(status) (status & KTP_STATUS_DRY_RUN)
 #define KTP_STATUS_IS_EXIT(status) (status & KTP_STATUS_EXIT)
 

+ 54 - 1
klish/ktp/ktp_session.c

@@ -35,6 +35,9 @@ struct ktp_session_s {
 	ktp_status_e cmd_features;
 	bool_t cmd_features_available;
 	bool_t stop_on_answer; // Stop the loop when answer is received (for non-interactive mode)
+	bool_t stdout_need_newline; // Does stdout has final line feed. If no then newline is needed
+	bool_t stderr_need_newline; // Does stderr has final line feed. If no then newline is needed
+	int last_stream; // Last active stream: stdout or stderr
 };
 
 
@@ -70,6 +73,9 @@ ktp_session_t *ktp_session_new(int sock, faux_eloop_t *eloop)
 	ktp->request_done = BOOL_FALSE;
 	ktp->cmd_features = KTP_STATUS_NONE;
 	ktp->cmd_features_available = BOOL_FALSE;
+	ktp->stdout_need_newline = BOOL_FALSE;
+	ktp->stderr_need_newline = BOOL_FALSE;
+	ktp->last_stream = STDOUT_FILENO;
 
 	// Async object
 	ktp->async = faux_async_new(sock);
@@ -299,6 +305,14 @@ static bool_t ktp_session_process_stdout(ktp_session_t *ktp, const faux_msg_t *m
 	if (!faux_msg_get_param_by_type(msg, KTP_PARAM_LINE, (void **)&line, &len))
 		return BOOL_TRUE; // It's strange but not a bug
 
+	if (len > 0) {
+		if (line[len - 1] == '\n')
+			ktp->stdout_need_newline = BOOL_FALSE;
+		else
+			ktp->stdout_need_newline = BOOL_TRUE;
+		ktp->last_stream = STDOUT_FILENO;
+	}
+
 	return ((ktp_session_stdout_cb_fn)ktp->cb[KTP_SESSION_CB_STDOUT].fn)(
 		ktp, line, len, ktp->cb[KTP_SESSION_CB_STDOUT].udata);
 }
@@ -319,6 +333,14 @@ static bool_t ktp_session_process_stderr(ktp_session_t *ktp, const faux_msg_t *m
 			(void **)&line, &len))
 		return BOOL_TRUE; // It's strange but not a bug
 
+	if (len > 0) {
+		if (line[len - 1] == '\n')
+			ktp->stderr_need_newline = BOOL_FALSE;
+		else
+			ktp->stderr_need_newline = BOOL_TRUE;
+		ktp->last_stream = STDERR_FILENO;
+	}
+
 	return ((ktp_session_stdout_cb_fn)ktp->cb[KTP_SESSION_CB_STDERR].fn)(
 		ktp, line, len, ktp->cb[KTP_SESSION_CB_STDERR].udata);
 }
@@ -377,7 +399,8 @@ static bool_t ktp_session_process_cmd_ack(ktp_session_t *ktp, const faux_msg_t *
 		// Only first 'incompleted' cmd ack sets cmd features
 		if (!ktp->cmd_features_available) {
 			ktp->cmd_features_available = BOOL_TRUE;
-			ktp->cmd_features = status & KTP_STATUS_INTERACTIVE;
+			ktp->cmd_features = status &
+				(KTP_STATUS_INTERACTIVE | KTP_STATUS_NEED_STDIN);
 		}
 		// Execute external callback
 		if (ktp->cb[KTP_SESSION_CB_CMD_ACK_INCOMPLETED].fn)
@@ -617,6 +640,9 @@ static bool_t ktp_session_drop_state(ktp_session_t *ktp, faux_error_t *error)
 	ktp->request_done = BOOL_FALSE;
 	ktp->cmd_features = KTP_STATUS_NONE;
 	ktp->cmd_features_available = BOOL_FALSE;
+	ktp->stdout_need_newline = BOOL_FALSE;
+	ktp->stderr_need_newline = BOOL_FALSE;
+	ktp->last_stream = STDOUT_FILENO;
 
 	return BOOL_TRUE;
 }
@@ -746,3 +772,30 @@ bool_t ktp_session_retcode(ktp_session_t *ktp, int *retcode)
 
 	return ktp->cmd_retcode_available; // Sign of server answer
 }
+
+
+bool_t ktp_session_stdout_need_newline(ktp_session_t *ktp)
+{
+	if (!ktp)
+		return BOOL_FALSE;
+
+	return ktp->stdout_need_newline;
+}
+
+
+bool_t ktp_session_stderr_need_newline(ktp_session_t *ktp)
+{
+	if (!ktp)
+		return BOOL_FALSE;
+
+	return ktp->stderr_need_newline;
+}
+
+
+int ktp_session_last_stream(ktp_session_t *ktp)
+{
+	if (!ktp)
+		return BOOL_FALSE;
+
+	return ktp->last_stream;
+}

+ 2 - 2
klish/ktp/ktpd_session.c

@@ -368,6 +368,8 @@ static bool_t ktpd_session_process_cmd(ktpd_session_t *ktpd, faux_msg_t *msg)
 		ktp_status_e status = KTP_STATUS_INCOMPLETED;
 		if (kexec_interactive(ktpd->exec))
 			status |= KTP_STATUS_INTERACTIVE;
+		if (kexec_need_stdin(ktpd->exec))
+			status |= KTP_STATUS_NEED_STDIN;
 		ack = ktp_msg_preform(cmd, status);
 		faux_msg_send_async(ack, ktpd->async);
 		faux_msg_free(ack);
@@ -945,8 +947,6 @@ static bool_t ktpd_session_process_stdin(ktpd_session_t *ktpd, faux_msg_t *msg)
 
 	if (!ktpd->exec)
 		return BOOL_FALSE;
-	if (!kexec_interactive(ktpd->exec))
-		return BOOL_FALSE;
 	fd = kexec_stdin(ktpd->exec);
 	if (fd < 0)
 		return BOOL_FALSE;

+ 3 - 0
klish/ktp_session.h

@@ -98,6 +98,9 @@ bool_t ktp_session_stdin(ktp_session_t *ktp, const char *line, size_t line_len);
 bool_t ktp_session_stdout_close(ktp_session_t *ktp);
 bool_t ktp_session_retcode(ktp_session_t *ktp, int *retcode);
 ktp_status_e ktp_session_cmd_features(const ktp_session_t *ktp);
+bool_t ktp_session_stdout_need_newline(ktp_session_t *ktp);
+bool_t ktp_session_stderr_need_newline(ktp_session_t *ktp);
+int ktp_session_last_stream(ktp_session_t *ktp);
 
 
 // Server KTP session

+ 16 - 6
klish/xml-helper/load.c

@@ -376,7 +376,8 @@ static bool_t process_action(const kxml_node_t *element, void *parent,
 	iaction.sym = kxml_node_attr(element, "sym");
 	iaction.lock = kxml_node_attr(element, "lock");
 	iaction.interrupt = kxml_node_attr(element, "interrupt");
-	iaction.interactive = kxml_node_attr(element, "interactive");
+	iaction.in = kxml_node_attr(element, "in");
+	iaction.out = kxml_node_attr(element, "out");
 	iaction.exec_on = kxml_node_attr(element, "exec_on");
 	iaction.update_retcode = kxml_node_attr(element, "update_retcode");
 	iaction.permanent = kxml_node_attr(element, "permanent");
@@ -420,7 +421,8 @@ err:
 	kxml_node_attr_free(iaction.sym);
 	kxml_node_attr_free(iaction.lock);
 	kxml_node_attr_free(iaction.interrupt);
-	kxml_node_attr_free(iaction.interactive);
+	kxml_node_attr_free(iaction.in);
+	kxml_node_attr_free(iaction.out);
 	kxml_node_attr_free(iaction.exec_on);
 	kxml_node_attr_free(iaction.update_retcode);
 	kxml_node_attr_free(iaction.permanent);
@@ -798,6 +800,7 @@ static bool_t process_command(const kxml_node_t *element, void *parent,
 	bool_t res = BOOL_FALSE;
 	ktags_e tag = kxml_node_tag(element);
 	bool_t is_name = BOOL_FALSE;
+	bool_t is_filter = BOOL_FALSE;
 	kentry_entrys_node_t *iter = NULL;
 	kentry_t *nested_entry = NULL;
 	bool_t ptype_exists = BOOL_FALSE;
@@ -862,10 +865,15 @@ static bool_t process_command(const kxml_node_t *element, void *parent,
 	}
 	ientry.order = "false";
 	// Filter
-	if (KTAG_FILTER == tag)
-		ientry.filter = "true";
-	else
-		ientry.filter = "false";
+	ientry.filter = kxml_node_attr(element, "filter");
+	if (ientry.filter) {
+		is_filter = BOOL_TRUE;
+	} else {
+		if (KTAG_FILTER == tag)
+			ientry.filter = "true";
+		else
+			ientry.filter = "false";
+	}
 
 	if (!(entry = add_entry_to_hierarchy(element, parent, &ientry, error)))
 		goto err;
@@ -907,6 +915,8 @@ err:
 		kxml_node_attr_free(ientry.value);
 		kxml_node_attr_free(ientry.restore);
 	}
+	if (is_filter)
+		kxml_node_attr_free(ientry.filter);
 
 	return res;
 }

+ 4 - 2
tinyrl/tinyrl.h

@@ -89,8 +89,10 @@ void tinyrl_line_to_hist(tinyrl_t *tinyrl);
 void tinyrl_reset_hist_pos(tinyrl_t *tinyrl);
 void *tinyrl_udata(const tinyrl_t *tinyrl);
 void tinyrl_set_udata(tinyrl_t *tinyrl, void *udata);
-void tty_raw_mode(tinyrl_t *tinyrl);
-void tty_restore_mode(tinyrl_t *tinyrl);
+void tinyrl_raw_mode(tinyrl_t *tinyrl);
+void tinyrl_restore_mode(tinyrl_t *tinyrl);
+void tinyrl_enable_isig(tinyrl_t *tinyrl);
+void tinyrl_disable_isig(tinyrl_t *tinyrl);
 int tinyrl_read(tinyrl_t *tinyrl);
 void tinyrl_redisplay(tinyrl_t *tinyrl);
 void tinyrl_save_last(tinyrl_t *tinyrl);

+ 42 - 5
tinyrl/tinyrl/tinyrl.c

@@ -77,7 +77,7 @@ tinyrl_t *tinyrl_new(FILE *istream, FILE *ostream,
 	tinyrl->hist = hist_new(hist_fname, hist_stifle);
 	tinyrl_hist_restore(tinyrl);
 
-	tty_raw_mode(tinyrl);
+	tinyrl_raw_mode(tinyrl);
 
 	return tinyrl;
 }
@@ -89,7 +89,7 @@ void tinyrl_free(tinyrl_t *tinyrl)
 	if (!tinyrl)
 		return;
 
-	tty_restore_mode(tinyrl);
+	tinyrl_restore_mode(tinyrl);
 
 	tinyrl_hist_save(tinyrl);
 	hist_free(tinyrl->hist);
@@ -103,7 +103,7 @@ void tinyrl_free(tinyrl_t *tinyrl)
 }
 
 
-void tty_raw_mode(tinyrl_t *tinyrl)
+void tinyrl_raw_mode(tinyrl_t *tinyrl)
 {
 	struct termios new_termios = {};
 	FILE *istream = NULL;
@@ -119,7 +119,6 @@ void tty_raw_mode(tinyrl_t *tinyrl)
 		return;
 	new_termios.c_iflag = 0;
 	new_termios.c_oflag = OPOST | ONLCR;
-//	new_termios.c_oflag = ONLCR;
 	new_termios.c_lflag = 0;
 
 //	new_termios.c_cflag = CS8 | CREAD;
@@ -137,7 +136,7 @@ void tty_raw_mode(tinyrl_t *tinyrl)
 }
 
 
-void tty_restore_mode(tinyrl_t *tinyrl)
+void tinyrl_restore_mode(tinyrl_t *tinyrl)
 {
 	FILE *istream = NULL;
 	int fd = -1;
@@ -151,6 +150,44 @@ void tty_restore_mode(tinyrl_t *tinyrl)
 }
 
 
+void tinyrl_enable_isig(tinyrl_t *tinyrl)
+{
+	struct termios new_termios = {};
+	FILE *istream = NULL;
+	int fd = -1;
+
+	if (!tinyrl)
+		return;
+	istream = vt100_istream(tinyrl->term);
+	if (!istream)
+		return;
+	fd = fileno(istream);
+	if (tcgetattr(fd, &new_termios) < 0)
+		return;
+	new_termios.c_lflag |= (ISIG | NOFLSH);
+	tcsetattr(fd, TCSADRAIN, &new_termios);
+}
+
+
+void tinyrl_disable_isig(tinyrl_t *tinyrl)
+{
+	struct termios new_termios = {};
+	FILE *istream = NULL;
+	int fd = -1;
+
+	if (!tinyrl)
+		return;
+	istream = vt100_istream(tinyrl->term);
+	if (!istream)
+		return;
+	fd = fileno(istream);
+	if (tcgetattr(fd, &new_termios) < 0)
+		return;
+	new_termios.c_lflag &= ~(ISIG | NOFLSH);
+	tcsetattr(fd, TCSADRAIN, &new_termios);
+}
+
+
 bool_t tinyrl_bind_key(tinyrl_t *tinyrl, int key, tinyrl_key_func_t *fn)
 {
 	assert(tinyrl);