blob: 60a984692befd8b23f2c1f69dbc00b54200242f3 [file] [log] [blame]
// 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
}