ronantakizawa/codereview-bench huggingface dataset:
Repository: syncthing/syncthing
File: lib/connections/quic_listen.go (Go)

Code before:
	} else if uri == nil && existingAddress != nil {
		t.notifyAddressesChanged(t)
	}
}

func (t *quicListener) serve(ctx context.Context) error {
	network := strings.ReplaceAll(t.uri.Scheme, "quic", "udp")

	packetConn, err := net.ListenPacket(network, t.uri.Host)
	if err != nil {
		l.Infoln("Listen (BEP/quic):", err)
		return err
	}
	defer func() { _ = packetConn.Close() }()

	svc, conn := stun.New(t.cfg, t, packetConn)
	defer func() { _ = conn.Close() }()
	wrapped := &stunConnQUICWrapper{
		PacketConn: conn,
		underlying: packetConn.(*net.UDPConn),
	}

	go svc.Serve(ctx)

	registry.Register(t.uri.Scheme, wrapped)
	defer registry.Unregister(t.uri.Scheme, wrapped)

	listener, err := quic.Listen(wrapped, t.tlsCfg, quicConfig)
	if err != nil {
		l.Infoln("Listen (BEP/quic):", err)
		return err
	}
	t.notifyAddressesChanged(t)
	defer listener.Close()
	defer t.clearAddresses(t)

	l.Infof("QUIC listener (%v) starting", packetConn.LocalAddr())
	defer l.Infof("QUIC listener (%v) shutting down", packetConn.LocalAddr())

	acceptFailures := 0
	const maxAcceptFailures = 10

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		session, err := listener.Accept(ctx)
		if err == context.Canceled {

Review comment (suggestion):
Unregister doesn't happen anymore in case of an error in `quic.Listen`, as the defer is moved below that.

Code after:
		t.notifyAddressesChanged(t)
	} else if uri == nil && existingAddress != nil {
		t.notifyAddressesChanged(t)
	}
}

func (t *quicListener) serve(ctx context.Context) error {
	network := strings.ReplaceAll(t.uri.Scheme, "quic", "udp")

	packetConn, err := net.ListenPacket(network, t.uri.Host)
	if err != nil {
		l.Infoln("Listen (BEP/quic):", err)
		return err
	}

	svc, conn := stun.New(t.cfg, t, packetConn)
	wrapped := &stunConnQUICWrapper{
		PacketConn: conn,
		underlying: packetConn.(*net.UDPConn),
	}

	go svc.Serve(ctx)

	registry.Register(t.uri.Scheme, wrapped)

	listener, err := quic.Listen(wrapped, t.tlsCfg, quicConfig)
	if err != nil {
		l.Infoln("Listen (BEP/quic):", err)
		return err
	}
	t.notifyAddressesChanged(t)

	l.Infof("QUIC listener (%v) starting", packetConn.LocalAddr())
	t.mut.Lock()
	t.laddr = packetConn.LocalAddr()
	t.mut.Unlock()

	defer func() {
		l.Infof("QUIC listener (%v) shutting down", packetConn.LocalAddr())
		t.mut.Lock()
		t.laddr = nil
		t.mut.Unlock()
		registry.Unregister(t.uri.Scheme, wrapped)
		t.clearAddresses(t)
		_ = listener.Close()
		_ = conn.Close()
		_ = packetConn.Close()
	}()

	acceptFailures := 0
	const maxAcceptFailures = 10
Repository: syncthing/syncthing
File: cmd/syncthing/cli/main.go (Go)

Code before:
		Subcommands: []cli.Command{
			configCommand,
			showCommand,
			operationCommand,
			errorsCommand,
			debugCommand,
		},
	}}

	tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
	if !tty {
		// Not a TTY, consume from stdin
		scanner := bufio.NewScanner(os.Stdin)
		for scanner.Scan() {
			input, err := shlex.Split(scanner.Text())
			if err != nil {
				return errors.Wrap(err, "parsing input")
			}
			if len(input) == 0 {
				continue
			}
			err = app.Run(append(os.Args, input...))
			if err != nil {
				return err
			}
		}
		err = scanner.Err()
		if err != nil {
			return err
		}
	} else {
		err = app.Run(os.Args)
		if err != nil {
			return err
		}
	}

	return nil
}

