Merge pull request #196 from puremourning/auto-port
Automatically pick a local port for CodeLLDB
This commit is contained in:
commit
915fb29a9f
6 changed files with 175 additions and 40 deletions
48
README.md
48
README.md
|
|
@ -33,6 +33,8 @@ For a tutorial and usage overview, take a look at the
|
||||||
* [Launch and attach by PID:](#launch-and-attach-by-pid)
|
* [Launch and attach by PID:](#launch-and-attach-by-pid)
|
||||||
* [Launch with options](#launch-with-options)
|
* [Launch with options](#launch-with-options)
|
||||||
* [Breakpoints](#breakpoints)
|
* [Breakpoints](#breakpoints)
|
||||||
|
* [Exception breakpoints](#exception-breakpoints)
|
||||||
|
* [Clear breakpoints](#clear-breakpoints)
|
||||||
* [Stepping](#stepping)
|
* [Stepping](#stepping)
|
||||||
* [Variables and scopes](#variables-and-scopes)
|
* [Variables and scopes](#variables-and-scopes)
|
||||||
* [Watches](#watches)
|
* [Watches](#watches)
|
||||||
|
|
@ -66,7 +68,7 @@ For a tutorial and usage overview, take a look at the
|
||||||
* [Motivation](#motivation)
|
* [Motivation](#motivation)
|
||||||
* [License](#license)
|
* [License](#license)
|
||||||
|
|
||||||
<!-- Added by: ben, at: Mon 6 Jul 2020 11:30:01 BST -->
|
<!-- Added by: ben, at: Thu 9 Jul 2020 18:19:20 BST -->
|
||||||
|
|
||||||
<!--te-->
|
<!--te-->
|
||||||
|
|
||||||
|
|
@ -307,6 +309,30 @@ should be):
|
||||||
./install_gadget.py --all --disable-tcl
|
./install_gadget.py --all --disable-tcl
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to just add a new adapter without destroying the exisitng ones, add
|
||||||
|
`--update-gadget-config`, as in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./install_gadget.py --enable-tcl
|
||||||
|
$ ./install_gadget.py --enable-rust --update-gadget-config
|
||||||
|
$ ./install_gadget.py --enable-java --update-gadget-config
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to maintain `configurations` outside of the vimspector repository
|
||||||
|
(this can be useful if you have custom gadgets or global configurations),
|
||||||
|
you can tell the installer to use a different basedir, then set
|
||||||
|
`g:vimspector_base_dir` to point to that directory, for example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./install_gadget.py --basedir $HOME/.vim/vimspector-config --all --force-all
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add this to your `.vimrc`:
|
||||||
|
|
||||||
|
```viml
|
||||||
|
let g:vimspector_base_dir=expand( '$HOME/.vim/vimspector-config' )
|
||||||
|
```
|
||||||
|
|
||||||
See `--help` for more info.
|
See `--help` for more info.
|
||||||
|
|
||||||
## Manual gadget installation
|
## Manual gadget installation
|
||||||
|
|
@ -571,7 +597,7 @@ This would start the `Run Test` configuration with `${Test}` set to `'Name of
|
||||||
the test'` and Vimspector would _not_ prompt the user to enter or confirm these
|
the test'` and Vimspector would _not_ prompt the user to enter or confirm these
|
||||||
things.
|
things.
|
||||||
|
|
||||||
See [this issue](https://github.com/puremourning/vimspector/issues/97) for
|
See [our YouCompleteMe integration guide](#usage-with-youcompleteme) for
|
||||||
another example where it can be used to specify the port to connect the [java
|
another example where it can be used to specify the port to connect the [java
|
||||||
debugger](#java---partially-supported)
|
debugger](#java---partially-supported)
|
||||||
|
|
||||||
|
|
@ -599,6 +625,24 @@ whatever dialect the debugger understands when evaluating expressions.
|
||||||
When using the `<leader><F9>` mapping, the user is prompted to enter these
|
When using the `<leader><F9>` mapping, the user is prompted to enter these
|
||||||
expressions in a command line (with history).
|
expressions in a command line (with history).
|
||||||
|
|
||||||
|
### Exception breakpoints
|
||||||
|
|
||||||
|
When starting debugging, you may be asekd a few questions about how to handle
|
||||||
|
exceptoins. These are "exception breakpoints" and vimspector remembers your
|
||||||
|
choices while Vim is still running.
|
||||||
|
|
||||||
|
Typically you can accept the defaults (just keep pressing `<CR>`!) as most debug
|
||||||
|
adapter defaults are sane, but if you want to break on, say `uncaught exception`
|
||||||
|
then answer `Y` to that (for example).
|
||||||
|
|
||||||
|
You can configure your choices in the `.vimspector.json`. See
|
||||||
|
[the configuration guide][vimspector-ref-exception] for details on that.
|
||||||
|
|
||||||
|
### Clear breakpoints
|
||||||
|
|
||||||
|
* Use `vimspector#ClearBreakpoints()`
|
||||||
|
to clear all breakpoints including the memory of exception breakpoint choices.
|
||||||
|
|
||||||
## Stepping
|
## Stepping
|
||||||
|
|
||||||
* Step in/out, finish, continue, pause etc. using the WinBar, or mappings.
|
* Step in/out, finish, continue, pause etc. using the WinBar, or mappings.
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,21 @@ for Vimspector.
|
||||||
* [Debug adapter configuration](#debug-adapter-configuration)
|
* [Debug adapter configuration](#debug-adapter-configuration)
|
||||||
* [Debug profile configuration](#debug-profile-configuration)
|
* [Debug profile configuration](#debug-profile-configuration)
|
||||||
* [Replacements and variables](#replacements-and-variables)
|
* [Replacements and variables](#replacements-and-variables)
|
||||||
|
* [The splat operator](#the-splat-operator)
|
||||||
* [Configuration Format](#configuration-format)
|
* [Configuration Format](#configuration-format)
|
||||||
* [Files and locations](#files-and-locations)
|
* [Files and locations](#files-and-locations)
|
||||||
* [Adapter configurations](#adapter-configurations)
|
* [Adapter configurations](#adapter-configurations)
|
||||||
* [Debug configurations](#debug-configurations)
|
* [Debug configurations](#debug-configurations)
|
||||||
* [Exception breakpionts](#exception-breakpionts)
|
* [Exception breakpionts](#exception-breakpionts)
|
||||||
* [Predefined Variables](#predefined-variables)
|
* [Predefined Variables](#predefined-variables)
|
||||||
|
* [Remote Debugging Support](#remote-debugging-support)
|
||||||
|
* [Python (debugpy) Example](#python-debugpy-example)
|
||||||
|
* [C-family (gdbserver) Example](#c-family-gdbserver-example)
|
||||||
|
* [Docker Example](#docker-example)
|
||||||
|
* [Appendix: Configuration file format](#appendix-configuration-file-format)
|
||||||
* [Appendix: Editor configuration](#appendix-editor-configuration)
|
* [Appendix: Editor configuration](#appendix-editor-configuration)
|
||||||
|
|
||||||
<!-- Added by: ben, at: Tue 28 Jan 2020 08:47:40 GMT -->
|
<!-- Added by: ben, at: Thu 9 Jul 2020 18:19:39 BST -->
|
||||||
|
|
||||||
<!--te-->
|
<!--te-->
|
||||||
|
|
||||||
|
|
@ -330,7 +336,7 @@ the configured response is empty string, the debug adapter default will be used.
|
||||||
Referring to the above example, the following tells the debug adapter to use the
|
Referring to the above example, the following tells the debug adapter to use the
|
||||||
default value for `caught` exceptoins and to break on `uncaught` exception:
|
default value for `caught` exceptoins and to break on `uncaught` exception:
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"example-debug-configuration": {
|
"example-debug-configuration": {
|
||||||
|
|
@ -344,6 +350,52 @@ default value for `caught` exceptoins and to break on `uncaught` exception:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The keys in the `exception` mapping are what Vimspector includes in the prompt.
|
||||||
|
For example, when prompted with the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
cpp_throw: Break on C++: on throw (Y/N/default: Y)?
|
||||||
|
```
|
||||||
|
|
||||||
|
The exception breakpoint "type" is `cpp_throw` and the default is `Y`.
|
||||||
|
|
||||||
|
Similarly:
|
||||||
|
|
||||||
|
```
|
||||||
|
cpp_catch: Break on C++: on catch (Y/N/default: N)?
|
||||||
|
```
|
||||||
|
|
||||||
|
The exception breakpoint "type" is `cpp_catch` and the default is `N`.
|
||||||
|
|
||||||
|
Use the following to set the values in config and not get asked:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"configurations": {
|
||||||
|
"example-debug-configuration": {
|
||||||
|
"adapter": "example-adapter-name",
|
||||||
|
"breakpoints": {
|
||||||
|
"exception": {
|
||||||
|
"cpp_throw": "Y",
|
||||||
|
"cpp_catch": "Y"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
To just accept the defaults for these exception breakpoint types, don't specify
|
||||||
|
a value, as in :
|
||||||
|
|
||||||
|
```json
|
||||||
|
"configurations": {
|
||||||
|
"example-debug-configuration": {
|
||||||
|
"adapter": "example-adapter-name",
|
||||||
|
"breakpoints": {
|
||||||
|
"exception": {
|
||||||
|
"cpp_throw": "",
|
||||||
|
"cpp_catch": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
## Predefined Variables
|
## Predefined Variables
|
||||||
|
|
||||||
The following variables are provided:
|
The following variables are provided:
|
||||||
|
|
@ -364,6 +416,7 @@ The following variables are provided:
|
||||||
* `${fileDirname}` - the current opened file's dirname
|
* `${fileDirname}` - the current opened file's dirname
|
||||||
* `${fileExtname}` - the current opened file's extension
|
* `${fileExtname}` - the current opened file's extension
|
||||||
* `${cwd}` - the current working directory of the active window on launch
|
* `${cwd}` - the current working directory of the active window on launch
|
||||||
|
* `${unusedLocalPort}` - an unused local TCP port
|
||||||
|
|
||||||
## Remote Debugging Support
|
## Remote Debugging Support
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -478,9 +478,9 @@ GADGETS = {
|
||||||
'type': 'CodeLLDB',
|
'type': 'CodeLLDB',
|
||||||
"command": [
|
"command": [
|
||||||
"${gadgetDir}/CodeLLDB/adapter/codelldb",
|
"${gadgetDir}/CodeLLDB/adapter/codelldb",
|
||||||
"--port", "${port}"
|
"--port", "${unusedLocalPort}"
|
||||||
],
|
],
|
||||||
"port": "${port}",
|
"port": "${unusedLocalPort}",
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"name": "lldb",
|
"name": "lldb",
|
||||||
|
|
|
||||||
|
|
@ -177,43 +177,52 @@ class DebugSession( object ):
|
||||||
return [ '', '' ]
|
return [ '', '' ]
|
||||||
return os.path.splitext( p )
|
return os.path.splitext( p )
|
||||||
|
|
||||||
self._variables = {
|
variables = {
|
||||||
'dollar': '$', # HACK. Hote '$$' also works.
|
'dollar': '$', # HACK. Hote '$$' also works.
|
||||||
'workspaceRoot': self._workspace_root,
|
'workspaceRoot': self._workspace_root,
|
||||||
'workspaceFolder': self._workspace_root,
|
'workspaceFolder': self._workspace_root,
|
||||||
'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME, install.GetOS() ),
|
'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME, install.GetOS() ),
|
||||||
'file': current_file,
|
'file': current_file,
|
||||||
'relativeFile': relpath( current_file, self._workspace_root ),
|
}
|
||||||
'fileBasename': os.path.basename( current_file ),
|
|
||||||
|
calculus = {
|
||||||
|
'relativeFile': lambda: relpath( current_file,
|
||||||
|
self._workspace_root ),
|
||||||
|
'fileBasename': lambda: os.path.basename( current_file ),
|
||||||
'fileBasenameNoExtension':
|
'fileBasenameNoExtension':
|
||||||
splitext( os.path.basename( current_file ) )[ 0 ],
|
lambda: splitext( os.path.basename( current_file ) )[ 0 ],
|
||||||
'fileDirname': os.path.dirname( current_file ),
|
'fileDirname': lambda: os.path.dirname( current_file ),
|
||||||
'fileExtname': splitext( os.path.basename( current_file ) )[ 1 ],
|
'fileExtname': lambda: splitext( os.path.basename( current_file ) )[ 1 ],
|
||||||
# NOTE: this is the window-local cwd for the current window, *not* Vim's
|
# NOTE: this is the window-local cwd for the current window, *not* Vim's
|
||||||
# working directory.
|
# working directory.
|
||||||
'cwd': os.getcwd(),
|
'cwd': os.getcwd,
|
||||||
|
'unusedLocalPort': utils.GetUnusedLocalPort,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pretend that vars passed to the launch command were typed in by the user
|
# Pretend that vars passed to the launch command were typed in by the user
|
||||||
# (they may have been in theory)
|
# (they may have been in theory)
|
||||||
USER_CHOICES.update( launch_variables )
|
USER_CHOICES.update( launch_variables )
|
||||||
self._variables.update( launch_variables )
|
variables.update( launch_variables )
|
||||||
|
|
||||||
self._variables.update(
|
variables.update(
|
||||||
utils.ParseVariables( adapter.get( 'variables', {} ),
|
utils.ParseVariables( adapter.get( 'variables', {} ),
|
||||||
self._variables,
|
variables,
|
||||||
|
calculus,
|
||||||
USER_CHOICES ) )
|
USER_CHOICES ) )
|
||||||
self._variables.update(
|
variables.update(
|
||||||
utils.ParseVariables( configuration.get( 'variables', {} ),
|
utils.ParseVariables( configuration.get( 'variables', {} ),
|
||||||
self._variables,
|
variables,
|
||||||
|
calculus,
|
||||||
USER_CHOICES ) )
|
USER_CHOICES ) )
|
||||||
|
|
||||||
|
|
||||||
utils.ExpandReferencesInDict( configuration,
|
utils.ExpandReferencesInDict( configuration,
|
||||||
self._variables,
|
variables,
|
||||||
|
calculus,
|
||||||
USER_CHOICES )
|
USER_CHOICES )
|
||||||
utils.ExpandReferencesInDict( adapter,
|
utils.ExpandReferencesInDict( adapter,
|
||||||
self._variables,
|
variables,
|
||||||
|
calculus,
|
||||||
USER_CHOICES )
|
USER_CHOICES )
|
||||||
|
|
||||||
if not adapter:
|
if not adapter:
|
||||||
|
|
|
||||||
|
|
@ -345,9 +345,9 @@ def IsCurrent( window, buf ):
|
||||||
return vim.current.window == window and vim.current.window.buffer == buf
|
return vim.current.window == window and vim.current.window.buffer == buf
|
||||||
|
|
||||||
|
|
||||||
def ExpandReferencesInObject( obj, mapping, user_choices ):
|
def ExpandReferencesInObject( obj, mapping, calculus, user_choices ):
|
||||||
if isinstance( obj, dict ):
|
if isinstance( obj, dict ):
|
||||||
ExpandReferencesInDict( obj, mapping, user_choices )
|
ExpandReferencesInDict( obj, mapping, calculus, user_choices )
|
||||||
elif isinstance( obj, list ):
|
elif isinstance( obj, list ):
|
||||||
j_offset = 0
|
j_offset = 0
|
||||||
obj_copy = list( obj )
|
obj_copy = list( obj )
|
||||||
|
|
@ -360,6 +360,7 @@ def ExpandReferencesInObject( obj, mapping, user_choices ):
|
||||||
# *${something} - expand list in place
|
# *${something} - expand list in place
|
||||||
value = ExpandReferencesInString( obj_copy[ i ][ 1: ],
|
value = ExpandReferencesInString( obj_copy[ i ][ 1: ],
|
||||||
mapping,
|
mapping,
|
||||||
|
calculus,
|
||||||
user_choices )
|
user_choices )
|
||||||
obj.pop( j )
|
obj.pop( j )
|
||||||
j_offset -= 1
|
j_offset -= 1
|
||||||
|
|
@ -369,14 +370,18 @@ def ExpandReferencesInObject( obj, mapping, user_choices ):
|
||||||
else:
|
else:
|
||||||
obj[ j ] = ExpandReferencesInObject( obj_copy[ i ],
|
obj[ j ] = ExpandReferencesInObject( obj_copy[ i ],
|
||||||
mapping,
|
mapping,
|
||||||
|
calculus,
|
||||||
user_choices )
|
user_choices )
|
||||||
elif isinstance( obj, str ):
|
elif isinstance( obj, str ):
|
||||||
obj = ExpandReferencesInString( obj, mapping, user_choices )
|
obj = ExpandReferencesInString( obj, mapping, calculus, user_choices )
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def ExpandReferencesInString( orig_s, mapping, user_choices ):
|
def ExpandReferencesInString( orig_s,
|
||||||
|
mapping,
|
||||||
|
calculus,
|
||||||
|
user_choices ):
|
||||||
s = os.path.expanduser( orig_s )
|
s = os.path.expanduser( orig_s )
|
||||||
s = os.path.expandvars( s )
|
s = os.path.expandvars( s )
|
||||||
|
|
||||||
|
|
@ -393,15 +398,19 @@ def ExpandReferencesInString( orig_s, mapping, user_choices ):
|
||||||
# HACK: This is seemingly the only way to get the key. str( e ) returns
|
# HACK: This is seemingly the only way to get the key. str( e ) returns
|
||||||
# the key surrounded by '' for unknowable reasons.
|
# the key surrounded by '' for unknowable reasons.
|
||||||
key = e.args[ 0 ]
|
key = e.args[ 0 ]
|
||||||
default_value = user_choices.get( key, None )
|
|
||||||
mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ),
|
if key in calculus:
|
||||||
default_value )
|
mapping[ key ] = calculus[ key ]()
|
||||||
user_choices[ key ] = mapping[ key ]
|
else:
|
||||||
_logger.debug( "Value for %s not set in %s (from %s): set to %s",
|
default_value = user_choices.get( key, None )
|
||||||
key,
|
mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ),
|
||||||
s,
|
default_value )
|
||||||
orig_s,
|
user_choices[ key ] = mapping[ key ]
|
||||||
mapping[ key ] )
|
_logger.debug( "Value for %s not set in %s (from %s): set to %s",
|
||||||
|
key,
|
||||||
|
s,
|
||||||
|
orig_s,
|
||||||
|
mapping[ key ] )
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
UserMessage( 'Invalid $ in string {}: {}'.format( s, e ),
|
UserMessage( 'Invalid $ in string {}: {}'.format( s, e ),
|
||||||
persist = True )
|
persist = True )
|
||||||
|
|
@ -412,12 +421,18 @@ def ExpandReferencesInString( orig_s, mapping, user_choices ):
|
||||||
|
|
||||||
# TODO: Should we just run the substitution on the whole JSON string instead?
|
# TODO: Should we just run the substitution on the whole JSON string instead?
|
||||||
# That woul dallow expansion in bool and number values, such as ports etc. ?
|
# That woul dallow expansion in bool and number values, such as ports etc. ?
|
||||||
def ExpandReferencesInDict( obj, mapping, user_choices ):
|
def ExpandReferencesInDict( obj, mapping, calculus, user_choices ):
|
||||||
for k in obj.keys():
|
for k in obj.keys():
|
||||||
obj[ k ] = ExpandReferencesInObject( obj[ k ], mapping, user_choices )
|
obj[ k ] = ExpandReferencesInObject( obj[ k ],
|
||||||
|
mapping,
|
||||||
|
calculus,
|
||||||
|
user_choices )
|
||||||
|
|
||||||
|
|
||||||
def ParseVariables( variables_list, mapping, user_choices ):
|
def ParseVariables( variables_list,
|
||||||
|
mapping,
|
||||||
|
calculus,
|
||||||
|
user_choices ):
|
||||||
new_variables = {}
|
new_variables = {}
|
||||||
new_mapping = mapping.copy()
|
new_mapping = mapping.copy()
|
||||||
|
|
||||||
|
|
@ -431,7 +446,10 @@ def ParseVariables( variables_list, mapping, user_choices ):
|
||||||
if 'shell' in v:
|
if 'shell' in v:
|
||||||
new_v = v.copy()
|
new_v = v.copy()
|
||||||
# Bit of a hack. Allows environment variables to be used.
|
# Bit of a hack. Allows environment variables to be used.
|
||||||
ExpandReferencesInDict( new_v, new_mapping, user_choices )
|
ExpandReferencesInDict( new_v,
|
||||||
|
new_mapping,
|
||||||
|
calculus,
|
||||||
|
user_choices )
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.update( new_v.get( 'env' ) or {} )
|
env.update( new_v.get( 'env' ) or {} )
|
||||||
|
|
@ -455,6 +473,7 @@ def ParseVariables( variables_list, mapping, user_choices ):
|
||||||
else:
|
else:
|
||||||
new_variables[ n ] = ExpandReferencesInObject( v,
|
new_variables[ n ] = ExpandReferencesInObject( v,
|
||||||
mapping,
|
mapping,
|
||||||
|
calculus,
|
||||||
user_choices )
|
user_choices )
|
||||||
|
|
||||||
return new_variables
|
return new_variables
|
||||||
|
|
@ -575,3 +594,13 @@ def GetVimspectorBase():
|
||||||
return base.decode( 'utf-8' )
|
return base.decode( 'utf-8' )
|
||||||
else:
|
else:
|
||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def GetUnusedLocalPort():
|
||||||
|
import socket
|
||||||
|
sock = socket.socket()
|
||||||
|
# This tells the OS to give us any free port in the range [1024 - 65535]
|
||||||
|
sock.bind( ( '', 0 ) )
|
||||||
|
port = sock.getsockname()[ 1 ]
|
||||||
|
sock.close()
|
||||||
|
return port
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ class VariablesView( object ):
|
||||||
scope ), {
|
scope ), {
|
||||||
'command': 'variables',
|
'command': 'variables',
|
||||||
'arguments': {
|
'arguments': {
|
||||||
'variablesReference': scope.scope[ 'variablesReference' ]
|
'variablesReference': scope.VariablesReference(),
|
||||||
},
|
},
|
||||||
} )
|
} )
|
||||||
|
|
||||||
|
|
@ -295,10 +295,10 @@ class VariablesView( object ):
|
||||||
watch.result.IsExpandedByUser() ):
|
watch.result.IsExpandedByUser() ):
|
||||||
self._connection.DoRequest( partial( self._ConsumeVariables,
|
self._connection.DoRequest( partial( self._ConsumeVariables,
|
||||||
self._watch.draw,
|
self._watch.draw,
|
||||||
watch.result.result ), {
|
watch.result ), {
|
||||||
'command': 'variables',
|
'command': 'variables',
|
||||||
'arguments': {
|
'arguments': {
|
||||||
'variablesReference': watch.result.result[ 'variablesReference' ]
|
'variablesReference': watch.result.VariablesReference(),
|
||||||
},
|
},
|
||||||
} )
|
} )
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue