package main
import (
"bytes"
"errors"
"fmt"
"net/netip"
"os"
"strconv"
"strings"
"syscall"
"time"
)
// A lockfile marks a served tree. Its name is the endpoint that serves it,
// eg 127.31.1.1-8080.lock, its body says how to reach and stop the server,
// and deleting it withdraws the tree. A persistent lock, written by
// --persist, ends in capitals, eg 127.31.1.1-8080.LOCK: it holds no PID,
// outlives its server and a reboot, and the next server on its endpoint
// serves the tree again.
const (
lockSuffix = ".lock"
persistSuffix = ".LOCK"
)
// endpointName joins address and port with a dash. A colon trips up shell
// completion, scp, rsync and Make.
func endpointName(ep netip.AddrPort) string {
return fmt.Sprintf("%s-%d", ep.Addr(), ep.Port())
}
func lockName(ep netip.AddrPort) string {
return endpointName(ep) + lockSuffix
}
func persistName(ep netip.AddrPort) string {
return endpointName(ep) + persistSuffix
}
// isPersist reports whether a lock path names a persistent lock.
func isPersist(path string) bool {
return strings.HasSuffix(path, persistSuffix)
}
// lockNames reports whether base is either lock of ep.
func lockNames(ep netip.AddrPort, base string) bool {
return base == lockName(ep) || base == persistName(ep)
}
func isLockName(name string) bool {
base, ok := strings.CutSuffix(name, lockSuffix)
if !ok {
if base, ok = strings.CutSuffix(name, persistSuffix); !ok {
return false
}
}
// An IPv4 address holds no dash, so the first one ends it.
addr, port, ok := strings.Cut(base, "-")
if !ok {
return false
}
a, err := netip.ParseAddr(addr)
if err != nil || !a.Is4() {
return false
}
n, err := strconv.Atoi(port)
return err == nil && n > 0 && n < 1<<16
}
type lockInfo struct {
pid int
url string
bootID string
since time.Time // when a server record was written, as the server started
}
func parseLock(b []byte) lockInfo {
var l lockInfo
for _, line := range strings.Split(string(b), "\n") {
k, v, ok := strings.Cut(line, ": ")
if !ok {
continue
}
switch k {
case "pid":
l.pid, _ = strconv.Atoi(v)
case "url":
l.url = v
case "boot_id":
l.bootID = v
}
}
return l
}
// bootID changes on every boot, so a lock from before a reboot is stale
// even when its PID now belongs to a live process.
func bootID() string {
b, _ := os.ReadFile("/proc/sys/kernel/random/boot_id")
return strings.TrimSpace(string(b))
}
func (l lockInfo) live(boot string) bool {
return l.url != "" && pidLive(l.pid, l.bootID, boot)
}
// pidLive reports whether pid, recorded in the boot was, still runs in
// the boot now.
func pidLive(pid int, was, now string) bool {
if pid <= 0 {
return false
}
if was != "" && now != "" && was != now {
return false
}
err := syscall.Kill(pid, 0)
return err == nil || errors.Is(err, syscall.EPERM)
}
// readLive reads the lock at path and reports whether its server runs. A
// persistent lock holds no PID, so it takes the PID of the server that
// the registry of ep names. That server serves the tree only if it wrote
// the lock, after it started: a lock from an earlier server, kept for
// the next, may name a URL that another tree has taken since.
func readLive(ep netip.AddrPort, path string) (lockInfo, bool) {
b, err := os.ReadFile(path)
if err != nil {
return lockInfo{}, false
}
l := parseLock(b)
if isPersist(path) {
srv, ok := serverOf(ep)
if !ok || l.url == "" {
return l, false
}
info, err := os.Stat(path)
if err != nil || info.ModTime().Before(srv.since) {
return l, false
}
l.pid, l.bootID = srv.pid, srv.bootID
}
return l, l.live(bootID())
}
// findLive returns the lock at path if its server still runs. A stale lock
// is deleted on the way, so a crash leaves nothing behind for long; a
// persistent one stays for the next server. Callers hold the registry
// lock, so the server cannot be halfway through writing it.
func findLive(ep netip.AddrPort, path string) (lockInfo, bool) {
l, ok := readLive(ep, path)
if ok || isPersist(path) {
return l, ok
}
if os.Remove(path) == nil {
logInfo("removed stale lock %s: pid %d is gone or from an earlier boot", path, l.pid)
}
return lockInfo{}, false
}
type lock struct {
path string
info os.FileInfo
body []byte // what this server wrote, to tell its lock from a new one
}
// createLock writes the lock at path. A persistent lock that is already
// there, from an earlier server, is rewritten in place, so its URL is the
// one this server answers on.
func createLock(path, root, url string) (*lock, error) {
flags := os.O_WRONLY | os.O_CREATE | os.O_EXCL
if isPersist(path) {
flags = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
}
f, err := os.OpenFile(path, flags, 0o644)
if err != nil {
return nil, err
}
var body string
if isPersist(path) {
body = fmt.Sprintf("# "+progName+" serves this directory, after reboot too. Delete this file to stop serving it.\n"+
"url: %s\nroot: %s\nstarted: %s\n", url, root, time.Now().Format(time.RFC3339))
} else {
body = fmt.Sprintf("# "+progName+" is serving this directory. Delete this file to stop serving it.\n"+
"url: %s\nroot: %s\npid: %d\nstarted: %s\nboot_id: %s\n",
url, root, os.Getpid(), time.Now().Format(time.RFC3339), bootID())
}
_, err = f.WriteString(body)
if cerr := f.Close(); err == nil {
err = cerr
}
if err != nil {
os.Remove(path)
return nil, err
}
info, err := os.Stat(path)
if err != nil {
return nil, err
}
return &lock{path: path, info: info, body: []byte(body)}, nil
}
// gone reports whether the lock was deleted or replaced by another file.
// The file's identity alone cannot tell: ext4 hands a new file the inode
// of one just deleted, so a replacement can look like the same file. The
// body tells, since another server's lock holds another PID or start.
func (l *lock) gone() bool {
info, err := os.Stat(l.path)
if err != nil || !os.SameFile(info, l.info) {
return true
}
b, err := os.ReadFile(l.path)
return err != nil || !bytes.Equal(b, l.body)
}
// persist reports whether the lock outlives its server.
func (l *lock) persist() bool {
return isPersist(l.path)
}
// remove deletes the lock unless it is already gone, so it never deletes
// a lock that belongs to another server.
func (l *lock) remove() {
if !l.gone() {
os.Remove(l.path)
}
}