Initial commit: hotpcalc CLI
This commit is contained in:
@@ -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") }
|
||||
Reference in New Issue
Block a user