Skip to content

Commit 2cb6696

Browse files
authored
[cmd/opampsupervisor] Make supervisor runnable as Windows service (#35275)
**Description:** <Describe what has changed.> <!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> Add support for running supervisor as a Windows Service. Updates entry point to run a service handler if being ran as a Windows Service by implementing the [handler interface](https://pkg.go.dev/golang.org/x/sys/windows/svc#Handler). Also updates the Windows Commander to allocate a console if running as a service. We send a CTRL_BREAK_EVENT console event to the agent to signal a shutdown however windows services do not run with consoles. If running as service we need to allocate a console to send the signal and then free the console. **Link to tracking Issue:** <Issue number if applicable> Closes #34774 **Testing:** <Describe what testing was performed and which tests were added.> - Tested using a windows VM and using `sc.exe` for creating the service **Documentation:** <Describe the documentation added.>
1 parent 6945c86 commit 2cb6696

File tree

11 files changed

+483
-9
lines changed

11 files changed

+483
-9
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: opampsupervisor
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Make supervisor runnable as a Windows Service.
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [34774]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: []

.github/workflows/e2e-tests-windows.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,48 @@ jobs:
8282
run: |
8383
cd cmd/opampsupervisor
8484
go test -v --tags=e2e
85+
86+
windows-supervisor-service-test:
87+
runs-on: windows-latest
88+
if: ${{ github.actor != 'dependabot[bot]' && (contains(github.event.pull_request.labels.*.name, 'Run Windows') || github.event_name == 'push' || github.event_name == 'merge_group') }}
89+
needs: [collector-build]
90+
steps:
91+
- name: Checkout Repo
92+
uses: actions/checkout@v4
93+
- name: Setup Go
94+
uses: actions/setup-go@v5
95+
with:
96+
go-version: ~1.22.7
97+
cache: false
98+
- name: Cache Go
99+
uses: actions/cache@v4
100+
env:
101+
cache-name: cache-go-modules
102+
with:
103+
path: |
104+
~\go\pkg\mod
105+
~\AppData\Local\go-build
106+
key: go-build-cache-${{ runner.os }}-${{ matrix.group }}-go-${{ hashFiles('**/go.sum') }}
107+
- name: Ensure required ports in the dynamic range are available
108+
run: |
109+
& ${{ github.workspace }}\.github\workflows\scripts\win-required-ports.ps1
110+
- name: Download Collector Binary
111+
uses: actions/download-artifact@v4
112+
with:
113+
name: collector-binary
114+
path: bin/
115+
- name: Build supervisor
116+
run: cd cmd/opampsupervisor; go build
117+
- name: Install supervisor as a service
118+
run: |
119+
New-Service -Name "opampsupervisor" -StartupType "Manual" -BinaryPathName "${PWD}\cmd\opampsupervisor --config ${PWD}\cmd\opampsupervisor\supervisor\testdata\supervisor_windows_service_test_config.yaml\"
120+
eventcreate.exe /t information /id 1 /l application /d "Creating event provider for 'opampsupervisor'" /so opampsupervisor
121+
- name: Test supervisor service
122+
working-directory: ${{ github.workspace }}/cmd/opampsupervisor
123+
run: |
124+
go test -timeout 90s -run ^TestSupervisorAsService$ -v -tags=win32service
125+
- name: Remove opampsupervisor service
126+
if: always()
127+
run: |
128+
Remove-Service opampsupervisor
129+
Remove-Item HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\Application\opampsupervisor

cmd/opampsupervisor/main.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55

66
import (
77
"flag"
8+
"fmt"
89
"log"
910
"os"
1011
"os/signal"
@@ -15,33 +16,39 @@ import (
1516
)
1617

1718
func main() {
19+
if err := run(); err != nil {
20+
log.Fatal(err)
21+
}
22+
}
23+
24+
func runInteractive() error {
1825
configFlag := flag.String("config", "", "Path to a supervisor configuration file")
1926
flag.Parse()
2027

2128
cfg, err := config.Load(*configFlag)
2229
if err != nil {
23-
log.Fatal("failed to load config: %w", err)
30+
return fmt.Errorf("failed to load config: %w", err)
2431
}
2532

2633
logger, err := telemetry.NewLogger(cfg.Telemetry.Logs)
2734
if err != nil {
28-
log.Fatal("failed to create logger: %w", err)
35+
return fmt.Errorf("failed to create logger: %w", err)
2936
}
3037

3138
supervisor, err := supervisor.NewSupervisor(logger, cfg)
3239
if err != nil {
33-
logger.Error(err.Error())
34-
os.Exit(-1)
35-
return
40+
return fmt.Errorf("failed to create supervisor: %w", err)
3641
}
3742

3843
err = supervisor.Start()
3944
if err != nil {
40-
log.Fatal("failed to start supervisor: %w", err)
45+
return fmt.Errorf("failed to start supervisor: %w", err)
4146
}
4247

4348
interrupt := make(chan os.Signal, 1)
4449
signal.Notify(interrupt, os.Interrupt)
4550
<-interrupt
4651
supervisor.Shutdown()
52+
53+
return nil
4754
}

cmd/opampsupervisor/main_others.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build !windows
5+
6+
package main
7+
8+
func run() error {
9+
return runInteractive()
10+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build windows
5+
6+
package main
7+
8+
import (
9+
"errors"
10+
"fmt"
11+
12+
"golang.org/x/sys/windows"
13+
"golang.org/x/sys/windows/svc"
14+
15+
"github.com/open-telemetry/opentelemetry-collector-contrib/cmd/opampsupervisor/supervisor"
16+
)
17+
18+
var (
19+
kernel32API = windows.NewLazySystemDLL("kernel32.dll")
20+
21+
allocConsoleProc = kernel32API.NewProc("AllocConsole")
22+
freeConsoleProc = kernel32API.NewProc("FreeConsole")
23+
)
24+
25+
func run() error {
26+
// always allocate a console in case we're running as service
27+
if err := allocConsole(); err != nil {
28+
if !errors.Is(err, windows.ERROR_ACCESS_DENIED) {
29+
// Per https://learn.microsoft.com/en-us/windows/console/allocconsole#remarks
30+
// AllocConsole fails with this error when there's already a console attached, such as not being ran as service
31+
// ignore this error and only return other errors
32+
return fmt.Errorf("alloc console: %w", err)
33+
}
34+
35+
}
36+
defer func() {
37+
_ = freeConsole()
38+
}()
39+
40+
// No need to supply service name when startup is invoked through
41+
// the Service Control Manager directly.
42+
if err := svc.Run("", supervisor.NewSvcHandler()); err != nil {
43+
if errors.Is(err, windows.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) {
44+
// Per https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-startservicectrldispatchera#return-value
45+
// this means that the process is not running as a service, so run interactively.
46+
47+
return runInteractive()
48+
}
49+
return fmt.Errorf("failed to start supervisor: %w", err)
50+
}
51+
return nil
52+
}
53+
54+
// windows services don't get created with a console
55+
// need to allocate a console in order to send CTRL_BREAK_EVENT to agent sub process
56+
func allocConsole() error {
57+
ret, _, err := allocConsoleProc.Call()
58+
if ret == 0 {
59+
return err
60+
}
61+
return nil
62+
}
63+
64+
// free console once we're done with it
65+
func freeConsole() error {
66+
ret, _, err := freeConsoleProc.Call()
67+
if ret == 0 {
68+
return err
69+
}
70+
return nil
71+
}

cmd/opampsupervisor/supervisor/commander/commander_windows.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package commander
77

88
import (
9+
"fmt"
910
"os"
1011
"syscall"
1112

@@ -24,7 +25,7 @@ func sendShutdownSignal(process *os.Process) error {
2425
// See: https://learn.microsoft.com/en-us/windows/console/generateconsolectrlevent
2526
r, _, e := ctrlEventProc.Call(syscall.CTRL_BREAK_EVENT, uintptr(process.Pid))
2627
if r == 0 {
27-
return e
28+
return fmt.Errorf("sendShutdownSignal to PID '%d': %w", process.Pid, e)
2829
}
2930

3031
return nil

0 commit comments

Comments
 (0)