Ubuntu : Shutdown 시에만 특정 명령을 실행하게끔 할 수 있을까? systemd 활용.

여기서 중요한 부분은, ‘Shutdown 시’에 있다. 즉, 재부팅(reboot) 시에는 작동하지 않고, 오로지 Shutdown 시에만 특정 명령을 수행하도록 할 수 있을까?

물론, 리눅스 월드에선 뭐든 된다. 쉽게 될 수도, 아주 복잡할 수도 있어서 그렇지.

이 글의 원제(?)는 사실 이랬다.

mpd : mpc 를 활용한, 서버가 꺼질 때 자동으로 연주를 멈추게 하는 법. 다시 말해, mpd 타이머 만들기.

그랬다가 좀 더 범용으로 바꿨다. 그만큼 이번 꽃삽질(?)을 통해 얻은게 많다는 얘기.
mpc 에 대한 글은 아래에.


TL;DR

먼저, Shutdown 시에 실행할 스크립트(/usr/bin/chk_mpd_is_running)를 만든다.
내 목적은 mpc 를 이용해 mpd 를 멈춤(Pause) 상태로 만드는데에 있었고, 그를 해결하기 위해 다음과 같은 스크립트를 생각해봤다.

# 여기는 편법(?) 코드.
if [[ $(/bin/systemctl list-jobs | egrep 'reboot.target.*start') ]]; then
        echo "재부팅합니다."
        exit 0
fi

