Skip to content

Commit 4c5fd06

Browse files
committed
refactor(callstack): Move callstack into independent package
1 parent c1b3a5e commit 4c5fd06

File tree

13 files changed

+311
-224
lines changed

13 files changed

+311
-224
lines changed
Lines changed: 1 addition & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,18 @@
1616
* limitations under the License.
1717
*/
1818

19-
package kevent
19+
package callstack
2020

2121
import (
22-
"expvar"
23-
"github.com/rabbitstack/fibratus/pkg/kevent/kparams"
24-
"github.com/rabbitstack/fibratus/pkg/util/multierror"
2522
"github.com/rabbitstack/fibratus/pkg/util/va"
26-
log "github.com/sirupsen/logrus"
2723
"golang.org/x/arch/x86/x86asm"
2824
"golang.org/x/sys/windows"
2925
"os"
3026
"path/filepath"
3127
"strconv"
3228
"strings"
33-
"sync"
34-
"time"
3529
)
3630

37-
// maxDequeFlushPeriod specifies the maximum period
38-
// for the events to reside in the deque.
39-
var maxDequeFlushPeriod = time.Second * 30
40-
var flusherInterval = time.Second * 5
41-
42-
// callstackFlushes computes overall callstack dequeue flushes
43-
var callstackFlushes = expvar.NewInt("callstack.flushes")
44-
4531
// unbacked represents the identifier for unbacked regions in stack frames
4632
const unbacked = "unbacked"
4733

@@ -370,133 +356,3 @@ func (s Callstack) CallsiteInsns(pid uint32, leading bool) []string {
370356
}
371357
return opcodes
372358
}
373-
374-
// CallstackDecorator maintains a FIFO queue where events
375-
// eligible for stack enrichment are queued. Upon arrival
376-
// of the respective stack walk event, the acting event is
377-
// popped from the queue and enriched with return addresses
378-
// which are later subject to symbolization.
379-
type CallstackDecorator struct {
380-
buckets map[uint64][]*Kevent
381-
q *Queue
382-
mux sync.Mutex
383-
384-
flusher *time.Ticker
385-
quit chan struct{}
386-
}
387-
388-
// NewCallstackDecorator creates a new callstack decorator
389-
// which receives the event queue for long-standing event
390-
// flushing.
391-
func NewCallstackDecorator(q *Queue) *CallstackDecorator {
392-
c := &CallstackDecorator{
393-
q: q,
394-
buckets: make(map[uint64][]*Kevent),
395-
flusher: time.NewTicker(flusherInterval),
396-
quit: make(chan struct{}, 1),
397-
}
398-
399-
go c.doFlush()
400-
401-
return c
402-
}
403-
404-
// Push pushes a new event to the queue.
405-
func (cd *CallstackDecorator) Push(e *Kevent) {
406-
cd.mux.Lock()
407-
defer cd.mux.Unlock()
408-
409-
// append the event to the bucket indexed by stack id
410-
id := e.StackID()
411-
q, ok := cd.buckets[id]
412-
if !ok {
413-
cd.buckets[id] = []*Kevent{e}
414-
} else {
415-
cd.buckets[id] = append(q, e)
416-
}
417-
}
418-
419-
// Pop receives the stack walk event and pops the oldest
420-
// originating event with the same pid,tid tuple formerly
421-
// coined as stack identifier. The originating event is then
422-
// decorated with callstack return addresses.
423-
func (cd *CallstackDecorator) Pop(e *Kevent) *Kevent {
424-
cd.mux.Lock()
425-
defer cd.mux.Unlock()
426-
427-
id := e.StackID()
428-
q, ok := cd.buckets[id]
429-
if !ok {
430-
return e
431-
}
432-
433-
var evt *Kevent
434-
if len(q) > 0 {
435-
evt, cd.buckets[id] = q[0], q[1:]
436-
}
437-
438-
if evt == nil {
439-
return e
440-
}
441-
442-
callstack := e.Kparams.MustGetSlice(kparams.Callstack)
443-
evt.AppendParam(kparams.Callstack, kparams.Slice, callstack)
444-
445-
return evt
446-
}
447-
448-
// Stop shutdowns the callstack decorator flusher.
449-
func (cd *CallstackDecorator) Stop() {
450-
cd.quit <- struct{}{}
451-
}
452-
453-
// RemoveBucket removes the bucket and all enqueued events.
454-
func (cd *CallstackDecorator) RemoveBucket(id uint64) {
455-
cd.mux.Lock()
456-
defer cd.mux.Unlock()
457-
delete(cd.buckets, id)
458-
}
459-
460-
func (cd *CallstackDecorator) doFlush() {
461-
for {
462-
select {
463-
case <-cd.flusher.C:
464-
errs := cd.flush()
465-
if len(errs) > 0 {
466-
log.Warnf("callstack: unable to flush queued events: %v", multierror.Wrap(errs...))
467-
}
468-
case <-cd.quit:
469-
return
470-
}
471-
}
472-
}
473-
474-
// flush pushes events to the event queue if they have
475-
// been living in the deque more than the maximum allowed
476-
// flush period.
477-
func (cd *CallstackDecorator) flush() []error {
478-
cd.mux.Lock()
479-
defer cd.mux.Unlock()
480-
481-
if len(cd.buckets) == 0 {
482-
return nil
483-
}
484-
485-
errs := make([]error, 0)
486-
487-
for id, q := range cd.buckets {
488-
for i, evt := range q {
489-
if time.Since(evt.Timestamp) < maxDequeFlushPeriod {
490-
continue
491-
}
492-
callstackFlushes.Add(1)
493-
err := cd.q.push(evt)
494-
if err != nil {
495-
errs = append(errs, err)
496-
}
497-
cd.buckets[id] = append(q[:i], q[i+1:]...)
498-
}
499-
}
500-
501-
return errs
502-
}