func parseFlags(c *preCli) error {
	// kong only needs to parse the global arguments after "cli" and before the
	// subcommand (if any).
	if len(os.Args) <= 2 {
		return nil
	}
	args := os.Args[2:]
	for i := 0; i < len(args); i++ {
		if !strings.HasPrefix(args[i], "--") {
			args = args[:i]
			break

Review comment (suggestion):
This continue is kind of awkward. Made me pause for a second to figure out what the code was doing. Here's a suggestion:

Instead of the continue try this:

```go
if len(input) != 0 {
    err = app.Run(append(args, input...))
    if err != nil {
        return err
    }
}
```
						


Code after:
		Subcommands: []cli.Command{
			configCommand,
			showCommand,
			operationCommand,
			errorsCommand,
			debugCommand,
			{
				Name:     "-",
				HideHelp: true,
				Usage:    "Read commands from stdin",
				Action: func(ctx *cli.Context) error {
					if ctx.NArg() > 0 {
						return errors.New("command does not expect any arguments")
					}

					// Drop the `-` not to recurse into self.
					args := make([]string, len(os.Args)-1)
					copy(args, os.Args)

					fmt.Println("Reading commands from stdin...", args)
					scanner := bufio.NewScanner(os.Stdin)
					for scanner.Scan() {
						input, err := shlex.Split(scanner.Text())
						if err != nil {
							return errors.Wrap(err, "parsing input")
						}
						if len(input) == 0 {
							continue
						}
						err = app.Run(append(args, input...))
						if err != nil {
							return err
						}
					}
					return scanner.Err()
				},
			},
		},
	}}

	return app.Run(os.Args)
}

func parseFlags(c *preCli) error {
	// kong only needs to parse the global arguments after "cli" and before the
	// subcommand (if any).
	if len(os.Args) <= 2 {
		return nil
	}
	args := os.Args[2:]
	for i := 0; i < len(args); i++ {
Repository: syncthing/syncthing
File: lib/model/folder_recvonly_test.go (Go)

Code before:
		}
		f := fi.(protocol.FileInfo)
		f.LocalFlags = 0
		f.Version = protocol.Vector{}.Update(device1.Short())
		files = append(files, f)
		return true
	})
	snap.Release()
	must(t, m.IndexUpdate(device1, "ro", files))

	// Ensure the pull to resolve conflicts (content identical) happened
	must(t, f.doInSync(func() error {
		f.pull()
		return nil
	}))

	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories+size.Deleted != 0 {
		t.Fatalf("Receive only: expected all zero: %+v", size)
	}
}

func TestRecvOnlyRevertOwnID(t *testing.T) {
	// If the folder was receive-only in the past, the global item might have
	// only our id in the version vector and be valid. There was a bug based on
	// the incorrect assumpation that this can never happen.

	// Get us a model up and running

	m, f, wcfgCancel := setupROFolder(t)
	defer wcfgCancel()
	ffs := f.Filesystem()
	defer cleanupModel(m)

	// Create some test data

	must(t, ffs.MkdirAll(".stfolder", 0755))
	data := []byte("hello\n")
	name := "foo"
	must(t, writeFile(ffs, name, data, 0644))

	// Make sure the file is scanned and locally changed
	must(t, m.ScanFolder("ro"))
	fi, ok := m.testCurrentFolderFile(f.ID, name)
	if !ok {
		t.Fatal("File missing")
	} else if !fi.IsReceiveOnlyChanged() {
		t.Fatal("File should be receiveonly changed")
	}
	fi.LocalFlags = 0
	v := fi.Version.Counters[0].Value

Review comment (bug):
```suggestion
	// the incorrect assumption that this can never happen.
```

Code after:
		}
		f := fi.(protocol.FileInfo)
		f.LocalFlags = 0
		f.Version = protocol.Vector{}.Update(device1.Short())
		files = append(files, f)
		return true
	})
	snap.Release()
	must(t, m.IndexUpdate(device1, "ro", files))

	// Ensure the pull to resolve conflicts (content identical) happened
	must(t, f.doInSync(func() error {
		f.pull()
		return nil
	}))

	size = receiveOnlyChangedSize(t, m, "ro")
	if size.Files+size.Directories+size.Deleted != 0 {
		t.Fatalf("Receive only: expected all zero: %+v", size)
	}
}

