From 41c230f180db3349baaa4b1ebba657e4a651d656 Mon Sep 17 00:00:00 2001 From: cah Date: Mon, 28 Jul 2025 17:22:12 +0200 Subject: [PATCH 1/7] Fix race condition during CSM message exchange --- options/tcpOptions.go | 16 +++++++++++++++- tcp/client.go | 21 +++++++++++++++++++++ tcp/client/config.go | 1 + tcp/client/conn.go | 28 ++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/options/tcpOptions.go b/options/tcpOptions.go index ec2f9fb6..becb37c3 100644 --- a/options/tcpOptions.go +++ b/options/tcpOptions.go @@ -2,6 +2,7 @@ package options import ( "crypto/tls" + "time" tcpClient "github.com/plgd-dev/go-coap/v3/tcp/client" tcpServer "github.com/plgd-dev/go-coap/v3/tcp/server" @@ -34,11 +35,24 @@ func (o DisableTCPSignalMessageCSMOpt) TCPClientApply(cfg *tcpClient.Config) { cfg.DisableTCPSignalMessageCSM = true } -// WithDisableTCPSignalMessageCSM don't send CSM when client conn is created. func WithDisableTCPSignalMessageCSM() DisableTCPSignalMessageCSMOpt { return DisableTCPSignalMessageCSMOpt{} } +type CSMExchangeTimeoutOpt struct { + timeout time.Duration +} + +func (o CSMExchangeTimeoutOpt) TCPServerApply(cfg *tcpClient.Config) { + cfg.CSMExchangeTimeout = o.timeout +} + +func WithCSMExchangeTimeout(timeout time.Duration) CSMExchangeTimeoutOpt { + return CSMExchangeTimeoutOpt{ + timeout: timeout, + } +} + // TLSOpt tls configuration option. type TLSOpt struct { tlsCfg *tls.Config diff --git a/tcp/client.go b/tcp/client.go index 54349433..6ba352d4 100644 --- a/tcp/client.go +++ b/tcp/client.go @@ -7,6 +7,7 @@ import ( "time" "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/plgd-dev/go-coap/v3/message/pool" coapNet "github.com/plgd-dev/go-coap/v3/net" "github.com/plgd-dev/go-coap/v3/net/blockwise" @@ -100,6 +101,17 @@ func Client(conn net.Conn, opts ...Option) *client.Conn { return cc.Context().Err() == nil }) + var csmExchangeDone chan struct{} + if cfg.CSMExchangeTimeout != 0 && !cfg.DisablePeerTCPSignalMessageCSMs { + csmExchangeDone = make(chan struct{}) + + cc.OnTCPSignalReceivedHandler(func(code codes.Code) { + if code == codes.CSM { + close(csmExchangeDone) + } + }) + } + go func() { err := cc.Run() if err != nil { @@ -107,5 +119,14 @@ func Client(conn net.Conn, opts ...Option) *client.Conn { } }() + // if CSM messages are enabled, wait for the CSM messages to be exchanged + if cfg.CSMExchangeTimeout != 0 && !cfg.DisablePeerTCPSignalMessageCSMs { + select { + case <-time.After(cfg.CSMExchangeTimeout): + cfg.Errors(fmt.Errorf("%v: timeout waiting for tcp signal csm exchange", cc.RemoteAddr())) + case <-csmExchangeDone: + } + } + return cc } diff --git a/tcp/client/config.go b/tcp/client/config.go index b3c71857..0eda7c9e 100644 --- a/tcp/client/config.go +++ b/tcp/client/config.go @@ -50,4 +50,5 @@ type Config struct { DisablePeerTCPSignalMessageCSMs bool CloseSocket bool DisableTCPSignalMessageCSM bool + CSMExchangeTimeout time.Duration } diff --git a/tcp/client/conn.go b/tcp/client/conn.go index 018ead4d..b430fc54 100644 --- a/tcp/client/conn.go +++ b/tcp/client/conn.go @@ -27,6 +27,8 @@ type InactivityMonitor interface { CheckInactivity(now time.Time, cc *Conn) } +type TCPSignalReceivedHandler func(codes.Code) + type ( HandlerFunc = func(*responsewriter.ResponseWriter[*Conn], *pool.Message) ErrorFunc = func(error) @@ -51,6 +53,7 @@ type Conn struct { blockwiseSZX blockwise.SZX peerMaxMessageSize atomic.Uint32 disablePeerTCPSignalMessageCSMs bool + tcpSignalReceivedHandler TCPSignalReceivedHandler peerBlockWiseTranferEnabled atomic.Bool receivedMessageReader *client.ReceivedMessageReader[*Conn] @@ -267,6 +270,10 @@ func (cc *Conn) Run() (err error) { return cc.session.Run(cc) } +func (cc *Conn) OnTCPSignalReceivedHandler(handler TCPSignalReceivedHandler) { + cc.tcpSignalReceivedHandler = handler +} + // AddOnClose calls function on close connection event. func (cc *Conn) AddOnClose(f EventFunc) { cc.session.AddOnClose(f) @@ -382,6 +389,11 @@ func (cc *Conn) handleSignals(r *pool.Message) bool { if r.HasOption(message.TCPBlockWiseTransfer) { cc.peerBlockWiseTranferEnabled.Store(true) } + + // signal CSM message is received. + if cc.tcpSignalReceivedHandler != nil { + cc.tcpSignalReceivedHandler(codes.CSM) + } return true case codes.Ping: // if r.HasOption(message.TCPCustody) { @@ -390,21 +402,37 @@ func (cc *Conn) handleSignals(r *pool.Message) bool { if err := cc.sendPong(r.Token()); err != nil && !coapNet.IsConnectionBrokenError(err) { cc.Session().errors(fmt.Errorf("cannot handle ping signal: %w", err)) } + + if cc.tcpSignalReceivedHandler != nil { + cc.tcpSignalReceivedHandler(codes.Ping) + } return true case codes.Release: // if r.HasOption(message.TCPAlternativeAddress) { // TODO // } + + if cc.disablePeerTCPSignalMessageCSMs { + cc.tcpSignalReceivedHandler(codes.Release) + } return true case codes.Abort: // if r.HasOption(message.TCPBadCSMOption) { // TODO // } + + if cc.disablePeerTCPSignalMessageCSMs { + cc.tcpSignalReceivedHandler(codes.Abort) + } return true case codes.Pong: if h, ok := cc.tokenHandlerContainer.LoadAndDelete(r.Token().Hash()); ok { cc.processReceivedMessage(r, cc, h) } + + if cc.tcpSignalReceivedHandler != nil { + cc.tcpSignalReceivedHandler(codes.Pong) + } return true } return false From 8111d36deb8609cabbdef6cf262a69f77bdbd700 Mon Sep 17 00:00:00 2001 From: cah Date: Thu, 31 Jul 2025 09:55:09 +0200 Subject: [PATCH 2/7] Rename handler registration function and fix some issues --- options/tcpOptions.go | 2 +- tcp/client.go | 2 +- tcp/client/conn.go | 44 ++++++++++++++++++++++--------------------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/options/tcpOptions.go b/options/tcpOptions.go index becb37c3..9e8cf5a9 100644 --- a/options/tcpOptions.go +++ b/options/tcpOptions.go @@ -43,7 +43,7 @@ type CSMExchangeTimeoutOpt struct { timeout time.Duration } -func (o CSMExchangeTimeoutOpt) TCPServerApply(cfg *tcpClient.Config) { +func (o CSMExchangeTimeoutOpt) TCPClientApply(cfg *tcpClient.Config) { cfg.CSMExchangeTimeout = o.timeout } diff --git a/tcp/client.go b/tcp/client.go index 6ba352d4..cfa7480f 100644 --- a/tcp/client.go +++ b/tcp/client.go @@ -105,7 +105,7 @@ func Client(conn net.Conn, opts ...Option) *client.Conn { if cfg.CSMExchangeTimeout != 0 && !cfg.DisablePeerTCPSignalMessageCSMs { csmExchangeDone = make(chan struct{}) - cc.OnTCPSignalReceivedHandler(func(code codes.Code) { + cc.SetTCPSignalReceivedHandler(func(code codes.Code) { if code == codes.CSM { close(csmExchangeDone) } diff --git a/tcp/client/conn.go b/tcp/client/conn.go index b430fc54..9453ccde 100644 --- a/tcp/client/conn.go +++ b/tcp/client/conn.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "sync" "time" "github.com/plgd-dev/go-coap/v3/message" @@ -54,6 +55,7 @@ type Conn struct { peerMaxMessageSize atomic.Uint32 disablePeerTCPSignalMessageCSMs bool tcpSignalReceivedHandler TCPSignalReceivedHandler + handlerMutex sync.RWMutex peerBlockWiseTranferEnabled atomic.Bool receivedMessageReader *client.ReceivedMessageReader[*Conn] @@ -270,7 +272,9 @@ func (cc *Conn) Run() (err error) { return cc.session.Run(cc) } -func (cc *Conn) OnTCPSignalReceivedHandler(handler TCPSignalReceivedHandler) { +func (cc *Conn) SetTCPSignalReceivedHandler(handler TCPSignalReceivedHandler) { + cc.handlerMutex.Lock() + defer cc.handlerMutex.Unlock() cc.tcpSignalReceivedHandler = handler } @@ -377,6 +381,14 @@ func (cc *Conn) sendPong(token message.Token) error { return cc.Session().WriteMessage(req) } +func (cc *Conn) handleTcpSignalReceived(code codes.Code) { + cc.handlerMutex.RLock() + defer cc.handlerMutex.RUnlock() + if cc.tcpSignalReceivedHandler != nil { + cc.tcpSignalReceivedHandler(code) + } +} + func (cc *Conn) handleSignals(r *pool.Message) bool { switch r.Code() { case codes.CSM: @@ -391,9 +403,7 @@ func (cc *Conn) handleSignals(r *pool.Message) bool { } // signal CSM message is received. - if cc.tcpSignalReceivedHandler != nil { - cc.tcpSignalReceivedHandler(codes.CSM) - } + cc.handleTcpSignalReceived(codes.CSM) return true case codes.Ping: // if r.HasOption(message.TCPCustody) { @@ -403,36 +413,28 @@ func (cc *Conn) handleSignals(r *pool.Message) bool { cc.Session().errors(fmt.Errorf("cannot handle ping signal: %w", err)) } - if cc.tcpSignalReceivedHandler != nil { - cc.tcpSignalReceivedHandler(codes.Ping) + cc.handleTcpSignalReceived(codes.Ping) + return true + case codes.Pong: + if h, ok := cc.tokenHandlerContainer.LoadAndDelete(r.Token().Hash()); ok { + cc.processReceivedMessage(r, cc, h) } + + cc.handleTcpSignalReceived(codes.Pong) return true case codes.Release: // if r.HasOption(message.TCPAlternativeAddress) { // TODO // } - if cc.disablePeerTCPSignalMessageCSMs { - cc.tcpSignalReceivedHandler(codes.Release) - } + cc.handleTcpSignalReceived(codes.Release) return true case codes.Abort: // if r.HasOption(message.TCPBadCSMOption) { // TODO // } - if cc.disablePeerTCPSignalMessageCSMs { - cc.tcpSignalReceivedHandler(codes.Abort) - } - return true - case codes.Pong: - if h, ok := cc.tokenHandlerContainer.LoadAndDelete(r.Token().Hash()); ok { - cc.processReceivedMessage(r, cc, h) - } - - if cc.tcpSignalReceivedHandler != nil { - cc.tcpSignalReceivedHandler(codes.Pong) - } + cc.handleTcpSignalReceived(codes.Abort) return true } return false From cf9d4857792a0009cedcf6883b25ab2fe685836f Mon Sep 17 00:00:00 2001 From: cah Date: Thu, 31 Jul 2025 15:05:57 +0200 Subject: [PATCH 3/7] Return nil in case of timeout --- tcp/client.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tcp/client.go b/tcp/client.go index cfa7480f..d27f8e0b 100644 --- a/tcp/client.go +++ b/tcp/client.go @@ -123,9 +123,15 @@ func Client(conn net.Conn, opts ...Option) *client.Conn { if cfg.CSMExchangeTimeout != 0 && !cfg.DisablePeerTCPSignalMessageCSMs { select { case <-time.After(cfg.CSMExchangeTimeout): - cfg.Errors(fmt.Errorf("%v: timeout waiting for tcp signal csm exchange", cc.RemoteAddr())) + err := fmt.Errorf("%v: timeout waiting for CSM exchange with peer", cc.RemoteAddr()) + cfg.Errors(err) + cc.Close() // Close connection on timeout + return nil // or return cc with an error state case <-csmExchangeDone: + // CSM exchange completed successfully } + // Clear the handler after exchange is complete or timed out + cc.SetTCPSignalReceivedHandler(nil) } return cc From c58872da69587aeaac0428b4348fc5de0c6ea9db Mon Sep 17 00:00:00 2001 From: cah Date: Thu, 31 Jul 2025 15:12:56 +0200 Subject: [PATCH 4/7] Fix naming for linter --- tcp/client/conn.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tcp/client/conn.go b/tcp/client/conn.go index 9453ccde..1f8a0342 100644 --- a/tcp/client/conn.go +++ b/tcp/client/conn.go @@ -381,7 +381,7 @@ func (cc *Conn) sendPong(token message.Token) error { return cc.Session().WriteMessage(req) } -func (cc *Conn) handleTcpSignalReceived(code codes.Code) { +func (cc *Conn) handleTCPSignalReceived(code codes.Code) { cc.handlerMutex.RLock() defer cc.handlerMutex.RUnlock() if cc.tcpSignalReceivedHandler != nil { @@ -403,7 +403,7 @@ func (cc *Conn) handleSignals(r *pool.Message) bool { } // signal CSM message is received. - cc.handleTcpSignalReceived(codes.CSM) + cc.handleTCPSignalReceived(codes.CSM) return true case codes.Ping: // if r.HasOption(message.TCPCustody) { @@ -413,28 +413,28 @@ func (cc *Conn) handleSignals(r *pool.Message) bool { cc.Session().errors(fmt.Errorf("cannot handle ping signal: %w", err)) } - cc.handleTcpSignalReceived(codes.Ping) + cc.handleTCPSignalReceived(codes.Ping) return true case codes.Pong: if h, ok := cc.tokenHandlerContainer.LoadAndDelete(r.Token().Hash()); ok { cc.processReceivedMessage(r, cc, h) } - cc.handleTcpSignalReceived(codes.Pong) + cc.handleTCPSignalReceived(codes.Pong) return true case codes.Release: // if r.HasOption(message.TCPAlternativeAddress) { // TODO // } - cc.handleTcpSignalReceived(codes.Release) + cc.handleTCPSignalReceived(codes.Release) return true case codes.Abort: // if r.HasOption(message.TCPBadCSMOption) { // TODO // } - cc.handleTcpSignalReceived(codes.Abort) + cc.handleTCPSignalReceived(codes.Abort) return true } return false From b64cd82b85df614b78e051bc373e1c9e71d4f2a5 Mon Sep 17 00:00:00 2001 From: cah Date: Thu, 31 Jul 2025 15:28:53 +0200 Subject: [PATCH 5/7] Fix linter context issues --- net/conn_test.go | 3 ++- net/tlslistener_test.go | 19 ++++++++++++------- tcp/client.go | 7 ++++++- tcp/server_test.go | 3 ++- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/net/conn_test.go b/net/conn_test.go index 93e7d476..85ab479e 100644 --- a/net/conn_test.go +++ b/net/conn_test.go @@ -65,7 +65,8 @@ func TestConnWriteWithContext(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tcpConn, err := net.Dial("tcp", listener.Addr().String()) + dialer := net.Dialer{} + tcpConn, err := dialer.DialContext(context.Background(), "tcp", listener.Addr().String()) require.NoError(t, err) c := NewConn(tcpConn) defer func() { diff --git a/net/tlslistener_test.go b/net/tlslistener_test.go index c36ea990..bd25554c 100644 --- a/net/tlslistener_test.go +++ b/net/tlslistener_test.go @@ -100,12 +100,16 @@ func TestTLSListenerAcceptWithContext(t *testing.T) { cert, err := tls.X509KeyPair(CertPEMBlock, KeyPEMBlock) assert.NoError(t, err) - c, err := tls.DialWithDialer(&net.Dialer{ - Timeout: time.Millisecond * 400, - }, "tcp", listener.Addr().String(), &tls.Config{ - InsecureSkipVerify: true, - Certificates: []tls.Certificate{cert}, - }) + d := &tls.Dialer{ + NetDialer: &net.Dialer{ + Timeout: time.Millisecond * 400, + }, + Config: &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{cert}, + }, + } + c, err := d.DialContext(context.Background(), "tcp", listener.Addr().String()) if err != nil { continue } @@ -186,7 +190,8 @@ func TestTLSListenerCheckForInfinitLoop(t *testing.T) { cert, err := tls.X509KeyPair(CertPEMBlock, KeyPEMBlock) assert.NoError(t, err) func() { - conn, err := net.Dial("tcp", listener.Addr().String()) + dialer := net.Dialer{} + conn, err := dialer.DialContext(context.Background(), "tcp", listener.Addr().String()) if err != nil { return } diff --git a/tcp/client.go b/tcp/client.go index d27f8e0b..7bbaa801 100644 --- a/tcp/client.go +++ b/tcp/client.go @@ -1,6 +1,7 @@ package tcp import ( + "context" "crypto/tls" "fmt" "net" @@ -31,7 +32,11 @@ func Dial(target string, opts ...Option) (*client.Conn, error) { var conn net.Conn var err error if cfg.TLSCfg != nil { - conn, err = tls.DialWithDialer(cfg.Dialer, cfg.Net, target, cfg.TLSCfg) + d := &tls.Dialer{ + NetDialer: cfg.Dialer, + Config: cfg.TLSCfg, + } + conn, err = d.DialContext(context.Background(), cfg.Net, target) } else { conn, err = cfg.Dialer.DialContext(cfg.Ctx, cfg.Net, target) } diff --git a/tcp/server_test.go b/tcp/server_test.go index 124bd992..a7ed20e0 100644 --- a/tcp/server_test.go +++ b/tcp/server_test.go @@ -301,7 +301,8 @@ func TestServerKeepAliveMonitor(t *testing.T) { assert.NoError(t, errS) }() - cc, err := net.Dial("tcp", ld.Addr().String()) + dialer := net.Dialer{} + cc, err := dialer.DialContext(context.Background(), "tcp", ld.Addr().String()) require.NoError(t, err) defer func() { _ = cc.Close() From d24f3de9993557e214d3cb7a51cdcdcdfe533600 Mon Sep 17 00:00:00 2001 From: cah Date: Tue, 12 Aug 2025 13:24:28 +0200 Subject: [PATCH 6/7] Add tests for CSM Option --- tcp/client.go | 13 ++++---- tcp/client_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/tcp/client.go b/tcp/client.go index 7bbaa801..69bbc56c 100644 --- a/tcp/client.go +++ b/tcp/client.go @@ -1,7 +1,6 @@ package tcp import ( - "context" "crypto/tls" "fmt" "net" @@ -36,7 +35,7 @@ func Dial(target string, opts ...Option) (*client.Conn, error) { NetDialer: cfg.Dialer, Config: cfg.TLSCfg, } - conn, err = d.DialContext(context.Background(), cfg.Net, target) + conn, err = d.DialContext(cfg.Ctx, cfg.Net, target) } else { conn, err = cfg.Dialer.DialContext(cfg.Ctx, cfg.Net, target) } @@ -44,11 +43,11 @@ func Dial(target string, opts ...Option) (*client.Conn, error) { return nil, err } opts = append(opts, options.WithCloseSocket()) - return Client(conn, opts...), nil + return Client(conn, opts...) } // Client creates client over tcp/tcp-tls connection. -func Client(conn net.Conn, opts ...Option) *client.Conn { +func Client(conn net.Conn, opts ...Option) (*client.Conn, error) { cfg := client.DefaultConfig for _, o := range opts { o.TCPClientApply(&cfg) @@ -130,8 +129,8 @@ func Client(conn net.Conn, opts ...Option) *client.Conn { case <-time.After(cfg.CSMExchangeTimeout): err := fmt.Errorf("%v: timeout waiting for CSM exchange with peer", cc.RemoteAddr()) cfg.Errors(err) - cc.Close() // Close connection on timeout - return nil // or return cc with an error state + cc.Close() // Close connection on timeout + return nil, err // or return cc with an error state case <-csmExchangeDone: // CSM exchange completed successfully } @@ -139,5 +138,5 @@ func Client(conn net.Conn, opts ...Option) *client.Conn { cc.SetTCPSignalReceivedHandler(nil) } - return cc + return cc, nil } diff --git a/tcp/client_test.go b/tcp/client_test.go index 5c4a1198..4bfafe69 100644 --- a/tcp/client_test.go +++ b/tcp/client_test.go @@ -18,6 +18,7 @@ import ( "github.com/plgd-dev/go-coap/v3/options/config" "github.com/plgd-dev/go-coap/v3/pkg/runner/periodic" "github.com/plgd-dev/go-coap/v3/tcp/client" + "github.com/plgd-dev/go-coap/v3/tcp/server" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/atomic" @@ -839,3 +840,79 @@ func TestConnRequestMonitorDropRequest(t *testing.T) { require.Error(t, err) require.ErrorIs(t, err, context.DeadlineExceeded) } + +func TestConnWithCSMExchangeTimeout(t *testing.T) { + + type args struct { + clientOptions []Option + serverOptions []server.Option + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "client-server-no-csm", + args: args{ + clientOptions: []Option{}, + serverOptions: []server.Option{}, + }, + wantErr: false, + }, + { + name: "client-server-csm-success", + args: args{ + clientOptions: []Option{ + options.WithCSMExchangeTimeout(time.Second * 3), + }, + }, + wantErr: false, + }, + { + name: "client-server-csm-timeout", + args: args{ + clientOptions: []Option{ + options.WithCSMExchangeTimeout(time.Second * 3), + }, + serverOptions: []server.Option{ + options.WithDisableTCPSignalMessageCSM(), + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + l, err := coapNet.NewTCPListener("tcp", "") + require.NoError(t, err) + defer func() { + errC := l.Close() + require.NoError(t, errC) + }() + var wg sync.WaitGroup + defer wg.Wait() + + s := NewServer(tt.args.serverOptions...) + defer s.Stop() + wg.Add(1) + go func() { + defer wg.Done() + errS := s.Serve(l) + assert.NoError(t, errS) + }() + + client, err := Dial(l.Addr().String(), + tt.args.clientOptions...) + if tt.wantErr { + require.Nil(t, client) + require.Error(t, err) + } else { + require.NotNil(t, client) + require.NoError(t, err) + } + }) + } +} From 158fa8c915808e8f03cdf7db4d15fd53d1f59431 Mon Sep 17 00:00:00 2001 From: cah Date: Tue, 12 Aug 2025 13:43:28 +0200 Subject: [PATCH 7/7] gofumpt tcp/client_test.go --- tcp/client_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/tcp/client_test.go b/tcp/client_test.go index 4bfafe69..4cddc71e 100644 --- a/tcp/client_test.go +++ b/tcp/client_test.go @@ -842,7 +842,6 @@ func TestConnRequestMonitorDropRequest(t *testing.T) { } func TestConnWithCSMExchangeTimeout(t *testing.T) { - type args struct { clientOptions []Option serverOptions []server.Option @@ -885,7 +884,6 @@ func TestConnWithCSMExchangeTimeout(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l, err := coapNet.NewTCPListener("tcp", "") require.NoError(t, err) defer func() {