Skip to content

Commit

Permalink
Implement/external grpc handler (#251)
Browse files Browse the repository at this point in the history
* implement statesync handler and add some manifests

* add Dockerfile

* fix

* refactor kvs

* add iroiro

* fix

* fix ci

* fix tests

* implement block handler

* Commit from GitHub Actions (compile_PB)

* fix ci

* fix deadlock

* add godoc

* fix

* fix ci

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
gotti and github-actions[bot] authored Nov 13, 2022
1 parent 866ae16 commit e81f112
Show file tree
Hide file tree
Showing 31 changed files with 1,488 additions and 991 deletions.
13 changes: 12 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pull:

start: pull up

pb: statesync speedControl ats
pb: statesync speedControl ats block

statesync: statesync_internal statesync_external statesync_positioning

Expand Down Expand Up @@ -76,3 +76,14 @@ ats_go:
ats_python:
cd auto_operation && \
./protoc-gen.sh

block: block_external

block_external:
protoc \
--go_out=external \
--go_opt=Mproto/block.proto=./spec \
--go-grpc_out=external/spec \
--go-grpc_opt=Mproto/block.proto=. \
-I./ \
proto/block.proto
1 change: 1 addition & 0 deletions backend/compose.debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ services:
environment:
- "CLIENTSIDESERVER_PORT=8080"
- "INTERNALSERVER_ADDR=internal:54321"
- "CLIENTSIDESERVER_ATSADDRESS=internal:54321"
internal:
build:
context: ./internal
Expand Down
60 changes: 6 additions & 54 deletions backend/external/cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,64 +1,16 @@
package main

import (
"log"
"ueckoken/plarail2022-external/internal"
"ueckoken/plarail2022-external/pkg/envStore"
"ueckoken/plarail2022-external/pkg/syncController"

"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
)

const namespace = "plarailexternal"

func main() {
clientHandler2syncController := make(chan syncController.StationState, 16)
syncController2clientHandler := make(chan syncController.StationState, 64)
initEspStatus2syncController := make(chan syncController.StationState)

envVal := envStore.GetEnv()

clientConn := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "clients_connections_seconds",
Help: "Number of connections handling websocket",
},
[]string{},
)

clientConnTotal := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "clients_connections_total",
Help: "Total client connection",
},
[]string{},
)

controlCommandTotal := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "client_commands_total",
Help: "Total client commands",
},
[]string{},
)