func TestRecvOnlyRevertOwnID(t *testing.T) {
	// If the folder was receive-only in the past, the global item might have
	// only our id in the version vector and be valid. There was a bug based on
	// the incorrect assumption that this can never happen.

	// Get us a model up and running

	m, f, wcfgCancel := setupROFolder(t)
	defer wcfgCancel()
	ffs := f.Filesystem()
	defer cleanupModel(m)

	// Create some test data

	must(t, ffs.MkdirAll(".stfolder", 0755))
	data := []byte("hello\n")
	name := "foo"
	must(t, writeFile(ffs, name, data, 0644))

	// Make sure the file is scanned and locally changed
	must(t, m.ScanFolder("ro"))
	fi, ok := m.testCurrentFolderFile(f.ID, name)
	if !ok {
		t.Fatal("File missing")
	} else if !fi.IsReceiveOnlyChanged() {
		t.Fatal("File should be receiveonly changed")
	}
	fi.LocalFlags = 0
	v := fi.Version.Counters[0].Value
Repository: syncthing/syncthing
File: lib/db/lowlevel.go (Go)

Code before:
	it.Release()
	if err := it.Error(); err != nil {
		return err
	}

	// Remember the number of unique keys we kept until the next pass.
	db.gcKeyCount = matchedBlocks
	if matchedVersions > matchedBlocks {
		db.gcKeyCount = matchedVersions
	}

	if err := t.Commit(); err != nil {
		return err
	}

	l.Debugf("Finished GC (discarded/remaining: %v/%v blocks, %v/%v versions)", discardedBlocks, matchedBlocks, discardedVersions, matchedVersions)

	return nil
}

func (db *Lowlevel) recordIndirectionHashesForFile(f *protocol.FileInfo) {
	db.recordIndirectionHashes(IndirectionHashesOnly{BlocksHash: f.BlocksHash, VersionHash: f.VersionHash})
}

func (db *Lowlevel) recordIndirectionHashes(hs IndirectionHashesOnly) {
	db.filtersMut.Lock()
	if db.blockFilter != nil && len(hs.BlocksHash) > 0 {
		db.blockFilter.add(hs.BlocksHash)
	}
	if db.versionFilter != nil && len(hs.VersionHash) > 0 {
		db.versionFilter.add(hs.VersionHash)
	}
	db.filtersMut.Unlock()
}

func newBloomFilter(capacity int) *bloomFilter {
	var buf [16]byte
	io.ReadFull(rand.Reader, buf[:])

	return &bloomFilter{
		f: blobloom.NewOptimized(blobloom.Config{
			Capacity: uint64(capacity),
			FPRate:   indirectGCBloomFalsePositiveRate,
			MaxBits:  8 * indirectGCBloomMaxBytes,
		}),

		k0: binary.LittleEndian.Uint64(buf[:8]),
		k1: binary.LittleEndian.Uint64(buf[8:]),
	}
}


Review comment (suggestion):
As the lock only protects concurrent access to the blockfilters, but not the blockfilter variables (that's `gcMut`), we could first do the nil checks and abort early, thus preventing pointless locking. As the shortcut will be taken the majority of the time, I think the micro-optimisation might just be worth it :)

Code after:
		default:
		}

		key := versionKey(it.Key())
		if db.versionFilter.has(key.Hash()) {
			matchedVersions++
			continue
		}
		if err := t.Delete(key); err != nil {
			return err
		}
		discardedVersions++
	}
	it.Release()
	if err := it.Error(); err != nil {
		return err
	}

	// Remember the number of unique keys we kept until the next pass.
	db.gcKeyCount = matchedBlocks
	if matchedVersions > matchedBlocks {
		db.gcKeyCount = matchedVersions
	}

	if err := t.Commit(); err != nil {
		return err
	}

	l.Debugf("Finished GC (discarded/remaining: %v/%v blocks, %v/%v versions)", discardedBlocks, matchedBlocks, discardedVersions, matchedVersions)

	return nil
}

