diff mbox

[OpenWrt-Devel,procd] service: use PTY (instead of a pipe) for reading service output

Message ID 1434713675-13013-1-git-send-email-zajec5@gmail.com
State Changes Requested
Headers show

Commit Message

Rafał Miłecki June 19, 2015, 11:34 a.m. UTC
Using pipe automatically switches service to block buffering which kind
of breaks our logging. We won't get anything from FD until the buffer
gets filled fully or the service exits. This makes log messages appear
with an unwanted delay.
Switching to PTY fixes this issue. Service prints to the "standard"
stdout (no piping) but the messages go (line buffered) to the master FD
anyway.
It was successfully tested with all 4 variants of "stdout" and "stderr"
ubus arguments.

Signed-off-by: Rafał Miłecki <zajec5@gmail.com>
---
 CMakeLists.txt     |  2 +-
 service/instance.c | 35 +++++++++++++++++++++++++----------
 2 files changed, 26 insertions(+), 11 deletions(-)

Comments

Yousong Zhou June 19, 2015, 12:44 p.m. UTC | #1
On 19 June 2015 at 19:34, Rafał Miłecki <zajec5@gmail.com> wrote:
> Using pipe automatically switches service to block buffering which kind
> of breaks our logging. We won't get anything from FD until the buffer
> gets filled fully or the service exits. This makes log messages appear
> with an unwanted delay.
> Switching to PTY fixes this issue. Service prints to the "standard"
> stdout (no piping) but the messages go (line buffered) to the master FD
> anyway.
> It was successfully tested with all 4 variants of "stdout" and "stderr"
> ubus arguments.
>

I do not like this because

 - Conventionally, services will drop their inherited controlling
terminal for preparation of becoming a daemon.  Adding back that
controlling terminal by login_tty() for alleviating possible delay
when logging stdio sounds not right.
 - I am biased... in that the ossherd [1] package depends on the fact
processes started by procd do not have controlling terminals and this
will unattended password login with ssh  :)

Anyway, the implementation is not complete.

 - epipe[] is still there.
 - aslave needs to be closed when login_pty() failed
 - login_pty() and dup2() used together
 - aslave should be closed in parent

Cheers

                yousong


