1
0
mirror of https://github.com/rclone/rclone.git synced 2025-11-29 05:47:23 +02:00
Files
rclone/fstest/testserver/testserver.go
Nick Craig-Wood 55655efabf testserver: fix tests failing due to stopped servers
Before this fix there were various issues with the test server
framework, most noticeably servers stopping when they shouldn't
causing timeouts. This was caused by the reference counting in the Go
code not being engineered to work in multiple processes so it was not
working at all properly.

This fix moves the reference counting logic to the start scripts and
in turn removes that logic from the Go code. This means that the
reference counting is now global and works correctly over multiple
processes.
2025-11-04 11:45:15 +00:00

199 lines
4.8 KiB
Go

// Package testserver starts and stops test servers if required
package testserver
import (
"bytes"
"errors"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/fspath"
)
var (
findConfigOnce sync.Once
configDir string // where the config is stored
)
// Assume we are run somewhere within the rclone root
func findConfig() (string, error) {
dir := filepath.Join("fstest", "testserver", "init.d")
for range 5 {
fi, err := os.Stat(dir)
if err == nil && fi.IsDir() {
return filepath.Abs(dir)
} else if !os.IsNotExist(err) {
return "", err
}
dir = filepath.Join("..", dir)
}
return "", errors.New("couldn't find testserver config files - run from within rclone source")
}
// returns path to a script to start this server
func cmdPath(name string) string {
return filepath.Join(configDir, name)
}
// return true if the server with name has a start command
func hasStartCommand(name string) bool {
fi, err := os.Stat(cmdPath(name))
return err == nil && !fi.IsDir()
}
// run the command returning the output and an error
func run(name, command string) (out []byte, err error) {
script := cmdPath(name)
cmd := exec.Command(script, command)
out, err = cmd.CombinedOutput()
if err != nil {
err = fmt.Errorf("failed to run %s %s\n%s: %w", script, command, string(out), err)
}
return out, err
}
// envKey returns the environment variable name to set name, key
func envKey(name, key string) string {
return fmt.Sprintf("RCLONE_CONFIG_%s_%s", strings.ToUpper(name), strings.ToUpper(key))
}
// match a line of config var=value
var matchLine = regexp.MustCompile(`^([a-zA-Z_]+)=(.*)$`)
// Start the server and env vars so rclone can use it
func start(name string) error {
fs.Logf(name, "Starting server")
out, err := run(name, "start")
if err != nil {
return err
}
// parse the output and set environment vars from it
var connect string
var connectDelay time.Duration
for line := range bytes.SplitSeq(out, []byte("\n")) {
line = bytes.TrimSpace(line)
part := matchLine.FindSubmatch(line)
if part != nil {
key, value := part[1], part[2]
if string(key) == "_connect" {
connect = string(value)
continue
} else if string(key) == "_connect_delay" {
connectDelay, err = time.ParseDuration(string(value))
if err != nil {
return fmt.Errorf("bad _connect_delay: %w", err)
}
continue
}
// fs.Debugf(name, "key = %q, envKey = %q, value = %q", key, envKey(name, string(key)), value)
err = os.Setenv(envKey(name, string(key)), string(value))
if err != nil {
return err
}
}
}
if connect == "" {
fs.Logf(name, "Started server")
return nil
}
// If we got a _connect value then try to connect to it
const maxTries = 100
var rdBuf = make([]byte, 1)
for i := 1; i <= maxTries; i++ {
if i != 0 {
time.Sleep(time.Second)
}
fs.Logf(name, "Attempting to connect to %q try %d/%d", connect, i, maxTries)
conn, err := net.DialTimeout("tcp", connect, time.Second)
if err != nil {
fs.Debugf(name, "Connection to %q failed try %d/%d: %v", connect, i, maxTries, err)
continue
}
err = conn.SetReadDeadline(time.Now().Add(time.Second))
if err != nil {
return fmt.Errorf("failed to set deadline: %w", err)
}
n, err := conn.Read(rdBuf)
_ = conn.Close()
fs.Debugf(name, "Read %d, error: %v", n, err)
if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) {
// Try again
continue
}
if connectDelay > 0 {
fs.Logf(name, "Connect delay %v", connectDelay)
time.Sleep(connectDelay)
}
fs.Logf(name, "Started server and connected to %q", connect)
return nil
}
return fmt.Errorf("failed to connect to %q on %q", name, connect)
}
// Stops the named test server
func stop(name string) {
fs.Logf(name, "Stopping server")
_, err := run(name, "stop")
if err != nil {
fs.Errorf(name, "Failed to stop server: %v", err)
}
}
// No server to stop so do nothing
func stopNothing() {
}
// Start starts the test server for remoteName.
//
// This must be stopped by calling the function returned when finished.
func Start(remote string) (fn func(), err error) {
// don't start the local backend
if remote == "" {
return stopNothing, nil
}
parsed, err := fspath.Parse(remote)
if err != nil {
return nil, err
}
name := parsed.ConfigString
// don't start the local backend
if name == "" {
return stopNothing, nil
}
// Make sure we know where the config is
findConfigOnce.Do(func() {
configDir, err = findConfig()
})
if err != nil {
return nil, err
}
// If remote has no start command then do nothing
if !hasStartCommand(name) {
return stopNothing, nil
}
// Start the server
err = start(name)
if err != nil {
return nil, err
}
// And return a function to stop it
return func() {
stop(name)
}, nil
}