Closed bmeh closed 5 years ago
You could always send a raw message to handle this. I don't necessarily like the Config
route, as users may want to change their quit message before they quit (e.g. 'restarting' vs 'shutdown'). Can probably just implement a wrapper around Client.Close()
that can be supplied a string.
I've been playing around with it, and since there is no c.Cmd.Quit()
, I did use c.Cmd.SendRawf("QUIT :Received signal '%s' to shut down", sig)
right before c.Close()
, except the quit message is The TLS connection was non-properly terminated.
, unless I add a time.Sleep(5 * time.Second)
between the two function calls. Any ideas to make c.Cmd.SendRaw{f}
blocking perhaps, or what's the correct way to go about this? Adding a wrapper on your end around c.Close()
that would receive a string for the QUIT message, or adding c.Cmd.Quit()
which would be blocking to avoid incorrect QUIT messages such as the one I mentioned would be nice.
If the client sends a QUIT
, the server should automatically close the connection when it sees it. Calling Close()
right after is probably why you see that message.
Will poke it more when I get a chance.
Actually, scratch the "making c.Cmd.SendRaw{f}
blocking part, that would be a horrible idea. If there is such a thing as girc.DISCONNECTED
, I'll just add a handler and make(chan struct{})
or something. I'll just have to modify my code to to use for { select {} }
, but for that to work (along with auto-reconnect), I'd need girc.DISCONNECTED
, which apparently doesn't seem to work as intended. I added a handler for girc.DISCONNECTED
, but apparently c.Close()
doesn't trigger that event, which I would have suspected, but rather it triggers girc.STOPPED
instead. Now, it wouldn't be so bad, except in case of using command !stop
(via girc.PRIVMSG
handler that matches for !stop
), it quits with Connection closed
, but calling c.Close()
from within my for { select {} }
doesn't seem to trigger it, as in, nothing inside that handler gets executed, which I was hoping would happen.
disconnected := make(chan struct{})
c.Handlers.Add(girc.STOPPED, func(c *girc.Client, e girc.Event) {
log.Printf("DISCONNECTED")
disconnected <- struct{}{}
})
c.Handlers.Add(girc.PRIVMSG, func(c *girc.Client, e girc.Event) {
if strings.HasPrefix(e.Trailing, "!stop") {
//c.Cmd.SendRaw("QUIT :Received command '!stop'")
c.Close() // triggers girc.STOPPED
}
})
done := false
go func() {
for {
if err = c.Connect(); err != nil && !done {
log.Printf("Error connecting to %s (%d): %v",
cfg.Hostname, cfg.Port, err)
log.Print("Reconnecting in 15 seconds...")
time.Sleep(15 * time.Second)
} else {
return
}
}
}()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
for {
select {
case sig := <-sigs:
switch sig {
case syscall.SIGUSR1:
continue
case syscall.SIGINT, syscall.SIGTERM:
done = true
log.Printf("Received signal '%s' to shut down", sig)
if c.IsConnected() {
// c.Cmd.SendRawf("QUIT :Received signal '%s' to shut down", sig)
// time.Sleep(5 * time.Second) // ↑ would work with this workaround
c.Close() // doesn't trigger girc.STOPPED
}
return
}
case <-disconnected:
log.Printf("DISCONNECTED #2")
return
}
}
So the issues are:
1) Neither DISCONNECTED
and DISCONNECTED #2
gets printed upon receiving the signals that do call c.Close()
. Do you think this is an issue on my end? Due to c.Connect()
blocking, I had to call it inside a goroutine, but it works just fine apart from the necessary time.Sleep()
between c.Cmd.SendRaw("QUIT :foo")
and c.Close()
. It is worth noting that they do get printed when I am using the command !stop
, which calls c.Close()
2) It disconnects with the quit message Connection closed
upon receiving the matched !stop
via girc.PRIVMSG
, which cannot be changed without using the time.Sleep()
workaround.
3) I can't quit with a proper quit message when receiving the signals, without using the time.Sleep()
workaround.
4) Coming from another Go IRC library, it uses DISCONNECTED
instead of STOPPED
, so it was a bit confusing, in fact, I am still not sure what triggers girc.DISCONNECTED
. :P
Additionally, omitting c.Close()
doesn't seem to work (upon receiving SIGINT/SIGTERM), as c.Cmd.SendRawf()
doesn't get executed (since it is non-blocking) before returning, so the quit message remains The TLS connection was non-properly terminated.
.
If the client sends a QUIT, the server should automatically close the connection when it sees it. Calling Close() right after is probably why you see that message.
The reason I am calling c.Close()
is that I assume it does more than just sending QUIT :foo
, right?
Hmm, apparently trying this:
if c.IsConnected() {
c.Cmd.SendRawf("QUIT :Received signal '%s' to shut down", sig)
c.Close()
time.Sleep(2 * time.Second)
}
return
does print "DISCONNECTED" and quits with "Connection closed", but it depends on an arbitrary number of seconds, and it's of course non-deterministic, and a terrible workaround.
I did try eliminating c.Close()
from this code, but still requires at least time.Sleep(1 * time.Second)
to quit with Received signal 'interrupt' to shut down
instead of The TLS connection was non-properly terminated.
. Note that 1
is an arbitrary number, it may or may not work.
This seems to do the job perfectly with no arbitrary seconds of sleeping with time.Sleep()
required, apart from not closing the socket (which is probably not an issue in this case, since we are exiting the program):
for c.IsConnected() {
c.Cmd.SendRawf("QUIT :Received signal '%s' to shut down", sig)
}
return
Apparently this particular code works in this instance, too:
c.Handlers.Add(girc.PRIVMSG, func(c *girc.Client, e girc.Event) {
if strings.HasPrefix(e.Trailing, "!stop") {
for c.IsConnected() {
c.Cmd.SendRawf("QUIT :Foobar")
}
}
})
I get:
<@foo> !stop
-!- bar [bar@nil] has quit [Quit: Foobar]
---
2018/08/31 18:02:49 Error connecting to irc.foo.net (7000): EOF
2018/08/31 18:02:49 Reconnecting in 15 seconds...
2018/08/31 18:03:04 Error connecting to irc.foo.net (7000): Closing link: (unknown@nil) [Quit: Foobar]
2018/08/31 18:03:04 Reconnecting in 15 seconds...
Aaaand it does reconnect and works as expected! I am not sure what to make of the two errors right now, but I guess I could workaround them.
TL;DR: After having spent a few minutes on this, my issue has been resolved and both quitting with a specified error message (in two different instances), and auto-reconnect works.
What's left for you to do is perhaps add c.Cmd.Quit()
or the mentioned c.Close()
wrapper, although I would recommend documenting that using for c.IsConnected() { c.Cmd.SendRawf("QUIT :Foobar") }
works as intended, or perhaps your wrapper could contain this particular code, whichever you prefer. Don't forget to close the socket though!
For completeness' sake:
done := false
go func() {
for {
if err = c.Connect(); err != nil && !done {
log.Printf("Error connecting to %s (%d): %v",
cfg.Hostname, cfg.Port, err)
log.Print("Reconnecting in 15 seconds...")
time.Sleep(15 * time.Second)
} else {
return
}
}
}()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
for sig := range sigs {
switch sig {
case syscall.SIGUSR1:
continue
case syscall.SIGINT, syscall.SIGTERM:
done = true
log.Printf("Received signal '%s' to shut down", sig)
for c.IsConnected() {
c.Cmd.SendRawf("QUIT :Received signal '%s' to shut down", sig)
}
return
}
}
Utilizing for c.IsConnected() { c.Cmd.SendRawf("QUIT :Foobar") }
is prone to data races as well. It could disconnect between c.IsConnected()
and the call of c.Cmd.SendRawf()
, and it can trigger this bug: https://github.com/lrstanley/girc/issues/14 (will fix when I get a chance). Additionally, without any delay, you'd be potentially spamming the server with QUIT
requests.
The ideal solution is something like Cmd.Quit(reply string)
, then on the client side, using channels and a timeout, so if it doesn't close the connection after a certain amount of time, the end user then calls Close()
. This is where the wrapper may be able to come into play.
You are right about the unnecessary, and potential flooding. Sending QUIT
once and then doing for c.IsConnected() { continue }
could be sufficient - depending on the use case - otherwise use channels. I decided to go with channels, and named my function closeOrTimeout
. :)
Sorry for the delay in taking a better look at this. This should be resolved with https://github.com/lrstanley/girc/commit/05b8be7a4b8aa3ff571f0a5729642779ff893575; let me know if you have any issues.
This should be resolved with 05b8be7; let me know if you have any issues.
Great news, thanks a lot! One thing I noticed though is that the QUIT event doesn't seem to get propagated to a girc.ALL_EVENTS
handler, is that intentional? Because I would like to record the quit message in my IRC log through a handler.
The messages does show up in the debug log though.
UPDATE: Never mind, forgot that this is a TX event (which is why it doesn't show up in a ALL_EVENTS
handler). Registering a handler for CLIENT_CLOSED
works though.
How about we add a struct field
QuitMessage
totype Config
and use this quit message upon graceful exit?Are there any ways to do this currently, as in quit with a specified quit message whenever (for example when handling signals)?