func (db *Lowlevel) recordIndirectionHashesForFile(f *protocol.FileInfo) {
	db.recordIndirectionHashes(IndirectionHashesOnly{BlocksHash: f.BlocksHash, VersionHash: f.VersionHash})
}

func (db *Lowlevel) recordIndirectionHashes(hs IndirectionHashesOnly) {
	// must be called with gcMut held (at least read-held)
	if db.blockFilter != nil && len(hs.BlocksHash) > 0 {
		db.blockFilter.add(hs.BlocksHash)
	}
	if db.versionFilter != nil && len(hs.VersionHash) > 0 {
		db.versionFilter.add(hs.VersionHash)
	}
}

func newBloomFilter(capacity int) *bloomFilter {
	var buf [16]byte
	io.ReadFull(rand.Reader, buf[:])
Repository: syncthing/syncthing
File: lib/db/observed.go (Go)

Code before:
package db

import (
	"time"

	"github.com/syncthing/syncthing/lib/protocol"
)

func (db *Lowlevel) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) error {
	key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
	od := ObservedDevice{
		Time:    time.Now().Truncate(time.Second),
		Name:    name,
		Address: address,
	}
	bs, err := od.Marshal()
	if err != nil {
		return err
	}
	return db.Put(key, bs)
}

func (db *Lowlevel) RemovePendingDevice(device protocol.DeviceID) error {
	key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
	return db.Delete(key)
}

// PendingDevices enumerates all entries.  Invalid ones are dropped from the database
// after a warning log message, as a side-effect.
func (db *Lowlevel) PendingDevices() (map[protocol.DeviceID]ObservedDevice, error) {
	iter, err := db.NewPrefixIterator([]byte{KeyTypePendingDevice})
	if err != nil {
		return nil, err
	}
	defer iter.Release()
	res := make(map[protocol.DeviceID]ObservedDevice)
	for iter.Next() {
		keyDev := db.keyer.DeviceFromPendingDeviceKey(iter.Key())
		deviceID, err := protocol.DeviceIDFromBytes(keyDev)
		var od ObservedDevice
		if err != nil {
			goto deleteKey
		}
		if err = od.Unmarshal(iter.Value()); err != nil {
			goto deleteKey
		}
		res[deviceID] = od
		continue
	deleteKey:
		// Deleting invalid entries is the only possible "repair" measure and
		// appropriate for the importance of pending entries.  They will come back

Review comment (suggestion):
You didn't touch some places where this is called in addition since #7712.  I find it inconsistent that the same potential errors are not logged / reported in those cases.

Code after:
package db

import (
	"fmt"
	"time"

	"github.com/syncthing/syncthing/lib/protocol"
)

func (db *Lowlevel) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) error {
	key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
	od := ObservedDevice{
		Time:    time.Now().Truncate(time.Second),
		Name:    name,
		Address: address,
	}
	bs, err := od.Marshal()
	if err != nil {
		return err
	}
	return db.Put(key, bs)
}

func (db *Lowlevel) RemovePendingDevice(device protocol.DeviceID) error {
	key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
	return db.Delete(key)
}

// PendingDevices enumerates all entries.  Invalid ones are dropped from the database
// after a warning log message, as a side-effect.
func (db *Lowlevel) PendingDevices() (map[protocol.DeviceID]ObservedDevice, error) {
	iter, err := db.NewPrefixIterator([]byte{KeyTypePendingDevice})
	if err != nil {
		return nil, err
	}
	defer iter.Release()
	res := make(map[protocol.DeviceID]ObservedDevice)
	for iter.Next() {
		keyDev := db.keyer.DeviceFromPendingDeviceKey(iter.Key())
		deviceID, err := protocol.DeviceIDFromBytes(keyDev)
		var od ObservedDevice
		if err != nil {
			goto deleteKey
		}
		if err = od.Unmarshal(iter.Value()); err != nil {
			goto deleteKey
		}
		res[deviceID] = od
		continue
	deleteKey:
		// Deleting invalid entries is the only possible "repair" measure and
Repository: syncthing/syncthing
File: lib/fs/basicfs_watch_eventtypes_darwin.go (Go)

Code before:
// Copyright (C) 2017 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

// +build darwin,!kqueue,cgo

package fs

import "github.com/syncthing/notify"

const (
	subEventMask  = notify.Create | notify.Remove | notify.Write | notify.Rename | notify.FSEventsInodeMetaMod
	permEventMask = 0
	rmEventMask   = notify.Remove | notify.Rename
)


Review comment (security):
```suggestion
// Copyright (C) 2021 The Syncthing Authors.
```

Code after:
// Copyright (C) 2021 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

// +build darwin,!kqueue,cgo

package fs

import "github.com/syncthing/notify"

const (
	subEventMask  = notify.Create | notify.Remove | notify.Write | notify.Rename | notify.FSEventsInodeMetaMod
	permEventMask = 0
	rmEventMask   = notify.Remove | notify.Rename
)
Repository: syncthing/syncthing
File: lib/db/lowlevel.go (Go)

Code before:
	var buf [16]byte
	io.ReadFull(rand.Reader, buf[:])

	return &bloomFilter{
		f: blobloom.NewSyncOptimized(blobloom.Config{
			Capacity: uint64(capacity),
			FPRate:   indirectGCBloomFalsePositiveRate,
			MaxBits:  8 * indirectGCBloomMaxBytes,
		}),

		k0: binary.LittleEndian.Uint64(buf[:8]),
		k1: binary.LittleEndian.Uint64(buf[8:]),
	}
}

type bloomFilter struct {
	f      *blobloom.SyncFilter
	k0, k1 uint64 // Random key for SipHash.
}

func (b *bloomFilter) add(id []byte)      { b.f.Add(b.hash(id)) }
func (b *bloomFilter) has(id []byte) bool { return b.f.Has(b.hash(id)) }

// Hash function for the bloomfilter: SipHash of the SHA-256.
//
// The randomization in SipHash means we get different collisions across
// runs and colliding keys are not kept indefinitely.
func (b *bloomFilter) hash(id []byte) uint64 {
	if len(id) != sha256.Size {
		panic("bug: bloomFilter.hash passed something not a SHA256 hash")
	}
	return siphash.Hash(b.k0, b.k1, id)
}

// checkRepair checks folder metadata and sequences for miscellaneous errors.
func (db *Lowlevel) checkRepair() error {
	db.gcMut.RLock()
	defer db.gcMut.RUnlock()
	for _, folder := range db.ListFolders() {
		if _, err := db.getMetaAndCheckGCLocked(folder); err != nil {
			return err
		}
	}
	return nil
}

func (db *Lowlevel) getMetaAndCheck(folder string) (*metadataTracker, error) {
	db.gcMut.RLock()
	defer db.gcMut.RUnlock()

	return db.getMetaAndCheckGCLocked(folder)

Review comment (suggestion):
Intuitively it seems like it would make sense to create the `maphash.Hash` once and store it in the `bloomFilter`, and just `Reset()` it before each use? We can even use a zero `maphash.Hash` as it'll seed itself on first use.

Code after:
			Capacity: uint64(capacity),
			FPRate:   indirectGCBloomFalsePositiveRate,
			MaxBits:  8 * indirectGCBloomMaxBytes,
		}),
		seed: maphash.MakeSeed(),
	}
}

type bloomFilter struct {
	f    *blobloom.SyncFilter
	seed maphash.Seed
}

func (b *bloomFilter) add(id []byte)      { b.f.Add(b.hash(id)) }
func (b *bloomFilter) has(id []byte) bool { return b.f.Has(b.hash(id)) }

// Hash function for the bloomfilter: maphash of the SHA-256.
//
// The randomization in maphash should ensure that we get different collisions
// across runs, so colliding keys are not kept indefinitely.
func (b *bloomFilter) hash(id []byte) uint64 {
	if len(id) != sha256.Size {
		panic("bug: bloomFilter.hash passed something not a SHA256 hash")
	}
	var h maphash.Hash
	h.SetSeed(b.seed)
	h.Write(id)
	return h.Sum64()
}

