Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
G
Geth-Modification
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张蕾
Geth-Modification
Commits
9d8b512b
Commit
9d8b512b
authored
Jun 29, 2015
by
Jeffrey Wilcke
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1356 from Gustav-Simonsson/debug_develop
Debug develop
parents
b0a5be44
a7d22658
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
188 additions
and
54 deletions
+188
-54
.travis.yml
.travis.yml
+3
-6
README.md
README.md
+3
-4
monitorcmd.go
cmd/geth/monitorcmd.go
+55
-33
fetcher.go
eth/fetcher/fetcher.go
+17
-5
peer.go
p2p/peer.go
+9
-2
peer_test.go
p2p/peer_test.go
+95
-0
protocol.go
p2p/protocol.go
+6
-4
No files found.
.travis.yml
View file @
9d8b512b
...
...
@@ -2,7 +2,6 @@ language: go
go
:
-
1.4.2
before_install
:
-
sudo add-apt-repository ppa:beineri/opt-qt541 -y
-
sudo apt-get update -qq
-
sudo apt-get install -yqq libgmp3-dev
install
:
...
...
@@ -22,14 +21,12 @@ after_success:
-
if [ "$COVERALLS_TOKEN" ]; then goveralls -coverprofile=profile.cov -service=travis-ci -repotoken $COVERALLS_TOKEN; fi
env
:
global
:
-
PKG_CONFIG_PATH=/opt/qt54/lib/pkgconfig
-
LD_LIBRARY_PATH=/opt/qt54/lib
-
secure
:
"
U2U1AmkU4NJBgKR/uUAebQY87cNL0+1JHjnLOmmXwxYYyj5ralWb1aSuSH3qSXiT93qLBmtaUkuv9fberHVqrbAeVlztVdUsKAq7JMQH+M99iFkC9UiRMqHmtjWJ0ok4COD1sRYixxi21wb/JrMe3M1iL4QJVS61iltjHhVdM64="
notifications
:
webhooks
:
urls
:
-
https://webhooks.gitter.im/e/e09ccdce1048c5e03445
on_success
:
change
# options: [always|never|change] default: always
on_failure
:
always
# options: [always|never|change] default: always
on_start
:
false
# default: false
on_success
:
change
on_failure
:
always
on_start
:
false
README.md
View file @
9d8b512b
...
...
@@ -44,11 +44,10 @@ Executables
Go Ethereum comes with several wrappers/executables found in
[
the `cmd` directory
](
https://github.com/ethereum/go-ethereum/tree/develop/cmd
)
:
*
`mist`
Official Ethereum Browser (ethereum GUI client)
*
`geth`
Ethereum CLI (ethereum command line interface client)
*
`bootnode`
runs a bootstrap node for the Discovery Protocol
*
`ethtest`
test tool which runs with the
[
tests
](
https://github.com/ethereum/test
e
s
)
suite:
`
cat file | ethtest
`
.
*
`ethtest`
test tool which runs with the
[
tests
](
https://github.com/ethereum/tests
)
suite:
`
/path/to/test.json > ethtest --test BlockTests --stdin
`
.
*
`evm`
is a generic Ethereum Virtual Machine:
`evm -code 60ff60ff -gas
10000 -price 0 -dump`
. See
`-h`
for a detailed description.
*
`disasm`
disassembles EVM code:
`echo "6001" | disasm`
...
...
@@ -57,7 +56,7 @@ Go Ethereum comes with several wrappers/executables found in
Command line options
====================
Both
`mist`
and
`geth`
can be configured via command line options, environment variables and config files.
`geth`
can be configured via command line options, environment variables and config files.
To get the options available:
...
...
cmd/geth/monitorcmd.go
View file @
9d8b512b
...
...
@@ -103,33 +103,17 @@ func monitor(ctx *cli.Context) {
footer
.
Height
=
3
charts
:=
make
([]
*
termui
.
LineChart
,
len
(
monitored
))
units
:=
make
([]
int
,
len
(
monitored
))
data
:=
make
([][]
float64
,
len
(
monitored
))
for
i
:=
0
;
i
<
len
(
data
);
i
++
{
data
[
i
]
=
make
([]
float64
,
512
)
}
for
i
,
metric
:=
range
monitored
{
charts
[
i
]
=
termui
.
NewLineChart
()
if
runtime
.
GOOS
==
"windows"
{
charts
[
i
]
.
Mode
=
"dot"
}
charts
[
i
]
.
Data
=
make
([]
float64
,
512
)
charts
[
i
]
.
DataLabels
=
[]
string
{
""
}
charts
[
i
]
.
Height
=
(
termui
.
TermHeight
()
-
footer
.
Height
)
/
rows
charts
[
i
]
.
AxesColor
=
termui
.
ColorWhite
charts
[
i
]
.
PaddingBottom
=
-
2
charts
[
i
]
.
Border
.
Label
=
metric
charts
[
i
]
.
Border
.
LabelFgColor
=
charts
[
i
]
.
Border
.
FgColor
|
termui
.
AttrBold
charts
[
i
]
.
Border
.
FgColor
=
charts
[
i
]
.
Border
.
BgColor
for
i
:=
0
;
i
<
len
(
monitored
);
i
++
{
charts
[
i
]
=
createChart
((
termui
.
TermHeight
()
-
footer
.
Height
)
/
rows
)
row
:=
termui
.
Body
.
Rows
[
i
%
rows
]
row
.
Cols
=
append
(
row
.
Cols
,
termui
.
NewCol
(
12
/
cols
,
0
,
charts
[
i
]))
}
termui
.
Body
.
AddRows
(
termui
.
NewRow
(
termui
.
NewCol
(
12
,
0
,
footer
)))
termui
.
Body
.
Align
()
termui
.
Render
(
termui
.
Body
)
refreshCharts
(
xeth
,
monitored
,
data
,
charts
,
ctx
,
footer
)
refreshCharts
(
xeth
,
monitored
,
data
,
units
,
charts
,
ctx
,
footer
)
termui
.
Body
.
Align
()
termui
.
Render
(
termui
.
Body
)
// Watch for various system events, and periodically refresh the charts
...
...
@@ -149,7 +133,9 @@ func monitor(ctx *cli.Context) {
termui
.
Render
(
termui
.
Body
)
}
case
<-
refresh
:
refreshCharts
(
xeth
,
monitored
,
data
,
charts
,
ctx
,
footer
)
if
refreshCharts
(
xeth
,
monitored
,
data
,
units
,
charts
,
ctx
,
footer
)
{
termui
.
Body
.
Align
()
}
termui
.
Render
(
termui
.
Body
)
}
}
...
...
@@ -246,45 +232,63 @@ func fetchMetric(metrics map[string]interface{}, metric string) float64 {
// refreshCharts retrieves a next batch of metrics, and inserts all the new
// values into the active datasets and charts
func
refreshCharts
(
xeth
*
rpc
.
Xeth
,
metrics
[]
string
,
data
[][]
float64
,
charts
[]
*
termui
.
LineChart
,
ctx
*
cli
.
Context
,
footer
*
termui
.
Par
)
{
func
refreshCharts
(
xeth
*
rpc
.
Xeth
,
metrics
[]
string
,
data
[][]
float64
,
units
[]
int
,
charts
[]
*
termui
.
LineChart
,
ctx
*
cli
.
Context
,
footer
*
termui
.
Par
)
(
realign
bool
)
{
values
,
err
:=
retrieveMetrics
(
xeth
)
for
i
,
metric
:=
range
metrics
{
data
[
i
]
=
append
([]
float64
{
fetchMetric
(
values
,
metric
)},
data
[
i
][
:
len
(
data
[
i
])
-
1
]
...
)
updateChart
(
metric
,
data
[
i
],
charts
[
i
],
err
)
if
len
(
data
)
<
512
{
data
[
i
]
=
append
([]
float64
{
fetchMetric
(
values
,
metric
)},
data
[
i
]
...
)
}
else
{
data
[
i
]
=
append
([]
float64
{
fetchMetric
(
values
,
metric
)},
data
[
i
][
:
len
(
data
[
i
])
-
1
]
...
)
}
if
updateChart
(
metric
,
data
[
i
],
&
units
[
i
],
charts
[
i
],
err
)
{
realign
=
true
}
}
updateFooter
(
ctx
,
err
,
footer
)
return
}
// updateChart inserts a dataset into a line chart, scaling appropriately as to
// not display weird labels, also updating the chart label accordingly.
func
updateChart
(
metric
string
,
data
[]
float64
,
chart
*
termui
.
LineChart
,
err
error
)
{
func
updateChart
(
metric
string
,
data
[]
float64
,
base
*
int
,
chart
*
termui
.
LineChart
,
err
error
)
(
realign
bool
)
{
dataUnits
:=
[]
string
{
""
,
"K"
,
"M"
,
"G"
,
"T"
,
"E"
}
timeUnits
:=
[]
string
{
"ns"
,
"µs"
,
"ms"
,
"s"
,
"ks"
,
"ms"
}
colors
:=
[]
termui
.
Attribute
{
termui
.
ColorBlue
,
termui
.
ColorCyan
,
termui
.
ColorGreen
,
termui
.
ColorYellow
,
termui
.
ColorRed
,
termui
.
ColorRed
}
// Extract only part of the data that's actually visible
data
=
data
[
:
chart
.
Width
*
2
]
if
chart
.
Width
*
2
<
len
(
data
)
{
data
=
data
[
:
chart
.
Width
*
2
]
}
// Find the maximum value and scale under 1K
high
:=
data
[
0
]
for
_
,
value
:=
range
data
[
1
:
]
{
high
=
math
.
Max
(
high
,
value
)
high
:=
0.0
if
len
(
data
)
>
0
{
high
=
data
[
0
]
for
_
,
value
:=
range
data
[
1
:
]
{
high
=
math
.
Max
(
high
,
value
)
}
}
unit
,
scale
:=
0
,
1.0
for
high
>=
1000
{
high
,
unit
,
scale
=
high
/
1000
,
unit
+
1
,
scale
*
1000
}
// If the unit changes, re-create the chart (hack to set max height...)
if
unit
!=
*
base
{
realign
,
*
base
,
*
chart
=
true
,
unit
,
*
createChart
(
chart
.
Height
)
}
// Update the chart's data points with the scaled values
if
cap
(
chart
.
Data
)
<
len
(
data
)
{
chart
.
Data
=
make
([]
float64
,
len
(
data
))
}
chart
.
Data
=
chart
.
Data
[
:
len
(
data
)]
for
i
,
value
:=
range
data
{
chart
.
Data
[
i
]
=
value
/
scale
}
// Update the chart's label with the scale units
chart
.
Border
.
Label
=
metric
units
:=
dataUnits
if
strings
.
Contains
(
metric
,
"/Percentiles/"
)
||
strings
.
Contains
(
metric
,
"/pauses/"
)
{
units
=
timeUnits
}
chart
.
Border
.
Label
=
metric
if
len
(
units
[
unit
])
>
0
{
chart
.
Border
.
Label
+=
" ["
+
units
[
unit
]
+
"]"
}
...
...
@@ -292,6 +296,24 @@ func updateChart(metric string, data []float64, chart *termui.LineChart, err err
if
err
!=
nil
{
chart
.
LineColor
=
termui
.
ColorRed
|
termui
.
AttrBold
}
return
}
// createChart creates an empty line chart with the default configs.
func
createChart
(
height
int
)
*
termui
.
LineChart
{
chart
:=
termui
.
NewLineChart
()
if
runtime
.
GOOS
==
"windows"
{
chart
.
Mode
=
"dot"
}
chart
.
DataLabels
=
[]
string
{
""
}
chart
.
Height
=
height
chart
.
AxesColor
=
termui
.
ColorWhite
chart
.
PaddingBottom
=
-
2
chart
.
Border
.
LabelFgColor
=
chart
.
Border
.
FgColor
|
termui
.
AttrBold
chart
.
Border
.
FgColor
=
chart
.
Border
.
BgColor
return
chart
}
// updateFooter updates the footer contents based on any encountered errors.
...
...
eth/fetcher/fetcher.go
View file @
9d8b512b
...
...
@@ -7,6 +7,8 @@ import (
"math/rand"
"time"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/logger"
...
...
@@ -104,6 +106,7 @@ type Fetcher struct {
broadcastMeter
metrics
.
Meter
// Counter for metering the inbound propagations
broadcastTimer
metrics
.
Timer
// Counter and timer for metering the block forwarding
discardMeter
metrics
.
Meter
// Counter for metering the discarded blocks
futureMeter
metrics
.
Meter
// Counter for metering future blocks
}
// New creates a block fetcher to retrieve blocks based on hash announcements.
...
...
@@ -131,6 +134,7 @@ func New(getBlock blockRetrievalFn, validateBlock blockValidatorFn, broadcastBlo
broadcastMeter
:
metrics
.
GetOrRegisterMeter
(
"eth/sync/RemoteBroadcasts"
,
metrics
.
DefaultRegistry
),
broadcastTimer
:
metrics
.
GetOrRegisterTimer
(
"eth/sync/LocalBroadcasts"
,
metrics
.
DefaultRegistry
),
discardMeter
:
metrics
.
GetOrRegisterMeter
(
"eth/sync/DiscardedBlocks"
,
metrics
.
DefaultRegistry
),
futureMeter
:
metrics
.
GetOrRegisterMeter
(
"eth/sync/FutureBlocks"
,
metrics
.
DefaultRegistry
),
}
}
...
...
@@ -323,7 +327,7 @@ func (f *Fetcher) loop() {
hash
:=
block
.
Hash
()
// Filter explicitly requested blocks from hash announcements
if
_
,
ok
:=
f
.
fetching
[
hash
];
ok
{
if
f
.
fetching
[
hash
]
!=
nil
&&
f
.
queued
[
hash
]
==
nil
{
// Discard if already imported by other means
if
f
.
getBlock
(
hash
)
==
nil
{
explicit
=
append
(
explicit
,
block
)
...
...
@@ -416,14 +420,22 @@ func (f *Fetcher) insert(peer string, block *types.Block) {
return
}
// Quickly validate the header and propagate the block if it passes
if
err
:=
f
.
validateBlock
(
block
,
parent
);
err
!=
nil
{
switch
err
:=
f
.
validateBlock
(
block
,
parent
);
err
{
case
nil
:
// All ok, quickly propagate to our peers
f
.
broadcastTimer
.
UpdateSince
(
block
.
ReceivedAt
)
go
f
.
broadcastBlock
(
block
,
true
)
case
core
.
BlockFutureErr
:
f
.
futureMeter
.
Mark
(
1
)
// Weird future block, don't fail, but neither propagate
default
:
// Something went very wrong, drop the peer
glog
.
V
(
logger
.
Debug
)
.
Infof
(
"Peer %s: block #%d [%x] verification failed: %v"
,
peer
,
block
.
NumberU64
(),
hash
[
:
4
],
err
)
f
.
dropPeer
(
peer
)
return
}
f
.
broadcastTimer
.
UpdateSince
(
block
.
ReceivedAt
)
go
f
.
broadcastBlock
(
block
,
true
)
// Run the actual import and log any issues
if
_
,
err
:=
f
.
insertChain
(
types
.
Blocks
{
block
});
err
!=
nil
{
glog
.
V
(
logger
.
Warn
)
.
Infof
(
"Peer %s: block #%d [%x] import failed: %v"
,
peer
,
block
.
NumberU64
(),
hash
[
:
4
],
err
)
...
...
p2p/peer.go
View file @
9d8b512b
...
...
@@ -249,15 +249,22 @@ func countMatchingProtocols(protocols []Protocol, caps []Cap) int {
// matchProtocols creates structures for matching named subprotocols.
func
matchProtocols
(
protocols
[]
Protocol
,
caps
[]
Cap
,
rw
MsgReadWriter
)
map
[
string
]
*
protoRW
{
sort
.
Sort
(
capsByName
(
caps
))
sort
.
Sort
(
capsByName
AndVersion
(
caps
))
offset
:=
baseProtocolLength
result
:=
make
(
map
[
string
]
*
protoRW
)
outer
:
for
_
,
cap
:=
range
caps
{
for
_
,
proto
:=
range
protocols
{
if
proto
.
Name
==
cap
.
Name
&&
proto
.
Version
==
cap
.
Version
&&
result
[
cap
.
Name
]
==
nil
{
if
proto
.
Name
==
cap
.
Name
&&
proto
.
Version
==
cap
.
Version
{
// If an old protocol version matched, revert it
if
old
:=
result
[
cap
.
Name
];
old
!=
nil
{
offset
-=
old
.
Length
}
// Assign the new match
result
[
cap
.
Name
]
=
&
protoRW
{
Protocol
:
proto
,
offset
:
offset
,
in
:
make
(
chan
Msg
),
w
:
rw
}
offset
+=
proto
.
Length
continue
outer
}
}
...
...
p2p/peer_test.go
View file @
9d8b512b
...
...
@@ -196,3 +196,98 @@ func TestNewPeer(t *testing.T) {
p
.
Disconnect
(
DiscAlreadyConnected
)
// Should not hang
}
func
TestMatchProtocols
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
Remote
[]
Cap
Local
[]
Protocol
Match
map
[
string
]
protoRW
}{
{
// No remote capabilities
Local
:
[]
Protocol
{{
Name
:
"a"
}},
},
{
// No local protocols
Remote
:
[]
Cap
{{
Name
:
"a"
}},
},
{
// No mutual protocols
Remote
:
[]
Cap
{{
Name
:
"a"
}},
Local
:
[]
Protocol
{{
Name
:
"b"
}},
},
{
// Some matches, some differences
Remote
:
[]
Cap
{{
Name
:
"local"
},
{
Name
:
"match1"
},
{
Name
:
"match2"
}},
Local
:
[]
Protocol
{{
Name
:
"match1"
},
{
Name
:
"match2"
},
{
Name
:
"remote"
}},
Match
:
map
[
string
]
protoRW
{
"match1"
:
{
Protocol
:
Protocol
{
Name
:
"match1"
}},
"match2"
:
{
Protocol
:
Protocol
{
Name
:
"match2"
}}},
},
{
// Various alphabetical ordering
Remote
:
[]
Cap
{{
Name
:
"aa"
},
{
Name
:
"ab"
},
{
Name
:
"bb"
},
{
Name
:
"ba"
}},
Local
:
[]
Protocol
{{
Name
:
"ba"
},
{
Name
:
"bb"
},
{
Name
:
"ab"
},
{
Name
:
"aa"
}},
Match
:
map
[
string
]
protoRW
{
"aa"
:
{
Protocol
:
Protocol
{
Name
:
"aa"
}},
"ab"
:
{
Protocol
:
Protocol
{
Name
:
"ab"
}},
"ba"
:
{
Protocol
:
Protocol
{
Name
:
"ba"
}},
"bb"
:
{
Protocol
:
Protocol
{
Name
:
"bb"
}}},
},
{
// No mutual versions
Remote
:
[]
Cap
{{
Version
:
1
}},
Local
:
[]
Protocol
{{
Version
:
2
}},
},
{
// Multiple versions, single common
Remote
:
[]
Cap
{{
Version
:
1
},
{
Version
:
2
}},
Local
:
[]
Protocol
{{
Version
:
2
},
{
Version
:
3
}},
Match
:
map
[
string
]
protoRW
{
""
:
{
Protocol
:
Protocol
{
Version
:
2
}}},
},
{
// Multiple versions, multiple common
Remote
:
[]
Cap
{{
Version
:
1
},
{
Version
:
2
},
{
Version
:
3
},
{
Version
:
4
}},
Local
:
[]
Protocol
{{
Version
:
2
},
{
Version
:
3
}},
Match
:
map
[
string
]
protoRW
{
""
:
{
Protocol
:
Protocol
{
Version
:
3
}}},
},
{
// Various version orderings
Remote
:
[]
Cap
{{
Version
:
4
},
{
Version
:
1
},
{
Version
:
3
},
{
Version
:
2
}},
Local
:
[]
Protocol
{{
Version
:
2
},
{
Version
:
3
},
{
Version
:
1
}},
Match
:
map
[
string
]
protoRW
{
""
:
{
Protocol
:
Protocol
{
Version
:
3
}}},
},
{
// Versions overriding sub-protocol lengths
Remote
:
[]
Cap
{{
Version
:
1
},
{
Version
:
2
},
{
Version
:
3
},
{
Name
:
"a"
}},
Local
:
[]
Protocol
{{
Version
:
1
,
Length
:
1
},
{
Version
:
2
,
Length
:
2
},
{
Version
:
3
,
Length
:
3
},
{
Name
:
"a"
}},
Match
:
map
[
string
]
protoRW
{
""
:
{
Protocol
:
Protocol
{
Version
:
3
}},
"a"
:
{
Protocol
:
Protocol
{
Name
:
"a"
},
offset
:
3
}},
},
}
for
i
,
tt
:=
range
tests
{
result
:=
matchProtocols
(
tt
.
Local
,
tt
.
Remote
,
nil
)
if
len
(
result
)
!=
len
(
tt
.
Match
)
{
t
.
Errorf
(
"test %d: negotiation mismatch: have %v, want %v"
,
i
,
len
(
result
),
len
(
tt
.
Match
))
continue
}
// Make sure all negotiated protocols are needed and correct
for
name
,
proto
:=
range
result
{
match
,
ok
:=
tt
.
Match
[
name
]
if
!
ok
{
t
.
Errorf
(
"test %d, proto '%s': negotiated but shouldn't have"
,
i
,
name
)
continue
}
if
proto
.
Name
!=
match
.
Name
{
t
.
Errorf
(
"test %d, proto '%s': name mismatch: have %v, want %v"
,
i
,
name
,
proto
.
Name
,
match
.
Name
)
}
if
proto
.
Version
!=
match
.
Version
{
t
.
Errorf
(
"test %d, proto '%s': version mismatch: have %v, want %v"
,
i
,
name
,
proto
.
Version
,
match
.
Version
)
}
if
proto
.
offset
-
baseProtocolLength
!=
match
.
offset
{
t
.
Errorf
(
"test %d, proto '%s': offset mismatch: have %v, want %v"
,
i
,
name
,
proto
.
offset
-
baseProtocolLength
,
match
.
offset
)
}
}
// Make sure no protocols missed negotiation
for
name
,
_
:=
range
tt
.
Match
{
if
_
,
ok
:=
result
[
name
];
!
ok
{
t
.
Errorf
(
"test %d, proto '%s': not negotiated, should have"
,
i
,
name
)
continue
}
}
}
}
p2p/protocol.go
View file @
9d8b512b
...
...
@@ -43,8 +43,10 @@ func (cap Cap) String() string {
return
fmt
.
Sprintf
(
"%s/%d"
,
cap
.
Name
,
cap
.
Version
)
}
type
capsByName
[]
Cap
type
capsByName
AndVersion
[]
Cap
func
(
cs
capsByName
)
Len
()
int
{
return
len
(
cs
)
}
func
(
cs
capsByName
)
Less
(
i
,
j
int
)
bool
{
return
cs
[
i
]
.
Name
<
cs
[
j
]
.
Name
}
func
(
cs
capsByName
)
Swap
(
i
,
j
int
)
{
cs
[
i
],
cs
[
j
]
=
cs
[
j
],
cs
[
i
]
}
func
(
cs
capsByNameAndVersion
)
Len
()
int
{
return
len
(
cs
)
}
func
(
cs
capsByNameAndVersion
)
Swap
(
i
,
j
int
)
{
cs
[
i
],
cs
[
j
]
=
cs
[
j
],
cs
[
i
]
}
func
(
cs
capsByNameAndVersion
)
Less
(
i
,
j
int
)
bool
{
return
cs
[
i
]
.
Name
<
cs
[
j
]
.
Name
||
(
cs
[
i
]
.
Name
==
cs
[
j
]
.
Name
&&
cs
[
i
]
.
Version
<
cs
[
j
]
.
Version
)
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment