터미널 앱
WindTerm은 SSH·SFTP·Shell·Telnet·Tmux·Serial을 다루는 크로스플랫폼 터미널입니다. 원격 서버에 접속할 때도, 내 컴퓨터에서 셸을 실행할 때도 텍스트를 주고받는 화면을 제공합니다. README는 Windows·macOS·Linux 지원, 세션 관리, 분할 화면, 파일 관리, 자동 완성 등을 설명합니다.
이 저장소에서 배울 핵심은, 서로 다른 방식으로 들어오는 바이트를 일관된 인터페이스로 다루는 설계입니다. 다만 화면 렌더러와 세션을 조립하는 제품 전체 코드는 공개되어 있지 않습니다. 공개된 PTY·프로토콜 보조 코드·위젯·라이브러리를 각각 읽고, 연결을 확인할 수 있는 범위를 표시해야 합니다.
bash·zsh·PowerShell처럼 명령을 해석하고 프로그램을 실행합니다.
SSH는 그 창구와 멀리 있는 컴퓨터 사이를 연결하는 통신 수단입니다.SSH, Telnet, 로컬 PTY는 서로 다른 전화선입니다. 운영체제별 연결 방식도 다릅니다. 그러나 사용자는 같은 키보드로 입력하고 같은 화면에서 답을 읽고 싶습니다.
교환대의 바깥 모양보다 흥미로운 것은 그 안의 변환 장치입니다.
Pty는 운영체제별 차이를 가리고, 이벤트는 데이터 도착을 알리며,
검색기는 텍스트의 실제 저장 위치와 검색 규칙을 분리합니다.
| 상황 | 사용자가 하는 일 | 안쪽에서 만나는 개념 |
|---|---|---|
| 서버 점검 | SSH 세션에서 명령과 로그를 확인 | 소켓, 암호화, 채널, 부분 수신 |
| 로컬 개발 | 셸에서 대화형 프로그램을 사용 | 자식 프로세스, PTY, 창 크기, 이벤트 루프 |
| 대량 출력 탐색 | 스크롤하고 원하는 문자열을 검색 | 64비트 위치, 범위 변환, 정규식, 버퍼 |
src/Widgets/ScrollBar.cpp에서 64비트 위치를 다루는 코드는 확인할 수 있습니다.
이것만으로 제품의 모든 출력 기록이 무제한 저장된다고 증명할 수는 없습니다.
작은 부품의 책임과 제품 기능의 범위를 구별하는 습관 자체가 중요한 학습입니다.
WindTerm은 2026-10-23 GitHub Trending의 C 일간 목록에서 12위로 선정되었습니다. 누적 스타는 32,507개입니다. 순위는 그날의 관심을 보여주지만, 특정 기능이나 사건이 관심 증가의 원인이라는 사실까지 알려주지는 않습니다.
공식 README에 제시된 특징으로 보면, 여러 운영체제에서 쓰는 접속 도구를 하나로 모으고, 세션·파일·화면 분할을 함께 관리하려는 개발자에게 매력적인 구성입니다. 이는 제품 기능에 근거한 해석이며, 트렌딩 알고리즘이나 유입 경로를 측정한 결론은 아닙니다.
| 도구 | 중심 역할 | WindTerm에서 눈여겨볼 점 |
|---|---|---|
| WindTerm | 접속·로컬 셸·파일 전송을 통합한 GUI 클라이언트 | README 기준으로 SFTP 파일 관리, 세션 트리, 분할 화면, 동기 입력을 한 제품에 제공 |
| PuTTY | SSH·Telnet 클라이언트와 터미널 에뮬레이션 | WindTerm은 파일 관리자와 여러 세션의 작업 흐름까지 통합하는 데 초점 |
| Windows Terminal | PowerShell·명령 프롬프트·WSL 등 명령행 프로그램의 화면 호스트 | WindTerm은 화면 호스트에 더해 원격 접속과 파일 전송의 GUI도 제품 기능으로 제시 |
비교의 기준은 기능의 중심입니다. Windows Terminal도 탭과 분할 창을 지원하므로 분할 화면만으로 차별화된다고 말할 수는 없습니다. 역할 설명은 PuTTY 공식 소개와 Windows Terminal 공식 문서를 기준으로 합니다.
README는 동적 메모리 압축으로 작업 메모리의 20~90%를 줄일 수 있다고 설명합니다. 그러나 공개된 부품에서 그 압축 엔진의 전체 구현을 추적할 수는 없습니다. 오래된 벤치마크는 당시의 실험 결과로 읽어야 하며, 현재 제품 사이의 성능 순위로 바꾸어 읽으면 안 됩니다.
| README의 항목 | 기록된 조건과 결과 | 해석할 때의 한계 |
|---|---|---|
| SSH 출력 | WindTerm 1.72: 41.8 MB/s, 메모리 108.5 MB | Windows 10·WSL Ubuntu 18.04.2 등 당시 환경 |
| 백만 줄 출력 | WindTerm: 1.236초, 메모리 16.1 MB | 도구마다 스크롤백 제한과 측정 항목이 다름 |
| 측정 절차 | 캐시 준비 3회 후 5회 측정의 중앙값 | 환경 설명은 있지만 최신 릴리스의 재측정은 아님 |
benchmark/urandom_test.sh는 난수 데이터를 Base64로 바꾸어 파일에 쓰고,
time dd로 출력 시간을 재는 작은 스크립트입니다.
모든 경쟁 도구를 같은 설정으로 자동 실행하는 종합 벤치마크 도구는 아닙니다.
오래전 연비 시험표를 보고 오늘의 자동차 순위를 정할 수는 없습니다. 대신 도로, 속도, 짐의 무게를 함께 적었는지 배울 수 있습니다. 터미널도 출력량, 색상 처리, 기록 보존량, 네트워크 조건을 맞춰야 비교가 의미 있습니다.
이 프로젝트는 웹 서버와 브라우저 프론트엔드로 구성된 서비스가 아닙니다. 여기서 프론트엔드는 Qt 위젯이고, 백엔드에 해당하는 부분은 로컬 프로세스와 통신을 다루는 네이티브 코드입니다. 데이터베이스나 클라우드 배포를 필수 구성으로 추가할 근거는 공개 코드에 없습니다.
| 기술 | 확인한 파일·식별자 | 역할과 버전의 범위 |
|---|---|---|
| C++ / Qt | src/Widgets/ScrollBar.h의 QScrollBar | 스크롤바 확장. Qt 정확한 버전은 공개 빌드 설정에서 고정되지 않음 |
| Qt Widgets | Splitter·PopupWidget | 패널 접기·펼치기, 팝업 구성. 제품의 전체 창 구조는 미공개 |
| Qt Core | QObject·QByteArray·QThread | 이벤트, 바이트 배열, 스레드와 객체 수명 |
Q_OBJECT와 Q_SIGNALS는 Qt의 메타 객체 기능을 사용한다는 증거입니다.
이를 컴파일하려면 보통 Qt의 코드 생성 과정이 필요합니다.
그러나 공개된 최상위 앱 빌드 파일이 없어 이 제품의 Qt 패키징 설정까지 재현할 수는 없습니다.
| 구성 | 파일에서 확인한 값 | 무엇을 담당하는가 |
|---|---|---|
| libssh | src/libssh/CMakeLists.txt: project(libssh VERSION 0.9.5 LANGUAGES C) | SSH 통신, 인증, 채널, SFTP 등의 라이브러리 구현 |
| Onigmo | src/Onigmo/configure.in: AC_INIT(onig, 5.13.5) | 정규식 컴파일·검색과 여러 문자 인코딩 |
| PTY | src/Pty/: Unix·ConPTY·WinPTY 구현 | 셸이 실제 터미널에 연결된 것처럼 입출력 환경 구성 |
| OpenSSL | EVP_aes_256_cbc(), find_package(OpenSSL) | 암호 처리. WindTerm 제품의 정확한 링크 버전은 확인 불가 |
| Boost.Thread | src/Utility/ThreadLocal.h: boost/thread.hpp | 스레드 식별과 스레드별 저장소. 버전 고정값은 없음 |
| Telnet 보조 코드 | src/Protocols/TelnetProtocol.cpp | 명령 이름·옵션 이름·초기 옵션 표. 완전한 통신 루프는 이 파일에 없음 |
src/README.md의 libssh 기반 버전 표기는 0.95입니다.
실제 CMake의 프로젝트 버전은 0.9.5이므로 이 글에서는 빌드 파일 값을 사용합니다.
같은 파일의 LIBRARY_VERSION "4.8.6"은 라이브러리의 ABI 버전 체계이며 제품 버전이 아닙니다.
# src/libssh/CMakeLists.txt에서 발췌
cmake_minimum_required(VERSION 3.3.0)
project(libssh VERSION 0.9.5 LANGUAGES C)
set(LIBRARY_VERSION "4.8.6")
set(LIBRARY_SOVERSION "4")
# src/libssh/DefineOptions.cmake에서 발췌
option(WITH_SFTP "Build with SFTP support" ON)
option(WITH_SERVER "Build with SSH server support" ON)
option(UNIT_TESTING "Build with unit tests" OFF)
위 옵션은 libssh의 기본값입니다. WindTerm이 SSH 서버 제품이라는 뜻도,
배포 바이너리에 이 기본값을 그대로 적용했다는 뜻도 아닙니다.
WITH_GCRYPT·WITH_MBEDTLS 옵션과 OpenSSL 탐색 분기를 통해 암호 백엔드를 고릅니다.
| 항목 | 확인 내용 | 주의할 구분 |
|---|---|---|
| 의존성 하한 | CMake 3.3.0, 선택 분기의 GCrypt 1.5.0 | 각 라이브러리 빌드 파일의 요구값이며 앱 전체 최소 환경은 아님 |
| CI | src/libssh/.gitlab-ci.yml에 OpenSSL 1.0.x·1.1.x 작업 | 동봉된 libssh의 CI 정의. 현재 WindTerm CI 성공을 보장하지 않음 |
| 테스트 도구 | UNIT_TESTING에서 CMocka 탐색, CI의 ctest | 단위·클라이언트·서버·퍼징 테스트 설정을 읽을 수 있음 |
| Onigmo 빌드 | src/Onigmo/configure.in·src/Onigmo/Makefile.am·src/Onigmo/win32/Makefile | Autotools와 Windows용 빌드 설정 |
| 앱 최상위 | 앱용 CMake·qmake 프로젝트 및 Node.js·Rust·Go·Python 패키지 명세 없음 | 일반적인 한 줄 명령으로 전체 앱을 빌드할 수 있다고 안내할 수 없음 |
src/Pty/Pty.h의 생성자는 인자가 없는데 src/Pty/UnixPty.cpp는 Pty(parent)를 호출합니다.
createProcess()의 환경 인자도 QStringList와 QProcessEnvironment로 다릅니다.
src/Pty/Pty.h의 #include "Public/Spin.h"에 대응하는 파일 역시 공개 트리에 없습니다.
이런 차이 때문에 부품의 설계 의도는 읽을 수 있어도, 현재 파일 묶음의 즉시 컴파일 가능성을 전제해서는 안 됩니다.
의존성의 버전, 라이브러리의 빌드 옵션, 제품의 실제 배포 구성을 서로 구분하는 것이 스택 분석의 출발점입니다.
아래는 제품의 역할을 배치한 개념도입니다. 폴더에 해당 부품이 있다는 것은 확인되지만, 미공개 세션 관리자에서 각 부품을 실제로 호출하는 경로는 확인할 수 없습니다. 특히 터미널 제어 시퀀스 해석기·화면 렌더러·스크롤백 압축기를 이 그림만으로 복원할 수는 없습니다.
아키텍처를 읽는다는 것은 빈칸을 그럴듯하게 채우는 일이 아니라, 확인된 경계와 계약을 설명하는 일입니다.
src/Pty/Pty.h는 createProcess()·readAll()·
write()·resizeWindow()를 순수 가상 함수로 선언합니다.
상위 코드는 운영체제의 구체적인 핸들 대신 이 작업 목록을 바라보도록 설계되어 있습니다.
앞 절의 선언 불일치 때문에, 공개 구현들이 현재 그대로 같은 계약을 만족한다고 단정할 수는 없습니다.
src/Pty/UnixPty.cpp의 UnixPty::createProcess()는
posix_openpt()로 마스터를 열고, grantpt()·unlockpt() 뒤 슬레이브를 엽니다.
ShellProcess::setupChildProcess()는 슬레이브를 자식 프로세스의 표준 입출력에 연결합니다.
QSocketNotifier는 이름과 달리 소켓 이외의 파일 디스크립터도 감시합니다.
읽을 준비가 된 사실을 Qt 이벤트 루프에 연결하는 용도입니다.
다만 이 콜백은 EAGAIN이면 실행을 양보한 뒤 읽기를 재시도하므로, 비차단 설정만으로 콜백 전체가 즉시 반환한다고 볼 수는 없습니다.
자세한 계약은 Qt 공식 문서에서 확인할 수 있습니다.
이 링크의 Qt 6 문서는 개념 학습용이며 WindTerm의 Qt 버전을 뜻하지 않습니다.
Windows의 src/Pty/ConPty.cpp는 다른 방식을 택합니다.
PipeThread::run()이 512바이트 버퍼로 ReadFile()을 호출하고,
ConPty::appendBuffer()가 잠금 안에서 데이터를 추가한 뒤 잠금을 풀고 신호를 보냅니다.
// ConPty::appendBuffer()의 핵심 부분
{
ThreadLocker<SpinMutex> locker(m_mutex);
m_buffer.append(buffer);
}
emit readyRead();
바깥 중괄호는 장식이 아닙니다. locker의 소멸 시점을 정해서
신호 처리 과정까지 잠금을 붙잡지 않게 합니다.
다만 신호 수신 객체의 연결 방식과 스레드는 호출부를 봐야 하므로,
신호가 언제나 GUI 스레드에서 즉시 실행된다고 가정해서는 안 됩니다.
src/libssh/src/packet.c의 ssh_packet_socket_callback()은
수신된 바이트가 곧 완성된 SSH 패킷이라고 가정하지 않습니다.
session->packet_state를 확인하고 필요한 길이가 모였을 때 다음 단계로 진행합니다.
packet_len > MAX_PACKET_LEN 같은 검사는 네트워크 입력을 신뢰하지 않는 경계입니다.
패킷 처리를 깨우는 src/libssh/src/session.c의 ssh_handle_packets()는
폴링 컨텍스트를 준비하고 ssh_poll_ctx_dopoll(ctx, tm)을 호출합니다.
상위 API인 src/libssh/src/channels.c의 ssh_channel_read()는
ssh_channel_read_timeout()으로 위임합니다.
요청한 크기보다 적게 읽을 수 있고, 비차단 모드에서는 데이터가 아직 없을 수도 있습니다.
이 구현에서 0은 EOF뿐 아니라 비차단 모드에서 읽을 데이터가 없는 경우에도 반환되므로, 반환값만으로 EOF를 단정하면 안 됩니다.
따라서 버퍼 크기, 패킷 크기, 한 번의 API 반환 길이는 각각 다른 개념입니다.
택배 상자 한 개가 여러 차례에 걸쳐 도착하고, 한 번의 배송에 상자 여러 개가 섞일 수도 있다고 생각해 보세요. TCP는 배송 흐름이고 SSH 패킷은 상자입니다. 상태 기계는 어디까지 상자를 조립했는지 기억하는 작업표입니다.
src/Onigmo/oniguruma.h의 OnigIterator는
문자 접근 함수와 저장소 포인터를 묶습니다.
검색 알고리즘은 메모리가 연속인지 직접 결정하는 대신, 지정한 위치의 값을 읽는 함수를 호출할 수 있습니다.
// src/Onigmo/oniguruma.h의 선언에서 발췌
typedef UChar (*OnigCharAtFunc)(OnigPosition pos, const void* ptr);
typedef struct OnigIteratorStruct {
OnigCharAtFunc at;
const void* ptr;
} OnigIterator;
#define ONIG_CHARAT(pos) (it->at(pos, it->ptr))
src/Onigmo/sample/simple.c는 onig_default_charat와 연속 문자열로 반복자를 만들고,
onig_search(&it, ...)에 전달합니다.
src/Onigmo/regexec.c의 onig_match()·onig_search()에도 이 인자가 나타납니다.
src/README.md는 이 확장이 갭 버퍼나 인접하지 않은 메모리 블록의 검색을 가능하게 한다고 설명합니다.
함수 포인터 기반 API는 그 설명과 부합합니다.
다만 WindTerm의 실제 스크롤백 저장 구조와 연결하는 호출부는 공개되지 않았습니다.
src/Utility/ScopeGuard.h는 생성자에서 획득 함수를 호출하고,
소멸자에서 활성 상태를 확인하여 해제 함수를 실행합니다.
복사는 금지하고, 이동 생성자는 원본을 cancel()하여 정리 책임을 넘깁니다.
// src/Utility/ScopeGuard.h의 소멸자에서 발췌
~ScopeGuard() {
if (m_active) {
m_release();
}
}
// 학습용 사용 예: acquireResource/releaseResource는 예시용 함수명
ScopeGuard guard(
[] { acquireResource(); },
[] { releaseResource(); }
);
핵심은 함수 끝에 해제 코드를 외워서 붙이는 것이 아니라, 객체의 수명에 정리 책임을 붙이는 것입니다.
src/Utility/Spin.h의 ThreadLocker도 기본 설정에서는 생성 시 잠그고 소멸 시 해제합니다.
획득 도중 실패했을 때의 정리와 해제 함수의 예외 처리는 별도로 설계해야 합니다.
ConPty::resizeWindow()는 행과 열이 모두 달라야 호출하는 && 조건을 사용합니다.
성공 뒤 대입도 rows = m_rows; columns = m_columns;라서 멤버 갱신 방향과 다릅니다.
한 축만 바뀌는 사례를 표로 추적하기 좋은 검토 대상입니다.
ConPty::createProcess()는 일부 실패에서 오류 코드를 설정한 뒤에도 마지막에 true를 반환합니다.
이는 이 커밋의 함수 본문에서 확인한 사실입니다. 2.7.0 바이너리에 같은 코드가 쓰였는지는 확인되지 않습니다.
커밋 a8336c1의 추적 파일은 536개입니다.
아래 수치는 확장자로 묶어 줄바꿈 문자를 센 물리적 줄 수입니다.
빈 줄·주석·테스트·생성된 데이터 표도 포함하므로 순수 구현량이나 작성자의 기여량과 같지 않습니다.
| 종류 | 파일 수 | 물리적 줄 수 |
|---|---|---|
| C 소스 (.c) | 240 | 132,235 |
| C++ 소스 (.cpp) | 13 | 1,802 |
| 헤더 (.h) | 91 | 43,692 |
| Python (.py) | 5 | 1,800 |
| Ruby (.rb) | 4 | 2,139 |
| 셸 (.sh) | 5 | 8,461 — 생성된 빌드 보조 스크립트 포함 |
| C·C++·헤더를 부품별로 합산 | 파일 수 | 줄 수 |
|---|---|---|
| libssh | 252 | 106,158 |
| Onigmo | 67 | 68,659 |
| Protocols / Pty | 3 / 8 | 333 / 1,174 |
| Utility / Widgets | 8 / 6 | 726 / 679 |
| 전체 | 344 | 177,729 |
두 동봉 라이브러리가 이 합계의 약 98.4%를 차지합니다. 따라서 GitHub의 주 언어 C 표시는 자연스럽지만, 제품 GUI까지 C로만 구현되었다는 뜻은 아닙니다. 라이브러리 밖 네 폴더의 공개 C++·헤더는 합계 2,912줄이라 작은 단위로 읽기 좋습니다.
src/Utility/ScopeGuard.h — 57줄. 생성·이동·소멸을 종이에 추적합니다.src/Utility/CircularBuffer.h — 89줄. 인덱스와 실제 저장 위치를 구분합니다.src/Widgets/ScrollBar.cpp — 152줄. 모델 값과 UI 값의 변환을 읽습니다.src/Pty/UnixPty.cpp — 327줄. 파일 디스크립터와 이벤트를 연결합니다.src/Onigmo/sample/simple.c 뒤 src/Onigmo/regexec.c — 사용 계약부터 구현으로 내려갑니다.수천 줄짜리 검색 엔진을 첫 페이지부터 정독하는 것보다, 작은 API의 입력·출력·실패 조건을 정리한 뒤 그 API가 쓰는 내부 함수를 따라가는 편이 이해하기 쉽습니다.
src/Utility/CircularBuffer.h의 append()는
m_buffer[++m_currentIndex & m_mask] = value;로 저장합니다.
길이가 2의 거듭제곱일 때 index & (capacity - 1)로 저장 칸을 순환시킬 수 있습니다.
// 실제 용량이 8칸인 경우의 계산 예
capacity = 8
mask = 7 // 이진수 0111
논리 인덱스 0 1 2 3 4 5 6 7 8 9
저장 칸 0 1 2 3 4 5 6 7 0 1
// 이 구현에서 8칸을 만드는 요청값의 한 예는 7입니다.
// 생성자에 8을 전달하면 아래 매크로 때문에 실제 용량은 16이 됩니다.
물리 위치는 재사용되어도 논리 인덱스는 계속 증가합니다.
minIndex()는 max(0, currentIndex + 1 - capacity)로
아직 덮어쓰이지 않은 데이터의 시작점을 계산합니다.
| 관찰 지점 | 실제 구현 | 배울 질문 |
|---|---|---|
| 용량 올림 | 1 << (32 - qCountLeadingZeroBits(value)) | 요청값 7·8·9가 각각 8·16·16이 되는가? |
| 템플릿 | T* 멤버에 qint64* 할당, 크기도 sizeof(qint64) | 이름과 달리 임의의 T를 지원한다고 볼 수 있는가? |
| 구체 타입 | Int64CircularBuffer : CircularBuffer<qint64> | 실제 의도가 64비트 정수 전용이었다면 API를 어떻게 명확히 할까? |
| 범위 검사 | at()·연산자의 Q_ASSERT | 릴리스 빌드에서 잘못된 인덱스를 어떻게 다룰까? |
실습 아이디어: 실제 용량 8, 현재 인덱스 9일 때 유효 범위는 2~9입니다. 저장 칸 0·1에 무엇이 남는지 그려 보세요. 그런 다음 요청 용량의 의미를 “이상인 최소 거듭제곱”과 “초과하는 최소 거듭제곱” 중 하나로 정해 명세를 써 봅니다.
src/Widgets/ScrollBar.h는 최소·최대·현재 값을 qint64로 보관합니다.
반면 바탕 위젯의 범위를 사용할 때는 INT_MAX를 기준으로 값을 축소합니다.
ScrollBar::setValue()와 createConnections()를 왕복해서 읽어야 설계가 보입니다.
// minimum=0인 경우, 구현을 수식으로 요약
// maximum이 INT_MAX보다 클 때만 비례 변환
UI 값 ≈ 논리 값 × INT_MAX / maximum
논리 값 ≈ UI 값 × maximum / INT_MAX
// 예: maximum=10,000,000,000, 논리 값=5,000,000,000
UI 값 ≈ INT_MAX / 2
막대를 끌면 넓은 범위의 대략적인 위치로 이동하고,
한 줄 이동에서는 m_value + singleStep()처럼 논리 값을 직접 갱신합니다.
QSignalBlocker는 내부 위젯 값을 바꾸는 동안 신호를 막아 불필요한 재진입을 줄입니다.
비례 변환에는 부동소수점 연산과 정수 변환에 따른 오차가 있습니다. 또 이 구현은 maximum 중심의 수식을 사용하므로
최소값이 0이 아닌 범위도 같은 품질로 다룰 수 있는지는 별도로 따져야 합니다.
64비트 API가 있다는 사실만으로 모든 경계 조건이 해결되지는 않습니다.
실습 아이디어: 최대값이 INT_MAX - 1, INT_MAX,
INT_MAX + 1인 세 경우를 비교하세요.
드래그와 한 줄 이동에서 각각 어떤 정밀도가 필요한지 설명해 봅니다.
src/Utility/Spin.h의 SpinMutex는
atomic_flag의 acquire/release 연산으로 잠금을 구현합니다.
실패 횟수에 따라 처음에는 즉시 재시도하고, 이후에는 yield(), 100나노초 대기,
10마이크로초 대기로 양보합니다. 지정 시간은 운영체제가 보장하는 실제 대기 시간과 같지 않습니다.
src/Protocols/TelnetProtocol.cpp의 GetOptionVector()는
정적 벡터와 std::once_flag를 두고 std::call_once()로 초기화합니다.
하지만 반환형이 수정 가능한 참조이므로 초기화 이후의 읽기·쓰기까지 보호하는 것은 아닙니다.
call_once와 mutex는 서로 다른 시점의 문제를 해결합니다.실습 아이디어: 두 스레드가 GetOptionVector()의 결과를 받아
같은 옵션 상태를 바꾸는 순서를 종이에 그리세요.
세션별 상태라면 공유 정적 벡터가 적절한지, 불변 기본값을 복사하는 설계가 적절한지 토론할 수 있습니다.
src/Utility/Cryptographic.cpp는 비밀번호로 키를 유도하고 데이터를 암호화하는 유틸리티입니다.
src/README.md는 마스터 비밀번호로 사용자 데이터와 개인 키 등을 보호하는 데 사용한다고 설명합니다.
호출부와 저장 형식 전체는 이 파일만으로 확인할 수 없습니다.
| 단계 | 실제 함수·설정 | 뜻 |
|---|---|---|
| 키 유도 | deriveKeyPbkdf2(Sha3_512, ..., 100000, 48) | PBKDF2를 100,000회 적용해 48바이트 유도 |
| 분리 | key() 32바이트, iv() 16바이트 | 같은 유도 결과의 앞·뒤 부분을 키와 IV로 사용 |
| 암·복호화 | doCrypt(), EVP_aes_256_cbc() | OpenSSL의 AES-256-CBC 사용 |
| 저장 표현 | encrypt()의 toBase64() | 암호화된 바이트를 텍스트로 표현 |
| 솔트 | generate64() → 십진 문자열 → SHA-512 → Base64 | 해시 출력 길이와 난수의 원래 엔트로피는 별개 |
검토할 지점도 있습니다. doCrypt()는 EVP 호출의 반환값을 확인하지 않으며,
이 함수 안에는 인증 태그나 MAC을 검사하는 코드가 없습니다.
동일한 유도 결과를 재사용하면 IV도 같아지지만, 실제 저장 단위마다 솔트를 새로 만드는지는 호출부가 필요합니다.
OpenSSL EVP 공식 문서의 반환값과 인증 암호 관련 설명을 함께 읽으면, 알고리즘 선택 외에도 실패 처리와 무결성 검증이 필요함을 배울 수 있습니다. 이 파일을 검증된 비밀번호 저장 예제로 그대로 복제하지는 마세요.
실습 아이디어: 키 유도·암호화·인코딩을 서로 다른 색으로 표시하고, “Base64는 암호화인가?”, “48바이트 출력이 48바이트의 난수를 뜻하는가?”에 답을 써 봅니다.
src/Onigmo/enc/에는 UTF-8·UTF-16·EUC-KR 등 여러 인코딩 구현이 있습니다.
src/Onigmo/sample/simple.c는 ASCII 예제이므로 그대로 한글 검색의 동작을 설명하는 근거로 쓰면 안 됩니다.
유니코드 코드 포인트, 인코딩된 바이트 수, 화면에서 차지하는 칸 수는 구분해야 합니다.
실습 아이디어: 문자열 A한B의 UTF-8 바이트 경계와 문자 경계를 따로 표시하고,
반복자 콜백이 반환하는 UChar가 저장 단위를 읽는다는 점을 설명해 보세요.
문자 경계를 해석하는 일은 선택한 인코딩 처리와 함께 이루어집니다.
좋은 학습은 작동 원리의 설명에서 끝나지 않고, 그 원리가 성립하는 조건과 실패하는 경계까지 묻습니다.
README는 Windows·macOS·Linux 바이너리를 안내하지만, CPU·RAM·디스크의 공식 최저 사양을 정리한 표는 제시하지 않습니다. 다음 표에서 측정 환경을 설치 최소 조건으로 오해하지 않는 것이 중요합니다.
| 구분 | 확인된 사실 | 독자가 판단할 점 |
|---|---|---|
| 운영체제 | README 기준 Windows·macOS·Linux 지원 | 릴리스 자산에서 자신의 OS·CPU 아키텍처에 맞는 파일 확인 |
| CPU·메모리 | 과거 벤치마크: 2.3 GHz Intel Core i5, RAM 8 GB | 테스트에 사용한 장비이며 최저 사양이나 권장 사양은 아님 |
| macOS 측정 | README에 macOS 10.13 환경 기재 | 2.7.0의 최소 지원 OS 버전이라고 해석하면 안 됨 |
| GPU | 공개 자료에서 별도 GPU 최소 조건 확인 불가 | 일반 네이티브 GUI 읽기 학습에 전용 GPU를 요구할 근거 없음 |
| 소스 분석 | 텍스트 편집기, 검색 도구, 파일 열람 환경 | 전체 앱 빌드 없이도 이 글의 정적 분석 과제를 진행 가능 |
| 전체 앱 빌드 | 앱용 최상위 빌드 정의와 일부 연결부 미공개 | 컴파일러나 RAM을 추가해도 공개 범위의 빈칸이 해결되지는 않음 |
ConPty::isAvailable()는 커널 빌드 번호를 꺼내
CONPTY_MINIMAL_WINDOWS_VERSION과 비교합니다.
이 커밋의 상수는 18309이며 오류 문구에는 Windows 10 1809가 언급됩니다.
서로 다른 표기를 하나의 정확한 제품 최소 조건으로 합치지 않는 편이 옳습니다.
src/Pty/WinPty.cpp에는 winpty-agent.exe와 winpty.dll을 앱 실행 파일의 디렉터리에서 찾는 코드가 있습니다.
다른 백엔드가 있다는 사실과 제품이 언제 어느 백엔드를 선택하는지는 별개입니다.
실제 선택 로직은 공개된 이 파일들에서 확인되지 않습니다.
ConPTY를 별도 학습 프로젝트에서 다룬다면 파이프와 자식 프로세스의 수명도 함께 배워야 합니다. Microsoft의 ConPTY 세션 문서는 통신 채널을 먼저 만들고 프로세스를 연결하는 절차와 스레드·핸들 관리상의 주의점을 설명합니다.
소스 읽기는 평소 개발에 쓰는 노트북이면 시작할 수 있습니다. 실행 실험으로 확장할 때는 공식 최저 사양을 추정하기보다, 작은 출력량부터 시작해 프로세스 메모리와 반응 시간을 직접 기록하세요. 이 절의 제안은 학습 방법이며 제품의 공식 권장 사양이 아닙니다.
같은 컴퓨터에서도 스크롤백을 얼마나 보존하는지, 로컬 셸인지 SSH인지에 따라 측정 의미가 달라집니다. 기준 조건 없이 메모리 숫자 하나만 비교하면 성능을 잘못 이해하기 쉽습니다.
아래 과제는 소스를 읽고 표·설계·의사코드를 만드는 방식으로 진행할 수 있습니다. 구현을 추가하고 싶다면 원본과 분리된 학습 프로젝트로 확장하세요. 각 과제의 목표는 코드를 많이 쓰는 것이 아니라, 입력과 출력의 계약을 스스로 검증할 수 있게 만드는 것입니다.
읽을 곳: src/Pty/Pty.h, src/Pty/UnixPty.cpp, src/Pty/ConPty.cpp.
할 일: write()·readAll()·readyRead()의 역할을 구분하고,
Unix와 Windows의 출력 수신 경로를 각각 여섯 칸 이하의 흐름도로 만드세요.
완료 기준: 각 화살표에 실제 함수명을 하나 이상 적고,
미공개 GUI 연결부는 확인 불가라고 표시합니다. readyRead() 자체가 출력 문자열을 인자로 보내는지 선언으로 답해 보세요.
# 저장소 루트에서 선언과 구현을 찾는 읽기 전용 명령
rg -n 'readyRead|readAll|write\(' src/Pty
rg -n 'QSocketNotifier|ReadFile|appendBuffer' src/Pty
읽을 곳: src/Utility/CircularBuffer.h.
할 일: 요청 용량 7로 시작해 0~11을 차례로 추가한 표를 만드세요.
매 단계에 실제 용량, currentIndex(), minIndex(), 저장 칸을 적습니다.
완료 기준: 마지막 유효 인덱스가 4~11이고,
저장 칸 0~7의 값이 순서대로 8, 9, 10, 11, 4, 5, 6, 7임을 설명합니다.
요청 용량을 8로 바꾸었을 때 실제 용량이 왜 달라지는지도 계산합니다.
읽을 곳: src/Widgets/ScrollBar.cpp의
setRange()·setValue()·createConnections().
할 일: 범위 [0, 10,000,000,000]에서 양 끝과 중간값의 UI 위치를 계산하세요.
최소값을 5,000,000,000으로 바꾸면 기존 수식의 어떤 가정이 깨지는지 검토합니다.
완료 기준: 드래그의 근사 오차와 한 줄 이동의 정확도를 따로 정의합니다.
개선 수식의 초안은 (value - minimum) / (maximum - minimum)에서 시작하되,
길이가 0인 범위와 중간 곱셈의 오버플로도 조건에 포함합니다.
읽을 곳: src/Onigmo/oniguruma.h, src/Onigmo/sample/simple.c.
할 일: "abc"와 "def"를 따로 저장하되
논리적으로 "abcdef"처럼 읽는 at(pos, ptr) 콜백의 의사코드를 작성하세요.
완료 기준: 위치 2는 'c', 위치 3은 'd'입니다.
조각 경계를 가로지르는 "cde"의 예상 매치 범위를 [2, 5)로 적고,
전체 문자열을 합치지 않아도 읽을 수 있는 이유와 위치 유효 범위를 설명합니다.
// 학습용 의사코드: 원본 구현이 아니며 ASCII 바이트를 가정
at(pos, pair):
assert 0 <= pos < pair.leftSize + pair.rightSize
if pos < pair.leftSize:
return pair.left[pos]
return pair.right[pos - pair.leftSize]
읽을 곳: src/Pty/Pty.h와 세 운영체제 구현의
createProcess()·resizeWindow()·stop().
할 일: 생성 성공·실패, 크기 변경, 종료의 상태도를 만들고, 각 자원의 소유자와 해제 함수를 연결합니다. 인터페이스를 통일한다면 어떤 선언을 바꿔야 하는지도 적습니다.
완료 기준: 아래 사례에서 기대 반환값·기대 상태·실제 코드 분기를 나란히 비교합니다. 코드만으로 확정할 수 없는 API 실패 동작은 추가 확인이 필요한 항목으로 남깁니다.
| 사례 | 검증할 계약 |
|---|---|
| 실행 파일이 없음 | 실패 반환과 오류 정보가 일치하는가? |
| 파이프 생성 실패 | 이미 생성한 핸들을 한 번씩 해제하는가? |
| 행만 변경 / 열만 변경 | 한 축의 변경도 운영체제에 전달하는가? |
| 크기 변경 성공 / 실패 | 성공했을 때만 멤버의 크기가 갱신되는가? |
| 읽기 중 종료 | 읽기 스레드가 끝나기 전에 사용 중 자원이 해제되는가? |
| 반복 종료 | 두 번째 종료에서 같은 핸들을 다시 해제할 가능성이 있는가? |
C++ 문법을 전부 익힌 뒤 시작할 필요는 없습니다. 생성자·소멸자·참조·포인터를 읽을 수 있다면 작은 헤더부터 들어가고, 모르는 개념을 해당 주차의 결과물을 만드는 데 필요한 만큼 보충하면 됩니다.
| 주차 | 배울 주제 | 읽을 코드 | 남길 결과물 |
|---|---|---|---|
| 1주차 | C++ 객체 수명, 복사와 이동, RAII | src/Utility/ScopeGuard.h, src/Utility/Spin.h | 정리 함수가 호출되는 시점과 책임 이동 표 |
| 2주차 | 링 버퍼, 비트 연산, 범위 검사 | src/Utility/CircularBuffer.h | 덮어쓰기 표, 용량 정책, 경계값 명세 |
| 3주차 | Qt 신호·슬롯, 수치 모델, 이벤트 재진입 | src/Widgets/ScrollBar.cpp, src/Widgets/Splitter.cpp | UI 좌표 변환식과 신호 발생 순서도 |
| 4주차 | 프로세스, 파일 디스크립터, PTY, 비차단 입출력 | src/Pty/Pty.h, src/Pty/UnixPty.cpp, src/Pty/ConPty.cpp | 운영체제별 수신 경로와 실패 계약 |
| 5주차 | TCP 스트림, SSH 채널, 패킷 상태 기계 | src/libssh/src/packet.c, src/libssh/src/channels.c | 쪼개진 패킷의 상태 전이표와 반환값 사전 |
| 6주차 | 정규식 반복자, 인코딩, 의존성·CI 읽기 | src/Onigmo/sample/simple.c, src/libssh/CMakeLists.txt | 분할 버퍼 콜백 설계와 버전·옵션 근거표 |
표의 경로는 저장소 루트 기준이며, src/Utility/·src/Widgets/·src/Pty/와
라이브러리 코드가 있는 src/ 아래를 가리킵니다.
구현 순서는 자신이 쓰는 운영체제부터 시작해도 좋습니다.
공부의 결과물은 파일을 몇 개 읽었다는 기록보다, 다른 사람이 검토할 수 있는 계약·상태도·경계값 표일 때 더 오래 남습니다.
6주 뒤에는 링 버퍼나 분할 문자열 읽기처럼 범위가 작은 문제를 골라, 타입·용량·실패 조건부터 직접 정해 보세요. 제품 전체를 재현하려는 목표보다 하나의 계약을 끝까지 책임지는 경험이 이 코드와 잘 맞습니다.
| 용어 | 쉬운 설명 |
|---|---|
| 터미널 에뮬레이터 | 글자·색·커서 이동 같은 터미널 규칙을 화면에서 구현하는 프로그램 |
| 셸 | 입력한 명령을 해석하고 다른 프로그램을 실행하는 프로그램 |
| SSH | 원격 컴퓨터에 암호화된 연결로 접속하는 프로토콜 |
| SFTP | SSH 연결 위에서 파일과 디렉터리를 다루는 프로토콜. FTP에 TLS를 붙인 FTPS와 다름 |
| Telnet | 명령과 옵션 협상을 포함하는 터미널 통신 프로토콜. 기본 프로토콜은 내용을 암호화하지 않음 |
| Serial | 직렬 포트로 바이트를 주고받는 통신 방식. 네트워크 접속과는 연결 경로가 다름 |
| Tmux | 여러 터미널 작업을 세션과 창·분할 영역으로 관리하는 터미널 멀티플렉서 |
| PTY | 셸에 터미널처럼 보이는 가상 장치. 마스터·슬레이브 쌍으로 입출력을 연결 |
| ConPTY | Windows에서 외부 프로그램이 콘솔 입출력을 호스팅하도록 돕는 의사 콘솔 기능 |
| 파일 디스크립터 | 운영체제가 열린 파일·장치·소켓 등을 식별하도록 주는 정수 번호 |
| 이벤트 루프 | 입력·데이터 도착·타이머 같은 사건을 받아 알맞은 처리 함수를 호출하는 반복 구조 |
| 비차단 입출력 | 지금 처리할 데이터가 없을 때 오래 기다리지 않고 제어를 돌려주는 방식 |
| 신호·슬롯 | Qt에서 사건을 알리는 쪽과 반응하는 쪽을 연결하는 장치 |
| 상태 기계 | 현재 단계와 새 입력에 따라 다음 단계·행동을 정하는 구조 |
| 부분 수신 | 하나의 논리 메시지가 여러 번의 읽기로 나뉘어 들어오는 상황 |
| RAII | 객체 생성·소멸의 수명에 자원 획득·정리를 묶는 C++ 관용구 |
| 이동 의미론 | 값을 모두 복제하기보다 자원 관리 책임을 다른 객체로 넘기는 규칙 |
| 링 버퍼 | 고정된 저장 공간을 순환해서 사용하며 오래된 자리를 다시 쓰는 자료구조 |
| 비트 마스크 | 비트 연산으로 필요한 부분을 골라내는 값. 여기서는 순환 저장 칸 계산에 사용 |
| 스핀 잠금 | 잠금을 얻을 때까지 반복 시도하는 잠금. 이 구현은 대기 중 양보와 짧은 수면도 사용 |
| 반복자·접근 콜백 | 데이터가 어디 저장되었는지 감추고, 지정 위치의 값을 읽는 방법을 제공하는 장치 |
| 갭 버퍼 | 텍스트 중간에 빈 공간을 두어 커서 근처의 삽입·삭제 비용을 줄이는 저장 구조 |
| 스크롤백 | 현재 화면 위로 지나간 출력을 나중에 다시 볼 수 있도록 남긴 기록 |
| PBKDF2 / 솔트 | 비밀번호에서 키를 유도하는 반복 계산 / 같은 비밀번호도 다른 결과를 만들도록 넣는 별도 값 |
| IV | 암호화 모드가 사용하는 초기화 값. 비밀 키와 다른 역할이며 모드에 맞는 조건이 필요 |
| MAC / 인증 태그 | 데이터가 변조되지 않았는지 확인하는 값. 데이터를 숨기는 암호화와 역할이 다름 |
| Base64 | 임의의 바이트를 텍스트로 표현하는 인코딩. 비밀을 지켜주는 암호화는 아님 |
| ABI / 소스 버전 | 컴파일된 코드 사이의 호출·데이터 약속 / 소스 배포판의 버전. 숫자가 같을 필요는 없음 |
| 동봉 의존성 | 외부 라이브러리의 소스를 저장소 안에 함께 보관하는 방식 |
| 퍼징 | 다양하거나 비정상적인 입력을 대량으로 넣어 충돌·오류를 찾는 시험 방법 |
| 항목 | 상태와 근거의 범위 |
|---|---|
| 선정 | GitHub Trending C Daily #12 · 2026-10-23 |
| 스타 / 포크 | 32,507 / 2,482 · 2026-10-23 GitHub 기준 |
| 생성일 | 2019-10-09 |
| 최신 릴리스 | 2.7.0 · 2025-03-11 |
| 최근 push | 2025-03-11 · 저장소 메타데이터의 push 시각 |
| 분석한 최근 커밋 | a8336c1 · 2025-02-10 · README.md 갱신. push·릴리스 시각과 별개 |
| 주 언어 | GitHub 분류 C. GUI·유틸리티 등에는 C++·Qt 사용 |
| 코드 규모 | C 132,235줄 · C++ 1,802줄 · 헤더 43,692줄. 추적 파일 536개 |
| 동봉 라이브러리 | libssh 0.9.5, Onigmo 5.13.5. 앱 배포판의 전체 의존성 버전은 확인 불가 |
| 라이선스 분류 | GitHub의 저장소 라이선스 필드: 없음. 공개 소스에는 src/LICENSE의 Apache-2.0 본문 존재 |
| 구성 요소별 고지 | libssh에 LGPL-2.1 본문, Onigmo에 BSD 계열 고지, PTY 원본의 MIT 고지 존재 |
| 공개 범위 | README가 부분 오픈소스라고 명시. 전체 앱의 재현 가능한 빌드·렌더링 경로는 확인 불가 |
GitHub의 언어·라이선스 분류와 실제 파일의 구현·고지는 서로 다른 층위의 정보이므로 함께 읽어야 합니다.
① 부분 공개입니다. 전체 세션 관리자·렌더러·압축 엔진과 앱 빌드 설정이 공개되어 있지 않습니다. 이 글의 개념도를 제품 전체의 실제 호출 그래프로 받아들이면 안 됩니다.
② 커밋과 릴리스가 다릅니다. 코드는 a8336c1 기준입니다.
이 코드의 이상 징후가 2.7.0 바이너리에서도 발생하는지는 실행 검증 없이 판단할 수 없습니다.
본문의 성능 수치는 README에 기록된 과거 결과이며 새 측정값이 아닙니다.
③ 라이선스를 하나로 뭉뚱그리지 마세요. README는 공개한 소스에 Apache-2.0을 설명하고 상용·비상용 사용이 무료라고 안내합니다. 동시에 동봉 라이브러리에는 별도 고지가 있습니다. GitHub 필드가 비었다는 이유로 라이선스 고지가 전혀 없다고 적는 것도 부정확합니다.
④ 보안과 연결은 별도 검토가 필요합니다. Telnet의 기본 통신은 암호화되지 않습니다. 암호 유틸리티의 알고리즘 이름만으로 제품 전체의 자격 증명 보호를 보장할 수도 없습니다. 학습에는 실사용 비밀번호나 운영 서버 대신 테스트 데이터와 독립 환경을 사용하세요.
⑤ 유지보수 상태를 단정하지 마세요. 최근 push·릴리스의 날짜는 제시했지만, 날짜만으로 개발 중단이나 향후 지원 여부를 확정할 수는 없습니다. 실제 도입 전에는 공식 릴리스와 이슈를 확인해야 합니다.