mirror of
				https://github.com/go-delve/delve.git
				synced 2025-10-31 18:57:18 +08:00 
			
		
		
		
	service,terminal: propagating simultaneous breakpoints
This commit is contained in:
		| @ -193,18 +193,23 @@ starts and attaches to it, and enables you to immediately begin debugging your p | |||||||
| 							fmt.Fprintln(os.Stderr, state.Err) | 							fmt.Fprintln(os.Stderr, state.Err) | ||||||
| 							return 0 | 							return 0 | ||||||
| 						} | 						} | ||||||
| 						var args []string | 						for i := range state.Threads { | ||||||
| 						var fname string | 							th := state.Threads[i] | ||||||
| 						if state.CurrentThread != nil && state.CurrentThread.Function != nil { | 							if th.Breakpoint == nil { | ||||||
| 							fname = state.CurrentThread.Function.Name | 								continue | ||||||
| 						} |  | ||||||
| 						if state.BreakpointInfo != nil { |  | ||||||
| 							for _, arg := range state.BreakpointInfo.Arguments { |  | ||||||
| 								args = append(args, arg.SinglelineString()) |  | ||||||
| 							} | 							} | ||||||
|  | 							var args []string | ||||||
|  | 							var fname string | ||||||
|  | 							if th.Function != nil { | ||||||
|  | 								fname = th.Function.Name | ||||||
|  | 							} | ||||||
|  | 							if th.BreakpointInfo != nil { | ||||||
|  | 								for _, arg := range th.BreakpointInfo.Arguments { | ||||||
|  | 									args = append(args, arg.SinglelineString()) | ||||||
|  | 								} | ||||||
|  | 							} | ||||||
|  | 							fmt.Printf("%s(%s) %s:%d\n", fname, strings.Join(args, ", "), terminal.ShortenFilePath(th.File), th.Line) | ||||||
| 						} | 						} | ||||||
| 						fp := terminal.ShortenFilePath(state.CurrentThread.File) |  | ||||||
| 						fmt.Printf("%s(%s) %s:%d\n", fname, strings.Join(args, ", "), fp, state.CurrentThread.Line) |  | ||||||
| 					case <-sigChan: | 					case <-sigChan: | ||||||
| 						server.Stop(traceAttachPid == 0) | 						server.Stop(traceAttachPid == 0) | ||||||
| 						return 1 | 						return 1 | ||||||
|  | |||||||
| @ -37,6 +37,7 @@ func ConvertThread(th *proc.Thread) *Thread { | |||||||
| 		file     string | 		file     string | ||||||
| 		line     int | 		line     int | ||||||
| 		pc       uint64 | 		pc       uint64 | ||||||
|  | 		gid      int | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
| 	loc, err := th.Location() | 	loc, err := th.Location() | ||||||
| @ -47,12 +48,24 @@ func ConvertThread(th *proc.Thread) *Thread { | |||||||
| 		function = ConvertFunction(loc.Fn) | 		function = ConvertFunction(loc.Fn) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	var bp *Breakpoint | ||||||
|  |  | ||||||
|  | 	if th.CurrentBreakpoint != nil { | ||||||
|  | 		bp = ConvertBreakpoint(th.CurrentBreakpoint) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if g, _ := th.GetG(); g != nil { | ||||||
|  | 		gid = g.Id | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return &Thread{ | 	return &Thread{ | ||||||
| 		ID:       th.Id, | 		ID:          th.Id, | ||||||
| 		PC:       pc, | 		PC:          pc, | ||||||
| 		File:     file, | 		File:        file, | ||||||
| 		Line:     line, | 		Line:        line, | ||||||
| 		Function: function, | 		Function:    function, | ||||||
|  | 		GoroutineID: gid, | ||||||
|  | 		Breakpoint:  bp, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,19 +4,15 @@ import "reflect" | |||||||
|  |  | ||||||
| // DebuggerState represents the current context of the debugger. | // DebuggerState represents the current context of the debugger. | ||||||
| type DebuggerState struct { | type DebuggerState struct { | ||||||
| 	// Breakpoint is the current breakpoint at which the debugged process is |  | ||||||
| 	// suspended, and may be empty if the process is not suspended. |  | ||||||
| 	Breakpoint *Breakpoint `json:"breakPoint,omitempty"` |  | ||||||
| 	// CurrentThread is the currently selected debugger thread. | 	// CurrentThread is the currently selected debugger thread. | ||||||
| 	CurrentThread *Thread `json:"currentThread,omitempty"` | 	CurrentThread *Thread `json:"currentThread,omitempty"` | ||||||
| 	// SelectedGoroutine is the currently selected goroutine | 	// SelectedGoroutine is the currently selected goroutine | ||||||
| 	SelectedGoroutine *Goroutine `json:"currentGoroutine,omitempty"` | 	SelectedGoroutine *Goroutine `json:"currentGoroutine,omitempty"` | ||||||
| 	// Information requested by the current breakpoint | 	// List of all the process threads | ||||||
| 	BreakpointInfo *BreakpointInfo `json:"breakPointInfo,omitrempty"` | 	Threads []*Thread | ||||||
| 	// Exited indicates whether the debugged process has exited. | 	// Exited indicates whether the debugged process has exited. | ||||||
| 	Exited     bool `json:"exited"` | 	Exited     bool `json:"exited"` | ||||||
| 	ExitStatus int  `json:"exitStatus"` | 	ExitStatus int  `json:"exitStatus"` | ||||||
|  |  | ||||||
| 	// Filled by RPCClient.Continue, indicates an error | 	// Filled by RPCClient.Continue, indicates an error | ||||||
| 	Err error `json:"-"` | 	Err error `json:"-"` | ||||||
| } | } | ||||||
| @ -62,6 +58,14 @@ type Thread struct { | |||||||
| 	Line int `json:"line"` | 	Line int `json:"line"` | ||||||
| 	// Function is function information at the program counter. May be nil. | 	// Function is function information at the program counter. May be nil. | ||||||
| 	Function *Function `json:"function,omitempty"` | 	Function *Function `json:"function,omitempty"` | ||||||
|  |  | ||||||
|  | 	// ID of the goroutine running on this thread | ||||||
|  | 	GoroutineID int `json:"goroutineID"` | ||||||
|  |  | ||||||
|  | 	// Breakpoint this thread is stopped at | ||||||
|  | 	Breakpoint *Breakpoint `json:"breakPoint,omitempty"` | ||||||
|  | 	// Informations requested by the current breakpoint | ||||||
|  | 	BreakpointInfo *BreakpointInfo `json:"breakPointInfo,omitrempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type Location struct { | type Location struct { | ||||||
|  | |||||||
| @ -111,31 +111,26 @@ func (d *Debugger) State() (*api.DebuggerState, error) { | |||||||
|  |  | ||||||
| 	var ( | 	var ( | ||||||
| 		state     *api.DebuggerState | 		state     *api.DebuggerState | ||||||
| 		thread    *api.Thread |  | ||||||
| 		goroutine *api.Goroutine | 		goroutine *api.Goroutine | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
| 	if d.process.CurrentThread != nil { |  | ||||||
| 		thread = api.ConvertThread(d.process.CurrentThread) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if d.process.SelectedGoroutine != nil { | 	if d.process.SelectedGoroutine != nil { | ||||||
| 		goroutine = api.ConvertGoroutine(d.process.SelectedGoroutine) | 		goroutine = api.ConvertGoroutine(d.process.SelectedGoroutine) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var breakpoint *api.Breakpoint |  | ||||||
| 	bp := d.process.CurrentBreakpoint() |  | ||||||
| 	if bp != nil { |  | ||||||
| 		breakpoint = api.ConvertBreakpoint(bp) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	state = &api.DebuggerState{ | 	state = &api.DebuggerState{ | ||||||
| 		Breakpoint:        breakpoint, |  | ||||||
| 		CurrentThread:     thread, |  | ||||||
| 		SelectedGoroutine: goroutine, | 		SelectedGoroutine: goroutine, | ||||||
| 		Exited:            d.process.Exited(), | 		Exited:            d.process.Exited(), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	for i := range d.process.Threads { | ||||||
|  | 		th := api.ConvertThread(d.process.Threads[i]) | ||||||
|  | 		state.Threads = append(state.Threads, th) | ||||||
|  | 		if i == d.process.CurrentThread.Id { | ||||||
|  | 			state.CurrentThread = th | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return state, nil | 	return state, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -272,52 +267,59 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er | |||||||
| } | } | ||||||
|  |  | ||||||
| func (d *Debugger) collectBreakpointInformation(state *api.DebuggerState) error { | func (d *Debugger) collectBreakpointInformation(state *api.DebuggerState) error { | ||||||
| 	if state == nil || state.Breakpoint == nil { | 	if state == nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	bp := state.Breakpoint | 	for i := range state.Threads { | ||||||
| 	bpi := &api.BreakpointInfo{} | 		if state.Threads[i].Breakpoint == nil { | ||||||
| 	state.BreakpointInfo = bpi | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 	if bp.Goroutine { | 		bp := state.Threads[i].Breakpoint | ||||||
| 		g, err := d.process.CurrentThread.GetG() | 		bpi := &api.BreakpointInfo{} | ||||||
|  | 		state.Threads[i].BreakpointInfo = bpi | ||||||
|  |  | ||||||
|  | 		if bp.Goroutine { | ||||||
|  | 			g, err := d.process.CurrentThread.GetG() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			bpi.Goroutine = api.ConvertGoroutine(g) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if bp.Stacktrace > 0 { | ||||||
|  | 			rawlocs, err := d.process.CurrentThread.Stacktrace(bp.Stacktrace) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			bpi.Stacktrace, err = d.convertStacktrace(rawlocs, false) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		s, err := d.process.CurrentThread.Scope() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		bpi.Goroutine = api.ConvertGoroutine(g) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if bp.Stacktrace > 0 { | 		if len(bp.Variables) > 0 { | ||||||
| 		rawlocs, err := d.process.CurrentThread.Stacktrace(bp.Stacktrace) | 			bpi.Variables = make([]api.Variable, len(bp.Variables)) | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} | 		} | ||||||
| 		bpi.Stacktrace, err = d.convertStacktrace(rawlocs, false) | 		for i := range bp.Variables { | ||||||
| 		if err != nil { | 			v, err := s.EvalVariable(bp.Variables[i]) | ||||||
| 			return err | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			bpi.Variables[i] = *api.ConvertVar(v) | ||||||
|  | 		} | ||||||
|  | 		vars, err := s.FunctionArguments() | ||||||
|  | 		if err == nil { | ||||||
|  | 			bpi.Arguments = convertVars(vars) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	s, err := d.process.CurrentThread.Scope() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if len(bp.Variables) > 0 { |  | ||||||
| 		bpi.Variables = make([]api.Variable, len(bp.Variables)) |  | ||||||
| 	} |  | ||||||
| 	for i := range bp.Variables { |  | ||||||
| 		v, err := s.EvalVariable(bp.Variables[i]) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		bpi.Variables[i] = *api.ConvertVar(v) |  | ||||||
| 	} |  | ||||||
| 	args, err := s.FunctionArguments() |  | ||||||
| 	if err == nil { |  | ||||||
| 		bpi.Arguments = convertVars(args) |  | ||||||
| 	} |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -66,7 +66,21 @@ func (c *RPCClient) Continue() <-chan *api.DebuggerState { | |||||||
| 				state.Err = fmt.Errorf("Process %d has exited with status %d", c.ProcessPid(), state.ExitStatus) | 				state.Err = fmt.Errorf("Process %d has exited with status %d", c.ProcessPid(), state.ExitStatus) | ||||||
| 			} | 			} | ||||||
| 			ch <- state | 			ch <- state | ||||||
| 			if err != nil || state.Exited || state.Breakpoint == nil || !state.Breakpoint.Tracepoint { | 			if err != nil || state.Exited { | ||||||
|  | 				close(ch) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			isbreakpoint := false | ||||||
|  | 			istracepoint := true | ||||||
|  | 			for i := range state.Threads { | ||||||
|  | 				if state.Threads[i].Breakpoint != nil { | ||||||
|  | 					isbreakpoint = true | ||||||
|  | 					istracepoint = istracepoint && state.Threads[i].Breakpoint.Tracepoint | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if !isbreakpoint || !istracepoint { | ||||||
| 				close(ch) | 				close(ch) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  | |||||||
| @ -96,7 +96,7 @@ func TestRestart_duringStop(t *testing.T) { | |||||||
| 			t.Fatal(err) | 			t.Fatal(err) | ||||||
| 		} | 		} | ||||||
| 		state := <-c.Continue() | 		state := <-c.Continue() | ||||||
| 		if state.Breakpoint == nil { | 		if state.CurrentThread.Breakpoint == nil { | ||||||
| 			t.Fatal("did not hit breakpoint") | 			t.Fatal("did not hit breakpoint") | ||||||
| 		} | 		} | ||||||
| 		if err := c.Restart(); err != nil { | 		if err := c.Restart(); err != nil { | ||||||
| @ -433,12 +433,12 @@ func TestClientServer_traceContinue(t *testing.T) { | |||||||
| 		count := 0 | 		count := 0 | ||||||
| 		contChan := c.Continue() | 		contChan := c.Continue() | ||||||
| 		for state := range contChan { | 		for state := range contChan { | ||||||
| 			if state.Breakpoint != nil { | 			if state.CurrentThread != nil && state.CurrentThread.Breakpoint != nil { | ||||||
| 				count++ | 				count++ | ||||||
|  |  | ||||||
| 				t.Logf("%v", state) | 				t.Logf("%v", state) | ||||||
|  |  | ||||||
| 				bpi := state.BreakpointInfo | 				bpi := state.CurrentThread.BreakpointInfo | ||||||
|  |  | ||||||
| 				if bpi.Goroutine == nil { | 				if bpi.Goroutine == nil { | ||||||
| 					t.Fatalf("No goroutine information") | 					t.Fatalf("No goroutine information") | ||||||
| @ -494,8 +494,8 @@ func TestClientServer_traceContinue2(t *testing.T) { | |||||||
| 		countSayhi := 0 | 		countSayhi := 0 | ||||||
| 		contChan := c.Continue() | 		contChan := c.Continue() | ||||||
| 		for state := range contChan { | 		for state := range contChan { | ||||||
| 			if state.Breakpoint != nil { | 			if state.CurrentThread != nil && state.CurrentThread.Breakpoint != nil { | ||||||
| 				switch state.Breakpoint.ID { | 				switch state.CurrentThread.Breakpoint.ID { | ||||||
| 				case bp1.ID: | 				case bp1.ID: | ||||||
| 					countMain++ | 					countMain++ | ||||||
| 				case bp2.ID: | 				case bp2.ID: | ||||||
|  | |||||||
| @ -837,6 +837,15 @@ func printStack(stack []api.Stackframe, ind string) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func printcontext(t *Term, state *api.DebuggerState) error { | func printcontext(t *Term, state *api.DebuggerState) error { | ||||||
|  | 	for i := range state.Threads { | ||||||
|  | 		if (state.CurrentThread != nil) && (state.Threads[i].ID == state.CurrentThread.ID) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if state.Threads[i].Breakpoint != nil { | ||||||
|  | 			printcontextThread(t, state.Threads[i]) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if state.CurrentThread == nil { | 	if state.CurrentThread == nil { | ||||||
| 		fmt.Println("No current thread available") | 		fmt.Println("No current thread available") | ||||||
| 		return nil | 		return nil | ||||||
| @ -846,64 +855,70 @@ func printcontext(t *Term, state *api.DebuggerState) error { | |||||||
| 		t.Println("=>", "no source available") | 		t.Println("=>", "no source available") | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	var fn *api.Function |  | ||||||
| 	if state.CurrentThread.Function != nil { | 	printcontextThread(t, state.CurrentThread) | ||||||
| 		fn = state.CurrentThread.Function |  | ||||||
|  | 	if state.CurrentThread.Breakpoint == nil || !state.CurrentThread.Breakpoint.Tracepoint { | ||||||
|  | 		return printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func printcontextThread(t *Term, th *api.Thread) { | ||||||
|  | 	fn := th.Function | ||||||
|  |  | ||||||
|  | 	if th.Breakpoint == nil { | ||||||
|  | 		fmt.Printf("> %s() %s:%d\n", fn.Name, ShortenFilePath(th.File), th.Line) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if state.Breakpoint != nil { | 	args := "" | ||||||
| 		args := "" | 	if th.Breakpoint.Tracepoint && fn != nil { | ||||||
| 		if state.Breakpoint.Tracepoint { | 		var arg []string | ||||||
| 			var arg []string | 		for _, ar := range fn.Args { | ||||||
| 			for _, ar := range state.CurrentThread.Function.Args { | 			arg = append(arg, ar.SinglelineString()) | ||||||
| 				arg = append(arg, ar.SinglelineString()) |  | ||||||
| 			} |  | ||||||
| 			args = strings.Join(arg, ", ") |  | ||||||
| 		} | 		} | ||||||
|  | 		args = strings.Join(arg, ", ") | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 		if hitCount, ok := state.Breakpoint.HitCount[strconv.Itoa(state.SelectedGoroutine.ID)]; ok { | 	if hitCount, ok := th.Breakpoint.HitCount[strconv.Itoa(th.GoroutineID)]; ok { | ||||||
| 			fmt.Printf("> %s(%s) %s:%d (hits goroutine(%d):%d total:%d)\n", | 		fmt.Printf("> %s(%s) %s:%d (hits goroutine(%d):%d total:%d)\n", | ||||||
| 				fn.Name, | 			fn.Name, | ||||||
| 				args, | 			args, | ||||||
| 				ShortenFilePath(state.CurrentThread.File), | 			ShortenFilePath(th.File), | ||||||
| 				state.CurrentThread.Line, | 			th.Line, | ||||||
| 				state.SelectedGoroutine.ID, | 			th.GoroutineID, | ||||||
| 				hitCount, | 			hitCount, | ||||||
| 				state.Breakpoint.TotalHitCount) | 			th.Breakpoint.TotalHitCount) | ||||||
| 		} else { |  | ||||||
| 			fmt.Printf("> %s(%s) %s:%d (hits total:%d)\n", |  | ||||||
| 				fn.Name, |  | ||||||
| 				args, |  | ||||||
| 				ShortenFilePath(state.CurrentThread.File), |  | ||||||
| 				state.CurrentThread.Line, |  | ||||||
| 				state.Breakpoint.TotalHitCount) |  | ||||||
| 		} |  | ||||||
| 	} else { | 	} else { | ||||||
| 		fmt.Printf("> %s() %s:%d\n", fn.Name, ShortenFilePath(state.CurrentThread.File), state.CurrentThread.Line) | 		fmt.Printf("> %s(%s) %s:%d (hits total:%d)\n", | ||||||
|  | 			fn.Name, | ||||||
|  | 			args, | ||||||
|  | 			ShortenFilePath(th.File), | ||||||
|  | 			th.Line, | ||||||
|  | 			th.Breakpoint.TotalHitCount) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if state.BreakpointInfo != nil { | 	if th.BreakpointInfo != nil { | ||||||
| 		bpi := state.BreakpointInfo | 		bpi := th.BreakpointInfo | ||||||
|  |  | ||||||
| 		if bpi.Goroutine != nil { | 		if bpi.Goroutine != nil { | ||||||
| 			writeGoroutineLong(os.Stdout, bpi.Goroutine, "\t") | 			writeGoroutineLong(os.Stdout, bpi.Goroutine, "\t") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		ss := make([]string, len(bpi.Variables)) | 		if len(bpi.Variables) > 0 { | ||||||
| 		for i, v := range bpi.Variables { | 			ss := make([]string, len(bpi.Variables)) | ||||||
| 			ss[i] = fmt.Sprintf("%s: %v", v.Name, v.MultilineString("")) | 			for i, v := range bpi.Variables { | ||||||
|  | 				ss[i] = fmt.Sprintf("%s: %s", v.Name, v.MultilineString("")) | ||||||
|  | 			} | ||||||
|  | 			fmt.Printf("\t%s\n", strings.Join(ss, ", ")) | ||||||
| 		} | 		} | ||||||
| 		fmt.Printf("\t%s\n", strings.Join(ss, ", ")) |  | ||||||
|  |  | ||||||
| 		if bpi.Stacktrace != nil { | 		if bpi.Stacktrace != nil { | ||||||
| 			fmt.Printf("\tStack:\n") | 			fmt.Printf("\tStack:\n") | ||||||
| 			printStack(bpi.Stacktrace, "\t\t") | 			printStack(bpi.Stacktrace, "\t\t") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if state.Breakpoint != nil && state.Breakpoint.Tracepoint { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	return printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func printfile(t *Term, filename string, line int, showArrow bool) error { | func printfile(t *Term, filename string, line int, showArrow bool) error { | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 aarzilli
					aarzilli