# 여기가 진짜.
TEST=$(MPD_HOST=password@/run/mpd/socket mpc | sed -n -e 2p)
if [[ "$TEST" =~ \[playing ]]; then
        echo "연주중이므로 멈춥니다."
        MPD_HOST=password@/run/mpd/socket mpc pause
fi

편법 코드에 대해서는 아래에서 장황하게 설명하기로 하고, ‘진짜’ 코드에 대해서만 간단히 살펴보겠다.

mpc 설정법을 설명한 문서를 찾기가 쉽진 않았다. 게다가 mpd 에는 비밀번호가 걸려있는 상태. 아무튼, 저런 식(MPD_HOST=password@/run/mpd/socket mpc)으로 실행해줘야 원하는 결과를 얻을 수 있다.

이 파일은 적당한 위치 (/usr/bin 등)에 넣어준다.
실행코드를 만들었으니, 실행 조건도 만들어줘야 한다.

/etc/systemd/system/mpd-pause.service 등으로 적절히 이름을 주고 파일을 만든다. 확장자 service 는 필수.

[Unit]
Description=mpd/mpc pause if playing when poweroff
After=mpd.service
Conflicts=reboot.target
Before=poweroff.target halt.target shutdown.target
Requires=mpd.service

[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/bin/true
ExecStop=/usr/bin/chk_mpd_is_running

[Install]
WantedBy=multi-user.target

** 원래는 WantedBy=poweroff.target halt.target 로 했었다. 그러나 이게 잘못됐다는 사실을 알아냈고, 위에는 제대로된 설정을 기록해놨다.
그러나, 아래 글은 잘못된 설정을 기준으로 쓰여졌으므로, 그걸 몽땅 뒤엎을 수도 없어서 그냥 놔두기로 했다.

그리고 다음 명령 실행.

sudo systemctl enable --now mpd-pause.service

–now 는 enable 과 start 를 동시에 수행해준다.

이제, mpd 가 연주 중인 상태에서 서버가 종료(reboot 아닌 그냥 poweroff)될 때, mpc 를 이용하여 mpd 를 멈춤 상태로 먼저 전환시킨다.
reboot 때는 이 작업을 하지 않고 그냥 넘어간다.


발단 : mpd timer!

굳이 이런 작업을 생각해낸 이유는 뭘까? mpd 에 타이머 기능이 없다는게 가장 큰 요인으로 작용했다.

잘 무렵 mpd 로 음악을 듣다가, 그냥 잠이 들었다. 서버는 자동으로 꺼지게 설정할 수 있었고, 앰프도 타이머 설정을 해놨다.
여기까진 전혀 문제가 없다.

다음 날, 서버를 켰다. 허나, 음악을 들을 생각은 없었으므로 앰프는 켜지 않았다. 그런데.. mpd 는 이전 종료시에 ‘재생’ 중이었으므로, 재시동된 이후에도 계속 자기 소임을 다하고 있는 중이었다. 재생목록이 엄청나게 길거나, 반복 설정이 되어 있는 경우, 난 인식하지 못하고 있는 사이 주구장창 mpd 는 ‘연주 중’일 예정이다.

이걸 피하고 싶어서 이런 방법을 고안하게 됐다. mpd/mpc 에 대한 얘기는 다른 글에서.

먼저 든 생각은 그냥 ‘단순한 스크립트로 mpd 를 멈추게 하자’였다. 다시 말해, systemd 같은 복잡한 상위 개념은 안중에도 없었단 얘기.

위의 스크립트 중 아래 부분만 사용해서 mpd 를 멈추게끔 했다.

# 여기가 진짜.
TEST=$(MPD_HOST=password@/run/mpd/socket mpc | sed -n -e 2p)
if [[ "$TEST" =~ \[playing ]]; then
        echo "연주중이므로 멈춥니다."
        MPD_HOST=password@/run/mpd/socket mpc pause
fi

그리고 shutdown 명령을 주면 된다.
간단하다.

헌데.. 여기엔 큰 문제가 있다.
워낙에 ‘일정 시간 후 자동종료’, 즉 이른바 Timer 기능을 구현하고자 이 삽질을 시작했는데, 저 스크립트로는 타이머를 작동시킬 수가 없다.

굳이하려면, sleep 등을 이용해서 지정한 시간만큼 기다린 후에 명령을 실행하게끔 해야 한다. 그러나 여기엔 또 문제가 있으니.. 명령을 실행한 뒤 그 터미널을 그대로 열어놔야만 한다. & 를 붙여봐도 소용이 없다. 터미널을 닫으면, 그 명령도 죽는다.

그러자면 tmux 등을 사용해야 하는데… 여러모로 번거로워진다.

그리하여, systemd 로 사고를 확장했다.

전개 : 종료시 스크립트 호출!

이리 저리 구글을 뒤져, 다음과 같이 Systemd 형식을 갖출 수 있었다.

[Unit]
Description=mpd/mpc pause if playing when poweroff
Before=poweroff.target halt.target shutdown.target

[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/bin/true
ExecStop=/usr/bin/chk_mpd_is_running

[Install]
WantedBy=poweroff.target halt.target

** 위 설정에는 오류가 있다. 그에 관한 길고 지루한 얘기는 다른 글에서.

Systemd 에 대해 정확하게 이해를 하고 있진 못하기에, 그저 장님 문고리 잡는 식이었는데, 아무튼 작동은 했다. (사실, 위 Unit 설계대로라면 작동하지 않아야 한다. WantedBy 에는 multi-user.target 이 들어가야만 하고, poweroff.target halt.target 은 불필요하다. 작동한 이유는, 그 이전에 했던 꽃삽질들 때문이다. 역시 다른 글에 이 내용이 있다.)

그러나.. mpd pause 는 작동하지 않았다. 그냥 스크립트를 수동으로 돌리면 되는데, 어쩐지 Systemd 로는 안됐다. 이유가 뭘까..?

하여, 로그를 살펴봤다.

$ journalctl -n 2000 | grep -i "mpd\|music player" 
...
Aug 05 14:42:23 onestep-srv systemd[1]: Stopping mpd/mpc pause if playing when poweroff...
Aug 05 14:42:23 onestep-srv systemd[1]: Stopping Music Player Daemon...
Aug 05 14:42:23 onestep-srv systemd[1]: Stopped Music Player Daemon.
Aug 05 14:42:23 onestep-srv chk_mpd_is_running[10051]: mpd error: Connection closed by the server
Aug 05 14:42:23 onestep-srv chk_mpd_is_running[10051]: mpd error: Connection closed by the server

문제는 명확했다. mpc pause 작업보다 mpd 가 먼저 stop 되는 바람에 mpc 는 제 할 일을 할 수가 없었다.

이를 위해서는 mpd 종료보다 이 작업이 먼저 실행되게끔해야만 했다. 당연히 systemd 에는 이런 상황을 설정해줄 수가 있다.
그런데, 여기서 좀 내 머리를 복잡하게 하는 상황이 생겨버렸다.

이게 시스템이 ‘시작(boot)’될 때 실행되어야할 작업이라면, 당연히 mpd 가 먼저 실행되고, 그 다음에 이 작업이 이어져야한다. 하지만, 지금은 그 반대다.
시작이 아니라 종료이므로, mpd 는 이 작업 뒤에 종료되어야만 한다. 따라서, 이렇게 설정해야 맞지 않을까??

[Unit]
Description=mpd/mpc pause if playing when poweroff
Before=mpd.service poweroff.target halt.target shutdown.target

[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/bin/true
ExecStop=/usr/bin/chk_mpd_is_running

[Install]
WantedBy=poweroff.target halt.target

mpd.service 보다 먼저 이 작업을 실행하라는 의미로 before 에 mpd.service 를 추가했다.
그러나, 이 가정은 틀렸다. 그리하여 또 여러시간을 소비했다.

답은 after 였다. 종료라는 상황을 생각하지 말고, 시작상황에 맞춰 after 를 넣으면, 종료될 때도 그 순서를 지켜 종료가 된다.
시작이라면 당연히 mpd.service 가 먼저 실행되어야하므로, after=mpd.service 를 넣어줘야 한다.
또, 여기서는 없어도 되는 듯 하지만, requires 항목까지도 넣어주는게 좀 더 확실한 결과를 얻을 수 있을 듯 하다.
after, requires 에 관한 얘기는, 리눅스 정보의 보고, Archlinux 에서 찾을 수 있었다.
(이 부분은 나중에 덧붙였지만) StackExchange 에서도 좋은 내용을 찾았다.

“The suggested solution is to run the service unit as a normal service – have a look at the [Install] section. So everything has to be thought reverse, dependencies too. Because the shutdown order is the reverse startup order.”

다시 말해서, 그냥 순방향(시스템 시동시점)을 생각하고 Unit 파일을 작성하면, 종료될 때는 그 순서를 자동으로 반대로 적용해준다는 듯 하다.

이렇게해서 모든 문제가 풀리는가 했는데..

위기 : shutdown, reboot 구분이 안되는데?

저 상황, 저 조건에선 shutdown 과 reboot 의 구분이 되지 않는다. 종료되든, 재시작되든, 모두 mpd 를 멈춰버리게 만든다.

내가 원하는 건 오직 shutdown 때다. 재부팅 시에는 이 작업이 진행되지 않아야 한다.
이 문제로 여러 논쟁(?)이 있는 모양이다.

어떤 이는 그냥 systemd 설정으로만으로도 충분하다고 하고, 또 어떤 이는 살짝 편법을 써야만 한다고 한다.
내가 실험해본 바로는, 그냥은 안된다. 역시나 묘수가 필요했다.

이 꼼수(?)에 관해, 위 StackExchange 의 답변과 같은 내용이 또 다른 글에 정리가 되어 있었다.

간단히 정리하자면 이런 거다.
스크립트 안에서, 현재 상황이 reboot 중인 지를 판단한다. 그를 위해서 systemctl list-jobs 라는 명령을 사용한다. 이 중 reboot.target start waiting 이 있다면, 재시작중임을 인지하고 명령을 실행하지 않는다.
그렇지 않을 경우에만 진행!

이제 문제는 풀렸다.
저 글들에서는, 명령 자체는 간단하지만 나같은 초심자에겐 살짝 혼란스러운 문법으로 해법을 제시했다.

systemctl list-jobs | egrep -q 'reboot.target.*start' || echo "stopping"  >> /tmp/file

이 명령은 OR Operation 이고, 앞 조건이 참이면 그냥 거기서 끝이 난다. 시스템이 재부팅 중이라면, 다시 말해서 systemctl list-jobs | egrep -q 'reboot.target.*start' 이 True 라면 || 이후는 실행되지 않는다.

따라서, reboot 되지 않을 경우에만 실행하기 원하는 명령을 || 뒤에 넣어놓으면 된다.

그런데, 내가 해야할 작업은 저 뒤에 한줄로 넣기엔 무리가 있다. 따라서 if 문을 사용하기로 했다.
여기서 또 한번 삽질이 필요했다.

if [[ $(/bin/systemctl list-jobs | egrep -q 'reboot.target.*start') ]]; then
        echo "재부팅합니다."
        exit 0
fi

TEST=$(MPD_HOST=password@/run/mpd/socket mpc | sed -n -e 2p)
if [[ "$TEST" =~ \[playing ]]; then
        echo "연주중이므로 멈춥니다."
        MPD_HOST=password@/run/mpd/socket mpc pause
fi

reboot.target 이 있고, start 예정이면 그냥 이 스크립트를 끝내고(exit 0), 아닐 경우에만 아래 내용이 실행되게끔 했다.
그런데, 틀림없이 reboot 중인데도 아래 내용이 계속 실행이 된다. (journal 로 확인이 가능했다.)

왜…???

bash 의 || 때와 달리, if test 에서는 /bin/systemctl list-jobs | egrep -q 'reboot.target.*start' 를 True 로 인식하지 못했다. 이유는 모르겠다.
제대로 작동하게 하기 위해선, -q(quiet) 를 빼줘야 한다.

if [[ $(/bin/systemctl list-jobs | egrep 'reboot.target.*start') ]]; then
        echo "재부팅합니다."
        exit 0
fi

이렇게 해줘야 True/False 를 제대로 판별해낼 수 있게 된다.

절정/결말 : 완벽?

위와 같이 바꾼 뒤, 내가 원했던 대로 완벽하게 작동하기 시작했다.
shutdown -P 30 등으로 타이머를 가동해도 아무런 이상이 없다.

이걸 알아내느라 시간은 꽤 들었지만, 그래도 그 덕에 systemd 에 대해 조금은 더 알게됐으니.. 다음엔 좀 더 시행착오가 줄어들겠지.

안녕하세요. 글 남겨주셔서 고맙습니다.