1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
|
package main
import (
"bufio"
"encoding/hex"
"flag"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
)
// TraceCommand runs a command under strace and decodes PipeWire messages
func TraceCommand(args []string) error {
// Create a new FlagSet for the trace subcommand
fs := flag.NewFlagSet("trace", flag.ContinueOnError)
verbose := fs.Bool("verbose", false, "Show raw strace output for decoded messages")
// Parse flags
err := fs.Parse(args)
if err != nil {
return err
}
// Remaining args are the command to execute
cmdArgs := fs.Args()
if len(cmdArgs) == 0 {
return fmt.Errorf("usage: pw-sesh trace [--verbose] <command> [args...]")
}
// Run strace with the command
// -f: follow forks
// -s 99999: don't truncate strings
// -v: verbose mode
// -x: print non-ASCII strings in hex
// -e trace=sendmsg,recvmsg,connect: only trace these syscalls
straceArgs := []string{
"-f",
"-s", "99999",
"-v",
"-x",
"-e", "trace=sendmsg,recvmsg,connect,socket",
"--",
}
straceArgs = append(straceArgs, cmdArgs...)
cmd := exec.Command("strace", straceArgs...)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
// Capture stderr (where strace output goes)
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("failed to get stderr pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start strace: %w", err)
}
// Parse strace output in real-time
go parseStraceOutput(stderr, *verbose)
if err := cmd.Wait(); err != nil {
// Ignore strace exit errors (command might exit with non-zero)
fmt.Fprintf(os.Stderr, "Command finished (exit code may be non-zero)\n")
}
return nil
}
// parseStraceOutput parses strace output and decodes PipeWire messages
func parseStraceOutput(r io.Reader, verbose bool) {
scanner := bufio.NewScanner(r)
// Increase buffer size for very long strace lines
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 10*1024*1024) // 10MB max line size
// Track which file descriptors are PipeWire sockets
pipewireFds := make(map[int]bool)
// Regex patterns (handle optional [pid XXXXX] prefix from strace -f)
socketRe := regexp.MustCompile(`socket\(AF_UNIX, .*?\) = (\d+)`)
connectRe := regexp.MustCompile(`connect\((\d+), \{sa_family=AF_UNIX, sun_path="([^"]+)"\}`)
sendmsgRe := regexp.MustCompile(`sendmsg\((\d+), .*iov_base="(\\x[0-9a-f]{2}[^"]*)"`)
recvmsgRe := regexp.MustCompile(`recvmsg\((\d+), .*msg_iov=\[\{iov_base="(\\x[0-9a-f]{2}[^"]*)"`)
for scanner.Scan() {
line := scanner.Text()
decoded := false
// Check for socket creation
if matches := socketRe.FindStringSubmatch(line); matches != nil {
// We'll mark it as PipeWire when we see the connect
}
// Check for connect to PipeWire socket
if matches := connectRe.FindStringSubmatch(line); matches != nil {
fd, _ := strconv.Atoi(matches[1])
path := matches[2]
if strings.Contains(path, "pipewire") {
pipewireFds[fd] = true
fmt.Fprintf(os.Stderr, "\n[PW-TRACE] Connected to PipeWire socket: fd=%d, path=%s\n", fd, path)
decoded = true
}
}
// Check for sendmsg on PipeWire socket
if matches := sendmsgRe.FindStringSubmatch(line); matches != nil {
fd, _ := strconv.Atoi(matches[1])
if pipewireFds[fd] {
hexData := matches[2]
fmt.Fprintf(os.Stderr, "\n[PW-TRACE] >>> SEND on fd=%d\n", fd)
decodeMessages(hexData, "SEND")
decoded = true
}
}
// Check for recvmsg on PipeWire socket
if matches := recvmsgRe.FindStringSubmatch(line); matches != nil {
fd, _ := strconv.Atoi(matches[1])
if pipewireFds[fd] {
hexData := matches[2]
fmt.Fprintf(os.Stderr, "\n[PW-TRACE] <<< RECV on fd=%d\n", fd)
decodeMessages(hexData, "RECV")
decoded = true
}
}
// Pass through the strace line only if verbose OR if we didn't decode it
if verbose || !decoded {
fmt.Fprintln(os.Stderr, line)
}
}
}
// decodeMessages decodes PipeWire messages from hex string
func decodeMessages(hexStr string, direction string) {
// Convert \xNN format to actual bytes
hexStr = strings.ReplaceAll(hexStr, "\\x", "")
data, err := hex.DecodeString(hexStr)
if err != nil {
fmt.Fprintf(os.Stderr, "[PW-TRACE] Error decoding hex: %v\n", err)
return
}
// Parse all messages
offset := 0
msgNum := 0
for offset < len(data) {
if len(data)-offset < 16 {
break
}
// Decode header
header, err := DecodeHeader(data[offset : offset+16])
if err != nil {
fmt.Fprintf(os.Stderr, "[PW-TRACE] Error decoding header: %v\n", err)
break
}
msgNum++
msgSize := 16 + int(header.Size)
if offset+msgSize > len(data) {
fmt.Fprintf(os.Stderr, "[PW-TRACE] Incomplete message at offset %d\n", offset)
break
}
// Print message info
fmt.Fprintf(os.Stderr, "[PW-TRACE] Message #%d: ID=%d, Opcode=%d, Size=%d, HeaderSeq=%d\n",
msgNum, header.ID, header.Opcode, header.Size, header.HeaderSeq)
// Decode opcode name
opcodeName := getOpcodeName(header.ID, header.Opcode, direction)
fmt.Fprintf(os.Stderr, "[PW-TRACE] Type: %s\n", opcodeName)
// Try to decode payload as POD if we have data
if header.Size > 0 {
payload := data[offset+16 : offset+msgSize]
// Special handling for specific message types
if strings.Contains(opcodeName, "Registry::Global") {
decodeRegistryGlobal(payload, " ")
} else if strings.Contains(opcodeName, "Client::UpdateProperties") {
decodeClientUpdateProperties(payload, " ")
} else {
decodePODSummary(payload, " ")
}
}
offset += msgSize
}
}
// getOpcodeName returns a human-readable name for object ID + opcode
func getOpcodeName(id uint32, opcode uint8, direction string) string {
if id == 0 {
// Core object
if direction == "SEND" {
switch opcode {
case 1:
return "Core::Hello"
case 2:
return "Core::Sync"
case 3:
return "Core::Pong"
case 4:
return "Core::Error"
case 5:
return "Core::GetRegistry"
case 6:
return "Core::CreateObject"
}
} else {
switch opcode {
case 0:
return "Core::Info"
case 1:
return "Core::Done"
case 2:
return "Core::Ping"
case 3:
return "Core::Error"
case 4:
return "Core::RemoveId"
case 5:
return "Core::BoundId"
case 6:
return "Core::AddMem"
case 7:
return "Core::RemoveMem"
}
}
}
if id == 1 {
// Client object
if direction == "SEND" {
switch opcode {
case 2:
return "Client::UpdateProperties"
case 3:
return "Client::GetPermissions"
case 4:
return "Client::UpdatePermissions"
}
} else {
switch opcode {
case 0:
return "Client::Info"
case 1:
return "Client::Permissions"
}
}
}
// Registry object (could be any ID, but commonly appears after GetRegistry)
if direction == "RECV" && opcode == 0 {
return "Registry::Global (announcing object)"
}
if direction == "RECV" && opcode == 1 {
return "Registry::GlobalRemove"
}
return fmt.Sprintf("Unknown (ID=%d, Op=%d)", id, opcode)
}
// decodePODSummary prints a summary of POD structure
func decodePODSummary(data []byte, prefix string) {
pod, _, err := DecodePOD(data)
if err != nil {
fmt.Fprintf(os.Stderr, "%sPayload: <not POD or error: %v>\n", prefix, err)
return
}
switch pod.Type {
case TypeStruct:
children, err := pod.GetStructChildren()
if err == nil {
fmt.Fprintf(os.Stderr, "%sStruct with %d fields:\n", prefix, len(children))
for i, child := range children {
if i >= 10 {
fmt.Fprintf(os.Stderr, "%s ... (%d more fields)\n", prefix, len(children)-i)
break
}
fmt.Fprintf(os.Stderr, "%s [%d] %s\n", prefix, i, podValueSummary(child))
}
}
case TypeString:
str, _ := pod.GetString()
fmt.Fprintf(os.Stderr, "%sString: %q\n", prefix, str)
case TypeInt:
val, _ := pod.GetInt()
fmt.Fprintf(os.Stderr, "%sInt: %d\n", prefix, val)
case TypeLong:
val, _ := pod.GetLong()
fmt.Fprintf(os.Stderr, "%sLong: %d\n", prefix, val)
default:
fmt.Fprintf(os.Stderr, "%sPOD Type: %d\n", prefix, pod.Type)
}
}
// podValueSummary returns a short summary of a POD value
func podValueSummary(pod *POD) string {
switch pod.Type {
case TypeString:
str, _ := pod.GetString()
if len(str) > 50 {
return fmt.Sprintf("String(%q...)", str[:50])
}
return fmt.Sprintf("String(%q)", str)
case TypeInt:
val, _ := pod.GetInt()
return fmt.Sprintf("Int(%d)", val)
case TypeLong:
val, _ := pod.GetLong()
return fmt.Sprintf("Long(%d)", val)
case TypeStruct:
children, _ := pod.GetStructChildren()
return fmt.Sprintf("Struct(%d fields)", len(children))
default:
return fmt.Sprintf("Type(%d)", pod.Type)
}
}
// decodeClientUpdateProperties decodes a Client::UpdateProperties message payload
func decodeClientUpdateProperties(data []byte, prefix string) {
pod, _, err := DecodePOD(data)
if err != nil {
fmt.Fprintf(os.Stderr, "%sError decoding: %v\n", prefix, err)
return
}
children, err := pod.GetStructChildren()
if err != nil || len(children) < 1 {
fmt.Fprintf(os.Stderr, "%sInvalid Client::UpdateProperties structure\n", prefix)
return
}
// First field is the properties dict
propsStruct := children[0]
propsChildren, err := propsStruct.GetStructChildren()
if err != nil || len(propsChildren) == 0 {
fmt.Fprintf(os.Stderr, "%sError decoding properties: %v\n", prefix, err)
return
}
fmt.Fprintf(os.Stderr, "%sProperties:\n", prefix)
// First field is n_items count
if len(propsChildren) >= 1 {
// Following fields are key-value pairs
for i := 1; i+1 < len(propsChildren); i += 2 {
key, err1 := propsChildren[i].GetString()
value, err2 := propsChildren[i+1].GetString()
if err1 == nil && err2 == nil {
fmt.Fprintf(os.Stderr, "%s %s = %q\n", prefix, key, value)
}
}
}
}
// decodeRegistryGlobal decodes a Registry::Global message payload
func decodeRegistryGlobal(data []byte, prefix string) {
pod, _, err := DecodePOD(data)
if err != nil {
fmt.Fprintf(os.Stderr, "%sError decoding: %v\n", prefix, err)
return
}
children, err := pod.GetStructChildren()
if err != nil || len(children) < 5 {
fmt.Fprintf(os.Stderr, "%sInvalid Registry::Global structure\n", prefix)
return
}
// Extract fields
globalID, _ := children[0].GetInt()
permissions, _ := children[1].GetInt()
typeName, _ := children[2].GetString()
version, _ := children[3].GetInt()
fmt.Fprintf(os.Stderr, "%sGlobal ID: %d\n", prefix, globalID)
fmt.Fprintf(os.Stderr, "%sType: %s (version %d)\n", prefix, typeName, version)
fmt.Fprintf(os.Stderr, "%sPermissions: %d\n", prefix, permissions)
// Decode properties (field 4)
if len(children) >= 5 {
propsStruct := children[4]
propsChildren, err := propsStruct.GetStructChildren()
if err == nil && len(propsChildren) > 0 {
fmt.Fprintf(os.Stderr, "%sProperties:\n", prefix)
// First field is n_items count
if len(propsChildren) >= 1 {
// Following fields are key-value pairs
for i := 1; i+1 < len(propsChildren); i += 2 {
key, err1 := propsChildren[i].GetString()
value, err2 := propsChildren[i+1].GetString()
if err1 == nil && err2 == nil {
fmt.Fprintf(os.Stderr, "%s %s = %q\n", prefix, key, value)
}
}
}
}
}
}
|