cli 툴 만들어보았다 이름하여 porst-cli
왜 만들었나
개발하다 보면 "이 포트 뭐가 쓰고 있지?" 하는 순간이 자주 온다. 특히나 회사에서는 한 번에 여러 레포의 서버를 띄워두고 작업을 많이 하고 있기도 하다.
lsof -i :3000 치면 되긴 하는데, 출력이 너무 장황하고 매번 명령어 기억하기도 귀찮았다.
그래서 만듦. ports-cli 리스닝 중인 포트를 예쁘게 보여주고, 프로세스 경로까지 알려주는 CLI 도구.
기능
ports # 모든 리스닝 포트 보기
ports -u # 내가 실행한 프로세스만 보기
ports bye 3000 # 3000번 포트 프로세스 죽이기
ports -u 커맨드의 출력은 이렇게 나온다:
PORT PROCESS PATH
---- ------- ----
3000 my-app ~/Documents/GitHub/my-app
5432 postgres -
8080 test-server ~/Documents/GitHub/test-server
내가 딱 보고 싶었던 건 현재 열린 포트, 열려 있는 앱 이름, 해당 앱의 경로.
Node 프로세스는 package.json에서 프로젝트 이름을 읽어서 보여준다. 단순히 node라고 나오는 게 아니라 실제 프로젝트 이름이 나오니까 훨씬 직관적이라고 생각해서 그렇게 구현했당.
개발 과정을 버전 순으로 나열하면 다음과 같다.
v1.0.0 - 기본 기능
처음에는 단순했다. lsof로 리스닝 포트 가져오고, Node 프로세스면 package.json에서 이름 파싱해서 보여주는 것.
# Node 프로세스면 프로젝트 이름 가져오기
if [ "$proc" = "node" ]; then
cwd=$(lsof -p "$pid" 2>/dev/null | grep cwd | awk '{print $9}')
if [ -f "$cwd/package.json" ]; then
name=$(grep '"name"' "$cwd/package.json" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
if [ -n "$name" ]; then
proc="$name"
fi
fi
fi
v1.0.1 - -u 옵션 추가
근데 쓰다 보니까 문제가 생겼다. postgres, redis 를 비롯한 AirPlay 등의 시스템 프로세스까지 다 나오니까 내가 띄운 개발 서버를 찾기가 어려웠다.
그래서 -u (--user) 옵션을 추가했다.
사용자가 직접 띄운 프로세스가 있는 것만 필터링하는 기능이다. 쉽게 말하면, 경로가 있는 프로세스들? 시스템 프로세스는 PATH가 -로 나오니까, 이걸 기준으로 필터링하면 내가 직접 실행한 Node 프로세스만 볼 수 있다.
if [ "$USER_ONLY" = true ]; then
[ "$display_path" = "-" ] && continue
[ -z "$display_path" ] && continue
fi
여기서 삽질을 좀 했다. while read 루프 안에서 declare -A seen_ports 로 중복 포트를 추적하고 있었는데, 이게 동작을 안 했음...
원인은 파이프라인의 subshell 문제였다. lsof | while read 하면 while 루프가 subshell에서 실행되면서 변수가 부모 쉘에 전달이 안 된다.
해결책은 process substitution:
# 이렇게 하면 안 됨 (subshell 문제)
lsof -iTCP -sTCP:LISTEN | while read line; do
...
done
# 이렇게 해야 됨
while read line; do
...
done < <(lsof -iTCP -sTCP:LISTEN)
v1.2.0 - ports bye 명령 추가
이것도 그 즉시 포트를 킬하고 싶어서 만든 건데, 매번 PID 복사해서 kill -9 치기 귀찮아서 ports bye <port> 명령을 추가했다. bye~
kill_port() {
local port=$1
local pid=$(lsof -ti TCP:$port -sTCP:LISTEN 2>/dev/null)
if [ -z "$pid" ]; then
echo "${RED}No process found on port $port${RESET}"
exit 1
fi
kill -9 $pid 2>/dev/null
if [ $? -eq 0 ]; then
echo "${GREEN}✓ Killed $proc_name (PID: $pid) on port $port${RESET}"
fi
}
그리고 또 버그 ... macOS 기본 bash가 3.x 버전이라 declare -A (associative array)를 지원 안 한다. Homebrew로 배포하려면 기본 bash에서도 돌아가야 해서 문자열 기반으로 변경했다.
# bash 4.x 이상에서만 동작
declare -A seen_ports
seen_ports[$port]=1
# bash 3.x 호환
seen_ports=""
if echo "$seen_ports" | grep -q ":${port}:"; then
continue
fi
seen_ports="${seen_ports}:${port}:"
이왕 만든 거 Homebrew로 배포하고 싶었다. 왜냐면 한 번도 안 해 보기도 했고, 회사에서 만든 프로젝트라 집에서는 편하게 설치하고 싶었기 때문이다. 진짜 별거 없는 이유... 지만...
- homebrew-tap 레포 생성
GitHub에 homebrew-tap 레포를 만들고, Formula/ports-cli.rb 파일 작성:
class PortsCli < Formula
desc "Show listening ports with process names and paths"
homepage "https://github.com/givvemee/ports-cli"
url "https://github.com/givvemee/ports-cli/archive/refs/tags/v1.2.0.tar.gz"
sha256 "3fd0635de73b07fb87d6bdd533312e9b3fa2ab3ed286dacf2be58cf3c27d2e86"
license "MIT"
def install
bin.install "bin/ports"
end
test do
system "#{bin}/ports", "--version"
end
end
- 릴리즈 태그 만들기
git tag v1.2.0
git push origin v1.2.0
- sha256 해시 구하기
curl -sL https://github.com/givvemee/ports-cli/archive/refs/tags/v1.2.0.tar.gz | shasum -a 256
이게 좀 귀찮은데, 태그 만들 때마다 새로 해시 구해서 Formula 업데이트해야 한다. 근데 크게 변경 사항은 없을 것 같으니까 조금의 귀찮음은 감안할 수 있다.
- 설치
brew tap givvemee/tap
brew install ports-cli
끝! 별 건 아니지만 요긴하게 잘 쓰고 있다.