111

I have a long-running server process inside a screen session on my Linux server. It's a bit unstable (and sadly not my software so I can't fix that!), so I want to script a nightly restart of the process to help stability. The only way to make it do a graceful shutdown is to go to the screen process, switch to the window it's running in, and enter the string "stop" on its control console.

Are there any smart redirection contortions I can do to make a cronjob send that stop command at a fixed time every day?

1
  • You might be able to make a systemd service -- Then run as a cron job to: service my-ling-service restart, every time your cron job fires.
    – will
    Sep 6, 2022 at 16:05

9 Answers 9

108

This answer doesn't solve the problem, but it's left here because 30+ people found it useful, otherwise I would have deleted it long time ago.

Write to /proc/*pid of the program*/fd/0. The fd subdirectory contains the descriptors of all the opened files and file descriptor 0 is the standard input (1 is stdout and 2 is stderr).

You can use this to output messages on the tty where a program is running, though it does not allow you to write to the program itself.

Example

Terminal 1:

[ciupicri@hermes ~]$ cat
shows on the tty but bypasses cat

Terminal 2:

[ciupicri@hermes ~]$ pidof cat
7417
[ciupicri@hermes ~]$ echo "shows on the tty but bypasses cat" > /proc/7417/fd/0
13
  • 3
    @James Lawrie: then have a look at proc(5) and proc.txt. Sep 6, 2010 at 12:46
  • 4
    +2 no matter how much you think you know, there's always more to learn :) slick.
    – user15590
    Sep 6, 2010 at 14:55
  • 4
    Be aware though that the proc fd only redirects to what is used as a source of stdin. In your example, if you enter something in terminal 1 it will print it out again (it is send to cats stdin and cat prints it), thus resulting in you seeing it twice. On the other hand if you send something to fd/0 it will be sent to the console but not to cat, and thus only displayed once. As cat simply prints input out again with this example you can not really see if your input or output is being printed, thus this misconception. /fd/0 points to the console /pts; see ls -l /proc/7417/fd/0.
    – Kissaki
    Jul 2, 2013 at 10:09
  • 7
    real-world example: I have started gphoto2 --get-all-files and it asks for a confirmation 100 times. When I echo "y" >/proc/PID/fd/0, gphoto2 does not proceed, however, "y" is printed in the terminal. May 25, 2015 at 7:50
  • 8
    This doesn't work like the 60 people who upvoted without testing it thought it did. It simply prints your redirected input to the virtual terminal your process is running in, but it doesn't write your input to stdin of that process. Jun 17, 2017 at 5:48
56

Screen based solution

Start the server like this:

# screen -d -m -S ServerFault tr a-z A-Z # replace with your server

screen will start in detached mode, so if you want to see what's going on, run:

# screen -r ServerFault

Control the server like this:

# screen -S ServerFault -p 0 -X stuff "stop^M"
# screen -S ServerFault -p 0 -X stuff "start^M"
# screen -S ServerFault -p 0 -X stuff "^D" # send EOF

(this answer is based on sending text input to a detached screen from the Unix & Linux sibling site)

Explanation of the parameters:

-d -m
  Start screen in detached mode. This creates a new session but doesn’t attach to it. This is useful for system startup scripts.

-S sessionname
  Set the name of the new session to sessionname.

-r [pid.tty.host]
-r sessionowner/[pid.tty.host]
  Resume a detached screen session.

-p number_or_name|-|=|+
  Preselect a window. This is useful when you want to reattach to a specific window or you want to send a command via the -X option to a specific window.

-X
  Send the specified command to a running screen session e.g. stuff.

stuff [string]
  Stuff the string string in the input buffer of the current window. This is like the paste command but with much less overhead. Without a parameter, screen will prompt for a string to stuff. You cannot paste large buffers with the stuff command.

tmux based solution

Start the server like this:

# tmux new-session -d -s ServerFault 'tr a-z A-Z' # replace with your server

tmux will start in detached mode, so if you want to see what's going on, run:

# tmux attach-session -t ServerFault

Control the server like this:

# tmux send-keys -t ServerFault -l stop
# tmux send-keys -t ServerFault Enter
# tmux send-keys -t ServerFault -l start
# tmux send-keys -t ServerFault Enter
# tmux send-keys -t ServerFault C-d # send EOF

