refactor(debug): move watchpoint slot state into the session
This commit is contained in:
+4
-5
@@ -36,7 +36,7 @@ type Condition struct {
|
|||||||
func (c *Condition) Eval(regs *Regs) bool {
|
func (c *Condition) Eval(regs *Regs) bool {
|
||||||
actual, ok := regs.RegValue(c.Reg)
|
actual, ok := regs.RegValue(c.Reg)
|
||||||
if !ok {
|
if !ok {
|
||||||
return true // unknown register — don't block
|
return true // unknown register, don't block
|
||||||
}
|
}
|
||||||
var expected uint64
|
var expected uint64
|
||||||
switch {
|
switch {
|
||||||
@@ -48,7 +48,7 @@ func (c *Condition) Eval(regs *Regs) bool {
|
|||||||
}
|
}
|
||||||
expected = v
|
expected = v
|
||||||
case c.MemAddr != 0:
|
case c.MemAddr != 0:
|
||||||
// Register-memory comparison — requires a Session, not available here.
|
// Register-memory comparison, requires a Session, not available here.
|
||||||
// Fall back to treating as constant (the caller should resolve).
|
// Fall back to treating as constant (the caller should resolve).
|
||||||
expected = c.Value
|
expected = c.Value
|
||||||
default:
|
default:
|
||||||
@@ -72,8 +72,7 @@ func (c *Condition) Eval(regs *Regs) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Breakpoints manages the set of breakpoints for a Session.
|
// Breakpoints manages the software breakpoints of one Session.
|
||||||
// Breakpoints manages software breakpoints for a debuggee.
|
|
||||||
type Breakpoints struct {
|
type Breakpoints struct {
|
||||||
t tracer
|
t tracer
|
||||||
bps map[uint64]*Breakpoint
|
bps map[uint64]*Breakpoint
|
||||||
@@ -200,7 +199,7 @@ func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
|||||||
}
|
}
|
||||||
// Check the condition (if any).
|
// Check the condition (if any).
|
||||||
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
||||||
// Condition not met — restore the byte but do NOT rewind RIP.
|
// Condition not met, restore the byte but do NOT rewind RIP.
|
||||||
// The process continues from the next instruction (past the INT3).
|
// The process continues from the next instruction (past the INT3).
|
||||||
word, err := bm.t.Peek(trapAddr)
|
word, err := bm.t.Peek(trapAddr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
+4
-5
@@ -275,8 +275,7 @@ func TestBreakpointInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWatchpointSlotTracking(t *testing.T) {
|
func TestWatchpointSlotTracking(t *testing.T) {
|
||||||
wpSlots = [4]bool{} // reset
|
s := &Session{} // per-session slots start free
|
||||||
s := &Session{}
|
|
||||||
|
|
||||||
// All four slots are free initially.
|
// All four slots are free initially.
|
||||||
for i := range 4 {
|
for i := range 4 {
|
||||||
@@ -289,8 +288,8 @@ func TestWatchpointSlotTracking(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint).
|
// Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint).
|
||||||
wpSlots[0] = true
|
s.wpSlots[0] = true
|
||||||
wpSlots[2] = true
|
s.wpSlots[2] = true
|
||||||
|
|
||||||
if !s.IsWatchpointSlotUsed(0) {
|
if !s.IsWatchpointSlotUsed(0) {
|
||||||
t.Error("slot 0 should be in use")
|
t.Error("slot 0 should be in use")
|
||||||
@@ -318,7 +317,7 @@ func TestWatchpointSlotTracking(t *testing.T) {
|
|||||||
|
|
||||||
// Mark all slots used: FindFreeWatchpointSlot returns -1.
|
// Mark all slots used: FindFreeWatchpointSlot returns -1.
|
||||||
for i := range 4 {
|
for i := range 4 {
|
||||||
wpSlots[i] = true
|
s.wpSlots[i] = true
|
||||||
}
|
}
|
||||||
if got := s.FindFreeWatchpointSlot(); got != -1 {
|
if got := s.FindFreeWatchpointSlot(); got != -1 {
|
||||||
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ type Session struct {
|
|||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
stopped bool
|
stopped bool
|
||||||
exited bool
|
exited bool
|
||||||
codeBase uint64 // base address of the JIT code in the debuggee
|
codeBase uint64 // base address of the JIT code in the debuggee
|
||||||
|
wpSlots [16]bool // hardware watchpoint slots in use (DR0-DR3, arm64 BADVR0-15)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch starts the debuggee subprocess (gasm debug --target ...) and
|
// Launch starts the debuggee subprocess (gasm debug --target ...) and
|
||||||
|
|||||||
@@ -20,14 +20,11 @@ const (
|
|||||||
WatchRead WatchpointType = 3 // trigger on read or write
|
WatchRead WatchpointType = 3 // trigger on read or write
|
||||||
)
|
)
|
||||||
|
|
||||||
// wpSlots tracks watchpoint slot occupancy (DR0-DR3).
|
|
||||||
var wpSlots [4]bool
|
|
||||||
|
|
||||||
// FindFreeWatchpointSlot returns the index of the first free watchpoint slot
|
// FindFreeWatchpointSlot returns the index of the first free watchpoint slot
|
||||||
// (0-3), or -1 if all four hardware watchpoints are in use.
|
// (0-3), or -1 if all four hardware watchpoints are in use.
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range 4 {
|
for i := range 4 {
|
||||||
if !wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +36,7 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return s.wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||||
@@ -47,7 +44,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||||
}
|
}
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +93,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||||
return fmt.Errorf("debug: set DR7: %w", err)
|
return fmt.Errorf("debug: set DR7: %w", err)
|
||||||
}
|
}
|
||||||
wpSlots[slot] = true
|
s.wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +102,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||||
}
|
}
|
||||||
if !wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
}
|
}
|
||||||
dr7, err := ptracePeekUser(s.pid, 0x38)
|
dr7, err := ptracePeekUser(s.pid, 0x38)
|
||||||
@@ -116,14 +113,14 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearAllWatchpoints removes all hardware watchpoints.
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := range 4 {
|
for slot := range 4 {
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ const (
|
|||||||
WatchRead WatchpointType = 3
|
WatchRead WatchpointType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// wpSlots tracks watchpoint slot occupancy.
|
|
||||||
var wpSlots [16]bool // arm64 supports up to 16 watchpoints
|
|
||||||
|
|
||||||
const maxWatchpoints = 16
|
const maxWatchpoints = 16
|
||||||
|
|
||||||
// hwBreakState mirrors the kernel's struct user_hwdebug_state.
|
// hwBreakState mirrors the kernel's struct user_hwdebug_state.
|
||||||
@@ -45,7 +42,7 @@ const (
|
|||||||
|
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range maxWatchpoints {
|
for i := range maxWatchpoints {
|
||||||
if !wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +53,7 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return s.wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||||
@@ -64,7 +61,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +102,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
return fmt.Errorf("debug: set watchpoint: %w", err)
|
return fmt.Errorf("debug: set watchpoint: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wpSlots[slot] = true
|
s.wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +110,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if !wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,13 +123,13 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if err := s.setHWBreakState(state); err != nil {
|
if err := s.setHWBreakState(state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
for slot := 0; slot < maxWatchpoints; slot++ {
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,11 @@ const (
|
|||||||
WatchRead WatchpointType = 3
|
WatchRead WatchpointType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// wpSlots tracks watchpoint slot occupancy.
|
|
||||||
var wpSlots [4]bool
|
|
||||||
|
|
||||||
const maxWatchpoints = 4
|
const maxWatchpoints = 4
|
||||||
|
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range maxWatchpoints {
|
for i := range maxWatchpoints {
|
||||||
if !wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +36,7 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return s.wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint.
|
// SetWatchpoint installs a hardware watchpoint.
|
||||||
@@ -47,7 +44,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
if size != 1 && size != 2 && size != 4 && size != 8 {
|
if size != 1 && size != 2 && size != 4 && size != 8 {
|
||||||
@@ -85,7 +82,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wpSlots[slot] = true
|
s.wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,20 +90,20 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if !wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
for slot := 0; slot < maxWatchpoints; slot++ {
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,11 @@ const (
|
|||||||
WatchRead WatchpointType = 3
|
WatchRead WatchpointType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// wpSlots tracks watchpoint slot occupancy.
|
|
||||||
var wpSlots [4]bool
|
|
||||||
|
|
||||||
const maxWatchpoints = 4
|
const maxWatchpoints = 4
|
||||||
|
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range maxWatchpoints {
|
for i := range maxWatchpoints {
|
||||||
if !wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +36,7 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return s.wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint.
|
// SetWatchpoint installs a hardware watchpoint.
|
||||||
@@ -47,7 +44,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
if size != 1 && size != 2 && size != 4 && size != 8 {
|
if size != 1 && size != 2 && size != 4 && size != 8 {
|
||||||
@@ -87,7 +84,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wpSlots[slot] = true
|
s.wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +92,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if !wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,13 +100,13 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
for slot := 0; slot < maxWatchpoints; slot++ {
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user