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  }