Explanation of the parameters:

new-session [-AdDEPX] [-c start-directory] [-e environment] [-f flags] [-F format] [-n window-name] [-s session-name] [-t group-name] [-x width] [-y height] [shell-command]
(alias: new)
  Create a new session with name session-name.
  The new session is attached to the current terminal unless -d is given. window-name and shell-command are the name of and shell command to execute in the initial window.

send-keys [-FHlMRX] [-N repeat-count] [-t target-pane] key
(alias: send)
  Send a key or keys to a window. Each argument key is the name of the key (such as 'C-a' or 'NPage') to send; if the string is not recognised as a key, it is sent as a series of characters. All arguments are sent sequentially from first to last.
  The -l flag disables key name lookup and processes the keys as literal UTF-8 characters. The -H flag expects each key to be a hexadecimal number for an ASCII character.

3
  • This is the real solution :) Feb 20, 2020 at 20:07
  • And how can I get the immediate STDERR/STDOUT of the process right after the stuff went in? Apr 7 at 2:31
  • @TomRoggero you attach screen or tmux to it. Or perhaps you're looking for something like tmux capture-pane -pS ServerFault ? (man page) You might also be better off using something like Expect or its alternatives, e.g. Pexpect. Apr 8 at 7:47
23

It is possible to send input text to a running process without running the screen utility, or any other fancy utility. And it can be done by sending this input text to the process' standard input "file" /proc/PID#/fd/0.

However, the input text needs to be sent in a special way to be read by the process. Sending the input text via the regular file write method will not cause the process to receive the text. This is because doing so will only append to that "file", but will not trigger the process to read the bytes.

To trigger the process to read the bytes, it is necessary to do an IOCTL operation of type TIOCSTI for every single byte to be sent. This will place the byte into the process' standard input queue.

This is discussed here with some examples in C, Perl, and Python:

https://unix.stackexchange.com/questions/48103/construct-a-command-by-putting-a-string-into-a-tty/48221

--

So to answer the original question asked almost 9 years ago, the cron job would need to run some small utility script / program similar to the examples people wrote for that other question, which would send the string "stop\n" to that server process in the question, by sending each of the 5 bytes via an IOCTL operation of type TIOCSTI.

Of course this will only work on systems that support the TIOCSTI IOCTL operation type (like Linux), and only from the root user account, as these "files" under /proc/ are "owned" by root.

9

There is a more elegant solution that avoids the use of tail -f and the forever loops that waste system resources.

  • First create a named pipe to route STDIN through: mkfifo /data/in.

  • Then block it for writing, so it does not get closed when your process read all of the current contents: sleep infinity > /data/in &. Sleeping forever is better than tailf -f /dev/null because tailf uses inotify resources and will be triggered each time some app sends data to /dev/null. You can see this by running strace on it. It is also better than cat > /dev/null & because cat will be itself disconnected from STDIN, which in turn will close /data/in.

  • Start your process in the background with the /data/in providing STDIN: application < /data/in &. This works better than using piping from tail tail -f /data/in | application & because the pipe will only get terminated if the tail stops, but if your application crashes the pipe will keep running.

  • Halt waiting for the application to finish. wait $(pidof application). This uses no resources and if the application crashes your code after the wait will be executed. You can add an application restart loop around it if you wish.

  • To terminate the application gracefully trap and relay the system signals to it with trap 'kill -SIGTERM $(pidof app)' SIGTERM

4

Try this to start:

# screen
# cd /path/to/wd
# mkfifo cmd
# my_cmd <cmd
C-A d

And this to kill:

# cd /path/to/wd
# echo "stop" > cmd
# rm cmd
1
  • 3
    This is good, but it might have the disadvantage of not being able to send other commands while the program is running. If the program stops when it hits EOF on stdin then on the first echo "xxx" > cmd the program will stop (because the pipe will be closed). Though some programs are smart enough to reopen (rewind(3)) their stdin when they encounter EOF. Sep 6, 2010 at 12:30
4

I had a program which was started under xterm and I used gdb to manipulate the xterm process and write to its pseudoterminal master.

First find out the file descriptor number used by the xterm process for /dev/ptmx:

$ ls -l /proc/$(pidof xterm)/fd/
...
lrwx------. 1 apple apple 64 Sep 10 01:30 5 -> /dev/ptmx

Next attach gdb to the process and call write with the file descriptor and the string to send to the program as if it was typed by you:

$ gdb --pid $(pidof xterm)
GNU gdb (GDB) ...
Attaching to process ...
(gdb) call (int) write(5, "please stop\n", 12)
$1 = 12
(gdb) quit
A debugging session is active.

    Inferior 1 [process 108361] will be detached.

Quit anyway? (y or n) y
Detaching from program: /usr/bin/xterm, process ...
[Inferior 1 (process ...) detached]

If for some reason xterm has opened /dev/ptmx multiple times, try all the file descriptors. In my case it happened because that xterm instance was started with the spawn-new-terminal action from another xterm and it inherited its /dev/pmtx file descriptor.

N.B. You may need to allow attaching to a running process with something like:

sysctl -w kernel.yama.ptrace_scope=0
2
  • If the program to control was started from a X terminal (xterm, xfce4-terminal, konsole, etc), maybe something like xdotool would be more appropriate. Sep 12, 2022 at 10:58
  • There's also reptyr - "an utility for taking an existing running program and attaching it to a new terminal. Started a long-running process over ssh, but have to leave and don't want to interrupt it? Just start a screen, use reptyr to grab it, and then kill the ssh session and head on home". Oct 5, 2022 at 19:44
3

Since I cannot comment the most accepted answer of Cristian Ciupitu (of 2010), I have to put this in a separate answer:

This question was already solved in this thread: https://stackoverflow.com/questions/5374255/how-to-write-data-to-existing-processs-stdin-from-external-process

In short:

You have to start your process with a pipe for stdin which does not block nor close when the current input was written through. This can be implemented by a simple endless loop which will be piped to the process in question:

$ (while [ 1 ]; do sleep 1; done) | yourProgramToStart

I can confirm that this is different of krissi's way to open a pipe which was not working in my case. The shown solution did work instead.

You can then write to the .../fd/0 file of the process to send instructions to it. The only drawback is that you need to terminate the bash process as well which is executing the endless-loop after the server did shut down.

1

If you want to avoid having to stop the while loop in this answer you can use tail -f which will exit when the process exits.

Instead of writing to /prod/${pid}/fd/0 a pipe is needed

$ mkfifo input_pipe
$ tail -f input_pipe | your_program

The cron command

echo "some message" > input_pipe

The big caveat is you cannot enter commands in screen itself at this point and you cannot see any input.

0

Most solutions do not help if a program was already startet, i.e. on another shell from maybe another computer which you loose control of.

In this case the code shown in https://unix.stackexchange.com/questions/48103/construct-a-command-by-putting-a-string-into-a-tty with some adaptions can be used.

Use Case:

  • start apt dist-upgrade on your computer at work and forget about it
  • go home (yes that happens)
  • apt might now hang waiting for an input

At home you can find out the PTS of the apt process

root@linux:~# w
16:50:27 up 148 days, 18:16,  5 users,  load average: 0.77, 0.42, 0.32
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
root     pts/1    10.0.0.1 14:37    3:39   5.07s  5.03s apt dist-upgrade

Now at home compile this code (adapted from the link above)

#include <sys/ioctl.h>
#include <termios.h>
#include <stdio.h>
#include <stdlib.h>

void stackchar(char c)
{
  if (ioctl(0, TIOCSTI, &c) < 0) {
    perror("ioctl");
    exit(1);
  }
}
int main(int argc, char *argv[])
{
  int i, j;
  char c;

  for (i = 1; i < argc; i++) {
    if (i > 1) stackchar(' ');
    for (j=0; (c = argv[i][j]); j++) {
      stackchar(c);
    }
  }
  stackchar('\r');
  stackchar('\n');
  exit(0);
}

Lets compile it

root@linux:~# gcc pushTty.c -o pushTty

Then do (maybe multiple times) to send Y as an input

root@linux:~# ./pushTty Y </dev/pts/1

or to just send a newline (using the default apt answer)

root@linux:~# ./pushTty </dev/pts/1

and apt will eventually finish "clean"

You must log in to answer this question.