blob: 231db5e85a0920998368f89ad3d9a878a8203b8d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package debugging
import (
"encoding/hex"
"fmt"
"io"
"os"
"path"
"reflect"
"regexp"
"runtime"
"slices"
"strings"
"time"
"unsafe"
)
var _debug = CreateDebugPrinter()
func Btox(data []byte, sep string) string {
hexString := hex.EncodeToString(data)
if sep != "" {
pairs := make([]string, len(hexString)/2)
for i := 0; i < len(hexString)-1; i += 2 {
pairs[i/2] = hexString[i : i+2]
}
hexString = strings.Join(pairs, ".")
}
return hexString
}
func Xtob(hexString string) ([]byte, error) {
compile, err := regexp.Compile("[^0-9a-fA-F]")
if err != nil {
return nil, err
}
replaceAll := compile.ReplaceAll([]byte(hexString), nil)
decodeString, err := hex.DecodeString(string(replaceAll))
if err != nil {
return nil, err
}
return decodeString, nil
}
type Debuggable interface {
GetDebugAttr(attr string) any
}
type DebugContentPrinter interface {
PrintDebugContents(indent int, file io.Writer, _ids []uintptr)
}
type DebugContents struct {
debuggables []Debuggable
debuggableContents map[Debuggable][]string
extraPrinters []DebugContentPrinter
}
func NewDebugContents(debuggable Debuggable, contents ...string) *DebugContents {
return &DebugContents{
debuggables: []Debuggable{debuggable},
debuggableContents: map[Debuggable][]string{
debuggable: contents,
},
}
}
var _ DebugContentPrinter = (*DebugContents)(nil)
func (d *DebugContents) AddExtraPrinters(printers ...DebugContentPrinter) {
d.extraPrinters = append(d.extraPrinters, printers...)
}
func (d *DebugContents) AddDebugContents(debuggable Debuggable, contents ...string) {
d.debuggables = append(d.debuggables, debuggable)
d.debuggableContents[debuggable] = contents
}
func (d *DebugContents) Format(s fmt.State, v rune) {
if d == nil {
if _debug != nil {
_debug("nil debug attempt") // TODO: this should no happen
}
return
}
switch v {
case 's', 'v':
// TODO: check if that hacky hacky makes sense
if len(d.debuggables) > 0 {
debuggable := d.debuggables[0]
_, _ = fmt.Fprintf(s, "<%s at %p>", TypeName(debuggable), debuggable)
}
case 'r':
// TODO: check if that hacky hacky makes sense
if len(d.debuggables) > 0 {
debuggable := d.debuggables[0]
_, _ = fmt.Fprintf(s, "<%s at %p>\n", TypeName(debuggable), debuggable)
}
d.PrintDebugContents(0, s, nil)
}
}
func (d *DebugContents) PrintDebugContents(indent int, file io.Writer, _ids []uintptr) {
if indent == 0 {
indent = 1
}
if file == nil {
file = os.Stderr
}
// loop through the debuggables and look for debugContents
attrToDebuggable := map[string]Debuggable{} // Reverse map to know where to get the attr from
var attrs []string
cids := map[uintptr]struct{}{
uintptr(unsafe.Pointer(d)): {},
}
var ownFn []DebugContentPrinter // TODO: make them member and add a add method
for _, debuggable := range d.debuggables {
debugContents, ok := d.debuggableContents[debuggable]
if !ok {
continue
}
// already seen it?
if _, ok := cids[uintptr(unsafe.Pointer(&debuggable))]; ok {
// continue // TODO: seems the logic is broken: e.g. RecurringTask is seen but it should be a own entity
}
cids[uintptr(unsafe.Pointer(&debuggable))] = struct{}{}
for _, attr := range debugContents {
if !slices.Contains(attrs, attr) {
cleanAttr := attr
for strings.HasSuffix(cleanAttr, "-") || strings.HasSuffix(cleanAttr, "*") || strings.HasSuffix(cleanAttr, "+") {
cleanAttr = strings.TrimSuffix(cleanAttr, "-")
cleanAttr = strings.TrimSuffix(cleanAttr, "*")
cleanAttr = strings.TrimSuffix(cleanAttr, "+")
}
attrToDebuggable[cleanAttr] = debuggable
attrs = append(attrs, attr)
}
}
}
// a bit of debugging
if _debug != nil {
_debug(" - attrs: %r", attrs)
//_debug(" - ownFn: %r", ownFn)
}
for _, printer := range d.extraPrinters {
printer.PrintDebugContents(indent, file, _ids)
}
for _, attr := range attrs {
// assume you're going deep, but not into lists and dicts
goDeep := true
goListDict := false
goHexed := false
// attribute list might want to go deep
if strings.HasSuffix(attr, "-") {
goDeep = false
attr = attr[:len(attr)-1]
} else if strings.HasSuffix(attr, "*") {
goHexed = true
attr = attr[:len(attr)-1]
} else if strings.HasSuffix(attr, "+") {
goDeep = false
goListDict = true
attr = attr[:len(attr)-1]
if strings.HasSuffix(attr, "+") {
goDeep = true
attr = attr[:len(attr)-1]
}
}
debuggable := attrToDebuggable[attr]
if debuggable == nil {
panic("attr misconfiguration " + attr)
}
value := debuggable.GetDebugAttr(attr)
// skip nil
if value == nil {
continue
}
// standard output
if goListDict && isList(value) {
list, ok := toList(value)
if !ok {
panic("impossible")
}
_, _ = fmt.Fprintf(file, "%s%s = [\n", strings.Repeat(" ", indent), attr)
indent += 1
for i, elem := range list {
_, _ = fmt.Fprintf(file, "%s[%d] %r\n", strings.Repeat(" ", indent), i, elem)
if deepDebugContent, ok := elem.(interface {
DebugContents(int, io.Writer, []uintptr)
}); goDeep && ok {
if slices.Contains(_ids, uintptr(unsafe.Pointer(&deepDebugContent))) {
_ids = append(_ids, uintptr(unsafe.Pointer(&deepDebugContent)))
deepDebugContent.DebugContents(indent+1, file, _ids)
}
}
}
indent -= 1
_, _ = fmt.Fprintf(file, "%s ]\n", strings.Repeat(" ", indent))
} else if goListDict && isDict(value) {
panic("add support for map") // TODO: add map support
} else if goHexed && isString(value) { // TODO: add support
panic("add support")
} else {
printVerb := verbForType(value, 'r')
_, _ = fmt.Fprintf(file, "%s%s = %"+string(printVerb)+"\n", strings.Repeat(" ", indent), attr, value)
// go nested if it is debuggable
if deepDebugContent, ok := value.(DebugContentPrinter); goDeep && ok {
if slices.Contains(_ids, uintptr(unsafe.Pointer(&deepDebugContent))) {
_ids = append(_ids, uintptr(unsafe.Pointer(&deepDebugContent)))
deepDebugContent.PrintDebugContents(indent+1, file, _ids)
}
}
}
}
// go through the functions
slices.Reverse(ownFn)
for _, printer := range ownFn {
printer.PrintDebugContents(indent, file, _ids)
}
}
func isList(input any) bool {
reflectValue := reflect.ValueOf(input)
switch reflectValue.Kind() {
case reflect.Slice, reflect.Array:
return true
default:
return false
}
}
func toList(input any) (list []any, ok bool) {
reflectValue := reflect.ValueOf(input)
switch reflectValue.Kind() {
case reflect.Slice, reflect.Array:
default:
return nil, false
}
list = make([]any, reflectValue.Len())
for i := 0; i < reflectValue.Len(); i++ {
elem := reflectValue.Index(i).Interface()
list[i] = elem
}
return list, true
}
func isDict(input any) bool {
reflectValue := reflect.ValueOf(input)
switch reflectValue.Kind() {
case reflect.Map:
return true
default:
return false
}
}
func toMap(input any) (_map map[string]any, ok bool) {
reflectValue := reflect.ValueOf(input)
switch reflectValue.Kind() {
case reflect.Map:
default:
return nil, false
}
_map = make(map[string]any)
for _, k := range reflectValue.MapKeys() {
value := reflectValue.MapIndex(k)
_map[k.String()] = value.Interface() //TODO: check if we further need to convert map key
}
return _map, true
}
func isString(input any) bool {
_, ok := input.(string)
return ok
}
func TypeName(anything any) string {
typeOf := reflect.TypeOf(anything)
if typeOf.Kind() == reflect.Ptr {
typeOf = typeOf.Elem()
}
return typeOf.Name()
}
// TODO: migrate comp debug logger to here...
type LoggingFormatter struct {
// TODO: implement me
}
type DefaultRFormatter struct {
header string
extraPrinters []DebugContentPrinter
}
func NewDefaultRFormatter(extraPrinters ...DebugContentPrinter) *DefaultRFormatter {
pc, file, _, ok := runtime.Caller(1)
if !ok {
panic("oh no")
}
dir := path.Dir(file)
rootIndex := strings.Index(dir, "bacgopes")
dir = dir[rootIndex:]
dirPrefix := path.Base(dir) + "_"
base := path.Base(file)
prefix := strings.TrimSuffix(base, ".go")
prefix = strings.TrimPrefix(prefix, dirPrefix)
return &DefaultRFormatter{
header: fmt.Sprintf("<%s at 0x%x>", prefix, pc),
extraPrinters: extraPrinters,
}
}
func (d *DefaultRFormatter) Format(s fmt.State, v rune) {
if d.header == "" && len(d.extraPrinters) == 0 {
panic("misconfiguration")
}
switch v {
case 'r':
_, _ = s.Write([]byte(d.header))
if len(d.extraPrinters) > 0 {
_, _ = s.Write([]byte("\n"))
}
for _, printer := range d.extraPrinters {
printer.PrintDebugContents(1, s, nil)
}
case 'v', 's':
_, _ = s.Write([]byte(d.header))
}
}
func StructName() string {
_, file, _, ok := runtime.Caller(1)
if !ok {
return ""
}
dir := path.Dir(file)
rootIndex := strings.Index(dir, "bacgopes")
dir = dir[rootIndex:]
dirPrefix := path.Base(dir) + "_"
base := path.Base(file)
prefix := strings.TrimSuffix(base, ".go")
return strings.TrimPrefix(prefix, dirPrefix)
}
type DebugPrinter = func(format string, a ...any)
const projectName = "bacgopes"
var customProjectName = os.Getenv("BACGOPES_DEBUG_CUSTOM_PROJECT_NAME")
var customReplaces map[string]string
func init() {
customReplaces = map[string]string{}
for _, replace := range strings.Split(os.Getenv("BACGOPES_DEBUG_CUSTOM_REPLACES"), ",") {
if replace == "" || !strings.Contains(replace, "=") {
continue
}
kv := strings.SplitN(replace, "=", 2)
if len(kv) != 2 {
panic("invalid replace " + replace)
}
customReplaces[kv[0]] = kv[1]
}
}
func CreateDebugPrinter() DebugPrinter {
_, file, _, ok := runtime.Caller(1)
if !ok {
return nil
}
dir := path.Dir(file)
rootIndex := strings.Index(dir, "bacgopes")
dir = dir[rootIndex:]
qualifier := strings.ReplaceAll(dir, "/", ".")
dirPrefix := path.Base(dir) + "_"
bacgopesDebug := os.Getenv("BACGOPES_DEBUG")
if strings.Contains(bacgopesDebug, qualifier) {
return func(format string, a ...any) {
pc, file, _, ok := runtime.Caller(1)
if !ok {
return
}
base := path.Base(file)
prefix := strings.TrimSuffix(base, ".go")
if !strings.HasPrefix(prefix, dirPrefix) && !strings.Contains(prefix, "tests") && false { // TODO: disabled for now as it makes more trouble for the rest
// Attach the fuction name // TODO: check if that makes sense, only a workaround for bind at the moment
details := runtime.FuncForPC(pc)
name := details.Name()
name = name[strings.LastIndex(name, ".")+1:]
prefix = strings.ToLower(name)
}
prefix = strings.TrimPrefix(prefix, dirPrefix)
formatString := "DEBUG:" + qualifier + "." + prefix + ":" + format + "\n"
formatString = cleanupFormatString(formatString)
if customProjectName != "" {
formatString = strings.ReplaceAll(formatString, projectName, customProjectName)
}
formatString = fixVerbs(formatString, a...)
output := fmt.Sprintf(formatString, a...)
if strings.HasSuffix(output, "\n\n") { // TODO: another hacky workaround
output = strings.TrimSuffix(output, "\n")
}
for k, v := range customReplaces {
output = strings.ReplaceAll(output, k, v)
}
_, _ = os.Stdout.Write([]byte(output))
}
}
return nil
}
func cleanupFormatString(s string) string {
// TODO: investigate via comm.comm is happening
s = strings.ReplaceAll(s, ".comm.comm:", ".comm:")
s = strings.ReplaceAll(s, "pdu.comm_PCI:", "comm.PCI:")
s = strings.ReplaceAll(s, "pdu.comm_PDUData:", "comm.PDUData:")
s = strings.ReplaceAll(s, "DEBUG:"+projectName+".tests", "DEBUG:tests")
return s
}
func fixVerbs(formatString string, values ...any) string {
length := len(formatString)
verbNumber := -1
for i, r := range formatString {
switch r {
case '%':
nextIndex := i + 1
if nextIndex >= length {
continue
}
followRune := formatString[nextIndex]
if followRune != '%' {
verbNumber++
}
if followRune == 'r' && verbNumber < len(values) { // TODO: this completely breaks at indexed verbs... better fix assap
runes := []rune(formatString)
runes[nextIndex] = verbForType(values[verbNumber], 'r')
formatString = string(runes)
}
}
}
return formatString
}
func verbForType(value any, printVerb rune) rune {
switch value.(type) {
case string:
printVerb = 's'
case bool:
printVerb = 't'
case int8, uint8, int16, uint16, int32, uint32, int64, uint64, int, uint, uintptr:
printVerb = 'd'
case float32, float64:
printVerb = 'f'
case complex64, complex128:
printVerb = 'v' // TODO: what is it meant for?
case time.Time, time.Duration:
printVerb = 's'
case []byte:
printVerb = 'v'
}
return printVerb
}