pkg/callstack/callstack_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2021-present by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package callstack
20+
21+
import (
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
"testing"
25+
)
26+
27+
func TestCallstack(t *testing.T) {
28+
var callstack Callstack
29+
callstack.Init(9)
30+
31+
assert.Equal(t, 9, cap(callstack))
32+
33+
callstack.PushFrame(Frame{Addr: 0x2638e59e0a5, Offset: 0, Symbol: "?", Module: "unbacked"})
34+
callstack.PushFrame(Frame{Addr: 0x7ffb313853b2, Offset: 0x10a, Symbol: "Java_java_lang_ProcessImpl_create", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"})
35+
callstack.PushFrame(Frame{Addr: 0x7ffb3138592e, Offset: 0x3a2, Symbol: "Java_java_lang_ProcessImpl_waitForTimeoutInterruptibly", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"})
36+
callstack.PushFrame(Frame{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"})
37+
callstack.PushFrame(Frame{Addr: 0x7ffb5d8e61f4, Offset: 0x54, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNEL32.DLL"})
38+
callstack.PushFrame(Frame{Addr: 0x7ffb5c1d0396, Offset: 0x66, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"})
39+
callstack.PushFrame(Frame{Addr: 0xfffff8015662a605, Offset: 0x9125, Symbol: "setjmpex", Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe"})
40+
callstack.PushFrame(Frame{Addr: 0xfffff801568e9c33, Offset: 0x2ef3, Symbol: "LpcRequestPort", Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe"})
41+
callstack.PushFrame(Frame{Addr: 0xfffff8015690b644, Offset: 0x45b4, Symbol: "ObDeleteCapturedInsertInfo", Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe"})
42+
43+
assert.True(t, callstack.ContainsUnbacked())
44+
assert.Equal(t, 9, callstack.Depth())
45+
assert.Equal(t, "0xfffff8015690b644 C:\\WINDOWS\\system32\\ntoskrnl.exe!ObDeleteCapturedInsertInfo+0x45b4|0xfffff801568e9c33 C:\\WINDOWS\\system32\\ntoskrnl.exe!LpcRequestPort+0x2ef3|0xfffff8015662a605 C:\\WINDOWS\\system32\\ntoskrnl.exe!setjmpex+0x9125|0x7ffb5c1d0396 C:\\WINDOWS\\System32\\KERNELBASE.dll!CreateProcessW+0x66|0x7ffb5d8e61f4 C:\\WINDOWS\\System32\\KERNEL32.DLL!CreateProcessW+0x54|0x7ffb5c1d0396 C:\\WINDOWS\\System32\\KERNELBASE.dll!CreateProcessW+0x61|0x7ffb3138592e C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll!Java_java_lang_ProcessImpl_waitForTimeoutInterruptibly+0x3a2|0x7ffb313853b2 C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll!Java_java_lang_ProcessImpl_create+0x10a|0x2638e59e0a5 unbacked!?", callstack.String())
46+
assert.Equal(t, "KERNELBASE.dll|KERNEL32.DLL|KERNELBASE.dll|java.dll|unbacked", callstack.Summary())
47+
48+
uframe := callstack.FinalUserFrame()
49+
require.NotNil(t, uframe)
50+
assert.Equal(t, "7ffb5c1d0396", uframe.Addr.String())
51+
assert.Equal(t, "CreateProcessW", uframe.Symbol)
52+
assert.Equal(t, "C:\\WINDOWS\\System32\\KERNELBASE.dll", uframe.Module)
53+
54+
kframe := callstack.FinalKernelFrame()
55+
require.NotNil(t, kframe)
56+
assert.Equal(t, "fffff8015690b644", kframe.Addr.String())
57+
assert.Equal(t, "ObDeleteCapturedInsertInfo", kframe.Symbol)
58+
assert.Equal(t, "C:\\WINDOWS\\system32\\ntoskrnl.exe", kframe.Module)
59+
}

pkg/filter/accessor_windows_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package filter
2020

2121
import (
22+
"github.com/rabbitstack/fibratus/pkg/callstack"
2223
"github.com/rabbitstack/fibratus/pkg/kevent"
2324
"github.com/rabbitstack/fibratus/pkg/kevent/ktypes"
2425
ptypes "github.com/rabbitstack/fibratus/pkg/ps/types"
@@ -107,22 +108,22 @@ func TestIsFieldAccessible(t *testing.T) {
107108
},
108109
{
109110
newThreadAccessor(),
110-
&kevent.Kevent{Type: ktypes.CreateProcess, Category: ktypes.Process, Callstack: []kevent.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}},
111+
&kevent.Kevent{Type: ktypes.CreateProcess, Category: ktypes.Process, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}},
111112
true,
112113
},
113114
{
114115
newThreadAccessor(),
115-
&kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry, Callstack: []kevent.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}},
116+
&kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}},
116117
true,
117118
},
118119
{
119120
newRegistryAccessor(),
120-
&kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry, Callstack: []kevent.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}},
121+
&kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}},
121122
true,
122123
},
123124
{
124125
newNetworkAccessor(),
125-
&kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry, Callstack: []kevent.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}},
126+
&kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}},
126127
false,
127128
},
128129
{

pkg/filter/filter_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package filter
2020

2121
import (
2222
"github.com/rabbitstack/fibratus/internal/etw/processors"
23+
"github.com/rabbitstack/fibratus/pkg/callstack"
2324
"github.com/rabbitstack/fibratus/pkg/config"
2425
"github.com/rabbitstack/fibratus/pkg/filter/fields"
2526
"github.com/rabbitstack/fibratus/pkg/fs"
@@ -377,14 +378,14 @@ func TestThreadFilter(t *testing.T) {
377378
require.NoError(t, windows.WriteProcessMemory(windows.CurrentProcess(), base, &insns[0], uintptr(len(insns)), nil))
378379

379380
kevt.Callstack.Init(8)
380-
kevt.Callstack.PushFrame(kevent.Frame{PID: kevt.PID, Addr: 0x2638e59e0a5, Offset: 0, Symbol: "?", Module: "unbacked"})
381-
kevt.Callstack.PushFrame(kevent.Frame{PID: kevt.PID, Addr: va.Address(base), Offset: 0, Symbol: "?", Module: "unbacked"})
382-
kevt.Callstack.PushFrame(kevent.Frame{PID: kevt.PID, Addr: 0x7ffb313853b2, Offset: 0x10a, Symbol: "Java_java_lang_ProcessImpl_create", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"})
383-
kevt.Callstack.PushFrame(kevent.Frame{PID: kevt.PID, Addr: 0x7ffb3138592e, Offset: 0x3a2, Symbol: "Java_java_lang_ProcessImpl_waitForTimeoutInterruptibly", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"})
384-
kevt.Callstack.PushFrame(kevent.Frame{PID: kevt.PID, Addr: 0x7ffb5d8e61f4, Offset: 0x54, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNEL32.DLL"})
385-
kevt.Callstack.PushFrame(kevent.Frame{PID: kevt.PID, Addr: 0x7ffb5c1d0396, ModuleAddress: 0x7ffb5c1d0396, Offset: 0x66, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"})
386-
kevt.Callstack.PushFrame(kevent.Frame{PID: kevt.PID, Addr: 0xfffff8072ebc1f6f, Offset: 0x4ef, Symbol: "FltRequestFileInfoOnCreateCompletion", Module: "C:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS"})
387-
kevt.Callstack.PushFrame(kevent.Frame{PID: kevt.PID, Addr: 0xfffff8072eb8961b, Offset: 0x20cb, Symbol: "FltGetStreamContext", Module: "C:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS"})
381+
kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x2638e59e0a5, Offset: 0, Symbol: "?", Module: "unbacked"})
382+
kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: va.Address(base), Offset: 0, Symbol: "?", Module: "unbacked"})
383+
kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x7ffb313853b2, Offset: 0x10a, Symbol: "Java_java_lang_ProcessImpl_create", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"})
384+
kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x7ffb3138592e, Offset: 0x3a2, Symbol: "Java_java_lang_ProcessImpl_waitForTimeoutInterruptibly", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"})
385+
kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x7ffb5d8e61f4, Offset: 0x54, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNEL32.DLL"})
386+
kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x7ffb5c1d0396, ModuleAddress: 0x7ffb5c1d0396, Offset: 0x66, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"})
387+
kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0xfffff8072ebc1f6f, Offset: 0x4ef, Symbol: "FltRequestFileInfoOnCreateCompletion", Module: "C:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS"})
388+
kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0xfffff8072eb8961b, Offset: 0x20cb, Symbol: "FltGetStreamContext", Module: "C:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS"})
388389

389390
var tests = []struct {
390391
filter string
@@ -504,7 +505,7 @@ func TestThreadFilter(t *testing.T) {
504505
var n uintptr
505506
require.NoError(t, windows.WriteProcessMemory(pi.Process, ntdll, &insns[0], uintptr(len(insns)), &n))
506507

507-
kevt.Callstack[0] = kevent.Frame{PID: kevt.PID, Addr: va.Address(ntdll), Offset: 0, Symbol: "?", Module: "C:\\Windows\\System32\\ntdll.dll"}
508+
kevt.Callstack[0] = callstack.Frame{PID: kevt.PID, Addr: va.Address(ntdll), Offset: 0, Symbol: "?", Module: "C:\\Windows\\System32\\ntdll.dll"}
508509

509510
var tests1 = []struct {
510511
filter string

pkg/filter/ql/function.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ package ql
2020

2121
import (
2222
"fmt"
23+
"github.com/rabbitstack/fibratus/pkg/callstack"
2324
"github.com/rabbitstack/fibratus/pkg/filter/fields"
24-
"github.com/rabbitstack/fibratus/pkg/kevent"
2525
"github.com/rabbitstack/fibratus/pkg/pe"
2626
pstypes "github.com/rabbitstack/fibratus/pkg/ps/types"
2727
"github.com/rabbitstack/fibratus/pkg/util/signature"
@@ -243,7 +243,7 @@ func (f *Foreach) Call(args []interface{}) (interface{}, bool) {
243243
return true, true
244244
}
245245
}
246-
case kevent.Callstack:
246+
case callstack.Callstack:
247247
var pid uint32
248248
var proc windows.Handle
249249
var err error
@@ -536,7 +536,7 @@ func (f *Foreach) mmapMapValuer(segments []*BoundSegmentLiteral, mmap pstypes.Mm
536536
}
537537

538538
// callstackMapValuer returns map valuer with thread stack frame data.
539-
func (f *Foreach) callstackMapValuer(segments []*BoundSegmentLiteral, frame kevent.Frame, proc windows.Handle) MapValuer {
539+
func (f *Foreach) callstackMapValuer(segments []*BoundSegmentLiteral, frame callstack.Frame, proc windows.Handle) MapValuer {
540540
var valuer = MapValuer{}
541541
for _, seg := range segments {
542542
key := seg.Value

pkg/kevent/formatter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const (
5555
host = ".Host"
5656
pe = ".PE"
5757
kparsAccessor = ".Kparams."
58-
callstack = ".Callstack"
58+
cstack = ".Callstack"
5959
)
6060

6161
var (
@@ -90,7 +90,7 @@ var kfields = map[string]bool{
9090
meta: true,
9191
host: true,
9292
pe: true,
93-
callstack: true,
93+
cstack: true,
9494
}
9595

9696
func hintFields() string {

pkg/kevent/formatter_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (f *Formatter) Format(kevt *Kevent) []byte {
6262
}
6363
// add callstack summary
6464
if !kevt.Callstack.IsEmpty() {
65-
values[callstack] = kevt.Callstack.String()
65+
values[cstack] = kevt.Callstack.String()
6666
}
6767

6868
if f.expandKparamsDot {

pkg/kevent/kevent.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package kevent
2020

2121
import (
2222
"fmt"
23+
"github.com/rabbitstack/fibratus/pkg/callstack"
2324
kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version"
2425
"github.com/rabbitstack/fibratus/pkg/kevent/kparams"
2526
"github.com/rabbitstack/fibratus/pkg/kevent/ktypes"
@@ -98,7 +99,7 @@ type Kevent struct {
9899
// PS represents process' metadata and its allocated resources such as handles, DLLs, etc.
99100
PS *pstypes.PS `json:"ps,omitempty"`
100101
// Callstack represents the call stack for the thread that generated the event.
101-
Callstack Callstack `json:"callstack"`
102+
Callstack callstack.Callstack `json:"callstack"`
102103
// WaitEnqueue indicates if this event should temporarily defer pushing to
103104
// the consumer output queue. This is usually required in event processors
104105
// to propagate certain events stored in processor's state when the related

0 commit comments

Comments
 (0)