Added truncate
This commit is contained in:
parent
bce97223f0
commit
61af364008
1 changed files with 132 additions and 13 deletions
145
fdcheck.go
145
fdcheck.go
|
|
@ -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 (пункты 3–5).%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 процесса (пункты 3–4).%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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue