go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/spantest/spantest_native.go (about) 1 // Copyright 2021 The LUCI Authors. 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 //go:build linux 16 // +build linux 17 18 package spantest 19 20 import ( 21 "context" 22 "fmt" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "strings" 27 "syscall" 28 29 "go.chromium.org/luci/common/errors" 30 31 "go.chromium.org/luci/common/system/filesystem" 32 "go.chromium.org/luci/common/system/port" 33 ) 34 35 // emulatorRelativePath is the relative path of the Cloud Spanner Emulator binary to gcloud root. 36 const emulatorRelativePath = "bin/cloud_spanner_emulator/emulator_main" 37 38 // findEmulatorPath finds the path to Cloud Spanner Emulator binary. 39 // 40 // Note that this should only work on Linux because only gcloud on Linux contains 41 // Cloud Spanner Emulator component. For Windows and MacOS users, the emulator 42 // requires Docker to be installed on your system and available on the system path. 43 func findEmulatorPath() (string, error) { 44 o, err := exec.Command("gcloud", "info", "--format=value(installation.sdk_root)").Output() 45 if err != nil { 46 return "", err 47 } 48 49 emulatorPath := filepath.Join(strings.TrimSuffix(string(o), "\n"), emulatorRelativePath) 50 switch _, err = os.Stat(emulatorPath); { 51 case os.IsNotExist(err): 52 return "", fmt.Errorf("cannot find cloud spanner emulator binary at %v. \n Please run `make install-spanner-emulator`", emulatorPath) 53 case err != nil: 54 return "", err 55 } 56 57 return emulatorPath, nil 58 } 59 60 // StartEmulator starts a Cloud Spanner Emulator instance. 61 func StartEmulator(ctx context.Context) (*Emulator, error) { 62 emulatorPath, err := findEmulatorPath() 63 if err != nil { 64 return nil, errors.Annotate(err, "find emulator").Err() 65 } 66 67 p, err := port.PickUnusedPort() 68 if err != nil { 69 return nil, errors.Annotate(err, "picking port").Err() 70 } 71 72 hostport := fmt.Sprintf("localhost:%d", p) 73 e := &Emulator{ 74 hostport: hostport, 75 } 76 77 if e.cfgDir, err = e.createSpannerEmulatorConfig(); err != nil { 78 return nil, err 79 } 80 81 ctx, e.cancel = context.WithCancel(ctx) 82 e.cmd = exec.CommandContext(ctx, emulatorPath, "--host_port", e.hostport) 83 e.cmd.Env = append(e.cmd.Env, fmt.Sprintf("SPANNER_EMULATOR_HOST=%s", e.hostport), fmt.Sprintf("CLOUDSDK_CONFIG=%s", e.cfgDir)) 84 e.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 85 // Without this, test will hang when finish. 86 // But unfortunately this is only supported on Linux. 87 e.cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL} 88 e.cmd.Stdout = os.Stdout 89 e.cmd.Stderr = os.Stderr 90 91 if err = e.cmd.Start(); err != nil { 92 return nil, err 93 } 94 95 return e, nil 96 } 97 98 // Stop kills the emulator process and removes the temporary gcloud config directory. 99 func (e *Emulator) Stop() error { 100 if e.cmd != nil { 101 e.cmd.Process.Release() 102 e.cancel() 103 e.cmd = nil 104 } 105 106 if e.cfgDir != "" { 107 if err := filesystem.RemoveAll(e.cfgDir); err == nil { 108 return errors.Annotate(err, "failed to remove the temporary config directory").Err() 109 } 110 e.cfgDir = "" 111 } 112 return nil 113 }