github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/graceful/manager_windows.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 // This code is heavily inspired by the archived gofacebook/gracenet/net.go handler 7 8 //go:build windows 9 10 package graceful 11 12 import ( 13 "context" 14 "os" 15 "runtime/pprof" 16 "strconv" 17 "sync" 18 "time" 19 20 "github.com/gitbundle/modules/log" 21 "github.com/gitbundle/modules/setting" 22 23 "golang.org/x/sys/windows/svc" 24 "golang.org/x/sys/windows/svc/debug" 25 ) 26 27 // WindowsServiceName is the name of the Windows service 28 var WindowsServiceName = "gitbundle" 29 30 const ( 31 hammerCode = 128 32 hammerCmd = svc.Cmd(hammerCode) 33 acceptHammerCode = svc.Accepted(hammerCode) 34 ) 35 36 // Manager manages the graceful shutdown process 37 type Manager struct { 38 ctx context.Context 39 isChild bool 40 lock *sync.RWMutex 41 state state 42 shutdownCtx context.Context 43 hammerCtx context.Context 44 terminateCtx context.Context 45 managerCtx context.Context 46 shutdownCtxCancel context.CancelFunc 47 hammerCtxCancel context.CancelFunc 48 terminateCtxCancel context.CancelFunc 49 managerCtxCancel context.CancelFunc 50 runningServerWaitGroup sync.WaitGroup 51 createServerWaitGroup sync.WaitGroup 52 terminateWaitGroup sync.WaitGroup 53 shutdownRequested chan struct{} 54 55 toRunAtShutdown []func() 56 toRunAtHammer []func() 57 toRunAtTerminate []func() 58 } 59 60 func newGracefulManager(ctx context.Context) *Manager { 61 manager := &Manager{ 62 isChild: false, 63 lock: &sync.RWMutex{}, 64 ctx: ctx, 65 } 66 manager.createServerWaitGroup.Add(numberOfServersToCreate) 67 manager.start() 68 return manager 69 } 70 71 func (g *Manager) start() { 72 // Make contexts 73 g.terminateCtx, g.terminateCtxCancel = context.WithCancel(g.ctx) 74 g.shutdownCtx, g.shutdownCtxCancel = context.WithCancel(g.ctx) 75 g.hammerCtx, g.hammerCtxCancel = context.WithCancel(g.ctx) 76 g.managerCtx, g.managerCtxCancel = context.WithCancel(g.ctx) 77 78 // Next add pprof labels to these contexts 79 g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "with-terminate")) 80 g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels("graceful-lifecycle", "with-shutdown")) 81 g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "with-hammer")) 82 g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "with-manager")) 83 84 // Now label this and all goroutines created by this goroutine with the graceful-lifecycle manager 85 pprof.SetGoroutineLabels(g.managerCtx) 86 defer pprof.SetGoroutineLabels(g.ctx) 87 88 // Make channels 89 g.shutdownRequested = make(chan struct{}) 90 91 // Set the running state 92 g.setState(stateRunning) 93 if skip, _ := strconv.ParseBool(os.Getenv("SKIP_MINWINSVC")); skip { 94 log.Trace("Skipping SVC check as SKIP_MINWINSVC is set") 95 return 96 } 97 98 // Make SVC process 99 run := svc.Run 100 101 //lint:ignore SA1019 We use IsAnInteractiveSession because IsWindowsService has a different permissions profile 102 isAnInteractiveSession, err := svc.IsAnInteractiveSession() 103 if err != nil { 104 log.Error("Unable to ascertain if running as an Windows Service: %v", err) 105 return 106 } 107 if isAnInteractiveSession { 108 log.Trace("Not running a service ... using the debug SVC manager") 109 run = debug.Run 110 } 111 go func() { 112 _ = run(WindowsServiceName, g) 113 }() 114 } 115 116 // Execute makes Manager implement svc.Handler 117 func (g *Manager) Execute(args []string, changes <-chan svc.ChangeRequest, status chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { 118 if setting.StartupTimeout > 0 { 119 status <- svc.Status{State: svc.StartPending} 120 } else { 121 status <- svc.Status{State: svc.StartPending, WaitHint: uint32(setting.StartupTimeout / time.Millisecond)} 122 } 123 124 log.Trace("Awaiting server start-up") 125 // Now need to wait for everything to start... 126 if !g.awaitServer(setting.StartupTimeout) { 127 log.Trace("... start-up failed ... Stopped") 128 return false, 1 129 } 130 131 log.Trace("Sending Running state to SVC") 132 133 // We need to implement some way of svc.AcceptParamChange/svc.ParamChange 134 status <- svc.Status{ 135 State: svc.Running, 136 Accepts: svc.AcceptStop | svc.AcceptShutdown | acceptHammerCode, 137 } 138 139 log.Trace("Started") 140 141 waitTime := 30 * time.Second 142 143 loop: 144 for { 145 select { 146 case <-g.ctx.Done(): 147 log.Trace("Shutting down") 148 g.DoGracefulShutdown() 149 waitTime += setting.GracefulHammerTime 150 break loop 151 case <-g.shutdownRequested: 152 log.Trace("Shutting down") 153 waitTime += setting.GracefulHammerTime 154 break loop 155 case change := <-changes: 156 switch change.Cmd { 157 case svc.Interrogate: 158 log.Trace("SVC sent interrogate") 159 status <- change.CurrentStatus 160 case svc.Stop, svc.Shutdown: 161 log.Trace("SVC requested shutdown - shutting down") 162 g.DoGracefulShutdown() 163 waitTime += setting.GracefulHammerTime 164 break loop 165 case hammerCode: 166 log.Trace("SVC requested hammer - shutting down and hammering immediately") 167 g.DoGracefulShutdown() 168 g.DoImmediateHammer() 169 break loop 170 default: 171 log.Debug("Unexpected control request: %v", change.Cmd) 172 } 173 } 174 } 175 176 log.Trace("Sending StopPending state to SVC") 177 status <- svc.Status{ 178 State: svc.StopPending, 179 WaitHint: uint32(waitTime / time.Millisecond), 180 } 181 182 hammerLoop: 183 for { 184 select { 185 case change := <-changes: 186 switch change.Cmd { 187 case svc.Interrogate: 188 log.Trace("SVC sent interrogate") 189 status <- change.CurrentStatus 190 case svc.Stop, svc.Shutdown, hammerCmd: 191 log.Trace("SVC requested hammer - hammering immediately") 192 g.DoImmediateHammer() 193 break hammerLoop 194 default: 195 log.Debug("Unexpected control request: %v", change.Cmd) 196 } 197 case <-g.hammerCtx.Done(): 198 break hammerLoop 199 } 200 } 201 202 log.Trace("Stopped") 203 return false, 0 204 } 205 206 // DoImmediateHammer causes an immediate hammer 207 func (g *Manager) DoImmediateHammer() { 208 g.doHammerTime(0 * time.Second) 209 } 210 211 // DoGracefulShutdown causes a graceful shutdown 212 func (g *Manager) DoGracefulShutdown() { 213 g.lock.Lock() 214 select { 215 case <-g.shutdownRequested: 216 g.lock.Unlock() 217 default: 218 close(g.shutdownRequested) 219 g.lock.Unlock() 220 g.doShutdown() 221 } 222 } 223 224 // RegisterServer registers the running of a listening server. 225 // Any call to RegisterServer must be matched by a call to ServerDone 226 func (g *Manager) RegisterServer() { 227 g.runningServerWaitGroup.Add(1) 228 } 229 230 func (g *Manager) awaitServer(limit time.Duration) bool { 231 c := make(chan struct{}) 232 go func() { 233 defer close(c) 234 g.createServerWaitGroup.Wait() 235 }() 236 if limit > 0 { 237 select { 238 case <-c: 239 return true // completed normally 240 case <-time.After(limit): 241 return false // timed out 242 case <-g.IsShutdown(): 243 return false 244 } 245 } else { 246 select { 247 case <-c: 248 return true // completed normally 249 case <-g.IsShutdown(): 250 return false 251 } 252 } 253 }