proc.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. // This file is subject to a 1-clause BSD license.
  2. // Its contents can be found in the enclosed LICENSE file.
  3. /*
  4. Package proc provides process forking and initialization functionality for
  5. one or more IRC clients. This is intended to facilitate zero-downtime binary
  6. upgrades by allowing the bot to fork itself and passing existing network
  7. connections to the new child process.
  8. A parent forks itself through the Fork() call. The child process then gets
  9. access to the client connections through a call to InheritedFiles().
  10. Before calling this, ensure that `flag.Parse()` has been called at least once.
  11. */
  12. package proc
  13. import (
  14. "flag"
  15. "log"
  16. "os"
  17. "os/exec"
  18. "os/signal"
  19. "strconv"
  20. "syscall"
  21. )
  22. // connectionCount defines the number of connections passed into a forked
  23. // process.
  24. var connectionCount uint
  25. func init() {
  26. flag.UintVar(&connectionCount, "fork", 0, "Number of inherited file descriptors")
  27. }
  28. // Wait polls for OS signals to either kill or fork this process.
  29. //
  30. // The signals it waits for are: SIGKILL, SIGINT, SIGTERM and SIGUSR1.
  31. // The latter one being responsible for forking this process. The others
  32. // are there so we may cleanly exit this process.
  33. func Wait(argv []string, clients ...*os.File) {
  34. signals := make(chan os.Signal, 1)
  35. signal.Notify(
  36. signals,
  37. syscall.SIGKILL,
  38. syscall.SIGINT,
  39. syscall.SIGTERM,
  40. syscall.SIGUSR1,
  41. )
  42. // If the bot is run for the first time in a new session,
  43. // it should be forked at least once to play nice with systemd.
  44. if connectionCount == 0 {
  45. log.Println("[proc] forking process...")
  46. err := doFork(argv, clients...)
  47. if err != nil {
  48. log.Println(err)
  49. }
  50. }
  51. log.Println("[proc] Waiting for signals...")
  52. for sig := range signals {
  53. log.Println("[proc] received signal:", sig)
  54. if sig != syscall.SIGUSR1 {
  55. return
  56. }
  57. log.Println("[proc] forking process...")
  58. err := doFork(argv, clients...)
  59. if err != nil {
  60. log.Println(err)
  61. }
  62. }
  63. }
  64. // Kill sends SIGINT to the current process. This can be used to cleanly
  65. // break out of a signal polling loop from anywhere in the program.
  66. func Kill() { syscall.Kill(os.Getpid(), syscall.SIGINT) }
  67. // KillParent sends SIGINT to the parent process. This is intended to be
  68. // called by a child after it has been forked and has re-initialized the
  69. // inherited connections. The parent may now shut down.
  70. func KillParent() { syscall.Kill(os.Getppid(), syscall.SIGINT) }
  71. // Fork sends SIGUSR1 to the current process. This kickstarts the
  72. // forking process.
  73. func Fork() { syscall.Kill(os.Getpid(), syscall.SIGUSR1) }
  74. // doFork forks the current process into a child process and passes the
  75. // given client connections along to be inherited.
  76. //
  77. // The specified argv list define custom command line parameters which should
  78. // be used in the invocation.
  79. //
  80. // The client list contains any file descriptors which should be inherited
  81. // by the client.
  82. //
  83. // The forked process is called with the `-fork N` command line parameter.
  84. // Where N is the number of file descriptors being passed along. This is
  85. // used by the InheritedFiles() call below to rebuild the files.
  86. func doFork(argv []string, clients ...*os.File) error {
  87. // Build the command line arguments for our child process.
  88. // This includes any custom arguments defined in the profile.
  89. args := append([]string{"-fork", strconv.Itoa(len(clients))}, argv...)
  90. // Initialize the command runner.
  91. cmd := exec.Command(os.Args[0], args...)
  92. cmd.ExtraFiles = make([]*os.File, len(clients))
  93. cmd.Stdout = os.Stdout
  94. cmd.Stderr = os.Stderr
  95. cmd.ExtraFiles = clients
  96. // Fork the process.
  97. return cmd.Start()
  98. }
  99. // InheritedFiles returns a list of N file descriptors inherited from a
  100. // previous session through the Fork call.
  101. //
  102. // This function assumes that flag.Parse() has been called at least once
  103. // already. The `-fork` flag has been registered during initialization of
  104. // this package.
  105. func InheritedFiles() []*os.File {
  106. if connectionCount == 0 {
  107. return nil
  108. }
  109. out := make([]*os.File, connectionCount)
  110. for i := range out {
  111. out[i] = os.NewFile(3+uintptr(i), "conn"+strconv.Itoa(i))
  112. }
  113. return out
  114. }