websocket server sends pings; added ws_client; events refactor

This commit is contained in:
Jae Kwon 2015-08-04 13:15:10 -07:00
parent 37c68e838e
commit 60310cc23f
11 changed files with 243 additions and 159 deletions

View File

@ -10,6 +10,7 @@ install:
go install github.com/tendermint/tendermint/cmd/debora
go install github.com/tendermint/tendermint/cmd/stdinwriter
go install github.com/tendermint/tendermint/cmd/logjack
go install github.com/tendermint/tendermint/cmd/sim_txs
@echo `git rev-parse --verify HEAD` >> $(TMROOT)/revisions
build:
@ -18,6 +19,7 @@ build:
go build -o build/debora github.com/tendermint/tendermint/cmd/debora
go build -o build/stdinwriter github.com/tendermint/tendermint/cmd/stdinwriter
go build -o build/logjack github.com/tendermint/tendermint/cmd/logjack
go build -o build/sim_txs github.com/tendermint/tendermint/cmd/sim_txs
build_race:
go build -race -o build/tendermint github.com/tendermint/tendermint/cmd/tendermint
@ -25,6 +27,7 @@ build_race:
go build -race -o build/debora github.com/tendermint/tendermint/cmd/debora
go build -race -o build/stdinwriter github.com/tendermint/tendermint/cmd/stdinwriter
go build -race -o build/logjack github.com/tendermint/tendermint/cmd/logjack
go build -race -o build/sim_txs github.com/tendermint/tendermint/cmd/sim_txs
test: build
-rm -rf ~/.tendermint_test_bak

View File

