sleepit.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. // This code is released under the MIT License
  2. // Copyright (c) 2020 Marco Molteni and the timeit contributors.
  3. package main
  4. import (
  5. "flag"
  6. "fmt"
  7. "os"
  8. "os/signal"
  9. "time"
  10. )
  11. const usage = `sleepit: sleep for the specified duration, optionally handling signals
  12. When the line "sleepit: ready" is printed, it means that it is safe to send signals to it
  13. Usage: sleepit <command> [<args>]
  14. Commands
  15. default Use default action: on reception of SIGINT terminate abruptly
  16. handle Handle signals: on reception of SIGINT perform cleanup before exiting
  17. version Show the sleepit version`
  18. // Filled by the linker.
  19. var fullVersion = "unknown" // example: v0.0.9-8-g941583d027-dirty
  20. func main() {
  21. os.Exit(run(os.Args[1:]))
  22. }
  23. func run(args []string) int {
  24. if len(args) < 1 {
  25. fmt.Fprintln(os.Stderr, usage)
  26. return 2
  27. }
  28. defaultCmd := flag.NewFlagSet("default", flag.ExitOnError)
  29. defaultSleep := defaultCmd.Duration("sleep", 5*time.Second, "Sleep duration")
  30. handleCmd := flag.NewFlagSet("handle", flag.ExitOnError)
  31. handleSleep := handleCmd.Duration("sleep", 5*time.Second, "Sleep duration")
  32. handleCleanup := handleCmd.Duration("cleanup", 5*time.Second, "Cleanup duration")
  33. handleTermAfter := handleCmd.Int("term-after", 0,
  34. "Terminate immediately after `N` signals.\n"+
  35. "Default is to terminate only when the cleanup phase has completed.")
  36. versionCmd := flag.NewFlagSet("version", flag.ExitOnError)
  37. switch args[0] {
  38. case "default":
  39. _ = defaultCmd.Parse(args[1:])
  40. if len(defaultCmd.Args()) > 0 {
  41. fmt.Fprintf(os.Stderr, "default: unexpected arguments: %v\n", defaultCmd.Args())
  42. return 2
  43. }
  44. return supervisor(*defaultSleep, 0, 0, nil)
  45. case "handle":
  46. _ = handleCmd.Parse(args[1:])
  47. if *handleTermAfter == 1 {
  48. fmt.Fprintf(os.Stderr, "handle: term-after cannot be 1\n")
  49. return 2
  50. }
  51. if len(handleCmd.Args()) > 0 {
  52. fmt.Fprintf(os.Stderr, "handle: unexpected arguments: %v\n", handleCmd.Args())
  53. return 2
  54. }
  55. sigCh := make(chan os.Signal, 1)
  56. signal.Notify(sigCh, os.Interrupt) // Ctrl-C -> SIGINT
  57. return supervisor(*handleSleep, *handleCleanup, *handleTermAfter, sigCh)
  58. case "version":
  59. _ = versionCmd.Parse(args[1:])
  60. if len(versionCmd.Args()) > 0 {
  61. fmt.Fprintf(os.Stderr, "version: unexpected arguments: %v\n", versionCmd.Args())
  62. return 2
  63. }
  64. fmt.Printf("sleepit version %s\n", fullVersion)
  65. return 0
  66. default:
  67. fmt.Fprintln(os.Stderr, usage)
  68. return 2
  69. }
  70. }
  71. func supervisor(
  72. sleep time.Duration,
  73. cleanup time.Duration,
  74. termAfter int,
  75. sigCh <-chan os.Signal,
  76. ) int {
  77. fmt.Printf("sleepit: ready\n")
  78. fmt.Printf("sleepit: PID=%d sleep=%v cleanup=%v\n",
  79. os.Getpid(), sleep, cleanup)
  80. cancelWork := make(chan struct{})
  81. workerDone := worker(cancelWork, sleep, "work")
  82. cancelCleaner := make(chan struct{})
  83. var cleanerDone <-chan struct{}
  84. sigCount := 0
  85. for {
  86. select {
  87. case sig := <-sigCh:
  88. sigCount++
  89. fmt.Printf("sleepit: got signal=%s count=%d\n", sig, sigCount)
  90. if sigCount == 1 {
  91. // since `cancelWork` is unbuffered, sending will be synchronous:
  92. // we are ensured that the worker has terminated before starting cleanup.
  93. // This is important in some real-life situations.
  94. cancelWork <- struct{}{}
  95. cleanerDone = worker(cancelCleaner, cleanup, "cleanup")
  96. }
  97. if sigCount == termAfter {
  98. cancelCleaner <- struct{}{}
  99. return 4
  100. }
  101. case <-workerDone:
  102. return 0
  103. case <-cleanerDone:
  104. return 3
  105. }
  106. }
  107. }
  108. // Start a worker goroutine and return immediately a `workerDone` channel.
  109. // The goroutine will prepend its prints with the prefix `name`.
  110. // The goroutine will simulate some work and will terminate when one of the following
  111. // conditions happens:
  112. // 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.
  113. // 2. When something happens on channel `canceled`. Note that this simulates real-life,
  114. // so cancellation is not instantaneous: if the caller wants a synchronous cancel,
  115. // it should send a message; if instead it wants an asynchronous cancel, it should
  116. // close the channel.
  117. func worker(
  118. canceled <-chan struct{},
  119. howlong time.Duration,
  120. name string,
  121. ) <-chan struct{} {
  122. workerDone := make(chan struct{})
  123. deadline := time.Now().Add(howlong)
  124. go func() {
  125. fmt.Printf("sleepit: %s started\n", name)
  126. for {
  127. select {
  128. case <-canceled:
  129. fmt.Printf("sleepit: %s canceled\n", name)
  130. return
  131. default:
  132. if doSomeWork(deadline) {
  133. fmt.Printf("sleepit: %s done\n", name) // <== NOTE THIS LINE
  134. workerDone <- struct{}{}
  135. return
  136. }
  137. }
  138. }
  139. }()
  140. return workerDone
  141. }
  142. // Do some work and then return, so that the caller can decide wether to continue or not.
  143. // Return true when all work is done.
  144. func doSomeWork(deadline time.Time) bool {
  145. if time.Now().After(deadline) {
  146. return true
  147. }
  148. timeout := 100 * time.Millisecond
  149. time.Sleep(timeout)
  150. return false
  151. }