Initial commit: hotpcalc CLI

This commit is contained in:
2026-09-20 20:04:18 +03:00
commit 8f42dc690e
24 changed files with 5182 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
package store
import (
"fmt"
"os"
"strings"
"time"
)
const (
// lockTimeout — сколько ждём освобождения хранилища другим процессом.
lockTimeout = 5 * time.Second
// lockStaleAfter — после этого возраста замок считается брошенным
// (процесс убит, не успев его снять).
lockStaleAfter = 60 * time.Second
)
// Lock — межпроцессная блокировка хранилища на основе файла, создаваемого
// с O_EXCL. В отличие от flock работает одинаково на Unix и Windows.
type Lock struct {
path string
warn func(string)
}
// acquireLock захватывает замок для файла хранилища path.
// warn получает предупреждения (обычно печатается в stderr); может быть nil.
func acquireLock(path string, warn func(string)) (*Lock, error) {
if warn == nil {
warn = func(string) {}
}
l := &Lock{path: path + ".lock", warn: warn}
deadline := time.Now().Add(lockTimeout)
delay := 10 * time.Millisecond
brokeStale := false
for {
f, err := os.OpenFile(l.path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err == nil {
fmt.Fprintf(f, "pid %d, %s\n", os.Getpid(), time.Now().Format(time.RFC3339))
f.Close()
return l, nil
}
if !os.IsExist(err) {
return nil, fmt.Errorf("не удалось создать файл блокировки %s: %w", l.path, err)
}
// Замок занят: возможно, его владелец давно умер.
if !brokeStale {
if fi, statErr := os.Stat(l.path); statErr == nil && time.Since(fi.ModTime()) > lockStaleAfter {
l.warn(fmt.Sprintf("снимаю брошенный файл блокировки %s (возраст %s)",
l.path, time.Since(fi.ModTime()).Round(time.Second)))
os.Remove(l.path)
brokeStale = true
continue
}
}
if time.Now().After(deadline) {
return nil, fmt.Errorf("хранилище занято другим процессом: файл блокировки %s.\n"+
"Если процесс уже завершён, удалите этот файл вручную", l.path)
}
time.Sleep(delay)
if delay < 200*time.Millisecond {
delay *= 2
}
}
}
// release снимает блокировку.
func (l *Lock) release() error {
if l == nil {
return nil
}
if err := os.Remove(l.path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("не удалось снять блокировку %s: %w", l.path, err)
}
return nil
}
// IsLockPath сообщает, является ли путь служебным файлом блокировки.
func IsLockPath(p string) bool { return strings.HasSuffix(p, ".lock") }