Files
caddy/service_windows_test.go
T
Krish Singh Hura 8e237f1955 windows: serialise service status updates (#8274)
* publish service status reported before the SCM handler attaches
* notify: serialize Windows service status updates
2026-10-25 02:27:06 +10:00

151 lines
4.2 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2/notify"
"golang.org/x/sys/windows/svc"
)
func TestRunnerStatusOrdering(t *testing.T) {
for _, order := range []string{"ready first", "runner first", "concurrent"} {
t.Run(order, func(t *testing.T) {
notify.SetGlobalStatus(nil)
t.Cleanup(func() { notify.SetGlobalStatus(nil) })
requests := make(chan svc.ChangeRequest)
statuses := make(chan svc.Status, 4)
done := make(chan struct{})
if order == "ready first" {
_ = notify.Ready()
}
go func() {
defer close(done)
runner{}.Execute(nil, requests, statuses)
}()
if order == "runner first" {
select {
case s := <-statuses:
if s.State != svc.StartPending {
t.Fatalf("initial status: %+v", s)
}
case <-time.After(5 * time.Second):
t.Fatal("runner did not start")
}
}
requestDone := make(chan struct{})
go func() {
defer close(requestDone)
defer close(requests)
if order != "ready first" {
_ = notify.Ready()
}
// The snapshot is deliberately stale: the owner must publish RUNNING.
requests <- svc.ChangeRequest{Cmd: svc.Interrogate, CurrentStatus: svc.Status{State: svc.StartPending}}
}()
t.Cleanup(func() {
select {
case <-done:
case <-time.After(5 * time.Second):
t.Error("runner did not return")
}
})
select {
case <-requestDone:
case <-time.After(5 * time.Second):
t.Fatal("notification or Interrogate blocked")
}
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("runner did not return")
}
close(statuses)
want := svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
var last svc.Status
ready := false
for s := range statuses {
ready = ready || s == want
if ready && s != want {
t.Fatalf("stale status after RUNNING: %+v", s)
}
last = s
}
if last != want {
t.Fatalf("last status: got %+v, want %+v", last, want)
}
returned := make(chan struct{})
go func() { _ = notify.Ready(); close(returned) }()
select {
case <-returned:
case <-time.After(5 * time.Second):
t.Fatal("Ready blocked after runner returned")
}
})
}
}
func TestRunnerStopRacesReady(t *testing.T) {
const envKey = "CADDY_TEST_SERVICE_CONTROL"
controls := map[string]svc.Cmd{"stop": svc.Stop, "shutdown": svc.Shutdown}
if name := os.Getenv(envKey); name != "" {
requests := make(chan svc.ChangeRequest)
statuses := make(chan svc.Status)
go runner{}.Execute(nil, requests, statuses)
go func() {
for s := range statuses {
fmt.Println(s.State)
}
}()
go func() {
for {
_ = notify.Ready()
}
}()
requests <- svc.ChangeRequest{Cmd: controls[name]}
time.Sleep(10 * time.Second)
os.Exit(5) // Stop/Shutdown must exit the process before this fallback.
}
for name := range controls {
t.Run(name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
child := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestRunnerStopRacesReady$", "-test.count=1")
child.Env = append(os.Environ(), envKey+"="+name)
out, err := child.Output()
if err != nil {
t.Fatalf("child failed: %v; output:\n%s", err, out)
}
stopping := false
for line := range strings.FieldsSeq(string(out)) {
stopping = stopping || line == fmt.Sprint(svc.StopPending)
if stopping && line == fmt.Sprint(svc.Running) {
t.Fatalf("RUNNING after STOP_PENDING:\n%s", out)
}
}
if !stopping {
t.Fatalf("STOP_PENDING missing:\n%s", out)
}
})
}
}