// checkRepair checks folder metadata and sequences for miscellaneous errors.
func (db *Lowlevel) checkRepair() error {
	db.gcMut.RLock()
	defer db.gcMut.RUnlock()
	for _, folder := range db.ListFolders() {
		if _, err := db.getMetaAndCheckGCLocked(folder); err != nil {
			return err
		}
	}
	return nil
}

func (db *Lowlevel) getMetaAndCheck(folder string) (*metadataTracker, error) {
	db.gcMut.RLock()
	defer db.gcMut.RUnlock()

	return db.getMetaAndCheckGCLocked(folder)
}

func (db *Lowlevel) getMetaAndCheckGCLocked(folder string) (*metadataTracker, error) {
	fixed, err := db.checkLocalNeed([]byte(folder))
Repository: syncthing/syncthing
File: lib/relay/client/static.go (Go)

Code before:
func (c *staticClient) URI() *url.URL {
	return c.uri
}

func (c *staticClient) connect(ctx context.Context) error {
	if c.uri.Scheme != "relay" {
		return fmt.Errorf("unsupported relay scheme: %v", c.uri.Scheme)
	}

	timeoutCtx, cancel := context.WithTimeout(ctx, c.connectTimeout)
	defer cancel()
	tcpConn, err := dialer.DialContext(timeoutCtx, "tcp", c.uri.Host)
	if err != nil {
		return err
	}

	// Copy the TLS config and set the server name we're connecting to. In
	// many cases this will be an IP address, in which case it's a no-op. In
	// other cases it will be a hostname, which will cause the TLS stack to
	// send SNI.
	cfg := *c.config
	if host, _, err := net.SplitHostPort(c.uri.Host); err == nil {
		cfg.ServerName = host
	}

	conn := tls.Client(tcpConn, &cfg)

	if err := conn.SetDeadline(time.Now().Add(c.connectTimeout)); err != nil {
		conn.Close()
		return err
	}

	if err := performHandshakeAndValidation(conn, c.uri); err != nil {
		conn.Close()
		return err
	}

	c.conn = conn
	return nil
}

func (c *staticClient) disconnect() {
	l.Debugln(c, "disconnecting")
	c.conn.Close()
}

func (c *staticClient) join() error {
	if err := protocol.WriteMessage(c.conn, protocol.JoinRelayRequest{}); err != nil {
		return err
	}


Review comment (suggestion):
The DeepSource complaint about the first line is real:

```suggestion
	cfg := c.config
	if host, _, err := net.SplitHostPort(c.uri.Host); err == nil {
		cfg = cfg.Clone()
		cfg.ServerName = host
	}

	conn := tls.Client(tcpConn, cfg)
```

Code after:
func (c *staticClient) URI() *url.URL {
	return c.uri
}

func (c *staticClient) connect(ctx context.Context) error {
	if c.uri.Scheme != "relay" {
		return fmt.Errorf("unsupported relay scheme: %v", c.uri.Scheme)
	}

	timeoutCtx, cancel := context.WithTimeout(ctx, c.connectTimeout)
	defer cancel()
	tcpConn, err := dialer.DialContext(timeoutCtx, "tcp", c.uri.Host)
	if err != nil {
		return err
	}

	// Copy the TLS config and set the server name we're connecting to. In
	// many cases this will be an IP address, in which case it's a no-op. In
	// other cases it will be a hostname, which will cause the TLS stack to
	// send SNI.
	cfg := c.config
	if host, _, err := net.SplitHostPort(c.uri.Host); err == nil {
		cfg = cfg.Clone()
		cfg.ServerName = host
	}

	conn := tls.Client(tcpConn, cfg)

	if err := conn.SetDeadline(time.Now().Add(c.connectTimeout)); err != nil {
		conn.Close()
		return err
	}

	if err := performHandshakeAndValidation(conn, c.uri); err != nil {
		conn.Close()
		return err
	}

	c.conn = conn
	return nil
}

func (c *staticClient) disconnect() {
	l.Debugln(c, "disconnecting")
	c.conn.Close()
}

func (c *staticClient) join() error {
	if err := protocol.WriteMessage(c.conn, protocol.JoinRelayRequest{}); err != nil {
		return err
	}