TrendShift 딥다이브 · 2026-08-10 · Daily #20

witr 딥다이브
— "이거 왜 돌고 있지?"를 한 명령으로 역추적하는 프로세스 계보 추적기

pranshuparmar/witr는 이름 그대로 Why Is This Running — "이 프로세스·이 포트·이 컨테이너·이 파일이 대체 실행 중인가"를 답해 주는 진단 도구다. 정체불명의 대상을 그것을 실행시킨 인과 사슬(예: systemd → pm2 → node)로 거꾸로 걸어 올라가 한 줄로 보여준다. Go로 짠 단일 정적 바이너리 하나이며, 리눅스·맥·윈도우·FreeBSD에서 똑같이 돌고, 스크립트에 붙이기 좋은 인터랙티브 TUI까지 들어 있다. 2025년 말 공개 후 8개월 만에 별 2만 개를 넘겼다.
(저장소: pranshuparmar/witr · Go 1.25 · Apache-2.0 · 최신 v0.3.3 · Linux/macOS/Windows/FreeBSD × amd64/arm64 · 의존성 9개 · ⭐20.9k · 작성자 Pranshu Parmar · TrendShift Daily #20)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 지금 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

"프로세스·포트·컨테이너·파일을 그걸 시작시킨 근원까지 거꾸로 추적하는 CLI + TUI"

witr(Why Is This Running)는 시스템 위에서 뭔가가 돌고 있을 때 "그게 무엇인가"가 아니라 "그게 거기 있는가"를 답한다. 대상은 네 가지 형태로 지정할 수 있다 — 프로세스 이름·PID, 네트워크 포트, 열린 파일, 컨테이너. witr는 그 대상을 우선 하나의 PID로 환원한 뒤, 그 PID의 부모의 부모의 부모…PID 1(init)까지 거슬러 올라가 "누가 이걸 낳았는지"의 족보를 만든다.

한눈에 이해하는 비유

ps·lsof가 "현장 사진"이라면, witr는 "가계도"다

ps·top·lsof·ss는 지금 이 순간의 상태를 한 장의 스냅샷으로 보여준다. "8080 포트를 node가 쓰고 있다"까지는 알려주지만, 그 node를 누가 띄웠는지는 여러 도구의 출력을 머릿속에서 이어 붙여야 겨우 짐작한다.

witr는 그 이어 붙이는 일을 대신 해준다. "이 node는 pm2가 띄웠고, 그 pm2는 systemd가 관리하는 서비스"라는 계보를 자동으로 그려, 되살아나는 유령 프로세스의 진범을 한 번에 지목한다.

표준 출력은 이렇게 생겼다. 대상(node)이 무엇인지, 언제 시작됐는지, 그리고 Why It Exists 줄에 인과 사슬이 그대로 찍힌다.

Target      : node
Process     : node (pid 14233)
User        : pm2
Command     : node index.js
Started     : 2 days ago (Mon 2026-02-02 11:42:10 +05:30)

Why It Exists :
  systemd (pid 1) → pm2 (pid 5034) → node (pid 14233)

Source      : pm2
Working Dir : /opt/apps/expense-manager
Git Repo    : expense-manager (main)
Sockets     : 127.0.0.1:5001 (TCP | LISTENING)

설계 철학은 한 문장으로 요약된다 — "모든 것을 프로세스 질문으로 취급하라(treat everything as a process question)". 포트든 파일이든 컨테이너든 결국 PID로 환원되고, PID만 정해지면 나머지(계보·근원·경고)는 같은 파이프라인으로 처리된다. 이 단순한 환원이 witr의 코드 구조 전체를 지탱한다.

2왜 지금 주목받는가

"흔한 통증 + 압도적인 배포 완성도 + 설치 없이 체험"의 3연타

witr가 8개월 만에 별 2만 개를 모은 건 대단한 신기술이라서가 아니다. "이 포트 누가 쓰지?", "이 좀비는 왜 안 죽지?" 같은, 개발자라면 매주 겪는 통증을 명령 한 줄로 끝내 주기 때문이다. 여기에 배포·마케팅 완성도가 붙어 폭발했다.

① 누구나 겪는 문제를 정면으로 푼다

"8080 포트가 이미 쓰이고 있습니다"라는 에러를 보고 lsof -i :8080 → PID 확인 → ps -ef | grep → 부모 추적… 하던 3~4단계를 witr --port 8080 한 줄로 대체한다. 정체불명의 프로세스, 되살아나는 서비스, 누가 잡고 있는지 모를 파일 락 — 전부 같은 방식으로 근원을 짚는다.

② 4개 OS 네이티브 + 8개 컨테이너 런타임

리눅스 전용 /proc 트릭에 머물지 않고 macOS·Windows·FreeBSD까지 각 OS의 네이티브 방식으로 지원한다. 컨테이너도 Docker 하나가 아니라 Podman·nerdctl·CRI(crictl)·Incus·LXC·LXD·FreeBSD jail까지 8종을 인식해, 컨테이너 안 프로세스의 계보를 호스트 관점에서 이어 준다.

③ 설치 없이 브라우저에서 체험

공식 사이트(pranshuparmar.github.io/witr)는 witr의 출력 계층을 JavaScript로 충실히 재구현한 인터랙티브 플레이그라운드다. 실제 바이너리가 찍을 바이트(ANSI 색 코드까지)를 그대로 브라우저에서 보여준다. "설치부터 하고 판단"이 아니라 "먼저 만져 보고 결정"하게 만드는 이 훅이 유입을 크게 키웠다.

④ 배포와 스크립트 친화성

Homebrew·apt·conda·AUR·winget·npm·nix·scoop·choco 등 20종이 넘는 패키지 매니저에 등재돼 있어 "어떻게 깔지" 고민이 없다. 게다가 종료 코드(exit code)가 의미를 갖는다 — 정상 0, 경고 1, 못 찾음 2, 권한 부족 3… 그래서 CI·헬스체크 스크립트에 그대로 물릴 수 있다.

도구답하는 질문witr와의 차이
ps / top무엇이 돌고 있나 (상태 스냅샷)부모-자식 계보를 "근원"으로 해석해 주지는 않음
lsof / ss이 포트·파일을 어떤 PID가 쓰나PID까지만. 그 PID를 누가 띄웠는지는 별도 추적
systemctl status이 서비스의 상태·로그systemd가 띄운 것만. 셸·컨테이너·pm2 기원은 모름
witr이게 왜 돌고 있나 (근원+맥락)위 도구들의 출력을 하나의 인과 사슬로 통합

3기술 스택 전체 지도

"Go 표준 라이브러리로 OS를 직접 뜯고, 화면은 Charm 3종 세트로 그린다"

witr는 Go 100%다(go 1.25). 놀라운 점은 의존성이 겨우 9개라는 것 — 프로세스·네트워크·컨테이너를 들여다보는 핵심 로직은 gopsutil 같은 범용 라이브러리에 기대지 않고 전부 자체 구현했다. 외부 의존성은 "화면 그리기"와 "systemd 대화"에만 쓴다. 그래서 이 저장소는 시스템 프로그래밍을 날것으로 배우기에 대단히 좋은 교재가 된다.

CLI · TUI 계층 (Charm 생태계)

패키지 (버전)역할
spf13/cobra v1.10.2CLI 프레임워크. 명령·플래그·셸 자동완성·man 페이지 생성.
charmbracelet/bubbletea v1.3.10TUI 프레임워크. Elm식 Model/Update/View 메시지 루프.
charmbracelet/bubbles v1.0.0TUI 위젯 모음 — table(표), textinput(검색창), viewport(스크롤 영역).
charmbracelet/lipgloss v1.1.0터미널 스타일링 — 색·테두리·패딩, 밝은/어두운 배경 자동 적응.
muesli/reflow v0.3.1터미널 폭에 맞춘 텍스트 줄바꿈(reflow).
mattn/go-isatty v0.0.20출력이 실제 터미널인지(TTY) 파이프인지 감지 → 색·인터랙티브 여부 결정.

시스템 대화 계층

패키지 (버전)역할
coreos/go-systemd/v22 v22.7.0systemd의 D-Bus API. 유닛 속성·재시작 횟수(NRestarts)·타이머 스케줄 조회.
godbus/dbus/v5 v5.1.0D-Bus 프로토콜 저수준 구현(go-systemd의 백엔드).
golang.org/x/sys v0.38.0저수준 시스템콜. Windows Win32 API, 유닉스 syscall 직접 호출.
의존성 목록이 말해 주는 것

보통 이런 도구는 프로세스·네트워크 정보를 gopsutil 하나로 퉁친다. witr는 일부러 그러지 않았다. "OS를 직접 파싱하는 코드"가 곧 이 프로젝트의 본체이고, 외부 라이브러리는 그 결과를 예쁘게 보여주는 껍데기(Charm 3종)와 systemd와 대화하는 통역기(D-Bus)뿐이다.

빌드 · 배포 도구

크로스컴파일과 패키징은 GoReleaser가 맡는다(4 OS × 2 아키텍처, .deb/.rpm/.apk 생성, SHA256 체크섬). Nix flake로 재현 가능한 빌드를 제공하고, golangci-lint로 정적 분석, //go:generate로 man 페이지·마크다운 문서를 소스에서 자동 생성한다. CGO_ENABLED=0 정적 링크라 실행에 별도 런타임이 필요 없다.

4아키텍처 심화 분석

"타깃을 PID로 → PID를 계보로 → 계보에서 근원을 판별 → 4가지 포맷으로 출력"

witr의 데이터 흐름은 네 정거장을 지난다. 사용자가 준 대상(이름·포트·파일·컨테이너)을 ①PID로 해석하고, 그 PID를 ②계보(ancestry)로 역추적하고, 계보를 훑어 ③근원(source)을 판별한 뒤, ④출력 렌더러가 표준/짧게/트리/JSON 중 하나로 찍는다.

witr <대상> | +---------------+----------------+ [대상 없음 / -i] [이름·포트·파일·컨테이너 지정] | | internal/tui internal/app (cobra 라우팅) (Bubble Tea 루프) | | internal/target/resolve.go ── "대상 → PID(들)" | ├─ PID → 그대로 사용 | ├─ Port → /proc/net/tcp* → inode → /proc/*/fd | ├─ Name → /proc/*/comm + cmdline | ├─ File → /proc/*/fd 심볼릭 링크 매칭 | └─ Container → 런타임 CLI(docker/podman ps …) | | []int (PID 목록) | v | internal/pipeline/analyze.go AnalyzePID() | | | +--------------------------+---------------------------+ | v v v | proc.ResolveAncestry source.Detect(계보) proc.ReadExtendedInfo | (PPID 체인 역추적) (systemd/docker/ssh…) (verbose: mem/io/fd) | | | | | +------------+-------------+---------------------------+ | v model.Result { 계보, 근원, 경고 … } +----------------> internal/output (standard / short / tree / json / env) | SanitizeTerminal (악성 ANSI 방어) → 화면

핵심 알고리즘 ① — 계보 역추적 (internal/proc/ancestry.go)

witr의 심장이다. 대상 PID에서 시작해 부모 PID(PPID)를 따라 위로 걸어 올라가며 사슬을 쌓고, 순환을 막으려 방문한 PID를 seen 맵에 기록한다. PID 1이나 PPID 0에 닿으면 멈추고, 마지막에 사슬을 뒤집어 "뿌리(init)가 맨 앞"에 오게 만든다.

func ResolveAncestry(pid int) ([]model.Process, error) {
    var chain []model.Process
    seen := make(map[int]bool)
    current := pid
    for current > 0 {
        if seen[current] { break }        // 순환 방지
        seen[current] = true
        p, err := ReadProcess(current)     // OS별 구현 (리눅스: /proc 파싱)
        if err != nil { break }
        chain = append(chain, p)
        if p.PPID == 0 || p.PID == 1 { break }
        current = p.PPID
    }
    // 뿌리가 앞에 오도록 뒤집기
    for i, j := 0, len(chain)-1; i < j; i, j = i+1, j-1 {
        chain[i], chain[j] = chain[j], chain[i]
    }
    return chain, nil
}

핵심 알고리즘 ② — 한 프로세스 읽기 (process_linux.go)

리눅스에서 한 PID의 모든 정보를 /proc/<pid>/ 아래 파일에서 직접 읽는다. 읽는 중 프로세스가 사라지는 경합(TOCTOU)을 의식해 stat을 먼저 잡는다. 주요 소스는 아래와 같다.

파일얻는 것
/proc/<pid>/statcomm(괄호 안 이름), PPID, 상태, 시작 시각(starttime), CPU 시간, RSS
/proc/<pid>/cmdline전체 명령행(널 문자 구분)
/proc/<pid>/cwd작업 디렉토리(심링크 readlink)
/proc/<pid>/exe실행 바이너리 경로. " (deleted)" 접미사로 삭제된 바이너리 탐지
/proc/<pid>/cgroup컨테이너·systemd 유닛 식별(docker/kubepods/.service 문자열 매칭)
/proc/<pid>/environ환경변수(SSH_CLIENT, TMUX 등 근원 판별에 활용)
파싱 함정
/proc/<pid>/stat 의 comm 은 괄호 안에 있고, 공백·괄호가 들어갈 수 있다

프로세스 이름이 (my app)처럼 공백·괄호를 품으면 필드를 앞에서부터 쪼갤 수 없다. 그래서 witr는 마지막 )를 찾아 그 뒤부터 필드를 나눈다. 코드 주석도 "stat format is evil"이라 적혀 있다. 시작 시각은 부팅 시각 + starttime/CLK_TCK로 환산한다.

핵심 알고리즘 ③ — "왜"의 정체 판별 (internal/source/detect.go)

역추적한 계보를 받아 우선순위 순서로 검사해 단 하나의 근원을 고른다. 검사 순서가 곧 정책이다 — 컨테이너가 가장 강한 맥락이고, 그 다음이 원격 접속, 셸, 서비스 매니저 순이다.

func Detect(ancestry []model.Process) model.Source {
    if s := detectContainer(ancestry); s != nil { return *s }  // 1. 컨테이너
    if s := detectSSH(ancestry);       s != nil { return *s }  // 2. SSH 세션
    if s := detectShell(ancestry);     s != nil { return *s }  // 3. 대화형 셸/유저툴
    if s := detectSystemd(ancestry);   s != nil { return *s }  // 4. systemd
    if s := detectLaunchd(ancestry);   s != nil { return *s }  // 5. launchd(macOS)
    if s := detectBsdRc(ancestry);     s != nil { return *s }  // 6. rc.d(FreeBSD)
    if s := detectSupervisor(ancestry);s != nil { return *s }  // 7. pm2/supervisord
    if s := detectCron(ancestry);      s != nil { return *s }  // 8. cron
    if s := detectWindowsService(ancestry); s != nil { return *s } // 9. Win 서비스
    if s := detectInit(ancestry);      s != nil { return *s }  // 10. PID1 총괄
    return model.Source{Type: model.SourceUnknown}
}

각 판별기는 계보에서 단서를 캔다. detectSSH는 사슬에 sshd가 있고 환경변수 SSH_CLIENT가 있으면 원격 IP·TTY를 뽑아 "SSH session from 1.2.3.4"로 만든다. detectSystemd는 cgroup에서 유닛명을 공짜로 얻은 뒤 D-Bus로 재시작 횟수·타이머 스케줄까지 보강한다(예전엔 systemctl show를 매번 fork했는데 단일 D-Bus 연결로 대체해 성능을 개선한 흔적이 코드에 남아 있다).

핵심 알고리즘 ④ — 포트에서 PID 찾기 (net_linux.go)

포트→PID는 두 단계다. 먼저 /proc/net/tcp를 파싱해 해당 포트와 일치하는 소켓의 inode를 모으고(포트는 4자리 16진수로 인코딩돼 있다), 그다음 /proc/*/fd/* 심볼릭 링크를 전부 훑어 socket:[inode]를 가진 PID를 찾는다.

targetHex := fmt.Sprintf("%04X", port)   // 포트를 4자리 대문자 16진수로
// /proc/net/tcp 한 줄:  fields[1]="로컬IP:PORT"(hex), fields[3]=상태, fields[9]=inode
if state == "0A" /* TCP_LISTEN */ && localParts[1] == targetHex {
    inodes[fields[9]] = true              // 이 포트를 LISTEN 중인 소켓 inode 수집
}
// 이후 /proc/<pid>/fd 의 각 심링크를 readlink → "socket:[inode]" 대조 → PID 확정

이 소켓 테이블은 2초 TTL 캐시로 감싸 계보를 훑는 동안 반복 파싱을 피한다. IPv6는 /proc/net/tcp6가 리틀엔디언 워드로 저장하므로 바이트 재배열이 필요하고, :: 리스너가 IPv4까지 받는 듀얼스택 상황도 합성해 보여준다.

크로스플랫폼 전략 — 같은 인터페이스, OS별 4벌 구현

이 저장소의 가장 큰 학습 포인트다. ReadProcess·ResolvePort 같은 동일한 함수를 Go 빌드 태그(//go:build linux 등)로 OS별 파일에 각각 구현했다. internal/proc/ 한 폴더 안에 process_linux.go·process_darwin.go·process_windows.go·process_freebsd.go 네 벌이 공존한다.

관심사LinuxmacOSWindowsFreeBSD
프로세스/proc 직접 파싱ps shelloutWin32 ToolHelp32procstat/ps
네트워크/proc/net/tcp*lsof/netstatWin32 iphlpapisockstat
서비스 매니저systemd (D-Bus)launchdSCMrc.d

특히 Windows는 PowerShell·WMI를 피하고(hang·느린 시작 회피) golang.org/x/sys/windows순수 Win32 API를 직접 호출한다. 지원하지 않는 OS에서는 unsupported.go가 안내 메시지를 찍고 종료한다.

CLI 대 TUI — 하나의 바이너리, 두 얼굴

app.go가 모드를 가른다. 대상 인자가 있으면 cobra 기반 CLI 1회 실행(결과를 찍고 의미 있는 종료 코드 반환), 인자가 없거나 -i Bubble Tea TUI가 뜬다. TUI는 Processes/Ports/Containers/Locks 네 탭을 table 위젯으로 보여주고, 3초에서 시작해 부하에 따라 간격을 늘리는 적응형 자동 새로고침, 마우스·정렬·검색, 그리고 유닉스에서는 프로세스에 시그널을 보내는 액션(KILL/TERM/STOP/CONT/renice)까지 지원한다.

5디렉토리 구조 해부

"internal(앱 내부) vs pkg(공개 데이터 모델)의 교과서적 분리"
witr/ ├── cmd/witr/ │ ├── main.go 진입점. 버전 주입(ldflags), //go:generate 문서 생성 │ └── unsupported.go 미지원 OS용 스텁 (빌드 태그 분기) ├── internal/ ← Go가 외부 import를 막는 "앱 내부" 코드 │ ├── app/ cobra 루트 명령·플래그·라우팅·종료 코드 (app.go ~826줄) │ ├── pipeline/ analyze.go — PID를 받아 Result를 조립하는 오케스트레이터 │ ├── target/ "대상 → PID" 해석: resolve.go + {port,name,file}_{os}.go │ ├── proc/ ★핵심★ OS별 프로세스/네트워크/컨테이너 검사 (파일 최다) │ │ ├── ancestry.go PPID 역추적 │ │ ├── process_{linux,darwin,…}.go ReadProcess 4벌 │ │ ├── net_{os}.go, fd_{os}.go 소켓/파일디스크립터 │ │ ├── locks_{os}.go 파일 락(/proc/locks) │ │ ├── container*.go 8개 런타임 어댑터(인터페이스+등록) │ │ ├── systemd_linux.go systemd D-Bus 보강 │ │ └── git.go .git 워크트리 감지 │ ├── source/ "왜"의 정체 판별: detect.go + systemd/ssh/shell/cron… │ ├── output/ 렌더러: standard/short/tree/json/env + sanitize.go(보안) │ ├── tui/ Bubble Tea: model/update/view/theme/mouse/actions │ └── version/ VERSION 파일 + ldflags 변수 ├── pkg/model/ ★공개★ 외부가 import 가능한 순수 데이터 타입 │ Process / Result / Source / Socket / Container / Lock … ├── docs/ 브라우저 플레이그라운드(JS 재구현) + CLI 문서 + fixtures ├── vendor/ 의존성 벤더링(재현 가능 빌드) ├── .goreleaser.yml 크로스컴파일·패키징 ├── flake.nix Nix ├── install.sh / .ps1 원라인 설치 스크립트 └── README.md (~943줄)

internal/pkg/의 구분이 핵심이다. Go는 internal/ 아래 코드를 외부 모듈이 import하지 못하도록 언어 차원에서 막는다. witr는 앱의 구현 전부를 internal/에 숨기고, 재사용할 가치가 있는 순수 데이터 타입만 pkg/model/에 공개했다. "이건 우리 앱 내부 사정, 이건 남들이 써도 되는 계약"을 폴더로 못 박은 셈이다. 실제 코드량이 가장 많은 곳은 internal/proc/ — OS별 4벌 구현이 몰려 있는 이 도구의 심장이다.

6학습 포인트

"이 저장소 하나로 배울 수 있는 것 — 시스템 프로그래밍의 실전 지도"

witr는 "터미널 도구를 어떻게 만드는가"의 모범 답안이자, 리눅스가 프로세스·네트워크를 어떻게 노출하는지 배우는 실물 교재다. 배울 것을 계층별로 정리하면 이렇다.

① /proc 파일시스템으로 OS 들여다보기

리눅스는 커널 내부 상태를 /proc라는 가짜 파일시스템으로 노출한다. witr가 읽는 /proc/<pid>/stat·cmdline·cgroup·fd/proc/net/tcpps·ss·lsof가 내부에서 하는 일과 정확히 같다. 이 도구들의 "마법"이 사실은 텍스트 파일 파싱임을 몸으로 이해하게 된다.

② 프로세스 트리와 PPID

모든 프로세스는 부모를 갖고, 부모가 먼저 죽으면 자식은 PID 1(init/systemd)에 입양(reparenting)된다. witr의 계보 역추적은 이 트리를 거꾸로 오르는 것 — 트리 자료구조·재귀·순환 방지가 실제 문제에 쓰이는 사례다.

③ 소켓 inode ↔ 파일 디스크립터 매핑

"어떤 프로세스가 이 포트를 쓰나"의 답은 /proc/net/tcp의 소켓 inode와 /proc/*/fdsocket:[inode] 링크를 이어 붙이는 것이다. TCP 상태 머신(LISTEN/ESTABLISHED/TIME_WAIT), 16진수 인코딩, IPv6 바이트 순서까지 — 네트워크의 밑바닥을 실습으로 배운다.

④ Go의 크로스플랫폼 추상화

빌드 태그 + 파일명 규칙(_linux.go/_darwin.go)으로 같은 인터페이스를 OS별로 구현하는 방식은 Go 크로스플랫폼 코드의 정석이다. #ifdef 없이 파일 단위로 분기하는 이 패턴은 그대로 자기 프로젝트에 옮겨 쓸 수 있다.

⑤ 현대적 CLI/TUI 만들기 (cobra + Bubble Tea)

cobra로 서브명령·플래그·자동완성을, Bubble Tea의 Elm 아키텍처(Model/Update/View)로 상태 기반 TUI를 짜는 법을 실전 규모로 볼 수 있다. lipgloss의 배경 밝기 적응, bubbles의 table/viewport 조합도 좋은 참고가 된다.

⑥ 적대적 입력을 렌더링하는 보안 감각

output/sanitize.go는 프로세스 이름·명령행에 숨은 악성 ANSI 이스케이프를 무력화한다. 신뢰할 수 없는 데이터(남이 띄운 프로세스의 메타데이터)를 터미널에 그릴 때 반드시 필요한 방어 — 로그 뷰어·모니터링 도구를 만들 때 놓치기 쉬운 포인트를 정확히 짚는다.

7하드웨어 / 시스템 요구사항

"단일 정적 바이너리 — 요구사항은 사실상 없고, 대신 '권한'과 'OS별 기능 차이'가 관건"

witr는 CGO_ENABLED=0으로 정적 링크된 바이너리 파일 하나다(수 MB). 런타임 의존성도, 특별한 하드웨어도 필요 없다. 다만 두 가지를 알아야 한다 — 남의 프로세스를 자세히 보려면 권한이 필요하고, 기능은 OS마다 다르다.

항목요구 / 참고
설치패키지 매니저 20종+(brew·apt·winget·npm·nix…) 또는 원라인 스크립트. 소스 빌드는 go install …/cmd/witr@latest
빌드 시 GoGo 1.25 (실행에는 불필요, 빌드할 때만)
권한Linux/FreeBSD에서 타 유저 프로세스 상세는 sudo, Windows는 관리자 PowerShell 필요. macOS는 SIP로 환경변수 등 일부 제한
컨테이너 조회해당 런타임 CLI(docker/podman/nerdctl/crictl/incus/lxc/jls)가 PATH에 있어야 함
systemd 보강재시작 횟수·타이머 스케줄은 systemd가 init인 리눅스에서만(D-Bus)
기능 호환성 (OS별 차이)
"프로세스·포트·파일·컨테이너 선택 + 네트워크 + git 감지"는 4개 OS 전부 지원
환경변수 표시는 macOS(SIP)·Windows(보호 프로세스)에서 제한될 수 있고, tmux/screen·스케줄 감지·Snap/Flatpak·Linux capability 경고는 리눅스 중심이다. 파일 락과 프로세스 액션(시그널 전송)은 Windows에서 미지원. "왜 이 기능이 내 OS에선 안 뜨지?"의 답은 대개 이 매트릭스에 있다.

자주 쓰는 CLI 형태

witr <이름>(이름으로), witr --port 8080(포트로), witr --file /var/log/app.log(파일로), witr --container myapp(컨테이너로). 출력 스위치는 -s 짧게(한 줄), -t 트리, --json JSON, --warnings 경고만, --verbose 상세, -x 정확히 일치, -i 인터랙티브 TUI.

8직접 해볼 수 있는 실습 과제

"witr를 쓰는 것에서, witr의 원리를 손으로 재현하는 것까지 — 난이도별 5단계"
LEVEL 1 · 입문 세 가지 출력 형태 비교

내 셸과 dev 서버를 witr로 들여다보기

witr $$로 지금 이 셸의 계보를 보고, -t를 붙여 트리로도 본다. 그다음 개발 서버를 띄운 뒤 witr --port 3000, witr node --json | jq로 같은 정보를 세 형태로 출력해 비교한다. 목표: 대상 지정 방식(PID/포트/이름)과 출력 포맷의 감을 잡기.

LEVEL 2 · 초급 /proc를 눈으로 확인

witr가 파싱하는 원본 파일 직접 읽기

cat /proc/self/stat을 실행해 2번째 필드(PPID)22번째 필드(starttime)를 눈으로 찾아본다. ls -l /proc/self/fd로 파일 디스크립터가 심볼릭 링크임을, cat /proc/self/cgroup로 cgroup 경로를 확인한다. 배우는 것: witr의 "마법"이 실은 텍스트 파싱임을 체감.

LEVEL 3 · 중급 계보 역추적 재구현

ResolveAncestry를 100줄로 다시 짜기

Python이나 Go로 "PID를 받아 /proc/<pid>/stat에서 PPID를 읽고, PID 1까지 올라가며 이름을 이어 붙이는" 스크립트를 작성한다. 순환 방지용 seen 집합도 넣는다. 배우는 것: 프로세스 트리 순회 + witr 핵심 로직의 본질.

LEVEL 4 · 고급 포트 → PID 파이프라인

lsof 없이 포트 소유자 찾기

포트 번호를 4자리 16진수로 바꿔 /proc/net/tcp에서 LISTEN(상태 0A) 소켓의 inode를 찾고, /proc/*/fd/*를 훑어 socket:[inode]를 가진 PID를 역으로 찾는 스크립트를 만든다. 배우는 것: 소켓 inode ↔ FD 매핑, TCP 상태·hex 인코딩.

LEVEL 5 · 도전 기여 감각으로 확장

새 컨테이너 런타임 어댑터 또는 미니 TUI 만들기

ContainerRuntime 인터페이스(Name/Available/List/HostPID 4개 메서드)를 참고해 새 어댑터를 흉내 내 보거나, Bubble Tea로 /proc를 읽어 프로세스 목록을 실시간 표시하는 미니 뷰어를 만든다. 배우는 것: 인터페이스 기반 플러그인 설계 + MVU 아키텍처 실전.

9관련 기술 심화 학습 로드맵

"witr를 '읽고 확장하는 사람'이 되기 위한 6주 코스"
주차주제무엇을 · 왜
1주/proc 파일시스템stat·status·cmdline·cgroup·fd를 직접 파싱. ps·lsof의 내부를 재현.
2주프로세스 트리·시그널PPID·reparenting·좀비·PID 1. 시그널(TERM/KILL/STOP/CONT)과 renice까지.
3주네트워크 소켓 밑바닥TCP 상태 머신, 소켓 inode↔FD, /proc/net/tcp hex 인코딩, IPv6·듀얼스택.
4주서비스 매니저 · cgroupsystemd 유닛/타이머/D-Bus, launchd, cgroup v1/v2, 컨테이너·네임스페이스.
5주Go 시스템 프로그래밍빌드 태그로 OS 분기, golang.org/x/sys syscall, cobra CLI, 인터페이스 설계.
6주Bubble Tea TUIElm(MVU) 아키텍처, bubbles 위젯, lipgloss 스타일링, 적응형 새로고침·마우스.
추천 학습 순서
"OS를 읽는 법"을 먼저, "예쁘게 그리는 법"은 나중에

처음부터 TUI 렌더링을 파려 하면 정작 witr의 본질(OS를 파싱하는 internal/proc/)을 놓친다. 1~3주는 리눅스가 정보를 어떻게 노출하는지에 집중하고(여기서 witr의 90%가 이해된다), 4주부터 systemd·컨테이너로 맥락을 넓힌 뒤, 5~6주에 Go 구현 기법과 TUI를 얹는 순서가 가장 지속 가능하다.

10핵심 키워드 사전

witr 문서·코드에서 반복되는 용어들
용어
/proc 파일시스템
리눅스가 커널·프로세스 상태를 파일처럼 노출하는 가짜 파일시스템. /proc/<pid>/ 아래에 그 프로세스의 stat·cmdline·fd·cgroup 등이 들어 있다. witr의 리눅스 백엔드 전체가 여기 위에 서 있다.
용어
PPID · ancestry(계보)
PPID는 부모 프로세스의 PID. PPID를 PID 1까지 따라 올라가며 만든 사슬이 ancestry다. witr가 "왜 이게 돌지"에 답하는 뼈대.
용어
PID 1 · reparenting
부팅 후 첫 프로세스(init/systemd)로 모든 프로세스의 조상. 부모가 먼저 죽은 고아 프로세스는 PID 1에 입양(reparenting)된다 — 계보가 끊기는 지점.
용어
소켓 inode ↔ FD
열린 소켓은 커널에서 inode 번호를 갖고, 프로세스의 /proc/<pid>/fdsocket:[inode] 심링크로 나타난다. 이 둘을 이어 붙이면 "포트→PID"가 풀린다.
용어
cgroup
프로세스를 그룹으로 묶어 자원을 제어하는 커널 기능. 경로에 docker/kubepods/.service 같은 문자열이 박혀 있어 컨테이너·systemd 유닛 식별의 단서가 된다.
용어
systemd unit · D-Bus
systemd가 관리하는 서비스·타이머의 단위가 unit. witr는 D-Bus(프로세스 간 통신 버스)로 systemd에 물어 재시작 횟수·타이머 스케줄을 가져온다.
용어
Go 빌드 태그(build tag)
//go:build linux 주석이나 _linux.go 파일명 규칙으로 "이 파일은 이 OS에서만 컴파일"을 지정하는 방식. 같은 인터페이스를 OS별로 다르게 구현하는 크로스플랫폼의 열쇠.
용어
Bubble Tea · Elm(MVU) 아키텍처
상태(Model)를 메시지로 갱신(Update)하고 그 상태로 화면을 그리는(View) 단방향 흐름. Charm의 Bubble Tea가 이 방식으로 TUI를 만든다.
용어
ContainerRuntime 인터페이스
Docker·Podman 등 8개 런타임을 Name/Available/List/HostPID 4개 메서드로 추상화한 Go 인터페이스. 각 구현이 init()에서 레지스트리에 자동 등록된다(플러그인 패턴).
용어
터미널 sanitize · 종료 코드
프로세스 이름에 숨은 악성 ANSI 이스케이프를 무력화하는 방어(sanitize.go). 종료 코드는 0 정상·1 경고·2 못 찾음·3 권한… 처럼 의미를 가져 스크립트에 물릴 수 있다.

11참고 링크

원본을 직접 확인하고 싶을 때