Skip to content

Commit 42459fc

Browse files
authored
bugfix(client): panic: send on closed channel (#179)
* bugfix(client): panic: send on closed channel Fix: panic: send on closed channel goroutine 751872 [running]: github.com/andreykaipov/goobs.(*Client).writeEvent(...) /home/xaionaro/.gvm/pkgsets/go1.22.1/global/pkg/mod/github.com/andreykaipov/goobs@v1.4.1/client.go:363 github.com/andreykaipov/goobs.(*Client).handleOpcodes(0xc0020c81a0, 0xc0013846c0) /home/xaionaro/.gvm/pkgsets/go1.22.1/global/pkg/mod/github.com/andreykaipov/goobs@v1.4.1/client.go:338 +0x5a5 created by github.com/andreykaipov/goobs.(*Client).connect in goroutine 751658 /home/xaionaro/.gvm/pkgsets/go1.22.1/global/pkg/mod/github.com/andreykaipov/goobs@v1.4.1/client.go:200 Essentially by design we should close a channel only after we finished all possible writing to it. Thus moving the close statement of channel IncomingResponses to be called right after the writer to the channel is finished. * bugfix: Possible race condition in the disconnection procedure There were three problems: 1. Instead of closing c.Disconnect we were writing a single event there. Thus if somebody will try to read from the channel the second time, they'll get blocked, and thus SendRequest will go into the `default` section of the `select`, which is potentially a panic if `c.Opcodes` is already closed. 2. The `close` statements in `markDisconnect` are executed without locking c.client.mutex. As a result there is a possible race condition that for example c.IncomingEvents is closed, but c.client.Disconnected is not (which may lead to a panic). 3. The place of the closure of c.client.IncomingResponses is confusing after 0442e5b. The problems are now fixed: 1. Now we just close `c.Disconnect` instead of writing an event into it. 2. Now the `close` statements are made atomic via c.client.mutex. 3. Now we have a comment explaining the closure of c.client.IncomingResponses
1 parent 3c6c796 commit 42459fc

File tree

2 files changed

+33
-12
lines changed

2 files changed

+33
-12
lines changed

api/client.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,20 @@ type Client struct {
4444
// Ya like logs?
4545
Log Logger
4646

47-
Disconnected chan bool
47+
Disconnected chan struct{}
4848

49-
mutex sync.Mutex
49+
mutex sync.Mutex
50+
closeOnce sync.Once
51+
}
52+
53+
func (c *Client) Close() {
54+
c.closeOnce.Do(func() {
55+
c.mutex.Lock()
56+
defer c.mutex.Unlock()
57+
58+
close(c.Disconnected)
59+
close(c.Opcodes)
60+
})
5061
}
5162

5263
// SendRequest abstracts the logic every subclient uses to send a request and

client.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,9 @@ func (c *Client) writeMessage(messageType int, data []byte) error {
144144

145145
func (c *Client) markDisconnected() {
146146
c.once.Do(func() {
147-
select {
148-
case c.client.Disconnected <- true:
149-
default:
150-
}
151-
152147
c.client.Log.Printf("[TRACE] Closing internal channels")
148+
c.client.Close()
153149
close(c.IncomingEvents)
154-
close(c.client.Opcodes)
155-
close(c.client.IncomingResponses)
156-
close(c.client.Disconnected)
157150
})
158151
}
159152

@@ -169,7 +162,7 @@ func New(host string, opts ...Option) (*Client, error) {
169162
requestHeader: http.Header{"User-Agent": []string{"goobs/" + LibraryVersion}},
170163
eventSubscriptions: subscriptions.All,
171164
client: &api.Client{
172-
Disconnected: make(chan bool),
165+
Disconnected: make(chan struct{}),
173166
IncomingResponses: make(chan *opcodes.RequestResponse),
174167
Opcodes: make(chan opcodes.Opcode),
175168
ResponseTimeout: 10000,
@@ -221,7 +214,24 @@ func (c *Client) connect() (err error) {
221214
authComplete := make(chan error)
222215

223216
go c.handleRawServerMessages(authComplete)
224-
go c.handleOpcodes(authComplete)
217+
go func() {
218+
c.handleOpcodes(authComplete)
219+
220+
// we write to IncomingResponses only from one place:
221+
// * c.handleOpcodes
222+
// and we also read from it in:
223+
// * c.client.SendRequest
224+
// thus the `close` must happen only after c.handleOpcodes finished,
225+
// and is desired to happen after c.client.SendRequest will stop
226+
// using the channel as well (to avoid handling nil Responses).
227+
//
228+
// This line right here:
229+
// * it is after c.handleOpcodes.
230+
// * this place is reachable only after c.client.Opcodes is closed,
231+
// which is possible only when c.client.Disconnected is closed,
232+
// which means c.client.SendRequest would not write into c.client.IncomingResponses.
233+
close(c.client.IncomingResponses)
234+
}()
225235

226236
timer := time.NewTimer(c.client.ResponseTimeout * time.Millisecond)
227237
defer timer.Stop()

0 commit comments

Comments
 (0)