-
Notifications
You must be signed in to change notification settings - Fork 0
windows: restart daemon children without stopping LanternSvc #615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,23 +75,37 @@ func install(dataPath, logPath, logLevel string, environment daemonEnvironment) | |
| } | ||
| defer service.Close() | ||
|
|
||
| err = service.SetRecoveryActions([]mgr.RecoveryAction{ | ||
| if err := configureWindowsServiceRecovery(service); err != nil { | ||
| return err | ||
| } | ||
| if err := service.Start(); err != nil { | ||
| return fmt.Errorf("failed to start service: %w", err) | ||
| } | ||
|
|
||
| slog.Info("Windows service installed successfully") | ||
| return nil | ||
| } | ||
|
|
||
| type windowsServiceRecoveryConfigurer interface { | ||
| SetRecoveryActions([]mgr.RecoveryAction, uint32) error | ||
| SetRecoveryActionsOnNonCrashFailures(bool) error | ||
| } | ||
|
|
||
| func configureWindowsServiceRecovery(service windowsServiceRecoveryConfigurer) error { | ||
|
Comment on lines
+94
to
+99
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Document the internal service contracts. Add Go doc comments immediately above As per coding guidelines, “Use Go doc comments ( Also applies to: 169-193 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| if err := service.SetRecoveryActions([]mgr.RecoveryAction{ | ||
| {Type: mgr.ServiceRestart, Delay: 1 * time.Second}, | ||
| {Type: mgr.ServiceRestart, Delay: 2 * time.Second}, | ||
| {Type: mgr.ServiceRestart, Delay: 4 * time.Second}, | ||
| {Type: mgr.ServiceRestart, Delay: 8 * time.Second}, | ||
| {Type: mgr.ServiceRestart, Delay: 16 * time.Second}, | ||
| {Type: mgr.ServiceRestart, Delay: 32 * time.Second}, | ||
| {Type: mgr.ServiceRestart, Delay: 64 * time.Second}, | ||
| }, 60) | ||
| if err != nil { | ||
| }, 60); err != nil { | ||
| return fmt.Errorf("failed to set service recovery actions: %w", err) | ||
| } | ||
| if err := service.Start(); err != nil { | ||
| return fmt.Errorf("failed to start service: %w", err) | ||
| if err := service.SetRecoveryActionsOnNonCrashFailures(true); err != nil { | ||
| return fmt.Errorf("failed to enable recovery actions for non-crash failures: %w", err) | ||
| } | ||
|
|
||
| slog.Info("Windows service installed successfully") | ||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -152,10 +166,51 @@ func maybePlatformService() bool { | |
| return true | ||
| } | ||
|
|
||
| type service struct{} | ||
| type windowsServiceChild interface { | ||
| Done() <-chan error | ||
| RequestShutdown() | ||
| WaitOrKill(time.Duration) error | ||
| HandleCrash(error) | ||
| info(string, ...any) | ||
| } | ||
|
|
||
| type windowsServiceChildProcess struct { | ||
| *childProcess | ||
| } | ||
|
|
||
| func (c *windowsServiceChildProcess) info(message string, args ...any) { | ||
| c.logger.Info(message, args...) | ||
| } | ||
|
|
||
| type windowsServiceBackoff interface { | ||
| Wait(context.Context) | ||
| Reset() | ||
| } | ||
|
|
||
| type service struct { | ||
| spawnChild func([]string, string, string, string) (windowsServiceChild, error) | ||
| newBackoff func() windowsServiceBackoff | ||
| } | ||
|
|
||
| // newWindowsService returns a production service handler. Its injected process and backoff | ||
| // functions keep supervision tests independent of OS processes and wall-clock delays. | ||
| func newWindowsService() *service { | ||
| return &service{ | ||
| spawnChild: func(args []string, dataPath, logPath, logLevel string) (windowsServiceChild, error) { | ||
| child, err := spawnChild(args, dataPath, logPath, logLevel) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return &windowsServiceChildProcess{childProcess: child}, nil | ||
| }, | ||
| newBackoff: func() windowsServiceBackoff { | ||
| return common.NewBackoff(daemonRestartBackoffMax) | ||
| }, | ||
|
Comment on lines
+211
to
+213
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline common/backoff.go --items all --type function --match Wait
sed -n '1,80p' common/backoff.go
rg -n -C2 'common\.NewBackoff|daemonRestartBackoffMax' cmd/lanternd/lanternd_windows.goRepository: getlantern/radiance Length of output: 1455 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,240p' cmd/lanternd/lanternd_windows.go
printf '\n--- backoff references and restart policy ---\n'
rg -n -C3 'daemonRestartBackoffMax|newBackoff|Backoff|exponential|quadratic|restart backoff' --glob '*.go' --glob '*.md' .Repository: getlantern/radiance Length of output: 19421 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '235,290p' cmd/lanternd/lanternd_windows.go
sed -n '340,410p' cmd/lanternd/lanternd.go
sed -n '1,60p' cmd/lantern/watch.goRepository: getlantern/radiance Length of output: 5001 Use capped exponential backoff.
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| func startWindowsService() error { | ||
| return svc.Run(serviceName, &service{}) | ||
| return svc.Run(serviceName, newWindowsService()) | ||
| } | ||
|
|
||
| func (s *service) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { | ||
|
|
@@ -170,33 +225,71 @@ func (s *service) Execute(args []string, r <-chan svc.ChangeRequest, status chan | |
| slog.Error("Failed to parse service arguments", "error", err) | ||
| return true, 1 | ||
| } | ||
| return s.run(config, r, status) | ||
| } | ||
|
|
||
| // Run the daemon as a child process so we can clean up network state if it crashes, | ||
| // regardless of whether the SCM is configured to restart the service. | ||
| // run supervises the daemon child so crash cleanup and restart do not depend on SCM recovery. | ||
| func (s *service) run(config serviceRunConfig, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { | ||
| childArgs := config.args() | ||
| child, err := spawnChild(childArgs, config.dataPath, config.logPath, config.logLevel) | ||
| child, err := s.spawnChild(childArgs, config.dataPath, config.logPath, config.logLevel) | ||
| if err != nil { | ||
| slog.Error("Failed to start daemon", "error", err) | ||
| return true, 1 | ||
| } | ||
|
|
||
| status <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} | ||
| child.logger.Info("Running as Windows service") | ||
| child.info("Running as Windows service") | ||
|
|
||
| backoff := s.newBackoff() | ||
| startedAt := time.Now() | ||
| childDone := child.Done() | ||
| var restartReady <-chan struct{} | ||
| serviceContext, cancelService := context.WithCancel(context.Background()) | ||
| defer cancelService() | ||
|
|
||
| for { | ||
| select { | ||
| case err := <-child.Done(): | ||
| case err := <-childDone: | ||
| if err != nil { | ||
| child.HandleCrash(err) | ||
| } | ||
| return true, 1 | ||
| if time.Since(startedAt) > daemonRestartBackoffResetAfter { | ||
| backoff.Reset() | ||
| } | ||
| child.info("Restarting daemon process") | ||
| child = nil | ||
| childDone = nil | ||
|
|
||
| restartDone := make(chan struct{}) | ||
| restartReady = restartDone | ||
| go func() { | ||
| backoff.Wait(serviceContext) | ||
| close(restartDone) | ||
| }() | ||
| case <-restartReady: | ||
| restartReady = nil | ||
|
|
||
| child, err = s.spawnChild(childArgs, config.dataPath, config.logPath, config.logLevel) | ||
| if err != nil { | ||
| slog.Error("Failed to restart daemon", "error", err) | ||
| return true, 1 | ||
| } | ||
| startedAt = time.Now() | ||
| childDone = child.Done() | ||
| child.info("Running as Windows service") | ||
| case change := <-r: | ||
| switch change.Cmd { | ||
| case svc.Stop, svc.Shutdown: | ||
| status <- svc.Status{State: svc.StopPending} | ||
| child.logger.Info("Service stop requested") | ||
| child.RequestShutdown() | ||
| child.WaitOrKill(15 * time.Second) | ||
| cancelService() | ||
| if restartReady != nil { | ||
| <-restartReady | ||
| } | ||
| if child != nil { | ||
| child.info("Service stop requested") | ||
| child.RequestShutdown() | ||
| child.WaitOrKill(15 * time.Second) | ||
| } | ||
|
atavism marked this conversation as resolved.
|
||
| return false, windows.NO_ERROR | ||
| case svc.Interrogate: | ||
| status <- change.CurrentStatus | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document the shared restart timing constants.
These constants define the restart backoff contract used by both
babysitand the Windows service supervisor. Add identifier-leading Go doc comments that explain the maximum delay and the stable-runtime threshold.As per coding guidelines, unexported identifiers with non-obvious contracts require Go doc comments.
Suggested documentation
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines