No /dev/tty when script is invoked as UDEV trigger

1

I have raspberry pi with stock raspbian installed. I have also installed RetroPI which works just fine.

What I wanted to do, is to write small autostart script that would start emulationstation (retropi main run script) when game pad is connected via bluetooth. I mave made simple udev rule

pi@raspberrypi:~ $ cat /etc/udev/rules.d/99-zlocal.rules
SUBSYSTEM=="bluetooth",SUBSYSTEMS=="amba", ATTRS{id}=="00241011", ACTION=="add", RUN+="/usr/local/bin/emulator-controll.sh start"

which works as expected, script is ran when gamepad is paired.

The problem is, that if i run my script via eg SSH like

sudo emulator-controll.sh start        #sudo is optional here, but I want the same conditions as in udev trigger context

emulationstation as expected. However, if the same script is used as action trigger for udev rule above, it fails with errors like

Wed Aug 21 00:02:33 CEST 2019
Invoking user: root
Invoked start command
Invoking emulation station start command
Done
'unknown': I need something more specific.
tput: unknown terminal "unknown"
Segmentation fault
/usr/bin/emulationstation: line 23: /dev/tty: No such device or address
tput: unknown terminal "unknown"

I think that for some reasons (im linux newb) tty is not accessible from udev trigger context. Can I fix it somehow?

PS. I have tried to run background deamon (to use named pipes) that is launching on boot time, but effect is exactly the same - no tty in script execution context which makes me think that is is general behavior of background tasks

Script itself is irrelevant IMHO, but I will provide it as well for clarity:

#!/bin/bash
#/bin/date >> /tmp/udev.log
#env >> /tmp/udev.log
#echo "Connected" > /dev/pts/2

log=/var/log/emulator-controll.log

{
readarray -t PIDS <<< $(ps aux | grep [e]mulationstation | awk '{print $2}')
echo ">${PIDS[@]}<"
echo >> $log
/bin/date >> $log
echo "Invoking user: $(whoami)"
case "$1" in
        start )
                echo "Invoked start command" >> $log
                if [[ "${#PIDS[@]}" -ge 3 ]]; then
                        echo "Emulator is already running. No action is taken" >> $log
                else
                        echo "Invoking emulation station start command" >> $log
                        export DISPLAY=:0
                        sudo -u pi nohup emulationstation  &
                fi
                echo "Done" >> $log
                ;;
        stop )
                echo "Invoked stop command" >> $log
                if [[ -z "${PIDS[0]}" ]]; then
                        echo "Emulator is not running, no action is taken" >> $log
                else
                        echo "Killing processes with PIDs ${PIDS[@]}" >> $log
                        for pid in "${PIDS[@]}"; do
                                kill $pid
                        done
                        echo "Done" >> $log
                fi
                ;;
        *)
                echo "Usage: $0 {start|stop}"
esac

} >> $log 2>>$log

Antoniossss

Posted 2019-08-20T22:05:30.060

Reputation: 111

