Linux

[Ubuntu] 원격 Shell에서 로그인 사용자 디스플레이에 GUI 프로그램 실행하기

Posted 2016. 03. 06 Updated 2016. 03. 06 Views 11065 Replies 0
?

단축키

Prev이전 문서

Next다음 문서

ESC닫기

크게 작게 위로 아래로 댓글로 가기 인쇄

제가 사용하는 작업 책상에는 주 모니터와 부 모니터 외에, 책상 위에 비스듬히 눕혀져 있는 작은 모니터가 하나 더 놓여 있습니다. 근래에는 멘탈이 조금 뒤숭숭해서 자주 하지는 않지만, 비행 시뮬레이션 게임을 하기 위해 집에 굴러다니던 작은 모니터 화면에 F-16의 MFD Frame을 붙여서 제작한 것입니다.

화면 일부가 가려져 있기 때문에 플심을 할 때 외에는 잘 켜지 않았었는데, 최근 프로젝트를 진행하면서 보조 모니터가 추가로 필요해진 관계로 터미널 창 몇개를 띄워서 명령을 실행하거나 로그를 보는 용도로 사용해 왔습니다.

게임을 하는 것이 아니라서 거기에 붙어 있는 28개의 버튼이 달린 두 개의 MFD Frame은 화면을 가리는 불편함만 야기하는 항상 아오안이었는데, 문득 이걸로 음악 리모컨이나 하면 편하겠다는 생각이 들었습니다. 언제나 그랬듯이, 삽질 대장정은 항상 우연한 계기로 시작됩니다... (ㅠㅠ)

다음 동영상이 바로 그 결과물입니다.

동영상으로 보기에는 뭐 별거 없어보이지만, 굴러다니던 공개 라이브러리 쓰려다가 느리고 맘에 안들어서 Device Driver 윗단부터 모조리 새로 만들었습니다. (삽질의 결과물이 항상 이렇습니다. 겉보기엔 별거 없어 보여도 그 속은[...])

(여담이지만, 조이스틱 Event를 읽기 위한 CPython 모듈 및 Event Trigger 모두 직접 제작하였습니다. CPython 모듈 부분도 포스팅할 내용이 산더미지만, 이 부분은 아직 정리가 덜 되어서 나중에 찬찬히 포스팅 하도록 하겠습니다.)

모듈까지 다 만들고 테스트까지 마쳤는데, Background Service로 제작해서 부팅시 자동으로 시작되도록 등록하는 과정에서 다소 스트레스 받는 일이 터집니다. 도대체 왜 안돼냐고!!!


증상: 실행한 프로그램이 디스플레이를 못 찾는다.

그 증상을 요약하면 다음과 같습니다.

단순히 명령(파일 복사, 음악 리모컨 등)을 실행하는 것은 이상없이 잘 동작하지만, GUI와 관련된 명령(Gnome Terminal 열기 등)만 수행하려 하면 2~3초간 먹통이 되었다가 오류가 나면서 아무 일도 일어나지 않는 현상이 발생했습니다.

그리고 이 현상은 부팅 직후 자동으로 실행하는 최초 데몬에서만 발생하고, 이후 Terminal을 열어 서비스 재시작(/etc/init.d/joymacro restart)을 해 주면 무슨 일 있었냐는 듯이 정상적으로 잘 동작했습니다. Hㅏ.. (그래도, 언제 되고 안 되는지는 처음부터 파악할 수 있었으므로 그나마 양호한 편에 속하는 디버깅이긴 합니다.)

흡사, 원격에서 SSH로 접속했을 때 나오는 Shell에서 gedit나 sublime과 같은 GUI기반 프로그램을 실행했을 때 나타나는 증상과 비슷해 보였습니다. 여기에서 디스플레이와 관련이 있다는 힌트를 입수하고, 이를 바탕으로 다양한 시도를 한 끝에 원인을 찾아낼 수 있었습니다.


원격 Shell/Background Service에서 GUI 프로그램 실행하기

이 글에서 다루는 내용이 적용되는 상황을 간략히 설명하면 다음과 같습니다.

"Ubuntu로 부팅을 하고 로그인까지 해서 바탕화면을 노려보고 있습니다. 이 때, 원격에서 이 PC에 SSH로 접속해서 현재 보고 있는 화면에 Gnome Terminal을 띄우고 싶습니다."

