|  | // Copyright 2019 The Fuchsia 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 runtests | 
|  |  | 
|  | import ( | 
|  | "encoding/json" | 
|  | "fmt" | 
|  | "io" | 
|  | "os" | 
|  | "path" | 
|  | "path/filepath" | 
|  |  | 
|  | "go.fuchsia.dev/fuchsia/tools/net/sshutil" | 
|  |  | 
|  | "github.com/pkg/sftp" | 
|  | ) | 
|  |  | 
|  | // DataSinkCopier copies data sinks from a remote host after a runtests invocation. | 
|  | type DataSinkCopier struct { | 
|  | viewer    remoteViewer | 
|  | sshClient *sshutil.Client | 
|  | remoteDir string | 
|  | } | 
|  |  | 
|  | // NewDataSinkCopier constructs a copier using the specified ssh client. | 
|  | func NewDataSinkCopier(client *sshutil.Client, remoteDir string) (*DataSinkCopier, error) { | 
|  | sftpClient, err := client.NewSFTPClient() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | viewer := &sftpViewer{sftpClient} | 
|  | copier := &DataSinkCopier{ | 
|  | viewer:    viewer, | 
|  | sshClient: client, | 
|  | remoteDir: remoteDir, | 
|  | } | 
|  | return copier, nil | 
|  | } | 
|  |  | 
|  | // Copy copies data sinks using the copier's remote viewer. | 
|  | func (c DataSinkCopier) Copy(references []DataSinkReference, localDir string) (DataSinkMap, error) { | 
|  | return copyDataSinks(c.viewer, references, c.remoteDir, localDir) | 
|  | } | 
|  |  | 
|  | // GetReference returns a reference to the remote data sinks. | 
|  | func (c DataSinkCopier) GetReference() (DataSinkReference, error) { | 
|  | return getDataSinkReference(c.viewer, c.remoteDir) | 
|  | } | 
|  |  | 
|  | // Reconnect should be called after the sshClient has been disconnected and | 
|  | // reconnected. It closes the old viewer and creates a new viewer using the | 
|  | // refreshed sshClient. | 
|  | func (c *DataSinkCopier) Reconnect() error { | 
|  | // This may fail because the underlying ssh session has already been | 
|  | // closed, which is fine and expected, so no need to check the | 
|  | // returned error. | 
|  | c.viewer.close() | 
|  |  | 
|  | sftpClient, err := c.sshClient.NewSFTPClient() | 
|  | if err != nil { | 
|  | return fmt.Errorf("failed to create new SFTP client: %w", err) | 
|  | } | 
|  | c.viewer = &sftpViewer{sftpClient} | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func (c DataSinkCopier) Close() error { | 
|  | return c.viewer.close() | 
|  | } | 
|  |  | 
|  | // DataSinkReference holds information about data sinks on the target. | 
|  | type DataSinkReference DataSinkMap | 
|  |  | 
|  | // Size returns the number of sinks held by the reference. | 
|  | func (d DataSinkReference) Size() int { | 
|  | numSinks := 0 | 
|  | for _, files := range d { | 
|  | numSinks += len(files) | 
|  | } | 
|  | return numSinks | 
|  | } | 
|  |  | 
|  | // remoteView provides an interface for fetching a summary.json and copying | 
|  | // files from a remote host after a runtests invocation. | 
|  | type remoteViewer interface { | 
|  | summary(string) (*TestSummary, error) | 
|  | copyFile(string, string) error | 
|  | close() error | 
|  | } | 
|  |  | 
|  | type sftpViewer struct { | 
|  | client *sftp.Client | 
|  | } | 
|  |  | 
|  | func (v sftpViewer) summary(summaryPath string) (*TestSummary, error) { | 
|  | f, err := v.client.Open(summaryPath) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | defer f.Close() | 
|  |  | 
|  | var summary TestSummary | 
|  | if err = json.NewDecoder(f).Decode(&summary); err != nil { | 
|  | return nil, err | 
|  | } | 
|  | return &summary, nil | 
|  | } | 
|  |  | 
|  | func (v sftpViewer) copyFile(remote, local string) error { | 
|  | remoteFile, err := v.client.Open(remote) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | defer remoteFile.Close() | 
|  |  | 
|  | if err = os.MkdirAll(filepath.Dir(local), 0777); err != nil { | 
|  | return err | 
|  | } | 
|  | localFile, err := os.Create(local) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | defer localFile.Close() | 
|  |  | 
|  | _, err = io.Copy(localFile, remoteFile) | 
|  | return err | 
|  | } | 
|  |  | 
|  | func (v sftpViewer) close() error { | 
|  | return v.client.Close() | 
|  | } | 
|  |  | 
|  | // GetDataSinkReference retrieves the summary.json produced by runtests (assuming only | 
|  | // a single test was run) and gets the data sinks specified in the summary. | 
|  | func getDataSinkReference(viewer remoteViewer, remoteOutputDir string) (DataSinkReference, error) { | 
|  | summaryPath := path.Join(remoteOutputDir, TestSummaryFilename) | 
|  | summary, err := viewer.summary(summaryPath) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("failed to read test summary from %q: %w", summaryPath, err) | 
|  | } | 
|  |  | 
|  | sinks := DataSinkReference{} | 
|  | for _, details := range summary.Tests { | 
|  | for name, files := range details.DataSinks { | 
|  | sinks[name] = files | 
|  | } | 
|  | } | 
|  | return sinks, nil | 
|  | } | 
|  |  | 
|  | // CopyDataSinks copies the data sinks specified in references from the | 
|  | // remoteOutputDir on the target to the localOutputDir on the host. | 
|  | // It returns a DataSinkMap of the copied files, removing duplicates across | 
|  | // the references. | 
|  | func copyDataSinks(viewer remoteViewer, references []DataSinkReference, remoteOutputDir, localOutputDir string) (DataSinkMap, error) { | 
|  | sinks := DataSinkMap{} | 
|  | copied := make(map[string]struct{}) | 
|  | for _, ref := range references { | 
|  | for name, files := range ref { | 
|  | if _, ok := sinks[name]; !ok { | 
|  | sinks[name] = []DataSink{} | 
|  | } | 
|  | for _, file := range files { | 
|  | if _, ok := copied[file.File]; ok { | 
|  | continue | 
|  | } | 
|  | src := path.Join(remoteOutputDir, file.File) | 
|  | dest := filepath.Join(localOutputDir, file.File) | 
|  | if err := viewer.copyFile(src, dest); err != nil { | 
|  | return nil, fmt.Errorf("failed to copy data sink %q: %w", file.File, err) | 
|  | } | 
|  | copied[file.File] = struct{}{} | 
|  | sinks[name] = append(sinks[name], file) | 
|  | } | 
|  | } | 
|  | } | 
|  | return sinks, nil | 
|  | } |