A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
at did-resolver 215 lines 5.0 kB view raw
1// detector/script.go 2package detector 3 4import ( 5 "bufio" 6 "context" 7 "fmt" 8 "net" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 "time" 14 15 "github.com/goccy/go-json" 16 17 "tangled.org/atscan.net/plcbundle/plc" 18) 19 20// ScriptDetector runs a JavaScript detector via Unix socket 21type ScriptDetector struct { 22 name string 23 scriptPath string 24 socketPath string 25 serverCmd *exec.Cmd 26 conn net.Conn 27 writer *bufio.Writer 28 reader *bufio.Reader 29} 30 31func NewScriptDetector(scriptPath string) (*ScriptDetector, error) { 32 if _, err := exec.LookPath("bun"); err != nil { 33 return nil, fmt.Errorf("bun runtime not found in PATH") 34 } 35 36 absPath, err := filepath.Abs(scriptPath) 37 if err != nil { 38 return nil, fmt.Errorf("invalid script path: %w", err) 39 } 40 41 name := strings.TrimSuffix(filepath.Base(scriptPath), filepath.Ext(scriptPath)) 42 socketPath := filepath.Join(os.TempDir(), fmt.Sprintf("detector-%d-%s.sock", os.Getpid(), name)) 43 44 sd := &ScriptDetector{ 45 name: name, 46 scriptPath: absPath, 47 socketPath: socketPath, 48 } 49 50 if err := sd.startServer(); err != nil { 51 return nil, err 52 } 53 54 return sd, nil 55} 56 57func (d *ScriptDetector) Name() string { return d.name } 58func (d *ScriptDetector) Description() string { return "JavaScript detector: " + d.name } 59func (d *ScriptDetector) Version() string { return "1.0.0" } 60 61func (d *ScriptDetector) startServer() error { 62 userCode, err := os.ReadFile(d.scriptPath) 63 if err != nil { 64 return fmt.Errorf("failed to read script: %w", err) 65 } 66 67 wrapperScript := d.createSocketWrapper(string(userCode)) 68 os.Remove(d.socketPath) 69 70 d.serverCmd = exec.Command("bun", "run", "-", d.socketPath) 71 d.serverCmd.Stdout = os.Stderr 72 d.serverCmd.Stderr = os.Stderr 73 74 stdin, err := d.serverCmd.StdinPipe() 75 if err != nil { 76 return fmt.Errorf("failed to create stdin pipe: %w", err) 77 } 78 79 if err := d.serverCmd.Start(); err != nil { 80 return fmt.Errorf("failed to start server: %w", err) 81 } 82 83 stdin.Write([]byte(wrapperScript)) 84 stdin.Close() 85 86 if err := d.connectToServer(); err != nil { 87 d.serverCmd.Process.Kill() 88 os.Remove(d.socketPath) 89 return err 90 } 91 92 return nil 93} 94 95func (d *ScriptDetector) createSocketWrapper(userCode string) string { 96 return fmt.Sprintf(`// Auto-generated socket server wrapper 97// User's detect function 98%s 99 100// Unix socket server 101const socketPath = process.argv[2]; 102 103try { 104 await Bun.file(socketPath).unlink(); 105} catch {} 106 107const server = Bun.listen({ 108 unix: socketPath, 109 socket: { 110 data(socket, data) { 111 try { 112 const operation = JSON.parse(data.toString()); 113 const labels = detect({ op: operation }) || []; 114 socket.write(JSON.stringify({ labels }) + '\n'); 115 } catch (error) { 116 socket.write(JSON.stringify({ labels: [], error: error.message }) + '\n'); 117 } 118 }, 119 error(socket, error) {}, 120 close(socket) {} 121 } 122}); 123 124console.error('Detector server ready on socket:', socketPath); 125`, userCode) 126} 127 128func (d *ScriptDetector) connectToServer() error { 129 maxRetries := 50 130 for i := 0; i < maxRetries; i++ { 131 conn, err := net.Dial("unix", d.socketPath) 132 if err == nil { 133 d.conn = conn 134 d.writer = bufio.NewWriter(conn) 135 d.reader = bufio.NewReader(conn) 136 return nil 137 } 138 time.Sleep(50 * time.Millisecond) 139 } 140 return fmt.Errorf("failed to connect to socket within timeout") 141} 142 143func (d *ScriptDetector) Detect(ctx context.Context, op plc.PLCOperation) (*Match, error) { 144 if d.conn == nil { 145 return nil, fmt.Errorf("not connected to server") 146 } 147 148 // Use RawJSON directly 149 data := op.RawJSON 150 if len(data) == 0 { 151 var err error 152 data, err = json.Marshal(op) 153 if err != nil { 154 return nil, fmt.Errorf("failed to serialize operation: %w", err) 155 } 156 } 157 158 if _, err := d.writer.Write(data); err != nil { 159 return nil, fmt.Errorf("failed to write to socket: %w", err) 160 } 161 if _, err := d.writer.WriteString("\n"); err != nil { 162 return nil, fmt.Errorf("failed to write newline: %w", err) 163 } 164 if err := d.writer.Flush(); err != nil { 165 return nil, fmt.Errorf("failed to flush: %w", err) 166 } 167 168 line, err := d.reader.ReadString('\n') 169 if err != nil { 170 return nil, fmt.Errorf("failed to read response: %w", err) 171 } 172 173 var result struct { 174 Labels []string `json:"labels"` 175 Error string `json:"error,omitempty"` 176 } 177 178 if err := json.Unmarshal([]byte(line), &result); err != nil { 179 return nil, fmt.Errorf("failed to parse response: %w", err) 180 } 181 182 if result.Error != "" { 183 return nil, fmt.Errorf("detector error: %s", result.Error) 184 } 185 186 if len(result.Labels) == 0 { 187 return nil, nil 188 } 189 190 return &Match{ 191 Reason: strings.Join(result.Labels, "_"), 192 Category: "custom", 193 Confidence: 0.95, 194 Note: fmt.Sprintf("Labels: %s", strings.Join(result.Labels, ", ")), 195 Metadata: map[string]interface{}{ 196 "labels": result.Labels, 197 "detector": d.name, 198 }, 199 }, nil 200} 201 202func (d *ScriptDetector) Close() error { 203 if d.conn != nil { 204 d.conn.Close() 205 d.conn = nil 206 } 207 208 if d.serverCmd != nil && d.serverCmd.Process != nil { 209 d.serverCmd.Process.Kill() 210 d.serverCmd.Wait() 211 } 212 213 os.Remove(d.socketPath) 214 return nil 215}