화면을 보고 있는 사용자 입장에서는 내가 실행하지도 않은 Terminal 창이 벌컥 하고 나타나서 당황하게 될 수도 있는 상황입니다. 물론, 명령을 실행한 원격 Shell에서는 아무 일도 일어나지 않습니다. (간간히 Stdout및 Stderr 메시지가 뜨기는 할 것입니다.)

우선, 무턱대고 다음과 같이 Terminal을 실행하는 명령을 치면..

gnome-terminal

다음과 같은 오류메시지가 뜨면서 아무 일도 일어나지 않을 것입니다.

Failed to parse arguments: Cannot open display:

이는 지극히 당연한 결과입니다. SSH로 접속한 Shell에는 Display가 붙어 있지 않기 때문입니다.

원격으로 접속한 Shell에는 Character만 주고받을 수 있는 Virtual TTY장치만 붙어있을 뿐, 어떠한 디스플레이도 할당되지 않습니다. 따라서 gedit나 sublime과 같은 편집기를 SSH에서 사용할 수 없는 것입니다. 우리가 리눅스를 공부할 때 Vi/Vim이나 Emacs와 같은 콘솔 기반 에디터를 최소 하나는 쓸 수 있도록 익혀 두어야 하는 이유이기도 합니다. (물론, SSH Tunneling기법 등을 활용해 GUI프로그램을 원격에서 사용하는 방법이 있기는 합니다만, 이는 이 글에서는 논외로 하겠습니다.)

따라서, 디스플레이가 붙어 있지 않은 원격 Shell에서 디스플레이에 무언가를 띄우기 위해서는 Shell에 로그인한 사용자의 디스플레이를 수동으로 붙여줘야 합니다. 필요한 환경변수는 바로 DISPLAYDBUS_SESSION_BUS_ADDRESS입니다. 여기에 할당해야 할 값은 다음 명령을 통해 확인할 수 있습니다.

cat /proc/<gnome-session의 PID>/environ

gnome-session의 PID는 pidof명령으로 확인할 수 있습니다.

pidof gnome-session

▶ 위에서 기술한 내용을 세 줄의 명령으로 요약하면 다음과 같습니다.

다음과 같이 환경변수 두 개를 지정하고 다시 Terminal을 실행해 보도록 합니다. (명령이 좀 깁니다. 스크롤바에 주의하세요. 스크롤바 뒤에 명령 있어요)

export DISPLAY=$(cat /proc/$(pidof gnome-session)/environ | tr '\0' '\n' | grep DISPLAY | cut -d '=' -f2-)
export DBUS_SESSION_BUS_ADDRESS=$(cat /proc/$(pidof gnome-session)/environ | tr '\0' '\n' | grep DBUS_SESSION_BUS_ADDRESS | cut -d '=' -f2-)
gnome-terminal

이제 노려보고 있는 화면에 Gnome Terminal 화면이 뙇! 하고 나타날 것입니다. 핵심은 1,2줄에 있는 두 개의 환경변수 DISPLAYDBUS_SESSION_BUS_ADDRESS입니다. 간혹 온라인상의 다른 가이드에서 DISPLAY만 설정하도록 하는 경우가 있었는데, 반드시 이 두가지를 모두 지정해 주어야 합니다. 제가 실험해 본 결과, DISPLAY만 설정한 경우 동작하는 경우(gedit, sublime)와 그렇지 않은 경우(gnome-terminal)가 나뉘었습니다. 동작하는 경우에도 명령을 실행하고 10여초 뒤에 실행되는 등 문제가 발생하였습니다. (프로그램 내부적으로 설정하지 않은 환경변수를 자체적으로 찾아내느라 시간이 소요되는 것으로 보입니다.)

참고로, 특별한 경우가 아니라면 다음과 같이 DISPLAY값은 고정해 두어도 무관합니다.

export DISPLAY=:0
export DBUS_SESSION_BUS_ADDRESS=$(cat /proc/$(pidof gnome-session)/environ | tr '\0' '\n' | grep DBUS_SESSION_BUS_ADDRESS | cut -d '=' -f2-)

DISPLAY의 값인 ':0'은 0번째 디스플레이를 의미합니다. DBUS_SESSION_BUS_ADDRESS는 현재 로그인한 사용자가 접속해 있는 Display Server의 Session 주소를 의미합니다. 이 값은 대략 다음과 같은 형태로 되어 있습니다.

unix:abstract=/tmp/dbus-XlTACkvih6