It may not be the best approach, still running emulationstation inside screen or tmux will probably help. Something like sudo -u pi tmux new-session -d -s some_name emulationstation. Untested, hence not an answer. A useful side effect is you can use the exit status of tmux has-session -t some_name to know if the utility is running, and tmux kill-session -t some_name to kill it. Unless emulationstation daemonizes itself (I don't know the utility at all); but it probably doesn't, since it tries to do something with its controlling terminal. – Kamil Maciorowski – 2019-08-21T07:43:44.993

I have tried that with screen, but the problem is, that it simply wont start - lvl0: Error initializing SDL!

lvl0: Renderer failed to initialize! lvl0: Window failed to initialize!

It is part of RetroPI suite. It acts as frontend for emulators and uses HW acceleration to draw. – Antoniossss – 2019-08-21T07:50:33.363

Answers

0

When the kernel triggers actions and udev runs scripts as part of the rules, the environment of those scripts is very restricted.

There can be many users logged in simultaneously into a single Linux computer, each user with multiple tty's, so of course a script run from the kernel doesn't have a tty: No user has started this script, so even if one could assign a tty to it, which of the many tty's in use should it be? And the same goes for an X connection (which is also frequently asked).

There are more restrictions, as you can see from man 7 udev:

RUN

Add a program to the list of programs to be executed for a specific device. This can only be used for very short running tasks. Running an event process for a long period of time may block all further events for this or a dependent device. Long running tasks need to be immediately detached from the event process itself. If the option RUN{fail_event_on_error} is specified, and the executed program returns non-zero, the event will be marked as failed for a possible later handling.

If I read your script correctly, you want to start or stop something on a given X connection that will be running for a very long time. This can only work if you split your task in two tasks: Have some kind of demon that is started when the corresponding X server starts (the X server needs to be started so you can get the authorization cookie), and then tell the demon in the udev run script that it should do certain things, like starting or stopping a service.

In this way, the udev run script can exit immediately.

If you are using systemd, you can also start a unit from an udev rule (see e.g. here).

Edit

If you want to use tty1, consider changing the login service (getty) that runs on tty1 to your demon, which you can then trigger from the script.

So e.g. here for details of a possible way to do it, vary as needed.

dirkt

Posted 2019-08-20T22:05:30.060

Reputation: 11 627

About the script: yes and no. Yes - because i want to run long running process, No - because I want to do it in background - so script itself is fast and I don't see a problem here. Next - not on X server, but on main console - thus main monitor (physically). What I want to do is to "autostart" application in response to HD event (connection of wireless controller over BT in this case) – Antoniossss – 2019-08-22T06:55:15.723

I have tried as well to run this script as BG task (with /etc/rc.local) that listens on dedicated named pipe. UDEV event pushes event to that pipe. However, this has the exact same effect. Event is triggered, bg task receives it but fails to run given app due to missing tty. I thought if I use rd.local, script will have tty bound to it. (Linux neewb) – Antoniossss – 2019-08-22T06:58:10.883

I could - for clarification - add assumption that destination TTY exists and it is TTY1 always. Because it is how it is - system boots and autologs as pi to tty1 – Antoniossss – 2019-08-22T06:59:06.830

If you are using systemd, you can also start a unit from an udev rule Actually I am starting now my "listener" on boot time after multi-user.target. Udev only sends a "message" to that service about event. – Antoniossss – 2019-08-22T07:07:41.607

As for edit - it could work, but it removes access to terminal which is big nono. – Antoniossss – 2019-08-22T08:02:20.200

The alternative then is to start the demon already with /dev/tty1 as controlling terminal, and make sure that this happens after the autologin has completed. – dirkt – 2019-08-22T08:06:04.053

I tried that by putting that into rc.local - but there were still no TTY attached ..... I think it is just because it runs as background job not on FG. Obviously if I run that in foreground, works like charm. Event If i start (if fg) in different session, it still works - eg. from ssh. BUT not from screen – Antoniossss – 2019-08-22T08:08:26.860

Hmm in the link you provided I noticed something like [Service] StandardInput=tty StandardOutput=tty. Does it mean that i could specify tty1 for service and "share" terminal? That would probably allow me to just emulationstation & without buffer injection. Is that correct? – Antoniossss – 2019-08-22T08:11:49.700

And in particular, you need to set the controlling terminal (google). Just putting something into rc.local doesn't do that, so it's evident this couldn't work. Not sure how it works in systemd; I'd have to look that up, too. – dirkt – 2019-08-22T08:13:41.853

0

Instead of trying to attach script to TTY or assign tty to the script, I have managed to achieve my goal by injecting command directly to destination tty's input buffer with this:

sudo writevt /dev/tty1 "emulationstation $(echo -ne '\r')"

this types in command and hits "enter" on the main terminal.

PS. writevt is part of console-tools and that is available on Raspbian (at least today)

Antoniossss

Posted 2019-08-20T22:05:30.060

Reputation: 111

Also note that this will fail whenever you are actually running some command on tty1 that will expect input (you said you need to use tty1), while the udev rule fires. – dirkt – 2019-08-22T11:13:56.393

True, but this is not going to be usecase. Thats for raspberrypi so nobody is going to work on tty1 besides emulationstation. – Antoniossss – 2019-08-22T11:48:19.183