| 1 | package skillwatch |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | ) |
| 8 | |
| 9 | // wireKind identifies one frame of the host <-> watcher helper pipe protocol. |
| 10 | // The helper process is an internal entry of the existing host executable; the |
| 11 | // protocol must stay compatible across a helper and host built from the same |
| 12 | // source tree, which the host enforces by spawning its own executable. |
| 13 | type wireKind uint8 |
| 14 | |
| 15 | const ( |
| 16 | // Host -> helper. |
| 17 | wireRegister wireKind = 1 // watch dirs under one logical registration |
| 18 | wireCancel wireKind = 2 // revoke a logical registration |
| 19 | wireShutdown wireKind = 3 // helper exits after replying |
| 20 | wirePing wireKind = 4 // control-path liveness probe |
| 21 | |
| 22 | // Helper -> host. |
| 23 | wireReady wireKind = 5 // helper finished initializing |
| 24 | wireRegistered wireKind = 6 // registration confirmed |
| 25 | wireEvent wireKind = 7 // coalescing input: one filesystem event |
| 26 | wireError wireKind = 8 // registration or backend failure |
| 27 | wirePong wireKind = 9 // ping reply |
| 28 | ) |
| 29 | |
| 30 | // Op flags mirror the fsnotify operations the service reacts to. Keeping the |
| 31 | // protocol independent of fsnotify types lets the helper binary compile without |
| 32 | // dragging host-side assumptions across the pipe. |
| 33 | type Op uint32 |
| 34 | |
| 35 | const ( |
| 36 | OpCreate Op = 1 << iota |
| 37 | OpRemove |
| 38 | OpRename |
| 39 | OpWrite |
| 40 | OpChmod |
| 41 | ) |
| 42 | |
| 43 | func (o Op) String() string { |
| 44 | switch o { |
| 45 | case OpCreate: |
| 46 | return "create" |
| 47 | case OpRemove: |
| 48 | return "remove" |
| 49 | case OpRename: |
| 50 | return "rename" |
| 51 | case OpWrite: |
| 52 | return "write" |
| 53 | case OpChmod: |
| 54 | return "chmod" |
| 55 | } |
| 56 | return "op" |
| 57 | } |
| 58 | |
| 59 | const wireMaxFrameBytes = 8 << 20 |
| 60 | |
| 61 | // frame is one length-prefixed JSON message. ID is a logical registration; |
| 62 | // RootGen is the host generation the registration was created under, so late |
| 63 | // events from a superseded registration stay recognizable and get dropped. |
| 64 | type frame struct { |
| 65 | Kind wireKind `json:"kind"` |
| 66 | ID uint64 `json:"id,omitempty"` |
| 67 | RootGen uint64 `json:"rootGen,omitempty"` |
| 68 | Op Op `json:"op,omitempty"` |
| 69 | Dirs []string `json:"dirs,omitempty"` |
| 70 | Msg string `json:"msg,omitempty"` |
| 71 | } |
| 72 | |
| 73 | func writeFrame(w io.Writer, f frame) error { |
| 74 | payload, err := json.Marshal(f) |
| 75 | if err != nil { |
| 76 | return err |
| 77 | } |
| 78 | if len(payload) > wireMaxFrameBytes { |
| 79 | return fmt.Errorf("watch helper frame exceeds %d bytes", wireMaxFrameBytes) |
| 80 | } |
| 81 | var size [4]byte |
| 82 | n := uint32(len(payload)) |
| 83 | size[0], size[1], size[2], size[3] = byte(n), byte(n>>8), byte(n>>16), byte(n>>24) |
| 84 | if _, err := w.Write(size[:]); err != nil { |
| 85 | return err |
| 86 | } |
| 87 | _, err = w.Write(payload) |
| 88 | return err |
| 89 | } |
| 90 | |
| 91 | func readFrame(r io.Reader) (frame, error) { |
| 92 | var size [4]byte |
| 93 | if _, err := io.ReadFull(r, size[:]); err != nil { |
| 94 | return frame{}, err |
| 95 | } |
| 96 | n := uint32(size[0]) | uint32(size[1])<<8 | uint32(size[2])<<16 | uint32(size[3])<<24 |
| 97 | if n == 0 || n > wireMaxFrameBytes { |
| 98 | return frame{}, fmt.Errorf("watch helper frame size %d out of range", n) |
| 99 | } |
| 100 | payload := make([]byte, n) |
| 101 | if _, err := io.ReadFull(r, payload); err != nil { |
| 102 | return frame{}, err |
| 103 | } |
| 104 | var f frame |
| 105 | if err := json.Unmarshal(payload, &f); err != nil { |
| 106 | return frame{}, err |
| 107 | } |
| 108 | return f, nil |
| 109 | } |
| 110 |