From 0642028735adcb2d431e5e146ca87accb4954900 Mon Sep 17 00:00:00 2001 From: Bernhard Guillon Date: Tue, 7 Jul 2026 09:20:10 +0200 Subject: Add Windows-assisted initial sync for faster startup New protocol: - Windows binary scans directory (fast native NTFS) and sends manifest - Container receives manifest, compares with local /data state - Only copies files that differ (size, modtime, mode, symlink) Protocol messages: - manifest: sends full file metadata from Windows - manifest_done: signals manifest transfer complete - event_create/write/remove: file change events Flow: 1. Container connects to socket 2. Windows binary sends manifest (file metadata) 3. Container scans /data (fast local volume) 4. Container copies only changed files from /host (9P bind mount) 5. Container enters event-waiting loop This dramatically speeds up initial sync by avoiding slow 9P scans. --- cmd_windows.go | 165 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 137 insertions(+), 28 deletions(-) (limited to 'cmd_windows.go') diff --git a/cmd_windows.go b/cmd_windows.go index b68851a..61c7f1d 100644 --- a/cmd_windows.go +++ b/cmd_windows.go @@ -3,8 +3,8 @@ package main import ( + "bufio" "context" - "encoding/json" "flag" "fmt" "net" @@ -38,10 +38,12 @@ func run() { } SetLogLevel(level) - logInfo("watcher: starting on %s", *sourceDir) - logInfo("watcher: serving on %s", *socketPath) + logInfo("watcher: scanning %s ...", *sourceDir) stack := NewGitignoreStack(*sourceDir) + manifest := scanDirectory(*sourceDir, stack) + + logInfo("watcher: found %d files, serving on %s", len(manifest.Files), *socketPath) watcher, err := fsnotify.NewWatcher() if err != nil { @@ -86,40 +88,25 @@ func run() { logDebug("client connected: %s", conn.RemoteAddr()) go func(c net.Conn) { - defer func() { - clientsMu.Lock() - delete(clients, c) - clientsMu.Unlock() - c.Close() - }() - buf := make([]byte, 1) - for { - if _, err := c.Read(buf); err != nil { - return - } + sendManifest(c, manifest) + + scanner := bufio.NewScanner(c) + for scanner.Scan() { } + + clientsMu.Lock() + delete(clients, c) + clientsMu.Unlock() + c.Close() }(conn) } }() broadcast := func(evt FileEvent) { - data, err := json.Marshal(evt) - if err != nil { - return - } - data = append(data, '\n') - clientsMu.Lock() defer clientsMu.Unlock() for c := range clients { - if _, err := c.Write(data); err != nil { - go func() { - clientsMu.Lock() - delete(clients, c) - clientsMu.Unlock() - c.Close() - }() - } + sendEvent(c, evt) } } @@ -205,6 +192,39 @@ func run() { return } + // Update manifest + srcPath := filepath.Join(*sourceDir, path) + switch evt.Op { + case "create": + if info, err := os.Lstat(srcPath); err == nil { + linkTarget := "" + if info.Mode()&os.ModeSymlink != 0 { + linkTarget, _ = os.Readlink(srcPath) + } + manifest.Files[path] = FileMeta{ + Size: info.Size(), + Mode: info.Mode(), + ModTime: info.ModTime(), + Symlink: linkTarget, + } + } + case "write": + if info, err := os.Lstat(srcPath); err == nil { + linkTarget := "" + if info.Mode()&os.ModeSymlink != 0 { + linkTarget, _ = os.Readlink(srcPath) + } + manifest.Files[path] = FileMeta{ + Size: info.Size(), + Mode: info.Mode(), + ModTime: info.ModTime(), + Symlink: linkTarget, + } + } + case "remove": + delete(manifest.Files, path) + } + broadcast(evt) }) mu.Unlock() @@ -218,6 +238,95 @@ func run() { } } +func scanDirectory(root string, stack *GitignoreStack) *Manifest { + manifest := &Manifest{Files: make(map[string]FileMeta)} + + filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + + relPath, err := filepath.Rel(root, path) + if err != nil { + return nil + } + + if relPath == "." { + return nil + } + + if isGitDir(relPath) { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + + if info.IsDir() && hasGitignore(path) { + stack.Push(path) + } + + if stack.IsIgnored(relPath) { + return nil + } + + linkTarget := "" + if info.Mode()&os.ModeSymlink != 0 { + linkTarget, _ = os.Readlink(path) + } + + manifest.Files[relPath] = FileMeta{ + Size: info.Size(), + Mode: info.Mode(), + ModTime: info.ModTime(), + Symlink: linkTarget, + } + + return nil + }) + + return manifest +} + +func sendManifest(conn net.Conn, manifest *Manifest) { + msg := &ProtocolMessage{ + Type: "manifest", + Files: manifest.ToJSON(), + } + data, err := EncodeMessage(msg) + if err != nil { + return + } + data = append(data, '\n') + conn.Write(data) + + done := &ProtocolMessage{Type: "manifest_done"} + doneData, _ := EncodeMessage(done) + doneData = append(doneData, '\n') + conn.Write(doneData) +} + +func sendEvent(conn net.Conn, evt FileEvent) { + msg := &ProtocolMessage{ + Type: "event", + Path: evt.Path, + } + switch evt.Op { + case "create": + msg.Type = "event_create" + case "write": + msg.Type = "event_write" + case "remove": + msg.Type = "event_remove" + } + data, err := EncodeMessage(msg) + if err != nil { + return + } + data = append(data, '\n') + conn.Write(data) +} + func addWatchersRecursively(watcher *fsnotify.Watcher, root string, stack *GitignoreStack) error { return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { -- cgit v1.2.3