bot.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. // This file is subject to a 1-clause BSD license.
  2. // Its contents can be found in the enclosed LICENSE file.
  3. package main
  4. import (
  5. "flag"
  6. "log"
  7. "net"
  8. "os"
  9. "os/exec"
  10. "os/signal"
  11. "syscall"
  12. "notabug.org/mouz/bot/irc"
  13. "notabug.org/mouz/bot/irc/proto"
  14. "notabug.org/mouz/bot/plugins"
  15. _ "notabug.org/mouz/bot/plugins/action"
  16. _ "notabug.org/mouz/bot/plugins/admin"
  17. _ "notabug.org/mouz/bot/plugins/owm"
  18. _ "notabug.org/mouz/bot/plugins/knmi"
  19. _ "notabug.org/mouz/bot/plugins/reminder"
  20. _ "notabug.org/mouz/bot/plugins/url"
  21. )
  22. // isFork becomes true as soon as the bot has forked for the first time.
  23. var isFork bool
  24. // shuttingDown is true if and only if the bot is in the process of
  25. // gracefully shutting down
  26. var shuttingDown bool = false
  27. func init() {
  28. flag.BoolVar(&isFork, "fork", false, "Whether this is a fork.")
  29. }
  30. // Bot defines state for a single IRC bot.
  31. type Bot struct {
  32. profile irc.Profile
  33. client *Client
  34. }
  35. // Run opens a new connection, or inherits the existing one and then begins
  36. // the client's message poll routine.
  37. func (b *Bot) Run() error {
  38. // Initialize the connection.
  39. err := b.Open()
  40. if err != nil {
  41. return err
  42. }
  43. // Make connection available to plugins
  44. irc.Connection = b.client
  45. // Spin up the connection's read loop.
  46. go func() {
  47. log.Println("[bot] Entering data loop...")
  48. err := b.client.Run()
  49. // err will always be non-nil here
  50. if e, ok := err.(*net.OpError); ok {
  51. if e.Err.Error() == "use of closed network connection" {
  52. // This can be the error value if the bot is in the
  53. // process of shutting down gracefully, the connection
  54. // is closed, and a pending read or write was
  55. // unblocked by that. Just let the shutting down of
  56. // the bot continue and ignore the error.
  57. if shuttingDown {
  58. log.Printf("[bot] ignoring '%+v'\n", e.Err)
  59. return
  60. }
  61. }
  62. }
  63. // Any other error is fatal, so a supervisor like systemd can
  64. // try to restart the bot.
  65. log.Fatal("[bot] exit 1: ", err)
  66. }()
  67. // Wait for external signals. These will either make the bot shut
  68. // down or start the fork.
  69. wait(b)
  70. shuttingDown = true
  71. return b.client.Close()
  72. }
  73. // PayloadHandler handles incoming server messages.
  74. func (b *Bot) PayloadHandler(payload []byte) {
  75. var r irc.Request
  76. // Try to parse the payload into a request.
  77. if !parseRequest(&r, payload) {
  78. return
  79. }
  80. // If Target points to the bot's own name, then this message came from
  81. // a user as a PM. Change the Target to the sender's name, so any replies
  82. // we create, end up at the right destination. In any other case, the
  83. // target is set to the channel name from whence the message came.
  84. if b.profile.IsMe(r.Target) {
  85. r.Target = r.SenderName
  86. }
  87. // Run the appropriate handler for housekeeping.
  88. switch r.Type {
  89. case "ERROR":
  90. log.Println("[bot] Network error:", r.Data)
  91. return
  92. case "PING":
  93. proto.Pong(b.client, r.Data)
  94. return
  95. }
  96. // Notify plugins of message.
  97. plugins.Dispatch(b.client, &r)
  98. }
  99. // Open either establishes a new connection or inherits an existing one
  100. // from a parent process.
  101. func (b *Bot) Open() error {
  102. p := b.profile
  103. // Are we a fork? Then we should inherit the existing connection.
  104. if isFork {
  105. log.Println("[bot] Inherit connection to:", p.Address())
  106. err := b.client.OpenFd(os.NewFile(3, "conn0"))
  107. if err != nil {
  108. return err
  109. }
  110. // We're done inheriting. Have the parent process break out of
  111. // its wait() call by sending SIGINT to it.
  112. syscall.Kill(os.Getppid(), syscall.SIGINT)
  113. return nil
  114. }
  115. log.Println("[bot] Opening new connection to:", p.Address())
  116. // New bot - create a new connection.
  117. err := b.client.Open(p.Address())
  118. if err != nil {
  119. return err
  120. }
  121. // Perform initial handshake.
  122. proto.User(b.client, p.Nickname(), "8", p.Nickname())
  123. proto.Nick(b.client, p.Nickname(), "")
  124. return nil
  125. }
  126. // wait polls for OS signals to either kill or fork this process.
  127. // The signals it waits for are: SIGINT, SIGTERM and SIGUSR1.
  128. // The latter one being responsible for forking this process. The others
  129. // are there so we may cleanly exit this process.
  130. func wait(b *Bot) {
  131. signals := make(chan os.Signal, 1)
  132. signal.Notify(
  133. signals,
  134. syscall.SIGINT,
  135. syscall.SIGTERM,
  136. syscall.SIGUSR1,
  137. )
  138. // If the bot is just started, it should fork itself at least once
  139. // to play nice with systemd. Forking is triggered by sending
  140. // SIGUSR1 to the current process.
  141. if !isFork {
  142. syscall.Kill(os.Getpid(), syscall.SIGUSR1)
  143. }
  144. log.Println("[bot] Waiting for signals...")
  145. for sig := range signals {
  146. log.Println("[bot] received signal:", sig)
  147. switch sig {
  148. case syscall.SIGINT, syscall.SIGTERM:
  149. // break out of the wait loop
  150. return
  151. case syscall.SIGUSR1:
  152. log.Println("[bot] forking process...")
  153. err := fork(b)
  154. if err != nil {
  155. log.Println("[bot]", err)
  156. }
  157. }
  158. }
  159. }
  160. // fork forks the current process into a child process and passes the
  161. // client connection along. The forked process is called with the
  162. // `-fork` command line switch.
  163. func fork(b *Bot) error {
  164. // Build the command line arguments for our child process.
  165. // This includes any custom arguments defined in the profile.
  166. argv := b.profile.ForkArgs()
  167. args := append([]string{"-fork"}, argv...)
  168. // Initialize the command runner.
  169. cmd := exec.Command(os.Args[0], args...)
  170. cmd.Stdout = os.Stdout
  171. cmd.Stderr = os.Stderr
  172. fd, _ := b.client.File()
  173. cmd.ExtraFiles = []*os.File{fd}
  174. // Fork the process.
  175. return cmd.Start()
  176. }