| // Copyright 2021 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Package main implements the executionservice for running tests. |
| package main |
| |
| import ( |
| "context" |
| "flag" |
| "fmt" |
| "io" |
| "log" |
| "net" |
| "os" |
| "path/filepath" |
| "strings" |
| "time" |
| |
| "go.chromium.org/chromiumos/test/execution/cmd/cros-test/internal/common" |
| "go.chromium.org/chromiumos/test/execution/errors" |
| "go.chromium.org/chromiumos/test/util/metadata" |
| "go.chromium.org/chromiumos/test/util/portdiscovery" |
| ) |
| |
| // Version is the version info of this command. It is filled in during emerge. |
| var Version = "<unknown>" |
| |
| // defaultPort is the port where cros-test will bind to in server mode if a port is not provided. |
| var defaultPort = 8001 |
| |
| // createLogFile creates a file and its parent directory for logging purpose. |
| func createLogFile(fullPath string) (*os.File, error) { |
| if err := os.MkdirAll(fullPath, 0755); err != nil { |
| return nil, errors.NewStatusError(errors.IOCreateError, |
| fmt.Errorf("failed to create directory %v: %w", fullPath, err)) |
| } |
| |
| logFullPathName := filepath.Join(fullPath, "log.txt") |
| |
| // Log the full output of the command to disk. |
| logFile, err := os.Create(logFullPathName) |
| if err != nil { |
| return nil, errors.NewStatusError(errors.IOCreateError, |
| fmt.Errorf("failed to create file %v: %w", fullPath, err)) |
| } |
| return logFile, nil |
| } |
| |
| // newLogger creates a logger. Using go default logger for now. |
| func newLogger(logFile *os.File) *log.Logger { |
| mw := io.MultiWriter(logFile, os.Stderr) |
| return log.New(mw, "", log.LstdFlags|log.LUTC) |
| } |
| |
| type args struct { |
| // Common input params. |
| logPath string |
| inputPath string |
| outputPath string |
| resultsDirPath string |
| tlwAddr string |
| metadataDirPath string |
| version bool |
| |
| // Server mode params |
| port int |
| } |
| |
| // runCLI is the entry point for running cros-test (executionservice) in CLI mode. |
| func runCLI(ctx context.Context, d []string) int { |
| t := time.Now() |
| defaultLogPath := filepath.Join(common.TestExecServerRoot, t.Format("20060102-150405")) |
| defaultRequestFile := filepath.Join(common.TestExecServerRoot, common.TestRequestJSONFile) |
| defaultResultFile := filepath.Join(common.TestExecServerRoot, common.TestResultJSONFile) |
| |
| a := args{} |
| |
| fs := flag.NewFlagSet("Run cros-test", flag.ExitOnError) |
| fs.StringVar(&a.logPath, "log", defaultLogPath, fmt.Sprintf("Path to record execution logs. Default value is %s", defaultLogPath)) |
| fs.StringVar(&a.inputPath, "input", defaultRequestFile, "specify the test execution request json input file") |
| fs.StringVar(&a.outputPath, "output", defaultResultFile, "specify the test execution response json output file") |
| fs.StringVar(&a.resultsDirPath, "resultdir", common.TestResultDir, "specify default directory for test harnesses to store their run result") |
| fs.StringVar(&a.tlwAddr, "tlwaddr", "", "specify the tlw address") |
| fs.StringVar(&a.metadataDirPath, "metadatadir", common.TestMetadataDir, "specify a directory that contain all test metadata proto files.") |
| fs.BoolVar(&a.version, "version", false, "print version and exit") |
| fs.Parse(d) |
| |
| if a.version { |
| fmt.Println("executionservice version ", Version) |
| return 0 |
| } |
| |
| logFile, err := createLogFile(a.logPath) |
| if err != nil { |
| log.Fatalln("Failed to create log file", err) |
| return 2 |
| } |
| defer logFile.Close() |
| |
| logger := newLogger(logFile) |
| logger.Println("Starting executionservice version ", Version) |
| |
| req, err := readInput(a.inputPath) |
| if err != nil { |
| log.Fatalf("Failed to read request input: %s", err) |
| return 2 |
| } |
| |
| metadata, err := metadata.ReadDir(a.metadataDirPath) |
| if err != nil { |
| log.Fatalf("Failed to read metadata input: %s", err) |
| return 2 |
| } |
| |
| rspn, err := runTests(ctx, logger, a.resultsDirPath, a.tlwAddr, metadata, req) |
| if err != nil { |
| logger.Fatalln("Failed to run tests: ", err) |
| return 1 |
| } |
| |
| logger.Printf("Writing line to %s", a.outputPath) |
| if err := writeOutput(a.outputPath, rspn); err != nil { |
| logger.Fatalf("Failed to write output file to %s: %d", a.outputPath, err) |
| return 2 |
| } |
| return 0 |
| } |
| |
| // startServer is the entry point for running cros-test (executionservice) in server mode. |
| func startServer(d []string) int { |
| a := args{} |
| t := time.Now() |
| defaultLogPath := filepath.Join(common.TestExecServerRoot, t.Format("20060102-150405")) |
| |
| fs := flag.NewFlagSet("Start executionservice server", flag.ExitOnError) |
| fs.StringVar(&a.logPath, "log", defaultLogPath, fmt.Sprintf("Path to record execution logs. Default value is %s", defaultLogPath)) |
| fs.StringVar(&a.resultsDirPath, "resultdir", common.TestResultDir, "specify the test execution request json input file") |
| fs.StringVar(&a.tlwAddr, "tlwaddr", "", "specify the tlw address") |
| fs.StringVar(&a.metadataDirPath, "metadatadir", common.TestMetadataDir, "specify a directory that contain all test metadata proto files.") |
| fs.IntVar(&a.port, "port", defaultPort, fmt.Sprintf("Specify the port for the server. Default value %d.", defaultPort)) |
| fs.Parse(d) |
| |
| logFile, err := createLogFile(a.logPath) |
| if err != nil { |
| log.Fatalln("Failed to create log file", err) |
| return 2 |
| } |
| defer logFile.Close() |
| |
| logger := newLogger(logFile) |
| |
| l, err := net.Listen("tcp", fmt.Sprintf(":%d", a.port)) |
| if err != nil { |
| logger.Fatalln("Failed to create a net listener: ", err) |
| return 2 |
| } |
| logger.Println("Starting executionservice on port ", a.port) |
| |
| // Write port number to ~/.cftmeta for go/cft-port-discovery |
| err = portdiscovery.WriteServiceMetadata("cros-test", l.Addr().String(), logger) |
| if err != nil { |
| logger.Println("Warning: error when writing to metadata file: ", err) |
| } |
| |
| metadata, err := metadata.ReadDir(a.metadataDirPath) |
| if err != nil { |
| log.Fatalf("Failed to read metadata input: %s", err) |
| return 2 |
| } |
| |
| server, closer := NewServer(logger, a.resultsDirPath, a.tlwAddr, metadata) |
| defer closer() |
| err = server.Serve(l) |
| if err != nil { |
| logger.Fatalln("Failed to initialize server: ", err) |
| return 2 |
| } |
| return 0 |
| } |
| |
| // Specify run mode for CLI. |
| type runMode string |
| |
| const ( |
| runCli runMode = "cli" |
| runServer runMode = "server" |
| runVersion runMode = "version" |
| runHelp runMode = "help" |
| |
| runCliDefault runMode = "cliDefault" |
| ) |
| |
| func getRunMode() (runMode, error) { |
| if len(os.Args) > 1 { |
| for _, a := range os.Args { |
| if a == "-version" { |
| return runVersion, nil |
| } |
| } |
| switch strings.ToLower(os.Args[1]) { |
| case "cli": |
| return runCli, nil |
| case "server": |
| return runServer, nil |
| case "help": |
| return runHelp, nil |
| } |
| } |
| |
| // If we did not find special run mode then just run CLI to match legacy behavior. |
| return runCliDefault, nil |
| } |
| |
| func mainInternal(ctx context.Context) int { |
| runMode, err := getRunMode() |
| if err != nil { |
| log.Fatalln(err) |
| return 2 |
| } |
| switch runMode { |
| |
| case runCliDefault: |
| log.Printf("No mode specified, assuming CLI.") |
| return runCLI(ctx, os.Args) |
| case runCli: |
| log.Printf("Running CLI mode!") |
| return runCLI(ctx, os.Args[2:]) |
| case runServer: |
| log.Printf("Running server mode!") |
| return startServer(os.Args[2:]) |
| case runVersion: |
| log.Printf("executionservice version: %s", Version) |
| return 0 |
| } |
| return 0 |
| } |
| |
| func main() { |
| os.Exit(mainInternal(context.Background())) |
| } |