| // Copyright 2020 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Package chart provides utility for displaying a chart on chart tablet paired |
| // with DUT in camerabox setup. |
| package chart |
| |
| import ( |
| "context" |
| "fmt" |
| "path/filepath" |
| "regexp" |
| "strings" |
| "time" |
| |
| cryptossh "golang.org/x/crypto/ssh" |
| |
| "chromiumos/tast/dut" |
| "chromiumos/tast/errors" |
| "chromiumos/tast/shutil" |
| "chromiumos/tast/ssh" |
| "chromiumos/tast/ssh/linuxssh" |
| "chromiumos/tast/testing" |
| ) |
| |
| // displayScript is the script installed on chart tablet for displaying chart. |
| const displayScript = "/usr/local/autotest/bin/display_chart.py" |
| |
| // displayOutputLog is path on chart tablet placing logs from stdout/stderr of display chart script. |
| const displayOutputLog = "/tmp/chart_service.log" |
| |
| // DisplayDefaultLevel presents the default display level |
| const DisplayDefaultLevel = -1 |
| |
| // Chart displays chart files on the chart tablet in a camerabox setup. |
| type Chart struct { |
| // conn is the SSH connection to the chart tablet. |
| conn *ssh.Conn |
| // dir is the directory saving all chart files on chart tablet. |
| dir string |
| // pid is the process id of running display chart script. |
| pid string |
| // fifo is the path to fifo on chart tablet which can be used to write |
| // display configuration into it. |
| fifo string |
| } |
| |
| // NamePath is the reference path to the chart to be displayed on the chart |
| // service. The path is determined by the relative path to the chart directory. |
| type NamePath string |
| |
| // cleanup cleans up chart's (half-)initialized members and saves logs of chart process to |outDir|. |
| func cleanup(ctx context.Context, conn *ssh.Conn, dir, pid, outDir string) error { |
| // The member initialization are chained in order of |conn|>|dir|>|pid| |
| // with later one in the chain requiring previous one to be successfully |
| // initialized first. So check later one exist can assume the previous |
| // also exist. |
| cleanupDisplayProcess := func(ctx context.Context, conn *ssh.Conn, pid, outDir string) (retErr error) { |
| if len(pid) == 0 { |
| return nil |
| } |
| defer func() { |
| // Collect logs. |
| if err := linuxssh.GetFile(ctx, conn, displayOutputLog, filepath.Join(outDir, filepath.Base(displayOutputLog)), linuxssh.PreserveSymlinks); err != nil { |
| if retErr != nil { |
| testing.ContextLogf(ctx, "Failed to pull chart script logs from %v: %v", displayOutputLog, err) |
| } else { |
| retErr = errors.Wrapf(err, "failed pull chart script logs from %v", displayOutputLog) |
| } |
| } |
| if err := conn.CommandContext(ctx, "rm", displayOutputLog).Run(); err != nil { |
| if retErr != nil { |
| testing.ContextLogf(ctx, "Failed to clean up %v on chart tablet: %v", displayOutputLog, err) |
| } else { |
| retErr = errors.Wrapf(err, "failed to clean up %v on chart tablet", displayOutputLog) |
| } |
| } |
| }() |
| |
| if err := conn.CommandContext(ctx, "kill", "-2", pid).Run(); err != nil { |
| return errors.Wrap(err, "failed to send interrupt signal to close display script") |
| } |
| // Here we assume the script closing process should be very quick |
| // and thus don't need to wait for its closing to end. |
| |
| return nil |
| } |
| cleanupFns := [](func() error){ |
| func() error { return cleanupDisplayProcess(ctx, conn, pid, outDir) }, |
| func() error { |
| if len(dir) == 0 { |
| return nil |
| } |
| if err := conn.CommandContext(ctx, "rm", "-rf", dir).Run(); err != nil { |
| return errors.Wrapf(err, "failed remove chart directory %v", dir) |
| } |
| return nil |
| }, |
| func() error { |
| if conn == nil { |
| return nil |
| } |
| if err := conn.Close(ctx); err != nil { |
| return errors.Wrap(err, "failed to close SSH connection to chart tablet") |
| } |
| return nil |
| }, |
| } |
| var firstErr error |
| for _, cleanup := range cleanupFns { |
| if err := cleanup(); err != nil { |
| if firstErr == nil { |
| firstErr = errors.Wrap(err, "failed to run cleanup process") |
| } else { |
| testing.ContextLog(ctx, "Failed to run cleanup process: ", err) |
| } |
| } |
| } |
| return firstErr |
| } |
| |
| // connectChart dials SSH connection to chart tablet with the auth key of DUT. |
| func connectChart(ctx context.Context, d *dut.DUT, hostname string) (*ssh.Conn, error) { |
| var sopt ssh.Options |
| ssh.ParseTarget(hostname, &sopt) |
| sopt.KeyDir = d.KeyDir() |
| sopt.KeyFile = d.KeyFile() |
| sopt.ConnectTimeout = 10 * time.Second |
| return ssh.New(ctx, &sopt) |
| } |
| |
| // New starts chart service waiting for display chart command on the host |
| // either |altHostname| or |d|'s corresponding chart tablet, prepares all chart |
| // files to be displayed from |chartPaths| and returns a new |Chart| instance |
| // and the |NamePath| for each input chart for further be displayed with. |
| func New(ctx context.Context, d *dut.DUT, altHostname, outDir string, chartPaths []string) (*Chart, []NamePath, error) { |
| var conn *ssh.Conn |
| |
| // Connect to chart tablet. |
| if len(altHostname) > 0 { |
| c, err := connectChart(ctx, d, altHostname) |
| if err != nil { |
| return nil, nil, errors.Wrapf(err, "failed to connect to chart with hostname %v", altHostname) |
| } |
| conn = c |
| } else { |
| c, err := d.DefaultCameraboxChart(ctx) |
| if err != nil { |
| return nil, nil, errors.Wrap(err, "failed to connect to chart with default '-tablet' suffix hostname") |
| } |
| conn = c |
| } |
| |
| return SetUp(ctx, conn, outDir, chartPaths) |
| } |
| |
| // copyChart copies the chart from local to host chart tablet. |
| func copyChart(ctx context.Context, conn *ssh.Conn, chartLocalPath, chartHostDir string) error { |
| chartHostPath := filepath.Join(chartHostDir, filepath.Base(chartLocalPath)) |
| if _, err := linuxssh.PutFiles( |
| ctx, conn, map[string]string{chartLocalPath: chartHostPath}, linuxssh.DereferenceSymlinks); err != nil { |
| return errors.Wrapf(err, "failed to send chart file in path %v to chart tablet", chartLocalPath) |
| } |
| return nil |
| } |
| |
| // SetUp sets up the chart with the given ssh connection, prepares all chart |
| // files to be displayed from |chartPaths| and returns a new |Chart| instance |
| // and the |NamePath| for each input chart for further be displayed with. It |
| // uses |displayLevel| to set the brightness, the range is in [0.0, 100.0]. |
| func SetUp(ctx context.Context, conn *ssh.Conn, outDir string, chartPaths []string) (_ *Chart, _ []NamePath, retErr error) { |
| var dir, pid string |
| defer func() { |
| if retErr != nil { |
| if err := cleanup(ctx, conn, dir, pid, outDir); err != nil { |
| testing.ContextLog(ctx, "Failed to cleanup: ", err) |
| } |
| } |
| }() |
| |
| // Create temp directory for saving chart files. |
| out, err := conn.CommandContext(ctx, "mktemp", "-d", "/tmp/chart_XXXXXX").Output() |
| if err != nil { |
| return nil, nil, errors.Wrap(err, "failed to create chart file directory on chart tablet") |
| } |
| dir = strings.TrimSpace(string(out)) |
| |
| // Due to the limitation of telemetry |SetHTTPServerDirectories()| |
| // which requires all files prepared before start the http server. |
| // Copies all chart files into chart directory in setup stage. |
| fileMap := make(map[string]string, 0) |
| namePaths := make([]NamePath, 0) |
| for _, localPath := range chartPaths { |
| baseName := filepath.Base(localPath) |
| chartHostPath := filepath.Join(dir, baseName) |
| fileMap[localPath] = chartHostPath |
| namePaths = append(namePaths, NamePath(baseName)) |
| } |
| if _, err := linuxssh.PutFiles(ctx, conn, fileMap, linuxssh.DereferenceSymlinks); err != nil { |
| return nil, nil, errors.Wrapf(err, "failed to send chart files %v to chart tablet", chartPaths) |
| } |
| |
| // Start chart service. |
| displayCmd := fmt.Sprintf( |
| "(python %s %s > %s 2>&1) & echo -n $!", |
| shutil.Escape(displayScript), shutil.Escape(dir), |
| shutil.Escape(displayOutputLog)) |
| testing.ContextLog(ctx, "Start display chart process: ", displayCmd) |
| out, err = conn.CommandContext(ctx, "sh", "-c", displayCmd).Output() |
| if err != nil { |
| return nil, nil, errors.Wrap(err, "failed to run display chart script") |
| } |
| pid = strings.TrimSpace(string(out)) |
| |
| testing.ContextLog(ctx, "Poll for 'is ready' message for ensuring chart is ready") |
| const chartReadyMsg = "Chart is ready." |
| var fifo string |
| fifoPathRegex := regexp.MustCompile(chartReadyMsg + ` Fifo:\s(\S+)`) |
| if err := testing.Poll(ctx, func(ctx context.Context) error { |
| output, err := conn.CommandContext(ctx, "grep", chartReadyMsg, displayOutputLog).Output() |
| switch err.(type) { |
| case nil: |
| m := fifoPathRegex.FindSubmatch(output) |
| if len(m) != 0 { |
| testing.ContextLog(ctx, "Chart start in fifo mode") |
| fifo = string(m[1]) |
| } else { |
| return testing.PollBreak(errors.New("chart does not support fifo mode")) |
| } |
| return nil |
| case *cryptossh.ExitError: |
| // Grep failed to find ready message, wait for next poll. |
| return err |
| default: |
| return testing.PollBreak(err) |
| } |
| }, &testing.PollOptions{Timeout: time.Minute}); err != nil { |
| return nil, nil, errors.Wrap(err, "failed to wait for chart ready") |
| } |
| testing.ContextLog(ctx, "Display chart complete") |
| |
| return &Chart{conn, dir, pid, fifo}, namePaths, nil |
| } |
| |
| // Display displays the chart file specified by |namePath|. |
| func (c *Chart) Display(ctx context.Context, namePath NamePath) error { |
| cmd := fmt.Sprintf(`echo '{"chart_name": %q}' > %s`, namePath, c.fifo) |
| if err := c.conn.CommandContext(ctx, "bash", "-c", cmd).Run(); err != nil { |
| return errors.Wrapf(err, "failed to change displayed chart to %v", namePath) |
| } |
| return nil |
| } |
| |
| // SetDisplayLevel sets the display level ranged [0.0, 100.0]. |
| func (c *Chart) SetDisplayLevel(ctx context.Context, lv float32) error { |
| cmd := fmt.Sprintf(`echo '{"display_level": %.1f}' > %s`, lv, c.fifo) |
| if err := c.conn.CommandContext(ctx, "bash", "-c", cmd).Run(); err != nil { |
| return errors.Wrapf(err, "failed to change display level to %v", lv) |
| } |
| return nil |
| } |
| |
| // Close closes the chart process and saves its logs to |outDir|. |
| func (c *Chart) Close(ctx context.Context, outDir string) error { |
| if err := cleanup(ctx, c.conn, c.dir, c.pid, outDir); err != nil { |
| return errors.Wrap(err, "failed to close chart") |
| } |
| return nil |
| } |