github.com/scaleoutsean/fusego@v0.0.0-20220224074057-4a6429e46bb8/samples/subprocess.go (about) 1 // Copyright 2015 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package samples 16 17 import ( 18 "bytes" 19 "context" 20 "flag" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "log" 25 "os" 26 "os/exec" 27 "path" 28 "sync" 29 30 "github.com/jacobsa/ogletest" 31 ) 32 33 var fToolPath = flag.String( 34 "mount_sample", 35 "", 36 "Path to the mount_sample tool. If unset, we will compile it.") 37 38 var fDebug = flag.Bool("debug", false, "If true, print fuse debug info.") 39 40 // A struct that implements common behavior needed by tests in the samples/ 41 // directory where the file system is mounted by a subprocess. Use it as an 42 // embedded field in your test fixture, calling its SetUp method from your 43 // SetUp method after setting the MountType and MountFlags fields. 44 type SubprocessTest struct { 45 // The type of the file system to mount. Must be recognized by mount_sample. 46 MountType string 47 48 // Additional flags to be passed to the mount_sample tool. 49 MountFlags []string 50 51 // A list of files to pass to mount_sample. The given string flag will be 52 // used to pass the file descriptor number. 53 MountFiles map[string]*os.File 54 55 // A context object that can be used for long-running operations. 56 Ctx context.Context 57 58 // The directory at which the file system is mounted. 59 Dir string 60 61 // Anothing non-nil in this slice will be closed by TearDown. The test will 62 // fail if closing fails. 63 ToClose []io.Closer 64 65 mountSampleErr <-chan error 66 } 67 68 // Mount the file system and initialize the other exported fields of the 69 // struct. Panics on error. 70 func (t *SubprocessTest) SetUp(ti *ogletest.TestInfo) { 71 err := t.initialize(ti.Ctx) 72 if err != nil { 73 panic(err) 74 } 75 } 76 77 // Private state for getToolPath. 78 var getToolContents_Contents []byte 79 var getToolContents_Err error 80 var getToolContents_Once sync.Once 81 82 // Implementation detail of getToolPath. 83 func getToolContentsImpl() ([]byte, error) { 84 // Fast path: has the user set the flag? 85 if *fToolPath != "" { 86 contents, err := ioutil.ReadFile(*fToolPath) 87 if err != nil { 88 return nil, fmt.Errorf("Reading mount_sample contents: %v", err) 89 } 90 91 return contents, err 92 } 93 94 // Create a temporary directory into which we will compile the tool. 95 tempDir, err := ioutil.TempDir("", "sample_test") 96 if err != nil { 97 return nil, fmt.Errorf("TempDir: %v", err) 98 } 99 100 toolPath := path.Join(tempDir, "mount_sample") 101 102 // Ensure that we kill the temporary directory when we're finished here. 103 defer os.RemoveAll(tempDir) 104 105 // Run "go build". 106 cmd := exec.Command( 107 "go", 108 "build", 109 "-o", 110 toolPath, 111 "github.com/scaleoutsean/fusego/samples/mount_sample") 112 113 output, err := cmd.CombinedOutput() 114 if err != nil { 115 return nil, fmt.Errorf( 116 "mount_sample exited with %v, output:\n%s", 117 err, 118 string(output)) 119 } 120 121 // Slurp the tool contents. 122 contents, err := ioutil.ReadFile(toolPath) 123 if err != nil { 124 return nil, fmt.Errorf("ReadFile: %v", err) 125 } 126 127 return contents, nil 128 } 129 130 // Build the mount_sample tool if it has not yet been built for this process. 131 // Return its contents. 132 func getToolContents() ([]byte, error) { 133 // Get hold of the binary contents, if we haven't yet. 134 getToolContents_Once.Do(func() { 135 getToolContents_Contents, getToolContents_Err = getToolContentsImpl() 136 }) 137 138 return getToolContents_Contents, getToolContents_Err 139 } 140 141 func waitForMountSample( 142 cmd *exec.Cmd, 143 errChan chan<- error, 144 stderr *bytes.Buffer) { 145 // However we exit, write the error to the channel. 146 var err error 147 defer func() { 148 errChan <- err 149 }() 150 151 // Wait for the command. 152 err = cmd.Wait() 153 if err == nil { 154 return 155 } 156 157 // Make exit errors nicer. 158 if exitErr, ok := err.(*exec.ExitError); ok { 159 err = fmt.Errorf( 160 "mount_sample exited with %v. Stderr:\n%s", 161 exitErr, 162 stderr.String()) 163 164 return 165 } 166 167 err = fmt.Errorf("Waiting for mount_sample: %v", err) 168 } 169 170 func waitForReady(readyReader *os.File, c chan<- struct{}) { 171 _, err := readyReader.Read(make([]byte, 1)) 172 if err != nil { 173 log.Printf("Readying from ready pipe: %v", err) 174 return 175 } 176 177 c <- struct{}{} 178 } 179 180 // Like SetUp, but doens't panic. 181 func (t *SubprocessTest) initialize(ctx context.Context) error { 182 // Initialize the context. 183 t.Ctx = ctx 184 185 // Set up a temporary directory. 186 var err error 187 t.Dir, err = ioutil.TempDir("", "sample_test") 188 if err != nil { 189 return fmt.Errorf("TempDir: %v", err) 190 } 191 192 // Build/read the mount_sample tool. 193 toolContents, err := getToolContents() 194 if err != nil { 195 return fmt.Errorf("getTooltoolContents: %v", err) 196 } 197 198 // Create a temporary file to hold the contents of the tool. 199 toolFile, err := ioutil.TempFile("", "sample_test") 200 if err != nil { 201 return fmt.Errorf("TempFile: %v", err) 202 } 203 204 defer toolFile.Close() 205 206 // Ensure that it is deleted when we leave. 207 toolPath := toolFile.Name() 208 defer os.Remove(toolPath) 209 210 // Write out the tool contents and make them executable. 211 if _, err = toolFile.Write(toolContents); err != nil { 212 return fmt.Errorf("toolFile.Write: %v", err) 213 } 214 215 if err = toolFile.Chmod(0500); err != nil { 216 return fmt.Errorf("toolFile.Chmod: %v", err) 217 } 218 219 // Close the tool file to prevent "text file busy" errors below. 220 err = toolFile.Close() 221 toolFile = nil 222 if err != nil { 223 return fmt.Errorf("toolFile.Close: %v", err) 224 } 225 226 // Set up basic args for the subprocess. 227 args := []string{ 228 "--type", 229 t.MountType, 230 "--mount_point", 231 t.Dir, 232 } 233 234 args = append(args, t.MountFlags...) 235 236 // Set up a pipe for the "ready" status. 237 readyReader, readyWriter, err := os.Pipe() 238 if err != nil { 239 return fmt.Errorf("Pipe: %v", err) 240 } 241 242 defer readyReader.Close() 243 defer readyWriter.Close() 244 245 t.MountFiles["ready_file"] = readyWriter 246 247 // Set up inherited files and appropriate flags. 248 var extraFiles []*os.File 249 for flag, file := range t.MountFiles { 250 // Cf. os/exec.Cmd.ExtraFiles 251 fd := 3 + len(extraFiles) 252 253 extraFiles = append(extraFiles, file) 254 args = append(args, "--"+flag) 255 args = append(args, fmt.Sprintf("%d", fd)) 256 } 257 258 // Set up a command. 259 var stderr bytes.Buffer 260 mountCmd := exec.Command(toolPath, args...) 261 mountCmd.Stderr = &stderr 262 mountCmd.ExtraFiles = extraFiles 263 264 // Handle debug mode. 265 if *fDebug { 266 mountCmd.Stderr = os.Stderr 267 mountCmd.Args = append(mountCmd.Args, "--debug") 268 } 269 270 // Start the command. 271 if err := mountCmd.Start(); err != nil { 272 return fmt.Errorf("mountCmd.Start: %v", err) 273 } 274 275 // Launch a goroutine that waits for it and returns its status. 276 mountSampleErr := make(chan error, 1) 277 go waitForMountSample(mountCmd, mountSampleErr, &stderr) 278 279 // Wait for the tool to say the file system is ready. In parallel, watch for 280 // the tool to fail. 281 readyChan := make(chan struct{}, 1) 282 go waitForReady(readyReader, readyChan) 283 284 select { 285 case <-readyChan: 286 case err := <-mountSampleErr: 287 return err 288 } 289 290 // TearDown is no responsible for joining. 291 t.mountSampleErr = mountSampleErr 292 293 return nil 294 } 295 296 // Unmount the file system and clean up. Panics on error. 297 func (t *SubprocessTest) TearDown() { 298 err := t.destroy() 299 if err != nil { 300 panic(err) 301 } 302 } 303 304 // Like TearDown, but doesn't panic. 305 func (t *SubprocessTest) destroy() (err error) { 306 // Make sure we clean up after ourselves after everything else below. 307 308 // Close what is necessary. 309 for _, c := range t.ToClose { 310 if c == nil { 311 continue 312 } 313 314 ogletest.ExpectEq(nil, c.Close()) 315 } 316 317 // If we didn't try to mount the file system, there's nothing further to do. 318 if t.mountSampleErr == nil { 319 return nil 320 } 321 322 // In the background, initiate an unmount. 323 unmountErrChan := make(chan error) 324 go func() { 325 unmountErrChan <- unmount(t.Dir) 326 }() 327 328 // Make sure we wait for the unmount, even if we've already returned early in 329 // error. Return its error if we haven't seen any other error. 330 defer func() { 331 // Wait. 332 unmountErr := <-unmountErrChan 333 if unmountErr != nil { 334 if err != nil { 335 log.Println("unmount:", unmountErr) 336 return 337 } 338 339 err = fmt.Errorf("unmount: %v", unmountErr) 340 return 341 } 342 343 // Clean up. 344 ogletest.ExpectEq(nil, os.Remove(t.Dir)) 345 }() 346 347 // Wait for the subprocess. 348 if err := <-t.mountSampleErr; err != nil { 349 return err 350 } 351 352 return nil 353 }