blob: 231a34129ba2caad003f23c728f0ddc1cbb3ff8e [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 sl4f
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"sync/atomic"
"time"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
"go.fuchsia.dev/fuchsia/tools/net/sshutil"
)
// Client is a wrapper around sl4f that supports auto-installing and starting
// sl4f on the target.
//
// If the target already has sl4f running, this client will not start a new
// instance.
//
// This client requires the target to contain the "pkgctl" binary and for the
// target to have the "run" and "sl4f" packages available at
// "fuchsia-pkg://host_target_testing_sl4f".
type Client struct {
sshClient *sshutil.Client
url string
repoName string
seq uint64
server *sshutil.Session
}
func NewClient(ctx context.Context, sshClient *sshutil.Client, addr string, repoName string) (*Client, error) {
c := &Client{
sshClient: sshClient,
repoName: repoName,
url: fmt.Sprintf("http://%s", strings.ReplaceAll(addr, "%", "%25")),
}
if err := c.connect(ctx); err != nil {
return nil, err
}
return c, nil
}
func (c *Client) Close() {
if c.server != nil {
// Closing the session kills the remote command.
c.server.Close()
c.server = nil
}
}
func (c *Client) connect(ctx context.Context) error {
logger.Infof(ctx, "connecting to sl4f")
// If an ssh connection re-establishes without a reboot, sl4f may already be running.
pingCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
if err := c.ping(pingCtx); err == nil {
logger.Infof(ctx, "already connected to sl4f")
cancel()
return nil
}
cancel()
// Start the CFv2 sl4f daemon.
cmd := []string{"start_sl4f"}
if err := c.sshClient.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
// FIXME(fxbug.dev/99571): We can remove the CFv1 fallback once we do a stepping stone
// release, and update our min supported version to match the stepping stone.
logger.Warningf(ctx, "unable to launch CFv2 sl4f, trying CFv1 sl4f: %s", err)
// In order to run components via SSH, we need the `run` package to be
// cached on the device. Since builds can be configured to not
// automatically cache packages, we need to explicitly resolve it.
cmd = []string{"pkgctl", "resolve", fmt.Sprintf("fuchsia-pkg://%s/run/0", c.repoName)}
if err := c.sshClient.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
logger.Errorf(ctx, "unable to resolve `run` package: %s", err)
return err
}
// Additionally, we must resolve the sl4f package before attempting to
// run it if the build is not configured to automatically cache
// packages.
cmd = []string{"pkgctl", "resolve", fmt.Sprintf("fuchsia-pkg://%s/sl4f/0", c.repoName)}
if err := c.sshClient.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
logger.Errorf(ctx, "unable to resolve `sl4f` package: %s", err)
return err
}
// Start the sl4f daemon.
cmd = []string{"run", fmt.Sprintf("fuchsia-pkg://%s/sl4f#meta/sl4f.cmx", c.repoName)}
server, err := c.sshClient.Start(ctx, cmd, os.Stdout, os.Stderr)
if err != nil {
logger.Errorf(ctx, "unable to launch sl4f: %s", err)
return err
}
c.server = server
}
// Wait a few seconds for it to respond to requests.
pingCtx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
for pingCtx.Err() == nil {
if err := c.ping(pingCtx); err == nil {
return nil
}
time.Sleep(time.Second)
}
logger.Errorf(ctx, "unable to ping sl4f: %s", pingCtx.Err())
return pingCtx.Err()
}
// ping attempts to perform an sl4f command that should always succeed if the server is up.
func (c *Client) ping(ctx context.Context) error {
var response string
if err := c.call(ctx, "device_facade.GetDeviceName", nil, &response); err != nil {
return err
}
return nil
}
func (c *Client) call(ctx context.Context, method string, params interface{}, result interface{}) error {
type request struct {
Method string `json:"method"`
Id string `json:"id"`
Params interface{} `json:"params"`
}
id := fmt.Sprintf("%d", atomic.AddUint64(&c.seq, 1))
body, err := json.Marshal(request{
Method: method,
Id: id,
Params: params,
})
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, "GET", c.url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var response struct {
Id string `json:"id"`
Result json.RawMessage `json:"result"`
Error json.RawMessage `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return err
}
// TODO(fxbug.dev/39973) sl4f currently response with different id values, but
// over HTTP, we can safely assume this response is for the request we
// made over this connection
//if response.Id != id {
// return fmt.Errorf("server responded with invalid ID: %q != %q", id, response.Id)
//}
var response_error string
if err := json.Unmarshal(response.Error, &response_error); err != nil {
return fmt.Errorf("unable to decode error from server: %s", response.Error)
}
if response_error != "" {
return fmt.Errorf("error from server: %s", response_error)
}
if err := json.Unmarshal(response.Result, result); err != nil {
return err
}
return nil
}