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 }