httpServer := internal.HTTPServer{
ClientHandler2syncController: clientHandler2syncController,
SyncController2clientHandler: syncController2clientHandler,
Environment: envVal,
NumberOfClientConnection: clientConn,
TotalClientConnection: clientConnTotal,
TotalCLientCommands: controlCommandTotal,
Clients: &internal.ClientsCollection{},
}
syncController := syncController.SyncController{
ClientHandler2syncController: clientHandler2syncController,
SyncController2clientHandler: syncController2clientHandler,
Environment: envVal,
InitServoRoute: initEspStatus2syncController,
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalln("failed to initialize zap")
}
go httpServer.StartServer()
syncController.StartSyncController()
internal.Run(logger)
}
3 changes: 3 additions & 0 deletions backend/external/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/prometheus/client_golang v1.14.0
github.com/vrischmann/envconfig v1.3.0
go.uber.org/zap v1.23.0
google.golang.org/grpc v1.50.1
google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v2 v2.4.0
Expand All @@ -25,6 +26,8 @@ require (
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
Expand Down
20 changes: 11 additions & 9 deletions backend/external/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
Expand Down Expand Up @@ -178,6 +179,7 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand All @@ -186,16 +188,11 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_golang v1.13.1 h1:3gMjIY2+/hzmqhtUC/aQNYldJA6DtH3CgQvwS+02K1c=
github.com/prometheus/client_golang v1.13.1/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
Expand Down Expand Up @@ -225,8 +222,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk=
github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand All @@ -237,6 +234,13 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
Expand Down Expand Up @@ -486,8 +490,6 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.50.0 h1:fPVVDxY9w++VjTZsYvXWqEf9Rqar/e+9zYfxKK+W+YU=
google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
Expand Down Expand Up @@ -521,8 +523,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
14 changes: 14 additions & 0 deletions backend/external/internal/blocksync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package internal

import (
"go.uber.org/zap"
"ueckoken/plarail2022-external/pkg/synccontroller"
"ueckoken/plarail2022-external/spec"
)

// startBlockSync starts sync controller for block state.
func startBlockSync(logger *zap.Logger, syncInput chan synccontroller.KV[spec.Blocks_BlockId, spec.NotifyStateRequest_State], syncOutput chan<- synccontroller.KV[spec.Blocks_BlockId, spec.NotifyStateRequest_State]) {
s := synccontroller.NewSyncController(logger, syncInput, syncOutput)

go s.Run()
}
123 changes: 123 additions & 0 deletions backend/external/internal/grpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package internal

import (
"context"
"fmt"
"net"
"ueckoken/plarail2022-external/pkg/envStore"
"ueckoken/plarail2022-external/pkg/synccontroller"
"ueckoken/plarail2022-external/spec"

"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

// GrpcStateHandler is a handler for gRPC.
type GrpcStateHandler struct {
logger *zap.Logger
env *envStore.Env
spec.UnimplementedControlServer
stateOutput chan<- synccontroller.KV[spec.Stations_StationId, spec.Command2InternalRequest_State]
stateInput <-chan synccontroller.KV[spec.Stations_StationId, spec.Command2InternalRequest_State]
}

// NewGrpcHandler creates gRPC handler.
func NewGrpcHandler(logger *zap.Logger, env *envStore.Env, stateOutput chan<- synccontroller.KV[spec.Stations_StationId, spec.Command2InternalRequest_State], stateInput <-chan synccontroller.KV[spec.Stations_StationId, spec.Command2InternalRequest_State]) *GrpcStateHandler {
return &GrpcStateHandler{logger: logger, env: env, stateOutput: stateOutput, stateInput: stateInput}
}

// Command2Internal handles requests from ATS.
func (g GrpcStateHandler) Command2Internal(_ context.Context, req *spec.RequestSync) (*spec.ResponseSync, error) {
s := synccontroller.KV[spec.Stations_StationId, spec.Command2InternalRequest_State]{
Key: req.GetStation().GetStationId(),
Value: spec.Command2InternalRequest_State(req.GetState()),
}
g.stateOutput <- s
return &spec.ResponseSync{Response: spec.ResponseSync_SUCCESS}, nil
}

// / handleInput transmits changes received in channel to ATS.
func (g GrpcStateHandler) handleInput(ctx context.Context) {
con, err := grpc.DialContext(ctx, g.env.ClientSideServer.ATSAddress.String(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
g.logger.Error("failed to connect ATS", zap.Error(err))
}
defer con.Close()
for d := range g.stateInput {
client := spec.NewControlClient(con)
req := &spec.RequestSync{
Station: &spec.Stations{StationId: d.Key},
State: spec.RequestSync_State(d.Value),
}
res, err := client.Command2Internal(ctx, req)
if err != nil {
g.logger.Error("failed to send data to ATS", zap.Any("payload", req), zap.Error(err))
}
if res.GetResponse() != spec.ResponseSync_SUCCESS {
g.logger.Error("ATS response seems to be unsuccessfull", zap.Any("payload", res.GetResponse()))
}
}
}

type GrpcBlockHandler struct {
env *envStore.Env
logger *zap.Logger
spec.UnimplementedBlockStateSyncServer
stateOutput chan<- synccontroller.KV[spec.Blocks_BlockId, spec.NotifyStateRequest_State]
stateInput <-chan synccontroller.KV[spec.Blocks_BlockId, spec.NotifyStateRequest_State]
}

// NewGrpcBlockHandler creates gRPC handler.
func NewGrpcBlockHandler(logger *zap.Logger, env *envStore.Env, stateOutput chan<- synccontroller.KV[spec.Blocks_BlockId, spec.NotifyStateRequest_State], stateInput <-chan synccontroller.KV[spec.Blocks_BlockId, spec.NotifyStateRequest_State]) *GrpcBlockHandler {
return &GrpcBlockHandler{logger: logger, env: env, stateOutput: stateOutput, stateInput: stateInput}
}

// NotifyState handles requests from ATS.
func (g GrpcBlockHandler) NotifyState(_ context.Context, req *spec.NotifyStateRequest) (*spec.NotifyStateResponse, error) {
g.stateOutput <- synccontroller.KV[spec.Blocks_BlockId, spec.NotifyStateRequest_State]{Key: req.GetBlock().GetBlockId(), Value: req.GetState()}
return &spec.NotifyStateResponse{Response: spec.NotifyStateResponse_SUCCESS}, nil
}

// handleInput transmits changes received in channel to ATS.
func (g GrpcBlockHandler) handleInput(ctx context.Context) {
con, err := grpc.DialContext(ctx, g.env.ClientSideServer.ATSAddress.String(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
g.logger.Error("failed to connect ATS", zap.Error(err))
}
defer con.Close()
for d := range g.stateInput {
client := spec.NewBlockStateSyncClient(con)
req := &spec.NotifyStateRequest{
Block: &spec.Blocks{BlockId: d.Key},
State: d.Value,
}
res, err := client.NotifyState(ctx, req)
if err != nil {
g.logger.Error("failed to send data to ATS", zap.Any("payload", req), zap.Error(err))
}
if res.GetResponse() != spec.NotifyStateResponse_SUCCESS {
g.logger.Error("ATS response seems to be unsuccessfull", zap.Any("payload", res.GetResponse()))
}
}
}

// GRPCListenAndServe listens and serve.
func GRPCListenAndServe(ctx context.Context, logger *zap.Logger, port uint, handler *GrpcStateHandler, blockhandler *GrpcBlockHandler) {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
logger.Panic("failed to listen", zap.Error(err))
}
go handler.handleInput(ctx)
go blockhandler.handleInput(ctx)
s := grpc.NewServer()
spec.RegisterControlServer(s, handler)
spec.RegisterBlockStateSyncServer(s, blockhandler)
if err := s.Serve(lis); err != nil {
logger.Panic("failed to server", zap.Error(err))
}
}
Loading

0 comments on commit e81f112

Please sign in to comment.