Added truncate

This commit is contained in:
Magnus Root 2026-07-27 13:13:01 +03:00
parent bce97223f0
commit 61af364008

View file

@ -236,11 +236,18 @@ func fdHandleAction(e fdEntry, reader *bufio.Reader) {
fmt.Printf("%sОпределена служба systemd:%s %s\n", cBlue, cReset, unit) fmt.Printf("%sОпределена служба systemd:%s %s\n", cBlue, cReset, unit)
} }
appendMode, appendErr := fdIsAppendMode(e.PID, e.FDNum)
fmt.Println("\nВыберите действие:") fmt.Println("\nВыберите действие:")
fmt.Println(" 1) Восстановить содержимое в новый файл (recover)") fmt.Println(" 1) Восстановить содержимое в новый файл (recover)")
fmt.Println(" 2) Мягкий reopen сигналом (SIGHUP / SIGUSR1)") fmt.Println(" 2) Мягкий reopen сигналом (SIGHUP / SIGUSR1)")
fmt.Println(" 3) Перезапустить службу (systemctl restart)") fmt.Println(" 3) Перезапустить службу (systemctl restart)")
fmt.Println(" 4) Убить процесс (SIGTERM → SIGKILL)") fmt.Println(" 4) Убить процесс (SIGTERM → SIGKILL)")
if appendErr == nil && appendMode {
fmt.Println(" 5) Truncate на месте — без рестарта (файл открыт в O_APPEND, безопасно)")
} else {
fmt.Println(" 5) Truncate на месте — без рестарта (⚠️ НЕ O_APPEND, риск повреждения данных)")
}
fmt.Println(" 0) Отмена") fmt.Println(" 0) Отмена")
fmt.Printf("%s>>> %s", cGreen, cReset) fmt.Printf("%s>>> %s", cGreen, cReset)
@ -256,6 +263,8 @@ func fdHandleAction(e fdEntry, reader *bufio.Reader) {
fdActionRestart(e, unit, hasUnit) fdActionRestart(e, unit, hasUnit)
case "4": case "4":
fdActionKill(e) fdActionKill(e)
case "5":
fdActionTruncate(e, reader, appendMode, appendErr)
case "0", "": case "0", "":
fmt.Println("Отменено.") fmt.Println("Отменено.")
default: default:
@ -265,10 +274,24 @@ func fdHandleAction(e fdEntry, reader *bufio.Reader) {
// 1. Восстановление содержимого в новый файл // 1. Восстановление содержимого в новый файл
func fdActionRecover(e fdEntry, reader *bufio.Reader) { func fdActionRecover(e fdEntry, reader *bufio.Reader) {
defaultDir := cfgFDRecoverDir dest := fdPromptRecoverPath(e, reader)
written, err := fdRecoverToFile(e, dest)
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"+
" место освободится только после truncate/kill/restart (пункты 35).%s\n", cYellow, e.PID, cReset)
}
// fdPromptRecoverPath спрашивает путь для сохранения снимка, предлагая разумный дефолт.
func fdPromptRecoverPath(e fdEntry, reader *bufio.Reader) string {
timestamp := time.Now().Format("20060102_150405") timestamp := time.Now().Format("20060102_150405")
defaultName := fmt.Sprintf("%s_pid%d_fd%s_%s", filepath.Base(e.OriginalPath), e.PID, e.FDNum, timestamp) defaultName := fmt.Sprintf("%s_pid%d_fd%s_%s", filepath.Base(e.OriginalPath), e.PID, e.FDNum, timestamp)
defaultPath := filepath.Join(defaultDir, defaultName) defaultPath := filepath.Join(cfgFDRecoverDir, defaultName)
fmt.Printf("Путь для сохранения [%s]: ", defaultPath) fmt.Printf("Путь для сохранения [%s]: ", defaultPath)
dest, _ := reader.ReadString('\n') dest, _ := reader.ReadString('\n')
@ -276,35 +299,33 @@ func fdActionRecover(e fdEntry, reader *bufio.Reader) {
if dest == "" { if dest == "" {
dest = defaultPath dest = defaultPath
} }
return dest
}
// fdRecoverToFile копирует текущее содержимое удалённого файла в dest.
func fdRecoverToFile(e fdEntry, dest string) (int64, error) {
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
fmt.Printf("%s❌ Не удалось создать директорию %s: %v%s\n", cRed, filepath.Dir(dest), err, cReset) return 0, fmt.Errorf("не удалось создать директорию %s: %w", filepath.Dir(dest), err)
return
} }
src, err := os.Open(e.FDPath) src, err := os.Open(e.FDPath)
if err != nil { if err != nil {
fmt.Printf("%s❌ Не удалось открыть %s: %v%s\n", cRed, e.FDPath, err, cReset) return 0, fmt.Errorf("не удалось открыть %s: %w", e.FDPath, err)
return
} }
defer src.Close() defer src.Close()
out, err := os.Create(dest) out, err := os.Create(dest)
if err != nil { if err != nil {
fmt.Printf("%s❌ Не удалось создать %s: %v%s\n", cRed, dest, err, cReset) return 0, fmt.Errorf("не удалось создать %s: %w", dest, err)
return
} }
defer out.Close() defer out.Close()
written, err := io.Copy(out, src) written, err := io.Copy(out, src)
if err != nil { if err != nil {
fmt.Printf("%s❌ Ошибка копирования: %v%s\n", cRed, err, cReset) return written, fmt.Errorf("ошибка копирования: %w", err)
return
} }
fmt.Printf("%s✅ Восстановлено %s в %s%s\n", cGreen, fdHumanSize(written), dest, cReset) return written, nil
fmt.Printf("%s Это снимок текущего содержимого. Процесс %d продолжит писать в удалённый инод,\n"+
" место освободится только после kill/restart процесса (пункты 34).%s\n", cYellow, e.PID, cReset)
} }
// 2. Мягкий reopen сигнал (SIGHUP/SIGUSR1) — для служб, поддерживающих переоткрытие // 2. Мягкий reopen сигнал (SIGHUP/SIGUSR1) — для служб, поддерживающих переоткрытие
@ -402,3 +423,101 @@ func fdProcessAlive(pid int) bool {
_, err := os.Stat(fmt.Sprintf("/proc/%d", pid)) _, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
return err == nil return err == nil
} }
// ── Truncate на месте (copytruncate без рестарта) ────────────────────────────
// fdIsAppendMode определяет, открыт ли fd в режиме O_APPEND через /proc/PID/fdinfo/N.
// Это критично: если O_APPEND — ядро само двигает позицию записи в конец файла
// перед каждым write(), поэтому truncate-до-нуля безопасен (ровно то, что делает
// logrotate в режиме copytruncate). Если нет — у процесса свой курсор записи,
// и после truncate следующая запись создаст дыру не в том месте, где ожидает
// приложение (риск повреждения данных / SIGBUS при mmap).
func fdIsAppendMode(pid int, fdNum string) (bool, error) {
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/fdinfo/%s", pid, fdNum))
if err != nil {
return false, fmt.Errorf("не удалось прочитать fdinfo: %w", err)
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "flags:") {
fields := strings.Fields(line)
if len(fields) < 2 {
return false, fmt.Errorf("неожиданный формат строки flags")
}
flags, err := strconv.ParseInt(fields[1], 8, 64)
if err != nil {
return false, fmt.Errorf("не удалось распарсить flags: %w", err)
}
return flags&syscall.O_APPEND != 0, nil
}
}
return false, fmt.Errorf("строка flags не найдена в fdinfo")
}
// 5. Truncate на месте: обнуляет файл через тот же inode, процесс не перезапускается.
func fdActionTruncate(e fdEntry, reader *bufio.Reader, appendMode bool, appendErr error) {
fmt.Printf("\n%s── Truncate на месте (copytruncate) ──%s\n", cBlue, cReset)
fmt.Println("Механизм: открываем тот же inode через /proc/PID/fd/N и обнуляем его.")
fmt.Println("Место освобождается мгновенно, процесс НЕ перезапускается и продолжает писать в этот же fd.")
switch {
case appendErr != nil:
fmt.Printf("%s⚠ Не удалось определить режим открытия (%v).\n"+
" Считаю файл НЕ append-safe — truncate может повредить данные.%s\n", cYellow, appendErr, cReset)
case appendMode:
fmt.Printf("%s✅ Файл открыт в режиме O_APPEND — это типичный лог, truncate безопасен\n"+
" (процесс сам переходит в конец файла перед каждой записью).%s\n", cGreen, cReset)
default:
fmt.Printf("%s⚠ Файл НЕ открыт в O_APPEND! У процесса свой курсор записи —\n"+
" после truncate следующая запись попадёт не в начало, а в старую позицию,\n"+
" образуя дыру. Если файл ещё и замаплен в память (mmap) — возможен SIGBUS\n"+
" и падение процесса. Это НЕ похоже на append-лог (БД, бинарный формат,\n"+
" временный файл с произвольным доступом). Рекомендуется kill/restart вместо этого.%s\n",
cRed, cReset)
}
fmt.Print("\nСохранить снимок содержимого перед truncate? [Y/n]: ")
snapAns, _ := reader.ReadString('\n')
snapAns = strings.TrimSpace(strings.ToLower(snapAns))
if snapAns != "n" && snapAns != "no" {
dest := fdPromptRecoverPath(e, reader)
written, err := fdRecoverToFile(e, dest)
if err != nil {
fmt.Printf("%s❌ Снимок не сохранён: %v — truncate прерван для безопасности.%s\n", cRed, err, cReset)
return
}
fmt.Printf("%s✅ Снимок сохранён: %s (%s)%s\n", cGreen, dest, fdHumanSize(written), cReset)
}
if !appendMode {
fmt.Print("\nФайл не в режиме O_APPEND. Введите заглавными 'ПОНИМАЮ РИСК' для продолжения: ")
confirm, _ := reader.ReadString('\n')
if strings.TrimSpace(confirm) != "ПОНИМАЮ РИСК" {
fmt.Println("Отменено.")
return
}
} else {
fmt.Print("\nПодтвердите truncate, введите 'TRUNCATE': ")
confirm, _ := reader.ReadString('\n')
if strings.TrimSpace(confirm) != "TRUNCATE" {
fmt.Println("Отменено.")
return
}
}
out, err := os.OpenFile(e.FDPath, os.O_WRONLY, 0)
if err != nil {
fmt.Printf("%s❌ Не удалось открыть %s на запись: %v%s\n", cRed, e.FDPath, err, cReset)
return
}
defer out.Close()
if err := out.Truncate(0); err != nil {
fmt.Printf("%s❌ Ошибка truncate: %v%s\n", cRed, err, cReset)
return
}
fmt.Printf("%s✅ Файл обнулён (%s освобождено), процесс %d (%s) продолжает работать без рестарта.%s\n",
cGreen, fdHumanSize(e.SizeBytes), e.PID, e.ProcessName, cReset)
}