Search code examples
clinuxipcdaemonopenwrt

How to create an attachable CLI (terminal) for daemon?


Problem

I would like to program an attachable command line interface for my daemon.

I developped a daemon running 24/7 on Linux Openwrt:

#!/bin/sh /etc/rc.common
START=98
USE_PROCD=1
PROCD_DEBUG=1

start_service() {
  procd_open_instance
  procd_set_param command "/myProgram"
  procd_set_param respawn
  procd_close_instance
}

I would like to add a debug user interfaces for test. So we could live tune some parameters/actions and print log. Something like the screen package. Hence i want to create a command line interface for this daemon.

Research

Stdin/Stdout

Ideally i would like to write directly to the stdin of the daemon and read the stdout.

Daemon

Duplicate stdin to a file. Duplicate stoud to a file.

Client

A launched C program by the tester. It would relay stdin to stdinfile of daemon and stdoutfile of daemon to stdout.

Critic

  • That would be maybe the simplest way and I could read stdout.
  • I couldn't find any exemples, it makes me think i'm overlooking something.
  • Theres a risk I fill the flash by writing endlessly to the stdoutfile.

Pipes

The creation of 2 named pipe can be possible.

Daemon

The daemon would create a named input pipe and poll the pipe by making non blocking read. A second output pipe is necessary to write the return of the command received.

Client

A launched C program by the tester. It would relay stdin to input pipe and output pipe to stdout.

Critic

  • I don't know if I can properly redirect the stdout of the daemon to output pipe. Which means I wont be able to print the stdout logs but only specific cli coded response.

MessageQ

Same issues as pipe.

Sockets

Seems rather complex for a simple application.

Shared Memory

The paradigm does not seems appropriate.

Pty

Maybe something can be done with pseudo terminals but I don't understand them even after reading explanations: attach a terminal to a process running as a daemon (to run an ncurses UI)

Screen/Tmux

I don't have screen or tmux in my repository.

Question

What is the proper way to create a CLI for a daemon ? Where could I find an exemple ?


Solution

  • I would use a Unix domain stream socket, with the CLI thread in a blocking accept() until a connection is obtained.

    These sockets are bidirectional, and you can write a trivial CLI application to read from standard input to the connected socket, and from the connected socket to standard output. (That same trivial CLI program could be used to redirect the output over e.g. SSH to ones local computer with much more storage, running the CLI program remotely using something like ssh -l username openwrt-device.name-or-address cli-program | tee local-logfile. OpenWrt devices often don't have suitable storage for log files, so this can be very useful.)

    Use vdprintf() to implement your own printf() that writes to the connected CLI.

    Because sockets are bidirectional, if you want to use locking –– for example, to avoid mixing logging output and CLI responses ––, use a mutex for writing; the read side does not need to take the mutex at all.

    You cannot really use <stdio.h> FILE * stream handles for this, because its internal buffering can yield unexpected results.

    Assuming your service daemon uses sockets or files, it can be very useful to reserve the file descriptor used for the bidirectional CLI connection, by initially opening /dev/null read-write (O_RDWR). Then, when the connection is accept()ed, use dup2() to move the accepted connection descriptor to the reserved one. When the connection is to be closed, use shutdown(fd, SHUT_RDWR) first, then open /dev/null, and dup that descriptor over the connection to be closed. This causes the connection to be closed and the descriptor to be reopened to /dev/null, in an atomic manner: the descriptor is never "unused" in between. (If it is ever close()d in a normal manner, another thread opening a file or socket or accepting a new connection may reuse that descriptor, causing all sorts of odd effects.)

    Finally, consider using an internal (cyclic) buffer to store the most recent output messages. You do not need to use a human-readable format, you can use e.g. the first character (codes 1 to 254) to encode the severity or log level, keeping NUL (0) as the end-of-string mark, and 255 as the "CLI response" mark, so that your CLI program can use e.g. ANSI colors to color the output if output is a terminal. (For example, "\033[1;31m" changes output to bright red, and "\033[0m" returns the output back to normal/default. The \033 refers to a single character, code 27, ASCII ESC.) This can be very useful to efficiently indicate the priority/severity of each separate output chunk to the human user. The Linux kernel uses a very similar method in its kernel logging facility.