[DEPRECATED] Go implementation of plcbundle
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}