github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fstest/testserver/testserver.go (about) 1 // Package testserver starts and stops test servers if required 2 package testserver 3 4 import ( 5 "bytes" 6 "errors" 7 "fmt" 8 "net" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "regexp" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/rclone/rclone/fs" 18 "github.com/rclone/rclone/fs/fspath" 19 ) 20 21 var ( 22 once sync.Once 23 configDir string // where the config is stored 24 // Note of running servers 25 runningMu sync.Mutex 26 running = map[string]int{} 27 errNotFound = errors.New("command not found") 28 ) 29 30 // Assume we are run somewhere within the rclone root 31 func findConfig() (string, error) { 32 dir := filepath.Join("fstest", "testserver", "init.d") 33 for i := 0; i < 5; i++ { 34 fi, err := os.Stat(dir) 35 if err == nil && fi.IsDir() { 36 return filepath.Abs(dir) 37 } else if !os.IsNotExist(err) { 38 return "", err 39 } 40 dir = filepath.Join("..", dir) 41 } 42 return "", errors.New("couldn't find testserver config files - run from within rclone source") 43 } 44 45 // run the command returning the output and an error 46 func run(name, command string) (out []byte, err error) { 47 cmdPath := filepath.Join(configDir, name) 48 fi, err := os.Stat(cmdPath) 49 if err != nil || fi.IsDir() { 50 return nil, errNotFound 51 } 52 cmd := exec.Command(cmdPath, command) 53 out, err = cmd.CombinedOutput() 54 if err != nil { 55 err = fmt.Errorf("failed to run %s %s\n%s: %w", cmdPath, command, string(out), err) 56 } 57 return out, err 58 } 59 60 // Check to see if the server is running 61 func isRunning(name string) bool { 62 _, err := run(name, "status") 63 return err == nil 64 } 65 66 // envKey returns the environment variable name to set name, key 67 func envKey(name, key string) string { 68 return fmt.Sprintf("RCLONE_CONFIG_%s_%s", strings.ToUpper(name), strings.ToUpper(key)) 69 } 70 71 // match a line of config var=value 72 var matchLine = regexp.MustCompile(`^([a-zA-Z_]+)=(.*)$`) 73 74 // Start the server and set its env vars 75 // Call with the mutex held 76 func start(name string) error { 77 out, err := run(name, "start") 78 if err != nil { 79 return err 80 } 81 fs.Logf(name, "Starting server") 82 // parse the output and set environment vars from it 83 var connect string 84 for _, line := range bytes.Split(out, []byte("\n")) { 85 line = bytes.TrimSpace(line) 86 part := matchLine.FindSubmatch(line) 87 if part != nil { 88 key, value := part[1], part[2] 89 if string(key) == "_connect" { 90 connect = string(value) 91 continue 92 } 93 94 // fs.Debugf(name, "key = %q, envKey = %q, value = %q", key, envKey(name, string(key)), value) 95 err = os.Setenv(envKey(name, string(key)), string(value)) 96 if err != nil { 97 return err 98 } 99 } 100 } 101 if connect == "" { 102 return nil 103 } 104 // If we got a _connect value then try to connect to it 105 const maxTries = 30 106 var rdBuf = make([]byte, 1) 107 for i := 1; i <= maxTries; i++ { 108 if i != 0 { 109 time.Sleep(time.Second) 110 } 111 fs.Debugf(name, "Attempting to connect to %q try %d/%d", connect, i, maxTries) 112 conn, err := net.DialTimeout("tcp", connect, time.Second) 113 if err != nil { 114 fs.Debugf(name, "Connection to %q failed try %d/%d: %v", connect, i, maxTries, err) 115 continue 116 } 117 118 err = conn.SetReadDeadline(time.Now().Add(time.Second)) 119 if err != nil { 120 return fmt.Errorf("failed to set deadline: %w", err) 121 } 122 n, err := conn.Read(rdBuf) 123 _ = conn.Close() 124 fs.Debugf(name, "Read %d, error: %v", n, err) 125 if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) { 126 // Try again 127 continue 128 } 129 //time.Sleep(30 * time.Second) 130 return nil 131 } 132 return fmt.Errorf("failed to connect to %q on %q", name, connect) 133 } 134 135 // Start starts the named test server which can be stopped by the 136 // function returned. 137 func Start(remoteName string) (fn func(), err error) { 138 if remoteName == "" { 139 // don't start the local backend 140 return func() {}, nil 141 } 142 parsed, err := fspath.Parse(remoteName) 143 if err != nil { 144 return nil, err 145 } 146 name := parsed.ConfigString 147 if name == "" { 148 // don't start the local backend 149 return func() {}, nil 150 } 151 152 // Make sure we know where the config is 153 once.Do(func() { 154 configDir, err = findConfig() 155 }) 156 if err != nil { 157 return nil, err 158 } 159 160 runningMu.Lock() 161 defer runningMu.Unlock() 162 163 if running[name] <= 0 { 164 // if server isn't running check to see if this server has 165 // been started already but not by us and stop it if so 166 if os.Getenv(envKey(name, "type")) == "" && isRunning(name) { 167 stop(name) 168 } 169 if !isRunning(name) { 170 err = start(name) 171 if err == errNotFound { 172 // if no file found then don't start or stop 173 return func() {}, nil 174 } else if err != nil { 175 return nil, err 176 } 177 running[name] = 0 178 } else { 179 running[name] = 1 180 } 181 } 182 running[name]++ 183 184 return func() { 185 runningMu.Lock() 186 defer runningMu.Unlock() 187 stop(name) 188 }, nil 189 190 } 191 192 // Stops the named test server 193 // Call with the mutex held 194 func stop(name string) { 195 running[name]-- 196 if running[name] <= 0 { 197 _, err := run(name, "stop") 198 if err != nil { 199 fs.Errorf(name, "Failed to stop server: %v", err) 200 } 201 running[name] = 0 202 fs.Logf(name, "Stopped server") 203 } 204 }