master
go 349 lines 7.61 KB
Raw
1 // Package jmxbridge provides process management for JSON-based Java helper bridges.
2 // SPDX-License-Identifier: GPL-3.0-or-later
3
4 package jmxbridge
5
6 import (
7 "bufio"
8 "context"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "io"
13 "os"
14 "os/exec"
15 "strings"
16 "sync"
17 )
18
19 // Logger is the minimal logging contract required by the bridge.
20 type Logger interface {
21 Debugf(format string, args ...any)
22 Infof(format string, args ...any)
23 Warningf(format string, args ...any)
24 Errorf(format string, args ...any)
25 }
26
27 // Config contains options for launching the helper process.
28 type Config struct {
29 JavaExecPath string
30 JarPath string
31 JarData []byte
32 JarFileName string
33 WorkingDir string
34 }
35
36 // Command represents a JSON command sent to the helper.
37 type Command map[string]any
38
39 // Response represents a generic helper response.
40 type Response struct {
41 Status string `json:"status"`
42 Message string `json:"message,omitempty"`
43 Details string `json:"details,omitempty"`
44 Recoverable bool `json:"recoverable,omitempty"`
45 Data map[string]any `json:"data,omitempty"`
46 }
47
48 // Option configures a Client.
49 type Option func(*Client)
50
51 // ProcessFactory builds a process abstraction. It may be overridden for tests.
52 type ProcessFactory func(ctx context.Context, javaPath string, jarPath string) (process, error)
53
54 type process interface {
55 Start() error
56 Stdin() io.WriteCloser
57 Stdout() io.ReadCloser
58 Stderr() io.ReadCloser
59 Wait() error
60 }
61
62 // Client manages the lifecycle of the helper process.
63 type Client struct {
64 cfg Config
65 logger Logger
66
67 processFactory ProcessFactory
68
69 mu sync.Mutex
70 proc process
71 cancel context.CancelFunc
72 stdin io.WriteCloser
73 scanner *bufio.Scanner
74 stderr io.ReadCloser
75 stderrWG sync.WaitGroup
76 running bool
77
78 jarPath string
79 removeJar bool
80 }
81
82 // NewClient creates a Client with optional customisations.
83 func NewClient(cfg Config, logger Logger, opts ...Option) (*Client, error) {
84 if logger == nil {
85 return nil, errors.New("jmxbridge: logger is required")
86 }
87
88 if cfg.JarPath == "" && len(cfg.JarData) == 0 {
89 return nil, errors.New("jmxbridge: either JarPath or JarData must be provided")
90 }
91
92 if cfg.JarFileName == "" {
93 cfg.JarFileName = "netdata_jmx_helper.jar"
94 }
95
96 client := &Client{
97 cfg: cfg,
98 logger: logger,
99 processFactory: defaultProcessFactory,
100 }
101
102 for _, opt := range opts {
103 opt(client)
104 }
105
106 return client, nil
107 }
108
109 // WithProcessFactory overrides the process factory (useful for tests).
110 func WithProcessFactory(factory ProcessFactory) Option {
111 return func(c *Client) {
112 c.processFactory = factory
113 }
114 }
115
116 // Start launches the helper and sends the initial command.
117 func (c *Client) Start(ctx context.Context, initCmd Command) error {
118 c.mu.Lock()
119 defer c.mu.Unlock()
120
121 if c.running {
122 return nil
123 }
124
125 jarPath, removeJar, err := c.prepareJar()
126 if err != nil {
127 return err
128 }
129
130 procCtx, cancel := context.WithCancel(context.Background())
131 proc, err := c.processFactory(procCtx, c.cfg.JavaExecPath, jarPath)
132 if err != nil {
133 cancel()
134 if removeJar {
135 _ = os.Remove(jarPath)
136 }
137 return err
138 }
139
140 stdin := proc.Stdin()
141 stdout := proc.Stdout()
142 stderr := proc.Stderr()
143
144 if err := proc.Start(); err != nil {
145 cancel()
146 if removeJar {
147 _ = os.Remove(jarPath)
148 }
149 return fmt.Errorf("jmxbridge: failed to start helper: %w", err)
150 }
151
152 c.stderrWG.Add(1)
153 go c.consumeStderr(stderr)
154
155 c.proc = proc
156 c.cancel = cancel
157 c.stdin = stdin
158 c.scanner = bufio.NewScanner(stdout)
159 c.stderr = stderr
160 c.running = true
161 c.jarPath = jarPath
162 c.removeJar = removeJar
163
164 if initCmd != nil {
165 c.mu.Unlock()
166 _, err := c.Send(ctx, initCmd)
167 c.mu.Lock()
168 if err != nil {
169 c.internalShutdownLocked()
170 return fmt.Errorf("jmxbridge: helper init failed: %w", err)
171 }
172 }
173
174 c.logger.Infof("jmxbridge: helper started")
175 return nil
176 }
177
178 // Send transmits a command and waits for the corresponding response.
179 func (c *Client) Send(ctx context.Context, cmd Command) (*Response, error) {
180 c.mu.Lock()
181 defer c.mu.Unlock()
182
183 if !c.running {
184 return nil, errors.New("jmxbridge: helper not running")
185 }
186
187 payload, err := json.Marshal(cmd)
188 if err != nil {
189 return nil, fmt.Errorf("jmxbridge: failed to marshal command: %w", err)
190 }
191
192 if _, err := c.stdin.Write(append(payload, '\n')); err != nil {
193 return nil, fmt.Errorf("jmxbridge: failed to send command: %w", err)
194 }
195
196 scanner := c.scanner
197 if scanner == nil {
198 return nil, errors.New("jmxbridge: response scanner not initialised")
199 }
200
201 type result struct {
202 line string
203 err error
204 }
205
206 resCh := make(chan result, 1)
207 go func(s *bufio.Scanner) {
208 if s.Scan() {
209 resCh <- result{line: s.Text()}
210 } else {
211 resCh <- result{err: s.Err()}
212 }
213 }(scanner)
214
215 select {
216 case <-ctx.Done():
217 return nil, fmt.Errorf("jmxbridge: waiting for response cancelled: %w", ctx.Err())
218 case res := <-resCh:
219 if res.err != nil {
220 return nil, fmt.Errorf("jmxbridge: failed to read response: %w", res.err)
221 }
222 var resp Response
223 if err := json.Unmarshal([]byte(res.line), &resp); err != nil {
224 return nil, fmt.Errorf("jmxbridge: invalid response: %w", err)
225 }
226 status := strings.ToUpper(resp.Status)
227 if status != "OK" && status != "SUCCESS" {
228 return &resp, fmt.Errorf("jmxbridge: helper returned status %s: %s", resp.Status, resp.Message)
229 }
230 return &resp, nil
231 }
232 }
233
234 // Shutdown terminates the helper process and cleans resources.
235 func (c *Client) Shutdown() {
236 c.mu.Lock()
237 defer c.mu.Unlock()
238 c.internalShutdownLocked()
239 }
240
241 func (c *Client) internalShutdownLocked() {
242 if !c.running {
243 return
244 }
245
246 if c.cancel != nil {
247 c.cancel()
248 }
249
250 if c.stdin != nil {
251 _ = c.stdin.Close()
252 }
253
254 if c.proc != nil {
255 _ = c.proc.Wait()
256 }
257
258 if c.stderr != nil {
259 c.stderr.Close()
260 }
261 c.stderrWG.Wait()
262
263 if c.removeJar {
264 _ = os.Remove(c.jarPath)
265 }
266
267 c.proc = nil
268 c.stdin = nil
269 c.scanner = nil
270 c.stderr = nil
271 c.cancel = nil
272 c.running = false
273 c.logger.Infof("jmxbridge: helper stopped")
274 }
275
276 func (c *Client) prepareJar() (string, bool, error) {
277 if c.cfg.JarPath != "" {
278 return c.cfg.JarPath, false, nil
279 }
280
281 dir := c.cfg.WorkingDir
282 if dir == "" {
283 dir = os.TempDir()
284 }
285
286 f, err := os.CreateTemp(dir, c.cfg.JarFileName)
287 if err != nil {
288 return "", false, fmt.Errorf("jmxbridge: failed to create temp jar: %w", err)
289 }
290 if _, err := f.Write(c.cfg.JarData); err != nil {
291 _ = f.Close()
292 _ = os.Remove(f.Name())
293 return "", false, fmt.Errorf("jmxbridge: failed to write jar: %w", err)
294 }
295 if err := f.Close(); err != nil {
296 _ = os.Remove(f.Name())
297 return "", false, fmt.Errorf("jmxbridge: failed to close jar: %w", err)
298 }
299
300 return f.Name(), true, nil
301 }
302
303 func (c *Client) consumeStderr(r io.Reader) {
304 defer c.stderrWG.Done()
305 scanner := bufio.NewScanner(r)
306 for scanner.Scan() {
307 c.logger.Debugf("jmxbridge stderr: %s", scanner.Text())
308 }
309 }
310
311 func defaultProcessFactory(ctx context.Context, javaPath, jarPath string) (process, error) {
312 if javaPath == "" {
313 javaPath = "java"
314 }
315
316 cmd := exec.CommandContext(ctx, javaPath, "-jar", jarPath)
317 stdin, err := cmd.StdinPipe()
318 if err != nil {
319 return nil, err
320 }
321 stdout, err := cmd.StdoutPipe()
322 if err != nil {
323 return nil, err
324 }
325 stderr, err := cmd.StderrPipe()
326 if err != nil {
327 return nil, err
328 }
329
330 return &execProcess{
331 cmd: cmd,
332 stdin: stdin,
333 stdout: stdout,
334 stderr: stderr,
335 }, nil
336 }
337
338 type execProcess struct {
339 cmd *exec.Cmd
340 stdin io.WriteCloser
341 stdout io.ReadCloser
342 stderr io.ReadCloser
343 }
344
345 func (p *execProcess) Start() error { return p.cmd.Start() }
346 func (p *execProcess) Stdin() io.WriteCloser { return p.stdin }
347 func (p *execProcess) Stdout() io.ReadCloser { return p.stdout }
348 func (p *execProcess) Stderr() io.ReadCloser { return p.stderr }
349 func (p *execProcess) Wait() error { return p.cmd.Wait() }