Commit 49d9a8bf authored by wei.xuan's avatar wei.xuan

Merge branch 'fix_conn' into 'master'

feat:update logger

See merge request !5
parents 7a10af48 817de85a
......@@ -20,7 +20,6 @@ import (
"virjar.com/majora-go/client"
"virjar.com/majora-go/common"
"virjar.com/majora-go/infra"
"virjar.com/majora-go/logger"
"virjar.com/majora-go/model"
)
......@@ -55,9 +54,10 @@ func init() {
}
func initial(cfg *model.Configure) {
_ = getty.SetLoggerCallerDisable()
_ = getty.SetLoggerLevel(getty.LoggerLevelDebug)
logger.SetLogLevel(cfg.LogLevel)
if cfg.LogLevel > 0 {
_ = getty.SetLoggerCallerDisable()
_ = getty.SetLoggerLevel(getty.LoggerLevel(cfg.LogLevel - 1))
}
addr := fmt.Sprintf("127.0.0.1:%d", cfg.PprofPort)
getty.GetLogger().Infof("enable pprof: %s", addr)
if cfg.PprofPort > 0 {
......@@ -78,7 +78,7 @@ func initial(cfg *model.Configure) {
if !cfg.DisableUpdate {
if _, err := infra.Update("majora", Version); err != nil {
logger.Error().Msgf("check update error %+s", err.Error())
getty.GetLogger().Errorf("check update error %+s", err.Error())
}
}
}
......
package client
import (
"bufio"
"fmt"
"net"
"strings"
......@@ -10,7 +9,6 @@ import (
"github.com/adamweixuan/getty"
"virjar.com/majora-go/common"
"virjar.com/majora-go/logger"
"virjar.com/majora-go/protocol"
)
......@@ -34,7 +32,7 @@ func (client *Client) handleConnect(packet *protocol.MajoraPacket, session getty
hostPort := strings.Split(packet.Extra, ":")
if len(packet.Extra) == 0 || len(hostPort) != 2 {
getty.GetLogger().Errorf("invalid extra %s", packet.Extra)
getty.GetLogger().Errorf("[handleConnect] invalid extra %s", packet.Extra)
client.closeVirtualConnection(session, packet.SerialNumber)
return
}
......@@ -42,31 +40,33 @@ func (client *Client) handleConnect(packet *protocol.MajoraPacket, session getty
addr := fmt.Sprintf("%s:%s", hostPort[0], hostPort[1])
dialer := net.Dialer{
Timeout: common.ConnTimeout,
}
if client.localAddr != nil {
dialer.LocalAddr = client.localAddr
Timeout: common.UpstreamTimeout,
LocalAddr: client.localAddr,
}
conn, err := dialer.Dial(common.TCP, addr)
if err != nil {
getty.GetLogger().Errorf("connect to %s->%s", addr, err.Error())
getty.GetLogger().Errorf("[handleConnect] %d->connect to %s->%s", packet.SerialNumber, addr, err.Error())
client.closeVirtualConnection(session, packet.SerialNumber)
return
}
getty.GetLogger().Infof("connect success to %d->%s ", packet.SerialNumber, addr)
client.AddConnection(packet, conn, addr)
tcpConn := conn.(*net.TCPConn)
_ = tcpConn.SetNoDelay(true)
_ = tcpConn.SetKeepAlive(true)
client.AddConnection(packet, tcpConn, addr)
majoraPacket := protocol.TypeConnectReady.CreatePacket()
majoraPacket.SerialNumber = packet.SerialNumber
majoraPacket.Extra = client.config.ClientID
if _, _, err := session.WritePkg(majoraPacket, time.Second*30); err != nil {
getty.GetLogger().Errorf("write pkg to nat server with error %s", err.Error())
if _, _, err := session.WritePkg(majoraPacket, common.SessionTimeout); err != nil {
getty.GetLogger().Errorf("[handleConnect] %d->write pkg to nat server with error %s", packet.SerialNumber,
err.Error())
client.closeVirtualConnection(session, packet.SerialNumber)
return
} else {
getty.GetLogger().Infof("[handleConnect] %d->connect success to %s ", packet.SerialNumber, addr)
}
client.handleUpStream(conn, packet, session)
client.handleUpStream(tcpConn, packet, session)
}(packet)
}
......@@ -74,63 +74,60 @@ func (client *Client) handleTransfer(packet *protocol.MajoraPacket, session gett
go func(packet *protocol.MajoraPacket) {
load, ok := client.connStore.Load(packet.SerialNumber)
if !ok {
getty.GetLogger().Errorf("fatal can not find connection for %d", packet.SerialNumber)
getty.GetLogger().Errorf("[handleTransfer] %d-> can not find connection", packet.SerialNumber)
client.closeVirtualConnection(session, packet.SerialNumber)
return
}
conn := load.(net.Conn)
conn := load.(*net.TCPConn)
cnt, err := conn.Write(packet.Data)
if err != nil {
getty.GetLogger().Errorf("write to upstream fail for %d->", packet.SerialNumber, err)
getty.GetLogger().Errorf("[handleTransfer] %d->write to upstream fail for %s", packet.SerialNumber, err)
client.closeVirtualConnection(session, packet.SerialNumber)
return
}
if cnt != len(packet.Data) {
getty.GetLogger().Errorf("write not all data for %d->expect->%d/%d",
getty.GetLogger().Errorf("[handleTransfer] %d-> write not all data for expect->%d/%d",
packet.SerialNumber, len(packet.Data), cnt)
client.closeVirtualConnection(session, packet.SerialNumber)
return
}
getty.GetLogger().Debugf("handleTransfer success %d->%+v", packet.SerialNumber, string(packet.Data))
getty.GetLogger().Debugf("[handleTransfer] %d-> success %+v", packet.SerialNumber, string(packet.Data))
}(packet)
}
func (client *Client) handleUpStream(conn net.Conn, packet *protocol.MajoraPacket, session getty.Session) {
getty.GetLogger().Infof("serialNum %d -> handleUpStream start...", packet.SerialNumber)
reader := bufio.NewReader(conn)
func (client *Client) handleUpStream(conn *net.TCPConn, packet *protocol.MajoraPacket, session getty.Session) {
getty.GetLogger().Infof("[handleUpStream] %d-> handleUpStream start...", packet.SerialNumber)
defer func() {
_ = conn.Close()
}()
for {
if _, err := reader.Peek(1); err != nil {
getty.GetLogger().Warnf("%d -> handleUpStream peek with error:%s", packet.SerialNumber, err.Error())
client.OnClose(session, conn, packet.SerialNumber)
break
}
bufsize := reader.Buffered()
getty.GetLogger().Debugf("bufsize %d", bufsize)
buf := make([]byte, bufsize)
_, err := reader.Read(buf)
buf := make([]byte, common.BufSize) // 4k
cnt, err := conn.Read(buf)
if err != nil {
getty.GetLogger().Errorf("handleUpStream read with error:%d->%+v", packet.SerialNumber, err)
getty.GetLogger().Errorf("[handleUpStream] %d->read with error:%+v,l:%s->r:%s",
packet.SerialNumber, err, conn.LocalAddr(), conn.RemoteAddr())
client.OnClose(session, conn, packet.SerialNumber)
break
}
pack := protocol.TypeTransfer.CreatePacket()
pack.Data = buf
pack.Data = buf[:cnt]
pack.SerialNumber = packet.SerialNumber
if _, _, err := session.WritePkg(pack, time.Second*10); err != nil {
getty.GetLogger().Debugf("handleUpStream fail %d->%+v", packet.SerialNumber, err.Error())
if _, _, err := session.WritePkg(pack, common.SessionTimeout); err != nil {
getty.GetLogger().Debugf("[handleUpStream] %d-> write to server fail %+v", packet.SerialNumber, err.Error())
client.OnClose(session, conn, packet.SerialNumber)
break
} else {
getty.GetLogger().Debugf("handleUpStream success %d->%+v", packet.SerialNumber, string(packet.Data))
getty.GetLogger().Debugf("[handleUpStream] %d->success %+v", packet.SerialNumber, string(packet.Data))
}
}
}
func (client *Client) handleDisconnectMessage(session getty.Session, packet *protocol.MajoraPacket) {
go func() {
getty.GetLogger().Infof("[handleDisconnectMessage] %d->%v", packet.SerialNumber, session.IsClosed())
if conn, ok := client.connStore.Load(packet.SerialNumber); ok {
client.OnClose(session, conn.(net.Conn), packet.SerialNumber)
}
......@@ -139,7 +136,7 @@ func (client *Client) handleDisconnectMessage(session getty.Session, packet *pro
func (client *Client) handleControlMessage(_ *protocol.MajoraPacket) {
go func() {
logger.Debug().Msg("handleControlMessage ")
getty.GetLogger().Debugf("handleControlMessage")
}()
}
......@@ -150,9 +147,13 @@ func (client *Client) handleDestroyMessage() {
}()
}
func (client *Client) AddConnection(packet *protocol.MajoraPacket, conn net.Conn, addr string) {
func (client *Client) AddConnection(packet *protocol.MajoraPacket, conn *net.TCPConn, addr string) {
load, ok := client.connStore.Load(packet.SerialNumber)
if ok {
getty.GetLogger().Errorf("[AddConnection] %d->error, has one %+v...", packet.SerialNumber, load)
}
client.connStore.Store(packet.SerialNumber, conn)
getty.GetLogger().Infof("create connection for %d->%s", packet.SerialNumber, addr)
getty.GetLogger().Infof("[AddConnection] %d->%s success", packet.SerialNumber, addr)
}
// OnClose 1. 本地缓存删除 2. 关闭连接 3. 通知natserver
......@@ -169,13 +170,13 @@ func (client *Client) OnClose(netSession getty.Session, upStreamSession net.Conn
//closeVirtualConnection disconnect to server
func (client *Client) closeVirtualConnection(session getty.Session, serialNumber int64) {
getty.GetLogger().Infof("[closeVirtualConnection] %d session status:%v", serialNumber, session.IsClosed())
getty.GetLogger().Warnf("[closeVirtualConnection] %d->session status:%v", serialNumber, session.IsClosed())
if !session.IsClosed() {
majoraPacket := protocol.TypeDisconnect.CreatePacket()
majoraPacket.SerialNumber = serialNumber
majoraPacket.Extra = client.config.ClientID
if _, _, err := session.WritePkg(majoraPacket, time.Second*30); err != nil {
getty.GetLogger().Errorf("[closeVirtualConnection] error %d->%s", serialNumber, err.Error())
if _, _, err := session.WritePkg(majoraPacket, common.SessionTimeout); err != nil {
getty.GetLogger().Errorf("[closeVirtualConnection] ->%d error %s", serialNumber, err.Error())
}
}
}
......@@ -188,7 +189,7 @@ func (client *Client) CloseAll(session getty.Session) {
}()
client.connStore.Range(func(key, value interface{}) bool {
serialNumber := key.(int64)
conn, _ := value.(net.Conn)
conn, _ := value.(*net.TCPConn)
getty.GetLogger().Infof("[CloseAll] close serialNumber -> %d", serialNumber)
client.OnClose(session, conn, serialNumber)
return true
......
......@@ -50,7 +50,9 @@ func (m *MajoraEventListener) OnCron(session getty.Session) {
func (m *MajoraEventListener) OnMessage(session getty.Session, input interface{}) {
majoraPacket := input.(*protocol.MajoraPacket)
getty.GetLogger().Infof("receive packet %d->%s", majoraPacket.SerialNumber, majoraPacket.Ttype.ToString())
getty.GetLogger().Infof("receive packet from server %d->%s,extra:%s", majoraPacket.SerialNumber,
majoraPacket.Ttype.ToString(), majoraPacket.Extra)
switch majoraPacket.Ttype {
case protocol.TypeHeartbeat:
m.client.handleHeartbeat(session)
......
......@@ -7,6 +7,7 @@ import (
"github.com/adamweixuan/getty"
gxsync "github.com/adamweixuan/gostnops/sync"
"virjar.com/majora-go/common"
"virjar.com/majora-go/infra"
)
......@@ -49,22 +50,21 @@ func InitialSession(session getty.Session, client *Client) (err error) {
if err = tcpConn.SetKeepAlive(true); err != nil {
return err
}
if err = tcpConn.SetKeepAlivePeriod(10 * time.Second); err != nil {
if err = tcpConn.SetKeepAlivePeriod(common.KeepAliveTimeout); err != nil {
return err
}
if err = tcpConn.SetReadBuffer(262144); err != nil {
if err = tcpConn.SetReadBuffer(common.MB); err != nil {
return err
}
if err = tcpConn.SetWriteBuffer(524288); err != nil {
if err = tcpConn.SetWriteBuffer(common.MB); err != nil {
return err
}
session.SetName("majora-cli")
//session.SetMaxMsgLen(128 * 1024) // max message package length is 128k
session.SetMaxMsgLen(8 * 1024) // max message package length is 128k
session.SetReadTimeout(time.Second * 10)
session.SetWriteTimeout(time.Second * 10)
session.SetWaitTime(time.Second * 10)
session.SetName(common.SessionName)
session.SetMaxMsgLen(common.KB8) // max message package length is 128k
session.SetReadTimeout(common.ReadTimeout)
session.SetWriteTimeout(common.WriteTimeout)
session.SetWaitTime(common.WaitTimeout)
if client.config.Redial.Valid() {
getty.GetLogger().Infof("ReconnInterval %+v", client.config.Redial.RedialDuration)
......
......@@ -4,7 +4,7 @@ import (
"os/exec"
"strings"
"virjar.com/majora-go/logger"
"github.com/adamweixuan/getty"
)
var (
......@@ -18,7 +18,7 @@ var (
shellCmd = &ShellCmd{}
)
type ShellCmd struct {}
type ShellCmd struct{}
func (e *ShellCmd) Action() string {
return ActionExecShell
......@@ -30,15 +30,15 @@ func (e *ShellCmd) Handle(param map[string]string, callback Callback) {
callback.OnCmdResponse(false, cmdErrorMap)
return
}
logger.Info().Msgf("exec cmd %s", targetCmd)
getty.GetLogger().Infof("exec cmd %s", targetCmd)
trueCmd := strings.Split(targetCmd, " ")
cmd := exec.Command(trueCmd[0], trueCmd[1:]...) //nolint:gosec
cmd := exec.Command(trueCmd[0], trueCmd[1:]...)
out, err := cmd.CombinedOutput()
if err != nil {
logger.Error().Msgf("exec error %+v", err)
getty.GetLogger().Errorf("exec error %+v", err)
return
}
......
......@@ -41,6 +41,26 @@ const (
)
const (
MB = 1024 * 1024
KB8 = 1024 * 8
BufSize = 1024 * 4
)
const (
ReadTimeout = time.Minute
WriteTimeout = time.Minute
WaitTimeout = time.Second * 30
KeepAliveTimeout = time.Second * 10
SessionTimeout = time.Second * 30
UpstreamTimeout = time.Minute
)
const (
SessionName = "majora-cli"
)
const (
DefNatServerHost = "majora.virjar.com"
DefNatServerPort = 5879
DefNatAddr = "majora.virjar.com:5879"
......
tunnel_addr = 127.0.0.1:5879
;tunnel_addr = aoba.vip:5879
;tunnel_addr = 127.0.0.1:5879
tunnel_addr = aoba.vip:5879
dns_server = 114.114.114.114:53
;bind to local ip
;local_ip = 192.168.0.100
......@@ -7,7 +7,7 @@ dns_server = 114.114.114.114:53
pprof_port = 16666
disable_update = false
; default is info
log_level = 0
log_level = 1
reconn_interval = 5s
;client_id =
......
......@@ -7,7 +7,6 @@ require (
github.com/adamweixuan/gostnops v1.11.20-0.20211029124314-3f0589fceea6
github.com/blang/semver/v4 v4.0.0
github.com/google/uuid v1.3.0
github.com/rs/zerolog v1.25.0
gopkg.in/ini.v1 v1.63.2
)
......
......@@ -6,11 +6,9 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
......@@ -25,9 +23,6 @@ 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=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
......
......@@ -5,7 +5,7 @@ import (
"os/exec"
"time"
"virjar.com/majora-go/logger"
"github.com/adamweixuan/getty"
)
func Restart() {
......@@ -14,16 +14,17 @@ func Restart() {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
logger.Debug().Msgf("Restart ... %+v", cmd)
getty.GetLogger().Debugf("Restart ... %+v", cmd)
if err := cmd.Run(); err != nil {
logger.Error().Msgf("Restart error %+v", err)
getty.GetLogger().Errorf("Restart error %+v", err)
}
}
func RestartBySignal(signal chan struct{}) {
go func() {
time.Sleep(time.Second * 5)
logger.Info().Msgf("=============cleanup==============")
getty.GetLogger().Infof("=============cleanup==============")
signal <- struct{}{}
}()
......@@ -32,8 +33,8 @@ func RestartBySignal(signal chan struct{}) {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
logger.Info().Msgf("[RestartBySignal] ... %+v", cmd)
getty.GetLogger().Infof("[RestartBySignal] ... %+v", cmd)
if err := cmd.Run(); err != nil {
logger.Error().Msgf("Restart error %+v", err)
getty.GetLogger().Errorf("Restart error %+v", err)
}
}
package logger
import (
"os"
"time"
"github.com/rs/zerolog"
)
var (
logger zerolog.Logger
)
func init() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339Nano, NoColor: true}
logger = zerolog.New(output).With().Timestamp().Logger()
}
var (
Error = logger.Error
Info = logger.Info
Warn = logger.Warn
Debug = logger.Debug
)
func SetLogLevel(level int) {
zerolog.SetGlobalLevel(zerolog.Level(level))
}
......@@ -3,10 +3,10 @@ package model
import (
"time"
"github.com/adamweixuan/getty"
"github.com/google/uuid"
"gopkg.in/ini.v1"
"virjar.com/majora-go/common"
"virjar.com/majora-go/logger"
)
type Redial struct {
......@@ -59,7 +59,7 @@ func NewDefMajoraConf() *Configure {
func InitConf(path string) *Configure {
conf := NewDefMajoraConf()
if err := ini.MapTo(conf, path); err != nil {
logger.Error().Msgf("InitConf with error %s, use default...", err.Error())
getty.GetLogger().Errorf("InitConf with error %s, use default...", err.Error())
}
return conf
}
......
......@@ -3,8 +3,8 @@ package protocol
import (
"bufio"
"github.com/adamweixuan/getty"
"virjar.com/majora-go/common"
"virjar.com/majora-go/logger"
)
type Decoder interface {
......@@ -33,7 +33,7 @@ func (mpd *MajoraPacketDecoder) Decode(reader *bufio.Reader) (pack *MajoraPacket
// type
msgType, err := common.ReadByte(reader)
if err != nil {
logger.Error().Msgf("read type error %+v", err)
getty.GetLogger().Errorf("read type error %+v", err)
return nil, common.ErrInvalidSize
}
pack = &MajoraPacket{}
......@@ -42,20 +42,20 @@ func (mpd *MajoraPacketDecoder) Decode(reader *bufio.Reader) (pack *MajoraPacket
// num
pack.SerialNumber, err = common.ReadInt64(reader)
if err != nil {
logger.Error().Msgf("read type error %+v", err)
getty.GetLogger().Errorf("read type error %+v", err)
return nil, common.ErrInvalidSize
}
// extra size
extraSize, err := common.ReadByte(reader)
if err != nil {
logger.Error().Msgf("read type error %+v", err)
getty.GetLogger().Errorf("read type error %+v", err)
return nil, common.ErrInvalidSize
}
extra, err := common.ReadN(int(extraSize), reader)
if err != nil {
logger.Error().Msgf("read type error %+v", err)
getty.GetLogger().Errorf("read type error %+v", err)
return nil, common.ErrInvalidSize
}
pack.Extra = string(extra)
......@@ -63,14 +63,14 @@ func (mpd *MajoraPacketDecoder) Decode(reader *bufio.Reader) (pack *MajoraPacket
// dataFrame
dataSize := int(frameLen) - common.TypeSize - common.SerialNumberSize - common.ExtraSize - int(extraSize)
if dataSize < 0 {
logger.Error().Msgf("read type error %+v", err)
getty.GetLogger().Errorf("read type error %+v", err)
return nil, common.ErrInvalidSize
}
if dataSize > 0 {
data, err := common.ReadN(dataSize, reader)
if err != nil {
logger.Error().Msgf("read type error %+v", err)
getty.GetLogger().Errorf("read type error %+v", err)
}
pack.Data = data
}
......
......@@ -3,7 +3,7 @@ package protocol
import (
"bytes"
"virjar.com/majora-go/logger"
"github.com/adamweixuan/getty"
)
func EncodeExtra(extra map[string]string) []byte {
......@@ -33,7 +33,7 @@ func DecodeExtra(input []byte) map[string]string {
headerSize, err := buffer.ReadByte()
if err != nil {
logger.Error().Msgf("DecodeExtra error %+v", err)
getty.GetLogger().Errorf("DecodeExtra error %+v", err)
return map[string]string{}
}
data := make(map[string]string)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment