-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsys
More file actions
executable file
·435 lines (390 loc) · 16.4 KB
/
sys
File metadata and controls
executable file
·435 lines (390 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
#!/usr/bin/env bash
# sys - systemd 서비스 관리 도우미 (user/system 자동 감지)
#
# 사용법: sys --help
# 기본 설정
SYS_WATCH=()
SYS_STATE_DIR="${HOME}/.cache/sys-cli"
# 사용자 설정 파일이 있으면 override
CONFIG_FILE="${HOME}/.config/sys-cli/config"
[[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE"
# 번호 ↔ 서비스명 매핑은 셸(PPID)별로 분리해서 저장한다.
# → 여러 셸에서 동시에 sys ls 를 실행해도 서로의 번호가 섞이지 않음.
SYS_STATE="${SYS_STATE_DIR}/map-${PPID}"
# 색상: stdout 이 터미널일 때만 활성화 (pipe/redirect 시 끔).
# NO_COLOR 환경변수가 있으면 강제로 끔 (표준: https://no-color.org).
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
C_RED=$'\033[31m'
C_GREEN=$'\033[32m'
C_YELLOW=$'\033[33m'
C_RESET=$'\033[0m'
else
C_RED=""
C_GREEN=""
C_YELLOW=""
C_RESET=""
fi
sys_help() {
echo "sys - systemd 서비스 관리 도우미 (user/system 자동 감지)"
echo ""
echo "USAGE:"
echo " sys <COMMAND> [UNIT|ID]"
echo ""
echo "COMMANDS:"
echo " start 서비스 시작"
echo " stop 서비스 정지"
echo " log, logs 실시간 로그 따라가기 (journalctl -f)"
echo " l, ls, list 서비스 목록 (번호 매겨서 표시)"
echo " status, st 서비스 상태 + 최근 로그"
echo " restart, r 서비스 재시작"
echo " enable 부팅 시 자동 시작 등록"
echo " disable 부팅 시 자동 시작 해제"
echo " delete, del, rm sys ls 목록에서 제거 (USER는 파일까지, SYS_WATCH는 항목만)"
echo " cat .service 파일 내용 출력 (읽기 전용)"
echo " edit .service 파일 편집 (편집 전 .bak 자동 백업)"
echo " daemon .service 파일 수정 후 systemd 재로딩"
echo " help, -h, --help 이 도움말 출력"
echo ""
echo "UNIT 자리에 ID 숫자도 사용 가능 (sys ls의 번호)"
echo " 예: sys status 1 → sys ls의 1번 서비스 상태"
echo " 번호 ID는 셸마다 따로 관리되니, 각 셸에서 'sys ls'를 먼저 실행하세요."
echo ""
echo "EXAMPLES:"
printf ' %-37s # %s\n' \
"sys ls" "전체 서비스 목록" \
"sys status <UNIT or ID#>" "단일 서비스 상태" \
"sys status 1" "1번 서비스 상태" \
"sys restart <UNIT or ID#>" "단일 서비스 재시작" \
"sys log <UNIT or ID#>" "실시간 로그 tail" \
"sys log <UNIT or ID#> -n 100" "최근 100줄부터 tail" \
"sys log <UNIT or ID#> --head 100" "시작부터 100줄 (스냅샷)" \
"sys log <UNIT or ID#> -n 100 -g ERROR" "ERROR만 필터링" \
"sys cat <UNIT or ID#>" ".service 파일 내용 확인" \
"sys edit <UNIT or ID#>" ".service 파일 편집 (.bak 자동 백업)" \
"sys delete <UNIT or ID#>" "sys ls 목록에서 제거" \
"sys daemon" ".service 수정 후 reload"
echo ""
echo "LOG OPTIONS:"
echo " -n, --lines N 마지막 N줄부터 시작 (실시간 tail 유지)"
echo " --head N 시작부터 N줄만 보기 (스냅샷)"
echo " -g, --grep PATTERN 정규식 패턴으로 필터"
echo ""
echo "CONFIG:"
echo " ~/.config/sys-cli/config 에서 SYS_WATCH 등을 설정할 수 있습니다."
}
# 한 섹션 출력: 헤더 + 데이터 + 번호 ID + BOOT 마커
# 사용법: print_section "USER SERVICES" "scope" "${services[@]}"
# scope: "--user" 또는 "" (system)
print_section() {
local title="$1"
local scope="$2"
shift 2
local services=("$@")
local use_sudo=""
[[ "$scope" != "--user" ]] && use_sudo="sudo"
[[ ${#services[@]} -eq 0 ]] && return
echo " === $title ==="
# systemctl show 한 번으로 모든 서비스의 속성을 받아온다.
# → unit 마다 systemctl 을 띄우면 N번 fork 가 발생해서 느려짐.
local units=() s
for s in "${services[@]}"; do
units+=("${s}.service")
done
local show_out
show_out=$($use_sudo systemctl $scope show \
--property=Id,LoadState,ActiveState,SubState,UnitFileState,Description \
-- "${units[@]}" 2>/dev/null)
local -A load_state active_state sub_state file_state description
local current_id=""
local line
while IFS= read -r line; do
case "$line" in
Id=*) current_id="${line#Id=}" ;;
LoadState=*) load_state[$current_id]="${line#LoadState=}" ;;
ActiveState=*) active_state[$current_id]="${line#ActiveState=}" ;;
SubState=*) sub_state[$current_id]="${line#SubState=}" ;;
UnitFileState=*) file_state[$current_id]="${line#UnitFileState=}" ;;
Description=*) description[$current_id]="${line#Description=}" ;;
esac
done <<< "$show_out"
# 헤더와 행의 컬럼 폭을 정확히 일치시켜야 시각적으로 정렬됨.
# SUB 는 8자로 타이트하게 (running/exited/dead/failed 모두 fit).
# BOOT 헤더는 " BOOT " 로 가운데 정렬 (마커도 같은 위치에 옴).
printf " %3s %-32s %-9s %-10s %-8s %-6s %s\n" \
"ID" "UNIT" "LOAD" "ACTIVE" "SUB" " BOOT " "DESCRIPTION"
# 행
local unit boot_marker active_val sub_val c_active c_sub c_boot
local active_padded sub_padded boot_padded
for s in "${services[@]}"; do
echo "$s" >> "$SYS_STATE_TMP"
unit="${s}.service"
active_val="${active_state[$unit]:-?}"
sub_val="${sub_state[$unit]:-?}"
# UnitFileState 값 → ✓/✗/- 매핑 + BOOT 색상
case "${file_state[$unit]:-}" in
enabled|enabled-runtime|alias) boot_marker="✓"; c_boot="$C_GREEN" ;;
disabled) boot_marker="✗"; c_boot="$C_RED" ;;
*) boot_marker="-"; c_boot="" ;;
esac
# ACTIVE 색상
case "$active_val" in
active) c_active="$C_GREEN" ;;
activating|deactivating|reloading) c_active="$C_YELLOW" ;;
failed|inactive) c_active="$C_RED" ;;
*) c_active="" ;;
esac
# SUB 색상
case "$sub_val" in
running) c_sub="$C_GREEN" ;;
start-pre|start|start-post|reload|auto-restart) c_sub="$C_YELLOW" ;;
stop-pre|stop|stop-post|stop-sigterm|stop-sigkill) c_sub="$C_YELLOW" ;;
failed|dead|exited) c_sub="$C_RED" ;;
*) c_sub="" ;;
esac
# ANSI 코드는 시각폭 0 이지만 printf %-Ns 는 바이트로 패딩하므로,
# 먼저 평문으로 정확한 폭만큼 패딩한 뒤 색을 입혀야 정렬이 안 깨짐.
# BOOT 마커는 ✓/✗ 가 UTF-8 3바이트라 printf 폭 계산이 어긋남
# → 항상 화면상 1글자임을 알고 있으니 수동으로 " BOOT " 헤더 가운데에
# 오도록 " ✓ " (2 + 1 + 3 = 화면상 6칸) 형태로 만든다.
printf -v active_padded "%-10s" "$active_val"
printf -v sub_padded "%-8s" "$sub_val"
boot_padded=" ${boot_marker} "
printf " %2d) %-32s %-9s %s%s%s %s%s%s %s%s%s %s\n" \
"$IDX" "$unit" \
"${load_state[$unit]:-?}" \
"$c_active" "$active_padded" "$C_RESET" \
"$c_sub" "$sub_padded" "$C_RESET" \
"$c_boot" "$boot_padded" "$C_RESET" \
"${description[$unit]:-}"
((IDX++))
done
echo
}
cmd="$1"
target="$2"
if [[ "$cmd" == "-h" || "$cmd" == "--help" || "$cmd" == "help" || -z "$cmd" ]]; then
sys_help
exit 0
fi
# ls는 target 처리 없이 바로
if [[ "$cmd" == "l" || "$cmd" == "ls" || "$cmd" == "list" ]]; then
mkdir -p "$SYS_STATE_DIR"
# 이미 종료된 셸이 남긴 매핑 파일 정리 (PID가 살아있지 않으면 삭제)
for f in "$SYS_STATE_DIR"/map-* "$SYS_STATE_DIR"/.map-*; do
[[ -e "$f" ]] || continue
pid="${f##*/map-}"; pid="${pid%%.*}"
[[ "$pid" =~ ^[0-9]+$ ]] && ! kill -0 "$pid" 2>/dev/null && rm -f "$f"
done
# 매핑을 임시 파일에 모은 뒤 mv 로 원자적 교체한다.
# → 쓰는 도중 다른 셸이 읽어도 항상 완전한 파일만 보게 됨 (race 방지).
SYS_STATE_TMP="$(mktemp "${SYS_STATE_DIR}/.map-${PPID}.XXXXXX")"
trap 'rm -f "$SYS_STATE_TMP"' EXIT
IDX=1
# User services (본인이 ~/.config/systemd/user/ 에 만든 것만)
user_services=()
if [[ -d "$HOME/.config/systemd/user" ]]; then
for f in "$HOME/.config/systemd/user"/*.service; do
[[ -e "$f" ]] && user_services+=("$(basename "$f" .service)")
done
fi
print_section "USER SERVICES" "--user" "${user_services[@]}"
print_section "SYSTEM SERVICES (watched)" "" "${SYS_WATCH[@]}"
mv -f "$SYS_STATE_TMP" "$SYS_STATE"
if [[ "$IDX" -eq 1 ]]; then
echo "표시할 서비스가 없어요."
echo " - user service: ~/.config/systemd/user/ 에 .service 파일을 만드세요."
echo " - system service: ~/.config/sys-cli/config 의 SYS_WATCH 에 이름을 추가하세요."
else
cat <<'EOF'
UNIT service file name
LOAD loaded / not-found / masked / error
ACTIVE active / inactive / failed / activating
SUB running / exited / dead / failed
BOOT ✓ enabled / ✗ disabled / - static, masked, etc.
DESCRIPTION Description= field from the unit file
EOF
fi
exit 0
fi
# daemon은 target 불필요
if [[ "$cmd" == "daemon" ]]; then
systemctl --user daemon-reload 2>/dev/null
sudo systemctl daemon-reload
exit 0
fi
# 숫자면 매핑 파일에서 이름 찾기
if [[ "$target" =~ ^[0-9]+$ ]]; then
if [[ -f "$SYS_STATE" ]]; then
resolved=$(sed -n "${target}p" "$SYS_STATE")
if [[ -n "$resolved" ]]; then
target="$resolved"
else
echo "sys: ID $target에 해당하는 서비스가 없어요. 'sys ls'로 번호 다시 확인하세요." >&2
exit 1
fi
else
echo "sys: 이 셸의 매핑이 없어요. 먼저 'sys ls'를 실행하세요." >&2
exit 1
fi
fi
target="${target%.service}"
if [[ -z "$target" ]]; then
echo "sys: 서비스 이름 또는 ID가 필요해요." >&2
echo "도움말 보려면: sys --help" >&2
exit 1
fi
# delete: sys ls 목록(USER 파일 또는 SYS_WATCH 항목)에서 제거
# systemctl cat 검사를 우회 → SYS_WATCH 에 잘못 적힌 이름도 정리 가능
if [[ "$cmd" == "delete" || "$cmd" == "del" || "$cmd" == "rm" ]]; then
user_file="$HOME/.config/systemd/user/${target}.service"
is_user=false
is_watched=false
[[ -f "$user_file" ]] && is_user=true
for w in "${SYS_WATCH[@]}"; do
[[ "$w" == "$target" ]] && is_watched=true && break
done
if ! $is_user && ! $is_watched; then
echo "sys: '$target' 은(는) sys ls 목록에 없어요." >&2
echo " - user service 파일($user_file) 없음" >&2
echo " - SYS_WATCH 에도 없음" >&2
exit 1
fi
if $is_user; then
echo "USER SERVICE '$target' 를 다음 순서로 제거합니다:"
echo " 1) systemctl --user stop $target"
echo " 2) systemctl --user disable $target"
echo " 3) rm $user_file"
echo " 4) systemctl --user daemon-reload"
read -r -p "진행할까요? [y/N]: " ans
[[ "$ans" =~ ^[Yy]$ ]] || { echo "취소됨."; exit 0; }
systemctl --user stop "$target" 2>/dev/null
systemctl --user disable "$target" 2>/dev/null
rm -f "$user_file" && echo "✓ $user_file 제거됨"
systemctl --user daemon-reload
echo "✓ 완료."
exit 0
fi
# is_watched (and not user): config 파일의 SYS_WATCH 줄만 갱신
# 실제 unit 파일 경로를 알려준다 (FragmentPath) → 사용자가 직접 지울 수 있게
unit_path=$(systemctl show -p FragmentPath --value -- "${target}.service" 2>/dev/null)
echo "SYS_WATCH 에서 '$target' 을(를) 제거합니다 (실제 system service 는 그대로 둠)."
if [[ -n "$unit_path" ]]; then
echo ""
echo " 실제 .service 파일 위치: $unit_path"
echo " 파일까지 완전히 제거하려면 (수동):"
echo " sudo systemctl stop $target"
echo " sudo systemctl disable $target"
echo " sudo rm $unit_path"
echo " sudo systemctl daemon-reload"
echo ""
else
echo " (참고: systemd 에서 unit 파일을 못 찾았어요. 이 이름이 SYS_WATCH 에만 잘못 남아있을 수 있음.)"
fi
read -r -p "SYS_WATCH 에서만 제거 진행할까요? [y/N]: " ans
[[ "$ans" =~ ^[Yy]$ ]] || { echo "취소됨."; exit 0; }
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "sys: $CONFIG_FILE 가 없어요." >&2
exit 1
fi
sys_watch_line=$(grep -nE '^[[:space:]]*SYS_WATCH=' "$CONFIG_FILE" | head -1)
if [[ -z "$sys_watch_line" ]]; then
echo "sys: $CONFIG_FILE 에서 'SYS_WATCH=' 줄을 찾을 수 없어요." >&2
exit 1
fi
# 멀티라인 배열은 자동 편집 불가 — 같은 줄에 닫는 ')' 가 있어야 함
if [[ "${sys_watch_line#*:}" != *")"* ]]; then
echo "sys: SYS_WATCH 가 여러 줄에 걸쳐 있어서 자동 편집 불가." >&2
echo " $CONFIG_FILE 를 직접 편집해서 '$target' 을(를) 빼주세요." >&2
exit 1
fi
new_list=()
for w in "${SYS_WATCH[@]}"; do
[[ "$w" != "$target" ]] && new_list+=("$w")
done
new_value="SYS_WATCH=(${new_list[*]})"
# mac/linux 모두 동작하도록 sed -i.bak 후 백업 제거
sed -i.bak "s|^[[:space:]]*SYS_WATCH=.*|${new_value}|" "$CONFIG_FILE" && rm -f "${CONFIG_FILE}.bak"
echo "✓ $CONFIG_FILE 의 SYS_WATCH 갱신: $new_value"
exit 0
fi
# 정확한 이름 매칭만 (user 우선, 없으면 system)
match=""
scope_flag=""
use_sudo=""
if systemctl --user cat -- "${target}.service" >/dev/null 2>&1; then
match="$target"
scope_flag="--user"
elif systemctl cat -- "${target}.service" >/dev/null 2>&1; then
match="$target"
use_sudo="sudo"
else
echo "sys: '$target' 서비스를 찾을 수 없어요." >&2
exit 1
fi
case "$cmd" in
status|st) $use_sudo systemctl $scope_flag status "$match" ;;
start) $use_sudo systemctl $scope_flag start "$match" ;;
stop) $use_sudo systemctl $scope_flag stop "$match" ;;
restart|r) $use_sudo systemctl $scope_flag restart "$match" ;;
log|logs)
# 추가 인자(journalctl 옵션들)는 그대로 통과
extra_args=("${@:3}")
head_count=""
filtered_args=()
# --head N 옵션 추출 (있으면 snapshot 모드)
i=0
while [[ $i -lt ${#extra_args[@]} ]]; do
arg="${extra_args[$i]}"
if [[ "$arg" == "--head" ]]; then
head_count="${extra_args[$((i+1))]:-}"
if ! [[ "$head_count" =~ ^[0-9]+$ ]]; then
echo "sys: --head 뒤에 숫자가 필요해요 (예: --head 100)" >&2
exit 1
fi
((i+=2))
continue
fi
filtered_args+=("$arg")
((i++))
done
if [[ -n "$head_count" ]]; then
# snapshot 모드: -f 없이, 앞에서부터 N줄만
if command -v ccze >/dev/null 2>&1; then
$use_sudo journalctl $scope_flag -o cat --no-pager -u "$match" "${filtered_args[@]}" 2>/dev/null | head -n "$head_count" | ccze -A
else
$use_sudo journalctl $scope_flag -o cat --no-pager -u "$match" "${filtered_args[@]}" 2>/dev/null | head -n "$head_count"
fi
else
# 기본 모드: -f 실시간 tail
if command -v ccze >/dev/null 2>&1; then
$use_sudo journalctl $scope_flag -o cat -f -u "$match" "${filtered_args[@]}" | ccze -A
else
$use_sudo journalctl $scope_flag -o cat -f -u "$match" "${filtered_args[@]}"
fi
fi
;;
enable) $use_sudo systemctl $scope_flag enable "$match" ;;
disable) $use_sudo systemctl $scope_flag disable "$match" ;;
cat) $use_sudo systemctl $scope_flag cat -- "$match" ;;
edit)
# 편집 전에 원본 .service 파일을 <원본>.bak 으로 복사 (직전 버전 1개만 유지)
unit_path=$($use_sudo systemctl $scope_flag show -p FragmentPath --value -- "${match}.service" 2>/dev/null)
if [[ -n "$unit_path" && -f "$unit_path" ]]; then
if $use_sudo cp -p "$unit_path" "${unit_path}.bak" 2>/dev/null; then
echo "✓ 백업: ${unit_path}.bak"
else
echo "⚠ 백업 실패: ${unit_path}.bak (계속 진행)" >&2
fi
else
echo "⚠ FragmentPath 를 찾지 못해 백업을 건너뜁니다." >&2
fi
# --full: drop-in override 가 아니라 원본 파일을 직접 편집
# systemctl edit 은 저장 후 자동으로 daemon-reload 까지 수행
$use_sudo systemctl $scope_flag edit --full "$match"
;;
*)
echo "sys: 알 수 없는 명령어 '$cmd'" >&2
echo "도움말 보려면: sys --help" >&2
exit 1
;;
esac