github.com/davidmanzanares/dsd@v0.1.2-0.20210106152357-a35988f5d245/dsdl/run.go (about)

     1  package dsdl
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"path"
     8  	"time"
     9  
    10  	"github.com/davidmanzanares/dsd/types"
    11  )
    12  
    13  // RunConf is the Runner's configuration
    14  type RunConf struct {
    15  	Args      []string
    16  	HotReload bool
    17  	OnSuccess RunReaction
    18  	OnFailure RunReaction
    19  	Polling   time.Duration
    20  }
    21  
    22  // DefaultPolling for Run when RunConf.Polling is set to the zero time.Duration
    23  const DefaultPolling = 5 * time.Second
    24  
    25  // RunReaction is the action to take when the deployed application exits
    26  type RunReaction int
    27  
    28  const (
    29  	// Exit will stop the Runner's goroutine, returning "Stopped" to WaitForEvent()
    30  	Exit RunReaction = iota
    31  	// Wait will wait for future update (deploys), which will be started
    32  	Wait
    33  	// Restart will restart the application inmediatly
    34  	Restart
    35  )
    36  
    37  // Runner manages the download, execution and updating process of a deployed application
    38  type Runner struct {
    39  	events   chan RunEvent
    40  	commands chan string
    41  
    42  	conf           RunConf
    43  	provider       types.Provider
    44  	currentVersion types.Version
    45  	appExe         string
    46  	spawned        *os.Process
    47  	exit           chan exitType
    48  }
    49  type exitType struct {
    50  	code int
    51  	v    types.Version
    52  }
    53  
    54  // RunEventType is the type of events generated by Runner
    55  type RunEventType int
    56  
    57  const (
    58  	// AppStarted events are sent when the application process gets started
    59  	AppStarted RunEventType = iota
    60  	// AppExit events are sent when the application process ends without being interrupted/killed for hotreloading updates
    61  	AppExit
    62  	// Stopped events are sent when the runner ends its execution, this is controlled by the OnSuccess/OnFailure properties of RunConf
    63  	Stopped
    64  )
    65  
    66  // RunEvent is an event generated by the Runner
    67  // Version is only valid for AppStarted events
    68  // ExitCode is only valid for Stopped events
    69  type RunEvent struct {
    70  	Type     RunEventType
    71  	Version  types.Version
    72  	ExitCode int
    73  	Reason   string
    74  }
    75  
    76  func (e RunEvent) String() string {
    77  	if e.Type == AppStarted {
    78  		return fmt.Sprintf("AppStarted{Version: %s Reason: %s}", e.Version, e.Reason)
    79  	} else if e.Type == AppExit {
    80  		return fmt.Sprintf("AppExit{Version: %s, ExitCode: %d}", e.Version, e.ExitCode)
    81  	} else if e.Type == Stopped {
    82  		return "Stopped"
    83  	} else {
    84  		panic(e)
    85  	}
    86  }
    87  
    88  // Run runs a deployed application on service with a configuration
    89  func Run(service string, conf RunConf) (*Runner, error) {
    90  	p, err := getProviderFromService(service)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	if conf.Polling == 0 {
    96  		conf.Polling = DefaultPolling
    97  	}
    98  
    99  	r := &Runner{events: make(chan RunEvent, 10), commands: make(chan string, 10), provider: p, conf: conf}
   100  	go r.manager()
   101  	r.commands <- "update"
   102  	return r, nil
   103  }
   104  
   105  // WaitForEvent waits for the generation of the next RunEvent
   106  func (r *Runner) WaitForEvent() RunEvent {
   107  	ev, ok := <-r.events
   108  	if !ok {
   109  		return RunEvent{Type: Stopped}
   110  	}
   111  	return ev
   112  }
   113  
   114  // Stop stops the current runner, interrupting/killing the application
   115  func (r *Runner) Stop() {
   116  	r.commands <- "stop"
   117  }
   118  
   119  func (r *Runner) manager() {
   120  	defer close(r.events)
   121  	for {
   122  		select {
   123  		case exit := <-r.exit:
   124  			r.events <- RunEvent{Type: AppExit, Version: exit.v, ExitCode: exit.code}
   125  			r.spawned = nil
   126  			if exit.code == 0 {
   127  				if r.conf.OnSuccess == Restart {
   128  					r.run("restarted on success")
   129  				} else if r.conf.OnSuccess == Exit {
   130  					return
   131  				} else {
   132  					r.exit = nil
   133  				}
   134  			} else {
   135  				if r.conf.OnFailure == Restart {
   136  					r.run("restarted on failure")
   137  				} else if r.conf.OnFailure == Exit {
   138  					return
   139  				} else {
   140  					r.exit = nil
   141  				}
   142  			}
   143  		case <-time.After(r.conf.Polling):
   144  			if r.conf.HotReload || r.spawned == nil {
   145  				r.update()
   146  			}
   147  		case cmd := <-r.commands:
   148  			if cmd == "update" {
   149  				r.update()
   150  				continue
   151  			} else if cmd == "stop" {
   152  				r.kill()
   153  				return
   154  			} else {
   155  				panic(fmt.Sprint("Unkown command:", cmd))
   156  			}
   157  		}
   158  	}
   159  }
   160  func (r *Runner) kill() {
   161  	if r.spawned != nil {
   162  		// TODO call Interrupt first
   163  		//r.spawned.Signal(os.Interrupt)
   164  		//time.Sleep(time.Second)
   165  
   166  		err := kill(r.spawned)
   167  		if err != nil {
   168  			log.Println(err)
   169  		}
   170  
   171  		r.spawned = nil
   172  		for range r.exit {
   173  		}
   174  	}
   175  }
   176  
   177  func (r *Runner) update() {
   178  	v, err := r.provider.GetCurrentVersion()
   179  	if err != nil {
   180  		log.Println(err)
   181  		return
   182  	}
   183  	if v.Name == r.currentVersion.Name {
   184  		return
   185  	}
   186  	exe, err := download(r.provider, v)
   187  	if err != nil {
   188  		log.Println(err)
   189  		return
   190  	}
   191  	if exe == "" {
   192  		log.Println("Error, executable not found")
   193  		return
   194  	}
   195  	r.appExe = exe
   196  	r.currentVersion = v
   197  	r.run("update")
   198  }
   199  
   200  func (r *Runner) run(reason string) {
   201  	r.kill()
   202  	wd, err := os.Getwd()
   203  	if err != nil {
   204  		log.Println(err)
   205  		return
   206  	}
   207  	// TODO windows should kill the process tree (grand children too)
   208  	r.spawned, err = os.StartProcess(path.Join(wd, r.appExe), append([]string{r.appExe}, r.conf.Args...),
   209  		&os.ProcAttr{
   210  			Dir:   path.Dir(r.appExe),
   211  			Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
   212  			Sys:   runSysProcAttr()})
   213  	if err != nil {
   214  		log.Println(err)
   215  		return
   216  	}
   217  	r.events <- RunEvent{Type: AppStarted, Version: r.currentVersion, Reason: reason}
   218  	r.exit = make(chan exitType)
   219  	go func(spawned *os.Process, v types.Version, exitCh chan exitType) {
   220  		state, _ := spawned.Wait()
   221  		exitCh <- exitType{code: state.ExitCode(), v: v}
   222  		close(exitCh)
   223  	}(r.spawned, r.currentVersion, r.exit)
   224  }