1package logging
2
3import (
4 "bufio"
5 "fmt"
6 "io"
7 "log"
8 "os"
9 "strings"
10 "time"
11
12 "golang.org/x/sys/unix"
13)
14
15type OutputCapture struct {
16 originalStdout *os.File
17 originalStderr *os.File
18 stdoutRead *os.File
19 stdoutWrite *os.File
20 stderrRead *os.File
21 stderrWrite *os.File
22 logger *Logger
23 stopChan chan struct{}
24 started bool
25}
26
27func NewOutputCapture(logger *Logger) *OutputCapture {
28 return &OutputCapture{
29 originalStdout: os.Stdout,
30 originalStderr: os.Stderr,
31 logger: logger,
32 stopChan: make(chan struct{}),
33 }
34}
35
36func (c *OutputCapture) Start() error {
37 if c.started {
38 return nil
39 }
40
41 // Create pipes for stdout
42 stdoutR, stdoutW, err := os.Pipe()
43 if err != nil {
44 return err
45 }
46
47 // Create pipes for stderr
48 stderrR, stderrW, err := os.Pipe()
49 if err != nil {
50 if closeErr := stdoutR.Close(); closeErr != nil {
51 log.Printf("failed to close stdout read pipe: %v", closeErr)
52 }
53 if closeErr := stdoutW.Close(); closeErr != nil {
54 log.Printf("failed to close stdout write pipe: %v", closeErr)
55 }
56 return err
57 }
58
59 // Store pipe handles
60 c.stdoutRead = stdoutR
61 c.stdoutWrite = stdoutW
62 c.stderrRead = stderrR
63 c.stderrWrite = stderrW
64
65 // Redirect stdout and stderr
66 os.Stdout = stdoutW
67 os.Stderr = stderrW
68
69 // Also redirect file descriptors at syscall level for C code
70 if err := unix.Dup3(int(stdoutW.Fd()), 1, 0); err != nil {
71 log.Printf("failed to redirect stdout: %v", err)
72 }
73 if err := unix.Dup3(int(stderrW.Fd()), 2, 0); err != nil {
74 log.Printf("failed to redirect stderr: %v", err)
75 }
76
77 // Start goroutines to read and log
78 go c.pipeToLogger(stdoutR, "STDOUT")
79 go c.pipeToLogger(stderrR, "STDERR")
80
81 c.started = true
82 return nil
83}
84
85func (c *OutputCapture) Stop() {
86 if !c.started {
87 return
88 }
89
90 close(c.stopChan)
91
92 // Restore original stdout and stderr
93 os.Stdout = c.originalStdout
94 os.Stderr = c.originalStderr
95
96 // Restore file descriptors
97 if err := unix.Dup3(int(c.originalStdout.Fd()), 1, 0); err != nil {
98 log.Printf("failed to restore stdout: %v", err)
99 }
100 if err := unix.Dup3(int(c.originalStderr.Fd()), 2, 0); err != nil {
101 log.Printf("failed to restore stderr: %v", err)
102 }
103
104 // Close pipes
105 if c.stdoutWrite != nil {
106 if err := c.stdoutWrite.Close(); err != nil {
107 log.Printf("failed to close stdout write pipe: %v", err)
108 }
109 }
110 if c.stderrWrite != nil {
111 if err := c.stderrWrite.Close(); err != nil {
112 log.Printf("failed to close stderr write pipe: %v", err)
113 }
114 }
115 if c.stdoutRead != nil {
116 if err := c.stdoutRead.Close(); err != nil {
117 log.Printf("failed to close stdout read pipe: %v", err)
118 }
119 }
120 if c.stderrRead != nil {
121 if err := c.stderrRead.Close(); err != nil {
122 log.Printf("failed to close stderr read pipe: %v", err)
123 }
124 }
125
126 c.started = false
127}
128
129func (c *OutputCapture) pipeToLogger(r io.Reader, prefix string) {
130 scanner := bufio.NewScanner(r)
131 for scanner.Scan() {
132 select {
133 case <-c.stopChan:
134 return
135 default:
136 line := scanner.Text()
137 if line != "" && c.logger != nil {
138 // Avoid feedback loops by not logging our own log messages
139 if !strings.Contains(line, "[STDOUT]") && !strings.Contains(line, "[STDERR]") {
140 // Write directly to original stdout/stderr instead of going through logger
141 // to avoid capture recursion
142 timestamp := time.Now().Format("2006-01-02 15:04:05")
143 logLine := fmt.Sprintf("[%s] INFO [%s] %s\n", timestamp, prefix, line)
144 if prefix == "STDERR" {
145 if _, err := c.originalStderr.WriteString(logLine); err != nil {
146 log.Printf("failed to write to stderr: %v", err)
147 }
148 } else {
149 if _, err := c.originalStdout.WriteString(logLine); err != nil {
150 log.Printf("failed to write to stdout: %v", err)
151 }
152 }
153 }
154 }
155 }
156 }
157}