Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support quit messages through type Config, c.Cmd.Quit(), or a c.Close() wrapper #16

Closed
bmeh opened this issue Aug 31, 2018 · 8 comments
Closed

Comments

@bmeh
Copy link

bmeh commented Aug 31, 2018

How about we add a struct field QuitMessage to type 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)?

@lrstanley
Copy link
Owner

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.

@bmeh
Copy link
Author

bmeh commented Aug 31, 2018

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.

@lrstanley
Copy link
Owner

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.

@bmeh bmeh changed the title Support quit messages through type Config Support quit messages through type Config, c.Cmd.Quit(), or a c.Close() wrapper Aug 31, 2018
@bmeh
Copy link
Author

bmeh commented Aug 31, 2018

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
    }
  }

@lrstanley
Copy link
Owner

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: #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.

@bmeh
Copy link
Author

bmeh commented Aug 31, 2018

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. :)

nmeum added a commit to nmeum/hii that referenced this issue Dec 23, 2018
To ensure that new listeners / files aren't created after / during the
cleanup (e.g. through RPL_MONONLINE messages).

Discussion: Sending a QUIT command and blocking until the client quit
would be preferable, unfortunately such a command is not available.

See: lrstanley/girc#16
nmeum added a commit to nmeum/hii that referenced this issue Dec 24, 2018
To ensure that new listeners / files aren't created after / during the
cleanup (e.g. through RPL_MONONLINE messages).

Discussion: Sending a QUIT command and blocking until the client quit
would be preferable, unfortunately such a command is not available.

See: lrstanley/girc#16
nmeum added a commit to nmeum/hii that referenced this issue Dec 25, 2018
To ensure that new listeners / files aren't created after / during the
cleanup (e.g. through RPL_MONONLINE messages).

Discussion: Sending a QUIT command and blocking until the client quit
would be preferable, unfortunately such a function is not available.

See: lrstanley/girc#16
nmeum added a commit to nmeum/hii that referenced this issue Dec 25, 2018
To ensure that new listeners / files aren't created after / during the
cleanup (e.g. through RPL_MONONLINE messages).

Discussion: Sending a QUIT command and blocking until the client quit
would be preferable, unfortunately such a function is not available.

See: lrstanley/girc#16
@lrstanley
Copy link
Owner

Sorry for the delay in taking a better look at this. This should be resolved with 05b8be7; let me know if you have any issues.

@nmeum
Copy link
Contributor

nmeum commented Jul 30, 2019

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.

nmeum pushed a commit to nmeum/girc that referenced this issue Apr 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants