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