> Signed-off-by: Rafał Miłecki <zajec5@gmail.com>
> ---
>  CMakeLists.txt     |  2 +-
>  service/instance.c | 35 +++++++++++++++++++++++++----------
>  2 files changed, 26 insertions(+), 11 deletions(-)
>
> diff --git a/CMakeLists.txt b/CMakeLists.txt
> index dfa9413..7e1fad3 100644
> --- a/CMakeLists.txt
> +++ b/CMakeLists.txt
> @@ -14,7 +14,7 @@ SET(SOURCES procd.c signal.c watchdog.c state.c       inittab.c rcS.c ubus.c system.c
>         service/service.c service/instance.c service/validate.c service/trigger.c service/watch.c
>         plug/coldplug.c plug/hotplug.c utils/utils.c)
>
> -SET(LIBS ubox ubus json-c blobmsg_json json_script)
> +SET(LIBS util ubox ubus json-c blobmsg_json json_script)
>
>  IF(DEBUG)
>    ADD_DEFINITIONS(-DDEBUG -g3)
> diff --git a/service/instance.c b/service/instance.c
> index 35b2def..110db36 100644
> --- a/service/instance.c
> +++ b/service/instance.c
> @@ -23,6 +23,8 @@
>  #include <pwd.h>
>  #include <libgen.h>
>  #include <unistd.h>
> +#include <pty.h>
> +#include <utmp.h>
>
>  #include <libubox/md5.h>
>
> @@ -273,7 +275,7 @@ instance_run(struct service_instance *in, int _stdout, int _stderr)
>                 dup2(_stdin, STDIN_FILENO);
>                 closefd(_stdin);
>         }
> -       if (_stdout > -1) {
> +       if (_stdout > -1 && _stdout != STDOUT_FILENO) {
>                 dup2(_stdout, STDOUT_FILENO);
>                 closefd(_stdout);
>         }
> @@ -314,8 +316,9 @@ instance_free_stdio(struct service_instance *in)
>  void
>  instance_start(struct service_instance *in)
>  {
> +       int amaster = -1, aslave = -1;
>         int pid;
> -       int opipe[2] = { -1, -1 };
> +       int _stdout = -1;
>         int epipe[2] = { -1, -1 };
>
>         if (!avl_is_empty(&in->errors.avl)) {
> @@ -327,13 +330,19 @@ instance_start(struct service_instance *in)
>                 return;
>
>         instance_free_stdio(in);
> +
> +       /* We don't want block buffered stdout so use PTY instead of a pipe */
>         if (in->_stdout.fd.fd > -2) {
> -               if (pipe(opipe)) {
> -                       ULOG_WARN("pipe() failed: %d (%s)\n", errno, strerror(errno));
> -                       opipe[0] = opipe[1] = -1;
> +               if (openpty(&amaster, &aslave, NULL, NULL, NULL)) {
> +                       ULOG_WARN("openpty() failed: %d (%s)\n", errno, strerror(errno));
> +                       amaster = aslave = -1;
> +               } else {
> +                       /* Child should just use stdout which will go to the master */
> +                       _stdout = STDOUT_FILENO;
>                 }
>         }
>
> +       /* Use pipe as stderr is always unbuffered by default */
>         if (in->_stderr.fd.fd > -2) {
>                 if (pipe(epipe)) {
>                         ULOG_WARN("pipe() failed: %d (%s)\n", errno, strerror(errno));
> @@ -353,9 +362,15 @@ instance_start(struct service_instance *in)
>
>         if (!pid) {
>                 uloop_done();
> -               closefd(opipe[0]);
> +               close(amaster);
>                 closefd(epipe[0]);
> -               instance_run(in, opipe[1], epipe[1]);
> +               if (aslave > -1) {
> +                       if (login_tty(aslave)) {
> +                               ULOG_WARN("login_tty() failed: %d (%s)\n", errno, strerror(errno));
> +                               _stdout = -1;
> +                       }
> +               }
> +               instance_run(in, _stdout, epipe[1]);
>                 return;
>         }
>
> @@ -364,9 +379,9 @@ instance_start(struct service_instance *in)
>         clock_gettime(CLOCK_MONOTONIC, &in->start);
>         uloop_process_add(&in->proc);
>
> -       if (opipe[0] > -1) {
> -               ustream_fd_init(&in->_stdout, opipe[0]);
> -               closefd(opipe[1]);
> +       if (amaster > -1) {
> +               ustream_fd_init(&in->_stdout, amaster);
> +               closefd(aslave);
>         }
>
>         if (epipe[0] > -1) {
> --
> 1.8.4.5
> _______________________________________________
> openwrt-devel mailing list
> openwrt-devel@lists.openwrt.org
> https://lists.openwrt.org/cgi-bin/mailman/listinfo/openwrt-devel
Rafał Miłecki June 19, 2015, 1:16 p.m. UTC | #2
On 19 June 2015 at 14:44, Yousong Zhou <yszhou4tech@gmail.com> wrote:
> On 19 June 2015 at 19:34, Rafał Miłecki <zajec5@gmail.com> wrote:
>> Using pipe automatically switches service to block buffering which kind
>> of breaks our logging. We won't get anything from FD until the buffer
>> gets filled fully or the service exits. This makes log messages appear
>> with an unwanted delay.
>> Switching to PTY fixes this issue. Service prints to the "standard"
>> stdout (no piping) but the messages go (line buffered) to the master FD
>> anyway.
>> It was successfully tested with all 4 variants of "stdout" and "stderr"
>> ubus arguments.
>>
>
> I do not like this because
>
>  - Conventionally, services will drop their inherited controlling
> terminal for preparation of becoming a daemon.  Adding back that
> controlling terminal by login_tty() for alleviating possible delay
> when logging stdio sounds not right.

Do you have any other idea?
The delay isn't "possible", I think it occurs with 90%+ of apps. I'm
not aware of many apps (except for grep maybe) that calls "fflush"
after every printf. It simply makes reading real-time logs with
"logread" not working at all.


>  - I am biased... in that the ossherd [1] package depends on the fact
> processes started by procd do not have controlling terminals and this
> will unattended password login with ssh  :)

I'm afraid I don't understand this part. In any case, you don't have
to use logging in your init.d script. It's an option.


> Anyway, the implementation is not complete.
>
>  - epipe[] is still there.

It is supposed to be. That's the only way to keep logging of stdout
and stderr separated. Read my comment (in the code) about epipe.


>  - aslave needs to be closed when login_pty() failed

I'll fix that.


>  - login_pty() and dup2() used together

That's correct, expected, on purpose.


>  - aslave should be closed in parent

Like closefd(aslave) which my patch does?
Jo-Philipp Wich June 19, 2015, 2:14 p.m. UTC | #3
Hi.

> Do you have any other idea?

Yes, we create a shared library which calls setbuf(stderr/out, NULL) in
an __attribute__((constructor)) function, then specify this library in
LD_PRELOAD when exec'ing stdio-relay enabled processes from procd.

This is equivalent to the implementation used by the coreutils "stdbuf"
applet.

~ Jow
Yousong Zhou June 19, 2015, 2:50 p.m. UTC | #4
On Jun 19, 2015 9:16 PM, "Rafał Miłecki" <zajec5@gmail.com> wrote:
>
> On 19 June 2015 at 14:44, Yousong Zhou <yszhou4tech@gmail.com> wrote:
> > On 19 June 2015 at 19:34, Rafał Miłecki <zajec5@gmail.com> wrote:
> >> Using pipe automatically switches service to block buffering which kind
> >> of breaks our logging. We won't get anything from FD until the buffer
> >> gets filled fully or the service exits. This makes log messages appear
> >> with an unwanted delay.
> >> Switching to PTY fixes this issue. Service prints to the "standard"
> >> stdout (no piping) but the messages go (line buffered) to the master FD
> >> anyway.
> >> It was successfully tested with all 4 variants of "stdout" and "stderr"
> >> ubus arguments.
> >>
> >
> > I do not like this because
> >
> >  - Conventionally, services will drop their inherited controlling
> > terminal for preparation of becoming a daemon.  Adding back that
> > controlling terminal by login_tty() for alleviating possible delay
> > when logging stdio sounds not right.
>
> Do you have any other idea?
> The delay isn't "possible", I think it occurs with 90%+ of apps. I'm
> not aware of many apps (except for grep maybe) that calls "fflush"
> after every printf. It simply makes reading real-time logs with
> "logread" not working at all.
>
>
> >  - I am biased... in that the ossherd [1] package depends on the fact
> > processes started by procd do not have controlling terminals and this
> > will unattended password login with ssh  :)
>
> I'm afraid I don't understand this part. In any case, you don't have
> to use logging in your init.d script. It's an option.
>

It's a hacky init script trying to start SOCKS5 proxies with
openssh-client.  Password authentication has to be used in this case.

>
> > Anyway, the implementation is not complete.
> >
> >  - epipe[] is still there.
>
> It is supposed to be. That's the only way to keep logging of stdout
> and stderr separated. Read my comment (in the code) about epipe.
>
>
> >  - aslave needs to be closed when login_pty() failed
>
> I'll fix that.
>
>
> >  - login_pty() and dup2() used together
>
> That's correct, expected, on purpose.
>

but login_pty will do dup2 on all 3 fd's with aslave.  it's duplicate work
and has side effects (regarding a pty stdin).

I think currently not many programs need procd_set_param stdout.  most of
the time if not always stderr, apart from syslog, is used for logging and
error reporting.  If that is agreed, then we can conclude that it is at
least a relatively rare case that we need to log stdout of the program, and
if that is the case we can afford and should patch the program itself to
use line buffered io with stdout.

Regards

                    yousong

>
> >  - aslave should be closed in parent
>
> Like closefd(aslave) which my patch does?

sorry, i missed that one before.
diff mbox

Patch

diff --git a/CMakeLists.txt b/CMakeLists.txt
index dfa9413..7e1fad3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -14,7 +14,7 @@  SET(SOURCES procd.c signal.c watchdog.c state.c	inittab.c rcS.c	ubus.c system.c
 	service/service.c service/instance.c service/validate.c service/trigger.c service/watch.c
 	plug/coldplug.c plug/hotplug.c utils/utils.c)
 
-SET(LIBS ubox ubus json-c blobmsg_json json_script)
+SET(LIBS util ubox ubus json-c blobmsg_json json_script)
 
 IF(DEBUG)
   ADD_DEFINITIONS(-DDEBUG -g3)
diff --git a/service/instance.c b/service/instance.c
index 35b2def..110db36 100644
--- a/service/instance.c
+++ b/service/instance.c
@@ -23,6 +23,8 @@ 
 #include <pwd.h>
 #include <libgen.h>
 #include <unistd.h>
+#include <pty.h>
+#include <utmp.h>
 
 #include <libubox/md5.h>
 
@@ -273,7 +275,7 @@  instance_run(struct service_instance *in, int _stdout, int _stderr)
 		dup2(_stdin, STDIN_FILENO);
 		closefd(_stdin);
 	}
-	if (_stdout > -1) {
+	if (_stdout > -1 && _stdout != STDOUT_FILENO) {
 		dup2(_stdout, STDOUT_FILENO);
 		closefd(_stdout);
 	}
@@ -314,8 +316,9 @@  instance_free_stdio(struct service_instance *in)
 void
 instance_start(struct service_instance *in)
 {
+	int amaster = -1, aslave = -1;
 	int pid;
-	int opipe[2] = { -1, -1 };
+	int _stdout = -1;
 	int epipe[2] = { -1, -1 };
 
 	if (!avl_is_empty(&in->errors.avl)) {
@@ -327,13 +330,19 @@  instance_start(struct service_instance *in)
 		return;
 
 	instance_free_stdio(in);
+
+	/* We don't want block buffered stdout so use PTY instead of a pipe */
 	if (in->_stdout.fd.fd > -2) {
-		if (pipe(opipe)) {
-			ULOG_WARN("pipe() failed: %d (%s)\n", errno, strerror(errno));
-			opipe[0] = opipe[1] = -1;
+		if (openpty(&amaster, &aslave, NULL, NULL, NULL)) {
+			ULOG_WARN("openpty() failed: %d (%s)\n", errno, strerror(errno));
+			amaster = aslave = -1;
+		} else {
+			/* Child should just use stdout which will go to the master */
+			_stdout = STDOUT_FILENO;
 		}
 	}
 
+	/* Use pipe as stderr is always unbuffered by default */
 	if (in->_stderr.fd.fd > -2) {
 		if (pipe(epipe)) {
 			ULOG_WARN("pipe() failed: %d (%s)\n", errno, strerror(errno));
@@ -353,9 +362,15 @@  instance_start(struct service_instance *in)
 
 	if (!pid) {
 		uloop_done();
-		closefd(opipe[0]);
+		close(amaster);
 		closefd(epipe[0]);
-		instance_run(in, opipe[1], epipe[1]);
+		if (aslave > -1) {
+			if (login_tty(aslave)) {
+				ULOG_WARN("login_tty() failed: %d (%s)\n", errno, strerror(errno));
+				_stdout = -1;
+			}
+		}
+		instance_run(in, _stdout, epipe[1]);
 		return;
 	}
 
@@ -364,9 +379,9 @@  instance_start(struct service_instance *in)
 	clock_gettime(CLOCK_MONOTONIC, &in->start);
 	uloop_process_add(&in->proc);
 
-	if (opipe[0] > -1) {
-		ustream_fd_init(&in->_stdout, opipe[0]);
-		closefd(opipe[1]);
+	if (amaster > -1) {
+		ustream_fd_init(&in->_stdout, amaster);
+		closefd(aslave);
 	}
 
 	if (epipe[0] > -1) {