여기서 주의할 사항이 있는데, 이 값은 Ubuntu Desktop에 접속할 때마다 달라지므로 값을 고정해버리면 안된다는 점입니다. DISPLAY처럼 값을 고정해 버리면 이번만 잘 되고, 다음번에 부팅하면 또다시 안 되는 상황이 벌어집니다..

SSH에서 접속한 경우 뿐만 아니라, Background Service상에서 GUI기반 프로그램을 실행해서 사용자가 보고 있는 모니터에 표시야 할 경우에도 위와 같이 두 개의 환경변수를 설정해 주어야 합니다. 이는 Service 명령으로 Daemon을 구동할 때, 환경변수가 함께 전달되지 않고 무시되기 때문입니다.


번외: Shell의 환경변수(Environment Variables)

Shell에서 명령을 실행할 때 겉으로는 명령어와 인수(Argument)들만 전달되는 것처럼 보이는데, 실제로는 여기에 더해서 환경변수(Environment Variables)들이 함께 묶여서 프로그램으로 전달됩니다. (실제로는 프로그램에서 환경변수들을 참조하여 동작 방식을 결정하는 것입니다.)

환경 변수는 로그인한 사용자마다, 사용자가 실행한 Shell마다 서로 달라집니다. 대체 무슨 내용들이 써져있는지 궁금하다면, 다음과 같이 env명령을 실행해서 현재 Shell의 환경변수들을 확인해 볼 수 있습니다.

env

출력된 내용을 자세히 살펴보면 위에서 기술했던 DISPLAY와 DBUS_SESSION_BUS_ADDRESS도 포함되어 있는 것을 확인할 수 있을 것입니다.

참고로, 이 환경변수들을 통한 해킹 기법도 존재합니다. 간단하게는 PATH변수를 조작해서 사용자가 의도했던 프로그램 대신 몰래 심어둔 바이너리를 실행하게 할 수도 있고, 이를 참조하는 프로그램을 타겟으로 환경변수를 조작하여 실행시 타겟 프로그램을 잘못 동작하도록 유도할 수도 있습니다.
이는 source나 .(dot)명령을 사용해서 환경변수들을 현재 Shell로 Import할 때 주의를 기울여야 하는 이유이기도 합니다.

service명령과 /etc/init.d/...의 차이

환경변수를 통한 보안상/안정성상 문제를 막기 위해 리눅스에서는 service 명령으로 서비스를 실행할 때 명령을 실행한 Shell의 환경변수는 모두 무시하고 '깨끗한' Shell에서 서비스를 구동하도록 되어 있습니다. 이렇게 함으로써 서비스를 실행한 사용자의 Shell의 환경변수가 '오염되어' 있더라도 정상적인 서비스의 실행을 보장할 수 있는 것입니다.

리눅스에서 서비스를 실행하는 명령은 다음과 같이 두 가지 종류가 알려져 있습니다.

service <service_name> start
/etc/init.d/<service_name> start

보통 이 두 명령이 결국 같은 것이라고 알고 있는 경우가 많은데, 서비스가 실행되어 잘 굴러간다는 결과는 같더라도 그 시작과정에서 엄연한 차이가 있습니다. 요약하자면, 서비스를 시작할 때는 service명령을 사용해야지 /etc/init.d/...는 특별한 경우가 아니라면 사용을 자제해야 합니다.

service 명령은 명령을 실행한 사용자의 Shell 환경변수를 배제하고 서비스를 실행하지만, /etc/init.d/...는  Shell의 환경변수를 그대로 들고가서 서비스를 구동합니다. 통상 서비스 시작 명령이 sudo와 붙어서 root권한으로 실행된다는 점을 고려하면, 치명적인 보안상 결함이 아닐 수 없습니다.

service 명령도 결국은 /etc/init.d/ 디렉토리에 있는 initscript를 실행하게 되지만, 그 전에 환경변수를 모두 날려서 만에하나 있을 수 있는 보안상 위협을 제거해 주는 것입니다.

'su'와 'su -'의 차이

비슷한 예시로, Super User 권한을 획득하는 명령인 'su'와 'su -'도 들 수 있습니다.

'su'명령은 이전 사용자의 환경변수를 그대로 들고가지만 (홈 디렉토리 등 일부 값이 바뀌긴 합니다),
'su -'명령은 이전 사용자의 환경변수를 모두 날리고 root의 환경변수를 새롭게 가지고 옵니다.

이 둘 중에 어떤 것을 사용해야 하는지는... 이제 잘 아실 거라 생각합니다.^^


연관글
  1. CLCD 20x2 BL (0)