123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- // This code is released under the MIT License
- // Copyright (c) 2020 Marco Molteni and the timeit contributors.
- package main
- import (
- "flag"
- "fmt"
- "os"
- "os/signal"
- "time"
- )
- const usage = `sleepit: sleep for the specified duration, optionally handling signals
- When the line "sleepit: ready" is printed, it means that it is safe to send signals to it
- Usage: sleepit <command> [<args>]
- Commands
- default Use default action: on reception of SIGINT terminate abruptly
- handle Handle signals: on reception of SIGINT perform cleanup before exiting
- version Show the sleepit version`
- // Filled by the linker.
- var fullVersion = "unknown" // example: v0.0.9-8-g941583d027-dirty
- func main() {
- os.Exit(run(os.Args[1:]))
- }
- func run(args []string) int {
- if len(args) < 1 {
- fmt.Fprintln(os.Stderr, usage)
- return 2
- }
- defaultCmd := flag.NewFlagSet("default", flag.ExitOnError)
- defaultSleep := defaultCmd.Duration("sleep", 5*time.Second, "Sleep duration")
- handleCmd := flag.NewFlagSet("handle", flag.ExitOnError)
- handleSleep := handleCmd.Duration("sleep", 5*time.Second, "Sleep duration")
- handleCleanup := handleCmd.Duration("cleanup", 5*time.Second, "Cleanup duration")
- handleTermAfter := handleCmd.Int("term-after", 0,
- "Terminate immediately after `N` signals.\n"+
- "Default is to terminate only when the cleanup phase has completed.")
- versionCmd := flag.NewFlagSet("version", flag.ExitOnError)
- switch args[0] {
- case "default":
- _ = defaultCmd.Parse(args[1:])
- if len(defaultCmd.Args()) > 0 {
- fmt.Fprintf(os.Stderr, "default: unexpected arguments: %v\n", defaultCmd.Args())
- return 2
- }
- return supervisor(*defaultSleep, 0, 0, nil)
- case "handle":
- _ = handleCmd.Parse(args[1:])
- if *handleTermAfter == 1 {
- fmt.Fprintf(os.Stderr, "handle: term-after cannot be 1\n")
- return 2
- }
- if len(handleCmd.Args()) > 0 {
- fmt.Fprintf(os.Stderr, "handle: unexpected arguments: %v\n", handleCmd.Args())
- return 2
- }
- sigCh := make(chan os.Signal, 1)
- signal.Notify(sigCh, os.Interrupt) // Ctrl-C -> SIGINT
- return supervisor(*handleSleep, *handleCleanup, *handleTermAfter, sigCh)
- case "version":
- _ = versionCmd.Parse(args[1:])
- if len(versionCmd.Args()) > 0 {
- fmt.Fprintf(os.Stderr, "version: unexpected arguments: %v\n", versionCmd.Args())
- return 2
- }
- fmt.Printf("sleepit version %s\n", fullVersion)
- return 0
- default:
- fmt.Fprintln(os.Stderr, usage)
- return 2
- }
- }
- func supervisor(
- sleep time.Duration,
- cleanup time.Duration,
- termAfter int,
- sigCh <-chan os.Signal,
- ) int {
- fmt.Printf("sleepit: ready\n")
- fmt.Printf("sleepit: PID=%d sleep=%v cleanup=%v\n",
- os.Getpid(), sleep, cleanup)
- cancelWork := make(chan struct{})
- workerDone := worker(cancelWork, sleep, "work")
- cancelCleaner := make(chan struct{})
- var cleanerDone <-chan struct{}
- sigCount := 0
- for {
- select {
- case sig := <-sigCh:
- sigCount++
- fmt.Printf("sleepit: got signal=%s count=%d\n", sig, sigCount)
- if sigCount == 1 {
- // since `cancelWork` is unbuffered, sending will be synchronous:
- // we are ensured that the worker has terminated before starting cleanup.
- // This is important in some real-life situations.
- cancelWork <- struct{}{}
- cleanerDone = worker(cancelCleaner, cleanup, "cleanup")
- }
- if sigCount == termAfter {
- cancelCleaner <- struct{}{}
- return 4
- }
- case <-workerDone:
- return 0
- case <-cleanerDone:
- return 3
- }
- }
- }
- // Start a worker goroutine and return immediately a `workerDone` channel.
- // The goroutine will prepend its prints with the prefix `name`.
- // The goroutine will simulate some work and will terminate when one of the following
- // conditions happens:
- // 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.
- // 2. When something happens on channel `canceled`. Note that this simulates real-life,
- // so cancellation is not instantaneous: if the caller wants a synchronous cancel,
- // it should send a message; if instead it wants an asynchronous cancel, it should
- // close the channel.
- func worker(
- canceled <-chan struct{},
- howlong time.Duration,
- name string,
- ) <-chan struct{} {
- workerDone := make(chan struct{})
- deadline := time.Now().Add(howlong)
- go func() {
- fmt.Printf("sleepit: %s started\n", name)
- for {
- select {
- case <-canceled:
- fmt.Printf("sleepit: %s canceled\n", name)
- return
- default:
- if doSomeWork(deadline) {
- fmt.Printf("sleepit: %s done\n", name) // <== NOTE THIS LINE
- workerDone <- struct{}{}
- return
- }
- }
- }
- }()
- return workerDone
- }
- // Do some work and then return, so that the caller can decide wether to continue or not.
- // Return true when all work is done.
- func doSomeWork(deadline time.Time) bool {
- if time.Now().After(deadline) {
- return true
- }
- timeout := 100 * time.Millisecond
- time.Sleep(timeout)
- return false
- }
|