Added file descriptor module
This commit is contained in:
parent
8e2544b431
commit
bce97223f0
5 changed files with 430 additions and 4 deletions
|
|
@ -78,6 +78,7 @@ var (
|
|||
cfgStateFilePath = "/tmp/port_checker_state.json"
|
||||
cfgServiceStateFilePath = "/tmp/auspex_service_state.json"
|
||||
cfgDockerStateFilePath = "/tmp/auspex_docker_state.json"
|
||||
cfgFDRecoverDir = "/var/tmp/auspex-recovered"
|
||||
)
|
||||
|
||||
// --- Параметры проверки портов ---
|
||||
|
|
@ -223,6 +224,9 @@ func applyConfig(env map[string]string) {
|
|||
if v, ok := env["LOG_DIR"]; ok && v != "" {
|
||||
cfgLogDir = v
|
||||
}
|
||||
if v, ok := env["FD_RECOVER_DIR"]; ok && v != "" {
|
||||
cfgFDRecoverDir = v
|
||||
}
|
||||
|
||||
// Thresholds
|
||||
if v, ok := env["CERT_CRITICAL_DAYS"]; ok && v != "" {
|
||||
|
|
|
|||
404
fdcheck.go
Normal file
404
fdcheck.go
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// fdcheck — поиск "фантомных" удалённых-но-открытых файловых дескрипторов
|
||||
// =============================================================================
|
||||
//
|
||||
// Ситуация: файл удалён (rm), но место на диске не освободилось, потому что
|
||||
// какой-то процесс всё ещё держит открытый файловый дескриптор на него.
|
||||
// Модуль находит такие дескрипторы через /proc/[pid]/fd/*, показывает какой
|
||||
// процесс и сколько места держит, и даёт интерактивно выбрать действие:
|
||||
// восстановить содержимое, отправить reopen-сигнал, перезапустить службу
|
||||
// или убить процесс.
|
||||
// =============================================================================
|
||||
|
||||
type fdEntry struct {
|
||||
PID int
|
||||
ProcessName string
|
||||
FDNum string
|
||||
FDPath string // /proc/PID/fd/N
|
||||
OriginalPath string
|
||||
SizeBytes int64
|
||||
Owner string
|
||||
}
|
||||
|
||||
var fdDeletedSuffix = " (deleted)"
|
||||
|
||||
// runFDCheck — точка входа модуля
|
||||
func runFDCheck() error {
|
||||
if os.Geteuid() != 0 {
|
||||
fmt.Printf("%s⚠️ Запущено не от root — часть процессов будет недоступна для просмотра (Permission denied в /proc/PID/fd).%s\n\n", cYellow, cReset)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
for {
|
||||
fmt.Println("🔎 Сканирую /proc в поисках удалённых-но-открытых файлов...")
|
||||
entries := fdScan()
|
||||
|
||||
if len(entries) == 0 {
|
||||
fmt.Printf("\n%s✅ Удалённые-но-открытые дескрипторы не найдены.%s\n", cGreen, cReset)
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].SizeBytes > entries[j].SizeBytes
|
||||
})
|
||||
|
||||
fdPrintTable(entries)
|
||||
|
||||
fmt.Printf("\n%sВведите номер записи для действия (Enter — обновить список, q — выход): %s", cGreen, cReset)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
if input == "q" || input == "Q" || input == "quit" || input == "exit" {
|
||||
return nil
|
||||
}
|
||||
if input == "" {
|
||||
continue // обновить список
|
||||
}
|
||||
|
||||
idx, err := strconv.Atoi(input)
|
||||
if err != nil || idx < 1 || idx > len(entries) {
|
||||
fmt.Printf("%sНекорректный номер.%s\n\n", cRed, cReset)
|
||||
continue
|
||||
}
|
||||
|
||||
fdHandleAction(entries[idx-1], reader)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Сканирование ─────────────────────────────────────────────────────────────
|
||||
|
||||
func fdScan() []fdEntry {
|
||||
var results []fdEntry
|
||||
|
||||
procEntries, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
fmt.Printf("%sОшибка чтения /proc: %v%s\n", cRed, err, cReset)
|
||||
return results
|
||||
}
|
||||
|
||||
for _, pe := range procEntries {
|
||||
pid, err := strconv.Atoi(pe.Name())
|
||||
if err != nil {
|
||||
continue // не PID-директория
|
||||
}
|
||||
|
||||
fdDir := fmt.Sprintf("/proc/%d/fd", pid)
|
||||
fds, err := os.ReadDir(fdDir)
|
||||
if err != nil {
|
||||
continue // нет прав или процесс уже завершился
|
||||
}
|
||||
|
||||
procName := fdGetProcessName(pid)
|
||||
owner := fdGetOwner(pid)
|
||||
|
||||
for _, fdInfo := range fds {
|
||||
fdPath := filepath.Join(fdDir, fdInfo.Name())
|
||||
|
||||
target, err := os.Readlink(fdPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(target, fdDeletedSuffix) {
|
||||
continue
|
||||
}
|
||||
|
||||
originalPath := strings.TrimSuffix(target, fdDeletedSuffix)
|
||||
|
||||
// Отфильтровываем то, что не является настоящей утечкой места на диске:
|
||||
// memfd, анонимные inode, сокеты/пайпы и всё вне обычной ФС.
|
||||
if !strings.HasPrefix(originalPath, "/") ||
|
||||
strings.HasPrefix(originalPath, "/dev/") ||
|
||||
strings.HasPrefix(originalPath, "/proc/") ||
|
||||
strings.Contains(originalPath, "memfd:") ||
|
||||
strings.HasPrefix(originalPath, "anon_inode:") ||
|
||||
strings.HasPrefix(originalPath, "socket:") ||
|
||||
strings.HasPrefix(originalPath, "pipe:") {
|
||||
continue
|
||||
}
|
||||
|
||||
var size int64
|
||||
if st, err := os.Stat(fdPath); err == nil {
|
||||
size = st.Size()
|
||||
}
|
||||
|
||||
results = append(results, fdEntry{
|
||||
PID: pid,
|
||||
ProcessName: procName,
|
||||
FDNum: fdInfo.Name(),
|
||||
FDPath: fdPath,
|
||||
OriginalPath: originalPath,
|
||||
SizeBytes: size,
|
||||
Owner: owner,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func fdGetProcessName(pid int) string {
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
func fdGetOwner(pid int) string {
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
|
||||
if err != nil {
|
||||
return "?"
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.HasPrefix(line, "Uid:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
if u, err := user.LookupId(fields[1]); err == nil {
|
||||
return u.Username
|
||||
}
|
||||
return fields[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
// ── Вывод таблицы ─────────────────────────────────────────────────────────────
|
||||
|
||||
func fdPrintTable(entries []fdEntry) {
|
||||
var totalSize int64
|
||||
fmt.Printf("\n%s%-4s %-8s %-16s %-6s %-10s %-10s %s%s\n",
|
||||
cCyan, "№", "PID", "Процесс", "FD", "Владелец", "Размер", "Путь (удалён)", cReset)
|
||||
fmt.Println(strings.Repeat("─", 100))
|
||||
|
||||
for i, e := range entries {
|
||||
fmt.Printf("%-4d %-8d %-16s %-6s %-10s %-10s %s\n",
|
||||
i+1, e.PID, fdTruncate(e.ProcessName, 16), e.FDNum, e.Owner,
|
||||
fdHumanSize(e.SizeBytes), e.OriginalPath)
|
||||
totalSize += e.SizeBytes
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("─", 100))
|
||||
fmt.Printf("%sНайдено записей: %d | Суммарно занято (с учётом возможных дублей): %s%s\n",
|
||||
cYellow, len(entries), fdHumanSize(totalSize), cReset)
|
||||
}
|
||||
|
||||
func fdTruncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n-1] + "…"
|
||||
}
|
||||
|
||||
func fdHumanSize(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// ── Интерактивное меню действий ───────────────────────────────────────────────
|
||||
|
||||
func fdHandleAction(e fdEntry, reader *bufio.Reader) {
|
||||
fmt.Printf("\n%sДескриптор:%s PID %d (%s), fd %s, владелец %s\n",
|
||||
cBlue, cReset, e.PID, e.ProcessName, e.FDNum, e.Owner)
|
||||
fmt.Printf("%sПуть:%s %s (%s)\n", cBlue, cReset, e.OriginalPath, fdHumanSize(e.SizeBytes))
|
||||
|
||||
unit, hasUnit := fdDetectSystemdUnit(e.PID)
|
||||
if hasUnit {
|
||||
fmt.Printf("%sОпределена служба systemd:%s %s\n", cBlue, cReset, unit)
|
||||
}
|
||||
|
||||
fmt.Println("\nВыберите действие:")
|
||||
fmt.Println(" 1) Восстановить содержимое в новый файл (recover)")
|
||||
fmt.Println(" 2) Мягкий reopen сигналом (SIGHUP / SIGUSR1)")
|
||||
fmt.Println(" 3) Перезапустить службу (systemctl restart)")
|
||||
fmt.Println(" 4) Убить процесс (SIGTERM → SIGKILL)")
|
||||
fmt.Println(" 0) Отмена")
|
||||
|
||||
fmt.Printf("%s>>> %s", cGreen, cReset)
|
||||
choice, _ := reader.ReadString('\n')
|
||||
choice = strings.TrimSpace(choice)
|
||||
|
||||
switch choice {
|
||||
case "1":
|
||||
fdActionRecover(e, reader)
|
||||
case "2":
|
||||
fdActionReopenSignal(e, reader)
|
||||
case "3":
|
||||
fdActionRestart(e, unit, hasUnit)
|
||||
case "4":
|
||||
fdActionKill(e)
|
||||
case "0", "":
|
||||
fmt.Println("Отменено.")
|
||||
default:
|
||||
fmt.Printf("%sНеизвестный выбор.%s\n", cRed, cReset)
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Восстановление содержимого в новый файл
|
||||
func fdActionRecover(e fdEntry, reader *bufio.Reader) {
|
||||
defaultDir := cfgFDRecoverDir
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
defaultName := fmt.Sprintf("%s_pid%d_fd%s_%s", filepath.Base(e.OriginalPath), e.PID, e.FDNum, timestamp)
|
||||
defaultPath := filepath.Join(defaultDir, defaultName)
|
||||
|
||||
fmt.Printf("Путь для сохранения [%s]: ", defaultPath)
|
||||
dest, _ := reader.ReadString('\n')
|
||||
dest = strings.TrimSpace(dest)
|
||||
if dest == "" {
|
||||
dest = defaultPath
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
|
||||
fmt.Printf("%s❌ Не удалось создать директорию %s: %v%s\n", cRed, filepath.Dir(dest), err, cReset)
|
||||
return
|
||||
}
|
||||
|
||||
src, err := os.Open(e.FDPath)
|
||||
if err != nil {
|
||||
fmt.Printf("%s❌ Не удалось открыть %s: %v%s\n", cRed, e.FDPath, err, cReset)
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
fmt.Printf("%s❌ Не удалось создать %s: %v%s\n", cRed, dest, err, cReset)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
written, err := io.Copy(out, src)
|
||||
if err != nil {
|
||||
fmt.Printf("%s❌ Ошибка копирования: %v%s\n", cRed, err, cReset)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s✅ Восстановлено %s в %s%s\n", cGreen, fdHumanSize(written), dest, cReset)
|
||||
fmt.Printf("%sℹ️ Это снимок текущего содержимого. Процесс %d продолжит писать в удалённый инод,\n"+
|
||||
" место освободится только после kill/restart процесса (пункты 3–4).%s\n", cYellow, e.PID, cReset)
|
||||
}
|
||||
|
||||
// 2. Мягкий reopen сигнал (SIGHUP/SIGUSR1) — для служб, поддерживающих переоткрытие
|
||||
// файлов логов без полного рестарта (nginx, angie, rsyslog и т.п.)
|
||||
func fdActionReopenSignal(e fdEntry, reader *bufio.Reader) {
|
||||
fmt.Println("Какой сигнал отправить?")
|
||||
fmt.Println(" 1) SIGHUP (большинство демонов: nginx, angie, rsyslog, sshd)")
|
||||
fmt.Println(" 2) SIGUSR1 (некоторые демоны используют его для reopen логов)")
|
||||
fmt.Printf("%s>>> %s", cGreen, cReset)
|
||||
|
||||
choice, _ := reader.ReadString('\n')
|
||||
choice = strings.TrimSpace(choice)
|
||||
|
||||
var sig syscall.Signal
|
||||
var sigName string
|
||||
switch choice {
|
||||
case "1", "":
|
||||
sig, sigName = syscall.SIGHUP, "SIGHUP"
|
||||
case "2":
|
||||
sig, sigName = syscall.SIGUSR1, "SIGUSR1"
|
||||
default:
|
||||
fmt.Printf("%sНекорректный выбор.%s\n", cRed, cReset)
|
||||
return
|
||||
}
|
||||
|
||||
if err := syscall.Kill(e.PID, sig); err != nil {
|
||||
fmt.Printf("%s❌ Не удалось отправить %s процессу %d: %v%s\n", cRed, sigName, e.PID, err, cReset)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s✅ %s отправлен процессу %d (%s).%s\n", cGreen, sigName, e.PID, e.ProcessName, cReset)
|
||||
fmt.Printf("%sℹ️ Сигнал сработает только если процесс явно обрабатывает reopen-логику.\n"+
|
||||
" Если через пару секунд дескриптор всё ещё в списке — сигнал не помог, используйте restart/kill.%s\n",
|
||||
cYellow, cReset)
|
||||
}
|
||||
|
||||
// 3. Перезапуск через systemd (если удалось определить unit)
|
||||
func fdActionRestart(e fdEntry, unit string, hasUnit bool) {
|
||||
if !hasUnit {
|
||||
fmt.Printf("%s❌ Не удалось определить systemd-unit для PID %d (процесс не в cgroup системной службы).\n"+
|
||||
" Перезапустите процесс вручную или используйте kill (пункт 4).%s\n", cRed, e.PID, cReset)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Выполняю: systemctl restart %s\n", unit)
|
||||
cmd := exec.Command("systemctl", "restart", unit)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("%s❌ Ошибка перезапуска %s: %v\n%s%s\n", cRed, unit, err, string(output), cReset)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s✅ Служба %s перезапущена.%s\n", cGreen, unit, cReset)
|
||||
}
|
||||
|
||||
// fdDetectSystemdUnit пытается определить systemd-unit процесса через его cgroup.
|
||||
func fdDetectSystemdUnit(pid int) (string, bool) {
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pid))
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`([A-Za-z0-9_.@-]+\.service)`)
|
||||
matches := re.FindStringSubmatch(string(data))
|
||||
if len(matches) < 2 {
|
||||
return "", false
|
||||
}
|
||||
return matches[1], true
|
||||
}
|
||||
|
||||
// 4. Убить процесс: сначала SIGTERM, при неудаче — SIGKILL
|
||||
func fdActionKill(e fdEntry) {
|
||||
fmt.Printf("Отправляю SIGTERM процессу %d (%s)...\n", e.PID, e.ProcessName)
|
||||
if err := syscall.Kill(e.PID, syscall.SIGTERM); err != nil {
|
||||
fmt.Printf("%s❌ Не удалось отправить SIGTERM: %v%s\n", cRed, err, cReset)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
if fdProcessAlive(e.PID) {
|
||||
fmt.Printf("Процесс всё ещё жив, отправляю SIGKILL...\n")
|
||||
if err := syscall.Kill(e.PID, syscall.SIGKILL); err != nil {
|
||||
fmt.Printf("%s❌ Не удалось отправить SIGKILL: %v%s\n", cRed, err, cReset)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%s✅ Процесс %d завершён, место на диске будет освобождено.%s\n", cGreen, e.PID, cReset)
|
||||
fmt.Printf("%sℹ️ Если процесс управляется systemd/супервизором — он должен перезапуститься автоматически\n"+
|
||||
" и заново открыть файл по исходному пути.%s\n", cYellow, cReset)
|
||||
}
|
||||
|
||||
func fdProcessAlive(pid int) bool {
|
||||
_, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
|
||||
return err == nil
|
||||
}
|
||||
2
go.mod
2
go.mod
|
|
@ -31,5 +31,5 @@ require (
|
|||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
||||
4
go.sum
4
go.sum
|
|
@ -50,9 +50,13 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
|
|||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
|
|
|
|||
14
main.go
14
main.go
|
|
@ -104,6 +104,13 @@ func runByFlag(flag string, extraArgs []string) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "-fd":
|
||||
fmt.Printf("%s🗑️ Auspex — Удалённые-но-открытые файловые дескрипторы%s\n\n", cCyan, cReset)
|
||||
if err := runFDCheck(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "-v", "--version":
|
||||
fmt.Printf("auspex v%s\n", appVersion)
|
||||
|
||||
|
|
@ -129,6 +136,7 @@ func showMenu() {
|
|||
fmt.Printf(" %s6%s) %s-sv%s Проверка состояния служб (systemd)\n", cGreen, cReset, cCyan, cReset)
|
||||
fmt.Printf(" %s7%s) %s-dc%s Проверка контейнеров Docker\n", cGreen, cReset, cCyan, cReset)
|
||||
fmt.Printf(" %s8%s) %s-w %s Проверка доступности сайтов (lynx) [sid]\n", cGreen, cReset, cCyan, cReset)
|
||||
fmt.Printf(" %s9%s) %s-fd%s Удалённые-но-открытые файловые дескрипторы\n", cGreen, cReset, cCyan, cReset)
|
||||
fmt.Printf(" %s0%s) Выход\n\n", cGreen, cReset)
|
||||
fmt.Printf("%sДля быстрого вызова: auspex <ключ>%s\n", cYellow, cReset)
|
||||
fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l | auspex -sv | auspex -dc | auspex -w <sid>%s\n\n", cYellow, cReset)
|
||||
|
|
@ -187,6 +195,11 @@ func showMenu() {
|
|||
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||
}
|
||||
return
|
||||
case "9", "-fd", "fd":
|
||||
if err := runFDCheck(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||
}
|
||||
return
|
||||
case "0", "q", "quit", "exit":
|
||||
fmt.Println("Выход.")
|
||||
return
|
||||
|
|
@ -219,6 +232,7 @@ func printUsage() {
|
|||
fmt.Printf(" %-12s Проверка состояния служб systemd\n", "-sv")
|
||||
fmt.Printf(" %-12s Проверка контейнеров Docker\n", "-dc")
|
||||
fmt.Printf(" %-12s Проверка доступности сайтов через lynx [sid]\n", "-w")
|
||||
fmt.Printf(" %-12s Удалённые-но-открытые файловые дескрипторы (recover/reopen/restart/kill)\n", "-fd")
|
||||
fmt.Printf(" %-12s Показать версию\n", "-v")
|
||||
fmt.Printf(" %-12s Показать эту справку\n\n", "-h")
|
||||
fmt.Printf("Без аргументов запускается интерактивное меню.\n")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue