ssh 세션이 끊겨도 GUI 터미널의 스크롤백을 깨뜨리지 않고 다시 붙을 수 있도록, 자체 터미널 에뮬레이션 없이 트래픽을 통과시키는 유틸리티 ‘alden’을 소개한다. mosh/tmux/screen과의 차이, 설계 철학, 사용법, 그리고 0.2 버전의 변경 사항을 설명하고, 독자 댓글 토론을 싣는다.
alden: detachable terminal sessions without breaking scrollback - Ansuz - mskala's home page
===============
People before tribes
« How to play Diversity | Home | Matthew Explains »
수 2025-06-11 by mskala 사용된 태그: linux, software, networking 오랫동안 끊어진 ssh 세션을 다시 이어 붙이는 무언가가 필요했습니다. 전형적인 상황은 이렇습니다. ssh로 서버에 접속해 오래 걸리는 작업을 시작하고, 그 과정을 지켜보려고 창을 열어 둡니다. 그러다 연결이 끊깁니다. 로컬 네트워크 연결을 잃었을 수도 있고, 내 쪽 정전일 수도 있고, 어떤 이유로든 컴퓨터를 재부팅해야 할 수도 있죠. 그때 서버 쪽 작업은 어떻게 될까요?
대개 연결 손실은 서버 측 작업이 SIGHUP을 받아 자동으로 종료됨을 의미합니다. 그런 일을 피하고 싶다면, 미리 "nohup", "disown", 출력 리디렉션, "tail -f" 같은 도구로 대비해, 연결이 끊겨도 작업이 계속 돌아가게 만들 수 있습니다. 하지만 그런 방식에는 많은 타협이 따릅니다. 특히 어떤 방식으로든 대화형인 작업에는 잘 맞지 않습니다. 작업을 계속 돌게 만들 수는 있더라도, 일단 서버의 잠재의식 속으로 띄워 보내고 나면 그 작업을 _제어_하고 상호작용할 능력이 거의 없어집니다.
끊어진 터미널 세션에 다시 연결하는 방법을 제공하는 도구들이 있습니다. 잘 알려진 것으로 mosh, tmux, screen이 있죠. 하지만 이들 모두 스크롤백을 망가뜨립니다. 즉, 이들을 실행하는 동안 화면 위에서 텍스트가 위로 밀려 사라지면, GUI 터미널 에뮬레이터의 고유 스크롤백 기능으로 그 텍스트에 스크롤해 돌아갈 수 없습니다. 그들은. 스크롤백을. 망칩니다. 웹페이지에서 특히 중요하지만 터미널 기반 앱에서도 중요한 가장 기본적인 UI 원칙 중 하나를 어깁니다. 스크롤바는 건드리지 말 것.
tmux와 screen 패키지는 스크롤백을 제공한다, 심지어 스크롤백을 "지원한다"고까지 말하지만, 그건 제한적이고 불만족스러운 의미에서만 사실입니다. mosh, tmux, screen은 기본적으로 모두 자체적으로 터미널 에뮬레이터이며, GUI 터미널 에뮬레이터 안에 추가 레이어로 동작합니다. tmux와 screen의 경우, 이들 자체의 스크롤백 기능이 있습니다. 그래서 GUI에 내장된 스크롤바가 아닌 tmux나 screen에 고유한 또 다른 명령 집합을 사용하면, tmux나 screen 안에서 스크롤되어 사라진 텍스트의 기록에 접근할 수 있습니다. 하지만 이들 중 무엇을 쓰든 GUI의 고유 스크롤백은 여전히 망가진다는 사실은 변하지 않습니다.
mosh 개발자들은 여기에 모욕을 더합니다. mosh가 스크롤백을 망가뜨린다는 불만에 대한 그들의 상투적 답은, 사용자에게 mosh 안에서 tmux나 screen을 쓰라고 하는 것입니다. 그들은 2012년부터 그 입장을 고수해 왔습니다.
문제의 일부는 mosh의 프로토콜이 스크롤백과 조화시키기 쉽지 않은 추상화에 기반해 있다는 점입니다. 연결이 끊겼다가 재개되는 _문자 스트림_을 전달하는 대신, mosh의 프로토콜은 연결 양 끝 사이에서 _변수를 동기화_한다는 아이디어에 기반합니다. 이 구분은 컴퓨터 과학적으로 중요하며, mosh가 창작자들이 최우선시하는 기능들을 수행하는 데 필요합니다. 변수 동기화 접근의 장점 중 하나는, 클라이언트가 놓쳤을 수도 있는 과거 기록을 서버가 임의 크기로 버퍼링할 필요가 없다는 것입니다. 최신 변수 상태만 유지하고, 최신이 아닌 부분을 클라이언트가 질의해 가져갈 수 있게 해주면 됩니다.
하지만 변수를 동기화한다는 언어로는 스크롤백 문제를 명확히 진술하는 것조차 어렵고, 하물며 실제로 해결하는 것은 더 어렵습니다. 제안들 중에는 "가상" 화면을 실제 화면 높이의 세 배쯤으로 정의하고, 네이티브 스크롤바가 닿을 수 있는 곳에 _그것_을 동기화하자는 것 등이 있었습니다. 마치 스크롤백 사용자들이 두 쪽 정도의 이전 기록만으로 만족할 것처럼요. 스크롤백이 진짜로 작동하도록 하려면, 사실상 문자 스트림이라는 추상화로 돌아가야 합니다.
13년이 지났고, 이번 주에 드디어 충분히 짜증이 나서 제가 진짜로 원하던 유틸리티를 앉아서 만들었습니다. 즉, 자체적으로 터미널 에뮬레이터로 동작하지 않고, 트래픽을 네이티브 터미널로 그대로 전달해 스크롤백이 망가지지 않게 하는, 터미널 연결 재개 도구입니다.
결과물을 "alden"이라 부르고, 최신 소스 릴리스를 여기에서 받을 수 있습니다:
GPL3입니다. 분명히 진행 중인 작업입니다. 제 목적에는 이미 꽤 잘 작동하지만, 다른 사람들이 이걸 쓰고 싶어 하고, 특히 다른 사람들이 이걸 글로 써 준다면, 추가 기능을 더하고 이식성을 개선할 여력도 생길 겁니다.
빌드한 후 "alden" 커맨드라인 프로그램을 실행하면 새 셸이 시작되며, 클라이언트와 서버 안에 래핑됩니다. 네트워크 연결을 잃거나 다른 이유로 클라이언트가 죽더라도, 서버와 셸은 살아남아야 합니다. 그런 다음 다시 로그인해서, 다시 "alden"을 입력하면 기존 서버와 셸에 연결됩니다. 자세한 내용은 패키지에 포함된 man 페이지를 참고하세요. 더 자세한 내용은 추후에 다른 문서를 통해 다음 릴리스에서 제공할 수도 있습니다.
mosh와 달리, 이 프로그램은 자체적인 고신뢰 네트워킹 프로토콜을 구현하지 않습니다. 수행하는 IPC는 명명 파이프 같은 로컬 메커니즘뿐입니다. 여전히 ssh 연결을 만들고 그 안에서 alden을 실행해야 하며, 패킷 손실이 많은 정말 나쁜 네트워크에서는 ssh 이상으로 잘 동작하지 않습니다. 이 도구가 지키려는 것은, 기본적으로 쓸 만하던 네트워크 연결이 갑자기 완전히 끊기는 상황입니다.
설계상, alden은 어떤 방향으로든 터미널 제어 시퀀스를 해석하거나 변환하지 않습니다. 가장 가까운 것은 SIGWINCH 터미널 크기 업데이트를 그대로 전달한다는 점입니다. 따라서 창 크기를 조정하면, alden 세션 안의 인지 가능한 프로그램들은 이를 감지하고 그에 맞춰 스스로 조정할 수 있어야 합니다.
다운로드해 사용기를 보내주신 모든 분께 감사드립니다. 위 링크에 새 버전을 올렸습니다. 새 버전에서는:
댓글 16개
클라이언트를 서버에서 "분리"하려면 어떻게 하나요? 다음을 시도해 봤습니다:
# terminal 1
$ ./alden
$ pstree -s -p $$ -l
systemd(1)───systemd(1109599)───x-terminal-emul(1110718)───zsh(1179908)───alden(1181725)───alden(1181726)───zsh(1181727)───pstree(1184549)
# terminal 2
$ kill -SIGHUP 1181726 # no observable change to terminal 1.
$ ./alden -r
No server.
여러 다른 시그널도 시도해 봤는데, 전혀 효과가 없거나 서버가 클라이언트와 함께 종료되었습니다.
jyn - 2025-06-16 06:56
세션을 의도적으로 분리하려면, 서버가 아니라 클라이언트를 죽이세요. 당신의 예에서라면 1181725 프로세스일 겁니다. 여기서 요점은 클라이언트를 잃었을 때 서버가 살아남는 것입니다. 서버는 그 외에는 일반적인 프로세스이므로 -HUP으로 죽일 수 있고, 일반적으로 시그널에 대해 스스로를 보존하도록 설계되어 있지 않습니다.
GUI 터미널 에뮬레이터를 사용 중이라면, 창을 그냥 닫아보는 것도 좋습니다. 그 편이 네트워크 연결 끊김 상황을 더 잘 모의할 수 있습니다.
세션을 의도적으로 분리하는 것은 제가 상정한 본래 용도는 아니었지만, 적어도 테스트에는 유용합니다. 이걸 수행하는 기능을 커맨드라인 프로그램에 내장하는 것이 가치 있다고 보시나요?
Matthew Skala - 2025-06-16 07:09
덧붙여서 - 제 코드부터 읽어야겠네요. 서버는 실제로 -HUP을 무시합니다. 그래서 서버에 -HUP을 보냈을 때 "변화 없음"을 관찰하신 것입니다. 그렇더라도 시그널을 보낼 올바른 대상은 서버가 아니라 클라이언트입니다.
Matthew Skala - 2025-06-16 07:22
아! 부모 프로세스가 클라이언트이고 자식이 서버라는 걸 몰랐네요. 저는 보통 그 반대라고 생각하거든요. 네, 부모에 HUP을 보내니 딱 좋게 작동했습니다.
제 메모: 두 번째로 재연결할 때는 클라이언트 PID가 pstree에 보이지 않네요. 아마 서버의 출력을 스트리밍만 하고 실제로 셸을 생성한 부모가 아니기 때문이겠죠. 그 경우 PID를 얻는 유일한 방법은 시작 시 alden -v를 사용하는 것 같습니다.
이제 일관되게 다시 붙을 수 있게 되니, alden이 제가 상상했던 것과 다르게 동작한다는 걸 알 수 있었습니다. 재접속할 때 스크롤백을 복원하지 않고, 분리된 이후에 출력된 것만 출력하네요. 그건 이해가 되지만, 제가 하려던 것—tmux의 세션 지속성을 alden과 kitty의 멀티플렉싱 지원 위에 쌓는 것—을 하기가 더 어려워집니다. 이게 어떻게 가능할지 상상 중입니다. https://github.com/kovidgoyal/kitty/issues/2454 에 네이티브 스크롤백을 저장/복원하는 방법이 나와 있고, 서버의 PID도 얻을 수 있으니(머신마다 고유), 그 위에 저장/복원 메커니즘을 만들 수 있을 것 같습니다. 그리고 https://sw.kovidgoyal.net/kitty/launch/#watching-launched-windows 와 결합해, 패널이 닫힐 때 스크롤백을 저장하고 alden에 재접속할 때 복원할 수 있겠네요.
현재 클라이언트와 서버의 PID를 얻는 내장 명령과, 현재 클라이언트를 분리(detach)하는 명령을 추가해 주실 수 있다면 아주 도움이 될 것 같습니다. 나머지는 다 거기서 가능해지고, 제 문제가 되고 당신의 문제는 아니게 됩니다.
작업해 주셔서 감사합니다!
jyn - 2025-06-16 19:19
아, 한 가지 더 있으면 좋을 것: 터미널 출력을 네이티브 터미널 에뮬레이터뿐 아니라 파일에도 tee로 복사하는 옵션이 있으면 좋겠습니다. 그리고 클라이언트가 현재 연결되어 있지 않더라도 모든 출력을 그 파일에 쓰도록요. 지금은 클라이언트가 분리되면 출력이 버려지기 때문에, 위에서 별도로 쌓을 방법이 없다고 생각합니다. 별도의 파일이면 메모리에서 뭔가 버퍼링할 필요도 없을 테니, 그다지 복잡하지 않길 바랍니다?
jyn - 2025-06-16 19:25
댓글에 입력하신 주소로 이메일을 보내드릴게요. 이 스레드보다 이메일이 기술적 세부사항을 논의하기에 더 쉬울 것 같습니다.
Matthew Skala - 2025-06-17 05:47
이건 Eternal Terminal과 어떻게 다른가요?
Foobar - 2025-06-23 10:40
저는 Eternal Terminal을 Git에서 받아 빌드하려다, 의존성까지 내려받아 설치하려는 디렉터리 트리가 2.6GB에 이르자 포기했습니다. 더 진행하는 데 필요한 작업량에 비해 별 가치가 없어 보였고, 무엇보다 그것이 애초에 스크롤백을 정말로 망치지 않는지도 (지금도) 확실하지 않았습니다. 제 코드는 훨씬, 훨씬 작습니다.
Matthew Skala - 2025-06-23 11:11
아마 여러 iTerm 패널이 모두 같은 호스트로 SSH하는 맥락에서 이걸 사용할 것 같습니다. 클라이언트가 어느 서버에 연결할지 제어할 방법이 있나요? 현재로서는 여러 서버가 실행 중이면 클라이언트가 임의로 선택하는 것처럼 보입니다.
Evan - 2025-06-23 15:38
다음 버전에서 계획 중입니다. 그걸 열어두면 다음 질문은 각 서버가 실제로 어느 것인지 어떻게 알아내느냐가 되고, 벌집을 발로 찬 격이긴 합니다만, 서버의 PID를 위한 옵션을 두는 건 충분히 쉽습니다.
Matthew Skala - 2025-06-23 15:57
소스 코드를 어딘가에 호스팅할 계획이 있나요?
Nikolay Kolev - 2025-06-23 17:34
그게 바로 tarball입니다.
제가 의미하신 게 버전 관리 저장소를 공개하는 것이라면, 그럴 계획은 없습니다.
Matthew Skala - 2025-06-23 17:43
훌륭합니다, 감사합니다! 이식성을 언급하셨길래 — 저는 macOS라 tarball을 받아 빌드하고, 마침내 실행되게까지 만들었습니다. 플랫폼 간 차이를 보완할 사소한 이슈들(예: macOS에는 pipe2가 없지만, 세 번째 인자로 0을 넘기시기에 pipe2 호출은 그냥 pipe 호출과 같습니다)은 있었지만, 한 가지 정말 짜증 나는 근본 문제가 있었습니다 — 문서의 모든 표시에 반해, macOS에서는 명명 파이프에 조언적 잠금(advisory lock)을 사용할 수 없습니다! 문서대로라면, 당신의 잠금 방식은 모든 플랫폼에서 작동해야 하는데도 말이죠...
그래서 macOS 지원을 추가하려면, 당신이 지금처럼 우아한 방식으로 하시는 파이프 잠금을, 파이프 옆에 보조 잠금파일을 두는 식으로 바꿔야 합니다. macOS 지원에 관심이 있으시다면, 패치를 만들어 보내드릴 수도 있습니다.
Josh Kopin - 2025-06-24 07:17
제보 감사합니다. 해당 플랫폼에서 필요하다면 /tmp에 잠금 전용으로 파일을 하나 더 만드는 것도 큰일은 아닙니다. (파이프에서 flock이 작동하지 않는) 이 상황을 위한 Autoconf 테스트가 있는지 아시나요? 단지 MacOS를 감지해 MacOS에선 적용된다고 가정하는 방법 말고요.
Matthew Skala - 2025-06-24 07:25
PuTTY에는 기록을 보존하는 "reconnect"(메뉴) 옵션이 있습니다.
ReD - 2025-06-24 19:50
제대로 비교할 수는 없습니다. PuTTY는 GUI 터미널 에뮬레이터입니다. 이미 쓰고 있는 터미널 안에서 실행할 수 있는 것이 아니고, 스크롤백 기능을 제공하긴 하지만( tmux와 screen처럼) 기존 터미널의 스크롤백을 망치지 않는 건 아닙니다. 또한 같은 클라이언트 머신에서만 "재연결"이 가능해 보이고, 클라이언트 머신이 재부팅되지 않았을 때만, 어쩌면 창이 닫히지 않았을 때만 가능한 것 같습니다. 연결 손실로 서버 측 프로세스가 SIGHUP을 받았다면 — 흔한 경우죠 — 우리가 지키려던 장기 작업은 PuTTY 재연결을 쓰기도 전에 이미 죽어 있을 겁니다. 그리고 클라이언트가 연결되어 있지 않을 때 서버 쪽에서 출력이 버퍼링되지도 않을 겁니다(최선의 경우 작업이 대기). 그러니 PuTTY의 기능은, 그것을 쓰는 이들에게 분명 유용하겠지만, 완전히 다른 문제 집합을 해결합니다.
Matthew Skala - 2025-06-25 05:30
Name
Email (optional field)
URL (optional field)
Comment
스팸 방지를 위해 여기에는 "bonobo"라고 답하세요. 여기에는 「bonobo」라고 답해 주세요. 스팸을 퇴치합시다!
정답은 무엇인가요?
저는 어떤 이유로든, 어떤 방식으로든 댓글을 삭제하거나 편집할 권리를 보유합니다. 새 댓글은 다른 사용자에게 보이기 전에 일정 기간 보류됩니다.
aardvarkacademicanimearchastrologyballadbandicootcargocolourcompsciconspiracycopenhagencopyrightcryptocthulhudaresdragondreamseconomyelectronicsemploymentenvironmentfandomfictionfinancegameshalloweenhardwarekdelatexlinguisticslinkslinuxlivejournalmathmetamusicmythnetworkingnorth-coastnov01occultpandemicpersonalphilosophypoetrypoliticspornographyprivacyprogrammingpsychologypublishingpuddingreferencereligionscifisecuritysexsocialnetsoftwaresonnetspamsportstraveltypographywebwebcomicswinnipegアニメ作りましょう写真宗教日本語日記
© 1997-2025 Matthew Skala