提交 e46ab3bd 编写于 作者: P Péter Szilágyi

eth, p2p, rpc/api: polish protocol info gathering

上级 05f74077
...@@ -472,62 +472,10 @@ func New(config *Config) (*Ethereum, error) { ...@@ -472,62 +472,10 @@ func New(config *Config) (*Ethereum, error) {
return eth, nil return eth, nil
} }
type NodeInfo struct { // Network retrieves the underlying P2P network server. This should eventually
Name string // be moved out into a protocol independent package, but for now use an accessor.
NodeUrl string func (s *Ethereum) Network() *p2p.Server {
NodeID string return s.net
IP string
DiscPort int // UDP listening port for discovery protocol
TCPPort int // TCP listening port for RLPx
Td string
ListenAddr string
}
func (s *Ethereum) NodeInfo() *NodeInfo {
node := s.net.Self()
return &NodeInfo{
Name: s.Name(),
NodeUrl: node.String(),
NodeID: node.ID.String(),
IP: node.IP.String(),
DiscPort: int(node.UDP),
TCPPort: int(node.TCP),
ListenAddr: s.net.ListenAddr,
Td: s.BlockChain().GetTd(s.BlockChain().CurrentBlock().Hash()).String(),
}
}
type PeerInfo struct {
ID string
Name string
Caps string
RemoteAddress string
LocalAddress string
}
func newPeerInfo(peer *p2p.Peer) *PeerInfo {
var caps []string
for _, cap := range peer.Caps() {
caps = append(caps, cap.String())
}
return &PeerInfo{
ID: peer.ID().String(),
Name: peer.Name(),
Caps: strings.Join(caps, ", "),
RemoteAddress: peer.RemoteAddr().String(),
LocalAddress: peer.LocalAddr().String(),
}
}
// PeersInfo returns an array of PeerInfo objects describing connected peers
func (s *Ethereum) PeersInfo() (peersinfo []*PeerInfo) {
for _, peer := range s.net.Peers() {
if peer != nil {
peersinfo = append(peersinfo, newPeerInfo(peer))
}
}
return
} }
func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) { func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) {
......
...@@ -34,6 +34,7 @@ import ( ...@@ -34,6 +34,7 @@ import (
"github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/logger/glog"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/pow" "github.com/ethereum/go-ethereum/pow"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
...@@ -55,6 +56,8 @@ type hashFetcherFn func(common.Hash) error ...@@ -55,6 +56,8 @@ type hashFetcherFn func(common.Hash) error
type blockFetcherFn func([]common.Hash) error type blockFetcherFn func([]common.Hash) error
type ProtocolManager struct { type ProtocolManager struct {
networkId int
fastSync bool fastSync bool
txpool txPool txpool txPool
blockchain *core.BlockChain blockchain *core.BlockChain
...@@ -91,6 +94,7 @@ func NewProtocolManager(fastSync bool, networkId int, mux *event.TypeMux, txpool ...@@ -91,6 +94,7 @@ func NewProtocolManager(fastSync bool, networkId int, mux *event.TypeMux, txpool
} }
// Create the protocol manager with the base fields // Create the protocol manager with the base fields
manager := &ProtocolManager{ manager := &ProtocolManager{
networkId: networkId,
fastSync: fastSync, fastSync: fastSync,
eventMux: mux, eventMux: mux,
txpool: txpool, txpool: txpool,
...@@ -111,14 +115,23 @@ func NewProtocolManager(fastSync bool, networkId int, mux *event.TypeMux, txpool ...@@ -111,14 +115,23 @@ func NewProtocolManager(fastSync bool, networkId int, mux *event.TypeMux, txpool
// Compatible; initialise the sub-protocol // Compatible; initialise the sub-protocol
version := version // Closure for the run version := version // Closure for the run
manager.SubProtocols = append(manager.SubProtocols, p2p.Protocol{ manager.SubProtocols = append(manager.SubProtocols, p2p.Protocol{
Name: "eth", Name: ProtocolName,
Version: version, Version: version,
Length: ProtocolLengths[i], Length: ProtocolLengths[i],
Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error { Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
peer := manager.newPeer(int(version), networkId, p, rw) peer := manager.newPeer(int(version), p, rw)
manager.newPeerCh <- peer manager.newPeerCh <- peer
return manager.handle(peer) return manager.handle(peer)
}, },
NodeInfo: func() interface{} {
return manager.NodeInfo()
},
PeerInfo: func(id discover.NodeID) interface{} {
if p := manager.peers.Peer(fmt.Sprintf("%x", id[:8])); p != nil {
return p.Info()
}
return nil
},
}) })
} }
if len(manager.SubProtocols) == 0 { if len(manager.SubProtocols) == 0 {
...@@ -188,8 +201,8 @@ func (pm *ProtocolManager) Stop() { ...@@ -188,8 +201,8 @@ func (pm *ProtocolManager) Stop() {
glog.V(logger.Info).Infoln("Ethereum protocol handler stopped") glog.V(logger.Info).Infoln("Ethereum protocol handler stopped")
} }
func (pm *ProtocolManager) newPeer(pv, nv int, p *p2p.Peer, rw p2p.MsgReadWriter) *peer { func (pm *ProtocolManager) newPeer(pv int, p *p2p.Peer, rw p2p.MsgReadWriter) *peer {
return newPeer(pv, nv, p, newMeteredMsgWriter(rw)) return newPeer(pv, p, newMeteredMsgWriter(rw))
} }
// handle is the callback invoked to manage the life cycle of an eth peer. When // handle is the callback invoked to manage the life cycle of an eth peer. When
...@@ -199,7 +212,7 @@ func (pm *ProtocolManager) handle(p *peer) error { ...@@ -199,7 +212,7 @@ func (pm *ProtocolManager) handle(p *peer) error {
// Execute the Ethereum handshake // Execute the Ethereum handshake
td, head, genesis := pm.blockchain.Status() td, head, genesis := pm.blockchain.Status()
if err := p.Handshake(td, head, genesis); err != nil { if err := p.Handshake(pm.networkId, td, head, genesis); err != nil {
glog.V(logger.Debug).Infof("%v: handshake failed: %v", p, err) glog.V(logger.Debug).Infof("%v: handshake failed: %v", p, err)
return err return err
} }
...@@ -730,3 +743,22 @@ func (self *ProtocolManager) txBroadcastLoop() { ...@@ -730,3 +743,22 @@ func (self *ProtocolManager) txBroadcastLoop() {
self.BroadcastTx(event.Tx.Hash(), event.Tx) self.BroadcastTx(event.Tx.Hash(), event.Tx)
} }
} }
// EthNodeInfo represents a short summary of the Ethereum sub-protocol metadata known
// about the host peer.
type EthNodeInfo struct {
Network int `json:"network"` // Ethereum network ID (0=Olympic, 1=Frontier, 2=Morden)
Difficulty *big.Int `json:"difficulty"` // Total difficulty of the host's blockchain
Genesis string `json:"genesis"` // SHA3 hash of the host's genesis block
Head string `json:"head"` // SHA3 hash of the host's best owned block
}
// NodeInfo retrieves some protocol metadata about the running host node.
func (self *ProtocolManager) NodeInfo() *EthNodeInfo {
return &EthNodeInfo{
Network: self.networkId,
Difficulty: self.blockchain.GetTd(self.blockchain.CurrentBlock().Hash()),
Genesis: fmt.Sprintf("%x", self.blockchain.Genesis().Hash()),
Head: fmt.Sprintf("%x", self.blockchain.CurrentBlock().Hash()),
}
}
...@@ -117,7 +117,7 @@ func newTestPeer(name string, version int, pm *ProtocolManager, shake bool) (*te ...@@ -117,7 +117,7 @@ func newTestPeer(name string, version int, pm *ProtocolManager, shake bool) (*te
var id discover.NodeID var id discover.NodeID
rand.Read(id[:]) rand.Read(id[:])
peer := pm.newPeer(version, NetworkId, p2p.NewPeer(id, name, nil), net) peer := pm.newPeer(version, p2p.NewPeer(id, name, nil), net)
// Start the peer on a new thread // Start the peer on a new thread
errc := make(chan error, 1) errc := make(chan error, 1)
......
...@@ -44,16 +44,21 @@ const ( ...@@ -44,16 +44,21 @@ const (
handshakeTimeout = 5 * time.Second handshakeTimeout = 5 * time.Second
) )
// PeerInfo represents a short summary of the Ethereum sub-protocol metadata known
// about a connected peer.
type PeerInfo struct {
Version int `json:"version"` // Ethereum protocol version negotiated
Difficulty *big.Int `json:"difficulty"` // Total difficulty of the peer's blockchain
Head string `json:"head"` // SHA3 hash of the peer's best owned block
}
type peer struct { type peer struct {
*p2p.Peer id string
*p2p.Peer
rw p2p.MsgReadWriter rw p2p.MsgReadWriter
version int // Protocol version negotiated version int // Protocol version negotiated
network int // Network ID being on
id string
head common.Hash head common.Hash
td *big.Int td *big.Int
lock sync.RWMutex lock sync.RWMutex
...@@ -62,20 +67,28 @@ type peer struct { ...@@ -62,20 +67,28 @@ type peer struct {
knownBlocks *set.Set // Set of block hashes known to be known by this peer knownBlocks *set.Set // Set of block hashes known to be known by this peer
} }
func newPeer(version, network int, p *p2p.Peer, rw p2p.MsgReadWriter) *peer { func newPeer(version int, p *p2p.Peer, rw p2p.MsgReadWriter) *peer {
id := p.ID() id := p.ID()
return &peer{ return &peer{
Peer: p, Peer: p,
rw: rw, rw: rw,
version: version, version: version,
network: network,
id: fmt.Sprintf("%x", id[:8]), id: fmt.Sprintf("%x", id[:8]),
knownTxs: set.New(), knownTxs: set.New(),
knownBlocks: set.New(), knownBlocks: set.New(),
} }
} }
// Info gathers and returns a collection of metadata known about a peer.
func (p *peer) Info() *PeerInfo {
return &PeerInfo{
Version: p.version,
Difficulty: p.Td(),
Head: fmt.Sprintf("%x", p.Head()),
}
}
// Head retrieves a copy of the current head (most recent) hash of the peer. // Head retrieves a copy of the current head (most recent) hash of the peer.
func (p *peer) Head() (hash common.Hash) { func (p *peer) Head() (hash common.Hash) {
p.lock.RLock() p.lock.RLock()
...@@ -268,20 +281,22 @@ func (p *peer) RequestReceipts(hashes []common.Hash) error { ...@@ -268,20 +281,22 @@ func (p *peer) RequestReceipts(hashes []common.Hash) error {
// Handshake executes the eth protocol handshake, negotiating version number, // Handshake executes the eth protocol handshake, negotiating version number,
// network IDs, difficulties, head and genesis blocks. // network IDs, difficulties, head and genesis blocks.
func (p *peer) Handshake(td *big.Int, head common.Hash, genesis common.Hash) error { func (p *peer) Handshake(network int, td *big.Int, head common.Hash, genesis common.Hash) error {
// Send out own handshake in a new thread
errc := make(chan error, 2) errc := make(chan error, 2)
var status statusData // safe to read after two values have been received from errc var status statusData // safe to read after two values have been received from errc
go func() { go func() {
errc <- p2p.Send(p.rw, StatusMsg, &statusData{ errc <- p2p.Send(p.rw, StatusMsg, &statusData{
ProtocolVersion: uint32(p.version), ProtocolVersion: uint32(p.version),
NetworkId: uint32(p.network), NetworkId: uint32(network),
TD: td, TD: td,
CurrentBlock: head, CurrentBlock: head,
GenesisBlock: genesis, GenesisBlock: genesis,
}) })
}() }()
go func() { go func() {
errc <- p.readStatus(&status, genesis) errc <- p.readStatus(network, &status, genesis)
}() }()
timeout := time.NewTimer(handshakeTimeout) timeout := time.NewTimer(handshakeTimeout)
defer timeout.Stop() defer timeout.Stop()
...@@ -299,7 +314,7 @@ func (p *peer) Handshake(td *big.Int, head common.Hash, genesis common.Hash) err ...@@ -299,7 +314,7 @@ func (p *peer) Handshake(td *big.Int, head common.Hash, genesis common.Hash) err
return nil return nil
} }
func (p *peer) readStatus(status *statusData, genesis common.Hash) (err error) { func (p *peer) readStatus(network int, status *statusData, genesis common.Hash) (err error) {
msg, err := p.rw.ReadMsg() msg, err := p.rw.ReadMsg()
if err != nil { if err != nil {
return err return err
...@@ -317,8 +332,8 @@ func (p *peer) readStatus(status *statusData, genesis common.Hash) (err error) { ...@@ -317,8 +332,8 @@ func (p *peer) readStatus(status *statusData, genesis common.Hash) (err error) {
if status.GenesisBlock != genesis { if status.GenesisBlock != genesis {
return errResp(ErrGenesisBlockMismatch, "%x (!= %x)", status.GenesisBlock, genesis) return errResp(ErrGenesisBlockMismatch, "%x (!= %x)", status.GenesisBlock, genesis)
} }
if int(status.NetworkId) != p.network { if int(status.NetworkId) != network {
return errResp(ErrNetworkIdMismatch, "%d (!= %d)", status.NetworkId, p.network) return errResp(ErrNetworkIdMismatch, "%d (!= %d)", status.NetworkId, network)
} }
if int(status.ProtocolVersion) != p.version { if int(status.ProtocolVersion) != p.version {
return errResp(ErrProtocolVersionMismatch, "%d (!= %d)", status.ProtocolVersion, p.version) return errResp(ErrProtocolVersionMismatch, "%d (!= %d)", status.ProtocolVersion, p.version)
......
...@@ -33,6 +33,9 @@ const ( ...@@ -33,6 +33,9 @@ const (
eth63 = 63 eth63 = 63
) )
// Official short name of the protocol used during capability negotiation.
var ProtocolName = "eth"
// Supported versions of the eth protocol (first is primary). // Supported versions of the eth protocol (first is primary).
var ProtocolVersions = []uint{eth63, eth62, eth61} var ProtocolVersions = []uint{eth63, eth62, eth61}
......
...@@ -40,8 +40,8 @@ func TestFastSyncDisabling(t *testing.T) { ...@@ -40,8 +40,8 @@ func TestFastSyncDisabling(t *testing.T) {
// Sync up the two peers // Sync up the two peers
io1, io2 := p2p.MsgPipe() io1, io2 := p2p.MsgPipe()
go pmFull.handle(pmFull.newPeer(63, NetworkId, p2p.NewPeer(discover.NodeID{}, "empty", nil), io2)) go pmFull.handle(pmFull.newPeer(63, p2p.NewPeer(discover.NodeID{}, "empty", nil), io2))
go pmEmpty.handle(pmEmpty.newPeer(63, NetworkId, p2p.NewPeer(discover.NodeID{}, "full", nil), io1)) go pmEmpty.handle(pmEmpty.newPeer(63, p2p.NewPeer(discover.NodeID{}, "full", nil), io1))
time.Sleep(250 * time.Millisecond) time.Sleep(250 * time.Millisecond)
pmEmpty.synchronise(pmEmpty.peers.BestPeer()) pmEmpty.synchronise(pmEmpty.peers.BestPeer())
......
...@@ -359,3 +359,49 @@ func (rw *protoRW) ReadMsg() (Msg, error) { ...@@ -359,3 +359,49 @@ func (rw *protoRW) ReadMsg() (Msg, error) {
return Msg{}, io.EOF return Msg{}, io.EOF
} }
} }
// PeerInfo represents a short summary of the information known about a connected
// peer. Sub-protocol independent fields are contained and initialized here, with
// protocol specifics delegated to all connected sub-protocols.
type PeerInfo struct {
ID string `json:"id"` // Unique node identifier (also the encryption key)
Name string `json:"name"` // Name of the node, including client type, version, OS, custom data
Caps []string `json:"caps"` // Sum-protocols advertised by this particular peer
Network struct {
LocalAddress string `json:"localAddress"` // Local endpoint of the TCP data connection
RemoteAddress string `json:"remoteAddress"` // Remote endpoint of the TCP data connection
} `json:"network"`
Protocols map[string]interface{} `json:"protocols"` // Sub-protocol specific metadata fields
}
// Info gathers and returns a collection of metadata known about a peer.
func (p *Peer) Info() *PeerInfo {
// Gather the protocol capabilities
var caps []string
for _, cap := range p.Caps() {
caps = append(caps, cap.String())
}
// Assemble the generic peer metadata
info := &PeerInfo{
ID: p.ID().String(),
Name: p.Name(),
Caps: caps,
Protocols: make(map[string]interface{}),
}
info.Network.LocalAddress = p.LocalAddr().String()
info.Network.RemoteAddress = p.RemoteAddr().String()
// Gather all the running protocol infos
for _, proto := range p.running {
protoInfo := interface{}("unknown")
if query := proto.Protocol.PeerInfo; query != nil {
if metadata := query(p.ID()); metadata != nil {
protoInfo = metadata
} else {
protoInfo = "handshake"
}
}
info.Protocols[proto.Name] = protoInfo
}
return info
}
...@@ -16,7 +16,11 @@ ...@@ -16,7 +16,11 @@
package p2p package p2p
import "fmt" import (
"fmt"
"github.com/ethereum/go-ethereum/p2p/discover"
)
// Protocol represents a P2P subprotocol implementation. // Protocol represents a P2P subprotocol implementation.
type Protocol struct { type Protocol struct {
...@@ -39,6 +43,15 @@ type Protocol struct { ...@@ -39,6 +43,15 @@ type Protocol struct {
// any protocol-level error (such as an I/O error) that is // any protocol-level error (such as an I/O error) that is
// encountered. // encountered.
Run func(peer *Peer, rw MsgReadWriter) error Run func(peer *Peer, rw MsgReadWriter) error
// NodeInfo is an optional helper method to retrieve protocol specific metadata
// about the host node.
NodeInfo func() interface{}
// PeerInfo is an optional helper method to retrieve protocol specific metadata
// about a certain peer in the network. If an info retrieval function is set,
// but returns nil, it is assumed that the protocol handshake is still running.
PeerInfo func(id discover.NodeID) interface{}
} }
func (p Protocol) cap() Cap { func (p Protocol) cap() Cap {
......
...@@ -689,3 +689,66 @@ func (srv *Server) runPeer(p *Peer) { ...@@ -689,3 +689,66 @@ func (srv *Server) runPeer(p *Peer) {
NumConnections: srv.PeerCount(), NumConnections: srv.PeerCount(),
}) })
} }
// NodeInfo represents a short summary of the information known about the host.
type NodeInfo struct {
ID string `json:"id"` // Unique node identifier (also the encryption key)
Name string `json:"name"` // Name of the node, including client type, version, OS, custom data
Enode string `json:"enode"` // Enode URL for adding this peer from remote peers
IP string `json:"ip"` // IP address of the node
Ports struct {
Discovery int `json:"discovery"` // UDP listening port for discovery protocol
Listener int `json:"listener"` // TCP listening port for RLPx
} `json:"ports"`
ListenAddr string `json:"listenAddr"`
Protocols map[string]interface{} `json:"protocols"`
}
// Info gathers and returns a collection of metadata known about the host.
func (srv *Server) NodeInfo() *NodeInfo {
node := srv.Self()
// Gather and assemble the generic node infos
info := &NodeInfo{
Name: srv.Name,
Enode: node.String(),
ID: node.ID.String(),
IP: node.IP.String(),
ListenAddr: srv.ListenAddr,
Protocols: make(map[string]interface{}),
}
info.Ports.Discovery = int(node.UDP)
info.Ports.Listener = int(node.TCP)
// Gather all the running protocol infos (only once per protocol type)
for _, proto := range srv.Protocols {
if _, ok := info.Protocols[proto.Name]; !ok {
nodeInfo := interface{}("unknown")
if query := proto.NodeInfo; query != nil {
nodeInfo = proto.NodeInfo()
}
info.Protocols[proto.Name] = nodeInfo
}
}
return info
}
// PeersInfo returns an array of metadata objects describing connected peers.
func (srv *Server) PeersInfo() []*PeerInfo {
// Gather all the generic and sub-protocol specific infos
infos := make([]*PeerInfo, 0, srv.PeerCount())
for _, peer := range srv.Peers() {
if peer != nil {
infos = append(infos, peer.Info())
}
}
// Sort the result array alphabetically by node identifier
for i := 0; i < len(infos); i++ {
for j := i + 1; j < len(infos); j++ {
if infos[i].ID > infos[j].ID {
infos[i], infos[j] = infos[j], infos[i]
}
}
}
return infos
}
...@@ -32,6 +32,7 @@ import ( ...@@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/logger/glog"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc/codec" "github.com/ethereum/go-ethereum/rpc/codec"
"github.com/ethereum/go-ethereum/rpc/comms" "github.com/ethereum/go-ethereum/rpc/comms"
...@@ -80,15 +81,17 @@ type adminhandler func(*adminApi, *shared.Request) (interface{}, error) ...@@ -80,15 +81,17 @@ type adminhandler func(*adminApi, *shared.Request) (interface{}, error)
// admin api provider // admin api provider
type adminApi struct { type adminApi struct {
xeth *xeth.XEth xeth *xeth.XEth
network *p2p.Server
ethereum *eth.Ethereum ethereum *eth.Ethereum
codec codec.Codec codec codec.Codec
coder codec.ApiCoder coder codec.ApiCoder
} }
// create a new admin api instance // create a new admin api instance
func NewAdminApi(xeth *xeth.XEth, ethereum *eth.Ethereum, codec codec.Codec) *adminApi { func NewAdminApi(xeth *xeth.XEth, network *p2p.Server, ethereum *eth.Ethereum, codec codec.Codec) *adminApi {
return &adminApi{ return &adminApi{
xeth: xeth, xeth: xeth,
network: network,
ethereum: ethereum, ethereum: ethereum,
codec: codec, codec: codec,
coder: codec.New(nil), coder: codec.New(nil),
...@@ -137,11 +140,11 @@ func (self *adminApi) AddPeer(req *shared.Request) (interface{}, error) { ...@@ -137,11 +140,11 @@ func (self *adminApi) AddPeer(req *shared.Request) (interface{}, error) {
} }
func (self *adminApi) Peers(req *shared.Request) (interface{}, error) { func (self *adminApi) Peers(req *shared.Request) (interface{}, error) {
return self.ethereum.PeersInfo(), nil return self.network.PeersInfo(), nil
} }
func (self *adminApi) NodeInfo(req *shared.Request) (interface{}, error) { func (self *adminApi) NodeInfo(req *shared.Request) (interface{}, error) {
return self.ethereum.NodeInfo(), nil return self.network.NodeInfo(), nil
} }
func (self *adminApi) DataDir(req *shared.Request) (interface{}, error) { func (self *adminApi) DataDir(req *shared.Request) (interface{}, error) {
......
...@@ -165,7 +165,7 @@ func ParseApiString(apistr string, codec codec.Codec, xeth *xeth.XEth, eth *eth. ...@@ -165,7 +165,7 @@ func ParseApiString(apistr string, codec codec.Codec, xeth *xeth.XEth, eth *eth.
for i, name := range names { for i, name := range names {
switch strings.ToLower(strings.TrimSpace(name)) { switch strings.ToLower(strings.TrimSpace(name)) {
case shared.AdminApiName: case shared.AdminApiName:
apis[i] = NewAdminApi(xeth, eth, codec) apis[i] = NewAdminApi(xeth, eth.Network(), eth, codec)
case shared.DebugApiName: case shared.DebugApiName:
apis[i] = NewDebugApi(xeth, eth, codec) apis[i] = NewDebugApi(xeth, eth, codec)
case shared.DbApiName: case shared.DbApiName:
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册