| // 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 main |
| |
| import ( |
| "bufio" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "math" |
| "os" |
| "path" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/golang/glog" |
| "github.com/pkg/sftp" |
| "golang.org/x/crypto/ssh" |
| ) |
| |
| type TargetConnection struct { |
| client *ssh.Client |
| } |
| |
| func newSigner(keyFile string) (ssh.Signer, error) { |
| if keyFile == "" { |
| // The default key lives here. |
| keyFile = path.Join(fuchsiaRoot, ".ssh", "pkey") |
| } |
| key, err := ioutil.ReadFile(keyFile) |
| if err != nil { |
| return nil, err |
| } |
| return ssh.ParsePrivateKey(key) |
| } |
| |
| func findDefaultTarget() (string, error) { |
| netaddr := path.Join(getZirconBuildRoot(), "tools", "netaddr") |
| output, err := getCommandOutput(netaddr, "--fuchsia") |
| output = strings.TrimSpace(output) |
| if err != nil { |
| return "", errors.New(output) |
| } |
| return "[" + output + "]", nil |
| } |
| |
| func NewTargetConnection(hostname, keyFile string) (*TargetConnection, error) { |
| if hostname == "" { |
| defaultHostname, err := findDefaultTarget() |
| if err != nil { |
| return nil, fmt.Errorf("Can not look up default target: %s", |
| err.Error()) |
| } |
| hostname = defaultHostname |
| } |
| |
| signer, err := newSigner(keyFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| config := &ssh.ClientConfig{ |
| User: "fuchsia", |
| Auth: []ssh.AuthMethod{ |
| ssh.PublicKeys(signer), |
| }, |
| HostKeyCallback: ssh.InsecureIgnoreHostKey(), |
| } |
| |
| client, err := ssh.Dial("tcp", hostname+":22", config) |
| if err != nil { |
| return nil, err |
| } |
| return &TargetConnection{ |
| client: client, |
| }, nil |
| } |
| |
| func (c *TargetConnection) Close() { |
| c.client.Close() |
| } |
| |
| func (c *TargetConnection) RunCommand(command string) error { |
| session, err := c.client.NewSession() |
| if err != nil { |
| return err |
| } |
| defer session.Close() |
| |
| session.Stdin = os.Stdin |
| session.Stdout = os.Stdout |
| session.Stderr = os.Stderr |
| glog.Info("Running: ", command) |
| return session.Run(command) |
| } |
| |
| func (c *TargetConnection) GetFile(remotePath string, localPath string) error { |
| client, err := sftp.NewClient(c.client) |
| if err != nil { |
| return err |
| } |
| defer client.Close() |
| |
| remoteFile, err := client.Open(remotePath) |
| if err != nil { |
| return err |
| } |
| defer remoteFile.Close() |
| |
| localFile, err := os.Create(localPath) |
| if err != nil { |
| return err |
| } |
| defer localFile.Close() |
| |
| _, err = io.Copy(localFile, remoteFile) |
| return err |
| } |
| |
| // SyncClk synchronizes local and remote clock over TargetConnection. |
| // It selects the shortest round trip of 10 attempts to read remote timestamps. |
| // Return is local-remote time offset, length of best round trip, error. |
| // If error is not nil, first two paramters should not be trusted. |
| func (c *TargetConnection) SyncClk() (offset time.Duration, delta time.Duration, err error) { |
| delta = time.Hour |
| |
| session, err := c.client.NewSession() |
| if err != nil { |
| return offset, delta, err |
| } |
| defer session.Close() |
| |
| cin, err := session.StdinPipe() |
| if err != nil { |
| return offset, delta, err |
| } |
| cerrPipe, err := session.StderrPipe() |
| if err != nil { |
| return offset, delta, err |
| } |
| cerr := bufio.NewReader(cerrPipe) |
| |
| err = session.Start("trace time") |
| if err != nil { |
| return offset, delta, err |
| } |
| |
| // Expect one line of "how to use timesync" output. |
| _, err = cerr.ReadString('\n') |
| if err != nil { |
| return offset, delta, err |
| } |
| |
| // Take 10 samples; each time: |
| // Send "t" to request time. |
| // Read one line; expect a timestamp. |
| for i := 0; i < 10; i++ { |
| start := time.Now() |
| n, err := cin.Write([]byte("t")) |
| if err != nil { |
| return offset, delta, err |
| } |
| if n != 1 { |
| return offset, delta, errors.New("Failed to send timestamp request") |
| } |
| |
| // Read a line ending with '\n'. |
| usBytes, err := cerr.ReadBytes('\n') |
| |
| end := time.Now() |
| |
| if err != nil { |
| return offset, delta, err |
| } |
| |
| // If this wasn't the fastest RTT we've seen so far, then continue. |
| d := end.Sub(start) |
| if d > delta { |
| continue |
| } |
| |
| // Store new best delta; sync local time to midpoint between start and end. |
| delta = d |
| t1 := start.Add(delta / 2) |
| |
| // Parse remote timestamp. |
| usStr := string(usBytes[:len(usBytes)-1]) |
| floatUSecs, err := strconv.ParseFloat(usStr, 64) |
| if err != nil { |
| return offset, delta, errors.New("Failed to parse timestamp") |
| } |
| nanoSecs := int64(math.Round(floatUSecs * 1000.0)) |
| t2 := time.Unix(nanoSecs/1000000000, nanoSecs%1000000000) |
| |
| // Store offset between local and remote timestamps. |
| offset = t1.Sub(t2) |
| } |
| |
| // Instruct "timesync" utility to quit with "q" keystroke. |
| // Wait for it to shutdown. |
| n, err := cin.Write([]byte("q")) |
| if err != nil { |
| return offset, delta, err |
| } |
| if n != 1 { |
| return offset, delta, errors.New("Failed to send quit request") |
| } |
| err = cin.Close() |
| if err != nil { |
| return offset, delta, err |
| } |
| err = session.Wait() |
| if err != nil { |
| return offset, delta, err |
| } |
| |
| return offset, delta, nil |
| } |