carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "io"
5 "net"
6 "os"
7 "sync"
8 "sync/atomic"
9
10 "github.com/carlosframework/platform/internal/applog"
11 "github.com/carlosframework/platform/internal/edge"
12)
13
14// logListener is the agent's end of a unit's stdout (design §6.4): the
15// unit's StandardOutput=file: connects to this AF_UNIX socket once, at
16// exec, and every line is teed to the agent's own stdout (journald) and to
17// the app log sink, exactly as teeStd does for an exec child. One
18// connection per listener is accepted; a second is closed unread. The
19// accepted flag never resets by design: a listener belongs to one
20// invocation, and a re-exec of the unit gets a fresh listener from
21// prepare. EOF ends log collection and nothing else: it is never a sign
22// the process is gone (codex R1-4).
23type logListener struct {
24 ln net.Listener
25 emit func(string)
26 accepted atomic.Bool
27 closed atomic.Bool
28 // conn is the one accepted connection, kept so Close can shut it and
29 // unblock the tee; wg covers the accept loop and the tee.
30 mu sync.Mutex
31 conn net.Conn
32 wg sync.WaitGroup
33}
34
35func listenLogs(path string, emit func(string)) (*logListener, error) {
36 ln, err := edge.SecureUnixListen(path)
37 if err != nil {
38 return nil, err
39 }
40 l := &logListener{ln: ln, emit: emit}
41 l.wg.Add(1)
42 go l.accept()
43 return l, nil
44}
45
46func (l *logListener) accept() {
47 defer l.wg.Done()
48 for {
49 c, err := l.ln.Accept()
50 if err != nil {
51 return
52 }
53 if l.accepted.Swap(true) {
54 c.Close()
55 continue
56 }
57 l.mu.Lock()
58 if l.closed.Load() {
59 // Close ran between Accept and here; nothing may outlive it.
60 l.mu.Unlock()
61 c.Close()
62 continue
63 }
64 l.conn = c
65 l.wg.Add(1)
66 l.mu.Unlock()
67 go l.tee(c)
68 }
69}
70
71func (l *logListener) tee(c net.Conn) {
72 defer l.wg.Done()
73 defer c.Close()
74 var sink io.Writer = os.Stdout
75 if l.emit != nil {
76 sink = io.MultiWriter(os.Stdout, applog.NewLineWriter(execLogLineMax, l.emit))
77 }
78 // applog.LineWriter owns line assembly and the length cap, so no
79 // buffering layer is needed between the connection and the sink.
80 _, _ = io.Copy(sink, c)
81}
82
83// Exited always reports false: a log listener knows nothing about process
84// state, and this method exists so a caller cannot mistake it for one.
85func (l *logListener) Exited() bool { return false }
86
87// Close stops accepting, shuts the accepted connection so a blocked tee
88// returns, and waits for both goroutines. Nothing spawned by the listener
89// outlives Close: the process it belonged to is gone or being replaced,
90// and a late line must not reach the sink under the old invocation.
91func (l *logListener) Close() error {
92 if l.closed.Swap(true) {
93 return nil
94 }
95 err := l.ln.Close()
96 l.mu.Lock()
97 if l.conn != nil {
98 l.conn.Close()
99 }
100 l.mu.Unlock()
101 l.wg.Wait()
102 return err
103}
104