PrintToVeraeTimeStamp/internal/winprint/watch.go

108 lines
3 KiB
Go

package winprint
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
// Watcher polls a drop folder written by the spooler through a Local file
// port (Microsoft Print To PDF → C:\ProgramData\Verae\spool\fallback\verae-job.pdf).
// When a file is stable and can be opened exclusively it is moved into the
// per-user work dir under a unique name and handed to Process.
type Watcher struct {
Dir string
WorkDir string
Interval time.Duration
Process Processor
}
// Run blocks forever.
func (w *Watcher) Run() {
if w.Interval <= 0 {
w.Interval = time.Second
}
_ = os.MkdirAll(w.WorkDir, 0o700)
vlog.Printf("watch: polling %s every %s (work=%s)", w.Dir, w.Interval, w.WorkDir)
sizes := map[string]int64{}
warned := false
for {
ents, err := os.ReadDir(w.Dir)
if err != nil {
if !warned {
vlog.Errorf("watch: read %s: %v (fallback printer disabled until it exists)", w.Dir, err)
warned = true
}
time.Sleep(5 * time.Second)
continue
}
warned = false
seen := map[string]bool{}
for _, e := range ents {
if e.IsDir() || strings.HasPrefix(e.Name(), ".") || strings.HasSuffix(strings.ToLower(e.Name()), ".tmp") {
continue
}
p := filepath.Join(w.Dir, e.Name())
seen[p] = true
st, err := os.Stat(p)
if err != nil || st.Size() == 0 {
continue
}
prev, ok := sizes[p]
sizes[p] = st.Size()
if !ok || prev != st.Size() || time.Since(st.ModTime()) < 2*time.Second {
continue // still growing
}
w.claim(p, st.Size())
delete(sizes, p)
}
for p := range sizes {
if !seen[p] {
delete(sizes, p)
}
}
time.Sleep(w.Interval)
}
}
func (w *Watcher) claim(p string, size int64) {
id := vlog.NewJobID("watch")
lj := vlog.Job(id)
dst := filepath.Join(w.WorkDir, id+".pdf")
// Rename fails while the spooler still holds the file open (Windows sharing rules).
if err := os.Rename(p, dst); err != nil {
// cross-volume or locked: try copy+remove
b, rerr := os.ReadFile(p)
if rerr != nil {
lj.Debugf("watch: %s not ready: %v / %v", p, err, rerr)
return
}
if werr := os.WriteFile(dst, b, 0o600); werr != nil {
lj.Errorf("watch: copy to %s: %v", dst, werr)
return
}
if derr := os.Remove(p); derr != nil {
lj.Errorf("watch: copied but could not remove %s: %v (will re-detect; check ACLs)", p, derr)
}
}
lj.Printf("watch: claimed %s (%d bytes, magic=%q) → %s", p, size, sniff(dst), dst)
j := Job{ID: id, Source: "watch", Title: "Printed " + time.Now().Format("2006-01-02 150405"), Format: "application/pdf", Path: dst, Bytes: size, Received: time.Now()}
start := time.Now()
err := func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return w.Process(j)
}()
if err != nil {
lj.Errorf("watch: job FAILED after %s: %v", time.Since(start).Round(time.Millisecond), err)
return
}
lj.Printf("watch: job completed in %s", time.Since(start).Round(time.Millisecond))
}