123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 | // Package queue is the append-only job log written by the post-receive hook
// and drained by repo-render under an exclusive flock.
package queue
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/unix"
)
// Job is one queue line: repo TAB ref TAB old TAB new.
type Job struct {
Repo string
Ref string
Old string
New string
}
// Format renders the job as the hook would write it.
func (j Job) Format() string {
return fmt.Sprintf("%s\t%s\t%s\t%s\n", j.Repo, j.Ref, j.Old, j.New)
}
// Append adds one job with a single write(2). The hook and the renderer
// both run as git, so the file is git's alone.
//
// Appends hold a shared flock and Take holds an exclusive one, so a job is
// never written into a queue file that Take has already read. An appender
// that opened the file just before Take renamed it notices, once it has the
// lock, that the name now points elsewhere, and starts over on the new file.
func Append(path string, j Job) error {
for {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
if err != nil {
return err
}
if err := unix.Flock(int(f.Fd()), unix.LOCK_SH); err != nil {
_ = f.Close() // nothing written yet
return err
}
if !stillNamed(f, path) {
_ = f.Close() // nothing written yet
continue
}
_, err = f.WriteString(j.Format())
if cerr := f.Close(); err == nil {
err = cerr
}
return err
}
}
// stillNamed reports whether path still refers to the open file f.
func stillNamed(f *os.File, path string) bool {
a, err := f.Stat()
if err != nil {
return false
}
b, err := os.Stat(path)
return err == nil && os.SameFile(a, b)
}
// Take atomically claims the current queue contents: the file is renamed
// aside (new kicks append to a fresh file) and parsed. The caller deletes the
// returned claimed path once every job in it has been handled; a crash leaves
// it in place for Recover.
//
// The caller must hold the render lock, which makes it the only process
// creating claimed files: the name it picks stays free until the rename.
func Take(path string) (jobs []Job, claimed string, err error) {
// A crashed run with a reused pid may have left this pid's name behind,
// still waiting to be handled; renaming onto it would lose its jobs.
claimed = fmt.Sprintf("%s.%d", path, os.Getpid())
for n := 1; ; n++ {
if _, err := os.Lstat(claimed); errors.Is(err, fs.ErrNotExist) {
break
} else if err != nil {
return nil, "", err
}
claimed = fmt.Sprintf("%s.%d.%d", path, os.Getpid(), n)
}
if err := os.Rename(path, claimed); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, "", nil
}
return nil, "", err
}
f, err := os.Open(claimed)
if err != nil {
return nil, "", err
}
defer func() { _ = f.Close() }() // read only, through parse
// Wait out appenders that got the file before the rename. Later ones
// see the rename and write to the fresh queue instead.
if err := unix.Flock(int(f.Fd()), unix.LOCK_EX); err != nil {
return nil, "", err
}
jobs, err = parse(claimed)
return jobs, claimed, err
}
// Recover returns jobs from claimed files left behind by crashed runs, and
// those files. It leaves the files in place: the caller deletes them once
// the jobs are handled, so a second crash recovers them again. Jobs are
// idempotent so re-running them is safe.
func Recover(path string) (jobs []Job, files []string, err error) {
// Listed, not globbed: the directory's own name may hold glob
// characters.
dir, prefix := filepath.Dir(path), filepath.Base(path)+"."
ents, err := os.ReadDir(dir)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, nil
}
if err != nil {
return nil, nil, err
}
for _, e := range ents {
if !strings.HasPrefix(e.Name(), prefix) {
continue
}
m := filepath.Join(dir, e.Name())
js, err := parse(m)
if err != nil {
return nil, nil, err
}
jobs = append(jobs, js...)
files = append(files, m)
}
return jobs, files, nil
}
func parse(path string) ([]Job, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var jobs []Job
for line := range strings.Lines(string(data)) {
line = strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r")
f := strings.Split(line, "\t")
if len(f) != 4 {
// A blank, torn or hand-edited line, however long: never let one
// bad line wedge the queue.
continue
}
jobs = append(jobs, Job{Repo: f[0], Ref: f[1], Old: f[2], New: f[3]})
}
return jobs, nil
}
// Lock takes a blocking exclusive flock on path and returns the file; the
// lock is released when the file is closed or the process exits.
func Lock(path string) (*os.File, error) {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644)
if err != nil {
return nil, err
}
if err := unix.Flock(int(f.Fd()), unix.LOCK_EX); err != nil {
_ = f.Close() // the flock error is the one to report
return nil, err
}
return f, nil
}
|