mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-12-16 04:20:29 +01:00
LLDB Debugger: threads
git-svn-id: trunk@58326 -
This commit is contained in:
parent
929517bab6
commit
a44d260582
@ -1,5 +1,4 @@
|
||||
(*
|
||||
settings set prompt ((lldb)) \r\n
|
||||
settings set target.output-path /tmp/out.txt
|
||||
*)
|
||||
unit LldbDebugger;
|
||||
@ -9,8 +8,9 @@ unit LldbDebugger;
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, math, DbgIntfDebuggerBase, LazLoggerBase, LazClasses,
|
||||
LazFileUtils, Maps, strutils, DebugProcess, LldbInstructions, LldbHelper;
|
||||
Classes, SysUtils, math, DbgIntfDebuggerBase, DbgIntfBaseTypes, LazLoggerBase,
|
||||
LazClasses, LazFileUtils, Maps, strutils, DebugProcess, LldbInstructions,
|
||||
LldbHelper;
|
||||
|
||||
type
|
||||
|
||||
@ -279,58 +279,19 @@ procedure TLldbDebuggerCommandThreads.ThreadInstructionSucceeded(Sender: TObject
|
||||
var
|
||||
Instr: TLldbInstructionThreadList absolute Sender;
|
||||
i, j, line: Integer;
|
||||
s, func, filename, name: String;
|
||||
s, func, filename, name, d: String;
|
||||
found, foundFunc, foundArg: TStringArray;
|
||||
TId, CurThrId, addr: LongInt;
|
||||
TId, CurThrId: LongInt;
|
||||
CurThr: Boolean;
|
||||
Arguments: TStringList;
|
||||
addr: TDBGPtr;
|
||||
begin
|
||||
CurrentThreads.Clear;
|
||||
for i := 0 to Length(Instr.Res) - 1 do begin
|
||||
CurThr := False;
|
||||
s := Instr.Res[i];
|
||||
if (Length(s) > 1) and (s[1] = '*') then begin
|
||||
s[1] := ' ';
|
||||
CurThr := True;
|
||||
end;
|
||||
j := pos(', stop reason', s);
|
||||
if j > 0 then s := copy(s, 1, j-1);
|
||||
|
||||
TId := 0;
|
||||
addr := 0;
|
||||
func := s;
|
||||
filename := '';
|
||||
line := 0;
|
||||
name := '';
|
||||
if StrMatches(s, [' thread #'{id}, ': '{}, ''], found) then begin
|
||||
TId := StrToIntDef(found[0], -1);
|
||||
s := found[1];
|
||||
end;
|
||||
|
||||
Arguments := nil;
|
||||
if StrMatches(s, ['tid = '{}, ', '{addr}, ' '{exe}, '`'{remainder}, ''], found) then begin
|
||||
if CurThr then begin
|
||||
CurThrId := TId;
|
||||
DebugLn(['Parsing threads, new current ',TId, ' dbg has ', Debugger.FCurrentThreadId]);
|
||||
Debugger.FCurrentThreadId := TId;
|
||||
end;
|
||||
name := found[0];
|
||||
addr := StrToIntDef(found[1], 0);
|
||||
|
||||
if StrMatches(found[3], [''{func}, '',' at '{file}, ':'{line}, ''], foundFunc) then begin
|
||||
Arguments := TStringList.Create;
|
||||
if StrMatches(foundFunc[0], ['', '(', '',')'], foundArg) then begin
|
||||
Arguments.CommaText := foundArg[1];
|
||||
foundFunc[0] := foundArg[0];
|
||||
end;
|
||||
func := foundFunc[0];
|
||||
line := StrToIntDef(foundFunc[2], 0);
|
||||
filename := foundFunc[1];
|
||||
end
|
||||
else begin
|
||||
func := found[2]+' '+found[3];
|
||||
end;
|
||||
end;
|
||||
ParseThreadLocation(s, TId, CurThr, name, addr, func, Arguments, filename, line, d);
|
||||
if CurThr then
|
||||
CurThrId := TId;
|
||||
|
||||
CurrentThreads.Add(
|
||||
CurrentThreads.CreateEntry(
|
||||
@ -350,18 +311,6 @@ begin
|
||||
CurrentThreads.SetValidity(ddsValid);
|
||||
|
||||
Finished;
|
||||
|
||||
(*
|
||||
"(lldb) thread list"
|
||||
"Process 11984 stopped"
|
||||
"* thread #1: tid = 0x1a1c, 0x0042951d project1.exe`FORMCREATE(this=0x00151060, SENDER=0x00151060) at unit1.pas:59, stop reason = breakpoint 2.1"
|
||||
" thread #2: tid = 0x16ac, 0x7700eb6c ntdll.dll`NtDelayExecution + 12"
|
||||
" thread #3: tid = 0x2930, 0x7700eb6c ntdll.dll`NtDelayExecution + 12"
|
||||
" thread #4: tid = 0x2bf8, 0x770104bc ntdll.dll`NtWaitForWorkViaWorkerFactory + 12"
|
||||
"(lldb) p 112236"
|
||||
"(int) $1 = 112236"
|
||||
*)
|
||||
|
||||
end;
|
||||
|
||||
procedure TLldbDebuggerCommandThreads.DoExecute;
|
||||
@ -400,6 +349,8 @@ begin
|
||||
if Debugger = nil then Exit;
|
||||
if not(Debugger.State in [dsPause, dsInternalPause]) then exit;
|
||||
|
||||
TLldbDebugger(Debugger).FCurrentThreadId := ANewId;
|
||||
|
||||
if CurrentThreads <> nil
|
||||
then CurrentThreads.CurrentThreadId := ANewId;
|
||||
end;
|
||||
@ -416,51 +367,28 @@ end;
|
||||
procedure TLldbCallStack.StackInstructionFinished(Sender: TObject);
|
||||
var
|
||||
Instr: TLldbInstructionStackTrace absolute Sender;
|
||||
i: Integer;
|
||||
i, FId, line: Integer;
|
||||
e: TCallStackEntry;
|
||||
found, foundArg: TStringArray;
|
||||
Arguments: TStringList;
|
||||
It: TMapIterator;
|
||||
s: String;
|
||||
s, func, filename, d: String;
|
||||
frame: LongInt;
|
||||
IsCur: Boolean;
|
||||
addr: TDBGPtr;
|
||||
begin
|
||||
It := TMapIterator.Create(Instr.Callstack.RawEntries);
|
||||
|
||||
for i := 0 to Length(Instr.Res) - 1 do begin
|
||||
s := Instr.Res[i];
|
||||
if (Length(s) > 3) and (s[3] = '*') then s[3] := ' ';
|
||||
if StrMatches(s, [' frame #'{id}, ': '{addr}, ' '{exe}, '`'{func}, '',' at '{file}, ':'{line}, ''], found) then begin
|
||||
frame := StrToIntDef(found[0], -1);
|
||||
if It.Locate(frame) then begin
|
||||
Arguments := TStringList.Create;
|
||||
if StrMatches(found[3], ['', '(', '',')'], foundArg) then begin
|
||||
Arguments.CommaText := foundArg[1];
|
||||
found[3] := foundArg[0];
|
||||
end;
|
||||
|
||||
e := TCallStackEntry(It.DataPtr^);
|
||||
e.Init(StrToInt64Def(found[1],0), Arguments, found[3], found[4], '', StrToIntDef(found[5], -1));
|
||||
Arguments.Free;
|
||||
end;
|
||||
ParseFrameLocation(s, FId, IsCur, addr, func, Arguments, filename, line, d);
|
||||
if It.Locate(FId) then begin
|
||||
e := TCallStackEntry(It.DataPtr^);
|
||||
e.Init(addr, Arguments, func, filename, '', line);
|
||||
end;
|
||||
end;
|
||||
It.Free;
|
||||
|
||||
|
||||
{
|
||||
<< << TCmdLineDebugger.ReadLn " * frame #0: 0x00429258 project1.exe`
|
||||
FORMCREATE(this=0x04a91060, SENDER=0x04a91060) at unit1.pas:39"
|
||||
<< << TCmdLineDebugger.ReadLn " frame #1: 0x0041ab6f project1.exe`DOCREATE(this=0x04a91060) at customform.inc:939"
|
||||
<< << TCmdLineDebugger.ReadLn " frame #2: 0x00418bd8 project1.exe`AFTERCONSTRUCTION(this=0x04a91060) at customform.inc:149"
|
||||
<< << TCmdLineDebugger.ReadLn " frame #3: 0x0042023a project1.exe`CREATE(this=0x000000c7, vmt=0x04a91060, THEOWNER=0x04a91060) at customform.inc:3184"
|
||||
<< << TCmdLineDebugger.ReadLn " frame #4: 0x0042746e project1.exe`CREATEFORM(this=0x000000c7, INSTANCECLASS=0x04a91060, REFERENCE=<unavailable>) at application.inc:2241"
|
||||
<< << TCmdLineDebugger.ReadLn " frame #5: 0x00402a42 project1.exe`main at project1.lpr:19"
|
||||
procedure Init(const AnAddress: TDbgPtr;
|
||||
const AnArguments: TStrings; const AFunctionName: String;
|
||||
const {%H-}FileName, {%H-}FullName: String;
|
||||
const ALine: Integer; AState: TDebuggerDataState = ddsValid); virtual;
|
||||
}
|
||||
|
||||
inherited RequestEntries(Instr.Callstack);
|
||||
end;
|
||||
|
||||
@ -472,7 +400,7 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
|
||||
ACallstack.Count := ARequiredMinCount + 1;
|
||||
ACallstack.Count := ARequiredMinCount + 1; // TODO: get data, and return correct result
|
||||
ACallstack.SetCountValidity(ddsValid);
|
||||
end;
|
||||
|
||||
@ -485,7 +413,7 @@ begin
|
||||
exit;
|
||||
end;
|
||||
|
||||
tid := 0; // Debugger.Threads.CurrentThreads.CurrentThreadId; // FCurrentThreadId ?
|
||||
tid := Debugger.Threads.CurrentThreads.CurrentThreadId; // FCurrentThreadId ?
|
||||
cs := TCallStackBase(CurrentCallStackList.EntriesForThreads[tid]);
|
||||
idx := cs.NewCurrentIndex; // NEW-CURRENT
|
||||
|
||||
@ -756,6 +684,10 @@ begin
|
||||
Instr.ReleaseReference;
|
||||
|
||||
Instr := TLldbInstructionSettingSet.Create('stop-line-count-before', '0');
|
||||
QueueInstruction(Instr);
|
||||
Instr.ReleaseReference;
|
||||
|
||||
Instr := TLldbInstructionSettingSet.Create('stop-disassembly-count', '0');
|
||||
Instr.OnFinish := @InstructionSucceeded;
|
||||
QueueInstruction(Instr);
|
||||
Instr.ReleaseReference;
|
||||
@ -899,6 +831,11 @@ procedure TLldbDebugger.DoAfterLineReceived(var ALine: String);
|
||||
var
|
||||
Instr: TLldbInstructionTargetDelete;
|
||||
found: TStringArray;
|
||||
AnId, SrcLine: Integer;
|
||||
AnIsCurrent: Boolean;
|
||||
AnAddr: TDBGPtr;
|
||||
AFuncName, AFile, AReminder: String;
|
||||
AnArgs: TStringList;
|
||||
begin
|
||||
if ALine = '' then
|
||||
exit;
|
||||
@ -907,6 +844,7 @@ begin
|
||||
FCurrentThreadId := StrToIntDef(found[0], 0);
|
||||
FCurrentStackFrame := 0;
|
||||
FDebugInstructionQueue.SetKnownThreadAndFrame(FCurrentThreadId, 0);
|
||||
Threads.CurrentThreads.CurrentThreadId := FCurrentThreadId;
|
||||
SetState(dsPause);
|
||||
ALine := '';
|
||||
end;
|
||||
@ -923,11 +861,13 @@ begin
|
||||
Instr.ReleaseReference;
|
||||
end;
|
||||
|
||||
if StrMatches(ALine, [' frame #0: ' {addr}, ' ' {}, '`' {fname}, '(', '',' at ', ':', ''], found) then begin
|
||||
FCurrentLocation.Address := StrToInt64Def(found[0], 0);
|
||||
FCurrentLocation.FuncName := found[2];
|
||||
FCurrentLocation.SrcFile := found[4];
|
||||
FCurrentLocation.SrcLine := StrToIntDef(found[5], -1);
|
||||
if ParseFrameLocation(ALine, AnId, AnIsCurrent, AnAddr, AFuncName, AnArgs,
|
||||
AFile, SrcLine, AReminder)
|
||||
then begin
|
||||
FCurrentLocation.Address := AnAddr;
|
||||
FCurrentLocation.FuncName := AFuncName;
|
||||
FCurrentLocation.SrcFile := AFile;
|
||||
FCurrentLocation.SrcLine := SrcLine;
|
||||
DoCurrent(FCurrentLocation);
|
||||
ALine := '';
|
||||
end;
|
||||
|
||||
@ -5,7 +5,7 @@ unit LldbHelper;
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, strutils;
|
||||
Classes, SysUtils, math, DbgIntfBaseTypes, strutils;
|
||||
|
||||
function LastPos(ASearch, AString: string): Integer;
|
||||
|
||||
@ -14,6 +14,15 @@ function StrContains(AString, AFind: string): Boolean;
|
||||
function StrMatches(AString: string; AFind: array of string): Boolean;
|
||||
function StrMatches(AString: string; AFind: array of string; out AGapsContent: TStringArray): Boolean;
|
||||
|
||||
function ParseThreadLocation(AnInput: String; out AnId: Integer;
|
||||
out AnIsCurrent: Boolean; out AName: String; out AnAddr: TDBGPtr;
|
||||
out AFuncName: String; out AnArgs: TStringList; out AFile: String;
|
||||
out ALine: Integer; out AReminder: String): Boolean;
|
||||
function ParseFrameLocation(AnInput: String; out AnId: Integer;
|
||||
out AnIsCurrent: Boolean; out AnAddr: TDBGPtr; out AFuncName: String;
|
||||
out AnArgs: TStringList; out AFile: String; out ALine: Integer;
|
||||
out AReminder: String): Boolean;
|
||||
|
||||
implementation
|
||||
|
||||
function LastPos(ASearch, AString: string): Integer;
|
||||
@ -95,5 +104,129 @@ begin
|
||||
SetLength(AGapsContent, ResIdx);
|
||||
end;
|
||||
|
||||
(* Examples
|
||||
"* thread #1: tid = 0x1a1c, 0x0042951d project1.exe`FORMCREATE(this=0x00151060, SENDER=0x00151060) at unit1.pas:59, stop reason = breakpoint 2.1"
|
||||
" thread #2: tid = 0x16ac, 0x7700eb6c ntdll.dll`NtDelayExecution + 12"
|
||||
|
||||
" * frame #0: 0x7700eb6c ntdll.dll`NtDelayExecution + 12"
|
||||
" frame #1: 0x767a5f5b KernelBase.dll`SleepEx + 155"
|
||||
" frame #3: 0x004521c9 project1.exe"
|
||||
" frame #7: 0x761a8654 kernel32.dll`BaseThreadInitThunk + 36"
|
||||
" frame #2: 0x048fa158"
|
||||
" frame #1: 0x0041ab6f project1.exe`DOCREATE(this=0x04a91060) at customform.inc:939"
|
||||
" frame #5: 0x00402a42 project1.exe`main at project1.lpr:19"
|
||||
*)
|
||||
|
||||
procedure ParseLocation(AnInput: String; out AnAddr: TDBGPtr; out AFuncName: String;
|
||||
out AnArgs: TStringList; out AFile: String; out ALine: Integer; out AReminder: String);
|
||||
var
|
||||
found: TStringArray;
|
||||
i, j, k: Integer;
|
||||
begin
|
||||
if pos(' ', AnInput) = 0 then begin
|
||||
AnAddr := StrToInt64Def(AnInput, 0);
|
||||
AnInput := '';
|
||||
end
|
||||
else
|
||||
if StrMatches(AnInput, [''{addr}, ' '{remainder}, ''], found) then begin
|
||||
AnAddr := StrToInt64Def(found[0], 0);
|
||||
AnInput := found[1];
|
||||
end
|
||||
else
|
||||
AnAddr := 0;
|
||||
|
||||
AnArgs := nil;
|
||||
AFile := '';
|
||||
ALine := 0;
|
||||
AReminder := '';
|
||||
if StrMatches(AnInput, [''{exe}, '`'{remainder}, ''], found) then begin
|
||||
AnInput := found[1];
|
||||
i := pos(' ', AnInput);
|
||||
j := pos('(', AnInput);
|
||||
k := pos(')', AnInput);
|
||||
if ((i = 0) or (i > j)) and (j > 1) and (k > j) then begin
|
||||
AFuncName := Copy(AnInput, 1, j-1);
|
||||
AnArgs := TStringList.Create;
|
||||
AnArgs.CommaText := copy(AnInput, j+1, k-j-1);
|
||||
AnInput := Copy(AnInput, k+1, Length(AnInput));
|
||||
end
|
||||
else begin
|
||||
i := Max(i, pos(', ', AnInput));
|
||||
if i = 0 then i := Length(AnInput) + 1;
|
||||
AFuncName := Copy(AnInput, 1, i-1);
|
||||
AnInput := Copy(AnInput, i, Length(AnInput));
|
||||
end;
|
||||
|
||||
if StrMatches(AnInput, [' at ', ':', ''], found) then begin
|
||||
AFile := found[0];
|
||||
i := pos(', ', found[1]);
|
||||
if i = 0 then i := Length(found[1]) + 1;
|
||||
ALine := StrToIntDef(copy(found[1], 1, i-1), 0);
|
||||
AReminder := copy(found[1], i, Length(found[1]));
|
||||
end
|
||||
else
|
||||
AReminder := AnInput;
|
||||
end
|
||||
else begin
|
||||
AFuncName := AnInput;
|
||||
end;
|
||||
end;
|
||||
|
||||
function ParseThreadLocation(AnInput: String; out AnId: Integer; out
|
||||
AnIsCurrent: Boolean; out AName: String; out AnAddr: TDBGPtr; out
|
||||
AFuncName: String; out AnArgs: TStringList; out AFile: String; out
|
||||
ALine: Integer; out AReminder: String): Boolean;
|
||||
var
|
||||
found: TStringArray;
|
||||
begin
|
||||
Result := False;
|
||||
AnIsCurrent := (Length(AnInput) > 1) and (AnInput[1] = '*');
|
||||
if AnIsCurrent then AnInput[1] := ' ';
|
||||
|
||||
if not StrMatches(AnInput, [' thread #'{id}, ': '{}, ''], found) then begin
|
||||
AnId := -1;
|
||||
AName := '';
|
||||
ParseLocation('', AnAddr, AFuncName, AnArgs, AFile, ALine, AReminder);
|
||||
exit;
|
||||
end;
|
||||
|
||||
AnId := StrToIntDef(found[0], -1);
|
||||
AnInput := found[1];
|
||||
Result := True;
|
||||
|
||||
if StrMatches(AnInput, ['tid = '{tid}, ', '{}, ''], found) then begin
|
||||
AName := found[0];
|
||||
AnInput := found[1];
|
||||
end
|
||||
else
|
||||
AName := '';
|
||||
|
||||
ParseLocation(AnInput, AnAddr, AFuncName, AnArgs, AFile, ALine, AReminder);
|
||||
end;
|
||||
|
||||
function ParseFrameLocation(AnInput: String; out AnId: Integer; out
|
||||
AnIsCurrent: Boolean; out AnAddr: TDBGPtr; out AFuncName: String; out
|
||||
AnArgs: TStringList; out AFile: String; out ALine: Integer; out
|
||||
AReminder: String): Boolean;
|
||||
var
|
||||
found: TStringArray;
|
||||
begin
|
||||
Result := False;
|
||||
AnIsCurrent := (Length(AnInput) > 3) and (AnInput[3] = '*');
|
||||
if AnIsCurrent then AnInput[3] := ' ';
|
||||
|
||||
if not StrMatches(AnInput, [' frame #'{id}, ': '{}, ''], found) then begin
|
||||
AnId := -1;
|
||||
ParseLocation('', AnAddr, AFuncName, AnArgs, AFile, ALine, AReminder);
|
||||
exit;
|
||||
end;
|
||||
|
||||
AnId := StrToIntDef(found[0], -1);
|
||||
AnInput := found[1];
|
||||
Result := True;
|
||||
|
||||
ParseLocation(AnInput, AnAddr, AFuncName, AnArgs, AFile, ALine, AReminder);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ type
|
||||
procedure DoBeforeHandleLineReceived(var ALine: String); override;
|
||||
|
||||
function GetSelectFrameInstruction(AFrame: Integer): TDBGInstruction; override;
|
||||
//function GetSelectThreadInstruction(AThreadId: Integer): TDBGInstruction; override;
|
||||
function GetSelectThreadInstruction(AThreadId: Integer): TDBGInstruction; override;
|
||||
public
|
||||
procedure CancelAllForCommand(ACommand: TObject); // Does NOT include the current or running instruction
|
||||
end;
|
||||
@ -125,6 +125,17 @@ type
|
||||
constructor Create(AnId: Integer);
|
||||
end;
|
||||
|
||||
{ TLldbInstructionThreadSelect }
|
||||
|
||||
TLldbInstructionThreadSelect = class(TLldbInstruction)
|
||||
private
|
||||
FIndex: Integer;
|
||||
protected
|
||||
function ProcessInputFromDbg(const AData: String): Boolean; override;
|
||||
public
|
||||
constructor Create(AnIndex: Integer);
|
||||
end;
|
||||
|
||||
{ TLldbInstructionFrameSelect }
|
||||
|
||||
TLldbInstructionFrameSelect = class(TLldbInstruction)
|
||||
@ -238,6 +249,12 @@ begin
|
||||
Result := TLldbInstructionFrameSelect.Create(AFrame);
|
||||
end;
|
||||
|
||||
function TLldbInstructionQueue.GetSelectThreadInstruction(AThreadId: Integer
|
||||
): TDBGInstruction;
|
||||
begin
|
||||
Result := TLldbInstructionThreadSelect.Create(AThreadId);
|
||||
end;
|
||||
|
||||
procedure TLldbInstructionQueue.CancelAllForCommand(ACommand: TObject);
|
||||
var
|
||||
Instr, NextInstr: TLldbInstruction;
|
||||
@ -469,6 +486,26 @@ begin
|
||||
inherited Create(Format('breakpoint delete %d', [AnId]));
|
||||
end;
|
||||
|
||||
{ TLldbInstructionThreadSelect }
|
||||
|
||||
function TLldbInstructionThreadSelect.ProcessInputFromDbg(const AData: String
|
||||
): Boolean;
|
||||
begin
|
||||
Result := inherited ProcessInputFromDbg(AData);
|
||||
|
||||
if not Result then begin // if Result=true then self is destroyed;
|
||||
Queue.SetKnownThread(FIndex);
|
||||
MarkAsSuccess;
|
||||
end;
|
||||
Result := true;
|
||||
end;
|
||||
|
||||
constructor TLldbInstructionThreadSelect.Create(AnIndex: Integer);
|
||||
begin
|
||||
FIndex := AnIndex;
|
||||
inherited Create(Format('thread select %d', [AnIndex]));
|
||||
end;
|
||||
|
||||
{ TLldbInstructionFrameSelect }
|
||||
|
||||
function TLldbInstructionFrameSelect.ProcessInputFromDbg(const AData: String
|
||||
@ -481,6 +518,13 @@ begin
|
||||
MarkAsSuccess;
|
||||
end;
|
||||
Result := true;
|
||||
(* TODO: ?
|
||||
ReadLn "* thread #3"
|
||||
ReadLn " frame #0: 0x7700eb6c ntdll.dll`NtDelayExecution + 12"
|
||||
|
||||
This falls through to TLldbDebugger.DoAfterLineReceived
|
||||
and sets the current location in the editor.
|
||||
*)
|
||||
end;
|
||||
|
||||
constructor TLldbInstructionFrameSelect.Create(AnIndex: Integer);
|
||||
@ -780,7 +824,7 @@ constructor TLldbInstructionStackTrace.Create(FrameCount: Integer;
|
||||
ACallstack: TCallStackBase);
|
||||
begin
|
||||
FCallstack := ACallstack;
|
||||
inherited Create(Format('bt %d', [FrameCount]));
|
||||
inherited Create(Format('bt %d', [FrameCount]), ACallstack.ThreadId);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user