@ -1,11 +1,9 @@
package main
import (
"bytes"
"encoding/hex"
"flag"
"fmt"
"time"
acm "github.com/tendermint/tendermint/account"
. "github.com/tendermint/tendermint/common"
@ -18,26 +16,26 @@ const Version = "0.0.1"
const sleepSeconds = 1 // Every second
// Parse command-line options
func parseFlags() (privKeyHex string, numAccounts int, remote string, version bool) {
func parseFlags() (privKeyHex string, numAccounts int, remote string) {
var version bool
flag.StringVar(&privKeyHex, "priv-key", "", "Private key bytes in HEX")
flag.IntVar(&numAccounts, "num-accounts", 1000, "Deterministically generates this many sub-accounts")
flag.StringVar(&remote, "remote", "http://localhost:46657", "Remote RPC host:port")
flag.BoolVar(&version, "version", false, "Version")
flag.Parse()
if version {
Exit(Fmt("sim_txs version %v", Version))
}
return
}
func main() {
// Read options
privKeyHex, numAccounts, remote, version := parseFlags()
if version {
fmt.Println(Fmt("sim_txs version %v", Version))
return
}
privKeyHex, numAccounts, remote := parseFlags()
// Print args.
// fmt.Println(privKeyHex, numAccounts, remote, version)
// fmt.Println(privKeyHex, numAccounts, remote)
privKeyBytes, err := hex.DecodeString(privKeyHex)
if err != nil {
@ -57,33 +55,55 @@ func main() {
fmt.Println("Root account", rootAccount)
}
go func() {
// Construct a new send Tx
accounts := make([]*acm.Account, numAccounts)
privAccounts := make([]*acm.PrivAccount, numAccounts)
for i := 0; i < numAccounts; i++ {
privAccounts[i] = root.Generate(i)
account, err := getAccount(remote, privAccounts[i].Address)
if err != nil {
fmt.Println("Error", err)
return
} else {
accounts[i] = account
}
// Load all accounts
accounts := make([]*acm.Account, numAccounts+1)
accounts[0] = rootAccount
privAccounts := make([]*acm.PrivAccount, numAccounts+1)
privAccounts[0] = root
for i := 1; i < numAccounts; i++ {
privAccounts[i] = root.Generate(i)
account, err := getAccount(remote, privAccounts[i].Address)
if err != nil {
fmt.Println("Error", err)
return
} else {
accounts[i] = account
}
}
// Test: send from root to accounts[1]
sendTx := makeRandomTransaction(10, rootAccount.Sequence+1, root, 2, accounts)
fmt.Println(sendTx)
wsClient, err := rpcclient.NewWSClient("http://localhost:46657/websocket")
if err != nil {
Exit(Fmt("Failed to establish websocket connection: %v", err))
}
wsClient.Subscribe(types.EventStringAccInput(sendTx.Outputs[0].Address))
go func() {
for {
sendTx := makeRandomTransaction(rootAccount, root, accounts, privAccounts)
// Broadcast it.
err := broadcastSendTx(remote, sendTx)
if err != nil {
Exit(Fmt("Failed to broadcast SendTx: %v", err))
return
}
// Broadcast 1 tx!
time.Sleep(10 * time.Millisecond)
foo := <-wsClient.EventsCh
fmt.Println("!!", foo)
}
}()
/*
go func() {
for {
sendTx := makeRandomTransaction(rootAccount, root, accounts, privAccounts)
// Broadcast it.
err := broadcastSendTx(remote, sendTx)
if err != nil {
Exit(Fmt("Failed to broadcast SendTx: %v", err))
return
}
// Broadcast 1 tx!
time.Sleep(10 * time.Millisecond)
}
}()
*/
// Trap signal
TrapSignal(func() {
fmt.Println("sim_txs shutting down")
@ -112,64 +132,43 @@ func broadcastSendTx(remote string, sendTx *types.SendTx) error {
return nil
}
func makeRandomTransaction(rootAccount *acm.Account, rootPrivAccount *acm.PrivAccount, accounts []*acm.Account, privAccounts []*acm.PrivAccount) *types.SendTx {
allAccounts := append(accounts, rootAccount)
allPrivAccounts := append(privAccounts, rootPrivAccount)
// Make a random send transaction from srcIndex to N other accounts.
// balance: balance to send from input
// sequence: sequence to sign with
// inputPriv: input privAccount
func makeRandomTransaction(balance int64, sequence int, inputPriv *acm.PrivAccount, sendCount int, accounts []*acm.Account) *types.SendTx {
// Find accout with the most money
inputBalance := int64(0)
inputAccount := (*acm.Account)(nil)
inputPrivAccount := (*acm.PrivAccount)(nil)
for i, account := range allAccounts {
if account == nil {
continue
}
if inputBalance < account.Balance {
inputBalance = account.Balance
inputAccount = account
inputPrivAccount = allPrivAccounts[i]
}
}
if inputAccount == nil {
Exit("No accounts have any money")
return nil
}
// Remember which accounts were chosen
accMap := map[string]struct{}{}
accMap[string(inputPriv.Address)] = struct{}{}
// Find a selection of accounts to send to
outputAccounts := map[string]*acm.Account{}
for i := 0; i < 2; i++ {
outputs := []*acm.Account{}
for i := 0; i < sendCount; i++ {
for {
idx := RandInt() % len(accounts)
if bytes.Equal(accounts[idx].Address, inputAccount.Address) {
account := accounts[idx]
if _, ok := accMap[string(account.Address)]; ok {
continue
}
if _, ok := outputAccounts[string(accounts[idx].Address)]; ok {
continue
}
outputAccounts[string(accounts[idx].Address)] = accounts[idx]
accMap[string(account.Address)] = struct{}{}
outputs = append(outputs, account)
break
}
}
// Construct SendTx
sendTx := types.NewSendTx()
err := sendTx.AddInputWithNonce(inputPrivAccount.PubKey, inputAccount.Balance, inputAccount.Sequence+1)
err := sendTx.AddInputWithNonce(inputPriv.PubKey, balance, sequence)
if err != nil {
panic(err)
}
for _, outputAccount := range outputAccounts {
sendTx.AddOutput(outputAccount.Address, inputAccount.Balance/int64(len(outputAccounts)))
// XXX FIXME???
outputAccount.Balance += inputAccount.Balance / int64(len(outputAccounts))
for _, output := range outputs {
sendTx.AddOutput(output.Address, balance/int64(len(outputs)))
}
// Sign SendTx
sendTx.SignInput("tendermint_testnet_7", 0, inputPrivAccount)
// Hack: Listen for events or create a new RPC call for this.
// XXX FIXME
inputAccount.Sequence += 1
inputAccount.Balance = 0 // FIXME???
sendTx.SignInput("tendermint_testnet_9", 0, inputPriv)
return sendTx
}

View File

@ -7,9 +7,9 @@ import (
"io/ioutil"
"net/http"
"github.com/tendermint/tendermint/wire"
. "github.com/tendermint/tendermint/common"
. "github.com/tendermint/tendermint/rpc/types"
"github.com/tendermint/tendermint/wire"
)
func Call(remote string, method string, params []interface{}, dest interface{}) (interface{}, error) {

102
rpc/client/ws_client.go Normal file
View File

@ -0,0 +1,102 @@
package rpcclient
import (
"encoding/json"
"net/http"
"strings"
"github.com/tendermint/tendermint/Godeps/_workspace/src/github.com/gorilla/websocket"
. "github.com/tendermint/tendermint/common"
_ "github.com/tendermint/tendermint/config/tendermint_test"
"github.com/tendermint/tendermint/rpc/types"
"github.com/tendermint/tendermint/wire"
)
const wsEventsChannelCapacity = 10
const wsResponsesChannelCapacity = 10
type WSClient struct {
QuitService
*websocket.Conn
EventsCh chan rpctypes.RPCEventResult
ResponsesCh chan rpctypes.RPCResponse
}
// create a new connection
func NewWSClient(addr string) (*WSClient, error) {
dialer := websocket.DefaultDialer
rHeader := http.Header{}
con, _, err := dialer.Dial(addr, rHeader)
if err != nil {
return nil, err
}
wsClient := &WSClient{
Conn: con,
EventsCh: make(chan rpctypes.RPCEventResult, wsEventsChannelCapacity),
ResponsesCh: make(chan rpctypes.RPCResponse, wsResponsesChannelCapacity),
}
wsClient.QuitService = *NewQuitService(log, "WSClient", wsClient)
return wsClient, nil
}
func (wsc *WSClient) OnStart() {
wsc.QuitService.OnStart()
go wsc.receiveEventsRoutine()
}
func (wsc *WSClient) OnStop() {
wsc.QuitService.OnStop()
}
func (wsc *WSClient) receiveEventsRoutine() {
for {
_, data, err := wsc.ReadMessage()
if err != nil {
log.Info("WSClient failed to read message: %v", err)
wsc.Stop()
break
} else {
var response rpctypes.RPCResponse
if err := json.Unmarshal(data, &response); err != nil {
log.Info("WSClient failed to parse message: %v", err)
wsc.Stop()
break
}
if strings.HasSuffix(response.Id, "#event") {
var eventResult rpctypes.RPCEventResult
var err error
wire.ReadJSONObject(&eventResult, response.Result, &err)
if err != nil {
log.Info("WSClient failed to parse RPCEventResult: %v", err)
wsc.Stop()
break
}
wsc.EventsCh <- eventResult
} else {
wsc.ResponsesCh <- response
}
}
}
}
// subscribe to an event
func (wsc *WSClient) Subscribe(eventid string) error {
err := wsc.WriteJSON(rpctypes.RPCRequest{
JSONRPC: "2.0",
Id: "",
Method: "subscribe",
Params: []interface{}{eventid},
})
return err
}
// unsubscribe from an event
func (wsc *WSClient) Unsubscribe(eventid string) error {
err := wsc.WriteJSON(rpctypes.RPCRequest{
JSONRPC: "2.0",
Id: "",
Method: "unsubscribe",
Params: []interface{}{eventid},
})
return err
}

View File

@ -12,10 +12,10 @@ import (
"time"
"github.com/tendermint/tendermint/Godeps/_workspace/src/github.com/gorilla/websocket"
"github.com/tendermint/tendermint/wire"
. "github.com/tendermint/tendermint/common"
"github.com/tendermint/tendermint/events"
. "github.com/tendermint/tendermint/rpc/types"
"github.com/tendermint/tendermint/wire"
)
func RegisterRPCFuncs(mux *http.ServeMux, funcMap map[string]*RPCFunc) {
@ -205,8 +205,9 @@ func _jsonStringToArg(ty reflect.Type, arg string) (reflect.Value, error) {
const (
writeChanCapacity = 20
WSWriteTimeoutSeconds = 10 // exposed for tests
WSReadTimeoutSeconds = 10 // exposed for tests
wsWriteTimeoutSeconds = 30 // each write times out after this
wsReadTimeoutSeconds = 30 // connection times out if we haven't received *anything* in this long, not even pings.
wsPingTickerSeconds = 10 // send a ping every PingTickerSeconds.
)
// a single websocket connection
@ -219,6 +220,7 @@ type WSConnection struct {
baseConn *websocket.Conn
writeChan chan RPCResponse
readTimeout *time.Timer
pingTicker *time.Timer
funcMap map[string]*RPCFunc
evsw *events.EventSwitch
@ -245,14 +247,15 @@ func (wsc *WSConnection) OnStart() {
go wsc.readRoutine()
// Custom Ping handler to touch readTimeout
wsc.readTimeout = time.NewTimer(time.Second * WSReadTimeoutSeconds)
wsc.readTimeout = time.NewTimer(time.Second * wsReadTimeoutSeconds)
wsc.pingTicker = time.NewTimer(time.Second * wsPingTickerSeconds)
wsc.baseConn.SetPingHandler(func(m string) error {
wsc.baseConn.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*WSWriteTimeoutSeconds))
wsc.readTimeout.Reset(time.Second * WSReadTimeoutSeconds)
wsc.baseConn.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds))
wsc.readTimeout.Reset(time.Second * wsReadTimeoutSeconds)
return nil
})
wsc.baseConn.SetPongHandler(func(m string) error {
wsc.readTimeout.Reset(time.Second * WSReadTimeoutSeconds)
wsc.readTimeout.Reset(time.Second * wsReadTimeoutSeconds)
return nil
})
go wsc.readTimeoutRoutine()
@ -265,6 +268,7 @@ func (wsc *WSConnection) OnStop() {
wsc.QuitService.OnStop()
wsc.evsw.RemoveListener(wsc.id)
wsc.readTimeout.Stop()
wsc.pingTicker.Stop()
// The write loop closes the websocket connection
// when it exits its loop, and the read loop
// closes the writeChan
@ -302,7 +306,7 @@ func (wsc *WSConnection) readRoutine() {
default:
var in []byte
// Do not set a deadline here like below:
// wsc.baseConn.SetReadDeadline(time.Now().Add(time.Second * WSReadTimeoutSeconds))
// wsc.baseConn.SetReadDeadline(time.Now().Add(time.Second * wsReadTimeoutSeconds))
// The client may not send anything for a while.
// We use `readTimeout` to handle read timeouts.
_, in, err := wsc.baseConn.ReadMessage()
@ -332,7 +336,8 @@ func (wsc *WSConnection) readRoutine() {
} else {
log.Notice("Subscribe to event", "id", wsc.id, "event", event)
wsc.evsw.AddListenerForEvent(wsc.id, event, func(msg interface{}) {
wsc.writeRPCResponse(NewRPCResponse(request.Id, RPCEventResult{event, msg}, ""))
// NOTE: RPCResponses of subscribed events have id suffix "#event"
wsc.writeRPCResponse(NewRPCResponse(request.Id+"#event", RPCEventResult{event, msg}, ""))
})
continue
}
@ -340,6 +345,7 @@ func (wsc *WSConnection) readRoutine() {
if len(request.Params) == 0 {
log.Notice("Unsubscribe from all events", "id", wsc.id)
wsc.evsw.RemoveListener(wsc.id)
wsc.writeRPCResponse(NewRPCResponse(request.Id, nil, ""))
continue
} else if len(request.Params) == 1 {
if event, ok := request.Params[0].(string); !ok {
@ -348,6 +354,7 @@ func (wsc *WSConnection) readRoutine() {
} else {
log.Notice("Unsubscribe from event", "id", wsc.id, "event", event)
wsc.evsw.RemoveListenerForEvent(event, wsc.id)
wsc.writeRPCResponse(NewRPCResponse(request.Id, nil, ""))
continue
}
} else {
@ -383,19 +390,26 @@ func (wsc *WSConnection) readRoutine() {
// receives on a write channel and writes out on the socket
func (wsc *WSConnection) writeRoutine() {
defer wsc.baseConn.Close()
n, err := new(int64), new(error)
var n, err = int64(0), error(nil)
for {
select {
case <-wsc.Quit:
return
case <-wsc.pingTicker.C:
err := wsc.baseConn.WriteMessage(websocket.PingMessage, []byte{})
if err != nil {
log.Error("Failed to write ping message on websocket", "error", err)
wsc.Stop()
return
}
case msg := <-wsc.writeChan:
buf := new(bytes.Buffer)
wire.WriteJSON(msg, buf, n, err)
if *err != nil {
wire.WriteJSON(msg, buf, &n, &err)
if err != nil {
log.Error("Failed to marshal RPCResponse to JSON", "error", err)
} else {
wsc.baseConn.SetWriteDeadline(time.Now().Add(time.Second * WSWriteTimeoutSeconds))
if err := wsc.baseConn.WriteMessage(websocket.TextMessage, buf.Bytes()); err != nil {
wsc.baseConn.SetWriteDeadline(time.Now().Add(time.Second * wsWriteTimeoutSeconds))
if err = wsc.baseConn.WriteMessage(websocket.TextMessage, buf.Bytes()); err != nil {
log.Warn("Failed to write response on websocket", "error", err)
wsc.Stop()
return
@ -407,8 +421,9 @@ func (wsc *WSConnection) writeRoutine() {
//----------------------------------------
// main manager for all websocket connections
// holds the event switch
// Main manager for all websocket connections
// Holds the event switch
// NOTE: The websocket path is defined externally, e.g. in node/node.go
type WebsocketManager struct {
websocket.Upgrader
funcMap map[string]*RPCFunc

View File

@ -10,7 +10,6 @@ import (
"github.com/tendermint/tendermint/Godeps/_workspace/src/github.com/gorilla/websocket"
_ "github.com/tendermint/tendermint/config/tendermint_test"
"github.com/tendermint/tendermint/rpc/server"
"github.com/tendermint/tendermint/rpc/types"
"github.com/tendermint/tendermint/types"
"github.com/tendermint/tendermint/wire"
@ -66,20 +65,22 @@ func waitForEvent(t *testing.T, con *websocket.Conn, eventid string, dieOnTimeou
quitCh := make(chan struct{})
defer close(quitCh)
// Write pings repeatedly
// TODO: Maybe move this out to something that manages the con?
go func() {
pingTicker := time.NewTicker((time.Second * rpcserver.WSReadTimeoutSeconds) / 2)
for {
select {
case <-quitCh:
pingTicker.Stop()
return
case <-pingTicker.C:
con.WriteControl(websocket.PingMessage, []byte("whatevs"), time.Now().Add(time.Second))
/*
// TODO delete: we moved pinging to the server.
// Write pings repeatedly
go func() {
pingTicker := time.NewTicker((time.Second * rpcserver.WSReadTimeoutSeconds) / 2)
for {
select {
case <-quitCh:
pingTicker.Stop()
return
case <-pingTicker.C:
con.WriteControl(websocket.PingMessage, []byte("whatevs"), time.Now().Add(time.Second))
}
}
}
}()
}()
*/
// Read message
go func() {
@ -212,8 +213,8 @@ func unmarshalValidateSend(amt int64, toAddr []byte) func(string, []byte) error
JSONRPC string `json:"jsonrpc"`
Id string `json:"id"`
Result struct {
Event string `json:"event"`
Data *types.SendTx `json:"data"`
Event string `json:"event"`
Data types.EventMsgTx `json:"data"`
} `json:"result"`
Error string `json:"error"`
}
@ -228,7 +229,7 @@ func unmarshalValidateSend(amt int64, toAddr []byte) func(string, []byte) error
if eid != response.Result.Event {
return fmt.Errorf("Eventid is not correct. Got %s, expected %s", response.Result.Event, eid)
}
tx := response.Result.Data
tx := response.Result.Data.Tx.(*types.SendTx)
if bytes.Compare(tx.Inputs[0].Address, user[0].Address) != 0 {
return fmt.Errorf("Senders do not match up! Got %x, expected %x", tx.Inputs[0].Address, user[0].Address)
}

View File

@ -337,11 +337,11 @@ func ExecTx(blockCache *BlockCache, tx types.Tx, runCall bool, evc events.Fireab
// if the evc is nil, nothing will happen
if evc != nil {
for _, i := range tx.Inputs {
evc.FireEvent(types.EventStringAccInput(i.Address), tx)
evc.FireEvent(types.EventStringAccInput(i.Address), types.EventMsgTx{tx, nil, ""})
}
for _, o := range tx.Outputs {
evc.FireEvent(types.EventStringAccOutput(o.Address), tx)
evc.FireEvent(types.EventStringAccOutput(o.Address), types.EventMsgTx{tx, nil, ""})
}
}
return nil
@ -494,8 +494,8 @@ func ExecTx(blockCache *BlockCache, tx types.Tx, runCall bool, evc events.Fireab
if err != nil {
exception = err.Error()
}
evc.FireEvent(types.EventStringAccInput(tx.Input.Address), types.EventMsgCallTx{tx, ret, exception})
evc.FireEvent(types.EventStringAccOutput(tx.Address), types.EventMsgCallTx{tx, ret, exception})
evc.FireEvent(types.EventStringAccInput(tx.Input.Address), types.EventMsgTx{tx, ret, exception})
evc.FireEvent(types.EventStringAccOutput(tx.Address), types.EventMsgTx{tx, ret, exception})
}
} else {
// The mempool does not call txs until
@ -893,7 +893,7 @@ func ExecTx(blockCache *BlockCache, tx types.Tx, runCall bool, evc events.Fireab
}
if evc != nil {
evc.FireEvent(types.EventStringAccInput(tx.Input.Address), tx)
evc.FireEvent(types.EventStringAccInput(tx.Input.Address), types.EventMsgTx{tx, nil, ""})
evc.FireEvent(types.EventStringPermissions(ptypes.PermFlagToString(permFlag)), tx)
}

View File

@ -1073,7 +1073,7 @@ func execTxWaitEvent(t *testing.T, blockCache *BlockCache, tx types.Tx, eventid
}
switch ev := msg.(type) {
case types.EventMsgCallTx:
case types.EventMsgTx:
return ev, ev.Exception
case types.EventMsgCall:
return ev, ev.Exception

View File

@ -6,58 +6,22 @@ import (
// Functions to generate eventId strings
func EventStringAccInput(addr []byte) string {
return fmt.Sprintf("Acc/%X/Input", addr)
}
func EventStringAccOutput(addr []byte) string {
return fmt.Sprintf("Acc/%X/Output", addr)
}
func EventStringAccCall(addr []byte) string {
return fmt.Sprintf("Acc/%X/Call", addr)
}
func EventStringLogEvent(addr []byte) string {
return fmt.Sprintf("Log/%X", addr)
}
func EventStringPermissions(name string) string {
return fmt.Sprintf("Permissions/%s", name)
}
func EventStringNameReg(name string) string {
return fmt.Sprintf("NameReg/%s", name)
}
func EventStringBond() string {
return "Bond"
}
func EventStringUnbond() string {
return "Unbond"
}
func EventStringRebond() string {
return "Rebond"
}
func EventStringDupeout() string {
return "Dupeout"
}
func EventStringNewBlock() string {
return "NewBlock"
}
func EventStringFork() string {
return "Fork"
}
func EventStringAccInput(addr []byte) string { return fmt.Sprintf("Acc/%X/Input", addr) }
func EventStringAccOutput(addr []byte) string { return fmt.Sprintf("Acc/%X/Output", addr) }
func EventStringAccCall(addr []byte) string { return fmt.Sprintf("Acc/%X/Call", addr) }
func EventStringLogEvent(addr []byte) string { return fmt.Sprintf("Log/%X", addr) }
func EventStringPermissions(name string) string { return fmt.Sprintf("Permissions/%s", name) }
func EventStringBond() string { return "Bond" }
func EventStringUnbond() string { return "Unbond" }
func EventStringRebond() string { return "Rebond" }
func EventStringDupeout() string { return "Dupeout" }
func EventStringNewBlock() string { return "NewBlock" }
func EventStringFork() string { return "Fork" }
// Most event messages are basic types (a block, a transaction)
// but some (an input to a call tx or a receive) are more exotic:
type EventMsgCallTx struct {
type EventMsgTx struct {
Tx Tx `json:"tx"`
Return []byte `json:"return"`
Exception string `json:"exception"`

View File

@ -175,7 +175,7 @@ func runVMWaitEvents(t *testing.T, ourVm *VM, caller, callee *Account, subscribe
}()
msg := <-ch
switch ev := msg.(type) {
case types.EventMsgCallTx:
case types.EventMsgTx:
return ev.Exception
case types.EventMsgCall:
return ev.Exception